マクロツイーター

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

tangle し過ぎると落ちる

といっても、あの tangle でなく別の tangle の話。

奥底「Lua 内で TeX コードの実行を完了させる」で紹介されている「tangle モジュール」を新しいバンドル(IXbase)に持ってくるために手直しをしていたが、そこで重大な欠点に気付いた、という話。

通常は Lua のコードの中で TeX のコードを実行することはできず、tex.print() でコードを出力させても、それが実行されるのは Lua の実効コンテキストを抜けた後である。tangle モジュールを使うと、Lua コード内で、「それまで tex.print() で出したコードの実行を完了させる」ことが可能になる。詳しくは「Lua 内で…」にあるサンプルを見てほしい。

で、何が問題かというと、「TeX の制限に引っかかるせいで、一つの Lua の実行の中では 14 回までしか tangle.run_tex() が呼べない」ということである。これを見るために、「Lua 内で…」のサンプル(課題は「文字列を指定の字幅で切り捨てる」)で、わざとループ回数を増やすために二分探索でなく線形探索に変えた版を試してみる。

% 文字コードは UTF-8
\documentclass[a4paper]{article}
\usepackage{ixbase0}

\begin{execluacodeblock}
function truncate_to_width(str)
  local wdt = ixbase.length.ttwTarget.width
  local max = str:len()
  for l = 1, str:len() do
    -- あえて線形探索を使う
    texio.write("("..l..")") -- デバッグ出力(*)
    -- str:sub(1,l) を組版しその幅を \ttwMeasured に代入する
    tex.print([[\settowidth{\ttwMeasured}{]]..str:sub(1, l)..[[}%]])
    tangle.run_tex() -- TeX の実行を完了させる
    local wdm = ixbase.length.ttwMeasured.width
    if wdm > wdt then
      max = l - 1; break
    end
  end
  tex.print(str:sub(1, max))
end

\end{execluacodeblock}

\newlength{\ttwTarget}
\newlength{\ttwMeasured}
\newcommand*{\truncatetowidth}[2]{%
  \setlength{\ttwTarget}{#1}%
  \directlua{
    tangle.execute(truncate_to_width, "\luaescapestring{\detokenize{#2}}")
  }%
}

\begin{document}
\begin{itemize}
\item \truncatetowidth{10pt}{I never feel like saying hello to world!}
\item \truncatetowidth{50pt}{I never feel like saying hello to world!}
\item \truncatetowidth{90pt}{I never feel like saying hello to world!}
\end{itemize}
\end{document}

これをコンパイルすると、エラーが出て終了する。

This is LuaTeX, Version beta-0.70.2-2012052919 (TeX Live 2012/W32TeX)
 restricted \write18 enabled.
……(中略)……
(c:/usr/local/share/texmf/tex/latex/base/omscmr.fd)(1)(2)(3)(1)(2)(3)(4)(5)(6)(
7)(8)(9)(10)(11)(12)(13)(1)(2)(3)(4)(5)(6)(7)(8)(9)(10)(11)(12)(13)(14)(15)
! TeX capacity exceeded, sorry [text input levels=15].
l.2 \directlua{tangle.resume()}
                               \relax
l.40 ...{I never feel like saying hello to world!}

 670 words of node memory still in use:
   12 hlist, 1 rule, 29 glue, 1 kern, 6 penalty, 14 glyph, 62 glue_spec, 1 shap
e, 1 write, 2 local_par, 1 dir nodes
   avail lists: 2:12,3:1,4:5,5:1,6:13,7:1,9:1
!  ==> Fatal error occurred, no output PDF file produced!
Transcript written on test.log.

デバッグ出力 (*) を見ると、3 回目の \truncatetowidth の実行で、15 回目のループに入ったところで「tex input level」の制限超過によって落ちてしまっている。

tex input level」というのは、\input による他ファイルの読み込みのネストの回数である。メインの文書で \input したらそこで 1 段、その中で \input したらそこで 2 段、……となり、TeX の制限として、ネストは 15 段までしか許されない。普通に \input を使うという用途ではこれで十分過ぎるであろう。

今回の件で何故この制限が関係するかというと、TeX から \directluaLua を呼び出して、その中で tex.print() された「書出コード」を Lua 実行後に実行する、という一連の流れの中で、「書出コード」を処理するのに内部で他ファイル読込(\input)の機構を利用しているからだと思われる。((e-TeX\scantokens が内部的な他ファイル読込を用いているが、多分それと同じだろう。なお、別に物理的な一時ファイルが生成される訳ではなく、すべてメモリ上で行われている。))今の tangle.run_tex() の実装では、Lus のコルーチン*1を利用して、Lua の実行から脱出した後にまた Lua に戻るために、予め「書出コード」に \directlua{ tangle.resume() } を入れておくという方法を採っている。従って、仮想的なファイルからの \input のネストが増えていく、という状況になっているのだと思われる。

解決策の案はあるのだけど……、面倒だなあ。

*1:Ruby1.9 における Fiber と同じもの。