Skip to content

Commit 3daa56d

Browse files
authored
fix(compile): use BTreeMap order for tagged alternation variant indices (#235)
1 parent 526279c commit 3daa56d

3 files changed

Lines changed: 73 additions & 20 deletions

File tree

crates/plotnik-lib/src/compile/sequences.rs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
//! - Sequences: `{a b c}` - siblings matched in order
55
//! - Alternations: `[a b c]` - first matching branch wins
66
7+
use std::collections::BTreeMap;
8+
9+
use plotnik_core::Symbol;
10+
11+
use crate::analyze::type_check::{TypeId, TypeShape};
712
use crate::bytecode::ir::{EffectIR, Instruction, Label, MatchIR, MemberRef};
813
use crate::bytecode::{EffectOpcode, Nav};
914
use crate::parser::ast::{self, Expr, SeqItem};
10-
use crate::analyze::type_check::TypeShape;
1115

1216
use super::capture::CaptureEffects;
1317
use super::navigation::{compute_nav_modes, is_down_nav, is_skippable_quantifier, repeat_nav_for};
@@ -190,11 +194,15 @@ impl Compiler<'_> {
190194
let alt_type_shape = alt_type_id.and_then(|id| self.type_ctx.get_type(id));
191195
let is_enum = alt_type_shape.is_some_and(|shape| matches!(shape, TypeShape::Enum(_)));
192196

193-
// For tagged alternations: get variant types for scope pushing
194-
// For untagged alternations: get merged struct fields for Null injection
195-
let variant_types: Vec<_> = match alt_type_shape {
196-
Some(TypeShape::Enum(variants)) => variants.values().copied().collect(),
197-
_ => vec![],
197+
// For tagged alternations: build map from label Symbol to (member index, payload TypeId)
198+
// This ensures we use the correct BTreeMap order indices, not AST iteration order
199+
let variant_info: BTreeMap<Symbol, (u16, TypeId)> = match alt_type_shape {
200+
Some(TypeShape::Enum(variants)) => variants
201+
.iter()
202+
.enumerate()
203+
.map(|(idx, (&sym, &type_id))| (sym, (idx as u16, type_id)))
204+
.collect(),
205+
_ => BTreeMap::new(),
198206
};
199207
let merged_fields = alt_type_id.and_then(|id| self.type_ctx.get_struct_fields(id));
200208

@@ -206,12 +214,21 @@ impl Compiler<'_> {
206214

207215
// Compile each branch, collecting entry labels
208216
let mut successors = Vec::new();
209-
for (variant_idx, branch) in branches.iter().enumerate() {
217+
for branch in branches.iter() {
210218
let Some(body) = branch.body() else {
211219
continue;
212220
};
213221

214222
if is_enum {
223+
// Look up variant info by branch label (using BTreeMap order, not AST order)
224+
let label = branch.label().expect("tagged branch must have label");
225+
let label_text = label.text();
226+
let (variant_idx, payload_type_id) = variant_info
227+
.iter()
228+
.find(|(sym, _)| self.interner.resolve(**sym) == label_text)
229+
.map(|(_, info)| *info)
230+
.expect("variant must exist for labeled branch");
231+
215232
// Tagged branch: E(variant_ref) → body → EndE → exit
216233
// Outer capture effects go on EndEnum, not on the branch body
217234
let mut end_effects = vec![EffectIR::simple(EffectOpcode::EndEnum, 0)];
@@ -230,19 +247,15 @@ impl Compiler<'_> {
230247
}));
231248

232249
// Compile body with variant's scope (no outer capture - it's on EndEnum)
233-
let body_entry = if let Some(&payload_type_id) = variant_types.get(variant_idx) {
234-
self.with_scope(payload_type_id, |this| {
235-
this.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default())
236-
})
237-
} else {
238-
self.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default())
239-
};
250+
let body_entry = self.with_scope(payload_type_id, |this| {
251+
this.compile_expr_inner(&body, ende_step, branch_nav, CaptureEffects::default())
252+
});
240253

241254
// Create deferred member reference for the enum variant
242255
let e_effect = if let Some(type_id) = alt_type_id {
243-
EffectIR::with_member(EffectOpcode::Enum, MemberRef::deferred(type_id, variant_idx as u16))
256+
EffectIR::with_member(EffectOpcode::Enum, MemberRef::deferred(type_id, variant_idx))
244257
} else {
245-
EffectIR::simple(EffectOpcode::Enum, variant_idx)
258+
EffectIR::simple(EffectOpcode::Enum, variant_idx as usize)
246259
};
247260

248261
let e_step = self.fresh_label();

crates/plotnik-lib/src/engine/engine_tests.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@ fn build_trivia_types(module: &Module) -> Vec<u16> {
6060
}
6161

6262
/// Resolve entrypoint by name or use the default.
63-
fn resolve_entrypoint(
64-
module: &Module,
65-
name: Option<&str>,
66-
) -> crate::bytecode::Entrypoint {
63+
fn resolve_entrypoint(module: &Module, name: Option<&str>) -> crate::bytecode::Entrypoint {
6764
let entrypoints = module.entrypoints();
6865
let strings = module.strings();
6966

@@ -212,6 +209,23 @@ fn alternation_tagged_ident() {
212209
);
213210
}
214211

212+
/// Regression: tagged alternation with named definition reference.
213+
/// When a definition is parsed before the alternation, Symbol interning order
214+
/// differs from AST branch order. The variant index must use BTreeMap order
215+
/// (by Symbol), not AST iteration order.
216+
#[test]
217+
fn alternation_tagged_definition_ref_backtrack() {
218+
snap!(
219+
indoc! {r#"
220+
Block = (call_expression function: (identifier) @name)
221+
Statement = [Assign: (assignment_expression) @a Block: (Block) @b]
222+
Q = (program (expression_statement (Statement) @stmt))
223+
"#},
224+
"foo()",
225+
entry: "Q"
226+
);
227+
}
228+
215229
#[test]
216230
fn alternation_merge_num() {
217231
snap!(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
source: crates/plotnik-lib/src/engine/engine_tests.rs
3+
---
4+
Block = (call_expression function: (identifier) @name)
5+
Statement = [Assign: (assignment_expression) @a Block: (Block) @b]
6+
Q = (program (expression_statement (Statement) @stmt))
7+
---
8+
foo()
9+
---
10+
{
11+
"stmt": {
12+
"$tag": "Block",
13+
"$data": {
14+
"b": {
15+
"name": {
16+
"kind": "identifier",
17+
"text": "foo",
18+
"span": [
19+
0,
20+
3
21+
]
22+
}
23+
}
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)