From 69a05274f28e5a0f0f1cee3d05ee67f07922600a Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 09:05:30 -0300 Subject: [PATCH 01/21] refactor: Enhance query AST handling --- crates/plotnik-lib/src/parser/ast.rs | 28 +++++++++--------- crates/plotnik-lib/src/parser/mod.rs | 28 +++--------------- crates/plotnik-lib/src/query/alt_kind.rs | 2 +- crates/plotnik-lib/src/query/mod.rs | 29 +++++++++---------- crates/plotnik-lib/src/query/printer.rs | 22 +++++++------- .../src/query/shape_cardinalities.rs | 20 ++++++------- 6 files changed, 52 insertions(+), 77 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index 3b53efb3..7e2e2b90 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -15,7 +15,7 @@ macro_rules! ast_node { (node.kind() == SyntaxKind::$kind).then(|| Self(node)) } - pub fn syntax(&self) -> &SyntaxNode { + pub fn as_cst(&self) -> &SyntaxNode { &self.0 } } @@ -83,25 +83,23 @@ impl Expr { } } - pub fn syntax(&self) -> &SyntaxNode { + pub fn as_cst(&self) -> &SyntaxNode { match self { - Expr::Tree(n) => n.syntax(), - Expr::Ref(n) => n.syntax(), - Expr::Str(n) => n.syntax(), - Expr::Alt(n) => n.syntax(), - Expr::Seq(n) => n.syntax(), - Expr::Capture(n) => n.syntax(), - Expr::Quantifier(n) => n.syntax(), - Expr::Field(n) => n.syntax(), - Expr::NegatedField(n) => n.syntax(), - Expr::Wildcard(n) => n.syntax(), - Expr::Anchor(n) => n.syntax(), + Expr::Tree(n) => n.as_cst(), + Expr::Ref(n) => n.as_cst(), + Expr::Str(n) => n.as_cst(), + Expr::Alt(n) => n.as_cst(), + Expr::Seq(n) => n.as_cst(), + Expr::Capture(n) => n.as_cst(), + Expr::Quantifier(n) => n.as_cst(), + Expr::Field(n) => n.as_cst(), + Expr::NegatedField(n) => n.as_cst(), + Expr::Wildcard(n) => n.as_cst(), + Expr::Anchor(n) => n.as_cst(), } } } -// --- Accessors --- - impl Root { pub fn defs(&self) -> impl Iterator + '_ { self.0.children().filter_map(Def::cast) diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index 1d9ef9a0..a74a1d45 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -52,35 +52,15 @@ pub use core::Parser; use crate::PassResult; use lexer::lex; -/// Parse result containing the green tree. -/// -/// The tree is always complete—diagnostics are returned separately. -/// Error nodes in the tree represent recovery points. -#[derive(Debug, Clone)] -pub struct Parse { - cst: rowan::GreenNode, -} - -impl Parse { - pub fn as_cst(&self) -> &rowan::GreenNode { - &self.cst - } - - /// Creates a typed view over the immutable green tree. - /// This is cheap—SyntaxNode is a thin wrapper with parent pointers. - pub fn syntax(&self) -> SyntaxNode { - SyntaxNode::new_root(self.cst.clone()) - } -} - /// Main entry point. Returns Err on fuel exhaustion. -pub fn parse(source: &str) -> PassResult { +pub fn parse(source: &str) -> PassResult { parse_with_parser(Parser::new(source, lex(source))) } /// Parse with a pre-configured parser (for custom fuel limits). -pub(crate) fn parse_with_parser(mut parser: Parser) -> PassResult { +pub(crate) fn parse_with_parser(mut parser: Parser) -> PassResult { parser.parse_root(); let (cst, diagnostics) = parser.finish()?; - Ok((Parse { cst }, diagnostics)) + let root = Root::cast(SyntaxNode::new_root(cst)).expect("parser always produces Root"); + Ok((root, diagnostics)) } diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index 2e6100a8..7dc78d71 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -113,5 +113,5 @@ fn check_mixed_alternation(alt: &Alt, errors: &mut Diagnostics) { } fn branch_range(branch: &Branch) -> TextRange { - branch.syntax().text_range() + branch.as_cst().text_range() } diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 6e37fc96..ef393c25 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -28,7 +28,7 @@ use std::collections::HashMap; use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::lexer::lex; -use crate::parser::{self, Parse, Parser, Root, SyntaxNode}; +use crate::parser::{self, Parser, Root, SyntaxNode}; use named_defs::SymbolTable; use shape_cardinalities::ShapeCardinality; @@ -95,7 +95,7 @@ impl<'a> QueryBuilder<'a> { #[derive(Debug, Clone)] pub struct Query<'a> { source: &'a str, - parse: Parse, + ast: Root, symbols: SymbolTable, shape_cardinalities: HashMap, // Diagnostics per pass @@ -120,25 +120,22 @@ impl<'a> Query<'a> { QueryBuilder::new(source) } - fn from_parse(source: &'a str, parse: Parse, parse_diagnostics: Diagnostics) -> Self { - let root = Root::cast(parse.syntax()).expect("parser always produces Root"); - + fn from_parse(source: &'a str, ast: Root, parse_diagnostics: Diagnostics) -> Self { let ((), alt_kind_diagnostics) = - alt_kind::validate(&root).expect("alt_kind::validate is infallible"); + alt_kind::validate(&ast).expect("alt_kind::validate is infallible"); let (symbols, resolve_diagnostics) = - named_defs::resolve(&root).expect("named_defs::resolve is infallible"); + named_defs::resolve(&ast).expect("named_defs::resolve is infallible"); let ((), ref_cycle_diagnostics) = - ref_cycles::validate(&root, &symbols).expect("ref_cycles::validate is infallible"); + ref_cycles::validate(&ast, &symbols).expect("ref_cycles::validate is infallible"); - let (shape_cardinalities, shape_diagnostics) = - shape_cardinalities::analyze(&root, &symbols) - .expect("shape_cardinalities::analyze is infallible"); + let (shape_cardinalities, shape_diagnostics) = shape_cardinalities::analyze(&ast, &symbols) + .expect("shape_cardinalities::analyze is infallible"); Self { source, - parse, + ast, symbols, shape_cardinalities, parse_diagnostics, @@ -154,12 +151,12 @@ impl<'a> Query<'a> { self.source } - pub fn syntax(&self) -> SyntaxNode { - self.parse.syntax() + pub fn as_cst(&self) -> &SyntaxNode { + self.ast.as_cst() } - pub fn root(&self) -> Root { - Root::cast(self.parse.syntax()).expect("parser always produces Root") + pub fn root(&self) -> &Root { + &self.ast } pub fn symbols(&self) -> &SymbolTable { diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index fbb8581c..80dab91b 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -64,9 +64,9 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { return self.format_symbols(w); } if self.raw { - return self.format_cst(&self.query.syntax(), 0, w); + return self.format_cst(self.query.as_cst(), 0, w); } - self.format_root(&self.query.root(), w) + self.format_root(self.query.root(), w) } fn format_symbols(&self, w: &mut impl Write) -> std::fmt::Result { @@ -83,7 +83,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { let mut body_nodes: HashMap = HashMap::new(); for def in self.query.root().defs() { if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { - body_nodes.insert(name_tok.text().to_string(), body.syntax().clone()); + body_nodes.insert(name_tok.text().to_string(), body.as_cst().clone()); } } @@ -166,8 +166,8 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { } fn format_root(&self, root: &ast::Root, w: &mut impl Write) -> std::fmt::Result { - let card = self.cardinality_mark(root.syntax()); - let span = self.span_str(root.syntax().text_range()); + let card = self.cardinality_mark(root.as_cst()); + let span = self.span_str(root.as_cst().text_range()); writeln!(w, "Root{}{}", card, span)?; for def in root.defs() { @@ -183,8 +183,8 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_def(&self, def: &ast::Def, indent: usize, w: &mut impl Write) -> std::fmt::Result { let prefix = " ".repeat(indent); - let card = self.cardinality_mark(def.syntax()); - let span = self.span_str(def.syntax().text_range()); + let card = self.cardinality_mark(def.as_cst()); + let span = self.span_str(def.as_cst().text_range()); let name = def.name().map(|t| t.text().to_string()); match name { @@ -200,8 +200,8 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_expr(&self, expr: &ast::Expr, indent: usize, w: &mut impl Write) -> std::fmt::Result { let prefix = " ".repeat(indent); - let card = self.cardinality_mark(expr.syntax()); - let span = self.span_str(expr.syntax().text_range()); + let card = self.cardinality_mark(expr.as_cst()); + let span = self.span_str(expr.as_cst().text_range()); match expr { ast::Expr::Tree(t) => { @@ -294,8 +294,8 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { w: &mut impl Write, ) -> std::fmt::Result { let prefix = " ".repeat(indent); - let card = self.cardinality_mark(branch.syntax()); - let span = self.span_str(branch.syntax().text_range()); + let card = self.cardinality_mark(branch.as_cst()); + let span = self.span_str(branch.as_cst().text_range()); let label = branch.label().map(|t| t.text().to_string()); match label { diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index 8dc1749e..af4e9351 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -36,12 +36,12 @@ pub fn analyze( for def in root.defs() { if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { - def_bodies.insert(name_tok.text().to_string(), body.syntax().clone()); + def_bodies.insert(name_tok.text().to_string(), body.as_cst().clone()); } } - compute_node_cardinality(&root.syntax().clone(), symbols, &def_bodies, &mut result); - validate_node(&root.syntax().clone(), &result, &mut errors); + compute_node_cardinality(&root.as_cst().clone(), symbols, &def_bodies, &mut result); + validate_node(&root.as_cst().clone(), &result, &mut errors); Ok((result, errors)) } @@ -78,14 +78,14 @@ fn compute_single( if let Some(def) = Def::cast(node.clone()) { return def .body() - .map(|b| get_or_compute(b.syntax(), symbols, def_bodies, cache)) + .map(|b| get_or_compute(b.as_cst(), symbols, def_bodies, cache)) .unwrap_or(ShapeCardinality::Invalid); } if let Some(branch) = Branch::cast(node.clone()) { return branch .body() - .map(|b| get_or_compute(b.syntax(), symbols, def_bodies, cache)) + .map(|b| get_or_compute(b.as_cst(), symbols, def_bodies, cache)) .unwrap_or(ShapeCardinality::Invalid); } @@ -112,12 +112,12 @@ fn compute_single( Expr::Capture(ref cap) => { let inner = ensure_capture_has_inner(cap.inner()); - get_or_compute(inner.syntax(), symbols, def_bodies, cache) + get_or_compute(inner.as_cst(), symbols, def_bodies, cache) } Expr::Quantifier(ref q) => { let inner = ensure_quantifier_has_inner(q.inner()); - get_or_compute(inner.syntax(), symbols, def_bodies, cache) + get_or_compute(inner.as_cst(), symbols, def_bodies, cache) } Expr::Ref(ref r) => ref_cardinality(r, symbols, def_bodies, cache), @@ -148,7 +148,7 @@ fn seq_cardinality( match children.len() { 0 => ShapeCardinality::One, - 1 => get_or_compute(children[0].syntax(), symbols, def_bodies, cache), + 1 => get_or_compute(children[0].as_cst(), symbols, def_bodies, cache), _ => ShapeCardinality::Many, } } @@ -182,7 +182,7 @@ fn validate_node( && let Some(value) = field.value() { let card = cardinalities - .get(value.syntax()) + .get(value.as_cst()) .copied() .unwrap_or(ShapeCardinality::One); @@ -198,7 +198,7 @@ fn validate_node( "field `{}` value must match a single node, not a sequence", field_name ), - value.syntax().text_range(), + value.as_cst().text_range(), ) .emit(); } From 24128f7e92877411ed14ecd68598a8d74ba0c7e0 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 09:14:43 -0300 Subject: [PATCH 02/21] Add text_range method to AST nodes --- crates/plotnik-lib/src/parser/ast.rs | 21 +++++++++++++++++++ crates/plotnik-lib/src/query/alt_kind.rs | 2 +- crates/plotnik-lib/src/query/printer.rs | 8 +++---- .../src/query/shape_cardinalities.rs | 2 +- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index 7e2e2b90..576cffc8 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -4,6 +4,7 @@ //! Cast is infallible for correct `SyntaxKind` - validation happens elsewhere. use super::cst::{SyntaxKind, SyntaxNode, SyntaxToken}; +use rowan::TextRange; macro_rules! ast_node { ($name:ident, $kind:ident) => { @@ -18,6 +19,10 @@ macro_rules! ast_node { pub fn as_cst(&self) -> &SyntaxNode { &self.0 } + + pub fn text_range(&self) -> TextRange { + self.0.text_range() + } } }; } @@ -98,6 +103,22 @@ impl Expr { Expr::Anchor(n) => n.as_cst(), } } + + pub fn text_range(&self) -> TextRange { + match self { + Expr::Tree(n) => n.text_range(), + Expr::Ref(n) => n.text_range(), + Expr::Str(n) => n.text_range(), + Expr::Alt(n) => n.text_range(), + Expr::Seq(n) => n.text_range(), + Expr::Capture(n) => n.text_range(), + Expr::Quantifier(n) => n.text_range(), + Expr::Field(n) => n.text_range(), + Expr::NegatedField(n) => n.text_range(), + Expr::Wildcard(n) => n.text_range(), + Expr::Anchor(n) => n.text_range(), + } + } } impl Root { diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index 7dc78d71..96245d68 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -113,5 +113,5 @@ fn check_mixed_alternation(alt: &Alt, errors: &mut Diagnostics) { } fn branch_range(branch: &Branch) -> TextRange { - branch.as_cst().text_range() + branch.text_range() } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 80dab91b..9bd35c98 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -167,7 +167,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_root(&self, root: &ast::Root, w: &mut impl Write) -> std::fmt::Result { let card = self.cardinality_mark(root.as_cst()); - let span = self.span_str(root.as_cst().text_range()); + let span = self.span_str(root.text_range()); writeln!(w, "Root{}{}", card, span)?; for def in root.defs() { @@ -184,7 +184,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_def(&self, def: &ast::Def, indent: usize, w: &mut impl Write) -> std::fmt::Result { let prefix = " ".repeat(indent); let card = self.cardinality_mark(def.as_cst()); - let span = self.span_str(def.as_cst().text_range()); + let span = self.span_str(def.text_range()); let name = def.name().map(|t| t.text().to_string()); match name { @@ -201,7 +201,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_expr(&self, expr: &ast::Expr, indent: usize, w: &mut impl Write) -> std::fmt::Result { let prefix = " ".repeat(indent); let card = self.cardinality_mark(expr.as_cst()); - let span = self.span_str(expr.as_cst().text_range()); + let span = self.span_str(expr.text_range()); match expr { ast::Expr::Tree(t) => { @@ -295,7 +295,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { ) -> std::fmt::Result { let prefix = " ".repeat(indent); let card = self.cardinality_mark(branch.as_cst()); - let span = self.span_str(branch.as_cst().text_range()); + let span = self.span_str(branch.text_range()); let label = branch.label().map(|t| t.text().to_string()); match label { diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index af4e9351..280daeed 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -198,7 +198,7 @@ fn validate_node( "field `{}` value must match a single node, not a sequence", field_name ), - value.as_cst().text_range(), + value.text_range(), ) .emit(); } From 8ea7a8736a679ce781c22d61c9774c987a954fe6 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 09:27:04 -0300 Subject: [PATCH 03/21] Remove Anchor from Expr and handle Anchor separately in printer --- crates/plotnik-lib/src/parser/ast.rs | 103 +++++++----------- crates/plotnik-lib/src/query/alt_kind.rs | 6 +- crates/plotnik-lib/src/query/named_defs.rs | 4 +- crates/plotnik-lib/src/query/printer.rs | 37 +++++-- crates/plotnik-lib/src/query/ref_cycles.rs | 2 +- .../src/query/shape_cardinalities.rs | 1 - .../src/query/shape_cardinalities_tests.rs | 4 +- 7 files changed, 72 insertions(+), 85 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index 576cffc8..aa92917b 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -27,6 +27,31 @@ macro_rules! ast_node { }; } +macro_rules! define_expr { + ($($variant:ident),+ $(,)?) => { + /// Expression: any pattern that can appear in the tree. + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub enum Expr { + $($variant($variant)),+ + } + + impl Expr { + pub fn cast(node: SyntaxNode) -> Option { + $(if let Some(n) = $variant::cast(node.clone()) { return Some(Expr::$variant(n)); })+ + None + } + + pub fn as_cst(&self) -> &SyntaxNode { + match self { $(Expr::$variant(n) => n.as_cst()),+ } + } + + pub fn text_range(&self) -> TextRange { + match self { $(Expr::$variant(n) => n.text_range()),+ } + } + } + }; +} + ast_node!(Root, Root); ast_node!(Def, Def); ast_node!(Tree, Tree); @@ -54,72 +79,18 @@ pub enum AltKind { Mixed, } -/// Expression: any pattern that can appear in the tree. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Expr { - Tree(Tree), - Ref(Ref), - Str(Str), - Alt(Alt), - Seq(Seq), - Capture(Capture), - Quantifier(Quantifier), - Field(Field), - NegatedField(NegatedField), - Wildcard(Wildcard), - Anchor(Anchor), -} - -impl Expr { - pub fn cast(node: SyntaxNode) -> Option { - match node.kind() { - SyntaxKind::Tree => Tree::cast(node).map(Expr::Tree), - SyntaxKind::Ref => Ref::cast(node).map(Expr::Ref), - SyntaxKind::Str => Str::cast(node).map(Expr::Str), - SyntaxKind::Alt => Alt::cast(node).map(Expr::Alt), - SyntaxKind::Seq => Seq::cast(node).map(Expr::Seq), - SyntaxKind::Capture => Capture::cast(node).map(Expr::Capture), - SyntaxKind::Quantifier => Quantifier::cast(node).map(Expr::Quantifier), - SyntaxKind::Field => Field::cast(node).map(Expr::Field), - SyntaxKind::NegatedField => NegatedField::cast(node).map(Expr::NegatedField), - SyntaxKind::Wildcard => Wildcard::cast(node).map(Expr::Wildcard), - SyntaxKind::Anchor => Anchor::cast(node).map(Expr::Anchor), - _ => None, - } - } - - pub fn as_cst(&self) -> &SyntaxNode { - match self { - Expr::Tree(n) => n.as_cst(), - Expr::Ref(n) => n.as_cst(), - Expr::Str(n) => n.as_cst(), - Expr::Alt(n) => n.as_cst(), - Expr::Seq(n) => n.as_cst(), - Expr::Capture(n) => n.as_cst(), - Expr::Quantifier(n) => n.as_cst(), - Expr::Field(n) => n.as_cst(), - Expr::NegatedField(n) => n.as_cst(), - Expr::Wildcard(n) => n.as_cst(), - Expr::Anchor(n) => n.as_cst(), - } - } - - pub fn text_range(&self) -> TextRange { - match self { - Expr::Tree(n) => n.text_range(), - Expr::Ref(n) => n.text_range(), - Expr::Str(n) => n.text_range(), - Expr::Alt(n) => n.text_range(), - Expr::Seq(n) => n.text_range(), - Expr::Capture(n) => n.text_range(), - Expr::Quantifier(n) => n.text_range(), - Expr::Field(n) => n.text_range(), - Expr::NegatedField(n) => n.text_range(), - Expr::Wildcard(n) => n.text_range(), - Expr::Anchor(n) => n.text_range(), - } - } -} +define_expr!( + Tree, + Ref, + Str, + Alt, + Seq, + Capture, + Quantifier, + Field, + NegatedField, + Wildcard, +); impl Root { pub fn defs(&self) -> impl Iterator + '_ { diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index 96245d68..90cbdd08 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -62,11 +62,7 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { validate_expr(&value, errors); } } - Expr::Ref(_) - | Expr::Str(_) - | Expr::Wildcard(_) - | Expr::Anchor(_) - | Expr::NegatedField(_) => {} + Expr::Ref(_) | Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} } } diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs index 18ed6cfe..c48fcab3 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/named_defs.rs @@ -128,7 +128,7 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { let Some(value) = f.value() else { return }; collect_refs(&value, refs); } - Expr::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => {} + Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} } } @@ -174,7 +174,7 @@ fn collect_reference_diagnostics( let Some(value) = f.value() else { return }; collect_reference_diagnostics(&value, symbols, diagnostics); } - Expr::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => {} + Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} } } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 9bd35c98..1fee623f 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -210,9 +210,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { Some(ty) => writeln!(w, "{}Tree{}{} {}", prefix, card, span, ty)?, None => writeln!(w, "{}Tree{}{}", prefix, card, span)?, } - for child in t.children() { - self.format_expr(&child, indent + 1, w)?; - } + self.format_tree_children(t.as_cst(), indent + 1, w)?; } ast::Expr::Ref(r) => { let name = r.name().map(|t| t.text().to_string()).unwrap_or_default(); @@ -233,9 +231,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { } ast::Expr::Seq(s) => { writeln!(w, "{}Seq{}{}", prefix, card, span)?; - for child in s.children() { - self.format_expr(&child, indent + 1, w)?; - } + self.format_tree_children(s.as_cst(), indent + 1, w)?; } ast::Expr::Capture(c) => { let name = c.name().map(|t| t.text().to_string()).unwrap_or_default(); @@ -280,13 +276,38 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { ast::Expr::Wildcard(_) => { writeln!(w, "{}Wildcard{}{}", prefix, card, span)?; } - ast::Expr::Anchor(_) => { - writeln!(w, "{}Anchor{}{}", prefix, card, span)?; + } + Ok(()) + } + + fn format_tree_children( + &self, + node: &SyntaxNode, + indent: usize, + w: &mut impl Write, + ) -> std::fmt::Result { + use crate::parser::cst::SyntaxKind; + for child in node.children() { + if child.kind() == SyntaxKind::Anchor { + self.format_anchor(&ast::Anchor::cast(child).unwrap(), indent, w)?; + } else if let Some(expr) = ast::Expr::cast(child) { + self.format_expr(&expr, indent, w)?; } } Ok(()) } + fn format_anchor( + &self, + anchor: &ast::Anchor, + indent: usize, + w: &mut impl Write, + ) -> std::fmt::Result { + let prefix = " ".repeat(indent); + let span = self.span_str(anchor.text_range()); + writeln!(w, "{}Anchor{}", prefix, span) + } + fn format_branch( &self, branch: &ast::Branch, diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index c8bd8da8..b770885f 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -110,7 +110,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { Expr::Field(f) => f.value().map(|v| expr_has_escape(&v, scc)).unwrap_or(true), - Expr::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => true, + Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => true, } } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index 280daeed..d25828d5 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -103,7 +103,6 @@ fn compute_single( Expr::Tree(_) | Expr::Str(_) | Expr::Wildcard(_) - | Expr::Anchor(_) | Expr::Field(_) | Expr::NegatedField(_) | Expr::Alt(_) => ShapeCardinality::One, diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs index b2817621..14f3e21c 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs @@ -290,14 +290,14 @@ fn tagged_alt_shapes() { } #[test] -fn anchor_is_one() { +fn anchor_has_no_cardinality() { let query = Query::new("(block . (statement))").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ Tree¹ block - Anchor¹ + Anchor Tree¹ statement "); } From 54fc3419ede31d0a353e2c6436357ce29e4705bf Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 09:35:53 -0300 Subject: [PATCH 04/21] Refactor NegatedField to be a predicate, not an expression --- AGENTS.md | 2 +- crates/plotnik-lib/src/parser/ast.rs | 11 +---------- crates/plotnik-lib/src/query/alt_kind.rs | 2 +- crates/plotnik-lib/src/query/named_defs.rs | 4 ++-- crates/plotnik-lib/src/query/printer.rs | 18 ++++++++++++++---- crates/plotnik-lib/src/query/ref_cycles.rs | 2 +- .../src/query/shape_cardinalities.rs | 9 +++------ .../src/query/shape_cardinalities_tests.rs | 4 ++-- 8 files changed, 25 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b9d959cc..9a9991a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ Grammar: `(type)`, `[a b]` (alt), `{a b}` (seq), `_` (wildcard), `@name`, `::Typ SyntaxKind: `Root`, `Tree`, `Ref`, `Str`, `Field`, `Capture`, `Type`, `Quantifier`, `Seq`, `Alt`, `Branch`, `Wildcard`, `Anchor`, `NegatedField`, `Def` -Expr = `Tree | Ref | Str | Alt | Seq | Capture | Quantifier | Field | NegatedField | Wildcard | Anchor`. Quantifier/Capture wrap their target. +Expr = `Tree | Ref | Str | Alt | Seq | Capture | Quantifier | Field | Wildcard`. Quantifier/Capture wrap their target. `Anchor` and `NegatedField` are predicates (not expressions). ## Diagnostics diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index aa92917b..c6182cc2 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -80,16 +80,7 @@ pub enum AltKind { } define_expr!( - Tree, - Ref, - Str, - Alt, - Seq, - Capture, - Quantifier, - Field, - NegatedField, - Wildcard, + Tree, Ref, Str, Alt, Seq, Capture, Quantifier, Field, Wildcard, ); impl Root { diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index 90cbdd08..e39f24a0 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -62,7 +62,7 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { validate_expr(&value, errors); } } - Expr::Ref(_) | Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} + Expr::Ref(_) | Expr::Str(_) | Expr::Wildcard(_) => {} } } diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs index c48fcab3..d5287c1d 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/named_defs.rs @@ -128,7 +128,7 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { let Some(value) = f.value() else { return }; collect_refs(&value, refs); } - Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} + Expr::Str(_) | Expr::Wildcard(_) => {} } } @@ -174,7 +174,7 @@ fn collect_reference_diagnostics( let Some(value) = f.value() else { return }; collect_reference_diagnostics(&value, symbols, diagnostics); } - Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => {} + Expr::Str(_) | Expr::Wildcard(_) => {} } } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 1fee623f..d5f981b5 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -269,10 +269,6 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { }; self.format_expr(&value, indent + 1, w)?; } - ast::Expr::NegatedField(f) => { - let name = f.name().map(|t| t.text().to_string()).unwrap_or_default(); - writeln!(w, "{}NegatedField{}{} !{}", prefix, card, span, name)?; - } ast::Expr::Wildcard(_) => { writeln!(w, "{}Wildcard{}{}", prefix, card, span)?; } @@ -290,6 +286,8 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { for child in node.children() { if child.kind() == SyntaxKind::Anchor { self.format_anchor(&ast::Anchor::cast(child).unwrap(), indent, w)?; + } else if child.kind() == SyntaxKind::NegatedField { + self.format_negated_field(&ast::NegatedField::cast(child).unwrap(), indent, w)?; } else if let Some(expr) = ast::Expr::cast(child) { self.format_expr(&expr, indent, w)?; } @@ -308,6 +306,18 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { writeln!(w, "{}Anchor{}", prefix, span) } + fn format_negated_field( + &self, + nf: &ast::NegatedField, + indent: usize, + w: &mut impl Write, + ) -> std::fmt::Result { + let prefix = " ".repeat(indent); + let span = self.span_str(nf.text_range()); + let name = nf.name().map(|t| t.text().to_string()).unwrap_or_default(); + writeln!(w, "{}NegatedField{} !{}", prefix, span, name) + } + fn format_branch( &self, branch: &ast::Branch, diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index b770885f..4cffc641 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -110,7 +110,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { Expr::Field(f) => f.value().map(|v| expr_has_escape(&v, scc)).unwrap_or(true), - Expr::Str(_) | Expr::Wildcard(_) | Expr::NegatedField(_) => true, + Expr::Str(_) | Expr::Wildcard(_) => true, } } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index d25828d5..8aef31a6 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -100,12 +100,9 @@ fn compute_single( }; match expr { - Expr::Tree(_) - | Expr::Str(_) - | Expr::Wildcard(_) - | Expr::Field(_) - | Expr::NegatedField(_) - | Expr::Alt(_) => ShapeCardinality::One, + Expr::Tree(_) | Expr::Str(_) | Expr::Wildcard(_) | Expr::Field(_) | Expr::Alt(_) => { + ShapeCardinality::One + } Expr::Seq(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs index 14f3e21c..25dd33c9 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs @@ -303,14 +303,14 @@ fn anchor_has_no_cardinality() { } #[test] -fn negated_field_is_one() { +fn negated_field_has_no_cardinality() { let query = Query::new("(function !async)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ Tree¹ function - NegatedField¹ !async + NegatedField !async "); } From be18a027b55eb3846b8b25329f40cf40cafc4979 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 09:38:47 -0300 Subject: [PATCH 05/21] Anchor printing --- crates/plotnik-lib/src/parser/ast_tests.rs | 6 +++--- crates/plotnik-lib/src/query/printer.rs | 12 +++--------- crates/plotnik-lib/src/query/printer_tests.rs | 2 +- .../src/query/shape_cardinalities_tests.rs | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index cf974604..842835e1 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -205,13 +205,13 @@ fn quantifier_non_greedy() { fn anchor() { let query = Query::new("(block . (statement))").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def Tree block - Anchor + . Tree statement - "#); + "); } #[test] diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index d5f981b5..b466de0a 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -285,7 +285,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { use crate::parser::cst::SyntaxKind; for child in node.children() { if child.kind() == SyntaxKind::Anchor { - self.format_anchor(&ast::Anchor::cast(child).unwrap(), indent, w)?; + self.mark_anchor(indent, w)?; } else if child.kind() == SyntaxKind::NegatedField { self.format_negated_field(&ast::NegatedField::cast(child).unwrap(), indent, w)?; } else if let Some(expr) = ast::Expr::cast(child) { @@ -295,15 +295,9 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { Ok(()) } - fn format_anchor( - &self, - anchor: &ast::Anchor, - indent: usize, - w: &mut impl Write, - ) -> std::fmt::Result { + fn mark_anchor(&self, indent: usize, w: &mut impl Write) -> std::fmt::Result { let prefix = " ".repeat(indent); - let span = self.span_str(anchor.text_range()); - writeln!(w, "{}Anchor{}", prefix, span) + writeln!(w, "{}.", prefix) } fn format_negated_field( diff --git a/crates/plotnik-lib/src/query/printer_tests.rs b/crates/plotnik-lib/src/query/printer_tests.rs index 3ebd0ff3..32914184 100644 --- a/crates/plotnik-lib/src/query/printer_tests.rs +++ b/crates/plotnik-lib/src/query/printer_tests.rs @@ -116,7 +116,7 @@ fn printer_wildcard_and_anchor() { Def Tree call Wildcard - Anchor + . Tree arg "); } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs index 25dd33c9..932f1416 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs @@ -297,7 +297,7 @@ fn anchor_has_no_cardinality() { Root¹ Def¹ Tree¹ block - Anchor + . Tree¹ statement "); } From a584aa52678abc5bb14dad8f534683280608ff40 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:05:56 -0300 Subject: [PATCH 06/21] Restructure AST --- crates/plotnik-lib/src/parser/ast.rs | 78 ++++++--- crates/plotnik-lib/src/parser/ast_tests.rs | 156 +++++++++--------- crates/plotnik-lib/src/parser/mod.rs | 4 +- crates/plotnik-lib/src/query/alt_kind.rs | 12 +- .../plotnik-lib/src/query/alt_kind_tests.rs | 10 +- crates/plotnik-lib/src/query/named_defs.rs | 24 +-- crates/plotnik-lib/src/query/printer.rs | 49 +++--- crates/plotnik-lib/src/query/printer_tests.rs | 72 ++++---- crates/plotnik-lib/src/query/ref_cycles.rs | 20 +-- .../src/query/shape_cardinalities.rs | 10 +- .../src/query/shape_cardinalities_tests.rs | 110 ++++++------ 11 files changed, 295 insertions(+), 250 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index c6182cc2..45a2651d 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -54,20 +54,53 @@ macro_rules! define_expr { ast_node!(Root, Root); ast_node!(Def, Def); -ast_node!(Tree, Tree); +ast_node!(NamedNode, Tree); ast_node!(Ref, Ref); -ast_node!(Str, Str); ast_node!(Alt, Alt); ast_node!(Branch, Branch); ast_node!(Seq, Seq); -ast_node!(Capture, Capture); +ast_node!(CapturedExpr, Capture); ast_node!(Type, Type); -ast_node!(Quantifier, Quantifier); -ast_node!(Field, Field); +ast_node!(QuantifiedExpr, Quantifier); +ast_node!(FieldExpr, Field); ast_node!(NegatedField, NegatedField); -ast_node!(Wildcard, Wildcard); ast_node!(Anchor, Anchor); +/// Anonymous node: string literal (`"+"`) or wildcard (`_`). +/// Maps from CST `Str` or `Wildcard`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AnonymousNode(SyntaxNode); + +impl AnonymousNode { + pub fn cast(node: SyntaxNode) -> Option { + matches!(node.kind(), SyntaxKind::Str | SyntaxKind::Wildcard).then(|| Self(node)) + } + + pub fn as_cst(&self) -> &SyntaxNode { + &self.0 + } + + pub fn text_range(&self) -> TextRange { + self.0.text_range() + } + + /// Returns the string value if this is a literal, `None` if wildcard. + pub fn value(&self) -> Option { + if self.0.kind() == SyntaxKind::Wildcard { + return None; + } + self.0 + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|t| t.kind() == SyntaxKind::StrVal) + } + + /// Returns true if this is the "any" wildcard (`_`). + pub fn is_any(&self) -> bool { + self.0.kind() == SyntaxKind::Wildcard + } +} + /// Whether an alternation uses tagged or untagged branches. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AltKind { @@ -80,7 +113,14 @@ pub enum AltKind { } define_expr!( - Tree, Ref, Str, Alt, Seq, Capture, Quantifier, Field, Wildcard, + NamedNode, + Ref, + AnonymousNode, + Alt, + Seq, + CapturedExpr, + QuantifiedExpr, + FieldExpr, ); impl Root { @@ -106,7 +146,7 @@ impl Def { } } -impl Tree { +impl NamedNode { pub fn node_type(&self) -> Option { self.0 .children_with_tokens() @@ -122,6 +162,13 @@ impl Tree { }) } + /// Returns true if the node type is wildcard (`_`), matching any named node. + pub fn is_any(&self) -> bool { + self.node_type() + .map(|t| t.kind() == SyntaxKind::Underscore) + .unwrap_or(false) + } + pub fn children(&self) -> impl Iterator + '_ { self.0.children().filter_map(Expr::cast) } @@ -189,7 +236,7 @@ impl Seq { } } -impl Capture { +impl CapturedExpr { pub fn name(&self) -> Option { self.0 .children_with_tokens() @@ -215,7 +262,7 @@ impl Type { } } -impl Quantifier { +impl QuantifiedExpr { pub fn inner(&self) -> Option { self.0.children().find_map(Expr::cast) } @@ -238,7 +285,7 @@ impl Quantifier { } } -impl Field { +impl FieldExpr { pub fn name(&self) -> Option { self.0 .children_with_tokens() @@ -259,12 +306,3 @@ impl NegatedField { .find(|t| t.kind() == SyntaxKind::Id) } } - -impl Str { - pub fn value(&self) -> Option { - self.0 - .children_with_tokens() - .filter_map(|it| it.into_token()) - .find(|t| t.kind() == SyntaxKind::StrVal) - } -} diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index 842835e1..f9252285 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -5,11 +5,11 @@ use indoc::indoc; fn simple_tree() { let query = Query::new("(identifier)").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree identifier - "#); + NamedNode identifier + "); } #[test] @@ -20,24 +20,24 @@ fn nested_tree() { let query = Query::new(input).unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree function_definition - Field name: - Tree identifier - "#); + NamedNode function_definition + FieldExpr name: + NamedNode identifier + "); } #[test] fn wildcard() { let query = Query::new("(_)").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree _ - "#); + NamedNode (any) + "); } #[test] @@ -47,7 +47,7 @@ fn literal() { insta::assert_snapshot!(query.dump_ast(), @r#" Root Def - Str "if" + AnonymousNode "if" "#); } @@ -55,35 +55,35 @@ fn literal() { fn capture() { let query = Query::new("(identifier) @name").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Capture @name - Tree identifier - "#); + CapturedExpr @name + NamedNode identifier + "); } #[test] fn capture_with_type() { let query = Query::new("(identifier) @name :: string").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Capture @name :: string - Tree identifier - "#); + CapturedExpr @name :: string + NamedNode identifier + "); } #[test] fn named_definition() { let query = Query::new("Expr = (expression)").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def Expr - Tree expression - "#); + NamedNode expression + "); } #[test] @@ -95,14 +95,14 @@ fn reference() { let query = Query::new(input).unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def Expr - Tree identifier + NamedNode identifier Def - Tree call + NamedNode call Ref Expr - "#); + "); } #[test] @@ -114,9 +114,9 @@ fn alternation_unlabeled() { Def Alt Branch - Tree identifier + NamedNode identifier Branch - Tree number + NamedNode number "); } @@ -133,9 +133,9 @@ fn alternation_tagged() { Def Alt Branch Ident: - Tree identifier + NamedNode identifier Branch Num: - Tree number + NamedNode number "); } @@ -143,62 +143,62 @@ fn alternation_tagged() { fn sequence() { let query = Query::new("{(a) (b) (c)}").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def Seq - Tree a - Tree b - Tree c - "#); + NamedNode a + NamedNode b + NamedNode c + "); } #[test] fn quantifier_star() { let query = Query::new("(statement)*").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Quantifier * - Tree statement - "#); + QuantifiedExpr * + NamedNode statement + "); } #[test] fn quantifier_plus() { let query = Query::new("(statement)+").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Quantifier + - Tree statement - "#); + QuantifiedExpr + + NamedNode statement + "); } #[test] fn quantifier_optional() { let query = Query::new("(statement)?").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Quantifier ? - Tree statement - "#); + QuantifiedExpr ? + NamedNode statement + "); } #[test] fn quantifier_non_greedy() { let query = Query::new("(statement)*?").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Quantifier *? - Tree statement - "#); + QuantifiedExpr *? + NamedNode statement + "); } #[test] @@ -208,9 +208,9 @@ fn anchor() { insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree block + NamedNode block . - Tree statement + NamedNode statement "); } @@ -218,12 +218,12 @@ fn anchor() { fn negated_field() { let query = Query::new("(function !async)").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree function + NamedNode function NegatedField !async - "#); + "); } #[test] @@ -244,15 +244,15 @@ fn complex_example() { Def Expression Alt Branch Ident: - Capture @name :: string - Tree identifier + CapturedExpr @name :: string + NamedNode identifier Branch Binary: - Tree binary_expression - Capture @left - Field left: + NamedNode binary_expression + CapturedExpr @left + FieldExpr left: Ref Expression - Capture @right - Field right: + CapturedExpr @right + FieldExpr right: Ref Expression "); } @@ -273,11 +273,11 @@ fn ast_with_errors() { fn supertype() { let query = Query::new("(expression/binary_expression)").unwrap(); assert!(query.is_valid()); - insta::assert_snapshot!(query.dump_ast(), @r#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree expression - "#); + NamedNode expression + "); } #[test] @@ -294,16 +294,16 @@ fn multiple_fields() { insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Capture @expr - Tree binary_expression - Capture @left - Field left: - Tree _ - Capture @op - Field operator: - Wildcard - Capture @right - Field right: - Tree _ + CapturedExpr @expr + NamedNode binary_expression + CapturedExpr @left + FieldExpr left: + NamedNode (any) + CapturedExpr @op + FieldExpr operator: + AnonymousNode (any) + CapturedExpr @right + FieldExpr right: + NamedNode (any) "); } diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index a74a1d45..44d6e348 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -43,8 +43,8 @@ pub use cst::{SyntaxKind, SyntaxNode, SyntaxToken}; // Re-exports from ast (was nodes) pub use ast::{ - Alt, AltKind, Anchor, Branch, Capture, Def, Expr, Field, NegatedField, Quantifier, Ref, Root, - Seq, Str, Tree, Type, Wildcard, + Alt, AltKind, Anchor, AnonymousNode, Branch, CapturedExpr, Def, Expr, FieldExpr, NamedNode, + NegatedField, QuantifiedExpr, Ref, Root, Seq, Type, }; pub use core::Parser; diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index e39f24a0..efeb3782 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -37,8 +37,8 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { } assert_alt_no_bare_exprs(alt); } - Expr::Tree(tree) => { - for child in tree.children() { + Expr::NamedNode(node) => { + for child in node.children() { validate_expr(&child, errors); } } @@ -47,22 +47,22 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { validate_expr(&child, errors); } } - Expr::Capture(cap) => { + Expr::CapturedExpr(cap) => { if let Some(inner) = cap.inner() { validate_expr(&inner, errors); } } - Expr::Quantifier(q) => { + Expr::QuantifiedExpr(q) => { if let Some(inner) = q.inner() { validate_expr(&inner, errors); } } - Expr::Field(f) => { + Expr::FieldExpr(f) => { if let Some(value) = f.value() { validate_expr(&value, errors); } } - Expr::Ref(_) | Expr::Str(_) | Expr::Wildcard(_) => {} + Expr::Ref(_) | Expr::AnonymousNode(_) => {} } } diff --git a/crates/plotnik-lib/src/query/alt_kind_tests.rs b/crates/plotnik-lib/src/query/alt_kind_tests.rs index 4d1e7c95..c4d80556 100644 --- a/crates/plotnik-lib/src/query/alt_kind_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kind_tests.rs @@ -9,9 +9,9 @@ fn tagged_alternation_valid() { Def Alt Branch A: - Tree a + NamedNode a Branch B: - Tree b + NamedNode b "); } @@ -24,9 +24,9 @@ fn untagged_alternation_valid() { Def Alt Branch - Tree a + NamedNode a Branch - Tree b + NamedNode b "); } @@ -109,6 +109,6 @@ fn single_branch_no_error() { Def Alt Branch A: - Tree a + NamedNode a "); } diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs index d5287c1d..93a0ac7b 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/named_defs.rs @@ -95,8 +95,8 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { let Some(name_token) = r.name() else { return }; refs.insert(name_token.text().to_string()); } - Expr::Tree(tree) => { - for child in tree.children() { + Expr::NamedNode(node) => { + for child in node.children() { collect_refs(&child, refs); } } @@ -116,19 +116,19 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { collect_refs(&child, refs); } } - Expr::Capture(cap) => { + Expr::CapturedExpr(cap) => { let Some(inner) = cap.inner() else { return }; collect_refs(&inner, refs); } - Expr::Quantifier(q) => { + Expr::QuantifiedExpr(q) => { let Some(inner) = q.inner() else { return }; collect_refs(&inner, refs); } - Expr::Field(f) => { + Expr::FieldExpr(f) => { let Some(value) = f.value() else { return }; collect_refs(&value, refs); } - Expr::Str(_) | Expr::Wildcard(_) => {} + Expr::AnonymousNode(_) => {} } } @@ -141,8 +141,8 @@ fn collect_reference_diagnostics( Expr::Ref(r) => { check_ref_diagnostic(r, symbols, diagnostics); } - Expr::Tree(tree) => { - for child in tree.children() { + Expr::NamedNode(node) => { + for child in node.children() { collect_reference_diagnostics(&child, symbols, diagnostics); } } @@ -162,19 +162,19 @@ fn collect_reference_diagnostics( collect_reference_diagnostics(&child, symbols, diagnostics); } } - Expr::Capture(cap) => { + Expr::CapturedExpr(cap) => { let Some(inner) = cap.inner() else { return }; collect_reference_diagnostics(&inner, symbols, diagnostics); } - Expr::Quantifier(q) => { + Expr::QuantifiedExpr(q) => { let Some(inner) = q.inner() else { return }; collect_reference_diagnostics(&inner, symbols, diagnostics); } - Expr::Field(f) => { + Expr::FieldExpr(f) => { let Some(value) = f.value() else { return }; collect_reference_diagnostics(&value, symbols, diagnostics); } - Expr::Str(_) | Expr::Wildcard(_) => {} + Expr::AnonymousNode(_) => {} } } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index b466de0a..365f264a 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -204,21 +204,29 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { let span = self.span_str(expr.text_range()); match expr { - ast::Expr::Tree(t) => { - let node_type = t.node_type().map(|tok| tok.text().to_string()); - match node_type { - Some(ty) => writeln!(w, "{}Tree{}{} {}", prefix, card, span, ty)?, - None => writeln!(w, "{}Tree{}{}", prefix, card, span)?, + ast::Expr::NamedNode(n) => { + if n.is_any() { + writeln!(w, "{}NamedNode{}{} (any)", prefix, card, span)?; + } else { + let node_type = n.node_type().map(|tok| tok.text().to_string()); + match node_type { + Some(ty) => writeln!(w, "{}NamedNode{}{} {}", prefix, card, span, ty)?, + None => writeln!(w, "{}NamedNode{}{}", prefix, card, span)?, + } } - self.format_tree_children(t.as_cst(), indent + 1, w)?; + self.format_tree_children(n.as_cst(), indent + 1, w)?; } ast::Expr::Ref(r) => { let name = r.name().map(|t| t.text().to_string()).unwrap_or_default(); writeln!(w, "{}Ref{}{} {}", prefix, card, span, name)?; } - ast::Expr::Str(s) => { - let value = s.value().map(|t| t.text().to_string()).unwrap_or_default(); - writeln!(w, "{}Str{}{} \"{}\"", prefix, card, span, value)?; + ast::Expr::AnonymousNode(a) => { + if a.is_any() { + writeln!(w, "{}AnonymousNode{}{} (any)", prefix, card, span)?; + } else { + let value = a.value().map(|t| t.text().to_string()).unwrap_or_default(); + writeln!(w, "{}AnonymousNode{}{} \"{}\"", prefix, card, span, value)?; + } } ast::Expr::Alt(a) => { writeln!(w, "{}Alt{}{}", prefix, card, span)?; @@ -233,45 +241,44 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { writeln!(w, "{}Seq{}{}", prefix, card, span)?; self.format_tree_children(s.as_cst(), indent + 1, w)?; } - ast::Expr::Capture(c) => { + ast::Expr::CapturedExpr(c) => { let name = c.name().map(|t| t.text().to_string()).unwrap_or_default(); let type_ann = c .type_annotation() .and_then(|t| t.name()) .map(|t| t.text().to_string()); match type_ann { - Some(ty) => { - writeln!(w, "{}Capture{}{} @{} :: {}", prefix, card, span, name, ty)? - } - None => writeln!(w, "{}Capture{}{} @{}", prefix, card, span, name)?, + Some(ty) => writeln!( + w, + "{}CapturedExpr{}{} @{} :: {}", + prefix, card, span, name, ty + )?, + None => writeln!(w, "{}CapturedExpr{}{} @{}", prefix, card, span, name)?, } let Some(inner) = c.inner() else { return Ok(()); }; self.format_expr(&inner, indent + 1, w)?; } - ast::Expr::Quantifier(q) => { + ast::Expr::QuantifiedExpr(q) => { let op = q .operator() .map(|t| t.text().to_string()) .unwrap_or_default(); - writeln!(w, "{}Quantifier{}{} {}", prefix, card, span, op)?; + writeln!(w, "{}QuantifiedExpr{}{} {}", prefix, card, span, op)?; let Some(inner) = q.inner() else { return Ok(()); }; self.format_expr(&inner, indent + 1, w)?; } - ast::Expr::Field(f) => { + ast::Expr::FieldExpr(f) => { let name = f.name().map(|t| t.text().to_string()).unwrap_or_default(); - writeln!(w, "{}Field{}{} {}:", prefix, card, span, name)?; + writeln!(w, "{}FieldExpr{}{} {}:", prefix, card, span, name)?; let Some(value) = f.value() else { return Ok(()); }; self.format_expr(&value, indent + 1, w)?; } - ast::Expr::Wildcard(_) => { - writeln!(w, "{}Wildcard{}{}", prefix, card, span)?; - } } Ok(()) } diff --git a/crates/plotnik-lib/src/query/printer_tests.rs b/crates/plotnik-lib/src/query/printer_tests.rs index 32914184..df340681 100644 --- a/crates/plotnik-lib/src/query/printer_tests.rs +++ b/crates/plotnik-lib/src/query/printer_tests.rs @@ -7,7 +7,7 @@ fn printer_with_spans() { insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..6] Def [0..6] - Tree [0..6] call + NamedNode [0..6] call "); } @@ -17,7 +17,7 @@ fn printer_with_cardinalities() { insta::assert_snapshot!(q.printer().with_cardinalities(true).dump(), @r" Root¹ Def¹ - Tree¹ call + NamedNode¹ call "); } @@ -51,9 +51,9 @@ fn printer_alt_branches() { Def Alt Branch A: - Tree a + NamedNode a Branch B: - Tree b + NamedNode b "); } @@ -63,8 +63,8 @@ fn printer_capture_with_type() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def - Capture @x :: T - Tree call + CapturedExpr @x :: T + NamedNode call "); } @@ -74,14 +74,14 @@ fn printer_quantifiers() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def - Quantifier * - Tree a + QuantifiedExpr * + NamedNode a Def - Quantifier + - Tree b + QuantifiedExpr + + NamedNode b Def - Quantifier ? - Tree c + QuantifiedExpr ? + NamedNode c "); } @@ -91,9 +91,9 @@ fn printer_field() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def - Tree call - Field name: - Tree id + NamedNode call + FieldExpr name: + NamedNode id "); } @@ -103,7 +103,7 @@ fn printer_negated_field() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def - Tree call + NamedNode call NegatedField !name "); } @@ -114,10 +114,10 @@ fn printer_wildcard_and_anchor() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def - Tree call - Wildcard + NamedNode call + AnonymousNode (any) . - Tree arg + NamedNode arg "); } @@ -127,8 +127,8 @@ fn printer_string_literal() { insta::assert_snapshot!(q.printer().dump(), @r#" Root Def - Tree call - Str "foo" + NamedNode call + AnonymousNode "foo" "#); } @@ -142,9 +142,9 @@ fn printer_ref() { insta::assert_snapshot!(q.printer().dump(), @r" Root Def Expr - Tree call + NamedNode call Def - Tree func + NamedNode func Ref Expr "); } @@ -223,16 +223,16 @@ fn printer_spans_comprehensive() { insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..39] Def [0..28] Foo - Tree [6..28] call - Field [12..22] name: - Tree [18..22] id + NamedNode [6..28] call + FieldExpr [12..22] name: + NamedNode [18..22] id NegatedField [23..27] !bar Def [29..38] Alt [29..38] Branch [30..33] - Tree [30..33] a + NamedNode [30..33] a Branch [34..37] - Tree [34..37] b + NamedNode [34..37] b "); } @@ -243,8 +243,8 @@ fn printer_spans_seq() { Root [0..9] Def [0..9] Seq [0..9] - Tree [1..4] a - Tree [5..8] b + NamedNode [1..4] a + NamedNode [5..8] b "); } @@ -254,11 +254,11 @@ fn printer_spans_quantifiers() { insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..9] Def [0..4] - Quantifier [0..4] * - Tree [0..3] a + QuantifiedExpr [0..4] * + NamedNode [0..3] a Def [5..9] - Quantifier [5..9] + - Tree [5..8] b + QuantifiedExpr [5..9] + + NamedNode [5..8] b "); } @@ -270,8 +270,8 @@ fn printer_spans_alt_branches() { Def [0..15] Alt [0..15] Branch [1..7] A: - Tree [4..7] a + NamedNode [4..7] a Branch [8..14] B: - Tree [11..14] b + NamedNode [11..14] b "); } diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index 4cffc641..42a80e0a 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -74,8 +74,8 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { }; !scc.contains(name_token.text()) } - Expr::Tree(tree) => { - let children: Vec<_> = tree.children().collect(); + Expr::NamedNode(node) => { + let children: Vec<_> = node.children().collect(); children.is_empty() || children.iter().all(|c| expr_has_escape(c, scc)) } @@ -89,7 +89,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { Expr::Seq(seq) => seq.children().all(|c| expr_has_escape(&c, scc)), - Expr::Quantifier(q) => match q.operator().map(|op| op.kind()) { + Expr::QuantifiedExpr(q) => match q.operator().map(|op| op.kind()) { Some( SyntaxKind::Question | SyntaxKind::Star @@ -103,14 +103,14 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { _ => true, }, - Expr::Capture(cap) => cap + Expr::CapturedExpr(cap) => cap .inner() .map(|inner| expr_has_escape(&inner, scc)) .unwrap_or(true), - Expr::Field(f) => f.value().map(|v| expr_has_escape(&v, scc)).unwrap_or(true), + Expr::FieldExpr(f) => f.value().map(|v| expr_has_escape(&v, scc)).unwrap_or(true), - Expr::Str(_) | Expr::Wildcard(_) => true, + Expr::AnonymousNode(_) => true, } } @@ -214,7 +214,7 @@ fn find_ref_in_expr(expr: &Expr, target: &str) -> Option { None } } - Expr::Tree(tree) => tree + Expr::NamedNode(node) => node .children() .find_map(|child| find_ref_in_expr(&child, target)), Expr::Alt(alt) => alt @@ -222,11 +222,11 @@ fn find_ref_in_expr(expr: &Expr, target: &str) -> Option { .find_map(|b| b.body().and_then(|body| find_ref_in_expr(&body, target))) .or_else(|| alt.exprs().find_map(|e| find_ref_in_expr(&e, target))), Expr::Seq(seq) => seq.children().find_map(|c| find_ref_in_expr(&c, target)), - Expr::Capture(cap) => cap + Expr::CapturedExpr(cap) => cap .inner() .and_then(|inner| find_ref_in_expr(&inner, target)), - Expr::Quantifier(q) => q.inner().and_then(|inner| find_ref_in_expr(&inner, target)), - Expr::Field(f) => f.value().and_then(|v| find_ref_in_expr(&v, target)), + Expr::QuantifiedExpr(q) => q.inner().and_then(|inner| find_ref_in_expr(&inner, target)), + Expr::FieldExpr(f) => f.value().and_then(|v| find_ref_in_expr(&v, target)), _ => None, } } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index 8aef31a6..6c3988c4 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -16,7 +16,7 @@ use super::invariants::{ use super::named_defs::SymbolTable; use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Branch, Def, Expr, Field, Ref, Root, Seq, SyntaxNode, Type}; +use crate::parser::{Branch, Def, Expr, FieldExpr, Ref, Root, Seq, SyntaxNode, Type}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -100,18 +100,18 @@ fn compute_single( }; match expr { - Expr::Tree(_) | Expr::Str(_) | Expr::Wildcard(_) | Expr::Field(_) | Expr::Alt(_) => { + Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::Alt(_) => { ShapeCardinality::One } Expr::Seq(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), - Expr::Capture(ref cap) => { + Expr::CapturedExpr(ref cap) => { let inner = ensure_capture_has_inner(cap.inner()); get_or_compute(inner.as_cst(), symbols, def_bodies, cache) } - Expr::Quantifier(ref q) => { + Expr::QuantifiedExpr(ref q) => { let inner = ensure_quantifier_has_inner(q.inner()); get_or_compute(inner.as_cst(), symbols, def_bodies, cache) } @@ -174,7 +174,7 @@ fn validate_node( cardinalities: &HashMap, errors: &mut Diagnostics, ) { - if let Some(field) = Field::cast(node.clone()) + if let Some(field) = FieldExpr::cast(node.clone()) && let Some(value) = field.value() { let card = cardinalities diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs index 932f1416..61e6a387 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs @@ -8,7 +8,7 @@ fn tree_is_one() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ identifier + NamedNode¹ identifier "); } @@ -20,7 +20,7 @@ fn singleton_seq_is_one() { Root¹ Def¹ Seq¹ - Tree¹ identifier + NamedNode¹ identifier "); } @@ -34,7 +34,7 @@ fn nested_singleton_seq_is_one() { Seq¹ Seq¹ Seq¹ - Tree¹ identifier + NamedNode¹ identifier "); } @@ -46,8 +46,8 @@ fn multi_seq_is_many() { Root¹ Def⁺ Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b "); } @@ -60,9 +60,9 @@ fn alt_is_one() { Def¹ Alt¹ Branch¹ - Tree¹ a + NamedNode¹ a Branch¹ - Tree¹ b + NamedNode¹ b "); } @@ -79,10 +79,10 @@ fn alt_with_seq_branches() { Alt¹ Branch⁺ Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b Branch¹ - Tree¹ c + NamedNode¹ c "); } @@ -97,9 +97,9 @@ fn ref_to_tree_is_one() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ Def¹ X - Tree¹ identifier + NamedNode¹ identifier Def¹ - Tree¹ call + NamedNode¹ call Ref¹ X "); } @@ -116,10 +116,10 @@ fn ref_to_seq_is_many() { Root⁺ Def⁺ X Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b Def¹ - Tree¹ call + NamedNode¹ call Ref⁺ X "); } @@ -131,9 +131,9 @@ fn field_with_tree() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ call - Field¹ name: - Tree¹ identifier + NamedNode¹ call + FieldExpr¹ name: + NamedNode¹ identifier "); } @@ -144,13 +144,13 @@ fn field_with_alt() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ call - Field¹ name: + NamedNode¹ call + FieldExpr¹ name: Alt¹ Branch¹ - Tree¹ identifier + NamedNode¹ identifier Branch¹ - Tree¹ string + NamedNode¹ string "); } @@ -161,11 +161,11 @@ fn field_with_seq_error() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ call - Field¹ name: + NamedNode¹ call + FieldExpr¹ name: Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b "); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: field `name` value must match a single node, not a sequence @@ -187,11 +187,11 @@ fn field_with_ref_to_seq_error() { Root⁺ Def⁺ X Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b Def¹ - Tree¹ call - Field¹ name: + NamedNode¹ call + FieldExpr¹ name: Ref⁺ X "); insta::assert_snapshot!(query.dump_diagnostics(), @r" @@ -209,8 +209,8 @@ fn quantifier_preserves_inner_shape() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Quantifier¹ * - Tree¹ identifier + QuantifiedExpr¹ * + NamedNode¹ identifier "); } @@ -221,8 +221,8 @@ fn capture_preserves_inner_shape() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Capture¹ @name - Tree¹ identifier + CapturedExpr¹ @name + NamedNode¹ identifier "); } @@ -233,10 +233,10 @@ fn capture_on_seq() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def⁺ - Capture⁺ @items + CapturedExpr⁺ @items Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b "); } @@ -255,18 +255,18 @@ fn complex_nested_shapes() { Def¹ Stmt Alt¹ Branch¹ - Tree¹ expr_stmt + NamedNode¹ expr_stmt Branch¹ - Tree¹ return_stmt + NamedNode¹ return_stmt Def¹ - Tree¹ function_definition - Capture¹ @name - Field¹ name: - Tree¹ identifier - Field¹ body: - Tree¹ block - Capture¹ @stmts - Quantifier¹ * + NamedNode¹ function_definition + CapturedExpr¹ @name + FieldExpr¹ name: + NamedNode¹ identifier + FieldExpr¹ body: + NamedNode¹ block + CapturedExpr¹ @stmts + QuantifiedExpr¹ * Ref¹ Stmt "); } @@ -283,9 +283,9 @@ fn tagged_alt_shapes() { Def¹ Alt¹ Branch¹ Ident: - Tree¹ identifier + NamedNode¹ identifier Branch¹ Num: - Tree¹ number + NamedNode¹ number "); } @@ -296,9 +296,9 @@ fn anchor_has_no_cardinality() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ block + NamedNode¹ block . - Tree¹ statement + NamedNode¹ statement "); } @@ -309,7 +309,7 @@ fn negated_field_has_no_cardinality() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ function + NamedNode¹ function NegatedField !async "); } @@ -321,7 +321,7 @@ fn tree_with_wildcard_type() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ _ + NamedNode¹ (any) "); } @@ -332,7 +332,7 @@ fn bare_wildcard_is_one() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Wildcard¹ + AnonymousNode¹ (any) "); } @@ -354,7 +354,7 @@ fn literal_is_one() { insta::assert_snapshot!(query.dump_with_cardinalities(), @r#" Root¹ Def¹ - Str¹ "if" + AnonymousNode¹ "if" "#); } From 4dbaacd827f527b94691464cbc7ac1345ac0b904 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:08:41 -0300 Subject: [PATCH 07/21] Update AGENTS.md --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9a9991a2..c98760bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,14 +66,14 @@ Run: `cargo run -p plotnik-cli -- ` Inputs: `-q/--query `, `--query-file `, `--source `, `-s/--source-file `, `-l/--lang ` -Output: `--query`, `--source`, `--only-symbols`, `--cst`, `--raw`, `--spans`, `--cardinalities` +Output: `--show-query`, `--show-source`, `--only-symbols`, `--cst`, `--raw`, `--spans`, `--cardinalities` ```sh -cargo run -p plotnik-cli -- debug -q '(identifier) @id' --query +cargo run -p plotnik-cli -- debug -q '(identifier) @id' --show-query cargo run -p plotnik-cli -- debug -q '(identifier) @id' --only-symbols -cargo run -p plotnik-cli -- debug -s app.ts --source -cargo run -p plotnik-cli -- debug -s app.ts --source --raw -cargo run -p plotnik-cli -- debug -q '(function_declaration) @fn' -s app.ts -l typescript --query +cargo run -p plotnik-cli -- debug -s app.ts --show-source +cargo run -p plotnik-cli -- debug -s app.ts --show-source --raw +cargo run -p plotnik-cli -- debug -q '(function_declaration) @fn' -s app.ts -l typescript --show-query ``` ## Syntax From 8febe01828e4c250417984d87edbfdbd43e1e56d Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:11:47 -0300 Subject: [PATCH 08/21] Rename Alt and Seq AST types to AltExpr and SeqExpr --- crates/plotnik-lib/src/parser/ast.rs | 12 ++++++------ crates/plotnik-lib/src/parser/mod.rs | 4 ++-- crates/plotnik-lib/src/query/alt_kind.rs | 8 ++++---- crates/plotnik-lib/src/query/invariants.rs | 4 ++-- crates/plotnik-lib/src/query/named_defs.rs | 8 ++++---- crates/plotnik-lib/src/query/printer.rs | 4 ++-- crates/plotnik-lib/src/query/ref_cycles.rs | 8 ++++---- crates/plotnik-lib/src/query/shape_cardinalities.rs | 8 ++++---- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/plotnik-lib/src/parser/ast.rs b/crates/plotnik-lib/src/parser/ast.rs index 45a2651d..0a378354 100644 --- a/crates/plotnik-lib/src/parser/ast.rs +++ b/crates/plotnik-lib/src/parser/ast.rs @@ -56,9 +56,9 @@ ast_node!(Root, Root); ast_node!(Def, Def); ast_node!(NamedNode, Tree); ast_node!(Ref, Ref); -ast_node!(Alt, Alt); +ast_node!(AltExpr, Alt); ast_node!(Branch, Branch); -ast_node!(Seq, Seq); +ast_node!(SeqExpr, Seq); ast_node!(CapturedExpr, Capture); ast_node!(Type, Type); ast_node!(QuantifiedExpr, Quantifier); @@ -116,8 +116,8 @@ define_expr!( NamedNode, Ref, AnonymousNode, - Alt, - Seq, + AltExpr, + SeqExpr, CapturedExpr, QuantifiedExpr, FieldExpr, @@ -183,7 +183,7 @@ impl Ref { } } -impl Alt { +impl AltExpr { pub fn kind(&self) -> AltKind { let mut tagged = false; let mut untagged = false; @@ -230,7 +230,7 @@ impl Branch { } } -impl Seq { +impl SeqExpr { pub fn children(&self) -> impl Iterator + '_ { self.0.children().filter_map(Expr::cast) } diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index 44d6e348..ca365470 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -43,8 +43,8 @@ pub use cst::{SyntaxKind, SyntaxNode, SyntaxToken}; // Re-exports from ast (was nodes) pub use ast::{ - Alt, AltKind, Anchor, AnonymousNode, Branch, CapturedExpr, Def, Expr, FieldExpr, NamedNode, - NegatedField, QuantifiedExpr, Ref, Root, Seq, Type, + AltExpr, AltKind, Anchor, AnonymousNode, Branch, CapturedExpr, Def, Expr, FieldExpr, NamedNode, + NegatedField, QuantifiedExpr, Ref, Root, SeqExpr, Type, }; pub use core::Parser; diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index efeb3782..b85bdc13 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -10,7 +10,7 @@ use super::invariants::{ }; use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Alt, AltKind, Branch, Expr, Root}; +use crate::parser::{AltExpr, AltKind, Branch, Expr, Root}; pub fn validate(root: &Root) -> PassResult<()> { let mut errors = Diagnostics::new(); @@ -28,7 +28,7 @@ pub fn validate(root: &Root) -> PassResult<()> { fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { match expr { - Expr::Alt(alt) => { + Expr::AltExpr(alt) => { check_mixed_alternation(alt, errors); for branch in alt.branches() { if let Some(body) = branch.body() { @@ -42,7 +42,7 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { validate_expr(&child, errors); } } - Expr::Seq(seq) => { + Expr::SeqExpr(seq) => { for child in seq.children() { validate_expr(&child, errors); } @@ -66,7 +66,7 @@ fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { } } -fn check_mixed_alternation(alt: &Alt, errors: &mut Diagnostics) { +fn check_mixed_alternation(alt: &AltExpr, errors: &mut Diagnostics) { if alt.kind() != AltKind::Mixed { return; } diff --git a/crates/plotnik-lib/src/query/invariants.rs b/crates/plotnik-lib/src/query/invariants.rs index 3c47d051..f1db2cf8 100644 --- a/crates/plotnik-lib/src/query/invariants.rs +++ b/crates/plotnik-lib/src/query/invariants.rs @@ -2,7 +2,7 @@ #![cfg_attr(coverage_nightly, coverage(off))] -use crate::parser::{Alt, Branch, Root}; +use crate::parser::{AltExpr, Branch, Root}; #[inline] pub fn assert_root_no_bare_exprs(root: &Root) { @@ -13,7 +13,7 @@ pub fn assert_root_no_bare_exprs(root: &Root) { } #[inline] -pub fn assert_alt_no_bare_exprs(alt: &Alt) { +pub fn assert_alt_no_bare_exprs(alt: &AltExpr) { assert!( alt.exprs().next().is_none(), "alt_kind: unexpected bare Expr in Alt (parser should wrap in Branch)" diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs index 93a0ac7b..e2b12897 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/named_defs.rs @@ -100,7 +100,7 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { collect_refs(&child, refs); } } - Expr::Alt(alt) => { + Expr::AltExpr(alt) => { for branch in alt.branches() { let Some(body) = branch.body() else { continue }; collect_refs(&body, refs); @@ -111,7 +111,7 @@ fn collect_refs(expr: &Expr, refs: &mut IndexSet) { "named_defs: unexpected bare Expr in Alt (parser should wrap in Branch)" ); } - Expr::Seq(seq) => { + Expr::SeqExpr(seq) => { for child in seq.children() { collect_refs(&child, refs); } @@ -146,7 +146,7 @@ fn collect_reference_diagnostics( collect_reference_diagnostics(&child, symbols, diagnostics); } } - Expr::Alt(alt) => { + Expr::AltExpr(alt) => { for branch in alt.branches() { let Some(body) = branch.body() else { continue }; collect_reference_diagnostics(&body, symbols, diagnostics); @@ -157,7 +157,7 @@ fn collect_reference_diagnostics( "named_defs: unexpected bare Expr in Alt (parser should wrap in Branch)" ); } - Expr::Seq(seq) => { + Expr::SeqExpr(seq) => { for child in seq.children() { collect_reference_diagnostics(&child, symbols, diagnostics); } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 365f264a..a316ac02 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -228,7 +228,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { writeln!(w, "{}AnonymousNode{}{} \"{}\"", prefix, card, span, value)?; } } - ast::Expr::Alt(a) => { + ast::Expr::AltExpr(a) => { writeln!(w, "{}Alt{}{}", prefix, card, span)?; for branch in a.branches() { self.format_branch(&branch, indent + 1, w)?; @@ -237,7 +237,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { self.format_expr(&expr, indent + 1, w)?; } } - ast::Expr::Seq(s) => { + ast::Expr::SeqExpr(s) => { writeln!(w, "{}Seq{}{}", prefix, card, span)?; self.format_tree_children(s.as_cst(), indent + 1, w)?; } diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index 42a80e0a..df688073 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -79,7 +79,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { children.is_empty() || children.iter().all(|c| expr_has_escape(c, scc)) } - Expr::Alt(alt) => { + Expr::AltExpr(alt) => { alt.branches().any(|b| { b.body() .map(|body| expr_has_escape(&body, scc)) @@ -87,7 +87,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { }) || alt.exprs().any(|e| expr_has_escape(&e, scc)) } - Expr::Seq(seq) => seq.children().all(|c| expr_has_escape(&c, scc)), + Expr::SeqExpr(seq) => seq.children().all(|c| expr_has_escape(&c, scc)), Expr::QuantifiedExpr(q) => match q.operator().map(|op| op.kind()) { Some( @@ -217,11 +217,11 @@ fn find_ref_in_expr(expr: &Expr, target: &str) -> Option { Expr::NamedNode(node) => node .children() .find_map(|child| find_ref_in_expr(&child, target)), - Expr::Alt(alt) => alt + Expr::AltExpr(alt) => alt .branches() .find_map(|b| b.body().and_then(|body| find_ref_in_expr(&body, target))) .or_else(|| alt.exprs().find_map(|e| find_ref_in_expr(&e, target))), - Expr::Seq(seq) => seq.children().find_map(|c| find_ref_in_expr(&c, target)), + Expr::SeqExpr(seq) => seq.children().find_map(|c| find_ref_in_expr(&c, target)), Expr::CapturedExpr(cap) => cap .inner() .and_then(|inner| find_ref_in_expr(&inner, target)), diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index 6c3988c4..ac9bc1c7 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -16,7 +16,7 @@ use super::invariants::{ use super::named_defs::SymbolTable; use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Branch, Def, Expr, FieldExpr, Ref, Root, Seq, SyntaxNode, Type}; +use crate::parser::{Branch, Def, Expr, FieldExpr, Ref, Root, SeqExpr, SyntaxNode, Type}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -100,11 +100,11 @@ fn compute_single( }; match expr { - Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::Alt(_) => { + Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::AltExpr(_) => { ShapeCardinality::One } - Expr::Seq(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), + Expr::SeqExpr(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), Expr::CapturedExpr(ref cap) => { let inner = ensure_capture_has_inner(cap.inner()); @@ -135,7 +135,7 @@ fn get_or_compute( } fn seq_cardinality( - seq: &Seq, + seq: &SeqExpr, symbols: &SymbolTable, def_bodies: &HashMap, cache: &mut HashMap, From 9a022c09650f29913ab267304b4c268317f72a8b Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:24:18 -0300 Subject: [PATCH 09/21] Refactor SymbolTable to be more lightweight and efficient --- crates/plotnik-lib/src/query/mod.rs | 6 +- crates/plotnik-lib/src/query/named_defs.rs | 102 +++------------------ crates/plotnik-lib/src/query/printer.rs | 57 ++++++++++-- crates/plotnik-lib/src/query/ref_cycles.rs | 97 ++++++++++++++------ 4 files changed, 133 insertions(+), 129 deletions(-) diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index ef393c25..c7dc4b52 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -96,7 +96,7 @@ impl<'a> QueryBuilder<'a> { pub struct Query<'a> { source: &'a str, ast: Root, - symbols: SymbolTable, + symbols: SymbolTable<'a>, shape_cardinalities: HashMap, // Diagnostics per pass parse_diagnostics: Diagnostics, @@ -125,7 +125,7 @@ impl<'a> Query<'a> { alt_kind::validate(&ast).expect("alt_kind::validate is infallible"); let (symbols, resolve_diagnostics) = - named_defs::resolve(&ast).expect("named_defs::resolve is infallible"); + named_defs::resolve(&ast, source).expect("named_defs::resolve is infallible"); let ((), ref_cycle_diagnostics) = ref_cycles::validate(&ast, &symbols).expect("ref_cycles::validate is infallible"); @@ -159,7 +159,7 @@ impl<'a> Query<'a> { &self.ast } - pub fn symbols(&self) -> &SymbolTable { + pub fn symbols(&self) -> &SymbolTable<'a> { &self.symbols } diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs index e2b12897..fdb16759 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/named_defs.rs @@ -4,49 +4,16 @@ //! 1. Collect all `Name = expr` definitions //! 2. Check that all `(UpperIdent)` references are defined -use indexmap::{IndexMap, IndexSet}; -use rowan::TextRange; +use indexmap::IndexMap; use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Expr, Ref, Root}; +use crate::parser::{Expr, Ref, Root, ast}; -#[derive(Debug, Clone)] -pub struct SymbolTable { - defs: IndexMap, -} - -#[derive(Debug, Clone)] -pub struct DefInfo { - pub name: String, - pub range: TextRange, - pub refs: IndexSet, -} - -impl SymbolTable { - pub fn get(&self, name: &str) -> Option<&DefInfo> { - self.defs.get(name) - } - - pub fn names(&self) -> impl Iterator { - self.defs.keys().map(|s| s.as_str()) - } +pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>; - pub fn iter(&self) -> impl Iterator { - self.defs.values() - } - - pub fn len(&self) -> usize { - self.defs.len() - } - - pub fn is_empty(&self) -> bool { - self.defs.is_empty() - } -} - -pub fn resolve(root: &Root) -> PassResult { - let mut defs = IndexMap::new(); +pub fn resolve<'src>(root: &Root, source: &'src str) -> PassResult> { + let mut symbols: SymbolTable<'src> = IndexMap::new(); let mut diagnostics = Diagnostics::new(); // Pass 1: collect definitions @@ -55,25 +22,21 @@ pub fn resolve(root: &Root) -> PassResult { continue; }; - let name = name_token.text().to_string(); let range = name_token.text_range(); + let name = &source[range.start().into()..range.end().into()]; - if defs.contains_key(&name) { + if symbols.contains_key(name) { diagnostics .error(format!("duplicate definition: `{}`", name), range) .emit(); continue; } - let mut refs = IndexSet::new(); if let Some(body) = def.body() { - collect_refs(&body, &mut refs); + symbols.insert(name, body); } - defs.insert(name.clone(), DefInfo { name, range, refs }); } - let symbols = SymbolTable { defs }; - // Pass 2: check references for def in root.defs() { let Some(body) = def.body() else { continue }; @@ -89,52 +52,9 @@ pub fn resolve(root: &Root) -> PassResult { Ok((symbols, diagnostics)) } -fn collect_refs(expr: &Expr, refs: &mut IndexSet) { - match expr { - Expr::Ref(r) => { - let Some(name_token) = r.name() else { return }; - refs.insert(name_token.text().to_string()); - } - Expr::NamedNode(node) => { - for child in node.children() { - collect_refs(&child, refs); - } - } - Expr::AltExpr(alt) => { - for branch in alt.branches() { - let Some(body) = branch.body() else { continue }; - collect_refs(&body, refs); - } - // Parser wraps all alt children in Branch nodes - assert!( - alt.exprs().next().is_none(), - "named_defs: unexpected bare Expr in Alt (parser should wrap in Branch)" - ); - } - Expr::SeqExpr(seq) => { - for child in seq.children() { - collect_refs(&child, refs); - } - } - Expr::CapturedExpr(cap) => { - let Some(inner) = cap.inner() else { return }; - collect_refs(&inner, refs); - } - Expr::QuantifiedExpr(q) => { - let Some(inner) = q.inner() else { return }; - collect_refs(&inner, refs); - } - Expr::FieldExpr(f) => { - let Some(value) = f.value() else { return }; - collect_refs(&value, refs); - } - Expr::AnonymousNode(_) => {} - } -} - fn collect_reference_diagnostics( expr: &Expr, - symbols: &SymbolTable, + symbols: &SymbolTable<'_>, diagnostics: &mut Diagnostics, ) { match expr { @@ -178,11 +98,11 @@ fn collect_reference_diagnostics( } } -fn check_ref_diagnostic(r: &Ref, symbols: &SymbolTable, diagnostics: &mut Diagnostics) { +fn check_ref_diagnostic(r: &Ref, symbols: &SymbolTable<'_>, diagnostics: &mut Diagnostics) { let Some(name_token) = r.name() else { return }; let name = name_token.text(); - if symbols.get(name).is_some() { + if symbols.contains_key(name) { return; } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index a316ac02..b61b0ff5 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -1,8 +1,9 @@ use std::fmt::Write; +use indexmap::IndexSet; use rowan::NodeOrToken; -use crate::parser::{self as ast, SyntaxNode}; +use crate::parser::{self as ast, Expr, SyntaxNode}; use super::Query; use super::shape_cardinalities::ShapeCardinality; @@ -70,7 +71,6 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { } fn format_symbols(&self, w: &mut impl Write) -> std::fmt::Result { - use indexmap::IndexSet; use std::collections::HashMap; let symbols = &self.query.symbols; @@ -78,7 +78,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { return Ok(()); } - let defined: IndexSet<&str> = symbols.names().collect(); + let defined: IndexSet<&str> = symbols.keys().copied().collect(); let mut body_nodes: HashMap = HashMap::new(); for def in self.query.root().defs() { @@ -87,7 +87,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { } } - for name in symbols.names() { + for name in symbols.keys() { let mut visited = IndexSet::new(); self.format_symbol_tree(name, 0, &defined, &body_nodes, &mut visited, w)?; } @@ -123,8 +123,9 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { writeln!(w, "{}{}{}", prefix, name, card)?; visited.insert(name.to_string()); - if let Some(def) = self.query.symbols.get(name) { - let mut refs: Vec<_> = def.refs.iter().map(|s| s.as_str()).collect(); + if let Some(body) = self.query.symbols.get(name) { + let refs_set = collect_refs(body); + let mut refs: Vec<_> = refs_set.iter().map(|s| s.as_str()).collect(); refs.sort(); for r in refs { self.format_symbol_tree(r, indent + 1, defined, body_nodes, visited, w)?; @@ -369,3 +370,47 @@ impl Query<'_> { QueryPrinter::new(self) } } + +fn collect_refs(expr: &Expr) -> IndexSet { + let mut refs = IndexSet::new(); + collect_refs_into(expr, &mut refs); + refs +} + +fn collect_refs_into(expr: &Expr, refs: &mut IndexSet) { + match expr { + Expr::Ref(r) => { + let Some(name_token) = r.name() else { return }; + refs.insert(name_token.text().to_string()); + } + Expr::NamedNode(node) => { + for child in node.children() { + collect_refs_into(&child, refs); + } + } + Expr::AltExpr(alt) => { + for branch in alt.branches() { + let Some(body) = branch.body() else { continue }; + collect_refs_into(&body, refs); + } + } + Expr::SeqExpr(seq) => { + for child in seq.children() { + collect_refs_into(&child, refs); + } + } + Expr::CapturedExpr(cap) => { + let Some(inner) = cap.inner() else { return }; + collect_refs_into(&inner, refs); + } + Expr::QuantifiedExpr(q) => { + let Some(inner) = q.inner() else { return }; + collect_refs_into(&inner, refs); + } + Expr::FieldExpr(f) => { + let Some(value) = f.value() else { return }; + collect_refs_into(&value, refs); + } + Expr::AnonymousNode(_) => {} + } +} diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index df688073..1ee98d28 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -11,31 +11,24 @@ use crate::PassResult; use crate::diagnostics::Diagnostics; use crate::parser::{Def, Expr, Root, SyntaxKind}; -pub fn validate(root: &Root, symbols: &SymbolTable) -> PassResult<()> { +pub fn validate(root: &Root, symbols: &SymbolTable<'_>) -> PassResult<()> { let sccs = find_sccs(symbols); let mut errors = Diagnostics::new(); for scc in sccs { if scc.len() == 1 { let name = &scc[0]; - let Some(def_info) = symbols.get(name) else { + let Some(body) = symbols.get(name.as_str()) else { continue; }; - if !def_info.refs.contains(name) { + let refs = collect_refs(body); + if !refs.contains(name) { continue; } - let Some(def) = find_def_by_name(root, name) else { - continue; - }; - - let Some(body) = def.body() else { - continue; - }; - let scc_set: IndexSet<&str> = std::iter::once(name.as_str()).collect(); - if !expr_has_escape(&body, &scc_set) { + if !expr_has_escape(body, &scc_set) { let chain = build_self_ref_chain(root, name); emit_error(&mut errors, name, &scc, chain); } @@ -114,9 +107,53 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { } } -fn find_sccs(symbols: &SymbolTable) -> Vec> { +fn collect_refs(expr: &Expr) -> IndexSet { + let mut refs = IndexSet::new(); + collect_refs_into(expr, &mut refs); + refs +} + +fn collect_refs_into(expr: &Expr, refs: &mut IndexSet) { + match expr { + Expr::Ref(r) => { + let Some(name_token) = r.name() else { return }; + refs.insert(name_token.text().to_string()); + } + Expr::NamedNode(node) => { + for child in node.children() { + collect_refs_into(&child, refs); + } + } + Expr::AltExpr(alt) => { + for branch in alt.branches() { + let Some(body) = branch.body() else { continue }; + collect_refs_into(&body, refs); + } + } + Expr::SeqExpr(seq) => { + for child in seq.children() { + collect_refs_into(&child, refs); + } + } + Expr::CapturedExpr(cap) => { + let Some(inner) = cap.inner() else { return }; + collect_refs_into(&inner, refs); + } + Expr::QuantifiedExpr(q) => { + let Some(inner) = q.inner() else { return }; + collect_refs_into(&inner, refs); + } + Expr::FieldExpr(f) => { + let Some(value) = f.value() else { return }; + collect_refs_into(&value, refs); + } + Expr::AnonymousNode(_) => {} + } +} + +fn find_sccs(symbols: &SymbolTable<'_>) -> Vec> { struct State<'a> { - symbols: &'a SymbolTable, + symbols: &'a SymbolTable<'a>, index: usize, stack: Vec, on_stack: IndexSet, @@ -132,18 +169,19 @@ fn find_sccs(symbols: &SymbolTable) -> Vec> { state.stack.push(name.to_string()); state.on_stack.insert(name.to_string()); - if let Some(def_info) = state.symbols.get(name) { - for ref_name in &def_info.refs { - if state.symbols.get(ref_name).is_none() { + if let Some(body) = state.symbols.get(name) { + let refs = collect_refs(body); + for ref_name in &refs { + if state.symbols.get(ref_name.as_str()).is_none() { continue; } - if !state.indices.contains_key(ref_name) { + if !state.indices.contains_key(ref_name.as_str()) { strongconnect(ref_name, state); - let ref_lowlink = state.lowlinks[ref_name]; + let ref_lowlink = state.lowlinks[ref_name.as_str()]; let my_lowlink = state.lowlinks.get_mut(name).unwrap(); *my_lowlink = (*my_lowlink).min(ref_lowlink); - } else if state.on_stack.contains(ref_name) { - let ref_index = state.indices[ref_name]; + } else if state.on_stack.contains(ref_name.as_str()) { + let ref_index = state.indices[ref_name.as_str()]; let my_lowlink = state.lowlinks.get_mut(name).unwrap(); *my_lowlink = (*my_lowlink).min(ref_index); } @@ -174,8 +212,8 @@ fn find_sccs(symbols: &SymbolTable) -> Vec> { sccs: Vec::new(), }; - for name in symbols.names() { - if !state.indices.contains_key(name) { + for name in symbols.keys() { + if !state.indices.contains_key(*name) { strongconnect(name, &mut state); } } @@ -186,8 +224,8 @@ fn find_sccs(symbols: &SymbolTable) -> Vec> { .filter(|scc| { scc.len() > 1 || symbols - .get(&scc[0]) - .map(|d| d.refs.contains(&scc[0])) + .get(scc[0].as_str()) + .map(|body| collect_refs(body).contains(scc[0].as_str())) .unwrap_or(false) }) .collect() @@ -239,7 +277,7 @@ fn build_self_ref_chain(root: &Root, name: &str) -> Vec<(TextRange, String)> { fn build_cycle_chain( root: &Root, - symbols: &SymbolTable, + symbols: &SymbolTable<'_>, scc: &[String], ) -> Vec<(TextRange, String)> { let scc_set: IndexSet<&str> = scc.iter().map(|s| s.as_str()).collect(); @@ -251,7 +289,7 @@ fn build_cycle_chain( current: &str, start: &str, scc_set: &IndexSet<&str>, - symbols: &SymbolTable, + symbols: &SymbolTable<'_>, visited: &mut IndexSet, path: &mut Vec, ) -> bool { @@ -261,8 +299,9 @@ fn build_cycle_chain( visited.insert(current.to_string()); path.push(current.to_string()); - if let Some(def_info) = symbols.get(current) { - for ref_name in &def_info.refs { + if let Some(body) = symbols.get(current) { + let refs = collect_refs(body); + for ref_name in &refs { if scc_set.contains(ref_name.as_str()) && find_path(ref_name, start, scc_set, symbols, visited, path) { From 4e917b2f6d312abd306e0d55a93a31d7da332af0 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:28:26 -0300 Subject: [PATCH 10/21] Refactor collect_refs to use SyntaxNode traversal --- crates/plotnik-lib/src/query/printer.rs | 47 ++++--------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index b61b0ff5..4ebb028c 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -372,45 +372,10 @@ impl Query<'_> { } fn collect_refs(expr: &Expr) -> IndexSet { - let mut refs = IndexSet::new(); - collect_refs_into(expr, &mut refs); - refs -} - -fn collect_refs_into(expr: &Expr, refs: &mut IndexSet) { - match expr { - Expr::Ref(r) => { - let Some(name_token) = r.name() else { return }; - refs.insert(name_token.text().to_string()); - } - Expr::NamedNode(node) => { - for child in node.children() { - collect_refs_into(&child, refs); - } - } - Expr::AltExpr(alt) => { - for branch in alt.branches() { - let Some(body) = branch.body() else { continue }; - collect_refs_into(&body, refs); - } - } - Expr::SeqExpr(seq) => { - for child in seq.children() { - collect_refs_into(&child, refs); - } - } - Expr::CapturedExpr(cap) => { - let Some(inner) = cap.inner() else { return }; - collect_refs_into(&inner, refs); - } - Expr::QuantifiedExpr(q) => { - let Some(inner) = q.inner() else { return }; - collect_refs_into(&inner, refs); - } - Expr::FieldExpr(f) => { - let Some(value) = f.value() else { return }; - collect_refs_into(&value, refs); - } - Expr::AnonymousNode(_) => {} - } + expr.as_cst() + .descendants() + .filter_map(ast::Ref::cast) + .filter_map(|r| r.name()) + .map(|tok| tok.text().to_string()) + .collect() } From 0a6d68abd0910ea919c5bb91463df30043640a13 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:36:52 -0300 Subject: [PATCH 11/21] Rename `named_defs` module to `symbol_table` --- AGENTS.md | 4 +-- crates/plotnik-lib/src/query/mod.rs | 27 +++++++++---------- crates/plotnik-lib/src/query/mod_tests.rs | 1 - crates/plotnik-lib/src/query/printer.rs | 4 +-- crates/plotnik-lib/src/query/ref_cycles.rs | 2 +- .../src/query/shape_cardinalities.rs | 2 +- .../query/{named_defs.rs => symbol_table.rs} | 6 ++--- ...ed_defs_tests.rs => symbol_table_tests.rs} | 0 8 files changed, 21 insertions(+), 25 deletions(-) rename crates/plotnik-lib/src/query/{named_defs.rs => symbol_table.rs} (93%) rename crates/plotnik-lib/src/query/{named_defs_tests.rs => symbol_table_tests.rs} (100%) diff --git a/AGENTS.md b/AGENTS.md index c98760bb..a57a290a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ crates/ printer.rs # QueryPrinter for AST output invariants.rs # Query invariant checks alt_kind.rs # Alternation validation - named_defs.rs # Name resolution, symbol table + symbol_table.rs # Name resolution, symbol table ref_cycles.rs # Escape analysis (recursion validation) shape_cardinalities.rs # Shape inference *_tests.rs # Test files per module @@ -47,7 +47,7 @@ docs/ ```rust parser::parse() // Parse → CST alt_kind::validate() // Validate alternation kinds -named_defs::resolve() // Resolve names → SymbolTable +symbol_table::resolve() // Resolve names → SymbolTable ref_cycles::validate() // Validate recursion termination shape_cardinalities::analyze() // Infer and validate shape cardinalities ``` diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index c7dc4b52..56edacd1 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -6,22 +6,22 @@ mod printer; pub use printer::QueryPrinter; pub mod alt_kind; -pub mod named_defs; pub mod ref_cycles; pub mod shape_cardinalities; +pub mod symbol_table; #[cfg(test)] mod alt_kind_tests; #[cfg(test)] mod mod_tests; #[cfg(test)] -mod named_defs_tests; -#[cfg(test)] mod printer_tests; #[cfg(test)] mod ref_cycles_tests; #[cfg(test)] mod shape_cardinalities_tests; +#[cfg(test)] +mod symbol_table_tests; use std::collections::HashMap; @@ -29,8 +29,8 @@ use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::lexer::lex; use crate::parser::{self, Parser, Root, SyntaxNode}; -use named_defs::SymbolTable; use shape_cardinalities::ShapeCardinality; +use symbol_table::SymbolTable; /// Builder for configuring and creating a [`Query`]. pub struct QueryBuilder<'a> { @@ -96,7 +96,7 @@ impl<'a> QueryBuilder<'a> { pub struct Query<'a> { source: &'a str, ast: Root, - symbols: SymbolTable<'a>, + symbol_table: SymbolTable<'a>, shape_cardinalities: HashMap, // Diagnostics per pass parse_diagnostics: Diagnostics, @@ -124,19 +124,20 @@ impl<'a> Query<'a> { let ((), alt_kind_diagnostics) = alt_kind::validate(&ast).expect("alt_kind::validate is infallible"); - let (symbols, resolve_diagnostics) = - named_defs::resolve(&ast, source).expect("named_defs::resolve is infallible"); + let (symbol_table, resolve_diagnostics) = + symbol_table::resolve(&ast, source).expect("symbol_table::resolve is infallible"); let ((), ref_cycle_diagnostics) = - ref_cycles::validate(&ast, &symbols).expect("ref_cycles::validate is infallible"); + ref_cycles::validate(&ast, &symbol_table).expect("ref_cycles::validate is infallible"); - let (shape_cardinalities, shape_diagnostics) = shape_cardinalities::analyze(&ast, &symbols) - .expect("shape_cardinalities::analyze is infallible"); + let (shape_cardinalities, shape_diagnostics) = + shape_cardinalities::analyze(&ast, &symbol_table) + .expect("shape_cardinalities::analyze is infallible"); Self { source, ast, - symbols, + symbol_table, shape_cardinalities, parse_diagnostics, alt_kind_diagnostics, @@ -159,10 +160,6 @@ impl<'a> Query<'a> { &self.ast } - pub fn symbols(&self) -> &SymbolTable<'a> { - &self.symbols - } - pub fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { self.shape_cardinalities .get(node) diff --git a/crates/plotnik-lib/src/query/mod_tests.rs b/crates/plotnik-lib/src/query/mod_tests.rs index 56ce056c..0248a083 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -4,7 +4,6 @@ use super::*; fn valid_query() { let q = Query::new("Expr = (expression)").unwrap(); assert!(q.is_valid()); - assert!(q.symbols().get("Expr").is_some()); } #[test] diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 4ebb028c..5f4a35b6 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -73,7 +73,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { fn format_symbols(&self, w: &mut impl Write) -> std::fmt::Result { use std::collections::HashMap; - let symbols = &self.query.symbols; + let symbols = &self.query.symbol_table; if symbols.is_empty() { return Ok(()); } @@ -123,7 +123,7 @@ impl<'q, 'src> QueryPrinter<'q, 'src> { writeln!(w, "{}{}{}", prefix, name, card)?; visited.insert(name.to_string()); - if let Some(body) = self.query.symbols.get(name) { + if let Some(body) = self.query.symbol_table.get(name) { let refs_set = collect_refs(body); let mut refs: Vec<_> = refs_set.iter().map(|s| s.as_str()).collect(); refs.sort(); diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index 1ee98d28..f712e48a 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -6,7 +6,7 @@ use indexmap::{IndexMap, IndexSet}; use rowan::TextRange; -use super::named_defs::SymbolTable; +use super::symbol_table::SymbolTable; use crate::PassResult; use crate::diagnostics::Diagnostics; use crate::parser::{Def, Expr, Root, SyntaxKind}; diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index ac9bc1c7..87a8cde6 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -13,7 +13,7 @@ use super::invariants::{ ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name, }; -use super::named_defs::SymbolTable; +use super::symbol_table::SymbolTable; use crate::PassResult; use crate::diagnostics::Diagnostics; use crate::parser::{Branch, Def, Expr, FieldExpr, Ref, Root, SeqExpr, SyntaxNode, Type}; diff --git a/crates/plotnik-lib/src/query/named_defs.rs b/crates/plotnik-lib/src/query/symbol_table.rs similarity index 93% rename from crates/plotnik-lib/src/query/named_defs.rs rename to crates/plotnik-lib/src/query/symbol_table.rs index fdb16759..58e9a9b8 100644 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -1,4 +1,4 @@ -//! Name resolution: builds symbol table and checks references. +//! Symbol table: name resolution and reference checking. //! //! Two-pass approach: //! 1. Collect all `Name = expr` definitions @@ -46,7 +46,7 @@ pub fn resolve<'src>(root: &Root, source: &'src str) -> PassResult { diff --git a/crates/plotnik-lib/src/query/named_defs_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs similarity index 100% rename from crates/plotnik-lib/src/query/named_defs_tests.rs rename to crates/plotnik-lib/src/query/symbol_table_tests.rs From dceded4ba7f119b100e64f71807954654b2d23a7 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:48:46 -0300 Subject: [PATCH 12/21] Refactor shape cardinality to work with AST nodes --- crates/plotnik-lib/src/query/mod.rs | 43 ++++++- .../src/query/shape_cardinalities.rs | 115 ++++++------------ 2 files changed, 77 insertions(+), 81 deletions(-) diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 56edacd1..b6b76c9d 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -27,8 +27,9 @@ use std::collections::HashMap; use crate::Result; use crate::diagnostics::Diagnostics; +use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; -use crate::parser::{self, Parser, Root, SyntaxNode}; +use crate::parser::{self, Parser, Root, SyntaxNode, ast}; use shape_cardinalities::ShapeCardinality; use symbol_table::SymbolTable; @@ -97,7 +98,7 @@ pub struct Query<'a> { source: &'a str, ast: Root, symbol_table: SymbolTable<'a>, - shape_cardinalities: HashMap, + shape_cardinality_table: HashMap, // Diagnostics per pass parse_diagnostics: Diagnostics, alt_kind_diagnostics: Diagnostics, @@ -138,7 +139,7 @@ impl<'a> Query<'a> { source, ast, symbol_table, - shape_cardinalities, + shape_cardinality_table: shape_cardinalities, parse_diagnostics, alt_kind_diagnostics, resolve_diagnostics, @@ -161,9 +162,39 @@ impl<'a> Query<'a> { } pub fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { - self.shape_cardinalities - .get(node) - .copied() + // Error nodes are invalid + if node.kind() == SyntaxKind::Error { + return ShapeCardinality::Invalid; + } + + // Root: cardinality based on definition count + if let Some(root) = Root::cast(node.clone()) { + return if root.defs().count() > 1 { + ShapeCardinality::Many + } else { + ShapeCardinality::One + }; + } + + // Def: delegate to body's cardinality + if let Some(def) = ast::Def::cast(node.clone()) { + return def + .body() + .and_then(|b| self.shape_cardinality_table.get(&b).copied()) + .unwrap_or(ShapeCardinality::Invalid); + } + + // Branch: delegate to body's cardinality + if let Some(branch) = ast::Branch::cast(node.clone()) { + return branch + .body() + .and_then(|b| self.shape_cardinality_table.get(&b).copied()) + .unwrap_or(ShapeCardinality::Invalid); + } + + // Expr: direct lookup + ast::Expr::cast(node.clone()) + .and_then(|e| self.shape_cardinality_table.get(&e).copied()) .unwrap_or(ShapeCardinality::One) } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index 87a8cde6..e5214e32 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -6,9 +6,6 @@ //! //! `Invalid` marks nodes where cardinality cannot be determined (error nodes, //! undefined refs, etc.). -//! -//! Root node cardinality indicates definition count (one vs multiple subqueries), -//! not node matching semantics. use super::invariants::{ ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name, @@ -16,7 +13,7 @@ use super::invariants::{ use super::symbol_table::SymbolTable; use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Branch, Def, Expr, FieldExpr, Ref, Root, SeqExpr, SyntaxNode, Type}; +use crate::parser::{Expr, FieldExpr, Ref, Root, SeqExpr, SyntaxNode, ast}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -29,122 +26,90 @@ pub enum ShapeCardinality { pub fn analyze( root: &Root, symbols: &SymbolTable, -) -> PassResult> { - let mut result = HashMap::new(); +) -> PassResult> { + let mut cache = HashMap::new(); let mut errors = Diagnostics::new(); - let mut def_bodies: HashMap = HashMap::new(); + let mut def_bodies: HashMap = HashMap::new(); for def in root.defs() { if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { - def_bodies.insert(name_tok.text().to_string(), body.as_cst().clone()); + def_bodies.insert(name_tok.text().to_string(), body); } } - compute_node_cardinality(&root.as_cst().clone(), symbols, &def_bodies, &mut result); - validate_node(&root.as_cst().clone(), &result, &mut errors); + compute_all_cardinalities(root.as_cst(), symbols, &def_bodies, &mut cache); + validate_node(root.as_cst(), &cache, &mut errors); - Ok((result, errors)) + Ok((cache, errors)) } -fn compute_node_cardinality( +fn compute_all_cardinalities( node: &SyntaxNode, symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - let card = get_or_compute(node, symbols, def_bodies, cache); + def_bodies: &HashMap, + cache: &mut HashMap, +) { + if let Some(expr) = Expr::cast(node.clone()) { + get_or_compute(&expr, symbols, def_bodies, cache); + } for child in node.children() { - compute_node_cardinality(&child, symbols, def_bodies, cache); + compute_all_cardinalities(&child, symbols, def_bodies, cache); } - - card } fn compute_single( - node: &SyntaxNode, + expr: &Expr, symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, + def_bodies: &HashMap, + cache: &mut HashMap, ) -> ShapeCardinality { - if let Some(root) = Root::cast(node.clone()) { - return if root.defs().count() > 1 { - ShapeCardinality::Many - } else { - ShapeCardinality::One - }; - } - - if let Some(def) = Def::cast(node.clone()) { - return def - .body() - .map(|b| get_or_compute(b.as_cst(), symbols, def_bodies, cache)) - .unwrap_or(ShapeCardinality::Invalid); - } - - if let Some(branch) = Branch::cast(node.clone()) { - return branch - .body() - .map(|b| get_or_compute(b.as_cst(), symbols, def_bodies, cache)) - .unwrap_or(ShapeCardinality::Invalid); - } - - // Type annotations are metadata, not matching expressions - if Type::cast(node.clone()).is_some() { - return ShapeCardinality::One; - } - - // Error nodes and other non-Expr nodes: mark as Invalid - let Some(expr) = Expr::cast(node.clone()) else { - return ShapeCardinality::Invalid; - }; - match expr { Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::AltExpr(_) => { ShapeCardinality::One } - Expr::SeqExpr(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), + Expr::SeqExpr(seq) => seq_cardinality(seq, symbols, def_bodies, cache), - Expr::CapturedExpr(ref cap) => { + Expr::CapturedExpr(cap) => { let inner = ensure_capture_has_inner(cap.inner()); - get_or_compute(inner.as_cst(), symbols, def_bodies, cache) + get_or_compute(&inner, symbols, def_bodies, cache) } - Expr::QuantifiedExpr(ref q) => { + Expr::QuantifiedExpr(q) => { let inner = ensure_quantifier_has_inner(q.inner()); - get_or_compute(inner.as_cst(), symbols, def_bodies, cache) + get_or_compute(&inner, symbols, def_bodies, cache) } - Expr::Ref(ref r) => ref_cardinality(r, symbols, def_bodies, cache), + Expr::Ref(r) => ref_cardinality(r, symbols, def_bodies, cache), } } fn get_or_compute( - node: &SyntaxNode, + expr: &Expr, symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, + def_bodies: &HashMap, + cache: &mut HashMap, ) -> ShapeCardinality { - if let Some(&c) = cache.get(node) { + if let Some(&c) = cache.get(expr) { return c; } - let c = compute_single(node, symbols, def_bodies, cache); - cache.insert(node.clone(), c); + let c = compute_single(expr, symbols, def_bodies, cache); + cache.insert(expr.clone(), c); c } fn seq_cardinality( seq: &SeqExpr, symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, + def_bodies: &HashMap, + cache: &mut HashMap, ) -> ShapeCardinality { let children: Vec<_> = seq.children().collect(); match children.len() { 0 => ShapeCardinality::One, - 1 => get_or_compute(children[0].as_cst(), symbols, def_bodies, cache), + 1 => get_or_compute(&children[0], symbols, def_bodies, cache), _ => ShapeCardinality::Many, } } @@ -152,8 +117,8 @@ fn seq_cardinality( fn ref_cardinality( r: &Ref, symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, + def_bodies: &HashMap, + cache: &mut HashMap, ) -> ShapeCardinality { let name_tok = ensure_ref_has_name(r.name()); let name = name_tok.text(); @@ -162,23 +127,23 @@ fn ref_cardinality( return ShapeCardinality::Invalid; } - let Some(body_node) = def_bodies.get(name) else { + let Some(body) = def_bodies.get(name) else { return ShapeCardinality::Invalid; }; - get_or_compute(body_node, symbols, def_bodies, cache) + get_or_compute(body, symbols, def_bodies, cache) } fn validate_node( node: &SyntaxNode, - cardinalities: &HashMap, + cardinalities: &HashMap, errors: &mut Diagnostics, ) { if let Some(field) = FieldExpr::cast(node.clone()) && let Some(value) = field.value() { let card = cardinalities - .get(value.as_cst()) + .get(&value) .copied() .unwrap_or(ShapeCardinality::One); From 13f41b27a21f778ddd0fe50f0de85256d123b68c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 10:56:01 -0300 Subject: [PATCH 13/21] Refactor validation steps in Query to use method calls --- crates/plotnik-lib/src/query/alt_kind.rs | 22 +++++----- crates/plotnik-lib/src/query/mod.rs | 36 +++++++---------- crates/plotnik-lib/src/query/ref_cycles.rs | 21 ++++++---- .../src/query/shape_cardinalities.rs | 40 ++++++++++--------- crates/plotnik-lib/src/query/symbol_table.rs | 15 +++++-- 5 files changed, 73 insertions(+), 61 deletions(-) diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs index b85bdc13..cf73e9bf 100644 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ b/crates/plotnik-lib/src/query/alt_kind.rs @@ -5,25 +5,23 @@ use rowan::TextRange; +use super::Query; use super::invariants::{ assert_alt_no_bare_exprs, assert_root_no_bare_exprs, ensure_both_branch_kinds, }; -use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{AltExpr, AltKind, Branch, Expr, Root}; +use crate::parser::{AltExpr, AltKind, Branch, Expr}; -pub fn validate(root: &Root) -> PassResult<()> { - let mut errors = Diagnostics::new(); - - for def in root.defs() { - if let Some(body) = def.body() { - validate_expr(&body, &mut errors); +impl Query<'_> { + pub(super) fn validate_alt_kinds(&mut self) { + for def in self.ast.defs() { + if let Some(body) = def.body() { + validate_expr(&body, &mut self.alt_kind_diagnostics); + } } - } - assert_root_no_bare_exprs(root); - - Ok(((), errors)) + assert_root_no_bare_exprs(&self.ast); + } } fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index b6b76c9d..eb399239 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -122,30 +122,24 @@ impl<'a> Query<'a> { } fn from_parse(source: &'a str, ast: Root, parse_diagnostics: Diagnostics) -> Self { - let ((), alt_kind_diagnostics) = - alt_kind::validate(&ast).expect("alt_kind::validate is infallible"); - - let (symbol_table, resolve_diagnostics) = - symbol_table::resolve(&ast, source).expect("symbol_table::resolve is infallible"); - - let ((), ref_cycle_diagnostics) = - ref_cycles::validate(&ast, &symbol_table).expect("ref_cycles::validate is infallible"); - - let (shape_cardinalities, shape_diagnostics) = - shape_cardinalities::analyze(&ast, &symbol_table) - .expect("shape_cardinalities::analyze is infallible"); - - Self { + let mut query = Self { source, ast, - symbol_table, - shape_cardinality_table: shape_cardinalities, + symbol_table: SymbolTable::default(), + shape_cardinality_table: HashMap::new(), parse_diagnostics, - alt_kind_diagnostics, - resolve_diagnostics, - ref_cycle_diagnostics, - shape_diagnostics, - } + alt_kind_diagnostics: Diagnostics::new(), + resolve_diagnostics: Diagnostics::new(), + ref_cycle_diagnostics: Diagnostics::new(), + shape_diagnostics: Diagnostics::new(), + }; + + query.validate_alt_kinds(); + query.resolve_symbols(); + query.validate_ref_cycles(); + query.analyze_shape_cardinalities(); + + query } #[allow(dead_code)] diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs index f712e48a..10e8dbc4 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/ref_cycles.rs @@ -6,14 +6,23 @@ use indexmap::{IndexMap, IndexSet}; use rowan::TextRange; +use super::Query; use super::symbol_table::SymbolTable; -use crate::PassResult; use crate::diagnostics::Diagnostics; use crate::parser::{Def, Expr, Root, SyntaxKind}; -pub fn validate(root: &Root, symbols: &SymbolTable<'_>) -> PassResult<()> { +impl Query<'_> { + pub(super) fn validate_ref_cycles(&mut self) { + validate_into( + &self.ast, + &self.symbol_table, + &mut self.ref_cycle_diagnostics, + ); + } +} + +fn validate_into(root: &Root, symbols: &SymbolTable<'_>, errors: &mut Diagnostics) { let sccs = find_sccs(symbols); - let mut errors = Diagnostics::new(); for scc in sccs { if scc.len() == 1 { @@ -30,7 +39,7 @@ pub fn validate(root: &Root, symbols: &SymbolTable<'_>) -> PassResult<()> { let scc_set: IndexSet<&str> = std::iter::once(name.as_str()).collect(); if !expr_has_escape(body, &scc_set) { let chain = build_self_ref_chain(root, name); - emit_error(&mut errors, name, &scc, chain); + emit_error(errors, name, &scc, chain); } continue; } @@ -50,11 +59,9 @@ pub fn validate(root: &Root, symbols: &SymbolTable<'_>) -> PassResult<()> { if !any_has_escape { let chain = build_cycle_chain(root, symbols, &scc); - emit_error(&mut errors, &scc[0], &scc, chain); + emit_error(errors, &scc[0], &scc, chain); } } - - Ok(((), errors)) } fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs index e5214e32..7f8f5637 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shape_cardinalities.rs @@ -7,13 +7,13 @@ //! `Invalid` marks nodes where cardinality cannot be determined (error nodes, //! undefined refs, etc.). +use super::Query; use super::invariants::{ ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name, }; use super::symbol_table::SymbolTable; -use crate::PassResult; use crate::diagnostics::Diagnostics; -use crate::parser::{Expr, FieldExpr, Ref, Root, SeqExpr, SyntaxNode, ast}; +use crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode, ast}; use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -23,24 +23,28 @@ pub enum ShapeCardinality { Invalid, } -pub fn analyze( - root: &Root, - symbols: &SymbolTable, -) -> PassResult> { - let mut cache = HashMap::new(); - let mut errors = Diagnostics::new(); - let mut def_bodies: HashMap = HashMap::new(); - - for def in root.defs() { - if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { - def_bodies.insert(name_tok.text().to_string(), body); - } - } +impl Query<'_> { + pub(super) fn analyze_shape_cardinalities(&mut self) { + let mut def_bodies: HashMap = HashMap::new(); - compute_all_cardinalities(root.as_cst(), symbols, &def_bodies, &mut cache); - validate_node(root.as_cst(), &cache, &mut errors); + for def in self.ast.defs() { + if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { + def_bodies.insert(name_tok.text().to_string(), body); + } + } - Ok((cache, errors)) + compute_all_cardinalities( + self.ast.as_cst(), + &self.symbol_table, + &def_bodies, + &mut self.shape_cardinality_table, + ); + validate_node( + self.ast.as_cst(), + &self.shape_cardinality_table, + &mut self.shape_diagnostics, + ); + } } fn compute_all_cardinalities( diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index 58e9a9b8..cba9a2e5 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -6,13 +6,22 @@ use indexmap::IndexMap; -use crate::PassResult; use crate::diagnostics::Diagnostics; use crate::parser::{Expr, Ref, Root, ast}; +use super::Query; + pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>; -pub fn resolve<'src>(root: &Root, source: &'src str) -> PassResult> { +impl<'a> Query<'a> { + pub(super) fn resolve_symbols(&mut self) { + let (symbols, diagnostics) = resolve(&self.ast, self.source); + self.symbol_table = symbols; + self.resolve_diagnostics = diagnostics; + } +} + +fn resolve<'src>(root: &Root, source: &'src str) -> (SymbolTable<'src>, Diagnostics) { let mut symbols: SymbolTable<'src> = IndexMap::new(); let mut diagnostics = Diagnostics::new(); @@ -49,7 +58,7 @@ pub fn resolve<'src>(root: &Root, source: &'src str) -> PassResult Date: Fri, 5 Dec 2025 11:02:00 -0300 Subject: [PATCH 14/21] Refactor Query parsing into multiple steps --- crates/plotnik-lib/src/query/mod.rs | 65 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index eb399239..3e3ae232 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -25,6 +25,8 @@ mod symbol_table_tests; use std::collections::HashMap; +use rowan::GreenNodeBuilder; + use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::cst::SyntaxKind; @@ -72,19 +74,13 @@ impl<'a> QueryBuilder<'a> { /// /// Returns `Err` if fuel limits are exceeded. pub fn build(self) -> Result> { - let tokens = lex(self.source); - let mut parser = Parser::new(self.source, tokens); - - if let Some(limit) = self.exec_fuel { - parser = parser.with_exec_fuel(limit); - } - - if let Some(limit) = self.recursion_fuel { - parser = parser.with_recursion_fuel(limit); - } - - let (parse, parse_diagnostics) = parser::parse_with_parser(parser)?; - Ok(Query::from_parse(self.source, parse, parse_diagnostics)) + let mut query = Query::empty(self.source); + query.parse(self.exec_fuel, self.recursion_fuel)?; + query.validate_alt_kinds(); + query.resolve_symbols(); + query.validate_ref_cycles(); + query.analyze_shape_cardinalities(); + Ok(query) } } @@ -107,6 +103,14 @@ pub struct Query<'a> { shape_diagnostics: Diagnostics, } +fn empty_root() -> Root { + let mut builder = GreenNodeBuilder::new(); + builder.start_node(SyntaxKind::Root.into()); + builder.finish_node(); + let green = builder.finish(); + Root::cast(SyntaxNode::new_root(green)).expect("we just built a Root node") +} + impl<'a> Query<'a> { /// Parse and analyze a query from source text. /// @@ -121,25 +125,40 @@ impl<'a> Query<'a> { QueryBuilder::new(source) } - fn from_parse(source: &'a str, ast: Root, parse_diagnostics: Diagnostics) -> Self { - let mut query = Self { + fn empty(source: &'a str) -> Self { + Self { source, - ast, + ast: empty_root(), symbol_table: SymbolTable::default(), shape_cardinality_table: HashMap::new(), - parse_diagnostics, + parse_diagnostics: Diagnostics::new(), alt_kind_diagnostics: Diagnostics::new(), resolve_diagnostics: Diagnostics::new(), ref_cycle_diagnostics: Diagnostics::new(), shape_diagnostics: Diagnostics::new(), - }; + } + } - query.validate_alt_kinds(); - query.resolve_symbols(); - query.validate_ref_cycles(); - query.analyze_shape_cardinalities(); + fn parse( + &mut self, + exec_fuel: Option>, + recursion_fuel: Option>, + ) -> Result<()> { + let tokens = lex(self.source); + let mut parser = Parser::new(self.source, tokens); + + if let Some(limit) = exec_fuel { + parser = parser.with_exec_fuel(limit); + } + + if let Some(limit) = recursion_fuel { + parser = parser.with_recursion_fuel(limit); + } - query + let (ast, diagnostics) = parser::parse_with_parser(parser)?; + self.ast = ast; + self.parse_diagnostics = diagnostics; + Ok(()) } #[allow(dead_code)] From 8fb17b6d8d68c5c64885705e257df181b9f2574c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:13:36 -0300 Subject: [PATCH 15/21] Update diagnostics methods for better usability --- crates/plotnik-cli/src/commands/debug/mod.rs | 3 +- crates/plotnik-lib/src/diagnostics/mod.rs | 8 ++++ crates/plotnik-lib/src/lib.rs | 8 ++-- crates/plotnik-lib/src/query/dump.rs | 2 +- crates/plotnik-lib/src/query/mod.rs | 48 ++------------------ 5 files changed, 20 insertions(+), 49 deletions(-) diff --git a/crates/plotnik-cli/src/commands/debug/mod.rs b/crates/plotnik-cli/src/commands/debug/mod.rs index 6aad3358..d9e5c93d 100644 --- a/crates/plotnik-cli/src/commands/debug/mod.rs +++ b/crates/plotnik-cli/src/commands/debug/mod.rs @@ -96,7 +96,8 @@ pub fn run(args: DebugArgs) { if let Some(ref q) = query && !q.is_valid() { - eprint!("{}", q.render_diagnostics_colored(args.color)); + let src = query_source.as_ref().unwrap(); + eprint!("{}", q.diagnostics().render_colored(src, args.color)); } } diff --git a/crates/plotnik-lib/src/diagnostics/mod.rs b/crates/plotnik-lib/src/diagnostics/mod.rs index 19b8bc64..b265d4d4 100644 --- a/crates/plotnik-lib/src/diagnostics/mod.rs +++ b/crates/plotnik-lib/src/diagnostics/mod.rs @@ -71,6 +71,14 @@ impl Diagnostics { DiagnosticsPrinter::new(&self.messages, source) } + pub fn render(&self, source: &str) -> String { + self.printer(source).render() + } + + pub fn render_colored(&self, source: &str, colored: bool) -> String { + self.printer(source).colored(colored).render() + } + pub fn extend(&mut self, other: Diagnostics) { self.messages.extend(other.messages); } diff --git a/crates/plotnik-lib/src/lib.rs b/crates/plotnik-lib/src/lib.rs index 0750c095..0e145f8e 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -5,13 +5,15 @@ //! ``` //! use plotnik_lib::Query; //! -//! let query = Query::new(r#" +//! let source = r#" //! Expr = [(identifier) (number)] //! (assignment left: (Expr) @lhs right: (Expr) @rhs) -//! "#).expect("valid query"); +//! "#; +//! +//! let query = Query::new(source).expect("valid query"); //! //! if !query.is_valid() { -//! eprintln!("{}", query.render_diagnostics()); +//! eprintln!("{}", query.diagnostics().render(source)); //! } //! ``` diff --git a/crates/plotnik-lib/src/query/dump.rs b/crates/plotnik-lib/src/query/dump.rs index 5e03811b..4e7ad1a2 100644 --- a/crates/plotnik-lib/src/query/dump.rs +++ b/crates/plotnik-lib/src/query/dump.rs @@ -28,7 +28,7 @@ mod test_helpers { } pub fn dump_diagnostics(&self) -> String { - self.render_diagnostics() + self.diagnostics().render(self.source) } } } diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 3e3ae232..ec9426a4 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -161,20 +161,15 @@ impl<'a> Query<'a> { Ok(()) } - #[allow(dead_code)] - pub fn source(&self) -> &str { - self.source - } - - pub fn as_cst(&self) -> &SyntaxNode { + pub(crate) fn as_cst(&self) -> &SyntaxNode { self.ast.as_cst() } - pub fn root(&self) -> &Root { + pub(crate) fn root(&self) -> &Root { &self.ast } - pub fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { + pub(crate) fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { // Error nodes are invalid if node.kind() == SyntaxKind::Error { return ShapeCardinality::Invalid; @@ -212,7 +207,7 @@ impl<'a> Query<'a> { } /// All diagnostics combined from all passes. - pub fn all_diagnostics(&self) -> Diagnostics { + pub fn diagnostics(&self) -> Diagnostics { let mut all = Diagnostics::new(); all.extend(self.parse_diagnostics.clone()); all.extend(self.alt_kind_diagnostics.clone()); @@ -222,30 +217,6 @@ impl<'a> Query<'a> { all } - pub fn parse_diagnostics(&self) -> &Diagnostics { - &self.parse_diagnostics - } - - pub fn alt_kind_diagnostics(&self) -> &Diagnostics { - &self.alt_kind_diagnostics - } - - pub fn resolve_diagnostics(&self) -> &Diagnostics { - &self.resolve_diagnostics - } - - pub fn ref_cycle_diagnostics(&self) -> &Diagnostics { - &self.ref_cycle_diagnostics - } - - pub fn shape_diagnostics(&self) -> &Diagnostics { - &self.shape_diagnostics - } - - pub fn diagnostics(&self) -> Diagnostics { - self.all_diagnostics() - } - /// Query is valid if there are no error-severity diagnostics (warnings are allowed). pub fn is_valid(&self) -> bool { !self.parse_diagnostics.has_errors() @@ -254,15 +225,4 @@ impl<'a> Query<'a> { && !self.ref_cycle_diagnostics.has_errors() && !self.shape_diagnostics.has_errors() } - - pub fn render_diagnostics(&self) -> String { - self.all_diagnostics().printer(self.source).render() - } - - pub fn render_diagnostics_colored(&self, colored: bool) -> String { - self.all_diagnostics() - .printer(self.source) - .colored(colored) - .render() - } } From 732cac3a569436aab6529de2a9e2c6589e267a7c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:20:48 -0300 Subject: [PATCH 16/21] Refactor module names for clarity and consistency --- AGENTS.md | 16 ++++----- .../src/query/{alt_kind.rs => alt_kinds.rs} | 0 .../{alt_kind_tests.rs => alt_kinds_tests.rs} | 0 crates/plotnik-lib/src/query/mod.rs | 36 +++++++++---------- crates/plotnik-lib/src/query/printer.rs | 2 +- .../src/query/{ref_cycles.rs => recursion.rs} | 4 +-- ...ref_cycles_tests.rs => recursion_tests.rs} | 0 .../{shape_cardinalities.rs => shapes.rs} | 4 +-- ...cardinalities_tests.rs => shapes_tests.rs} | 0 crates/plotnik-lib/src/query/symbol_table.rs | 2 +- 10 files changed, 32 insertions(+), 32 deletions(-) rename crates/plotnik-lib/src/query/{alt_kind.rs => alt_kinds.rs} (100%) rename crates/plotnik-lib/src/query/{alt_kind_tests.rs => alt_kinds_tests.rs} (100%) rename crates/plotnik-lib/src/query/{ref_cycles.rs => recursion.rs} (99%) rename crates/plotnik-lib/src/query/{ref_cycles_tests.rs => recursion_tests.rs} (100%) rename crates/plotnik-lib/src/query/{shape_cardinalities.rs => shapes.rs} (98%) rename crates/plotnik-lib/src/query/{shape_cardinalities_tests.rs => shapes_tests.rs} (100%) diff --git a/AGENTS.md b/AGENTS.md index a57a290a..2433c4f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,10 +29,10 @@ crates/ dump.rs # dump_* debug output methods (test-only) printer.rs # QueryPrinter for AST output invariants.rs # Query invariant checks - alt_kind.rs # Alternation validation + alt_kinds.rs # Alternation validation symbol_table.rs # Name resolution, symbol table - ref_cycles.rs # Escape analysis (recursion validation) - shape_cardinalities.rs # Shape inference + recursion.rs # Escape analysis (recursion validation) + shapes.rs # Shape inference *_tests.rs # Test files per module lib.rs # Re-exports Query, Diagnostics, Error plotnik-cli/ # CLI tool @@ -45,11 +45,11 @@ docs/ ## Pipeline ```rust -parser::parse() // Parse → CST -alt_kind::validate() // Validate alternation kinds -symbol_table::resolve() // Resolve names → SymbolTable -ref_cycles::validate() // Validate recursion termination -shape_cardinalities::analyze() // Infer and validate shape cardinalities +parser::parse() // Parse → CST +alt_kinds::validate() // Validate alternation kinds +symbol_table::resolve() // Resolve names → SymbolTable +recursion::validate() // Validate recursion termination +shapes::infer() // Infer and validate shape cardinalities ``` Module = "what", function = "action". diff --git a/crates/plotnik-lib/src/query/alt_kind.rs b/crates/plotnik-lib/src/query/alt_kinds.rs similarity index 100% rename from crates/plotnik-lib/src/query/alt_kind.rs rename to crates/plotnik-lib/src/query/alt_kinds.rs diff --git a/crates/plotnik-lib/src/query/alt_kind_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs similarity index 100% rename from crates/plotnik-lib/src/query/alt_kind_tests.rs rename to crates/plotnik-lib/src/query/alt_kinds_tests.rs diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index ec9426a4..8bb172d2 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -5,21 +5,21 @@ mod invariants; mod printer; pub use printer::QueryPrinter; -pub mod alt_kind; -pub mod ref_cycles; -pub mod shape_cardinalities; +pub mod alt_kinds; +pub mod recursion; +pub mod shapes; pub mod symbol_table; #[cfg(test)] -mod alt_kind_tests; +mod alt_kinds_tests; #[cfg(test)] mod mod_tests; #[cfg(test)] mod printer_tests; #[cfg(test)] -mod ref_cycles_tests; +mod recursion_tests; #[cfg(test)] -mod shape_cardinalities_tests; +mod shapes_tests; #[cfg(test)] mod symbol_table_tests; @@ -32,7 +32,7 @@ use crate::diagnostics::Diagnostics; use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; use crate::parser::{self, Parser, Root, SyntaxNode, ast}; -use shape_cardinalities::ShapeCardinality; +use shapes::ShapeCardinality; use symbol_table::SymbolTable; /// Builder for configuring and creating a [`Query`]. @@ -77,9 +77,9 @@ impl<'a> QueryBuilder<'a> { let mut query = Query::empty(self.source); query.parse(self.exec_fuel, self.recursion_fuel)?; query.validate_alt_kinds(); - query.resolve_symbols(); - query.validate_ref_cycles(); - query.analyze_shape_cardinalities(); + query.resolve_names(); + query.validate_recursion(); + query.infer_shapes(); Ok(query) } } @@ -99,8 +99,8 @@ pub struct Query<'a> { parse_diagnostics: Diagnostics, alt_kind_diagnostics: Diagnostics, resolve_diagnostics: Diagnostics, - ref_cycle_diagnostics: Diagnostics, - shape_diagnostics: Diagnostics, + recursion_diagnostics: Diagnostics, + shapes_diagnostics: Diagnostics, } fn empty_root() -> Root { @@ -134,8 +134,8 @@ impl<'a> Query<'a> { parse_diagnostics: Diagnostics::new(), alt_kind_diagnostics: Diagnostics::new(), resolve_diagnostics: Diagnostics::new(), - ref_cycle_diagnostics: Diagnostics::new(), - shape_diagnostics: Diagnostics::new(), + recursion_diagnostics: Diagnostics::new(), + shapes_diagnostics: Diagnostics::new(), } } @@ -212,8 +212,8 @@ impl<'a> Query<'a> { all.extend(self.parse_diagnostics.clone()); all.extend(self.alt_kind_diagnostics.clone()); all.extend(self.resolve_diagnostics.clone()); - all.extend(self.ref_cycle_diagnostics.clone()); - all.extend(self.shape_diagnostics.clone()); + all.extend(self.recursion_diagnostics.clone()); + all.extend(self.shapes_diagnostics.clone()); all } @@ -222,7 +222,7 @@ impl<'a> Query<'a> { !self.parse_diagnostics.has_errors() && !self.alt_kind_diagnostics.has_errors() && !self.resolve_diagnostics.has_errors() - && !self.ref_cycle_diagnostics.has_errors() - && !self.shape_diagnostics.has_errors() + && !self.recursion_diagnostics.has_errors() + && !self.shapes_diagnostics.has_errors() } } diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 5f4a35b6..3d3d0f34 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -6,7 +6,7 @@ use rowan::NodeOrToken; use crate::parser::{self as ast, Expr, SyntaxNode}; use super::Query; -use super::shape_cardinalities::ShapeCardinality; +use super::shapes::ShapeCardinality; pub struct QueryPrinter<'q, 'src> { query: &'q Query<'src>, diff --git a/crates/plotnik-lib/src/query/ref_cycles.rs b/crates/plotnik-lib/src/query/recursion.rs similarity index 99% rename from crates/plotnik-lib/src/query/ref_cycles.rs rename to crates/plotnik-lib/src/query/recursion.rs index 10e8dbc4..cbc459dd 100644 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ b/crates/plotnik-lib/src/query/recursion.rs @@ -12,11 +12,11 @@ use crate::diagnostics::Diagnostics; use crate::parser::{Def, Expr, Root, SyntaxKind}; impl Query<'_> { - pub(super) fn validate_ref_cycles(&mut self) { + pub(super) fn validate_recursion(&mut self) { validate_into( &self.ast, &self.symbol_table, - &mut self.ref_cycle_diagnostics, + &mut self.recursion_diagnostics, ); } } diff --git a/crates/plotnik-lib/src/query/ref_cycles_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs similarity index 100% rename from crates/plotnik-lib/src/query/ref_cycles_tests.rs rename to crates/plotnik-lib/src/query/recursion_tests.rs diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shapes.rs similarity index 98% rename from crates/plotnik-lib/src/query/shape_cardinalities.rs rename to crates/plotnik-lib/src/query/shapes.rs index 7f8f5637..a6e0bad0 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -24,7 +24,7 @@ pub enum ShapeCardinality { } impl Query<'_> { - pub(super) fn analyze_shape_cardinalities(&mut self) { + pub(super) fn infer_shapes(&mut self) { let mut def_bodies: HashMap = HashMap::new(); for def in self.ast.defs() { @@ -42,7 +42,7 @@ impl Query<'_> { validate_node( self.ast.as_cst(), &self.shape_cardinality_table, - &mut self.shape_diagnostics, + &mut self.shapes_diagnostics, ); } } diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs similarity index 100% rename from crates/plotnik-lib/src/query/shape_cardinalities_tests.rs rename to crates/plotnik-lib/src/query/shapes_tests.rs diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index cba9a2e5..ee86188c 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -14,7 +14,7 @@ use super::Query; pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>; impl<'a> Query<'a> { - pub(super) fn resolve_symbols(&mut self) { + pub(super) fn resolve_names(&mut self) { let (symbols, diagnostics) = resolve(&self.ast, self.source); self.symbol_table = symbols; self.resolve_diagnostics = diagnostics; From 53816fdc1b26da79c0ee9971ba80504352b959a7 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:30:37 -0300 Subject: [PATCH 17/21] Refactor recursion detection using Tarjan's SCC algorithm --- crates/plotnik-lib/src/query/alt_kinds.rs | 121 +++--- crates/plotnik-lib/src/query/recursion.rs | 434 +++++++++---------- crates/plotnik-lib/src/query/shapes.rs | 176 +++----- crates/plotnik-lib/src/query/symbol_table.rs | 172 ++++---- 4 files changed, 395 insertions(+), 508 deletions(-) diff --git a/crates/plotnik-lib/src/query/alt_kinds.rs b/crates/plotnik-lib/src/query/alt_kinds.rs index cf73e9bf..af16df76 100644 --- a/crates/plotnik-lib/src/query/alt_kinds.rs +++ b/crates/plotnik-lib/src/query/alt_kinds.rs @@ -9,101 +9,82 @@ use super::Query; use super::invariants::{ assert_alt_no_bare_exprs, assert_root_no_bare_exprs, ensure_both_branch_kinds, }; -use crate::diagnostics::Diagnostics; use crate::parser::{AltExpr, AltKind, Branch, Expr}; impl Query<'_> { pub(super) fn validate_alt_kinds(&mut self) { - for def in self.ast.defs() { - if let Some(body) = def.body() { - validate_expr(&body, &mut self.alt_kind_diagnostics); - } + let defs: Vec<_> = self.ast.defs().collect(); + for def in defs { + let Some(body) = def.body() else { continue }; + self.validate_alt_expr(&body); } assert_root_no_bare_exprs(&self.ast); } -} -fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { - match expr { - Expr::AltExpr(alt) => { - check_mixed_alternation(alt, errors); - for branch in alt.branches() { - if let Some(body) = branch.body() { - validate_expr(&body, errors); + fn validate_alt_expr(&mut self, expr: &Expr) { + match expr { + Expr::AltExpr(alt) => { + self.check_mixed_alternation(alt); + for branch in alt.branches() { + let Some(body) = branch.body() else { continue }; + self.validate_alt_expr(&body); } + assert_alt_no_bare_exprs(alt); } - assert_alt_no_bare_exprs(alt); - } - Expr::NamedNode(node) => { - for child in node.children() { - validate_expr(&child, errors); + Expr::NamedNode(node) => { + for child in node.children() { + self.validate_alt_expr(&child); + } } - } - Expr::SeqExpr(seq) => { - for child in seq.children() { - validate_expr(&child, errors); + Expr::SeqExpr(seq) => { + for child in seq.children() { + self.validate_alt_expr(&child); + } } - } - Expr::CapturedExpr(cap) => { - if let Some(inner) = cap.inner() { - validate_expr(&inner, errors); + Expr::CapturedExpr(cap) => { + let Some(inner) = cap.inner() else { return }; + self.validate_alt_expr(&inner); } - } - Expr::QuantifiedExpr(q) => { - if let Some(inner) = q.inner() { - validate_expr(&inner, errors); + Expr::QuantifiedExpr(q) => { + let Some(inner) = q.inner() else { return }; + self.validate_alt_expr(&inner); } - } - Expr::FieldExpr(f) => { - if let Some(value) = f.value() { - validate_expr(&value, errors); + Expr::FieldExpr(f) => { + let Some(value) = f.value() else { return }; + self.validate_alt_expr(&value); } + Expr::Ref(_) | Expr::AnonymousNode(_) => {} } - Expr::Ref(_) | Expr::AnonymousNode(_) => {} } -} - -fn check_mixed_alternation(alt: &AltExpr, errors: &mut Diagnostics) { - if alt.kind() != AltKind::Mixed { - return; - } - - let branches: Vec = alt.branches().collect(); - - let mut first_tagged: Option<&Branch> = None; - let mut first_untagged: Option<&Branch> = None; - for branch in &branches { - if branch.label().is_some() { - if first_tagged.is_none() { - first_tagged = Some(branch); - } - } else if first_untagged.is_none() { - first_untagged = Some(branch); + fn check_mixed_alternation(&mut self, alt: &AltExpr) { + if alt.kind() != AltKind::Mixed { + return; } - if first_tagged.is_some() && first_untagged.is_some() { - break; - } - } + let branches: Vec = alt.branches().collect(); + let first_tagged = branches.iter().find(|b| b.label().is_some()); + let first_untagged = branches.iter().find(|b| b.label().is_none()); - let (tagged_branch, untagged_branch) = ensure_both_branch_kinds(first_tagged, first_untagged); + let (tagged_branch, untagged_branch) = + ensure_both_branch_kinds(first_tagged, first_untagged); - let tagged_range = tagged_branch - .label() - .map(|t| t.text_range()) - .unwrap_or_else(|| branch_range(tagged_branch)); + let tagged_range = tagged_branch + .label() + .map(|t| t.text_range()) + .unwrap_or_else(|| branch_range(tagged_branch)); - let untagged_range = branch_range(untagged_branch); + let untagged_range = branch_range(untagged_branch); - errors - .error( - "mixed tagged and untagged branches in alternation", - untagged_range, - ) - .related_to("tagged branch here", tagged_range) - .emit(); + self.alt_kind_diagnostics + .error( + "mixed tagged and untagged branches in alternation", + untagged_range, + ) + .related_to("tagged branch here", tagged_range) + .emit(); + } } fn branch_range(branch: &Branch) -> TextRange { diff --git a/crates/plotnik-lib/src/query/recursion.rs b/crates/plotnik-lib/src/query/recursion.rs index cbc459dd..780ea9db 100644 --- a/crates/plotnik-lib/src/query/recursion.rs +++ b/crates/plotnik-lib/src/query/recursion.rs @@ -7,68 +7,225 @@ use indexmap::{IndexMap, IndexSet}; use rowan::TextRange; use super::Query; -use super::symbol_table::SymbolTable; -use crate::diagnostics::Diagnostics; -use crate::parser::{Def, Expr, Root, SyntaxKind}; +use crate::parser::{Def, Expr, SyntaxKind}; impl Query<'_> { pub(super) fn validate_recursion(&mut self) { - validate_into( - &self.ast, - &self.symbol_table, - &mut self.recursion_diagnostics, - ); - } -} + let sccs = self.find_sccs(); -fn validate_into(root: &Root, symbols: &SymbolTable<'_>, errors: &mut Diagnostics) { - let sccs = find_sccs(symbols); + for scc in sccs { + let scc_set: IndexSet<&str> = scc.iter().map(|s| s.as_str()).collect(); + + let has_escape = scc.iter().any(|name| { + self.symbol_table + .get(name.as_str()) + .map(|body| expr_has_escape(body, &scc_set)) + .unwrap_or(true) + }); - for scc in sccs { - if scc.len() == 1 { - let name = &scc[0]; - let Some(body) = symbols.get(name.as_str()) else { + if has_escape { continue; + } + + let chain = if scc.len() == 1 { + self.build_self_ref_chain(&scc[0]) + } else { + self.build_cycle_chain(&scc) }; + self.emit_recursion_error(&scc[0], &scc, chain); + } + } - let refs = collect_refs(body); - if !refs.contains(name) { - continue; + fn find_sccs(&self) -> Vec> { + struct State<'a, 'src> { + query: &'a Query<'src>, + index: usize, + stack: Vec, + on_stack: IndexSet, + indices: IndexMap, + lowlinks: IndexMap, + sccs: Vec>, + } + + fn strongconnect(name: &str, state: &mut State<'_, '_>) { + state.indices.insert(name.to_string(), state.index); + state.lowlinks.insert(name.to_string(), state.index); + state.index += 1; + state.stack.push(name.to_string()); + state.on_stack.insert(name.to_string()); + + if let Some(body) = state.query.symbol_table.get(name) { + let refs = collect_refs(body); + for ref_name in &refs { + if state.query.symbol_table.get(ref_name.as_str()).is_none() { + continue; + } + if !state.indices.contains_key(ref_name.as_str()) { + strongconnect(ref_name, state); + let ref_lowlink = state.lowlinks[ref_name.as_str()]; + let my_lowlink = state.lowlinks.get_mut(name).unwrap(); + *my_lowlink = (*my_lowlink).min(ref_lowlink); + } else if state.on_stack.contains(ref_name.as_str()) { + let ref_index = state.indices[ref_name.as_str()]; + let my_lowlink = state.lowlinks.get_mut(name).unwrap(); + *my_lowlink = (*my_lowlink).min(ref_index); + } + } + } + + if state.lowlinks[name] == state.indices[name] { + let mut scc = Vec::new(); + loop { + let w = state.stack.pop().unwrap(); + state.on_stack.swap_remove(&w); + scc.push(w.clone()); + if w == name { + break; + } + } + state.sccs.push(scc); } + } - let scc_set: IndexSet<&str> = std::iter::once(name.as_str()).collect(); - if !expr_has_escape(body, &scc_set) { - let chain = build_self_ref_chain(root, name); - emit_error(errors, name, &scc, chain); + let mut state = State { + query: self, + index: 0, + stack: Vec::new(), + on_stack: IndexSet::new(), + indices: IndexMap::new(), + lowlinks: IndexMap::new(), + sccs: Vec::new(), + }; + + for name in self.symbol_table.keys() { + if !state.indices.contains_key(*name) { + strongconnect(name, &mut state); } - continue; } + state + .sccs + .into_iter() + .filter(|scc| { + scc.len() > 1 + || self + .symbol_table + .get(scc[0].as_str()) + .map(|body| collect_refs(body).contains(scc[0].as_str())) + .unwrap_or(false) + }) + .collect() + } + + fn find_def_by_name(&self, name: &str) -> Option { + self.ast + .defs() + .find(|d| d.name().map(|n| n.text() == name).unwrap_or(false)) + } + + fn find_reference_location(&self, from: &str, to: &str) -> Option { + let def = self.find_def_by_name(from)?; + let body = def.body()?; + find_ref_in_expr(&body, to) + } + + fn build_self_ref_chain(&self, name: &str) -> Vec<(TextRange, String)> { + self.find_reference_location(name, name) + .map(|range| vec![(range, format!("`{}` references itself", name))]) + .unwrap_or_default() + } + + fn build_cycle_chain(&self, scc: &[String]) -> Vec<(TextRange, String)> { let scc_set: IndexSet<&str> = scc.iter().map(|s| s.as_str()).collect(); - let mut any_has_escape = false; - - for name in &scc { - if let Some(def) = find_def_by_name(root, name) - && let Some(body) = def.body() - && expr_has_escape(&body, &scc_set) - { - any_has_escape = true; - break; + let mut visited = IndexSet::new(); + let mut path = Vec::new(); + let start = &scc[0]; + + fn find_path<'a>( + current: &str, + start: &str, + scc_set: &IndexSet<&str>, + query: &Query<'a>, + visited: &mut IndexSet, + path: &mut Vec, + ) -> bool { + if visited.contains(current) { + return current == start && path.len() > 1; } + visited.insert(current.to_string()); + path.push(current.to_string()); + + if let Some(body) = query.symbol_table.get(current) { + let refs = collect_refs(body); + for ref_name in &refs { + if scc_set.contains(ref_name.as_str()) + && find_path(ref_name, start, scc_set, query, visited, path) + { + return true; + } + } + } + + path.pop(); + false } - if !any_has_escape { - let chain = build_cycle_chain(root, symbols, &scc); - emit_error(errors, &scc[0], &scc, chain); + find_path(start, start, &scc_set, self, &mut visited, &mut path); + + path.iter() + .enumerate() + .filter_map(|(i, from)| { + let to = &path[(i + 1) % path.len()]; + self.find_reference_location(from, to).map(|range| { + let msg = if i == path.len() - 1 { + format!("`{}` references `{}` (completing cycle)", from, to) + } else { + format!("`{}` references `{}`", from, to) + }; + (range, msg) + }) + }) + .collect() + } + + fn emit_recursion_error( + &mut self, + primary_name: &str, + scc: &[String], + related: Vec<(TextRange, String)>, + ) { + let cycle_str = if scc.len() == 1 { + format!("`{}` → `{}`", primary_name, primary_name) + } else { + let mut cycle: Vec<_> = scc.iter().map(|s| format!("`{}`", s)).collect(); + cycle.push(format!("`{}`", scc[0])); + cycle.join(" → ") + }; + + let range = related + .first() + .map(|(r, _)| *r) + .unwrap_or_else(|| TextRange::empty(0.into())); + + let mut builder = self.recursion_diagnostics.error( + format!( + "recursive pattern can never match: cycle {} has no escape path", + cycle_str + ), + range, + ); + + for (rel_range, rel_msg) in related { + builder = builder.related_to(rel_msg, rel_range); } + + builder.emit(); } } fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { match expr { Expr::Ref(r) => { - // A Ref is always a reference to a user-defined expression - // If it's in the SCC, it doesn't provide an escape path let Some(name_token) = r.name() else { return true; }; @@ -78,7 +235,6 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { let children: Vec<_> = node.children().collect(); children.is_empty() || children.iter().all(|c| expr_has_escape(c, scc)) } - Expr::AltExpr(alt) => { alt.branches().any(|b| { b.body() @@ -86,9 +242,7 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { .unwrap_or(true) }) || alt.exprs().any(|e| expr_has_escape(&e, scc)) } - Expr::SeqExpr(seq) => seq.children().all(|c| expr_has_escape(&c, scc)), - Expr::QuantifiedExpr(q) => match q.operator().map(|op| op.kind()) { Some( SyntaxKind::Question @@ -102,14 +256,11 @@ fn expr_has_escape(expr: &Expr, scc: &IndexSet<&str>) -> bool { .unwrap_or(true), _ => true, }, - Expr::CapturedExpr(cap) => cap .inner() .map(|inner| expr_has_escape(&inner, scc)) .unwrap_or(true), - Expr::FieldExpr(f) => f.value().map(|v| expr_has_escape(&v, scc)).unwrap_or(true), - Expr::AnonymousNode(_) => true, } } @@ -158,106 +309,14 @@ fn collect_refs_into(expr: &Expr, refs: &mut IndexSet) { } } -fn find_sccs(symbols: &SymbolTable<'_>) -> Vec> { - struct State<'a> { - symbols: &'a SymbolTable<'a>, - index: usize, - stack: Vec, - on_stack: IndexSet, - indices: IndexMap, - lowlinks: IndexMap, - sccs: Vec>, - } - - fn strongconnect(name: &str, state: &mut State<'_>) { - state.indices.insert(name.to_string(), state.index); - state.lowlinks.insert(name.to_string(), state.index); - state.index += 1; - state.stack.push(name.to_string()); - state.on_stack.insert(name.to_string()); - - if let Some(body) = state.symbols.get(name) { - let refs = collect_refs(body); - for ref_name in &refs { - if state.symbols.get(ref_name.as_str()).is_none() { - continue; - } - if !state.indices.contains_key(ref_name.as_str()) { - strongconnect(ref_name, state); - let ref_lowlink = state.lowlinks[ref_name.as_str()]; - let my_lowlink = state.lowlinks.get_mut(name).unwrap(); - *my_lowlink = (*my_lowlink).min(ref_lowlink); - } else if state.on_stack.contains(ref_name.as_str()) { - let ref_index = state.indices[ref_name.as_str()]; - let my_lowlink = state.lowlinks.get_mut(name).unwrap(); - *my_lowlink = (*my_lowlink).min(ref_index); - } - } - } - - if state.lowlinks[name] == state.indices[name] { - let mut scc = Vec::new(); - loop { - let w = state.stack.pop().unwrap(); - state.on_stack.swap_remove(&w); - scc.push(w.clone()); - if w == name { - break; - } - } - state.sccs.push(scc); - } - } - - let mut state = State { - symbols, - index: 0, - stack: Vec::new(), - on_stack: IndexSet::new(), - indices: IndexMap::new(), - lowlinks: IndexMap::new(), - sccs: Vec::new(), - }; - - for name in symbols.keys() { - if !state.indices.contains_key(*name) { - strongconnect(name, &mut state); - } - } - - state - .sccs - .into_iter() - .filter(|scc| { - scc.len() > 1 - || symbols - .get(scc[0].as_str()) - .map(|body| collect_refs(body).contains(scc[0].as_str())) - .unwrap_or(false) - }) - .collect() -} - -fn find_def_by_name(root: &Root, name: &str) -> Option { - root.defs() - .find(|d| d.name().map(|n| n.text() == name).unwrap_or(false)) -} - -fn find_reference_location(root: &Root, from: &str, to: &str) -> Option { - let def = find_def_by_name(root, from)?; - let body = def.body()?; - find_ref_in_expr(&body, to) -} - fn find_ref_in_expr(expr: &Expr, target: &str) -> Option { match expr { Expr::Ref(r) => { let name_token = r.name()?; if name_token.text() == target { - Some(name_token.text_range()) - } else { - None + return Some(name_token.text_range()); } + None } Expr::NamedNode(node) => node .children() @@ -272,103 +331,6 @@ fn find_ref_in_expr(expr: &Expr, target: &str) -> Option { .and_then(|inner| find_ref_in_expr(&inner, target)), Expr::QuantifiedExpr(q) => q.inner().and_then(|inner| find_ref_in_expr(&inner, target)), Expr::FieldExpr(f) => f.value().and_then(|v| find_ref_in_expr(&v, target)), - _ => None, - } -} - -fn build_self_ref_chain(root: &Root, name: &str) -> Vec<(TextRange, String)> { - find_reference_location(root, name, name) - .map(|range| vec![(range, format!("`{}` references itself", name))]) - .unwrap_or_default() -} - -fn build_cycle_chain( - root: &Root, - symbols: &SymbolTable<'_>, - scc: &[String], -) -> Vec<(TextRange, String)> { - let scc_set: IndexSet<&str> = scc.iter().map(|s| s.as_str()).collect(); - let mut visited = IndexSet::new(); - let mut path = Vec::new(); - let start = &scc[0]; - - fn find_path( - current: &str, - start: &str, - scc_set: &IndexSet<&str>, - symbols: &SymbolTable<'_>, - visited: &mut IndexSet, - path: &mut Vec, - ) -> bool { - if visited.contains(current) { - return current == start && path.len() > 1; - } - visited.insert(current.to_string()); - path.push(current.to_string()); - - if let Some(body) = symbols.get(current) { - let refs = collect_refs(body); - for ref_name in &refs { - if scc_set.contains(ref_name.as_str()) - && find_path(ref_name, start, scc_set, symbols, visited, path) - { - return true; - } - } - } - - path.pop(); - false - } - - find_path(start, start, &scc_set, symbols, &mut visited, &mut path); - - path.iter() - .enumerate() - .filter_map(|(i, from)| { - let to = &path[(i + 1) % path.len()]; - find_reference_location(root, from, to).map(|range| { - let msg = if i == path.len() - 1 { - format!("`{}` references `{}` (completing cycle)", from, to) - } else { - format!("`{}` references `{}`", from, to) - }; - (range, msg) - }) - }) - .collect() -} - -fn emit_error( - errors: &mut Diagnostics, - primary_name: &str, - scc: &[String], - related: Vec<(TextRange, String)>, -) { - let cycle_str = if scc.len() == 1 { - format!("`{}` → `{}`", primary_name, primary_name) - } else { - let mut cycle: Vec<_> = scc.iter().map(|s| format!("`{}`", s)).collect(); - cycle.push(format!("`{}`", scc[0])); - cycle.join(" → ") - }; - - let range = related - .first() - .map(|(r, _)| *r) - .unwrap_or_else(|| TextRange::empty(0.into())); - - let mut builder = errors.error( - format!( - "recursive pattern can never match: cycle {} has no escape path", - cycle_str - ), - range, - ); - - for (rel_range, rel_msg) in related { - builder = builder.related_to(rel_msg, rel_range); + Expr::AnonymousNode(_) => None, } - - builder.emit(); } diff --git a/crates/plotnik-lib/src/query/shapes.rs b/crates/plotnik-lib/src/query/shapes.rs index a6e0bad0..8916d131 100644 --- a/crates/plotnik-lib/src/query/shapes.rs +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -11,10 +11,7 @@ use super::Query; use super::invariants::{ ensure_capture_has_inner, ensure_quantifier_has_inner, ensure_ref_has_name, }; -use super::symbol_table::SymbolTable; -use crate::diagnostics::Diagnostics; -use crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode, ast}; -use std::collections::HashMap; +use crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ShapeCardinality { @@ -25,128 +22,89 @@ pub enum ShapeCardinality { impl Query<'_> { pub(super) fn infer_shapes(&mut self) { - let mut def_bodies: HashMap = HashMap::new(); + self.compute_all_cardinalities(self.ast.as_cst().clone()); + self.validate_shapes(self.ast.as_cst().clone()); + } - for def in self.ast.defs() { - if let (Some(name_tok), Some(body)) = (def.name(), def.body()) { - def_bodies.insert(name_tok.text().to_string(), body); - } + fn compute_all_cardinalities(&mut self, node: SyntaxNode) { + if let Some(expr) = Expr::cast(node.clone()) { + self.get_or_compute(&expr); } - compute_all_cardinalities( - self.ast.as_cst(), - &self.symbol_table, - &def_bodies, - &mut self.shape_cardinality_table, - ); - validate_node( - self.ast.as_cst(), - &self.shape_cardinality_table, - &mut self.shapes_diagnostics, - ); + for child in node.children() { + self.compute_all_cardinalities(child); + } } -} -fn compute_all_cardinalities( - node: &SyntaxNode, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) { - if let Some(expr) = Expr::cast(node.clone()) { - get_or_compute(&expr, symbols, def_bodies, cache); + fn get_or_compute(&mut self, expr: &Expr) -> ShapeCardinality { + if let Some(&c) = self.shape_cardinality_table.get(expr) { + return c; + } + let c = self.compute_single(expr); + self.shape_cardinality_table.insert(expr.clone(), c); + c } - for child in node.children() { - compute_all_cardinalities(&child, symbols, def_bodies, cache); - } -} + fn compute_single(&mut self, expr: &Expr) -> ShapeCardinality { + match expr { + Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::AltExpr(_) => { + ShapeCardinality::One + } -fn compute_single( - expr: &Expr, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - match expr { - Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::AltExpr(_) => { - ShapeCardinality::One - } + Expr::SeqExpr(seq) => self.seq_cardinality(seq), - Expr::SeqExpr(seq) => seq_cardinality(seq, symbols, def_bodies, cache), + Expr::CapturedExpr(cap) => { + let inner = ensure_capture_has_inner(cap.inner()); + self.get_or_compute(&inner) + } - Expr::CapturedExpr(cap) => { - let inner = ensure_capture_has_inner(cap.inner()); - get_or_compute(&inner, symbols, def_bodies, cache) - } + Expr::QuantifiedExpr(q) => { + let inner = ensure_quantifier_has_inner(q.inner()); + self.get_or_compute(&inner) + } - Expr::QuantifiedExpr(q) => { - let inner = ensure_quantifier_has_inner(q.inner()); - get_or_compute(&inner, symbols, def_bodies, cache) + Expr::Ref(r) => self.ref_cardinality(r), } - - Expr::Ref(r) => ref_cardinality(r, symbols, def_bodies, cache), } -} -fn get_or_compute( - expr: &Expr, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - if let Some(&c) = cache.get(expr) { - return c; - } - let c = compute_single(expr, symbols, def_bodies, cache); - cache.insert(expr.clone(), c); - c -} + fn seq_cardinality(&mut self, seq: &SeqExpr) -> ShapeCardinality { + let children: Vec<_> = seq.children().collect(); -fn seq_cardinality( - seq: &SeqExpr, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - let children: Vec<_> = seq.children().collect(); - - match children.len() { - 0 => ShapeCardinality::One, - 1 => get_or_compute(&children[0], symbols, def_bodies, cache), - _ => ShapeCardinality::Many, + match children.len() { + 0 => ShapeCardinality::One, + 1 => self.get_or_compute(&children[0]), + _ => ShapeCardinality::Many, + } } -} -fn ref_cardinality( - r: &Ref, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - let name_tok = ensure_ref_has_name(r.name()); - let name = name_tok.text(); - - if symbols.get(name).is_none() { - return ShapeCardinality::Invalid; + fn ref_cardinality(&mut self, r: &Ref) -> ShapeCardinality { + let name_tok = ensure_ref_has_name(r.name()); + let name = name_tok.text(); + + let Some(body) = self.symbol_table.get(name).cloned() else { + return ShapeCardinality::Invalid; + }; + + self.get_or_compute(&body) } - let Some(body) = def_bodies.get(name) else { - return ShapeCardinality::Invalid; - }; + fn validate_shapes(&mut self, node: SyntaxNode) { + let Some(field) = FieldExpr::cast(node.clone()) else { + for child in node.children() { + self.validate_shapes(child); + } + return; + }; - get_or_compute(body, symbols, def_bodies, cache) -} + let Some(value) = field.value() else { + for child in node.children() { + self.validate_shapes(child); + } + return; + }; -fn validate_node( - node: &SyntaxNode, - cardinalities: &HashMap, - errors: &mut Diagnostics, -) { - if let Some(field) = FieldExpr::cast(node.clone()) - && let Some(value) = field.value() - { - let card = cardinalities + let card = self + .shape_cardinality_table .get(&value) .copied() .unwrap_or(ShapeCardinality::One); @@ -157,7 +115,7 @@ fn validate_node( .map(|t| t.text().to_string()) .unwrap_or_else(|| "field".to_string()); - errors + self.shapes_diagnostics .error( format!( "field `{}` value must match a single node, not a sequence", @@ -167,9 +125,9 @@ fn validate_node( ) .emit(); } - } - for child in node.children() { - validate_node(&child, cardinalities, errors); + for child in node.children() { + self.validate_shapes(child); + } } } diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index ee86188c..e1620e59 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -6,8 +6,7 @@ use indexmap::IndexMap; -use crate::diagnostics::Diagnostics; -use crate::parser::{Expr, Ref, Root, ast}; +use crate::parser::{Expr, Ref, ast}; use super::Query; @@ -15,110 +14,97 @@ pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>; impl<'a> Query<'a> { pub(super) fn resolve_names(&mut self) { - let (symbols, diagnostics) = resolve(&self.ast, self.source); - self.symbol_table = symbols; - self.resolve_diagnostics = diagnostics; - } -} - -fn resolve<'src>(root: &Root, source: &'src str) -> (SymbolTable<'src>, Diagnostics) { - let mut symbols: SymbolTable<'src> = IndexMap::new(); - let mut diagnostics = Diagnostics::new(); - - // Pass 1: collect definitions - for def in root.defs() { - let Some(name_token) = def.name() else { - continue; - }; - - let range = name_token.text_range(); - let name = &source[range.start().into()..range.end().into()]; + // Pass 1: collect definitions + for def in self.ast.defs() { + let Some(name_token) = def.name() else { + continue; + }; + + let range = name_token.text_range(); + let name = &self.source[range.start().into()..range.end().into()]; + + if self.symbol_table.contains_key(name) { + self.resolve_diagnostics + .error(format!("duplicate definition: `{}`", name), range) + .emit(); + continue; + } - if symbols.contains_key(name) { - diagnostics - .error(format!("duplicate definition: `{}`", name), range) - .emit(); - continue; + let Some(body) = def.body() else { + continue; + }; + self.symbol_table.insert(name, body); } - if let Some(body) = def.body() { - symbols.insert(name, body); + // Pass 2: check references + let defs: Vec<_> = self.ast.defs().collect(); + for def in defs { + let Some(body) = def.body() else { continue }; + self.collect_reference_diagnostics(&body); } - } - // Pass 2: check references - for def in root.defs() { - let Some(body) = def.body() else { continue }; - collect_reference_diagnostics(&body, &symbols, &mut diagnostics); + // Parser wraps all top-level exprs in Def nodes, so this should be empty + assert!( + self.ast.exprs().next().is_none(), + "symbol_table: unexpected bare Expr in Root (parser should wrap in Def)" + ); } - // Parser wraps all top-level exprs in Def nodes, so this should be empty - assert!( - root.exprs().next().is_none(), - "symbol_table: unexpected bare Expr in Root (parser should wrap in Def)" - ); - - (symbols, diagnostics) -} - -fn collect_reference_diagnostics( - expr: &Expr, - symbols: &SymbolTable<'_>, - diagnostics: &mut Diagnostics, -) { - match expr { - Expr::Ref(r) => { - check_ref_diagnostic(r, symbols, diagnostics); - } - Expr::NamedNode(node) => { - for child in node.children() { - collect_reference_diagnostics(&child, symbols, diagnostics); + fn collect_reference_diagnostics(&mut self, expr: &Expr) { + match expr { + Expr::Ref(r) => { + self.check_ref_diagnostic(r); } - } - Expr::AltExpr(alt) => { - for branch in alt.branches() { - let Some(body) = branch.body() else { continue }; - collect_reference_diagnostics(&body, symbols, diagnostics); + Expr::NamedNode(node) => { + for child in node.children() { + self.collect_reference_diagnostics(&child); + } } - // Parser wraps all alt children in Branch nodes - assert!( - alt.exprs().next().is_none(), - "symbol_table: unexpected bare Expr in Alt (parser should wrap in Branch)" - ); - } - Expr::SeqExpr(seq) => { - for child in seq.children() { - collect_reference_diagnostics(&child, symbols, diagnostics); + Expr::AltExpr(alt) => { + for branch in alt.branches() { + let Some(body) = branch.body() else { continue }; + self.collect_reference_diagnostics(&body); + } + // Parser wraps all alt children in Branch nodes + assert!( + alt.exprs().next().is_none(), + "symbol_table: unexpected bare Expr in Alt (parser should wrap in Branch)" + ); } + Expr::SeqExpr(seq) => { + for child in seq.children() { + self.collect_reference_diagnostics(&child); + } + } + Expr::CapturedExpr(cap) => { + let Some(inner) = cap.inner() else { return }; + self.collect_reference_diagnostics(&inner); + } + Expr::QuantifiedExpr(q) => { + let Some(inner) = q.inner() else { return }; + self.collect_reference_diagnostics(&inner); + } + Expr::FieldExpr(f) => { + let Some(value) = f.value() else { return }; + self.collect_reference_diagnostics(&value); + } + Expr::AnonymousNode(_) => {} } - Expr::CapturedExpr(cap) => { - let Some(inner) = cap.inner() else { return }; - collect_reference_diagnostics(&inner, symbols, diagnostics); - } - Expr::QuantifiedExpr(q) => { - let Some(inner) = q.inner() else { return }; - collect_reference_diagnostics(&inner, symbols, diagnostics); - } - Expr::FieldExpr(f) => { - let Some(value) = f.value() else { return }; - collect_reference_diagnostics(&value, symbols, diagnostics); - } - Expr::AnonymousNode(_) => {} } -} -fn check_ref_diagnostic(r: &Ref, symbols: &SymbolTable<'_>, diagnostics: &mut Diagnostics) { - let Some(name_token) = r.name() else { return }; - let name = name_token.text(); + fn check_ref_diagnostic(&mut self, r: &Ref) { + let Some(name_token) = r.name() else { return }; + let name = name_token.text(); - if symbols.contains_key(name) { - return; - } + if self.symbol_table.contains_key(name) { + return; + } - diagnostics - .error( - format!("undefined reference: `{}`", name), - name_token.text_range(), - ) - .emit(); + self.resolve_diagnostics + .error( + format!("undefined reference: `{}`", name), + name_token.text_range(), + ) + .emit(); + } } From 0369dc05304e96875e3561bd6705b656a205eaf1 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:45:27 -0300 Subject: [PATCH 18/21] Add fuel state tracking to parser and query --- crates/plotnik-lib/src/parser/core.rs | 39 ++++++++++++++++++++++----- crates/plotnik-lib/src/parser/mod.rs | 16 ++++++----- crates/plotnik-lib/src/query/mod.rs | 25 +++++++++-------- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 03ab2132..5af05485 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -16,8 +16,17 @@ use crate::diagnostics::Diagnostics; use crate::Error; -const DEFAULT_EXEC_FUEL: u32 = 1_000_000; -const DEFAULT_RECURSION_FUEL: u32 = 4096; +pub const DEFAULT_EXEC_FUEL: u32 = 1_000_000; +pub const DEFAULT_RECURSION_FUEL: u32 = 4096; + +/// Fuel consumption state returned after parsing. +#[derive(Debug, Clone, Copy, Default)] +pub struct FuelState { + pub exec_initial: Option, + pub exec_remaining: Option, + pub recursion_limit: Option, + pub recursion_max_depth: u32, +} /// Tracks an open delimiter for better error messages on unclosed constructs. #[derive(Debug, Clone, Copy)] @@ -53,12 +62,18 @@ pub struct Parser<'src> { /// Loop detection fuel. Resets on bump(). Panics when exhausted. pub(super) debug_fuel: std::cell::Cell, - /// Execution fuel. Never replenishes. + /// Execution fuel initial value. None = infinite. + exec_fuel_initial: Option, + + /// Execution fuel remaining. Never replenishes. exec_fuel_remaining: Option, - /// Recursion depth limit. + /// Recursion depth limit. None = infinite. recursion_fuel_limit: Option, + /// Maximum recursion depth reached during parsing. + max_depth: u32, + /// Fatal error that stops parsing (fuel exhaustion). fatal_error: Option, } @@ -76,14 +91,17 @@ impl<'src> Parser<'src> { last_diagnostic_pos: None, delimiter_stack: Vec::with_capacity(8), debug_fuel: std::cell::Cell::new(256), + exec_fuel_initial: Some(DEFAULT_EXEC_FUEL), exec_fuel_remaining: Some(DEFAULT_EXEC_FUEL), recursion_fuel_limit: Some(DEFAULT_RECURSION_FUEL), + max_depth: 0, fatal_error: None, } } /// Set execution fuel limit. None = infinite. pub fn with_exec_fuel(mut self, limit: Option) -> Self { + self.exec_fuel_initial = limit; self.exec_fuel_remaining = limit; self } @@ -94,12 +112,18 @@ impl<'src> Parser<'src> { self } - pub fn finish(mut self) -> Result<(GreenNode, Diagnostics), Error> { + pub fn finish(mut self) -> Result<(GreenNode, Diagnostics, FuelState), Error> { self.drain_trivia(); if let Some(err) = self.fatal_error { return Err(err); } - Ok((self.builder.finish(), self.diagnostics)) + let fuel_state = FuelState { + exec_initial: self.exec_fuel_initial, + exec_remaining: self.exec_fuel_remaining, + recursion_limit: self.recursion_fuel_limit, + recursion_max_depth: self.max_depth, + }; + Ok((self.builder.finish(), self.diagnostics, fuel_state)) } /// Check if a fatal error has occurred. @@ -360,6 +384,9 @@ impl<'src> Parser<'src> { return false; } self.depth += 1; + if self.depth > self.max_depth { + self.max_depth = self.depth; + } self.reset_debug_fuel(); true } diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index ca365470..d51342f4 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -47,20 +47,24 @@ pub use ast::{ NegatedField, QuantifiedExpr, Ref, Root, SeqExpr, Type, }; -pub use core::Parser; +pub use core::{DEFAULT_EXEC_FUEL, DEFAULT_RECURSION_FUEL, FuelState, Parser}; -use crate::PassResult; +use crate::Error; +use crate::diagnostics::Diagnostics; use lexer::lex; +/// Result of parsing: AST, diagnostics, and fuel state. +pub type ParseResult = Result<(T, Diagnostics, FuelState), Error>; + /// Main entry point. Returns Err on fuel exhaustion. -pub fn parse(source: &str) -> PassResult { +pub fn parse(source: &str) -> ParseResult { parse_with_parser(Parser::new(source, lex(source))) } /// Parse with a pre-configured parser (for custom fuel limits). -pub(crate) fn parse_with_parser(mut parser: Parser) -> PassResult { +pub(crate) fn parse_with_parser(mut parser: Parser) -> ParseResult { parser.parse_root(); - let (cst, diagnostics) = parser.finish()?; + let (cst, diagnostics, fuel_state) = parser.finish()?; let root = Root::cast(SyntaxNode::new_root(cst)).expect("parser always produces Root"); - Ok((root, diagnostics)) + Ok((root, diagnostics, fuel_state)) } diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 8bb172d2..317a875c 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -31,15 +31,15 @@ use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; -use crate::parser::{self, Parser, Root, SyntaxNode, ast}; +use crate::parser::{self, FuelState, Parser, Root, SyntaxNode, ast}; use shapes::ShapeCardinality; use symbol_table::SymbolTable; /// Builder for configuring and creating a [`Query`]. pub struct QueryBuilder<'a> { source: &'a str, - exec_fuel: Option>, - recursion_fuel: Option>, + exec_fuel: Option, + recursion_fuel: Option, } impl<'a> QueryBuilder<'a> { @@ -57,7 +57,7 @@ impl<'a> QueryBuilder<'a> { /// Execution fuel never replenishes. It protects against large inputs. /// Returns error when exhausted. pub fn with_exec_fuel(mut self, limit: Option) -> Self { - self.exec_fuel = Some(limit); + self.exec_fuel = limit; self } @@ -66,7 +66,7 @@ impl<'a> QueryBuilder<'a> { /// Recursion fuel restores when exiting recursion. It protects against /// deeply nested input. Returns error when exhausted. pub fn with_recursion_fuel(mut self, limit: Option) -> Self { - self.recursion_fuel = Some(limit); + self.recursion_fuel = limit; self } @@ -95,6 +95,7 @@ pub struct Query<'a> { ast: Root, symbol_table: SymbolTable<'a>, shape_cardinality_table: HashMap, + fuel_state: FuelState, // Diagnostics per pass parse_diagnostics: Diagnostics, alt_kind_diagnostics: Diagnostics, @@ -131,6 +132,7 @@ impl<'a> Query<'a> { ast: empty_root(), symbol_table: SymbolTable::default(), shape_cardinality_table: HashMap::new(), + fuel_state: FuelState::default(), parse_diagnostics: Diagnostics::new(), alt_kind_diagnostics: Diagnostics::new(), resolve_diagnostics: Diagnostics::new(), @@ -139,25 +141,22 @@ impl<'a> Query<'a> { } } - fn parse( - &mut self, - exec_fuel: Option>, - recursion_fuel: Option>, - ) -> Result<()> { + fn parse(&mut self, exec_fuel: Option, recursion_fuel: Option) -> Result<()> { let tokens = lex(self.source); let mut parser = Parser::new(self.source, tokens); if let Some(limit) = exec_fuel { - parser = parser.with_exec_fuel(limit); + parser = parser.with_exec_fuel(Some(limit)); } if let Some(limit) = recursion_fuel { - parser = parser.with_recursion_fuel(limit); + parser = parser.with_recursion_fuel(Some(limit)); } - let (ast, diagnostics) = parser::parse_with_parser(parser)?; + let (ast, diagnostics, fuel_state) = parser::parse_with_parser(parser)?; self.ast = ast; self.parse_diagnostics = diagnostics; + self.fuel_state = fuel_state; Ok(()) } From 8b2c49a552591785082b5a5560f8d177c931004a Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:51:43 -0300 Subject: [PATCH 19/21] Refactor parser to use ParseResult and simplify parsing --- crates/plotnik-lib/src/parser/core.rs | 25 +++++++++++++++++++++++-- crates/plotnik-lib/src/parser/mod.rs | 24 +----------------------- crates/plotnik-lib/src/query/mod.rs | 24 +++++++++++------------- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 5af05485..452da50d 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -9,8 +9,9 @@ use rowan::{Checkpoint, GreenNode, GreenNodeBuilder, TextRange, TextSize}; +use super::ast::Root; use super::cst::token_sets::ROOT_EXPR_FIRST; -use super::cst::{SyntaxKind, TokenSet}; +use super::cst::{SyntaxKind, SyntaxNode, TokenSet}; use super::lexer::{Token, token_text}; use crate::diagnostics::Diagnostics; @@ -28,6 +29,14 @@ pub struct FuelState { pub recursion_max_depth: u32, } +/// Result of a successful parse operation. +#[derive(Debug)] +pub struct ParseResult { + pub root: Root, + pub diagnostics: Diagnostics, + pub fuel_state: FuelState, +} + /// Tracks an open delimiter for better error messages on unclosed constructs. #[derive(Debug, Clone, Copy)] pub(super) struct OpenDelimiter { @@ -112,7 +121,19 @@ impl<'src> Parser<'src> { self } - pub fn finish(mut self) -> Result<(GreenNode, Diagnostics, FuelState), Error> { + /// Parse the input and return the result. + pub fn parse(mut self) -> Result { + self.parse_root(); + let (cst, diagnostics, fuel_state) = self.finish()?; + let root = Root::cast(SyntaxNode::new_root(cst)).expect("parser always produces Root"); + Ok(ParseResult { + root, + diagnostics, + fuel_state, + }) + } + + fn finish(mut self) -> Result<(GreenNode, Diagnostics, FuelState), Error> { self.drain_trivia(); if let Some(err) = self.fatal_error { return Err(err); diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index d51342f4..5bf6d2cf 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -38,33 +38,11 @@ mod lexer_tests; #[cfg(test)] mod tests; -// Re-exports from cst (was syntax_kind) pub use cst::{SyntaxKind, SyntaxNode, SyntaxToken}; -// Re-exports from ast (was nodes) pub use ast::{ AltExpr, AltKind, Anchor, AnonymousNode, Branch, CapturedExpr, Def, Expr, FieldExpr, NamedNode, NegatedField, QuantifiedExpr, Ref, Root, SeqExpr, Type, }; -pub use core::{DEFAULT_EXEC_FUEL, DEFAULT_RECURSION_FUEL, FuelState, Parser}; - -use crate::Error; -use crate::diagnostics::Diagnostics; -use lexer::lex; - -/// Result of parsing: AST, diagnostics, and fuel state. -pub type ParseResult = Result<(T, Diagnostics, FuelState), Error>; - -/// Main entry point. Returns Err on fuel exhaustion. -pub fn parse(source: &str) -> ParseResult { - parse_with_parser(Parser::new(source, lex(source))) -} - -/// Parse with a pre-configured parser (for custom fuel limits). -pub(crate) fn parse_with_parser(mut parser: Parser) -> ParseResult { - parser.parse_root(); - let (cst, diagnostics, fuel_state) = parser.finish()?; - let root = Root::cast(SyntaxNode::new_root(cst)).expect("parser always produces Root"); - Ok((root, diagnostics, fuel_state)) -} +pub use core::{DEFAULT_EXEC_FUEL, DEFAULT_RECURSION_FUEL, FuelState, ParseResult, Parser}; diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 317a875c..5f077b31 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -31,7 +31,7 @@ use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; -use crate::parser::{self, FuelState, Parser, Root, SyntaxNode, ast}; +use crate::parser::{FuelState, ParseResult, Parser, Root, SyntaxNode, ast}; use shapes::ShapeCardinality; use symbol_table::SymbolTable; @@ -143,18 +143,16 @@ impl<'a> Query<'a> { fn parse(&mut self, exec_fuel: Option, recursion_fuel: Option) -> Result<()> { let tokens = lex(self.source); - let mut parser = Parser::new(self.source, tokens); - - if let Some(limit) = exec_fuel { - parser = parser.with_exec_fuel(Some(limit)); - } - - if let Some(limit) = recursion_fuel { - parser = parser.with_recursion_fuel(Some(limit)); - } - - let (ast, diagnostics, fuel_state) = parser::parse_with_parser(parser)?; - self.ast = ast; + let parser = Parser::new(self.source, tokens) + .with_exec_fuel(exec_fuel) + .with_recursion_fuel(recursion_fuel); + + let ParseResult { + root, + diagnostics, + fuel_state, + } = parser.parse()?; + self.ast = root; self.parse_diagnostics = diagnostics; self.fuel_state = fuel_state; Ok(()) From b203b1e83b6f71f9b09f73018ec16bc87a0caf5c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 11:59:09 -0300 Subject: [PATCH 20/21] Simplify docstrings and remove verbose comments --- crates/plotnik-lib/src/parser/core.rs | 82 ++-------------- crates/plotnik-lib/src/parser/cst.rs | 118 ++++------------------- crates/plotnik-lib/src/parser/grammar.rs | 19 +--- crates/plotnik-lib/src/query/dump.rs | 2 + crates/plotnik-lib/src/query/mod.rs | 6 +- crates/plotnik-lib/src/query/printer.rs | 2 + 6 files changed, 43 insertions(+), 186 deletions(-) diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 452da50d..cdd3dde5 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -1,11 +1,4 @@ -//! Core parser state machine and low-level operations. -//! -//! This module contains the `Parser` struct and all foundational methods: -//! - Token access and lookahead -//! - Trivia buffering and attachment -//! - Tree construction via Rowan -//! - Diagnostic recording and recovery -//! - Fuel-based limits (debug, execution, recursion) +//! Parser state machine and low-level operations. use rowan::{Checkpoint, GreenNode, GreenNodeBuilder, TextRange, TextSize}; @@ -20,7 +13,6 @@ use crate::Error; pub const DEFAULT_EXEC_FUEL: u32 = 1_000_000; pub const DEFAULT_RECURSION_FUEL: u32 = 4096; -/// Fuel consumption state returned after parsing. #[derive(Debug, Clone, Copy, Default)] pub struct FuelState { pub exec_initial: Option, @@ -29,7 +21,6 @@ pub struct FuelState { pub recursion_max_depth: u32, } -/// Result of a successful parse operation. #[derive(Debug)] pub struct ParseResult { pub root: Root, @@ -37,53 +28,30 @@ pub struct ParseResult { pub fuel_state: FuelState, } -/// Tracks an open delimiter for better error messages on unclosed constructs. #[derive(Debug, Clone, Copy)] pub(super) struct OpenDelimiter { - #[allow(dead_code)] // useful for future mismatch detection (e.g., `(]`) + #[allow(dead_code)] // for future mismatch detection (e.g., `(]`) pub kind: SyntaxKind, pub span: TextRange, } -/// Parser state machine. -/// -/// The token stream is processed left-to-right. Trivia tokens (whitespace, comments) -/// are buffered separately and flushed as leading trivia when starting a new node. -/// This gives predictable trivia attachment without backtracking. +/// Trivia tokens (whitespace, comments) are buffered and flushed as leading trivia +/// when starting a new node. This gives predictable trivia attachment without backtracking. pub struct Parser<'src> { pub(super) source: &'src str, pub(super) tokens: Vec, - /// Current position in `tokens`. Monotonically increases. pub(super) pos: usize, - /// Trivia accumulated since last non-trivia token. - /// Drained into tree at `start_node()` / `checkpoint()`. pub(super) trivia_buffer: Vec, pub(super) builder: GreenNodeBuilder<'static>, pub(super) diagnostics: Diagnostics, - /// Current recursion depth. pub(super) depth: u32, - /// Last diagnostic position - used to suppress cascading diagnostics at same span pub(super) last_diagnostic_pos: Option, - /// Stack of open delimiters for "unclosed X started here" messages. pub(super) delimiter_stack: Vec, - - // Fuel limits - /// Loop detection fuel. Resets on bump(). Panics when exhausted. pub(super) debug_fuel: std::cell::Cell, - - /// Execution fuel initial value. None = infinite. exec_fuel_initial: Option, - - /// Execution fuel remaining. Never replenishes. exec_fuel_remaining: Option, - - /// Recursion depth limit. None = infinite. recursion_fuel_limit: Option, - - /// Maximum recursion depth reached during parsing. max_depth: u32, - - /// Fatal error that stops parsing (fuel exhaustion). fatal_error: Option, } @@ -108,20 +76,17 @@ impl<'src> Parser<'src> { } } - /// Set execution fuel limit. None = infinite. pub fn with_exec_fuel(mut self, limit: Option) -> Self { self.exec_fuel_initial = limit; self.exec_fuel_remaining = limit; self } - /// Set recursion depth limit. None = infinite. pub fn with_recursion_fuel(mut self, limit: Option) -> Self { self.recursion_fuel_limit = limit; self } - /// Parse the input and return the result. pub fn parse(mut self) -> Result { self.parse_root(); let (cst, diagnostics, fuel_state) = self.finish()?; @@ -147,12 +112,11 @@ impl<'src> Parser<'src> { Ok((self.builder.finish(), self.diagnostics, fuel_state)) } - /// Check if a fatal error has occurred. pub(super) fn has_fatal_error(&self) -> bool { self.fatal_error.is_some() } - /// Current token kind. Returns `Error` at EOF (acts as sentinel). + /// Returns `Error` at EOF (acts as sentinel). pub(super) fn current(&self) -> SyntaxKind { self.nth(0) } @@ -161,7 +125,6 @@ impl<'src> Parser<'src> { self.debug_fuel.set(256); } - /// Lookahead by `n` tokens (0 = current). Consumes debug fuel (panics if stuck). pub(super) fn nth(&self, lookahead: usize) -> SyntaxKind { self.ensure_progress(); @@ -170,7 +133,6 @@ impl<'src> Parser<'src> { .map_or(SyntaxKind::Error, |t| t.kind) } - /// Consume execution fuel. Sets fatal error if exhausted. fn consume_exec_fuel(&mut self) { if let Some(ref mut remaining) = self.exec_fuel_remaining { if *remaining == 0 { @@ -197,7 +159,6 @@ impl<'src> Parser<'src> { self.pos >= self.tokens.len() } - /// Check if at EOF or fatal error occurred. pub(super) fn should_stop(&self) -> bool { self.eof() || self.has_fatal_error() } @@ -210,13 +171,12 @@ impl<'src> Parser<'src> { set.contains(self.current()) } - /// Peek past trivia. Buffers trivia tokens for later attachment. pub(super) fn peek(&mut self) -> SyntaxKind { self.skip_trivia_to_buffer(); self.current() } - /// Lookahead `n` non-trivia tokens. Used for LL(k) decisions like `field:`. + /// LL(k) lookahead past trivia. pub(super) fn peek_nth(&mut self, n: usize) -> SyntaxKind { self.skip_trivia_to_buffer(); let mut count = 0; @@ -253,14 +213,12 @@ impl<'src> Parser<'src> { self.drain_trivia(); } - /// Start node, attaching any buffered trivia first. pub(super) fn start_node(&mut self, kind: SyntaxKind) { self.drain_trivia(); self.builder.start_node(kind.into()); } - /// Wrap previously-parsed content. Used for quantifiers: parse `(foo)`, then - /// see `*`, wrap retroactively into `Quantifier(NamedNode(...), Star)`. + /// Wrap previously-parsed content using checkpoint. pub(super) fn start_node_at(&mut self, checkpoint: Checkpoint, kind: SyntaxKind) { self.builder.start_node_at(checkpoint, kind.into()); } @@ -269,13 +227,11 @@ impl<'src> Parser<'src> { self.builder.finish_node(); } - /// Checkpoint before parsing. If we later need to wrap, use `start_node_at`. pub(super) fn checkpoint(&mut self) -> Checkpoint { self.drain_trivia(); self.builder.checkpoint() } - /// Consume current token into tree. Resets debug fuel, consumes exec fuel. pub(super) fn bump(&mut self) { assert!(!self.eof(), "bump called at EOF"); @@ -289,7 +245,6 @@ impl<'src> Parser<'src> { self.pos += 1; } - /// Skip current token without adding to tree. Used for invalid separators. pub(super) fn skip_token(&mut self) { assert!(!self.eof(), "skip_token called at EOF"); @@ -309,7 +264,7 @@ impl<'src> Parser<'src> { } } - /// Expect token. On mismatch: emit diagnostic but don't consume (allows parent recovery). + /// On mismatch: emit diagnostic but don't consume (allows parent recovery). pub(super) fn expect(&mut self, kind: SyntaxKind, what: &str) -> bool { if self.eat(kind) { return true; @@ -328,8 +283,6 @@ impl<'src> Parser<'src> { self.diagnostics.error(message, range).emit(); } - /// Wrap unexpected token in Error node and consume it. - /// Ensures progress even on garbage input. pub(super) fn error_and_bump(&mut self, message: &str) { self.error(message); if !self.eof() { @@ -339,9 +292,7 @@ impl<'src> Parser<'src> { } } - /// Skip tokens until we hit a recovery point. Wraps skipped tokens in Error node. - /// If already at recovery token, just emits diagnostic without consuming. - #[allow(dead_code)] // Used by future grammar rules (named expressions) + #[allow(dead_code)] pub(super) fn error_recover(&mut self, message: &str, recovery: TokenSet) { if self.at_set(recovery) || self.should_stop() { self.error(message); @@ -356,13 +307,6 @@ impl<'src> Parser<'src> { self.finish_node(); } - /// Synchronize to a token that can start a new definition at root level. - /// Consumes tokens into an Error node until we see: - /// - `UpperIdent` followed by `=` (named definition) - /// - A token in EXPR_FIRST (potential anonymous definition) - /// - EOF - /// - /// Returns true if any tokens were consumed. pub(super) fn synchronize_to_def_start(&mut self) -> bool { if self.should_stop() { return false; @@ -382,8 +326,6 @@ impl<'src> Parser<'src> { true } - /// Check if current position looks like the start of a definition. - /// Uses peek() to skip trivia before checking. fn at_def_start(&mut self) -> bool { let kind = self.peek(); // Named def: UpperIdent followed by = @@ -417,7 +359,6 @@ impl<'src> Parser<'src> { self.reset_debug_fuel(); } - /// Push an opening delimiter onto the stack for tracking unclosed constructs. pub(super) fn push_delimiter(&mut self, kind: SyntaxKind) { self.delimiter_stack.push(OpenDelimiter { kind, @@ -425,12 +366,10 @@ impl<'src> Parser<'src> { }); } - /// Pop the most recent opening delimiter from the stack. pub(super) fn pop_delimiter(&mut self) -> Option { self.delimiter_stack.pop() } - /// Record a diagnostic with a related location (e.g., where an unclosed delimiter started). pub(super) fn error_with_related( &mut self, message: impl Into, @@ -449,8 +388,6 @@ impl<'src> Parser<'src> { .emit(); } - /// Get the end position of the last non-trivia token before current position. - /// Used when trivia may have been buffered ahead but we need the expression's end. pub(super) fn last_non_trivia_end(&self) -> Option { for i in (0..self.pos).rev() { if !self.tokens[i].kind.is_trivia() { @@ -460,7 +397,6 @@ impl<'src> Parser<'src> { None } - /// Record a diagnostic with an associated fix suggestion. pub(super) fn error_with_fix( &mut self, range: TextRange, diff --git a/crates/plotnik-lib/src/parser/cst.rs b/crates/plotnik-lib/src/parser/cst.rs index 6f50bc8a..e82020ee 100644 --- a/crates/plotnik-lib/src/parser/cst.rs +++ b/crates/plotnik-lib/src/parser/cst.rs @@ -1,35 +1,16 @@ //! Syntax kinds for the query language. //! -//! This module defines all token and node kinds used in the syntax tree, -//! along with a `TokenSet` bitset for efficient membership testing in the parser. -//! -//! ## Architecture -//! -//! The `SyntaxKind` enum has a dual role: -//! - Token kinds (terminals): produced by the lexer, represent atomic text spans -//! - Node kinds (non-terminals): created by the parser, represent composite structures -//! -//! Rowan requires a `Language` trait implementation to convert between our `SyntaxKind` -//! and its internal `rowan::SyntaxKind` (a newtype over `u16`). That's what `QLang` provides. -//! -//! Logos is derived directly on this enum; node kinds simply lack token/regex attributes. +//! `SyntaxKind` serves dual roles: token kinds (from lexer) and node kinds (from parser). +//! Logos derives token recognition; node kinds lack token/regex attributes. +//! `QLang` implements Rowan's `Language` trait for tree construction. #![allow(dead_code)] // Some items are for future use use logos::Logos; use rowan::Language; -/// All kinds of tokens and nodes in the syntax tree. -/// -/// ## Layout -/// -/// Variants are ordered: tokens first, then nodes, then `__LAST` sentinel. -/// The `#[repr(u16)]` ensures we can safely transmute from the discriminant. -/// -/// ## Token vs Node distinction -/// -/// The parser only ever builds nodes; tokens come from the lexer. -/// A token's text is sliced from source on demand via its span. +/// All token and node kinds. Tokens first, then nodes, then `__LAST` sentinel. +/// `#[repr(u16)]` enables safe transmute in `kind_from_raw`. #[derive(Logos, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u16)] pub enum SyntaxKind { @@ -51,8 +32,7 @@ pub enum SyntaxKind { #[token("}")] BraceClose, - /// Double colon for type annotations: `@name :: Type` - /// Must be defined before single Colon for correct precedence + /// `::` for type annotations. Defined before `Colon` for correct precedence. #[token("::")] DoubleColon, @@ -80,7 +60,7 @@ pub enum SyntaxKind { #[token("?")] Question, - /// Non-greedy `*?` quantifier (matches minimum repetitions) + /// Non-greedy `*?` quantifier #[token("*?")] StarQuestion, @@ -104,42 +84,33 @@ pub enum SyntaxKind { #[token("|")] Pipe, - /// String literal (double or single quoted) - split by lexer post-processing + /// String literal (split by lexer into quote + content + quote) #[regex(r#""(?:[^"\\]|\\.)*""#)] #[regex(r"'(?:[^'\\]|\\.)*'")] StringLiteral, - /// Double quote character (from string literal splitting) DoubleQuote, - - /// Single quote character (from string literal splitting) SingleQuote, - - /// String content between quotes (from string literal splitting) + /// String content between quotes StrVal, - /// ERROR keyword for matching parser error nodes #[token("ERROR")] KwError, - /// MISSING keyword for matching error recovery nodes #[token("MISSING")] KwMissing, - /// Loose identifier for all naming contexts (definitions, fields, node types, etc.) - /// Accepts dots and hyphens for tree-sitter compatibility; parser validates per context. - /// Defined after KwError/KwMissing so keywords take precedence. + /// Identifier. Accepts dots/hyphens for tree-sitter compat; parser validates per context. + /// Defined after keywords so they take precedence. #[regex(r"[a-zA-Z][a-zA-Z0-9_.\-]*")] Id, #[token(".")] Dot, - /// At sign for captures: `@` #[token("@")] At, - /// Horizontal whitespace (spaces, tabs) #[regex(r"[ \t]+")] Whitespace, @@ -153,49 +124,33 @@ pub enum SyntaxKind { #[regex(r"/\*(?:[^*]|\*[^/])*\*/")] BlockComment, - /// XML-like tags explicitly matched as errors (common LLM mistake) + /// XML-like tags matched as errors (common LLM output) #[regex(r"<[a-zA-Z_:][a-zA-Z0-9_:\.\-]*(?:\s+[^>]*)?>")] #[regex(r"")] #[regex(r"<[a-zA-Z_:][a-zA-Z0-9_:\.\-]*\s*/\s*>")] XMLGarbage, - /// Tree-sitter predicate syntax (unsupported, for clear error messages) - /// Matches #eq?, #match?, #set!, #is?, etc. + /// Tree-sitter predicates (unsupported) #[regex(r"#[a-zA-Z_][a-zA-Z0-9_]*[?!]?")] Predicate, - /// Consecutive unrecognized characters coalesced into one token + /// Coalesced unrecognized characters Garbage, - /// Generic error token Error, - /// Root node containing the entire query + // --- Node kinds (non-terminals) --- Root, - /// Tree expression: `(type children...)`, `(_)`, `(ERROR)`, `(MISSING ...)` Tree, - /// Reference to user-defined expression: `(Expr)` where Expr is PascalCase Ref, - /// String literal node containing quote tokens and content Str, - /// Field specification: `name: expr` Field, - /// Capture wrapping an expression: `(expr) @name` or `(expr) @name :: Type` Capture, - /// Type annotation: `::Type` after a capture Type, - /// Quantifier wrapping an expression, e.g., `(expr)*` becomes `Quantifier { Tree, Star }` Quantifier, - /// Sibling sequence: `{expr1 expr2 ...}` Seq, - /// Choice between alternatives: `[a b c]` Alt, - /// Branch in a tagged alternation: `Label: expr` Branch, - /// Wildcard: `_` matches any node Wildcard, - /// Anchor: `.` constrains position relative to siblings Anchor, - /// Negated field assertion: `!field` asserts field is absent NegatedField, - /// Named expression definition: `Name = expr` Def, // Must be last - used for bounds checking in `kind_from_raw` @@ -206,16 +161,11 @@ pub enum SyntaxKind { use SyntaxKind::*; impl SyntaxKind { - /// Returns `true` if this is a trivia token (whitespace or comment). - /// - /// Trivia tokens are buffered during parsing and attached to the next node - /// as leading trivia. This preserves formatting information in the CST. #[inline] pub fn is_trivia(self) -> bool { matches!(self, Whitespace | Newline | LineComment | BlockComment) } - /// Returns `true` if this is an error token. #[inline] pub fn is_error(self) -> bool { matches!(self, Error | XMLGarbage | Garbage | Predicate) @@ -229,10 +179,7 @@ impl From for rowan::SyntaxKind { } } -/// Language tag for parameterizing Rowan's tree types. -/// -/// This is a zero-sized enum (uninhabited) used purely as a type-level marker. -/// Rowan uses it to associate syntax trees with our `SyntaxKind`. +/// Language tag for Rowan's tree types. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum QLang {} @@ -255,12 +202,7 @@ pub type SyntaxNode = rowan::SyntaxNode; pub type SyntaxToken = rowan::SyntaxToken; pub type SyntaxElement = rowan::NodeOrToken; -/// A set of `SyntaxKind`s implemented as a 64-bit bitset. -/// -/// ## Usage -/// -/// Used throughout the parser for O(1) membership testing of FIRST/FOLLOW/RECOVERY sets. -/// The limitation is 64 variants max, which is enforced by compile-time asserts in `new()`. +/// 64-bit bitset of `SyntaxKind`s for O(1) membership testing. #[derive(Clone, Copy, PartialEq, Eq)] pub struct TokenSet(u64); @@ -268,8 +210,6 @@ impl TokenSet { /// Creates an empty token set. pub const EMPTY: TokenSet = TokenSet(0); - /// Creates a token set from a slice of kinds. - /// /// Panics at compile time if any kind's discriminant >= 64. #[inline] pub const fn new(kinds: &[SyntaxKind]) -> Self { @@ -284,7 +224,6 @@ impl TokenSet { TokenSet(bits) } - /// Creates a token set containing exactly one kind. #[inline] pub const fn single(kind: SyntaxKind) -> Self { let kind = kind as u16; @@ -292,7 +231,6 @@ impl TokenSet { TokenSet(1 << kind) } - /// Returns `true` if the set contains the given kind. #[inline] pub const fn contains(&self, kind: SyntaxKind) -> bool { let kind = kind as u16; @@ -302,7 +240,6 @@ impl TokenSet { self.0 & (1 << kind) != 0 } - /// Returns the union of two token sets. #[inline] pub const fn union(self, other: TokenSet) -> TokenSet { TokenSet(self.0 | other.0) @@ -322,19 +259,11 @@ impl std::fmt::Debug for TokenSet { } } -/// Pre-defined token sets used throughout the parser. -/// -/// ## Recovery sets -/// -/// Recovery sets follow matklad's resilient parsing approach: when the parser -/// encounters an unexpected token, it consumes tokens until it finds one in -/// the recovery set (typically the FOLLOW set of ancestor productions). -/// This prevents cascading errors and allows parsing to continue. +/// Pre-defined token sets for the parser. pub mod token_sets { use super::*; - /// Tokens that can start an expression (FIRST set of the expression production). - /// Note: At is not included because captures wrap expressions, they don't start them. + /// FIRST set of expr. `At` excluded (captures wrap, not start). pub const EXPR_FIRST: TokenSet = TokenSet::new(&[ ParenOpen, BracketOpen, @@ -349,9 +278,7 @@ pub mod token_sets { KwMissing, ]); - /// Tokens that can start a valid expression at root level (anonymous definition). - /// Excludes bare Id (only valid as node type inside parens), Dot (anchor), - /// and Negation (negated field) which only make sense inside tree context. + /// FIRST set for root-level expressions. Excludes `Dot`/`Negation` (tree-internal). pub const ROOT_EXPR_FIRST: TokenSet = TokenSet::new(&[ ParenOpen, BracketOpen, @@ -364,7 +291,6 @@ pub mod token_sets { KwMissing, ]); - /// Quantifier tokens that can follow a pattern. pub const QUANTIFIERS: TokenSet = TokenSet::new(&[ Star, Plus, @@ -374,10 +300,7 @@ pub mod token_sets { QuestionQuestion, ]); - /// Trivia tokens. pub const TRIVIA: TokenSet = TokenSet::new(&[Whitespace, Newline, LineComment, BlockComment]); - - /// Invalid separator tokens (comma, pipe) - for error recovery pub const SEPARATORS: TokenSet = TokenSet::new(&[Comma, Pipe]); pub const TREE_RECOVERY: TokenSet = TokenSet::new(&[ParenOpen, BracketOpen, BraceOpen]); @@ -389,7 +312,6 @@ pub mod token_sets { pub const ROOT_RECOVERY: TokenSet = TokenSet::new(&[ParenOpen, BracketOpen, BraceOpen, Id]); - /// Recovery set for named definitions (Name = ...) pub const DEF_RECOVERY: TokenSet = TokenSet::new(&[ParenOpen, BracketOpen, BraceOpen, Id, Equals]); diff --git a/crates/plotnik-lib/src/parser/grammar.rs b/crates/plotnik-lib/src/parser/grammar.rs index 6915e6cd..86d0b807 100644 --- a/crates/plotnik-lib/src/parser/grammar.rs +++ b/crates/plotnik-lib/src/parser/grammar.rs @@ -153,11 +153,8 @@ impl Parser<'_> { self.exit_recursion(); } - /// Tree expression: `(type ...)`, `(_ ...)`, `(ERROR)`, `(MISSING ...)`. - /// Also handles supertype/subtype: `(expression/binary_expression)`. - /// Parse a tree expression `(type ...)` or a reference `(RefName)`. - /// PascalCase identifiers without children become `Ref` nodes. - /// PascalCase identifiers with children emit an error but parse as `Tree`. + /// `(type ...)` | `(_ ...)` | `(ERROR)` | `(MISSING ...)` | `(RefName)` | `(expr/subtype)` + /// PascalCase without children → Ref; with children → error but parses as Tree. fn parse_tree(&mut self) { let checkpoint = self.checkpoint(); self.push_delimiter(SyntaxKind::ParenOpen); @@ -470,8 +467,7 @@ impl Parser<'_> { self.finish_node(); } - /// String literal: `"if"`, `'+'`, etc. - /// Parses: quote + optional content + quote into a Str node + /// `"if"` | `'+'` fn parse_str(&mut self) { self.start_node(SyntaxKind::Str); self.bump_string_tokens(); @@ -493,9 +489,7 @@ impl Parser<'_> { self.bump(); } - /// Parse capture suffix: `@name` or `@name :: Type` - /// Called after the expression to capture has already been parsed. - /// Expects current token to be `At`, followed by `Id`. + /// `@name` | `@name :: Type` fn parse_capture_suffix(&mut self) { self.bump(); // consume At @@ -562,7 +556,7 @@ impl Parser<'_> { self.finish_node(); } - /// Anchor for anonymous nodes: `.` + /// `.` anchor fn parse_anchor(&mut self) { self.start_node(SyntaxKind::Anchor); self.expect(SyntaxKind::Dot, "'.' anchor"); @@ -834,7 +828,6 @@ impl Parser<'_> { } } -/// Convert a name to snake_case. fn to_snake_case(s: &str) -> String { let mut result = String::new(); for (i, c) in s.chars().enumerate() { @@ -850,7 +843,6 @@ fn to_snake_case(s: &str) -> String { result } -/// Convert a name to PascalCase. fn to_pascal_case(s: &str) -> String { let mut result = String::new(); let mut capitalize_next = true; @@ -867,7 +859,6 @@ fn to_pascal_case(s: &str) -> String { result } -/// Capitalize the first letter of a string. fn capitalize_first(s: &str) -> String { assert_nonempty(s); let mut chars = s.chars(); diff --git a/crates/plotnik-lib/src/query/dump.rs b/crates/plotnik-lib/src/query/dump.rs index 4e7ad1a2..1f7ed97b 100644 --- a/crates/plotnik-lib/src/query/dump.rs +++ b/crates/plotnik-lib/src/query/dump.rs @@ -1,3 +1,5 @@ +//! Test-only dump methods for query inspection. + #[cfg(test)] mod test_helpers { use crate::Query; diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 5f077b31..e522f229 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -1,4 +1,8 @@ -//! Query processing: parsing, analysis, and validation pipeline. +//! Query processing pipeline. +//! +//! Stages: parse → alt_kinds → symbol_table → recursion → shapes. +//! Each stage populates its own diagnostics. Use `is_valid()` to check +//! if any stage produced errors. mod dump; mod invariants; diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index 3d3d0f34..2f02712e 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -1,3 +1,5 @@ +//! AST/CST pretty-printer for debugging and test snapshots. + use std::fmt::Write; use indexmap::IndexSet; From b09fd4928d00311ec09fb9379174157134e73a6c Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 5 Dec 2025 12:37:52 -0300 Subject: [PATCH 21/21] Refactor `Query` construction and analysis pipeline --- crates/plotnik-cli/src/commands/debug/mod.rs | 3 +- crates/plotnik-lib/src/lib.rs | 9 +- crates/plotnik-lib/src/parser/ast_tests.rs | 42 +++--- crates/plotnik-lib/src/parser/core.rs | 40 ++--- crates/plotnik-lib/src/parser/mod.rs | 2 +- .../tests/grammar/alternations_tests.rs | 34 ++--- .../src/parser/tests/grammar/anchors_tests.rs | 12 +- .../parser/tests/grammar/captures_tests.rs | 32 ++-- .../parser/tests/grammar/definitions_tests.rs | 22 +-- .../src/parser/tests/grammar/fields_tests.rs | 14 +- .../src/parser/tests/grammar/nodes_tests.rs | 30 ++-- .../parser/tests/grammar/quantifiers_tests.rs | 10 +- .../parser/tests/grammar/sequences_tests.rs | 20 +-- .../src/parser/tests/grammar/special_tests.rs | 22 +-- .../src/parser/tests/grammar/trivia_tests.rs | 14 +- .../parser/tests/recovery/coverage_tests.rs | 34 ++--- .../parser/tests/recovery/incomplete_tests.rs | 36 ++--- .../parser/tests/recovery/unclosed_tests.rs | 24 +-- .../parser/tests/recovery/unexpected_tests.rs | 48 +++--- .../parser/tests/recovery/validation_tests.rs | 124 +++++++-------- .../plotnik-lib/src/query/alt_kinds_tests.rs | 14 +- crates/plotnik-lib/src/query/mod.rs | 141 +++++++++--------- crates/plotnik-lib/src/query/mod_tests.rs | 8 +- crates/plotnik-lib/src/query/printer_tests.rs | 40 ++--- .../plotnik-lib/src/query/recursion_tests.rs | 66 ++++---- crates/plotnik-lib/src/query/shapes_tests.rs | 54 +++---- .../src/query/symbol_table_tests.rs | 36 ++--- 27 files changed, 451 insertions(+), 480 deletions(-) diff --git a/crates/plotnik-cli/src/commands/debug/mod.rs b/crates/plotnik-cli/src/commands/debug/mod.rs index d9e5c93d..23603f88 100644 --- a/crates/plotnik-cli/src/commands/debug/mod.rs +++ b/crates/plotnik-cli/src/commands/debug/mod.rs @@ -39,7 +39,7 @@ pub fn run(args: DebugArgs) { }; let query = query_source.as_ref().map(|src| { - Query::new(src).unwrap_or_else(|e| { + Query::try_from(src).unwrap_or_else(|e| { eprintln!("error: {}", e); std::process::exit(1); }) @@ -98,6 +98,7 @@ pub fn run(args: DebugArgs) { { let src = query_source.as_ref().unwrap(); eprint!("{}", q.diagnostics().render_colored(src, args.color)); + std::process::exit(1); } } diff --git a/crates/plotnik-lib/src/lib.rs b/crates/plotnik-lib/src/lib.rs index 0e145f8e..89202b12 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -10,11 +10,8 @@ //! (assignment left: (Expr) @lhs right: (Expr) @rhs) //! "#; //! -//! let query = Query::new(source).expect("valid query"); -//! -//! if !query.is_valid() { -//! eprintln!("{}", query.diagnostics().render(source)); -//! } +//! let query = Query::try_from(source).expect("out of fuel"); +//! eprintln!("{}", query.diagnostics().render(source)); //! ``` #![cfg_attr(coverage_nightly, feature(coverage_attribute))] @@ -30,7 +27,7 @@ pub mod query; pub type PassResult = std::result::Result<(T, Diagnostics), Error>; pub use diagnostics::{Diagnostics, DiagnosticsPrinter, Severity}; -pub use query::{Query, QueryBuilder}; +pub use query::Query; /// Errors that can occur during query parsing. #[derive(Debug, Clone, thiserror::Error)] diff --git a/crates/plotnik-lib/src/parser/ast_tests.rs b/crates/plotnik-lib/src/parser/ast_tests.rs index f9252285..40c5bc7f 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -3,7 +3,7 @@ use indoc::indoc; #[test] fn simple_tree() { - let query = Query::new("(identifier)").unwrap(); + let query = Query::try_from("(identifier)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -18,7 +18,7 @@ fn nested_tree() { (function_definition name: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -31,7 +31,7 @@ fn nested_tree() { #[test] fn wildcard() { - let query = Query::new("(_)").unwrap(); + let query = Query::try_from("(_)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -42,7 +42,7 @@ fn wildcard() { #[test] fn literal() { - let query = Query::new(r#""if""#).unwrap(); + let query = Query::try_from(r#""if""#).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r#" Root @@ -53,7 +53,7 @@ fn literal() { #[test] fn capture() { - let query = Query::new("(identifier) @name").unwrap(); + let query = Query::try_from("(identifier) @name").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -65,7 +65,7 @@ fn capture() { #[test] fn capture_with_type() { - let query = Query::new("(identifier) @name :: string").unwrap(); + let query = Query::try_from("(identifier) @name :: string").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -77,7 +77,7 @@ fn capture_with_type() { #[test] fn named_definition() { - let query = Query::new("Expr = (expression)").unwrap(); + let query = Query::try_from("Expr = (expression)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -93,7 +93,7 @@ fn reference() { (call (Expr)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -107,7 +107,7 @@ fn reference() { #[test] fn alternation_unlabeled() { - let query = Query::new("[(identifier) (number)]").unwrap(); + let query = Query::try_from("[(identifier) (number)]").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -126,7 +126,7 @@ fn alternation_tagged() { [Ident: (identifier) Num: (number)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -141,7 +141,7 @@ fn alternation_tagged() { #[test] fn sequence() { - let query = Query::new("{(a) (b) (c)}").unwrap(); + let query = Query::try_from("{(a) (b) (c)}").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -155,7 +155,7 @@ fn sequence() { #[test] fn quantifier_star() { - let query = Query::new("(statement)*").unwrap(); + let query = Query::try_from("(statement)*").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -167,7 +167,7 @@ fn quantifier_star() { #[test] fn quantifier_plus() { - let query = Query::new("(statement)+").unwrap(); + let query = Query::try_from("(statement)+").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -179,7 +179,7 @@ fn quantifier_plus() { #[test] fn quantifier_optional() { - let query = Query::new("(statement)?").unwrap(); + let query = Query::try_from("(statement)?").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -191,7 +191,7 @@ fn quantifier_optional() { #[test] fn quantifier_non_greedy() { - let query = Query::new("(statement)*?").unwrap(); + let query = Query::try_from("(statement)*?").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -203,7 +203,7 @@ fn quantifier_non_greedy() { #[test] fn anchor() { - let query = Query::new("(block . (statement))").unwrap(); + let query = Query::try_from("(block . (statement))").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -216,7 +216,7 @@ fn anchor() { #[test] fn negated_field() { - let query = Query::new("(function !async)").unwrap(); + let query = Query::try_from("(function !async)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -237,7 +237,7 @@ fn complex_example() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -259,7 +259,7 @@ fn complex_example() { #[test] fn ast_with_errors() { - let query = Query::new("(call (Undefined))").unwrap(); + let query = Query::try_from("(call (Undefined))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: undefined reference: `Undefined` @@ -271,7 +271,7 @@ fn ast_with_errors() { #[test] fn supertype() { - let query = Query::new("(expression/binary_expression)").unwrap(); + let query = Query::try_from("(expression/binary_expression)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -289,7 +289,7 @@ fn multiple_fields() { right: (_) @right) @expr "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index cdd3dde5..2d459d6c 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -10,22 +10,11 @@ use crate::diagnostics::Diagnostics; use crate::Error; -pub const DEFAULT_EXEC_FUEL: u32 = 1_000_000; -pub const DEFAULT_RECURSION_FUEL: u32 = 4096; - -#[derive(Debug, Clone, Copy, Default)] -pub struct FuelState { - pub exec_initial: Option, - pub exec_remaining: Option, - pub recursion_limit: Option, - pub recursion_max_depth: u32, -} - #[derive(Debug)] pub struct ParseResult { pub root: Root, pub diagnostics: Diagnostics, - pub fuel_state: FuelState, + pub exec_fuel_consumed: u32, } #[derive(Debug, Clone, Copy)] @@ -51,7 +40,6 @@ pub struct Parser<'src> { exec_fuel_initial: Option, exec_fuel_remaining: Option, recursion_fuel_limit: Option, - max_depth: u32, fatal_error: Option, } @@ -68,10 +56,9 @@ impl<'src> Parser<'src> { last_diagnostic_pos: None, delimiter_stack: Vec::with_capacity(8), debug_fuel: std::cell::Cell::new(256), - exec_fuel_initial: Some(DEFAULT_EXEC_FUEL), - exec_fuel_remaining: Some(DEFAULT_EXEC_FUEL), - recursion_fuel_limit: Some(DEFAULT_RECURSION_FUEL), - max_depth: 0, + exec_fuel_initial: None, + exec_fuel_remaining: None, + recursion_fuel_limit: None, fatal_error: None, } } @@ -89,27 +76,25 @@ impl<'src> Parser<'src> { pub fn parse(mut self) -> Result { self.parse_root(); - let (cst, diagnostics, fuel_state) = self.finish()?; + let (cst, diagnostics, exec_fuel_consumed) = self.finish()?; let root = Root::cast(SyntaxNode::new_root(cst)).expect("parser always produces Root"); Ok(ParseResult { root, diagnostics, - fuel_state, + exec_fuel_consumed, }) } - fn finish(mut self) -> Result<(GreenNode, Diagnostics, FuelState), Error> { + fn finish(mut self) -> Result<(GreenNode, Diagnostics, u32), Error> { self.drain_trivia(); if let Some(err) = self.fatal_error { return Err(err); } - let fuel_state = FuelState { - exec_initial: self.exec_fuel_initial, - exec_remaining: self.exec_fuel_remaining, - recursion_limit: self.recursion_fuel_limit, - recursion_max_depth: self.max_depth, + let exec_fuel_consumed = match (self.exec_fuel_initial, self.exec_fuel_remaining) { + (Some(initial), Some(remaining)) => initial.saturating_sub(remaining), + _ => 0, }; - Ok((self.builder.finish(), self.diagnostics, fuel_state)) + Ok((self.builder.finish(), self.diagnostics, exec_fuel_consumed)) } pub(super) fn has_fatal_error(&self) -> bool { @@ -347,9 +332,6 @@ impl<'src> Parser<'src> { return false; } self.depth += 1; - if self.depth > self.max_depth { - self.max_depth = self.depth; - } self.reset_debug_fuel(); true } diff --git a/crates/plotnik-lib/src/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index 5bf6d2cf..254bd9cc 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -45,4 +45,4 @@ pub use ast::{ NegatedField, QuantifiedExpr, Ref, Root, SeqExpr, Type, }; -pub use core::{DEFAULT_EXEC_FUEL, DEFAULT_RECURSION_FUEL, FuelState, ParseResult, Parser}; +pub use core::{ParseResult, Parser}; diff --git a/crates/plotnik-lib/src/parser/tests/grammar/alternations_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/alternations_tests.rs index b9664fe1..325108d4 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/alternations_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/alternations_tests.rs @@ -7,7 +7,7 @@ fn alternation() { [(identifier) (string)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -34,7 +34,7 @@ fn alternation_with_anonymous() { ["true" "false"] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -61,7 +61,7 @@ fn alternation_with_capture() { [(identifier) (string)] @value "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -94,7 +94,7 @@ fn alternation_with_quantifier() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -127,7 +127,7 @@ fn alternation_nested() { [(binary) (unary)]) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -159,7 +159,7 @@ fn alternation_in_field() { arguments: [(string) (number)]) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -193,7 +193,7 @@ fn unlabeled_alternation_three_items() { [(identifier) (number) (string)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -228,7 +228,7 @@ fn tagged_alternation_simple() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -259,7 +259,7 @@ fn tagged_alternation_single_line() { [A: (a) B: (b) C: (c)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -300,7 +300,7 @@ fn tagged_alternation_with_captures() { ] @stmt "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -357,7 +357,7 @@ fn tagged_alternation_with_type_annotation() { ] @chain :: MemberChain "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -411,7 +411,7 @@ fn tagged_alternation_nested() { ]) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -450,7 +450,7 @@ fn tagged_alternation_in_named_def() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -493,7 +493,7 @@ fn tagged_alternation_with_quantifier() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -529,7 +529,7 @@ fn tagged_alternation_with_sequence() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -570,7 +570,7 @@ fn tagged_alternation_with_nested_alternation() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -617,7 +617,7 @@ fn tagged_alternation_full_example() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/anchors_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/anchors_tests.rs index 39557ff4..4f314ebf 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/anchors_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/anchors_tests.rs @@ -7,7 +7,7 @@ fn anchor_first_child() { (block . (first_statement)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -31,7 +31,7 @@ fn anchor_last_child() { (block (last_statement) .) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -55,7 +55,7 @@ fn anchor_adjacency() { (dotted_name (identifier) @a . (identifier) @b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -89,7 +89,7 @@ fn anchor_both_ends() { (array . (element) .) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -115,7 +115,7 @@ fn anchor_multiple_adjacent() { (tuple . (a) . (b) . (c) .) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -153,7 +153,7 @@ fn anchor_in_sequence() { {. (first) (second) .} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/captures_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/captures_tests.rs index bfe52306..fb0f8410 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/captures_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/captures_tests.rs @@ -7,7 +7,7 @@ fn capture() { (identifier) @name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -28,7 +28,7 @@ fn capture_nested() { (call function: (identifier) @func) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -58,7 +58,7 @@ fn multiple_captures() { right: (_) @right) @expr "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -99,7 +99,7 @@ fn capture_with_type_annotation() { (identifier) @name :: string "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -123,7 +123,7 @@ fn capture_with_custom_type() { (function_declaration) @fn :: FunctionDecl "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -147,7 +147,7 @@ fn capture_without_type_annotation() { (identifier) @name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -170,7 +170,7 @@ fn multiple_captures_with_types() { right: (_) @right :: string) @expr :: BinaryExpr "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -220,7 +220,7 @@ fn sequence_capture_with_type() { {(a) (b)} @seq :: MySequence "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -251,7 +251,7 @@ fn alternation_capture_with_type() { [(identifier) (number)] @value :: Value "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -284,7 +284,7 @@ fn quantified_capture_with_type() { (statement)+ @stmts :: Statement "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -313,7 +313,7 @@ fn nested_captures_with_types() { (statement)* @body_stmts :: Statement)) @func :: Function "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -369,7 +369,7 @@ fn capture_with_type_no_spaces() { (identifier) @name::string "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -393,7 +393,7 @@ fn capture_literal() { "foo" @keyword "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -414,7 +414,7 @@ fn capture_literal_with_type() { "return" @kw :: string "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -438,7 +438,7 @@ fn capture_literal_in_tree() { (binary_expression "+" @op) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -463,7 +463,7 @@ fn capture_literal_with_quantifier() { ","* @commas "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/definitions_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/definitions_tests.rs index efc19328..e61e763a 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/definitions_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/definitions_tests.rs @@ -7,7 +7,7 @@ fn simple_named_def() { Expr = (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -27,7 +27,7 @@ fn named_def_with_alternation() { Value = [(identifier) (number) (string)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -61,7 +61,7 @@ fn named_def_with_sequence() { Pair = {(identifier) (expression)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -91,7 +91,7 @@ fn named_def_with_captures() { right: (_) @right) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -140,7 +140,7 @@ fn multiple_named_defs() { Stmt = (statement) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -168,7 +168,7 @@ fn named_def_then_expression() { (program (Expr) @value) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -210,7 +210,7 @@ fn named_def_referencing_another() { Expr = [(identifier) (Literal)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -255,7 +255,7 @@ fn named_def_with_quantifier() { Statements = (statement)+ "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -279,7 +279,7 @@ fn named_def_complex_recursive() { arguments: (arguments)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -330,7 +330,7 @@ fn named_def_with_type_annotation() { body: (_) @body) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -374,7 +374,7 @@ fn unnamed_def_allowed_as_last() { (program (Expr) @value) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs index 5fa34818..b8a7c0fd 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/fields_tests.rs @@ -7,7 +7,7 @@ fn field_expression() { (call function: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -34,7 +34,7 @@ fn multiple_fields() { right: (expression)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -66,7 +66,7 @@ fn negated_field() { (function !async) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -89,7 +89,7 @@ fn negated_and_regular_fields() { name: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -120,7 +120,7 @@ fn mixed_children_and_fields() { else: (else_block)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -162,7 +162,7 @@ fn fields_and_quantifiers() { baz: (baz)+?) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -234,7 +234,7 @@ fn fields_with_quantifiers_and_captures() { (node foo: (bar)* @baz) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/nodes_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/nodes_tests.rs index 429903bc..c9986ba1 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/nodes_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/nodes_tests.rs @@ -3,7 +3,7 @@ use indoc::indoc; #[test] fn empty_input() { - let query = Query::new("").unwrap(); + let query = Query::try_from("").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @"Root"); } @@ -14,7 +14,7 @@ fn simple_named_node() { (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -32,7 +32,7 @@ fn nested_node() { (function_definition name: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -60,7 +60,7 @@ fn deeply_nested() { (d)))) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -93,7 +93,7 @@ fn sibling_children() { (statement)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -123,7 +123,7 @@ fn wildcard() { (_) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -141,7 +141,7 @@ fn anonymous_node() { "if" "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -159,7 +159,7 @@ fn anonymous_node_operator() { "+=" "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -177,7 +177,7 @@ fn supertype_basic() { (expression/binary_expression) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -197,7 +197,7 @@ fn supertype_with_string_subtype() { (expression/"()") "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -219,7 +219,7 @@ fn supertype_with_capture() { (expression/binary_expression) @expr "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -244,7 +244,7 @@ fn supertype_with_children() { right: (_) @right) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -285,7 +285,7 @@ fn supertype_nested() { (expression/call_expression)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -311,7 +311,7 @@ fn supertype_in_alternation() { [(expression/identifier) (expression/number)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -342,7 +342,7 @@ fn no_supertype_plain_node() { (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/quantifiers_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/quantifiers_tests.rs index bc76d3de..0e52d2ab 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/quantifiers_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/quantifiers_tests.rs @@ -7,7 +7,7 @@ fn quantifier_star() { (statement)* "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -27,7 +27,7 @@ fn quantifier_plus() { (statement)+ "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -47,7 +47,7 @@ fn quantifier_optional() { (statement)? "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -67,7 +67,7 @@ fn quantifier_with_capture() { (statement)* @statements "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -91,7 +91,7 @@ fn quantifier_inside_node() { (statement)*) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs index 39cafeaa..eea5749f 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/sequences_tests.rs @@ -7,7 +7,7 @@ fn simple_sequence() { {(a) (b)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -32,7 +32,7 @@ fn empty_sequence() { {} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -49,7 +49,7 @@ fn sequence_single_element() { {(identifier)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -70,7 +70,7 @@ fn sequence_with_captures() { {(comment)* @comments (function) @fn} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -103,7 +103,7 @@ fn sequence_with_quantifier() { {(a) (b)}+ "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -130,7 +130,7 @@ fn nested_sequences() { {{(a)} {(b)}} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -161,7 +161,7 @@ fn sequence_in_named_node() { (block {(statement) (statement)}) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -190,7 +190,7 @@ fn sequence_with_alternation() { {[(a) (b)] (c)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -224,7 +224,7 @@ fn sequence_comma_separated_expression() { {(number) {"," (number)}*} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -258,7 +258,7 @@ fn sequence_with_anchor() { {. (first) (second) .} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/special_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/special_tests.rs index 1f9d373c..7afa7c2b 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/special_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/special_tests.rs @@ -7,7 +7,7 @@ fn error_node() { (ERROR) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -25,7 +25,7 @@ fn error_node_with_capture() { (ERROR) @err "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -46,7 +46,7 @@ fn missing_node_bare() { (MISSING) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -64,7 +64,7 @@ fn missing_node_with_type() { (MISSING identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -83,7 +83,7 @@ fn missing_node_with_string() { (MISSING ";") "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -104,7 +104,7 @@ fn missing_node_with_capture() { (MISSING ";") @missing_semi "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -128,7 +128,7 @@ fn error_in_alternation() { [(ERROR) (identifier)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -155,7 +155,7 @@ fn missing_in_sequence() { {(MISSING ";") (identifier)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -184,7 +184,7 @@ fn special_node_nested() { body: (block (ERROR))) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -213,7 +213,7 @@ fn error_with_quantifier() { (ERROR)* "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -233,7 +233,7 @@ fn missing_with_quantifier() { (MISSING identifier)? "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/grammar/trivia_tests.rs b/crates/plotnik-lib/src/parser/tests/grammar/trivia_tests.rs index fff7dbc1..886def5f 100644 --- a/crates/plotnik-lib/src/parser/tests/grammar/trivia_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/grammar/trivia_tests.rs @@ -7,7 +7,7 @@ fn whitespace_preserved() { (identifier) @name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root @@ -31,7 +31,7 @@ fn comment_preserved() { (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root @@ -53,7 +53,7 @@ fn comment_inside_expression() { name: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root @@ -85,7 +85,7 @@ fn trivia_filtered_by_default() { (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -106,7 +106,7 @@ fn trivia_between_alternation_items() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root @@ -137,7 +137,7 @@ fn trivia_between_alternation_items() { fn whitespace_only() { let input = " "; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root @@ -151,7 +151,7 @@ fn comment_only_raw() { // just a comment "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst_full(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs index 21a2bb91..f1418da7 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/coverage_tests.rs @@ -12,9 +12,7 @@ fn deeply_nested_trees_hit_recursion_limit() { input.push(')'); } - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); + let result = Query::new(&input).with_recursion_fuel(Some(depth)).exec(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -34,9 +32,7 @@ fn deeply_nested_sequences_hit_recursion_limit() { input.push('}'); } - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); + let result = Query::new(&input).with_recursion_fuel(Some(depth)).exec(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -56,9 +52,7 @@ fn deeply_nested_alternations_hit_recursion_limit() { input.push(']'); } - let result = Query::builder(&input) - .with_recursion_fuel(Some(depth)) - .build(); + let result = Query::new(&input).with_recursion_fuel(Some(depth)).exec(); assert!( matches!(result, Err(crate::Error::RecursionLimitExceeded)), @@ -75,7 +69,7 @@ fn many_trees_exhaust_exec_fuel() { input.push_str("(a) "); } - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + let result = Query::new(&input).with_exec_fuel(Some(100)).exec(); assert!( matches!(result, Err(crate::Error::ExecFuelExhausted)), @@ -97,7 +91,7 @@ fn many_branches_exhaust_exec_fuel() { } input.push(']'); - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + let result = Query::new(&input).with_exec_fuel(Some(100)).exec(); assert!( matches!(result, Err(crate::Error::ExecFuelExhausted)), @@ -119,7 +113,7 @@ fn many_fields_exhaust_exec_fuel() { } input.push(')'); - let result = Query::builder(&input).with_exec_fuel(Some(100)).build(); + let result = Query::new(&input).with_exec_fuel(Some(100)).exec(); assert!( matches!(result, Err(crate::Error::ExecFuelExhausted)), @@ -134,7 +128,7 @@ fn named_def_missing_equals_with_garbage() { Expr ^^^ (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) @@ -159,7 +153,7 @@ fn named_def_missing_equals_recovers_to_next_def() { Valid = (ok) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) @@ -179,7 +173,7 @@ fn empty_double_quote_string() { (a "") "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -200,7 +194,7 @@ fn empty_single_quote_string() { (a '') "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -219,7 +213,7 @@ fn empty_single_quote_string() { fn single_quote_string_is_valid() { let input = "(node 'if')"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -239,7 +233,7 @@ fn single_quote_string_is_valid() { fn single_quote_in_alternation() { let input = "['public' 'private']"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -264,7 +258,7 @@ fn single_quote_in_alternation() { fn single_quote_with_escape() { let input = r"(node 'it\'s')"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root @@ -284,7 +278,7 @@ fn single_quote_with_escape() { fn missing_with_nested_tree_parses() { let input = "(MISSING (something))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_cst(), @r#" Root diff --git a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs index bf9dfab6..ea5c05fe 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/incomplete_tests.rs @@ -7,7 +7,7 @@ fn missing_capture_name() { (identifier) @ "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected capture name after '@' @@ -23,7 +23,7 @@ fn missing_field_value() { (call name:) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected expression after field name @@ -37,7 +37,7 @@ fn missing_field_value() { fn named_def_eof_after_equals() { let input = "Expr = "; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected expression after '=' in named definition @@ -53,7 +53,7 @@ fn missing_type_name() { (identifier) @name :: "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected type name after '::' (e.g., ::MyType or ::string) @@ -69,7 +69,7 @@ fn missing_negated_field_name() { (call !) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected field name after '!' (e.g., !value) @@ -85,7 +85,7 @@ fn missing_subtype() { (expression/) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected subtype after '/' (e.g., expression/binary_expression) @@ -101,7 +101,7 @@ fn tagged_branch_missing_expression() { [Label:] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected expression after branch label @@ -115,7 +115,7 @@ fn tagged_branch_missing_expression() { fn type_annotation_missing_name_at_eof() { let input = "(a) @x ::"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected type name after '::' (e.g., ::MyType or ::string) @@ -129,7 +129,7 @@ fn type_annotation_missing_name_at_eof() { fn type_annotation_missing_name_with_bracket() { let input = "[(a) @x :: ]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected type name after '::' (e.g., ::MyType or ::string) @@ -145,7 +145,7 @@ fn type_annotation_invalid_token_after() { (identifier) @name :: ( "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected type name after '::' (e.g., ::MyType or ::string) @@ -171,7 +171,7 @@ fn field_value_is_garbage() { (call name: %%%) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected expression after field name @@ -187,7 +187,7 @@ fn capture_with_invalid_char() { (identifier) @123 "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected capture name after '@' @@ -201,7 +201,7 @@ fn capture_with_invalid_char() { fn bare_capture_at_eof_triggers_sync() { let input = "@"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture '@' must follow an expression to capture @@ -217,7 +217,7 @@ fn bare_capture_at_root() { @name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture '@' must follow an expression to capture @@ -237,7 +237,7 @@ fn capture_at_start_of_alternation() { [@x (a)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -257,7 +257,7 @@ fn mixed_valid_invalid_captures() { (a) @ok @ @name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture '@' must follow an expression to capture @@ -281,7 +281,7 @@ fn field_equals_typo_missing_value() { (call name = ) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '=' is not valid for field constraints @@ -305,7 +305,7 @@ fn field_equals_typo_missing_value() { fn lowercase_branch_label_missing_expression() { let input = "[label:]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: tagged alternation labels must be Capitalized (they map to enum variants) diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs index 25b29b8d..792e7ebb 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unclosed_tests.rs @@ -7,7 +7,7 @@ fn missing_paren() { (identifier "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree; expected ')' @@ -25,7 +25,7 @@ fn missing_bracket() { [(identifier) (string) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed alternation; expected ']' @@ -43,7 +43,7 @@ fn missing_brace() { {(a) (b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed sequence; expected '}' @@ -61,7 +61,7 @@ fn nested_unclosed() { (a (b (c) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree; expected ')' @@ -79,7 +79,7 @@ fn deeply_nested_unclosed() { (a (b (c (d "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree; expected ')' @@ -97,7 +97,7 @@ fn unclosed_alternation_nested() { [(a) (b "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree; expected ')' @@ -115,7 +115,7 @@ fn empty_parens() { () "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: empty tree expression - expected node type or children @@ -132,7 +132,7 @@ fn unclosed_tree_shows_open_location() { (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed tree; expected ')' @@ -152,7 +152,7 @@ fn unclosed_alternation_shows_open_location() { (b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed alternation; expected ']' @@ -173,7 +173,7 @@ fn unclosed_sequence_shows_open_location() { (b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unclosed sequence; expected '}' @@ -190,7 +190,7 @@ fn unclosed_sequence_shows_open_location() { fn unclosed_double_quote_string() { let input = r#"(call "foo)"#; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected a child expression or closing delimiter @@ -210,7 +210,7 @@ fn unclosed_double_quote_string() { fn unclosed_single_quote_string() { let input = "(call 'foo)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter diff --git a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs index 8ec7c124..a6fd39e0 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/unexpected_tests.rs @@ -7,7 +7,7 @@ fn unexpected_token() { (identifier) ^^^ (string) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -27,7 +27,7 @@ fn multiple_consecutive_garbage() { ^^^ $$$ %%% (ok) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -43,7 +43,7 @@ fn garbage_at_start() { ^^^ (a) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -59,7 +59,7 @@ fn only_garbage() { ^^^ $$$ "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -75,7 +75,7 @@ fn garbage_inside_alternation() { [(a) ^^^ (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -91,7 +91,7 @@ fn garbage_inside_node() { (a (b) @@@ (c)) (d) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: expected capture name after '@' @@ -115,7 +115,7 @@ fn xml_tag_garbage() {
(identifier)
"#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -135,7 +135,7 @@ fn xml_self_closing() {
(a) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -151,7 +151,7 @@ fn predicate_unsupported() { (a (#eq? @x "foo") b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported @@ -179,7 +179,7 @@ fn predicate_match() { (identifier) #match? @name "test" "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported @@ -205,7 +205,7 @@ fn predicate_match() { fn predicate_in_tree() { let input = "(function #eq? @name \"test\")"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported @@ -229,7 +229,7 @@ fn predicate_in_alternation() { [(a) #eq? (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -245,7 +245,7 @@ fn predicate_in_sequence() { {(a) #set! (b)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: tree-sitter predicates (#eq?, #match?, #set!, etc.) are not supported @@ -263,7 +263,7 @@ fn multiline_garbage_recovery() { b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -283,7 +283,7 @@ fn top_level_garbage_recovery() { Expr = (a) ^^^ Expr2 = (b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -303,7 +303,7 @@ fn multiple_definitions_with_garbage_between() { C = (c) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -323,7 +323,7 @@ fn alternation_recovery_to_capture() { [^^^ @name] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -347,7 +347,7 @@ fn comma_between_defs() { A = (a), B = (b) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -361,7 +361,7 @@ fn comma_between_defs() { fn bare_colon_in_tree() { let input = "(a : (b))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unexpected token; expected a child expression or closing delimiter @@ -375,7 +375,7 @@ fn bare_colon_in_tree() { fn paren_close_inside_alternation() { let input = "[(a) ) (b)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: expected closing ']' for alternation @@ -397,7 +397,7 @@ fn paren_close_inside_alternation() { fn bracket_close_inside_sequence() { let input = "{(a) ] (b)}"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: expected closing '}' for sequence @@ -419,7 +419,7 @@ fn bracket_close_inside_sequence() { fn paren_close_inside_sequence() { let input = "{(a) ) (b)}"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: expected closing '}' for sequence @@ -441,7 +441,7 @@ fn paren_close_inside_sequence() { fn single_colon_type_annotation_followed_by_non_id() { let input = "(a) @x : (b)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ @@ -459,7 +459,7 @@ fn single_colon_type_annotation_followed_by_non_id() { fn single_colon_type_annotation_at_eof() { let input = "(a) @x :"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r#" error: unexpected token; expected an expression like (node), [choice], {sequence}, "literal", or _ diff --git a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs index f5e13c88..fd4a4361 100644 --- a/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs +++ b/crates/plotnik-lib/src/parser/tests/recovery/validation_tests.rs @@ -8,7 +8,7 @@ fn ref_with_children_error() { (Expr (child)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: reference `Expr` cannot contain children @@ -25,7 +25,7 @@ fn ref_with_multiple_children_error() { (Expr (a) (b) @cap) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: reference `Expr` cannot contain children @@ -42,7 +42,7 @@ fn ref_with_field_children_error() { (Expr name: (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: reference `Expr` cannot contain children @@ -56,7 +56,7 @@ fn ref_with_field_children_error() { fn reference_with_supertype_syntax_error() { let input = "(RefName/subtype)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: references cannot use supertype syntax (/) @@ -72,7 +72,7 @@ fn mixed_tagged_and_untagged() { [Tagged: (a) (b) Another: (c)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: mixed tagged and untagged branches in alternation @@ -90,7 +90,7 @@ fn error_with_unexpected_content() { (ERROR (something)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: (ERROR) takes no arguments @@ -106,7 +106,7 @@ fn bare_error_keyword() { ERROR "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) @@ -122,7 +122,7 @@ fn bare_missing_keyword() { MISSING "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: ERROR and MISSING must be inside parentheses: (ERROR) or (MISSING ...) @@ -138,7 +138,7 @@ fn upper_ident_in_alternation_not_followed_by_colon() { [(Expr) (Statement)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `Expr` @@ -158,7 +158,7 @@ fn upper_ident_not_followed_by_equals_is_expression() { (Expr) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `Expr` @@ -174,7 +174,7 @@ fn bare_upper_ident_not_followed_by_equals_is_error() { Expr "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) @@ -190,7 +190,7 @@ fn named_def_missing_equals() { Expr (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: bare identifier not allowed; nodes must be enclosed in parentheses, e.g., (identifier) @@ -212,7 +212,7 @@ fn unnamed_def_not_allowed_in_middle() { (last) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unnamed definition must be last in file; add a name: `Name = (first)` @@ -230,7 +230,7 @@ fn multiple_unnamed_defs_errors_for_all_but_last() { (third) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unnamed definition must be last in file; add a name: `Name = (first)` @@ -250,7 +250,7 @@ fn capture_space_after_dot_is_anchor() { (identifier) @foo . (other) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: unnamed definition must be last in file; add a name: `Name = (identifier) @foo` @@ -268,7 +268,7 @@ fn capture_space_after_dot_is_anchor() { fn def_name_lowercase_error() { let input = "lowercase = (x)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names must start with uppercase @@ -290,7 +290,7 @@ fn def_name_snake_case_suggests_pascal() { my_expr = (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names must start with uppercase @@ -312,7 +312,7 @@ fn def_name_kebab_case_suggests_pascal() { my-expr = (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names must start with uppercase @@ -334,7 +334,7 @@ fn def_name_dotted_suggests_pascal() { my.expr = (identifier) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names must start with uppercase @@ -354,7 +354,7 @@ fn def_name_dotted_suggests_pascal() { fn def_name_with_underscores_error() { let input = "Some_Thing = (x)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names cannot contain separators @@ -374,7 +374,7 @@ fn def_name_with_underscores_error() { fn def_name_with_hyphens_error() { let input = "Some-Thing = (x)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: definition names cannot contain separators @@ -396,7 +396,7 @@ fn capture_name_pascal_case_error() { (a) @Name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names must start with lowercase @@ -418,7 +418,7 @@ fn capture_name_pascal_case_with_hyphens_error() { (a) @My-Name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain hyphens @@ -440,7 +440,7 @@ fn capture_name_with_hyphens_error() { (a) @my-name "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain hyphens @@ -462,7 +462,7 @@ fn capture_dotted_error() { (identifier) @foo.bar "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain dots @@ -484,7 +484,7 @@ fn capture_dotted_multiple_parts() { (identifier) @foo.bar.baz "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain dots @@ -506,7 +506,7 @@ fn capture_dotted_followed_by_field() { (node) @foo.bar name: (other) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain dots @@ -532,7 +532,7 @@ fn capture_space_after_dot_breaks_chain() { (identifier) @foo. bar "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain dots @@ -562,7 +562,7 @@ fn capture_hyphenated_error() { (identifier) @foo-bar "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain hyphens @@ -584,7 +584,7 @@ fn capture_hyphenated_multiple() { (identifier) @foo-bar-baz "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain hyphens @@ -606,7 +606,7 @@ fn capture_mixed_dots_and_hyphens() { (identifier) @foo.bar-baz "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture names cannot contain dots @@ -628,7 +628,7 @@ fn field_name_pascal_case_error() { (call Name: (a)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: field names must start with lowercase @@ -648,7 +648,7 @@ fn field_name_pascal_case_error() { fn field_name_with_dots_error() { let input = "(call foo.bar: (x))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: field names cannot contain dots @@ -668,7 +668,7 @@ fn field_name_with_dots_error() { fn field_name_with_hyphens_error() { let input = "(call foo-bar: (x))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: field names cannot contain hyphens @@ -690,7 +690,7 @@ fn negated_field_with_upper_ident_parses() { (call !Arguments) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: field names must start with lowercase @@ -712,7 +712,7 @@ fn branch_label_snake_case_suggests_pascal() { [My_branch: (a) Other: (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: branch labels cannot contain separators @@ -734,7 +734,7 @@ fn branch_label_kebab_case_suggests_pascal() { [My-branch: (a) Other: (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: branch labels cannot contain separators @@ -756,7 +756,7 @@ fn branch_label_dotted_suggests_pascal() { [My.branch: (a) Other: (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: branch labels cannot contain separators @@ -776,7 +776,7 @@ fn branch_label_dotted_suggests_pascal() { fn branch_label_with_underscores_error() { let input = "[Some_Label: (x)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: branch labels cannot contain separators @@ -796,7 +796,7 @@ fn branch_label_with_underscores_error() { fn branch_label_with_hyphens_error() { let input = "[Some-Label: (x)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: branch labels cannot contain separators @@ -821,7 +821,7 @@ fn lowercase_branch_label() { ] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: tagged alternation labels must be Capitalized (they map to enum variants) @@ -853,7 +853,7 @@ fn lowercase_branch_label_suggests_capitalized() { [first: (a) Second: (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: tagged alternation labels must be Capitalized (they map to enum variants) @@ -873,7 +873,7 @@ fn lowercase_branch_label_suggests_capitalized() { fn mixed_case_branch_labels() { let input = "[foo: (a) Bar: (b)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: tagged alternation labels must be Capitalized (they map to enum variants) @@ -895,7 +895,7 @@ fn type_annotation_dotted_suggests_pascal() { (a) @x :: My.Type "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: type names cannot contain dots or hyphens @@ -917,7 +917,7 @@ fn type_annotation_kebab_suggests_pascal() { (a) @x :: My-Type "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: type names cannot contain dots or hyphens @@ -937,7 +937,7 @@ fn type_annotation_kebab_suggests_pascal() { fn type_name_with_dots_error() { let input = "(x) @name :: Some.Type"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: type names cannot contain dots or hyphens @@ -957,7 +957,7 @@ fn type_name_with_dots_error() { fn type_name_with_hyphens_error() { let input = "(x) @name :: Some-Type"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: type names cannot contain dots or hyphens @@ -977,7 +977,7 @@ fn type_name_with_hyphens_error() { fn comma_in_node_children() { let input = "(node (a), (b))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: ',' is not valid syntax; plotnik uses whitespace for separation @@ -997,7 +997,7 @@ fn comma_in_node_children() { fn comma_in_alternation() { let input = "[(a), (b), (c)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: ',' is not valid syntax; plotnik uses whitespace for separation @@ -1027,7 +1027,7 @@ fn comma_in_alternation() { fn comma_in_sequence() { let input = "{(a), (b)}"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: ',' is not valid syntax; plotnik uses whitespace for separation @@ -1047,7 +1047,7 @@ fn comma_in_sequence() { fn pipe_in_alternation() { let input = "[(a) | (b) | (c)]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '|' is not valid syntax; plotnik uses whitespace for separation @@ -1079,7 +1079,7 @@ fn pipe_between_branches() { [(a) | (b)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '|' is not valid syntax; plotnik uses whitespace for separation @@ -1099,7 +1099,7 @@ fn pipe_between_branches() { fn pipe_in_tree() { let input = "(a | b)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '|' is not valid syntax; plotnik uses whitespace for separation @@ -1123,7 +1123,7 @@ fn pipe_in_tree() { fn pipe_in_sequence() { let input = "{(a) | (b)}"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '|' is not valid syntax; plotnik uses whitespace for separation @@ -1143,7 +1143,7 @@ fn pipe_in_sequence() { fn field_equals_typo() { let input = "(node name = (identifier))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '=' is not valid for field constraints @@ -1163,7 +1163,7 @@ fn field_equals_typo() { fn field_equals_typo_no_space() { let input = "(node name=(identifier))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '=' is not valid for field constraints @@ -1183,7 +1183,7 @@ fn field_equals_typo_no_space() { fn field_equals_typo_no_expression() { let input = "(call name=)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '=' is not valid for field constraints @@ -1209,7 +1209,7 @@ fn field_equals_typo_in_tree() { (call name = (identifier)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: '=' is not valid for field constraints @@ -1229,7 +1229,7 @@ fn field_equals_typo_in_tree() { fn single_colon_type_annotation() { let input = "(identifier) @name : Type"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: single colon is not valid for type annotations @@ -1248,7 +1248,7 @@ fn single_colon_type_annotation() { fn single_colon_type_annotation_no_space() { let input = "(identifier) @name:Type"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: single colon is not valid for type annotations @@ -1269,7 +1269,7 @@ fn single_colon_type_annotation_with_space() { (a) @x : Type "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: single colon is not valid for type annotations @@ -1288,7 +1288,7 @@ fn single_colon_type_annotation_with_space() { fn single_colon_primitive_type() { let input = "@val : string"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: capture '@' must follow an expression to capture diff --git a/crates/plotnik-lib/src/query/alt_kinds_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs index c4d80556..4c1ee966 100644 --- a/crates/plotnik-lib/src/query/alt_kinds_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kinds_tests.rs @@ -2,7 +2,7 @@ use crate::Query; #[test] fn tagged_alternation_valid() { - let query = Query::new("[A: (a) B: (b)]").unwrap(); + let query = Query::try_from("[A: (a) B: (b)]").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -17,7 +17,7 @@ fn tagged_alternation_valid() { #[test] fn untagged_alternation_valid() { - let query = Query::new("[(a) (b)]").unwrap(); + let query = Query::try_from("[(a) (b)]").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root @@ -32,7 +32,7 @@ fn untagged_alternation_valid() { #[test] fn mixed_alternation_tagged_first() { - let query = Query::new("[A: (a) (b)]").unwrap(); + let query = Query::try_from("[A: (a) (b)]").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: mixed tagged and untagged branches in alternation @@ -46,7 +46,7 @@ fn mixed_alternation_tagged_first() { #[test] fn mixed_alternation_untagged_first() { - let query = Query::new( + let query = Query::try_from( r#" [ (a) @@ -68,7 +68,7 @@ fn mixed_alternation_untagged_first() { #[test] fn nested_mixed_alternation() { - let query = Query::new("(call [A: (a) (b)])").unwrap(); + let query = Query::try_from("(call [A: (a) (b)])").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: mixed tagged and untagged branches in alternation @@ -82,7 +82,7 @@ fn nested_mixed_alternation() { #[test] fn multiple_mixed_alternations() { - let query = Query::new("(foo [A: (a) (b)] [C: (c) (d)])").unwrap(); + let query = Query::try_from("(foo [A: (a) (b)] [C: (c) (d)])").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: mixed tagged and untagged branches in alternation @@ -102,7 +102,7 @@ fn multiple_mixed_alternations() { #[test] fn single_branch_no_error() { - let query = Query::new("[A: (a)]").unwrap(); + let query = Query::try_from("[A: (a)]").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_ast(), @r" Root diff --git a/crates/plotnik-lib/src/query/mod.rs b/crates/plotnik-lib/src/query/mod.rs index e522f229..6d4491ff 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -35,62 +35,19 @@ use crate::Result; use crate::diagnostics::Diagnostics; use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; -use crate::parser::{FuelState, ParseResult, Parser, Root, SyntaxNode, ast}; -use shapes::ShapeCardinality; -use symbol_table::SymbolTable; +use crate::parser::{ParseResult, Parser, Root, SyntaxNode, ast}; -/// Builder for configuring and creating a [`Query`]. -pub struct QueryBuilder<'a> { - source: &'a str, - exec_fuel: Option, - recursion_fuel: Option, -} +const DEFAULT_EXEC_FUEL: u32 = 1_000_000; +const DEFAULT_RECURSION_FUEL: u32 = 4096; -impl<'a> QueryBuilder<'a> { - /// Create a new builder for the given source. - pub fn new(source: &'a str) -> Self { - Self { - source, - exec_fuel: None, - recursion_fuel: None, - } - } - - /// Set execution fuel limit. None = infinite. - /// - /// Execution fuel never replenishes. It protects against large inputs. - /// Returns error when exhausted. - pub fn with_exec_fuel(mut self, limit: Option) -> Self { - self.exec_fuel = limit; - self - } - - /// Set recursion depth limit. None = infinite. - /// - /// Recursion fuel restores when exiting recursion. It protects against - /// deeply nested input. Returns error when exhausted. - pub fn with_recursion_fuel(mut self, limit: Option) -> Self { - self.recursion_fuel = limit; - self - } - - /// Build the query, running all analysis passes. - /// - /// Returns `Err` if fuel limits are exceeded. - pub fn build(self) -> Result> { - let mut query = Query::empty(self.source); - query.parse(self.exec_fuel, self.recursion_fuel)?; - query.validate_alt_kinds(); - query.resolve_names(); - query.validate_recursion(); - query.infer_shapes(); - Ok(query) - } -} +use shapes::ShapeCardinality; +use symbol_table::SymbolTable; /// A parsed and analyzed query. /// -/// Construction succeeds unless fuel limits are exceeded. +/// Create with [`new`](Self::new), optionally configure fuel limits, +/// then call [`exec`](Self::exec) to run analysis. +/// /// Check [`is_valid`](Self::is_valid) or [`diagnostics`](Self::diagnostics) /// to determine if the query has syntax/semantic issues. #[derive(Debug, Clone)] @@ -99,8 +56,9 @@ pub struct Query<'a> { ast: Root, symbol_table: SymbolTable<'a>, shape_cardinality_table: HashMap, - fuel_state: FuelState, - // Diagnostics per pass + exec_fuel: Option, + recursion_fuel: Option, + exec_fuel_consumed: u32, parse_diagnostics: Diagnostics, alt_kind_diagnostics: Diagnostics, resolve_diagnostics: Diagnostics, @@ -117,26 +75,18 @@ fn empty_root() -> Root { } impl<'a> Query<'a> { - /// Parse and analyze a query from source text. + /// Create a new query from source text. /// - /// Returns `Err` if fuel limits are exceeded. - /// Syntax/semantic diagnostics are collected and accessible via [`diagnostics`](Self::diagnostics). - pub fn new(source: &'a str) -> Result { - QueryBuilder::new(source).build() - } - - /// Create a builder for configuring parser limits. - pub fn builder(source: &'a str) -> QueryBuilder<'a> { - QueryBuilder::new(source) - } - - fn empty(source: &'a str) -> Self { + /// Call [`exec`](Self::exec) to run analysis passes. + pub fn new(source: &'a str) -> Self { Self { source, ast: empty_root(), symbol_table: SymbolTable::default(), shape_cardinality_table: HashMap::new(), - fuel_state: FuelState::default(), + exec_fuel: Some(DEFAULT_EXEC_FUEL), + recursion_fuel: Some(DEFAULT_RECURSION_FUEL), + exec_fuel_consumed: 0, parse_diagnostics: Diagnostics::new(), alt_kind_diagnostics: Diagnostics::new(), resolve_diagnostics: Diagnostics::new(), @@ -145,20 +95,51 @@ impl<'a> Query<'a> { } } - fn parse(&mut self, exec_fuel: Option, recursion_fuel: Option) -> Result<()> { + /// Set execution fuel limit. None = infinite. + /// + /// Execution fuel never replenishes. It protects against large inputs. + /// Returns error from [`exec`](Self::exec) when exhausted. + pub fn with_exec_fuel(mut self, limit: Option) -> Self { + self.exec_fuel = limit; + self + } + + /// Set recursion depth limit. None = infinite. + /// + /// Recursion fuel restores when exiting recursion. It protects against + /// deeply nested input. Returns error from [`exec`](Self::exec) when exhausted. + pub fn with_recursion_fuel(mut self, limit: Option) -> Self { + self.recursion_fuel = limit; + self + } + + /// Run all analysis passes. + /// + /// Returns `Err` if fuel limits are exceeded. + /// Syntax/semantic diagnostics are collected and accessible via [`diagnostics`](Self::diagnostics). + pub fn exec(mut self) -> Result { + self.try_parse()?; + self.validate_alt_kinds(); + self.resolve_names(); + self.validate_recursion(); + self.infer_shapes(); + Ok(self) + } + + fn try_parse(&mut self) -> Result<()> { let tokens = lex(self.source); let parser = Parser::new(self.source, tokens) - .with_exec_fuel(exec_fuel) - .with_recursion_fuel(recursion_fuel); + .with_exec_fuel(self.exec_fuel) + .with_recursion_fuel(self.recursion_fuel); let ParseResult { root, diagnostics, - fuel_state, + exec_fuel_consumed, } = parser.parse()?; self.ast = root; self.parse_diagnostics = diagnostics; - self.fuel_state = fuel_state; + self.exec_fuel_consumed = exec_fuel_consumed; Ok(()) } @@ -227,3 +208,19 @@ impl<'a> Query<'a> { && !self.shapes_diagnostics.has_errors() } } + +impl<'a> TryFrom<&'a str> for Query<'a> { + type Error = crate::Error; + + fn try_from(source: &'a str) -> Result { + Self::new(source).exec() + } +} + +impl<'a> TryFrom<&'a String> for Query<'a> { + type Error = crate::Error; + + fn try_from(source: &'a String) -> Result { + Self::new(source.as_str()).exec() + } +} diff --git a/crates/plotnik-lib/src/query/mod_tests.rs b/crates/plotnik-lib/src/query/mod_tests.rs index 0248a083..50e2fd2c 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -2,27 +2,27 @@ use super::*; #[test] fn valid_query() { - let q = Query::new("Expr = (expression)").unwrap(); + let q = Query::try_from("Expr = (expression)").unwrap(); assert!(q.is_valid()); } #[test] fn parse_error() { - let q = Query::new("(unclosed").unwrap(); + let q = Query::try_from("(unclosed").unwrap(); assert!(!q.is_valid()); assert!(q.dump_diagnostics().contains("expected")); } #[test] fn resolution_error() { - let q = Query::new("(call (Undefined))").unwrap(); + let q = Query::try_from("(call (Undefined))").unwrap(); assert!(!q.is_valid()); assert!(q.dump_diagnostics().contains("undefined reference")); } #[test] fn combined_errors() { - let q = Query::new("(call (Undefined) extra)").unwrap(); + let q = Query::try_from("(call (Undefined) extra)").unwrap(); assert!(!q.is_valid()); assert!(!q.diagnostics().is_empty()); } diff --git a/crates/plotnik-lib/src/query/printer_tests.rs b/crates/plotnik-lib/src/query/printer_tests.rs index df340681..be5b8f51 100644 --- a/crates/plotnik-lib/src/query/printer_tests.rs +++ b/crates/plotnik-lib/src/query/printer_tests.rs @@ -3,7 +3,7 @@ use indoc::indoc; #[test] fn printer_with_spans() { - let q = Query::new("(call)").unwrap(); + let q = Query::try_from("(call)").unwrap(); insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..6] Def [0..6] @@ -13,7 +13,7 @@ fn printer_with_spans() { #[test] fn printer_with_cardinalities() { - let q = Query::new("(call)").unwrap(); + let q = Query::try_from("(call)").unwrap(); insta::assert_snapshot!(q.printer().with_cardinalities(true).dump(), @r" Root¹ Def¹ @@ -23,7 +23,7 @@ fn printer_with_cardinalities() { #[test] fn printer_cst_with_trivia() { - let q = Query::new("(a) (b)").unwrap(); + let q = Query::try_from("(a) (b)").unwrap(); insta::assert_snapshot!(q.printer().raw(true).with_trivia(true).dump(), @r#" Root Def @@ -45,7 +45,7 @@ fn printer_alt_branches() { let input = indoc! {r#" [A: (a) B: (b)] "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -59,7 +59,7 @@ fn printer_alt_branches() { #[test] fn printer_capture_with_type() { - let q = Query::new("(call)@x :: T").unwrap(); + let q = Query::try_from("(call)@x :: T").unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -70,7 +70,7 @@ fn printer_capture_with_type() { #[test] fn printer_quantifiers() { - let q = Query::new("(a)* (b)+ (c)?").unwrap(); + let q = Query::try_from("(a)* (b)+ (c)?").unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -87,7 +87,7 @@ fn printer_quantifiers() { #[test] fn printer_field() { - let q = Query::new("(call name: (id))").unwrap(); + let q = Query::try_from("(call name: (id))").unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -99,7 +99,7 @@ fn printer_field() { #[test] fn printer_negated_field() { - let q = Query::new("(call !name)").unwrap(); + let q = Query::try_from("(call !name)").unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -110,7 +110,7 @@ fn printer_negated_field() { #[test] fn printer_wildcard_and_anchor() { - let q = Query::new("(call _ . (arg))").unwrap(); + let q = Query::try_from("(call _ . (arg))").unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def @@ -123,7 +123,7 @@ fn printer_wildcard_and_anchor() { #[test] fn printer_string_literal() { - let q = Query::new(r#"(call "foo")"#).unwrap(); + let q = Query::try_from(r#"(call "foo")"#).unwrap(); insta::assert_snapshot!(q.printer().dump(), @r#" Root Def @@ -138,7 +138,7 @@ fn printer_ref() { Expr = (call) (func (Expr)) "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().dump(), @r" Root Def Expr @@ -156,7 +156,7 @@ fn printer_symbols_with_cardinalities() { B = {(b) (c)} (entry (A) (B)) "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().only_symbols(true).with_cardinalities(true).dump(), @r" A¹ B⁺ @@ -170,7 +170,7 @@ fn printer_symbols_with_refs() { B = (b (A)) (entry (B)) "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().only_symbols(true).dump(), @r" A B @@ -185,7 +185,7 @@ fn printer_symbols_cycle() { B = [(b) (A)] (entry (A)) "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().only_symbols(true).dump(), @r" A B @@ -199,14 +199,14 @@ fn printer_symbols_cycle() { #[test] fn printer_symbols_undefined_ref() { let input = "(call (Undefined))"; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().only_symbols(true).dump(), @""); } #[test] fn printer_symbols_broken_ref() { let input = "A = (foo (Undefined))"; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().only_symbols(true).dump(), @r" A Undefined? @@ -219,7 +219,7 @@ fn printer_spans_comprehensive() { Foo = (call name: (id) !bar) [(a) (b)] "#}; - let q = Query::new(input).unwrap(); + let q = Query::try_from(input).unwrap(); insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..39] Def [0..28] Foo @@ -238,7 +238,7 @@ fn printer_spans_comprehensive() { #[test] fn printer_spans_seq() { - let q = Query::new("{(a) (b)}").unwrap(); + let q = Query::try_from("{(a) (b)}").unwrap(); insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..9] Def [0..9] @@ -250,7 +250,7 @@ fn printer_spans_seq() { #[test] fn printer_spans_quantifiers() { - let q = Query::new("(a)* (b)+").unwrap(); + let q = Query::try_from("(a)* (b)+").unwrap(); insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..9] Def [0..4] @@ -264,7 +264,7 @@ fn printer_spans_quantifiers() { #[test] fn printer_spans_alt_branches() { - let q = Query::new("[A: (a) B: (b)]").unwrap(); + let q = Query::try_from("[A: (a) B: (b)]").unwrap(); insta::assert_snapshot!(q.printer().with_spans(true).dump(), @r" Root [0..15] Def [0..15] diff --git a/crates/plotnik-lib/src/query/recursion_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs index 0cc79421..59b14b5b 100644 --- a/crates/plotnik-lib/src/query/recursion_tests.rs +++ b/crates/plotnik-lib/src/query/recursion_tests.rs @@ -3,25 +3,25 @@ use indoc::indoc; #[test] fn escape_via_alternation() { - let query = Query::new("E = [(x) (call (E))]").unwrap(); + let query = Query::try_from("E = [(x) (call (E))]").unwrap(); assert!(query.is_valid()); } #[test] fn escape_via_optional() { - let query = Query::new("E = (call (E)?)").unwrap(); + let query = Query::try_from("E = (call (E)?)").unwrap(); assert!(query.is_valid()); } #[test] fn escape_via_star() { - let query = Query::new("E = (call (E)*)").unwrap(); + let query = Query::try_from("E = (call (E)*)").unwrap(); assert!(query.is_valid()); } #[test] fn no_escape_via_plus() { - let query = Query::new("E = (call (E)+)").unwrap(); + let query = Query::try_from("E = (call (E)+)").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `E` → `E` has no escape path @@ -36,20 +36,20 @@ fn no_escape_via_plus() { #[test] fn escape_via_empty_tree() { - let query = Query::new("E = [(call) (E)]").unwrap(); + let query = Query::try_from("E = [(call) (E)]").unwrap(); assert!(query.is_valid()); } #[test] fn lazy_quantifiers_same_as_greedy() { - assert!(Query::new("E = (call (E)??)").unwrap().is_valid()); - assert!(Query::new("E = (call (E)*?)").unwrap().is_valid()); - assert!(!Query::new("E = (call (E)+?)").unwrap().is_valid()); + assert!(Query::try_from("E = (call (E)??)").unwrap().is_valid()); + assert!(Query::try_from("E = (call (E)*?)").unwrap().is_valid()); + assert!(!Query::try_from("E = (call (E)+?)").unwrap().is_valid()); } #[test] fn recursion_in_tree_child() { - let query = Query::new("E = (call (E))").unwrap(); + let query = Query::try_from("E = (call (E))").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `E` → `E` has no escape path @@ -64,28 +64,28 @@ fn recursion_in_tree_child() { #[test] fn recursion_in_field() { - let query = Query::new("E = (call body: (E))").unwrap(); + let query = Query::try_from("E = (call body: (E))").unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } #[test] fn recursion_in_capture() { - let query = Query::new("E = (call (E) @inner)").unwrap(); + let query = Query::try_from("E = (call (E) @inner)").unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } #[test] fn recursion_in_sequence() { - let query = Query::new("E = (call {(a) (E)})").unwrap(); + let query = Query::try_from("E = (call {(a) (E)})").unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } #[test] fn recursion_through_multiple_children() { - let query = Query::new("E = [(x) (call (a) (E))]").unwrap(); + let query = Query::try_from("E = [(x) (call (a) (E))]").unwrap(); assert!(query.is_valid()); } @@ -95,7 +95,7 @@ fn mutual_recursion_no_escape() { A = (foo (B)) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path @@ -116,7 +116,7 @@ fn mutual_recursion_one_has_escape() { A = [(x) (foo (B))] B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -127,7 +127,7 @@ fn three_way_cycle_no_escape() { B = (b (C)) C = (c (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } @@ -139,7 +139,7 @@ fn three_way_cycle_one_has_escape() { B = (b (C)) C = (c (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -151,7 +151,7 @@ fn diamond_dependency() { C = (c (D)) D = (d (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } @@ -162,7 +162,7 @@ fn cycle_ref_in_field() { A = (foo body: (B)) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path @@ -183,7 +183,7 @@ fn cycle_ref_in_capture() { A = (foo (B) @cap) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path @@ -204,7 +204,7 @@ fn cycle_ref_in_sequence() { A = (foo {(x) (B)}) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path @@ -225,7 +225,7 @@ fn cycle_with_quantifier_escape() { A = (foo (B)?) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -235,7 +235,7 @@ fn cycle_with_plus_no_escape() { A = (foo (B)+) B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } @@ -246,7 +246,7 @@ fn non_recursive_reference() { Leaf = (identifier) Tree = (call (Leaf)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -256,13 +256,13 @@ fn entry_point_uses_recursive_def() { E = [(x) (call (E))] (program (E)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } #[test] fn direct_self_ref_in_alternation() { - let query = Query::new("E = [(E) (x)]").unwrap(); + let query = Query::try_from("E = [(E) (x)]").unwrap(); assert!(query.is_valid()); } @@ -271,7 +271,7 @@ fn escape_via_literal_string() { let input = indoc! {r#" A = [(A) "escape"] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -280,7 +280,7 @@ fn escape_via_wildcard() { let input = indoc! {r#" A = [(A) _] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -289,7 +289,7 @@ fn escape_via_childless_tree() { let input = indoc! {r#" A = [(A) (leaf)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -298,7 +298,7 @@ fn escape_via_anchor() { let input = indoc! {r#" A = (foo . [(A) (x)]) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -307,7 +307,7 @@ fn no_escape_tree_all_recursive() { let input = indoc! {r#" A = (foo (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); assert!(query.dump_diagnostics().contains("recursive pattern")); } @@ -317,7 +317,7 @@ fn escape_in_capture_inner() { let input = indoc! {r#" A = [(x)@cap (foo (A))] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); } @@ -326,6 +326,6 @@ fn ref_in_quantifier_plus_no_escape() { let input = indoc! {r#" A = (foo (A)+) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); } diff --git a/crates/plotnik-lib/src/query/shapes_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs index 61e6a387..46f5964f 100644 --- a/crates/plotnik-lib/src/query/shapes_tests.rs +++ b/crates/plotnik-lib/src/query/shapes_tests.rs @@ -3,7 +3,7 @@ use indoc::indoc; #[test] fn tree_is_one() { - let query = Query::new("(identifier)").unwrap(); + let query = Query::try_from("(identifier)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -14,7 +14,7 @@ fn tree_is_one() { #[test] fn singleton_seq_is_one() { - let query = Query::new("{(identifier)}").unwrap(); + let query = Query::try_from("{(identifier)}").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -26,7 +26,7 @@ fn singleton_seq_is_one() { #[test] fn nested_singleton_seq_is_one() { - let query = Query::new("{{{(identifier)}}}").unwrap(); + let query = Query::try_from("{{{(identifier)}}}").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -40,7 +40,7 @@ fn nested_singleton_seq_is_one() { #[test] fn multi_seq_is_many() { - let query = Query::new("{(a) (b)}").unwrap(); + let query = Query::try_from("{(a) (b)}").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -53,7 +53,7 @@ fn multi_seq_is_many() { #[test] fn alt_is_one() { - let query = Query::new("[(a) (b)]").unwrap(); + let query = Query::try_from("[(a) (b)]").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -71,7 +71,7 @@ fn alt_with_seq_branches() { let input = indoc! {r#" [{(a) (b)} (c)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -92,7 +92,7 @@ fn ref_to_tree_is_one() { X = (identifier) (call (X)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ @@ -110,7 +110,7 @@ fn ref_to_seq_is_many() { X = {(a) (b)} (call (X)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ @@ -126,7 +126,7 @@ fn ref_to_seq_is_many() { #[test] fn field_with_tree() { - let query = Query::new("(call name: (identifier))").unwrap(); + let query = Query::try_from("(call name: (identifier))").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -139,7 +139,7 @@ fn field_with_tree() { #[test] fn field_with_alt() { - let query = Query::new("(call name: [(identifier) (string)])").unwrap(); + let query = Query::try_from("(call name: [(identifier) (string)])").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -156,7 +156,7 @@ fn field_with_alt() { #[test] fn field_with_seq_error() { - let query = Query::new("(call name: {(a) (b)})").unwrap(); + let query = Query::try_from("(call name: {(a) (b)})").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -181,7 +181,7 @@ fn field_with_ref_to_seq_error() { X = {(a) (b)} (call name: (X)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ @@ -204,7 +204,7 @@ fn field_with_ref_to_seq_error() { #[test] fn quantifier_preserves_inner_shape() { - let query = Query::new("(identifier)*").unwrap(); + let query = Query::try_from("(identifier)*").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -216,7 +216,7 @@ fn quantifier_preserves_inner_shape() { #[test] fn capture_preserves_inner_shape() { - let query = Query::new("(identifier) @name").unwrap(); + let query = Query::try_from("(identifier) @name").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -228,7 +228,7 @@ fn capture_preserves_inner_shape() { #[test] fn capture_on_seq() { - let query = Query::new("{(a) (b)} @items").unwrap(); + let query = Query::try_from("{(a) (b)} @items").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -248,7 +248,7 @@ fn complex_nested_shapes() { name: (identifier) @name body: (block (Stmt)* @stmts)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ @@ -276,7 +276,7 @@ fn tagged_alt_shapes() { let input = indoc! {r#" [Ident: (identifier) Num: (number)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -291,7 +291,7 @@ fn tagged_alt_shapes() { #[test] fn anchor_has_no_cardinality() { - let query = Query::new("(block . (statement))").unwrap(); + let query = Query::try_from("(block . (statement))").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -304,7 +304,7 @@ fn anchor_has_no_cardinality() { #[test] fn negated_field_has_no_cardinality() { - let query = Query::new("(function !async)").unwrap(); + let query = Query::try_from("(function !async)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -316,7 +316,7 @@ fn negated_field_has_no_cardinality() { #[test] fn tree_with_wildcard_type() { - let query = Query::new("(_)").unwrap(); + let query = Query::try_from("(_)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -327,7 +327,7 @@ fn tree_with_wildcard_type() { #[test] fn bare_wildcard_is_one() { - let query = Query::new("_").unwrap(); + let query = Query::try_from("_").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -338,7 +338,7 @@ fn bare_wildcard_is_one() { #[test] fn empty_seq_is_one() { - let query = Query::new("{}").unwrap(); + let query = Query::try_from("{}").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -349,7 +349,7 @@ fn empty_seq_is_one() { #[test] fn literal_is_one() { - let query = Query::new(r#""if""#).unwrap(); + let query = Query::try_from(r#""if""#).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r#" Root¹ @@ -360,7 +360,7 @@ fn literal_is_one() { #[test] fn invalid_error_node() { - let query = Query::new("(foo %)").unwrap(); + let query = Query::try_from("(foo %)").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_cst_with_cardinalities(), @r#" Root¹ @@ -376,7 +376,7 @@ fn invalid_error_node() { #[test] fn invalid_undefined_ref() { - let query = Query::new("(Undefined)").unwrap(); + let query = Query::try_from("(Undefined)").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -387,7 +387,7 @@ fn invalid_undefined_ref() { #[test] fn invalid_branch_without_body() { - let query = Query::new("[A:]").unwrap(); + let query = Query::try_from("[A:]").unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ @@ -403,7 +403,7 @@ fn invalid_ref_to_bodyless_def() { X = % (X) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root⁺ diff --git a/crates/plotnik-lib/src/query/symbol_table_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs index cdaa5c32..e512f798 100644 --- a/crates/plotnik-lib/src/query/symbol_table_tests.rs +++ b/crates/plotnik-lib/src/query/symbol_table_tests.rs @@ -4,7 +4,7 @@ use indoc::indoc; #[test] fn single_definition() { let input = "Expr = (expression)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @"Expr"); } @@ -17,7 +17,7 @@ fn multiple_definitions() { Decl = (declaration) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -33,7 +33,7 @@ fn valid_reference() { Call = (call_expression function: (Expr)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -46,7 +46,7 @@ fn valid_reference() { fn undefined_reference() { let input = "Call = (call_expression function: (Undefined))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `Undefined` @@ -60,7 +60,7 @@ fn undefined_reference() { fn self_reference() { let input = "Expr = [(identifier) (call (Expr))]"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -75,7 +75,7 @@ fn mutual_recursion() { B = (bar (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: recursive pattern can never match: cycle `B` → `A` → `B` has no escape path @@ -97,7 +97,7 @@ fn duplicate_definition() { Expr = (other) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: duplicate definition: `Expr` @@ -114,7 +114,7 @@ fn reference_in_alternation() { Value = [(Expr) (literal)] "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -130,7 +130,7 @@ fn reference_in_sequence() { Pair = {(Expr) (Expr)} "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -146,7 +146,7 @@ fn reference_in_quantifier() { List = (Expr)* "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -162,7 +162,7 @@ fn reference_in_capture() { Named = (Expr) @e "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" Expr @@ -178,7 +178,7 @@ fn entry_point_reference() { (call function: (Expr)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @"Expr"); } @@ -187,7 +187,7 @@ fn entry_point_reference() { fn entry_point_undefined_reference() { let input = "(call function: (Unknown))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `Unknown` @@ -200,7 +200,7 @@ fn entry_point_undefined_reference() { #[test] fn no_definitions() { let input = "(identifier)"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @""); } @@ -214,7 +214,7 @@ fn nested_references() { D = (d (C) (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" A @@ -235,7 +235,7 @@ fn nested_references() { fn multiple_undefined() { let input = "(foo (X) (Y) (Z))"; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(!query.is_valid()); insta::assert_snapshot!(query.dump_diagnostics(), @r" error: undefined reference: `X` @@ -260,7 +260,7 @@ fn reference_inside_tree_child() { B = (b (A)) "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" A @@ -276,7 +276,7 @@ fn reference_inside_capture() { B = (A)@x "#}; - let query = Query::new(input).unwrap(); + let query = Query::try_from(input).unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_symbols(), @r" A