「インフォシーク isweb ライト」サービス終了に伴い、 本サイトは以下のURLに移転することになりました。
現在のサイトは、10月末を以て終了となります。
http://zrbabbler.sp.land.to/lualatexlua1.html

Lua 内で TeX コードの実行を完了させる
~LuaTeX で call/cc しない件について~

1 つの Lua コードブロック中で、 それまでに tex.print() 等で書き出した TeX 命令を 実行させた状態で以降の処理に進むという 処理を簡単に記述できる枠組みを構築する。 この実現には Lua のコルーチン機能を利用している。
変更履歴

問題の説明

「思わず Lua で LaTeX してみた(仮)」の「事例 4: 文字列を指定の字幅で切り捨てる」節で 扱った例題をここでも例に用いる。 (従って、ixbase0 パッケージの使用を前提とする。)

\usepackage{ixbase0}

%% 普通の Lua コード(になるはず)
\begin{execluacodeblock}
function truncate_to_width(str)
  local wdt = ixbase.length.ttwTarget.width
  local l, u = 0, str:len()
  while l < u do
    local m = math.ceil((l + u) / 2)
    -- str:sub(1,m) を組版しその幅を \ttwMeasured に代入する
    tex.print([[\settowidth{\ttwMeasured}{]]..str:sub(1, m)..[[}%]])
    【前行の TeX コードの実行が完了した後で以降の処理に進みたい】
    local wdm = ixbase.length.ttwMeasured.width
    if wdm <= wdt then l = m
    else               u = m - 1
    end
  end
  tex.print(str:sub(1, l))
end

\end{execluacodeblock}

