F# 如何在FParsec中正确处理错位的关键字同时保留FParsec的内置错误消息和位置信息
问题描述
在编写编程语言解析器时,一个常见的问题是处理语法错误,即关键字在代码中位置错误的情况。
在FParsec中,我看到了两种解决这个问题的方法,但都不是很令人满意。
方法1 :编写一个语法,其中可能存在冲突的地方允许关键字和其他字面量之间进行选择(这是上述重复问题中的解决方案)。
在我的语法中,有些上下文中从不会出现关键字。因此,在这些上下文中重写我的语法以允许在字面量和关键字之间进行选择并不是一个真正的选择。否则,我将允许我的解析器接受实际上是语法错误的输入。
方法2 :识别可能与关键字冲突的字面量,如果它们消耗了关键字作为输入,则让它们的解析器失败。
这种方法有两个负面影响,我将在下面的示例中予以演示:
open FParsec
type Ast =
| Var of string
| Number of string
| Assignment of Ast * Ast
let assign = skipString ":=" >>. spaces
let number = regex "\d+" <?> "number" |>> Ast.Number
let var =
regex "[a-z]\w*"
.>> spaces
<?> "variable"
>>= (fun s ->
if List.contains s ["for"; "if"; "else"] then // check if s is a keyword
fail "variable expected" // fail if s is a keyword
else
preturn (Ast.Var s) // return Ast.Var s otherwise
)
let varOrNumber = choice [ var ; number ]
let assignment = (var .>> assign) .>>. varOrNumber |>> Ast.Assignment
let parser = assignment
let p1 = run parser "x := y"
printfn "%O" p1
let p2 = run parser "x := 42"
printfn "%O" p2
printfn "---------"
let p3 = run parser "# := y"
printfn "failure p3 (correct message and position)\n%O" p3
printfn "---------"
let p4 = run parser "for := y"
printfn "failure p4 (wrong message and position)\n%O" p4
printfn "---------"
let p5 = run parser "x := #"
printfn "failure p5 (correct message and position)\n%O" p5
printfn "---------"
let p6 = run parser "x := for"
printfn "failure p6 (wrong message and position)\n%O" p6
这将输出
“`F#
Success: Assignment (Var “x”, Var “y”)
Success: Assignment (Var “x”, Number “42”)
failure p3 (correct message and position)
Failure:
Error in Ln: 1 Col: 1
:= y
^
Expecting: variable
failure p4 (wrong message and position)
Failure:
Error in Ln: 1 Col: 5
for := y
^
variable expected
failure p5 (correct message and position)
Failure:
Error in Ln: 1 Col: 6
x := #
^
Expecting: number or variable
failure p6 (wrong message and position)
Failure:
Error in Ln: 1 Col: 9
x := for
^
Note: The error occurred at the end of the input stream.
variable expected
在这个例子中,解析器 `var` 被修改为在消耗的输入是某个关键字时失败。
注意这种方法的缺点:
1. 不可能创建一个匹配语法的每种可能错误上下文的自定义错误消息:在简单的例子 `p3` 和 `p4` 中,语法期望一个 `variable`,而在 `p5` 和 `p6` 中,语法期望一个 `variable` 或一个 `number`。尽管 `p3` 和 `p5` 输出了正确的FParsec错误消息,`p4` 和 `p6` 输出了自定义错误消息,该错误消息在 `p4` 的上下文中是(仅仅是偶然地)正确的。
2. 如果 `s` 是一个关键字,那么 lambda 函数 `fun s` 会失败并消耗关键字的输入,因此错误的位置会转移到错误关键字之后的第一个字符。这个位置与错误无关,因为实际上错误应该发生在消耗关键字之前,而不是之后。在我看来,一个错误的输入在失败时不应该被消耗。
是否有一种方法能编写一个FParsec解析器,能正确识别输入中的错误关键字,并且还能保留原始FParsec的错误消息和位置?
## 解决方案
FParsec文档描述了一个非常有用的解析器 `resultSatisfies` ,它能回溯到你想要的方式:
```F#
let resultSatisfies predicate msg (p: Parser<_,_>) : Parser<_,_> =
let error = messageError msg
fun stream ->
let state = stream.State
let reply = p stream
if reply.Status <> Ok || predicate reply.Result then reply
else
stream.BacktrackTo(state) // backtrack to beginning
Reply(Error, error)
</code></pre>
在你的情况下,我认为你可以像这样使用它:
```F#
let var =
regex "[a-z]\w*"
.>> spaces
> "variable"
|> resultSatisfies (fun s ->
List.contains s ["for"; "if"; "else"] |> not) "variable expected"
|>> Ast.Var
我还没有尝试根据上下文改变错误信息。我认为这是你自己需要努力解决的事情。
输出为:
```F#
Success: Assignment (Var "x", Var "y")
Success: Assignment (Var "x", Number "42")
---------
failure p3 (correct message and position)
Failure:
Error in Ln: 1 Col: 1
# := y
^
Expecting: variable
---------
failure p4 (wrong message and position)
Failure:
Error in Ln: 1 Col: 1
for := y
^
variable expected
---------
failure p5 (correct message and position)
Failure:
Error in Ln: 1 Col: 6
x := #
^
Expecting: number or variable
---------
failure p6 (wrong message and position)
Failure:
Error in Ln: 1 Col: 6
x := for
^
Expecting: number
Other error messages:
variable expected