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