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