tfm/
lib.rs

1//! # tfm: TeX font metric data
2//!
3//! This is a crate for working with TeX font metric data.
4//! It includes:
5//!
6//! - Functions to read and write TeX font metric (.tfm) files
7//!   to and from a value of type [`File`]
8//!   ([`deserialize`](File::deserialize), [`serialize`](File::serialize)).
9//!
10//! - Functions to read and write property list (.pl) files
11//!   to and from a value of type [`pl::File`]
12//!   ([`from_pl_source_code`](pl::File::from_pl_source_code), [`display`](pl::File::display)).
13//!
14//! - Converters from .tfm to .pl files and vice-versa
15//!   (using Rust's [`From`] trait to go between [`File`] and [`pl::File`]).
16//!
17//! ## Background
18//!
19//! Probably the most famous part of the implementation of TeX is the Knuth-Plass line breaking algorithm.
20//! This algorithm determines the "optimal" places to add line breaks when typesetting a paragraph of text.
21//! In order to run the algorithm one needs to provide the dimensions of all characters in the current font.
22//! These dimensions are used to size the boxes in the Knuth-Plass box and glue model.
23//!
24//! In TeX, character dimensions are provided using TeX font metric files.
25//! These are binary files.
26//! By convention they have a .tfm file extension.
27//! Unlike more modern file formats like TrueType, .tfm files only contain the font dimensions;
28//!     they don't contains the glyphs.
29//! In general,
30//!     .tfm files are produced by other software like Metafont,
31//!     placed in some well-known directory in the TeX distribution,
32//!     and then read into memory when TeX is running.
33//!
34//! Because .tfm files are binary files, it's hard to debug or tweak them.
35//! To remedy this, Knuth and his team developed another file format called a property list file
36//!     (extension .pl or .plst)
37//!     that contains the same information but in a modifiable text format.
38//! They then wrote two programs:
39//!     `tftopl` to convert a .tfm file to a .pl file,
40//!     and `pltotf` to convert a .pl file to a .tfm file.
41//!
42//! The general goal of this crate to fully re-implement all of the TeX font metric
43//!     code written by Knuth and others.
44//! This includes `tftopl`, `pltotf`, and also the parts of TeX itself that contain logic
45//!     for reading and interpreting .tfm files.
46//! However, unlike these monolithic software programs,
47//!     this re-implementation is in the form of a modular library in which
48//!     individual pieces of logic and be used and re-used.
49//!
50//! ## Basic example
51//!
52//! ```
53//! // Include the .tfm file for Computer Modern in size 10pt.
54//! let tfm_bytes = include_bytes!["../corpus/computer-modern/cmr10.tfm"];
55//!
56//! // Deserialize the .tfm file.
57//! let (tfm_file_or_error, deserialization_warnings) = tfm::File::deserialize(tfm_bytes);
58//! let mut tfm_file = tfm_file_or_error.expect("cmr10.tfm is a valid .tfm file");
59//! assert_eq![deserialization_warnings, vec![], "the data in cmr10.tfm is 100% valid, so there are no deserialization warnings"];
60//! // TODO assert_eq![tfm_file.header.design_size, tfm::FixWord::UNITY * 10]; make it 11 to be more interesting
61//! // TODO query some data
62//!
63//! // Validate the .tfm file.
64//! let validation_warnings = tfm_file.validate_and_fix();
65//! assert_eq![validation_warnings, vec![], "the data in cmr10.tfm is 100% valid, so there are no validation warnings"];
66//!
67//! // Convert the .tfm file to a .pl file and print it.
68//! let pl_file: tfm::pl::File = tfm_file.clone().into();
69//! // TODO query some data
70//! println!["cmr10.pl:\n{}", pl_file.display(/*indent=*/2, tfm::pl::CharDisplayFormat::Default)];
71//! ```
72//!
73//!
74//! ## Advanced functionality
75//!
76//! In addition to supporting the basic use cases of querying font metric data
77//!     and converting between different formats,
78//!     this crate has advanced functionality for performing additional tasks on font metric data.
79//! The full set of functionality can be understood by navigating through the crate documentation.
80//! But here are 3 highlights we think are interesting:
81//!
82//! - **Language analysis of .pl files**:
83//!   In `pltotf`, Knuth parses .pl files in a single pass.
84//!   This crate takes a common approach nowadays of parsing in multiple passes:
85//!   first constructing a [concrete syntax tree](pl::cst::Cst) (or parse tree),
86//!   next constructing a [fully typed and checked abstract syntax tree](pl::ast::Ast),
87//!   and finally building the [`pl::File`] itself.
88//!   Each of the passes is exposed, so you can e.g. just build the AST for the .pl file and
89//!   do some analysis on it.
90//!
91//! - **Debug output for .tfm files**:
92//!     
93//! - **Compilation of lig/kern programs**:
94//!
95//!
96//! ## Binaries
97//!
98//! The Texcraft project produces 3 binaries based on this crate:
99//!
100//! - `tftopl` and `pltotf`: re-implementations of Knuth's programs.
101//! - `tfmtools`: a new binary that has a bunch of different tools
102//!   for working with TeX font metric data.
103//!   Run `tfmtools help` to list all of the available tools.
104//!
105//! In the root of [the Texcraft repository](https://github.com/jamespfennell/texcraft)
106//!     these tools can be run with `cargo run --bin $NAME`
107//!     and built with `cargo build --bin $NAME`.
108//!
109//!
110//! ## Correctness
111//!
112//! As part of the development of this crate significant effort has been spent
113//!     ensuring it exactly replicates the work of Knuth.
114//! This correctness checking is largely based around diff testing the binaries
115//!     `tftopl` and `pltotf`.
116//! We verify that the Texcraft and Knuth implementations have the same output
117//!     and generate the same error messages.
118//!
119//! This diff testing has been performed in a few different ways:
120//!
121//! - We have run diff tests over all ~100,000 .tfm files in CTAN.
122//!   These tests verify that `tftopl` gives the correct result,
123//!   and that running `pltotf` on the output .pl file gives the correct result too.
124//!   Unfortunately running `pltotf` on the .pl files in CTAN is infeasible
125//!   because most of these files are Perl scripts, not property list files.
126//!
127//! - We have developed a fuzz testing harness (so far just for `tftopl`)
128//!   that generates highly heterogenous .tfm files and verifies that `tftopl` gives the correct result.
129//!   This fuzz testing has uncovered many issues in the Texcraft implementation,
130//!   and has even identified [a 30-year old bug](https://tug.org/pipermail/tex-k/2024-March/004031.html)
131//!   in Knuth's implementation of `tftopl`.
132//!
133//! Any .tfm or .pl file that exposes a bug in this library is added to
134//!     [our automated testing corpus](https://github.com/jamespfennell/texcraft/tree/main/crates/tfm/bin/tests/data).
135//! Running `cargo t` validates that Texcraft's binaries give the same result as Knuth's binaries
136//!     (the output of Knuth's binaries is in source control).
137//! This ensures there are no regressions.
138//!
139//! If you discover a .tfm or .pl file such that the Texcraft and Knuth implementations
140//!     diverge, this indicates there is a bug in this library.
141//! Please create an issue on the Texcraft GitHub repo.
142//! We will fix the bug and add your files to the testing corpus.
143
144pub mod algorithms;
145use std::{
146    collections::{BTreeSet, HashMap, HashSet},
147    num::NonZeroU8,
148};
149pub mod ligkern;
150pub mod pl;
151
152use std::collections::BTreeMap;
153
154mod debug;
155mod deserialize;
156mod serialize;
157mod validate;
158
159use common::font;
160pub use deserialize::DeserializationError;
161pub use deserialize::DeserializationWarning;
162pub use deserialize::RawFile;
163pub use deserialize::SubFileSizes;
164pub use validate::ValidationWarning;
165
166#[derive(Debug, Default)]
167pub struct Font {
168    pub file: File,
169    pub lig_kern_program: ligkern::CompiledProgram,
170}
171
172const TFM_CMR10: &[u8] = include_bytes!("../corpus/computer-modern/cmr10.tfm");
173
174impl Font {
175    pub fn build_from_file(mut file: File) -> (Self, Vec<ligkern::InfiniteLoopError>) {
176        let (lig_kern_program, lig_kern_warnings) =
177            ligkern::CompiledProgram::compile_from_tfm_file(&mut file);
178        let font = Font {
179            file,
180            lig_kern_program,
181        };
182        (font, lig_kern_warnings)
183    }
184
185    pub fn build_from_bytes(
186        tfm_bytes: &[u8],
187    ) -> Result<
188        (
189            Self,
190            Vec<DeserializationWarning>,
191            Vec<ligkern::InfiniteLoopError>,
192        ),
193        DeserializationError,
194    > {
195        let (file, deserialization_warnings) = match File::deserialize(tfm_bytes) {
196            (Err(err), _) => return Err(err),
197            (Ok(file), warnings) => (file, warnings),
198        };
199        let (font, lig_kern_warnings) = Self::build_from_file(file);
200        Ok((font, deserialization_warnings, lig_kern_warnings))
201    }
202    pub fn cmr10() -> Self {
203        Self::build_from_bytes(TFM_CMR10)
204            .expect("cmr10.tfm is valid")
205            .0
206    }
207}
208
209impl font::Format for Font {
210    fn null() -> Self {
211        Default::default()
212    }
213    fn width(&self, c: char) -> Option<common::Scaled> {
214        self.file.width_utf8(c)
215    }
216    fn height(&self, c: char) -> Option<common::Scaled> {
217        self.file.height_utf8(c)
218    }
219    fn depth(&self, c: char) -> Option<common::Scaled> {
220        self.file.depth_utf8(c)
221    }
222    fn default_space(&self) -> common::Glue {
223        common::Glue {
224            width: self.file.named_param_scaled(NamedParameter::Space).unwrap(),
225            stretch: self
226                .file
227                .named_param_scaled(NamedParameter::Stretch)
228                .unwrap(),
229            stretch_order: common::GlueOrder::Normal,
230            shrink: self
231                .file
232                .named_param_scaled(NamedParameter::Shrink)
233                .unwrap(),
234            shrink_order: common::GlueOrder::Normal,
235        }
236    }
237    fn extra_space(&self) -> common::Scaled {
238        self.file
239            .named_param_scaled(NamedParameter::ExtraSpace)
240            .unwrap()
241    }
242}
243
244impl font::TextBuilder for Font {
245    type TextIter<'a, Word: Iterator<Item = char>> = ligkern::RunIter<'a, Word>;
246
247    fn build_text<'a, Word: Iterator<Item = char>>(
248        &'a self,
249        word: Word,
250        options: font::BuildTextOptions,
251    ) -> Self::TextIter<'a, Word> {
252        self.lig_kern_program.run_with_options(
253            word,
254            ligkern::RunOptions {
255                disable_left_boundary: options.disable_left_boundary,
256                right_boundary_override: options.right_boundary_override,
257            },
258        )
259    }
260
261    fn has_replacement(&self, left: Option<char>, right: Option<char>) -> bool {
262        self.lig_kern_program.has_replacement(left, right)
263    }
264}
265
266/// Complete contents of a TeX font metric (.tfm) file.
267///
268/// The struct contain multiple vectors.
269/// In TeX and TFtoPL there is an optimization in which all of data in the vectors
270/// is stored in one large vector of 32-bit integers.
271/// The conversion from [u32] to the specific types like [FixWord] are then done when the
272/// data is needed.
273/// This makes the memory footprint of this type much more compact,
274///     and such a change may be considered in the future.
275///
276/// In fact in TeX the font data for all fonts is stored in one contiguous piece of memory
277///     (`font_info`, defined in TeX82.2021.549).
278/// This is a little too unsafe to pull off though.
279#[derive(Clone, Debug, PartialEq, Eq)]
280pub struct File {
281    /// Header.
282    pub header: Header,
283
284    /// Smallest character in the .tfm.
285    pub smallest_char: Char,
286
287    /// Character dimensions.
288    pub char_dimens: BTreeMap<Char, CharDimensions>,
289
290    /// Character tags.
291    ///
292    /// Note there is no correlation between a character having
293    ///     a tag and a character having a dimension.
294    /// All four combinations of (has or hasn't dimensions) and (has or hasn't a tag)
295    ///     are possible.
296    pub char_tags: BTreeMap<Char, CharTag>,
297
298    /// Tags that have been unset, but whose discriminant is still written to a .tfm file by PLtoTF.
299    pub unset_char_tags: BTreeMap<Char, u8>,
300
301    /// Character widths
302    pub widths: Vec<FixWord>,
303
304    /// Character heights
305    pub heights: Vec<FixWord>,
306
307    /// Character depths
308    pub depths: Vec<FixWord>,
309
310    /// Character italic corrections
311    pub italic_corrections: Vec<FixWord>,
312
313    /// Lig kern program.
314    pub lig_kern_program: ligkern::lang::Program,
315
316    /// Kerns. These are referenced from inside the lig kern commands.
317    pub kerns: Vec<FixWord>,
318
319    /// Extensible characters.
320    pub extensible_chars: Vec<ExtensibleRecipe>,
321
322    /// Font parameters.
323    pub params: Vec<FixWord>,
324}
325
326/// Data about one character in a .tfm file.
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct CharDimensions {
329    /// Index of the width of this character in the widths array.
330    ///
331    /// In valid TFM files, this index will always be non-zero.
332    /// This is because if the width index is zero it means there is no data for the character in the file.
333    /// In this case the other indices (height, etc.) are necessarily zero.
334    /// See TFtoPL.2014.? (where blocks of data with a zero width index are skipped)
335    ///     and PLtoTF.2014.? (where missing characters are written with all indices 0).
336    ///
337    /// There is one edge case where this index can be zero.
338    /// This is if the width index is invalid.
339    /// In this case tftopl essentially sets the width index to 0.
340    ///
341    /// Note that even if a character doesn't have dimensions, it can still have a tag.
342    pub width_index: WidthIndex,
343    /// Index of the height of this character in the height array.
344    pub height_index: u8,
345    pub depth_index: u8,
346    pub italic_index: u8,
347}
348
349#[derive(Copy, Clone, Debug, PartialEq, Eq)]
350pub enum WidthIndex {
351    Invalid,
352    Valid(NonZeroU8),
353}
354
355impl WidthIndex {
356    pub fn valid(&self) -> Option<NonZeroU8> {
357        match self {
358            WidthIndex::Invalid => None,
359            WidthIndex::Valid(n) => Some(*n),
360        }
361    }
362    pub fn get(&self) -> u8 {
363        match self {
364            WidthIndex::Invalid => 0,
365            WidthIndex::Valid(n) => n.get(),
366        }
367    }
368}
369
370/// Tag of a character in a .tfm file.
371#[derive(Clone, Debug, PartialEq, Eq)]
372pub enum CharTag {
373    Ligature(u8),
374    List(Char),
375    Extension(u8),
376}
377
378impl CharTag {
379    pub fn ligature(&self) -> Option<u8> {
380        match self {
381            CharTag::Ligature(l) => Some(*l),
382            CharTag::List(_) | CharTag::Extension(_) => None,
383        }
384    }
385    pub fn list(&self) -> Option<Char> {
386        match self {
387            CharTag::List(c) => Some(*c),
388            CharTag::Ligature(_) | CharTag::Extension(_) => None,
389        }
390    }
391    pub fn extension(&self) -> Option<u8> {
392        match self {
393            CharTag::Extension(u) => Some(*u),
394            CharTag::Ligature(_) | CharTag::List(_) => None,
395        }
396    }
397}
398
399/// Extensible recipe instruction in a .tfm file.
400#[derive(Debug, Default, Clone, PartialEq, Eq)]
401pub struct ExtensibleRecipe {
402    pub top: Option<Char>,
403    pub middle: Option<Char>,
404    pub bottom: Option<Char>,
405    pub rep: Char,
406}
407
408impl ExtensibleRecipe {
409    pub fn is_seven_bit(&self) -> bool {
410        self.chars().all(|c| c.is_seven_bit())
411    }
412
413    pub fn chars(&self) -> impl Iterator<Item = Char> {
414        [self.top, self.middle, self.bottom, Some(self.rep)]
415            .into_iter()
416            .flatten()
417    }
418}
419
420impl Default for File {
421    fn default() -> Self {
422        Self {
423            header: Default::default(),
424            smallest_char: Char(1),
425            char_dimens: Default::default(),
426            char_tags: Default::default(),
427            unset_char_tags: Default::default(),
428            widths: vec![FixWord::ZERO],
429            heights: vec![FixWord::ZERO],
430            depths: vec![FixWord::ZERO],
431            italic_corrections: vec![FixWord::ZERO],
432            lig_kern_program: Default::default(),
433            kerns: vec![],
434            extensible_chars: vec![],
435            params: Default::default(),
436        }
437    }
438}
439
440/// Print debugging information about a .tfm file.
441pub fn debug<'a>(
442    path: Option<&'a str>,
443    sub_file_sizes: SubFileSizes,
444    tfm_file: Option<&'a File>,
445    raw_file: Option<&'a RawFile<'a>>,
446    sections: Vec<Section>,
447) -> impl std::fmt::Display + 'a {
448    debug::Debug {
449        sub_file_sizes,
450        path,
451        raw_file,
452        tfm_file,
453        sections,
454    }
455}
456
457impl File {
458    pub fn from_raw_file(raw_file: &RawFile) -> Self {
459        deserialize::from_raw_file(raw_file)
460    }
461
462    pub fn validate_and_fix(&mut self) -> Vec<ValidationWarning> {
463        validate::validate_and_fix(self)
464    }
465
466    pub fn deserialize(
467        b: &[u8],
468    ) -> (
469        Result<File, DeserializationError>,
470        Vec<DeserializationWarning>,
471    ) {
472        deserialize::deserialize(b)
473    }
474
475    pub fn serialize(&self) -> Vec<u8> {
476        serialize::serialize(self)
477    }
478
479    pub fn replace_lig_kern_program(
480        &mut self,
481        mut program: ligkern::lang::Program,
482        entrypoints: HashMap<Char, u16>,
483    ) {
484        let entrypoints = program.pack_entrypoints(entrypoints);
485        self.kerns = program.unpack_kerns();
486        self.lig_kern_program = program;
487        self.char_tags
488            .retain(|_, tag| !matches!(tag, CharTag::Ligature(_)));
489        for (c, ep) in entrypoints {
490            self.char_tags.insert(c, CharTag::Ligature(ep));
491        }
492    }
493
494    /// Return a map from characters to the lig/kern entrypoint for that character.
495    /// TODO: can probably return impl Iterator<Item=(Char, u8)>
496    pub fn lig_kern_entrypoints(&self) -> HashMap<Char, u8> {
497        self.char_tags
498            .iter()
499            .filter_map(|(c, d)| match *d {
500                CharTag::Ligature(l) => Some((*c, l)),
501                _ => None,
502            })
503            .collect()
504    }
505
506    fn char_info_bounds(&self) -> Option<(Char, Char)> {
507        let mut r: Option<(Char, Char)> = None;
508        for c in self.char_dimens.keys().copied() {
509            r = Some(match r {
510                None => (c, c),
511                Some((lower, upper)) => (
512                    if c.0 < lower.0 { c } else { lower },
513                    if c.0 < upper.0 { upper } else { c },
514                ),
515            })
516        }
517        r
518    }
519
520    /// Calculate the checksum of this .tfm file.
521    pub fn checksum(&self) -> u32 {
522        // This checksum algorithm is in PLtoTF.2014.134.
523        let (bc, ec) = self.char_info_bounds().unwrap_or((Char(1), Char(0)));
524        let mut b = [bc.0, ec.0, bc.0, ec.0];
525        for c in bc.0..=ec.0 {
526            let char_dimens = match self.char_dimens.get(&Char(c)) {
527                None => continue,
528                Some(char_dimens) => char_dimens,
529            };
530            let width = self.widths[char_dimens.width_index.get() as usize].0;
531            // TODO: adjust based on the design units
532            let width = width + (c as i32 + 4) * 0o20_000_000;
533            let add = |b: u8, m: u8| -> u8 {
534                (((b as i32) + (b as i32) + width) % (m as i32))
535                    .try_into()
536                    .expect("(i32 % u8) is always a u8")
537            };
538            b = [
539                add(b[0], 255),
540                add(b[1], 253),
541                add(b[2], 251),
542                add(b[3], 247),
543            ]
544        }
545        u32::from_be_bytes(b)
546    }
547
548    pub fn named_param(&self, param: NamedParameter) -> Option<FixWord> {
549        self.params.get((param.number() - 1) as usize).copied()
550    }
551
552    pub fn named_param_scaled(&self, param: NamedParameter) -> Option<common::Scaled> {
553        self.named_param(param)
554            .map(|p| p.to_scaled(self.header.design_size))
555    }
556
557    pub fn width_utf8(&self, char: char) -> Option<common::Scaled> {
558        let char: Char = char.try_into().ok()?;
559        let dimens = self.char_dimens.get(&char)?;
560        let i = dimens.width_index.valid()?.get();
561        Some(
562            self.widths
563                .get(i as usize)?
564                .to_scaled(self.header.design_size),
565        )
566    }
567    pub fn height_utf8(&self, char: char) -> Option<common::Scaled> {
568        let char: Char = char.try_into().ok()?;
569        let dimens = self.char_dimens.get(&char)?;
570        let i = dimens.height_index;
571        Some(
572            self.heights
573                .get(i as usize)?
574                .to_scaled(self.header.design_size),
575        )
576    }
577    pub fn depth_utf8(&self, char: char) -> Option<common::Scaled> {
578        let char: Char = char.try_into().ok()?;
579        let dimens = self.char_dimens.get(&char)?;
580        let i = dimens.depth_index;
581        Some(
582            self.depths
583                .get(i as usize)?
584                .to_scaled(self.header.design_size),
585        )
586    }
587}
588
589impl From<crate::pl::File> for File {
590    fn from(pl_file: crate::pl::File) -> Self {
591        let mut char_bounds: Option<(Char, Char)> = None;
592
593        let lig_kern_entrypoints = pl_file.lig_kern_entrypoints(true);
594        let mut lig_kern_program = pl_file.lig_kern_program;
595        let kerns = lig_kern_program.unpack_kerns();
596        let lig_kern_entrypoints = lig_kern_program.pack_entrypoints(lig_kern_entrypoints);
597
598        let mut widths = pl_file.additional_widths;
599        let mut heights = pl_file.additional_heights;
600        let mut depths = pl_file.additional_depths;
601        let mut italic_corrections = pl_file.additional_italics;
602        for (char, char_dimens) in &pl_file.char_dimens {
603            widths.push(char_dimens.width.unwrap_or_default());
604            match char_dimens.height {
605                None | Some(FixWord::ZERO) => {}
606                Some(height) => heights.push(height),
607            }
608            match char_dimens.depth {
609                None | Some(FixWord::ZERO) => {}
610                Some(depth) => depths.push(depth),
611            }
612            match char_dimens.italic_correction {
613                None | Some(FixWord::ZERO) => {}
614                Some(italic_correction) => italic_corrections.push(italic_correction),
615            }
616            char_bounds = Some(match char_bounds {
617                None => (*char, *char),
618                Some((lower, upper)) => (
619                    if *char < lower { *char } else { lower },
620                    if *char > upper { *char } else { upper },
621                ),
622            })
623        }
624        let (widths, width_to_index) = compress(&widths, 255);
625        let (heights, height_to_index) = compress(&heights, 15);
626        let (depths, depth_to_index) = compress(&depths, 15);
627        let (italic_corrections, italic_correction_to_index) = compress(&italic_corrections, 63);
628        let mut extensible_chars = vec![];
629        let char_dimens = match char_bounds {
630            None => Default::default(),
631            Some((lower, upper)) => {
632                let mut m: BTreeMap<Char, CharDimensions> = Default::default();
633                for c in lower.0..=upper.0 {
634                    let pl_data = match pl_file.char_dimens.get(&Char(c)) {
635                        Some(pl_data) => pl_data,
636                        None => continue,
637                    };
638                    let width = pl_data.width.unwrap_or_default();
639                    let width_index = *width_to_index.get(&width).expect(
640                        "the map returned from compress(_,_) contains every input as a key",
641                    );
642                    m.insert(
643                        Char(c),
644                        CharDimensions {
645                            width_index: WidthIndex::Valid(width_index),
646                            height_index: match pl_data.height {
647                                None => 0,
648                                Some(height) => {
649                                    // If the height data is missing from the height_to_index map, it's because
650                                    // the height is 0. Similar with depths and italic corrections.
651                                    height_to_index
652                                        .get(&height)
653                                        .copied()
654                                        .map(NonZeroU8::get)
655                                        .unwrap_or(0)
656                                }
657                            },
658                            depth_index: match pl_data.depth {
659                                None => 0,
660                                Some(depth) => depth_to_index
661                                    .get(&depth)
662                                    .copied()
663                                    .map(NonZeroU8::get)
664                                    .unwrap_or(0),
665                            },
666                            italic_index: match pl_data.italic_correction {
667                                None => 0,
668                                Some(italic_correction) => italic_correction_to_index
669                                    .get(&italic_correction)
670                                    .copied()
671                                    .map(NonZeroU8::get)
672                                    .unwrap_or(0),
673                            },
674                        },
675                    );
676                }
677                m
678            }
679        };
680        let ordered_chars = {
681            let mut m: Vec<Char> = pl_file.char_tags.keys().copied().collect();
682            m.sort();
683            m
684        };
685        let char_tags = ordered_chars.into_iter().map(|c| {
686            (c,
687                match pl_file.char_tags.get(&c).unwrap() {
688                    pl::CharTag::Ligature(_) => {
689                        let entrypoint = *lig_kern_entrypoints.get(&c).expect("the map returned by crate::ligkern::lang::compress_entrypoints has a key for all chars with a lig tag");
690                        CharTag::Ligature(entrypoint)
691                    }
692                    pl::CharTag::List(c) => CharTag::List(*c),
693                    pl::CharTag::Extension(e) => {
694                        let index: u8 = extensible_chars.len().try_into().unwrap();
695                        extensible_chars.push(e.clone());
696                        CharTag::Extension(index)
697                    }
698                },
699            )
700        }).collect();
701
702        let mut file = Self {
703            header: pl_file.header,
704            smallest_char: char_bounds.map(|t| t.0).unwrap_or(Char(1)),
705            char_dimens,
706            char_tags,
707            unset_char_tags: pl_file.unset_char_tags.clone(),
708            widths,
709            heights,
710            depths,
711            italic_corrections,
712            lig_kern_program,
713            kerns,
714            extensible_chars,
715            params: pl_file.params,
716        };
717        if file.header.checksum.is_none() {
718            file.header.checksum = Some(file.checksum());
719        }
720        file
721    }
722}
723
724/// Lossy compression of numbers for TFM files.
725///
726/// The TFM file format can only store up to 15 heights, 15 depths and 63 italic corrections.
727/// If a property list file contains, e.g., more than 15 distinct heights, something has to give.
728/// PLtoTF contains a lossy compression algorithm that takes a list of values and returns another
729/// bounded list of values which approximates the original list.
730/// This is implemented in PLtoTF.2014.75-80 and re-implemented here.
731///
732/// ## How the algorithm works.
733///
734/// For a given delta, the algorithm partitions the ordered list of values such that within
735/// each partition the maximum distance between two elements is delta. All of the values within
736/// the partition are then approximated by `(interval_max+interval_min)/2`.
737/// As such, each value may be increased or decreased by up to `delta/2`.
738/// After this procedure, the list of values is replaced by the list of approximations.
739/// This compresses the list of values into a list whose size is the number of intervals.
740///
741/// Given this subroutine, the algorithm finds the smallest delta such that the number of intervals
742/// is less than the required maximum (e.g., 15 for heights).
743///
744/// There are some important features of this algorithm to note:
745///
746/// - For a given delta value there may be multiple possible partitions.
747///   The algorithm uses a greedy approach in which it maximizes the size of the first partition,
748///   then the size of the second partition, and so on.
749///
750/// - Distinct delta values can yield the same partition.
751///   For example, if the initial values are `[1, 4, 5]` then any delta in the range `[1, 3)`
752///   gives the same result (`[[1], [4, 5]]`)
753///   Whenever we check a delta, we are really checking the _interval of deltas_  that gives the same result.
754///   Both Knuth's and our implementations use this fact to speed up the search by reducing the search space.
755///   E.g. in the example above, after checking `delta=1` we _don't_ check `delta=2`.
756///
757/// ## This re-implementation
758///
759/// The re-implementation here follows PLtoTF closely enough, but with one modification.
760/// To find the optimal delta, Knuth first calculates the minimal possible delta.
761/// This is the minimum distance between adjacent elements in the ordered list of values.
762/// In general this will not be a valid solution because the number of intervals
763///     it generates will be large.
764/// So, he next finds the smallest `k` such that `2^k * min_delta` is a valid solution.
765/// Te then does a upwards linear search within the interval `[min_delta * 2^{k-1}, min_delta * 2^k]` to find
766///     the optimal delta.
767///
768/// Checking a particular delta is `O(n)`.
769/// The worst-case running time of Knuth's algorithm is then `O(n^3)` because the interval
770///     `[2^{k-1}, 2^k]` can contain `O(n^2)` distinct deltas to check in the worst-case [Note 1].
771///
772/// In the re-implementation here we realize that the problem of finding the smallest possible delta
773///     is a classic binary search problem.
774/// This is because if delta is a valid solution, any larger delta also is;
775///     and if delta is not a valid solution, any smaller delta is also not a valid solution.
776/// The re-implementation using binary search is `O(n log n)`.
777/// Moreover, the constant factors are about the same.
778///
779/// [Note 1] In the worst-case there are O(n^2) distinct deltas because each pair of elements yields a delta.
780/// Let m be the smallest delta and M the largest delta.
781/// In the initial `2^k`-based ranging scheme, the largest `K` satisfies `m 2^{K-1} < M <= m 2^K`,
782/// or `K-1 <= log_2(M/m)`. Thus there are `K=O(1)` ranges in this initial scheme.
783/// By the pigeon-hole principle, there exists a `k` such that the range `[m * 2^{k-1}, m * 2^k]`
784///     contains O(n^2) elements.
785/// In the worst-case, the solution is the maximum element of this range.
786pub fn compress(values: &[FixWord], max_size: u8) -> (Vec<FixWord>, HashMap<FixWord, NonZeroU8>) {
787    let max_size = max_size as usize;
788    let dedup_values = {
789        let s: HashSet<FixWord> = values.iter().copied().collect();
790        // remove the zero value for non-widths
791        // and then add it back in at the start
792        let mut v: Vec<FixWord> = s.into_iter().collect();
793        v.sort();
794        v
795    };
796    // After deduplication, it is possible we don't need to compress at all so we can exit early.
797    // This also handles the case when the values slice is empty.
798    if dedup_values.len() <= max_size {
799        let m: HashMap<FixWord, NonZeroU8> = dedup_values
800            .iter()
801            .enumerate()
802            .map(|(i, &w)| {
803                let i: u8 = i.try_into().expect("`dedup_values` has at most `max_size` elements, so the index is at most `max_size-1`");
804                let i: NonZeroU8 = (i+1).try_into().expect("`i<=max_size-1<=u8::MAX`, so `i+1<=u8::MAX`");
805                (w, i) })
806            .collect();
807        let mut dedup_values = dedup_values;
808        dedup_values.push(FixWord::ZERO);
809        dedup_values.rotate_right(1);
810        return (dedup_values, m);
811    }
812
813    // For the binary search we maintain lower and upper indices as usual.
814    // The optimal delta is in the interval [lower, upper].
815    //
816    // Invariant: delta<lower is never a solution.
817    // Because delta must be non-negative, we initialize it to zero.
818    let mut lower = FixWord::ZERO;
819    // Invariant: delta=upper is always solution.
820    // To initialize upper and begin the search we construct a solution that always works: a single
821    // interval encompassing the entire slice and the largest delta possible.
822    let max_delta = *dedup_values.last().unwrap() - *dedup_values.first().unwrap();
823    let mut upper = max_delta;
824    let mut solution = vec![dedup_values.len()];
825
826    let mut buffer = vec![];
827    while lower < upper {
828        // After the following line delta is potentially equal to lower. This is what we want as
829        // we know upper is a solution so to advance the search when upper=lower+1
830        // we need to check lower+1.
831        let delta = lower + (upper - lower) / 2;
832
833        let mut interval_start = *dedup_values.first().unwrap();
834        // The smallest delta such that the candidate solution will be the same.
835        // This is the maximum of all gaps that don't start a new interval.
836        let mut delta_lower = FixWord::ZERO;
837        // The largest delta such that the candidate solution will be different.
838        // This is the minimum of all gaps that start a new interval.
839        let mut delta_upper = max_delta;
840        for (i, &v) in dedup_values.iter().enumerate() {
841            let gap = v - interval_start;
842            if gap > delta {
843                // We need to start a new interval
844                if gap < delta_upper {
845                    delta_upper = gap;
846                }
847                buffer.push(i);
848                // If the candidate solution is already too big, we can exit early.
849                if buffer.len() >= max_size {
850                    break;
851                }
852                interval_start = v;
853            } else {
854                // We need to extend the current interval
855                // For any delta in the range [gap, delta] we would have made the same choice here.
856                if gap > delta_lower {
857                    delta_lower = gap;
858                }
859            }
860        }
861        buffer.push(dedup_values.len());
862
863        if buffer.len() <= max_size {
864            // solution
865            std::mem::swap(&mut buffer, &mut solution);
866            upper = delta_lower;
867        } else {
868            // not a solution
869            lower = delta_upper;
870        }
871        buffer.clear();
872    }
873
874    let mut value_to_index = HashMap::<FixWord, NonZeroU8>::new();
875    let mut result = vec![FixWord::ZERO];
876    let mut previous = 0_usize;
877    for i in solution {
878        let interval = &dedup_values[previous..i];
879        previous = i;
880        for &v in interval {
881            let index: u8 = result.len().try_into().expect("the `result` array contains at most `1+max_size` elements, so the index it at most `max_size` which is a u8");
882            let index: NonZeroU8 = index
883                .try_into()
884                .expect("the `result` array contains at least 1 element so this is never 0");
885            value_to_index.insert(v, index);
886        }
887        let replacement = (*interval.last().unwrap() + *interval.first().unwrap()) / 2;
888        result.push(replacement);
889    }
890
891    (result, value_to_index)
892}
893
894#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
895pub enum Section {
896    /// Sub-file sizes
897    SubFileSizes,
898    /// Header
899    Header,
900    /// Character data
901    CharInfos,
902    /// Widths array
903    Widths,
904    /// Heights array
905    Heights,
906    /// Depths array
907    Depths,
908    /// Italic corrections array
909    ItalicCorrections,
910    /// Lig/kern instructions
911    LigKern,
912    /// Kerns array
913    Kerns,
914    /// Extensible recipes
915    ExtensibleRecipes,
916    /// Params array
917    Params,
918}
919
920impl Section {
921    pub const NAMES: [&'static str; 11] = [
922        "sub-file-sizes",
923        "header",
924        "char-infos",
925        "widths",
926        "heights",
927        "depths",
928        "italic-corrections",
929        "lig-kern",
930        "kerns",
931        "extensible-recipes",
932        "params",
933    ];
934    pub const ALL_SECTIONS: [Section; 11] = [
935        Section::SubFileSizes,
936        Section::Header,
937        Section::CharInfos,
938        Section::Widths,
939        Section::Heights,
940        Section::Depths,
941        Section::ItalicCorrections,
942        Section::LigKern,
943        Section::Kerns,
944        Section::ExtensibleRecipes,
945        Section::Params,
946    ];
947}
948
949impl TryFrom<&str> for Section {
950    type Error = ();
951
952    fn try_from(value: &str) -> Result<Self, Self::Error> {
953        for i in 0..Section::NAMES.len() {
954            if Section::NAMES[i] == value {
955                return Ok(Section::ALL_SECTIONS[i]);
956            }
957        }
958        Err(())
959    }
960}
961
962impl std::fmt::Display for Section {
963    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
964        write!(f, "{}", Section::NAMES[*self as usize])
965    }
966}
967
968/// The TFM header, which contains metadata about the file.
969///
970/// This is defined in TFtoPL.2014.10.
971#[derive(Clone, Debug, Default, PartialEq, Eq)]
972pub struct Header {
973    /// The font checksum, if specified.
974    ///
975    /// In .tfm files checksums are always specified because the format has no
976    ///     way to omit a checksum.
977    ///
978    /// In .pl files checksums are specified if the `CHECKSUM` node appears.
979    /// If no checksum is specified in a .pl file, pltotf calculates the
980    ///     correct value and writes that.
981    ///
982    /// In TeX82, this is stored in the `font_check` array (TeX82.2021.549).
983    pub checksum: Option<u32>,
984    /// In TeX82, this is stored in the `font_dsize` array (TeX82.2021.549).
985    pub design_size: FixWord,
986    pub design_size_valid: bool,
987    pub character_coding_scheme: Option<String>,
988    pub font_family: Option<String>,
989    pub seven_bit_safe: Option<bool>,
990    pub face: Option<Face>,
991    /// The TFM format allows the header to contain arbitrary additional data.
992    pub additional_data: Vec<u32>,
993}
994
995impl Header {
996    /// Returns the default header when parsing property list files.
997    ///
998    /// This is defined in PLtoTF.2014.70.
999    pub fn pl_default() -> Header {
1000        Header {
1001            checksum: None,
1002            design_size: FixWord::ONE * 10,
1003            design_size_valid: true,
1004            character_coding_scheme: Some("UNSPECIFIED".into()),
1005            font_family: Some("UNSPECIFIED".into()),
1006            seven_bit_safe: None,
1007            face: Some(0.into()),
1008            additional_data: vec![],
1009        }
1010    }
1011
1012    /// Returns the default header when parsing .tfm files.
1013    ///
1014    /// This is defined in PLtoTF.2014.70.
1015    pub fn tfm_default() -> Header {
1016        Header {
1017            checksum: Some(0),
1018            design_size: FixWord::ZERO,
1019            design_size_valid: true,
1020            character_coding_scheme: None,
1021            font_family: None,
1022            seven_bit_safe: None,
1023            face: None,
1024            additional_data: vec![],
1025        }
1026    }
1027}
1028
1029/// A character in a TFM file.
1030///
1031/// TFM and PL files only support 1-byte characters.
1032#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
1033#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1034pub struct Char(pub u8);
1035
1036impl std::fmt::Display for Char {
1037    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1038        if (self.0 as char).is_ascii_graphic() {
1039            write!(f, "{}", self.0 as char)
1040        } else {
1041            write!(f, "\\u{{{:02x}}}", self.0)
1042        }
1043    }
1044}
1045
1046impl From<u8> for Char {
1047    fn from(value: u8) -> Self {
1048        Char(value)
1049    }
1050}
1051
1052impl TryFrom<char> for Char {
1053    type Error = std::char::TryFromCharError;
1054
1055    fn try_from(value: char) -> Result<Self, Self::Error> {
1056        let u: u8 = value.try_into()?;
1057        Ok(Char(u))
1058    }
1059}
1060
1061impl From<Char> for char {
1062    fn from(value: Char) -> Self {
1063        value.0 as char
1064    }
1065}
1066
1067macro_rules! const_chars {
1068    ( $( ($name: ident, $value: expr), )+ ) => {
1069        $(
1070            pub const $name: Char = Char($value);
1071        )+
1072    };
1073}
1074
1075impl Char {
1076    const_chars![
1077        (A, b'A'),
1078        (B, b'B'),
1079        (C, b'C'),
1080        (D, b'D'),
1081        (V, b'V'),
1082        (W, b'W'),
1083        (X, b'X'),
1084        (Y, b'Y'),
1085        (Z, b'Z'),
1086    ];
1087
1088    pub fn is_seven_bit(&self) -> bool {
1089        self.0 <= 127
1090    }
1091}
1092
1093#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1094#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1095pub enum FaceWeight {
1096    Light,
1097    Medium,
1098    Bold,
1099}
1100
1101#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1102#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1103pub enum FaceSlope {
1104    Roman,
1105    Italic,
1106}
1107
1108#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1109#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1110pub enum FaceExpansion {
1111    Regular,
1112    Condensed,
1113    Extended,
1114}
1115
1116#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1117#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1118pub enum Face {
1119    Valid(FaceWeight, FaceSlope, FaceExpansion),
1120    Other(u8),
1121}
1122
1123impl From<u8> for Face {
1124    fn from(value: u8) -> Self {
1125        if value >= 18 {
1126            return Face::Other(value);
1127        }
1128        let a = match (value % 6) / 2 {
1129            0 => FaceWeight::Medium,
1130            1 => FaceWeight::Bold,
1131            2 => FaceWeight::Light,
1132            _ => unreachable!(),
1133        };
1134        let b = match value % 2 {
1135            0 => FaceSlope::Roman,
1136            1 => FaceSlope::Italic,
1137            _ => unreachable!(),
1138        };
1139        let c = match value / 6 {
1140            0 => FaceExpansion::Regular,
1141            1 => FaceExpansion::Condensed,
1142            2 => FaceExpansion::Extended,
1143            _ => unreachable!(),
1144        };
1145        Face::Valid(a, b, c)
1146    }
1147}
1148
1149impl From<Face> for u8 {
1150    fn from(value: Face) -> Self {
1151        match value {
1152            Face::Valid(w, s, c) => {
1153                let a: u8 = match w {
1154                    FaceWeight::Medium => 0,
1155                    FaceWeight::Bold => 1,
1156                    FaceWeight::Light => 2,
1157                };
1158                let b: u8 = match s {
1159                    FaceSlope::Roman => 0,
1160                    FaceSlope::Italic => 1,
1161                };
1162                let c: u8 = match c {
1163                    FaceExpansion::Regular => 0,
1164                    FaceExpansion::Condensed => 1,
1165                    FaceExpansion::Extended => 2,
1166                };
1167                c * 6 + a * 2 + b
1168            }
1169            Face::Other(b) => b,
1170        }
1171    }
1172}
1173
1174/// A named TeX font metric parameter.
1175#[derive(PartialEq, Eq, Debug, Copy, Clone)]
1176#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1177pub enum NamedParameter {
1178    Slant,
1179    Space,
1180    Stretch,
1181    Shrink,
1182    XHeight,
1183    Quad,
1184    ExtraSpace,
1185    Num1,
1186    Num2,
1187    Num3,
1188    Denom1,
1189    Denom2,
1190    Sup1,
1191    Sup2,
1192    Sup3,
1193    Sub1,
1194    Sub2,
1195    SupDrop,
1196    SubDrop,
1197    Delim1,
1198    Delim2,
1199    AxisHeight,
1200    DefaultRuleThickness,
1201    BigOpSpacing1,
1202    BigOpSpacing2,
1203    BigOpSpacing3,
1204    BigOpSpacing4,
1205    BigOpSpacing5,
1206}
1207
1208impl NamedParameter {
1209    pub fn number(&self) -> u8 {
1210        match self {
1211            NamedParameter::Slant => 1,
1212            NamedParameter::Space => 2,
1213            NamedParameter::Stretch => 3,
1214            NamedParameter::Shrink => 4,
1215            NamedParameter::XHeight => 5,
1216            NamedParameter::Quad => 6,
1217            NamedParameter::ExtraSpace => 7,
1218            NamedParameter::Num1 => 8,
1219            NamedParameter::Num2 => 9,
1220            NamedParameter::Num3 => 10,
1221            NamedParameter::Denom1 => 11,
1222            NamedParameter::Denom2 => 12,
1223            NamedParameter::Sup1 => 13,
1224            NamedParameter::Sup2 => 14,
1225            NamedParameter::Sup3 => 15,
1226            NamedParameter::Sub1 => 16,
1227            NamedParameter::Sub2 => 17,
1228            NamedParameter::SupDrop => 18,
1229            NamedParameter::SubDrop => 19,
1230            NamedParameter::Delim1 => 20,
1231            NamedParameter::Delim2 => 21,
1232            NamedParameter::AxisHeight => 22,
1233            NamedParameter::DefaultRuleThickness => 8,
1234            NamedParameter::BigOpSpacing1 => 9,
1235            NamedParameter::BigOpSpacing2 => 10,
1236            NamedParameter::BigOpSpacing3 => 11,
1237            NamedParameter::BigOpSpacing4 => 12,
1238            NamedParameter::BigOpSpacing5 => 13,
1239        }
1240    }
1241}
1242
1243/// Warning from the compilation of "next larger character" instructions.
1244#[derive(PartialEq, Eq, Debug, Clone)]
1245pub enum NextLargerProgramWarning {
1246    NonExistentCharacter { original: Char, next_larger: Char },
1247    InfiniteLoop { original: Char, next_larger: Char },
1248}
1249
1250impl NextLargerProgramWarning {
1251    pub fn bad_char(&self) -> Char {
1252        match self {
1253            NextLargerProgramWarning::NonExistentCharacter {
1254                original,
1255                next_larger: _,
1256            } => *original,
1257            NextLargerProgramWarning::InfiniteLoop { original, .. } => *original,
1258        }
1259    }
1260
1261    /// Returns the warning message the TFtoPL program prints for this kind of error.
1262    pub fn tftopl_message(&self) -> String {
1263        match self {
1264            NextLargerProgramWarning::NonExistentCharacter {
1265                original: _,
1266                next_larger,
1267            } => {
1268                format![
1269                    "Bad TFM file: Character list link to nonexistent character '{:03o}.",
1270                    next_larger.0
1271                ]
1272            }
1273            NextLargerProgramWarning::InfiniteLoop { original, .. } => {
1274                format!["Bad TFM file: Cycle in a character list!\nCharacter '{:03o} now ends the list.", original.0]
1275            }
1276        }
1277    }
1278
1279    /// Returns the section in Knuth's TFtoPL (version 2014) in which this warning occurs.
1280    pub fn tftopl_section(&self) -> u8 {
1281        84
1282    }
1283
1284    /// Returns the section in Knuth's PLtoTF (version 2014) in which this warning occurs.
1285    pub fn pltotf_section(&self) -> u8 {
1286        match self {
1287            NextLargerProgramWarning::NonExistentCharacter { .. } => 111,
1288            NextLargerProgramWarning::InfiniteLoop { .. } => 113,
1289        }
1290    }
1291
1292    /// Returns the warning message the PLtoTF program prints for this kind of error.
1293    pub fn pltotf_message(&self) -> String {
1294        match self {
1295            NextLargerProgramWarning::NonExistentCharacter {
1296                original,
1297                next_larger: _,
1298            } => {
1299                format![
1300                    "The character NEXTLARGER than '{:03o} had no CHARACTER spec.",
1301                    original.0
1302                ]
1303            }
1304            NextLargerProgramWarning::InfiniteLoop { original, .. } => {
1305                format![
1306                    "A cycle of NEXTLARGER characters has been broken at '{:03o}.",
1307                    original.0
1308                ]
1309            }
1310        }
1311    }
1312}
1313/// Compiled program of "next larger character" instructions
1314///
1315/// The .tfm file format can associate a "next larger" character to any character in a font.
1316/// Next larger characters form sequences: i.e. B can be the next larger character for A,
1317///     and C can be the next larger character for B,
1318///     leading to the sequences A-B-C.
1319/// These next larger characters are used at certain points in TeX.
1320/// TeX occasionally traverses the entire sequence for a given starting character (e.g. A).
1321///
1322/// As with ligatures, next larger specifications can contain infinite loops -
1323///     e.g, if X is the next larger character for Y
1324///      and Y is the next larger character for X.
1325/// These loops are invalid and removed by TFtoPL and PLtoTF.
1326///
1327/// Motivated by the idea of "parse don't validate", this type represents
1328///     a compiled version of the next larger specifications in which infinite loops
1329///     are statically guaranteed not to exist.
1330///
1331/// The basic use of a valid program looks like this:
1332///
1333/// ```
1334/// # use tfm::*;
1335/// let edges = vec![
1336///     (Char::A, Char::B),
1337///     (Char::B, Char::C),
1338/// ];
1339/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1340///
1341/// assert_eq![warnings, vec![]];
1342///
1343/// let sequence_A: Vec<Char> = next_larger_program.get(Char::A).collect();
1344/// assert_eq!(sequence_A, vec![Char::B, Char::C]);
1345///
1346/// let sequence_B: Vec<Char> = next_larger_program.get(Char::B).collect();
1347/// assert_eq!(sequence_B, vec![Char::C]);
1348///
1349/// let sequence_C: Vec<Char> = next_larger_program.get(Char::C).collect();
1350/// assert_eq!(sequence_C, vec![]);
1351///
1352/// // Character that is not in the program.
1353/// let sequence_D: Vec<Char> = next_larger_program.get(Char::D).collect();
1354/// assert_eq!(sequence_D, vec![]);
1355/// ```
1356///
1357/// ## Warnings
1358///
1359/// There are two types of error that can occur when constructing the next
1360///     larger program.
1361/// Both of these errors are handled gracefully, so we officially refer to them as warnings.
1362/// The constructor returns them as values of type [`NextLargerProgramWarning`].
1363///
1364/// ### Infinite loops
1365///
1366/// The first error is that next larger programs can contain infinite loops -
1367///     e.g, if X is the next larger character for Y
1368///      and Y is the next larger character for X.
1369/// In this case the loop is broken by removing the next larger program for the
1370///     character with the largest 8-bit code, in this case Y.
1371/// A [`NextLargerProgramWarning::InfiniteLoop`] warning is returned
1372///     from the program constructor.
1373///
1374/// ```
1375/// # use tfm::*;
1376/// let edges = vec![
1377///     (Char::X, Char::Y),
1378///     (Char::Y, Char::X),
1379/// ];
1380/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1381///
1382/// assert_eq!(warnings, vec![NextLargerProgramWarning::InfiniteLoop{
1383///     original: Char::Y,
1384///     next_larger: Char::X,
1385/// }]);
1386///
1387/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1388/// assert_eq!(sequence_X, vec![Char::Y]);
1389///
1390/// let sequence_Y: Vec<Char> = next_larger_program.get(Char::Y).collect();
1391/// assert_eq!(sequence_Y, vec![]);
1392/// ```
1393///
1394/// ### Non-existent characters
1395///
1396/// The second error is that characters referred to in the next larger program
1397///     may not be defined in the .tfm or .pl file.
1398/// For example, a .pl file may contain the snippet `(CHARACTER C X (NEXTLARGER C Y))`
1399///     without defining the character Y.
1400/// The constructor [`NextLargerProgram::new`] accepts a function for checking if a
1401///     character exists.
1402/// In all cases a [`NextLargerProgramWarning::NonExistentCharacter`] warning is returned
1403///     if a non-existent character is encountered.
1404///
1405/// The behavior of the resulting next larger program is configured using the
1406///     `drop_non_existent_characters` argument.
1407/// If this is false, then the behavior is the same as PLtoTF and the program still
1408///     contains the character.
1409///
1410/// ```
1411/// # use tfm::*;
1412/// let edges = vec![
1413///     (Char::X, Char::Y),
1414/// ];
1415/// let character_exists = |c| {
1416///     if c == Char::Y {
1417///         false
1418///     } else {
1419///         true
1420///     }
1421/// };
1422/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), character_exists, false);
1423///
1424/// assert_eq!(warnings, vec![NextLargerProgramWarning::NonExistentCharacter{
1425///     original: Char::X,
1426///     next_larger: Char::Y,
1427/// }]);
1428///
1429/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1430/// assert_eq!(sequence_X, vec![Char::Y]);
1431/// ```
1432///
1433/// If `drop_non_existent_characters` is true, next larger instructions pointing at non-existent
1434///     characters are dropped.
1435/// This is how TFtoPL behaves.
1436///
1437///
1438/// ```
1439/// # use tfm::*;
1440/// let edges = vec![
1441///     (Char::X, Char::Y),
1442/// ];
1443/// let character_exists = |c| {
1444///     if c == Char::Y {
1445///         false
1446///     } else {
1447///         true
1448///     }
1449/// };
1450/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), character_exists, true);
1451///
1452/// assert_eq!(warnings, vec![NextLargerProgramWarning::NonExistentCharacter{
1453///     original: Char::X,
1454///     next_larger: Char::Y,
1455/// }]);
1456///
1457/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1458/// assert_eq!(sequence_X, vec![]);
1459/// ```
1460///
1461#[derive(Clone, Debug)]
1462pub struct NextLargerProgram {
1463    entrypoints: HashMap<Char, u8>,
1464    next_larger: Vec<(Char, NonZeroU8)>,
1465}
1466
1467impl NextLargerProgram {
1468    /// Build a new next larger program from an iterator over edges.
1469    pub fn new<I: Iterator<Item = (Char, Char)>, F: Fn(Char) -> bool>(
1470        edges: I,
1471        character_exists: F,
1472        drop_non_existent_characters: bool,
1473    ) -> (Self, Vec<NextLargerProgramWarning>) {
1474        // This function implements functionality in TFtoPL.2014.84 and PLtoTF.2014.{110,111,113}.
1475        let mut warnings: Vec<NextLargerProgramWarning> = vec![];
1476
1477        let mut node_to_larger = HashMap::<Char, Char>::new();
1478        let mut node_to_num_smaller = HashMap::<Char, usize>::new();
1479        for (smaller, larger) in edges {
1480            if !character_exists(larger) {
1481                warnings.push(NextLargerProgramWarning::NonExistentCharacter {
1482                    original: smaller,
1483                    next_larger: larger,
1484                });
1485                if drop_non_existent_characters {
1486                    continue;
1487                }
1488            }
1489            node_to_larger.insert(smaller, larger);
1490            node_to_num_smaller.entry(smaller).or_default();
1491            *node_to_num_smaller.entry(larger).or_default() += 1;
1492        }
1493
1494        let mut leaves: Vec<Char> = vec![];
1495        let mut non_leaves: BTreeSet<Char> = Default::default();
1496        for (node, num_smaller) in &node_to_num_smaller {
1497            if *num_smaller == 0 {
1498                leaves.push(*node);
1499            } else {
1500                non_leaves.insert(*node);
1501            }
1502        }
1503
1504        let mut sorted_chars = vec![];
1505        let mut infinite_loop_warnings = vec![];
1506        loop {
1507            while let Some(smaller) = leaves.pop() {
1508                if let Some(larger) = node_to_larger.get(&smaller).copied() {
1509                    let num_smaller = node_to_num_smaller
1510                        .get_mut(&larger)
1511                        .expect("`node_to_num_smaller` contains all nodes");
1512                    *num_smaller = num_smaller
1513                        .checked_sub(1)
1514                        .expect("the larger of a smaller must have at least one smaller");
1515                    if *num_smaller == 0 {
1516                        leaves.push(larger);
1517                        non_leaves.remove(&larger);
1518                    }
1519                }
1520                sorted_chars.push(smaller);
1521            }
1522
1523            // There could be pending nodes left because of a cycle.
1524            // We break the cycle at the largest node.
1525            let smaller = match non_leaves.last() {
1526                None => break,
1527                Some(child) => *child,
1528            };
1529            let larger = node_to_larger.remove(&smaller).expect(
1530                "General graph fact: sum_(node)#in_edges(node)=sum_(node)#out_edges(node).
1531                Fact about the next larger graph: #out_edges(node)<=1, because each char has at most one next larger char.
1532                If #out_edge(child)=0 then sum_(node)#in_edges(node)=sum_(node)#out_edges(node) < #nodes.
1533                Thus there exists another node with #in_edges(node)=0 and that node is a leaf.
1534                But `leaves.len()=0` at this line of code",
1535            );
1536            infinite_loop_warnings.push(NextLargerProgramWarning::InfiniteLoop {
1537                original: smaller,
1538                next_larger: larger,
1539            });
1540            leaves.push(larger);
1541            non_leaves.remove(&larger);
1542        }
1543        warnings.extend(infinite_loop_warnings.into_iter().rev());
1544
1545        let next_larger = {
1546            let parents: HashSet<Char> = node_to_larger.values().copied().collect();
1547            let mut node_to_position = HashMap::<Char, u8>::new();
1548            let mut next_larger: Vec<(Char, NonZeroU8)> = vec![];
1549
1550            for c in sorted_chars.iter().rev() {
1551                if !parents.contains(c) {
1552                    continue;
1553                }
1554                // The present character is the parent of at least one child, aka it is the next larger
1555                // character for another character. So it needs to be in the next_larger array.
1556                let child_position: u8 = next_larger
1557                    .len()
1558                    .try_into()
1559                    .expect("there are at most u8::MAX chars in the `next_larger` array");
1560                let offset = match node_to_larger.get(c) {
1561                    // The next_larger array contains at most 256 elements: one for each char.
1562                    // (Actually it contains at most 255 because one character necessarily does not
1563                    // have a child node and this character does not appear in the array.)
1564                    // Anyway, an offset of 256 sends the index outside the array bound, and so
1565                    // subsequent calls to iterator return None.
1566                    None => NonZeroU8::MAX,
1567                    Some(parent) => {
1568                        let parent_position = *node_to_position
1569                            .get(parent)
1570                            .expect("parent has already been inserted");
1571                        child_position
1572                            .checked_sub(parent_position)
1573                            .expect("parent inserted before so its position it is strictly smaller")
1574                            .try_into()
1575                            .expect("parent inserted before so its position it is strictly smaller")
1576                    }
1577                };
1578                next_larger.push((*c, offset));
1579                node_to_position.insert(*c, child_position);
1580            }
1581            next_larger.reverse();
1582            next_larger
1583        };
1584
1585        let entrypoints = {
1586            let node_to_position: HashMap<Char, u8> = next_larger
1587                .iter()
1588                .enumerate()
1589                .map(|(i, (c, _))| {
1590                    let u: u8 = i
1591                        .try_into()
1592                        .expect("there are at most u8::MAX chars in the `next_larger` array");
1593                    (*c, u)
1594                })
1595                .collect();
1596            let mut entrypoints = HashMap::<Char, u8>::new();
1597            for c in sorted_chars.iter().rev() {
1598                if let Some(parent) = node_to_larger.get(c) {
1599                    entrypoints.insert(
1600                        *c,
1601                        *node_to_position
1602                            .get(parent)
1603                            .expect("parent has already been inserted"),
1604                    );
1605                }
1606            }
1607            entrypoints
1608        };
1609
1610        (
1611            Self {
1612                entrypoints,
1613                next_larger,
1614            },
1615            warnings,
1616        )
1617    }
1618
1619    /// Get the next larger sequence for a character
1620    pub fn get(&self, c: Char) -> impl Iterator<Item = Char> + '_ {
1621        NextLargerProgramIter {
1622            current: self.entrypoints.get(&c).copied(),
1623            program: self,
1624        }
1625    }
1626
1627    /// Returns whether this program is seven-bit safe.
1628    ///
1629    /// A next larger program is seven-bit safe if the next larger sequences for
1630    ///     seven-bit characters only contain seven-bit characters.
1631    /// Conversely a program is seven-bit unsafe if there is a seven-bit
1632    ///     character whose next larger sequence contains a non-seven-bit character.
1633    ///
1634    /// ```
1635    /// # use tfm::*;
1636    /// let edges = vec![
1637    ///     (Char(250), Char(125)),
1638    ///     (Char(125), Char(126)),
1639    /// ];
1640    /// let (next_larger_program, _) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1641    /// assert_eq!(true, next_larger_program.is_seven_bit_safe());
1642    ///
1643    /// let edges = vec![
1644    ///     (Char(125), Char(250)),
1645    /// ];
1646    /// let (next_larger_program, _) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1647    /// assert_eq!(false, next_larger_program.is_seven_bit_safe());
1648    /// ```
1649    pub fn is_seven_bit_safe(&self) -> bool {
1650        // For each c, we only need to check the first element in c's next larger sequence.
1651        // If there is a subsequent element d of the sequence that is seven-bit unsafe,
1652        // we will find it when considering one of d's children.
1653        // This optimization makes this function O(n), rather than worst case O(n^2).
1654        self.entrypoints
1655            .keys()
1656            .copied()
1657            .filter(Char::is_seven_bit)
1658            .filter_map(|c| self.get(c).next())
1659            .all(|c| c.is_seven_bit())
1660    }
1661}
1662
1663#[derive(Clone, Debug)]
1664struct NextLargerProgramIter<'a> {
1665    current: Option<u8>,
1666    program: &'a NextLargerProgram,
1667}
1668
1669impl<'a> Iterator for NextLargerProgramIter<'a> {
1670    type Item = Char;
1671
1672    fn next(&mut self) -> Option<Self::Item> {
1673        // Note that the iterator is statically guaranteed to terminate!
1674        // If it returns None, it has already terminated.
1675        // If it returns Some, then self.current will be incremented by a strictly
1676        //  positive number.
1677        // Incrementing like this can only happen a finite number of times before overflow
1678        //  occurs, and then self.current is None and the iterator is terminated.
1679        match self.current {
1680            None => None,
1681            Some(current) => match self.program.next_larger.get(current as usize) {
1682                None => None,
1683                Some((c, inc)) => {
1684                    self.current = current.checked_add(inc.get());
1685                    Some(*c)
1686                }
1687            },
1688        }
1689    }
1690}
1691
1692impl common::FontFormat for File {
1693    const DEFAULT_FILE_EXTENSION: &'static str = "tfm";
1694    type Error = DeserializationError;
1695
1696    fn parse(b: &[u8]) -> Result<Self, Self::Error> {
1697        File::deserialize(b).0
1698    }
1699}
1700
1701/// Fixed-width numeric type used in TFM files.
1702///
1703/// This numeric type has 11 bits for the integer part,
1704/// 20 bits for the fractional part, and a single signed bit.
1705/// The inner value is the number multiplied by 2^20.
1706/// It is called a `fix_word` in TFtoPL.
1707///
1708/// In property list files, this type is represented as a decimal number
1709///   with up to 6 digits after the decimal point.
1710/// This is a non-lossy representation
1711///   because 10^(-6) is larger than 2^(-20).
1712#[derive(Default, PartialEq, Eq, Debug, Copy, Clone, PartialOrd, Ord, Hash)]
1713#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1714pub struct FixWord(pub i32);
1715
1716impl FixWord {
1717    /// Representation of the number 0 as a [FixWord].
1718    pub const ZERO: FixWord = FixWord(0);
1719
1720    /// Representation of the number 1 as a [FixWord].
1721    pub const ONE: FixWord = FixWord(1 << 20);
1722
1723    /// Returns true if the number is less than 16.0 in magnitude according to Knuth.
1724    ///
1725    /// The number +16.0 is not allowed.
1726    /// This is covered in the E2E tests.
1727    /// See `check_fix` in TFtoPL.2014.60.
1728    pub fn is_abs_less_than_16(&self) -> bool {
1729        *self >= FixWord::ONE * -16 && *self < FixWord::ONE * 16
1730    }
1731
1732    /// Calculates the scaled value of this number at the specified design size.
1733    ///
1734    /// All dimensions in a TFM file are specified as multiples of the design size
1735    /// (with the exception of the design size itself).
1736    /// The scaled value (i.e. the value is scaled points) is the TFM value multiplied
1737    /// by the design size.
1738    ///
1739    /// The raw value and the design size both have 20 binary fractional bits,
1740    /// so the product has 40 fractional bits.
1741    /// A scaled value has only 16 fractional bits, and how the truncation is performed
1742    /// can effect the result.
1743    /// The specific multiplication algorithm Knuth wrote (TeX.2021.571-572)
1744    /// truncates the design size first and then performs the multiplication
1745    /// and then truncates again.
1746    /// Additionally, the integer division in that algorithm rounds towards zero
1747    /// even for negative numbers (e.g. -1.25 rounds to -1 instead of -2).
1748    ///
1749    /// In order to ensure the result here matched Knuth's algorithm, we reimplement
1750    /// Knuth's algorithm.
1751    pub fn to_scaled(self, design_size: FixWord) -> common::Scaled {
1752        // TeX.2021.568
1753        let mut z = common::Scaled(design_size.0 / 16);
1754        // TeX.2021.572
1755        let mut alpha = 16;
1756        // Ensure that z < 2^23 sp
1757        while z.0 >= 0o40000000 {
1758            z /= 2;
1759            alpha *= 2;
1760        }
1761        let beta = 256 / alpha; // beta = 16 * z / design_size
1762        let alpha = z * alpha; // alpha = 16 * design_size
1763
1764        // TeX.2021.571 (store_scaled)
1765        let [a, b, c, d] = self.0.to_be_bytes();
1766        assert!(a == 0 || a == 255);
1767        let sw = (((z * (d as i32)) / 0o400 + (z * (c as i32))) / 0o400 + z * (b as i32)) / beta;
1768        if a == 255 {
1769            // In this case self < 0.
1770            // We have calculated sw using only the 3 least significant bytes of self,
1771            // which is equivalent to setting self=16+self at the start. We then undo
1772            // this by setting sw = sw - 16 * design_size.
1773            sw - alpha
1774        } else {
1775            sw
1776        }
1777    }
1778}
1779
1780impl std::fmt::Display for FixWord {
1781    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1782        // TFtoPL.2014.40-43
1783        if self.0 < 0 {
1784            write!(f, "-")?;
1785        }
1786        let integer_part = (self.0 / FixWord::ONE.0).abs();
1787        write!(f, "{integer_part}.")?;
1788        let mut fp = (self.0 % FixWord::ONE.0).abs();
1789        fp = 10 * fp + 5;
1790        let mut delta = 10;
1791        loop {
1792            if delta > 0o4_000_000 {
1793                fp = fp + 0o2_000_000 - delta / 2;
1794            }
1795            write!(f, "{}", fp / 0o4_000_000)?;
1796            fp = 10 * (fp % 0o4_000_000);
1797            delta *= 10;
1798            if fp <= delta {
1799                break;
1800            }
1801        }
1802        Ok(())
1803    }
1804}
1805
1806impl std::ops::Add<FixWord> for FixWord {
1807    type Output = FixWord;
1808    fn add(self, rhs: FixWord) -> Self::Output {
1809        FixWord(self.0 + rhs.0)
1810    }
1811}
1812impl std::ops::Sub<FixWord> for FixWord {
1813    type Output = FixWord;
1814    fn sub(self, rhs: FixWord) -> Self::Output {
1815        FixWord(self.0 - rhs.0)
1816    }
1817}
1818
1819impl std::ops::Mul<i32> for FixWord {
1820    type Output = FixWord;
1821
1822    fn mul(self, rhs: i32) -> Self::Output {
1823        FixWord(self.0 * rhs)
1824    }
1825}
1826
1827impl std::ops::Div<i32> for FixWord {
1828    type Output = FixWord;
1829
1830    fn div(self, rhs: i32) -> Self::Output {
1831        FixWord(self.0 / rhs)
1832    }
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837    use super::*;
1838
1839    #[test]
1840    fn to_scaled_test() {
1841        assert_eq!(common::Scaled::ZERO, FixWord::ZERO.to_scaled(FixWord::ONE));
1842        assert_eq!(common::Scaled::ONE, FixWord::ONE.to_scaled(FixWord::ONE));
1843    }
1844
1845    fn run_compress_test(
1846        values: Vec<FixWord>,
1847        max_size: u8,
1848        want: Vec<FixWord>,
1849        want_map: Vec<u8>,
1850    ) {
1851        let (got, got_map) = compress(&values, max_size);
1852        assert_eq!(got, want);
1853        let want_map: HashMap<FixWord, NonZeroU8> = want_map
1854            .into_iter()
1855            .enumerate()
1856            .map(|(i, t)| (values[i], t.try_into().unwrap()))
1857            .collect();
1858        assert_eq!(got_map, want_map);
1859    }
1860
1861    macro_rules! compress_tests {
1862        ( $( ($name: ident, $values: expr, $max_size: expr, $want: expr, $want_map: expr, ), )+ ) => {
1863            $(
1864                #[test]
1865                fn $name () {
1866                    let values = $values;
1867                    let max_size = $max_size;
1868                    let want = $want;
1869                    let want_map = $want_map;
1870                    run_compress_test(values, max_size, want, want_map);
1871                }
1872            )+
1873        };
1874    }
1875
1876    compress_tests!(
1877        (no_op_0, vec![], 1, vec![FixWord(0)], vec![],),
1878        (
1879            no_op_2,
1880            vec![FixWord::ONE * 2, FixWord::ONE],
1881            2,
1882            vec![FixWord(0), FixWord::ONE, FixWord::ONE * 2],
1883            vec![2, 1],
1884        ),
1885        (
1886            just_deduplication,
1887            vec![FixWord::ONE, FixWord::ONE],
1888            1,
1889            vec![FixWord(0), FixWord::ONE],
1890            vec![1, 1],
1891        ),
1892        (
1893            simple_compression_case,
1894            vec![FixWord::ONE, FixWord::ONE * 2],
1895            1,
1896            vec![FixWord(0), FixWord::ONE * 3 / 2],
1897            vec![1, 1],
1898        ),
1899        (
1900            simple_compression_case_2,
1901            vec![
1902                FixWord::ONE,
1903                FixWord::ONE * 2,
1904                FixWord::ONE * 200,
1905                FixWord::ONE * 201
1906            ],
1907            2,
1908            vec![FixWord(0), FixWord::ONE * 3 / 2, FixWord::ONE * 401 / 2],
1909            vec![1, 1, 2, 2],
1910        ),
1911        (
1912            lower_upper_close_edge_case_1,
1913            vec![FixWord(1), FixWord(3)],
1914            1,
1915            vec![FixWord(0), FixWord(2)],
1916            vec![1, 1],
1917        ),
1918        (
1919            lower_upper_close_edge_case_2,
1920            vec![FixWord(0), FixWord(2)],
1921            1,
1922            vec![FixWord(0), FixWord(1)],
1923            vec![1, 1],
1924        ),
1925        (
1926            lower_upper_close_edge_case_3,
1927            vec![FixWord(1), FixWord(4)],
1928            1,
1929            vec![FixWord(0), FixWord(2)],
1930            vec![1, 1],
1931        ),
1932        (
1933            lower_upper_close_edge_case_4,
1934            vec![FixWord(1), FixWord(2)],
1935            1,
1936            vec![FixWord(0), FixWord(1)],
1937            vec![1, 1],
1938        ),
1939    );
1940
1941    mod next_larger_tests {
1942        use super::*;
1943
1944        fn run(
1945            edges: Vec<(Char, Char)>,
1946            want_sequences: HashMap<Char, Vec<Char>>,
1947            want_warnings: Vec<NextLargerProgramWarning>,
1948        ) {
1949            let (program, got_warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1950            assert_eq!(got_warnings, want_warnings);
1951            for u in 0..=u8::MAX {
1952                let want_sequence = want_sequences
1953                    .get(&Char(u))
1954                    .map(Vec::as_slice)
1955                    .unwrap_or_default();
1956                let got_sequence: Vec<Char> = program.get(Char(u)).collect();
1957                assert_eq!(
1958                    got_sequence, want_sequence,
1959                    "got/want sequences for {u} do not match"
1960                );
1961            }
1962        }
1963
1964        fn big_infinite_loop_edges() -> Vec<(Char, Char)> {
1965            (0..=u8::MAX)
1966                .into_iter()
1967                .map(|u| (Char(u), Char(u.wrapping_add(1))))
1968                .collect()
1969        }
1970
1971        fn big_infinite_loop_sequences() -> HashMap<Char, Vec<Char>> {
1972            (0..=u8::MAX)
1973                .into_iter()
1974                .map(|u| {
1975                    let v: Vec<Char> = match u.checked_add(1) {
1976                        None => vec![],
1977                        Some(w) => (w..=u8::MAX).into_iter().map(Char).collect(),
1978                    };
1979                    (Char(u), v)
1980                })
1981                .collect()
1982        }
1983
1984        macro_rules! next_larger_tests {
1985            ( $( ($name: ident, $edges: expr, $want_sequences: expr, $want_warnings: expr, ), )+ ) => {
1986                $(
1987                    #[test]
1988                    fn $name () {
1989                        run($edges, $want_sequences, $want_warnings);
1990                    }
1991                )+
1992            };
1993        }
1994
1995        next_larger_tests!(
1996            (
1997                same_node_loop,
1998                vec![(Char::A, Char::A)],
1999                HashMap::from([(Char::A, vec![])]),
2000                vec![NextLargerProgramWarning::InfiniteLoop {
2001                    original: Char::A,
2002                    next_larger: Char::A,
2003                }],
2004            ),
2005            (
2006                two_loops,
2007                vec![
2008                    (Char::A, Char::B),
2009                    (Char::B, Char::C),
2010                    (Char::C, Char::B),
2011                    (Char::X, Char::Y),
2012                    (Char::Y, Char::Z),
2013                    (Char::Z, Char::X),
2014                ],
2015                HashMap::from([
2016                    (Char::A, vec![Char::B, Char::C]),
2017                    (Char::B, vec![Char::C]),
2018                    (Char::C, vec![]),
2019                    (Char::X, vec![Char::Y, Char::Z]),
2020                    (Char::Y, vec![Char::Z]),
2021                    (Char::Z, vec![]),
2022                ]),
2023                vec![
2024                    NextLargerProgramWarning::InfiniteLoop {
2025                        original: Char::C,
2026                        next_larger: Char::B,
2027                    },
2028                    NextLargerProgramWarning::InfiniteLoop {
2029                        original: Char::Z,
2030                        next_larger: Char::X,
2031                    },
2032                ],
2033            ),
2034            (
2035                path_leading_to_loop,
2036                vec![(Char::A, Char::B), (Char::B, Char::C), (Char::C, Char::B),],
2037                HashMap::from([
2038                    (Char::A, vec![Char::B, Char::C]),
2039                    (Char::B, vec![Char::C]),
2040                    (Char::C, vec![]),
2041                ]),
2042                vec![NextLargerProgramWarning::InfiniteLoop {
2043                    original: Char::C,
2044                    next_larger: Char::B,
2045                }],
2046            ),
2047            (
2048                big_infinite_loop,
2049                big_infinite_loop_edges(),
2050                big_infinite_loop_sequences(),
2051                vec![NextLargerProgramWarning::InfiniteLoop {
2052                    original: Char(u8::MAX),
2053                    next_larger: Char(0),
2054                }],
2055            ),
2056        );
2057    }
2058}