Skip to main content

retex_engine/
init.rs

1//! Engine construction, IniTeX initialization and the job driver.
2
3use std::collections::HashMap;
4use std::rc::Rc;
5
6use crate::Engine;
7use crate::cmd::Cmd;
8use crate::cond::CondState;
9use crate::eqtb::{Eqtb, Meaning};
10use crate::error::{ErrorState, Interaction, JumpOut};
11use crate::files::FileState;
12use crate::fonts::{FontInfo, FontTable};
13use crate::hash::{CsId, CsTable, NO_CS};
14use crate::input::{InputState, LineSource};
15use crate::maincontrol::Mode;
16use crate::nodes::Nodes;
17use crate::primitives_gen::{self as pg, Group};
18use crate::print::{Printer, Selector};
19use crate::save::SaveStack;
20use crate::scan::ValLevel;
21use crate::token::Token;
22
23/// Well-known control sequences.
24#[derive(Clone, Copy, Debug, Default)]
25pub struct KnownCs {
26    /// `\par`.
27    pub par: CsId,
28    /// `\inaccessible` (`frozen_protection`).
29    pub frozen_protection: CsId,
30    /// Frozen `\cr`.
31    pub frozen_cr: CsId,
32    /// Frozen `\endgroup`.
33    pub frozen_end_group: CsId,
34    /// Frozen `\right`.
35    pub frozen_right: CsId,
36    /// Frozen `\fi`.
37    pub frozen_fi: CsId,
38    /// Frozen `\endtemplate` (`end_template`).
39    pub frozen_end_template: CsId,
40    /// Frozen `\endtemplate` (`endv`).
41    pub frozen_endv: CsId,
42    /// Frozen `\relax`.
43    pub frozen_relax: CsId,
44    /// `\endwrite`.
45    pub end_write: CsId,
46    /// `\notexpanded:` (`frozen_dont_expand`).
47    pub frozen_dont_expand: CsId,
48    /// Frozen `\special`.
49    pub frozen_special: CsId,
50    /// Frozen `\nullfont`.
51    pub frozen_null_font: CsId,
52    /// The marker ending a local control loop.
53    pub end_local: CsId,
54}
55
56/// Options for a run.
57#[derive(Clone, Debug)]
58pub struct Options {
59    /// IniTeX mode.
60    pub ini: bool,
61    /// Initial interaction mode.
62    pub interaction: Interaction,
63    /// `-jobname`.
64    pub job_name: Option<String>,
65    /// `-output-directory`.
66    pub output_dir: Option<std::path::PathBuf>,
67    /// The first line: the remaining command-line arguments joined.
68    pub first_line: String,
69    /// `-file-line-error`.
70    pub file_line_error: bool,
71    /// Seconds since the epoch for `\time`, `\day`, … (from
72    /// `SOURCE_DATE_EPOCH` or the clock).
73    pub epoch: Option<i64>,
74    /// `-utc`: use UTC instead of local time for the job's date.
75    pub utc: bool,
76    /// Shell escape: 0 disabled, 1 enabled, 2 restricted to the commands
77    /// in `shell_escape_commands` (LuaTeX's `status.shell_escape`).
78    pub shell_escape: u8,
79    /// `max_print_line`, `error_line`, `half_error_line`.
80    pub line_lengths: (usize, usize, usize),
81}
82
83impl Default for Options {
84    fn default() -> Self {
85        Options {
86            ini: true,
87            interaction: Interaction::ErrorStop,
88            job_name: None,
89            output_dir: None,
90            first_line: String::new(),
91            file_line_error: false,
92            epoch: None,
93            utc: false,
94            shell_escape: 2,
95            line_lengths: (79, 79, 50),
96        }
97    }
98}
99
100/// Outcome of a run.
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum Outcome {
103    /// `\end` was reached.
104    Ended,
105    /// `\dump` was reached in IniTeX mode.
106    Dumped,
107    /// The run was aborted by a fatal error.
108    Aborted,
109}
110
111impl Engine {
112    /// A fresh IniTeX engine writing to `term`.
113    pub fn new(options: &Options, term: Box<dyn std::io::Write>) -> Engine {
114        let mut printer = Printer::new(term);
115        printer.max_print_line = options.line_lengths.0;
116        printer.error_line = options.line_lengths.1;
117        printer.half_error_line = options.line_lengths.2;
118        let mut e = Engine {
119            hash: CsTable::new(),
120            eqtb: Eqtb::initex(),
121            save: SaveStack::default(),
122            input: InputState::default(),
123            printer,
124            err: ErrorState::default(),
125            cond: CondState::default(),
126            files: FileState::default(),
127            kpse: None,
128            lua: None,
129            cur_cmd: Cmd::Relax,
130            cur_chr: 0,
131            cur_cs: NO_CS,
132            cur_tok: Token(0),
133            cur_val: 0,
134            cur_glue: crate::glue::Glue::ZERO,
135            cur_toks: None,
136            cur_val_level: ValLevel::Int,
137            cs: KnownCs::default(),
138            ini_version: options.ini,
139            shell_escape: options.shell_escape,
140            format_ident: String::new(),
141            format_name: String::new(),
142            job_name: options.job_name.clone().map(String::into_bytes),
143            after_token: Token(0),
144            expand_depth: 0,
145            mode: Mode::Vertical,
146            shown_mode: Mode::None,
147            nodes: Nodes::new(),
148            nest: Vec::new(),
149            fonts: FontTable::default(),
150            long_state: Cmd::Call,
151            def_ref_cs: NO_CS,
152            runaway_list: Vec::new(),
153            last_cs_name: NO_CS,
154            frozen_prims: HashMap::new(),
155            marks: crate::typeset::Marks::default(),
156            input_read_finished: false,
157            long_help_seen: false,
158            dead_cycles: 0,
159            insert_penalties: 0,
160            last_badness: 0,
161            single_mac_param: false,
162            pack_dir: -1,
163            random_seed: 0,
164            randoms: retex_arith::random::Randoms::new(0),
165            last_x_pos: 0,
166            last_y_pos: 0,
167            mag_set: 0,
168            font_dimen_loc: None,
169            depth_threshold: 0,
170            breadth_max: 0,
171            par_token_cs: NO_CS,
172            dump_requested: false,
173            start_time: crate::clock::StartTime::default(),
174            page: crate::typeset::PageState::default(),
175            lua_functions: HashMap::new(),
176            format_hook: None,
177            finish_hook: None,
178            deferred_writes: Vec::new(),
179            text_dirs: Vec::new(),
180            outer_text_dirs: Vec::new(),
181            pending_forms: Vec::new(),
182            form_objects: Vec::new(),
183            written_sink: crate::files::WrittenSink::default(),
184            math_glues: Vec::new(),
185            math_params_initialized: false,
186            ts: crate::typeset::TsState::default(),
187            total_pages: 0,
188            colorstack_values: HashMap::new(),
189            pdf: crate::pdfext::PdfState::default(),
190            languages: Vec::new(),
191            backend: None,
192            dvi: Default::default(),
193            local_level: 0,
194            backend_cs: HashMap::new(),
195            pending_lua_tokens: None,
196            diagnostics: None,
197            output_written: None,
198            arg_scratch: Vec::new(),
199            param_scratch: Vec::new(),
200            write_paths: Vec::new(),
201            snapshot_cs: None,
202            snapshot_hook: None,
203            images: Vec::new(),
204            last_image_index: 0,
205            last_image_pages: 0,
206            box_resources: Vec::new(),
207            last_box_index: 0,
208            input_line_for_lua: 0,
209        };
210        e.err.interaction = options.interaction;
211        e.err.file_line_error = options.file_line_error;
212        e.files.output_dir = options.output_dir.clone();
213        e.files.command_line = options.first_line.clone().into_bytes();
214        e.init_params(options.epoch, options.utc);
215        e.init_known_cs();
216        e.fonts.fonts.push(FontInfo::null());
217        e.fonts.id_cs.push(e.cs.frozen_null_font);
218        e.push_nest(Mode::Vertical);
219        e.page.init(&mut e.nodes);
220        // The contribution list is the list of the outer vertical mode.
221        let ch = e.nest[0].head;
222        e.nodes.free(e.page.contrib_head);
223        e.page.contrib_head = ch;
224        e.ts.font_in_short_display = -1;
225        if options.ini {
226            e.install_group(Group::Tex, b"");
227            e.install_group(Group::Core, b"");
228        }
229        e.err.history = crate::error::History::Spotless;
230        e
231    }
232
233    /// Sets IniTeX parameter defaults from LuaTeX's recorded values.
234    fn init_params(&mut self, epoch: Option<i64>, utc: bool) {
235        let data = include_str!("../data/luatex-initex-params.txt");
236        for line in data.lines() {
237            let mut it = line.splitn(3, ' ');
238            let (Some(cmd), Some(name), Some(value)) = (it.next(), it.next(), it.next()) else {
239                continue;
240            };
241            let Some(p) = crate::primitives::lookup(name.as_bytes()) else {
242                continue;
243            };
244            match cmd {
245                "assign_int" => {
246                    if let Ok(v) = value.parse::<i32>() {
247                        self.eqtb.int_par[p.chr as usize].val = v;
248                    }
249                }
250                "assign_dimen" => {
251                    if let Ok(v) = value.parse::<i32>() {
252                        self.eqtb.dimen_par[p.chr as usize].val = v;
253                    }
254                }
255                _ => {}
256            }
257        }
258        // Date and time (local, or UTC with `-utc`).
259        self.start_time = crate::clock::StartTime::new(epoch, utc);
260        self.set_date_and_time();
261        self.random_seed = crate::clock::startup_random_seed();
262        self.randoms.init(self.random_seed);
263    }
264
265    /// Sets `\time`, `\day`, `\month` and `\year` to the job's start
266    /// (`fix_date_and_time`), also after a format brought its own values.
267    fn set_date_and_time(&mut self) {
268        let c = self.start_time.civil();
269        use pg::assign_int as ip;
270        self.eqtb.int_par[ip::TIME as usize].val = (c.hour * 60 + c.minute) as i32;
271        self.eqtb.int_par[ip::DAY as usize].val = c.day as i32;
272        self.eqtb.int_par[ip::MONTH as usize].val = c.month as i32;
273        self.eqtb.int_par[ip::YEAR as usize].val = c.year as i32;
274    }
275
276    fn init_known_cs(&mut self) {
277        let h = &mut self.hash;
278        let par = h.lookup_or_insert(b"par");
279        let frozen = |h: &mut CsTable, n: &str| h.new_frozen(n.as_bytes());
280        let cs = KnownCs {
281            par,
282            frozen_protection: frozen(h, "inaccessible"),
283            frozen_cr: frozen(h, "cr"),
284            frozen_end_group: frozen(h, "endgroup"),
285            frozen_right: frozen(h, "right"),
286            frozen_fi: frozen(h, "fi"),
287            frozen_end_template: frozen(h, "endtemplate"),
288            frozen_endv: frozen(h, "endtemplate"),
289            frozen_relax: frozen(h, "relax"),
290            end_write: frozen(h, "endwrite"),
291            frozen_dont_expand: frozen(h, "notexpanded:"),
292            frozen_special: frozen(h, "special"),
293            frozen_null_font: frozen(h, "nullfont"),
294            end_local: frozen(h, "endlocalcontrol"),
295        };
296        self.cs = cs;
297        let set = |e: &mut Engine, id: CsId, m: Meaning| {
298            e.eqtb.ensure_cs(id);
299            e.eqtb.cs[id.0 as usize].val = m;
300            e.eqtb.cs[id.0 as usize].level = crate::eqtb::LEVEL_ONE;
301        };
302        let prim = |name: &str| {
303            let p = crate::primitives::lookup(name.as_bytes()).expect("primitive");
304            Meaning::prim(p.cmd, p.chr)
305        };
306        set(self, cs.frozen_cr, prim("cr"));
307        set(self, cs.frozen_end_group, prim("endgroup"));
308        set(self, cs.frozen_right, prim("right"));
309        set(self, cs.frozen_fi, prim("fi"));
310        set(self, cs.frozen_relax, prim("relax"));
311        set(self, cs.frozen_special, prim("special"));
312        set(self, cs.frozen_null_font, Meaning::prim(Cmd::SetFont, 0));
313        set(
314            self,
315            cs.frozen_end_template,
316            Meaning::prim(Cmd::EndTemplate, 0),
317        );
318        set(self, cs.frozen_endv, Meaning::prim(Cmd::Endv, 0));
319        set(
320            self,
321            cs.frozen_dont_expand,
322            Meaning::prim(Cmd::DontExpand, 0),
323        );
324        set(self, cs.end_write, Meaning::prim(Cmd::OuterCall, 0));
325        set(self, cs.frozen_protection, Meaning::UNDEFINED);
326        set(
327            self,
328            cs.end_local,
329            Meaning::prim(Cmd::Extension, crate::misc::ext::END_LOCAL_CONTROL),
330        );
331        self.par_token_cs = par;
332    }
333
334    /// Defines all primitives of `group` with the given name prefix.
335    pub fn install_group(&mut self, group: Group, prefix: &[u8]) {
336        let prims: Vec<_> = crate::primitives::group(group).collect();
337        for p in prims {
338            self.define_primitive(prefix, p.name.as_bytes());
339        }
340    }
341
342    /// Defines primitive `name` as `prefix name`, unless that control
343    /// sequence already has a meaning (`tex.enableprimitives`). Returns
344    /// whether it was defined.
345    pub fn define_primitive(&mut self, prefix: &[u8], name: &[u8]) -> bool {
346        let Some(p) = crate::primitives::lookup(name) else {
347            return false;
348        };
349        if p.cmd == Cmd::UndefinedCs {
350            return false;
351        }
352        let mut full = prefix.to_vec();
353        full.extend_from_slice(name);
354        let cs = self.hash.lookup_or_insert(&full);
355        if self.eqtb.meaning(cs).cmd != Cmd::UndefinedCs {
356            return false;
357        }
358        self.eqtb.ensure_cs(cs);
359        self.eqtb.cs[cs.0 as usize].val = Meaning::prim(p.cmd, p.chr);
360        self.eqtb.cs[cs.0 as usize].level = crate::eqtb::LEVEL_ONE;
361        true
362    }
363
364    /// Runs a complete job: reads the first line, executes until `\end`
365    /// or `\dump`, and cleans up.
366    pub fn run(&mut self) -> Outcome {
367        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
368            self.run_inner();
369        }));
370        match result {
371            Ok(()) => {
372                if self.dump_requested {
373                    Outcome::Dumped
374                } else {
375                    Outcome::Ended
376                }
377            }
378            Err(payload) => {
379                if payload.downcast_ref::<JumpOut>().is_some() {
380                    Outcome::Aborted
381                } else {
382                    std::panic::resume_unwind(payload);
383                }
384            }
385        }
386    }
387
388    fn run_inner(&mut self) {
389        let banner = format!(
390            "This is retex, Version {} (LuaTeX 1.22.0 compatible){}",
391            env!("CARGO_PKG_VERSION"),
392            if self.ini_version { "  (INITEX)" } else { "" }
393        );
394        self.printer.term_raw(banner.as_bytes());
395        self.printer
396            .term_raw(b"\n restricted system commands enabled.\n");
397        self.printer.term_offset = 0;
398        self.printer.update_terminal();
399        // The first line goes into a terminal level.
400        let first = self.files.command_line.clone();
401        self.begin_file_reading(LineSource::Terminal, Vec::new(), false);
402        if let Some(crate::input::Level::Text(t)) = self.input.stack.last_mut() {
403            t.buf = crate::utf8x::chars(&first);
404            t.loc = 0;
405            t.state = crate::input::LexState::NewLine;
406        }
407        let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
408        if (0..=0x10_FFFF).contains(&elc)
409            && let Some(crate::input::Level::Text(t)) = self.input.stack.last_mut()
410        {
411            t.buf.push(elc as u32);
412        }
413        self.printer.new_line_char = self.eqtb.int(pg::assign_int::NEWLINECHAR);
414        self.printer.selector = if self.err.interaction == Interaction::Batch {
415            Selector::NoPrint
416        } else {
417            Selector::TermOnly
418        };
419        let starts_with_escape = first
420            .first()
421            .is_some_and(|&b| self.eqtb.catcode(u32::from(b)) == 0);
422        if !first.is_empty() && !starts_with_escape {
423            self.start_input();
424        }
425        self.history_spotless();
426        self.main_control();
427        self.final_cleanup();
428        if self.dump_requested {
429            self.store_fmt_file();
430        }
431        self.close_files_and_terminate();
432    }
433
434    fn history_spotless(&mut self) {
435        if self.err.history == crate::error::History::FatalErrorStop {
436            self.err.history = crate::error::History::Spotless;
437        }
438    }
439
440    /// Writes the format file (`store_fmt_file`, §1302).
441    pub fn store_fmt_file(&mut self) {
442        if self.save.cur_level > crate::eqtb::LEVEL_ONE {
443            self.print_err("You can't dump inside a group");
444            self.help(&["`{...\\dump}' is a no-no."]);
445            self.fatal_error("*** (cannot \\dump inside a group)");
446        }
447        // The pre_dump callback runs first; LaTeX uses it to precompile
448        // data (e.g. Unicode tables) into lua.bytecode.
449        if let Some(h) = self.lua.clone()
450            && let Err(msg) = h.call_simple(self, "pre_dump")
451        {
452            self.lua_error(&msg);
453        }
454        let job =
455            String::from_utf8_lossy(self.job_name.as_deref().unwrap_or(b"texput")).into_owned();
456        let c = self.start_time.civil();
457        self.format_ident = format!(" (format={job} {}.{}.{})", c.year, c.month, c.day);
458        self.format_name = job.clone();
459        let mut path = std::path::PathBuf::from(format!("{job}.fmt"));
460        if let Some(dir) = &self.files.output_dir {
461            path = dir.join(path);
462        }
463        self.printer.selector = if self.err.interaction == Interaction::Batch {
464            Selector::LogOnly
465        } else {
466            Selector::TermAndLog
467        };
468        self.printer.print_nl("Beginning to dump on file ");
469        self.printer.print_bytes(path.to_string_lossy().as_bytes());
470        self.printer.print_nl("");
471        let ident = self.format_ident.clone();
472        self.printer.print_str(&ident);
473        let engine_section = self.dump_engine();
474        let lua_section = match self.lua.clone() {
475            Some(h) => h.dump(self),
476            None => Vec::new(),
477        };
478        if crate::timing::enabled() {
479            eprintln!(
480                "[format] {:>10} bytes  Lua (lua.bytecode)",
481                lua_section.len()
482            );
483        }
484        let data = crate::format::assemble(&[("engine", &engine_section), ("lua", &lua_section)]);
485        self.printer.print_nl("");
486        self.printer.print_int(self.hash.len() as i64);
487        self.printer.print_str(" multiletter control sequences");
488        self.printer.print_nl("");
489        self.printer.print_int(self.fonts.fonts.len() as i64 - 1);
490        self.printer.print_str(" preloaded fonts");
491        if let Err(e) = std::fs::write(&path, &data) {
492            self.print_err("I can't write on file `");
493            self.printer.print_bytes(path.to_string_lossy().as_bytes());
494            self.printer.print_str("'.");
495            self.help_owned(vec![e.to_string()]);
496            self.error();
497        }
498    }
499
500    /// Loads a format file into a freshly created (non-IniTeX) engine.
501    pub fn load_format(&mut self, data: &[u8]) -> Result<(), crate::format::FormatError> {
502        let sections = crate::format::disassemble(data)?;
503        crate::timing::mark("format: sections checked");
504        for (name, bytes) in &sections {
505            if *name == "engine" {
506                self.undump_engine(bytes)?;
507            }
508        }
509        // The date of the job, not of the format.
510        self.set_date_and_time();
511        crate::timing::mark("format: engine state loaded");
512        if let Some(h) = self.lua.clone() {
513            for (name, bytes) in &sections {
514                if *name == "lua" {
515                    h.undump(self, bytes)
516                        .map_err(|_| crate::format::FormatError::Corrupt("lua section"))?;
517                }
518            }
519        }
520        crate::timing::mark("format: Lua state loaded");
521        self.format_name = self
522            .format_ident
523            .trim_start_matches(" (format=")
524            .split(' ')
525            .next()
526            .unwrap_or("")
527            .to_owned();
528        Ok(())
529    }
530
531    /// Defines a macro from a string (for tests and bootstrapping).
532    pub fn define_macro_str(&mut self, name: &str, body: &str) {
533        let cs = self.hash.lookup_or_insert(name.as_bytes());
534        let mut toks = vec![Token::END_MATCH];
535        toks.extend(crate::the::str_toks(body.as_bytes()));
536        self.eqtb.ensure_cs(cs);
537        self.eqtb.cs[cs.0 as usize].val = Meaning {
538            cmd: Cmd::Call,
539            chr: 0,
540            mac: Some(Rc::new(crate::token::Macro {
541                protected: false,
542                tokens: Rc::from(toks),
543            })),
544        };
545    }
546}