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