我正在使用fslex / fsyacc实现脚本语言并且遇到大量用户输入的问题(即> 1k脚本),特别是当词法分析器分析一个非常大的字符串时会发生此错误。
我在自定义词法分析器函数中扫描字符串(允许用户使用反斜杠转义字符,如引号)。这是功能:
and myText pos builder = parse
| '\\' escapable { let char = lexbuf.LexemeChar(1)
builder.Append (escapeSpecialTextChar char) |> ignore
myText pos builder lexbuf
| newline { newline lexbuf;
builder.Append Environment.NewLine |> ignore
myText pos builder lexbuf
}
| (whitespace | letter | digit)+ { builder.Append (lexeme lexbuf) |> ignore
myText pos builder lexbuf
} // scan as many regular characters as possible
| '"' { TEXT (builder.ToString()) } // finished reading myText
| eof { raise (LexerError (sprintf "The text started at line %d, column %d has not been closed with \"." pos.pos_lnum (pos.pos_cnum - pos.pos_bol))) }
| _ { builder.Append (lexbuf.LexemeChar 0) |> ignore
myText pos builder lexbuf
} // read all other characters individually
该函数只解释各种字符,然后递归调用自身 - 或者在读取结束引号时返回读取字符串。当输入过大时,StackOverflowException
或_
规则中会抛出(whitespace | ...
。
生成的Lexer.fs
包含:
(* Rule myText *)
and myText pos builder (lexbuf : Microsoft.FSharp.Text.Lexing.LexBuffer<_>) = _fslex_myText pos builder 21 lexbuf
// [...]
and _fslex_myText pos builder _fslex_state lexbuf =
match _fslex_tables.Interpret(_fslex_state,lexbuf) with
| 0 -> (
# 105 "Lexer.fsl"
let char = lexbuf.LexemeChar(1) in
builder.Append (escapeSpecialTextChar char) |> ignore
myText pos builder lexbuf
// [...]
因此实际规则变为_fslex_myText
,myText
使用内部_fslex_state
调用它。
我认为这是问题所在:myText
不是尾递归的,因为它被转换为2个相互调用的函数,在某些时候它会产生StackOverflowException
。
所以我的问题确实是:
1)我的假设是正确的吗?或者F#编译器可以优化这两个函数,就像它使用尾递归函数一样,并生成一个while循环而不是递归调用? (功能编程对我来说还是新的)
2)如何使函数尾递归/阻止StackOverflowException
? FsLex文档并不是那么广泛,我无法弄清楚官方F#编译器的不同之处 - 显然它对大字符串没有问题,但它的字符串规则也会递归调用自己,所以我在在这里亏损:/
答案 0 :(得分:2)
如上所述,您的myText
函数似乎没问题 - F#编译器应该能够进行尾递归。
要检查一件事 - 您是在Debug
还是Release
配置中编译/运行此功能?默认情况下,F#编译器仅在Release
配置中进行尾调用优化。如果您在调试时需要尾调用,则需要在项目的属性中明确启用它(在Build
选项卡上)。