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
14 changes: 13 additions & 1 deletion crates/plotnik-lib/src/analyze/type_check/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Expand Down
26 changes: 26 additions & 0 deletions crates/plotnik-lib/src/analyze/type_check/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
5 changes: 1 addition & 4 deletions crates/plotnik-lib/src/compile/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/plotnik-lib/src/compile/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ impl Compiler<'_> {
nav_override,
capture_effects,
outer_capture,
cap.has_string_annotation(),
);
}

Expand Down
7 changes: 6 additions & 1 deletion crates/plotnik-lib/src/compile/quantifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 11 additions & 1 deletion crates/plotnik-lib/src/compile/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,17 @@ 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,
exit: Label,
nav_override: Option<Nav>,
capture_effects: Vec<EffectIR>,
outer_capture: CaptureEffects,
use_text_for_elements: bool,
) -> Label {
let mut end_effects = vec![EffectIR::simple(EffectOpcode::EndArr, 0)];
end_effects.extend(capture_effects);
Expand All @@ -217,8 +221,14 @@ impl Compiler<'_> {

let push_effects = CaptureEffects {
post: if self.quantifier_needs_node_for_push(inner) {
// Use Text if the capture has `:: string` annotation, else Node
let opcode = if use_text_for_elements {
EffectOpcode::Text
} else {
EffectOpcode::Node
};
vec![
EffectIR::simple(EffectOpcode::Node, 0),
EffectIR::simple(opcode, 0),
EffectIR::simple(EffectOpcode::Push, 0),
]
} else {
Expand Down
24 changes: 23 additions & 1 deletion crates/plotnik-lib/src/engine/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ fn execute_with_entry(query: &str, source: &str, entry: Option<&str>) -> String
let vm = VM::new(&tree, trivia, FuelLimits::default());

let entrypoint = resolve_entrypoint(&module, entry);
let effects = vm.execute(&module, &entrypoint).expect("execution failed");
let effects = vm
.execute(&module, 0, &entrypoint)
.expect("execution failed");

let materializer = ValueMaterializer::new(source, module.types(), module.strings());
let value = materializer.materialize(effects.as_slice(), entrypoint.result_type);
Expand Down Expand Up @@ -181,6 +183,26 @@ fn quantifier_struct_array() {
);
}

/// Regression: string annotation on array capture should extract text.
/// `(identifier)* @names :: string` should produce string[], not Node[].
#[test]
fn quantifier_star_with_string_annotation() {
snap!(
"Q = (program (expression_statement (array (identifier)* @names :: string)))",
"[a, b, c]"
);
}

/// Regression: string annotation on non-empty array capture.
/// `(identifier)+ @names :: string` should produce [string, ...string[]].
#[test]
fn quantifier_plus_with_string_annotation() {
snap!(
"Q = (program (expression_statement (array (identifier)+ @names :: string)))",
"[a, b, c]"
);
}

// ============================================================================
// 3. ALTERNATIONS
// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/plotnik-lib/src/engine/engine_tests.rs
---
Q = (program (expression_statement (array (identifier)+ @names :: string)))
---
[a, b, c]
---
{
"names": [
"a",
"b",
"c"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/plotnik-lib/src/engine/engine_tests.rs
---
Q = (program (expression_statement (array (identifier)* @names :: string)))
---
[a, b, c]
---
{
"names": [
"a",
"b",
"c"
]
}
6 changes: 6 additions & 0 deletions crates/plotnik-lib/src/parser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,12 @@ impl CapturedExpr {
pub fn type_annotation(&self) -> Option<Type> {
self.0.children().find_map(Type::cast)
}

/// Returns true if this capture has a `:: string` type annotation.
pub fn has_string_annotation(&self) -> bool {
self.type_annotation()
.is_some_and(|t| t.name().is_some_and(|n| n.text() == "string"))
}
}

impl Type {
Expand Down