Skip to main content

latexml/
ini_tex.rs

1//! Format dump mode — Rust equivalent of Perl's iniTeX + DumpFile.
2//!
3//! Usage: `latexml_oxide --init=latex.ltx --dest=latex_dump.oxide`
4//!
5//! Follows Perl's make formats scaffold (Makefile.PL + Core.pm::iniTeX):
6//! 1. Initialize the engine (load pools)
7//! 2. Take a snapshot of the state
8//! 3. Process the init file (e.g., latex.ltx) as raw TeX
9//! 4. Compute the diff (what changed)
10//! 5. Write the dump file with changed entries
11//!
12//! The resulting dump can be loaded at runtime to skip re-processing
13//! the LaTeX kernel on every test run.
14
15use std::path::Path;
16
17use once_cell::sync::Lazy;
18
19// Process-once cached env var (see WISDOM #56 — getenv hot-path race).
20static INIT_DEBUG: Lazy<bool> = Lazy::new(|| std::env::var_os("LATEXML_INIT_DEBUG").is_some());
21
22use latexml_core::{
23  binding::content::{InputDefinitionOptions, input_definitions},
24  state,
25};
26
27use crate::converter::Converter;
28
29/// Process an init file and write a format dump.
30/// Perl equivalent: Core.pm::iniTeX → TeX_Job.pool.ltxml::DumpFile
31pub fn dump_format(
32  _converter: &mut Converter,
33  init_file: &str,
34  destination: Option<&str>,
35) -> Result<usize, String> {
36  eprintln!("[ini_tex] Dumping format from {}", init_file);
37
38  // Strict Perl `iniTeX` + `DumpFile` order:
39  //
40  //   Core.pm L168-212 (iniTeX, default mode='Base'):
41  //     initializeState('Base.pool');     # ← Step 1 below
42  //     installDefinition('\jobname', ...);
43  //     installDefinition('\dump', Tokens());  # no-op
44  //     DumpFile($file, $dest);           # ← Step 2 below
45  //
46  //   TeX_Job.pool.ltxml L120-220 (DumpFile):
47  //     LoadPool($name . '_bootstrap');   # ← Step 3 below
48  //     $snap = ...                       # ← Step 4 below
49  //     loadTeXDefinitions($name, ...)    # ← Step 5 below
50  //     diff                              # ← Step 6 below
51  //     write                             # ← Step 7 below
52  //
53  // CRITICAL: Perl iniTeX defaults to `mode='Base'`, so `Base.pool` is
54  // loaded BEFORE any bootstrap. Without it, raw plain.tex / latex.ltx
55  // can't expand any TeX primitive — every `\def`, `\catcode`,
56  // `\let`, `\edef` etc. is undefined and we get an error cascade.
57  // After Base.pool, only `<name>_bootstrap` is loaded — NEVER
58  // `<name>_base`, `<name>_dump`, or `<name>_constructs`. Those
59  // pollute the diff with `:locked` flags, base/constructs
60  // definitions, etc. that the dump should NOT carry.
61
62  // Step 1: Load Base.pool equivalent (Perl `initializeState('Base.pool')`).
63  eprintln!("[ini_tex] Loading Base.pool (Perl `initializeState('Base.pool')`)");
64  if let Err(e) = latexml_package::engine::base::load_definitions() {
65    eprintln!("[ini_tex] base warning: {}", e);
66  }
67
68  // Mark this as init/dump mode so machinery elsewhere (notably
69  // `tex_file_io::\\input`'s LaTeX-style brace-arg auto-load of
70  // `LaTeX.pool`) skips behaviors that would corrupt the dump-build
71  // — see `tex_file_io.rs` for the gate.
72  state::assign_value("INI_TEX_MODE", true, Some(state::Scope::Global));
73
74  // Clear LaTeX/expl3/AmSTeX autoload triggers and `\documentstyle`
75  // installed by `tex.rs` during `prepare_session`. These triggers
76  // pre-define `\makeatletter`, `\documentclass`, etc. — which then
77  // poison the snapshot, causing raw `latex.ltx` at L1798
78  // (`\DeclareRobustCommand\makeatletter`) to hit the "redefining"
79  // branch in `\declare@robustcommand` (L1388), which calls
80  // `\@latex@info{Redefining ...}` — but `\@latex@info` isn't
81  // defined until L1799, triggering an undefined-CS cascade.
82  //
83  // Perl `Core.pm::iniTeX` defaults to `mode='Base'` for dump-build,
84  // so `Base.pool` is loaded but `TeX.pool`'s autoload triggers are
85  // NOT. Mirror that here by clearing them right before the snapshot.
86  for trigger in &[
87    // LaTeX autoload triggers (tex.rs L149-167)
88    "\\documentclass",
89    "\\newcommand",
90    "\\renewcommand",
91    "\\newenvironment",
92    "\\renewenvironment",
93    "\\NeedsTeXFormat",
94    "\\ProvidesPackage",
95    "\\RequirePackage",
96    "\\ProvidesFile",
97    "\\makeatletter",
98    "\\makeatother",
99    "\\begin",
100    "\\listfiles",
101    "\\nofiles",
102    "\\typeout",
103    "\\PassOptionsToPackage",
104    // `\@load@latex@pool` itself
105    "\\@load@latex@pool",
106    // expl3 autoload triggers
107    "\\ExplSyntaxOn",
108    "\\ProvidesExplClass",
109    "\\ProvidesExplPackage",
110    // AmSTeX/amsmath autoload triggers
111    "\\mathfrak",
112    "\\mathbb",
113    "\\Bbb",
114    "\\theoremstyle",
115    "\\numberwithin",
116    "\\align",
117    "\\subequations",
118    "\\multline",
119    "\\curraddr",
120    "\\subjclass",
121    // `\documentstyle` was also defined in tex.rs as a runtime macro
122    "\\documentstyle",
123  ] {
124    state::assign_meaning(
125      &latexml_core::T_CS!(*trigger),
126      latexml_core::common::store::Stored::None,
127      Some(state::Scope::Global),
128    );
129  }
130
131  // Step 2 + 3: install \jobname / \dump (no-op), then load bootstrap.
132  // (Perl Core.pm L204-207 + TeX_Job.pool.ltxml L127-129)
133  let init_lower = init_file.to_ascii_lowercase();
134  let is_plain_init = init_lower.contains("plain");
135
136  if is_plain_init {
137    eprintln!("[ini_tex] Loading plain_bootstrap (mirrors Perl `LoadPool('plain_bootstrap')`)");
138    if let Err(e) = latexml_package::engine::plain_bootstrap::load_definitions() {
139      eprintln!("[ini_tex] plain_bootstrap warning: {}", e);
140    }
141  } else {
142    eprintln!("[ini_tex] Loading latex_bootstrap (mirrors Perl `LoadPool('latex_bootstrap')`)");
143    // latex_bootstrap.rs L11 does `InnerPool!(plain_bootstrap)` itself
144    // (mirrors Perl `LoadPool('plain_bootstrap')` at the top of
145    // latex_bootstrap.pool.ltxml), so plain_bootstrap state is included.
146    if let Err(e) = latexml_package::engine::latex_bootstrap::load_definitions() {
147      eprintln!("[ini_tex] latex_bootstrap warning: {}", e);
148    }
149  }
150
151  // Perl `DumpFile` L132-138: snapshot all tables AFTER the bootstrap pool.
152  let snap = state::take_snapshot();
153  // Re-stage as "bootstrap" so `dump_writer` finds it for let-alias
154  // classification (early/late sections).
155  state::stage_snapshot_value("bootstrap", snap.clone());
156  let snap_size = snap.len();
157  eprintln!(
158    "[ini_tex] Snapshot taken at bootstrap ({} entries)",
159    snap_size
160  );
161
162  // Step 2: Process the init file as raw TeX definitions.
163  // Perl: loadTeXDefinitions($name, $path, type => $type)
164  // This digests the file through the engine, creating definitions.
165  let (_, name, ext) = split_path(init_file);
166  eprintln!("[ini_tex] Loading {} (ext: {})", name, ext);
167
168  // Lift the token limit for format dumps — expl3-code.tex alone uses ~5M tokens.
169  let saved_limit = latexml_core::gullet::set_token_limit(None);
170
171  // In init mode, suppress error/warning output during format loading.
172  // Raw latex.ltx redefines commands already in the compiled engine ("already defined"),
173  // and expl3-code.tex has forward references that produce transient errors.
174  // All these errors are benign — the dump captures the final correct state.
175  // Set LATEXML_INIT_DEBUG=1 to keep errors visible (for debugging the
176  // expl3 cascade — Perl parity target is zero errors during expl3 load).
177  let init_debug = *INIT_DEBUG;
178  let prev_suppress = latexml_core::common::error::set_suppress_log_output(!init_debug);
179
180  // Suppress known expl3 loading errors at the state level too
181  state::assign_value("SUPPRESS_UNDEFINED_ERRORS", !init_debug, None);
182  state::assign_value("SUPPRESS_UNEXPECTED_ERRORS", !init_debug, None);
183
184  // Lift the MAX_ERRORS cap during dump-build. Raw latex.ltx contains
185  // many CSes our engine reports as errors (forward references in
186  // expl3-code.tex, `\@onlypreamble` checks, autoload triggers, etc.).
187  // The default 10000-error cap aborts dump-build before plain.tex's
188  // `\outer\def\newread`, `\loop`, etc. land in the diff. Mirrors Perl
189  // `DumpFile`'s behavior — Perl runs latex.ltx through to `\dump`
190  // regardless of error count.
191  state::assign_value("MAX_ERRORS", 1_000_000_i64, None);
192
193  // Use the full filename with extension for proper file resolution
194  let load_name = if ext.is_empty() {
195    name.clone()
196  } else {
197    format!("{}.{}", name, ext)
198  };
199  let result = input_definitions(&load_name, InputDefinitionOptions {
200    noltxml: true,
201    ..InputDefinitionOptions::default()
202  });
203  if let Err(e) = result {
204    eprintln!("[ini_tex] Warning during loading: {}", e);
205  }
206
207  // Restore limits and suppression
208  latexml_core::gullet::restore_token_limit(saved_limit);
209  latexml_core::common::error::set_suppress_log_output(prev_suppress);
210  state::assign_value("SUPPRESS_UNDEFINED_ERRORS", false, None);
211  state::assign_value("SUPPRESS_UNEXPECTED_ERRORS", false, None);
212
213  // Step 3: Compute the diff — only entries that changed.
214  let diff = state::diff_snapshot(&snap);
215  eprintln!(
216    "[ini_tex] Post-load diff: {} changed entries (from {} pre-dump)",
217    diff.len(),
218    snap_size
219  );
220
221  // Step 4: Write the dump.
222  // Default: write text dump to resources/dumps/<kind>.<YYYY>.dump.txt for
223  // build.rs embedding. With --dest: write to the specified path.
224  let kind = if name.contains("latex") {
225    "latex"
226  } else {
227    "plain"
228  };
229  let ambient_year = latexml_engine::dump_paths::detect_ambient_texlive_year();
230  let (dest, is_text_dump) = match destination {
231    Some(d) if d.ends_with(".rs") => (d.to_string(), false),
232    Some(d) => (d.to_string(), true),
233    None => {
234      let year = ambient_year.ok_or_else(|| {
235        "Could not detect ambient TeXLive year (no kpsewhich/pdflatex). \
236         Pass --dest=<path> to override the dump filename."
237          .to_string()
238      })?;
239      let dump_name = latexml_engine::dump_paths::dump_filename(kind, year);
240      let dump_dir = "resources/dumps";
241      std::fs::create_dir_all(dump_dir)
242        .map_err(|e| format!("Failed to create {}: {}", dump_dir, e))?;
243      (format!("{}/{}", dump_dir, dump_name), true)
244    },
245  };
246
247  if is_text_dump {
248    // Write text format (loaded at runtime via dump_reader::load_from_str)
249    let write_count = latexml_core::dump_writer::write_dump(Path::new(&dest), &diff)?;
250    // Save versioned TeX Live stamp for staleness detection.
251    if let Some(year) = ambient_year {
252      save_texlive_version(year);
253    }
254    eprintln!("[ini_tex] Wrote {} text entries to {}", write_count, dest);
255    eprintln!("Format dump complete: {} entries written", write_count);
256    Ok(write_count)
257  } else {
258    // Write compiled Rust source (legacy format)
259    let tmp = format!("{}.tmp", dest);
260    let _write_count = latexml_core::dump_writer::write_dump(Path::new(&tmp), &diff)?;
261    let rs_count = latexml_core::dump_codegen::generate_rs(Path::new(&tmp), Path::new(&dest))?;
262    let _ = std::fs::remove_file(&tmp);
263    eprintln!(
264      "[ini_tex] Generated {} Rust definitions to {}",
265      rs_count, dest
266    );
267    eprintln!("Format dump complete: {} entries written", rs_count);
268    Ok(rs_count)
269  }
270}
271
272/// Generate a compiled Rust module from a dump file.
273/// Reads the text dump and produces a .rs file with direct state assignment calls.
274pub fn codegen_from_dump(dump_path: &str, output_path: &str) -> Result<usize, String> {
275  eprintln!("[ini_tex] Generating Rust module from {}", dump_path);
276  let count =
277    latexml_core::dump_codegen::generate_rs(Path::new(dump_path), Path::new(output_path))?;
278  eprintln!("[ini_tex] Generated {} entries to {}", count, output_path);
279  Ok(count)
280}
281
282fn save_texlive_version(year: u32) {
283  let version = std::process::Command::new("kpsewhich")
284    .arg("--version")
285    .output()
286    .ok()
287    .and_then(|o| {
288      if o.status.success() {
289        String::from_utf8(o.stdout).ok()
290      } else {
291        None
292      }
293    });
294  if let Some(v) = version {
295    let stamp = latexml_engine::dump_paths::version_filename(year);
296    let _ = std::fs::write(format!("resources/dumps/{}", stamp), v.trim());
297  }
298}
299
300fn split_path(path: &str) -> (String, String, String) {
301  let p = Path::new(path);
302  let dir = p
303    .parent()
304    .map(|d| d.to_string_lossy().to_string())
305    .unwrap_or_default();
306  let stem = p
307    .file_stem()
308    .map(|s| s.to_string_lossy().to_string())
309    .unwrap_or_default();
310  let ext = p
311    .extension()
312    .map(|e| e.to_string_lossy().to_string())
313    .unwrap_or_default();
314  (dir, stem, ext)
315}