我遇到了解析器有两个递归分支的问题。为了更容易地演示这个问题,我使用Luca Bolognese 撰写的文章中的 lambda 演算的简单语法作为示例:
<expression> ::= <name> | <function> | <application> <name> ::= nonblank character sequence <function> ::= \ <name> . <body> <body> ::= <expression> <application> ::= ( <function expression> <argument expression> ) <function expression> ::= <expression> <argument expression> ::= <expression>
文章中的解析器相当简洁:
let ws = " \t\n"
let specialChars = ".)(\\\n"
let pWs = spaces
let pName = manyChars (noneOf (ws + specialChars)) |>> EName
let pExpr, pExprRef = createParserForwardedToRef<Expression, Unit>()
let curry2 f a b = f(a,b)
let pFunction = pchar '\\' >>. pipe2 pName (pchar '.' >>. pExpr) (curry2 Function)
let pApplication = pchar '(' >>. pipe2 pExpr (pWs >>. pExpr) (curry2 Application)
.>> pWs .>> pchar ')'
do pExprRef := pFunction <|> pApplication <|> pName
let pExpressions = sepBy pExpr spaces1
let fparseString text =
match run pExpressions text with
| Success(result, _, _) -> result
| Failure(errorMsg, _, _) -> failwith (sprintf "Failure: %s" errorMsg)
我很感兴趣,pApplication
因为它们由两个pExpr
s 组成,而这两个 s 也可能是pApplication
s。解析器在以下基准测试中耗尽了堆栈空间:
let generateString level =
let rec loop i =
seq {
if i < level then
yield "("
yield! loop level
yield " "
yield! loop (i+1)
yield ")"
else
yield "(x x)"
}
loop 0 |> String.concat ""
let N = 5000
let s = generateString N;;
let _ = fparseString s;;
如何将解析器重写为尾递归?
当我尝试为类似 Lisp 的语言编写解析器并使用真实的基准测试它时,我意识到了这个问题。我有Term
,VarBinding
它们是相互递归的类型和一个let
解析器,它表现出与上述相同的问题pApplication
。我的解析器在 github 上,以防关于非尾递归问题的分析错误。