Skip to main content

retex_lua/
lib.rs

1//! Embedded Lua 5.3 and the LuaTeX Lua API.
2//!
3//! [`LuaTexHost`] implements the engine's [`LuaHost`] trait. During a
4//! `\directlua` call the engine is lent to Lua as scoped userdata kept in
5//! the Lua state's app data ([`EngineSlot`]). Library functions borrow it
6//! from there, so no Lua object can outlive the borrow. Nested calls
7//! (Lua → TeX → Lua) install a fresh scoped reference and restore the
8//! previous one afterwards.
9//!
10//! The libraries follow the LuaTeX reference manual (chapter 10, "The TeX
11//! related libraries") and are checked against the behaviour of the
12//! reference `luatex` binary.
13
14use std::rc::Rc;
15
16use mlua::{AnyUserData, Lua, Value};
17use retex_engine::input::LuaItem;
18use retex_engine::{Engine, LuaHost};
19
20mod callback;
21mod compat;
22mod extras;
23mod fontlib;
24mod fontloaderlib;
25mod fonttable;
26mod intarray;
27mod kpselib;
28mod langlib;
29mod lfslib;
30mod lualib;
31pub mod luaotfload;
32mod nestlib;
33pub mod nodelib;
34mod otfaux;
35mod otfnodes;
36mod pdflib;
37mod siolib;
38mod statuslib;
39mod texiolib;
40mod texlib;
41mod tokenlib;
42mod vflib;
43
44/// The scoped engine references lent to Lua, innermost last (app data:
45/// a type-indexed lookup, cheaper than a string-keyed registry entry for
46/// the millions of library calls of some documents).
47#[derive(Default)]
48pub(crate) struct EngineSlot(std::cell::RefCell<Vec<AnyUserData>>);
49
50/// Text printed by `tex.print` and friends and not yet read by TeX: one
51/// buffer per running `\directlua` chunk, above a base buffer for what
52/// is printed outside chunks (in callbacks). Observed in LuaTeX: the end
53/// of a chunk reads its own buffer, `tex.runtoks` reads all of them (so
54/// what a callback printed after its last `tex.runtoks` waits for the
55/// next one).
56pub(crate) struct PrintStack(pub Vec<Vec<LuaItem>>);
57
58impl Default for PrintStack {
59    fn default() -> Self {
60        Self(vec![Vec::new()])
61    }
62}
63
64/// Runs `f` with the engine currently lent to Lua.
65pub(crate) fn with_engine<R>(lua: &Lua, f: impl FnOnce(&mut Engine) -> R) -> mlua::Result<R> {
66    // Cloned, as `f` may lend the engine again (Lua → TeX → Lua).
67    let ud = lua
68        .app_data_ref::<EngineSlot>()
69        .and_then(|s| s.0.borrow().last().cloned())
70        .ok_or_else(|| mlua::Error::runtime("no engine"))?;
71    ud.borrow_mut_scoped::<Engine, R>(f)
72        .map_err(|e| mlua::Error::runtime(format!("engine borrow: {e}")))
73}
74
75/// Registry key of the factory used by [`create_function_plain_errors`].
76const PLAIN_ERRORS_KEY: &str = "retex.plainerrors";
77
78/// Registry key of the function that records which wrapped library
79/// function raised the last error.
80const SETNAME_KEY: &str = "retex.plainerrors.setname";
81
82/// Runs a command of the shell escape in process (retex's own makeindex,
83/// where no program can be started or retex's output modes want it):
84/// given the command line, the directory it runs in, and whether to only
85/// say if it would (`dry`), its exit status and terminal output, or
86/// `None` to leave the command to the shell.
87pub type CommandHook = std::rc::Rc<dyn Fn(&str, &std::path::Path, bool) -> Option<(i32, Vec<u8>)>>;
88
89thread_local! {
90    static COMMAND_HOOK: std::cell::RefCell<Option<CommandHook>> =
91        const { std::cell::RefCell::new(None) };
92}
93
94/// Installs (or removes) the [`CommandHook`] of `os.execute`.
95pub fn set_command_hook(hook: Option<CommandHook>) {
96    COMMAND_HOOK.with(|h| *h.borrow_mut() = hook);
97}
98
99thread_local! {
100    /// The name (`tex.getcatcode`) of the wrapped library function that
101    /// raised the last error, for the traceback (see [`lua_error_message`]).
102    static LAST_WRAPPED_ERROR: std::cell::RefCell<Option<String>> =
103        const { std::cell::RefCell::new(None) };
104}
105
106/// Like `Lua::create_function`, but errors reach Lua code as plain
107/// strings, as `luaL_error` raises them in LuaTeX's C libraries
108/// (`pcall(pdf.obj)` returns `false, "pdf.obj() needs at least one
109/// argument"`), instead of mlua's error userdata with a traceback.
110pub(crate) fn create_function_plain_errors<A, R, F>(lua: &Lua, f: F) -> mlua::Result<mlua::Function>
111where
112    A: mlua::FromLuaMulti,
113    R: mlua::IntoLuaMulti,
114    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
115{
116    plain_errors(lua, f, 0)
117}
118
119/// Like [`create_function_plain_errors`], but the message starts with
120/// the position of the calling Lua code (`[\directlua]:1: ...`), as for
121/// LuaTeX's `tex.getcatcode(-1)`; called directly by `pcall`, it has no
122/// position.
123pub(crate) fn create_function_caller_errors<A, R, F>(
124    lua: &Lua,
125    f: F,
126) -> mlua::Result<mlua::Function>
127where
128    A: mlua::FromLuaMulti,
129    R: mlua::IntoLuaMulti,
130    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
131{
132    plain_errors(lua, f, 2)
133}
134
135/// `error(message, level)` for the error of `f`.
136fn plain_errors<A, R, F>(lua: &Lua, f: F, level: u8) -> mlua::Result<mlua::Function>
137where
138    A: mlua::FromLuaMulti,
139    R: mlua::IntoLuaMulti,
140    F: Fn(&Lua, A) -> mlua::Result<R> + 'static,
141{
142    let inner = lua.create_function(move |lua, args: A| {
143        let mut out = match f(lua, args) {
144            Ok(r) => r.into_lua_multi(lua)?,
145            Err(e) => {
146                let msg = lua.create_string(lua_error_message(&e))?;
147                return Ok(mlua::MultiValue::from_vec(vec![
148                    Value::Boolean(false),
149                    Value::String(msg),
150                ]));
151            }
152        };
153        out.push_front(Value::Boolean(true));
154        Ok(out)
155    })?;
156    // `finish` runs as a tail call, so level 2 is the wrapper's caller.
157    let key = format!("{PLAIN_ERRORS_KEY}{level}");
158    let factory: mlua::Function = match lua.named_registry_value::<Value>(&key)? {
159        Value::Function(f) => f,
160        _ => {
161            let f = lua
162                .load(format!(
163                    "local f, setname = ...
164                     local wrapper
165                     local function finish(ok, ...)
166                       if ok then return ... end
167                       setname(wrapper)
168                       error((...), {level})
169                     end
170                     wrapper = function(...) return finish(f(...)) end
171                     return wrapper"
172                ))
173                .set_name("=[retex]")
174                .into_function()?;
175            lua.set_named_registry_value(&key, &f)?;
176            f
177        }
178    };
179    let setname: mlua::Function = match lua.named_registry_value::<Value>(SETNAME_KEY)? {
180        Value::Function(f) => f,
181        _ => {
182            let f = lua
183                .load(
184                    "local set = ...
185                     return function(fn)
186                       for k, v in pairs(package.loaded) do
187                         if type(v) == 'table' then
188                           for k2, v2 in pairs(v) do
189                             if v2 == fn then
190                               local n = k .. '.' .. tostring(k2)
191                               if k == '_G' then n = tostring(k2) end
192                               set(n)
193                               return
194                             end
195                           end
196                         end
197                       end
198                       set(nil)
199                     end",
200                )
201                .set_name("=[retex]")
202                .into_function()?
203                .call::<mlua::Function>(lua.create_function(|_, n: Option<String>| {
204                    LAST_WRAPPED_ERROR.with(|c| *c.borrow_mut() = n);
205                    Ok(())
206                })?)?;
207            lua.set_named_registry_value(SETNAME_KEY, &f)?;
208            f
209        }
210    };
211    factory.call((inner, setname))
212}
213
214/// Takes all that Lua printed and TeX did not read yet (`tex.runtoks`).
215pub(crate) fn take_printed(lua: &Lua) -> Vec<LuaItem> {
216    lua.app_data_mut::<PrintStack>()
217        .map(|mut ps| ps.0.iter_mut().flat_map(std::mem::take).collect::<Vec<_>>())
218        .unwrap_or_default()
219}
220
221/// Appends an item to the print buffer.
222pub(crate) fn push_print(lua: &Lua, item: LuaItem) {
223    if let Some(mut ps) = lua.app_data_mut::<PrintStack>()
224        && let Some(top) = ps.0.last_mut()
225    {
226        top.push(item);
227    }
228}
229
230/// The Lua interpreter with LuaTeX's libraries.
231pub struct LuaTexHost {
232    lua: Lua,
233    /// The table of `\luafunction`s (`lua.get_functions_table()`).
234    functions: std::cell::OnceCell<Option<mlua::Table>>,
235    /// expl3's intarray functions, run natively.
236    intarrays: intarray::Intarrays,
237}
238
239/// A sampling profiler for performance work (`RETEX_LUA_PROFILE=1`):
240/// every 1000 Lua VM instructions, the running function is charged one
241/// sample; the busiest functions are printed to stderr at exit.
242const PROFILER_START: &str = r#"
243local samples, getinfo = {}, debug.getinfo
244debug.sethook(function()
245  local i = getinfo(2, "S")
246  if i then
247    local k = i.short_src .. ":" .. i.linedefined
248    samples[k] = (samples[k] or 0) + 1
249  end
250end, "", 1000)
251__retex_profile = samples
252"#;
253
254const PROFILER_REPORT: &str = r#"
255local s = __retex_profile
256if not s then return end
257debug.sethook()
258local t, total = {}, 0
259for k, n in pairs(s) do t[#t + 1] = {k, n}; total = total + n end
260table.sort(t, function(a, b) return a[2] > b[2] end)
261io.stderr:write(string.format("[lua profile] %d samples (1 per 1000 instructions)\n", total))
262for i = 1, math.min(25, #t) do
263  io.stderr:write(string.format("[lua profile] %6d  %5.1f%%  %s\n", t[i][2], 100 * t[i][2] / total, t[i][1]))
264end
265"#;
266
267impl Drop for LuaTexHost {
268    fn drop(&mut self) {
269        if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
270            let _ = self.lua.load(PROFILER_REPORT).exec();
271        }
272    }
273}
274
275impl LuaTexHost {
276    /// Creates the interpreter and installs the libraries.
277    pub fn new() -> mlua::Result<Rc<Self>> {
278        // SAFETY: LuaTeX documents expect the full standard library,
279        // including `debug`. This is the crate's single audited unsafe site:
280        // mlua requires `unsafe` to load the debug library, which can break
281        // Lua's own invariants but not Rust's memory safety outside Lua.
282        #[allow(unsafe_code)]
283        let lua = unsafe {
284            Lua::unsafe_new_with(
285                mlua::StdLib::ALL,
286                // LuaTeX's fatal errors end the run inside `pcall` too.
287                mlua::LuaOptions::new().catch_rust_panics(false),
288            )
289        };
290        lua.set_app_data(PrintStack::default());
291        lua.set_app_data(EngineSlot::default());
292        retex_engine::timing::mark("create: Lua state");
293        compat::install(&lua)?;
294        texlib::install(&lua)?;
295        texiolib::install(&lua)?;
296        lualib::install(&lua)?;
297        callback::install(&lua)?;
298        kpselib::install(&lua)?;
299        statuslib::install(&lua)?;
300        lfslib::install(&lua)?;
301        nodelib::install(&lua)?;
302        tokenlib::install(&lua)?;
303        extras::install(&lua)?;
304        luaotfload::install(&lua)?;
305        langlib::install(&lua)?;
306        fontlib::install(&lua)?;
307        fontloaderlib::install(&lua)?;
308        pdflib::install(&lua)?;
309        vflib::install(&lua)?;
310        retex_engine::timing::mark("create: Lua libraries");
311        retex_lpeg::install(&lua)?;
312        retex_engine::timing::mark("create: LPeg");
313        siolib::install(&lua)?;
314        // The command line, as LuaTeX's `arg`: the program name as invoked
315        // at index 0, then the arguments verbatim (hyperxmp reads it).
316        let arg = lua.create_table()?;
317        for (i, a) in std::env::args_os().enumerate() {
318            arg.raw_set(i as i64, lua.create_string(a.as_encoded_bytes())?)?;
319        }
320        lua.globals().set("arg", arg)?;
321        // The job's output directory, for the commands it runs.
322        let output_directory = lua.create_function(|lua, ()| {
323            Ok(crate::with_engine(lua, |e| {
324                e.files
325                    .output_dir
326                    .as_ref()
327                    .map(|d| d.to_string_lossy().into_owned())
328            })
329            .ok()
330            .flatten())
331        })?;
332        // Whether it is retex's own (`.retex/…`): index and bibliography
333        // tools then run there, where their files are.
334        let private_output_directory = lua.create_function(|lua, ()| {
335            Ok(crate::with_engine(lua, |e| e.files.private_output_dir).unwrap_or(false))
336        })?;
337        // The command hook, for shell.lua: status and output, or nil.
338        let command_hook =
339            lua.create_function(|lua, (cmd, dir, dry): (mlua::String, String, bool)| {
340                let hook = COMMAND_HOOK.with(|h| h.borrow().clone());
341                let cmd = cmd.to_str()?.to_owned();
342                match hook.and_then(|h| h(&cmd, std::path::Path::new(&dir), dry)) {
343                    Some((status, out)) => Ok((Some(status), Some(lua.create_string(&out)?))),
344                    None => Ok((None, None)),
345                }
346            })?;
347        lua.load(include_str!("shell.lua"))
348            .set_name("=shell")
349            .call::<()>((output_directory, private_output_directory, command_hook))?;
350        // LuaTeX's libraries are also in package.loaded (require("tex")).
351        lua.load(
352            "for _, n in ipairs({'callback', 'fio', 'font', 'fontloader', 'gzip', 'img',
353               'kpse', 'lang', 'lua', 'md5', 'mplib', 'node', 'pdf', 'pdfe', 'pdfscanner',
354               'sha2', 'sio', 'status', 'tex', 'texio', 'token', 'unicode', 'vf', 'zip',
355               'zlib'}) do
356               if type(_G[n]) == 'table' and package.loaded[n] == nil then
357                 package.loaded[n] = _G[n]
358               end
359             end",
360        )
361        .set_name("=[retex]")
362        .exec()?;
363        if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
364            lua.load(PROFILER_START).set_name("=profiler").exec()?;
365        }
366        let intarrays = intarray::Intarrays::new(&lua);
367        Ok(Rc::new(LuaTexHost {
368            lua,
369            functions: std::cell::OnceCell::new(),
370            intarrays,
371        }))
372    }
373
374    /// The underlying Lua state.
375    pub fn lua(&self) -> &Lua {
376        &self.lua
377    }
378
379    /// Installs a host into an engine.
380    pub fn attach(engine: &mut Engine) -> mlua::Result<Rc<Self>> {
381        let host = LuaTexHost::new()?;
382        engine.lua = Some(host.clone() as Rc<dyn LuaHost>);
383        Ok(host)
384    }
385
386    /// Makes `io.open` and `io.lines` honour `openin_any` and
387    /// `openout_any`, as LuaTeX does when it typesets (not in `texlua`
388    /// scripts, observed).
389    pub fn check_io_names(&self) -> mlua::Result<()> {
390        kpselib::check_io_names(&self.lua)
391    }
392
393    /// `-safer`, as observed with LuaTeX: the functions that run programs
394    /// or change or open files return `nil` without doing anything, and
395    /// `debug.traceback` is removed.
396    pub fn make_safer(&self) -> mlua::Result<()> {
397        self.lua.load(SAFER).set_name("=[retex]").exec()
398    }
399
400    /// Sets the global `arg` table of a standalone script (`--luaonly`):
401    /// the script at index 0, its arguments from 1.
402    pub fn set_script_args(&self, script: &str, args: &[String]) -> mlua::Result<()> {
403        let t = self.lua.create_table()?;
404        t.set(0, script)?;
405        for (i, a) in args.iter().enumerate() {
406            t.set(i as i64 + 1, a.as_str())?;
407        }
408        self.lua.globals().set("arg", t)
409    }
410}
411
412const SAFER: &str = "local function none() return nil end
413for _, f in ipairs({'execute', 'remove', 'rename'}) do os[f] = none end
414os.tempdir = none
415io.popen = none
416io.open = none
417io.tmpfile = none
418if lfs then
419  for _, f in ipairs({'mkdir', 'chdir', 'rmdir', 'touch'}) do lfs[f] = none end
420end
421if debug then debug.traceback = nil end";
422
423/// `--luaconly`: compiles Lua files to bytecode, with `luac`'s options
424/// `-o name` (default `texluac.out`), `-s` (strip debug information), `-p`
425/// (parse only) and `-v` (version). Combining several files into one chunk
426/// and listings (`-l`) are not supported.
427pub fn luac(args: &[String]) -> Result<(), String> {
428    let mut out = "texluac.out".to_owned();
429    let (mut strip, mut parse_only) = (false, false);
430    let mut files = Vec::new();
431    let mut it = args.iter();
432    while let Some(a) = it.next() {
433        match a.as_str() {
434            "-o" => out = it.next().ok_or("'-o' needs argument")?.clone(),
435            "-s" => strip = true,
436            "-p" => parse_only = true,
437            "-v" => println!("Lua 5.3.6  Copyright (C) 1994-2020 Lua.org, PUC-Rio"),
438            "-l" => return Err("listings (-l) are not supported".to_owned()),
439            "--" => files.extend(it.by_ref().cloned()),
440            s if s.starts_with('-') && s != "-" => {
441                return Err(format!("unrecognized option '{s}'"));
442            }
443            _ => files.push(a.clone()),
444        }
445    }
446    if files.is_empty() {
447        return Err("no input files given".to_owned());
448    }
449    if files.len() > 1 && !parse_only {
450        return Err("combining several files into one chunk is not supported".to_owned());
451    }
452    let lua = Lua::new();
453    let mut chunks = Vec::new();
454    for f in &files {
455        let code = if f == "-" {
456            let mut v = Vec::new();
457            std::io::Read::read_to_end(&mut std::io::stdin(), &mut v).map_err(|e| e.to_string())?;
458            v
459        } else {
460            std::fs::read(f).map_err(|e| format!("cannot open {f}: {e}"))?
461        };
462        let func = lua
463            .load(code)
464            .set_name(format!("@{f}"))
465            .into_function()
466            .map_err(|e| e.to_string())?;
467        chunks.push(func);
468    }
469    if parse_only {
470        return Ok(());
471    }
472    let Some(main) = chunks.pop() else {
473        return Ok(());
474    };
475    let bytes = main.dump(strip);
476    std::fs::write(&out, bytes).map_err(|e| format!("cannot write {out}: {e}"))
477}
478
479impl LuaTexHost {
480    /// Runs `body` as a chunk of `\directlua`: with the engine lent to
481    /// Lua and its own buffer for `tex.print`, read when it ends.
482    fn run_chunk(
483        &self,
484        engine: &mut Engine,
485        body: impl FnOnce(&Lua) -> mlua::Result<()>,
486    ) -> Result<(), String> {
487        let lua = &self.lua;
488        if let Some(mut ps) = lua.app_data_mut::<PrintStack>() {
489            ps.0.push(Vec::new());
490        }
491        let result = self.lend(engine, body);
492        let items = lua
493            .app_data_mut::<PrintStack>()
494            .and_then(|mut ps| if ps.0.len() > 1 { ps.0.pop() } else { None })
495            .unwrap_or_default();
496        engine.push_lua_output(items);
497        result.map_err(|e| lua_error_message(&e))
498    }
499
500    /// Lends the engine to Lua while `f` runs (see the module docs).
501    fn lend<R>(
502        &self,
503        engine: &mut Engine,
504        f: impl FnOnce(&Lua) -> mlua::Result<R>,
505    ) -> mlua::Result<R> {
506        let lua = &self.lua;
507        engine.work_begin(retex_engine::profile::Work::Lua);
508        let r = lua.scope(|scope| {
509            let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
510            let slot = lua.app_data_ref::<EngineSlot>();
511            if let Some(s) = &slot {
512                s.0.borrow_mut().push(ud);
513            }
514            drop(slot);
515            let r = f(lua);
516            if let Some(s) = lua.app_data_ref::<EngineSlot>() {
517                s.0.borrow_mut().pop();
518            }
519            r
520        });
521        engine.work_end();
522        r
523    }
524}
525
526/// LuaTeX's fatal error for a callback that returns something other than
527/// a node (observed for a number, a string and a table): a number taken
528/// as a direct node could be any node, and lists would get cycles.
529fn not_a_node(engine: &mut Engine, v: &Value) -> ! {
530    let ty = match v {
531        Value::Integer(_) => "number",
532        _ => v.type_name(),
533    };
534    engine.luatex_fatal(
535        "node lib",
536        &format!("lua <node> expected, not an object with type {ty}"),
537    )
538}
539
540/// `p`, or the empty list if a callback freed it (`node.flush_list(h)
541/// return h`): the engine would use the freed nodes, whose places new
542/// nodes take (a box would contain itself). LuaTeX crashes.
543fn live(engine: &Engine, p: retex_engine::nodes::NodeRef) -> retex_engine::nodes::NodeRef {
544    if engine.nodes.is_free(p) {
545        retex_engine::nodes::NodeRef::NULL
546    } else {
547        p
548    }
549}
550
551impl LuaHost for LuaTexHost {
552    fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String> {
553        let chunk_name = format!("=[{name}]");
554        self.run_chunk(engine, |lua| {
555            lua.load(code).set_name(chunk_name.as_str()).exec()
556        })
557    }
558
559    fn call_function(&self, engine: &mut Engine, n: i32) -> Option<Result<(), String>> {
560        let functions = self
561            .functions
562            .get_or_init(|| self.lua.named_registry_value("retex.functions").ok())
563            .as_ref()?;
564        let Ok(Value::Function(f)) = functions.raw_get::<Value>(n) else {
565            return None;
566        };
567        if let Some(r) = self.intarrays.call(self, engine, n, &f) {
568            return Some(r);
569        }
570        Some(self.run_chunk(engine, |_| f.call::<()>(n)))
571    }
572
573    fn intarray_set(&self, gset: i32, writes: &[(i64, i64, &[i64])]) -> bool {
574        let f = self
575            .functions
576            .get_or_init(|| self.lua.named_registry_value("retex.functions").ok())
577            .as_ref()
578            .and_then(|t| t.raw_get::<mlua::Function>(gset).ok());
579        self.intarrays.set(&self.lua, gset, f.as_ref(), writes)
580    }
581
582    fn call_simple(&self, engine: &mut Engine, name: &str) -> Result<(), String> {
583        let Some(f) = callback::get(&self.lua, name) else {
584            return Ok(());
585        };
586        self.lend(engine, |_lua| f.call::<()>(()))
587            .map_err(|e| lua_error_message(&e))
588    }
589
590    fn call_with_string(&self, engine: &mut Engine, name: &str, arg: &str) -> Result<(), String> {
591        let Some(f) = callback::get(&self.lua, name) else {
592            return Ok(());
593        };
594        self.lend(engine, |_lua| f.call::<()>(arg))
595            .map_err(|e| lua_error_message(&e))
596    }
597
598    fn has_callback(&self, name: &str) -> bool {
599        if let Some((f, _)) = callback::hot(&self.lua, name) {
600            return f;
601        }
602        callback::get(&self.lua, name).is_some()
603    }
604
605    fn foreign_callback(&self, name: &str) -> bool {
606        if !self.has_callback(name) {
607            return false;
608        }
609        // luatexbase's descriptions of the functions in the callback's
610        // list; without luatexbase, a function registered directly.
611        let descriptions = self
612            .lua
613            .globals()
614            .get::<mlua::Table>("luatexbase")
615            .and_then(|t| t.get::<mlua::Function>("callback_descriptions"))
616            .and_then(|f| f.call::<mlua::Table>(name));
617        let Ok(descriptions) = descriptions else {
618            return true;
619        };
620        descriptions
621            .sequence_values::<String>()
622            .any(|d| d.map_or(true, |d| !d.starts_with("luaotfload.")))
623    }
624
625    fn define_font(&self, engine: &mut Engine, name: &[u8], size: i32) -> Option<u32> {
626        let f = callback::get(&self.lua, "define_font")?;
627        let next_id = engine.fonts.fonts.len() as i64;
628        let name = self.lua.create_string(name).ok()?;
629        let r = self.lend(engine, |_lua| {
630            f.call::<Value>((name, i64::from(size), next_id))
631        });
632        match r {
633            Ok(Value::Integer(n)) if n > 0 => Some(n as u32),
634            Ok(Value::Number(n)) if n > 0.0 => Some(n as u32),
635            Ok(Value::Table(t)) => {
636                if matches!(t.raw_get::<Value>("name"), Ok(Value::Nil)) {
637                    engine
638                        .luatex_fatal("font", &format!("lua-loaded font '{next_id}' has no name!"));
639                }
640                let info = fonttable::font_from_table(&t).ok()?;
641                let f = engine.register_font(info);
642                engine.check_lua_loaded_font(f);
643                if let Err(e) = self.lend(engine, |lua| fonttable::attach_commands(lua, &t, f)) {
644                    engine.lua_error(&lua_error_message(&e));
645                }
646                Some(f)
647            }
648            Ok(_) => None,
649            Err(e) => {
650                engine.lua_error(&lua_error_message(&e));
651                None
652            }
653        }
654    }
655
656    fn list_filter(
657        &self,
658        engine: &mut Engine,
659        callback_name: &str,
660        head: retex_engine::nodes::NodeRef,
661        group: &str,
662    ) -> retex_engine::ListFilter {
663        use retex_engine::ListFilter;
664        let Some(f) = callback::get(&self.lua, callback_name) else {
665            return ListFilter::Keep(head);
666        };
667        let r = self.lend(engine, |lua| {
668            let h = if head.is_null() {
669                Value::Nil
670            } else {
671                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
672            };
673            f.call::<Value>((h, group))
674        });
675        let r = match r {
676            Ok(Value::Boolean(false)) => return ListFilter::Flush,
677            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
678                Ok(n) => retex_engine::nodes::NodeRef(n.0),
679                // Observed: a token (or other userdata) is fatal.
680                Err(_) => not_a_node(engine, &Value::UserData(u.clone())),
681            },
682            // Observed: nil (or no value) discards the list, as false does.
683            Ok(Value::Nil) => return ListFilter::Flush,
684            Ok(Value::Boolean(true)) => head,
685            Ok(other) => not_a_node(engine, &other),
686            Err(e) => {
687                engine.lua_error(&lua_error_message(&e));
688                head
689            }
690        };
691        ListFilter::Keep(live(engine, r))
692    }
693
694    fn pack_quality(
695        &self,
696        engine: &mut Engine,
697        cb: &str,
698        incident: &str,
699        detail: i32,
700        b: retex_engine::nodes::NodeRef,
701        first: i32,
702        last: i32,
703    ) -> retex_engine::nodes::NodeRef {
704        use retex_engine::nodes::NodeRef;
705        let Some(f) = callback::get(&self.lua, cb) else {
706            return NodeRef::NULL;
707        };
708        let r = self.lend(engine, |lua| {
709            let h = Value::UserData(lua.create_userdata(nodelib::LNode(b.0))?);
710            f.call::<Value>((incident, detail, h, first, last))
711        });
712        match r {
713            Ok(Value::UserData(u)) if cb == "hpack_quality" => match u.borrow::<nodelib::LNode>() {
714                Ok(n) => live(engine, NodeRef(n.0)),
715                Err(_) => NodeRef::NULL,
716            },
717            Ok(Value::Nil) => NodeRef::NULL,
718            // Observed: hpack_quality must give a node or nothing.
719            Ok(other) if cb == "hpack_quality" => not_a_node(engine, &other),
720            Ok(_) => NodeRef::NULL,
721            Err(e) => {
722                engine.lua_error(&lua_error_message(&e));
723                NodeRef::NULL
724            }
725        }
726    }
727
728    fn node_pass(&self, engine: &mut Engine, cb: &str, head: retex_engine::nodes::NodeRef) -> bool {
729        if callback::hot(&self.lua, cb).is_some_and(|(f, d)| !f && !d) {
730            return false;
731        }
732        let Ok(reg) = self
733            .lua
734            .named_registry_value::<mlua::Table>("retex.callbacks")
735        else {
736            return false;
737        };
738        match reg.get::<Value>(cb) {
739            Ok(Value::Boolean(false)) => true,
740            Ok(Value::Function(f)) => {
741                let first = engine.nodes.next(head);
742                if first.is_null() {
743                    return true;
744                }
745                let tail = engine.nodes.tail_of(first);
746                let r = self.lend(engine, |lua| {
747                    let h = Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?);
748                    let t = Value::UserData(lua.create_userdata(nodelib::LNode(tail.0))?);
749                    f.call::<()>((h, t))
750                });
751                if let Err(e) = r {
752                    engine.lua_error(&lua_error_message(&e));
753                }
754                true
755            }
756            _ => false,
757        }
758    }
759
760    fn linebreak_filter(
761        &self,
762        engine: &mut Engine,
763        head: retex_engine::nodes::NodeRef,
764        display: bool,
765    ) -> Option<retex_engine::nodes::NodeRef> {
766        let f = callback::get(&self.lua, "linebreak_filter")?;
767        let r = self.lend(engine, |lua| {
768            let h = if head.is_null() {
769                Value::Nil
770            } else {
771                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
772            };
773            f.call::<Value>((h, display))
774        });
775        // Observed: anything but a node means the built-in line breaking.
776        let r = match r {
777            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
778                Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
779                Err(_) => None,
780            },
781            Ok(_) => None,
782            Err(e) => {
783                engine.lua_error(&lua_error_message(&e));
784                None
785            }
786        };
787        r.map(|p| live(engine, p)).filter(|p| !p.is_null())
788    }
789
790    fn append_to_vlist_filter(
791        &self,
792        engine: &mut Engine,
793        b: retex_engine::nodes::NodeRef,
794        location: &str,
795        prev_depth: i32,
796    ) -> Option<(retex_engine::nodes::NodeRef, Option<i32>)> {
797        let f = callback::get(&self.lua, "append_to_vlist_filter")?;
798        let r = self.lend(engine, |lua| {
799            let bx = Value::UserData(lua.create_userdata(nodelib::LNode(b.0))?);
800            // The last argument (mirrored) is false for the boxes TeX
801            // appends (observed).
802            f.call::<(Value, Value)>((bx, location, prev_depth, false))
803        });
804        match r {
805            Ok((list, depth)) => {
806                let head = match &list {
807                    Value::UserData(u) => u
808                        .borrow::<nodelib::LNode>()
809                        .map(|n| live(engine, retex_engine::nodes::NodeRef(n.0)))
810                        .unwrap_or(retex_engine::nodes::NodeRef::NULL),
811                    _ => retex_engine::nodes::NodeRef::NULL,
812                };
813                let depth = match depth {
814                    Value::Integer(d) => Some(d as i32),
815                    Value::Number(d) => Some(d.round() as i32),
816                    _ => None,
817                };
818                Some((head, depth))
819            }
820            Err(e) => {
821                engine.lua_error(&lua_error_message(&e));
822                Some((b, None))
823            }
824        }
825    }
826
827    fn mlist_to_hlist(
828        &self,
829        engine: &mut Engine,
830        head: retex_engine::nodes::NodeRef,
831        style: i32,
832        penalties: bool,
833    ) -> Option<retex_engine::nodes::NodeRef> {
834        let f = callback::get(&self.lua, "mlist_to_hlist")?;
835        let name = texlib::MATH_STYLE_NAMES
836            .get(style as usize)
837            .copied()
838            .unwrap_or("text");
839        let r = self.lend(engine, |lua| {
840            let h = if head.is_null() {
841                Value::Nil
842            } else {
843                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
844            };
845            f.call::<Value>((h, name, penalties))
846        });
847        let r = match r {
848            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
849                Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
850                Err(_) => None,
851            },
852            Ok(v @ (Value::Integer(_) | Value::Number(_) | Value::String(_) | Value::Table(_))) => {
853                not_a_node(engine, &v)
854            }
855            Ok(_) => None,
856            Err(e) => {
857                engine.lua_error(&lua_error_message(&e));
858                None
859            }
860        };
861        r.map(|p| live(engine, p))
862    }
863
864    fn pipe_read(&self, engine: &mut Engine, cmd: &[u8]) -> Option<Vec<u8>> {
865        let cmd = self.lua.create_string(cmd).ok()?;
866        let f: mlua::Function = self
867            .lua
868            .load(
869                "local cmd = ...
870                 local f = io.popen(cmd, 'r')
871                 if not f then return nil end
872                 local s = f:read('a')
873                 f:close()
874                 return s",
875            )
876            .set_name("=[retex]")
877            .into_function()
878            .ok()?;
879        match self.lend(engine, |_| f.call::<Value>(cmd)) {
880            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
881            _ => None,
882        }
883    }
884
885    fn input_level_string(&self, engine: &mut Engine, n: i64) -> Option<Vec<u8>> {
886        let f = callback::get(&self.lua, "input_level_string")?;
887        let r = self.lend(engine, |_| f.call::<Value>(n));
888        match r {
889            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
890            Ok(_) => None,
891            Err(e) => {
892                engine.lua_error(&lua_error_message(&e));
893                None
894            }
895        }
896    }
897
898    fn glyph_info(&self, engine: &mut Engine, p: retex_engine::nodes::NodeRef) -> Option<Vec<u8>> {
899        let f = callback::get(&self.lua, "glyph_info")?;
900        let r = self.lend(engine, |lua| {
901            let n = lua.create_userdata(nodelib::LNode(p.0))?;
902            f.call::<Value>(n)
903        });
904        match r {
905            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
906            Ok(_) => None,
907            Err(e) => {
908                engine.lua_error(&lua_error_message(&e));
909                None
910            }
911        }
912    }
913
914    fn call_reference(
915        &self,
916        engine: &mut Engine,
917        reference: i32,
918        args: retex_engine::ReferenceArgs,
919    ) -> Result<(), String> {
920        use retex_engine::ReferenceArgs;
921        self.lend(engine, |lua| {
922            let v: Value = nodelib::lua_refs_synced(lua)?.raw_get(reference)?;
923            match (v, args) {
924                (Value::Function(f), ReferenceArgs::Glyph(font, chr)) => f.call::<()>((font, chr)),
925                (Value::Function(f), ReferenceArgs::Node(p)) => {
926                    f.call::<()>(lua.create_userdata(nodelib::LNode(p.0))?)
927                }
928                (Value::String(code), _) => lua
929                    .load(code.as_bytes().as_ref())
930                    .set_name("=[vf command]")
931                    .exec(),
932                _ => Ok(()),
933            }
934        })
935        .map_err(|e| lua_error_message(&e))
936    }
937
938    fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
939        // Store the functions in `lua.bytecode` as (index, bytecode) pairs.
940        let mut out = retex_engine::format::Writer::default();
941        let mut entries: Vec<(i64, Vec<u8>)> = Vec::new();
942        if let Ok(lt) = self.lua.globals().get::<mlua::Table>("lua")
943            && let Ok(bc) = lt.get::<mlua::Table>("bytecode")
944        {
945            for (k, v) in bc.pairs::<i64, mlua::Function>().flatten() {
946                entries.push((k, v.dump(false)));
947            }
948        }
949        entries.sort_by_key(|(k, _)| *k);
950        out.u(entries.len() as u64);
951        for (k, b) in entries {
952            out.i(k);
953            out.bytes(&b);
954        }
955        out.buf
956    }
957
958    fn undump(&self, _engine: &mut Engine, data: &[u8]) -> Result<(), String> {
959        if data.is_empty() {
960            return Ok(());
961        }
962        let mut r = retex_engine::format::Reader::new(data);
963        let n = r.u().map_err(|e| e.to_string())?;
964        let lt: mlua::Table = self.lua.globals().get("lua").map_err(|e| e.to_string())?;
965        let bc: mlua::Table = lt.get("bytecode").map_err(|e| e.to_string())?;
966        for _ in 0..n {
967            let k = r.i().map_err(|e| e.to_string())?;
968            let b = r.bytes().map_err(|e| e.to_string())?;
969            let f = self
970                .lua
971                .load(b)
972                .set_mode(mlua::ChunkMode::Binary)
973                .into_function()
974                .map_err(|e| e.to_string())?;
975            bc.set(k, f).map_err(|e| e.to_string())?;
976        }
977        Ok(())
978    }
979}
980
981/// Formats a Lua error the way LuaTeX reports it: the message without
982/// mlua's decorations.
983pub fn lua_error_message(e: &mlua::Error) -> String {
984    match e {
985        mlua::Error::RuntimeError(s) => luatex_traceback(s),
986        mlua::Error::SyntaxError { message: s, .. } => s.clone(),
987        mlua::Error::CallbackError { cause, .. } => lua_error_message(cause),
988        other => other.to_string(),
989    }
990}
991
992/// Rewrites the traceback of an error message as LuaTeX shows it: without
993/// the frame of the message handler, and with a library function wrapped
994/// by [`create_function_plain_errors`] shown as the C function it is in
995/// LuaTeX (`[C]: in function 'tex.getcatcode'`) instead of the wrapper's
996/// `error` call, its frames and the tail call.
997fn luatex_traceback(s: &str) -> String {
998    let Some(at) = s.find("\nstack traceback:\n") else {
999        return s.to_string();
1000    };
1001    let (head, tb) = s.split_at(at + "\nstack traceback:\n".len());
1002    let lines: Vec<&str> = tb.split('\n').collect();
1003    let mut out: Vec<String> = Vec::with_capacity(lines.len());
1004    let mut i = 0;
1005    // The message handler's own frame (`in ?`, or `in metamethod` for an
1006    // error of the virtual machine).
1007    if lines
1008        .first()
1009        .is_some_and(|l| *l == "\t[C]: in ?" || l.starts_with("\t[C]: in metamethod "))
1010    {
1011        i = 1;
1012    }
1013    while i < lines.len() {
1014        let l = lines[i];
1015        let next_is_wrapper = lines
1016            .get(i + 1)
1017            .is_some_and(|n| n.starts_with("\t[retex]:"));
1018        if l == "\t[C]: in function 'error'" && next_is_wrapper {
1019            let name = LAST_WRAPPED_ERROR.with(|c| c.borrow().clone());
1020            out.push(match name {
1021                Some(n) => format!("\t[C]: in function '{n}'"),
1022                None => "\t[C]: in ?".to_string(),
1023            });
1024            i += 1;
1025            while i < lines.len()
1026                && (lines[i].starts_with("\t[retex]:") || lines[i] == "\t(...tail calls...)")
1027            {
1028                i += 1;
1029            }
1030            continue;
1031        }
1032        out.push(l.to_string());
1033        i += 1;
1034    }
1035    format!("{head}{}", out.join("\n"))
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040    use super::*;
1041
1042    #[test]
1043    fn arg_holds_the_command_line() {
1044        let host = LuaTexHost::new().expect("host");
1045        let (n, first): (i64, String) = host.lua().load("return #arg, arg[0]").eval().expect("arg");
1046        assert_eq!(n as usize, std::env::args().count() - 1);
1047        assert_eq!(first, std::env::args().next().unwrap());
1048    }
1049
1050    #[test]
1051    fn lpeg_is_installed() {
1052        let host = LuaTexHost::new().expect("host");
1053        let r: (i64, bool, String, bool) = host
1054            .lua()
1055            .load(
1056                r#"return lpeg.match(lpeg.R"az"^1, "abc1"), package.loaded.lpeg == lpeg,
1057                   lpeg.version(), package.loaded.re == nil"#,
1058            )
1059            .eval()
1060            .expect("lpeg");
1061        assert_eq!(r, (4, true, "1.0.1".to_string(), true));
1062    }
1063}