単純なマーク付けがなされた文字列の解析 #11

二重のswitch文による状態遷移はコンパイラが吐くコードにもよると思うが十分速いものだと思う。
ただ、ソースの記述量が多く、長くなる傾向があり、コンパクトさに欠ける。
状態stateと入力cによる二重のswitch文は二次元のテーブルと等価となる。
そこで、二重switch文をテーブル参照に置き換えてみることを検討する。
このテーブルの項目の内容は、行うべきアクションと次に遷移すべき状態の組でいいだろう。

#include <stdio.h>
#include <stdarg.h>

void eprintf(char const *format, ...);
void neprintf(char const *format, ...);

void eprintf(char const *format, ...)
{
    va_list ap;
    fputs("error: ", stderr);
    va_start(ap, format);
    vfprintf(stderr, format, ap);
    va_end(ap);
    fputc('\n', stderr);
}

void neprintf(char const *format, ...)
{
    va_list ap;
    fputc('\n', stderr);
    va_start(ap, format);
    eprintf(format, ap);
    va_end(ap);
}

#define DEFINE_ACTION(name, code) \
static void act_##name(int c); \
static void act_##name(int c) {(void)(c);{code;}}

#define P(name, next) {act_##name, next}

DEFINE_ACTION(null, {})
DEFINE_ACTION(eprint_1, eprintf("illegal right parenthesis"))
DEFINE_ACTION(neprint_1, neprintf("illegal right parenthesis"))
DEFINE_ACTION(eprint_2, eprintf("unexpected EOF"))
DEFINE_ACTION(neprint_2, neprintf("unexpected EOF"))
DEFINE_ACTION(eprint_3, eprintf("illegal left parenthesis"))
DEFINE_ACTION(neprint_3, neprintf("illegal left parenthesis"))
DEFINE_ACTION(eprint_4, eprintf("null string between parentheses"))
DEFINE_ACTION(start_string, printf("string [%c", c))
DEFINE_ACTION(end_string, puts("]"))
DEFINE_ACTION(start_marked_string, printf("marked-string [%c", c))
DEFINE_ACTION(end_marked_string, puts("]"))
DEFINE_ACTION(put_char, putchar(c))

enum tagState {START, RESTART, STRING, HEAD, BODY, ACCEPT, ERROR};

static struct tagTableItem {
    void (*action)(int c);
    enum tagState next_state;
} table[][4] = {
    { P(null, HEAD), P(eprint_1, ERROR), P(eprint_2, ERROR), P(start_string, STRING) },
    { P(null, HEAD), P(eprint_1, ERROR), P(null, ACCEPT), P(start_string, STRING) },
    { P(end_string, HEAD), P(neprint_1, ERROR), P(end_string, ACCEPT), P(put_char, STRING) },
    { P(eprint_3, ERROR), P(eprint_4, ERROR), P(eprint_2, ERROR), P(start_marked_string, BODY) },
    { P(neprint_3, ERROR), P(end_marked_string, RESTART), P(neprint_2, ERROR), P(put_char, BODY) }
};

static int code[] = {
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,0,1,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 2
};

int main(void)
{
    enum tagState state = START;
    do {
        int c = getchar();
        struct tagTableItem *t = &table[state][code[c == EOF ? 256 : c]];
        t->action(c);
        state = t->next_state;
    } while (state != ACCEPT && state != ERROR);
    return 0;
}

少しだけ短くなった。行数でみるなら約6割に。
少しだけなのは、テーブルやアクションの定義が追加されていることによるもので、
switch文の塊だったmain関数は目論見通り劇的に短くできた。

一文字入力を受けるごとに、現在の状態stateと入力cからテーブルの該当項目を参照して、
定められたアクションを実行し、次に遷移する状態をstateに代入する。
これを状態がACCEPTERRORになるまで繰り返すだけである。
テーブルtableの入力c項は本当は256文字+EOFの257項目あるので、
ACCEPTERRORを除く5状態との組み合わせで、
テーブルサイズは5×257=1285項目必要だが、これはちょっと大きい。
そこで、左括弧、右括弧、EOF、その他の文字の4種類に分類し、
この4分類と5状態の組み合わせの20項目でtableを構成している。
入力を4種類に分類するための表が配列codeである。

DEFINE_ACTIONマクロはstatic関数化したアクションをプロトタイプと定義に展開する。

速度的には二重のswitch文の方が速いだろうなあとは思う。
可読性についても向上しているかといわれるとどうだろう?返って読みにくいかも(ダメダメだ