retex_engine/native.rs
1//! Native implementations of macros from TeX-coded libraries (`pgfmath`,
2//! `l3fp`, …).
3//!
4//! A native implementation replaces one specific *definition* of a macro,
5//! recognized by a fingerprint of its parameter text and body, so a package
6//! version with a different definition, or a document that redefines the
7//! macro, simply runs the TeX code. Each [`Macro`] object is looked up once,
8//! on its first call (the result is cached in [`Macro::native`]); the
9//! lookup is by definition, so `\let` copies are recognized too.
10//!
11//! A native implementation runs after the macro's arguments have been
12//! scanned, instead of the macro's body. It must leave exactly the state the
13//! body would have left (assignments, registers, the tokens it puts back
14//! into the input), or return `false` *before changing anything*, and the
15//! body runs as usual. That makes partial implementations safe: anything
16//! unusual falls back to TeX.
17//!
18//! Implementations that perform assignments ([`Native::executes`]) only run
19//! where TeX would execute the body: when main control fetches the macro
20//! (not inside `\edef`, `\message`, `\csname`, a number being scanned, …).
21//!
22//! Expandable implementations put their final result back into the input,
23//! where TeX would put the body and expand it further. They do not run
24//! where the body is not expanded further: a single expansion step
25//! (`\expandafter`, `\span` in a preamble, Lua's `token.expand`; see
26//! [`Engine::one_step`]), where the body itself is the observable result.
27//! Everywhere else the next token of the body is expanded at once, so an
28//! implementation is correct if the body's full expansion is
29//! self-contained (it consumes nothing after the call) and produces its
30//! whole result before its first unexpandable token is seen; each
31//! implementation documents why its macro qualifies.
32//! No native implementation runs while tracing is on, so logs stay
33//! identical. An implementation that expands tokens itself must clear
34//! [`Engine::exec_context`] while doing so.
35//!
36//! `RETEX_NO_NATIVE=1` disables all native implementations: the reference
37//! for differential tests (output with and without must be identical).
38//!
39//! `RETEX_NATIVE_DEBUG=1` prints, for each first call of a macro whose
40//! *name* is registered, its fingerprint and whether it matched: the way to
41//! obtain fingerprints for a new package version.
42
43use std::sync::OnceLock;
44
45mod deps;
46mod etoolbox;
47mod hyperxmp;
48mod l3fp;
49mod l3keyval;
50mod l3regex;
51mod l3tl;
52mod latex;
53mod pgfdim;
54mod pgffunc;
55mod pgfkeys;
56mod pgflength;
57mod pgfmath;
58mod pgfsys;
59mod xcolor;
60mod xcolormix;
61
62use crate::Engine;
63use crate::primitives_gen::assign_int as ip;
64use crate::token::{CS_TOKEN_FLAG, Macro, Token, TokenList};
65
66/// The input after a native macro's arguments, from
67/// [`Engine::native_following_subst`]: the tokens of the token lists on
68/// top of the input stack, in reading order, read on demand.
69pub struct Following {
70 /// The tokens read so far, macro parameters replaced.
71 tokens: Vec<Token>,
72 /// For each token, the level it comes from (0: the top of the input
73 /// stack) and the index in that level's list just after it.
74 origin: Vec<(usize, usize)>,
75 /// Where reading goes on: level and index.
76 depth: usize,
77 next: usize,
78 /// No more token lists to read.
79 done: bool,
80}
81
82impl Following {
83 /// Token `i`, reading on as needed.
84 pub fn get(&mut self, e: &Engine, i: usize) -> Option<Token> {
85 while i >= self.tokens.len() && !self.done {
86 self.fill(e);
87 }
88 self.tokens.get(i).copied()
89 }
90
91 /// Reads up to a chunk more tokens.
92 fn fill(&mut self, e: &Engine) {
93 let stack = &e.input.stack;
94 let Some(level) = stack.len().checked_sub(self.depth + 1).map(|k| &stack[k]) else {
95 self.done = true;
96 return;
97 };
98 let crate::input::Level::Tokens(t) = level else {
99 self.done = true;
100 return;
101 };
102 if matches!(
103 t.kind,
104 crate::input::TokKind::VTemplate | crate::input::TokKind::UTemplate
105 ) {
106 self.done = true;
107 return;
108 }
109 if self.depth == 0 && self.next == 0 && self.tokens.is_empty() {
110 self.next = t.loc;
111 }
112 if self.next >= t.list.len() {
113 self.depth += 1;
114 self.next = match stack.len().checked_sub(self.depth + 1).map(|k| &stack[k]) {
115 Some(crate::input::Level::Tokens(t)) => t.loc,
116 _ => 0,
117 };
118 return;
119 }
120 let end = (self.next + 32).min(t.list.len());
121 for k in self.next..end {
122 let tok = t.list[k];
123 if !tok.is_cs() && tok.cmd() == crate::cmd::Cmd::CarRet {
124 let Some(p) = e
125 .input
126 .param_stack
127 .get(t.param_start + tok.chr() as usize - 1)
128 else {
129 self.done = true;
130 return;
131 };
132 self.tokens.extend_from_slice(p);
133 self.origin
134 .extend(std::iter::repeat_n((self.depth, k + 1), p.len()));
135 } else {
136 self.tokens.push(tok);
137 self.origin.push((self.depth, k + 1));
138 }
139 }
140 self.next = end;
141 }
142
143 /// Whether consuming the first `n` tokens ends at the end of a token
144 /// of the lists (not inside a parameter's tokens). The tokens must
145 /// have been read.
146 pub fn count(&mut self, e: &Engine, n: usize) -> Option<()> {
147 if n == 0 {
148 return Some(());
149 }
150 let _ = self.get(e, n);
151 if n >= self.tokens.len() || self.origin[n] != self.origin[n - 1] {
152 Some(())
153 } else {
154 None
155 }
156 }
157}
158
159/// A native implementation of one macro definition.
160pub struct Native {
161 /// The macro's usual name, for diagnostics and `RETEX_NATIVE_DEBUG`.
162 pub name: &'static str,
163 /// The definitions it implements: (number of tokens of parameter text,
164 /// end-of-match and body; fingerprint).
165 pub fingerprints: &'static [(usize, u64)],
166 /// Whether it performs assignments or other non-expandable actions
167 /// (else it only puts tokens back into the input, like an expandable
168 /// macro).
169 pub executes: bool,
170 /// Runs the implementation on the scanned arguments; `false` to let
171 /// the TeX definition run instead (with nothing changed).
172 pub run: fn(&mut Engine, &[TokenList]) -> bool,
173}
174
175/// The registry.
176pub static NATIVES: &[Native] = &[
177 pgfmath::PARSE,
178 l3fp::PARSE,
179 l3fp::TO_DECIMAL,
180 l3keyval::PARSE,
181 hyperxmp::ADD_TO_XML,
182 xcolor::COL_RLET,
183 xcolor::COLORLET,
184 xcolor::COLOR,
185 xcolor::MCOLOR,
186 pgfsys::BP,
187 pgfsys::LITERAL,
188 latex::IFNEXTCHAR,
189 pgfmath::SIN,
190 pgfmath::COS,
191 pgfmath::TAN,
192 pgfmath::ATAN,
193 pgfmath::ATAN2,
194 pgfmath::VECLEN,
195 pgfmath::DIVIDE,
196 pgfmath::MULTIPLY,
197 pgfmath::RECIPROCAL,
198 pgfmath::ADD,
199 pgfmath::SUBTRACT,
200 pgfmath::ABS,
201 pgfmath::DEG,
202 pgfmath::LN,
203 pgfmath::SQRT,
204 pgfmath::ROUND,
205 pgfmath::MOD,
206 pgfmath::EXP,
207 pgfmath::POW,
208 pgflength::SETLENGTH,
209 pgflength::ADDTOLENGTH,
210 etoolbox::IFBLANK,
211 etoolbox::NOTBLANK,
212 l3tl::TRIM_SPACES,
213 l3regex::IF_MATCH,
214 l3regex::COUNT,
215 l3regex::EXTRACT_ONCE,
216 l3regex::EXTRACT_ALL,
217 l3regex::SPLIT,
218 l3regex::REPLACE_ONCE,
219 l3regex::REPLACE_ALL,
220 latex::PGFUTIL_IFNEXTCHAR,
221 latex::PGFKEYS_IFNEXTCHAR,
222 pgfkeys::UNPACK,
223 pgfkeys::SPDEF,
224 pgfkeys::ADD_PATH,
225 pgfkeys::SPLIT_PATH,
226 pgfkeys::CASE_ONE,
227 pgfkeys::CASE_TWO,
228 pgfkeys::CASE_TWO_EXTERN,
229 pgfkeys::CASE_THREE,
230 pgfkeys::UNKNOWN,
231];
232
233/// The token counts that occur in the registry, sorted: a macro of any
234/// other length is not looked up further.
235fn lengths() -> &'static [usize] {
236 static L: OnceLock<Vec<usize>> = OnceLock::new();
237 L.get_or_init(|| {
238 if std::env::var_os("RETEX_NO_NATIVE").is_some() {
239 return Vec::new();
240 }
241 let mut v: Vec<usize> = NATIVES
242 .iter()
243 .flat_map(|n| n.fingerprints.iter().map(|f| f.0))
244 .collect();
245 v.sort_unstable();
246 v.dedup();
247 v
248 })
249}
250
251fn debug() -> bool {
252 static D: OnceLock<bool> = OnceLock::new();
253 *D.get_or_init(|| std::env::var_os("RETEX_NATIVE_DEBUG").is_some())
254}
255
256/// `RETEX_NATIVE_DEBUG=2`: also the fingerprint of each member of a
257/// dependency group when it is checked (to compare package versions).
258fn debug_members() -> bool {
259 static D: OnceLock<bool> = OnceLock::new();
260 *D.get_or_init(|| std::env::var_os("RETEX_NATIVE_DEBUG").is_some_and(|v| v == "2"))
261}
262
263impl Engine {
264 /// The fingerprint of a macro definition: FNV-1a over its tokens,
265 /// control sequences by name (so it does not depend on the format or on
266 /// the order in which names were created).
267 pub fn macro_fingerprint(&self, mac: &Macro) -> u64 {
268 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
269 let mut eat = |b: u8| h = (h ^ u64::from(b)).wrapping_mul(0x100_0000_01b3);
270 eat(u8::from(mac.protected));
271 for t in mac.tokens.iter() {
272 if t.0 >= CS_TOKEN_FLAG {
273 eat(0xFF);
274 for &b in self.hash.name(crate::hash::CsId(t.0 - CS_TOKEN_FLAG)) {
275 eat(b);
276 }
277 eat(0xFE);
278 } else {
279 for b in t.0.to_le_bytes() {
280 eat(b);
281 }
282 }
283 }
284 h
285 }
286
287 /// The registry index of the native implementation of `mac`, if any.
288 #[inline]
289 pub(crate) fn native_of(&self, mac: &Macro, cs: crate::hash::CsId) -> Option<usize> {
290 match mac.native.get() {
291 1 => None,
292 0 => self.native_lookup(mac, cs),
293 n => Some(n as usize - 2),
294 }
295 }
296
297 #[cold]
298 fn native_lookup(&self, mac: &Macro, cs: crate::hash::CsId) -> Option<usize> {
299 let len = mac.tokens.len();
300 let named = debug()
301 .then(|| {
302 let name = String::from_utf8_lossy(self.hash.name(cs)).into_owned();
303 NATIVES.iter().any(|n| n.name == name).then_some(name)
304 })
305 .flatten();
306 if lengths().binary_search(&len).is_err() {
307 if let Some(name) = named {
308 eprintln!(
309 "[native] \\{name}: ({len}, 0x{:016x}) no match",
310 self.macro_fingerprint(mac)
311 );
312 }
313 mac.native.set(1);
314 return None;
315 }
316 let fp = self.macro_fingerprint(mac);
317 let found = NATIVES
318 .iter()
319 .position(|n| n.fingerprints.contains(&(len, fp)));
320 if let Some(name) = named {
321 eprintln!(
322 "[native] \\{name}: ({len}, 0x{fp:016x}) {}",
323 if found.is_some() {
324 "matched"
325 } else {
326 "no match"
327 }
328 );
329 }
330 mac.native.set(found.map_or(1, |i| i as u16 + 2));
331 found
332 }
333
334 /// Runs native implementation `i` on `args`; whether it handled the
335 /// call. `one_step` if the macro's body would not be expanded further.
336 pub(crate) fn run_native(&mut self, i: usize, args: &[TokenList], one_step: bool) -> bool {
337 let n = &NATIVES[i];
338 if if n.executes {
339 !self.exec_context
340 } else {
341 one_step
342 } {
343 if debug() {
344 eprintln!(
345 "[native] \\{}: {}",
346 n.name,
347 if n.executes {
348 "not executed by main control"
349 } else {
350 "single expansion step"
351 }
352 );
353 }
354 return false;
355 }
356 for p in [
357 ip::TRACINGMACROS,
358 ip::TRACINGCOMMANDS,
359 ip::TRACINGASSIGNS,
360 ip::TRACINGIFS,
361 ip::TRACINGGROUPS,
362 ip::TRACINGRESTORES,
363 ip::TRACINGNESTING,
364 ] {
365 if self.eqtb.int(p) > 0 {
366 if debug() {
367 eprintln!("[native] \\{}: tracing", n.name);
368 }
369 return false;
370 }
371 }
372 (n.run)(self, args)
373 }
374
375 /// The rest of the token list being read after the macro's arguments,
376 /// if the input comes from a token list: for implementations whose TeX
377 /// code goes on to read more than its parameters (with further
378 /// macros). It may contain macro-parameter tokens (`out_param`), which
379 /// match no character or control sequence.
380 pub fn native_following(&self) -> Option<&[Token]> {
381 match self.input.stack.last()? {
382 crate::input::Level::Tokens(t) if t.kind != crate::input::TokKind::VTemplate => {
383 Some(&t.list[t.loc..])
384 }
385 _ => None,
386 }
387 }
388
389 /// The tokens that follow in the input, as TeX would read them, across
390 /// the token lists on top of the input stack (macro parameters
391 /// replaced), up to the first level that is not a token list or is a
392 /// u/v template.
393 pub fn native_following_subst(&self) -> Option<Following> {
394 Some(Following {
395 tokens: Vec::new(),
396 origin: Vec::new(),
397 depth: 0,
398 next: 0,
399 done: false,
400 })
401 }
402
403 /// Consumes the first `n` tokens of a [`Following`] (which must end at
404 /// the end of a token of the lists, see [`Following::count`]): ends the
405 /// lists they use up, and moves on in the last one.
406 pub fn native_consume(&mut self, f: &Following, n: usize) {
407 if n == 0 {
408 return;
409 }
410 let (depth, loc) = f.origin[n - 1];
411 for _ in 0..depth {
412 self.end_token_list();
413 }
414 if let Some(crate::input::Level::Tokens(t)) = self.input.stack.last_mut() {
415 debug_assert!(loc <= t.list.len());
416 t.loc = loc;
417 }
418 }
419
420 /// Consumes the first `n` tokens of
421 /// [`native_following`](Self::native_following), which must hold
422 /// balanced braces and no macro-parameter tokens: reading them would
423 /// have no other effect.
424 pub fn native_skip(&mut self, n: usize) {
425 if let Some(crate::input::Level::Tokens(t)) = self.input.stack.last_mut() {
426 debug_assert!(t.loc + n <= t.list.len());
427 t.loc += n;
428 }
429 }
430
431 /// Puts `tokens` back into the input, to be read next (what an
432 /// expandable macro's result does).
433 pub fn native_insert(&mut self, tokens: Vec<Token>) {
434 if !tokens.is_empty() {
435 self.begin_token_list(TokenList::from(tokens), crate::input::TokKind::Inserted);
436 }
437 }
438}