diff --git a/crates/plotnik-lib/src/compile/capture.rs b/crates/plotnik-lib/src/compile/capture.rs index ab3e9661..6c0ec8d9 100644 --- a/crates/plotnik-lib/src/compile/capture.rs +++ b/crates/plotnik-lib/src/compile/capture.rs @@ -44,11 +44,16 @@ impl Compiler<'_> { // Check if inner is a scope-creating expression (SeqExpr/AltExpr) that produces // a structured type (Struct/Enum). Named nodes with bubble captures don't count - // they still need Node because we're capturing the matched node, not the struct. - let creates_structured_scope = inner.is_some_and(|i| { - inner_creates_scope(i) + // + // For FieldExpr, look through to the value. The parser treats `field: expr @cap` as + // `(field: expr) @cap` so that quantifiers work on fields (e.g., `decorator: (x)*` + // for repeating fields). This means captures wrap the FieldExpr, but the value + // determines whether it produces a structured type. See `parse_expr_no_suffix`. + let creates_structured_scope = inner.and_then(unwrap_field_value).is_some_and(|ei| { + inner_creates_scope(&ei) && self .type_ctx - .get_term_info(i) + .get_term_info(&ei) .and_then(|info| info.flow.type_id()) .and_then(|id| self.type_ctx.get_type(id)) .is_some_and(|shape| matches!(shape, TypeShape::Struct(_) | TypeShape::Enum(_))) @@ -148,6 +153,17 @@ impl Compiler<'_> { } } +/// Unwrap FieldExpr to get its value, pass through other expressions. +/// +/// Used when checking properties of a captured expression - captures on fields +/// like `field: [A: B:] @cap` wrap the FieldExpr, but we need to inspect the value. +fn unwrap_field_value(expr: &Expr) -> Option { + match expr { + Expr::FieldExpr(f) => f.value(), + other => Some(other.clone()), + } +} + /// Check if inner needs struct wrapper for array iterations. /// /// Returns true when inner is a scope-creating expression (sequence/alternation) diff --git a/crates/plotnik-lib/src/compile/expressions.rs b/crates/plotnik-lib/src/compile/expressions.rs index 2cc1a690..113c0d14 100644 --- a/crates/plotnik-lib/src/compile/expressions.rs +++ b/crates/plotnik-lib/src/compile/expressions.rs @@ -453,6 +453,26 @@ impl Compiler<'_> { }; } + // Handle scope-creating scalar expressions (tagged enums) + // Enum produces its own value via EndEnum - capture effects go AFTER, not inside + let inner_is_scope_creating_scalar = !inner_is_bubble + && inner_creates_scope + && inner_info + .as_ref() + .and_then(|info| info.flow.type_id()) + .and_then(|id| self.type_ctx.get_type(id)) + .is_some_and(|shape| matches!(shape, TypeShape::Enum(_))); + + if inner_is_scope_creating_scalar { + let set_step = self.emit_effects_epsilon(exit, capture_effects, outer_capture); + return self.compile_expr_inner( + &inner, + set_step, + nav_override, + CaptureEffects::default(), + ); + } + // Array: Arr → quantifier (with Push) → EndArr+capture → exit // Check if inner is a * or + quantifier - these produce arrays regardless of arity let inner_is_array = is_star_or_plus_quantifier(Some(&inner)); diff --git a/crates/plotnik-lib/src/emit/codegen_tests.rs b/crates/plotnik-lib/src/emit/codegen_tests.rs index 2d0c5350..4f654d77 100644 --- a/crates/plotnik-lib/src/emit/codegen_tests.rs +++ b/crates/plotnik-lib/src/emit/codegen_tests.rs @@ -358,6 +358,16 @@ fn alternations_no_internal_captures() { "#}); } +#[test] +fn alternations_tagged_in_field_constraint() { + // Regression test: captured tagged alternation as field value should not emit Node effect. + // The capture `@kind` applies to the field expression, but the value determines + // whether it's a structured scope (enum in this case). + snap!(indoc! {r#" + Test = (foo field: [A: (x) @a B: (y)] @kind) + "#}); +} + // ============================================================================ // 7. ANCHORS // ============================================================================ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap index 23f3d1ad..b5368902 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap @@ -46,11 +46,12 @@ _ObjWrap: Test: 06 ε 07 - 07 ε 14, 20 + 07 ε 16, 22 09 ▶ - 10 ε [EndEnum Set(M4)] 09 - 12 ! (identifier) [Node Set(M0)] 10 - 14 ε [Enum(M2)] 12 - 16 ε [EndEnum Set(M4)] 09 - 18 ! (number) [Node Set(M1)] 16 - 20 ε [Enum(M3)] 18 + 10 ε [Set(M4)] 09 + 12 ε [EndEnum] 10 + 14 ! (identifier) [Node Set(M0)] 12 + 16 ε [Enum(M2)] 14 + 18 ε [EndEnum] 10 + 20 ! (number) [Node Set(M1)] 18 + 22 ε [Enum(M3)] 20 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap index 8fcaf7a7..e969187b 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap @@ -53,26 +53,27 @@ Test: 06 ε 07 07 ! (object) 08 08 ε [Arr] 10 - 10 ε 43, 20 + 10 ε 45, 20 12 ε [EndArr Set(M5)] 14 14 △ 19 15 ε [EndObj Push] 17 - 17 ε 49, 12 + 17 ε 51, 12 19 ▶ 20 ε [EndArr Set(M5)] 19 - 22 ε [EndEnum Set(M4)] 15 - 24 ! (pair) [Node Set(M0)] 22 - 26 ε [Enum(M2)] 24 - 28 ε [EndEnum Set(M4)] 15 - 30 ! (shorthand_property_identifier) [Node Set(M1)] 28 - 32 ε [Enum(M3)] 30 - 34 ε 26, 32 - 36 ε [Obj] 34 - 38 ▷ 41 - 39 ε 38, 20 - 41 ε 36, 39 - 43 ▽ 41 - 44 ▷ 47 - 45 ε 44, 12 - 47 ε 36, 45 - 49 ▷ 47 + 22 ε [Set(M4)] 15 + 24 ε [EndEnum] 22 + 26 ! (pair) [Node Set(M0)] 24 + 28 ε [Enum(M2)] 26 + 30 ε [EndEnum] 22 + 32 ! (shorthand_property_identifier) [Node Set(M1)] 30 + 34 ε [Enum(M3)] 32 + 36 ε 28, 34 + 38 ε [Obj] 36 + 40 ▷ 43 + 41 ε 40, 20 + 43 ε 38, 41 + 45 ▽ 43 + 46 ▷ 49 + 47 ε 46, 12 + 49 ε 38, 47 + 51 ▷ 49 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_in_field_constraint.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_in_field_constraint.snap new file mode 100644 index 00000000..2e84b2ac --- /dev/null +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_in_field_constraint.snap @@ -0,0 +1,59 @@ +--- +source: crates/plotnik-lib/src/emit/codegen_tests.rs +--- +Test = (foo field: [A: (x) @a B: (y)] @kind) +--- +[flags] +linked = false + +[strings] +S0 "Beauty will save the world" +S1 "a" +S2 "A" +S3 "B" +S4 "kind" +S5 "Test" +S6 "foo" +S7 "field" +S8 "x" +S9 "y" + +[type_defs] +T0 = +T1 = +T2 = Struct M0:1 ; { a } +T3 = Enum M1:2 ; A | B +T4 = Struct M3:1 ; { kind } + +[type_members] +M0: S1 → T1 ; a: +M1: S2 → T2 ; A: T2 +M2: S3 → T0 ; B: +M3: S4 → T3 ; kind: T3 + +[type_names] +N0: S5 → T4 ; Test + +[entrypoints] +Test = 06 :: T4 + +[transitions] +_ObjWrap: + 00 ε [Obj] 02 + 02 Trampoline 03 + 03 ε [EndObj] 05 + 05 ▶ + +Test: + 06 ε 07 + 07 ! (foo) 08 + 08 ▽ field: _ 09 + 09 ε 16, 21 + 11 ▶ + 12 ε [EndEnum Set(M3)] 23 + 14 ! (x) [Node Set(M0)] 12 + 16 ε [Enum(M1)] 14 + 18 ε [EndEnum Set(M3)] 23 + 20 ! (y) 18 + 21 ε [Enum(M2)] 20 + 23 △ 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_with_definition_ref.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_with_definition_ref.snap index 8911655e..f5a2d9ca 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_with_definition_ref.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_tagged_with_definition_ref.snap @@ -55,11 +55,12 @@ Inner: Test: 10 ε 11 - 11 ε 17, 23 + 11 ε 19, 25 13 ▶ - 14 ε [EndEnum Set(M4)] 13 - 16 ! (Inner) 06 : 14 - 17 ε [Enum(M2)] 16 - 19 ε [EndEnum Set(M4)] 13 - 21 ! (number) [Node Set(M1)] 19 - 23 ε [Enum(M3)] 21 + 14 ε [Set(M4)] 13 + 16 ε [EndEnum] 14 + 18 ! (Inner) 06 : 16 + 19 ε [Enum(M2)] 18 + 21 ε [EndEnum] 14 + 23 ! (number) [Node Set(M1)] 21 + 25 ε [Enum(M3)] 23 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_enum_with_type_annotation.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_enum_with_type_annotation.snap index 9133bcfa..458ede90 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_enum_with_type_annotation.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_enum_with_type_annotation.snap @@ -48,11 +48,12 @@ _ObjWrap: Test: 06 ε 07 - 07 ε 14, 20 + 07 ε 16, 22 09 ▶ - 10 ε [EndEnum Set(M4)] 09 - 12 ! (identifier) [Node Set(M0)] 10 - 14 ε [Enum(M2)] 12 - 16 ε [EndEnum Set(M4)] 09 - 18 ! (number) [Node Set(M1)] 16 - 20 ε [Enum(M3)] 18 + 10 ε [Set(M4)] 09 + 12 ε [EndEnum] 10 + 14 ! (identifier) [Node Set(M0)] 12 + 16 ε [Enum(M2)] 14 + 18 ε [EndEnum] 10 + 20 ! (number) [Node Set(M1)] 18 + 22 ε [Enum(M3)] 20 diff --git a/crates/plotnik-lib/src/parser/grammar/expressions.rs b/crates/plotnik-lib/src/parser/grammar/expressions.rs index f5168eb7..85e9311b 100644 --- a/crates/plotnik-lib/src/parser/grammar/expressions.rs +++ b/crates/plotnik-lib/src/parser/grammar/expressions.rs @@ -35,7 +35,14 @@ impl Parser<'_, '_> { } /// Parse expression without applying quantifier/capture suffix. - /// Used for field values so that `field: (x)*` parses as `(field: (x))*`. + /// + /// Used for field values so that suffixes apply to the whole field constraint: + /// - `field: (x)*` parses as `(field: (x))*` — repeat the field (e.g., decorators) + /// - `field: (x) @cap` parses as `(field: (x)) @cap` — capture the field expression + /// + /// For captures on structured values (enums/structs), the compilation handles this + /// by looking through FieldExpr to determine the actual value type. See + /// `build_capture_effects` in compile/capture.rs. pub(crate) fn parse_expr_no_suffix(&mut self) { self.parse_expr_inner(false) } diff --git a/docs/lang-reference.md b/docs/lang-reference.md index a64ab1fe..8fae5feb 100644 --- a/docs/lang-reference.md +++ b/docs/lang-reference.md @@ -481,6 +481,17 @@ Output type: { target: string, value: Node } ``` +### Quantifiers and Captures on Fields + +Quantifiers and captures after a field value apply to the entire field constraint, not just the value: + +``` +decorator: (decorator)* @decorators ; repeats the whole field +value: [A: (x) B: (y)] @kind ; captures the field (containing the alternation) +``` + +This allows repeating fields (useful for things like decorators in JavaScript). The capture still correctly produces the value's type—for alternations, you get the tagged union, not a raw node. + ### Negated Fields Assert a field is absent with `-`: