LuaTeX / LuaJITTeX

lualatex n'est pas LuaTeX. Suivez le lien symbolique sous TeX Live 2024 : il ne pointe pas vers luatex mais vers luahbtex — un binaire différent, avec HarfBuzz intégré. fmtutil.cnf dit la même chose : lualatex luahbtex ... lualatex.ini, si bien que le format lui-même est produit par LuaHBTeX. À l'exécution, la bannière se présente comme This is LuaHBTeX, Version 1.18.0 (TeX Live 2024), ce qu'on lit sans y prendre garde. Partant de ce qu'est réellement le moteur employé par LaTeX, cette page examine où Lua s'insère dans la composition (\directlua et les callbacks), ce qu'est devenu LuaJITTeX, et ce qui reste, une fois mesuré, de la réputation de lenteur de LuaLaTeX.

lualatex, c'est LuaHBTeX : ce qu'apporte HarfBuzz

Il embarque tout HarfBuzz dans le binaire. LuaHBTeX est entré dans TeX Live en 2020, et c'est alors que ce qui se cache derrière lualatex a été remplacé ; les notes de version précisaient que LuaTeX lui-même restait du LuaTeX ordinaire et que seul le format LaTeX changeait. Une ligne tranche la question : compilez \directlua{tex.print(status.luatex_engine)} avec lualatex et la page affiche luahbtex ; avec luatex, elle affiche luatex. Essayez require("luaharfbuzz") : cela réussit sous luahbtex, qui annonce HarfBuzz 8.3.0, tandis que le luatex nu échoue sur module 'luaharfbuzz' not found. HarfBuzz n'est pas un fichier posé quelque part sur le disque : il est lié statiquement à l'exécutable — d'où l'impossibilité pour un package de l'ajouter après coup, et d'où sa livraison comme binaire distinct.

shell
$ readlink $(which lualatex)
luahbtex

$ grep -E '^lualatex ' $(kpsewhich fmtutil.cnf)
lualatex luahbtex language.dat,language.dat.lua lualatex.ini

$ lualatex --version | head -1
This is LuaHBTeX, Version 1.18.0 (TeX Live 2024)

# Ask the engine what it is, from inside a document:
%   \directlua{tex.print(status.luatex_engine)}     -> luahbtex
%   \directlua{tex.print(_VERSION)}                 -> Lua 5.3
%   \directlua{local hb = require("luaharfbuzz")
%              tex.print(hb.version())}             -> 8.3.0

Conséquence pratique : il existe deux chemins de façonnage, et par défaut luaotfload emploie son propre moteur écrit en Lua. Demandez Renderer=Harfbuzz dans fontspec et le basculement vers HarfBuzz s'opère — le journal montre alors des entrées comme luaotfload.harf.finalize_hlist insérées dans hpack_filter et post_linebreak_filter. Pour les écritures dont le façonnage est complexe, le tibétain ou le bengali par exemple, HarfBuzz est le plus exact des deux ; c'est précisément pour cela que LuaHBTeX a été fabriqué. Dans un document entièrement composé en alphabet latin, la différence ne se verra pas.

Qui a construit LuaTeX, et pour quoi faire

Tapez luatex --credits : l'équipe de développement s'y présente comme Hans Hagen, Hartmut Henkel, Taco Hoekwater et Luigi Scarso, suivie de la liste des projets absorbés — TeX (Knuth), e-TeX (Peter Breitenlohner et d'autres), Omega (John Plaice, Yannis Haralambous), Aleph (Giuseppe Bilotta), pdfTeX (Hàn Thế Thành), MetaPost, Lua, et, annoté « used in LuajitTeX », LuaJIT (Mike Pall). Le filon d'Omega et d'Aleph, ces moteurs multilingues, est la part que l'on connaît le moins. Le développement est né dans le monde ConTeXt et dans le projet Oriental TeX, consacré à la composition savante de l'arabe et singulièrement aux éditions critiques : une subvention proposée par Idris Samawi Hamid, du département de philosophie de la Colorado State University, a financé la reprogrammation du cœur, avec l'appui de la NTG néerlandaise, de la TUG et de la DANTE allemande. L'idée de rendre TeX programmable est née d'un souhait très concret : composer l'arabe avec élégance.

pdfTeX servant de socle, LuaTeX écrit le PDF directement et hérite des extensions e-TeX ainsi que de l'essentiel de la machinerie de composition de pdfTeX. L'espace de noms \pdf... a toutefois été rangé : une grande part passe désormais par trois guichets — \pdfextension, \pdfvariable, \pdffeedback — qui reçoivent un mot-clé et des arguments, tandis que \pdfoutput est devenu \outputmode et \pdfximage, \saveimageresource. On écrit rarement tout cela à la main ; les packages LaTeX absorbent la différence. L'entrée est en UTF-8 d'emblée, et les polices se nomment depuis le système avec fontspec exactement comme sous XeTeX — mais le chargement revient au chargeur écrit en Lua par LuaTeX lui-même, luaotfload, adaptation du chargeur de polices de ConTeXt pour Plain TeX et LaTeX, aujourd'hui maintenue par le groupe latex3 de l'équipe LaTeX.

\directlua : exécuter du Lua au milieu du document

\directlua{...} exécute sur-le-champ le Lua qu'il contient, et ce que tex.print(...) renvoie réintègre le flux d'entrée de TeX pour être composé comme du texte ordinaire. Un détail piège tout le monde au premier essai : \directlua{tex.print(2^10)} n'imprime pas 1024 mais 1024.0. En Lua 5.3, la version qu'emploie LuaTeX, ^ renvoie toujours un flottant. Pour l'entier, on écrit math.tointeger(2^10). La même propriété mord lorsqu'un résultat Lua est affecté à une dimension ou à un compteur ; le manuel de LuaTeX avertit d'ailleurs que tostring et string.format peuvent renvoyer une notation scientifique et dérouter le côté TeX.

latex
\documentclass{article}
\begin{document}
% careful: 2^10 is a float in Lua 5.3, so this prints 1024.0
Two to the tenth is \directlua{tex.print(2^10)}.

% and this prints 1024
Two to the tenth is \directlua{tex.print(math.tointeger(2^10))}.
\end{document}

Son frère \latelua{...} s'exécute au contraire au moment où la page qui le porte est expédiée (shipout). Les tâches qui ne peuvent avoir lieu qu'une fois les numéros de page et les coordonnées finales fixés — dessiner sur le PDF, poser une annotation — lui reviennent. Compilez un document contenant les deux : le journal montre clairement la différence, la sortie de \directlua apparaissant avant [1 et celle de \latelua après. Un conseil pratique : le \directlua brut est susceptible sur les accolades et les caractères spéciaux ; pour tout ce qui dépasse une ligne, utilisez l'environnement luacode du package du même nom, où le Lua se colle tel quel et sans risque.

Les callbacks : insérer sa propre fonction dans la composition

La véritable puissance de LuaTeX réside dans les callbacks. TeX assemble le texte comme une liste chaînée de nœuds — caractères, boîtes et ressorts (glue), l'espace élastique. En enregistrant un callback, votre propre fonction Lua est appelée à chacune de ces articulations et peut inspecter et réécrire cette liste de nœuds. Le LuaTeX de TeX Live 2024 expose 74 callbacks, que le manuel range en six groupes : découverte de fichiers, traitement des données, traitement des listes de nœuds, rapport d'informations, liés au PDF et liés aux polices. L'exemple ci-dessous fonctionne réellement : il compte en combien de lignes un paragraphe a été coupé et en rend compte à la fin de l'exécution.

latex
\documentclass{article}
\usepackage{luacode}
\begin{luacode*}
local count = 0
luatexbase.add_to_callback("post_linebreak_filter",
  function(head)
    for line in node.traverse_id(node.id("hlist"), head) do
      count = count + 1
    end
    return head
  end, "count lines")
luatexbase.add_to_callback("stop_run",
  function() texio.write_nl("LINES TYPESET: " .. count) end, "report")
\end{luacode*}
\begin{document}
This paragraph is broken into lines by TeX, and the Lua function
registered on post\_linebreak\_filter counts them as they go past.
\end{document}

L'exemple enregistre via luatexbase.add_to_callback plutôt que par le callback.register brut, car ce dernier n'admet qu'une seule fonction par callback. Dans le monde LaTeX, plusieurs packages veulent intervenir au même endroit : luatexbase les rassemble en une liste ordonnée. Les outils de manipulation des nœuds sont un ensemble de tables visibles depuis Lua : tex est la fenêtre sur l'état interne de TeX (registres et dimensions), node crée, parcourt et libère les nœuds, token traite les tokens (les plus petites unités de sens de TeX), font les données de police et status les informations d'exécution. Même une machinerie de polices avancée comme luaotfload est écrite en Lua par-dessus tout cela.

CallbackMoment du déclenchementUsage typique
process_input_bufferà chaque ligne d'entrée lueprétraitement de l'entrée (reçoit et renvoie une chaîne)
pre_linebreak_filterjuste avant la coupure d'un paragraphe en lignesréécrire la liste de nœuds avant la coupure
post_linebreak_filterimmédiatement après la coupureagir sur les lignes achevées
hpack_filterà chaque assemblage d'une boîte horizontaleajuster après coup le contenu d'une boîte
ligaturing / kerningétapes de construction des ligatures et du crénageremplacer l'ajustement propre à la police
stop_runtout à la fin de l'exécutionrendre compte des totaux, faire le ménage

Ce qu'est devenu LuaJITTeX

Il n'a pas disparu. TeX Live 2024 livre deux exécutables, luajittex et luajithbtex, tous deux en version 1.18.0, et fmtutil.cnf définit des formats pour eux. À l'intérieur, ce n'est pas le Lua de référence mais LuaJIT, une implémentation à compilation à la volée vers le code machine. Interrogé directement, jit.version répond LuaJIT 2.1.0-beta3 tandis que _VERSION répond Lua 5.1. LuaJITTeX est donc figé au niveau de langage 5.1 et tourne sur une spécification différente de celle du LuaTeX principal, qui en est à 5.3. Le manuel de LuaTeX le dit expressément : LuaJIT n'est pas synchronisé avec le développement du Lua ordinaire, si bien que LuaJITTeX accuse un retard.

Cette différence n'a rien d'académique. Compilez la ligne identique \directlua{tex.print(2^10)} avec les quatre binaires : luatex et luahbtex impriment 1024.0, tandis que luajittex et luajithbtex impriment 1024 — parce que Lua 5.3 distingue entiers et flottants, et non 5.1. Changer de moteur change donc le contenu du document, raison parmi d'autres de ne pas recommander LuaJITTeX comme simple remplaçant de LuaLaTeX. Un second fait, plus décisif encore : fmtutil.cnf ne définit pour les moteurs LuaJIT que des formats de style Plain. Rien n'y correspond à un luajitlatex. Faire tourner LaTeX sur LuaJIT supposerait de construire soi-même un format ; en toute rigueur, cela ne figure donc pas, en pratique, au menu d'un utilisateur de LaTeX.

Une seconde lignée est LuaMetaTeX, réécriture allégée de LuaTeX qui soutient le ConTeXt moderne (LMTX / MkXL). Elle apporte une composition mathématique refondue, quantité de nouvelles primitives et la lignée Lua 5.4. TeX Live 2024 livre luametatex 2.11.02, et la commande context est elle-même un lien symbolique vers lui. La répartition est nette : LuaTeX pour le travail en LaTeX, LuaMetaTeX pour le ConTeXt actuel.

LuaLaTeX est-il vraiment lent ? Une mesure

Il est réellement plus lent, mais on situe mal où se paie le coût. Le démarrage est quasi identique : sur un document d'une page presque vide, pdfLaTeX a mis 0,20 s, XeLaTeX 0,30 s et LuaLaTeX 0,32 s (même machine, meilleur temps sur plusieurs passages). L'écart se creuse dans la part qui croît avec la quantité de texte composé. Sur le même document mathématique de 417 pages, pdfLaTeX a mis 0,40 s, XeLaTeX 0,53 s et LuaLaTeX 1,41 s. Retranchez le démarrage et regardez la composition seule : XeLaTeX coûte environ 1,2 fois pdfLaTeX, LuaLaTeX plus de cinq fois. Retournée, la conclusion est simple : sur un article de dix pages, la différence est imperceptible.

MesurepdfLaTeXXeLaTeXLuaLaTeX
1-page document0,20 s0,30 s0,32 s
417-page document0,40 s0,53 s1,41 s
typesetting onlyréférenceenviron 1,2 foisenviron 5,6 fois

Trois réponses pratiques. D'abord, pendant la rédaction, réduisez le nombre d'allers-retours plutôt que de courir après la vitesse brute : une compilation automatisée comme latexmk, qui ne recompile que ce qui a changé, apporte plus que le choix du moteur. Ensuite, ne confondez pas cela avec le coût unique du cache de polices. Lorsque luaotfload rencontre une police inconnue, il l'analyse et l'indexe, et cette exécution-là est spectaculairement plus lente. La plupart des témoignages sur « LuaLaTeX qui met des dizaines de secondes » relèvent de cela ; dès la deuxième exécution, on retrouve les chiffres ci-dessus. Enfin, en intégration continue, comptez les passages et les recompilations plutôt que le coût d'une compilation. Si le build enchaîne deux ou trois passes pour les renvois et la table des matières, tout écart par passe se trouve multiplié d'autant.

MetaPost intégré, et le japonais avec LuaTeX-ja

LuaTeX intègre le moteur de dessin MetaPost sous forme de bibliothèque, MPlib, si bien que les figures se produisent dans le même processus, sans appeler de programme externe. Depuis LaTeX, on y accède par le package luamplib et l'on écrit du MetaPost directement dans un environnement mplibcode. Compilez-en un et examinez le journal : nulle trace du lancement d'un mpost externe. Cela paraît anodin et compte pourtant : les figures se construisent aussi là où le shell escape est désactivé, et en intégration continue.

La composition japonaise revient à LuaTeX-ja (package luatexja). Il réimplémente du côté Lua, au moyen de callbacks, le savoir-faire japonais de pTeX — composition verticale, gestion de l'espacement intercaractère et de la ponctuation pilotée par les JFM (Japanese Font Metrics), espace entre texte japonais et occidental — ce qui en fait le plus vaste exemple pratique de la machinerie de callbacks décrite plus haut. Avec luatexja-fontspec, les polices japonaises se sélectionnent aussi à la manière de fontspec. Une précaution : le façonnage HarfBuzz de LuaHBTeX et les mécanismes de composition verticale et de CID de LuaTeX-ja demandent à être combinés avec soin. Le manuel de LuaTeX-ja avertit que des polices japonaises définies via HarfBuzz peuvent donner des résultats indésirables. Pour un vrai manuscrit, obtenez d'abord un PDF avec les polices Harano Aji livrées dans TeX Live et les réglages standards, puis n'introduisez fonctionnalités OpenType ou options HarfBuzz qu'après les avoir éprouvées sur un petit essai.

latex
\documentclass{ltjsarticle}
\usepackage{luatexja-fontspec}
% Harano Aji ships with TeX Live, so this builds anywhere
\setmainjfont{HaranoAjiMincho-Regular}
\setsansjfont{HaranoAjiGothic-Medium}
\begin{document}
\section{日本語}
Text and mathematics $E=mc^2$ go through the same engine.
\end{document}

Commencer un nouveau document en LuaLaTeX

  • Partir d'une source Unicode. Pas de inputenc, pas de fontenc. Migrer en conservant les déclarations de l'ère pdfLaTeX ne rapporte que des avertissements et une configuration de polices en double.
  • Faire de fontspec l'unique source de sélection des polices. Pour un manuscrit partagé, obtenir d'abord une compilation qui passe avec les polices livrées dans TeX Live, puis substituer.
  • Employer luatexja pour le japonais. Il couvre la composition verticale, les JFM et l'espace entre texte japonais et occidental.
  • Placer le Lua un peu long dans un environnement luacode. Le \directlua brut est susceptible sur les accolades et les caractères spéciaux.
  • Enregistrer les callbacks via luatexbase.add_to_callback. Le callback.register brut n'admet qu'une fonction par point d'accroche.
  • Construire d'abord un petit échantillon. Une page qui met à l'épreuve la fonte de labeur, la police japonaise, les formules et la bibliographie — puis s'en servir comme modèle.

La règle en une phrase : LuaLaTeX pour entrer dans la composition elle-même, XeLaTeX si l'on veut seulement les polices du système par leur nom, pdfLaTeX si le travail est surtout en anglais et que rapidité et compatibilité comptent. Une page comparant les trois de front existe par ailleurs. L'équipe LaTeX visant LuaTeX pour ses développements nouveaux, LuaLaTeX tend à devenir le choix par défaut des flux de travail Unicode/OpenType récents — en particulier ceux où l'on souhaite programmer quelque chose.