From 3625b529da81fb8ad4182477ef62f2e3321ff929 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 14:57:47 -0300 Subject: [PATCH] fix(engine): restore cursor position after Call returns --- crates/plotnik-lib/src/engine/frame.rs | 11 +++++++---- crates/plotnik-lib/src/engine/vm.rs | 13 ++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/plotnik-lib/src/engine/frame.rs b/crates/plotnik-lib/src/engine/frame.rs index a0632da0..78773410 100644 --- a/crates/plotnik-lib/src/engine/frame.rs +++ b/crates/plotnik-lib/src/engine/frame.rs @@ -10,6 +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, } /// Append-only arena for frames (cactus stack implementation). @@ -33,24 +35,25 @@ impl FrameArena { } /// Push a new frame, returns its index. - pub fn push(&mut self, return_addr: u16) -> u32 { + pub fn push(&mut self, return_addr: u16, cursor_position: u32) -> u32 { let idx = self.frames.len() as u32; self.frames.push(Frame { return_addr, parent: self.current, + cursor_position, }); self.current = Some(idx); idx } - /// Pop the current frame, returning its return address. + /// Pop the current frame, returning its return address and cursor position. /// /// Panics if the stack is empty. - pub fn pop(&mut self) -> u16 { + 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.return_addr, frame.cursor_position) } /// Restore frame state for backtracking. diff --git a/crates/plotnik-lib/src/engine/vm.rs b/crates/plotnik-lib/src/engine/vm.rs index e1c36143..c1043148 100644 --- a/crates/plotnik-lib/src/engine/vm.rs +++ b/crates/plotnik-lib/src/engine/vm.rs @@ -231,8 +231,12 @@ 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(); + tracer.trace_call(c.target.get()); - self.frames.push(c.next.get()); + self.frames.push(c.next.get(), saved_cursor); self.recursion_depth += 1; self.ip = c.target.get(); Ok(()) @@ -292,16 +296,19 @@ impl<'t> VM<'t> { return Err(RuntimeError::Accept); } - let return_addr = self.frames.pop(); + let (return_addr, saved_cursor) = self.frames.pop(); self.recursion_depth -= 1; // Prune frames (O(1) amortized) self.frames.prune(self.checkpoints.max_frame_ref()); - // Set matched_node to current cursor position so effects after + // Set matched_node BEFORE restoring cursor 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); + self.ip = return_addr; Ok(()) }