plotnik_lib/engine/
interpreter.rs

1//! The core query interpreter.
2//!
3//! Executes a compiled query against a tree-sitter AST, producing an effect stream
4//! that can be materialized into a structured value.
5//!
6//! See ADR-0006 for detailed execution semantics.
7
8use std::collections::HashSet;
9
10use tree_sitter::{Node, TreeCursor};
11
12use crate::ir::{
13    CompiledQuery, EffectOp, Matcher, Nav, NavKind, NodeFieldId, NodeTypeId, RefTransition,
14    TransitionId,
15};
16
17use super::effect_stream::EffectStream;
18use super::error::RuntimeError;
19use super::materializer::Materializer;
20use super::value::Value;
21
22/// A saved execution state for backtracking.
23#[derive(Debug, Clone)]
24struct Checkpoint {
25    /// Tree-sitter descendant index for cursor restoration.
26    cursor_checkpoint: usize,
27    /// Number of ops in effect stream at save time.
28    effect_ops_watermark: usize,
29    /// Number of nodes in effect stream at save time.
30    effect_nodes_watermark: usize,
31    /// Current frame index at save time.
32    recursion_frame: Option<u32>,
33    /// Previous max_frame_watermark (for O(1) restore).
34    prev_max_watermark: Option<u32>,
35    /// Source transition for alternatives.
36    transition_id: TransitionId,
37    /// Index of next alternative to try.
38    next_alt: u32,
39}
40
41/// Stack of checkpoints with O(1) watermark maintenance.
42#[derive(Debug, Default)]
43struct CheckpointStack {
44    points: Vec<Checkpoint>,
45    /// Highest frame index referenced by any checkpoint.
46    max_frame_watermark: Option<u32>,
47}
48
49impl CheckpointStack {
50    fn new() -> Self {
51        Self::default()
52    }
53
54    fn push(&mut self, mut point: Checkpoint) {
55        point.prev_max_watermark = self.max_frame_watermark;
56        if let Some(frame) = point.recursion_frame {
57            self.max_frame_watermark = Some(match self.max_frame_watermark {
58                Some(max) => max.max(frame),
59                None => frame,
60            });
61        }
62        self.points.push(point);
63    }
64
65    fn pop(&mut self) -> Option<Checkpoint> {
66        let point = self.points.pop()?;
67        self.max_frame_watermark = point.prev_max_watermark;
68        Some(point)
69    }
70}
71
72/// A call frame for definition references.
73#[derive(Debug, Clone)]
74struct Frame {
75    /// Index of caller's frame (None if called from top level).
76    parent: Option<u32>,
77    /// Ref ID to verify Exit matches Enter.
78    ref_id: u16,
79    /// Transition that entered this call (to retrieve returns via successors()[1..]).
80    enter_transition: TransitionId,
81}
82
83/// Append-only arena of call frames.
84#[derive(Debug, Default)]
85struct FrameArena {
86    frames: Vec<Frame>,
87    /// Index of current frame (the "stack pointer").
88    current: Option<u32>,
89}
90
91impl FrameArena {
92    fn new() -> Self {
93        Self::default()
94    }
95
96    /// Push a new frame, returns its index.
97    fn push(&mut self, parent: Option<u32>, ref_id: u16, enter_transition: TransitionId) -> u32 {
98        let idx = self.frames.len() as u32;
99        self.frames.push(Frame {
100            parent,
101            ref_id,
102            enter_transition,
103        });
104        self.current = Some(idx);
105        idx
106    }
107
108    /// Get current frame.
109    fn current_frame(&self) -> Option<&Frame> {
110        self.current.map(|idx| &self.frames[idx as usize])
111    }
112
113    /// Exit current frame (set current to parent).
114    fn exit(&mut self) -> Option<&Frame> {
115        let frame = self.current_frame()?;
116        let parent = frame.parent;
117        let idx = self.current?;
118        self.current = parent;
119        Some(&self.frames[idx as usize])
120    }
121
122    /// Prune frames above the high-water mark.
123    fn prune(&mut self, checkpoints: &CheckpointStack) {
124        let high_water = match (self.current, checkpoints.max_frame_watermark) {
125            (None, None) => return,
126            (Some(c), None) => c,
127            (None, Some(m)) => m,
128            (Some(c), Some(m)) => c.max(m),
129        };
130        self.frames.truncate((high_water + 1) as usize);
131    }
132}
133
134/// Default execution fuel (transitions).
135const DEFAULT_EXEC_FUEL: u32 = 1_000_000;
136/// Default recursion fuel (Enter operations).
137const DEFAULT_RECURSION_FUEL: u32 = 1024;
138
139/// Query interpreter that executes a compiled query against an AST.
140pub struct QueryInterpreter<'q, 'tree> {
141    query: &'q CompiledQuery,
142    cursor: TreeCursor<'tree>,
143    source: &'tree str,
144    checkpoints: CheckpointStack,
145    frames: FrameArena,
146    effects: EffectStream<'tree>,
147    /// Trivia node type IDs (for skip-trivia navigation).
148    trivia_kinds: HashSet<NodeTypeId>,
149    /// Matched node slot (cleared at start of each transition).
150    matched_node: Option<Node<'tree>>,
151    /// Execution fuel remaining.
152    exec_fuel: u32,
153    /// Recursion fuel remaining.
154    recursion_fuel: u32,
155}
156
157impl<'q, 'tree> QueryInterpreter<'q, 'tree> {
158    /// Creates a new interpreter.
159    ///
160    /// The cursor should be positioned at the tree root.
161    pub fn new(query: &'q CompiledQuery, cursor: TreeCursor<'tree>, source: &'tree str) -> Self {
162        let trivia_kinds: HashSet<_> = query.trivia_kinds().iter().copied().collect();
163        Self {
164            query,
165            cursor,
166            source,
167            checkpoints: CheckpointStack::new(),
168            frames: FrameArena::new(),
169            effects: EffectStream::new(),
170            trivia_kinds,
171            matched_node: None,
172            exec_fuel: DEFAULT_EXEC_FUEL,
173            recursion_fuel: DEFAULT_RECURSION_FUEL,
174        }
175    }
176
177    /// Set execution fuel limit.
178    pub fn with_exec_fuel(mut self, fuel: u32) -> Self {
179        self.exec_fuel = fuel;
180        self
181    }
182
183    /// Set recursion fuel limit.
184    pub fn with_recursion_fuel(mut self, fuel: u32) -> Self {
185        self.recursion_fuel = fuel;
186        self
187    }
188
189    /// Run the query and return the result.
190    pub fn run(self) -> Result<Value<'tree>, RuntimeError> {
191        // Get the entry transition from the last entrypoint (main definition)
192        let start_transition = self
193            .query
194            .entrypoints()
195            .last()
196            .map(|ep| ep.target())
197            .unwrap_or(0);
198
199        self.run_from(start_transition)
200    }
201
202    /// Run the query from a specific transition and return the result.
203    pub fn run_from(mut self, start: TransitionId) -> Result<Value<'tree>, RuntimeError> {
204        match self.execute(start) {
205            Ok(true) => Ok(Materializer::materialize(&self.effects)),
206            Ok(false) => Ok(Value::Null), // No match
207            Err(e) => Err(e),
208        }
209    }
210
211    /// Execute from a given transition, returns true if matched.
212    fn execute(&mut self, start: TransitionId) -> Result<bool, RuntimeError> {
213        let mut current = start;
214
215        loop {
216            // Check fuel
217            if self.exec_fuel == 0 {
218                return Err(RuntimeError::ExecFuelExhausted);
219            }
220            self.exec_fuel -= 1;
221
222            // Clear matched_node slot at start of each transition
223            self.matched_node = None;
224
225            let view = self.query.transition_view(current);
226            let nav = view.nav();
227            let matcher = view.matcher();
228            let ref_marker = view.ref_marker();
229            let successors = view.successors();
230
231            // Step 1: Execute navigation
232            let nav_ok = self.execute_nav(nav);
233            if !nav_ok {
234                // Navigation failed, backtrack
235                if let Some(next) = self.backtrack()? {
236                    current = next;
237                    continue;
238                }
239                return Ok(false);
240            }
241
242            // Step 2: Try matcher (with skip policy from nav)
243            let match_ok = self.execute_matcher(matcher, nav);
244            if !match_ok {
245                // Match failed, backtrack
246                if let Some(next) = self.backtrack()? {
247                    current = next;
248                    continue;
249                }
250                return Ok(false);
251            }
252
253            // Step 3: Execute effects
254            for &effect in view.effects() {
255                self.execute_effect(effect);
256            }
257
258            // Step 4: Process ref_marker
259            match ref_marker {
260                RefTransition::None => {}
261                RefTransition::Enter(ref_id) => {
262                    if self.recursion_fuel == 0 {
263                        return Err(RuntimeError::RecursionLimitExceeded);
264                    }
265                    self.recursion_fuel -= 1;
266
267                    // Push frame with returns = successors[1..]
268                    self.frames.push(self.frames.current, ref_id, current);
269
270                    // Jump to definition entry = successors[0]
271                    if successors.is_empty() {
272                        panic!("Enter transition must have at least one successor");
273                    }
274                    current = successors[0];
275                    continue;
276                }
277                RefTransition::Exit(ref_id) => {
278                    // Verify ref_id matches
279                    let frame = self.frames.current_frame().expect("Exit without frame");
280                    assert_eq!(frame.ref_id, ref_id, "Exit ref_id mismatch");
281
282                    // Get returns from enter transition
283                    let enter_trans = frame.enter_transition;
284                    let enter_view = self.query.transition_view(enter_trans);
285                    let returns = &enter_view.successors()[1..];
286
287                    // Pop frame
288                    self.frames.exit();
289
290                    // Prune frames if possible
291                    self.frames.prune(&self.checkpoints);
292
293                    // Continue with returns as successors
294                    if returns.is_empty() {
295                        // Definition matched, no returns = we're done with this path
296                        // This shouldn't happen in well-formed graphs
297                        if let Some(next) = self.backtrack()? {
298                            current = next;
299                            continue;
300                        }
301                        return Ok(true);
302                    }
303
304                    // Save checkpoint for alternatives if multiple returns
305                    if returns.len() > 1 {
306                        self.save_checkpoint(enter_trans, 2); // Skip successors[0] and [1]
307                    }
308
309                    current = returns[0];
310                    continue;
311                }
312            }
313
314            // Step 5: Process successors
315            if successors.is_empty() {
316                // Terminal transition - match succeeded
317                return Ok(true);
318            }
319
320            // Save checkpoint for alternatives
321            if successors.len() > 1 {
322                self.save_checkpoint(current, 1);
323            }
324
325            current = successors[0];
326        }
327    }
328
329    /// Save a checkpoint for backtracking.
330    fn save_checkpoint(&mut self, transition_id: TransitionId, next_alt: u32) {
331        let checkpoint = Checkpoint {
332            cursor_checkpoint: self.cursor.descendant_index(),
333            effect_ops_watermark: self.effects.ops().len(),
334            effect_nodes_watermark: self.effects.nodes().len(),
335            recursion_frame: self.frames.current,
336            prev_max_watermark: None, // Set by CheckpointStack::push
337            transition_id,
338            next_alt,
339        };
340        self.checkpoints.push(checkpoint);
341    }
342
343    /// Backtrack to the next alternative. Returns the transition to try.
344    fn backtrack(&mut self) -> Result<Option<TransitionId>, RuntimeError> {
345        loop {
346            let Some(mut checkpoint) = self.checkpoints.pop() else {
347                return Ok(None);
348            };
349
350            // Restore cursor
351            self.cursor.goto_descendant(checkpoint.cursor_checkpoint);
352
353            // Restore effects
354            self.effects.truncate(
355                checkpoint.effect_ops_watermark,
356                checkpoint.effect_nodes_watermark,
357            );
358
359            // Restore frame
360            self.frames.current = checkpoint.recursion_frame;
361
362            // Get next alternative
363            let view = self.query.transition_view(checkpoint.transition_id);
364            let successors = view.successors();
365
366            if (checkpoint.next_alt as usize) < successors.len() {
367                let next = successors[checkpoint.next_alt as usize];
368                checkpoint.next_alt += 1;
369
370                // Re-save if more alternatives remain
371                if (checkpoint.next_alt as usize) < successors.len() {
372                    self.checkpoints.push(checkpoint);
373                }
374
375                return Ok(Some(next));
376            }
377            // No more alternatives at this checkpoint, try next
378        }
379    }
380
381    /// Execute navigation, returns true if successful.
382    fn execute_nav(&mut self, nav: Nav) -> bool {
383        match nav.kind {
384            NavKind::Stay => true,
385
386            NavKind::Next => self.cursor.goto_next_sibling(),
387
388            NavKind::NextSkipTrivia => {
389                while self.cursor.goto_next_sibling() {
390                    if !self.is_trivia(self.cursor.node()) {
391                        return true;
392                    }
393                }
394                false
395            }
396
397            NavKind::NextExact => self.cursor.goto_next_sibling(),
398
399            NavKind::Down => self.cursor.goto_first_child(),
400
401            NavKind::DownSkipTrivia => {
402                if !self.cursor.goto_first_child() {
403                    return false;
404                }
405                while self.is_trivia(self.cursor.node()) {
406                    if !self.cursor.goto_next_sibling() {
407                        return false;
408                    }
409                }
410                true
411            }
412
413            NavKind::DownExact => self.cursor.goto_first_child(),
414
415            NavKind::Up => {
416                for _ in 0..nav.level {
417                    if !self.cursor.goto_parent() {
418                        return false;
419                    }
420                }
421                true
422            }
423
424            NavKind::UpSkipTrivia => {
425                // Validate we're at last non-trivia child before ascending
426                let current_id = self.cursor.node().id();
427                if let Some(parent) = self.cursor.node().parent() {
428                    let child_count = parent.child_count() as u32;
429                    let mut found_current = false;
430                    for i in 0..child_count {
431                        if let Some(child) = parent.child(i) {
432                            if child.id() == current_id {
433                                found_current = true;
434                                continue;
435                            }
436                            if found_current && !self.is_trivia(child) {
437                                return false;
438                            }
439                        }
440                    }
441                }
442                self.cursor.goto_parent()
443            }
444
445            NavKind::UpExact => {
446                // Validate we're at last child
447                let node = self.cursor.node();
448                if let Some(parent) = node.parent() {
449                    let child_count = parent.child_count();
450                    if child_count > 0 {
451                        let last_child = parent.child((child_count - 1) as u32);
452                        if last_child.map(|c| c.id()) != Some(node.id()) {
453                            return false;
454                        }
455                    }
456                }
457                self.cursor.goto_parent()
458            }
459        }
460    }
461
462    /// Execute matcher with skip policy, returns true if matched.
463    fn execute_matcher(&mut self, matcher: &Matcher, nav: Nav) -> bool {
464        match matcher {
465            Matcher::Epsilon => true,
466
467            Matcher::Node {
468                kind,
469                field,
470                negated_fields,
471            } => {
472                let matched = self.try_match_node(*kind, *field, *negated_fields, true, nav);
473                if matched {
474                    self.matched_node = Some(self.cursor.node());
475                }
476                matched
477            }
478
479            Matcher::Anonymous {
480                kind,
481                field,
482                negated_fields,
483            } => {
484                let matched = self.try_match_node(*kind, *field, *negated_fields, false, nav);
485                if matched {
486                    self.matched_node = Some(self.cursor.node());
487                }
488                matched
489            }
490
491            Matcher::Wildcard => {
492                self.matched_node = Some(self.cursor.node());
493                true
494            }
495        }
496    }
497
498    /// Try to match a node with the given constraints.
499    fn try_match_node(
500        &mut self,
501        kind: NodeTypeId,
502        field: Option<NodeFieldId>,
503        negated_fields: crate::ir::Slice<NodeFieldId>,
504        named: bool,
505        nav: Nav,
506    ) -> bool {
507        // Determine skip policy
508        let can_skip = match nav.kind {
509            NavKind::Next | NavKind::Down => true,
510            NavKind::NextSkipTrivia | NavKind::DownSkipTrivia => false, // Already handled trivia
511            _ => false,
512        };
513
514        loop {
515            let node = self.cursor.node();
516
517            // Check named/anonymous
518            if named != node.is_named() {
519                if can_skip && self.cursor.goto_next_sibling() {
520                    continue;
521                }
522                return false;
523            }
524
525            // Check kind
526            if node.kind_id() != kind {
527                if can_skip && self.cursor.goto_next_sibling() {
528                    continue;
529                }
530                return false;
531            }
532
533            // Check field constraint
534            if let Some(field_id) = field {
535                let actual_field = self.cursor.field_id();
536                if actual_field != Some(field_id) {
537                    if can_skip && self.cursor.goto_next_sibling() {
538                        continue;
539                    }
540                    return false;
541                }
542            }
543
544            // Check negated fields
545            let neg_fields = self.query.resolve_negated_fields(negated_fields);
546            for &neg_field in neg_fields {
547                if node.child_by_field_id(neg_field.get()).is_some() {
548                    if can_skip && self.cursor.goto_next_sibling() {
549                        continue;
550                    }
551                    return false;
552                }
553            }
554
555            return true;
556        }
557    }
558
559    /// Execute an effect operation.
560    fn execute_effect(&mut self, effect: EffectOp) {
561        self.effects.push_op(effect);
562
563        if matches!(effect, EffectOp::CaptureNode) {
564            let node = self.matched_node.expect("CaptureNode without matched node");
565            self.effects.push_node(node, self.source);
566        }
567    }
568
569    /// Check if a node is trivia.
570    fn is_trivia(&self, node: Node) -> bool {
571        self.trivia_kinds.contains(&node.kind_id())
572    }
573}