l3luatex: expl3でもLuaしたい

2018-12-06 (updated: 2025-12-09) #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) で定義されているカテゴリーコード・テーブル\catcodetable@latexを渡すことで空白が無視されるのを避けることができます。

\lua_now:e { tex.print(\int_use:c { catcodetable@latex }, "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 { catcodetable@latex }, "\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回目は出る幕なく枠が埋まってしまったので諦めました。ある意味、本稿はその贖罪のつもりで書いています。 ↩︎

  2. 引数指定子eは一部のTeX処理系に存在するプリミティブ\expandedに対応します。 ↩︎

  3. 引数指定子x\edefを用いて完全展開するので、これを関数定義内で用いるとその関数は完全展開可能ではなくなってしまいます。expl3的には、この制約を避けるために先頭完全展開を指示する引数指定子fが存在していますが、eが使用可能な限りはその方が遥かに使い勝手が良いので、今後は可能な範囲でfeに置き換えていく予定のようです。 ↩︎

  4. このパッケージは将来的にはl3kernel本体に取り込まれる予定のようです。 ↩︎

  5. これは\lateluaの挙動です。 ↩︎

  6. これらのLuaインターフェースについては一応interface3.pdfに説明が載っているので、ユーザが使用しても良い公開インターフェース扱いなのだと思いますが、2018年12月現在それほど凝った関数はないようなので、主にLaTeX3チームが内部的に使用するために定義したものと思われます。 ↩︎

  7. この<scaled seconds>が具体的に何なのか、情報がなく不明です。 ↩︎

  8. 通常のTeX言語でこの処理を実現する場合には、\lowercaseトリックやそれに類するテクニックが必要で、特に完全展開可能にするのは難しいでしょう。 ↩︎