1use 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#[derive(Clone, Copy, Debug, Default)]
25pub struct KnownCs {
26 pub par: CsId,
28 pub frozen_protection: CsId,
30 pub frozen_cr: CsId,
32 pub frozen_end_group: CsId,
34 pub frozen_right: CsId,
36 pub frozen_fi: CsId,
38 pub frozen_end_template: CsId,
40 pub frozen_endv: CsId,
42 pub frozen_relax: CsId,
44 pub end_write: CsId,
46 pub frozen_dont_expand: CsId,
48 pub frozen_special: CsId,
50 pub frozen_null_font: CsId,
52 pub end_local: CsId,
54}
55
56#[derive(Clone, Debug)]
58pub struct Options {
59 pub ini: bool,
61 pub interaction: Interaction,
63 pub job_name: Option<String>,
65 pub output_dir: Option<std::path::PathBuf>,
67 pub first_line: String,
69 pub file_line_error: bool,
71 pub epoch: Option<i64>,
74 pub utc: bool,
76 pub shell_escape: u8,
79 pub line_lengths: (usize, usize, usize),
81 pub halt_on_error: bool,
83 pub output_comment: Option<String>,
86 pub int_params: Vec<(u32, i32)>,
90}
91
92impl Default for Options {
93 fn default() -> Self {
94 Options {
95 ini: true,
96 interaction: Interaction::ErrorStop,
97 job_name: None,
98 output_dir: None,
99 first_line: String::new(),
100 file_line_error: false,
101 epoch: None,
102 utc: false,
103 shell_escape: 2,
104 line_lengths: (79, 79, 50),
105 halt_on_error: false,
106 output_comment: None,
107 int_params: Vec::new(),
108 }
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub enum Outcome {
115 Ended,
117 Dumped,
119 Aborted,
121}
122
123impl Engine {
124 pub fn new(options: &Options, term: Box<dyn std::io::Write>) -> Engine {
126 let mut printer = Printer::new(term);
127 printer.max_print_line = options.line_lengths.0;
128 printer.error_line = options.line_lengths.1;
129 printer.half_error_line = options.line_lengths.2;
130 let mut e = Engine {
131 hash: CsTable::new(),
132 eqtb: Eqtb::initex(),
133 save: SaveStack::default(),
134 input: InputState::default(),
135 printer,
136 err: ErrorState::default(),
137 cond: CondState::default(),
138 files: FileState::default(),
139 kpse: None,
140 lua: None,
141 cur_cmd: Cmd::Relax,
142 cur_chr: 0,
143 cur_cs: NO_CS,
144 cur_tok: Token(0),
145 cur_val: 0,
146 cur_glue: crate::glue::Glue::ZERO,
147 cur_toks: None,
148 cur_val_level: ValLevel::Int,
149 cs: KnownCs::default(),
150 ini_version: options.ini,
151 shell_escape: options.shell_escape,
152 format_ident: String::new(),
153 format_name: String::new(),
154 math_124: false,
155 math_op_nucleus: false,
156 job_name: options.job_name.clone().map(String::into_bytes),
157 after_token: Token(0),
158 expand_depth: 0,
159 mode: Mode::Vertical,
160 shown_mode: Mode::None,
161 nodes: Nodes::new(),
162 nest: Vec::new(),
163 fonts: FontTable::default(),
164 long_state: Cmd::Call,
165 def_ref_cs: NO_CS,
166 runaway_list: Vec::new(),
167 last_cs_name: NO_CS,
168 frozen_prims: HashMap::new(),
169 marks: crate::typeset::Marks::default(),
170 input_read_finished: false,
171 long_help_seen: false,
172 dead_cycles: 0,
173 insert_penalties: 0,
174 last_badness: 0,
175 single_mac_param: false,
176 pack_dir: -1,
177 random_seed: 0,
178 randoms: retex_arith::random::Randoms::new(0),
179 last_x_pos: 0,
180 last_y_pos: 0,
181 mag_set: 0,
182 font_dimen_loc: None,
183 depth_threshold: 0,
184 breadth_max: 0,
185 par_token_cs: NO_CS,
186 dump_requested: false,
187 exec_context: false,
188 one_step: false,
189 origins: None,
190 profiler: None,
191 start_ints: options.int_params.clone(),
192 output_comment: options.output_comment.clone(),
193 start_time: crate::clock::StartTime::default(),
194 page: crate::typeset::PageState::default(),
195 lua_functions: HashMap::new(),
196 format_hook: None,
197 finish_hook: None,
198 deferred_writes: Vec::new(),
199 text_dirs: Vec::new(),
200 outer_text_dirs: Vec::new(),
201 outer_math_dirs: Vec::new(),
202 pending_forms: Vec::new(),
203 vf_frames: Vec::new(),
204 form_objects: Vec::new(),
205 written_sink: crate::files::WrittenSink::default(),
206 math_glues: Vec::new(),
207 math_params_initialized: false,
208 synctex: Default::default(),
209 ts: crate::typeset::TsState::default(),
210 total_pages: 0,
211 colorstack_values: HashMap::new(),
212 pdf: crate::pdfext::PdfState::default(),
213 languages: Vec::new(),
214 backend: None,
215 dvi: Default::default(),
216 local_level: 0,
217 lua_scanned_box: None,
218 lua_scan_waiting: false,
219 backend_cs: HashMap::new(),
220 pending_lua_tokens: None,
221 diagnostics: None,
222 prompt_hook: None,
223 progress_hook: None,
224 diagnostics_shown: 0,
225 prompted: false,
226 output_written: None,
227 arg_scratch: Vec::new(),
228 param_scratch: Vec::new(),
229 write_paths: Vec::new(),
230 snapshot_cs: None,
231 snapshot_hook: None,
232 page_hook: None,
233 final_hook: None,
234 begun_level: None,
235 fp_cache: None,
236 edit_request: None,
237 images: Vec::new(),
238 last_image_index: 0,
239 last_image_pages: 0,
240 svg_fonts_set: false,
241 box_resources: Vec::new(),
242 last_box_index: 0,
243 input_line_for_lua: 0,
244 };
245 e.err.interaction = options.interaction;
246 e.err.file_line_error = options.file_line_error;
247 e.err.halt_on_error = options.halt_on_error;
248 e.files.output_dir = options.output_dir.clone();
249 e.files.command_line = options.first_line.clone().into_bytes();
250 e.init_params(options.epoch, options.utc);
251 e.init_known_cs();
252 e.fonts.fonts.push(FontInfo::null());
253 e.fonts.id_cs.push(e.cs.frozen_null_font);
254 e.push_nest(Mode::Vertical);
255 e.page.init(&mut e.nodes);
256 let ch = e.nest[0].head;
258 e.nodes.free(e.page.contrib_head);
259 e.page.contrib_head = ch;
260 e.ts.font_in_short_display = -1;
261 if options.ini {
262 e.install_group(Group::Tex, b"");
263 e.install_group(Group::Core, b"");
264 }
265 e.err.history = crate::error::History::Spotless;
266 e
267 }
268
269 fn init_params(&mut self, epoch: Option<i64>, utc: bool) {
271 let data = include_str!("../data/luatex-initex-params.txt");
272 for line in data.lines() {
273 let mut it = line.splitn(3, ' ');
274 let (Some(cmd), Some(name), Some(value)) = (it.next(), it.next(), it.next()) else {
275 continue;
276 };
277 let Some(p) = crate::primitives::lookup(name.as_bytes()) else {
278 continue;
279 };
280 match cmd {
281 "assign_int" => {
282 if let Ok(v) = value.parse::<i32>() {
283 self.eqtb.int_par[p.chr as usize].val = v;
284 }
285 }
286 "assign_dimen" => {
287 if let Ok(v) = value.parse::<i32>() {
288 self.eqtb.dimen_par[p.chr as usize].val = v;
289 }
290 }
291 _ => {}
292 }
293 }
294 self.start_time = crate::clock::StartTime::new(epoch, utc);
296 self.set_date_and_time();
297 self.random_seed = crate::clock::startup_random_seed();
298 self.randoms.init(self.random_seed);
299 }
300
301 fn set_date_and_time(&mut self) {
304 let c = self.start_time.civil();
305 use pg::assign_int as ip;
306 self.eqtb.int_par[ip::TIME as usize].val = (c.hour * 60 + c.minute) as i32;
307 self.eqtb.int_par[ip::DAY as usize].val = c.day as i32;
308 self.eqtb.int_par[ip::MONTH as usize].val = c.month as i32;
309 self.eqtb.int_par[ip::YEAR as usize].val = c.year as i32;
310 }
311
312 fn init_known_cs(&mut self) {
313 let h = &mut self.hash;
314 let par = h.lookup_or_insert(b"par");
315 let frozen = |h: &mut CsTable, n: &str| h.new_frozen(n.as_bytes());
316 let cs = KnownCs {
317 par,
318 frozen_protection: frozen(h, "inaccessible"),
319 frozen_cr: frozen(h, "cr"),
320 frozen_end_group: frozen(h, "endgroup"),
321 frozen_right: frozen(h, "right"),
322 frozen_fi: frozen(h, "fi"),
323 frozen_end_template: frozen(h, "endtemplate"),
324 frozen_endv: frozen(h, "endtemplate"),
325 frozen_relax: frozen(h, "relax"),
326 end_write: frozen(h, "endwrite"),
327 frozen_dont_expand: frozen(h, "notexpanded:"),
328 frozen_special: frozen(h, "special"),
329 frozen_null_font: frozen(h, "nullfont"),
330 end_local: frozen(h, "endlocalcontrol"),
331 };
332 self.cs = cs;
333 let set = |e: &mut Engine, id: CsId, m: Meaning| {
334 e.eqtb.ensure_cs(id);
335 e.eqtb.cs[id.0 as usize].val = m;
336 e.eqtb.cs[id.0 as usize].level = crate::eqtb::LEVEL_ONE;
337 };
338 let prim = |name: &str| {
339 let p = crate::primitives::lookup(name.as_bytes()).expect("primitive");
340 Meaning::prim(p.cmd, p.chr)
341 };
342 set(self, cs.frozen_cr, prim("cr"));
343 set(self, cs.frozen_end_group, prim("endgroup"));
344 set(self, cs.frozen_right, prim("Umiddle"));
348 set(self, cs.frozen_fi, prim("fi"));
349 set(self, cs.frozen_relax, prim("relax"));
350 set(self, cs.frozen_special, prim("special"));
351 set(self, cs.frozen_null_font, Meaning::prim(Cmd::SetFont, 0));
352 set(
353 self,
354 cs.frozen_end_template,
355 Meaning::prim(Cmd::EndTemplate, 0),
356 );
357 set(self, cs.frozen_endv, Meaning::prim(Cmd::Endv, 0));
358 set(
359 self,
360 cs.frozen_dont_expand,
361 Meaning::prim(Cmd::DontExpand, 0),
362 );
363 set(self, cs.end_write, Meaning::prim(Cmd::OuterCall, 0));
364 set(self, cs.frozen_protection, Meaning::UNDEFINED);
365 set(
366 self,
367 cs.end_local,
368 Meaning::prim(Cmd::Extension, crate::misc::ext::END_LOCAL_CONTROL),
369 );
370 self.par_token_cs = par;
371 }
372
373 pub fn install_group(&mut self, group: Group, prefix: &[u8]) {
375 let prims: Vec<_> = crate::primitives::group(group).collect();
376 for p in prims {
377 self.define_primitive(prefix, p.name.as_bytes());
378 }
379 }
380
381 pub fn define_primitive(&mut self, prefix: &[u8], name: &[u8]) -> bool {
385 let Some(p) = crate::primitives::lookup(name) else {
386 return false;
387 };
388 if p.cmd == Cmd::UndefinedCs {
389 return false;
390 }
391 let mut full = prefix.to_vec();
392 full.extend_from_slice(name);
393 let cs = self.hash.lookup_or_insert(&full);
394 if self.eqtb.meaning(cs).cmd != Cmd::UndefinedCs {
395 return false;
396 }
397 self.eqtb.ensure_cs(cs);
398 self.eqtb.cs[cs.0 as usize].val = Meaning::prim(p.cmd, p.chr);
399 self.eqtb.cs[cs.0 as usize].level = crate::eqtb::LEVEL_ONE;
400 true
401 }
402
403 pub fn run(&mut self) -> Outcome {
406 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
407 self.run_inner();
408 }));
409 match result {
410 Ok(()) => {
411 if self.dump_requested {
412 Outcome::Dumped
413 } else {
414 Outcome::Ended
415 }
416 }
417 Err(payload) => {
418 if payload.downcast_ref::<JumpOut>().is_some() {
419 Outcome::Aborted
420 } else {
421 std::panic::resume_unwind(payload);
422 }
423 }
424 }
425 }
426
427 fn run_inner(&mut self) {
428 for (i, v) in std::mem::take(&mut self.start_ints) {
429 self.eqtb.set(
430 crate::eqtb::Loc::IntPar(i),
431 crate::eqtb::Val::Int(i64::from(v)),
432 crate::eqtb::LEVEL_ONE,
433 );
434 if i == crate::primitives_gen::assign_int::SYNCTEX {
435 self.synctex.option = Some(v);
436 }
437 }
438 self.synctex_changed();
439 let banner = format!(
440 "This is retex, Version {} (LuaTeX 1.22.0 compatible){}",
441 env!("CARGO_PKG_VERSION"),
442 if self.ini_version { " (INITEX)" } else { "" }
443 );
444 self.printer.term_raw(banner.as_bytes());
445 self.printer.term_raw(b"\n");
446 let shell = self.shell_escape_banner();
447 if !shell.is_empty() {
448 self.printer.term_raw(shell.as_bytes());
449 self.printer.term_raw(b"\n");
450 }
451 self.printer.term_offset = 0;
452 self.printer.update_terminal();
453 let first = self.files.command_line.clone();
455 self.begin_file_reading(LineSource::Terminal, Vec::new(), false);
456 if let Some(crate::input::Level::Text(t)) = self.input.stack.last_mut() {
457 t.buf = crate::utf8x::chars(&first);
458 t.loc = 0;
459 t.state = crate::input::LexState::NewLine;
460 }
461 let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
462 if (0..=0x10_FFFF).contains(&elc)
463 && let Some(crate::input::Level::Text(t)) = self.input.stack.last_mut()
464 {
465 t.buf.push(elc as u32);
466 }
467 self.printer.new_line_char = self.eqtb.int(pg::assign_int::NEWLINECHAR);
468 self.printer.selector = if self.err.interaction == Interaction::Batch {
469 Selector::NoPrint
470 } else {
471 Selector::TermOnly
472 };
473 let starts_with_escape = first
474 .first()
475 .is_some_and(|&b| self.eqtb.catcode(u32::from(b)) == 0);
476 if !first.is_empty() && !starts_with_escape {
477 self.start_input();
478 }
479 self.history_spotless();
480 self.main_control();
481 self.final_cleanup();
482 if self.dump_requested {
483 self.store_fmt_file();
484 }
485 self.close_files_and_terminate();
486 }
487
488 fn history_spotless(&mut self) {
489 if self.err.history == crate::error::History::FatalErrorStop {
490 self.err.history = crate::error::History::Spotless;
491 }
492 }
493
494 pub fn store_fmt_file(&mut self) {
496 if self.save.cur_level > crate::eqtb::LEVEL_ONE {
497 self.print_err("You can't dump inside a group");
498 self.help(&["`{...\\dump}' is a no-no."]);
499 self.fatal_error("*** (cannot \\dump inside a group)");
500 }
501 if let Some(h) = self.lua.clone()
504 && let Err(msg) = h.call_simple(self, "pre_dump")
505 {
506 self.lua_error(&msg);
507 }
508 let job =
509 String::from_utf8_lossy(self.job_name.as_deref().unwrap_or(b"texput")).into_owned();
510 let c = self.start_time.civil();
511 self.format_ident = format!(" (format={job} {}.{}.{})", c.year, c.month, c.day);
512 self.format_name = job.clone();
513 let mut path = std::path::PathBuf::from(format!("{job}.fmt"));
514 if let Some(dir) = &self.files.output_dir {
515 path = dir.join(path);
516 }
517 self.printer.selector = if self.err.interaction == Interaction::Batch {
518 Selector::LogOnly
519 } else {
520 Selector::TermAndLog
521 };
522 self.printer.print_nl("Beginning to dump on file ");
523 self.printer.print_bytes(path.to_string_lossy().as_bytes());
524 self.printer.print_nl("");
525 let ident = self.format_ident.clone();
526 self.printer.print_str(&ident);
527 let engine_section = self.dump_engine();
528 let lua_section = match self.lua.clone() {
529 Some(h) => h.dump(self),
530 None => Vec::new(),
531 };
532 if crate::timing::enabled() {
533 eprintln!(
534 "[format] {:>10} bytes Lua (lua.bytecode)",
535 lua_section.len()
536 );
537 }
538 let origins = self.dump_origins();
539 let mut sections: Vec<(&str, &[u8])> =
540 vec![("engine", &engine_section), ("lua", &lua_section)];
541 if let Some(o) = &origins {
542 sections.push(("origins", o));
543 }
544 let data = crate::format::assemble(§ions);
545 self.printer.print_nl("");
546 self.printer.print_int(self.hash.len() as i64);
547 self.printer.print_str(" multiletter control sequences");
548 self.printer.print_nl("");
549 self.printer.print_int(self.fonts.fonts.len() as i64 - 1);
550 self.printer.print_str(" preloaded fonts");
551 if let Err(e) = std::fs::write(&path, &data) {
552 self.print_err("I can't write on file `");
553 self.printer.print_bytes(path.to_string_lossy().as_bytes());
554 self.printer.print_str("'.");
555 self.help_owned(vec![e.to_string()]);
556 self.error();
557 }
558 }
559
560 pub fn load_format(&mut self, data: &[u8]) -> Result<(), crate::format::FormatError> {
562 let sections = crate::format::disassemble(data)?;
563 crate::timing::mark("format: sections checked");
564 for (name, bytes) in §ions {
565 if *name == "engine" {
566 self.undump_engine(bytes)?;
567 }
568 }
569 self.set_date_and_time();
571 let p = crate::primitives::lookup(b"Umiddle").expect("primitive");
574 let id = self.cs.frozen_right;
575 self.eqtb.ensure_cs(id);
576 self.eqtb.cs[id.0 as usize].val = Meaning::prim(p.cmd, p.chr);
577 crate::timing::mark("format: engine state loaded");
578 if let Some(h) = self.lua.clone() {
579 for (name, bytes) in §ions {
580 if *name == "lua" {
581 h.undump(self, bytes)
582 .map_err(|_| crate::format::FormatError::Corrupt("lua section"))?;
583 }
584 }
585 }
586 crate::timing::mark("format: Lua state loaded");
587 self.format_name = self
588 .format_ident
589 .trim_start_matches(" (format=")
590 .split(' ')
591 .next()
592 .unwrap_or("")
593 .to_owned();
594 self.math_124 = self
595 .latex_release()
596 .is_some_and(|d| d.as_slice() >= b"2026-06-01".as_slice());
597 for (name, bytes) in §ions {
598 if *name == "origins" {
599 self.undump_origins(bytes)?;
600 }
601 }
602 Ok(())
603 }
604
605 pub fn latex_release(&self) -> Option<Vec<u8>> {
614 let body = |name: &[u8]| -> Option<Vec<u8>> {
615 let cs = self.hash.lookup(name)?;
616 let mac = self.eqtb.meaning(cs).mac.as_ref()?;
617 let tokens = mac.tokens.strip_prefix(&[crate::token::Token::END_MATCH])?;
618 tokens
619 .iter()
620 .map(|t| (!t.is_cs() && t.chr() < 0x80).then_some(t.chr() as u8))
621 .collect()
622 };
623 if body(b"fmtname")? != b"LaTeX2e" {
624 return None;
625 }
626 let v = body(b"fmtversion")?;
627 let v: Vec<u8> = v
628 .iter()
629 .map(|&c| if c == b'/' { b'-' } else { c })
630 .collect();
631 (v.len() == 10).then_some(v)
632 }
633
634 pub fn define_macro_str(&mut self, name: &str, body: &str) {
636 let cs = self.hash.lookup_or_insert(name.as_bytes());
637 let mut toks = vec![Token::END_MATCH];
638 toks.extend(crate::the::str_toks(body.as_bytes()));
639 self.eqtb.ensure_cs(cs);
640 self.eqtb.cs[cs.0 as usize].val = Meaning {
641 cmd: Cmd::Call,
642 chr: 0,
643 mac: Some(Rc::new(crate::token::Macro::new(false, Rc::from(toks)))),
644 };
645 }
646}