1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum FigureMode {
30 #[default]
32 Off,
33 Stats,
35 On,
37 Verify,
39}
40
41#[derive(Default)]
43pub struct Figures {
44 pub mode: FigureMode,
46 pub cs: Option<CsId>,
48 pub end_cs: Option<CsId>,
50 pub dir: Option<std::path::PathBuf>,
52 pub report: bool,
55 active: Option<Recording>,
57 replaying: Option<(u16, u64)>,
59 pub pictures: Vec<Picture>,
61 cache: HashMap<u64, Record>,
63 hits: (usize, u64),
65 pub mismatches: Vec<String>,
67 names: (usize, u64),
70}
71
72struct Recording {
74 level: u16,
76 started: Instant,
77 file: String,
78 line: i32,
79 source: Option<u64>,
81 key: Option<u64>,
83 globals: Vec<Loc>,
84 reasons: Vec<&'static str>,
85 nest: usize,
88 head: NodeRef,
89 tail: NodeRef,
90 vertical: bool,
93 hash_len: usize,
94 fonts_len: usize,
95 id_cs: Vec<CsId>,
97 diagnostics_len: usize,
98}
99
100pub struct Picture {
102 pub file: String,
104 pub line: i32,
106 pub us: u64,
108 pub source: Option<u64>,
110 pub globals: usize,
112 pub reasons: Vec<&'static str>,
114}
115
116impl Figures {
117 #[inline]
119 pub fn recording(&self) -> bool {
120 self.active.is_some()
121 }
122
123 #[inline]
125 pub fn busy(&self) -> bool {
126 self.active.is_some() || self.replaying.is_some()
127 }
128
129 #[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 #[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 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
186fn 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 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
218fn 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
227fn 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
239const PURE_LUA_FUNCTIONS: &[&[u8]] = &[b"__str_if_eq:nn", b"__kernel_str_mdfive_hash:n"];
242
243impl Engine {
244 #[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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 pub(crate) fn figure_paragraph_end(&mut self) {
477 let nest = self.nest.len();
478 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 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 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 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 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 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 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 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 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 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 fn capture_figure(&mut self, r: &Recording, us: u64) -> Result<Record, &'static str> {
712 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 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 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 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 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}