--- breaks: false tags: public-tech --- # Menhir で書いた構文解析器が吐いたエラーを丁寧に表示する この記事は [OCaml Tips Advent Calendar 2022](https://adventar.org/calendars/8396) の四日目です。 OCamllex + Menhir で字句解析器・構文解析器を書いて実行すると、 ユーザーの入力が不正で構文解析に失敗した場合に例外が送出される。 これをうまくハンドルすることで、具体的にどの入力がまずかったかという 情報をグラフィカルに表示できる。ただし OCamllex や Menhir が投げる例外そのものには 位置情報が載っていない。代わりに `Lexing.lexeme_start_p` を呼び出して得られる `position` レコードの情報を使う。 とりあえず、例として使う `lib/lexer.mll` と `lib/parser.mly` を作る。 スペース・改行区切りの整数値の列を受け取る。 ```ocaml= $ vim lib/lexer.mll {} rule main = parse | ' '+ { main lexbuf } | '\n' { Lexing.new_line lexbuf; main lexbuf } | ['_' 'a'-'z' 'A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '\'']* { Parser.ID (Lexing.lexeme lexbuf) } | "-"? ['0'-'9']+ { Parser.INTV (int_of_string (Lexing.lexeme lexbuf)) } | eof { Parser.EOF } $ vim lib/parser.mly %{ %} %token EOF %token <int> INTV %token <string> ID %start toplevel %type <int list> toplevel %% toplevel: | l=list(INTV) EOF { l } ``` これを呼び出す関数を作る。まず `Lexing.from_channel` を使って字句解析器を作り、 これを `Parser.toplevel` に渡すことで構文解析器を行う。字句解析に失敗した場合 この関数が例外を送出するため `try with` で囲っておく。 ```ocaml= $ vim lib/hello.ml let parse_int_from_stdin () = let lex = Lexing.from_channel stdin in Lexing.set_filename lex "stdin"; try let num = Parser.toplevel Lexer.main lex in Some num with e -> let pos = Lexing.lexeme_start_p lex in let col = pos.pos_cnum - pos.pos_bol in Printf.fprintf stderr "%s\027[1m\027[31m^\027[0m\n%s:%d:%d: syntax error. (%s)\n" (String.make col ' ') pos.pos_fname pos.pos_lnum (col + 1) (Printexc.to_string e); None ``` さて例外が送出された場合 `Lexing.lexeme_start_p` 関数を呼び出すことで、 字句解析がどこで止まったのかを知ることができる。この関数が返す `position` レコードは、行番号を表す `pos_lnum` フィールドと、行先頭のオフセットを表す `pos_bol` 及び、停止箇所のオフセット `pos_cnum` を返す。 そこで、これらの情報から行番号・列番号を復元し、エラーメッセージとして表示する。 `Printf.fprintf` に渡しているエスケープシーケンスによって、当該位置に赤文字で キャレットを表示する。 この関数を呼び出すように `bin/main.ml` を書き換えて(ここは省略) 実行すると次のようになる。 ``` $ dune exec bin/main.exe 42 300 -42 1000 foo ^ stdin:2:10: syntax error. (Yourfavname__Parser.MenhirBasics.Error) ``` ![](https://i.imgur.com/6poyKFq.png) ## 参考 - https://v2.ocaml.org/api/Lexing.html