Scala 3中inline为何能解决解析器栈溢出问题?
问题解析
原始代码栈溢出的根本原因
你的BadParser中,expr的定义是parens(expr) | PUNCT("."),而parens是普通的严格求值函数(参数没有=>修饰)。当首次访问expr时,会先计算parens(expr):
- 调用
parens需要先求值参数expr - 求值
expr又需要计算parens(expr) - 这就形成了构造Parser阶段的无限递归,直接触发JVM栈溢出,根本到不了解析输入的步骤。
为什么log能解决问题
log组合子的第一个参数是按名传递的(签名为def log[T](p: => Parser[T])(name: String))。按名参数的求值会被延迟,只有在实际需要使用这个Parser分支时才会触发parens(expr)的计算。这样就打破了构造阶段的无限递归链:expr构造时,log(parens(expr))不会立即求值parens(expr),而是将其包装为一个延迟计算的闭包,直到解析时尝试该分支才会递归调用expr。
为什么inline能解决问题
Scala 3的inline会将函数调用直接展开为函数体代码:
- 标记
inline def parens(inline p: Parser[T])后,parens(expr)会被展开为PUNCT("(") ~ expr ~ PUNCT(")") - 此时
expr的定义变为(PUNCT("(") ~ expr ~ PUNCT(")")) | PUNCT(".")
这里的关键是,展开后的代码中,~组合子的参数是expr(一个def),而Scala的def是惰性求值的——只有在解析时需要尝试该分支时,才会调用expr,避免了构造阶段的无限递归。
关于Scala 3官方文档的语义说明
官方文档说“inline参数不会改变函数语义”,指的是对于定义良好、无未定义行为的程序,inline展开后的行为与原函数一致。而你的原始代码存在构造阶段无限递归的未定义行为,inline只是通过改变代码展开方式,避免了这种未定义行为的触发,并不违反语义一致性的承诺。
内容的提问来源于stack exchange,提问作者jared jacobson
相关产品推荐
相关产品推荐

