I have this code that results in a stack overflow.
// Lexer.Token defined elsewhere
object BadParser extends Parsers {
import Lexer.Token
type Elem = Token
private def PUNCT = Token.PUNCT
private def parens[T](p: Parser[T]) = PUNCT("(") ~ p ~ PUNCT(")")
def expr: Parser[Any] = parens(expr) | PUNCT(".")
}
class ParensTest extends AnyFlatSpec {
def testBad(input: String) =
val tokens = Lexer.scan(input)
BadParser.expr(tokens)
"bad parser" should "not recur forever but it does" in {
testBad("((.))")
}
}
that is weird enough as it is but it gets weirder. when I try inspecting the parens(expr) like this:
def expr: Parser[Any] = log(parens(expr))("parens expr") | PUNCT(".")
there is no longer a stack overflow.
I decided to try inlining the parens function and the argument like this:
inline parens(inline p... and that also fixed it.
the confusion for me is that the docs on inline https://docs.scala-lang.org/scala3/guides/macros/inline.html say that inlining parameters shouldn't change the semantics of the function but it clearly does. What am I missing?
(my understanding of inline is that it forces the compiler to actually inline, not just give its best effort, and when you say the parameter is inline, it will inline the parameter instead of passing by value or reference)
Edit: I'm using the Scala Parser Combinator library
By-name (also known as lazy evaluation) prevents infinite-recursion in this case. i.e.
private def parens[T](p: => Parser[T]) = PUNCT("(") ~ p ~ PUNCT(")")
The argument of operator ~
is defined as by-name, so inlining would help you. However, it would not give nice result if too much call-site has been inlined.