マーク付き文字列解析器の整理 #4
よく考えてみるとyylexからmclexを分離したからといって可搬性の問題は解消されてなかった。
例えば、
$ echo -n "a(b" | ./mclex_test2 NORMAL_STRING [a] START_MARK NORMAL_STRING [b]
の場合、
(1) main(): mclex()を呼び出す。 (2) mclex(): getchar()から'a'を読み込む。 (3) mclex(): 'a'を返す。 (4) main(): mclex()から'a'を読み込む。 (5) main(): "NORMAL_STRING ["を出力する。 (6) main(): 'a'を出力する。 (7) main(): mclex()を呼び出す。 (8) mclex(): getchar()から'('を読み込む。 (9) mclex(): getchar()から'b'を読み込む。 (10) mclex(): ungetc()で'b'を書き戻す。 (11) mclex(): START_MARKを返す。 (12) main(): mclex()からSTART_MARKを読み込む。 (13) main(): mcunput()でSTART_MARKを書き戻す。 (14) mcunput(): ungetc()で'('を書き戻す。 (15) main(): "]"を出力する。 (16) main(): mclex()を呼び出す。 (17) mclex(): getchar()から'('を読み込む。 (18) mclex(): getchar()から'b'を読み込む。 (19) mclex(): ungetc()で'b'を書き戻す。 (20) mclex(): START_MARKを返す。 (21) main(): mclex()からSTART_MARKを読み込む。 (22) main(): "START_MARK"を出力する。 ...snip
のように、(10)と(14)で連続して二文字をungetcで書き戻すことになる。
実行例のmclex_test2では正しい出力が得られているが、既述のように標準関数としては保証外である。
mclex.cを修正する。
以前の記事で作成したunget.cは使わない。
mclex.c
#include <stdio.h> #include <stdlib.h> #include "mclex.h" #include "y.tab.h" #define ACT_ERROR(msg) \ { \ fputs("error: " msg "\n", stderr); \ exit(1); \ } \ \ #define ACT_UNEXPECTED_EOF ACT_ERROR("mclex detected unexpected EOF") #define ACT_NOT_DIFFER_MC ACT_ERROR("meta characters must differ from each other anytime") #define ACT_INVALID_TOKEN ACT_ERROR("push invalid-typed token back") static int input(void); static void unput(int); static int start_mark = '('; static int end_mark = ')'; static int start_annotation = ':'; /* semantic value */ int mclval; /* lexer for processing only meta-character change */ int mclex(void) { for (;;) { int c = input(), c2; if (c == end_mark) return END_MARK; if (c == start_annotation) { mclval = start_annotation; return START_ANNOTATION; } if (c != start_mark) return c; c2 = input(); if (c2 == start_mark) { int c3 = input(); if (c3 == EOF) ACT_UNEXPECTED_EOF if (c3 == end_mark || c3 == start_annotation) ACT_NOT_DIFFER_MC start_mark = c3; } else if (c2 == end_mark) { int c3 = input(); if (c3 == EOF) ACT_UNEXPECTED_EOF if (c3 == start_annotation || c3 == start_mark) ACT_NOT_DIFFER_MC end_mark = c3; } else if (c2 == start_annotation) { int c3 = input(); if (c3 == EOF) ACT_UNEXPECTED_EOF if (c3 == start_mark || c3 == end_mark) ACT_NOT_DIFFER_MC start_annotation = c3; } else { unput(c2); return START_MARK; } } } /* push a token back to this lexer */ void mcunput(int type) { switch (type) { case EOF: unput(type); break; case START_MARK: unput(start_mark); break; case END_MARK: unput(end_mark); break; case START_ANNOTATION: unput(start_annotation); break; default: ACT_INVALID_TOKEN } } /* init lexer with meta-characters */ int mcinit(int sm, int em, int sa) { if (sm == em || em == sa || sa == sm) return 1; start_mark = sm; end_mark = em; start_annotation = sa; return 0; } static int mcbuf[2]; static int mcpbuf = 0; static int input(void) { return mcpbuf == 0 ? getchar() : mcbuf[--mcpbuf]; } static void unput(int c) { mcbuf[mcpbuf++] = c; }
インタフェイスは変更していないので、mclex.cの変更だけで、前回のmclex_test2.cをコンパイル、実行できる。
$ echo -n "a(b" | ./mclex_test2 NORMAL_STRING [a] START_MARK NORMAL_STRING [b] $ echo -n "this symbol string ((''(''(''()')('):parens and quotes) is analysable" | ./mclex_test2 NORMAL_STRING [this symbol string ] START_MARK NORMAL_STRING [('')] START_ANNOTATION [:] NORMAL_STRING [parens and quotes] END_MARK NORMAL_STRING [ is analysable]
いや、わざと面倒な変更シーケンスの書き方にしているだけで、
$ echo -n "this symbol string ()>((<<(''):parens and quotes> is analysable" | ./mclex_test2
で十分だ。