解析一个函数的调用 - FParsec

Parser the call of a function - FParsec

我尝试解析一个函数的调用,这里是变体:

add 8 2
add x y
add (inc x) (dec y)
funcWithoutArgs

根据我在代码中分配分析器的方式,也许还有它们的编码方式,我会遇到错误,以及成功但不需要的分析。 例如,这个:

add 4 7

returns 以下 AST:

[Call ("foo",[Number 4]);
 Number 7]

因此他只接受第一个参数。

当我这样做时:

foo x y

他把这个 AST 发给我:

[Call ("foo",[Call ("x",[Call ("y",[])])])]

这不是我想要的,因为在这里,每个参数都会调用下一个参数。

另一个例子,当我这样做时:

foo x y
inc x

我得到:

[Call ("foo",[Call ("x",[Call ("y",[Call ("inc",[Call ("x",[])])])])])]

它的作用与上面相同,但还会调用该行后面的代码。当我向我的分析器请求换行时(见代码),它向我发送了这个:

[Call ("foo",[]); Call ("x",[]); Call ("y",[]); Call ("inc",[]); Call ("x",[])]

即使在括号中也不起作用:

foo (x) (y)

给予:

[Call ("foo",[]); Call ("x",[]); Call ("y",[])]

并且:

add (inc x) (dec y)

给予:

Error in Ln: 1 Col: 1
Note: The error occurred on an empty line.

The parser backtracked after:
  Error in Ln: 2 Col: 5
  add (inc x) (dec y)
      ^
  Expecting: end of input or integer number (32-bit, signed)

  The parser backtracked after:
    Error in Ln: 2 Col: 10
    add (inc x) (dec y)
             ^
    Expecting: ')'

[]

简而言之,我的函数调用分析器不能正常工作。每次我改变一些东西,比如一条新线,一次尝试,或者一个不同的层次结构,有些东西不起作用...... 你知道如何解决这个非常烦人的问题吗?

这是使用的最小功能代码:

open FParsec

// Ast

type Expression =
    | Number of int
    | Call of string * Expression list

type Program = Expression list

// Tools

let private bws p =
    spaces >>? p .>>? spaces

let private suiteOf p =
    sepEndBy p spaces1

let inline private betweenParentheses p label =
    between (pstring "(") (pstring ")") p
    <?> (label + " between parentheses")

let private identifier =
    many1Satisfy2 isLetter (fun c -> isLetter c)

// Expressions

let rec private call = parse {
        let! call = pipe2 (spaces >>? identifier) (spaces >>? parameters)
                        (fun id parameters -> Call(id, parameters)) // .>>? newline
        return call
    }

and private parameters = suiteOf expression

and private callFuncWithoutArgs =
    identifier |>> fun id -> Call(id, [])

and private number = pint32 |>> Number

and private betweenParenthesesExpression =
    parse { let! ex = betweenParentheses expression "expression"
            return ex }

and private expression =
    bws (attempt betweenParenthesesExpression <|>
         attempt number <|>
         attempt call <|>
         callFuncWithoutArgs)

// -------------------------------

let parse code =
    let parser = many expression .>>? eof

    match run parser code with
        | Success(result, _, _) -> result
        | Failure(msg, _, _) ->
            printfn "%s" msg
            []

System.Console.Clear()

parse @"
add 4 7

foo x y

inc x

foo (x) (y)

add (inc x) (dec y)

" |> printfn "%A"

您的主要问题是您的解析器的高级设计有误。

