2014/02//26
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止中

KAGParserのひみつ(学研のアレ風に)

■はじめに

本書では、吉里吉里で利用されている KAGParser.cppについて、C++のソースコードレベルで 我輩が調べたことをメモしておく。

KAGParserは読みづらい。関数が長い上に機能ごとに分割されておらず、しかも 機能追加を繰り返した結果、ちょっとびっくりするような実装になっているためだ。 こんなの概要知らんと読めんて!ということで、ある程度はお役に立つんじゃない かしら、と思って、 拙作ExtKAGParser を作る過程で調べたことを書き残す所存ナリ。

「ばーかばーかオマエのゆってること全然違うのぜー」というのを発見したひとは、 是非教えてください。教えてもらえると主に我輩が助かります。 そして今後頼れるアニキとして頼る気マンマンです。

■基本的な概念

KAGParserのキモはGetNextTag()(から呼ばれる_GetNextTag())。TJSからこの関数が 呼ばれると、KAGParserは現在実行中のスクリプトの実行中の位置から、一つのタグ 情報をTJSの辞書配列に入れて返す(※文字を表示する場合は[ch text=]タグに直して これを返す)。以上終わり。あとはもうオマケだと思って頂いてもいいくらい。

KAGParserはKAGシナリオファイル(.ks)を基本的に行単位で処理し、 そのパースと処理は _GetNextTag() 関数が一人で全部やっちゃっているという エースで四番っぷり。スゲぇ。処理概要は大体以下のようなカンジ。

複数行にまたがる処理はできない。できないったらできない。KAGParserExとか あるけど、アレはなんとか処理しようと努力してスゴい実装になっちゃった例。 拙作ExtKAGParserは更にコ汚い実装になっちゃった例。いっそKAGParser全部 書き換えないと、コ汚くない実装はできないんじゃあるまいか。とかいいながら、 最初から書き直すのは車輪の再発明だから絶対イヤだと思ってる我輩ガイル。

■KAGParserクラスの重要な変数

これだけ知っとけば KAGParser.cpp 読めるだろう、というくらいの変数を以下に示す。 KAGParserのクラスリファレンスも参照した方がいいと思うが、名称が違ったり するので参考になる部分とならない部分があるのはご承知おきを。

変数名役割
StorageName ttstr 現在実行中のKAGシナリオ(.ks)ファイル名。同じファイルの短い名称が入った StorageShortNameなんてのもあるけど気にしない
LineCount tjs_int 現在実行中のKAGシナリオ(.ks)ファイルの行数
CurLine tjs_int KAGシナリオ(.ks)ファイル中で現在実行中の行の行番号(0〜LineCount-1)
CurLineStr tjs_char* 現在処理中の行の文字列。殆どの場合Lines[CurLine] (Lines[CurLine]はttstrなので、正確には Lines[CurLine].Start) と同じだが、 (今は)embとマクロ実行の時だけはそれぞれの内容が展開されるため、 Lines[CurLine]とは異なる。
CurPos tjs_int CurLineStr中の、現在処理中の(先頭からの)位置(0〜TJS_strlen(CurLineStr))
LineBuffer ttstr embとマクロ実行時の、展開された文字列。その瞬間であれば CurLineStr と全く同じだと思うけど、CurLineStr が tjs_char* なので、実際の文字列の保存用として 分けてある(んだと思う)。KAGシナリオ(.ks)ファイルの各行Lines[*]は定数であり、 変更するわけにはいかないので、内容の変更が必要なときのために別の変数が用意されている(んだと思う)。
LineBufferUsing bool LineBufferが使用中かどうかのフラグ。これがfalseならば、CurLineStrは Lines[CurLine].Startと等しく、trueならCurLineStrはLineBuffer.Start と 等しいはず。
ExludeLevel tjs_int 現在実行中の部分を実行するかどうかを決める変数(-1〜IfLevel)。 -1 なら実行する、>=0 なら実行しない。 実際には、[If][elif]などでの条件によって変化し、実行しないと判断されたら その時の IfLevel と等しくなる
IfLevel tjs_int [if][elif][endif]で囲まれたスクリプトの、[if]でのネスト数を示す(0〜)。 [if]A[endif]ならばA実行中は IfLevel=1、[if][if]B[endif]{endif]ならば B実行中はIfLevel=2となる。実際には、[if]の条件が成立「しなかったとき」に ExcludeLevelに代入され、[if][endif]で囲まれた領域を実行しないことを 表すために使用される。

あと、KAGParserクラスの中の変数じゃないけれど、_GetNextTag()関数の ローカル変数 condition は覚えておいて損はないと思う。

変数名役割
condition bool KAGタグの中で cond= で指定したTJS式の評価結果(=true/false)

[if]の実現方法

KAGParserの[if]の実現方法は面白い。ExcludeLevelとIfLevelという二つの変数だけを 使って、複数のネストに対応する[if]を実現している。

_GetNextTag()では、上で説明した 「ExcludeLevelが-1かどうか」だけでその行(正確には行じゃなくてトークン)を 実行するかしないかを判断する。すなわち、[if]で条件に合致 しなかったら[endif]までスキップされるわけじゃなくて、実行しない行も 全て解析はしちゃうということ。それは無駄ではないか、というお話は もちろんあるけれど、後付の仕組みとしてはまぁアリなんじゃないかと思う (初見で理解は難しいけれど)。

簡単に言えば、[if]を実現する二つの変数には、以下のルールがある。

例を挙げよう。以下のようなルーチンでは、IfLevelおよびExcludeLevelは、 コメントのように設定される。

