Skip to main content

link_cli/
query_processor.rs

1//! QueryProcessor - Handles LiNo query parsing and execution
2//!
3//! This module provides the QueryProcessor for processing LiNo queries.
4//! Corresponds to BasicQueryProcessor, MixedQueryProcessor, and AdvancedMixedQueryProcessor in C#
5
6use anyhow::Result;
7use std::collections::{HashMap, HashSet};
8
9use crate::changes_simplifier::simplify_changes;
10use crate::error::LinkError;
11use crate::link::Link;
12use crate::link_reference_validator::LinkReferenceValidator;
13use crate::lino_link::LinoLink;
14use crate::named_type_links::NamedTypeLinks;
15use crate::parser::Parser;
16use crate::query_types::{Pattern, ResolvedLink};
17
18// Pattern matching lives in a submodule; see query_processor/matching.rs.
19mod matching;
20// Write-side operations live in a submodule; see query_processor/mutations.rs.
21mod mutations;
22
23/// The `(before, after)` states a query reports, where `None` is no link:
24/// `(None, Some(link))` is a creation and `(Some(link), None)` a deletion.
25pub type Changes = Vec<(Option<Link>, Option<Link>)>;
26
27/// QueryProcessor handles LiNo query parsing and execution
28/// Corresponds to AdvancedMixedQueryProcessor in C#
29pub struct QueryProcessor {
30    trace: bool,
31    auto_create_missing_references: bool,
32}
33
34impl QueryProcessor {
35    /// Creates a new QueryProcessor
36    pub fn new(trace: bool) -> Self {
37        Self {
38            trace,
39            auto_create_missing_references: false,
40        }
41    }
42
43    pub fn with_auto_create_missing_references(
44        mut self,
45        auto_create_missing_references: bool,
46    ) -> Self {
47        self.auto_create_missing_references = auto_create_missing_references;
48        self
49    }
50
51    /// Processes a LiNo query and returns the list of changes.
52    ///
53    /// Every scenario is simplified on the way out, matching the C# CLI, which
54    /// collects the raw handler calls and runs `SimplifyChanges` over them once
55    /// in `Program.cs` regardless of which branch of the processor produced
56    /// them.
57    pub fn process_query(&self, storage: &mut impl NamedTypeLinks, query: &str) -> Result<Changes> {
58        let changes = self.process_query_raw(storage, query)?;
59        Ok(self.simplify_changes_list(&changes))
60    }
61
62    /// The processor proper: applies `query` and reports the raw
63    /// `(before, after)` states, in the order they happened.
64    fn process_query_raw(&self, storage: &mut impl NamedTypeLinks, query: &str) -> Result<Changes> {
65        self.trace_msg(&format!("[ProcessQuery] Query: \"{}\"", query));
66
67        let query = query.trim();
68        if query.is_empty() {
69            self.trace_msg("[ProcessQuery] Query is empty, returning.");
70            return Ok(vec![]);
71        }
72
73        let parser = Parser::new();
74        let parsed_links = parser.parse(query)?;
75
76        self.trace_msg(&format!(
77            "[ProcessQuery] Parser returned {} top-level link(s).",
78            parsed_links.len()
79        ));
80
81        if parsed_links.is_empty() {
82            self.trace_msg("[ProcessQuery] No top-level parsed links found, returning.");
83            return Ok(vec![]);
84        }
85
86        // Accept both the wrapped form `((restriction) (substitution))` and
87        // the C# parser-compatible form `restriction substitution`.
88        let (restriction_link, substitution_link) = match &parsed_links[0].values {
89            Some(values) if values.len() >= 2 => (&values[0], &values[1]),
90            _ if parsed_links.len() >= 2 => (&parsed_links[0], &parsed_links[1]),
91            _ => {
92                self.trace_msg("[ProcessQuery] Query has fewer than 2 links, returning.");
93                return Ok(vec![]);
94            }
95        };
96
97        self.trace_msg(&format!(
98            "[ProcessQuery] Restriction link => Id={:?} Values.Count={}",
99            restriction_link.id,
100            restriction_link.values_count()
101        ));
102        self.trace_msg(&format!(
103            "[ProcessQuery] Substitution link => Id={:?} Values.Count={}",
104            substitution_link.id,
105            substitution_link.values_count()
106        ));
107
108        let mut changes_list = Vec::new();
109
110        // If both restriction and substitution are empty, do nothing
111        if restriction_link.is_empty() && substitution_link.is_empty() {
112            self.trace_msg(
113                "[ProcessQuery] Restriction & substitution both empty => no operation, returning.",
114            );
115            return Ok(vec![]);
116        }
117
118        // Creation scenario: no restriction, only substitution
119        if restriction_link.is_empty() && !substitution_link.is_empty() {
120            self.trace_msg(
121                "[ProcessQuery] No restriction, but substitution is non-empty => creation scenario.",
122            );
123            if let Some(values) = &substitution_link.values {
124                changes_list.extend(self.validate_links_exist_or_will_be_created(
125                    storage,
126                    &[],
127                    values,
128                )?);
129
130                for link_to_create in values {
131                    let created_id =
132                        self.ensure_link_created(storage, link_to_create, &mut changes_list)?;
133                    self.trace_msg(&format!(
134                        "[ProcessQuery] Created link ID #{} from substitution pattern.",
135                        created_id
136                    ));
137                }
138            }
139            storage.save()?;
140            return Ok(changes_list);
141        }
142
143        // Deletion scenario: restriction but no substitution
144        if !restriction_link.is_empty() && substitution_link.is_empty() {
145            self.trace_msg(
146                "[ProcessQuery] Restriction non-empty, substitution empty => deletion scenario.",
147            );
148            let restriction_values = restriction_link.values.as_deref().unwrap_or(&[]);
149            changes_list.extend(self.validate_links_exist_or_will_be_created(
150                storage,
151                restriction_values,
152                &[],
153            )?);
154
155            let restriction_patterns = self.patterns_from_lino(restriction_link);
156            let mut links_to_delete = Vec::new();
157            for pattern in &restriction_patterns {
158                links_to_delete.extend(self.matched_links(storage, pattern, &HashMap::new())?);
159            }
160            links_to_delete.sort_by_key(|link| link.index);
161            links_to_delete.dedup_by_key(|link| link.index);
162
163            for link in links_to_delete {
164                if storage.exists(link.index) {
165                    self.delete_observed(storage, link.index, &mut changes_list)?;
166                    self.trace_msg(&format!("[ProcessQuery] Deleted link ID #{}.", link.index));
167                }
168            }
169            storage.save()?;
170            return Ok(changes_list);
171        }
172
173        // Update/Mixed scenario: both restriction and substitution have values
174        self.trace_msg(
175            "[ProcessQuery] Both restriction and substitution non-empty => update/mixed scenario.",
176        );
177
178        let restriction_patterns = self.patterns_from_lino(restriction_link);
179        let substitution_patterns = self.patterns_from_lino(substitution_link);
180        let restriction_values = restriction_link.values.as_deref().unwrap_or(&[]);
181        let substitution_values = substitution_link.values.as_deref().unwrap_or(&[]);
182        changes_list.extend(self.validate_links_exist_or_will_be_created(
183            storage,
184            restriction_values,
185            substitution_values,
186        )?);
187        let solutions = self.find_all_solutions(storage, &restriction_patterns)?;
188
189        if solutions.is_empty() {
190            self.trace_msg("[ProcessQuery] No solutions found => returning.");
191            if !changes_list.is_empty() {
192                storage.save()?;
193            }
194            return Ok(changes_list);
195        }
196
197        let mut all_solutions_no_operation = true;
198        for solution in &solutions {
199            if !self.solution_is_no_operation(
200                storage,
201                solution,
202                &restriction_patterns,
203                &substitution_patterns,
204            )? {
205                all_solutions_no_operation = false;
206                break;
207            }
208        }
209
210        if all_solutions_no_operation {
211            for solution in &solutions {
212                for pattern in &restriction_patterns {
213                    for link in self.matched_links(storage, pattern, solution)? {
214                        if !changes_list.contains(&(Some(link), Some(link))) {
215                            changes_list.push((Some(link), Some(link)));
216                        }
217                    }
218                }
219            }
220            return Ok(changes_list);
221        }
222
223        let mut all_planned_operations = Vec::new();
224        for solution in &solutions {
225            let restriction_links = self.resolve_patterns(
226                storage,
227                &restriction_patterns,
228                solution,
229                false,
230                &mut changes_list,
231            )?;
232            let substitution_links = self.resolve_patterns(
233                storage,
234                &substitution_patterns,
235                solution,
236                true,
237                &mut changes_list,
238            )?;
239            all_planned_operations
240                .extend(self.determine_operations(&restriction_links, &substitution_links));
241        }
242
243        let intended_final_states = Self::intended_final_states(&all_planned_operations);
244
245        for (before, after) in all_planned_operations {
246            self.apply_operation(storage, before, after, &mut changes_list)?;
247        }
248
249        self.restore_unexpected_deletions(storage, &intended_final_states, &mut changes_list)?;
250
251        storage.save()?;
252
253        Ok(changes_list)
254    }
255
256    fn validate_links_exist_or_will_be_created(
257        &self,
258        storage: &mut impl NamedTypeLinks,
259        restriction_patterns: &[LinoLink],
260        substitution_patterns: &[LinoLink],
261    ) -> Result<Changes> {
262        LinkReferenceValidator::new(self.trace, self.auto_create_missing_references)
263            .validate_links_exist_or_will_be_created(
264                storage,
265                restriction_patterns,
266                substitution_patterns,
267            )
268    }
269
270    fn patterns_from_lino(&self, lino_link: &LinoLink) -> Vec<Pattern> {
271        let mut patterns = lino_link
272            .values
273            .as_ref()
274            .map(|values| {
275                values
276                    .iter()
277                    .map(Self::create_pattern_from_lino)
278                    .collect::<Vec<_>>()
279            })
280            .unwrap_or_default();
281
282        if lino_link.id.is_some() {
283            patterns.insert(0, Self::create_pattern_from_lino(lino_link));
284        }
285
286        patterns
287    }
288
289    fn create_pattern_from_lino(lino_link: &LinoLink) -> Pattern {
290        let index = lino_link.id.clone().unwrap_or_default();
291        match &lino_link.values {
292            Some(values) if values.len() == 2 => Pattern::new(
293                index,
294                Some(Self::create_pattern_from_lino(&values[0])),
295                Some(Self::create_pattern_from_lino(&values[1])),
296            ),
297            _ => Pattern::new(index, None, None),
298        }
299    }
300
301    fn find_all_solutions(
302        &self,
303        storage: &mut impl NamedTypeLinks,
304        patterns: &[Pattern],
305    ) -> Result<Vec<HashMap<String, u32>>> {
306        let mut partial_solutions = vec![HashMap::new()];
307
308        for pattern in patterns {
309            let mut new_solutions = Vec::new();
310            for solution in &partial_solutions {
311                for match_solution in self.match_pattern(storage, pattern, solution)? {
312                    if Self::solutions_are_compatible(solution, &match_solution) {
313                        let mut combined = solution.clone();
314                        combined.extend(match_solution);
315                        new_solutions.push(combined);
316                    }
317                }
318            }
319            partial_solutions = new_solutions;
320            if partial_solutions.is_empty() {
321                break;
322            }
323        }
324
325        Ok(partial_solutions)
326    }
327
328    fn solutions_are_compatible(
329        existing: &HashMap<String, u32>,
330        new_assignments: &HashMap<String, u32>,
331    ) -> bool {
332        new_assignments
333            .iter()
334            .all(|(key, value)| existing.get(key).is_none_or(|existing| existing == value))
335    }
336
337    fn resolve_patterns_readonly(
338        &self,
339        storage: &mut impl NamedTypeLinks,
340        patterns: &[Pattern],
341        solution: &HashMap<String, u32>,
342        is_substitution: bool,
343    ) -> Result<Vec<ResolvedLink>> {
344        let mut resolved = Vec::new();
345        for pattern in patterns {
346            if let Some(link) =
347                self.resolve_pattern_readonly(storage, pattern, solution, is_substitution)?
348            {
349                resolved.push(link);
350            }
351        }
352        Ok(resolved)
353    }
354
355    fn resolve_pattern_readonly(
356        &self,
357        storage: &mut impl NamedTypeLinks,
358        pattern: &Pattern,
359        solution: &HashMap<String, u32>,
360        is_substitution: bool,
361    ) -> Result<Option<ResolvedLink>> {
362        if pattern.is_leaf() {
363            let index = self.resolve_identifier_readonly(
364                storage,
365                &pattern.index,
366                solution,
367                if is_substitution { 0 } else { u32::MAX },
368            )?;
369            return Ok(Some(ResolvedLink::new(index, u32::MAX, u32::MAX, None)));
370        }
371
372        let source_pattern = pattern
373            .source
374            .as_deref()
375            .ok_or_else(|| LinkError::InvalidFormat("Invalid source pattern".to_string()))?;
376        let target_pattern = pattern
377            .target
378            .as_deref()
379            .ok_or_else(|| LinkError::InvalidFormat("Invalid target pattern".to_string()))?;
380
381        let source = self
382            .resolve_pattern_readonly(storage, source_pattern, solution, is_substitution)?
383            .ok_or_else(|| LinkError::InvalidFormat("Invalid source pattern".to_string()))?
384            .index;
385        let target = self
386            .resolve_pattern_readonly(storage, target_pattern, solution, is_substitution)?
387            .ok_or_else(|| LinkError::InvalidFormat("Invalid target pattern".to_string()))?
388            .index;
389        let default_index = if is_substitution { 0 } else { u32::MAX };
390        let index =
391            self.resolve_identifier_readonly(storage, &pattern.index, solution, default_index)?;
392
393        Ok(Some(ResolvedLink::new(index, source, target, None)))
394    }
395
396    fn resolve_identifier_readonly(
397        &self,
398        storage: &mut impl NamedTypeLinks,
399        identifier: &str,
400        solution: &HashMap<String, u32>,
401        default_value: u32,
402    ) -> Result<u32> {
403        if identifier.is_empty() {
404            return Ok(default_value);
405        }
406        if identifier == "*" {
407            return Ok(u32::MAX);
408        }
409        if let Some(value) = solution.get(identifier) {
410            return Ok(*value);
411        }
412        if Self::is_variable(identifier) {
413            return Ok(default_value);
414        }
415        if let Ok(parsed) = identifier.parse::<u32>() {
416            return Ok(parsed);
417        }
418        Ok(storage.get_by_name(identifier)?.unwrap_or(default_value))
419    }
420
421    fn resolve_patterns(
422        &self,
423        storage: &mut impl NamedTypeLinks,
424        patterns: &[Pattern],
425        solution: &HashMap<String, u32>,
426        is_substitution: bool,
427        changes: &mut Changes,
428    ) -> Result<Vec<ResolvedLink>> {
429        let mut working_solution = solution.clone();
430        let mut visited_indexes = HashSet::new();
431        let mut resolved = Vec::new();
432        for pattern in patterns {
433            resolved.push(self.resolve_pattern(
434                storage,
435                pattern,
436                &mut working_solution,
437                is_substitution,
438                &mut visited_indexes,
439                changes,
440            )?);
441        }
442        Ok(resolved)
443    }
444
445    fn resolve_pattern(
446        &self,
447        storage: &mut impl NamedTypeLinks,
448        pattern: &Pattern,
449        solution: &mut HashMap<String, u32>,
450        is_substitution: bool,
451        visited_indexes: &mut HashSet<u32>,
452        changes: &mut Changes,
453    ) -> Result<ResolvedLink> {
454        if pattern.is_leaf() {
455            let index = self.resolve_identifier(
456                storage,
457                &pattern.index,
458                solution,
459                if is_substitution { 0 } else { u32::MAX },
460                is_substitution,
461                changes,
462            )?;
463            return Ok(ResolvedLink::new(index, u32::MAX, u32::MAX, None));
464        }
465
466        let mut source = self
467            .resolve_pattern(
468                storage,
469                pattern.source.as_deref().unwrap(),
470                solution,
471                is_substitution,
472                visited_indexes,
473                changes,
474            )?
475            .index;
476        let mut target = self
477            .resolve_pattern(
478                storage,
479                pattern.target.as_deref().unwrap(),
480                solution,
481                is_substitution,
482                visited_indexes,
483                changes,
484            )?
485            .index;
486        let default_index = if is_substitution { 0 } else { u32::MAX };
487        let mut index = self.resolve_identifier(
488            storage,
489            &pattern.index,
490            solution,
491            default_index,
492            false,
493            changes,
494        )?;
495        let mut name = None;
496
497        if is_substitution
498            && !pattern.index.is_empty()
499            && !Self::is_numeric_or_wildcard(&pattern.index)
500            && !Self::is_variable(&pattern.index)
501        {
502            name = Some(pattern.index.clone());
503            if index == 0 {
504                if let Some(existing_id) = storage.search(source, target) {
505                    index = existing_id;
506                }
507            }
508        }
509
510        if is_substitution {
511            Self::preserve_existing_substitution_parts(
512                storage,
513                pattern,
514                solution,
515                index,
516                &mut source,
517                &mut target,
518                visited_indexes,
519            )?;
520        }
521
522        Ok(ResolvedLink::new(index, source, target, name))
523    }
524
525    fn resolve_identifier(
526        &self,
527        storage: &mut impl NamedTypeLinks,
528        identifier: &str,
529        solution: &HashMap<String, u32>,
530        default_value: u32,
531        create_named_leaf: bool,
532        changes: &mut Changes,
533    ) -> Result<u32> {
534        if identifier.is_empty() {
535            return Ok(default_value);
536        }
537        if identifier == "*" {
538            return Ok(u32::MAX);
539        }
540        if let Some(value) = solution.get(identifier) {
541            return Ok(*value);
542        }
543        if Self::is_variable(identifier) {
544            return Ok(default_value);
545        }
546        if let Ok(parsed) = identifier.parse::<u32>() {
547            return Ok(parsed);
548        }
549        if create_named_leaf {
550            return self.ensure_named_point_link(storage, identifier, changes);
551        }
552        Ok(storage.get_by_name(identifier)?.unwrap_or(default_value))
553    }
554
555    fn determine_operations(
556        &self,
557        restrictions: &[ResolvedLink],
558        substitutions: &[ResolvedLink],
559    ) -> Vec<(Option<ResolvedLink>, Option<ResolvedLink>)> {
560        let mut operations = Vec::new();
561        let mut restriction_by_index = HashMap::new();
562        let mut substitution_by_index = HashMap::new();
563        let mut wildcard_restrictions = Vec::new();
564        let mut wildcard_substitutions = Vec::new();
565
566        for restriction in restrictions {
567            if Self::is_normal_index(restriction.index) {
568                restriction_by_index.insert(restriction.index, restriction.clone());
569            } else {
570                wildcard_restrictions.push(restriction.clone());
571            }
572        }
573
574        for substitution in substitutions {
575            if Self::is_normal_index(substitution.index) {
576                substitution_by_index.insert(substitution.index, substitution.clone());
577            } else {
578                wildcard_substitutions.push(substitution.clone());
579            }
580        }
581
582        let mut all_indices = restriction_by_index
583            .keys()
584            .chain(substitution_by_index.keys())
585            .copied()
586            .collect::<Vec<_>>();
587        all_indices.sort_unstable();
588        all_indices.dedup();
589
590        for index in all_indices {
591            match (
592                restriction_by_index.get(&index),
593                substitution_by_index.get(&index),
594            ) {
595                (Some(before), Some(after)) => {
596                    operations.push((Some(before.clone()), Some(after.clone())));
597                }
598                (Some(before), None) => operations.push((Some(before.clone()), None)),
599                (None, Some(after)) => operations.push((None, Some(after.clone()))),
600                (None, None) => {}
601            }
602        }
603
604        operations.extend(
605            wildcard_restrictions
606                .into_iter()
607                .map(|restriction| (Some(restriction), None)),
608        );
609        operations.extend(
610            wildcard_substitutions
611                .into_iter()
612                .map(|substitution| (None, Some(substitution))),
613        );
614
615        operations
616    }
617
618    fn apply_operation(
619        &self,
620        storage: &mut impl NamedTypeLinks,
621        before: Option<ResolvedLink>,
622        after: Option<ResolvedLink>,
623        changes: &mut Changes,
624    ) -> Result<()> {
625        match (before, after) {
626            (Some(before), None) => {
627                let mut links = self.links_matching_definition(storage, &before)?;
628                links.sort_by_key(|link| link.index);
629                links.dedup_by_key(|link| link.index);
630                for link in links {
631                    if storage.exists(link.index) {
632                        self.delete_observed(storage, link.index, changes)?;
633                    }
634                }
635            }
636            (None, Some(after)) => {
637                self.create_or_update_resolved_link(storage, &after, changes)?;
638            }
639            (Some(before), Some(after)) => {
640                if before.index == after.index && storage.exists(before.index) {
641                    let stored = storage.get_link(before.index).unwrap();
642                    if stored.source != after.source || stored.target != after.target {
643                        // A merge into a duplicate deletes `before.index`;
644                        // `restore_unexpected_deletions` puts it back when the
645                        // query asked for it, exactly as the C# processor does.
646                        self.update_observed(
647                            storage,
648                            before.index,
649                            after.source,
650                            after.target,
651                            changes,
652                        )?;
653                    } else {
654                        changes.push((Some(stored), Some(stored)));
655                    }
656                    if let Some(name) = &after.name {
657                        storage.set_name(before.index, name)?;
658                    }
659                } else {
660                    self.apply_operation(storage, Some(before), None, changes)?;
661                    self.apply_operation(storage, None, Some(after), changes)?;
662                }
663            }
664            (None, None) => {}
665        }
666
667        Ok(())
668    }
669
670    fn links_matching_definition(
671        &self,
672        storage: &mut impl NamedTypeLinks,
673        definition: &ResolvedLink,
674    ) -> Result<Vec<Link>> {
675        Ok(storage
676            .all_links()
677            .into_iter()
678            .filter(|link| {
679                (definition.index == 0
680                    || Self::is_any(definition.index)
681                    || link.index == definition.index)
682                    && (Self::is_any(definition.source) || link.source == definition.source)
683                    && (Self::is_any(definition.target) || link.target == definition.target)
684            })
685            .collect())
686    }
687
688    fn assign_variable(id: &str, value: u32, assignments: &mut HashMap<String, u32>) {
689        if Self::is_variable(id) && value != 0 {
690            assignments.insert(id.to_string(), value);
691        }
692    }
693
694    fn is_variable(identifier: &str) -> bool {
695        !identifier.is_empty() && identifier.starts_with('$')
696    }
697
698    fn is_any(value: u32) -> bool {
699        value == u32::MAX
700    }
701
702    /// Resolves a half a query left unspecified against the half already
703    /// stored, the way C# resolves its `any` constant on the way into the
704    /// store.
705    ///
706    /// The C# processor marks an unbound substitution variable — and a `*` — with
707    /// `links.Constants.Any`, which is a value the *store* understands:
708    /// `Update` leaves a half substituted with `any` exactly as it was, and a
709    /// link created from one gets `null` there. This processor marks the same
710    /// thing with [`u32::MAX`], which the store underneath does not recognise
711    /// (its `any` is `2147483644`, the hybrid-aware constant), so `() (($a $a))`
712    /// used to store the literal `4294967295` in both halves where C# stores
713    /// `(1: 0 0)`. Resolving at the write boundary keeps [`u32::MAX`] as this
714    /// crate's single internal marker while writing what C# writes.
715    fn resolve_unspecified(value: u32, existing: u32) -> u32 {
716        if Self::is_any(value) {
717            existing
718        } else {
719            value
720        }
721    }
722
723    /// Looks a doublet up with unspecified halves treated as wildcards, the way
724    /// C#'s `SearchOrDefault` does.
725    ///
726    /// `SearchOrDefault` runs through `Each`, which reads `any` in a query as
727    /// "every value" rather than as a literal address, so `() ((1 $a))` finds a
728    /// stored `(1: 1 1)` instead of creating a second link beside it.
729    /// [`NamedTypeLinks::search`] matches literally on purpose — it backs
730    /// uniqueness resolution — so the wildcard pass belongs here.
731    fn search_unspecified(
732        storage: &mut impl NamedTypeLinks,
733        source: u32,
734        target: u32,
735    ) -> Option<u32> {
736        if !Self::is_any(source) && !Self::is_any(target) {
737            return storage.search(source, target);
738        }
739        storage
740            .all_links()
741            .into_iter()
742            .filter(|link| {
743                (Self::is_any(source) || link.source == source)
744                    && (Self::is_any(target) || link.target == target)
745            })
746            .map(|link| link.index)
747            .min()
748    }
749
750    fn is_normal_index(value: u32) -> bool {
751        value != 0 && !Self::is_any(value)
752    }
753
754    fn is_numeric_or_wildcard(identifier: &str) -> bool {
755        identifier == "*" || identifier.parse::<u32>().is_ok()
756    }
757
758    /// Simplifies the changes list.
759    ///
760    /// A missing side — the state before a creation, or the state after a
761    /// deletion — becomes the null link `(0: 0 0)` on the way in and turns back
762    /// into `None` on the way out. C# has no option type here and feeds the
763    /// simplifier `default(Link<uint>)` for both, so routing the null states
764    /// around the simplifier (as this used to) both reported creations and
765    /// deletions in a different order than C# and hid them from the chain
766    /// collapsing that is the whole point of the pass.
767    fn simplify_changes_list(&self, changes: &[(Option<Link>, Option<Link>)]) -> Changes {
768        let to_simplify: Vec<(Link, Link)> = changes
769            .iter()
770            .map(|(before, after)| {
771                (
772                    before.unwrap_or_else(Link::null),
773                    after.unwrap_or_else(Link::null),
774                )
775            })
776            .collect();
777
778        simplify_changes(to_simplify)
779            .into_iter()
780            .map(|(before, after)| {
781                (
782                    (!before.is_null()).then_some(before),
783                    (!after.is_null()).then_some(after),
784                )
785            })
786            .collect()
787    }
788
789    /// Logs a trace message if tracing is enabled
790    fn trace_msg(&self, msg: &str) {
791        if self.trace {
792            eprintln!("{}", msg);
793        }
794    }
795}