l3luatex: expl3 でも Lua したい

2018-12-06   #expl3  #LuaTeX  #Lua 

今年の12月も,例年通り TeX & LaTeX アドベントカレンダーが絶賛開催中のようです.

今年の重点テーマは「とにかく Lua(La)TeX しよう」だそうですので,もしかすると「expl3 で LuaTeX 芸」をしようと考えている参加者もいるかもしれません1.expl3 はもちろん LuaTeX をサポートしているので,フツーに expl3 コーディングをしていればそれだけで LuaTeX 対応ができるのですが,実は expl3 には l3luatex という標準パッケージがあり,LuaTeX でのみ利用できる機能がこのパッケージに集められています.本稿では,この l3luatex パッケージについて簡単に解説してみようと思います.

TL; DR

全部読み飛ばして,結論へ Go!

l3luatex パッケージの使用上の注意

l3luatex パッケージは LuaTeX 特有の機能を expl3 から使えるようにするものなので,expl3 がサポートする LuaTeX 以外のエンジン(具体的には pdfTeX, XeTeX, pTeX, upTeX)でこのパッケージから提供されている関数を使用しようとするとエラーになってしまいます.こうした事態を避けるため,LuaTeX 以外のエンジンで実行される可能性のある expl3 コードで l3luatex の機能を用いる場合は \sys_if_engine_luatex:T { <code> } の中に書くなどの工夫をするとよいでしょう.

Lua コードの即時実行

LuaTeX 用の expl3 コードから Lua を実行する最も直感的な方法は \lua_now:e を使うことです.

\cs_new:Nn \my_test: { Hello! }
\lua_now:e { tex.print("\my_test:") } %=>Hello!

\lua_now:e は単純に LuaTeX のプリミティブ \directlua をラップしたマクロなので,引数に渡した Lua コードはその場で直ちに実行され,また展開可能です.ただし,その引数は Lua 処理系に Lua コードとして引き渡される前に次のように処理されます:

  1. 通常の TeX 入力と同様に字句解析される
  2. 字句解析結果のトークン列は完全展開される

なお,引数の展開を抑えたい場合には \lua_now:e の代わりに \lua_now:n を用います.

🍣 引数指定子 e について

当ブログではこれまで引数指定子 e が登場したことがなかったですが,これはほぼ x と同じで,該当する引数を事前に完全展開するものです2x との違いは

  • 関数定義内で使用しても完全展開可能性を維持できる3
  • # を二重にしてエスケープする必要がない

の2点です.

🍣🍣 expl3 内で空白を tex.print() したい場合

本稿の読者にとっては周知の事実かと思いますが,expl3 で空白文字を書きたいときには明示的に ~ を書く必要があります.ここでは,~ を含む文字列を \lua_now:e から tex.print() することを考えます(\lua_now:n でも結果は同じです).

\lua_now:e { tex.print("Hello~World!") } %=>HelloWorld!

もちろん,コードの意図としては Hello World! を PDF に出力したいわけですが,実際には空白が脱落して HelloWorld! が印字されてしまいます.

最初,この挙動がなぜ発生するのかわからず,またググっても \lua_now:e の使用例自体がほとんど見つからなかったので,手っ取り早く TeX - LaTeX Stack Exchange で質問をしてみました.

すると,ありがたいことに LaTeX3 チームの Joseph 氏・David 氏から詳細な回答をいただけたので,その内容を簡単にまとめておきます.

どうやら,上のコード例で ~ で明示したはずのスペースが消えてしまうのは Lua コードが処理された結果が TeX 側に戻ってくる際に(デフォルトでは)現在のカテゴリーコード設定の下で再び字句解析にかけられるため,Lua の実行結果 Hello World! 内のスペースは expl3 的に解釈(すなわち,空白文字のカテゴリーコードが9)され,消えてしまうということのようです.

幸いにも tex.print() 関数にはオプショナル引数としてカテゴリーコード・テーブル(実際は,テーブルに対応する整数値)を与えることができます.したがって,これを用いて LuaLaTeX (ltluatex) で定義されているカテゴリーコード・テーブル \[email protected] を渡すことで空白が無視されるのを避けることができます.

\lua_now:e { tex.print(\int_use:c { [email protected] }, "Hello~World!") } %=>Hello World!

また,LaTeX3 チームの実験的なパッケージである l3cctab4 を読み込めば,すべてを expl3 内で完結することも可能です.

\documentclass{article}
\usepackage{expl3}
\usepackage{l3cctab} % l3cctab パッケージの読み込みが必要
\begin{document}
\ExplSyntaxOn
\lua_now:e { tex.print(\int_use:N \c_document_cctab, "Hello~World!") }
\ExplSyntaxOn
\end{document}

Lua コードの遅延実行

Lua コードをその場で直ちに実行するのではなく,TeX がページ出力を行うタイミング(shipout 時)まで遅延するには \lua_shipout_e:n を用います.\lua_now:e\directlua を単純にラップしたマクロであるのに対して,\lua_shipout_e:n\latelua プリミティブをラップしたものです.

遅延の効果を確かめるために,次のような expl3/Lua コードを考えます:

\lua_now:e { print("\string\n now:~" .. tex.inputlineno) }
\lua_shipout_e:n { print("\string\n shipout:~" .. tex.inputlineno) }

これを実行するとコンソール出力に

now: <number>
(中略)
shipout: <number>

のような出力が現れるはずです.ここで now: の後には expl3 コード中で \lua_now:e のある行番号そのものが出力されるはずですが shipout: はしばしば実際に \lua_shipout_e:n がある行よりも後の行番号が表示されるでしょう.

なお,引数の展開を抑えたい場合には \lua_shipout:n を用います.

🍣 なぜ \lua_shipout:e ではないのか

