マーク付けに使う文字の変更 #5

方針にしたがって変更してみた。
まず、構文解析器。

parser.y
%{
...snip
static char *estrdupcat(const char *s1, const char *s2);
%}
...snip
%%
...snip
%%

int current_start_mark_char = '(';
int current_end_mark_char = ')';
int current_start_annotation_char = ':';

int main(int argc, char *argv[])
{
    int opt;
    while ((opt = getopt(argc, argv, "s:e:a:")) != -1) {
        switch (opt) {
        case 's':
            if (strlen(optarg) > 1) fprintf(stderr, "warning: start mark requires one character: %s\n", optarg);
            current_start_mark_char = optarg[0];
            break;
        case 'e':
            if (strlen(optarg) > 1) fprintf(stderr, "warning: end mark requires one character: %s\n", optarg);
            current_end_mark_char = optarg[0];
            break;
        case 'a':
            if (strlen(optarg) > 1) fprintf(stderr, "warning: start annotation requires one character: %s\n", optarg);
            current_start_annotation_char = optarg[0];
            break;
        default:
            fprintf(stderr, "usage: %s [-s start_mark] [-e end_mark] [-a start_annotation]\n", argv[0]);
            exit(1);
        }
    }
    if (
        current_start_mark_char == current_end_mark_char
        || current_end_mark_char == current_start_annotation_char
        || current_start_annotation_char == current_start_mark_char
    ) {
        fprintf(stderr, "error: meta characters must differ from each other\n");
        exit(1);
    }
    return yyparse();
}

void yyerror(char const *s)
{
...snip
}

static char *estrdupcat(const char *s1, const char *s2)
{
...snip
}

字句解析関係はflexが生成するものに任せるので、
関数yylexとyylex内で使用していた関数estrdupを削った。
大きな変更はこれだけで、構文規則は全くいじる必要がない。
後は、メタ文字を保持するstart_mark_char等の静的変数は、
構文解析側のmain関数で設定し字句解析側で利用するために、
staticでなくしている。それと名前をちょっと変えた。
構文解析器の変更点はこれだけである。
新たにflexで生成する字句解析側については、

lexer.l
%{
#define YYSTYPE char*

#include <stdlib.h>
#include <string.h>
#include "y.tab.h"

extern int current_start_mark_char;
extern int current_end_mark_char;
extern int current_start_annotation_char;

static char *estrdup(const char *s);

#define YY_INPUT(buf,result,max_size) \
    { \
        int i; \
        errno = 0; \
        while ((result = fread(buf, 1, max_size, yyin)) == 0 && ferror(yyin)) { \
            if (errno != EINTR) yy_fatal_error("input failed"); \
            errno = 0; \
            clearerr(yyin); \
        } \
        for (i = 0; i < result; i++) { \
            switch (buf[i]) { \
            case '(': buf[i] = current_start_mark_char; break; \
            case ')': buf[i] = current_end_mark_char; break; \
            case ':': buf[i] = current_start_annotation_char; break; \
            default: \
                if (buf[i] == current_start_mark_char) buf[i] = '('; \
                else if (buf[i] == current_end_mark_char) buf[i] = ')'; \
                else if (buf[i] == current_start_annotation_char) buf[i] = ':'; \
                break; \
            } \
        } \
    } \
\

#ifdef __STRICT_ANSI__
int fileno(FILE *);
#endif

%}

%option noyywrap nounput noinput

%%

[^():]+ {
            int i;
            yylval = estrdup(yytext);
            for (i = 0; i < yyleng; i++) {
                if (yylval[i] == current_start_mark_char) yylval[i] = '(';
                else if (yylval[i] == current_end_mark_char) yylval[i] = ')';
                else if (yylval[i] == current_start_annotation_char) yylval[i] = ':';
            }
            return STRING_EXCEPT_SPECIAL;
        }
\(      { return START_MARK; }
\)      { return END_MARK; }
:       {
            char s[2];
            s[0] = current_start_annotation_char;
            s[1] = '\0';
            yylval = estrdup(s);
            return START_ANNOTATION;
        }

%%

static char *estrdup(const char *s)
{
    char *p = malloc(strlen(s) + 1);
    if (p == NULL) {
        fputs("error: estrdup cannot allocate any memories.\n", stderr);
        exit(1);
    }
    return strcpy(p, s);
}

なんだかYY_INPUTマクロの長さが目立つなあ。
要点は、入力を字句解析に供する前にデフォルトのメタ文字と新たに指定されたメタ文字を交換して、
デフォルトのメタ文字用に構成された字句解析器を騙して解析させ、
構文解析器で必要な文字についてはもう一度デフォルトと新しいメタ文字を交換する、という二重置換である。
この再度の交換はメタ文字を含まない文字列[^():]+:の場合に必要だが、
括弧(に相当するメタ文字)に関してはその値そのものは構文解析で使わないので不要である。

$ bison -dy parser.y

$ flex lexer.l

$ gcc -std=c89 -pedantic -Wall -Wextra -Werror -o parser y.tab.c lex.yy.c

$ echo -n "Now, [(=left parenthesis] and [)=right parenthesis] and [:=colon] are not meta characters." | ./parser -s"[" -e"]" -a"="
string [Now, ]
marked-string [(]: annotation[left parenthesis]
string [ and ]
marked-string [)]: annotation[right parenthesis]
string [ and ]
marked-string [:]: annotation[colon]
string [ are not meta characters.]

$ echo -n '<html lang="ja"><body><img src="foo.jpg" alt="description"></body></html>' | ./parser -s"<" -e">" -a" "
marked-string [html]: annotation[lang="ja"]
marked-string [body]
marked-string [img]: annotation[src="foo.jpg" alt="description"]
marked-string [/body]
marked-string [/html]

$ echo -n "[p:::=x:=k]" | ./parser -s"[" -e"]" -a"="
marked-string [p:::]: annotation[x:=k]

$ echo -n "The (quick: swift) brown (fox) jumps over the (lazy: one of three virtues of a programmer) (dog)." | ./parser
string [The ]
marked-string [quick]: annotation[ swift]
string [ brown ]
marked-string [fox]
string [ jumps over the ]
marked-string [lazy]: annotation[ one of three virtues of a programmer]
string [ ]
marked-string [dog]
string [.]

うまく動いているようだ。
最後の実行例のようにメタ文字がデフォルトのままでも当然正しく動くのだが、
交換処理自体はデフォルトだろうが何だろうが気にせずやってるので、
同じ文字を交換するという無駄処理になっている。

それにしても、yylex自体を手書きする手間は省けたが、
代わりに二重の文字の置換処理でそれなりに記述量が増える結果に。
容量無制限のyytextの実装と字句解析処理をflexに肩代わりさせられることとのトレードオフとなるわけで。
変な策を弄していないだけyylex手書きのほうが素直でよいのかもしれない。