開始時は ExcludeLevel=-1, IfLevel=0 です。
ここは実行されます。
[if exp="1"]
	ここでは ExcludeLevel=-1、IfLevel=1です。
	ここは実行されます。
	[if exp="0"]
		ここではExlcudeLevel=2, IfLevel=2です。
		ExcludeLevel != -1 なので、ここは実行されません。
	[else]
		ここではExlcudeLevel=-1, IfLevel=2です。
		ここは実行されます。
	[endif]
	ここでは ExcludeLevel=-1、IfLevel=1です。
	ここは実行されます。
[else]
	ここでは ExcludeLevel=1、IfLevel=1です。
	ExcludeLevel != -1 なので、ここは実行されません。
[endif]
ここでは ExcludeLevel=-1, IfLevel=0 です。
ここは実行されます。

なんとなくでもわかるだろうか。こうやって、ネスト状態と条件の成立可否によって IfLevelとExcludeLevelを変化させることにより、 KAGシナリオ(.ks)スクリプト中で実行すべき箇所とそうでない箇所を判断しているのだ。 いい仕組みなのかどうかはよくわからないけど、上手にすれば もう少しKAGParserを高速化するネタはありそうな気がする。

IfLevel/ExcludeLevelは、[jump]や[call]でKAGシナリオ(.ks)ファイルを移動すると、 移動先ではリセットされる。[if]〜[endif]中にラベルが配置できず、 そこに飛び込むようなコードが書けないのはこのためだ。

マクロの実現方法

マクロは、改行の無い[タグ][タグ][タグ]...形式になって、Macrosメンバ変数の 中に辞書配列として格納される。マクロ実行時は、マクロを実行する行の CurLineStrを、辞書配列から引っ張り出したマクロ文字列で置き換えて実行する。 このため、CurLineStrはLines[CurLine].Startとは異なるため、LineBufferが 使用される。

たとえば、以下のようなマクロを登録するとする。

@macro name=abc
@if exp="1"
	@emb exp="'abc'"
@endif
@endmacro

フツーに登録すると、Macros辞書のabcに格納される。 実はコレはTJSのkag.conductor.macros.abc と等しい。 吉里吉里のデバッグコンソールで表示すると、以下のように表示される。 @タグ 形式ではなくて [タグ] 形式になっていること、無駄な改行や空白が 削除されていること、クォートがエスケープされていること、末尾に [macropop]が追記されていることに注意。

kag.conductor.macros.abc = (string)"[if exp=\"1\"][emb exp=\"\'abc\'\"][endif][macropop]"

で、実際にKAGシナリオ(.ks)スクリプト中で [abc] マクロが実行されると、 最初は以下のようになっていたものが、

CurLineStr = "[abc]"
LineBufferUsing = false

KAGParserが「はッ!?コレマクロやん!?」と気づいた時に、 以下のようにに変更されるという。難しいなオィ!

LineBuffer = "[if exp=\"1\"][emb exp=\"\'abc\'\"][endif][macropop]"(ただしttstr)
CurLineStr = "[if exp=\"1\"][emb exp=\"\'abc\'\"][endif][macropop]"(=LineBuffer.c_str())
LineBufferUsing = true

KAGParser中でマクロを実行する時は、現在の環境(=簡単に言えばTJSのmp)を 保存し、終了時に戻す、という処理になっている。 [macropop]はこの「戻す」という処理をする隠しタグだと思ってもらえればいい。 環境保存のためにマクロ用のスタック(MacroArgStack)が 用意されており、マクロが入れ子になっても対応できるようになっている。

[emb]の実現方法

さらっと書いちゃうと、[emb]タグは殆どマクロと同じで、exp=を評価したら結果を そのままCurLineStrに埋め込んじゃう。つまり、

[eval exp="tf.a = '文字列'"]
[emb exp="tf.a+'abc'"][r]

を実行すると、二行目のときに以下のようになっていたものが、

CurLineStr = "[emb exp="tf.a+'abc'"][r]"
LineBufferUsing = false

[emb]部分を実行後は以下のようになっている。で、これを再び解析して、 実際に文字を表示していく(正しくは[ch text=]タグとして返す)わけだ。

LineBuffer = "文字列abc[r]"(ただしttstr)
CurLineStr = "文字列abc[r]"(=LineBuffer.c_str())
LineBufferUsing = true

[call]の実現方法

KAGParserでの[call][return]は結構簡単に実装されている。ずばり以下の二つだけ。

  1. [call]時は、現在実行中の情報を全部 CallStack に退避する
  2. [return]時は、CallStack から[call]直前の情報を取り出して設定する

現在実行中の情報とは、上で述べたような「現在実行中のKAGシナリオ(.ks)ファイル名、 行数、行中の位置、マクロ変数、IfLevel/ExcludeLevelなどなど」殆ど全て。

実際には、KAGParser.cpp の PushCallStack()で現在の情報をCallStackに退避し、 PopCallStack()で以前の情報を取り出す。で、取り出した時に、「以前実行中だった 行」が「今も同じか」をチェックして、違ったらお決まりのメッセージ、 「シナリオファイルに変更があったため return の戻り先位置を特定できません」を 表示するというそんな。

■おわりに

とりあえずこれだけ知っていれば、KAGParser.cppはそこそこ読めると思う。 ちうかKAGParser.cppのコメントの少なさには泣けてくる。ドキュメントが 無いなら、もう少し書いといてくださいよぅお願い。

ほかにも気づいたことがあれば追記する予定。情報の正確性についてのそしりは 甘んじて受ける所存ナリ。最初に書いたようにご指摘歓迎します故。