CCLASS 生成プログラム

決定性状態遷移機械で使う文字クラス・テーブルを 4 ビットごとにビット・パックしたテーブルをいちいち手作業で書くのは面倒なので、 正規表現の文字セット表記からプログラムで生成することにします。

入力ファイルには、 一行につき一組の文字セットと文字クラス番号を空白で区切って記述します。 文字クラス番号は 10 進数で記入します。 先頭の行から文字クラス・テーブルへ順に埋めていきます。 例えば、 このプログラム自身で使う文字クラス・テーブルを生成する入力ファイルは次のように記述してあります。 ファイル名を引数に指定してプログラムを実行すると、 標準出力へ文字クラス表を出力します。

$ cat cls-charset.cset
// CCLASS table for charset.cpp

[!-~] 1     // vchar
[0-9] 2     // \d
[ ]   3     // \s
[\\]  4     // \
[[]   5     // [
[-]   6     // -
[]]   7     // ]
      8     // $ end of string
$ ./cclass-table cls-charset.cset
    static const uint32_t CCLASS[16] = {
    //                 tn  r                          
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
    //     !"#$%&'    ()*+,-./    01234567    89:;<=>?
        0x31111111, 0x11111611, 0x22222222, 0x22111111,
    //    @ABCDEFG    HIJKLMNO    PQRSTUVW    XYZ[\]^_
        0x11111111, 0x11111111, 0x11111111, 0x11154711,
    //    `abcdefg    hijklmno    pqrstuvw    xyz{|}~ 
        0x11111111, 0x11111111, 0x11111111, 0x11111110,
    };

コマンドライン引数は 1 つで、 コマンドライン・オプションはありません。 コマンドライン引数のファイル名から入力ストリームをオープンし、 読み込みながら table を埋めていき、 読み終えたら標準出力ストリームへ書式を整えて出力します。

//@<cclass-table.cpp@>=
#include <cstdlib>
#include <string>
#include <iostream>
#include <fstream>

typedef std::string::const_iterator scanptr_t;

bool input_table (std::istream& in, std::string& table);
bool parse_line (std::string const& line, std::string& cset, int& csetcls);
void output_table (std::ostream& out, std::string const& table);
std::string subst32 (std::string const& tmpl, std::string const param);

int
main (int argc, char *argv[])
{
    std::ifstream in;
    std::string table (256, '0');

    if (argc != 2) {
        std::cerr << "usage: " << argv[0] << " file\n";
        exit (EXIT_FAILURE);
    }
    in.open (argv[1]);
    if (! in.good ()) {
        std::cerr << argv[0] << ": cannot open file\n";
        exit (EXIT_FAILURE);
    }
    if (! input_table (in, table)) {
        std::cerr << argv[0] << ": syntax error\n";
        exit (EXIT_FAILURE);
    }
    in.close ();

    output_table (std::cout, table);

    return EXIT_SUCCESS;
}

//@<入力ファイルから table を作ります@>
//@<table を出力します@>

入力ストリームからは、 1 行ずつ読んで、 文字セットが得られると、 文字クラスを table に書き込みます。 文字セットには、 2 文字ごとのペアが順に並んでいます。 ペアの一つ目は文字範囲の始まり、 二つ目は文字範囲の終わりの文字になっています。 文字が一文字だけのときも文字範囲を作り、 一つ目と二つ目は同じ文字になっています。 行に文字セットと文字クラス番号のペアが書いてないときは、 文字セットに空を返し、 その行をコメントとして扱います。

//@<入力ファイルから table を作ります@>=
bool
input_table (std::istream& in, std::string& table)
{
    table.resize (256, '0');
    for (std::string line; std::getline (in, line); ) {
        std::string cset;
        int cls = 0;
        if (! parse_line (line, cset, cls))
            return false;
        cls = cls < 10 ? cls + '0' : cls < 16 ? cls + 'a' - 10 : '0';
        for (auto s = cset.cbegin (); s != cset.cend (); s += 2) {
            int first = static_cast<uint8_t> (s[0]);
            int const second = static_cast<uint8_t> (s[1]);
            table[first] = cls;
            for (; first <= second; ++first)
                table[first] = cls;
        }
    }
    return true;
}

