Восстановление основных ошибок с помощью FParsec - PullRequest
4 голосов
/ 12 февраля 2012

Предположим, у меня есть этот парсер:

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;?По сути, я хочу сканировать до ближайшего ';'но только если есть фатальная ошибка.И если это произойдет, я хочу объединить ошибки.

С уважением, Лассе Эспехолт

Обновление: recoverWith - хорошее решение, спасибо!Но учитывая:

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

test myProgram "monkey"

Я бы ожидал получить [] без ошибок.Или, может быть, немного более "честно":

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

1 Ответ

8 голосов
/ 12 февраля 2012

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
...