Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/plotnik-lib/src/bytecode/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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(" ‼", "▽", " "),
Expand Down
48 changes: 35 additions & 13 deletions crates/plotnik-lib/src/bytecode/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
pub enum Nav {
#[default]
Stay,
/// Stay at current position, exact match only (no continue_search).
StayExact,
Next,
NextSkip,
NextExact,
Expand All @@ -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 => {
Expand All @@ -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
Expand All @@ -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)]
Expand All @@ -89,6 +109,7 @@ mod tests {
fn nav_standard_roundtrip() {
for nav in [
Nav::Stay,
Nav::StayExact,
Nav::Next,
Nav::NextSkip,
Nav::NextExact,
Expand All @@ -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);
Expand Down
25 changes: 0 additions & 25 deletions crates/plotnik-lib/src/compile/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
28 changes: 11 additions & 17 deletions crates/plotnik-lib/src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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<Nav>) -> Label {
self.compile_expr_inner(expr, exit, nav_override, CaptureEffects::default())
Expand Down
26 changes: 12 additions & 14 deletions crates/plotnik-lib/src/compile/quantifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ pub struct QuantifierConfig<'a> {
}

impl Compiler<'_> {
/// Compile a quantified expression: `a?`, `a*`, `a+`.
pub(super) fn compile_quantified(&mut self, quant: &ast::QuantifiedExpr, exit: Label) -> Label {
self.compile_quantified_inner(quant, exit, None, CaptureEffects::default())
}

/// Compile a quantified expression with capture effects (passed to body).
pub(super) fn compile_quantified_inner(
&mut self,
Expand Down Expand Up @@ -340,8 +335,9 @@ impl Compiler<'_> {
// First iteration has no exit fallback (backtrack propagates to caller)
let loop_entry = self.fresh_label();

// Compile body ONCE with Nav::Stay
let body_entry = compile_body(self, Some(Nav::Stay), loop_entry);
// Compile body ONCE with Nav::StayExact (exact match at current position,
// skip-retry handles advancement if all branches fail)
let body_entry = compile_body(self, Some(Nav::StayExact), loop_entry);

// First iteration: skip-retry but NO exit (must match at least one)
let first_nav_mode = first_nav.unwrap_or(Nav::Down);
Expand Down Expand Up @@ -373,8 +369,9 @@ impl Compiler<'_> {
// When pattern fails (even on descendant), retry with next sibling
let loop_entry = self.fresh_label();

// Compile body ONCE with Nav::Stay (navigation handled separately)
let body_entry = compile_body(self, Some(Nav::Stay), loop_entry);
// Compile body ONCE with Nav::StayExact (exact match at current position,
// skip-retry handles advancement if all branches fail)
let body_entry = compile_body(self, Some(Nav::StayExact), loop_entry);

// First iteration: Down navigation with skip-retry
let first_nav_mode = first_nav.unwrap_or(Nav::Down);
Expand All @@ -397,8 +394,8 @@ impl Compiler<'_> {

QuantifierKind::Optional | QuantifierKind::OptionalNonGreedy => {
// Optional with skip-retry: matches 0 or 1 time
// Compile body with Nav::Stay
let body_entry = compile_body(self, Some(Nav::Stay), match_exit);
// Compile body with Nav::StayExact (exact match at current position)
let body_entry = compile_body(self, Some(Nav::StayExact), match_exit);

// Build exit-with-null path for when no match found
let skip_with_null = if needs_split_exits {
Expand Down Expand Up @@ -506,11 +503,12 @@ impl Compiler<'_> {
) -> Label {
let loop_entry = self.fresh_label();

// Compile body ONCE with Nav::Stay
// Compile body ONCE with Nav::StayExact (exact match at current position,
// skip-retry handles advancement if all branches fail)
let body_entry = if needs_struct_wrapper {
self.compile_struct_for_array(inner, loop_entry, Some(Nav::Stay), row_type_id)
self.compile_struct_for_array(inner, loop_entry, Some(Nav::StayExact), row_type_id)
} else {
self.compile_expr_inner(inner, loop_entry, Some(Nav::Stay), capture)
self.compile_expr_inner(inner, loop_entry, Some(Nav::StayExact), capture)
};

// First iteration: skip-retry with skip_exit as fallback
Expand Down
23 changes: 9 additions & 14 deletions crates/plotnik-lib/src/compile/sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ use super::navigation::{compute_nav_modes, is_down_nav, is_skippable_quantifier,
use super::Compiler;

impl Compiler<'_> {
/// Compile a sequence: `{a b c}`.
pub(super) fn compile_seq(&mut self, seq: &ast::SeqExpr, exit: Label) -> Label {
self.compile_seq_inner(seq, exit, None, CaptureEffects::default())
}

/// Compile a sequence with capture effects (passed to last item).
pub(super) fn compile_seq_inner(
&mut self,
Expand Down Expand Up @@ -173,11 +168,6 @@ impl Compiler<'_> {
self.compile_skippable_with_exits(first_expr, match_exit, skip_exit, first_nav, capture)
}

/// Compile an alternation: `[a b c]`.
pub(super) fn compile_alt(&mut self, alt: &ast::AltExpr, exit: Label) -> Label {
self.compile_alt_inner(alt, exit, None, CaptureEffects::default())
}

/// Compile an alternation with capture effects (passed to each branch).
pub(super) fn compile_alt_inner(
&mut self,
Expand Down Expand Up @@ -208,8 +198,13 @@ impl Compiler<'_> {
};
let merged_fields = alt_type_id.and_then(|id| self.type_ctx.get_struct_fields(id));

// Convert navigation to exact variant for alternation branches.
// Branches should match at their exact cursor position only -
// the search among positions is owned by the parent context.
// When first_nav is None (standalone definition), use StayExact.
let branch_nav = Some(first_nav.map_or(Nav::StayExact, Nav::to_exact));

// Compile each branch, collecting entry labels
// Each branch gets the same nav override since any branch could match first
let mut successors = Vec::new();
for (variant_idx, branch) in branches.iter().enumerate() {
let Some(body) = branch.body() else {
Expand Down Expand Up @@ -237,10 +232,10 @@ impl Compiler<'_> {
// Compile body with variant's scope (no outer capture - it's on EndEnum)
let body_entry = if let Some(&payload_type_id) = variant_types.get(variant_idx) {
self.with_scope(payload_type_id, |this| {
this.compile_expr_inner(&body, ende_step, first_nav, CaptureEffects::default())
this.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default())
})
} else {
self.compile_expr_inner(&body, ende_step, first_nav, CaptureEffects::default())
self.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default())
};

// Create deferred member reference for the enum variant
Expand All @@ -265,7 +260,7 @@ impl Compiler<'_> {
successors.push(e_step);
} else {
// Untagged branch: compile body, then inject Null for missing captures
let branch_entry = self.compile_expr_inner(&body, exit, first_nav, capture.clone());
let branch_entry = self.compile_expr_inner(&body, exit, branch_nav, capture.clone());

let Some(fields) = merged_fields else {
successors.push(branch_entry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ Test:
04 ε 09, 11
06 ▶
07 ε [EndObj] 06
09 (identifier) [Node Set(M0)] 07
11 (number) [Node Set(M0)] 07
09 ‼· (identifier) [Node Set(M0)] 07
11 ‼· (number) [Node Set(M0)] 07
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ Test:
06 ▶
07 ε [EndObj] 06
09 ε [EndEnum Set(M4)] 07
11 (identifier) [Node Set(M0)] 09
11 ‼· (identifier) [Node Set(M0)] 09
13 ε [Enum(M2)] 11
15 ε [EndEnum Set(M4)] 07
17 (number) [Node Set(M1)] 15
17 ‼· (number) [Node Set(M1)] 15
19 ε [Enum(M3)] 17
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Test = 01 :: T6
Test:
01 ε 02
02 ε [Obj] 04
04 (object) 05
04 ‼· (object) 05
05 ε [Arr] 07
07 ε 42, 19
09 ε [EndArr Set(M5)] 11
Expand All @@ -59,10 +59,10 @@ Test:
17 ε [EndObj] 16
19 ε [EndArr Set(M5)] 17
21 ε [EndEnum Set(M4)] 12
23 (pair) [Node Set(M0)] 21
23 ‼· (pair) [Node Set(M0)] 21
25 ε [Enum(M2)] 23
27 ε [EndEnum Set(M4)] 12
29 (shorthand_property_identifier) [Node Set(M1)] 27
29 ‼· (shorthand_property_identifier) [Node Set(M1)] 27
31 ε [Enum(M3)] 29
33 ε 25, 31
35 ε [Obj] 33
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ Test:
02 ε 09, 15
04 ▶
05 ε [EndEnum] 04
07 (identifier) [Node Set(M0)] 05
07 ‼· (identifier) [Node Set(M0)] 05
09 ε [Enum(M2)] 07
11 ε [EndEnum] 04
13 (number) [Node Set(M1)] 11
13 ‼· (number) [Node Set(M1)] 11
15 ε [Enum(M3)] 13
Loading