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