Skip to main content

link_cli/
query_processor_substitution.rs

1//! The substitution half of [`QueryProcessor`]: turning a matched restriction
2//! plus a substitution pattern into the links to write.
3
4use anyhow::Result;
5use std::collections::{HashMap, HashSet};
6
7use crate::error::LinkError;
8use crate::link::Link;
9use crate::named_type_links::NamedTypeLinks;
10use crate::query_processor::QueryProcessor;
11use crate::query_types::Pattern;
12
13impl QueryProcessor {
14    pub fn preserve_existing_substitution_parts(
15        storage: &mut impl NamedTypeLinks,
16        pattern: &Pattern,
17        solution: &mut HashMap<String, u32>,
18        index: u32,
19        source: &mut u32,
20        target: &mut u32,
21        visited_indexes: &mut HashSet<u32>,
22    ) -> Result<()> {
23        if !is_normal_index(index) || !storage.exists(index) {
24            return Ok(());
25        }
26
27        if !visited_indexes.insert(index) {
28            return Ok(());
29        }
30
31        let existing_link = storage.get_link(index).ok_or(LinkError::not_found(index))?;
32
33        if should_preserve_existing_part(pattern.source.as_deref(), solution)
34            && can_preserve_existing_part(existing_link, existing_link.source, visited_indexes)
35        {
36            *source = existing_link.source;
37            assign_variable_from_pattern(pattern.source.as_deref(), *source, solution);
38        } else if let Some(bound_source) =
39            resolved_variable_part(pattern.source.as_deref(), solution)
40        {
41            *source = bound_source;
42        }
43
44        if should_preserve_existing_part(pattern.target.as_deref(), solution)
45            && can_preserve_existing_part(existing_link, existing_link.target, visited_indexes)
46        {
47            *target = existing_link.target;
48            assign_variable_from_pattern(pattern.target.as_deref(), *target, solution);
49        } else if let Some(bound_target) =
50            resolved_variable_part(pattern.target.as_deref(), solution)
51        {
52            *target = bound_target;
53        }
54
55        visited_indexes.remove(&index);
56        Ok(())
57    }
58}
59
60fn should_preserve_existing_part(
61    pattern: Option<&Pattern>,
62    solution: &HashMap<String, u32>,
63) -> bool {
64    pattern.is_some_and(|pattern| {
65        pattern.is_leaf() && is_variable(&pattern.index) && !solution.contains_key(&pattern.index)
66    })
67}
68
69fn resolved_variable_part(
70    pattern: Option<&Pattern>,
71    solution: &HashMap<String, u32>,
72) -> Option<u32> {
73    pattern.and_then(|pattern| {
74        if pattern.is_leaf() && is_variable(&pattern.index) {
75            solution.get(&pattern.index).copied()
76        } else {
77            None
78        }
79    })
80}
81
82fn assign_variable_from_pattern(
83    pattern: Option<&Pattern>,
84    value: u32,
85    solution: &mut HashMap<String, u32>,
86) {
87    if let Some(pattern) = pattern {
88        assign_variable(&pattern.index, value, solution);
89    }
90}
91
92fn assign_variable(id: &str, value: u32, assignments: &mut HashMap<String, u32>) {
93    if is_variable(id) && value != 0 {
94        assignments.insert(id.to_string(), value);
95    }
96}
97
98fn can_preserve_existing_part(
99    existing_link: Link,
100    part: u32,
101    visited_indexes: &HashSet<u32>,
102) -> bool {
103    existing_link.is_full_point()
104        || existing_link.is_partial_point()
105        || !visited_indexes.contains(&part)
106}
107
108fn is_variable(identifier: &str) -> bool {
109    !identifier.is_empty() && identifier.starts_with('$')
110}
111
112fn is_normal_index(value: u32) -> bool {
113    value != 0 && value != u32::MAX
114}