【问题标题】:Basic error recovery with FParsec使用 FParsec 进行基本错误恢复
【发布时间】:2012-02-12 11:29:55
【问题描述】:

假设我有这个解析器:

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn "Success: %A" result
    | Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg

let myStatement =
    choice (seq [
                pchar '2' >>. pchar '+' .>> pchar '3' .>> pchar ';';
                pchar '3' >>. pchar '*' .>> pchar '4' .>> pchar ';';
            ])

let myProgram = many myStatement

test myProgram "2+3;3*4;3*4;" // Success: ['+'; '*'; '*']

现在,"2+3;2*4;3*4;3+3;" 将失败并出现 2*4; 附近的错误。但是,如果我想要2*4;3+3; 的错误,那么最佳实践是什么?基本上,我想扫描到最近的';'但仅当出现致命错误。如果发生这种情况,我想汇总错误。

亲切的问候,Lasse Espeholt

更新: recoverWith 是一个不错的解决方案,谢谢!但鉴于:

let myProgram = 
    (many1 (myStatement |> recoverWith '�')) <|>% []

test myProgram "monkey"

我希望得到[] 没有错误。或者更“公平”一点:

let myProgram = 
    (attempt (many1 (myStatement |> recoverWith '�'))) <|>% []

【问题讨论】:

    标签: .net f# fparsec


    【解决方案1】:

    FParsec 没有内置支持从致命的解析器错误中恢复,这将允许您获取部分解析器结果并从多个位置收集错误。但是,为此目的定义自定义组合器函数非常容易。

    例如,要从简单语句解析器中的错误中恢复,您可以定义以下 recoverWith 组合器:

    open FParsec
    
    type UserState = {
        Errors: (string * ParserError) list
    } with
        static member Create() = {Errors = []}
    
    type Parser<'t> = Parser<'t, UserState>
    
    // recover from error by skipping to the char after the next newline or ';'
    let recoverWith errorResult (p: Parser<_>) : Parser<_> =    
      fun stream ->
        let stateTag = stream.StateTag
        let mutable reply = p stream
        if reply.Status <> Ok then // the parser failed
            let error = ParserError(stream.Position, stream.UserState, reply.Error)
            let errorMsg = error.ToString(stream)
            stream.SkipCharsOrNewlinesWhile(fun c -> c <> ';' && c <> '\n') |> ignore                        
            stream.ReadCharOrNewline() |> ignore
            // To prevent infinite recovery attempts in certain situations,
            // the following check makes sure that either the parser p 
            // or our stream.Skip... commands consumed some input.
            if stream.StateTag <> stateTag then
                let oldErrors = stream.UserState.Errors
                stream.UserState <- {Errors = (errorMsg, error)::oldErrors}     
                reply <- Reply(errorResult)
        reply
    

    然后您可以按如下方式使用此组合器:

    let myStatement =
        choice [
            pchar '2' >>. pchar '+' .>> pchar '3' .>> pchar ';'
            pchar '3' >>. pchar '*' .>> pchar '4' .>> pchar ';'
        ]
    
    let myProgram = 
        many (myStatement |> recoverWith '�') .>> eof
    
    let test p str =
        let printErrors (errorMsgs: (string * ParserError) list) =        
            for msg, _ in List.rev errorMsgs do
                printfn "%s" msg        
    
        match runParserOnString p (UserState.Create()) "" str with
        | Success(result, {Errors = []}, _) -> printfn "Success: %A" result
        | Success(result, {Errors = errors}, _) ->
            printfn "Result with errors: %A\n" result
            printErrors errors
        | Failure(errorMsg, error, {Errors = errors}) -> 
            printfn "Failure: %s" errorMsg
            printErrors ((errorMsg, error)::errors)
    

    使用test myProgram "2+3;2*4;3*4;3+3" 进行测试将产生输出:

    结果错误:['+'; ''; '*'; '''] Ln 中的错误:1 列:6 2+3;2*4;3*4;3+3 ^ 期待:'+' Ln 中的错误:1 列:14 2+3;2*4;3*4;3+3 ^ 期待:'*'

    更新:

    嗯,我以为您想从致命错误中恢复,以便收集多个错误消息并可能产生部分结果。例如,对于语法高亮或允许您的用户一次修复多个错误很有用的东西。

    您的更新似乎表明您只想在出现解析器错误时忽略部分输入,这要简单得多:

    let skip1ToNextStatement =
        notEmpty // requires at least one char to be skipped
            (skipManySatisfy (fun c -> c <> ';' && c <> '\n') 
             >>. optional anyChar) // optional since we might be at the EOF
    
    let myProgram =     
        many (attempt myStatement <|> (skip1ToNextStatement >>% '�'))
        |>> List.filter (fun c -> c <> '�')
    

    更新 2:

    以下是recoverWith 的一个版本,它不聚合错误,仅在参数解析器使用输入(或以任何其他方式更改解析器状态)时尝试从错误中恢复:

    let recoverWith2 errorResult (p: Parser<_>) : Parser<_> =
      fun stream ->
        let stateTag = stream.StateTag
        let mutable reply = p stream
        if reply.Status <> Ok && stream.StateTag <> stateTag then
            stream.SkipCharsOrNewlinesWhile(fun c -> c <> ';' && c <> '\n') |> ignore
            stream.ReadCharOrNewline() |> ignore
            reply <- Reply(errorResult)
        reply
    

    【讨论】:

    • +1 很好,谢谢 :) 如果您不介意,我已经稍微更新了问题。
    • 嗯,不,我想恢复。但前提是消费了一些东西。目前你的第一个答案对我的申请来说很好,所以我会接受:) 但是,我以后可能需要更多。
    • 如果您只想在解析器函数在消耗输入后失败时恢复,您希望如何解析"monkey",而myStatement 解析器将在没有消耗任何输入的情况下始终失败?
    • 哎呀! :( 在我忘记删除.&gt;&gt; eof 的问题中。很抱歉浪费您的时间。现在,如果|&gt; recoverWith... 被删除,它会成功,但是当它在那里时会失败。在这种情况下,我希望@987654332 @ 在没有消费的时候什么都不做。
    • @StephanTolksdorf 我不得不说,你很高兴能留意 FParsec 问题并帮助使用它的人。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-05
    • 2021-11-26
    • 1970-01-01
    • 2020-02-15
    • 2019-04-08
    • 1970-01-01
    • 2021-09-21
    相关资源
    最近更新 更多