Closed29

re2cメモ

zenwerkzenwerk

プログラムインターフェース

http://re2c.org/manual/manual_c.html#program-interface

つかえるAPI形式が2つあって使える YY---- の変数が変わる。

ポインタAPI

  • Cバックエンドのデフォルト
  • re2cはもともとC言語向けのツールなので、歴史的経緯含め「デフォルトAPI」扱い
  • ポインタ演算が前提のAPI

汎用API

  • 非ポインタ演算API
  • Goバックエンドでのデフォルト
  • C言語の場合 --input custom もしくは re2c:flags:input = custom; オプションが必要
  • ポインタAPIで生成されるコードを自分でカスタマイズできるということ

汎用APIの2つの書き方

  1. 関数ライク

    • re2c:api:style = functions;
    • C言語バックエンドはデフォルトでこれ
    • APIプリミティブは括弧で囲まれた関数またはマクロとして定義
      • #define YYPEEK() *YYCURSOR → 変数名(引数) ポインタ演算など
  2. 自由形式(フリーフォーム)

    • re2c:api:style = free-form;
    • Goバックエンドではデフォルトでこれ
    • "..." で自由形式で記述。引数は @@ で与えられる(シギルという)
      • re2c:define:YYLESSTHAN = "YYLIMIT - YYCURSOR < @@{len}";

個人的にはC言語でも free-formre2c:define:foo = "..." で書いたほうがわかりやすい。

zenwerkzenwerk

入力終端処理

http://re2c.org/manual/manual_c.html#handling-the-end-of-input

レキサーはいつ停止するのか?

レキサーの行き着く状態は以下の3つ

  1. 定義したルールにマッチしながら、最後の状態(最後の遷移先)に到達する
  2. どのルールにも一致せず、デフォルトの状態になる
  3. 入力された文字列を最後まで読み込む

は成功、は失敗。どちらにしても遷移先がないのでレキサーは停止する。
は入力を終了とするかどうかは条件によって異なる。入力文字をすべて読み込むことはレキサーがどの状態でも起こりうるので、レクサーはそれを適切に処理しなければならない。

何を入力の終了とするかは正規表現ルールの複雑さ、パフォーマンス、入力バッファリングの必要性、その他の要素に関連して決まる。

また、re2cが自動生成するレキサーについての理解が必要になるため、入力終端処理の取り扱いが re2c で一番むずかしい箇所。

EOF(End of File)ルールとも。

zenwerkzenwerk

入力終端処理の方法4つ

re2c に用意された終端処理は大まかに以下の4つ

  1. 番兵文字をつかう(簡単・高効率・制限あり)
  2. パディングによる境界チェック(汎用的だが複雑)
  3. EOFルール → 番兵文字と境界チェックの組み合わせ
    1. 一般的で単純
      • 文法によってはパディングによる境界チェックよりも効率がよい
  4. ジェネリックAPIを使用
    1. ユーザー側ですべての動作を定義するので、生成されるコードでエラーが出る可能性がある。

Lex と違い re2c は入力バッファの管理を自分で行う必要があるため、一番面倒かつ分かりにくい箇所かもしれない

zenwerkzenwerk

番兵文字ルール(入力終端処理)

http://re2c.org/manual/manual_c.html#sentinel-character

番兵とは

データの終了を示すために配置される特殊なデータを指す。

実際にはこの用語は、微妙に異なる以下の2つの意味で使われる。

- 実データには出現しない、データの終了を表すための専用の値
- 入力データを処理するループの終了条件が複数ある場合に、条件判定の数を削減するために置くダミーのデータ

C言語の文字列の \0 ことヌル文字がそれ。
つまり、番兵に指定された文字はレキサーの解析ルール(正規表現)に使われてはいけないし、
レキサーに入力される文字列の最後には必ず番兵文字が付与されていることが条件。

設定

「番兵文字」ルールを使うためには以下のように設定する

  • re2c:yyfill:enable = 0 (バッファ再充填関数が不要)
  • re2c:eof = -1 (EOFルールを設定しない)
  • re2c:sentinel を設定するとレキサーの生成時に正しく設定されているかチェックできる
    • あくまでも設定チェックのための追加機能のようなもの
    • -Wsentinel-in-midrule を指定して警告出力すること

番兵文字ルールのサンプルコード

#include <assert.h>

// '\0'で終わる文字列が来ることを想定している(センチネル文字)。
// 文字列中のスペースで区切られた単語数をカウントする例。
// 入力が十分小さく、明確なセンチネル文字があるパターン。
static int lex(const char *YYCURSOR)
{
    int count = 0;
loop:
    /*!re2c
    re2c:define:YYCTYPE = char;
    // EOFチェックとYYFILLは不要になる
    re2c:yyfill:enable = 0;

    *      { return -1; }
    // re2c:eof = 0 として [\x00] を $ に置き換えても等価だが re2c:sentinel は eof = -1 じゃないとだめ。
    [\x00] { return count; } // センチネル文字
    [a-z]+ { ++count; goto loop; }
    [ ]+   { goto loop; }

    */
}

int main()
{
    assert(lex("") == 0);
    assert(lex("one two three") == 3);
    assert(lex("f0ur") == -1);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>

static int lex(const char *YYCURSOR)
{
	int count = 0;
loop:
{
	char yych;
	yych = *YYCURSOR;
	switch (yych) {
	case 0x00:	goto yy2; // センチネル文字
	case ' ':	goto yy6; // 区切り文字
	case 'a':
	case 'b':
	/* 省略 */
	case 'x':
	case 'y':
	case 'z':	goto yy9; // [a-z]+ { ++count; goto loop; } の継続
	default:	goto yy4; // [a-z]+ { ++count; goto loop; } の失敗
	}
yy2: // [\x00] { return count; }
	++YYCURSOR;
	{ return count; }
yy4: // *      { return -1; }
	++YYCURSOR;
	{ return -1; }
yy6: // [ ]+   { goto loop; }
	yych = *++YYCURSOR;
	switch (yych) {
	case ' ':	goto yy6;
	default:	goto yy8;
	}
yy8:
	{ goto loop; }
yy9:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'a':
	case 'b':
	case 'c':
/* 省略 */
	case 'w':
	case 'x':
	case 'y':
	case 'z':	goto yy9;
	default:	goto yy11;
	}
yy11: // [a-z]+ { ++count; goto loop; } の成功
	{ ++count; goto loop; }
}

}

int main()
{
	assert(lex("") == 0);
	assert(lex("one two three") == 3);
	assert(lex("f0ur") == -1);
	return 0;
}

基本的に goto loop; で先頭に戻ったあと、一番最初の switch に番兵文字判定がくる。

  • 番兵文字の判断はレキサーの初期状態のみに行われる。
    • 番兵文字はルールの途中に現れてはいけないという条件があるため
    • 字句を読み込みレキサーのステートマシンが確定したあと、次の字句解析が始まるのか、それとも番兵文字が現れるのか?
      • 番兵文字だった場合は終了処理に goto してレキサーが終了する
zenwerkzenwerk

番兵文字ルールは re2c:yyfill:enable = 0 が条件なので、バッファ再充填関数を使用できない。
よって、入力される文字があらかじめ予測できる or 固定長など予測できる範囲の場合に使えそう。

ファイル全体の読み込みの責務がレキサー以外の場合なども使えるかもしれない。

zenwerkzenwerk

境界チェック付き番兵文字(入力終端処理)

http://re2c.org/manual/manual_c.html#sentinel-character-with-bounds-checks

汎用的で、使用できる正規表現ルールの制限なしだが、ある文字が「番兵」として入力の最後に付与される点では同じ。

  • 単純な番兵文字ルールの場合は番兵に使用する文字は使用できなかった。
    • つまり入力の途中に「番兵文字」が出現しても意図せずレキサーが止まることがない。

動作の概要

レクサーが番兵文字を読み取ると、追加で境界チェックが実行される。

  • 現在のカーソルが範囲内にある場合、番兵文字を通常の文字として処理しレキサーが継続する。
  • カーソルが範囲外のとき、YYFILL で定義されたバッファ充填関数を実行し追加の文字列を読み込む(re2c:yyfill:enable = 1)。
    • YYFILL が成功した場合、「番兵文字」を通常の処理として処理しレキサーが継続する
    • YYFILL が失敗した場合、「番兵文字」として扱い、レキサーが終了する。

設定方法

  1. 「番兵文字」を re2c:eof = ... で設定する。
  2. 使用の有無に関わらず、re2c:yyfill:enable = ... でバッファ充填関数の設定を明示する
  3. 「番兵文字」の動作を $ で設定する。

サンプルコード

'foo bar buzz' のような引用符で囲まれた文字列中の単語数を数えるサンプル。

番兵文字は\0re2c:eof = 0;)。
NULL文字は入力の最後を検出するために不可欠なシンボル。

単純な「番兵文字ルール」と違い、NULL文字も他の文字と同様にルールの 途中で出現可能

  • 有効なルール → 'aaa\0aa'\0'
  • エラー入力 → 'aaa\0

境界チェックの条件は

  • デフォルトAPI → YYLIMIT <= YYCURSOR
  • 汎用API → YYLESSTHAN(1)

境界チェックが

  • 成功 → 入力文字の解析を継続する
  • 失敗 → レクサーは入力の終わりに達しており、停止する

この例では YYFILLは無効なので追加の文字読み込み処理はない。
よって入力の終わりは以下の3つの可能性

  • レクサーが初期状態の場合は、終端ルール $ にマッチしている
  • それ以外の場合
    • 前にマッチしていたルールにフォールバックする(*も含む)
    • もしくはデフォルト状態に戻る
#include <assert.h>

// expect a null-terminated string
static int lex(const char *str, unsigned int len)
{
    const char *YYCURSOR = str, *YYLIMIT = str + len, *YYMARKER;
    int count = 0;

loop:
    /*!re2c
    re2c:define:YYCTYPE = char;
    re2c:yyfill:enable = 0; // バッファ充填関数を使用しない
    re2c:eof = 0; // ヌル文字(\0)を番兵文字として指定

    *                           { return -1; } // 失敗
    $                           { return count; } // 番兵文字が出現したときのルール
    ['] ([^'\\] | [\\][^])* ['] { ++count; goto loop; }
    [ ]+                        { goto loop; }

    */
}

#define TEST(s, r) assert(lex(s, sizeof(s) - 1) == r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>

// expect a null-terminated string
static int lex(const char *str, unsigned int len)
{
    const char *YYCURSOR = str, *YYLIMIT = str + len, *YYMARKER;
    int count = 0;

loop:
    
{
	char yych;
	yych = *YYCURSOR;
	switch (yych) {
	case ' ':	goto yy4;
	case '\'':	goto yy7;
	default:
		// 初期状態での番兵文字と境界チェック
		if (YYLIMIT <= YYCURSOR) goto yyeof1;
		// 境界チェックに失敗している場合は不正な文字が来たと判断する
		goto yy2;
	}
yy2:
	++YYCURSOR;
yy3: // *    { return -1; }
	{ return -1; }
yy4: // [ ]+     { goto loop; }
	yych = *++YYCURSOR;
	switch (yych) {
	case ' ':	goto yy4;
	default:	goto yy6;
	}
yy6:
	{ goto loop; }
yy7:
	yych = *(YYMARKER = ++YYCURSOR);
	if (yych >= 0x01) goto yy9;
	// カーソルが境界に達してたら失敗
	if (YYLIMIT <= YYCURSOR) goto yy3;
yy8:
	yych = *++YYCURSOR;
yy9:
	switch (yych) {
	case '\'':	goto yy10;
	case '\\':	goto yy12;
	default:
		// カーソルが境界に達してたら失敗
		if (YYLIMIT <= YYCURSOR) goto yy13;
		goto yy8;
	}
yy10:
	++YYCURSOR;
	{ ++count; goto loop; }
yy12:
	yych = *++YYCURSOR;
	if (yych <= 0x00) {
		// カーソルが境界に達してたら失敗
		if (YYLIMIT <= YYCURSOR) goto yy13;
		goto yy8;
	}
	goto yy8;
yy13:
	YYCURSOR = YYMARKER;
	goto yy3;
yyeof1:
	{ return count; }
}

}

