昨日は大掃除をして疲れ果てたので The LEX & YACC Page や プログラミング言語を作る/yaccとlex などを読んでなるほど〜となっていた。 Lex の処理部分を改行しつつ書くと、正規表現にマッチしたら何かの処理をするという書き方が AWK に似ているということに気づいてひとりで納得したりした。
今日は JSON でパースの練習をするために、そのさきがけとしてまず JSON の字句解析をできる君を Lex で書いていた。 JSON の仕様は RFC8259 を斜め読みしている。 Strings の定義が以外と単純で、そうなのか、という感じだった。 Lex で書くのも楽。 Numbers は完全に忘れていたけど 1e3 みたいな表記もできたのだった。
unescaped [ !#-\[\]-] escaped \\["\\/bfnrt] escapedunicode \\u[0-9a-fA-F]{4} minus - plus \+ dot \. napiere [eE] digit19 [1-9] digit [0-9] %% \{ { printf("L_CBR\n"); } \} { printf("R_CBR\n"); } \[ { printf("L_SBR\n"); } \] { printf("R_SBR\n"); } : { printf("COLON\n"); } , { printf("COMMA\n"); } \"({unescaped}|{escaped}|{escapedunicode})*\" { printf("STRING\n"); } {minus}?(0|{digit19}{digit}*)({dot}{digit}+)?({napiere}({minus}|{plus})?{digit}+)? { printf("NUM\n"); } true { printf("V_TRUE\n"); } false { printf("V_FALSE\n"); } null { printf("V_NULL\n"); } " " { printf("WHITE\n"); } . { printf("CHAR\n"); }
余談だけど U+10FFFF は手元で上手く出せなくて U+10FFFF Unicode Character からコピペした (UTF8 では 0x10FFFF
なんて無理で 0xF4 0x8F 0xBF 0xBF
とかになるという話だった……)。
この lex ファイルを lex コマンドにかけて gcc コマンドにかけて、とやって得た実行ファイルを実行してやると、入力した JSON の文字を順番に読んでいって字句を解析する様子が見られる。改行を.
でマッチできていないのかよくわからないけど、 JSON ファイルに改行があるとそのまま出力にも改行で入ってしまう。壊れた JSON を渡すと、全く予期していないところで文字列が終わったりすることになるので、急に CHAR とかにフォールバック(?)して面白い。
$ cat in2.json { "クオートMAX": "\\s\\\"\\\"\\\"\\\\\"", "any array": [null, { "str": 0e2 }] } $ ./ajson1.out < in2.json L_CBR WHITE WHITE STRING COLON WHITE STRING COMMA WHITE WHITE STRING COLON WHITE L_SBR V_NULL COMMA WHITE L_CBR WHITE STRING COLON WHITE NUM WHITE R_CBR R_SBR R_CBR
あと、大学でやった実験のテキストの PDF を見つけてきて読みつつ、確かに定義部とかがあって定義しておけたのだった、とか、 Yacc のやることは Lex ができない括弧の対応とか入れ子になっていてとかそういう表現をカバーすることだったな、とか、 Lex と Yacc それぞれを実行して出てくる C言語のファイル lex.yy.c と y.tab.c をそのまま include して使うことができるのだった、とか色々思い出していた。
ちなみに Lex で頑張ってやった Strings や Numbers のつくりは、おそらく字句解析では部品だけにしておいて構文解析でカバーするという手もありそうで、そこは好きに境界を動かしてよさそう。今の感覚では正規表現でできるならできるだけやってしまったほうが BNF が単純になっていいのかな〜という感じ。 Yacc で定義すべき BNF のイメージはなんとなくついたし、そもそも RFC があるのでやはりそこに載っている通りにつくればできるはずなので、続きをやりたい。
(追記 201912302215) 改行は .
でマッチしないの仕様ですと教えてもらえたのと、そういえば RFC にちゃんと空白系はまとめてあったのでちゃんとやるとこうなった:
unescaped [ !#-\[\]-] escaped \\["\\/bfnrt] escapedunicode \\u[0-9a-fA-F]{4} minus - plus \+ dot \. napiere [eE] digit19 [1-9] digit [0-9] %% \{ { printf("L_CBR\n"); } \} { printf("R_CBR\n"); } \[ { printf("L_SBR\n"); } \] { printf("R_SBR\n"); } : { printf("COLON\n"); } , { printf("COMMA\n"); } \"({unescaped}|{escaped}|{escapedunicode})*\" { printf("STRING\n"); } {minus}?(0|{digit19}{digit}*)({dot}{digit}+)?({napiere}({minus}|{plus})?{digit}+)? { printf("NUM\n"); } true { printf("V_TRUE\n"); } false { printf("V_FALSE\n"); } null { printf("V_NULL\n"); } [ \t\n\r]+ { printf("WHITE\n"); } . { printf("CHAR\n"); }
$ ./ajson1.out < in2.json L_CBR WHITE STRING COLON WHITE STRING COMMA WHITE STRING COLON WHITE L_SBR V_NULL COMMA WHITE L_CBR WHITE STRING COLON WHITE NUM WHITE R_CBR R_SBR WHITE R_CBR WHITE