Skip to main content

retex_engine/
figures.rs

1//! The figure cache (`docs/design/figure-cache.md`): a `tikzpicture` read
2//! from a file is recorded (its key, the nodes it appends, its global
3//! assignments, the control sequences and fonts it makes) and, when the
4//! same picture starts again from the same state, replayed instead of
5//! typeset. The records are kept in the process and, with a directory,
6//! in files (`figfile.rs`), for the next builds: the key covers all
7//! meanings, so it repeats from build to build (an edit elsewhere) rather
8//! than from pass to pass (each pass reads new cross-references).
9//!
10//! `RETEX_FIGURES=stats` prints statistics at the end of each pass, `on`
11//! replays, and `verify` typesets every picture and compares it with the
12//! cache, reporting mismatches.
13
14use std::collections::HashMap;
15use std::rc::Rc;
16use std::time::Instant;
17
18use crate::Engine;
19use crate::eqtb::{Loc, Val};
20use crate::figfile::Record;
21use crate::fingerprint::Fp;
22use crate::hash::{CsId, CsKind};
23use crate::input::{Level, LineSource};
24use crate::maincontrol::Mode;
25use crate::nodes::NodeRef;
26
27/// What the cache does.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum FigureMode {
30    /// Nothing.
31    #[default]
32    Off,
33    /// Records statistics only.
34    Stats,
35    /// Records and replays.
36    On,
37    /// Records, and compares each picture with the cache.
38    Verify,
39}
40
41/// The cache's state.
42#[derive(Default)]
43pub struct Figures {
44    /// What it does.
45    pub mode: FigureMode,
46    /// `\tikzpicture` (the macro `\begin{tikzpicture}` calls), when on.
47    pub cs: Option<CsId>,
48    /// `\endtikzpicture`.
49    pub end_cs: Option<CsId>,
50    /// Where the records are kept between builds.
51    pub dir: Option<std::path::PathBuf>,
52    /// Whether the statistics are printed at the end of the run
53    /// (`RETEX_FIGURES` given).
54    pub report: bool,
55    /// The picture being recorded.
56    active: Option<Recording>,
57    /// The picture being replayed: its group level and key.
58    replaying: Option<(u16, u64)>,
59    /// The pictures recorded.
60    pub pictures: Vec<Picture>,
61    /// Recorded pictures by key.
62    cache: HashMap<u64, Record>,
63    /// Pictures replayed, and the time their recording took.
64    hits: (usize, u64),
65    /// Mismatches found by `verify`.
66    pub mismatches: Vec<String>,
67    /// The names of the control sequences hashed so far, and their hash
68    /// (names are only ever added).
69    names: (usize, u64),
70}
71
72/// A picture being recorded.
73struct Recording {
74    /// The group level of its environment: it ends when the group does.
75    level: u16,
76    started: Instant,
77    file: String,
78    line: i32,
79    /// The hash of its source text, if read from a file.
80    source: Option<u64>,
81    /// Its key (`on` and `verify`).
82    key: Option<u64>,
83    globals: Vec<Loc>,
84    reasons: Vec<&'static str>,
85    /// The semantic nest's depth, its current list and that list's tail
86    /// when the picture began.
87    nest: usize,
88    head: NodeRef,
89    tail: NodeRef,
90    /// Whether it began in vertical mode (its `\leavevmode` starts a
91    /// paragraph).
92    vertical: bool,
93    hash_len: usize,
94    fonts_len: usize,
95    /// The fonts' identifiers when it began.
96    id_cs: Vec<CsId>,
97    diagnostics_len: usize,
98}
99
100/// A picture recorded, for the statistics.
101pub struct Picture {
102    /// The file of its `\begin{tikzpicture}`.
103    pub file: String,
104    /// The line of its `\begin{tikzpicture}`.
105    pub line: i32,
106    /// Microseconds from its start to the end of its group.
107    pub us: u64,
108    /// The hash of its source text, if read from a file.
109    pub source: Option<u64>,
110    /// The number of global assignments it made.
111    pub globals: usize,
112    /// Why it could not be cached (empty: it could).
113    pub reasons: Vec<&'static str>,
114}
115
116impl Figures {
117    /// Whether a picture is being recorded.
118    #[inline]
119    pub fn recording(&self) -> bool {
120        self.active.is_some()
121    }
122
123    /// Whether a picture is being recorded or replayed.
124    #[inline]
125    pub fn busy(&self) -> bool {
126        self.active.is_some() || self.replaying.is_some()
127    }
128
129    /// Notes something that stops the picture being recorded from being
130    /// cached.
131    #[inline]
132    pub fn taint(&mut self, why: &'static str) {
133        if let Some(r) = &mut self.active
134            && !r.reasons.contains(&why)
135        {
136            r.reasons.push(why);
137        }
138    }
139
140    /// Notes a global assignment.
141    #[inline]
142    pub fn global(&mut self, loc: Loc) {
143        if let Some(r) = &mut self.active {
144            r.globals.push(loc);
145        }
146    }
147
148    /// The statistics, as lines for the terminal.
149    pub fn summary(&self) -> String {
150        let n = self.pictures.len();
151        let ok: Vec<&Picture> = self
152            .pictures
153            .iter()
154            .filter(|p| p.reasons.is_empty())
155            .collect();
156        let total: u64 = self.pictures.iter().map(|p| p.us).sum();
157        let cached: u64 = ok.iter().map(|p| p.us).sum();
158        let mut out = format!(
159            "[figures] {n} pictures typeset, {:.3} s; {} cacheable, {:.3} s; {} replayed, {:.3} s saved\n",
160            total as f64 / 1e6,
161            ok.len(),
162            cached as f64 / 1e6,
163            self.hits.0,
164            self.hits.1 as f64 / 1e6,
165        );
166        let mut reasons: Vec<(&str, usize)> = Vec::new();
167        for p in &self.pictures {
168            for r in &p.reasons {
169                match reasons.iter_mut().find(|(k, _)| k == r) {
170                    Some((_, c)) => *c += 1,
171                    None => reasons.push((r, 1)),
172                }
173            }
174        }
175        reasons.sort_by(|a, b| b.1.cmp(&a.1));
176        for (r, c) in reasons {
177            out.push_str(&format!("[figures]   {c} {r}\n"));
178        }
179        for m in &self.mismatches {
180            out.push_str(&format!("[figures] mismatch: {m}\n"));
181        }
182        out
183    }
184}
185
186/// Where the `\end{tikzpicture}` matching the picture just begun starts in
187/// `text` (comments skipped, nested pictures counted).
188fn end_offset(text: &[u8]) -> Option<usize> {
189    let (begin, end): (&[u8], &[u8]) = (b"\\begin{tikzpicture}", b"\\end{tikzpicture}");
190    let mut depth = 1;
191    let mut i = 0;
192    while i < text.len() {
193        match text[i] {
194            b'\\' if text[i..].starts_with(end) => {
195                depth -= 1;
196                if depth == 0 {
197                    return Some(i);
198                }
199                i += end.len();
200            }
201            b'\\' if text[i..].starts_with(begin) => {
202                depth += 1;
203                i += begin.len();
204            }
205            // A control symbol: `\%` is no comment.
206            b'\\' => i += 2,
207            b'%' => {
208                while i < text.len() && text[i] != b'\n' {
209                    i += 1;
210                }
211            }
212            _ => i += 1,
213        }
214    }
215    None
216}
217
218/// The characters of `buf` from `loc`, as UTF-8.
219fn line_rest(buf: &[u32], loc: usize) -> Vec<u8> {
220    let s: String = buf[loc.min(buf.len())..]
221        .iter()
222        .filter_map(|&c| char::from_u32(c))
223        .collect();
224    s.into_bytes()
225}
226
227/// The text after position `loc` of the line `buf` and the rest of the
228/// file `data` from `pos` (the two joined by a line end), up to the end of
229/// the matching `\end{tikzpicture}`; `None` without one.
230fn picture_source(buf: &[u32], loc: usize, data: &[u8], pos: usize) -> Option<Vec<u8>> {
231    let mut text = line_rest(buf, loc);
232    text.push(b'\n');
233    text.extend_from_slice(&data[pos.min(data.len())..]);
234    let i = end_offset(&text)?;
235    text.truncate(i + b"\\end{tikzpicture}".len());
236    Some(text)
237}
238
239/// Lua functions (`\luafunction`, `\luadef`) whose results depend on
240/// their arguments only: expl3's.
241const PURE_LUA_FUNCTIONS: &[&[u8]] = &[b"__str_if_eq:nn", b"__kernel_str_mdfive_hash:n"];
242
243impl Engine {
244    /// Before callback `name` runs: while recording, notes Lua code of a
245    /// package's own.
246    #[inline]
247    pub(crate) fn figure_callback(&mut self, name: &str) {
248        if self.figures.recording()
249            && let Some(h) = self.lua.clone()
250            && h.foreign_callback(name)
251        {
252            self.figures.taint("Lua callbacks");
253        }
254    }
255
256    /// Before Lua function `cs` runs: while recording, notes it unless it
257    /// is pure.
258    #[inline]
259    pub(crate) fn figure_lua_function(&mut self) {
260        if self.figures.recording() && !PURE_LUA_FUNCTIONS.contains(&self.hash.name(self.cur_cs)) {
261            self.figures.taint("Lua code");
262        }
263    }
264
265    /// Whether `\write` stream `stream` goes to a file.
266    pub(crate) fn write_file_open(&self, stream: i32) -> bool {
267        (0..=127).contains(&stream) && self.printer.write_open(stream as u32)
268    }
269
270    /// At a macro call: starts recording or replaying a picture at
271    /// `\tikzpicture`. Returns whether the macro is not to be expanded (a
272    /// replayed picture's `\tikzpicture` and `\endtikzpicture`).
273    #[inline]
274    pub(crate) fn check_figure(&mut self) -> bool {
275        if self.figures.cs.is_none() {
276            return false;
277        }
278        if let Some((level, _)) = self.figures.replaying {
279            return self.figures.end_cs == Some(self.cur_cs) && self.save.cur_level == level;
280        }
281        if self.figures.cs == Some(self.cur_cs) && !self.figures.recording() {
282            return self.start_figure();
283        }
284        false
285    }
286
287    /// The index of the innermost input file's level.
288    fn file_level(&self) -> Option<usize> {
289        self.input
290            .stack
291            .iter()
292            .rposition(|l| matches!(l, Level::Text(t) if t.is_input_file))
293    }
294
295    /// The key of the picture whose source hashes to `source`, read from
296    /// the input with SyncTeX tag `tag`: the state it starts from
297    /// (meanings, registers, fonts, mode, SyncTeX).
298    fn figure_key(&mut self, source: u64, tag: u32) -> u64 {
299        let meanings = self.figure_meanings();
300        let mut fp = Fp::default();
301        fp.u(source);
302        fp.u(meanings);
303        fp.u(self.fp_registers());
304        fp.u(self.fonts.fonts.len() as u64);
305        for f in &self.fonts.fonts {
306            fp.bytes(&f.name);
307            fp.i(f.size.into());
308        }
309        fp.u(matches!(self.mode, Mode::Vertical | Mode::InternalVertical).into());
310        fp.u(self.nodes.sync.is_some().into());
311        fp.u(tag.into());
312        fp.get()
313    }
314
315    /// The hash of the meanings, as [`Engine::fp_meanings`] (which `--watch`
316    /// uses) but faster: the names hashed once, the macros' tokens once
317    /// per macro.
318    fn figure_meanings(&mut self) -> u64 {
319        if self.fp_cache.is_none() {
320            self.prepare_fingerprints();
321        }
322        let (done, h) = self.figures.names;
323        let mut names = if done == 0 {
324            Fp::default()
325        } else {
326            Fp::resume(h)
327        };
328        for i in done..self.hash.len() {
329            let cs = CsId(i as u32);
330            names.bytes(self.hash.name(cs));
331            names.debug(self.hash.kind(cs));
332        }
333        self.figures.names = (self.hash.len(), names.get());
334        let mut h = Fp::default();
335        h.u(names.get());
336        for s in &self.eqtb.cs {
337            h.u(s.level.into());
338            self.fp_meaning(&mut h, &s.val);
339        }
340        h.get()
341    }
342
343    /// The file of the record with key `key`.
344    fn figure_path(&self, key: u64) -> Option<std::path::PathBuf> {
345        Some(self.figures.dir.as_ref()?.join(format!("{key:016x}.fig")))
346    }
347
348    /// Loads the record with key `key` from its file into the cache, if
349    /// there is one; SyncTeX lines of the input with tag `tag` move to
350    /// `line`.
351    fn load_figure(&mut self, key: u64, tag: u32, line: i32) {
352        let Some(path) = self.figure_path(key) else {
353            return;
354        };
355        let Ok(data) = std::fs::read(&path) else {
356            return;
357        };
358        match self.figure_from_bytes(&data, key, tag, line) {
359            Ok(rec) => {
360                self.figures.cache.insert(key, rec);
361                // Used now: the cache is trimmed by age.
362                if let Ok(f) = std::fs::File::options().write(true).open(&path) {
363                    let _ = f.set_modified(std::time::SystemTime::now());
364                }
365            }
366            Err(_) => {
367                let _ = std::fs::remove_file(&path);
368            }
369        }
370    }
371
372    /// Writes record `rec` with key `key` to its file; `Err`: why it
373    /// cannot be.
374    fn save_figure(&self, key: u64, rec: &Record) -> Result<(), &'static str> {
375        let Some(path) = self.figure_path(key) else {
376            return Ok(());
377        };
378        let bytes = self.figure_bytes(key, rec)?;
379        if let Some(dir) = path.parent() {
380            let _ = std::fs::create_dir_all(dir);
381        }
382        // Whole or not at all: another build may read it.
383        let tmp = path.with_extension(format!("tmp{}", retex_kpath::process_id()));
384        if std::fs::write(&tmp, bytes).is_ok() {
385            let _ = std::fs::rename(&tmp, &path);
386        }
387        Ok(())
388    }
389
390    fn start_figure(&mut self) -> bool {
391        // The picture's source: the file being read, right after
392        // `\begin{tikzpicture}`.
393        let mut source = None;
394        let mut file = String::new();
395        let mut line = 0;
396        let mut tag = 0;
397        if let Some(i) = self.file_level()
398            && let Level::Text(t) = &self.input.stack[i]
399        {
400            file = String::from_utf8_lossy(&t.name).into_owned();
401            line = t.line;
402            tag = t.sync.0;
403            let before: String = t.buf[..t.loc.min(t.buf.len())]
404                .iter()
405                .filter_map(|&c| char::from_u32(c))
406                .collect();
407            if before.trim_end().ends_with("\\begin{tikzpicture}")
408                && let LineSource::File { data, pos } = &t.source
409                && let Some(text) = picture_source(&t.buf, t.loc, data, *pos)
410            {
411                let mut fp = Fp::default();
412                fp.bytes(&text);
413                source = Some(fp.get());
414            }
415        }
416        // A picture an earlier build could not cache is not keyed again
417        // (the key takes milliseconds).
418        let known_uncacheable = source.is_some_and(|s| {
419            self.figures
420                .dir
421                .as_ref()
422                .is_some_and(|d| d.join(format!("{s:016x}.no")).exists())
423        });
424        let key = match (self.figures.mode, source) {
425            (FigureMode::On | FigureMode::Verify, Some(s)) if !known_uncacheable => {
426                Some(self.figure_key(s, tag))
427            }
428            _ => None,
429        };
430        if let Some(k) = key
431            && !self.figures.cache.contains_key(&k)
432        {
433            self.load_figure(k, tag, line);
434        }
435        if self.figures.mode == FigureMode::On
436            && let Some(k) = key
437            && self.figures.cache.contains_key(&k)
438            && self.replay_figure(k)
439        {
440            if std::env::var_os("RETEX_FIGURES_DEBUG").is_some() {
441                eprintln!("[figures] replayed {file}:{line} ({k:016x})");
442            }
443            return true;
444        }
445        let mut reasons = Vec::new();
446        if source.is_none() {
447            reasons.push("not read from a file");
448        }
449        let cl = self.cur_list();
450        self.figures.active = Some(Recording {
451            level: self.save.cur_level,
452            started: Instant::now(),
453            file,
454            line,
455            source,
456            key,
457            globals: Vec::new(),
458            reasons,
459            nest: self.nest.len(),
460            head: cl.head,
461            tail: cl.tail,
462            vertical: matches!(self.mode, Mode::Vertical | Mode::InternalVertical),
463            hash_len: self.hash.len(),
464            fonts_len: self.fonts.fonts.len(),
465            id_cs: self.fonts.id_cs.clone(),
466            diagnostics_len: self.diagnostics.as_ref().map_or(0, Vec::len),
467        });
468        false
469    }
470
471    /// While recording, a paragraph with something in it ends: if it is the
472    /// one the picture is in, or one it started, its lines go to the list
473    /// around it, which a record does not hold (an empty one adds nothing:
474    /// LaTeX's paragraph hooks end the paragraph just started and start it
475    /// again).
476    pub(crate) fn figure_paragraph_end(&mut self) {
477        let nest = self.nest.len();
478        // `\par` elsewhere than in a paragraph ends nothing.
479        if self.mode != Mode::Horizontal {
480            return;
481        }
482        if let Some(r) = &self.figures.active
483            && (nest == r.nest || (r.vertical && nest == r.nest + 1))
484        {
485            self.figures.taint("ends a paragraph");
486        }
487    }
488
489    /// Starts replaying the picture with key `key`: its control sequences
490    /// and fonts are made and its source is skipped up to its
491    /// `\end{tikzpicture}`. Returns `false` (and changes nothing that
492    /// matters) if it cannot be.
493    fn replay_figure(&mut self, key: u64) -> bool {
494        let Some(level_index) = self.file_level() else {
495            return false;
496        };
497        let entry = &self.figures.cache[&key];
498        let names = entry.names.clone();
499        let fonts = entry.fonts.clone();
500        let font_ids = entry.font_ids.clone();
501        // The fonts first: they must get the ids they had.
502        let first = self.fonts.fonts.len();
503        for (k, (name, size)) in fonts.iter().enumerate() {
504            let got = self.load_font(name, *size);
505            if got != Some((first + k) as u32) {
506                if std::env::var_os("RETEX_FIGURES_DEBUG").is_some() {
507                    eprintln!(
508                        "[figures] replay refused: font {} at {size} got {got:?}, not {}",
509                        String::from_utf8_lossy(name),
510                        first + k
511                    );
512                }
513                return false;
514            }
515        }
516        // The source up to `\end{tikzpicture}`.
517        let Level::Text(t) = &mut self.input.stack[level_index] else {
518            return false;
519        };
520        let LineSource::File { data, pos } = &t.source else {
521            return false;
522        };
523        let (data, pos) = (Rc::clone(data), *pos);
524        let rest = line_rest(&t.buf, t.loc);
525        let mut text = rest.clone();
526        text.push(b'\n');
527        text.extend_from_slice(&data[pos.min(data.len())..]);
528        let Some(i) = end_offset(&text) else {
529            return false;
530        };
531        if i < rest.len() {
532            // On this line: its characters go.
533            let n = std::str::from_utf8(&rest[..i]).map_or(0, |s| s.chars().count());
534            let loc = t.loc;
535            t.buf.drain(loc..loc + n);
536        } else {
537            // On a later line: the rest of this one goes, the lines between
538            // become comments (the line numbers stay), and what comes
539            // before `\end` on its line becomes spaces, which TeX skips at
540            // the start of a line.
541            let k = pos + (i - rest.len() - 1);
542            let start = data[..k]
543                .iter()
544                .rposition(|&b| b == b'\n')
545                .map_or(0, |p| p + 1)
546                .max(pos);
547            let mut new = Vec::with_capacity(data.len());
548            new.extend_from_slice(&data[..pos]);
549            for _ in data[pos..start].iter().filter(|&&b| b == b'\n') {
550                new.extend_from_slice(b"%\n");
551            }
552            new.extend(std::iter::repeat_n(b' ', k - start));
553            new.extend_from_slice(&data[k..]);
554            t.source = LineSource::File {
555                data: Rc::from(new),
556                pos,
557            };
558            t.loc = t.buf.len();
559        }
560        for (name, kind, meaning) in &names {
561            let cs = match kind {
562                CsKind::Active(c) => self.hash.active(*c),
563                CsKind::Frozen => self.hash.new_frozen(name),
564                _ => self.hash.lookup_or_insert(name),
565            };
566            if let Some(m) = meaning {
567                self.eqtb.ensure_cs(cs);
568                self.eqtb.cs[cs.0 as usize].val = m.clone();
569                if m.cmd != crate::cmd::Cmd::SetFont {
570                    self.frozen_prims.insert(name.clone(), cs);
571                }
572            }
573        }
574        if let Some(last) = self.hash.len().checked_sub(1) {
575            self.eqtb.ensure_cs(CsId(last as u32));
576        }
577        for (f, cs) in font_ids {
578            if let Some(slot) = self.fonts.id_cs.get_mut(f) {
579                *slot = cs;
580            }
581        }
582        self.figures.replaying = Some((self.save.cur_level, key));
583        true
584    }
585
586    /// After a group ends (with `aftergroup`: tokens were saved for after
587    /// it): ends the recording or the replay if it was the picture's.
588    pub(crate) fn check_figure_end(&mut self, aftergroup: bool) {
589        if let Some((level, key)) = self.figures.replaying {
590            if self.save.cur_level < level {
591                self.figures.replaying = None;
592                self.finish_replay(key);
593            }
594            return;
595        }
596        let Some(r) = &self.figures.active else {
597            return;
598        };
599        if self.save.cur_level >= r.level {
600            return;
601        }
602        if aftergroup {
603            self.figures.taint("\\aftergroup at its end");
604        }
605        let mut r = self.figures.active.take().expect("a recording");
606        let us = r.started.elapsed().as_micros() as u64;
607        let mut reasons = std::mem::take(&mut r.reasons);
608        let entry = if reasons.is_empty() && r.key.is_some() {
609            match self.capture_figure(&r, us) {
610                Ok(e) => Some(e),
611                Err(why) => {
612                    reasons.push(why);
613                    None
614                }
615            }
616        } else {
617            None
618        };
619        if let (Some(e), Some(k)) = (entry, r.key) {
620            if self.figures.mode == FigureMode::Verify
621                && let Some(old) = self.figures.cache.get(&k)
622            {
623                let diff = self.figure_differences(old, &e);
624                if !diff.is_empty() {
625                    self.figures.mismatches.push(format!(
626                        "{}:{}: {}",
627                        r.file,
628                        r.line,
629                        diff.join(", ")
630                    ));
631                }
632            } else {
633                if std::env::var_os("RETEX_FIGURES_DEBUG").is_some() {
634                    eprintln!("[figures] recorded {}:{} ({k:016x})", r.file, r.line);
635                }
636                if let Err(why) = self.save_figure(k, &e) {
637                    reasons.push(why);
638                }
639                self.figures.cache.insert(k, e);
640            }
641        }
642        if !reasons.is_empty()
643            && r.key.is_some()
644            && let (Some(s), Some(dir)) = (r.source, &self.figures.dir)
645        {
646            let _ = std::fs::create_dir_all(dir);
647            let _ = std::fs::write(dir.join(format!("{s:016x}.no")), reasons.join("\n"));
648        }
649        self.figures.pictures.push(Picture {
650            file: r.file,
651            line: r.line,
652            us,
653            source: r.source,
654            globals: r.globals.len(),
655            reasons,
656        });
657    }
658
659    /// What differs between two records of the same picture (`verify`).
660    fn figure_differences(&self, old: &Record, new: &Record) -> Vec<&'static str> {
661        let mut diff = Vec::new();
662        if self.list_hash(old.nodes) != self.list_hash(new.nodes) {
663            diff.push("nodes");
664        }
665        let same_names = old.names.len() == new.names.len()
666            && old.names.iter().zip(&new.names).all(|(a, b)| {
667                a.0 == b.0
668                    && a.1 == b.1
669                    && match (&a.2, &b.2) {
670                        (Some(x), Some(y)) => x.same_as(y),
671                        (None, None) => true,
672                        _ => false,
673                    }
674            });
675        if !same_names {
676            diff.push("control sequences");
677        }
678        if old.fonts != new.fonts || old.font_ids != new.font_ids {
679            diff.push("fonts");
680        }
681        if old.space_factor != new.space_factor {
682            diff.push("space factor");
683        }
684        if old.globals.len() != new.globals.len()
685            || old
686                .globals
687                .iter()
688                .zip(&new.globals)
689                .any(|((l1, v1), (l2, v2))| l1 != l2 || self.val_hash(v1) != self.val_hash(v2))
690        {
691            diff.push("global assignments");
692        }
693        diff
694    }
695
696    /// The hash of a node list's structure.
697    fn list_hash(&self, p: NodeRef) -> u64 {
698        let mut fp = Fp::default();
699        self.fp_list(&mut fp, p);
700        fp.get()
701    }
702
703    /// The hash of a value.
704    fn val_hash(&self, v: &Val) -> u64 {
705        let mut fp = Fp::default();
706        self.fp_val(&mut fp, v);
707        fp.get()
708    }
709
710    /// What a picture just recorded left behind, to replay it.
711    fn capture_figure(&mut self, r: &Recording, us: u64) -> Result<Record, &'static str> {
712        // Messages are not replayed.
713        if self
714            .diagnostics
715            .as_ref()
716            .is_some_and(|d| d.len() > r.diagnostics_len)
717        {
718            return Err("gives messages");
719        }
720        let cl = self.cur_list().clone();
721        let nodes = if self.nest.len() == r.nest && cl.head == r.head {
722            // The nodes after the list's tail when it began, which must
723            // still be in the list.
724            let mut p = cl.head;
725            while p != r.tail && !p.is_null() {
726                p = self.nodes.next(p);
727            }
728            if p.is_null() {
729                return Err("changes the list before it");
730            }
731            self.nodes.copy_list(self.nodes.next(r.tail))
732        } else if r.vertical {
733            // It started its paragraph (pgfpicture's `\leavevmode`, at its
734            // end), which LaTeX's paragraph hooks end and start again: not
735            // replayed exactly yet (a picture in a paragraph or a formula
736            // is).
737            return Err("starts a paragraph");
738        } else {
739            return Err("changes the semantic nest");
740        };
741        let mut names = Vec::new();
742        for i in r.hash_len..self.hash.len() {
743            let cs = CsId(i as u32);
744            let kind = self.hash.kind(cs).clone();
745            let meaning = match kind {
746                CsKind::Frozen => self.eqtb.cs.get(i).map(|s| s.val.clone()),
747                CsKind::Active(_) | CsKind::Null | CsKind::Single(_) | CsKind::Multi => None,
748                _ => return Err("makes internal control sequences"),
749            };
750            names.push((self.hash.name(cs).to_vec(), kind, meaning));
751        }
752        let fonts = (r.fonts_len..self.fonts.fonts.len())
753            .map(|f| {
754                let info = &self.fonts.fonts[f];
755                (info.name.clone(), info.size)
756            })
757            .collect();
758        let font_ids = self
759            .fonts
760            .id_cs
761            .iter()
762            .enumerate()
763            .filter(|&(f, cs)| r.id_cs.get(f) != Some(cs))
764            .map(|(f, &cs)| (f, cs))
765            .collect();
766        let mut locs: Vec<Loc> = Vec::new();
767        for l in &r.globals {
768            if !locs.contains(l) {
769                locs.push(*l);
770            }
771        }
772        let mut globals = Vec::with_capacity(locs.len());
773        for l in locs {
774            let v = match self.eqtb.get(l) {
775                Val::Node(b) => Val::Node(self.nodes.copy_list(b)),
776                v => v,
777            };
778            globals.push((l, v));
779        }
780        Ok(Record {
781            line: r.line,
782            names,
783            fonts,
784            font_ids,
785            globals,
786            space_factor: cl.space_factor,
787            us,
788            nodes,
789        })
790    }
791
792    /// Appends a copy of the list `p` to the current list.
793    fn append_copy(&mut self, p: NodeRef) {
794        let mut p = self.nodes.copy_list(p);
795        while !p.is_null() {
796            let next = self.nodes.next(p);
797            self.nodes.set_next(p, NodeRef::NULL);
798            self.tail_append(p);
799            p = next;
800        }
801    }
802
803    /// Ends the replay of the picture with key `key`, at the end of its
804    /// group: its nodes and global assignments.
805    fn finish_replay(&mut self, key: u64) {
806        let Some(entry) = self.figures.cache.get(&key) else {
807            return;
808        };
809        let (nodes, globals, space_factor, us) = (
810            entry.nodes,
811            entry.globals.clone(),
812            entry.space_factor,
813            entry.us,
814        );
815        self.append_copy(nodes);
816        for (l, v) in globals {
817            let v = match v {
818                Val::Node(b) => Val::Node(self.nodes.copy_list(b)),
819                v => v,
820            };
821            self.geq_define(l, v);
822        }
823        self.cur_list_mut().space_factor = space_factor;
824        self.figures.hits.0 += 1;
825        self.figures.hits.1 += us;
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn chars(s: &str) -> Vec<u32> {
834        s.chars().map(u32::from).collect()
835    }
836
837    #[test]
838    fn source_up_to_the_matching_end() {
839        let buf = chars("\\begin{tikzpicture}[x=1cm] % \\end{tikzpicture}\n");
840        let data =
841            b"\\draw (0,0);\n\\begin{tikzpicture}\\end{tikzpicture}\n\\end{tikzpicture} after\n";
842        let text = picture_source(&buf, 19, data, 0).unwrap();
843        assert_eq!(
844            String::from_utf8(text).unwrap(),
845            "[x=1cm] % \\end{tikzpicture}\n\n\\draw (0,0);\n\\begin{tikzpicture}\\end{tikzpicture}\n\\end{tikzpicture}"
846        );
847        assert!(picture_source(&buf, 19, b"\\draw;\n", 0).is_none());
848    }
849}