Home > Software design >  With FParsec is it possible to manipulate the error position when a parser fails?
With FParsec is it possible to manipulate the error position when a parser fails?

Time:03-31

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.

CodePudding user response:

Unfortunately I missed the >>=? operator which apparently is (at least semantically) equivalent to attempt that Brian Berns correctly suggested.

The problem with both of these approaches is the subsequent messages The parser backtracked after:[…] that can cascade if the preceding parsers are backtracking as well:

Error in Ln: 156 Col: 29 (UTF16-Col: 22)
        Block "main" 116x60 font=default fg=textForeground
                            ^
Note: The column count assumes a tab stop distance of 8 chars.
Expecting: space/tab

The parser backtracked after:
  Error in Ln: 156 Col: 42 (UTF16-Col: 35)
          Block "main" 116x60 font=default fg=textForeground
                                           ^
  Note: The column count assumes a tab stop distance of 8 chars.
  Expecting: space/tab
  Other error messages:
    keyword instead of identifier

CodePudding user response:

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
  • Related