plotnik_lib/query/
graph.rs

1//! Core types for build-time query graphs.
2//!
3//! The graph uses index-based node references (`NodeId`) with nodes stored
4//! in a `Vec`. Strings borrow from the source (`&'src str`) until IR emission.
5
6use crate::ir::Nav;
7use indexmap::IndexMap;
8use rowan::TextRange;
9
10/// Index into `BuildGraph::nodes`.
11pub type NodeId = u32;
12
13/// A graph fragment with single entry and exit points.
14///
15/// Every expression compiles to a fragment. Combinators connect fragments
16/// by manipulating entry/exit edges.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Fragment {
19    pub entry: NodeId,
20    pub exit: NodeId,
21}
22
23impl Fragment {
24    pub fn new(entry: NodeId, exit: NodeId) -> Self {
25        Self { entry, exit }
26    }
27
28    pub fn single(node: NodeId) -> Self {
29        Self {
30            entry: node,
31            exit: node,
32        }
33    }
34}
35
36/// Array collection mode for loop combinators.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ArrayMode {
39    /// No array collection (simple repetition)
40    None,
41    /// Collect elements into array (StartArray/PushElement/EndArray)
42    Simple,
43    /// Collect with object scope per iteration (for QIS)
44    Qis,
45}
46
47/// Build-time graph for query compilation.
48///
49/// Nodes are stored in a flat vector, referenced by `NodeId`.
50/// Definitions map names to their entry points.
51#[derive(Debug)]
52pub struct BuildGraph<'src> {
53    nodes: Vec<BuildNode<'src>>,
54    definitions: IndexMap<&'src str, NodeId>,
55}
56
57impl<'src> BuildGraph<'src> {
58    pub fn new() -> Self {
59        Self {
60            nodes: Vec::new(),
61            definitions: IndexMap::new(),
62        }
63    }
64
65    pub fn add_node(&mut self, node: BuildNode<'src>) -> NodeId {
66        let id = self.nodes.len() as NodeId;
67        self.nodes.push(node);
68        id
69    }
70
71    pub fn add_epsilon(&mut self) -> NodeId {
72        self.add_node(BuildNode::epsilon())
73    }
74
75    /// Clone a node, creating a new node with the same matcher, effects, and ref_marker,
76    /// but with the specified nav and copying the successors list.
77    pub fn clone_node_with_nav(&mut self, node_id: NodeId, nav: Nav) -> NodeId {
78        let original = &self.nodes[node_id as usize];
79        let cloned = BuildNode {
80            matcher: original.matcher.clone(),
81            effects: original.effects.clone(),
82            ref_marker: original.ref_marker.clone(),
83            successors: original.successors.clone(),
84            nav,
85            ref_name: original.ref_name,
86        };
87        self.add_node(cloned)
88    }
89
90    pub fn add_matcher(&mut self, matcher: BuildMatcher<'src>) -> NodeId {
91        self.add_node(BuildNode::with_matcher(matcher))
92    }
93
94    pub fn add_definition(&mut self, name: &'src str, entry: NodeId) {
95        self.definitions.insert(name, entry);
96    }
97
98    pub fn definition(&self, name: &str) -> Option<NodeId> {
99        self.definitions.get(name).copied()
100    }
101
102    pub fn definitions(&self) -> impl Iterator<Item = (&'src str, NodeId)> + '_ {
103        self.definitions.iter().map(|(k, v)| (*k, *v))
104    }
105
106    pub fn node(&self, id: NodeId) -> &BuildNode<'src> {
107        &self.nodes[id as usize]
108    }
109
110    pub fn node_mut(&mut self, id: NodeId) -> &mut BuildNode<'src> {
111        &mut self.nodes[id as usize]
112    }
113
114    pub fn len(&self) -> usize {
115        self.nodes.len()
116    }
117
118    pub fn is_empty(&self) -> bool {
119        self.nodes.is_empty()
120    }
121
122    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &BuildNode<'src>)> {
123        self.nodes.iter().enumerate().map(|(i, n)| (i as NodeId, n))
124    }
125
126    pub fn connect(&mut self, from: NodeId, to: NodeId) {
127        self.nodes[from as usize].successors.push(to);
128    }
129
130    pub fn connect_exit(&mut self, fragment: Fragment, to: NodeId) {
131        self.connect(fragment.exit, to);
132    }
133
134    pub fn matcher_fragment(&mut self, matcher: BuildMatcher<'src>) -> Fragment {
135        Fragment::single(self.add_matcher(matcher))
136    }
137
138    pub fn epsilon_fragment(&mut self) -> Fragment {
139        Fragment::single(self.add_epsilon())
140    }
141
142    /// Connect fragments in sequence: f1 → f2 → ... → fn
143    pub fn sequence(&mut self, fragments: &[Fragment]) -> Fragment {
144        match fragments.len() {
145            0 => self.epsilon_fragment(),
146            1 => fragments[0],
147            _ => {
148                for window in fragments.windows(2) {
149                    self.connect(window[0].exit, window[1].entry);
150                }
151                Fragment::new(fragments[0].entry, fragments[fragments.len() - 1].exit)
152            }
153        }
154    }
155
156    /// Connect fragments in parallel (alternation): entry → [f1|f2|...|fn] → exit
157    pub fn alternation(&mut self, fragments: &[Fragment]) -> Fragment {
158        if fragments.is_empty() {
159            return self.epsilon_fragment();
160        }
161        if fragments.len() == 1 {
162            return fragments[0];
163        }
164
165        let entry = self.add_epsilon();
166        let exit = self.add_epsilon();
167
168        for f in fragments {
169            self.connect(entry, f.entry);
170            self.connect(f.exit, exit);
171        }
172
173        Fragment::new(entry, exit)
174    }
175
176    /// Generic loop combinator for * and + quantifiers.
177    ///
178    /// - `at_least_one`: true for + (one or more), false for * (zero or more)
179    /// - `greedy`: true for greedy (try match first), false for lazy (try exit first)
180    /// - `mode`: array collection mode
181    fn build_repetition(
182        &mut self,
183        inner: Fragment,
184        at_least_one: bool,
185        greedy: bool,
186        mode: ArrayMode,
187        initial_nav: Nav,
188    ) -> Fragment {
189        let has_array = mode != ArrayMode::None;
190        let has_qis = mode == ArrayMode::Qis;
191
192        // Array wrapper nodes
193        let start = if has_array {
194            let s = self.add_epsilon();
195            self.node_mut(s).add_effect(BuildEffect::StartArray {
196                is_plus: at_least_one,
197            });
198            Some(s)
199        } else {
200            None
201        };
202
203        let end = if has_array {
204            let e = self.add_epsilon();
205            self.node_mut(e).add_effect(BuildEffect::EndArray);
206            Some(e)
207        } else {
208            None
209        };
210
211        // QIS object wrapper nodes
212        let (obj_start, obj_end) = if has_qis {
213            let os = self.add_epsilon();
214            self.node_mut(os).add_effect(BuildEffect::StartObject {
215                for_alternation: false,
216            });
217            let oe = self.add_epsilon();
218            self.node_mut(oe).add_effect(BuildEffect::EndObject);
219            (Some(os), Some(oe))
220        } else {
221            (None, None)
222        };
223
224        // Push node for array modes
225        let push = if has_array {
226            let p = self.add_epsilon();
227            self.node_mut(p).add_effect(BuildEffect::PushElement);
228            Some(p)
229        } else {
230            None
231        };
232
233        // Branch node (decision point for loop continuation)
234        let branch = self.add_epsilon();
235
236        // Exit node for non-array modes
237        let exit = if !has_array {
238            Some(self.add_epsilon())
239        } else {
240            None
241        };
242
243        // Determine the effective inner entry/exit (with QIS wrapping if needed)
244        let (loop_body_entry, loop_body_exit) = if has_qis {
245            self.connect(obj_start.unwrap(), inner.entry);
246            self.connect(inner.exit, obj_end.unwrap());
247            (obj_start.unwrap(), obj_end.unwrap())
248        } else {
249            (inner.entry, inner.exit)
250        };
251
252        // Set initial navigation on inner.entry (the actual matcher).
253        // In QIS mode, this is distinct from loop_body_entry (the object wrapper).
254        self.node_mut(inner.entry).set_nav(initial_nav);
255
256        // For re-entry (subsequent iterations), clone inner.entry with Next nav.
257        // This creates a separate path for re-entry that can skip non-matching siblings.
258        // In QIS mode, we clone inner.entry (not obj_start) to avoid duplicating the wrapper.
259        let try_next = self.clone_node_with_nav(inner.entry, Nav::next());
260
261        // QIS object wrapper for try_next re-entry path
262        let (try_next_entry, try_next_exit) = if has_qis {
263            let os = self.add_epsilon();
264            self.node_mut(os).add_effect(BuildEffect::StartObject {
265                for_alternation: false,
266            });
267            let oe = self.add_epsilon();
268            self.node_mut(oe).add_effect(BuildEffect::EndObject);
269            self.connect(os, try_next);
270            self.connect(try_next, oe);
271            (os, oe)
272        } else {
273            (try_next, try_next)
274        };
275
276        // Wire up the graph based on at_least_one and greedy
277        if at_least_one {
278            // + pattern: must match at least once
279            // Entry → loop_body_entry → body → push → re_entry → (try_next → body or exit)
280            let entry_point = start.unwrap_or(loop_body_entry);
281            let exit_point = end.or(exit).unwrap();
282
283            // re_entry is a branch point (no nav) that chooses: try more or exit
284            let re_entry = self.add_epsilon();
285
286            if let Some(s) = start {
287                self.connect(s, loop_body_entry);
288            }
289
290            if let Some(p) = push {
291                self.connect(loop_body_exit, p);
292                // try_next also needs to connect to push after matching
293                self.connect(try_next_exit, p);
294                self.connect(p, re_entry);
295            } else {
296                self.connect(loop_body_exit, re_entry);
297                self.connect(try_next_exit, re_entry);
298            }
299
300            // re_entry branches: try_next (Next nav) or exit
301            // If try_next's Next fails, backtrack finds re_entry checkpoint and tries exit
302            if greedy {
303                self.connect(re_entry, try_next_entry);
304                self.connect(re_entry, exit_point);
305            } else {
306                self.connect(re_entry, exit_point);
307                self.connect(re_entry, try_next_entry);
308            }
309
310            Fragment::new(entry_point, exit_point)
311        } else {
312            // * pattern: zero or more
313            // Entry → branch → (loop_body_entry → body → push → re_entry → try_next → body) or exit
314            let entry_point = start.unwrap_or(branch);
315            let exit_point = end.or(exit).unwrap();
316
317            // re_entry is a branch point (no nav) that chooses: try more or exit
318            let re_entry = self.add_epsilon();
319
320            if let Some(s) = start {
321                self.connect(s, branch);
322            }
323
324            if greedy {
325                self.connect(branch, loop_body_entry);
326                self.connect(branch, exit_point);
327            } else {
328                self.connect(branch, exit_point);
329                self.connect(branch, loop_body_entry);
330            }
331
332            if let Some(p) = push {
333                self.connect(loop_body_exit, p);
334                // try_next also needs to connect to push after matching
335                self.connect(try_next_exit, p);
336                self.connect(p, re_entry);
337            } else {
338                self.connect(loop_body_exit, re_entry);
339                self.connect(try_next_exit, re_entry);
340            }
341
342            // re_entry branches: try_next (Next nav) or exit
343            // If try_next's Next fails, backtrack finds re_entry checkpoint and tries exit
344            if greedy {
345                self.connect(re_entry, try_next_entry);
346                self.connect(re_entry, exit_point);
347            } else {
348                self.connect(re_entry, exit_point);
349                self.connect(re_entry, try_next_entry);
350            }
351
352            Fragment::new(entry_point, exit_point)
353        }
354    }
355
356    /// Generic optional combinator for ? quantifier.
357    ///
358    /// - `greedy`: true for greedy (try match first), false for lazy (try skip first)
359    /// - `qis`: true to wrap the optional value in an object scope
360    fn build_optional(&mut self, inner: Fragment, greedy: bool, qis: bool) -> Fragment {
361        let branch = self.add_epsilon();
362        let exit = self.add_epsilon();
363
364        if qis {
365            let obj_start = self.add_epsilon();
366            self.node_mut(obj_start)
367                .add_effect(BuildEffect::StartObject {
368                    for_alternation: false,
369                });
370
371            let obj_end = self.add_epsilon();
372            self.node_mut(obj_end).add_effect(BuildEffect::EndObject);
373
374            // Skip path needs ClearCurrent to indicate "nothing captured"
375            let skip = self.add_epsilon();
376            self.node_mut(skip).add_effect(BuildEffect::ClearCurrent);
377
378            self.connect(obj_start, inner.entry);
379            self.connect(inner.exit, obj_end);
380            self.connect(obj_end, exit);
381            self.connect(skip, exit);
382
383            if greedy {
384                self.connect(branch, obj_start);
385                self.connect(branch, skip);
386            } else {
387                self.connect(branch, skip);
388                self.connect(branch, obj_start);
389            }
390        } else {
391            let skip = self.add_epsilon();
392            self.node_mut(skip).add_effect(BuildEffect::ClearCurrent);
393
394            self.connect(skip, exit);
395            self.connect(inner.exit, exit);
396
397            if greedy {
398                self.connect(branch, inner.entry);
399                self.connect(branch, skip);
400            } else {
401                self.connect(branch, skip);
402                self.connect(branch, inner.entry);
403            }
404        }
405
406        Fragment::new(branch, exit)
407    }
408
409    /// Zero or more (greedy): inner*
410    pub fn zero_or_more(&mut self, inner: Fragment, nav: Nav) -> Fragment {
411        self.build_repetition(inner, false, true, ArrayMode::None, nav)
412    }
413
414    /// Zero or more (non-greedy): inner*?
415    pub fn zero_or_more_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
416        self.build_repetition(inner, false, false, ArrayMode::None, nav)
417    }
418
419    /// One or more (greedy): inner+
420    pub fn one_or_more(&mut self, inner: Fragment, nav: Nav) -> Fragment {
421        self.build_repetition(inner, true, true, ArrayMode::None, nav)
422    }
423
424    /// One or more (non-greedy): inner+?
425    pub fn one_or_more_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
426        self.build_repetition(inner, true, false, ArrayMode::None, nav)
427    }
428
429    /// Optional (greedy): inner?
430    pub fn optional(&mut self, inner: Fragment) -> Fragment {
431        self.build_optional(inner, true, false)
432    }
433
434    /// Optional (non-greedy): inner??
435    pub fn optional_lazy(&mut self, inner: Fragment) -> Fragment {
436        self.build_optional(inner, false, false)
437    }
438
439    /// Zero or more with array collection (greedy): inner*
440    pub fn zero_or_more_array(&mut self, inner: Fragment, nav: Nav) -> Fragment {
441        self.build_repetition(inner, false, true, ArrayMode::Simple, nav)
442    }
443
444    /// Zero or more with array collection (non-greedy): inner*?
445    pub fn zero_or_more_array_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
446        self.build_repetition(inner, false, false, ArrayMode::Simple, nav)
447    }
448
449    /// One or more with array collection (greedy): inner+
450    pub fn one_or_more_array(&mut self, inner: Fragment, nav: Nav) -> Fragment {
451        self.build_repetition(inner, true, true, ArrayMode::Simple, nav)
452    }
453
454    /// One or more with array collection (non-greedy): inner+?
455    pub fn one_or_more_array_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
456        self.build_repetition(inner, true, false, ArrayMode::Simple, nav)
457    }
458
459    /// Zero or more with QIS object wrapping (greedy): inner*
460    ///
461    /// Each iteration is wrapped in StartObject/EndObject to keep
462    /// multiple captures coupled per-iteration.
463    pub fn zero_or_more_array_qis(&mut self, inner: Fragment, nav: Nav) -> Fragment {
464        self.build_repetition(inner, false, true, ArrayMode::Qis, nav)
465    }
466
467    /// Zero or more with QIS object wrapping (non-greedy): inner*?
468    pub fn zero_or_more_array_qis_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
469        self.build_repetition(inner, false, false, ArrayMode::Qis, nav)
470    }
471
472    /// One or more with QIS object wrapping (greedy): inner+
473    pub fn one_or_more_array_qis(&mut self, inner: Fragment, nav: Nav) -> Fragment {
474        self.build_repetition(inner, true, true, ArrayMode::Qis, nav)
475    }
476
477    /// One or more with QIS object wrapping (non-greedy): inner+?
478    pub fn one_or_more_array_qis_lazy(&mut self, inner: Fragment, nav: Nav) -> Fragment {
479        self.build_repetition(inner, true, false, ArrayMode::Qis, nav)
480    }
481
482    /// Optional with QIS object wrapping: inner?
483    ///
484    /// Wraps the optional value in an object scope.
485    pub fn optional_qis(&mut self, inner: Fragment) -> Fragment {
486        self.build_optional(inner, true, true)
487    }
488
489    /// Optional with QIS object wrapping (non-greedy): inner??
490    pub fn optional_qis_lazy(&mut self, inner: Fragment) -> Fragment {
491        self.build_optional(inner, false, true)
492    }
493
494    /// Wrap definitions that don't already match the root node kind.
495    ///
496    /// For each definition whose entry matcher doesn't match `root_kind`,
497    /// prepends a transition that matches the root and descends into children.
498    /// This allows queries like `(function_declaration)` to work when the
499    /// interpreter starts at tree root (e.g., `program`).
500    pub fn wrap_definitions_with_root(&mut self, root_kind: &'src str) {
501        let def_names: Vec<&'src str> = self.definitions.keys().copied().collect();
502
503        for name in def_names {
504            let entry = self.definitions[name];
505
506            // Check if entry already matches root (directly or first reachable matcher)
507            if self.entry_matches_root(entry, root_kind) {
508                continue;
509            }
510
511            // Create wrapper: (root_kind) with Nav::stay
512            let wrapper = self.add_node(BuildNode::with_matcher(BuildMatcher::node(root_kind)));
513
514            // Add epsilon node with Nav::down between wrapper and original entry
515            let down_nav = self.add_epsilon();
516            self.node_mut(down_nav).set_nav(Nav::down());
517
518            // Connect wrapper → down_nav → original entry
519            self.connect(wrapper, down_nav);
520            self.connect(down_nav, entry);
521
522            // Update definition to point to wrapper
523            self.definitions.insert(name, wrapper);
524        }
525    }
526
527    /// Check if entry (or first reachable node matcher) already matches root kind.
528    fn entry_matches_root(&self, entry: NodeId, root_kind: &str) -> bool {
529        match &self.nodes[entry as usize].matcher {
530            BuildMatcher::Node { kind, .. } => *kind == root_kind,
531            BuildMatcher::Epsilon => {
532                // For epsilon entries, check first reachable node matchers
533                for &target in &self.nodes[entry as usize].successors {
534                    if self.entry_matches_root(target, root_kind) {
535                        return true;
536                    }
537                }
538                false
539            }
540            _ => false,
541        }
542    }
543}
544
545impl Default for BuildGraph<'_> {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551/// A node in the build graph.
552#[derive(Debug, Clone)]
553pub struct BuildNode<'src> {
554    pub matcher: BuildMatcher<'src>,
555    pub effects: Vec<BuildEffect<'src>>,
556    pub ref_marker: RefMarker,
557    pub successors: Vec<NodeId>,
558    pub nav: Nav,
559    pub ref_name: Option<&'src str>,
560}
561
562impl<'src> BuildNode<'src> {
563    pub fn epsilon() -> Self {
564        Self {
565            matcher: BuildMatcher::Epsilon,
566            effects: Vec::new(),
567            ref_marker: RefMarker::None,
568            successors: Vec::new(),
569            nav: Nav::stay(),
570            ref_name: None,
571        }
572    }
573
574    pub fn with_matcher(matcher: BuildMatcher<'src>) -> Self {
575        Self {
576            matcher,
577            effects: Vec::new(),
578            ref_marker: RefMarker::None,
579            successors: Vec::new(),
580            nav: Nav::stay(),
581            ref_name: None,
582        }
583    }
584
585    pub fn add_effect(&mut self, effect: BuildEffect<'src>) {
586        self.effects.push(effect);
587    }
588
589    pub fn set_ref_marker(&mut self, marker: RefMarker) {
590        self.ref_marker = marker;
591    }
592
593    pub fn set_nav(&mut self, nav: Nav) {
594        self.nav = nav;
595    }
596
597    pub fn is_epsilon(&self) -> bool {
598        matches!(self.matcher, BuildMatcher::Epsilon)
599    }
600}
601
602/// What a transition matches.
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub enum BuildMatcher<'src> {
605    Epsilon,
606    Node {
607        kind: &'src str,
608        field: Option<&'src str>,
609        negated_fields: Vec<&'src str>,
610    },
611    Anonymous {
612        literal: &'src str,
613        field: Option<&'src str>,
614    },
615    Wildcard {
616        field: Option<&'src str>,
617    },
618}
619
620impl<'src> BuildMatcher<'src> {
621    pub fn node(kind: &'src str) -> Self {
622        Self::Node {
623            kind,
624            field: None,
625            negated_fields: Vec::new(),
626        }
627    }
628
629    pub fn anonymous(literal: &'src str) -> Self {
630        Self::Anonymous {
631            literal,
632            field: None,
633        }
634    }
635
636    pub fn wildcard() -> Self {
637        Self::Wildcard { field: None }
638    }
639
640    pub fn with_field(mut self, field: &'src str) -> Self {
641        match &mut self {
642            BuildMatcher::Node { field: f, .. } => *f = Some(field),
643            BuildMatcher::Anonymous { field: f, .. } => *f = Some(field),
644            BuildMatcher::Wildcard { field: f } => *f = Some(field),
645            BuildMatcher::Epsilon => {}
646        }
647        self
648    }
649
650    pub fn with_negated_field(mut self, field: &'src str) -> Self {
651        if let BuildMatcher::Node { negated_fields, .. } = &mut self {
652            negated_fields.push(field);
653        }
654        self
655    }
656}
657
658/// Effect operations recorded during graph construction.
659#[derive(Debug, Clone, PartialEq, Eq)]
660pub enum BuildEffect<'src> {
661    CaptureNode,
662    /// Clear current value (set to None). Used on skip paths for optional captures.
663    ClearCurrent,
664    /// Start array collection. `is_plus` distinguishes `+` (true) from `*` (false).
665    StartArray {
666        is_plus: bool,
667    },
668    PushElement,
669    EndArray,
670    /// Start object scope. `for_alternation` is true when this object wraps a captured
671    /// tagged alternation (tags should create enum), false for QIS/sequence objects
672    /// (tags in inner alternations should be ignored).
673    StartObject {
674        for_alternation: bool,
675    },
676    EndObject,
677    Field {
678        name: &'src str,
679        span: TextRange,
680    },
681    StartVariant(&'src str),
682    EndVariant,
683    ToString,
684}
685
686/// Marker for definition call/return transitions.
687#[derive(Debug, Clone, PartialEq, Eq, Default)]
688pub enum RefMarker {
689    #[default]
690    None,
691    Enter {
692        ref_id: u32,
693    },
694    Exit {
695        ref_id: u32,
696    },
697}
698
699impl RefMarker {
700    pub fn enter(ref_id: u32) -> Self {
701        Self::Enter { ref_id }
702    }
703
704    pub fn exit(ref_id: u32) -> Self {
705        Self::Exit { ref_id }
706    }
707
708    pub fn is_none(&self) -> bool {
709        matches!(self, RefMarker::None)
710    }
711
712    pub fn is_some(&self) -> bool {
713        !matches!(self, RefMarker::None)
714    }
715
716    pub fn is_enter(&self) -> bool {
717        matches!(self, RefMarker::Enter { .. })
718    }
719
720    pub fn is_exit(&self) -> bool {
721        matches!(self, RefMarker::Exit { .. })
722    }
723}