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
73 changes: 71 additions & 2 deletions crates/plotnik-lib/src/compile/quantifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ impl Compiler<'_> {
QuantifierParse::Quantified { inner, kind } => (inner, kind),
};

// When the inner returns a structured type (enum/struct) and this is a star/plus
// quantifier without explicit capture, we still need array scope (Arr/Push/EndArr)
// because the type system expects an array of these values.
let needs_implicit_array = matches!(kind, QuantifierKind::Star | QuantifierKind::Plus)
&& self.is_ref_returning_structured(&inner);

if needs_implicit_array {
// Use array scope: Arr → quantifier with Push → EndArr → exit
// No capture effects on the array itself (no Set), just collect values
return self.compile_array_scope(
&Expr::QuantifiedExpr(quant.clone()),
exit,
nav_override,
vec![], // No capture effects (no @name to set)
capture,
false, // Not a string capture
);
}

let config = QuantifierConfig {
inner: &inner,
kind,
Expand Down Expand Up @@ -216,6 +235,22 @@ impl Compiler<'_> {
QuantifierParse::Quantified { inner, kind } => (inner, kind),
};

// When the inner returns a structured type (enum/struct) and this is a star/plus
// quantifier without explicit capture, we still need array scope (Arr/Push/EndArr)
// with split exits for the skip/match paths.
let needs_implicit_array = matches!(kind, QuantifierKind::Star | QuantifierKind::Plus)
&& self.is_ref_returning_structured(&inner);

if needs_implicit_array {
return self.compile_implicit_array_with_exits(
quant,
match_exit,
skip_exit,
nav_override,
capture,
);
}

// Handle null injection for both passed captures and internal captures
let skip_with_null = self.emit_null_for_skip_path(skip_exit, &capture);
let skip_with_internal_null = self.emit_null_for_internal_captures(skip_with_null, &inner);
Expand Down Expand Up @@ -274,8 +309,42 @@ impl Compiler<'_> {
push_effects,
);

// Emit Arr step at entry
self.emit_arr_step(inner_entry)
// Emit Arr step at entry (with outer pre-effects like Enum)
self.emit_arr_step(inner_entry, outer_capture.pre)
}

/// Compile an implicit array (star/plus without @capture) returning structured type,
/// as first-child with separate exits.
///
/// Like `compile_array_capture_with_exits` but without explicit capture effects.
/// Used when `(RefName)*` where RefName returns enum/struct.
fn compile_implicit_array_with_exits(
&mut self,
quant: &ast::QuantifiedExpr,
match_exit: Label,
skip_exit: Label,
nav_override: Option<Nav>,
outer_capture: CaptureEffects,
) -> Label {
// No capture effects since there's no @name
let capture_effects = vec![];

// Create two EndArr steps with different continuations
let match_endarr = self.emit_endarr_step(&capture_effects, &outer_capture.post, match_exit);
let skip_endarr = self.emit_endarr_step(&capture_effects, &outer_capture.post, skip_exit);

// Inner returns structured type, so no Node effect needed - just Push
let push_effects = CaptureEffects::new_post(vec![EffectIR::push()]);
let inner_entry = self.compile_star_for_array_with_exits(
&Expr::QuantifiedExpr(quant.clone()),
match_endarr,
skip_endarr,
nav_override,
push_effects,
);

// Emit Arr step at entry (with outer pre-effects like Enum)
self.emit_arr_step(inner_entry, outer_capture.pre)
}

/// Compile a star quantifier for array context with separate exits.
Expand Down
6 changes: 4 additions & 2 deletions crates/plotnik-lib/src/compile/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ impl Compiler<'_> {
let arr_step = self.fresh_label();
self.instructions.push(
MatchIR::epsilon(arr_step, inner_entry)
.pre_effects(outer_capture.pre)
.pre_effect(EffectIR::start_arr())
.into(),
);
Expand Down Expand Up @@ -284,11 +285,12 @@ impl Compiler<'_> {
label
}

/// Emit an Arr epsilon step.
pub(super) fn emit_arr_step(&mut self, successor: Label) -> Label {
/// Emit an Arr epsilon step with optional pre-effects before start_arr.
pub(super) fn emit_arr_step(&mut self, successor: Label, pre_effects: Vec<EffectIR>) -> Label {
let label = self.fresh_label();
self.instructions.push(
MatchIR::epsilon(label, successor)
.pre_effects(pre_effects)
.pre_effect(EffectIR::start_arr())
.into(),
);
Expand Down
40 changes: 40 additions & 0 deletions crates/plotnik-lib/src/engine/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,43 @@ fn regression_childless_node_with_inner_optional() {
"foo; bar; baz"
);
}

/// Regression test: recursive labeled alternation with uncaptured star quantifier
/// should produce arrays, not nested tagged unions.
///
/// When `(RefName)*` is inside a labeled alternation variant (like Descend),
/// and RefName returns an enum type, the quantifier needs to produce an array
/// even without an explicit `@capture` annotation.
#[test]
fn regression_recursive_alternation_uncaptured_array() {
snap!(
indoc! {r#"
E = [
A: (identifier) @x
B: (_ (E)*)
]
Test = (program (E)* @items)
"#},
"x; y;",
entry: "Test"
);
}

/// Regression test: labeled alternation variant with array capture payload.
///
/// When a labeled alternation variant has an array capture as its payload
/// (e.g., `[First: (E)* @arr]`), the Enum effect must be emitted BEFORE the Arr
/// effect. Previously, `compile_array_scope` forgot to emit `outer_capture.pre`,
/// causing a stack mismatch panic in the materializer.
#[test]
fn regression_enum_variant_array_capture_payload() {
snap!(
indoc! {r#"
E = [A: (identifier) @x B: (number) @y]
F = [First: (E)* @arr Second: (E)+ @arr2]
Test = (program (expression_statement (F)))
"#},
"x",
entry: "Test"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: crates/plotnik-lib/src/engine/engine_tests.rs
---
E = [A: (identifier) @x B: (number) @y]
F = [First: (E)* @arr Second: (E)+ @arr2]
Test = (program (expression_statement (F)))
---
x
---
{
"$tag": "First",
"$data": {
"arr": [
{
"$tag": "A",
"$data": {
"x": {
"kind": "identifier",
"text": "x",
"span": [
0,
1
]
}
}
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
source: crates/plotnik-lib/src/engine/engine_tests.rs
---
E = [
A: (identifier) @x
B: (_ (E)*)
]
Test = (program (E)* @items)
---
x; y;
---
{
"items": [
{
"$tag": "B",
"$data": [
{
"$tag": "A",
"$data": {
"x": {
"kind": "identifier",
"text": "x",
"span": [
0,
1
]
}
}
}
]
},
{
"$tag": "B",
"$data": [
{
"$tag": "A",
"$data": {
"x": {
"kind": "identifier",
"text": "y",
"span": [
3,
4
]
}
}
}
]
}
]
}