Module Bcfg.Error

Introspection of syntax errors.

The parser of bcfg is generated by Menhir, with the --table --inspection flags, and is driven through its incremental API. Instead of one function which either returns a value or raises an exception, the parser is a state machine: parser offers it one token at a time and looks at the resulting checkpoint. When the automaton cannot shift the token it has just been given, Menhir hands back the environment it was in, which is where this module starts.

From that environment we take the top of the LR stack. It gives two things: the Error.state the automaton is in, and the grammar symbol which brought it there, together with its semantic value. This module re-exports these two things with a vocabulary of its own, so that you do not have to depend on the generated Bcfg_parser.MenhirInterpreter module, nor on the names Menhir gives to the productions of our grammar.

What the value tells you.

The symbol is the last one which was recognised, not the token which was rejected. In other words, Error (Terminal Word, "listen") means "the automaton had just read the word listen, and whatever came next does not belong here". The offending piece of text is not in this value: it is designated by the Txtloc.t which accompanies the error, and Txtloc.lines_around_txtloc turns it into printable lines.

Because the type is a GADT, the semantic value is typed: matching Terminal Word gives a string, matching Non_terminal Directive gives a directive. This is what allows an error message to quote the name of the directive which was left open, rather than a byte offset.

How to use it.

Handling an error is a pattern matching on the pair made of the state number and the symbol. Here is a fragment of what bcfg validate does:

let pp_error_message ppf (state, Bcfg.Error.Error (symbol, v)) =
  match (state, symbol) with
  | 10, Bcfg.Error.Terminal LBrace ->
      Format.fprintf ppf
        "An opening curly bracket must always be followed by a line break."
  | 11, Bcfg.Error.Non_terminal Newlines ->
      Format.fprintf ppf "Missing a subdirective."
  | 15, Bcfg.Error.Non_terminal Directive ->
      Format.fprintf ppf
        "The subdirective %S should be followed by a closing curly bracket."
        v.Bcfg.name
  | state, _ -> Format.fprintf ppf "Invalid syntax (state %d)." state

let pp_error ~content ppf = function
  | `Parser_error (txtloc, Some err) ->
      Format.fprintf ppf "Error at %a:@\n%a@\n" Bcfg.Txtloc.pp txtloc
        pp_error_message err;
      let lines = Bcfg.Txtloc.lines_around_txtloc_string ~txtloc content in
      List.iter (pp_line ppf) lines
  | _ -> Format.fprintf ppf "Invalid syntax@\n"

How to find the state numbers.

The numbers are the ones Menhir itself uses. Running

$ menhir --list-errors lib/bcfg_parser.mly

(the build already does it, the result is parser.messages) enumerates every way of reaching an error state, and each entry looks like this:

config: WORD RBRACE
##
## Ends in an error in state: 4.
##
## directive -> WORD . list(WORD) children [ WORD RBRACE EOF ]
##
## The known suffix of the stack is as follows:
## WORD

which reads: an input of the shape WORD RBRACE fails in state 4, the dot shows how far the automaton got in the production, and the known suffix of the stack (WORD) tells you which constructor of Error.symbol will be on top, here Terminal Word. The pair to match is therefore (4, Terminal Word), and the value bound is the word itself, which the message can quote.

The numbers are not stable: they change as soon as the grammar changes. A pattern matching on them must always end with a default case, and it is worth testing the messages you write (this is what test/invalid.t does). Reading the chapter of the Menhir manual about the incremental and inspection APIs is recommended before going further.

type state = int

The number of a state of the LR automaton, as reported by menhir --list-errors.

type 'a terminal =
  1. | Error : unit terminal
    (*

    Menhir's error pseudo-token. Our grammar has no error production, so it never appears in practice.

    *)
  2. | Word : string terminal
    (*

    A word, quoted or not, already unescaped.

    *)
  3. | RBrace : unit terminal
    (*

    }

    *)
  4. | LBrace : unit terminal
    (*

    {

    *)
  5. | Newline : unit terminal
    (*

    A line break, possibly preceded by a comment.

    *)
  6. | Eof : unit terminal
    (*

    The end of the input.

    *)

The terminal symbols of the grammar, that is the tokens produced by the lexer, indexed by the type of their semantic value.

type 'a non_terminal =
  1. | Directives : directive list non_terminal
    (*

    A list of directives at the same level.

    *)
  2. | Newlines : unit non_terminal
    (*

    A run of line breaks.

    *)
  3. | Parameters : string list non_terminal
    (*

    The parameters which follow the name of a directive.

    *)
  4. | Directive : directive non_terminal
    (*

    A complete directive.

    *)
  5. | Children : directive list non_terminal
    (*

    The block between { and }.

    *)
  6. | Top : t non_terminal
    (*

    The whole configuration.

    *)

The non-terminal symbols of the grammar, that is the constructs which have been recognised so far, indexed by the type of their semantic value.

type 'a symbol =
  1. | Terminal : 'a terminal -> 'a symbol
  2. | Non_terminal : 'a non_terminal -> 'a symbol

A grammar symbol, terminal or not.

type t =
  1. | Error : 'a symbol * 'a -> t
    (*

    The symbol on top of the stack when the error was detected, together with its semantic value. The existential quantification is what lets the two travel together; matching the symbol reveals the type of the value.

    *)