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