---
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://v2.ocaml.org/api/Lexing.html