From 9c9a10d1e03ec38c654863540c5c4459663f1247 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 17:06:58 -0300 Subject: [PATCH] fix(engine): use depth-based cursor restoration in Call/Return When a callee advances via continue_search at the same tree level, the cursor should stay at the matched position. Previously, Return always restored to the exact position saved at Call time, causing duplicate matches when sequences contained alternations. Changed from saving/restoring cursor position (descendant_index) to saving/restoring cursor depth. On Return, we go up to the saved depth level, which preserves sibling advances while still restoring level when the callee descended into children. --- crates/plotnik-lib/src/engine/cursor.rs | 12 ++++++++++++ crates/plotnik-lib/src/engine/frame.rs | 12 ++++++------ crates/plotnik-lib/src/engine/vm.rs | 23 +++++++++++++++-------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/crates/plotnik-lib/src/engine/cursor.rs b/crates/plotnik-lib/src/engine/cursor.rs index dc803fe0..d1375570 100644 --- a/crates/plotnik-lib/src/engine/cursor.rs +++ b/crates/plotnik-lib/src/engine/cursor.rs @@ -78,6 +78,18 @@ impl<'t> CursorWrapper<'t> { self.cursor.field_id() } + /// Get current tree depth (root is 0). + #[inline] + pub fn depth(&self) -> u32 { + self.cursor.depth() + } + + /// Move cursor to parent node. + #[inline] + pub fn goto_parent(&mut self) -> bool { + self.cursor.goto_parent() + } + /// Check if a node type is trivia. #[inline] pub fn is_trivia(&self, node: &Node<'_>) -> bool { diff --git a/crates/plotnik-lib/src/engine/frame.rs b/crates/plotnik-lib/src/engine/frame.rs index 78773410..e32ade0b 100644 --- a/crates/plotnik-lib/src/engine/frame.rs +++ b/crates/plotnik-lib/src/engine/frame.rs @@ -10,8 +10,8 @@ pub struct Frame { pub return_addr: u16, /// Parent frame index (for cactus stack). pub parent: Option, - /// Cursor position before Call navigation (for restoration on Return). - pub cursor_position: u32, + /// Tree depth at Call time (for level restoration on Return). + pub saved_depth: u32, } /// Append-only arena for frames (cactus stack implementation). @@ -35,25 +35,25 @@ impl FrameArena { } /// Push a new frame, returns its index. - pub fn push(&mut self, return_addr: u16, cursor_position: u32) -> u32 { + pub fn push(&mut self, return_addr: u16, saved_depth: u32) -> u32 { let idx = self.frames.len() as u32; self.frames.push(Frame { return_addr, parent: self.current, - cursor_position, + saved_depth, }); self.current = Some(idx); idx } - /// Pop the current frame, returning its return address and cursor position. + /// Pop the current frame, returning its return address and saved depth. /// /// Panics if the stack is empty. pub fn pop(&mut self) -> (u16, u32) { let current_idx = self.current.expect("pop on empty frame stack"); let frame = self.frames[current_idx as usize]; self.current = frame.parent; - (frame.return_addr, frame.cursor_position) + (frame.return_addr, frame.saved_depth) } /// Restore frame state for backtracking. diff --git a/crates/plotnik-lib/src/engine/vm.rs b/crates/plotnik-lib/src/engine/vm.rs index c1043148..297ffa46 100644 --- a/crates/plotnik-lib/src/engine/vm.rs +++ b/crates/plotnik-lib/src/engine/vm.rs @@ -231,12 +231,13 @@ impl<'t> VM<'t> { self.navigate_to_field(c.nav, c.node_field, tracer)?; - // Save cursor position AFTER navigation - this is where the callee starts matching. - // On Return, we restore to this position so Nav::Next in quantifier loops works correctly. - let saved_cursor = self.cursor.descendant_index(); + // Save tree depth AFTER navigation. On Return, we go up to this depth. + // This allows continue_search advances at the same level to be preserved, + // while still restoring level when the callee descended into children. + let saved_depth = self.cursor.depth(); tracer.trace_call(c.target.get()); - self.frames.push(c.next.get(), saved_cursor); + self.frames.push(c.next.get(), saved_depth); self.recursion_depth += 1; self.ip = c.target.get(); Ok(()) @@ -296,18 +297,24 @@ impl<'t> VM<'t> { return Err(RuntimeError::Accept); } - let (return_addr, saved_cursor) = self.frames.pop(); + let (return_addr, saved_depth) = self.frames.pop(); self.recursion_depth -= 1; // Prune frames (O(1) amortized) self.frames.prune(self.checkpoints.max_frame_ref()); - // Set matched_node BEFORE restoring cursor so effects after + // Set matched_node BEFORE going up so effects after // a Call can capture the node that the callee matched. self.matched_node = Some(self.cursor.node()); - // Restore cursor position for subsequent navigation (e.g., Nav::Next in quantifier loop) - self.cursor.goto_descendant(saved_cursor); + // Go up to saved depth level. This preserves sibling advances + // (continue_search at same level) while restoring level when + // the callee descended into children. + while self.cursor.depth() > saved_depth { + if !self.cursor.goto_parent() { + break; + } + } self.ip = return_addr; Ok(())