bxgrassator を作っていたら、表題のような基本的な処理で躓いた、という話。
Grass の「擬似 I/O」(文書中で与えられた「入力文字列」から読み TeX コードに書き出す*1)を実装している時に、次のような処理が必要になった。*2
- 無引数マクロ
\InputStrには「文字列」が格納されている。 - 整数レジスタ
\CharValueが定義されている。 - 話を簡単にするため、8 ビット欧文 TeX での動作を仮定する。
- 以上の条件のもとで、次の要件を満たすマクロ
\GetCharValueを作りたい。\GetCharValueは引数を持たない。\GetCharValueを実行すると、\InputStrの先頭にある文字の符号位置を\CharValueに代入する。- ただし、
\InputStrが空の場合は何もしない。
以下に挙げるコードは、一見正しく動いているが、実は \GetCharValue に重大なバグがある。それが何か判るだろうか。ヒントは「\the-文字列の仕様」である。
%% plain TeX
% (ただし \bye 以外は LaTeX でも通用する)
\catcode`\@=11 %------------------------
%% \SetInputStr{<トークン列>}
% 入力のトークン列を「文字列化」して \InputStr に入れる。
\def\SetInputStr#1{%
\def\xx@x{#1}% 一旦マクロに格納
\expandafter\xx@set@input@str@a\meaning\xx@x\@nil % \meaning を適用する
}
% \meaning の展開結果は「macros:->(本体)」という the-文字列になる
\def\xx@set@input@str@a#1>#2\@nil{% 「>」をスキャン
\def\InputStr{#2}% 「>」から後を \InputStr に格納する
}
%% \CharValue: \GetCharValue の結果が返る。整数レジスタ。
\newcount\CharValue
%% \GetCharValue
% \InputStr が非空ならば先頭文字の符号値を \CharValue に代入する。
\def\GetCharValue{%
\ifx\InputStr\empty\else % \InputStr が非空ならば
\expandafter\xx@get@char@value@a
\fi
}
\def\xx@get@char@value@a{%
\expandafter\xx@get@char@value@b\InputStr\@nil
}
\def\xx@get@char@value@b#1#2\@nil{%
\CharValue=`#1\relax % 先頭文字の符号値を \CHarValue に代入
\def\InputStr{#2}% \InputStr を更新
}
\catcode`\@=12 %------------------------
%% テスト。入力を受け取って、\GetCharValue を 3 回呼ぶ。
\def\Test#1{%
\SetInputStr{#1}%
\TestOne \TestOne \TestOne
}
\def\TestOne{%
\CharValue=-1 %
\GetCharValue
\immediate\write16{value=\the\CharValue}%
}
% これらは問題ないのだが…
\Test{\TeX} % 92 84 101
\Test{?} % 63 -1 -1
\bye正しく動かない例。
\Test{A B} % 65 66 -1
% 正しくは 65 32 66 となるべき要するに、空白文字が読み飛ばされているのである。\meaning や \string で出力されるトークン列は、ほぼ全ての文字が「非特殊な記号」(カテゴリコード 12)となるが、唯一の例外で空白文字は「空白」としての性質(カテゴリコード 10)を保つのである。だから、単純に区切り無し引数のマクロを用いると「通常通り」空白文字は飛ばされてしまう、というわけである。
ここまで読んで、「じゃあどうすればいいの?」と疑問に思った人は、しばらく自分で考えてもらいたい。気が向いたら、後日解説するかも知れない。少なくとも、bxgrassator のソースに答えがあることは確かである。((bxgrassator.def の \bxgs@get@char マクロ。))