Skip to main content

sqry_core/graph/unified/bind/
query.rs

1//! `BindingQuery` builder — moved out of the old single-file `bind.rs` into
2//! its own file so the new `bind/` module layout can coexist with later P2U
3//! units that split the facade and the query bridge.
4//!
5//! P2U01 moves the struct and its `impl<'a>` block verbatim — no logic
6//! change. P2U07 rewires `resolve()` to delegate through the private
7//! `resolve_shared()` helper in `bind/plane.rs`, which is also called by
8//! `BindingPlane::resolve()`. This is the byte-equality refactor; the T19
9//! gate in `phase2_binding_plane_in_memory.rs` proves the output is
10//! identical to the pre-P2U07 snapshot captured before this change.
11//!
12//! P2U08 adds [`BindingQuery::resolve_with_witness`] — an opt-in upgrade path
13//! that returns the full [`super::plane::BindingResolution`] (result + witness
14//! step trace) without disturbing the existing [`BindingQuery::resolve`] API.
15
16use crate::graph::unified::concurrent::GraphSnapshot;
17use crate::graph::unified::resolution::{FileScope, ResolutionMode, SymbolQuery};
18
19use super::BindingResult;
20use super::plane::BindingResolution;
21
22/// Builder for binding queries.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// let result = BindingQuery::new("MyClass")
28///     .file_scope(FileScope::Any)
29///     .mode(ResolutionMode::AllowSuffixCandidates)
30///     .resolve(&snapshot);
31/// ```
32pub struct BindingQuery<'a> {
33    symbol: &'a str,
34    file_scope: FileScope<'a>,
35    mode: ResolutionMode,
36}
37
38impl<'a> BindingQuery<'a> {
39    /// Creates a new binding query for the given symbol.
40    ///
41    /// Defaults to `FileScope::Any` and `ResolutionMode::AllowSuffixCandidates`.
42    #[must_use]
43    pub fn new(symbol: &'a str) -> Self {
44        Self {
45            symbol,
46            file_scope: FileScope::Any,
47            mode: ResolutionMode::AllowSuffixCandidates,
48        }
49    }
50
51    /// Restricts the query to a specific file scope.
52    #[must_use]
53    pub fn file_scope(mut self, scope: FileScope<'a>) -> Self {
54        self.file_scope = scope;
55        self
56    }
57
58    /// Sets the resolution mode.
59    #[must_use]
60    pub fn mode(mut self, mode: ResolutionMode) -> Self {
61        self.mode = mode;
62        self
63    }
64
65    /// Resolves the query against the given snapshot.
66    ///
67    /// Delegates to [`super::plane::resolve_shared`] which is the shared
68    /// implementation core used by both `BindingQuery::resolve()` and
69    /// `BindingPlane::resolve()`. Returns only the `BindingResult` (without
70    /// the witness step trace) for backward compatibility with existing
71    /// callers.
72    ///
73    /// The byte-equality T19 gate in `phase2_binding_plane_in_memory.rs`
74    /// asserts that the output of this method is identical to the pre-P2U07
75    /// snapshot captured in `test-fixtures/phase2_binding_result_snapshots/`.
76    #[must_use]
77    pub fn resolve(self, snapshot: &GraphSnapshot) -> BindingResult {
78        let query = SymbolQuery {
79            symbol: self.symbol,
80            file_scope: self.file_scope,
81            mode: self.mode,
82        };
83        super::plane::resolve_shared(&query, snapshot).result
84    }
85
86    /// Opt-in upgrade path — resolves the query and returns the full
87    /// [`BindingResolution`] containing both the [`BindingResult`] and the
88    /// ordered witness step trace.
89    ///
90    /// Use this method when you need access to the resolution witness (e.g.,
91    /// for CLI `--explain` output, rule-layer consumers, or diagnostic tooling
92    /// introduced in Phase 2). Callers that only need the binding outcome
93    /// should continue to use [`BindingQuery::resolve`] which returns the
94    /// leaner [`BindingResult`] directly.
95    ///
96    /// The `result` field of the returned [`BindingResolution`] is byte-equal
97    /// to the value that [`BindingQuery::resolve`] would return for the same
98    /// query — both delegate to the same [`super::plane::resolve_shared`] core.
99    #[must_use]
100    pub fn resolve_with_witness(self, snapshot: &GraphSnapshot) -> BindingResolution {
101        let query = SymbolQuery {
102            symbol: self.symbol,
103            file_scope: self.file_scope,
104            mode: self.mode,
105        };
106        super::plane::resolve_shared(&query, snapshot)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::graph::node::Language;
114    use crate::graph::unified::concurrent::CodeGraph;
115    use crate::graph::unified::edge::kind::EdgeKind;
116    use crate::graph::unified::node::kind::NodeKind;
117    use crate::graph::unified::storage::arena::NodeEntry;
118
119    #[test]
120    fn builder_defaults() {
121        let query = BindingQuery::new("test_sym");
122        assert_eq!(query.symbol, "test_sym");
123        assert_eq!(query.file_scope, FileScope::Any);
124        assert_eq!(query.mode, ResolutionMode::AllowSuffixCandidates);
125    }
126
127    /// Builds a minimal graph containing a single named function under a root module.
128    fn make_graph_with_function(sym: &str) -> CodeGraph {
129        let mut graph = CodeGraph::new();
130        let path = std::path::PathBuf::from("/query-tests/test.rs");
131        let file_id = graph
132            .files_mut()
133            .register_with_language(&path, Some(Language::Rust))
134            .expect("register file");
135        let name = graph.strings_mut().intern(sym).expect("intern sym");
136        let qn = graph
137            .strings_mut()
138            .intern(&format!("crate::{sym}"))
139            .expect("intern qn");
140        let mod_name = graph.strings_mut().intern("root").expect("intern root");
141        let mod_qn = graph.strings_mut().intern("crate").expect("intern crate");
142        let mod_id = graph
143            .nodes_mut()
144            .alloc(
145                NodeEntry::new(NodeKind::Module, mod_name, file_id)
146                    .with_qualified_name(mod_qn)
147                    .with_byte_range(0, 100),
148            )
149            .expect("alloc mod");
150        graph
151            .indices_mut()
152            .add(mod_id, NodeKind::Module, mod_name, Some(mod_qn), file_id);
153        let fn_id = graph
154            .nodes_mut()
155            .alloc(
156                NodeEntry::new(NodeKind::Function, name, file_id)
157                    .with_qualified_name(qn)
158                    .with_byte_range(5, 80),
159            )
160            .expect("alloc fn");
161        graph
162            .indices_mut()
163            .add(fn_id, NodeKind::Function, name, Some(qn), file_id);
164        graph
165            .edges_mut()
166            .add_edge(mod_id, fn_id, EdgeKind::Contains, file_id);
167        graph
168    }
169
170    /// T19-companion: `resolve_with_witness().result` must be byte-equal to
171    /// `resolve()` for the same query parameters.
172    #[test]
173    fn resolve_with_witness_result_matches_resolve() {
174        let graph = make_graph_with_function("witness_fn");
175        let snapshot = graph.snapshot();
176
177        let result_only = BindingQuery::new("witness_fn")
178            .file_scope(FileScope::Any)
179            .mode(ResolutionMode::AllowSuffixCandidates)
180            .resolve(&snapshot);
181
182        let with_witness = BindingQuery::new("witness_fn")
183            .file_scope(FileScope::Any)
184            .mode(ResolutionMode::AllowSuffixCandidates)
185            .resolve_with_witness(&snapshot);
186
187        assert_eq!(
188            result_only, with_witness.result,
189            "resolve_with_witness().result must be byte-equal to resolve()"
190        );
191    }
192
193    /// `resolve_with_witness()` must carry a non-empty step trace when the
194    /// symbol is found — proving the witness is populated, not empty.
195    #[test]
196    fn resolve_with_witness_has_non_empty_steps_on_found() {
197        let graph = make_graph_with_function("stepped_fn");
198        let snapshot = graph.snapshot();
199
200        let resolution = BindingQuery::new("stepped_fn")
201            .file_scope(FileScope::Any)
202            .mode(ResolutionMode::AllowSuffixCandidates)
203            .resolve_with_witness(&snapshot);
204
205        assert!(
206            !resolution.witness.steps.is_empty(),
207            "witness step trace must be non-empty for a resolved symbol"
208        );
209    }
210
211    /// `resolve_with_witness()` for a missing symbol must be consistent:
212    /// `result` matches `resolve()` and the witness step trace is non-empty.
213    #[test]
214    fn resolve_with_witness_not_found_consistent_with_resolve() {
215        let graph = make_graph_with_function("any_fn");
216        let snapshot = graph.snapshot();
217
218        let result_only = BindingQuery::new("does_not_exist")
219            .file_scope(FileScope::Any)
220            .mode(ResolutionMode::AllowSuffixCandidates)
221            .resolve(&snapshot);
222
223        let with_witness = BindingQuery::new("does_not_exist")
224            .file_scope(FileScope::Any)
225            .mode(ResolutionMode::AllowSuffixCandidates)
226            .resolve_with_witness(&snapshot);
227
228        assert_eq!(
229            result_only, with_witness.result,
230            "resolve_with_witness().result must match resolve() for missing symbols"
231        );
232        assert!(
233            !with_witness.witness.steps.is_empty(),
234            "witness step trace must be non-empty even for unresolved symbols"
235        );
236    }
237}