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