Skip to main content

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}