F如何在FParsec中正确处理错位的关键字同时保留FParsec的内置错误消息和位置信息

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

Camera课程

Python教程

Java教程

Web教程

数据库教程

图形图像教程

办公软件教程

Linux教程

计算机教程

大数据教程

开发工具教程