From d6dee249a7fe3c5dd4fc146aca7a0a96d9ea8606 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 5 Jan 2026 12:21:18 -0300 Subject: [PATCH] fix: `:: string` annotation on quantified captures produces correct types --- .../src/analyze/type_check/infer.rs | 14 +++++++++- .../src/analyze/type_check/tests.rs | 26 +++++++++++++++++++ crates/plotnik-lib/src/compile/capture.rs | 5 +--- crates/plotnik-lib/src/compile/expressions.rs | 1 + crates/plotnik-lib/src/compile/quantifier.rs | 7 ++++- crates/plotnik-lib/src/compile/scope.rs | 12 ++++++++- crates/plotnik-lib/src/engine/engine_tests.rs | 24 ++++++++++++++++- ...uantifier_plus_with_string_annotation.snap | 14 ++++++++++ ...uantifier_star_with_string_annotation.snap | 14 ++++++++++ crates/plotnik-lib/src/parser/ast.rs | 6 +++++ 10 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_plus_with_string_annotation.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_star_with_string_annotation.snap diff --git a/crates/plotnik-lib/src/analyze/type_check/infer.rs b/crates/plotnik-lib/src/analyze/type_check/infer.rs index 4e423b44..5b112a4b 100644 --- a/crates/plotnik-lib/src/analyze/type_check/infer.rs +++ b/crates/plotnik-lib/src/analyze/type_check/infer.rs @@ -429,7 +429,19 @@ impl<'a, 'd> InferenceVisitor<'a, 'd> { annotation.unwrap_or(TYPE_NODE) } } - TypeFlow::Scalar(type_id) => annotation.unwrap_or(*type_id), + TypeFlow::Scalar(type_id) => { + // For array types with annotation, replace the element type + // e.g., `(identifier)* @names :: string` → string[] not string + if let Some(ann) = annotation + && let Some(TypeShape::Array { non_empty, .. }) = self.ctx.get_type(*type_id) + { + return self.ctx.intern_type(TypeShape::Array { + element: ann, + non_empty: *non_empty, + }); + } + annotation.unwrap_or(*type_id) + } TypeFlow::Bubble(type_id) => annotation.unwrap_or(*type_id), } } diff --git a/crates/plotnik-lib/src/analyze/type_check/tests.rs b/crates/plotnik-lib/src/analyze/type_check/tests.rs index b8077fd5..041fd02a 100644 --- a/crates/plotnik-lib/src/analyze/type_check/tests.rs +++ b/crates/plotnik-lib/src/analyze/type_check/tests.rs @@ -250,6 +250,32 @@ fn scalar_list_one_or_more() { "); } +#[test] +fn scalar_list_with_string_annotation_zero_or_more() { + let input = "Q = (identifier)* @names :: string"; + + let res = Query::expect_valid_types(input); + + insta::assert_snapshot!(res, @r" + export interface Q { + names: string[]; + } + "); +} + +#[test] +fn scalar_list_with_string_annotation_one_or_more() { + let input = "Q = (identifier)+ @names :: string"; + + let res = Query::expect_valid_types(input); + + insta::assert_snapshot!(res, @r" + export interface Q { + names: [string, ...string[]]; + } + "); +} + #[test] fn row_list_basic() { let input = indoc! {r#" diff --git a/crates/plotnik-lib/src/compile/capture.rs b/crates/plotnik-lib/src/compile/capture.rs index 3e8b9d07..ab3e9661 100644 --- a/crates/plotnik-lib/src/compile/capture.rs +++ b/crates/plotnik-lib/src/compile/capture.rs @@ -55,10 +55,7 @@ impl Compiler<'_> { }); if !is_structured_ref && !creates_structured_scope && !is_array { - let is_text = cap - .type_annotation() - .is_some_and(|t| t.name().is_some_and(|n| n.text() == "string")); - let opcode = if is_text { + let opcode = if cap.has_string_annotation() { EffectOpcode::Text } else { EffectOpcode::Node diff --git a/crates/plotnik-lib/src/compile/expressions.rs b/crates/plotnik-lib/src/compile/expressions.rs index b2342380..4c0317d0 100644 --- a/crates/plotnik-lib/src/compile/expressions.rs +++ b/crates/plotnik-lib/src/compile/expressions.rs @@ -370,6 +370,7 @@ impl Compiler<'_> { nav_override, capture_effects, outer_capture, + cap.has_string_annotation(), ); } diff --git a/crates/plotnik-lib/src/compile/quantifier.rs b/crates/plotnik-lib/src/compile/quantifier.rs index 7b682e1e..93f6b701 100644 --- a/crates/plotnik-lib/src/compile/quantifier.rs +++ b/crates/plotnik-lib/src/compile/quantifier.rs @@ -260,8 +260,13 @@ impl Compiler<'_> { let push_effects = CaptureEffects { post: if self.quantifier_needs_node_for_push(inner) { + let opcode = if cap.has_string_annotation() { + EffectOpcode::Text + } else { + EffectOpcode::Node + }; vec![ - EffectIR::simple(EffectOpcode::Node, 0), + EffectIR::simple(opcode, 0), EffectIR::simple(EffectOpcode::Push, 0), ] } else { diff --git a/crates/plotnik-lib/src/compile/scope.rs b/crates/plotnik-lib/src/compile/scope.rs index 38a87327..9795c3d4 100644 --- a/crates/plotnik-lib/src/compile/scope.rs +++ b/crates/plotnik-lib/src/compile/scope.rs @@ -191,6 +191,9 @@ impl Compiler<'_> { } /// Compile array scope: Arr → quantifier (with Push) → EndArr+capture → exit + /// + /// `use_text_for_elements` indicates whether to use `Text` effect for array elements + /// (true when the capture has `:: string` annotation). pub(super) fn compile_array_scope( &mut self, inner: &Expr, @@ -198,6 +201,7 @@ impl Compiler<'_> { nav_override: Option