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 { Lua::unsafe_new_with(mlua::StdLib::ALL, mlua::LuaOptions::new()) };
256        lua.set_app_data(PrintStack::default());
257        retex_engine::timing::mark("create: Lua state");
258        compat::install(&lua)?;
259        texlib::install(&lua)?;
260        texiolib::install(&lua)?;
261        lualib::install(&lua)?;
262        callback::install(&lua)?;
263        kpselib::install(&lua)?;
264        statuslib::install(&lua)?;
265        lfslib::install(&lua)?;
266        nodelib::install(&lua)?;
267        tokenlib::install(&lua)?;
268        extras::install(&lua)?;
269        luaotfload::install(&lua)?;
270        langlib::install(&lua)?;
271        fontlib::install(&lua)?;
272        fontloaderlib::install(&lua)?;
273        pdflib::install(&lua)?;
274        retex_engine::timing::mark("create: Lua libraries");
275        retex_lpeg::install(&lua)?;
276        retex_engine::timing::mark("create: LPeg");
277        siolib::install(&lua)?;
278        // The command line, as LuaTeX's `arg`: the program name as invoked
279        // at index 0, then the arguments verbatim (hyperxmp reads it).
280        let arg = lua.create_table()?;
281        for (i, a) in std::env::args_os().enumerate() {
282            arg.raw_set(i as i64, lua.create_string(a.as_encoded_bytes())?)?;
283        }
284        lua.globals().set("arg", arg)?;
285        // The job's output directory, for the commands it runs.
286        let output_directory = lua.create_function(|lua, ()| {
287            Ok(crate::with_engine(lua, |e| {
288                e.files
289                    .output_dir
290                    .as_ref()
291                    .map(|d| d.to_string_lossy().into_owned())
292            })
293            .ok()
294            .flatten())
295        })?;
296        lua.load(include_str!("shell.lua"))
297            .set_name("=shell")
298            .call::<()>(output_directory)?;
299        // LuaTeX's libraries are also in package.loaded (require("tex")).
300        lua.load(
301            "for _, n in ipairs({'callback', 'fio', 'font', 'fontloader', 'gzip', 'img',
302               'kpse', 'lang', 'lua', 'md5', 'mplib', 'node', 'pdf', 'pdfe', 'pdfscanner',
303               'sha2', 'sio', 'status', 'tex', 'texio', 'token', 'unicode', 'vf', 'zip',
304               'zlib'}) do
305               if type(_G[n]) == 'table' and package.loaded[n] == nil then
306                 package.loaded[n] = _G[n]
307               end
308             end",
309        )
310        .set_name("=[retex]")
311        .exec()?;
312        if std::env::var_os("RETEX_LUA_PROFILE").is_some() {
313            lua.load(PROFILER_START).set_name("=profiler").exec()?;
314        }
315        Ok(Rc::new(LuaTexHost { lua }))
316    }
317
318    /// The underlying Lua state.
319    pub fn lua(&self) -> &Lua {
320        &self.lua
321    }
322
323    /// Installs a host into an engine.
324    pub fn attach(engine: &mut Engine) -> mlua::Result<Rc<Self>> {
325        let host = LuaTexHost::new()?;
326        engine.lua = Some(host.clone() as Rc<dyn LuaHost>);
327        Ok(host)
328    }
329
330    /// Makes `io.open` and `io.lines` honour `openin_any` and
331    /// `openout_any`, as LuaTeX does when it typesets (not in `texlua`
332    /// scripts, observed).
333    pub fn check_io_names(&self) -> mlua::Result<()> {
334        kpselib::check_io_names(&self.lua)
335    }
336
337    /// `-safer`, as observed with LuaTeX: the functions that run programs
338    /// or change or open files return `nil` without doing anything, and
339    /// `debug.traceback` is removed.
340    pub fn make_safer(&self) -> mlua::Result<()> {
341        self.lua.load(SAFER).set_name("=[retex]").exec()
342    }
343
344    /// Sets the global `arg` table of a standalone script (`--luaonly`):
345    /// the script at index 0, its arguments from 1.
346    pub fn set_script_args(&self, script: &str, args: &[String]) -> mlua::Result<()> {
347        let t = self.lua.create_table()?;
348        t.set(0, script)?;
349        for (i, a) in args.iter().enumerate() {
350            t.set(i as i64 + 1, a.as_str())?;
351        }
352        self.lua.globals().set("arg", t)
353    }
354}
355
356const SAFER: &str = "local function none() return nil end
357for _, f in ipairs({'execute', 'remove', 'rename'}) do os[f] = none end
358os.tempdir = none
359io.popen = none
360io.open = none
361io.tmpfile = none
362if lfs then
363  for _, f in ipairs({'mkdir', 'chdir', 'rmdir', 'touch'}) do lfs[f] = none end
364end
365if debug then debug.traceback = nil end";
366
367/// `--luaconly`: compiles Lua files to bytecode, with `luac`'s options
368/// `-o name` (default `texluac.out`), `-s` (strip debug information), `-p`
369/// (parse only) and `-v` (version). Combining several files into one chunk
370/// and listings (`-l`) are not supported.
371pub fn luac(args: &[String]) -> Result<(), String> {
372    let mut out = "texluac.out".to_owned();
373    let (mut strip, mut parse_only) = (false, false);
374    let mut files = Vec::new();
375    let mut it = args.iter();
376    while let Some(a) = it.next() {
377        match a.as_str() {
378            "-o" => out = it.next().ok_or("'-o' needs argument")?.clone(),
379            "-s" => strip = true,
380            "-p" => parse_only = true,
381            "-v" => println!("Lua 5.3.6  Copyright (C) 1994-2020 Lua.org, PUC-Rio"),
382            "-l" => return Err("listings (-l) are not supported".to_owned()),
383            "--" => files.extend(it.by_ref().cloned()),
384            s if s.starts_with('-') && s != "-" => {
385                return Err(format!("unrecognized option '{s}'"));
386            }
387            _ => files.push(a.clone()),
388        }
389    }
390    if files.is_empty() {
391        return Err("no input files given".to_owned());
392    }
393    if files.len() > 1 && !parse_only {
394        return Err("combining several files into one chunk is not supported".to_owned());
395    }
396    let lua = Lua::new();
397    let mut chunks = Vec::new();
398    for f in &files {
399        let code = if f == "-" {
400            let mut v = Vec::new();
401            std::io::Read::read_to_end(&mut std::io::stdin(), &mut v).map_err(|e| e.to_string())?;
402            v
403        } else {
404            std::fs::read(f).map_err(|e| format!("cannot open {f}: {e}"))?
405        };
406        let func = lua
407            .load(code)
408            .set_name(format!("@{f}"))
409            .into_function()
410            .map_err(|e| e.to_string())?;
411        chunks.push(func);
412    }
413    if parse_only {
414        return Ok(());
415    }
416    let Some(main) = chunks.pop() else {
417        return Ok(());
418    };
419    let bytes = main.dump(strip);
420    std::fs::write(&out, bytes).map_err(|e| format!("cannot write {out}: {e}"))
421}
422
423impl LuaTexHost {
424    /// Lends the engine to Lua while `f` runs (see the module docs).
425    fn lend<R>(
426        &self,
427        engine: &mut Engine,
428        f: impl FnOnce(&Lua) -> mlua::Result<R>,
429    ) -> mlua::Result<R> {
430        let lua = &self.lua;
431        lua.scope(|scope| {
432            let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
433            let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
434            lua.set_named_registry_value(ENGINE_KEY, ud)?;
435            let r = f(lua);
436            lua.set_named_registry_value(ENGINE_KEY, prev)?;
437            r
438        })
439    }
440}
441
442/// LuaTeX's fatal error for a callback that returns something other than
443/// a node (observed for a number, a string and a table): a number taken
444/// as a direct node could be any node, and lists would get cycles.
445fn not_a_node(engine: &mut Engine, v: &Value) -> ! {
446    let ty = match v {
447        Value::Integer(_) => "number",
448        _ => v.type_name(),
449    };
450    engine.luatex_fatal(
451        "node lib",
452        &format!("lua <node> expected, not an object with type {ty}"),
453    )
454}
455
456/// `p`, or the empty list if a callback freed it (`node.flush_list(h)
457/// return h`): the engine would use the freed nodes, whose places new
458/// nodes take (a box would contain itself). LuaTeX crashes.
459fn live(engine: &Engine, p: retex_engine::nodes::NodeRef) -> retex_engine::nodes::NodeRef {
460    if engine.nodes.is_free(p) {
461        retex_engine::nodes::NodeRef::NULL
462    } else {
463        p
464    }
465}
466
467impl LuaHost for LuaTexHost {
468    fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String> {
469        let lua = &self.lua;
470        if let Some(mut ps) = lua.app_data_mut::<PrintStack>() {
471            ps.0.push(Vec::new());
472        }
473        let chunk_name = format!("=[{name}]");
474        let result = lua.scope(|scope| {
475            let ud = scope.create_any_userdata_ref_mut(&mut *engine)?;
476            let prev: Value = lua.named_registry_value(ENGINE_KEY)?;
477            lua.set_named_registry_value(ENGINE_KEY, ud)?;
478            let r = lua.load(code).set_name(chunk_name.as_str()).exec();
479            lua.set_named_registry_value(ENGINE_KEY, prev)?;
480            r
481        });
482        let items = lua
483            .app_data_mut::<PrintStack>()
484            .and_then(|mut ps| if ps.0.len() > 1 { ps.0.pop() } else { None })
485            .unwrap_or_default();
486        engine.push_lua_output(items);
487        result.map_err(|e| lua_error_message(&e))
488    }
489
490    fn call_simple(&self, engine: &mut Engine, name: &str) -> Result<(), String> {
491        let Some(f) = callback::get(&self.lua, name) else {
492            return Ok(());
493        };
494        self.lend(engine, |_lua| f.call::<()>(()))
495            .map_err(|e| lua_error_message(&e))
496    }
497
498    fn has_callback(&self, name: &str) -> bool {
499        callback::get(&self.lua, name).is_some()
500    }
501
502    fn define_font(&self, engine: &mut Engine, name: &[u8], size: i32) -> Option<u32> {
503        let f = callback::get(&self.lua, "define_font")?;
504        let next_id = engine.fonts.fonts.len() as i64;
505        let name = self.lua.create_string(name).ok()?;
506        let r = self.lend(engine, |_lua| {
507            f.call::<Value>((name, i64::from(size), next_id))
508        });
509        match r {
510            Ok(Value::Integer(n)) if n > 0 => Some(n as u32),
511            Ok(Value::Number(n)) if n > 0.0 => Some(n as u32),
512            Ok(Value::Table(t)) => {
513                let info = fonttable::font_from_table(&t).ok()?;
514                Some(engine.register_font(info))
515            }
516            Ok(_) => None,
517            Err(e) => {
518                engine.lua_error(&lua_error_message(&e));
519                None
520            }
521        }
522    }
523
524    fn list_filter(
525        &self,
526        engine: &mut Engine,
527        callback_name: &str,
528        head: retex_engine::nodes::NodeRef,
529        group: &str,
530    ) -> retex_engine::ListFilter {
531        use retex_engine::ListFilter;
532        let Some(f) = callback::get(&self.lua, callback_name) else {
533            return ListFilter::Keep(head);
534        };
535        let r = self.lend(engine, |lua| {
536            let h = if head.is_null() {
537                Value::Nil
538            } else {
539                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
540            };
541            f.call::<Value>((h, group))
542        });
543        let r = match r {
544            Ok(Value::Boolean(false)) => return ListFilter::Flush,
545            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
546                Ok(n) => retex_engine::nodes::NodeRef(n.0),
547                Err(_) => head,
548            },
549            // Observed: nil (or no value) discards the list, as false does.
550            Ok(Value::Nil) => return ListFilter::Flush,
551            Ok(Value::Boolean(true)) => head,
552            Ok(other) => not_a_node(engine, &other),
553            Err(e) => {
554                engine.lua_error(&lua_error_message(&e));
555                head
556            }
557        };
558        ListFilter::Keep(live(engine, r))
559    }
560
561    fn mlist_to_hlist(
562        &self,
563        engine: &mut Engine,
564        head: retex_engine::nodes::NodeRef,
565        style: i32,
566        penalties: bool,
567    ) -> Option<retex_engine::nodes::NodeRef> {
568        let f = callback::get(&self.lua, "mlist_to_hlist")?;
569        let name = texlib::MATH_STYLE_NAMES
570            .get(style as usize)
571            .copied()
572            .unwrap_or("text");
573        let r = self.lend(engine, |lua| {
574            let h = if head.is_null() {
575                Value::Nil
576            } else {
577                Value::UserData(lua.create_userdata(nodelib::LNode(head.0))?)
578            };
579            f.call::<Value>((h, name, penalties))
580        });
581        let r = match r {
582            Ok(Value::UserData(u)) => match u.borrow::<nodelib::LNode>() {
583                Ok(n) => Some(retex_engine::nodes::NodeRef(n.0)),
584                Err(_) => None,
585            },
586            Ok(v @ (Value::Integer(_) | Value::Number(_) | Value::String(_) | Value::Table(_))) => {
587                not_a_node(engine, &v)
588            }
589            Ok(_) => None,
590            Err(e) => {
591                engine.lua_error(&lua_error_message(&e));
592                None
593            }
594        };
595        r.map(|p| live(engine, p))
596    }
597
598    fn pipe_read(&self, engine: &mut Engine, cmd: &[u8]) -> Option<Vec<u8>> {
599        let cmd = self.lua.create_string(cmd).ok()?;
600        let f: mlua::Function = self
601            .lua
602            .load(
603                "local cmd = ...
604                 local f = io.popen(cmd, 'r')
605                 if not f then return nil end
606                 local s = f:read('a')
607                 f:close()
608                 return s",
609            )
610            .set_name("=[retex]")
611            .into_function()
612            .ok()?;
613        match self.lend(engine, |_| f.call::<Value>(cmd)) {
614            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
615            _ => None,
616        }
617    }
618
619    fn input_level_string(&self, engine: &mut Engine, n: i64) -> Option<Vec<u8>> {
620        let f = callback::get(&self.lua, "input_level_string")?;
621        let r = self.lend(engine, |_| f.call::<Value>(n));
622        match r {
623            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
624            Ok(_) => None,
625            Err(e) => {
626                engine.lua_error(&lua_error_message(&e));
627                None
628            }
629        }
630    }
631
632    fn glyph_info(&self, engine: &mut Engine, p: retex_engine::nodes::NodeRef) -> Option<Vec<u8>> {
633        let f = callback::get(&self.lua, "glyph_info")?;
634        let r = self.lend(engine, |lua| {
635            let n = lua.create_userdata(nodelib::LNode(p.0))?;
636            f.call::<Value>(n)
637        });
638        match r {
639            Ok(Value::String(s)) => Some(s.as_bytes().to_vec()),
640            Ok(_) => None,
641            Err(e) => {
642                engine.lua_error(&lua_error_message(&e));
643                None
644            }
645        }
646    }
647
648    fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
649        // Store the functions in `lua.bytecode` as (index, bytecode) pairs.
650        let mut out = retex_engine::format::Writer::default();
651        let mut entries: Vec<(i64, Vec<u8>)> = Vec::new();
652        if let Ok(lt) = self.lua.globals().get::<mlua::Table>("lua")
653            && let Ok(bc) = lt.get::<mlua::Table>("bytecode")
654        {
655            for (k, v) in bc.pairs::<i64, mlua::Function>().flatten() {
656                entries.push((k, v.dump(false)));
657            }
658        }
659        entries.sort_by_key(|(k, _)| *k);
660        out.u(entries.len() as u64);
661        for (k, b) in entries {
662            out.i(k);
663            out.bytes(&b);
664        }
665        out.buf
666    }
667
668    fn undump(&self, _engine: &mut Engine, data: &[u8]) -> Result<(), String> {
669        if data.is_empty() {
670            return Ok(());
671        }
672        let mut r = retex_engine::format::Reader::new(data);
673        let n = r.u().map_err(|e| e.to_string())?;
674        let lt: mlua::Table = self.lua.globals().get("lua").map_err(|e| e.to_string())?;
675        let bc: mlua::Table = lt.get("bytecode").map_err(|e| e.to_string())?;
676        for _ in 0..n {
677            let k = r.i().map_err(|e| e.to_string())?;
678            let b = r.bytes().map_err(|e| e.to_string())?;
679            let f = self
680                .lua
681                .load(b)
682                .set_mode(mlua::ChunkMode::Binary)
683                .into_function()
684                .map_err(|e| e.to_string())?;
685            bc.set(k, f).map_err(|e| e.to_string())?;
686        }
687        Ok(())
688    }
689}
690
691/// Formats a Lua error the way LuaTeX reports it: the message without
692/// mlua's decorations.
693pub fn lua_error_message(e: &mlua::Error) -> String {
694    match e {
695        mlua::Error::RuntimeError(s) => luatex_traceback(s),
696        mlua::Error::SyntaxError { message: s, .. } => s.clone(),
697        mlua::Error::CallbackError { cause, .. } => lua_error_message(cause),
698        other => other.to_string(),
699    }
700}
701
702/// Rewrites the traceback of an error message as LuaTeX shows it: without
703/// the frame of the message handler, and with a library function wrapped
704/// by [`create_function_plain_errors`] shown as the C function it is in
705/// LuaTeX (`[C]: in function 'tex.getcatcode'`) instead of the wrapper's
706/// `error` call, its frames and the tail call.
707fn luatex_traceback(s: &str) -> String {
708    let Some(at) = s.find("\nstack traceback:\n") else {
709        return s.to_string();
710    };
711    let (head, tb) = s.split_at(at + "\nstack traceback:\n".len());
712    let lines: Vec<&str> = tb.split('\n').collect();
713    let mut out: Vec<String> = Vec::with_capacity(lines.len());
714    let mut i = 0;
715    // The message handler's own frame (`in ?`, or `in metamethod` for an
716    // error of the virtual machine).
717    if lines
718        .first()
719        .is_some_and(|l| *l == "\t[C]: in ?" || l.starts_with("\t[C]: in metamethod "))
720    {
721        i = 1;
722    }
723    while i < lines.len() {
724        let l = lines[i];
725        let next_is_wrapper = lines
726            .get(i + 1)
727            .is_some_and(|n| n.starts_with("\t[retex]:"));
728        if l == "\t[C]: in function 'error'" && next_is_wrapper {
729            let name = LAST_WRAPPED_ERROR.with(|c| c.borrow().clone());
730            out.push(match name {
731                Some(n) => format!("\t[C]: in function '{n}'"),
732                None => "\t[C]: in ?".to_string(),
733            });
734            i += 1;
735            while i < lines.len()
736                && (lines[i].starts_with("\t[retex]:") || lines[i] == "\t(...tail calls...)")
737            {
738                i += 1;
739            }
740            continue;
741        }
742        out.push(l.to_string());
743        i += 1;
744    }
745    format!("{head}{}", out.join("\n"))
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    fn arg_holds_the_command_line() {
754        let host = LuaTexHost::new().expect("host");
755        let (n, first): (i64, String) = host.lua().load("return #arg, arg[0]").eval().expect("arg");
756        assert_eq!(n as usize, std::env::args().count() - 1);
757        assert_eq!(first, std::env::args().next().unwrap());
758    }
759
760    #[test]
761    fn lpeg_is_installed() {
762        let host = LuaTexHost::new().expect("host");
763        let r: (i64, bool, String, bool) = host
764            .lua()
765            .load(
766                r#"return lpeg.match(lpeg.R"az"^1, "abc1"), package.loaded.lpeg == lpeg,
767                   lpeg.version(), package.loaded.re == nil"#,
768            )
769            .eval()
770            .expect("lpeg");
771        assert_eq!(r, (4, true, "1.0.1".to_string(), true));
772    }
773}