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