你现在的设计是一个表达式可以是:

  1. 括号之间的表达式(可以说是 "sub-expression"(这里没问题)
  2. 一个数字(这里没问题)
  3. 带参数的调用,它是一个标识符,后跟 space 分隔的表达式列表(这是问题的主要部分)
  4. 不带参数的调用,它是单个标识符(这会导致问题)

看看表达式 foo x y,让我们按照解析器的顺序应用这些规则。没有括号并且 foo 不是数字,所以它不是 3 就是 4。首先我们尝试 3。foo 后跟 x yx y 是否解析为一种表达?为什么,是的,它确实如此:它解析为带参数的调用,其中 x 是函数,y 是参数。由于 x y 匹配 3,它根据规则 3 进行解析而不检查规则 4,因此 foo x y 匹配类似于 foo (x y) 将:使用单个参数调用 foo,这是使用参数 y.

调用 x

如何解决这个问题?好吧,您可以尝试交换 3 和 4 的顺序,以便在调用带参数的函数调用之前检查不带参数的函数调用(这会使 x y 解析为 x。但那会失败,因为 foo x y 会像 foo 一样匹配。所以将规则 4 放在规则 3 之前在这里不起作用。

真正的解决方案是将表达式的规则拆分为两个级别。 "inner" 级别,我称之为 "value",可能是:

  1. 括号中的表达式
  2. 一个数
  3. 不带参数的函数调用

"outer" 级别,表达式的解析规则,将是:

  1. 一个函数调用,带个参数,都是不是表达式
  2. 一个值

请注意,这些解析级别是相互递归的,因此您需要在实现中使用 createParserForwardedToRef。让我们看看 foo x y 将如何使用此设计进行解析:

首先,foo 解析为标识符,因此检查它是否可能是带参数的函数调用。 x 是否解析为一个值?是的,根据价值观规则 3。 y 是否解析为一个值?是的,根据价值观规则 3。所以 foo x y 解析为函数调用。

现在funcWithoutParameters呢?它会失败表达式的规则 1,因为它后面没有参数列表。所以它会检查表达式的规则 2,然后它会根据值的规则 3 进行匹配。

好的,伪代码的基本健全性检查有效,所以让我们把它变成代码。但首先,我会提到你的解析器中的 other 问题,我还没有提到,那就是你没有意识到 FParsec spaces 解析器 also matches newlines。因此,当您将 expression 解析器包装在 bws ("between whitespace") 中时,它还会在解析的文本后使用换行符。因此,当您解析类似以下内容时:

foo a b
inc c

suiteOf expression 看到列表 a b inc c 并将所有这些转换为 foo 的参数。在我下面的代码中,我区分了 FParsec 的 spaces 解析器(包括换行符)和解析器 only horizo​​ntal whitespace (space和制表符但不是换行符),在适当的地方使用每个。下面的代码实现了我在这个答案中提到的设计,它的输出对我来说对于你写的所有测试表达式来说都是正确的:

open FParsec

// Ast

type Expression =
    | Number of int
    | Call of string * Expression list

type Program = Expression list

// Tools

let private justSpaces  = skipMany  (pchar ' ' <|> pchar '\t')
let private justSpaces1 = skipMany1 (pchar ' ' <|> pchar '\t')

let private bws p =
    spaces >>? p .>>? spaces

let private suiteOf p =
    sepEndBy1 p (justSpaces1)

let inline private betweenParentheses p label =
    between (pstring "(") (pstring ")") p
    <?> (label + " between parentheses")

let private identifier =
    many1Satisfy2 isLetter (fun c -> isLetter c)

// Expressions

let private expression, expressionImpl = createParserForwardedToRef()

let private betweenParenthesesExpression =
    parse { let! ex = betweenParentheses expression "expression"
            return ex }

let private callFuncWithoutArgs =
    (identifier |>> fun id -> Call(id, []))

let private number = pint32 |>> Number

let private value =
    justSpaces >>? (attempt betweenParenthesesExpression <|>
                    attempt number <|>
                    callFuncWithoutArgs)

let private parameters = suiteOf value

let rec private callImpl = parse {
        let! call = pipe2 (justSpaces >>? identifier) (justSpaces >>? parameters)
                          (fun id parameters -> Call(id, parameters))
        return call }

let call = callImpl

expressionImpl.Value <-
    bws (attempt call <|>
         value)

// -------------------------------

let parse code =
    let parser = many expression .>>? (spaces >>. eof)

    match run parser code with
        | Success(result, _, _) -> result
        | Failure(msg, _, _) ->
            printfn "%s" msg
            []

System.Console.Clear()

parse @"
add 4 7

foo x y

inc x

foo (x) (y)

add (inc x) (dec y)
" |> printfn "%A"

P.S。我使用了 http://www.quanttec.com/fparsec/users-guide/debugging-a-parser.html 建议的以下运算符来极大地帮助我跟踪问题:

let (<!>) (p: Parser<_,_>) label : Parser<_,_> =
    fun stream ->
        printfn "%A: Entering %s" stream.Position label
        let reply = p stream
        printfn "%A: Leaving %s (%A)" stream.Position label reply.Status
        reply

用法:将let parseFoo = ...变成let parseFoo = ... <!> "foo"。然后您将在控制台中获得调试输出流,如下所示:

(Ln: 2, Col: 20): Entering expression
(Ln: 3, Col: 1): Entering call
(Ln: 3, Col: 5): Entering parameters
(Ln: 3, Col: 5): Entering bwParens
(Ln: 3, Col: 5): Leaving bwParens (Error)
(Ln: 3, Col: 5): Entering number
(Ln: 3, Col: 6): Leaving number (Ok)
(Ln: 3, Col: 7): Entering bwParens
(Ln: 3, Col: 7): Leaving bwParens (Error)
(Ln: 3, Col: 7): Entering number
(Ln: 3, Col: 8): Leaving number (Ok)
(Ln: 3, Col: 8): Leaving parameters (Ok)
(Ln: 3, Col: 8): Leaving call (Ok)
(Ln: 3, Col: 8): Leaving expression (Ok)

当您试图弄清楚为什么您的解析器没有按照您的预期运行时,这会很有帮助。