//@<parse_line を定義します@>

文字セットは、 正規表現で使う表記と同じで、 文字と文字範囲の並びを角括弧で囲んだものです。 制御コードのエスケープ・シーケンスはタブ (\t)、 復帰 (\r)、 改行 (\n) の 3 つだけに対応しています。 16 進数 2 桁の文字コード表記 (\x20 等) にも対応しています。 行頭の空白に続いて左角括弧で始まらない行は行末までコメント扱いします。 空白行もコメントとして扱います。 文字クラス番号の後も行末までコメント扱いして読み飛ばします。

//@<parse_line を定義します@>=
//@<lookup_cls インライン関数を定義します@>
//@<c7toi インライン関数を定義します@>
//@<decode_escseq インライン関数を定義します@>
//@<parse_line_actions インライン関数を定義します@>

bool
parse_line (std::string const& line, std::string& cset, int& csetcls)
{
    static const int SHIFT[12][9] = {
    //      vchar \d    \s    \     [     -     ]     $
        {0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
        {0, 0x0a, 0x0a, 0x01, 0x0a, 0x02, 0x0a, 0x0a, 0x0b}, // S1
        {0, 0x14, 0x14, 0x14, 0x03, 0x14, 0x14, 0x14, 0x00}, // S2
        {0, 0x24, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00}, // S3
        {0, 0x14, 0x14, 0x14, 0x03, 0x14, 0x05, 0x07, 0x00}, // S4
        {0, 0x44, 0x44, 0x44, 0x06, 0x44, 0x00, 0x37, 0x00}, // S5
        {0, 0x54, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00}, // S6
        {0, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00}, // S7
        {0, 0x00, 0x69, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00}, // S8
        {0, 0x00, 0x69, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x0b}, // S9
        {0, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b}, // Sa
        {1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // Sb
    };
    static const uint32_t CCLASS[16] = {
    //                 tn  r                          
        0x00000000, 0x00000000, 0x00000000, 0x00000000,
    //     !"#$%&'    ()*+,-./    01234567    89:;<=>?
        0x31111111, 0x11111611, 0x22222222, 0x22111111,
    //    @ABCDEFG    HIJKLMNO    PQRSTUVW    XYZ[\]^_
        0x11111111, 0x11111111, 0x11111111, 0x11154711,
    //    `abcdefg    hijklmno    pqrstuvw    xyz{|}~ 
        0x11111111, 0x11111111, 0x11111111, 0x11111110,
    };
    cset.clear ();
    csetcls = 0;
    std::string::const_iterator s = line.cbegin ();
    std::string::const_iterator const e = line.cend ();
    int next_state = 1;
    for (; s <= e; ++s) {
        uint32_t c = s == e ? 0 : static_cast<uint8_t> (*s);
        int const cls = s == e ? 8 : lookup_cls (CCLASS, c);
        int const prev_state = next_state;
        next_state = SHIFT[prev_state][cls] & 0x0f;
        if (! next_state)
            break;
        if (! parse_line_actions (SHIFT[prev_state][cls], s, e, cset, csetcls))
            break;
    }
    return (SHIFT[next_state][0] & 1) != 0;
}

文字コードから文字クラス表のビット・フィールドを取り出して、 文字クラスを求めます。

//@<lookup_cls インライン関数を定義します@>=
static inline int
lookup_cls (uint32_t const tbl[], uint32_t const octet)
{
    uint32_t const i = octet >> 3;
    uint32_t const count = (7 - (octet & 7)) << 2;
    return octet < 128 ? ((tbl[i] >> count) & 0x0f) : 0;
}

parse_line のアクションは、 文字範囲の左側の文字を追加する 0x10、 0x20、 0x30 の 3 つと、 右側の文字を上書きする 0x40、 0x50 の 2 つ、 文字クラスの数値を求める 0x60 の 3 種類があります。 文字範囲の左側の文字を追加するとき、 左右を同じ文字の範囲としていったん追加しておき、 右側の文字が指定されているときは、 右の方を上書きしています。

//@<parse_line_actions インライン関数を定義します@>=
static inline bool
parse_line_actions (uint32_t const action,
    scanptr_t& s, scanptr_t const e, std::string& cset, int& csetcls)
{
    uint32_t c;
    if (! decode_escseq (action, s, e, c))
        return false;
    switch (action & 0xf0) {
    case 0x10:
    case 0x20:
    case 0x30:
        cset.push_back (c); cset.push_back (c);
        break;
    case 0x40:
    case 0x50:
        cset.back () = c;
        break;
    case 0x60:
        csetcls = csetcls * 10 + c - '0';
        break;
    }
    return true;
}

文字範囲に書き込む文字コードは、 エスケープ・シーケンスをデコードして得たものです。 エスケープ・シーケンスではないのですが、 右角括弧直前のハイフンもエスケープ・シーケンスの一種として処理しています。

//@<decode_escseq インライン関数を定義します@>=
static inline bool
decode_escseq (uint32_t const action, scanptr_t& s, scanptr_t const e, uint32_t& c)
{
    c = s < e ? static_cast<uint8_t> (*s) : '\0';
    switch (action & 0xf0) {
    case 0x20:  // '\\' [ !-~]
    case 0x50:
        switch (c) {
        case 't': c = '\t'; break;
        case 'n': c = '\n'; break;
        case 'r': c = '\r'; break;
        case 'x':
            if (s + 2 < e) {
                uint32_t a = c7toi (static_cast<uint8_t> (s[1]));
                uint32_t b = c7toi (static_cast<uint8_t> (s[2]));
                if (a < 16U && b < 16U) {
                    c = (a << 4) + b;
                    s += 2;
                    return true;
                }
            }
            return false;
        }
        break;
    case 0x30:  // '-' ']'
        c = '-';
    }
    return true;
}

c7toi インライン関数は、 大文字小文字を区別せずに base36hex を求めます。

//@<c7toi インライン関数を定義します@>=
static inline uint32_t
c7toi (uint32_t const c)
{
    return '0' <= c && c <= '9' ? c - '0'
         : 'a' <= c && c <= 'z' ? c - 'a' + 10
         : 'A' <= c && c <= 'Z' ? c - 'A' + 10
         : 36;
}

ここまでで入力部分はおしまいです。 入力が終わるとテーブルができあがります。 テーブルの内容を簡易型のテンプレートを使って整形出力します。

//@<table を出力します@>=
void
output_table (std::ostream& out, std::string const& table)
{
    static const std::string A ("    static const uint32_t CCLASS[16] = {\n");
    static const std::string B ("    //    $$$$$$$$    $$$$$$$$    $$$$$$$$    $$$$$$$$\n");
    static const std::string C ("        0x$$$$$$$$, 0x$$$$$$$$, 0x$$$$$$$$, 0x$$$$$$$$,\n");
    static const std::string D ("    };\n");

    out << A;
    for (int row = 0; row < 128; row += 32) {
        std::string labels;
        for (int col = 0; col < 32; ++col) {
            int const ch = row + col;
            int const label = '\t' == ch ? 't'
                            : '\r' == ch ? 'r'
                            : '\n' == ch ? 'n'
                            : (32 <= ch && ch <= 126) ? ch
                            : ' ';
            labels.push_back (label);
        }
        std::string codes = table.substr (row, 32);
        out << subst32 (B, labels);
        out << subst32 (C, codes);
    }
    out << D;
}

//@<subst32 を定義します@>

テンプレート中のドル ($) を先頭から順に置換していきます。 $ の個数は 32 個に決め打ちしています。

//@<subst32 を定義します@>=
std::string
subst32 (std::string const& tmpl, std::string const param)
{
    std::string t;
    std::string::size_type q = 0;
    for (int i = 0; i < 32; ++i) {
        std::string::size_type const p1 = q;
        q = tmpl.find ('$', q);
        std::string::size_type const p2 = q;
        ++q;
        if (p1 < p2)
            t += tmpl.substr (p1, p2 - p1);
        t.push_back (param[i]);
    }
    if (q < tmpl.size ())
        t += tmpl.substr (q);
    return std::move (t);
}