FParsec:如何保存解析器成功的文本?

8
为了在后续步骤中创建更好的错误消息,我希望保存解析器成功的位置和文本。获取位置似乎很容易(因为有 getPosition 解析器),但我不知道如何访问文本。
假设我有这个类型来保存位置信息:
type SourceLocation = {
    from: Position
    to: Position
    text: string
}

我想创建一个函数,将SourceLocation添加到另一个解析器的结果中:
let trackLocation (parser: Parser<'A, 'B>): Parser<SourceLocation * 'A, 'B> =
    let mkLocation ((start: Position, data: 'A), stop: Position: 'Positon) =
        let location = { from = start; to = stop }  // how do I get the text?
        in (location, data)
    getPosition .>>. parser .>>. getPositon |>> mkLocation

由于解析器只是接受 CharStream 的函数,我认为可以使用流和我的位置中的 Index 一起获取文本,但我没有看到获取此文本的方法。

那么获取解析器成功的文本的正确方法是什么?


1
解析器的文本从哪里来?您是否在字符串或本地文件中拥有已解析文本的本地副本,还是它来自无法倒带的流?因为解决您的问题的一种可能的方法是说:“嘿,我有起始和结束位置,我只需要查找文本的那一部分”。例如,如果您的文本在名为inputText的字符串变量中,则只需要使用inputText.[start.Index .. stop.Index],然后您就可以得到匹配的文本。请注意,FParsec的Position.Index属性是int64,因此如果您的输入小于2^32字节,则可能需要转换为int - rmunn
或者可能是 inputText.[start.Index .. stop.Index - 1]:我没有尝试过 getPosition,也不知道你是否有闭区间或半开区间。在盲目应用我的建议之前,请检查防护栏错误。 - rmunn
我可以获取文本的副本,但这似乎有点不正规。难道没有仅使用解析器或解析器流来完成此操作的方法吗?最好将文本已经包含在AST中,这样在解析后就可以忘记输入文件了。 - danielspaniol
我认为FParsec的设计通常不需要处理文本,例如sepBy解析器返回一个Parser<'a list, 'u>,而您不必处理原始文本。但我认为CharStream.BacktrackTo方法可能是您所需的。给我一点时间,我会写出一个可能的方法。 - rmunn
4
还有 skippedwithSkippedString 组合子:http://www.quanttec.com/fparsec/reference/parser-overview.html#parsing-strings-with-the-help-of-other-parsers - Stephan Tolksdorf
显示剩余2条评论
1个回答

6
我认为你可能需要的是 CharStream.ReadFrom 方法

返回从状态字符串开始索引(包括)到流的当前 Index(不包括)之间的字符组成的字符串。

你需要这样操作:
let trackLocation (parser: Parser<'A, 'B>): Parser<SourceLocation * 'A, 'B> =
    fun (stream : CharStream<'B>) ->
        let oldState = stream.State
        let parseResult = parser stream
        if parseResult.Status = Ok then
            let newState = stream.State
            let matchedText = stream.ReadFrom (oldState, true)
            // Or (oldState, false) if you DON'T want to normalize newlines
            let location = { from = oldState.GetPosition stream
                             ``to`` = newState.GetPosition stream
                             text = matchedText }
            let result = (location, parseResult.Result)
            Reply(result)
        else
            Reply(parseResult.Status, parseResult.Error)

使用示例(也是我编写用于确认其工作的测试代码):

let pThing = trackLocation pfloat
let test p str =
    match run p str with
    | Success((loc, result), _, _)   -> printfn "Success: %A at location: %A" result loc; result
    | Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg; 0.0
test pThing "3.5"
// Prints: Success: 3.5 at location: {from = (Ln: 1, Col: 1);
//                                    to = (Ln: 1, Col: 4);
//                                    text = "3.5";}

编辑: FParsec 的作者 Stephan Tolksdorf 在评论中指出,组合器withSkippedString 存在。使用该组合器可能会更简单,因为您不必自己编写 CharStream 消费函数。 (skipped 组合器将返回解析器匹配的字符串,但不返回解析器的结果,而 withSkippedString 将解析器的结果 跳过的字符串传递到您提供的函数中)。使用 withSkippedString 组合器,您可以仅进行最小更改即可使用原始的 trackLocation 函数。更新后的 trackLocation 版本如下:

let trackLocation (parser: Parser<'A, 'B>): Parser<SourceLocation * 'A, 'B> =
    let mkLocation ((start: Position, (text: string, data: 'A)), stop: Position) =
        let location = { from = start; ``to`` = stop; text = text }
        in (location, data)
    getPosition .>>. (parser |> withSkippedString (fun a b -> a,b)) .>>. getPosition |>> mkLocation

我对元组的排列方式并不完全满意,因为它会导致一个嵌套在另一个元组中。使用不同的组合顺序可能会产生更好的签名。但由于这是一个内部函数,不打算供公众使用,所以在函数签名中出现了恶劣的元组嵌套可能并不是很严重,因此我将其保留为原样。如果您想要一个更好的函数签名,请自行重新排列它。

与我最初的答案相同的测试代码可以在此更新版本的函数中运行良好,并打印相同的结果:起始位置(第1行,第1列),结束位置(第1行,第4列)和解析文本"3.5"


感谢您提供这个出色的答案。您刚刚拯救了我的一天! - foresightyj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接