マーク付き文字列解析器の整理 #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

で十分だ。