#define TEST(s, r) assert(lex(s, sizeof(s) - 1) == r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

レキサーの初期状態(開始状態)に以下のチェックコードが挿入される

  • YYLIMIT がバッファの末尾アドレス、YYCURSORが現在のカーソル位置
    • カーソルがバッファの末尾を超えたときに初期状態が受け付けない文字がきたかどうか
    • つまり入力がバッファ内完全に収まる場合に良いだろうか(?)
default:
	// 初期状態での境界チェック
	if (YYLIMIT <= YYCURSOR) goto yyeof1;
	// 境界チェックに失敗している場合は不正な文字が来たと判断する
	goto yy2;
}
zenwerkzenwerk

↑の例では YYFILL がないため、より柔軟なルールを扱える「番兵文字ルール」という趣きがある。(lex() の開始時にYYCURSORを文字列の先頭アドレス、YYLIMIT を末尾アドレスに都度設定しているため)

zenwerkzenwerk

パディング付き境界チェック(入力終端処理)

https://re2c.org/manual/manual_c.html#bounds-checks-with-padding

パディングを使用した境界チェックを使用して、入力の終わりを処理する方法。

re2cの設定を指定しないときのデフォルト動作(C言語)

  • つまりデフォルト設定のままコード生成するということ
    • re2c:yyfill:enable はデフォルト値が 1 で有効設定
      • よってバッファ充填関数(YYFILL)の定義が必要
    • re2c:eof はデフォルト値が -1 で無効設定

汎用的かつ「境界チェック付き番兵文字」より(一般的には)早く動作する。
しかし「境界チェック付き番兵文字」よりは使用方法が複雑。

基礎アイディア

  1. レキサーの有限状態オートマトンを強連結成分(SCC)に分割
  2. SCCごとに1つの境界チェックのみを生成
  3. チェック時に一度に複数の文字をチェックする

この方法だとチェックの頻度が少なくなり、実行が高速になる
チェックで十分な入力がない場合、YYFILLを呼び出し失敗した場合はレキサーは停止する。

境界チェック

境界チェックは、レキサーのオートマトンの強く接続された構成要素(SCC)に依存するいくつかの状態でのみ生成される

それは以下の形式で表される。

  • デフォルトAPI → (YYLIMIT - YYCURSOR) < n
  • 汎用API → YYLESSTHAN(n)

n は、レキサーが処理を進めるのに必要な文字数の最小値。

  • レキサーを動かし続けるには、少なくともn文字供給されなければならない、ということ。
  • これはまた、次の境界チェックが最大n文字で行われることも意味する

境界チェックが成功した場合、レクサーはマッチングを続行する。

境界チェックが失敗した場合

  • レクサーは入力の終わりに到達し、YYFILL(n)を呼び出し、以下のパターン
    • 少なくともn個の入力文字を供給する
    • なにも返さない

マルチバイト文字のときのパディング問題

マルチバイト文字の場合は、入力の最後の字句を一致させるときに問題が発生する可能性がある。

  • 以下を読む限り、/*!max:re2c */は使用するエンコードによって変わる可能性があるな。
    • アスキー文字前提なら 1 だが utf-8 やら utf-32 なら 32 になる可能性がある(YYCTYPEcharのとき)

This approach has a problem with matching short lexemes at the end of input, because the multi-character check requires enough characters to cover the longest possible lexeme. To fix this problem, it is necessary to append a few fake characters at the end of input. The padding should not form a valid lexeme suffix to avoid fooling the lexer into matching it as part of the input. The minimum sufficient length of padding is YYMAXFILL and it is autogenerated by re2c with /!max:re2c/

このアプローチでは、入力の最後に短い語彙素を一致させることに問題があります。これは、複数文字のチェックでは、可能な限り長い語彙素をカバーするのに十分な文字が必要になるためです。この問題を修正するには、入力の最後にいくつかの偽の文字を追加する必要があります。字句解析器をだまして入力の一部として一致させることを避けるために、パディングは有効な語彙素接尾辞を形成するべきではありません。パディングの最小の十分な長さはYYMAXFILLであり、/ *!max:re2c * /を指定したre2cによって自動生成されます。このメソッドは、re2c:yyfill:enableのデフォルト値がゼロ以外であり、re2c:eofのデフォルト値が-1の場合に使用されます。

設定方法

  1. /*!max:re2c*/ を書く
  2. re2c:eof = -1 (デフォルト設定なので記載する必要なし)
  3. re2c:yyfill:enable = 1 (デフォルト設定なので記載する必要なし)
  4. re2c:define:YYFILL を定義する

サンプルコード

YYMAXFILLの分だけNULL文字を追加する。(/*!max:re2c*/

  • パディングに使う文字は、構文エラーにならない文字ならなんでも良い。
    • 今回のサンプルでは ' のような一重引用符はNG!

この方法には、パディング文字にマッチする「停止ルール」がある。

  • 今回の例ではNULL文字
    • 停止ルールは、パディングの開始位置でマッチしたときのみ成功する。
    • それ以外での(迷子の)NULL文字マッチは構文エラーとなる。
#include <assert.h>
#include <stdlib.h>
#include <string.h>

/* YYMAXFILL の値を自動生成する */
/*!max:re2c*/

// expect YYMAXFILL-padded string
static int lex(const char *str, unsigned int len)
{
    const char *YYCURSOR = str, *YYLIMIT = str + len + YYMAXFILL;
    int count = 0;
loop:
    /*!re2c
    re2c:api:style = free-form;
    re2c:define:YYCTYPE = char;
    re2c:define:YYFILL = "return -1;"; // 説明単純化のため常に失敗するように設定

    *                           { return -1; }
    // 停止ルール
    [\x00]                      { return YYCURSOR + YYMAXFILL - 1 == YYLIMIT ? count : -1; }
    ['] ([^'\\] | [\\][^])* ['] { ++count; goto loop; }
    [ ]+                        { goto loop; }

    */
}

// YYMAXFILL の分だけヌル文字を末尾にパディングしてレキサーに渡す
static void test(const char *str, unsigned int len, int res)
{
    char *s = (char*) malloc(len + YYMAXFILL);
    memcpy(s, str, len);
    memset(s + len, 0, YYMAXFILL); // YYMAXFILL分\0を末尾にパディングセットする
    int r = lex(s, len);
    free(s);
    assert(r == res);
}

#define TEST(s, r) test(s, sizeof(s) - 1, r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#define YYMAXFILL 1


// expect YYMAXFILL-padded string
static int lex(const char *str, unsigned int len)
{
    const char *YYCURSOR = str, *YYLIMIT = str + len + YYMAXFILL;
    int count = 0;

loop:
    
{
	char yych;
	if (YYLIMIT <= YYCURSOR) return -1; // YYFILL の処理
	yych = *YYCURSOR;
	switch (yych) {
	case 0x00:	goto yy2; // パディング文字
	case ' ':	goto yy6;
	case '\'':	goto yy9;
	default:	goto yy4;
	}
yy2:
	++YYCURSOR;
	// 停止ルールのチェック
	{ return YYCURSOR + YYMAXFILL - 1 == YYLIMIT ? count : -1; }
yy4:
	++YYCURSOR;
	{ return -1; }
yy6: // [ ]+  { goto loop; }
	++YYCURSOR;
	if (YYLIMIT <= YYCURSOR) return -1; // YYFILL
	yych = *YYCURSOR;
	switch (yych) {
	case ' ':	goto yy6;
	default:	goto yy8;
	}
yy8:
	{ goto loop; }
yy9:
	++YYCURSOR;
	if (YYLIMIT <= YYCURSOR) return -1; // YYFILL
	yych = *YYCURSOR;
	switch (yych) {
	case '\'':	goto yy11;
	case '\\':	goto yy13;
	default:	goto yy9;
	}
yy11:
	++YYCURSOR;
	{ ++count; goto loop; }
yy13:
	++YYCURSOR;
	if (YYLIMIT <= YYCURSOR) return -1; // YYFILL
	yych = *YYCURSOR;
	goto yy9;
}

}

// make a copy of the string with YYMAXFILL zeroes at the end
static void test(const char *str, unsigned int len, int res)
{
    char *s = (char*) malloc(len + YYMAXFILL);
    memcpy(s, str, len);
    memset(s + len, 0, YYMAXFILL); // YYMAXFILL分\0を末尾にパディングセットしている
    int r = lex(s, len);
    free(s);
    assert(r == res);
}

#define TEST(s, r) test(s, sizeof(s) - 1, r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

生成されるコードは、レキサーのオートマトンが遷移するごとに以下のようにYYFILLコードが挿入される

yy6: // [ ]+  { goto loop; }
	++YYCURSOR;
	if (YYLIMIT <= YYCURSOR) return -1; // YYFILL
	yych = *YYCURSOR;
	switch (yych) {
	case ' ':	goto yy6;
	default:	goto yy8;
	}

つまり、遷移するごとに現在位置がYYLIMITに達しているかチェックしている

最後、パディング文字でのルールで、以下のように確認している

  • (現在のカーソル位置+YYMAXFILL-1) が YYLIMIT と等しいか?
    • これが「停止ルール」
	{ return YYCURSOR + YYMAXFILL - 1 == YYLIMIT ? count : -1; }

これらのルールは以下のような終端文字をパディングするユーティリティ関数の存在が前提となっている。

// make a copy of the string with YYMAXFILL zeroes at the end
// 入力文字列の最後にYYMAXFILLの分だけ\0をパディングした文字列のコピーを作成する
static void test(const char *str, unsigned int len, int res)
{
    char *s = (char*) malloc(len + YYMAXFILL);
    memcpy(s, str, len);
    memset(s + len, 0, YYMAXFILL); // YYMAXFILL分\0を末尾にパディングセットしている
    int r = lex(s, len);
    free(s);
    assert(r == res);
}
zenwerkzenwerk

「パディング付き境界チェック」は、ようは「パディング」が「番兵文字」の扱いだけど、
「番兵文字」は単に出現したら終わり!という単純さがあるけど、この方法場合は別途「停止ルール」を定義する必要があるから「複雑」と作者は言っているのだろうか?

また「番兵文字」と違って、バッファ充填関数を使う前提の方法なので文字読み込みバッファ内の文字が同的に変化する前提の処理ということも「番兵文字」と呼ばず「パディング」と言っているのだろうか。

このサンプルでは YYFILL-1 を返すのみ、かつ固定長の文字列の末尾に YYMAXFILL の分だけ \0 を付与するので、単に「番兵文字」を長くしただけの方法のように感じられ、利点が薄く感じる。

YYFILL を実際にどう活用してパディング文字を扱うのか確認が必要。

  • YYFILL するたびにパディングしてたら、偽終了してしまうよね?
zenwerkzenwerk

汎用APIを使ったカスタムメソッド(入力終端処理)

http://re2c.org/manual/manual_c.html#custom-methods-with-generic-api

汎用APIは、文字の読み込みなどの基本的な動作を上書きできる。

  • 自動生成されるコードの各部分を自分でカスタムできる。
  • 当然、入力終端処理の部分もカスタムできる。

汎用APIは --input custom オプションか re2c:flags:input = custom を指定し、かつ re2c:yyfill:enable = 0 を指定したときに有効。

サンプルコード

「境界チェック付き番兵文字」のサンプルを汎用APIで実装する例。
以下の点が異なる

  • 入力文字列がNULL文字(\0)で終了しない
    • この方法は、単一の番兵文字でさえも、パディングをまったく行うことができない場合に使う
  • 入力の最後に番兵文字がこないことをカバーするために、YYPEEKが次の文字の入力前に境界チェックをするように再定義している。
    • 読み込みのたびにチェック処理を行うので非効率である点に注意
#include <assert.h>
#include <stdlib.h>
#include <string.h>

// expect a string without terminating null
static int lex(const char *str, unsigned int len)
{
    const char *cur = str, *lim = str + len, *mar;
    int count = 0;

loop:
    /*!re2c
    re2c:yyfill:enable = 0;
    re2c:eof = 0;
    re2c:flags:input = custom;
    re2c:api:style = free-form;
    re2c:define:YYCTYPE    = char;
    re2c:define:YYLESSTHAN = "cur >= lim";
    re2c:define:YYPEEK     = "cur < lim ? *cur : 0";  // fake null
    re2c:define:YYSKIP     = "++cur;";
    re2c:define:YYBACKUP   = "mar = cur;";
    re2c:define:YYRESTORE  = "cur = mar;";

    *                           { return -1; }
    $                           { return count; }
    ['] ([^'\\] | [\\][^])* ['] { ++count; goto loop; }
    [ ]+                        { goto loop; }

    */
}

// make a copy of the string without terminating null
// C言語の文字列の末尾ヌル文字を削除してレキサーに渡す関数
static void test(const char *str, unsigned int len, int res)
{
    char *s = (char*)malloc(len);
    memcpy(s, str, len);
    int r = lex(s, len);
    free(s);
    assert(r == res);
}

#define TEST(s, r) test(s, sizeof(s) - 1, r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdlib.h>
#include <string.h>

// expect a string without terminating null
static int lex(const char *str, unsigned int len)
{
    const char *cur = str, *lim = str + len, *mar;
    int count = 0;

loop:
    
{
	char yych;
	yych = cur < lim ? *cur : 0;
	switch (yych) {
	case ' ':	goto yy4;
	case '\'':	goto yy7;
	default:
		if (cur >= lim) goto yyeof1;
		goto yy2;
	}
yy2:
	++cur;
yy3:
	{ return -1; }
yy4:
	++cur;
	yych = cur < lim ? *cur : 0;
	switch (yych) {
	case ' ':	goto yy4;
	default:	goto yy6;
	}
yy6:
	{ goto loop; }
yy7:
	++cur;
	mar = cur;
	yych = cur < lim ? *cur : 0;
	if (yych >= 0x01) goto yy9;
	if (cur >= lim) goto yy3;
yy8:
	++cur;
	yych = cur < lim ? *cur : 0;
yy9:
	switch (yych) {
	case '\'':	goto yy10;
	case '\\':	goto yy12;
	default:
		if (cur >= lim) goto yy13;
		goto yy8;
	}
yy10:
	++cur;
	{ ++count; goto loop; }
yy12:
	++cur;
	yych = cur < lim ? *cur : 0;
	if (yych <= 0x00) {
		if (cur >= lim) goto yy13;
		goto yy8;
	}
	goto yy8;
yy13:
	cur = mar;
	goto yy3;
yyeof1:
	{ return count; }
}

}

// make a copy of the string without terminating null
static void test(const char *str, unsigned int len, int res)
{
    char *s = (char*) malloc(len);
    memcpy(s, str, len);
    int r = lex(s, len);
    free(s);
    assert(r == res);
}

#define TEST(s, r) test(s, sizeof(s) - 1, r)
int main()
{
    TEST("", 0);
    TEST("'qu\0tes' 'are' 'fine: \\'' ", 3);
    TEST("'unterminated\\'", -1);
    return 0;
}

生成されるC言語のコードがそれぞれ自分で定義した処理に置き換わっている。
どの部分をカスタマイズしたいのかを比較すれば、どのように変更したいかが把握しやすくなるだろう。

zenwerkzenwerk

汎用APIプリミティブ

YYPEEK

  • 引数なし(void)
  • 型がYYCTYPEで、現在の入力文字を返すように定義
    • つまりカーソルを表す関数かな?

現在文字を取得しようとするときに毎回走る処理。
これは「デフォルトAPI」では単に「++in->cursor」となる箇所。

YYSKIP

  • 引数なし
  • 現在の入力位置(カーソル)を一文字先にすすめる関数を定義すること

YYBACKUP

  • 引数なし
  • あとでYYRESTOREにリストアされる、現在のカーソル位置を保存する関数を定義すること

YYBACKUPCTX

  • 引数なし(ゼロ?)
  • あとでYYRESTORECTXでリストアされる、trailing context として現在のカーソル位置を保存する関数を定義すること。

YYSTAGP

  • 引数 tag
  • tagの値を現在のカーソル位置にする関数を定義する。

YYSTAGN

  • 引数 tag
  • tagの値をNULLやデフォルト値に変更する関数を定義する。

YYMTAGP

  • 引数 tag
  • tagの履歴に現在のカーソル位置を追加する関数を定義すること。

YYMTAGN

  • 引数tag
  • tagの履歴にNULLやデフォルト値を追加する関数を定義すること。

YYRESTORE

  • 引数なし
  • YYBACKUP で保存された位置にカーソルを戻す関数を定義すること。

YYRESTORECTX

  • 引数なし
  • YYBACKUPCTXで保存されたtrailing context position にカーソルを戻す関数を定義すること。

YYRESTORETAG

  • 引数 tag
  • trailing context positiontagの値に復元する関数を定義すること。

YYSHIFT

  • 引数 shiftintで負の数も可能)
  • shift数分、カーソル位置を移動する関数を定義すること。

