diff --git a/crates/plotnik-lib/src/bytecode/format.rs b/crates/plotnik-lib/src/bytecode/format.rs index ccb8b836..551120ba 100644 --- a/crates/plotnik-lib/src/bytecode/format.rs +++ b/crates/plotnik-lib/src/bytecode/format.rs @@ -86,6 +86,7 @@ impl Symbol { /// | --------------- | ------- | ----------------------------------- | /// | Stay | (blank) | No movement, 5 spaces | /// | Stay (epsilon) | ε | Only when no type/field constraints | +/// | StayExact | ‼· | Stay at position, exact match only | /// | Down | ▽ | First child, skip any | /// | DownSkip | !▽ | First child, skip trivia | /// | DownExact | ‼▽ | First child, exact | @@ -98,6 +99,7 @@ impl Symbol { pub fn nav_symbol(nav: Nav) -> Symbol { match nav { Nav::Stay => Symbol::EMPTY, + Nav::StayExact => Symbol::new(" ‼", "·", " "), Nav::Down => Symbol::new(" ", "▽", " "), Nav::DownSkip => Symbol::new(" !", "▽", " "), Nav::DownExact => Symbol::new(" ‼", "▽", " "), diff --git a/crates/plotnik-lib/src/bytecode/nav.rs b/crates/plotnik-lib/src/bytecode/nav.rs index 1ef102a7..b9bb80f3 100644 --- a/crates/plotnik-lib/src/bytecode/nav.rs +++ b/crates/plotnik-lib/src/bytecode/nav.rs @@ -7,6 +7,8 @@ pub enum Nav { #[default] Stay, + /// Stay at current position, exact match only (no continue_search). + StayExact, Next, NextSkip, NextExact, @@ -31,12 +33,13 @@ impl Nav { match mode { 0b00 => match payload { 0 => Self::Stay, - 1 => Self::Next, - 2 => Self::NextSkip, - 3 => Self::NextExact, - 4 => Self::Down, - 5 => Self::DownSkip, - 6 => Self::DownExact, + 1 => Self::StayExact, + 2 => Self::Next, + 3 => Self::NextSkip, + 4 => Self::NextExact, + 5 => Self::Down, + 6 => Self::DownSkip, + 7 => Self::DownExact, _ => panic!("invalid nav standard: {payload}"), }, 0b01 => { @@ -59,12 +62,13 @@ impl Nav { pub fn to_byte(self) -> u8 { match self { Self::Stay => 0, - Self::Next => 1, - Self::NextSkip => 2, - Self::NextExact => 3, - Self::Down => 4, - Self::DownSkip => 5, - Self::DownExact => 6, + Self::StayExact => 1, + Self::Next => 2, + Self::NextSkip => 3, + Self::NextExact => 4, + Self::Down => 5, + Self::DownSkip => 6, + Self::DownExact => 7, Self::Up(n) => { debug_assert!((1..=63).contains(&n)); 0b01_000000 | n @@ -79,6 +83,22 @@ impl Nav { } } } + + /// Convert navigation to its exact variant (no search loop). + /// + /// Used by alternation branches which should match at their exact + /// cursor position only - the search among positions is owned by + /// the parent context (quantifier's skip-retry, sequence advancement). + pub fn to_exact(self) -> Self { + match self { + Self::Down | Self::DownSkip => Self::DownExact, + Self::Next | Self::NextSkip => Self::NextExact, + Self::Stay => Self::StayExact, + Self::Up(n) | Self::UpSkipTrivia(n) => Self::UpExact(n), + // Already exact variants + Self::DownExact | Self::NextExact | Self::StayExact | Self::UpExact(_) => self, + } + } } #[cfg(test)] @@ -89,6 +109,7 @@ mod tests { fn nav_standard_roundtrip() { for nav in [ Nav::Stay, + Nav::StayExact, Nav::Next, Nav::NextSkip, Nav::NextExact, @@ -115,7 +136,8 @@ mod tests { #[test] fn nav_byte_encoding() { assert_eq!(Nav::Stay.to_byte(), 0b00_000000); - assert_eq!(Nav::Down.to_byte(), 0b00_000100); + assert_eq!(Nav::StayExact.to_byte(), 0b00_000001); + assert_eq!(Nav::Down.to_byte(), 0b00_000101); assert_eq!(Nav::Up(5).to_byte(), 0b01_000101); assert_eq!(Nav::UpSkipTrivia(3).to_byte(), 0b10_000011); assert_eq!(Nav::UpExact(1).to_byte(), 0b11_000001); diff --git a/crates/plotnik-lib/src/compile/expressions.rs b/crates/plotnik-lib/src/compile/expressions.rs index 2ceb84ac..12f57219 100644 --- a/crates/plotnik-lib/src/compile/expressions.rs +++ b/crates/plotnik-lib/src/compile/expressions.rs @@ -18,11 +18,6 @@ use super::navigation::{check_trailing_anchor, inner_creates_scope, is_skippable use super::Compiler; impl Compiler<'_> { - /// Compile a named node: `(identifier)` or `(call_expression arg: ...)`. - pub(super) fn compile_named_node(&mut self, node: &ast::NamedNode, exit: Label) -> Label { - self.compile_named_node_inner(node, exit, None, CaptureEffects::default()) - } - /// Compile a named node with capture effects. pub(super) fn compile_named_node_inner( &mut self, @@ -109,11 +104,6 @@ impl Compiler<'_> { entry } - /// Compile an anonymous node: `"+"` or `_`. - pub(super) fn compile_anonymous_node(&mut self, node: &ast::AnonymousNode, exit: Label) -> Label { - self.compile_anonymous_node_inner(node, exit, None, CaptureEffects::default()) - } - /// Compile an anonymous node with capture effects. pub(super) fn compile_anonymous_node_inner( &mut self, @@ -145,11 +135,6 @@ impl Compiler<'_> { entry } - /// Compile a reference: `(Expr)`. - pub(super) fn compile_ref(&mut self, r: &ast::Ref, exit: Label) -> Label { - self.compile_ref_inner(r, exit, None, None, CaptureEffects::default()) - } - /// Compile a reference with capture effects. /// /// For Call instructions, capture effects are placed in an epsilon after the call, @@ -207,11 +192,6 @@ impl Compiler<'_> { call_label } - /// Compile a field constraint: `name: pattern`. - pub(super) fn compile_field(&mut self, field: &ast::FieldExpr, exit: Label) -> Label { - self.compile_field_inner(field, exit, None, CaptureEffects::default()) - } - /// Compile a field constraint with capture effects (passed to inner pattern). pub(super) fn compile_field_inner( &mut self, @@ -294,11 +274,6 @@ impl Compiler<'_> { value_entry } - /// Compile a captured expression: `@name` or `pattern @name`. - pub(super) fn compile_captured(&mut self, cap: &ast::CapturedExpr, exit: Label) -> Label { - self.compile_captured_inner(cap, exit, None, CaptureEffects::default()) - } - /// Compile a captured expression with capture effects from outer layers. /// /// Capture effects are placed on the innermost match instruction: diff --git a/crates/plotnik-lib/src/compile/mod.rs b/crates/plotnik-lib/src/compile/mod.rs index 09622910..56717ba7 100644 --- a/crates/plotnik-lib/src/compile/mod.rs +++ b/crates/plotnik-lib/src/compile/mod.rs @@ -165,6 +165,12 @@ impl<'a> Compiler<'a> { .and_then(|tid| self.type_ctx.get_type(tid)) .is_some_and(|shape| matches!(shape, TypeShape::Struct(_))); + // Definition bodies use StayExact navigation: match at current position only. + // The caller (alternation, sequence, quantifier, or VM top-level) owns the search. + // This ensures named definition calls don't advance past positions that other + // alternation branches should try. + let body_nav = Some(Nav::StayExact); + let body_entry = if def_returns_struct { let type_id = self.type_ctx.get_def_type(def_id).expect("checked above"); @@ -182,7 +188,9 @@ impl<'a> Compiler<'a> { })); // Compile body with scope, targeting EndObj - let inner_entry = self.with_scope(type_id, |this| this.compile_expr(body, endobj_label)); + let inner_entry = self.with_scope(type_id, |this| { + this.compile_expr_with_nav(body, endobj_label, body_nav) + }); // Emit Obj → inner_entry let obj_label = self.fresh_label(); @@ -199,9 +207,9 @@ impl<'a> Compiler<'a> { obj_label } else if let Some(type_id) = self.type_ctx.get_def_type(def_id) { - self.with_scope(type_id, |this| this.compile_expr(body, return_label)) + self.with_scope(type_id, |this| this.compile_expr_with_nav(body, return_label, body_nav)) } else { - self.compile_expr(body, return_label) + self.compile_expr_with_nav(body, return_label, body_nav) }; // If body_entry differs from our pre-allocated entry, emit an epsilon jump @@ -212,20 +220,6 @@ impl<'a> Compiler<'a> { Ok(()) } - /// Compile an expression, returning its entry label. - pub(super) fn compile_expr(&mut self, expr: &Expr, exit: Label) -> Label { - match expr { - Expr::NamedNode(n) => self.compile_named_node(n, exit), - Expr::AnonymousNode(n) => self.compile_anonymous_node(n, exit), - Expr::Ref(r) => self.compile_ref(r, exit), - Expr::SeqExpr(s) => self.compile_seq(s, exit), - Expr::AltExpr(a) => self.compile_alt(a, exit), - Expr::QuantifiedExpr(q) => self.compile_quantified(q, exit), - Expr::FieldExpr(f) => self.compile_field(f, exit), - Expr::CapturedExpr(c) => self.compile_captured(c, exit), - } - } - /// Compile an expression with an optional navigation override. pub(super) fn compile_expr_with_nav(&mut self, expr: &Expr, exit: Label, nav_override: Option