tfm/ligkern/
compiler.rs

1use super::*;
2use std::collections::HashSet;
3
4pub fn compile(
5    program: &lang::Program,
6    design_size: FixWord,
7    kerns: &[FixWord],
8    entry_points: &HashMap<Char, u16>,
9) -> (CompiledProgram, Vec<InfiniteLoopError>) {
10    let pair_to_instruction = build_node_to_program_start_map(program, entry_points);
11    let (replacements, infinite_loop_errors) =
12        calculate_replacements(program, design_size, kerns, pair_to_instruction);
13    let program = CompiledProgram {
14        right_boundary_char: program.right_boundary_char,
15        replacements: replacements
16            .into_iter()
17            .map(|(node, replacement)| ((node.0.char_or(), node.1), replacement))
18            .collect(),
19    };
20    (program, infinite_loop_errors)
21}
22
23#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
24struct Node(LeftChar, Char);
25
26#[derive(Debug)]
27struct OngoingCalculation {
28    // Node this calculation is for.
29    // TODO: rename root?
30    node: Node,
31
32    finalized: Vec<IntermediateOp>,
33    // Characters that are still pending replacement. The next step is to apply the ligature
34    // rule for the node. After that, if the second element is not empty, the next step
35    // is to apply the ligature rule for (pending.1, pending.2).
36    pending: Pending,
37}
38
39#[derive(Debug)]
40struct Pending(Option<C>, C, Option<C>);
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct C {
44    pub c: Char,
45    pub is_lig: bool,
46    pub consumes_left: bool,
47    pub consumes_right: bool,
48}
49
50impl C {
51    fn left_char(c: LeftChar) -> Option<Self> {
52        match c {
53            LeftChar::Char(c) => Some(Self {
54                c,
55                is_lig: false,
56                consumes_left: true,
57                consumes_right: false,
58            }),
59            LeftChar::BoundaryChar => None,
60        }
61    }
62
63    fn char(c: Char, is_left_char: bool) -> Self {
64        Self {
65            c,
66            is_lig: false,
67            consumes_left: is_left_char,
68            consumes_right: !is_left_char,
69        }
70    }
71    fn merge(&self, left: &Option<C>, right: &C) -> Self {
72        let (left_is_lig, left_consumes_left, left_consumes_right) = match left {
73            None => (false, true, false),
74            Some(left) => (left.is_lig, left.consumes_left, left.consumes_right),
75        };
76        C {
77            c: self.c,
78            is_lig: self.is_lig
79                || (self.consumes_left && left_is_lig)
80                || (self.consumes_right && right.is_lig),
81            consumes_left: (self.consumes_left && left_consumes_left)
82                || (self.consumes_right && right.consumes_left),
83            consumes_right: (self.consumes_left && left_consumes_right)
84                || (self.consumes_right && right.consumes_right),
85        }
86    }
87}
88
89impl OngoingCalculation {
90    fn child(&self) -> Node {
91        Node(
92            match &self.pending.0 {
93                Some(c) => LeftChar::Char(c.c),
94                None => LeftChar::BoundaryChar,
95            },
96            self.pending.1.c,
97        )
98    }
99}
100
101#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
102enum LeftChar {
103    Char(Char),
104    BoundaryChar,
105}
106
107impl LeftChar {
108    fn char_or(&self) -> Option<Char> {
109        match self {
110            LeftChar::Char(c) => Some(*c),
111            LeftChar::BoundaryChar => None,
112        }
113    }
114    fn is_boundary(&self) -> bool {
115        match self {
116            LeftChar::Char(_) => false,
117            LeftChar::BoundaryChar => true,
118        }
119    }
120}
121
122impl From<Char> for LeftChar {
123    fn from(value: Char) -> Self {
124        LeftChar::Char(value)
125    }
126}
127
128impl TryFrom<LeftChar> for Char {
129    type Error = ();
130
131    fn try_from(value: LeftChar) -> Result<Self, Self::Error> {
132        match value {
133            LeftChar::Char(c) => Ok(c),
134            LeftChar::BoundaryChar => Err(()),
135        }
136    }
137}
138
139impl std::fmt::Display for LeftChar {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            LeftChar::Char(char) => write!(f, "{char}"),
143            LeftChar::BoundaryChar => Ok(()),
144        }
145    }
146}
147
148fn build_node_to_program_start_map(
149    program: &lang::Program,
150    entry_points: &HashMap<Char, u16>,
151) -> HashMap<Node, usize> {
152    let mut result = HashMap::<Node, usize>::new();
153    let all_entry_points = entry_points
154        .iter()
155        .map(|(c, e)| (LeftChar::Char(*c), *e))
156        .chain(
157            program
158                .left_boundary_char_entrypoint
159                .iter()
160                .map(|e| (LeftChar::BoundaryChar, *e)),
161        );
162    for (left, entry_point) in all_entry_points {
163        let mut next_instruction = Some(entry_point as usize);
164        while let Some(next) = next_instruction {
165            let instruction = match program.instructions.get(next) {
166                None => {
167                    // Invalid next instruction
168                    break;
169                }
170                Some(instruction) => instruction,
171            };
172            next_instruction = instruction
173                .next_instruction
174                .map(|increment| next + 1 + (increment as usize));
175
176            // We only insert the element if it doesn't already exist.
177            result
178                .entry(Node(left, instruction.right_char))
179                .or_insert(next);
180        }
181    }
182    result
183}
184
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub(crate) struct Replacement(pub(super) Vec<IntermediateOp>, pub C);
187
188macro_rules! filter_left_boundary {
189    ($head:expr, $($tail:expr,)*) => (
190        match $head {
191            None => vec![$($tail),*],
192            Some(head) => vec![head, $($tail),*],
193        }
194    );
195}
196
197fn calculate_replacements(
198    program: &lang::Program,
199    design_size: FixWord,
200    kerns: &[FixWord],
201    pair_to_instruction: HashMap<Node, usize>,
202) -> (HashMap<Node, Replacement>, Vec<InfiniteLoopError>) {
203    let mut result: HashMap<Node, Replacement> = Default::default();
204    let mut actionable: Vec<OngoingCalculation> = vec![];
205    let mut node_to_parents: HashMap<Node, Vec<OngoingCalculation>> = Default::default();
206    for (&pair, &index) in &pair_to_instruction {
207        let Node(left, right) = pair;
208        let operation = program.instructions[index].operation;
209        let operation = match operation {
210            lang::Operation::EntrypointRedirect(u, _) => {
211                // This reimplements the phantom ligature bug in tftopl.
212                // TODO: in tfmtools don't reimplement these bugs.
213                let [op_byte, remainder] = u.to_be_bytes();
214                lang::Operation::lig_kern_operation_from_bytes(op_byte, remainder)
215            }
216            operation => operation,
217        };
218        let (finalized, pending): (Vec<C>, Option<Pending>) = match operation {
219            lang::Operation::Kern(kern) => {
220                result.insert(
221                    pair,
222                    Replacement(
223                        match C::left_char(left) {
224                            None => vec![IntermediateOp::Kern(kern.to_scaled(design_size))],
225                            Some(left) => vec![
226                                IntermediateOp::C(left),
227                                IntermediateOp::Kern(kern.to_scaled(design_size)),
228                            ],
229                        },
230                        C::char(right, false),
231                    ),
232                );
233                continue;
234            }
235            lang::Operation::KernAtIndex(index) => {
236                let kern = kerns.get(index as usize).copied().unwrap_or_default();
237                result.insert(
238                    pair,
239                    Replacement(
240                        match C::left_char(left) {
241                            None => vec![IntermediateOp::Kern(kern.to_scaled(design_size))],
242                            Some(left) => vec![
243                                IntermediateOp::C(left),
244                                IntermediateOp::Kern(kern.to_scaled(design_size)),
245                            ],
246                        },
247                        C::char(right, false),
248                    ),
249                );
250                continue;
251            }
252            lang::Operation::EntrypointRedirect(_, _) => {
253                continue;
254            }
255            lang::Operation::Ligature {
256                char_to_insert,
257                post_lig_operation,
258                post_lig_tag_invalid: _,
259            } => match post_lig_operation {
260                lang::PostLigOperation::RetainBothMoveNowhere => (
261                    // finalized
262                    vec![],
263                    // pending
264                    Some(Pending(
265                        C::left_char(left),
266                        C {
267                            c: char_to_insert,
268                            is_lig: true,
269                            consumes_left: false,
270                            consumes_right: false,
271                        },
272                        Some(C::char(right, false)),
273                    )),
274                ),
275                lang::PostLigOperation::RetainBothMoveToInserted => (
276                    // finalized
277                    filter_left_boundary![C::left_char(left),],
278                    // pending
279                    Some(Pending(
280                        Some(C {
281                            c: char_to_insert,
282                            is_lig: true,
283                            consumes_left: left.is_boundary(),
284                            consumes_right: false,
285                        }),
286                        C::char(right, false),
287                        None,
288                    )),
289                ),
290                lang::PostLigOperation::RetainBothMoveToRight => (
291                    // finalized
292                    filter_left_boundary![
293                        C::left_char(left),
294                        C {
295                            c: char_to_insert,
296                            is_lig: true,
297                            consumes_left: left.is_boundary(),
298                            consumes_right: false,
299                        },
300                        C::char(right, false),
301                    ],
302                    // pending
303                    None,
304                ),
305                lang::PostLigOperation::RetainRightMoveToInserted => (
306                    // finalized
307                    vec![],
308                    // pending
309                    Some(Pending(
310                        Some(C {
311                            c: char_to_insert,
312                            is_lig: true,
313                            consumes_left: true,
314                            consumes_right: false,
315                        }),
316                        C::char(right, false),
317                        None,
318                    )),
319                ),
320                lang::PostLigOperation::RetainRightMoveToRight => (
321                    // finalized
322                    vec![
323                        C {
324                            c: char_to_insert,
325                            is_lig: true,
326                            consumes_left: true,
327                            consumes_right: false,
328                        },
329                        C::char(right, false),
330                    ],
331                    // pending
332                    None,
333                ),
334                lang::PostLigOperation::RetainLeftMoveNowhere => (
335                    // finalized
336                    vec![],
337                    // pending
338                    Some(Pending(
339                        C::left_char(left),
340                        C {
341                            c: char_to_insert,
342                            is_lig: true,
343                            consumes_left: false,
344                            consumes_right: true,
345                        },
346                        None,
347                    )),
348                ),
349                lang::PostLigOperation::RetainLeftMoveToInserted => (
350                    // finalized
351                    filter_left_boundary![
352                        C::left_char(left),
353                        C {
354                            c: char_to_insert,
355                            is_lig: true,
356                            consumes_left: left.is_boundary(),
357                            consumes_right: true,
358                        },
359                    ],
360                    // pending
361                    None,
362                ),
363                lang::PostLigOperation::RetainNeitherMoveToInserted => (
364                    // finalized
365                    vec![C {
366                        c: char_to_insert,
367                        is_lig: true,
368                        consumes_left: true,
369                        consumes_right: true,
370                    }],
371                    // pending
372                    None,
373                ),
374            },
375        };
376        if let Some(pending) = pending {
377            let finalized = finalized.into_iter().map(IntermediateOp::C).collect();
378            actionable.push(OngoingCalculation {
379                node: pair,
380                finalized,
381                pending,
382            });
383            node_to_parents.insert(pair, vec![]);
384        } else {
385            let mut finalized = finalized;
386            let last = finalized.pop().unwrap();
387            let finalized = finalized.into_iter().map(IntermediateOp::C).collect();
388            result.insert(pair, Replacement(finalized, last));
389        }
390    }
391
392    while let Some(mut calc) = actionable.pop() {
393        let child = calc.child();
394        if let Some(blocking) = node_to_parents.get_mut(&child) {
395            blocking.push(calc);
396            continue;
397        }
398        let last = match result.get(&child) {
399            None => {
400                // There is no lig/kern rule for this pair.
401                let mut last = calc.pending.1.clone();
402                match calc.pending.0.clone() {
403                    None => {
404                        // We are dropping the left node on the floor.
405                        last.consumes_left = true;
406                    }
407                    Some(left) => {
408                        calc.finalized.push(IntermediateOp::C(left));
409                    }
410                }
411                last
412            }
413            Some(replacement) => {
414                let left = calc.pending.0.clone();
415                let right = calc.pending.1.clone();
416                for elem in &replacement.0 {
417                    let elem = match elem {
418                        IntermediateOp::Kern(_) => elem.clone(),
419                        IntermediateOp::C(c) => IntermediateOp::C(c.merge(&left, &right)),
420                    };
421                    calc.finalized.push(elem);
422                }
423                replacement.1.merge(&left, &right)
424            }
425        };
426        match calc.pending.2 {
427            None => {
428                if let Some(blocking) = node_to_parents.remove(&calc.node) {
429                    actionable.extend(blocking);
430                }
431                result.insert(calc.node, Replacement(calc.finalized, last));
432            }
433            Some(new_right) => {
434                calc.pending = Pending(Some(last), new_right, None);
435                actionable.push(calc);
436            }
437        }
438    }
439
440    // Next we check for infinite loops. If there is one, the node_to_parents
441    // map will be non-empty and contain all the nodes that couldn't be calculated.
442    //
443    // The main complication here is that there are multiple nodes we can report
444    // as being the cause of the infinite loop. E.g., the loop could be
445    // (A,R) -> (B,R) -> (C,R) -> (A,R), and we could report any of these 3 nodes.
446    // What we want to do, though, is report the same node that Knuth does in tftopl
447    // and pltotf. Thus the algorithm here replicates what Knuth does.
448    //
449    // Knuth iterates over all nodes in the following order: in lexicographical order
450    // for the left pair (with the boundary char last), and in the instruction order
451    // for the right pair. I.e, if we have pairs (A,R) and (B,R) the pair whose
452    // instruction comes first in the lig/kern program will come first.
453    //
454    // Then, given such a node, Knuth performs a path traversal following each
455    // node's dependencies. The first node that is seen twice in the traversal is the
456    // node that is considered to break the infinite loop. Knuth then essentially breaks
457    // the loop and moves on. Note that Knuth's algorithm correctly handles cases
458    // like (A,R) -> (B,R) -> (C,R) -> (B,R) - in such a case, the first node we see (A,R)
459    // isn't actually what causes the loop.
460    //
461    // The process described in the last paragraph can happen multiple times if there
462    // are multiple infinite loops. Knuth reports the node from the last infinite loop.
463    // Note that we have E2E tests that cover these kinds of cases.
464    let mut node_to_child: HashMap<Node, Node> = node_to_parents
465        .into_values()
466        .flatten()
467        .map(|calc| (calc.node, calc.child()))
468        .collect();
469    let mut knuth_ordered_nodes: Vec<Node> = node_to_child.keys().copied().collect();
470    knuth_ordered_nodes.sort_by(|lhs, rhs| {
471        lhs.0
472            .cmp(&rhs.0)
473            .then(pair_to_instruction[lhs].cmp(&pair_to_instruction[rhs]))
474    });
475
476    let mut infinite_loop_errors: Vec<InfiniteLoopError> = vec![];
477    for mut node in knuth_ordered_nodes {
478        let mut seen = HashSet::<Node>::new();
479        while let Some(child) = node_to_child.remove(&node) {
480            seen.insert(node);
481            node = child;
482        }
483        // As mentioned above, when Knuth finds an infinite loop he breaks it.
484        // It's possible that the node that started this iteration is part of an infinite
485        // loop that has already been broken. For example the lig/kern program could be:
486        // - (A,R) -> (C,R) -> (C,R)
487        // - (B,R) -> (C,R) -> (C,R)
488        // In this case when considering (B,R) there is nothing to do because the (C,R)
489        // loop has already been broken.
490        //
491        // We detect this case by keeping track of which nodes we've seen for the first
492        // time in this traversal.
493        // If we haven't seen it, the loop has already been broken. There is an E2E test
494        // for this case.
495        if seen.contains(&node) {
496            infinite_loop_errors.push(InfiniteLoopError {
497                starting_pair: (node.0.char_or(), node.1),
498            });
499        }
500    }
501    (result, infinite_loop_errors)
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use lang::PostLigOperation::*;
508
509    fn new_kern(
510        next_instruction: Option<u8>,
511        right_char: char,
512        kern: FixWord,
513    ) -> lang::Instruction {
514        lang::Instruction {
515            next_instruction,
516            right_char: right_char.try_into().unwrap(),
517            operation: lang::Operation::Kern(kern),
518        }
519    }
520
521    pub fn new_lig(
522        next_instruction: Option<u8>,
523        right_char: char,
524        char_to_insert: char,
525        post_lig_operation: lang::PostLigOperation,
526    ) -> lang::Instruction {
527        lang::Instruction {
528            next_instruction,
529            right_char: right_char.try_into().unwrap(),
530            operation: lang::Operation::Ligature {
531                char_to_insert: char_to_insert.try_into().unwrap(),
532                post_lig_operation,
533                post_lig_tag_invalid: false,
534            },
535        }
536    }
537
538    fn run_success_test(
539        instructions: Vec<lang::Instruction>,
540        entry_points: Vec<(char, u16)>,
541        want_new: Vec<(char, char, Vec<IntermediateOp>, C)>,
542    ) {
543        let entry_points: HashMap<Char, u16> = entry_points
544            .into_iter()
545            .map(|(c, u)| (c.try_into().unwrap(), u))
546            .collect();
547        let want: HashMap<(Option<Char>, Char), Replacement> = want_new
548            .into_iter()
549            .map(|t| {
550                (
551                    (Some(t.0.try_into().unwrap()), t.1.try_into().unwrap()),
552                    Replacement(t.2, t.3),
553                )
554            })
555            .collect();
556        let program = lang::Program {
557            instructions,
558            ..Default::default()
559        };
560        let (compiled_program, infinite_loop_error_or) =
561            compile(&program, FixWord::ONE, &vec![], &entry_points);
562        assert!(infinite_loop_error_or.is_empty(), "no infinite loop errors");
563
564        let mut got: HashMap<(Option<Char>, Char), Replacement> = Default::default();
565        for pair in compiled_program.all_pairs_with_replacements() {
566            let replacement = compiled_program
567                .get_replacement(pair.0, Some(pair.1))
568                .unwrap();
569            got.insert(pair, replacement.clone());
570        }
571
572        assert_eq!(got, want);
573    }
574
575    macro_rules! success_tests {
576        ( $( ($name: ident, $instructions: expr, $entry_points: expr, $want_new: expr, ), )+ ) => {
577            $(
578                #[test]
579                fn $name() {
580                    let instructions = $instructions;
581                    let entry_points = $entry_points;
582                    let want_new = $want_new;
583                    run_success_test(instructions, entry_points, want_new);
584                }
585            )+
586        };
587    }
588
589    success_tests!(
590        (empty_program, vec![], vec![], vec![],),
591        (
592            kern,
593            vec![new_kern(None, 'V', FixWord::ONE)],
594            vec![('A', 0)],
595            vec![(
596                'A',
597                'V',
598                vec![
599                    IntermediateOp::C(C::char(Char::A, true)),
600                    IntermediateOp::Kern(core::Scaled::ONE)
601                ],
602                C::char(Char::V, false),
603            )],
604        ),
605        (
606            same_kern_for_multiple_left_characters,
607            vec![new_kern(None, 'V', FixWord::ONE)],
608            vec![('A', 0), ('B', 0)],
609            vec![
610                (
611                    'A',
612                    'V',
613                    vec![
614                        IntermediateOp::C(C::char(Char::A, true)),
615                        IntermediateOp::Kern(core::Scaled::ONE)
616                    ],
617                    C::char(Char::V, false),
618                ),
619                (
620                    'B',
621                    'V',
622                    vec![
623                        IntermediateOp::C(C::char(Char::B, true)),
624                        IntermediateOp::Kern(core::Scaled::ONE)
625                    ],
626                    C::char(Char::V, false),
627                ),
628            ],
629        ),
630        (
631            duplicate_kern,
632            vec![
633                new_kern(Some(0), 'V', FixWord::ONE * 2),
634                new_kern(None, 'V', FixWord::ONE * 3),
635            ],
636            vec![('A', 0)],
637            vec![(
638                'A',
639                'V',
640                vec![
641                    IntermediateOp::C(C::char(Char::A, true)),
642                    IntermediateOp::Kern(core::Scaled::ONE * 2)
643                ],
644                C::char(Char::V, false),
645            ),],
646        ),
647        (
648            kern_instructions_with_relationship,
649            vec![
650                new_kern(Some(0), 'V', FixWord::ONE * 2),
651                new_kern(None, 'W', FixWord::ONE * 3),
652                new_kern(None, 'X', FixWord::ONE * 4),
653            ],
654            vec![('A', 0), ('B', 1), ('C', 2)],
655            vec![
656                (
657                    'A',
658                    'V',
659                    vec![
660                        IntermediateOp::C(C::char(Char::A, true)),
661                        IntermediateOp::Kern(core::Scaled::ONE * 2)
662                    ],
663                    C::char(Char::V, false),
664                ),
665                (
666                    'A',
667                    'W',
668                    vec![
669                        IntermediateOp::C(C::char(Char::A, true)),
670                        IntermediateOp::Kern(core::Scaled::ONE * 3)
671                    ],
672                    C::char(Char::W, false),
673                ),
674                (
675                    'B',
676                    'W',
677                    vec![
678                        IntermediateOp::C(C::char(Char::B, true)),
679                        IntermediateOp::Kern(core::Scaled::ONE * 3)
680                    ],
681                    C::char(Char::W, false),
682                ),
683                (
684                    'C',
685                    'X',
686                    vec![
687                        IntermediateOp::C(C::char(Char::C, true)),
688                        IntermediateOp::Kern(core::Scaled::ONE * 4)
689                    ],
690                    C::char(Char::X, false),
691                ),
692            ],
693        ),
694        (
695            single_lig_1,
696            vec![new_lig(None, 'B', 'Z', RetainNeitherMoveToInserted)],
697            vec![('A', 0)],
698            vec![(
699                'A',
700                'B',
701                vec![],
702                C {
703                    c: Char::Z,
704                    is_lig: true,
705                    consumes_left: true,
706                    consumes_right: true,
707                }
708            ),],
709        ),
710        (
711            single_lig_2,
712            vec![
713                new_lig(Some(0), 'B', 'Z', RetainLeftMoveToInserted),
714                new_kern(None, 'Z', FixWord::ONE),
715            ],
716            vec![('A', 0)],
717            vec![
718                (
719                    'A',
720                    'B',
721                    vec![IntermediateOp::C(C::char(Char::A, true)),],
722                    C {
723                        c: Char::Z,
724                        is_lig: true,
725                        consumes_left: false,
726                        consumes_right: true,
727                    }
728                ),
729                (
730                    'A',
731                    'Z',
732                    vec![
733                        IntermediateOp::C(C::char(Char::A, true)),
734                        IntermediateOp::Kern(core::Scaled::ONE)
735                    ],
736                    C::char(Char::Z, false),
737                ),
738            ],
739        ),
740        (
741            retain_left_move_nowhere_1,
742            vec![
743                new_lig(Some(0), 'B', 'Z', RetainLeftMoveNowhere),
744                new_kern(None, 'Z', FixWord::ONE),
745            ],
746            vec![('A', 0)],
747            vec![
748                (
749                    'A',
750                    'B',
751                    vec![
752                        IntermediateOp::C(C::char(Char::A, true)),
753                        IntermediateOp::Kern(core::Scaled::ONE),
754                    ],
755                    C {
756                        c: Char::Z,
757                        is_lig: true,
758                        consumes_left: false,
759                        consumes_right: true,
760                    }
761                ),
762                (
763                    'A',
764                    'Z',
765                    vec![
766                        IntermediateOp::C(C::char(Char::A, true)),
767                        IntermediateOp::Kern(core::Scaled::ONE),
768                    ],
769                    C::char(Char::Z, false),
770                ),
771            ],
772        ),
773        (
774            retain_left_move_nowhere_2,
775            vec![new_lig(None, 'B', 'Z', RetainLeftMoveNowhere),],
776            vec![('A', 0)],
777            vec![(
778                'A',
779                'B',
780                vec![IntermediateOp::C(C::char(Char::A, true)),],
781                C {
782                    c: Char::Z,
783                    is_lig: true,
784                    consumes_left: false,
785                    consumes_right: true,
786                }
787            ),],
788        ),
789        (
790            single_lig_4,
791            vec![
792                new_lig(None, 'B', 'Z', RetainRightMoveToInserted),
793                new_kern(None, 'B', FixWord::ONE),
794            ],
795            vec![('A', 0), ('Z', 1)],
796            vec![
797                (
798                    'A',
799                    'B',
800                    vec![
801                        IntermediateOp::C(C {
802                            c: Char::Z,
803                            is_lig: true,
804                            consumes_left: true,
805                            consumes_right: false,
806                        }),
807                        IntermediateOp::Kern(core::Scaled::ONE),
808                    ],
809                    C::char(Char::B, false),
810                ),
811                (
812                    'Z',
813                    'B',
814                    vec![
815                        IntermediateOp::C(C::char(Char::Z, true)),
816                        IntermediateOp::Kern(core::Scaled::ONE),
817                    ],
818                    C::char(Char::B, false),
819                ),
820            ],
821        ),
822        (
823            single_lig_5,
824            vec![
825                new_lig(None, 'B', 'Z', RetainRightMoveToRight),
826                new_kern(None, 'B', FixWord::ONE),
827            ],
828            vec![('A', 0), ('Z', 1)],
829            vec![
830                (
831                    'A',
832                    'B',
833                    vec![IntermediateOp::C(C {
834                        c: Char::Z,
835                        is_lig: true,
836                        consumes_left: true,
837                        consumes_right: false,
838                    })],
839                    C::char(Char::B, false),
840                ),
841                (
842                    'Z',
843                    'B',
844                    vec![
845                        IntermediateOp::C(C::char(Char::Z, true)),
846                        IntermediateOp::Kern(core::Scaled::ONE),
847                    ],
848                    C::char(Char::B, false),
849                ),
850            ],
851        ),
852        (
853            retain_both_move_nowhere_1,
854            vec![
855                new_lig(Some(0), 'B', 'Z', RetainBothMoveNowhere,),
856                new_kern(None, 'Z', FixWord::ONE * 2),
857                new_kern(None, 'B', FixWord::ONE * 3),
858            ],
859            vec![('A', 0), ('Z', 2)],
860            vec![
861                (
862                    'A',
863                    'B',
864                    vec![
865                        IntermediateOp::C(C::char(Char::A, true)),
866                        IntermediateOp::Kern(core::Scaled::ONE * 2),
867                        IntermediateOp::C(C {
868                            c: Char::Z,
869                            is_lig: true,
870                            consumes_left: false,
871                            consumes_right: false,
872                        }),
873                        IntermediateOp::Kern(core::Scaled::ONE * 3),
874                    ],
875                    C::char(Char::B, false),
876                ),
877                (
878                    'A',
879                    'Z',
880                    vec![
881                        IntermediateOp::C(C::char(Char::A, true)),
882                        IntermediateOp::Kern(core::Scaled::ONE * 2),
883                    ],
884                    C::char(Char::Z, false),
885                ),
886                (
887                    'Z',
888                    'B',
889                    vec![
890                        IntermediateOp::C(C::char(Char::Z, true)),
891                        IntermediateOp::Kern(core::Scaled::ONE * 3),
892                    ],
893                    C::char(Char::B, false),
894                ),
895            ],
896        ),
897        (
898            retain_both_move_nowhere_2,
899            vec![new_lig(None, 'B', 'Z', RetainBothMoveNowhere),],
900            vec![('A', 0)],
901            vec![(
902                'A',
903                'B',
904                vec![
905                    IntermediateOp::C(C::char(Char::A, true)),
906                    IntermediateOp::C(C {
907                        c: Char::Z,
908                        is_lig: true,
909                        consumes_left: false,
910                        consumes_right: false,
911                    }),
912                ],
913                C::char(Char::B, false),
914            ),],
915        ),
916        (
917            retain_both_move_nowhere_3,
918            vec![
919                new_lig(None, 'B', 'Z', RetainBothMoveNowhere),
920                new_lig(None, 'B', 'Y', RetainRightMoveToRight),
921            ],
922            vec![('A', 0), ('Z', 1)],
923            vec![
924                (
925                    'A',
926                    'B',
927                    vec![
928                        IntermediateOp::C(C::char(Char::A, true)),
929                        IntermediateOp::C(C {
930                            c: Char::Y,
931                            is_lig: true,
932                            consumes_left: false,
933                            consumes_right: false,
934                        }),
935                    ],
936                    C::char(Char::B, false),
937                ),
938                (
939                    'Z',
940                    'B',
941                    vec![IntermediateOp::C(C {
942                        c: Char::Y,
943                        is_lig: true,
944                        consumes_left: true,
945                        consumes_right: false,
946                    }),],
947                    C::char(Char::B, false),
948                ),
949            ],
950        ),
951        (
952            retain_both_move_nowhere_4,
953            vec![
954                new_lig(Some(0), 'B', 'Z', RetainBothMoveNowhere),
955                new_lig(None, 'Z', 'Y', RetainBothMoveToRight),
956            ],
957            vec![('A', 0)],
958            vec![
959                (
960                    'A',
961                    'B',
962                    vec![
963                        IntermediateOp::C(C::char(Char::A, true)),
964                        IntermediateOp::C(C {
965                            c: Char::Y,
966                            is_lig: true,
967                            consumes_left: false,
968                            consumes_right: false,
969                        }),
970                        IntermediateOp::C(C {
971                            c: Char::Z,
972                            is_lig: true,
973                            consumes_left: false,
974                            consumes_right: false,
975                        }),
976                    ],
977                    C::char(Char::B, false),
978                ),
979                (
980                    'A',
981                    'Z',
982                    vec![
983                        IntermediateOp::C(C::char(Char::A, true)),
984                        IntermediateOp::C(C {
985                            c: Char::Y,
986                            is_lig: true,
987                            consumes_left: false,
988                            consumes_right: false,
989                        }),
990                    ],
991                    C {
992                        c: Char::Z,
993                        is_lig: false,
994                        consumes_left: false,
995                        consumes_right: true,
996                    }
997                ),
998            ],
999        ),
1000        (
1001            retain_both_move_to_inserted_1,
1002            vec![
1003                new_lig(Some(0), 'B', 'Z', RetainBothMoveToInserted,),
1004                new_kern(None, 'Z', FixWord::ONE * 2),
1005                new_kern(None, 'B', FixWord::ONE * 3),
1006            ],
1007            vec![('A', 0), ('Z', 2)],
1008            vec![
1009                (
1010                    'A',
1011                    'B',
1012                    vec![
1013                        IntermediateOp::C(C::char(Char::A, true)),
1014                        IntermediateOp::C(C {
1015                            c: Char::Z,
1016                            is_lig: true,
1017                            consumes_left: false,
1018                            consumes_right: false,
1019                        }),
1020                        IntermediateOp::Kern(core::Scaled::ONE * 3),
1021                    ],
1022                    C::char(Char::B, false),
1023                ),
1024                (
1025                    'A',
1026                    'Z',
1027                    vec![
1028                        IntermediateOp::C(C::char(Char::A, true)),
1029                        IntermediateOp::Kern(core::Scaled::ONE * 2),
1030                    ],
1031                    C::char(Char::Z, false),
1032                ),
1033                (
1034                    'Z',
1035                    'B',
1036                    vec![
1037                        IntermediateOp::C(C::char(Char::Z, true)),
1038                        IntermediateOp::Kern(core::Scaled::ONE * 3),
1039                    ],
1040                    C::char(Char::B, false),
1041                ),
1042            ],
1043        ),
1044        (
1045            retain_both_move_to_inserted_2,
1046            vec![
1047                new_lig(None, 'B', 'Z', RetainBothMoveToInserted),
1048                new_lig(None, 'B', 'Y', RetainRightMoveToInserted),
1049            ],
1050            vec![('A', 0), ('Z', 1)],
1051            vec![
1052                (
1053                    'A',
1054                    'B',
1055                    vec![
1056                        IntermediateOp::C(C::char(Char::A, true)),
1057                        IntermediateOp::C(C {
1058                            c: Char::Y,
1059                            is_lig: true,
1060                            consumes_left: false,
1061                            consumes_right: false,
1062                        }),
1063                    ],
1064                    C::char(Char::B, false),
1065                ),
1066                (
1067                    'Z',
1068                    'B',
1069                    vec![IntermediateOp::C(C {
1070                        c: Char::Y,
1071                        is_lig: true,
1072                        consumes_left: true,
1073                        consumes_right: false,
1074                    }),],
1075                    C::char(Char::B, false),
1076                ),
1077            ],
1078        ),
1079        (
1080            retain_both_move_to_inserted_3,
1081            vec![new_lig(None, 'B', 'Z', RetainBothMoveToInserted),],
1082            vec![('A', 0)],
1083            vec![(
1084                'A',
1085                'B',
1086                vec![
1087                    IntermediateOp::C(C::char(Char::A, true)),
1088                    IntermediateOp::C(C {
1089                        c: Char::Z,
1090                        is_lig: true,
1091                        consumes_left: false,
1092                        consumes_right: false,
1093                    }),
1094                ],
1095                C::char(Char::B, false),
1096            ),],
1097        ),
1098        (
1099            retain_both_move_to_inserted_4,
1100            vec![
1101                new_lig(None, 'B', 'Z', RetainBothMoveToInserted,),
1102                new_lig(None, 'B', 'Y', RetainRightMoveToRight),
1103            ],
1104            vec![('A', 0), ('Z', 1)],
1105            vec![
1106                (
1107                    'A',
1108                    'B',
1109                    vec![
1110                        IntermediateOp::C(C::char(Char::A, true)),
1111                        IntermediateOp::C(C {
1112                            c: Char::Y,
1113                            is_lig: true,
1114                            consumes_left: false,
1115                            consumes_right: false,
1116                        }),
1117                    ],
1118                    C::char(Char::B, false),
1119                ),
1120                (
1121                    'Z',
1122                    'B',
1123                    vec![IntermediateOp::C(C {
1124                        c: Char::Y,
1125                        is_lig: true,
1126                        consumes_left: true,
1127                        consumes_right: false,
1128                    }),],
1129                    C::char(Char::B, false),
1130                ),
1131            ],
1132        ),
1133        (
1134            retain_both_move_to_right_1,
1135            vec![
1136                new_lig(Some(0), 'B', 'Z', RetainBothMoveToRight,),
1137                new_kern(None, 'Z', FixWord::ONE * 2),
1138                new_kern(None, 'B', FixWord::ONE * 3),
1139            ],
1140            vec![('A', 0), ('Z', 2)],
1141            vec![
1142                (
1143                    'A',
1144                    'B',
1145                    vec![
1146                        IntermediateOp::C(C::char(Char::A, true)),
1147                        IntermediateOp::C(C {
1148                            c: Char::Z,
1149                            is_lig: true,
1150                            consumes_left: false,
1151                            consumes_right: false,
1152                        }),
1153                    ],
1154                    C::char(Char::B, false),
1155                ),
1156                (
1157                    'A',
1158                    'Z',
1159                    vec![
1160                        IntermediateOp::C(C::char(Char::A, true)),
1161                        IntermediateOp::Kern(core::Scaled::ONE * 2),
1162                    ],
1163                    C::char(Char::Z, false),
1164                ),
1165                (
1166                    'Z',
1167                    'B',
1168                    vec![
1169                        IntermediateOp::C(C::char(Char::Z, true)),
1170                        IntermediateOp::Kern(core::Scaled::ONE * 3),
1171                    ],
1172                    C::char(Char::B, false),
1173                ),
1174            ],
1175        ),
1176        (
1177            is_lig_propagated_1,
1178            vec![
1179                new_lig(None, 'B', 'Y', RetainBothMoveToInserted,),
1180                new_lig(None, 'B', 'Z', RetainLeftMoveToInserted,),
1181            ],
1182            vec![('A', 0), ('Y', 1)],
1183            vec![
1184                (
1185                    'A',
1186                    'B',
1187                    vec![
1188                        IntermediateOp::C(C::char(Char::A, true)),
1189                        IntermediateOp::C(C {
1190                            c: Char::Y,
1191                            is_lig: true,
1192                            consumes_left: false,
1193                            consumes_right: false,
1194                        }),
1195                    ],
1196                    C {
1197                        c: Char::Z,
1198                        is_lig: true,
1199                        consumes_left: false,
1200                        consumes_right: true,
1201                    },
1202                ),
1203                (
1204                    'Y',
1205                    'B',
1206                    vec![IntermediateOp::C(C::char(Char::Y, true)),],
1207                    C {
1208                        c: Char::Z,
1209                        is_lig: true,
1210                        consumes_left: false,
1211                        consumes_right: true,
1212                    }
1213                ),
1214            ],
1215        ),
1216        (
1217            is_lig_propagated_2,
1218            vec![
1219                new_lig(Some(0), 'B', 'Y', RetainLeftMoveNowhere,),
1220                new_lig(None, 'Y', 'Z', RetainRightMoveToRight,),
1221            ],
1222            vec![('A', 0)],
1223            vec![
1224                (
1225                    'A',
1226                    'B',
1227                    vec![IntermediateOp::C(C {
1228                        c: Char::Z,
1229                        is_lig: true,
1230                        consumes_left: true,
1231                        consumes_right: false,
1232                    }),],
1233                    C {
1234                        c: Char::Y,
1235                        is_lig: true,
1236                        consumes_left: false,
1237                        consumes_right: true,
1238                    },
1239                ),
1240                (
1241                    'A',
1242                    'Y',
1243                    vec![IntermediateOp::C(C {
1244                        c: Char::Z,
1245                        is_lig: true,
1246                        consumes_left: true,
1247                        consumes_right: false,
1248                    }),],
1249                    C::char(Char::Y, false)
1250                ),
1251            ],
1252        ),
1253    );
1254}