Skip to main content

retex_engine/
lib.rs

1//! The retex TeX engine: a clean-room implementation of LuaTeX's
2//! typesetting machine.
3//!
4//! The [`Engine`] holds all state of a TeX run. Its methods are spread
5//! over modules following the parts of *TeX: The Program*:
6//!
7//! * [`print`] and [`error`] — terminal and log output, error recovery;
8//! * [`hash`], [`eqtb`], [`save`] — control sequences, equivalents, grouping;
9//! * [`input`] — the input stack and the tokenizer (`get_next`);
10//! * [`expand`], [`cond`], [`scan`] — expansion, conditionals, scanning;
11//! * [`prefixed`] — assignments;
12//! * [`maincontrol`] — the chief executive.
13
14#![forbid(unsafe_code)]
15#![allow(clippy::too_many_lines)]
16
17pub mod align;
18pub mod backend;
19mod boxnote;
20pub mod build;
21pub mod clock;
22pub mod cmd;
23mod cmd_gen;
24pub mod codes;
25pub mod cond;
26pub mod diag;
27pub mod dirs;
28pub mod display;
29pub mod dvi;
30pub mod eqtb;
31pub mod error;
32pub mod expand;
33pub mod expansion;
34pub mod files;
35pub mod fonts;
36pub mod format;
37pub mod glue;
38pub mod hash;
39pub mod hmode;
40pub mod hyph;
41pub mod image;
42pub mod init;
43pub mod input;
44pub mod linebreak;
45pub mod macroprof;
46pub mod maincontrol;
47pub mod math;
48pub mod mathdisplay;
49pub mod mathparams;
50pub mod meaning;
51pub mod misc;
52pub mod mlist;
53pub mod native;
54pub mod nodeproc;
55pub mod nodes;
56pub mod pack;
57pub mod page;
58pub mod pagenote;
59pub mod pdfext;
60pub(crate) mod pdfship;
61pub mod prefixed;
62pub mod primitives;
63pub mod primitives_gen;
64pub mod print;
65pub mod protrusion;
66pub mod save;
67pub mod scan;
68pub mod shipout;
69pub mod showbox;
70pub mod snapshot;
71pub mod synctex;
72pub mod the;
73pub mod timing;
74pub mod token;
75pub mod typeset;
76pub mod utf8x;
77
78use cmd::Cmd;
79use cond::CondState;
80use eqtb::Eqtb;
81use error::ErrorState;
82use files::FileState;
83use hash::{CsId, CsTable};
84use input::InputState;
85use print::Printer;
86use save::SaveStack;
87use token::Token;
88
89/// Callbacks from the engine into an embedded Lua interpreter.
90///
91/// The engine does not depend on Lua. `\directlua` and related primitives
92/// call this trait, which `retex-lua` implements. The host is shared
93/// (`Rc`), so nested calls (Lua → TeX → Lua) reach it too.
94pub trait LuaHost {
95    /// Runs a chunk of Lua code for `\directlua` (or `\latelua`, etc.).
96    /// `name` is the chunk name. Text printed with `tex.print` must be
97    /// pushed back into the engine's input by the host.
98    fn run(&self, engine: &mut Engine, name: &str, code: &[u8]) -> Result<(), String>;
99
100    /// Data to store in the format file (e.g. `lua.bytecode`).
101    fn dump(&self, _engine: &mut Engine) -> Vec<u8> {
102        Vec::new()
103    }
104
105    /// Restores data stored by [`dump`](Self::dump).
106    fn undump(&self, _engine: &mut Engine, _data: &[u8]) -> Result<(), String> {
107        Ok(())
108    }
109
110    /// Calls callback `name` without arguments, ignoring its results
111    /// (`pre_dump`, `stop_run`, …). Does nothing if none is registered.
112    fn call_simple(&self, _engine: &mut Engine, _name: &str) -> Result<(), String> {
113        Ok(())
114    }
115
116    /// Calls callback `name` with one string argument, ignoring its
117    /// results (`buildpage_filter`, `contribute_filter`).
118    fn call_with_string(
119        &self,
120        _engine: &mut Engine,
121        _name: &str,
122        _arg: &str,
123    ) -> Result<(), String> {
124        Ok(())
125    }
126
127    /// Whether a Lua function is registered for callback `name`.
128    fn has_callback(&self, _name: &str) -> bool {
129        false
130    }
131
132    /// Calls the `define_font` callback. Returns the id of the font it
133    /// defined, or `None` if the font could not be loaded.
134    fn define_font(&self, _engine: &mut Engine, _name: &[u8], _size: i32) -> Option<u32> {
135        None
136    }
137
138    /// Calls a node-list filter callback (`pre_linebreak_filter`,
139    /// `hpack_filter`, `post_linebreak_filter`, …) on the list starting at
140    /// `head`. Returns the new head; [`ListFilter::Flush`] means the list
141    /// must be discarded.
142    fn list_filter(
143        &self,
144        _engine: &mut Engine,
145        _callback: &str,
146        head: nodes::NodeRef,
147        _group: &str,
148    ) -> ListFilter {
149        ListFilter::Keep(head)
150    }
151
152    /// Calls the `hpack_quality` or `vpack_quality` callback `cb` for box
153    /// `b` with `incident` (overfull, underfull, loose, tight) and its
154    /// detail; returns the node the callback gave (null if none).
155    #[allow(clippy::too_many_arguments)]
156    fn pack_quality(
157        &self,
158        _engine: &mut Engine,
159        _cb: &str,
160        _incident: &str,
161        _detail: i32,
162        _b: nodes::NodeRef,
163        _first: i32,
164        _last: i32,
165    ) -> nodes::NodeRef {
166        nodes::NodeRef::NULL
167    }
168
169    /// Runs the `hyphenate`, `ligaturing` or `kerning` callback `cb` on
170    /// the list after `head` (a temporary node), with its last node:
171    /// `true` if a function or `false` is registered, which replaces the
172    /// built-in pass.
173    fn node_pass(&self, _engine: &mut Engine, _cb: &str, _head: nodes::NodeRef) -> bool {
174        false
175    }
176
177    /// Calls the `linebreak_filter` callback on paragraph `head`
178    /// (`display`: a display follows): `Some(list)` if it returned a node
179    /// list, which replaces line breaking; `None` otherwise.
180    fn linebreak_filter(
181        &self,
182        _engine: &mut Engine,
183        _head: nodes::NodeRef,
184        _display: bool,
185    ) -> Option<nodes::NodeRef> {
186        None
187    }
188
189    /// Calls the `mlist_to_hlist` callback on math list `head` in style
190    /// `style` (0–7); `None` if the callback failed or none is set.
191    fn mlist_to_hlist(
192        &self,
193        _engine: &mut Engine,
194        _head: nodes::NodeRef,
195        _style: i32,
196        _penalties: bool,
197    ) -> Option<nodes::NodeRef> {
198        None
199    }
200
201    /// Runs shell command `cmd` under the shell escape rules (as Lua's
202    /// `io.popen` does) and returns its output; `None` if it may not run.
203    fn pipe_read(&self, _engine: &mut Engine, _cmd: &[u8]) -> Option<Vec<u8>> {
204        None
205    }
206
207    /// Calls the `input_level_string` callback with input-stack depth `n`;
208    /// `None` if it returned no string.
209    fn input_level_string(&self, _engine: &mut Engine, _n: i64) -> Option<Vec<u8>> {
210        None
211    }
212
213    /// Calls the `glyph_info` callback for glyph node `p`: `Some(text)` is
214    /// what to print, `None` means the callback returned no string.
215    fn glyph_info(&self, _engine: &mut Engine, _p: nodes::NodeRef) -> Option<Vec<u8>> {
216        None
217    }
218}
219
220/// Result of a node-list filter callback.
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum ListFilter {
223    /// Continue with this list (possibly the original head).
224    Keep(nodes::NodeRef),
225    /// The callback returned `false`: discard the list.
226    Flush,
227}
228
229impl Engine {
230    /// Whether the Lua callback `name` is set.
231    pub fn has_callback(&self, name: &str) -> bool {
232        self.lua.as_ref().is_some_and(|h| h.has_callback(name))
233    }
234
235    /// Calls the notification callback `name` (`buildpage_filter`,
236    /// `contribute_filter`) with `info`, if one is set.
237    pub fn notify_callback(&mut self, name: &str, info: &str) {
238        if let Some(h) = self.lua.clone()
239            && h.has_callback(name)
240            && let Err(e) = h.call_with_string(self, name, info)
241        {
242            self.lua_error(&e);
243        }
244    }
245
246    /// Calls `buildpage_filter` with `info` (where the page builder was
247    /// called from, observed), then builds the page.
248    pub fn build_page_from(&mut self, info: &str) {
249        self.notify_callback("buildpage_filter", info);
250        self.build_page();
251    }
252
253    /// Runs a node-list filter callback if one is registered (the hook
254    /// typesetting code calls at LuaTeX's callback points).
255    pub fn run_list_filter(
256        &mut self,
257        callback: &str,
258        head: nodes::NodeRef,
259        group: &str,
260    ) -> ListFilter {
261        match self.lua.clone() {
262            Some(h) if h.has_callback(callback) => h.list_filter(self, callback, head, group),
263            _ => ListFilter::Keep(head),
264        }
265    }
266}
267
268impl Engine {
269    /// Converts a finished formula's math list into an hlist, through the
270    /// `mlist_to_hlist` callback when one is set (LuaTeX calls it for the
271    /// outermost list of each formula and equation number; observed: not
272    /// for an empty one).
273    pub fn formula_to_hlist(
274        &mut self,
275        head: nodes::NodeRef,
276        style: i32,
277        penalties: bool,
278    ) -> nodes::NodeRef {
279        if head.is_null() {
280            return nodes::NodeRef::NULL;
281        }
282        if let Some(h) = self.lua.clone()
283            && h.has_callback("mlist_to_hlist")
284        {
285            if let Some(r) = h.mlist_to_hlist(self, head, style, penalties) {
286                if !r.is_null() {
287                    self.nodes.set_prev(r, nodes::NodeRef::NULL);
288                }
289                return r;
290            }
291            return nodes::NodeRef::NULL;
292        }
293        self.mlist_to_hlist(head, style, penalties)
294    }
295
296    /// Prints the input level before a line of macro tracing through the
297    /// `input_level_string` callback (LaTeX's `\tracingstacklevels`);
298    /// returns false if no callback is set. A newline in the string ends
299    /// the line.
300    pub fn print_input_level(&mut self) -> bool {
301        let Some(h) = self.lua.clone() else {
302            return false;
303        };
304        if !h.has_callback("input_level_string") {
305            return false;
306        }
307        let n = self.input.stack.len().saturating_sub(1) as i64;
308        if let Some(s) = h.input_level_string(self, n) {
309            for (i, part) in s.split(|&b| b == b'\n').enumerate() {
310                if i > 0 {
311                    self.printer.print_ln();
312                }
313                self.printer.print_bytes(part);
314            }
315        }
316        true
317    }
318
319    /// Runs the `glyph_info` callback for glyph `p` when one is set:
320    /// `None` without a callback, `Some(None)` when it returned no string.
321    pub fn run_glyph_info(&mut self, p: nodes::NodeRef) -> Option<Option<Vec<u8>>> {
322        match self.lua.clone() {
323            Some(h) if h.has_callback("glyph_info") => Some(h.glyph_info(self, p)),
324            _ => None,
325        }
326    }
327}
328
329/// A one-shot callback into the host, run with the engine borrowed.
330pub type EngineHook = Box<dyn FnOnce(&mut Engine)>;
331
332/// Shows diagnostics before the terminal is read (see
333/// [`Engine::prompt_hook`]).
334pub type PromptHook = Box<dyn FnMut(&[diag::Diagnostic], &str) -> bool>;
335
336/// The global state of a TeX run.
337pub struct Engine {
338    /// Control-sequence names.
339    pub hash: CsTable,
340    /// The table of equivalents.
341    pub eqtb: Eqtb,
342    /// The save stack and grouping state.
343    pub save: SaveStack,
344    /// The input stack.
345    pub input: InputState,
346    /// Terminal, log and `\write` output.
347    pub printer: Printer,
348    /// Error-handling state.
349    pub err: ErrorState,
350    /// Conditionals.
351    pub cond: CondState,
352    /// Files: `\openin`/`\read`, `\openout`, input file stack.
353    pub files: FileState,
354    /// The file finder.
355    pub kpse: Option<retex_kpath::Shared>,
356    /// The Lua interpreter, when available.
357    pub lua: Option<std::rc::Rc<dyn LuaHost>>,
358
359    /// The current command code (`cur_cmd`).
360    pub cur_cmd: Cmd,
361    /// The current modifier (`cur_chr`).
362    pub cur_chr: u32,
363    /// The current control sequence (`cur_cs`); [`hash::NO_CS`] for
364    /// characters.
365    pub cur_cs: CsId,
366    /// The current token (`cur_tok`).
367    pub cur_tok: Token,
368
369    /// The value most recently scanned (`cur_val`).
370    pub cur_val: i32,
371    /// The glue most recently scanned, when `cur_val_level` is glue.
372    pub cur_glue: glue::Glue,
373    /// The token list most recently scanned, when `cur_val_level` is tokens.
374    pub cur_toks: Option<token::TokenList>,
375    /// The level of `cur_val`.
376    pub cur_val_level: scan::ValLevel,
377
378    /// Well-known control sequences.
379    pub cs: init::KnownCs,
380    /// Whether the run is in IniTeX mode.
381    pub ini_version: bool,
382    /// Shell escape mode (see [`init::Options::shell_escape`]).
383    pub shell_escape: u8,
384    /// The format identification printed in the banner.
385    pub format_ident: String,
386    /// The job name, once known.
387    pub job_name: Option<Vec<u8>>,
388    /// Token saved by `\afterassignment` (`after_token`).
389    pub after_token: Token,
390    /// Number of nested expansions (LuaTeX's `expand_depth_count`).
391    pub expand_depth: u32,
392    /// The current mode (copy of the innermost nest level's mode).
393    pub mode: maincontrol::Mode,
394    /// The mode last shown by `show_cur_cmd_chr`.
395    pub shown_mode: maincontrol::Mode,
396    /// The format name (`\formatname`).
397    pub format_name: String,
398    /// Math as LuaTeX 1.24 (TeX Live 2026) typesets it, chosen for formats
399    /// of LaTeX 2026-06 and later (see [`Engine::load_format`]); else
400    /// LuaTeX 1.22 (TeX Live 2025), the version retex reports.
401    pub math_124: bool,
402    /// Building an operator's character nucleus, which gets no italic
403    /// correction in OpenType math (LuaTeX 1.24 as 1.22).
404    pub math_op_nucleus: bool,
405    /// Node memory.
406    pub nodes: nodes::Nodes,
407    /// The semantic nest; the last entry is the current list.
408    pub nest: Vec<maincontrol::ListState>,
409    /// Loaded fonts.
410    pub fonts: fonts::FontTable,
411    /// The `long_state` of the macro being called.
412    pub long_state: Cmd,
413    /// The control sequence being defined (for diagnostics).
414    pub def_ref_cs: CsId,
415    /// The partial token list printed by `runaway`.
416    pub runaway_list: Vec<Token>,
417    /// The control sequence last made by `\csname` (`\lastnamedcs`).
418    pub last_cs_name: CsId,
419    /// Frozen copies made by `\primitive`.
420    pub frozen_prims: std::collections::HashMap<Vec<u8>, CsId>,
421    /// Mark texts.
422    pub marks: typeset::Marks,
423    /// Set when a `\read` line has been consumed.
424    pub input_read_finished: bool,
425    /// Whether the long `\errmessage` help has been shown.
426    pub long_help_seen: bool,
427    /// `\deadcycles`.
428    pub dead_cycles: i32,
429    /// `\insertpenalties`.
430    pub insert_penalties: i32,
431    /// `\badness`.
432    pub last_badness: i32,
433    /// Token lists show a macro parameter character once, not doubled
434    /// (`\luaescapestring`).
435    pub single_mac_param: bool,
436    /// The direction the next box made by `hpack` or `vpackage` gets
437    /// (from `dir`/`bdir` of a box specification), or -1. It is set before
438    /// packing because over- and underfull box reports show it.
439    pub pack_dir: i8,
440    /// `\randomseed`: the seed `\setrandomseed` last gave (its absolute
441    /// value), or the one chosen at startup.
442    pub random_seed: i32,
443    /// The state of the random number generator (METAFONT's, see
444    /// [`retex_arith::random`]) behind `\uniformdeviate`, `\normaldeviate`
445    /// and the `tex` library's randomizers.
446    pub randoms: retex_arith::random::Randoms,
447    /// `\lastxpos`.
448    pub last_x_pos: i32,
449    /// `\lastypos`.
450    pub last_y_pos: i32,
451    /// The magnification frozen by `prepare_mag`.
452    pub mag_set: i32,
453    /// The font dimension located by `find_font_dimen`.
454    pub font_dimen_loc: Option<(u32, u32)>,
455    /// `\showboxdepth` in effect while displaying boxes.
456    pub depth_threshold: i32,
457    /// `\showboxbreadth` in effect while displaying boxes.
458    pub breadth_max: i32,
459    /// The paragraph-ending control sequence (`\partokenname`).
460    pub par_token_cs: CsId,
461    /// Whether `\dump` ended the run.
462    pub dump_requested: bool,
463    /// Whether a macro expanded now would have its body executed by main
464    /// control (see [`native`]).
465    pub exec_context: bool,
466    /// Whether the next [`expand`](Engine::expand) is a single expansion
467    /// step whose result is not expanded further (`\expandafter`, `\span`
468    /// in a preamble, Lua's `token.expand`): expandable native
469    /// implementations do not run there (see [`native`]). `expand` clears
470    /// it on entry.
471    pub one_step: bool,
472    /// The macro profiler (`RETEX_MACRO_PROFILE`).
473    pub macro_profile: Option<Box<macroprof::MacroProfile>>,
474    /// Integer parameters set from the command line when the job starts
475    /// ([`init::Options::int_params`]).
476    pub start_ints: Vec<(u32, i32)>,
477    /// `-output-comment`.
478    pub output_comment: Option<String>,
479    /// The job's start time (`\time`, the banner, the PDF creation date).
480    pub start_time: clock::StartTime,
481    /// The page builder.
482    pub page: typeset::PageState,
483    /// Lua functions registered for `\luafunction` (by number).
484    pub lua_functions: std::collections::HashMap<i32, u32>,
485    /// Called to store the format when `\dump` is executed.
486    pub format_hook: Option<EngineHook>,
487    /// Called at the end of the run to finish output files.
488    pub finish_hook: Option<EngineHook>,
489    /// Non-immediate `\write`s waiting for shipout.
490    pub deferred_writes: Vec<(i32, token::TokenList)>,
491    /// Text directions opened in the current box: (group level,
492    /// direction, the text direction before), innermost last (see
493    /// [`dirs`]).
494    pub text_dirs: Vec<(u16, i8, i8)>,
495    /// Box resources to write after the page or form being written, one
496    /// list per open page or form.
497    pub pending_forms: Vec<Vec<u32>>,
498    /// Objects referred to (`\pdfextension refobj`) in the box resources
499    /// being written, one list per open form.
500    pub form_objects: Vec<Vec<pdfext::PdfObject>>,
501    /// The `text_dirs` of the enclosing boxes.
502    pub outer_text_dirs: Vec<Vec<(u16, i8, i8)>>,
503    /// The math and text directions outside the open formulas, with the
504    /// save level of each formula's group.
505    pub outer_math_dirs: Vec<(u16, i32, i32)>,
506    /// Contents of files written by `\write`, for the pass driver.
507    pub written_sink: files::WrittenSink,
508    /// State of the typesetting routines.
509    pub ts: typeset::TsState,
510    /// Tokens printed from Lua waiting to be pushed as a token list.
511    pub pending_lua_tokens: Option<Vec<Token>>,
512    /// Default output mode: errors and warnings recorded for the driver
513    /// (`None` in compatibility mode).
514    pub diagnostics: Option<Vec<diag::Diagnostic>>,
515    /// Default output mode, interactive: shows the diagnostics recorded
516    /// since the last prompt, and the prompt, before the terminal is read.
517    /// Returns `false` if it showed nothing (the error only repeats one
518    /// already reported): TeX then goes on without asking.
519    pub prompt_hook: Option<PromptHook>,
520    /// Default output mode: called when a page is shipped or a file opened,
521    /// to show progress and the errors recorded so far.
522    pub progress_hook: Option<fn(&mut Engine)>,
523    /// The number of diagnostics the prompt hook has shown.
524    pub diagnostics_shown: usize,
525    /// Whether the terminal was read (an error or a `\read` prompt).
526    pub prompted: bool,
527    /// Image resources (`\saveimageresource`), in the order saved (their
528    /// indices are their object numbers).
529    pub images: Vec<image::ImageResource>,
530    /// `\lastsavedimageresourceindex`.
531    pub last_image_index: i32,
532    /// `\lastsavedimageresourcepages`.
533    pub last_image_pages: i32,
534    /// Whether SVG text has been given its font finder.
535    pub svg_fonts_set: bool,
536    /// Box resources (`\saveboxresource`), in the order saved (their
537    /// indices are their object numbers).
538    pub box_resources: Vec<image::BoxResource>,
539    /// `\lastsavedboxresourceindex`.
540    pub last_box_index: i32,
541    /// Scratch buffer for macro arguments (reused by `macro_call`).
542    pub arg_scratch: Vec<Token>,
543    /// Scratch buffer for the argument list of a macro call.
544    pub param_scratch: Vec<token::TokenList>,
545    /// The file of each `\write` stream opened (see [`snapshot`]).
546    pub write_paths: Vec<Option<std::path::PathBuf>>,
547    /// The macro at whose expansion [`snapshot_hook`](Self::snapshot_hook)
548    /// runs (LaTeX's `\document`).
549    pub snapshot_cs: Option<CsId>,
550    /// Called once, when `snapshot_cs` is expanded (see [`snapshot`]).
551    pub snapshot_hook: Option<snapshot::Hook>,
552    /// The file and line the user asked to edit at an error (`E`); the
553    /// program runs the editor at the end.
554    pub edit_request: Option<(Vec<u8>, i32)>,
555    /// The output file written at the end: name, pages and bytes.
556    pub output_written: Option<(String, u32, u64)>,
557    /// The line number reported while reading Lua output.
558    pub input_line_for_lua: i32,
559    /// Internal tokens made by `\pdfvariable`, by variable name.
560    pub backend_cs: std::collections::HashMap<&'static str, CsId>,
561    /// Nesting depth of local control loops (`tex.runtoks`).
562    pub local_level: u32,
563    /// The box `token.scan_list` scanned, once finished.
564    pub lua_scanned_box: Option<nodes::NodeRef>,
565    /// Whether `token.scan_list` runs local control for its box.
566    pub lua_scan_waiting: bool,
567    /// The output backend (PDF writer), if any.
568    pub backend: Option<Box<dyn shipout::Backend>>,
569    /// The DVI writer (`\outputmode=0`).
570    pub dvi: dvi::DviWriter,
571    /// Hyphenation languages, by `\language` number.
572    pub languages: Vec<Option<retex_hyph::Language>>,
573    /// Document-level PDF data from `\pdfextension`.
574    pub pdf: pdfext::PdfState,
575    /// Current values of the colour stacks during shipout.
576    pub colorstack_values: std::collections::HashMap<i32, Vec<Vec<u8>>>,
577    /// Number of pages shipped out.
578    pub total_pages: u32,
579    /// Muglue values of `\Umath…` spacing parameters.
580    pub math_glues: Vec<glue::Glue>,
581    /// Whether the parameters set by the first formula are initialized
582    /// (see [`mathparams`]).
583    pub math_params_initialized: bool,
584    /// SyncTeX output.
585    pub synctex: synctex::Synctex,
586}
587
588impl std::fmt::Debug for Engine {
589    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590        f.debug_struct("Engine")
591            .field("cur_cmd", &self.cur_cmd)
592            .field("cur_chr", &self.cur_chr)
593            .finish_non_exhaustive()
594    }
595}