YYSHIFTSTAG

  • 引数 tag, shift
  • tagshift数分移動する関数を定義すること。

YYSHIFTMTAG

  • 引数 tag, shift
  • tagの最新の値をshift数分移動する関数を定義すること。

YYLESSTHAN

  • 引数 len

  • 残りの入力文字数が len 未満のとき true を返すように定義

    • yyfill:enable = 0 のとき生成されない(YYFILLと同じ!)
  • yyfill:enable = 1 かつ eof = -1のとき

    • if (YYLESSTHANで定義した条件式) YYFILLで定義した関数; が挿入される。
  • yyfill:enable = 0 かつ yyfill:eofが設定されているとき(EOFルール有効)のとき

    • if (YYLESSTHANで定義した条件式) goto yyeof1 が挿入される
  • yyfill:enable = 1 かつ yyfill:eofが設定されているとき(EOFルール有効のとき)

    • if (YYLESSTHANで定義した条件式) {
        		if (YYFILL) goto yyFillLabel0;
        		goto yyeof1;
        	}
      
    • が生成される。

zenwerkzenwerk

バッファ再充填

http://re2c.org/manual/manual_c.html#buffer-refilling

入力文字が確保したメモリ領域に一度に全部マッピングできないときに必要な処理。

  • 入力が巨大か、Socketのようなストリーミング入力などの場合、etc...

