diff --git a/crates/plotnik-lib/src/compile/sequences.rs b/crates/plotnik-lib/src/compile/sequences.rs index 83a4628a..6e1d1180 100644 --- a/crates/plotnik-lib/src/compile/sequences.rs +++ b/crates/plotnik-lib/src/compile/sequences.rs @@ -4,10 +4,14 @@ //! - Sequences: `{a b c}` - siblings matched in order //! - Alternations: `[a b c]` - first matching branch wins +use std::collections::BTreeMap; + +use plotnik_core::Symbol; + +use crate::analyze::type_check::{TypeId, TypeShape}; use crate::bytecode::ir::{EffectIR, Instruction, Label, MatchIR, MemberRef}; use crate::bytecode::{EffectOpcode, Nav}; use crate::parser::ast::{self, Expr, SeqItem}; -use crate::analyze::type_check::TypeShape; use super::capture::CaptureEffects; use super::navigation::{compute_nav_modes, is_down_nav, is_skippable_quantifier, repeat_nav_for}; @@ -190,11 +194,15 @@ impl Compiler<'_> { let alt_type_shape = alt_type_id.and_then(|id| self.type_ctx.get_type(id)); let is_enum = alt_type_shape.is_some_and(|shape| matches!(shape, TypeShape::Enum(_))); - // For tagged alternations: get variant types for scope pushing - // For untagged alternations: get merged struct fields for Null injection - let variant_types: Vec<_> = match alt_type_shape { - Some(TypeShape::Enum(variants)) => variants.values().copied().collect(), - _ => vec![], + // For tagged alternations: build map from label Symbol to (member index, payload TypeId) + // This ensures we use the correct BTreeMap order indices, not AST iteration order + let variant_info: BTreeMap = match alt_type_shape { + Some(TypeShape::Enum(variants)) => variants + .iter() + .enumerate() + .map(|(idx, (&sym, &type_id))| (sym, (idx as u16, type_id))) + .collect(), + _ => BTreeMap::new(), }; let merged_fields = alt_type_id.and_then(|id| self.type_ctx.get_struct_fields(id)); @@ -206,12 +214,21 @@ impl Compiler<'_> { // Compile each branch, collecting entry labels let mut successors = Vec::new(); - for (variant_idx, branch) in branches.iter().enumerate() { + for branch in branches.iter() { let Some(body) = branch.body() else { continue; }; if is_enum { + // Look up variant info by branch label (using BTreeMap order, not AST order) + let label = branch.label().expect("tagged branch must have label"); + let label_text = label.text(); + let (variant_idx, payload_type_id) = variant_info + .iter() + .find(|(sym, _)| self.interner.resolve(**sym) == label_text) + .map(|(_, info)| *info) + .expect("variant must exist for labeled branch"); + // Tagged branch: E(variant_ref) → body → EndE → exit // Outer capture effects go on EndEnum, not on the branch body let mut end_effects = vec![EffectIR::simple(EffectOpcode::EndEnum, 0)]; @@ -230,19 +247,15 @@ 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, branch_nav, CaptureEffects::default()) - }) - } else { - self.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default()) - }; + let body_entry = self.with_scope(payload_type_id, |this| { + this.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default()) + }); // Create deferred member reference for the enum variant let e_effect = if let Some(type_id) = alt_type_id { - EffectIR::with_member(EffectOpcode::Enum, MemberRef::deferred(type_id, variant_idx as u16)) + EffectIR::with_member(EffectOpcode::Enum, MemberRef::deferred(type_id, variant_idx)) } else { - EffectIR::simple(EffectOpcode::Enum, variant_idx) + EffectIR::simple(EffectOpcode::Enum, variant_idx as usize) }; let e_step = self.fresh_label(); diff --git a/crates/plotnik-lib/src/engine/engine_tests.rs b/crates/plotnik-lib/src/engine/engine_tests.rs index db5dbb32..9203ffeb 100644 --- a/crates/plotnik-lib/src/engine/engine_tests.rs +++ b/crates/plotnik-lib/src/engine/engine_tests.rs @@ -60,10 +60,7 @@ fn build_trivia_types(module: &Module) -> Vec { } /// Resolve entrypoint by name or use the default. -fn resolve_entrypoint( - module: &Module, - name: Option<&str>, -) -> crate::bytecode::Entrypoint { +fn resolve_entrypoint(module: &Module, name: Option<&str>) -> crate::bytecode::Entrypoint { let entrypoints = module.entrypoints(); let strings = module.strings(); @@ -212,6 +209,23 @@ fn alternation_tagged_ident() { ); } +/// Regression: tagged alternation with named definition reference. +/// When a definition is parsed before the alternation, Symbol interning order +/// differs from AST branch order. The variant index must use BTreeMap order +/// (by Symbol), not AST iteration order. +#[test] +fn alternation_tagged_definition_ref_backtrack() { + snap!( + indoc! {r#" + Block = (call_expression function: (identifier) @name) + Statement = [Assign: (assignment_expression) @a Block: (Block) @b] + Q = (program (expression_statement (Statement) @stmt)) + "#}, + "foo()", + entry: "Q" + ); +} + #[test] fn alternation_merge_num() { snap!( diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_definition_ref_backtrack.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_definition_ref_backtrack.snap new file mode 100644 index 00000000..ba76b447 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_definition_ref_backtrack.snap @@ -0,0 +1,26 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Block = (call_expression function: (identifier) @name) +Statement = [Assign: (assignment_expression) @a Block: (Block) @b] +Q = (program (expression_statement (Statement) @stmt)) +--- +foo() +--- +{ + "stmt": { + "$tag": "Block", + "$data": { + "b": { + "name": { + "kind": "identifier", + "text": "foo", + "span": [ + 0, + 3 + ] + } + } + } + } +}