%% 簡単な LaTeX コード
\newlength{\ttwTarget}
\newlength{\ttwMeasured}
\newcommand*{\truncatetowidth}[2]{% {width}{string}
  \setlength{\ttwTarget}{#1}%
  \directlua{
    truncate_to_width("\luaescapestring{\detokenize{#2}}")
  }%
}

ここでの問題は、上のソースの【】内に書いたように、 「それまでに tex.print で書き出した TeX コードを 実行させた状態で次の処理に進みたい」ということである。 通常は、現在実行中の Lua コードブロック (\directlua の引数)の実行が終わらないと TeX コードの実行は始まらない。

tangle ライブラリ(仮称)

この問題を解決するためのライブラリ tangle(仮称)を作成してみた。 (今のところ、ixbase0 パッケージに一緒に含めている。) これは次のような関数からなる。

  • tangle.execute(func, ...)... を引数にして関数 func を呼び出す。 その func の実行の中の任意の時点で (そこから呼ばれた関数の実行中も服務)、 次項以降の機能を使うことができる。 重要な制限として、tangle.execute() の呼び出しは \directlua 中で最後に実行される文で なければならない。
  • tangle.run_tex() : 一旦 Lua の実行コンテキストを抜け出し、それまでに tex.print() 等で書き出された TeX コードを実行した後、 直ちに run_tex() の次の文から Lua 実行を再開する。

    実際には、この関数は \directlua{tangle.resume()}tex.print() で書き出してから tangle.suspend() を実行するという処理をしている。 そのため、以前に tex.print で書いた TeX コードの 内容によっては正しく動作しない可能性がある。

  • tangle.suspend() : Lua コードの実行を中断して TeX に戻る。 中断されたコードがある時に新たに tangle.execute() を実行するとエラーになる。
  • tangle.resume()tangle.suspend() により中断していた Lua コードの 実行を次の文から再開させる。 呼び出しの箇所について、tangle.execute() と同じ制限がある。

つまり、基本的には、途中で tangle.run_tex() を使いたいような Lua の実行について、その大元の呼び出しである \directlua が例えば

\directlua{
  local x = A(); B(); C(x)
}

であったら、それを

\directlua{
  tangle.execute(function()
    local x = A(); B(); C(x)
  end)
}

に変えればよい。 もしこの呼び出しが偶然単一の関数呼び出し、例えば

\directlua{
  some_func(a, b, c)
}

であある場合は

\directlua{
  tangle.execute(some_func, a, b, c)
}

という形に置き換えることもできる。

tangle を用いた実装

tangle を使って実現したのが以下のものである。 上掲の「構想」の記述と比べるとコードの変更が最小限になっていることがわかる。

% このファイルの文字コードは UTF-8 (日本語コメントを通すため)
\documentclass[a4paper]{article}
\usepackage{ixbase0}

%% 普通の Lua コード
\begin{execluacodeblock}
function truncate_to_width(str)
  local wdt = ixbase.length.ttwTarget.width
  local l, u = 0, str:len()
  while l < u do
    local m = math.ceil((l + u) / 2)
    -- str:sub(1,m) を組版しその幅を \ttwMeasured に代入する
    tex.print([[\settowidth{\ttwMeasured}{]]..str:sub(1, m)..[[}%]])
    tangle.run_tex() -- TeX の実行を完了させる
    local wdm = ixbase.length.ttwMeasured.width
    if wdm <= wdt then l = m
    else               u = m - 1
    end
  end
  tex.print(str:sub(1, l))
end

\end{execluacodeblock}

%% 簡単な LaTeX コード
\newlength{\ttwTarget}
\newlength{\ttwMeasured}
\newcommand*{\truncatetowidth}[2]{% {width}{string}
  \setlength{\ttwTarget}{#1}%
  \directlua{ % ここで tangle.execute() を呼び出す
    tangle.execute(truncate_to_width, "\luaescapestring{\detokenize{#2}}")
  }%
}

\begin{document}
\begin{itemize}
\item \truncatetowidth{10pt}{Hello world!}
\item \truncatetowidth{20pt}{Hello world!}
\item \truncatetowidth{30pt}{Hello world!}
\item \truncatetowidth{40pt}{Hello world!}
\item \truncatetowidth{50pt}{Hello world!}
\item \truncatetowidth{60pt}{Hello world!}
\item \truncatetowidth{70pt}{Hello world!}
\end{itemize}
\end{document}

(出力は元の版と同じである。)

tangle の中身

実際の ixbase0 を簡略化したもの。 (簡略化の際にバグを入れているかも…。)

tangle = {
  -- 子スレッドの状態定数
  _DONE = 0, -- 正常終了
  _TEX = 1,  -- run_tex() で中断
  _STOP = 2  -- suspend() で中断
  -- _current_co : 現在有効な子スレッド
}
-- 子スレッドを生成・起動
function tangle.execute(func, ...)
  if tangle._current_co then
    error("tangle is going now")
  end
  -- 子スレッド生成
  local args = { ... }
  local co = coroutine.create(function()
    return tangle._DONE, func(unpack(args)) }
  end)
  tangle._current_co = co
  -- 子スレッド起動
  return tangle._check(coroutine.resume(co))
  -- execute() は Lua 呼出(親スレッド)の末尾と決められて
  -- いるので子スレッドが終了/中断すると _check 処理の後
  -- 実行は TeX に戻る
end

-- 子スレッドを再開
function tangle.resume()
  if not tangle._current_co then
    error("tangle is not going")
  end
  return tangle._check(coroutine.resume(tangle._current_co))
  -- execute() と同様、実行は TeX に戻る
end

-- (子スレッドが呼ぶ) TeX を実行させる
function tangle.run_tex()
  -- この引数が中断時の「戻り値」となる
  coroutine.yield(tangle._TEX)
end

-- (子スレッドが呼ぶ) 実行を中断する
function tangle.suspend()
  coroutine.yield(tangle._STOP)
end

-- 子スレッド終了/中断時の後処理
function tangle._check(costat, tstat, ...)
  -- この引数は execute()/resume() の戻り値である。
  -- 子スレッドの戻り値は tstat 以降で、costat は Lua で
  -- 付加された、スレッドの正常終了を示すステータス値。
  if not costat then  -- 子スレッドでエラー発生
    tangle._current_co = nil
    error(tstat) -- この場合、第2引数はメッセージ
  elseif tstat == tangle._DONE then -- 正常終了
    tangle._current_co = nil -- もう子スレッドはない
  elseif tstat == tangle._TEX then -- TeX 戻り
    -- resume() を呼び出しを書き込む
    tex.print("\\directlua{tangle.resume()}\\relax")
  end             -- _STOP(中断)の場合は処理なし
  return ...      -- これは正常終了時の func の戻り値
end