expl3 の(特殊)引数指定子は,その関数が展開される前に特定の処理(x による完全展開や c による制御綴化など)を実行するように指示するものです.\lua_shipout_e:n の引数は確かに(e 式に)完全展開されますが,この処理は \lua_shipout_e:n 関数の展開時ではなく,shipout 時(すなわち Lua コードの実行直前)に行われます5.そのため,expl3 の命名規則にしたがって,\lua_shipout:e ではなく \lua_shipout_e:n という関数名が採用されています.

\cs_new:Nn \my_test: { Hello! }
\lua_shipout_e:n { print("\my_test:") } % コンソールに "Bye!" と出力される
\cs_set:Nn \my_test: { Bye! }

Lua コードのエスケープ

Lua コード中の文字列リテラルの中では(当然ですが)Lua の文法にしたがって一部の文字(', ", \)をエスケープする必要があります.\lua_now:e などの引数にすべて陽に文字列リテラルを書く場合は自分でエスケープすればよいですが,文字列リテラル内に可変部分(仮引数 #1 など)がある場合にきちんとエスケープをするのは少々厄介です.そうした場合は \lua_escape:e を利用すると便利です.

\cs_new:Npn \my_test:n #1
  {
    \lua_now:e { tex.print(\int_use:c { [email protected] }, "\lua_escape:e { #1 }") }
  }

\my_test:n { He ~ cried ~ "Snowman!" }

\lua_escape:e も LuaTeX のプリミティブ \luaescapestring の単純なラップマクロで,やはりエスケープ処理の前に引数の完全展開が行われます.これを抑制したい場合は \lua_escape:n を用います.

Lua インターフェース

l3kernel はわずかですが Lua 側のインターフェースも提供しています6.これらの Lua インターフェース(関数)はすべて l3kernel テーブル内に格納されており,expl3 サイドから \lua_now:e 等で Lua を呼び出すときは特に何もしなくても利用可能です.

expl3 の提供する Lua 関数はすべて結果を TeX 側に返すため,texlua で実行するような Lua スクリプトから呼び出す機会はほぼないと思いますが,kpse ライブラリをセットアップして expl3 モジュールを読み込むことで,そういった場合でも l3kernel テーブルを取得すること自体は可能です.

-- kpse ライブラリのセットアップ
kpse.set_program_name("luatex")

-- expl3 モジュールを読み込む
require("expl3")

ファイル関連

l3kernel.filesize(), l3kernel.filemoddate(), l3kernel.filemdfivesum() は,それぞれ与えられたファイル名についてファイルサイズ(バイト単位),変更日時,MD5 チェックサムが得られます.該当のファイルが存在しない場合,エラーなしで空文字列が返ります.

l3kernel.filesize("<file name>")
l3kernel.filemoddate("<file name>")
l3kernel.filemdfivesum("<file name>")

タイマー

l3kernel.elapsedtime() を実行すると,LuaTeX を起動してからの経過時間,またはもし l3kernel.resettimer() を実行していればその時点からの経過時間を <scaled seconds> で返すそうです7.これらの関数は l3benchmark パッケージで利用されているようです.

その他

l3kernel.charcat() は文字コード <charcode><catcode> を引数にとり,そのようなトークンを生成して TeX に渡します8.expl3 の \char_generate:nn は,LuaTeX ではこの関数を用いて実現されているようです.

l3kernel.charcat(<charcode>, <catcode>)

l3kernel.strcmp() は2つの文字列をとり,それらが一致する場合にはトークン 0 を TeX 側に返します(一致しない場合は(現在の実装では)1 が返るようです).

l3kernel.strcmp("<str1>", "<str2>")

結論

普通の LaTeX や TeX 言語コードの場合もそうですが,expl3 内に Lua コードを書くのも同様にアレです🤮(下手をすると,特殊なカテゴリーコード設定下なので一層状況が悪化している!?)

基本的には LuaTeX で実行することを前提とする Lua コードは外部ファイルに書いておき,シンプルに require() するのがよいでしょう.ということで,expl3 から Lua コードを呼び出したい場合のベストプラクティスはこちら:

\lua_now:e { require("<file name>") }

  1. 誠に残念なお知らせですが,当の筆者は今年は重点テーマをガン無視して TeX 芸ネタを書くらしいです.余白があればもう1回は LuaTeX 関連の記事にしようと思っていたのですが,(幸いにも)2回目は出る幕なく枠が埋まってしまったので諦めました.ある意味,本稿はその贖罪のつもりで書いています. [return]
  2. 引数指定子 e は一部の TeX 処理系に存在するプリミティブ \expanded に対応します. [return]
  3. 引数指定子 x\edef を用いて完全展開するので,これを関数定義内で用いるとその関数は完全展開可能ではなくなってしまいます.expl3 的には,この制約を避けるために先頭完全展開を指示する引数指定子 f が存在していますが,e が使用可能な限りはその方が遥かに使い勝手が良いので,今後は可能な範囲で fe に置き換えていく予定のようです. [return]
  4. このパッケージは将来的には l3kernel 本体に取り込まれる予定のようです. [return]
  5. これは \latelua の挙動です. [return]
  6. これらの Lua インターフェースについては一応 interface3.pdf に説明が載っているので,ユーザが使用しても良い公開インターフェース扱いなのだと思いますが,2018年12月現在それほど凝った関数はないようなので,主に LaTeX3 チームが内部的に使用するために定義したものと思われます. [return]
  7. この <scaled seconds> が具体的に何なのか,情報がなく不明です. [return]
  8. 通常の TeX 言語でこの処理を実現する場合には,\lowercase トリックやそれに類するテクニックが必要で,特に完全展開可能にするのは難しいでしょう. [return]