マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

TeX で、「ループを回して 1 文字ずつ読む」件について

bxgrassator を作っていたら、表題のような基本的な処理で躓いた、という話。

Grass の「擬似 I/O」(文書中で与えられた「入力文字列」から読み TeX コードに書き出す*1)を実装している時に、次のような処理が必要になった。*2

  • 無引数マクロ \InputStr には「文字列」が格納されている。
    • この「文字列」はユーザから与えられたトークン列を \meaning を用いて「文字列化」したもので、いわゆる「\the-文字列」になっている(つまり、\string で得た「文字列」と同じ)。((e-TeX 拡張の \detokenize を使った場合も同じ結果になる。))
      ※詳細については、後掲のコードの \SetInputStr を参照されたい。
  • 整数レジスタ \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 マクロ。))

*1:「実際の標準入力/出力を使う」ことにしなかったのは、「LaTeX では普通そんなことはしない」という考えがあったため。

*2:話を解りやすくするために実際の実装とは異なる部分がある。