2 つの再帰分岐を持つパーサーで問題が発生しました。問題を簡単に説明するために、例としてLuca Bolognese によって書かれた記事からラムダ計算の単純な文法を使用します。
<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
それらは 2 つの で構成されてpExpr
おり、それがs でもある可能性があるため、私は興味がありpApplication
ます。次のベンチマークでは、パーサーがスタック領域を使い果たしています。
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にあります。