なおバッファ再充填(YYFILL)関数はデフォルトで有効(re2c:yyfill:enable = 1

バッファ充填処理の基本的な方針は

  • 固定サイズのバッファをallocしてバッファに収まるチャンク単位で入力文字を処理
  • 現在のチャンクを処理したら、それをmove outして新しいデータをmove in
zenwerkzenwerk

バッファ再充填の動作要素

以下の各要素を使ってバッファ再充填関数が動作する。

cursor → 次に読み取る文字。

  • デフォルトAPI → YYCURSOR
  • 汎用API    → YYSKIPYYPEEK

limit → 利用可能な最後の入力文字の 後の位置

  • デフォルトAPI → YYLIMIT
  • 汎用API    → YYLESSTHAN で暗黙的に処理する
    • つまりYYLIMITのような明示的な定義は存在せず YYLESSTHAN のなかで最後の位置を操作する処理を入れてくださいということ(?)

marker → 最後に一致した字句の位置

  • デフォルトAPI → YYMARKER
  • 汎用API    → YYBACKUPYYRESTORE

token → 現在マッチしているトークンの開始位置

  • re2c側ではAPIの用意なし

context marker → trailing context(末尾コンテキスト)の位置

  • デフォルトAPI → YYCTXMARKER
  • 汎用API    → YYBACKUPCTXYYRESTORECTX

tag variables → /*!stags:re2c *//*!mtags:re2c */で定義した、サブマッチの位置。

  • デフォルトAPI → なし
  • 汎用API    → YYSTAGPYYSTAGNYYMTAGPYYMTAGN

上記要素のどれを使うかは場合によりけり。使う場合は YYFILL での処理で更新されなければならない(must

対象としてアクティブな領域は tokencursorの間の領域に含まれているため、バッファの先頭とtokenの間の領域はfree/delete可能で、tokenからlimitまでの領域をバッファの先頭に移動し、バッファの最後に空き領域を移動して、新しいデータを入力する

再充填領域は大きいほうがYYFILLの回数が減って高効率。

YYFILL関数の実装の詳細は、使用する入力終端処理によって変わる。
re2c:eof の場合は「パディング付き境界チェック」の場合(つまり re2c:eof = -1)よりもいくらか単純。

また、-f / --storable-state オプションを使用すると、YYFILLのセマンティクスが変わるので注意。

zenwerkzenwerk

EOFルールの場合のYYFILL(バッファ再充填処理)

http://re2c.org/manual/manual_c.html#yyfill-with-sentinel-character

re2c:eof = ... を設定している場合のバッファ再充填処理の実装は以下の方針となる

  • 引数なしの関数プリミティブとして動作する
    • 返り値が0なら成功、非ゼロ値なら失敗

YYFILL呼び出し条件は以下

  • デフォルトAPI → YYLIMIT <= YYCURSOR
  • 汎用API    → YYLESSTHAN()

以下のように動作させる

  • 少なくとも1文字以上バッファに供給し、入力位置を調整する
  • limit位置を、バッファ内の最後の入力文字の1文字後ろに常に設定する(must
    • limit の位置には re2c:eof で指定した番兵文字を設置する

動作図解

re2c:eof = # とした場合

  • bufferはバッファ開始位置
  • token は現在読み込んでいる字句の開始位置
  • marker は最後に一致した字句の位置
  • cursor は現在読み込んでいる位置
  • limit は番兵文字がある位置
YYFILL前
               <-- shift -->
             >-A------------B---------C-------------D#-----------E->
             buffer       token    marker         limit,
                                                  cursor

YYFILL後

buffer~tokenまではすでに解析済みの領域なので shift してバッファから消去する
shift した分だけ token,marker,cursor が後ろに移動する

>-A------------B---------C-------------D------------E#->
             buffer,  marker        cursor        limit
             token

バッファー全体を満たすのに十分な入力がない場合(入力ファイルの最後の方など)

YYFILL前
               <-- shift -->
             >-A------------B---------C-------------D#--E (EOF)
             buffer       token    marker         limit,
                                                  cursor

YYFILL後
shift 分だけtoken,marker,cursor がずれるが、
番兵文字は必ず入力文字の後ろに配置しなければらないので EndofFile の位置に従って limit 位置も移動する

>-A------------B---------C-------------D---E#........
             buffer,  marker       cursor limit
             token
zenwerkzenwerk

EOFルールのYYFILLのサンプルコード

入力ファイルinput.txt4096バイトのチャンクで読み取り、EOFルールを使用するプログラムの例

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define SIZE 4096

typedef struct {
    FILE *file;
    // 最後の入力文字の1文字後ろ番兵文字を付与する必要があるので SIZE+1 のメモリを用意する
    char buf[SIZE + 1], *lim, *cur, *mar, *tok;
    int eof;
} Input;

static int fill(Input *in)
{
    // ファイル終端に到達済み
    if (in->eof) {
        return 1;
    }
    // free = (現在解析している箇所 - バッファの開始地点) = 解析済みの領域
    const size_t free = in->tok - in->buf;
    if (free < 1) { // shift する領域がない
        return 2;
    }
    // free分だけバッファをシフトする
    memmove(in->buf, in->tok, in->lim - in->tok);
    // シフトした分だけアドレス位置を更新する
    in->lim -= free;
    in->cur -= free;
    in->mar -= free;
    in->tok -= free;
    // シフトした分だけファイルから読み込む
    in->lim += fread(in->lim, 1, free, in->file);
    // 番兵文字をセットする
    in->lim[0] = 0;
    // ファイル終端まで読み込んだかチェックしてフラグをセットしておく
    in->eof |= in->lim < in->buf + SIZE;
    return 0;
}

static void init(Input *in, FILE *file)
{
    in->file = file;
    in->cur = in->mar = in->tok = in->lim = in->buf + SIZE;
    in->eof = 0;
    fill(in);
}

static int lex(Input *in)
{
    int count = 0;
loop:
    in->tok = in->cur;

    /*!re2c
    re2c:eof = 0;
    re2c:api:style = free-form;
    re2c:define:YYCTYPE  = char;
    re2c:define:YYCURSOR = in->cur;
    re2c:define:YYMARKER = in->mar;
    re2c:define:YYLIMIT  = in->lim;
    re2c:define:YYFILL   = "fill(in) == 0";

    *                           { return -1; }
    $                           { return count; }
    ['] ([^'\\] | [\\][^])* ['] { ++count; goto loop; }
    [ ]+                        { goto loop; }

    */
}

int main()
{
    const char *fname = "input";
    const char str[] = "'qu\0tes' 'are' 'fine: \\'' ";
    FILE *f;
    Input in;

    // prepare input file: a few times the size of the buffer,
    // containing strings with zeroes and escaped quotes
    f = fopen(fname, "w");
    for (int i = 0; i < SIZE; ++i) {
        fwrite(str, 1, sizeof(str) - 1, f);
    }
    fclose(f);

    f = fopen(fname, "r");
    init(&in, f);
    assert(lex(&in) == SIZE * 3);
    fclose(f);

    remove(fname);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define SIZE 4096

typedef struct {
    FILE *file;
    char buf[SIZE + 1], *lim, *cur, *mar, *tok;
    int eof;
} Input;

static int fill(Input *in)
{
    if (in->eof) {
        return 1;
    }
    const size_t free = in->tok - in->buf;
    if (free < 1) {
        return 2;
    }
    memmove(in->buf, in->tok, in->lim - in->tok);
    in->lim -= free;
    in->cur -= free;
    in->mar -= free;
    in->tok -= free;
    in->lim += fread(in->lim, 1, free, in->file);
    in->lim[0] = 0;
    in->eof |= in->lim < in->buf + SIZE;
    return 0;
}

static void init(Input *in, FILE *file)
{
    in->file = file;
    in->cur = in->mar = in->tok = in->lim = in->buf + SIZE;
    in->eof = 0;
    fill(in);
}

static int lex(Input *in)
{
    int count = 0;
loop:
    in->tok = in->cur;
{
	char yych;
yyFillLabel0:
	yych = *in->cur;
	switch (yych) {
	case ' ':	goto yy4;
	case '\'':	goto yy7;
	default:
		/* 受諾できる文字以外のものが来た場合に yyfill チェック */
		if (in->lim <= in->cur) {
			/* yyfill が成功 = 新しい文字が充填されたので再度チェックするため戻る */
			if (fill(in) == 0) goto yyFillLabel0;
			/* 初期状態でのEOFルール適用 */
			goto yyeof1;
		}
		goto yy2;
	}
yy2:
	++in->cur;
yy3: // *      { return -1; }
	{ return -1; }
yy4: // [ ]+   { goto loop; }
	++in->cur;
yyFillLabel1:
	yych = *in->cur;
	switch (yych) {
	case ' ':	goto yy4;
	default:
		/* 受諾できる文字以外のものが来た場合に yyfill チェック */
		if (in->lim <= in->cur) {
			if (fill(in) == 0) goto yyFillLabel1;
		}
		goto yy6;
	}
yy6:
	{ goto loop; }
yy7:
	in->mar = ++in->cur;
yyFillLabel2:
	yych = *in->cur;
	if (yych >= 0x01) goto yy9;
	/* 受諾できる文字以外のものが来た場合に yyfill チェック */
	if (in->lim <= in->cur) {
		if (fill(in) == 0) goto yyFillLabel2;
		goto yy3;
	}
yy8:
	++in->cur;
yyFillLabel3:
	yych = *in->cur;
yy9:
	switch (yych) {
	case '\'':	goto yy10;
	case '\\':	goto yy12;
	default:
		/* 受諾できる文字以外のものが来た場合に yyfill チェック */
		if (in->lim <= in->cur) {
			if (fill(in) == 0) goto yyFillLabel3;
			goto yy13;
		}
		goto yy8;
	}
yy10:
	++in->cur;
	{ ++count; goto loop; }
yy12:
	++in->cur;
yyFillLabel4:
	yych = *in->cur;
	if (yych <= 0x00) {
		/* 受諾できる文字以外のものが来た場合に yyfill チェック */
		if (in->lim <= in->cur) {
			if (fill(in) == 0) goto yyFillLabel4;
			goto yy13;
		}
		goto yy8;
	}
	goto yy8;
yy13:
	in->cur = in->mar;
	goto yy3;
yyeof1:
	{ return count; }
}

}

int main()
{
    const char *fname = "input";
    const char str[] = "'qu\0tes' 'are' 'fine: \\'' ";
    FILE *f;
    Input in;

    // prepare input file: a few times the size of the buffer,
    // containing strings with zeroes and escaped quotes
    f = fopen(fname, "w");
    for (int i = 0; i < SIZE; ++i) {
        fwrite(str, 1, sizeof(str) - 1, f);
    }
    fclose(f);

    f = fopen(fname, "r");
    init(&in, f);
    assert(lex(&in) == SIZE * 3);
    fclose(f);

    remove(fname);
    return 0;
}
zenwerkzenwerk

YYFILL が入ると複雑に見えるが(実際複雑だけど…)自動生成されるコードは
「境界チェック番兵文字」に goto が追加されるだけ。

https://zenn.dev/zenwerk/scraps/3c9fd30757436c149ad1#comment-23e167bcbd375195500a

これが

{
	char yych;
	yych = *YYCURSOR;
	switch (yych) {
	case ' ':	goto yy4;
	case '\'':	goto yy7;
	default:
		// 初期状態での番兵文字と境界チェック
		if (YYLIMIT <= YYCURSOR) goto yyeof1;
		// 境界チェックに失敗している場合は不正な文字が来たと判断する
		goto yy2;
	}

こう変化する

{
	char yych;
yyFillLabel0:
	yych = *in->cur;
	switch (yych) {
	case ' ':	goto yy4;
	case '\'':	goto yy7;
	default:
		/* 受諾できる文字以外のものが来た場合に yyfill チェック */
		if (in->lim <= in->cur) {
			/* yyfill が成功 = 新しい文字が充填されたので再度チェックするため戻る */
			if (fill(in) == 0) goto yyFillLabel0;
			/* 初期状態でのEOFルール適用 */
			goto yyeof1;
		}
		goto yy2;
	}

YYFILLの出現で goto の分岐が増えるが、番兵文字の基本的な考え方である以下は変わらない

基本的に goto loop; で先頭に戻ったあと、一番最初の switch に番兵文字判定がくる。

- 番兵文字の判断はレキサーの初期状態のみに行われる。
    - 番兵文字はルールの途中に現れてはいけないという条件があるため
    - 字句を読み込みレキサーのステートマシンが確定したあと、次の字句解析が始まるのか、それとも番兵文字が現れるのか?
        - 番兵文字だった場合は終了処理に goto してレキサーが終了する
zenwerkzenwerk

パディング付き境界チェックのYYFILL(バッファ再充填処理)

EOFルール未使用(re2c:eof=-1)の場合、YYFILLは引数1つ取り返り値voidの関数ライクなプリミティブとなる。

  • C言語で例えるなら void YYFILL(int n)
  • 引数nは「最低限充填されるべき文字数」
    • 最低n文字充填しないとルールに沿ったLexingができなくなるらしい。
    • なのでn文字の読み込んでバッファに再充填できなかったら、呼び出し元に戻らずレキサーを終了させる
      • 失敗したときに呼び出し元の関数から戻るマクロとして実装するのがお勧め

YYFILL成功時

  • YYLIMITの位置を更新する
    • バッファ内の最後の入力位置の後
    • YYMAXFILLのパディング終了位置(引数分の文字を読み込んだがバッファ全体を埋めるほどの入力がない場合)

YYFILLの呼び出しトリガーは以下

  • デフォルトAPI → (YYLIMIT - YYCURSOR) < n
  • 汎用API    → YYLESSTHAN(n) が trueを返す

動作図解

# がパディング文字
need が YYFILL に渡される引数の値で、その数だけの文字が必要

YYFILL前
               <-- shift -->                 <-- need -->
             >-A------------B---------C-----D-------E---F--------G->
             buffer       token    marker cursor  limit

YYFILL後

シフトした分だけtoken,marker,cursorが移動する。
G地点までの文字でバッファ全体が埋まったのでパディングなし

>-A------------B---------C-----D-------E---F--------G->
             buffer,  marker cursor               limit

ファイル終端に差し掛かった場合

YYFILL前
               <-- shift -->                 <-- need -->
             >-A------------B---------C-----D-------E-F (EOF)
             buffer       token    marker cursor  limit

YYFILL後

F地点で入力が終わるためYYMAXFILLの分だけ末尾にパディングを追加する

>-A------------B---------C-----D-------E-F###############
             buffer,  marker cursor                   limit
             token                        <- YYMAXFILL ->
zenwerkzenwerk

パディング付き境界チェックYYFILLのサンプルコード

#include <assert.h>
#include <stdio.h>
#include <string.h>

/*!max:re2c*/
#define SIZE 4096

typedef struct {
    FILE *file;
    // YYMAXFILL分のパディング分だけバッファを広くとる
    char buf[SIZE + YYMAXFILL], *lim, *cur, *mar, *tok;
    int eof;
} Input;

static int fill(Input *in, size_t need)
{
    // ファイル終端に到達済み
    if (in->eof) {
        return 1;
    }
    // free = (現在解析している箇所 - バッファの開始地点) = 解析済みの領域
    const size_t free = in->tok - in->buf;
    if (free < need) {
        return 2;
    }
    // free分だけバッファをシフトする
    memmove(in->buf, in->tok, in->lim - in->tok);
    in->lim -= free;
    in->cur -= free;
    in->mar -= free;
    in->tok -= free;
    // シフトした分だけアドレス位置を更新する
    in->lim += fread(in->lim, 1, free, in->file);
    // ファイル終端まで読み込んだかチェックしてフラグをセットしておく
    if (in->lim < in->buf + SIZE) {
        // フラグをセットする
        in->eof = 1;
        // ヌル文字をパディングする
        memset(in->lim, 0, YYMAXFILL);
        in->lim += YYMAXFILL;
    }
    return 0;
}

static void init(Input *in, FILE *file)
{
    in->file = file;
    in->cur = in->mar = in->tok = in->lim = in->buf + SIZE;
    in->eof = 0;
    fill(in, 1);
}

static int lex(Input *in)
{
    int count = 0;
loop:
    in->tok = in->cur;
    /*!re2c
    re2c:api:style = free-form;
    re2c:define:YYCTYPE  = char;
    re2c:define:YYCURSOR = in->cur;
    re2c:define:YYMARKER = in->mar;
    re2c:define:YYLIMIT  = in->lim;
    re2c:define:YYFILL   = "if (fill(in, @@) != 0) return -1;"; // YYFILL失敗時はレキサーを終了する

    *                           { return -1; }
    // 停止ルール
    [\x00]                      { return (in->lim - in->cur == YYMAXFILL - 1) ? count : -1; }
    ['] ([^'\\] | [\\][^])* ['] { ++count; goto loop; }
    [ ]+                        { goto loop; }

    */
}

int main()
{
    const char *fname = "input";
    const char str[] = "'qu\0tes' 'are' 'fine: \\'' ";
    FILE *f;
    Input in;

    // prepare input file: a few times the size of the buffer,
    // containing strings with zeroes and escaped quotes
    f = fopen(fname, "w");
    for (int i = 0; i < SIZE; ++i) {
        fwrite(str, 1, sizeof(str) - 1, f);
    }
    fclose(f);

    f = fopen(fname, "r");
    init(&in, f);
    assert(lex(&in) == SIZE * 3);
    fclose(f);

    remove(fname);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define YYMAXFILL 1

#define SIZE 4096

typedef struct {
    FILE *file;
    char buf[SIZE + YYMAXFILL], *lim, *cur, *mar, *tok;
    int eof;
} Input;

static int fill(Input *in, size_t need)
{
    if (in->eof) {
        return 1;
    }
    const size_t free = in->tok - in->buf;
    if (free < need) {
        return 2;
    }
    memmove(in->buf, in->tok, in->lim - in->tok);
    in->lim -= free;
    in->cur -= free;
    in->mar -= free;
    in->tok -= free;
    in->lim += fread(in->lim, 1, free, in->file);
    if (in->lim < in->buf + SIZE) {
        in->eof = 1;
        memset(in->lim, 0, YYMAXFILL);
        in->lim += YYMAXFILL;
    }
    return 0;
}

static void init(Input *in, FILE *file)
{
    in->file = file;
    in->cur = in->mar = in->tok = in->lim = in->buf + SIZE;
    in->eof = 0;
    fill(in, 1);
}

static int lex(Input *in)
{
    int count = 0;
loop:
    in->tok = in->cur;
    
{
	char yych;
	if (in->lim <= in->cur) if (fill(in, 1) != 0) return -1; // YYFILL
	yych = *in->cur;
	switch (yych) {
	case 0x00:	goto yy2; // センチネル文字
	case ' ':	goto yy6;
	case '\'':	goto yy9;
	default:	goto yy4;
	}
yy2:
	++in->cur;
	{ return (in->lim - in->cur == YYMAXFILL - 1) ? count : -1; }
yy4: // *      { return -1; }
	++in->cur;
	{ return -1; }
yy6: // [ ]+   { goto loop; }
	++in->cur;
	if (in->lim <= in->cur) if (fill(in, 1) != 0) return -1; // YYFILL
	yych = *in->cur;
	switch (yych) {
	case ' ':	goto yy6;
	default:	goto yy8;
	}
yy8:
	{ goto loop; }
yy9:
	++in->cur;
	if (in->lim <= in->cur) if (fill(in, 1) != 0) return -1; // YYFILL
	yych = *in->cur;
	switch (yych) {
	case '\'':	goto yy11;
	case '\\':	goto yy13;
	default:	goto yy9;
	}
yy11:
	++in->cur;
	{ ++count; goto loop; }
yy13:
	++in->cur;
	if (in->lim <= in->cur) if (fill(in, 1) != 0) return -1; // YYFILL
	yych = *in->cur;
	goto yy9;
}

}

int main()
{
    const char *fname = "input";
    const char str[] = "'qu\0tes' 'are' 'fine: \\'' ";
    FILE *f;
    Input in;

    // prepare input file: a few times the size of the buffer,
    // containing strings with zeroes and escaped quotes
    f = fopen(fname, "w");
    for (int i = 0; i < SIZE; ++i) {
        fwrite(str, 1, sizeof(str) - 1, f);
    }
    fclose(f);

    f = fopen(fname, "r");
    init(&in, f);
    assert(lex(&in) == SIZE * 3);
    fclose(f);

    remove(fname);
    return 0;
}

YYFILL未定義の「パディング付き境界チェック」とほぼ同じコードが生成される。
re2c:eof と違うのは、オートマトンが移動してカーソルを移動した後にYYFILLチェックが挿入されること。
YYFILLでのバッファ容量確認後に、字句解析に入る。

re2c:eof の場合は、まず文字を読み込んで字句解析を行った後、失敗した場合にYYFILLでのバッファ容量確認処理を行う。

zenwerkzenwerk

これまで確認した諸々はコード上では「re2c:eof を設定するかどうか」という点での区別になる。

つまり re2c では以下のような分岐になる

re2c:eof

  1. 設定する → 境界チェック付き番兵文字(YYFILLの使用は任意)(汎用API可能)
  2. 設定しない
    1. YYFILLを使用しない → 単純な「番兵文字」ルール(汎用API可能)
    2. YYFILLを使用する  → パディング付き境界チェック

生成後のコードを見ても、単純にファイルを読み込んで字句解析をするだけなら、re2c:eofでもパディングでもほぼほぼ同じような処理、コードになると思うので
どちらでも大差はない感じがするが、個人的にはre2c:eofでやる方が簡単そうに思える。

これらの手法の使い分けは、使用したい正規表現ルールと字句解析を行いたいシチュエーションで変わるのだろうが、どういうルールで使い分けを行うべきなのかは一向に分からない。

また、汎用APIが使用できるのは re2c:yyfill:enable = 0 の制限から「境界チェック付き番兵文字」と単純な「番兵文字」に限られるので、その点でもre2c:eofを使うことを第一に考えておいたほうがよさそう。

zenwerkzenwerk

プル型・プッシュ型

プル型のレキサー

re2cのデフォルト

  • 必要なときに続々と次の字句を読み込もうと動作する

入力が少しずつ利用可能なシチュエーションではプル型は不便

  • パーサからレキサーが呼び出された場合
  • レキサーが次のメッセージを送信する前に、レキサーからの応答を待たなければならない他のプログラムと、ソケット等を介して通信している場合
  • その他いろいろ…

プッシュ型のレキサー

プッシュ型 → 新しい字句が必要になったら、現在の状態を保存して呼び出し元に return するような動作

  • その後、新しい字句が入力可能になったら保存した状態から再開する。
zenwerkzenwerk

Storable state

http://re2c.org/manual/manual_c.html#storable-state

プッシュ型のパーサーを作成するための方法。

-f / --storable-state オプションを指定 → 現在の状態を保存して呼び出し元へ return できるようになる

  • 戻ったときに保存した状態から再開できる。

Storable State で必要な設定

  • YYSETSTATE() / YYGETSTATE(state) の定義
  • yych, yyaccept, state 変数をレクサの状態として定義する。
    • state 変数は -1 で初期化すること。
  • YYFILL は新たな入力を取得せず、外部プログラムへ return しなければならない
    • 戻り値がレクサがさらに入力が必要かどうかを示す。
  • 外部プログラムは、レクサーがさらに入力を必要としているのか確認でき、適切に応答すること
  • レクサーに入る前にコードを実行する必要がある場合は、/*!getstate:re2c*/ディレクティブを使う
  • 生成コードをさらにいじるなら state:abort, state:nextlabel の設定をいじる

サンプルコード

  • stdin から読み取る
// re2c $INPUT -o $OUTPUT -f
#include <assert.h>
#include <stdio.h>
#include <string.h>

#define DEBUG    1
#define LOG(...) if (DEBUG) fprintf(stderr, __VA_ARGS__);
#define BUFSIZE  10

typedef struct {
    FILE *file;
    char buf[BUFSIZE + 1], *lim, *cur, *mar, *tok;
    unsigned yyaccept;
    int state;
} Input;

static void init(Input *in, FILE *f)
{
    in->file = f;
    in->cur = in->mar = in->tok = in->lim = in->buf + BUFSIZE;
    in->lim[0] = 0; // append sentinel symbol
    in->yyaccept = 0; // storable-state での必須変数
    in->state = -1; // storable-state での必須変数
}

typedef enum {END, READY, WAITING, BAD_PACKET, BIG_PACKET} Status;

static Status fill(Input *in)
{
    const size_t shift = in->tok - in->buf;
    const size_t free = BUFSIZE - (in->lim - in->tok);

    if (free < 1) return BIG_PACKET;

    // 処理した分だけ各ポインタの指すアドレスを -shift
    memmove(in->buf, in->tok, BUFSIZE - shift);
    in->lim -= shift;
    in->cur -= shift;
    in->mar -= shift;
    in->tok -= shift;

    // limを起点に最大free分だけfileから読み取り
    const size_t read = fread(in->lim, 1, free, in->file);
    // \0 が番兵文字で来るので、read 0 となり in->lim と in->cur が重なる
	LOG("read %ld bytes\n", read);
	if (read == 0) {
		LOG("in->lim = %p ; in->cur = %p\n", in->lim, in->cur);
	}
    in->lim += read; // 読み込み分だけ末尾を後ろへ
    in->lim[0] = 0; // append sentinel symbol

    return READY;
}

static Status lex(Input *in, unsigned int *recv)
{
    char yych; // storable-state での必須変数
    /*!getstate:re2c*/
loop:
    in->tok = in->cur;
    /*!re2c
        re2c:eof = 0;
        re2c:api:style = free-form;
        re2c:define:YYCTYPE    = "char";
        re2c:define:YYCURSOR   = "in->cur";
        re2c:define:YYMARKER   = "in->mar";
        re2c:define:YYLIMIT    = "in->lim";
        re2c:define:YYGETSTATE = "in->state";
        re2c:define:YYSETSTATE = "in->state = @@;";
        re2c:define:YYFILL     = "return WAITING;"; // storable-state の YYFILL は呼び出し元へ現在の状態を return しなければならない。さらなる入力は呼び出し元から供給され、レジュームする。

        packet = [a-z]+[;];

        *      { return BAD_PACKET; }
        $      { return END; }
        packet { *recv = *recv + 1; goto loop; }
    */
}

void test(const char **packets, Status status)
{
    const char *fname = "pipe";
    FILE *fw = fopen(fname, "w");
    FILE *fr = fopen(fname, "r");
    setvbuf(fw, NULL, _IONBF, 0);
    setvbuf(fr, NULL, _IONBF, 0);

    Input in;
    init(&in, fr);
    Status st;
    unsigned int send = 0, recv = 0;

    // 無限ループ
    for (;;) {
        // lex が字句を解析すると Status を返す
        st = lex(&in, &recv);
        if (st == END) {
            LOG("done: got %u packets\n", recv);
            break;
        } else if (st == WAITING) {
            LOG("waiting...\n");
            if (*packets) {
                LOG("sent packet %u\n", send);
                fprintf(fw, "%s", *packets++);
                ++send;
            }
            // バッファ充填は /*!re2c .. */ の外側で呼び出して管理する
            st = fill(&in);
            LOG("queue: '%s'\n", in.buf);
            // 異常終了
            if (st == BIG_PACKET) {
                LOG("error: packet too big\n");
                break;
            }
            assert(st == READY);
        } else {
            // 異常終了
            assert(st == BAD_PACKET);
            LOG("error: ill-formed packet\n");
            break;
        }
    }

    LOG("\n");
    assert(st == status);
    if (st == END) assert(recv == send);

    fclose(fw);
    fclose(fr);
    remove(fname);
}

int main()
{
    const char *packets1[] = {0};
    const char *packets2[] = {"zero;", "one;", "two;", "three;", "four;", 0};
    const char *packets3[] = {"zer0;", 0};
    const char *packets4[] = {"goooooooooogle;", 0};

    test(packets1, END);
    test(packets2, END);
    test(packets3, BAD_PACKET);
    test(packets4, BIG_PACKET);

    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define DEBUG    0
#define LOG(...) if (DEBUG) fprintf(stderr, __VA_ARGS__);
#define BUFSIZE  10

typedef struct {
    FILE *file;
    char buf[BUFSIZE + 1], *lim, *cur, *mar, *tok;
    unsigned yyaccept;
    int state;
} Input;

static void init(Input *in, FILE *f)
{
    in->file = f;
    in->cur = in->mar = in->tok = in->lim = in->buf + BUFSIZE;
    in->lim[0] = 0; // append sentinel symbol
    in->yyaccept = 0;
    in->state = -1;
}

typedef enum {END, READY, WAITING, BAD_PACKET, BIG_PACKET} Status;

static Status fill(Input *in)
{
    const size_t shift = in->tok - in->buf;
    const size_t free = BUFSIZE - (in->lim - in->tok);

    if (free < 1) return BIG_PACKET;

    memmove(in->buf, in->tok, BUFSIZE - shift);
    in->lim -= shift;
    in->cur -= shift;
    in->mar -= shift;
    in->tok -= shift;

    const size_t read = fread(in->lim, 1, free, in->file);
    in->lim += read;
    in->lim[0] = 0; // append sentinel symbol

    return READY;
}

static Status lex(Input *in, unsigned int *recv)
{
    char yych;
    /*!getstate:re2c で生成されるコードが以下 */
    switch (in->state) {
default:
	goto yy0; // 初期状態へ
case 0:
	if (in->lim <= in->cur) goto yyeof1;
	goto yyFillLabel0;
case 1:
	if (in->lim <= in->cur) goto yy4;
	goto yyFillLabel1;
case 2:
	if (in->lim <= in->cur) goto yy10;
	goto yyFillLabel2;
}

loop:
    in->tok = in->cur;
    

yy0:
yyFillLabel0:
	yych = *in->cur;
	switch (yych) {
	case 'a':
	case 'b':
	case 'c':
/* 省略 */
	case 'w':
	case 'x':
	case 'y':
	case 'z':	goto yy5;
	default:
		if (in->lim <= in->cur) {
			in->state = 0; // 状態を保存する
			return WAITING;
		}
		goto yy3;
	}
yy3:
	++in->cur;
yy4:
	{ return BAD_PACKET; }
yy5:
	in->mar = ++in->cur;
yyFillLabel1:
	yych = *in->cur;
	switch (yych) {
	case ';':	goto yy6;
	case 'a':
	case 'b':
	case 'c':
/* 省略 */
	case 'w':
	case 'x':
	case 'y':
	case 'z':	goto yy8;
	default:
		if (in->lim <= in->cur) {
			in->state = 1;  // 状態を保存する
			return WAITING; // YYFILL の置換結果
		}
		goto yy4;
	}
yy6:
	++in->cur;
	{ *recv = *recv + 1; goto loop; }
yy8:
	++in->cur;
yyFillLabel2:
	yych = *in->cur;
	switch (yych) {
	case ';':	goto yy6;
	case 'a':
	case 'b':
	case 'c':
/* 省略 */
	case 'x':
	case 'y':
	case 'z':	goto yy8;
	default:
		if (in->lim <= in->cur) {
			in->state = 2;  // 状態を保存する
			return WAITING;
		}
		goto yy10;
	}
yy10:
	in->cur = in->mar;
	goto yy4;
yyeof1:
	{ return END; }

}

void test(const char **packets, Status status)
{
    const char *fname = "pipe";
    FILE *fw = fopen(fname, "w");
    FILE *fr = fopen(fname, "r");
    setvbuf(fw, NULL, _IONBF, 0);
    setvbuf(fr, NULL, _IONBF, 0);

    Input in;
    init(&in, fr);
    Status st;
    unsigned int send = 0, recv = 0;

    for (;;) {
        st = lex(&in, &recv);
        if (st == END) {
            LOG("done: got %u packets\n", recv);
            break;
        } else if (st == WAITING) {
            LOG("waiting...\n");
            if (*packets) {
                LOG("sent packet %u\n", send);
                fprintf(fw, "%s", *packets++);
                ++send;
            }
            st = fill(&in);
            LOG("queue: '%s'\n", in.buf);
            if (st == BIG_PACKET) {
                LOG("error: packet too big\n");
                break;
            }
            assert(st == READY);
        } else {
            assert(st == BAD_PACKET);
            LOG("error: ill-formed packet\n");
            break;
        }
    }

    LOG("\n");
    assert(st == status);
    if (st == END) assert(recv == send);

    fclose(fw);
    fclose(fr);
    remove(fname);
}

int main()
{
    const char *packets1[] = {0};
    const char *packets2[] = {"zero;", "one;", "two;", "three;", "four;", 0};
    const char *packets3[] = {"zer0;", 0};
    const char *packets4[] = {"goooooooooogle;", 0};

    test(packets1, END);
    test(packets2, END);
    test(packets3, BAD_PACKET);
    test(packets4, BIG_PACKET);

    return 0;
}

YYFILLreturn するときに以下のようなコードが生成される。

	default:
		if (in->lim <= in->cur) {
			in->state = 1;  // YYSETSTATE (状態を保存する)
			return WAITING; // YYFILL の置換結果がこうなる
		}
		goto yy4;
	}

return 前に YYSETSTATE で定義されたコードで、どこの箇所で return されたのかを構造体や変数などに保存する。

/*!getstate:re2c*/ の箇所が以下のように置換される

  • re2c:define:YYGETSTATE から取得できる状態変数からステートマシンの状態へ goto する
    switch (in->state) {
default:
	goto yy0; // 初期状態へ
case 0:
	if (in->lim <= in->cur) goto yyeof1;
	goto yyFillLabel0;
case 1:
	if (in->lim <= in->cur) goto yy4;
	goto yyFillLabel1;
case 2:
	if (in->lim <= in->cur) goto yy10;
	goto yyFillLabel2;
}
zenwerkzenwerk

インタープリターのような逐次入力、ソケット通信、でやるならこのオプションを利用するのが必須か?
入力がある程度まとまってやってくることが確定していない場合はプッシュ型で作る方が定石か?

また Menhir などモダンな言語処理系は大体 push 型。

zenwerkzenwerk

ルールブロックの再利用

http://re2c.org/manual/manual_c.html#reusable-blocks

-r / --reusable オプションで有効になる機能。(re2c ver1.2 から使用可能)
/*!rules:re2c ... */ で指定されたブロックを /*use:re2c .. */` で再利用可能になる。

一つの rulesブロックを定義し、複数の use ブロックで再利用するという使い方を想定。

サンプルコード

UNICODE を受け取るルールブロックを UTF-8UTF-32 で解析するレキサーをそれぞれ定義する。

--input-encoding utf8 オプションを指定しないとUNICODEをちゃんと解析できない。

  • 指定しない場合、re2cはユニコード文字をプレーンなアスキーバイトシーケンスとして解釈する。
    • 16進数のエスケープシーケンスを使用する必要でるらしい(?)
// re2c $INPUT -o $OUTPUT -r --input-encoding utf8
#include <assert.h>
#include <stdint.h>

// 共通利用するルールブロック
/*!rules:re2c
    re2c:yyfill:enable = 0;

    "∀x ∃y: p(x, y)" { return 0; }
    *                { return 1; }
*/

static int lex_utf8(const uint8_t *YYCURSOR)
{
    const uint8_t *YYMARKER;
    // 再利用ブロック1 (UTF-8)
    /*!use:re2c
    re2c:define:YYCTYPE = uint8_t;
    re2c:flags:8 = 1;
    */
}

static int lex_utf32(const uint32_t *YYCURSOR)
{
    const uint32_t *YYMARKER;
    // 再利用ブロック2 (UTF-32)
    /*!use:re2c
    re2c:define:YYCTYPE = uint32_t;
    re2c:flags:8 = 0;
    re2c:flags:u = 1;
    */
}

int main()
{
    static const uint8_t s8[] = // UTF-8
        { 0xe2, 0x88, 0x80, 0x78, 0x20, 0xe2, 0x88, 0x83, 0x79
        , 0x3a, 0x20, 0x70, 0x28, 0x78, 0x2c, 0x20, 0x79, 0x29 };

    static const uint32_t s32[] = // UTF32
        { 0x00002200, 0x00000078, 0x00000020, 0x00002203
        , 0x00000079, 0x0000003a, 0x00000020, 0x00000070
        , 0x00000028, 0x00000078, 0x0000002c, 0x00000020
        , 0x00000079, 0x00000029 };

    assert(lex_utf8(s8) == 0);
    assert(lex_utf32(s32) == 0);
    return 0;
}

自動生成されるコードは以下

#include <assert.h>
#include <stdint.h>



static int lex_utf8(const uint8_t *YYCURSOR)
{
    const uint8_t *YYMARKER;
    
{
	uint8_t yych;
	yych = *YYCURSOR;
	switch (yych) {
	case 0xE2:	goto yy4;
	default:	goto yy2;
	}
yy2:
	++YYCURSOR;
yy3:
	{ return 1; }
yy4:
	yych = *(YYMARKER = ++YYCURSOR);
	switch (yych) {
	case 0x88:	goto yy5;
	default:	goto yy3;
	}
yy5:
	yych = *++YYCURSOR;
	switch (yych) {
	case 0x80:	goto yy7;
	default:	goto yy6;
	}
yy6:
	YYCURSOR = YYMARKER;
	goto yy3;
yy7:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'x':	goto yy8;
	default:	goto yy6;
	}
yy8:
	yych = *++YYCURSOR;
	switch (yych) {
	case ' ':	goto yy9;
	default:	goto yy6;
	}
yy9:
	yych = *++YYCURSOR;
	switch (yych) {
	case 0xE2:	goto yy10;
	default:	goto yy6;
	}
yy10:
	yych = *++YYCURSOR;
	switch (yych) {
	case 0x88:	goto yy11;
	default:	goto yy6;
	}
yy11:
	yych = *++YYCURSOR;
	switch (yych) {
	case 0x83:	goto yy12;
	default:	goto yy6;
	}
yy12:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'y':	goto yy13;
	default:	goto yy6;
	}
yy13:
	yych = *++YYCURSOR;
	switch (yych) {
	case ':':	goto yy14;
	default:	goto yy6;
	}
yy14:
	yych = *++YYCURSOR;
	switch (yych) {
	case ' ':	goto yy15;
	default:	goto yy6;
	}
yy15:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'p':	goto yy16;
	default:	goto yy6;
	}
yy16:
	yych = *++YYCURSOR;
	switch (yych) {
	case '(':	goto yy17;
	default:	goto yy6;
	}
yy17:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'x':	goto yy18;
	default:	goto yy6;
	}
yy18:
	yych = *++YYCURSOR;
	switch (yych) {
	case ',':	goto yy19;
	default:	goto yy6;
	}
yy19:
	yych = *++YYCURSOR;
	switch (yych) {
	case ' ':	goto yy20;
	default:	goto yy6;
	}
yy20:
	yych = *++YYCURSOR;
	switch (yych) {
	case 'y':	goto yy21;
	default:	goto yy6;
	}
yy21:
	yych = *++YYCURSOR;
	switch (yych) {
	case ')':	goto yy22;
	default:	goto yy6;
	}
yy22:
	++YYCURSOR;
	{ return 0; }
}

}

static int lex_utf32(const uint32_t *YYCURSOR)
{
    const uint32_t *YYMARKER;
    
{
	uint32_t yych;
	yych = *YYCURSOR;
	if (yych == 0x00002200) goto yy28;
	++YYCURSOR;
yy27:
	{ return 1; }
yy28:
	yych = *(YYMARKER = ++YYCURSOR);
	if (yych != 'x') goto yy27;
	yych = *++YYCURSOR;
	if (yych == ' ') goto yy31;
yy30:
	YYCURSOR = YYMARKER;
	goto yy27;
yy31:
	yych = *++YYCURSOR;
	if (yych != 0x00002203) goto yy30;
	yych = *++YYCURSOR;
	if (yych != 'y') goto yy30;
	yych = *++YYCURSOR;
	if (yych != ':') goto yy30;
	yych = *++YYCURSOR;
	if (yych != ' ') goto yy30;
	yych = *++YYCURSOR;
	if (yych != 'p') goto yy30;
	yych = *++YYCURSOR;
	if (yych != '(') goto yy30;
	yych = *++YYCURSOR;
	if (yych != 'x') goto yy30;
	yych = *++YYCURSOR;
	if (yych != ',') goto yy30;
	yych = *++YYCURSOR;
	if (yych != ' ') goto yy30;
	yych = *++YYCURSOR;
	if (yych != 'y') goto yy30;
	yych = *++YYCURSOR;
	if (yych != ')') goto yy30;
	++YYCURSOR;
	{ return 0; }
}

}

int main()
{
    static const uint8_t s8[] = // UTF-8
        { 0xe2, 0x88, 0x80, 0x78, 0x20, 0xe2, 0x88, 0x83, 0x79
        , 0x3a, 0x20, 0x70, 0x28, 0x78, 0x2c, 0x20, 0x79, 0x29 };

    static const uint32_t s32[] = // UTF32
        { 0x00002200, 0x00000078, 0x00000020, 0x00002203
        , 0x00000079, 0x0000003a, 0x00000020, 0x00000070
        , 0x00000028, 0x00000078, 0x0000002c, 0x00000020
        , 0x00000079, 0x00000029 };

    assert(lex_utf8(s8) == 0);
    assert(lex_utf32(s32) == 0);
    return 0;
}
zenwerkzenwerk

エンコーディングサポート

[^]any ルールと呼ぶ → すべての「コードポイント」にマッチするルール

  • つまりすべての「文字」にマッチする、やや抽象的なルール
    • コードポイント ≠ 具体的にエンコーディングされた文字表現ではないので注意

エンコーディングに関わらずすべてのコード表現(具体的な文字表現のバイト列)にマッチするためのルールは、これまで同様 * を使うこと。

:::message info
YYCTYPEのサイズは、コード単位のサイズと同じにすること
:::

ユニコードならコード単位は 1byte なのでunsigned char

  • utf-8 なら1byteのコード単位を1~4個組み合わせて1文字を表現している。

サポートするエンコーディング

  • ASCII
    • デフォルト設定
    • 固定長エンコーディング
      • [0-255] の 1byte
  • EBCDIC
    • -e / --ecb で有効の固定長エンコーディング
  • UCS2
    • -w / --wide-chars で有効
    • 2バイトの固定長エンコーディング
  • UTF8
    • -8 / --utf-8 / re2c:flags:8 = 1
    • [0-0x10FFFF] の間での1~4バイトの可変長エンコーディング
  • UTF16
    • -x / --utf-16 で有効
    • 1~2バイトの間の可変長エンコーディング(?)
  • UTF32
    • -u / --unicode で有効
    • 4バイト固定長エンコーディング

各種設定・オプション

/*!include:re2c "unicode_categories.re" */ すると re2c が事前定義しているユニコードのカテゴリを取り込める。

https://github.com/skvadrik/re2c/blob/master/include/unicode_categories.re

ユニコードリテラルを指定した正規表現ルールを定義するときは --input-encoding utf8 を指定すること。

UTF16のサロゲートペアの扱いを指定するときは --encoding-policy <fail | substitute | ignore> のオプションを指定すること。

サンプルコード

// re2c $INPUT -o $OUTPUT -8 --case-ranges -i
//
// Simplified "Unicode Identifier and Pattern Syntax"
// (see https://unicode.org/reports/tr31)

#include <assert.h>
#include <stdint.h>

/*!include:re2c "unicode_categories.re" */

static int lex(const char *YYCURSOR)
{
    const char *YYMARKER;
    /*!re2c
    re2c:define:YYCTYPE = 'unsigned char';
    re2c:yyfill:enable  = 0;

    id_start    = L | Nl | [$_]; // ユニコードの一般カテゴリを指定している
    id_continue = id_start | Mn | Mc | Nd | Pc | [\u200D\u05F3];
    identifier  = id_start id_continue*;

    identifier { return 0; }
    *          { return 1; }
    */
}

int main()
{
    assert(lex("_Ыдентификатор") == 0);
    assert(lex("$_Ыдентификатор") == 0);
    assert(lex("asdfasdfЫдентификатор") == 0);
    assert(lex("あ_Ыдентификатор") == 0);
    assert(lex("0a1cЫдентификатор") == 1);
    return 0;
}

自動生成されるコードは以下は長大なので省略

zenwerkzenwerk

-8 でUTF8を使用するときの /*!max:re2c *#define YYMAXFILL 4 となったので、
使用するエンコーディングがとりうる最長のバイト数になる。

zenwerkzenwerk

全二重通信的に動作するコマンド&レスポンス プロトコル

https://github.com/skvadrik/re2c/issues/241

最初のコメント

re2c を全二重通信でコミュニケーションするプロトコルで使いたい。
クライアントはサーバーへコマンドを送信、サーバーはre2cで字句解析してパースする

クライアントはレスポンスが帰る前に複数のコマンドを送信してくる可能性あり
もしくはレスポンスを待って、時間をおいてコマンドを送ることもある。

これについて直面したトラブルは以下。
YYMAXFILLパディングはyyfill(n)が実行される時では、相対的に大きい。
多くの場合、バッファには語彙素全体(処理したいコマンド文字列)に相当する未処理の入力が残っている可能性あり。

このとき、YYMAXFILLパディングするか、YYFILLdisableしてYYPEEKで処理を再定義するかで対処している例しかない(センチネル文字を追加するやつ)

しかし、この二つの戦略は、どちらも末尾に偽のデータを追加することが前提となっている。
でも、全二重のこのプロトコルの場合、コマンドの終わりが入力の終わりに対応することはめったにありません。
クライアントが一度、ちゃんとしたコマンド全体を送ってきたとき、サーバーからの応答を無期限にブロックすることを選択する(場合が)ある。
(つまり、コマンドを受け取ったので処理を完了するまではクライアントからの入力に対する応答を保留する必要があるということを言いたい?)

コマンドをパースするにはレキサーがコマンド末尾の字句を発行しないといけないが、まだバッファに余裕あるので、re2cがさらなる入力を待ってしまい、サーバー側がデッドロックする。

ドキュメントを読むと、YYFILLを無効にし、YYPEEKを再定義して、入力からバッファリングされていない文字の読み取りを実行できるように、「汎用API」に切り替える必要があります。

--storable-state, --input custom, re2c:yyfill:enable = 0 を組み合わせてバッファなし読み取りを行うプッシュレキサーを作成するとき、
YYSETSTATE(n) の呼び出しは、通常YYFILL(n) が行われた場所でのみ生成される。

YYSKIPはバッファなし読み込みに使用され、データが即座に利用できない場合には return する。
(外側のプログラムがデータが準備できるまで select する)
これは、YYSETSTATE(n) が呼び出す必要がある。じゃないとレキサーは間違ったSTATEに戻ってしまう。

より多くのYYSETSTATE(n)呼び出しを生成するための追加の設定はありますか?
またはプッシュレクサーを生成するときにこの方法でYYSKIP()を定義するべきでない?

なお、プロトコルはESMTPです。
コマンドは(ほとんど)CRLFで区切られたテキストです。
前述したように、クライアントが応答をブロックする前に、1つ以上のコマンドがサーバーに到着する可能性があります。
コマンドがTCPパケットを介して分割され、サーバーが非ブロッキングIOに基づいて構築されることがあるため、プッシュモデルレクサーが必要になります。
ほとんどのクライアントは他のコンピューターですが、時折人間がtelnet経由で接続する

BDAT nn CRLF が最も難しいコマンド。nn は固定長サイズのバイナリデータで、レキサーの正規表現ルールでは表現しない部分。

作者の返信(解決編)

re2c:eof--storable-state と組み合わせて書く。

re2c:eof とは「レクサーがEOF文字を検出するたびに、それが本物の文字であるか、より多くの入力が必要な状況であるかを確認する必要」があることをre2cに伝える設定。

re2cで使用される最終的なプログラムモデルは正確にはDFAではなく、いわゆるトンネルオートマトンであるため、機能しないと思います。これは、コードのサイズを縮小するために使用される最適化の1つです。トンネルオートマトンでは、すべての状態が消費されているわけではなく、一部の状態は、入力カーソルを進めずに、文字をディスパッチして他の状態に転送しているだけです(したがって「トンネル」という名前)。stateは最終的に消費するheadと派遣するbodyに分割されます(すべてではありません)。

zenwerkzenwerk

最終的なPoCコードは以下。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <assert.h>

/*!max:re2c*/
static const size_t SIZE = 4096;

struct input_t {
  char buf[SIZE + YYMAXFILL];
  char *lim;
  char *cur;
  char *tok;
  char *mark;
  int state;
  unsigned yyaccept;
  char yych;
  FILE *file;

  input_t(FILE * file)
    : buf()
    , lim(buf + SIZE)
    , cur(lim)
    , tok(lim)
    , mark(lim)
    , state(0)
    , yyaccept(0)
    , yych(0)
    , file(file)
  {}

  bool fill()
  {
    const size_t free = SIZE - (lim - tok); // バッファサイズ - (残りの解析しなければならない領域)
    if (free < 1) return false;
    const size_t shift = (tok - buf); // 解析済み領域

    printf("buf: %p\ntok: %p\ncur: %p\nlim: %p\n", buf, tok, cur, lim);
    printf("free:%ld | shift:%ld\n", free, shift);

    memmove(buf, tok, SIZE - shift);
    lim -= shift;
    cur -= shift;
    tok -= shift;
    mark -= shift;
    printf("> Reading input, can take up to %lu bytes\n", free);
    size_t bytes_read = fread(lim, 1, free, file);

    // YYLIMIT が YYCURSOR と一緒に伸長していく
    lim += bytes_read;
    // ノンバッファリング(nonblocking)な動作にするため番兵文字を末尾に追加する
    // cur と lim が同時に伸長するので同じアドレスになる -> (lim <= cur)が常に引っかかる -> 常に storable-state の return が動く
    lim[0] = 0;

    // quick make a copy of buffer with newlines replaced w/ _
    char b[SIZE+YYMAXFILL];
    int written;
    written = snprintf(b, SIZE+YYMAXFILL, "%s", buf);
    for(int i = 0; i < written; i++) {
      if ('\n' == b[i]) { b[i] = '_'; }
    }
    printf("> Read %lu bytes from input, current buffer: >%.40s<\n", bytes_read, b);

    return true;
  }
};

enum status_t {
  OK,
  FAIL,
  NEED_MORE_INPUT,
  WHITESPACE,
  WORD,
  THING
};
const char * STATUSES[] = {
  "OK",
  "FAIL",
  "NEED_MORE_INPUT",
  "WHITESPACE",
  "WORD",
  "THING"
};

#define YYGETSTATE()  in.state
#define YYSETSTATE(s) in.state = s
#define YYFILL() do { \
  printf("< Returning for more input\n"); \
  return NEED_MORE_INPUT; \
} while (0)

static status_t lex(input_t &in)
{
/*!getstate:re2c*/
    in.tok = in.cur;
/*!re2c
    re2c:define:YYCTYPE  = char;
    re2c:define:YYCURSOR = in.cur;
    re2c:define:YYLIMIT  = in.lim;
    re2c:define:YYMARKER = in.mark;
    re2c:variable:yych   = in.yych;
    re2c:eof             = 0;

    *                       { printf("< Unexpected character >%c<\n", in.yych); return FAIL; }
    $                       { printf("< EOF\n");                                return OK; }
    [\n ]+                  { printf("< whitespace\n");                         return WHITESPACE; }
    [a-zA-Z]+               { printf("< word\n");                               return WORD; }
    "THING\nWITH\nNEWLINES" { printf("< Thing w/ newlines\n");                  return THING; }
*/
}

int main()
{
  int fds[2];
  pipe(fds);
  fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
  FILE * write = fdopen(fds[1], "w");
  FILE * read = fdopen(fds[0], "r");
  input_t in(read);

  const char * packets[] = {
    "THING\n",
    "WITH\n",
    "NEWLINES\n",
    "H", "E", "L", "O", "\n",
    "HELO\n",
    "THING\nWITH\n",
    "NEWLINES"
  };
  size_t num_packets = sizeof(packets) / sizeof(char *);
  size_t current_packet = 0;

  enum status_t result = NEED_MORE_INPUT;

  while (OK != result) {
    switch (result) {
      case NEED_MORE_INPUT:
        if (current_packet == num_packets) {
          printf("Not enough input\n");
          goto end;
        }

        fwrite(packets[current_packet], strlen(packets[current_packet]), 1, write);
        fflush(write);
        current_packet++;
        printf("Packet %lu / %lu written\n", current_packet, num_packets);

        if (current_packet == num_packets) {
          printf("%lu / %lu packets sent, Closing down communication channel\n",
            current_packet, num_packets);
          fclose(write);
          write = NULL;
        }

        // NEED_MORE_INPUT 状態のたびに読み込む
        in.fill();
        break;

      case FAIL:
        goto end;

      default:
        // careful, need to reset state (re2c forgets to do it)
        // コマンドの入力が完了したら、レキサーを初期状態に戻して番兵文字チェックを動作させる
        // re2c:eof が有効なので lim[0] = 0 が動作しレキサーが終了する
        YYSETSTATE(0); 
        break;
    }

    result = lex(in);
    printf("Received response from lexer: %s\n", STATUSES[result]);
    printf("----------------------------------------------------------------------------------\n");
  }

end:

  // cleanup
  fclose(read);
  if (write) {
    fclose(write);
  }

  return result == OK ? 0 : 1;
}

#undef YYGETSTATE
#undef YYSETSTATE
#undef YYFILL

自動生成されるコードは以下

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <assert.h>

#define YYMAXFILL 19

static const size_t SIZE = 4096;

struct input_t {
  char buf[SIZE + YYMAXFILL];
  char *lim;
  char *cur;
  char *tok;
  char *mark;
  int state;
  unsigned yyaccept;
  char yych;
  FILE *file;

  input_t(FILE * file)
    : buf()
    , lim(buf + SIZE)
    , cur(lim)
    , tok(lim)
    , mark(lim)
    , state(0)
    , yyaccept(0)
    , yych(0)
    , file(file)
  {}

  bool fill()
  {
    const size_t free = SIZE - (lim - tok); // バッファサイズ - (残りの解析しなければならない領域)
    if (free < 1) return false;
    const size_t shift = (tok - buf); // 解析済み領域

    printf("buf: %p\ntok: %p\nlim: %p\n", buf, tok, lim);
    printf("free:%ld | shift:%ld\n", free, shift);

    memmove(buf, tok, SIZE - shift);
    lim -= shift;
    cur -= shift;
    tok -= shift;
    mark -= shift;
    printf("> Reading input, can take up to %lu bytes\n", free);
    size_t bytes_read = fread(lim, 1, free, file);
    lim += bytes_read;
    lim[0] = 0;

    // quick make a copy of buffer with newlines replaced w/ _
    char b[SIZE+YYMAXFILL];
    int written;
    written = snprintf(b, SIZE+YYMAXFILL, "%s", buf);
    for(int i = 0; i < written; i++) {
      if ('\n' == b[i]) { b[i] = '_'; }
    }
    printf("> Read %lu bytes from input, current buffer: >%.40s<\n", bytes_read, b);

    return true;
  }
};

enum status_t {
  OK,
  FAIL,
  NEED_MORE_INPUT,
  WHITESPACE,
  WORD,
  THING
};
const char * STATUSES[] = {
  "OK",
  "FAIL",
  "NEED_MORE_INPUT",
  "WHITESPACE",
  "WORD",
  "THING"
};

#define YYGETSTATE()  in.state
#define YYSETSTATE(s) in.state = s
#define YYFILL() do { \
  printf("< Returning for more input\n"); \
  return NEED_MORE_INPUT; \
} while (0)

static status_t lex(input_t &in)
{
switch (YYGETSTATE()) {
default:
	goto yy0;
case 0:
	if (in.lim <= in.cur) goto yyeof1;
	goto yyFillLabel0;
case 1:
	if (in.lim <= in.cur) goto yy7;
	goto yyFillLabel1;
/* 省略 */
case 20:
	if (in.lim <= in.cur) goto yy17;
	goto yyFillLabel20;
}



yy0:
yyFillLabel0:
	in.yych = *in.cur;
	switch (in.yych) {
	case '\n':
	case ' ':	goto yy5;
	case 'A':
/* 省略 */
	case 'y':
	case 'z':	goto yy8;
	case 'T':	goto yy11;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(0);
			YYFILL();
		}
		goto yy3;
	}
yy3:
	++in.cur;
	{ printf("< Unexpected character >%c<\n", in.yych); return FAIL; }
yy5:
	++in.cur;
yyFillLabel1:
	in.yych = *in.cur;
	switch (in.yych) {
	case '\n':
	case ' ':	goto yy5;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(1);
			YYFILL();
		}
		goto yy7;
	}
yy7:
	{ printf("< whitespace\n");                         return WHITESPACE; }
yy8:
	++in.cur;
yyFillLabel2:
	in.yych = *in.cur;
yy9:
	switch (in.yych) {
	case 'A':
	case 'B':
	case 'C':
/* 省略 */
	case 'x':
	case 'y':
	case 'z':	goto yy8;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(2);
			YYFILL();
		}
		goto yy10;
	}
yy10:
	{ printf("< word\n");                               return WORD; }
yy11:
	++in.cur;
yyFillLabel3:
	in.yych = *in.cur;
	switch (in.yych) {
	case 0x00:
		if (in.lim <= in.cur) {
			YYSETSTATE(3);
			YYFILL();
		}
		goto yy10;
	case 'H':	goto yy12;
	default:	goto yy9;
	}
yy12:
	++in.cur;
/ * 省略 */
yy27:
	++in.cur;
yyFillLabel18:
	in.yych = *in.cur;
	switch (in.yych) {
	case 'N':	goto yy28;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(18);
			YYFILL();
		}
		goto yy17;
	}
yy28:
	++in.cur;
yyFillLabel19:
	in.yych = *in.cur;
	switch (in.yych) {
	case 'E':	goto yy29;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(19);
			YYFILL();
		}
		goto yy17;
	}
yy29:
	++in.cur;
yyFillLabel20:
	in.yych = *in.cur;
	switch (in.yych) {
	case 'S':	goto yy30;
	default:
		if (in.lim <= in.cur) {
			YYSETSTATE(20);
			YYFILL();
		}
		goto yy17;
	}
yy30:
	++in.cur;
	{ printf("< Thing w/ newlines\n");                  return THING; }
yyeof1:
	{ printf("< EOF\n");                                return OK; }

}

int main()
{
  int fds[2];
  pipe(fds);
  fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK);
  FILE * write = fdopen(fds[1], "w");
  FILE * read = fdopen(fds[0], "r");
  input_t in(read);

  const char * packets[] = {
    "THING\n",
    "WITH\n",
    "NEWLINES\n",
    "H", "E", "L", "O", "\n",
    "HELO\n",
    "THING\nWITH\n",
    "NEWLINES"
  };
  size_t num_packets = sizeof(packets) / sizeof(char *);
  size_t current_packet = 0;

  enum status_t result = NEED_MORE_INPUT;

  while (OK != result) {
    switch (result) {
      case NEED_MORE_INPUT:
        if (current_packet == num_packets) {
          printf("Not enough input\n");
          goto end;
        }

        fwrite(packets[current_packet], strlen(packets[current_packet]), 1, write);
        fflush(write);
        current_packet++;
        printf("Packet %lu / %lu written\n", current_packet, num_packets);

        if (current_packet == num_packets) {
          printf("%lu / %lu packets sent, Closing down communication channel\n",
            current_packet, num_packets);
          fclose(write);
          write = NULL;
        }

        in.fill();
        break;

      case FAIL:
        goto end;

      default:
        // careful, need to reset state (re2c forgets to do it)
        YYSETSTATE(0);
        break;
    }

    result = lex(in);
    printf("Received response from lexer: %s\n", STATUSES[result]);
  }

end:

  // cleanup
  fclose(read);
  if (write) {
    fclose(write);
  }

  return result == OK ? 0 : 1;
}

#undef YYGETSTATE
#undef YYSETSTATE
#undef YYFILL
zenwerkzenwerk

実行結果

buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a46
lim: 0x7fffa1266a46

のように curlim の値がbuf の開始位置から読み込んだバイト数ずつ伸長していていくのがポイント。
このように動作すると常に YYFILL の条件につかまり re2c:eof のチェック対象となる。
すぐにレキサーから抜けれるように lim[0] = 0 を手動でセットしている。

Packet 1 / 11 written
buf: 0x7fffa1266a40
tok: 0x7fffa1267a40
cur: 0x7fffa1267a40
lim: 0x7fffa1267a40
free:4096 | shift:4096
> Reading input, can take up to 4096 bytes
> Read 6 bytes from input, current buffer: >THING_<
< Returning for more input
Received response from lexer: NEED_MORE_INPUT
----------------------------------------------------------------------------------
Packet 2 / 11 written
buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a46
lim: 0x7fffa1266a46
free:4090 | shift:0
> Reading input, can take up to 4090 bytes
> Read 5 bytes from input, current buffer: >THING_WITH_<
< Returning for more input
Received response from lexer: NEED_MORE_INPUT
----------------------------------------------------------------------------------
Packet 3 / 11 written
buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a4b
lim: 0x7fffa1266a4b
free:4085 | shift:0
> Reading input, can take up to 4085 bytes
> Read 9 bytes from input, current buffer: >THING_WITH_NEWLINES_<
< Thing w/ newlines
Received response from lexer: THING
----------------------------------------------------------------------------------
< Returning for more input
Received response from lexer: NEED_MORE_INPUT
----------------------------------------------------------------------------------

省略

Packet 9 / 11 written
buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a59
lim: 0x7fffa1266a59
free:4071 | shift:0
> Reading input, can take up to 4071 bytes
> Read 5 bytes from input, current buffer: >THING_WITH_NEWLINES_HELO_HELO_<
< whitespace
Received response from lexer: WHITESPACE
----------------------------------------------------------------------------------
< word
Received response from lexer: WORD
----------------------------------------------------------------------------------
< Returning for more input
Received response from lexer: NEED_MORE_INPUT
----------------------------------------------------------------------------------
Packet 10 / 11 written
buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a5e
lim: 0x7fffa1266a5e
free:4066 | shift:0
> Reading input, can take up to 4066 bytes
> Read 11 bytes from input, current buffer: >THING_WITH_NEWLINES_HELO_HELO_THING_WITH<
< whitespace
Received response from lexer: WHITESPACE
----------------------------------------------------------------------------------
< Returning for more input
Received response from lexer: NEED_MORE_INPUT
----------------------------------------------------------------------------------
Packet 11 / 11 written
11 / 11 packets sent, Closing down communication channel
buf: 0x7fffa1266a40
tok: 0x7fffa1266a40
cur: 0x7fffa1266a69
lim: 0x7fffa1266a69
free:4055 | shift:0
> Reading input, can take up to 4055 bytes
> Read 8 bytes from input, current buffer: >THING_WITH_NEWLINES_HELO_HELO_THING_WITH<
< Thing w/ newlines
Received response from lexer: THING
----------------------------------------------------------------------------------
< EOF
Received response from lexer: OK
----------------------------------------------------------------------------------
zenwerkzenwerk

re2c のポイントは buf, cur, tok, lim の各種ポインタと、 re2c:eofYYFILL をどう操作するか?ということを考えること。

このスクラップは2020/12/25にクローズされました