tfm/ligkern/
mod.rs

1//! Lig/kern programming.
2//!
3//! TFM files can include information about ligatures and kerns.
4//! A [ligature](https://en.wikipedia.org/wiki/Ligature_(writing))
5//!     is a special character that can replace two or more adjacent characters.
6//! For example, the pair of characters ae can be replaced by the æ ligature which is a single character.
7//! A [kern](https://en.wikipedia.org/wiki/Kerning) is special space inserted between
8//!     two adjacent characters to align them better.
9//! For example, a kern can be inserted between A and V to compensate for the large
10//!     amount of space created by the specific combination of these two characters.
11//!
12//! ## The lig/kern programming language
13//!
14//! TFM provides ligature and kern data in the form of
15//!     "instructions in a simple programming language that explains what to do for special letter pairs"
16//!     (quoting TFtoPL.2014.13).
17//! This lig/kern programming language can be used to specify instructions like
18//!     "replace the pair (a,e) by æ" and
19//!     "insert a kern of width -0.1pt between the pair (A,V)".
20//! But it can also specify more complex behaviors.
21//! For example, a lig/kern program can specify "replace the pair (x,y) by the pair (z,y)".
22//!
23//! In general for any pair of characters (x,y) the program specifies zero or one lig/kern instructions.
24//! After this instruction is executed, there may be a new
25//!     pair of characters remaining, as in the (x,y) to (z,y) instruction.
26//! The lig/kern instruction for this pair is then executed, if it exists.
27//! This process continues until there are no more instructions left to run.
28//!
29//! Lig/kern instructions are represented in this module by the [`lang::Instruction`] type.
30//!
31//! ## Related code by Knuth
32//!
33//! The TFtoPL and PLtoTF programs don't contain any code for running lig/kern programs.
34//! They only contain logic for translating between the `.tfm` and `.pl`
35//!     formats for lig/kern programs, and for doing some validation as described below.
36//! Lig/kern programs are actually executed in TeX; see KnuthTeX.2021.1032-1040.
37//!
38//! One of the challenges with lig/kern programs is that they can contain infinite loops.
39//! Here is a simple example of a lig/kern program with two instruction and an infinite loop:
40//!
41//! - Replace (x,y) with (z,y) (in property list format, `(LABEL C x)(LIG/ C y C z)`)
42//! - Replace (z,y) with (x,y) (in property list format, `(LABEL C z)(LIG/ C y C x)`)
43//!
44//! When this program runs (x,y) will be swapped with (z,y) ad infinitum.
45//! See TFtoPL.2014.88 for more examples.
46//!
47//! Both TFtoPL and PLtoTF contain code that checks that a lig/kern program
48//!     does not contain infinite loops (TFtoPL.2014.88-95 and PLtoTF.2014.116-125).
49//! The algorithm for detecting infinite loops is a topological sorting algorithm
50//!     over a graph where each node is a pair of characters.
51//! However it's a bit complicated because the full graph cannot be constructed without
52//!     running the lig/kern program.
53//!
54//! TeX does not check for infinite loops, presumably under the assumption that any `.tfm` file will have
55//!     been generated by PLtoTF and thus already validated.
56//! However TeX does check for interrupts when executing lig/kern programs so that
57//!     at least a user can terminate TeX if an infinite loop is hit.
58//! (See the `check_interrupt` line in KnuthTeX.2021.1040.)
59//!
60//! ## Functionality in this module
61//!
62//! This module handles lig/kern programs in a different way,
63//!     inspired by the ["parse don't validate"](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
64//!     philosophy.
65//! This module is able to represent raw lig/kern programs as a vector of [`lang::Instruction`] values.
66//! But can also _compile_ lig/kern programs (into a [`CompiledProgram`]).
67//! This compilation process essentially executes the lig/kern program for every possible character pair.
68//! The result is a map from each character pair to the full list of
69//!     replacement characters and kerns for that pair.
70//! If there is an infinite loop in the program this compilation will naturally fail.
71//! The compiled program is thus a "parsed" version of the lig/kern program
72//!     and it is impossible for infinite loops to appear in it.
73//!
74//! An advantage of this model is that the lig/kern program does not need to be repeatedly
75//!     executed in the main hot loop of TeX.
76//! This may make TeX faster.
77//! However the compiled lig/kern program does have a larger memory footprint than the raw program,
78//!     and so it may be slower if TeX is memory bound.
79
80mod compiler;
81use crate::ligkern::compiler::Replacement;
82use crate::Char;
83use crate::FixWord;
84use std::collections::HashMap;
85use std::rc::Rc;
86pub mod lang;
87
88/// A compiled lig/kern program.
89///
90/// The default value is an empty program with no kerns or ligatures.
91#[derive(Clone, Debug, Default)]
92pub struct CompiledProgram {
93    right_boundary_char: Option<Char>,
94    replacements: HashMap<(Option<Char>, Char), compiler::Replacement>,
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98enum IntermediateOp {
99    // Emit the kern.
100    Kern(common::Scaled),
101    // Emit the char in the payload.
102    C(compiler::C),
103}
104
105impl CompiledProgram {
106    /// Compile a lig/kern program.
107    pub fn compile(
108        program: &lang::Program,
109        design_size: FixWord,
110        kerns: &[FixWord],
111        entrypoints: HashMap<Char, u16>,
112    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
113        compiler::compile(program, design_size, kerns, &entrypoints)
114    }
115
116    /// Compile a lig/kern program from a PL file.
117    pub fn compile_from_pl_file(
118        pl_file: &super::pl::File,
119    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
120        let entrypoints = pl_file.lig_kern_entrypoints(false);
121        // The kerns array, which is used for KernAtIndex operations, is empty
122        // because PL files do not have such operations.
123        CompiledProgram::compile(
124            &pl_file.lig_kern_program,
125            pl_file.header.design_size,
126            &[],
127            entrypoints,
128        )
129    }
130
131    /// Compile a lig/kern program from a TFM file.
132    pub fn compile_from_tfm_file(
133        tfm_file: &mut super::File,
134    ) -> (CompiledProgram, Vec<InfiniteLoopError>) {
135        let entrypoints: HashMap<Char, u16> = tfm_file
136            .lig_kern_entrypoints()
137            .into_iter()
138            .filter_map(|(c, e)| {
139                tfm_file
140                    .lig_kern_program
141                    .unpack_entrypoint(e)
142                    .ok()
143                    .map(|e| (c, e))
144            })
145            .collect();
146        CompiledProgram::compile(
147            &tfm_file.lig_kern_program,
148            tfm_file.header.design_size,
149            &tfm_file.kerns,
150            entrypoints,
151        )
152    }
153
154    pub fn has_replacement(&self, left_char: Option<char>, right_char: Option<char>) -> bool {
155        self.get_replacement_utf8(left_char, right_char).is_some()
156    }
157
158    fn get_replacement(
159        &self,
160        left_char: Option<Char>,
161        right_char: Option<Char>,
162    ) -> Option<&Replacement> {
163        let right_char = match right_char {
164            None => self.right_boundary_char?,
165            Some(c) => c,
166        };
167        self.replacements.get(&(left_char, right_char))
168    }
169
170    fn get_replacement_utf8(
171        &self,
172        left_char: Option<char>,
173        right_char: Option<char>,
174    ) -> Option<&Replacement> {
175        let left_char = match left_char {
176            None => None,
177            Some(c) => {
178                let Ok(c) = c.try_into() else {
179                    return None;
180                };
181                Some(c)
182            }
183        };
184        let right_char = match right_char {
185            None => None,
186            Some(c) => {
187                let Ok(c) = c.try_into() else {
188                    return None;
189                };
190                Some(c)
191            }
192        };
193        self.get_replacement(left_char, right_char)
194    }
195
196    /// Returns an iterator over all pairs `(char,char)` that have a replacement
197    ///     specified in the lig/kern program.
198    pub fn all_pairs_with_replacements(&self) -> Vec<(Option<Char>, Char)> {
199        let mut v: Vec<(Option<Char>, Char)> = self.replacements.keys().copied().collect();
200        v.sort();
201        v
202    }
203
204    /// Returns whether this program is seven-bit safe.
205    ///
206    /// A lig/kern program is seven-bit safe if the replacement for any
207    ///     pair of seven-bit safe characters
208    ///     consists only of seven-bit characters.
209    /// Conversely a program is seven-bit unsafe if there is a
210    ///     pair of seven-bit characters whose replacement
211    ///     contains a non-seven-bit character.
212    pub fn is_seven_bit_safe(&self) -> bool {
213        self.all_pairs_with_replacements()
214            .into_iter()
215            .filter(|(l, r)| l.map(|c| c.is_seven_bit()).unwrap_or(true) && r.is_seven_bit())
216            .flat_map(|(l, r)| self.get_replacement(l, Some(r)))
217            .all(|rep| {
218                rep.0.iter().all(|op| match op {
219                    IntermediateOp::Kern(_) => true,
220                    IntermediateOp::C(c) => c.c.is_seven_bit(),
221                }) && rep.1.c.is_seven_bit()
222            })
223    }
224
225    /// Run the lig/kern program for the provided word.
226    pub fn run<'a>(&'a self, word: &'a str) -> RunIter<'a, std::str::Chars<'a>> {
227        self.run_with_options(word.chars(), Default::default())
228    }
229
230    /// Run the lig/kern program for the provided word with the provided options.
231    ///
232    /// This method also supports providing an arbitrary iterator over characters, rather
233    /// than a string slice. This facility is used in Boxworks to avoid allocations.
234    pub fn run_with_options<'a, Word>(
235        &'a self,
236        mut word: Word,
237        options: RunOptions,
238    ) -> RunIter<'a, Word>
239    where
240        Word: Iterator<Item = char>,
241    {
242        let next_left = if options.disable_left_boundary {
243            match word.next() {
244                Some(c) => NextLeft::Char(c),
245                None => NextLeft::None,
246            }
247        } else {
248            NextLeft::Boundary
249        };
250        RunIter {
251            program: self,
252            word,
253            consumes_left: true,
254            intermediate_ops: &[],
255            next_left,
256            ligature: None,
257            left: None,
258            right_boundary_override: options.right_boundary_override,
259        }
260    }
261}
262
263/// Options for the [CompiledProgram::run_with_options] method
264#[derive(Default, Debug)]
265pub struct RunOptions {
266    pub disable_left_boundary: bool,
267    pub right_boundary_override: Option<char>,
268}
269
270/// Replacement elements of a word in a lig/kern program.
271#[derive(PartialEq, Debug)]
272pub enum RunItem {
273    Char(char),
274    Kern(common::Scaled),
275    Ligature(Ligature),
276}
277
278#[derive(Default)]
279struct PendingLigature {
280    s: String,
281    includes_left_boundary: bool,
282    includes_right_boundary: bool,
283}
284
285impl PendingLigature {
286    fn into_ligature(self, c: char) -> Ligature {
287        Ligature {
288            c,
289            original: self.s.into(),
290            includes_left_boundary: self.includes_left_boundary,
291            includes_right_boundary: self.includes_right_boundary,
292        }
293    }
294}
295
296/// Iterator over the replacement elements of a word in a lig/kern program.
297pub struct RunIter<'a, Word> {
298    // First two fields are fixed for the iteration.
299    program: &'a CompiledProgram,
300    right_boundary_override: Option<char>,
301
302    // The word we're iterating over: gets consumed as we iterate.
303    word: Word,
304
305    // A ligature can be built up from multiple lig/kern passes - e.g.
306    // repeated LIG commands. This field stores information about the pending
307    // ligature before it is written out (e.g. by a LIG/> or the absence of
308    // a replacement).
309    ligature: Option<PendingLigature>,
310
311    // Intermediate ops: if the slice is non-empty the next step is to
312    // process is.
313    // consumes_left=true if the first character or ligature should be
314    // marked as consuming the left character used in the replacement
315    // (or left hand boundary).
316    // left is the character that was used as the left-hand side in the
317    // replacement, or None if it was the left hand boundary.
318    intermediate_ops: &'a [IntermediateOp],
319    consumes_left: bool,
320    left: Option<char>,
321
322    // If intermediate ops is empty, the next step is to evaluate the
323    // lig/kern program with the left character as specified in the enum.
324    next_left: NextLeft,
325}
326
327#[derive(Debug)]
328enum NextLeft {
329    Boundary,
330    Char(char),
331    Lig(char, char),
332    FinalLig(char),
333    None,
334}
335
336impl<'a, Word> RunIter<'a, Word> {
337    pub fn is_separation_point(&self) -> bool {
338        self.intermediate_ops.is_empty()
339            && !matches!(self.next_left, NextLeft::Lig(_, _) | NextLeft::FinalLig(_))
340    }
341}
342
343impl<'a, Word> Iterator for RunIter<'a, Word>
344where
345    Word: Iterator<Item = char>,
346{
347    type Item = RunItem;
348
349    fn next(&mut self) -> Option<Self::Item> {
350        if let Some((op, tail)) = self.intermediate_ops.split_first() {
351            self.intermediate_ops = tail;
352            return Some(match op {
353                IntermediateOp::Kern(kern) => RunItem::Kern(*kern),
354                IntermediateOp::C(compiler::C { c, is_lig: false }) => {
355                    self.consumes_left = false;
356                    match self.ligature.take() {
357                        Some(l) => {
358                            // This happens when left is a ligature.
359                            RunItem::Ligature(l.into_ligature((*c).into()))
360                        }
361                        None => RunItem::Char((*c).into()),
362                    }
363                }
364                IntermediateOp::C(compiler::C { c, is_lig: true }) => {
365                    let mut s = self.ligature.take().unwrap_or_default();
366                    if self.consumes_left {
367                        if let Some(left) = self.left {
368                            s.s.push(left);
369                        } else {
370                            s.includes_left_boundary = true;
371                        }
372                    }
373                    self.consumes_left = false;
374                    s.includes_right_boundary = matches!(self.next_left, NextLeft::None)
375                        && matches!(
376                            (tail.first(), tail.get(1)),
377                            (None, None) | (Some(IntermediateOp::Kern(_)), None)
378                        );
379                    RunItem::Ligature(s.into_ligature((*c).into()))
380                }
381            });
382        };
383        let (left, left_in_original): (Option<char>, bool) = match &self.next_left {
384            NextLeft::Boundary => (None, true),
385            NextLeft::Char(c) => (Some(*c), true),
386            NextLeft::Lig(c, right) => {
387                let s = self.ligature.get_or_insert_default();
388                if self.consumes_left {
389                    if let Some(left) = self.left {
390                        s.s.push(left);
391                    } else {
392                        s.includes_left_boundary = true;
393                    }
394                }
395                s.s.push(*right);
396                (Some(*c), false)
397            }
398            NextLeft::FinalLig(c) => {
399                let mut s = self.ligature.take().unwrap_or_default();
400                if self.consumes_left {
401                    if let Some(left) = self.left {
402                        s.s.push(left);
403                    } else {
404                        s.includes_left_boundary = true;
405                    }
406                }
407                s.includes_right_boundary = true;
408                let lig = RunItem::Ligature(s.into_ligature(*c));
409                self.next_left = NextLeft::None;
410                return Some(lig);
411            }
412            NextLeft::None => return None,
413        };
414        let right = self.word.next();
415        if left.is_none() && right.is_none() {
416            // TODO: remove this check?
417            return None;
418        }
419        let right_for_lookup = match right {
420            Some(r) => Some(r),
421            None => self.right_boundary_override,
422        };
423        // self.left is None if we're at the left boundary
424        // self.right is None if we're at the right boundary
425        match self.program.get_replacement_utf8(left, right_for_lookup) {
426            Some(replacement) => {
427                self.left = left;
428                self.consumes_left = left_in_original;
429                self.intermediate_ops = &replacement.0;
430                self.next_left = match (replacement.1.is_lig, right) {
431                    (false, None) => NextLeft::None,
432                    (false, Some(_)) => NextLeft::Char(replacement.1.c.into()),
433                    (true, Some(right)) => NextLeft::Lig(replacement.1.c.into(), right),
434                    (true, None) => NextLeft::FinalLig(replacement.1.c.into()),
435                };
436            }
437            None => {
438                self.next_left = match right {
439                    None => NextLeft::None,
440                    Some(right) => NextLeft::Char(right),
441                };
442                if let Some(left) = left {
443                    return Some(match self.ligature.take() {
444                        Some(l) => RunItem::Ligature(l.into_ligature(left)),
445                        None => RunItem::Char(left),
446                    });
447                }
448            }
449        }
450        self.next()
451    }
452}
453
454#[derive(PartialEq, Debug)]
455pub struct Ligature {
456    pub c: char,
457    pub original: Rc<str>,
458    pub includes_left_boundary: bool,
459    pub includes_right_boundary: bool,
460}
461
462/// An error returned from lig/kern compilation.
463///
464/// TODO: rename Cycle everywhere including the docs
465#[derive(Clone, Debug, PartialEq, Eq)]
466pub struct InfiniteLoopError {
467    /// The pair of characters the starts the infinite loop.
468    pub starting_pair: (Option<Char>, Char),
469}
470
471impl InfiniteLoopError {
472    pub fn pltotf_message(&self) -> String {
473        let left = match self.starting_pair.0 {
474            Some(c) => format!["'{:03o}", c.0],
475            None => "boundary".to_string(),
476        };
477        format!(
478            "Infinite ligature loop starting with {} and '{:03o}!",
479            left, self.starting_pair.1 .0
480        )
481    }
482    pub fn pltotf_section(&self) -> u8 {
483        125
484    }
485}
486
487/// One step in a lig/kern infinite loop.
488///
489/// A vector of these steps is returned in a [`InfiniteLoopError`].
490#[derive(Clone, Debug, PartialEq, Eq)]
491pub struct InfiniteLoopStep {
492    /// The index of the instruction to apply in this step.
493    pub instruction_index: usize,
494    /// The replacement text after applying this step.
495    ///
496    /// The boolean specifies whether the replacement begins with the
497    /// left boundary char.
498    pub post_replacement: (bool, Vec<Char>),
499    /// The position of the cursor after applying this step.
500    pub post_cursor_position: usize,
501}
502
503#[cfg(test)]
504mod tests {
505    use common::Scaled;
506
507    use super::Ligature as L;
508    use super::*;
509    use pretty_assertions::assert_eq;
510
511    const LIGAROO: &'static str = include_str!["ligaroo.plst"];
512
513    fn run_test(program: &str, input: &str, want: Vec<RunItem>) {
514        let source = LIGAROO.replace("(LIGTABLE", &format!["(LIGTABLE\n{program}"]);
515        let pl_file = crate::pl::File::from_pl_source_code(&source).0;
516
517        if std::env::var("TEXCRAFT_VERIFY").unwrap_or_default() == "tex" {
518            verify_against_tex(pl_file, input, want);
519            return;
520        }
521
522        let program = CompiledProgram::compile_from_pl_file(&pl_file).0;
523        let got: Vec<RunItem> = program.run(input).collect();
524        assert_eq!(got, want);
525    }
526
527    /// Verify the expected output of a test case against a real TeX engine.
528    ///
529    /// The property list file is converted to a TFM file, TeX builds an
530    /// `\hbox` from the input word set in a font backed by that TFM file,
531    /// and the box contents (dumped with `\showbox`) are parsed back into
532    /// run items and compared to the expected ones.
533    fn verify_against_tex(pl_file: crate::pl::File, input: &str, want: Vec<RunItem>) {
534        let tfm_file: crate::File = pl_file.into();
535        let stdout = run_tex(&tfm_file.serialize(), input);
536        let got = parse_showbox_output(&stdout);
537        let want: Vec<RunItem> = want.into_iter().map(normalize_for_tex).collect();
538        assert_eq!(got, want);
539    }
540
541    /// Run the `tex` binary on an `\hbox` containing the input word, set in
542    /// a font backed by the provided TFM bytes, and return the terminal
543    /// output which includes a `\showbox` dump of the box.
544    fn run_tex(tfm_bytes: &[u8], input: &str) -> String {
545        let mut dir = std::env::temp_dir();
546        dir.push("texcraft_tfm_ligkern");
547        // The thread name is the name of the unit test being run, so tests
548        // running in parallel get distinct directories.
549        dir.push(
550            std::thread::current()
551                .name()
552                .unwrap_or("texcraft_unknown_thread_name")
553                .replace("::", "__"),
554        );
555        std::fs::create_dir_all(&dir).unwrap();
556        std::fs::write(dir.join("ligaroo.tfm"), tfm_bytes).unwrap();
557
558        let tex_source = format!(
559            r"\nonstopmode
560\tracingonline=1
561\showboxbreadth=1000000
562\showboxdepth=100
563\font\ligkernfont=ligaroo
564\ligkernfont
565\setbox253=\hbox{{{input}}}
566\showbox253
567\end
568"
569        );
570        let input_path = dir.join("tex-input.tex");
571        std::fs::write(&input_path, tex_source).unwrap();
572
573        let output = std::process::Command::new("tex")
574            .current_dir(&dir)
575            // By default TeX wraps terminal output at 79 characters, which
576            // would split long box dump lines.
577            .env("max_print_line", "10000")
578            .arg(&input_path)
579            .output()
580            .expect("failed to run the `tex` binary");
581        // The exit status is not checked: `\showbox` counts as an error in
582        // TeX's book-keeping ("! OK.") and makes the exit status non-zero.
583        String::from_utf8(output.stdout).expect("stdout of TeX is utf-8")
584    }
585
586    /// Parse the contents of the box dumped with `\showbox253` into run items.
587    fn parse_showbox_output(stdout: &str) -> Vec<RunItem> {
588        let mut lines = stdout.lines();
589        for line in lines.by_ref() {
590            if line.starts_with(r"> \box253=") {
591                break;
592            }
593        }
594        let dimens_line = lines.next().expect(r"box dump contains an \hbox line");
595        assert!(
596            dimens_line.starts_with(r"\hbox("),
597            r"expected an \hbox dimensions line, got {dimens_line:?}"
598        );
599        // The box contents are the following lines, prefixed with a period
600        // because they are one level deep in the dumped box.
601        lines
602            .map_while(|line| line.strip_prefix('.'))
603            .map(parse_run_item)
604            .collect()
605    }
606
607    /// Parse one line of the box dump into a run item.
608    fn parse_run_item(line: &str) -> RunItem {
609        // Kern lines look like `\kern1.0` (or `\kern 1.0` for explicit kerns).
610        if let Some(width) = line.strip_prefix(r"\kern") {
611            let width = common::Scaled::parse_no_units(width.trim())
612                .expect("kern width fits in a scaled number");
613            return RunItem::Kern(width);
614        }
615        // Character lines look like `\ligkernfont A`, and ligature lines
616        // like `\ligkernfont 1 (ligature AB)`.
617        let tail = line
618            .strip_prefix(r"\ligkernfont ")
619            .expect(r"box content line is a \kern or a character in the test font");
620        let parse_char = |s: &str| {
621            let mut chars = s.chars();
622            let c = chars.next().expect("a character follows the font name");
623            assert_eq!(chars.next(), None, "expected a single character in {s:?}");
624            c
625        };
626        match tail.split_once(" (ligature ") {
627            None => RunItem::Char(parse_char(tail)),
628            Some((c, original)) => {
629                let original = original
630                    .strip_suffix(')')
631                    .expect("ligature original chars end with `)`");
632                RunItem::Ligature(Ligature {
633                    c: parse_char(c),
634                    original: original.into(),
635                    includes_left_boundary: false,
636                    includes_right_boundary: false,
637                })
638            }
639        }
640    }
641
642    /// Normalize a run item to the form in which it appears in TeX's box dump.
643    ///
644    /// TeX's box display marks a boundary character in a ligature's original
645    /// characters with a `|`. A `|` next to an empty original is ambiguous
646    /// between the left and the right boundary, so instead of parsing the
647    /// markers back into the boundary fields, the comparison happens on the
648    /// marked-up original characters with the boundary fields cleared.
649    fn normalize_for_tex(item: RunItem) -> RunItem {
650        match item {
651            RunItem::Ligature(ligature) => {
652                let mut original = String::new();
653                if ligature.includes_left_boundary {
654                    original.push('|');
655                }
656                original.push_str(&ligature.original);
657                if ligature.includes_right_boundary {
658                    original.push('|');
659                }
660                RunItem::Ligature(Ligature {
661                    c: ligature.c,
662                    original: original.into(),
663                    includes_left_boundary: false,
664                    includes_right_boundary: false,
665                })
666            }
667            item => item,
668        }
669    }
670
671    macro_rules! tests {
672        ( $(
673            ($name: ident, $program: expr, $input: expr, $want: expr, ),
674        )+ ) => { $(
675            #[test]
676            fn $name() {
677                #[allow(unused_imports)]
678                use RunItem::*;
679                let program = $program;
680                let input = $input;
681                let want = $want;
682                run_test(program, input, want);
683            }
684        )+ };
685    }
686
687    tests!(
688        (empty_input, "", "", vec![],),
689        // AB -> ^1
690        (
691            single_lig_1,
692            "
693                (LABEL C A)
694                (LIG C B C 1)
695                (KRN C 1 R 0.1)
696                (STOP)
697
698                (LABEL C 1)
699                (KRN C B R 0.3)
700                (STOP)
701            ",
702            "AB",
703            vec![Ligature(L {
704                c: '1',
705                original: "AB".into(),
706                includes_left_boundary: false,
707                includes_right_boundary: false,
708            })],
709        ),
710        // AB -> ^A1
711        (
712            single_lig_2,
713            "
714                (LABEL C A)
715                (/LIG C B C 1)
716                (KRN C 1 R 0.1)
717                (STOP)
718
719                (LABEL C 1)
720                (KRN C B R 0.3)
721                (STOP)
722            ",
723            "AB",
724            vec![
725                Char('A'),
726                Kern(Scaled::ONE),
727                Ligature(L {
728                    c: '1',
729                    original: "B".into(),
730                    includes_left_boundary: false,
731                    includes_right_boundary: false,
732                })
733            ],
734        ),
735        // AB -> A^1
736        (
737            single_lig_3,
738            "
739                (LABEL C A)
740                (/LIG> C B C 1)
741                (KRN C 1 R 0.1)
742                (STOP)
743
744                (LABEL C 1)
745                (KRN C B R 0.3)
746                (STOP)
747            ",
748            "AB",
749            vec![
750                Char('A'),
751                Ligature(L {
752                    c: '1',
753                    original: "B".into(),
754                    includes_left_boundary: false,
755                    includes_right_boundary: false,
756                }),
757            ],
758        ),
759        // AB -> ^1B
760        (
761            single_lig_4,
762            "
763                (LABEL C A)
764                (LIG/ C B C 1)
765                (KRN C 1 R 0.1)
766                (STOP)
767
768                (LABEL C 1)
769                (KRN C B R 0.3)
770                (STOP)
771            ",
772            "AB",
773            vec![
774                Ligature(L {
775                    c: '1',
776                    original: "A".into(),
777                    includes_left_boundary: false,
778                    includes_right_boundary: false,
779                }),
780                Kern(Scaled::ONE * 3),
781                Char('B'),
782            ],
783        ),
784        // AB -> 1^B
785        (
786            single_lig_5,
787            "
788                (LABEL C A)
789                (LIG/> C B C 1)
790                (KRN C 1 R 0.1)
791                (STOP)
792
793                (LABEL C 1)
794                (KRN C B R 0.3)
795                (STOP)
796            ",
797            "AB",
798            vec![
799                Ligature(L {
800                    c: '1',
801                    original: "A".into(),
802                    includes_left_boundary: false,
803                    includes_right_boundary: false,
804                }),
805                Char('B'),
806            ],
807        ),
808        // AB -> ^A1B
809        (
810            single_lig_6,
811            "
812                (LABEL C A)
813                (/LIG/ C B C 1)
814                (KRN C 1 R 0.1)
815                (STOP)
816
817                (LABEL C 1)
818                (KRN C B R 0.3)
819                (STOP)
820            ",
821            "AB",
822            vec![
823                Char('A'),
824                Kern(Scaled::ONE),
825                Ligature(L {
826                    c: '1',
827                    original: "".into(),
828                    includes_left_boundary: false,
829                    includes_right_boundary: false,
830                }),
831                Kern(Scaled::ONE * 3),
832                Char('B'),
833            ],
834        ),
835        // AB -> A^1B
836        (
837            single_lig_7,
838            "
839                (LABEL C A)
840                (/LIG/> C B C 1)
841                (KRN C 1 R 0.1)
842                (STOP)
843
844                (LABEL C 1)
845                (KRN C B R 0.3)
846                (STOP)
847            ",
848            "AB",
849            vec![
850                Char('A'),
851                Ligature(L {
852                    c: '1',
853                    original: "".into(),
854                    includes_left_boundary: false,
855                    includes_right_boundary: false,
856                }),
857                Kern(Scaled::ONE * 3),
858                Char('B'),
859            ],
860        ),
861        // AB -> A1^B
862        (
863            single_lig_8,
864            "
865                (LABEL C A)
866                (/LIG/>> C B C 1)
867                (KRN C 1 R 0.1)
868                (STOP)
869
870                (LABEL C 1)
871                (KRN C B R 0.3)
872                (STOP)
873            ",
874            "AB",
875            vec![
876                Char('A'),
877                Ligature(L {
878                    c: '1',
879                    original: "".into(),
880                    includes_left_boundary: false,
881                    includes_right_boundary: false,
882                }),
883                Char('B'),
884            ],
885        ),
886        // AB -> A^B
887        // This is the same as single_lig_5, but the replacement character
888        // is the same as the character that is removed. In theory lig(A, A)
889        // could be replaced by char(A), and this test verifies that it is not.
890        (
891            no_op_lig,
892            "
893                (LABEL C A)
894                (LIG/> C B C A)
895                (STOP)
896            ",
897            "AB",
898            vec![
899                Ligature(L {
900                    c: 'A',
901                    original: "A".into(),
902                    includes_left_boundary: false,
903                    includes_right_boundary: false,
904                }),
905                Char('B'),
906            ],
907        ),
908        // AB -> ^1, 1C -> ^2
909        (
910            multiple_lig_1,
911            "
912                (LABEL C A)
913                (LIG C B C 1)
914
915                (LABEL C 1)
916                (LIG C C C 2)
917                (STOP)
918            ",
919            "ABC",
920            vec![Ligature(L {
921                c: '2',
922                original: "ABC".into(),
923                includes_left_boundary: false,
924                includes_right_boundary: false,
925            }),],
926        ),
927        // AB -> ^A1B, 1B -> 2
928        (
929            multiple_lig_2,
930            "
931                (LABEL C A)
932                (/LIG/ C B C 1)
933                (LABEL C 1)
934                (LIG C B C 2)
935                (STOP)
936            ",
937            "AB",
938            vec![
939                Char('A'),
940                Ligature(L {
941                    c: '2',
942                    original: "B".into(),
943                    includes_left_boundary: false,
944                    includes_right_boundary: false,
945                }),
946            ],
947        ),
948        // AA -> 1^A multiple times
949        (
950            multiple_lig_3,
951            "
952                (LABEL C A)
953                (LIG/ C A C 1)
954                (STOP)
955            ",
956            "AAAAA",
957            vec![
958                Ligature(L {
959                    c: '1',
960                    original: "A".into(),
961                    includes_left_boundary: false,
962                    includes_right_boundary: false,
963                }),
964                Ligature(L {
965                    c: '1',
966                    original: "A".into(),
967                    includes_left_boundary: false,
968                    includes_right_boundary: false,
969                }),
970                Ligature(L {
971                    c: '1',
972                    original: "A".into(),
973                    includes_left_boundary: false,
974                    includes_right_boundary: false,
975                }),
976                Ligature(L {
977                    c: '1',
978                    original: "A".into(),
979                    includes_left_boundary: false,
980                    includes_right_boundary: false,
981                }),
982                Char('A'),
983            ],
984        ),
985        // AA -> ^A multiple times
986        (
987            multiple_lig_4,
988            "
989                (LABEL C A)
990                (LIG C A C A)
991                (STOP)
992            ",
993            "AAAAAA",
994            vec![Ligature(L {
995                c: 'A',
996                original: "AAAAAA".into(),
997                includes_left_boundary: false,
998                includes_right_boundary: false,
999            }),],
1000        ),
1001        // AB -> ^A1, A1 -> ^21
1002        (
1003            multiple_lig_5,
1004            "
1005                (LABEL C A)
1006                (/LIG C B C 1)
1007                (LIG/ C 1 C 2)
1008                (STOP)
1009            ",
1010            "AB",
1011            vec![
1012                Ligature(L {
1013                    c: '2',
1014                    original: "A".into(),
1015                    includes_left_boundary: false,
1016                    includes_right_boundary: false,
1017                }),
1018                Ligature(L {
1019                    c: '1',
1020                    original: "B".into(),
1021                    includes_left_boundary: false,
1022                    includes_right_boundary: false,
1023                }),
1024            ],
1025        ),
1026        // AB -> ^A1, A1 -> ^21, 21 -> 3
1027        (
1028            multiple_lig_6,
1029            "
1030                (LABEL C A)
1031                (/LIG C B C 1)
1032                (LIG/ C 1 C 2)
1033                (LABEL C 2)
1034                (LIG C 1 C 3)
1035                (STOP)
1036            ",
1037            "AB",
1038            vec![Ligature(L {
1039                c: '3',
1040                original: "AB".into(),
1041                includes_left_boundary: false,
1042                includes_right_boundary: false,
1043            })],
1044        ),
1045        // AB -> ^1, 1C -> 12^C
1046        (
1047            multiple_lig_7,
1048            "
1049                (LABEL C A)
1050                (LIG C B C 1)
1051                (STOP)
1052
1053                (LABEL C 1)
1054                (/LIG/>> C C C 2)
1055                (STOP)
1056            ",
1057            "ABC",
1058            vec![
1059                Ligature(L {
1060                    c: '1',
1061                    original: "AB".into(),
1062                    includes_left_boundary: false,
1063                    includes_right_boundary: false,
1064                }),
1065                Ligature(L {
1066                    c: '2',
1067                    original: "".into(),
1068                    includes_left_boundary: false,
1069                    includes_right_boundary: false,
1070                }),
1071                Char('C'),
1072            ],
1073        ),
1074        (
1075            kern_after_lig_1,
1076            "
1077                (LABEL C A)
1078                (LIG C B C 1)
1079                (STOP)
1080
1081                (LABEL C 1)
1082                (KRN C C R 0.1)
1083            ",
1084            "ABC",
1085            vec![
1086                Ligature(L {
1087                    c: '1',
1088                    original: "AB".into(),
1089                    includes_left_boundary: false,
1090                    includes_right_boundary: false,
1091                }),
1092                Kern(Scaled::ONE),
1093                Char('C'),
1094            ],
1095        ),
1096        (
1097            kern_after_lig_2,
1098            "
1099                (LABEL C A)
1100                (LIG C B C 1)
1101                (STOP)
1102
1103                (LABEL C 1)
1104                (KRN C A R 0.1)
1105            ",
1106            "ABAB",
1107            vec![
1108                Ligature(L {
1109                    c: '1',
1110                    original: "AB".into(),
1111                    includes_left_boundary: false,
1112                    includes_right_boundary: false,
1113                }),
1114                Kern(Scaled::ONE),
1115                Ligature(L {
1116                    c: '1',
1117                    original: "AB".into(),
1118                    includes_left_boundary: false,
1119                    includes_right_boundary: false,
1120                }),
1121            ],
1122        ),
1123        (
1124            left_boundary_char_1,
1125            "
1126                (LABEL BOUNDARYCHAR)
1127                (LIG C A C 1)
1128            ",
1129            "A",
1130            vec![Ligature(L {
1131                c: '1',
1132                original: "A".into(),
1133                includes_left_boundary: true,
1134                includes_right_boundary: false,
1135            }),],
1136        ),
1137        (
1138            left_boundary_char_2,
1139            "
1140                (LABEL BOUNDARYCHAR)
1141                (/LIG/ C A C 1)
1142                (/LIG/ C 1 C 2)
1143            ",
1144            "A",
1145            vec![
1146                Ligature(L {
1147                    c: '2',
1148                    original: "".into(),
1149                    includes_left_boundary: true,
1150                    includes_right_boundary: false,
1151                }),
1152                Ligature(L {
1153                    c: '1',
1154                    original: "".into(),
1155                    includes_left_boundary: false,
1156                    includes_right_boundary: false,
1157                }),
1158                Char('A'),
1159            ],
1160        ),
1161        (
1162            left_boundary_char_3,
1163            "
1164                (LABEL BOUNDARYCHAR)
1165                (/LIG/ C A C 1)
1166            ",
1167            "A",
1168            vec![
1169                Ligature(L {
1170                    c: '1',
1171                    original: "".into(),
1172                    includes_left_boundary: true,
1173                    includes_right_boundary: false,
1174                }),
1175                Char('A'),
1176            ],
1177        ),
1178        // |A -> ^|1
1179        (
1180            left_boundary_char_4,
1181            "
1182                (LABEL BOUNDARYCHAR)
1183                (/LIG C A C 1)
1184            ",
1185            "A",
1186            vec![Ligature(L {
1187                c: '1',
1188                original: "A".into(),
1189                includes_left_boundary: true,
1190                includes_right_boundary: false,
1191            }),],
1192        ),
1193        // |A -> |^1
1194        (
1195            left_boundary_char_5,
1196            "
1197                (LABEL BOUNDARYCHAR)
1198                (/LIG> C A C 1)
1199            ",
1200            "A",
1201            vec![Ligature(L {
1202                c: '1',
1203                original: "A".into(),
1204                includes_left_boundary: true,
1205                includes_right_boundary: false,
1206            }),],
1207        ),
1208        // |A -> ^1A
1209        (
1210            left_boundary_char_6,
1211            "
1212                (LABEL BOUNDARYCHAR)
1213                (LIG/ C A C 1)
1214            ",
1215            "A",
1216            vec![
1217                Ligature(L {
1218                    c: '1',
1219                    original: "".into(),
1220                    includes_left_boundary: true,
1221                    includes_right_boundary: false,
1222                }),
1223                Char('A'),
1224            ],
1225        ),
1226        // |A -> 1^A
1227        (
1228            left_boundary_char_7,
1229            "
1230                (LABEL BOUNDARYCHAR)
1231                (LIG/> C A C 1)
1232            ",
1233            "A",
1234            vec![
1235                Ligature(L {
1236                    c: '1',
1237                    original: "".into(),
1238                    includes_left_boundary: true,
1239                    includes_right_boundary: false,
1240                }),
1241                Char('A'),
1242            ],
1243        ),
1244        // |A -> |^1A
1245        (
1246            left_boundary_char_8,
1247            "
1248                (LABEL BOUNDARYCHAR)
1249                (/LIG/> C A C 1)
1250            ",
1251            "A",
1252            vec![
1253                Ligature(L {
1254                    c: '1',
1255                    original: "".into(),
1256                    includes_left_boundary: true,
1257                    includes_right_boundary: false,
1258                }),
1259                Char('A'),
1260            ],
1261        ),
1262        // |A -> |1^A
1263        (
1264            left_boundary_char_9,
1265            "
1266                (LABEL BOUNDARYCHAR)
1267                (/LIG/>> C A C 1)
1268            ",
1269            "A",
1270            vec![
1271                Ligature(L {
1272                    c: '1',
1273                    original: "".into(),
1274                    includes_left_boundary: true,
1275                    includes_right_boundary: false,
1276                }),
1277                Char('A'),
1278            ],
1279        ),
1280        (
1281            right_boundary_char_lig_1,
1282            "
1283                (LABEL C A)
1284                (LIG C R C 1)
1285                (STOP)
1286            ",
1287            "A",
1288            vec![Ligature(L {
1289                c: '1',
1290                original: "A".into(),
1291                includes_left_boundary: false,
1292                includes_right_boundary: true,
1293            }),],
1294        ),
1295        // AR -> ^AB
1296        (
1297            right_boundary_char_lig_2,
1298            "
1299                (LABEL C A)
1300                (/LIG C R C B)
1301                (STOP)
1302            ",
1303            "A",
1304            vec![
1305                Char('A'),
1306                Ligature(L {
1307                    c: 'B',
1308                    original: "".into(),
1309                    includes_left_boundary: false,
1310                    includes_right_boundary: true,
1311                }),
1312            ],
1313        ),
1314        // AR -> A^B
1315        (
1316            right_boundary_char_lig_3,
1317            "
1318                (LABEL C A)
1319                (/LIG> C R C B)
1320                (STOP)
1321            ",
1322            "A",
1323            vec![
1324                Char('A'),
1325                Ligature(L {
1326                    c: 'B',
1327                    original: "".into(),
1328                    includes_left_boundary: false,
1329                    includes_right_boundary: true,
1330                }),
1331            ],
1332        ),
1333        (
1334            right_boundary_char_lig_4,
1335            "
1336                (LABEL C A)
1337                (/LIG/ C R C B)
1338                (STOP)
1339            ",
1340            "A",
1341            vec![
1342                Char('A'),
1343                Ligature(L {
1344                    c: 'B',
1345                    original: "".into(),
1346                    includes_left_boundary: false,
1347                    includes_right_boundary: true,
1348                }),
1349            ],
1350        ),
1351        // AR -> ^BR
1352        (
1353            right_boundary_char_lig_5,
1354            "
1355                (LABEL C A)
1356                (LIG/ C R C B)
1357                (STOP)
1358            ",
1359            "A",
1360            vec![Ligature(L {
1361                c: 'B',
1362                original: "A".into(),
1363                includes_left_boundary: false,
1364                includes_right_boundary: true,
1365            }),],
1366        ),
1367        // AR -> BR^
1368        (
1369            right_boundary_char_lig_6,
1370            "
1371                (LABEL C A)
1372                (LIG/> C R C B)
1373                (STOP)
1374            ",
1375            "A",
1376            vec![Ligature(L {
1377                c: 'B',
1378                original: "A".into(),
1379                includes_left_boundary: false,
1380                includes_right_boundary: true,
1381            }),],
1382        ),
1383        // AR -> A^BR
1384        (
1385            right_boundary_char_lig_7,
1386            "
1387                (LABEL C A)
1388                (/LIG/> C R C B)
1389                (STOP)
1390            ",
1391            "A",
1392            vec![
1393                Char('A'),
1394                Ligature(L {
1395                    c: 'B',
1396                    original: "".into(),
1397                    includes_left_boundary: false,
1398                    includes_right_boundary: true,
1399                }),
1400            ],
1401        ),
1402        // AR -> AB^R
1403        (
1404            right_boundary_char_lig_8,
1405            "
1406                (LABEL C A)
1407                (/LIG/>> C R C B)
1408                (STOP)
1409            ",
1410            "A",
1411            vec![
1412                Char('A'),
1413                Ligature(L {
1414                    c: 'B',
1415                    original: "".into(),
1416                    includes_left_boundary: false,
1417                    includes_right_boundary: true,
1418                }),
1419            ],
1420        ),
1421        (
1422            right_boundary_char_lig_9,
1423            "
1424                (LABEL C A)
1425                (LIG C R C B)
1426                (LABEL C B)
1427                (LIG C R C C)
1428                (STOP)
1429            ",
1430            "A",
1431            vec![Ligature(L {
1432                c: 'B',
1433                original: "A".into(),
1434                includes_left_boundary: false,
1435                includes_right_boundary: true,
1436            }),],
1437        ),
1438        (
1439            right_boundary_char_lig_10,
1440            "
1441                (LABEL C A)
1442                (LIG/ C R C B)
1443                (LABEL C B)
1444                (LIG/ C R C C)
1445                (STOP)
1446            ",
1447            "A",
1448            vec![Ligature(L {
1449                c: 'C',
1450                original: "A".into(),
1451                includes_left_boundary: false,
1452                includes_right_boundary: true,
1453            }),],
1454        ),
1455        (
1456            right_boundary_char_lig_11,
1457            "
1458                (LABEL C A)
1459                (LIG C R C B)
1460                (LABEL C B)
1461                (LIG/ C R C C)
1462                (STOP)
1463            ",
1464            "A",
1465            vec![Ligature(L {
1466                c: 'B',
1467                original: "A".into(),
1468                includes_left_boundary: false,
1469                includes_right_boundary: true,
1470            }),],
1471        ),
1472        (
1473            right_boundary_char_lig_12,
1474            "
1475                (LABEL C A)
1476                (LIG/ C R C B)
1477                (LABEL C B)
1478                (LIG C R C C)
1479                (STOP)
1480            ",
1481            "A",
1482            vec![Ligature(L {
1483                c: 'C',
1484                original: "A".into(),
1485                includes_left_boundary: false,
1486                includes_right_boundary: true,
1487            }),],
1488        ),
1489        (
1490            right_boundary_char_kern_1,
1491            "
1492                (LABEL C A)
1493                (KRN C R R 1)
1494                (STOP)
1495            ",
1496            "A",
1497            vec![Char('A'), Kern(Scaled::ONE * 10),],
1498        ),
1499        (
1500            right_boundary_char_kern_2,
1501            "
1502                (LABEL C A)
1503                (LIG/ C R C B)
1504                (LABEL C B)
1505                (LIG/ C R C C)
1506                (LABEL C C)
1507                (KRN C R R 1)
1508                (STOP)
1509            ",
1510            "A",
1511            vec![
1512                Ligature(L {
1513                    c: 'C',
1514                    original: "A".into(),
1515                    includes_left_boundary: false,
1516                    includes_right_boundary: true,
1517                }),
1518                Kern(Scaled::ONE * 10),
1519            ],
1520        ),
1521        (
1522            right_boundary_char_kern_3,
1523            "
1524                (LABEL C A)
1525                (LIG C B C C)
1526                (LABEL C C)
1527                (KRN C R R 1)
1528                (STOP)
1529            ",
1530            "AB",
1531            vec![
1532                Ligature(L {
1533                    c: 'C',
1534                    original: "AB".into(),
1535                    includes_left_boundary: false,
1536                    includes_right_boundary: false,
1537                }),
1538                Kern(Scaled::ONE * 10),
1539            ],
1540        ),
1541    );
1542}