Search code examples
parsingf#parser-combinatorsfparsec

With FParsec is it possible to manipulate the error position when a parser fails?


As an example, I will take this simple C# parser by Phillip Trelford. In order to parse an identifier he writes this (slightly changed):

let reserved = ["for";"do"; "while";"if";"switch";"case";"default";"break" (*;...*)]
let pidentifierraw =
    let isIdentifierFirstChar c = isLetter c || c = '_'
    let isIdentifierChar c = isLetter c || isDigit c || c = '_'
    many1Satisfy2L isIdentifierFirstChar isIdentifierChar "identifier"
let pidentifier =
    pidentifierraw
    >>= fun s ->
        if reserved |> List.exists ((=) s) then fail "keyword instead of identifier"
        else preturn s

The problem with pidentifier is that when it fails, the position indicator is at the end of the stream. An example of mine:

Error in Ln: 156 Col: 41 (UTF16-Col: 34)
        Block "main" 116x60 font=default fg=textForeground
                                        ^
Note: The column count assumes a tab stop distance of 8 chars.
keyword instead of identifier

Obviously, not a C# snippet, but for the example's sake, I've used the pidentifier to parse the text after font=. Is it possible to tell FParsec to show the error at the beginning of the parsed input? Using >>?, .>>.? or any of the backtracking variants seems to have no effect.


Solution

  • I think what you want is attempt p, which will backtrack to the original parser state if parser p fails. So you could just define pidentifier as:

    let pidentifier =
        pidentifierraw
        >>= fun s ->
            if reserved |> List.exists ((=) s) then fail "keyword instead of identifier"
            else preturn s
        |> attempt   // rollback on failure
    

    Output is then something like:

    Failure:
    Error in Ln: 1 Col: 1
    default
    ^
    
    The parser backtracked after:
      Error in Ln: 1 Col: 8
      default
             ^
      Note: The error occurred at the end of the input stream.
      keyword instead of identifier
    

    Update

    If you don't want to see the backtracking info in the error message, you can use a simplified version of attempt, like this:

    let attempt (parser : Parser<_, _>) : Parser<_, _> =
        fun stream ->
            let mutable state = CharStreamState(stream)
            let reply = parser stream
            if reply.Status <> Ok then
                stream.BacktrackTo(&state)
            reply
    

    Output is now just:

    Failure:
    Error in Ln: 1 Col: 1
    default
    ^
    keyword instead of identifier