使用FParsec进行基本错误恢复

时间:2012-02-12 11:29:56

标签: .net f# fparsec

假设我有这个解析器:

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 '�'))) <|>% []

1 个答案:

答案 0 :(得分:8)

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"进行测试会产生输出:

Result with errors: ['+'; '�'; '*'; '�']

Error in Ln: 1 Col: 6
2+3;2*4;3*4;3+3
     ^
Expecting: '+'

Error in Ln: 1 Col: 14
2+3;2*4;3*4;3+3
             ^
Expecting: '*'

<强>更新

嗯,我以为你想从一个致命的错误中恢复,以收集多个错误信息,并可能产生部分结果。例如,某些内容可用于语法突出显示或允许用户一次修复多个错误。

您的更新似乎表明您只想在解析器错误的情况下忽略部分输入,这更简单:

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