diff --git a/AGENTS.md b/AGENTS.md index b9d959cc..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 - named_defs.rs # Name resolution, symbol table - ref_cycles.rs # Escape analysis (recursion validation) - shape_cardinalities.rs # Shape inference + alt_kinds.rs # Alternation validation + symbol_table.rs # Name resolution, symbol table + 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 -named_defs::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". @@ -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 @@ -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-cli/src/commands/debug/mod.rs b/crates/plotnik-cli/src/commands/debug/mod.rs index 6aad3358..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); }) @@ -96,7 +96,9 @@ 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)); + std::process::exit(1); } } 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..89202b12 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -5,14 +5,13 @@ //! ``` //! 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"); +//! "#; //! -//! if !query.is_valid() { -//! eprintln!("{}", query.render_diagnostics()); -//! } +//! let query = Query::try_from(source).expect("out of fuel"); +//! eprintln!("{}", query.diagnostics().render(source)); //! ``` #![cfg_attr(coverage_nightly, feature(coverage_attribute))] @@ -28,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.rs b/crates/plotnik-lib/src/parser/ast.rs index 3b53efb3..0a378354 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) => { @@ -15,29 +16,91 @@ macro_rules! ast_node { (node.kind() == SyntaxKind::$kind).then(|| Self(node)) } - pub fn syntax(&self) -> &SyntaxNode { + pub fn as_cst(&self) -> &SyntaxNode { &self.0 } + + pub fn text_range(&self) -> TextRange { + self.0.text_range() + } + } + }; +} + +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); +ast_node!(NamedNode, Tree); ast_node!(Ref, Ref); -ast_node!(Str, Str); -ast_node!(Alt, Alt); +ast_node!(AltExpr, Alt); ast_node!(Branch, Branch); -ast_node!(Seq, Seq); -ast_node!(Capture, Capture); +ast_node!(SeqExpr, Seq); +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 { @@ -49,58 +112,16 @@ 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 syntax(&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(), - } - } -} - -// --- Accessors --- +define_expr!( + NamedNode, + Ref, + AnonymousNode, + AltExpr, + SeqExpr, + CapturedExpr, + QuantifiedExpr, + FieldExpr, +); impl Root { pub fn defs(&self) -> impl Iterator + '_ { @@ -125,7 +146,7 @@ impl Def { } } -impl Tree { +impl NamedNode { pub fn node_type(&self) -> Option { self.0 .children_with_tokens() @@ -141,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) } @@ -155,7 +183,7 @@ impl Ref { } } -impl Alt { +impl AltExpr { pub fn kind(&self) -> AltKind { let mut tagged = false; let mut untagged = false; @@ -202,13 +230,13 @@ impl Branch { } } -impl Seq { +impl SeqExpr { pub fn children(&self) -> impl Iterator + '_ { self.0.children().filter_map(Expr::cast) } } -impl Capture { +impl CapturedExpr { pub fn name(&self) -> Option { self.0 .children_with_tokens() @@ -234,7 +262,7 @@ impl Type { } } -impl Quantifier { +impl QuantifiedExpr { pub fn inner(&self) -> Option { self.0.children().find_map(Expr::cast) } @@ -257,7 +285,7 @@ impl Quantifier { } } -impl Field { +impl FieldExpr { pub fn name(&self) -> Option { self.0 .children_with_tokens() @@ -278,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 cf974604..40c5bc7f 100644 --- a/crates/plotnik-lib/src/parser/ast_tests.rs +++ b/crates/plotnik-lib/src/parser/ast_tests.rs @@ -3,13 +3,13 @@ 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#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree identifier - "#); + NamedNode identifier + "); } #[test] @@ -18,72 +18,72 @@ 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#" + 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(); + let query = Query::try_from("(_)").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] 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 Def - Str "if" + AnonymousNode "if" "#); } #[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#" + 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(); + let query = Query::try_from("(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(); + let query = Query::try_from("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] @@ -93,30 +93,30 @@ 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#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def Expr - Tree identifier + NamedNode identifier Def - Tree call + NamedNode call Ref Expr - "#); + "); } #[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 Def Alt Branch - Tree identifier + NamedNode identifier Branch - Tree number + NamedNode number "); } @@ -126,104 +126,104 @@ 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 Def Alt Branch Ident: - Tree identifier + NamedNode identifier Branch Num: - Tree number + NamedNode number "); } #[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#" + 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(); + let query = Query::try_from("(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(); + let query = Query::try_from("(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(); + let query = Query::try_from("(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(); + let query = Query::try_from("(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 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#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree block - Anchor - Tree statement - "#); + NamedNode block + . + NamedNode statement + "); } #[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#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree function + NamedNode function NegatedField !async - "#); + "); } #[test] @@ -237,29 +237,29 @@ 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 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 "); } #[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,13 +271,13 @@ 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#" + insta::assert_snapshot!(query.dump_ast(), @r" Root Def - Tree expression - "#); + NamedNode expression + "); } #[test] @@ -289,21 +289,21 @@ 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 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/core.rs b/crates/plotnik-lib/src/parser/core.rs index 03ab2132..2d459d6c 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -1,65 +1,45 @@ -//! 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}; +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; use crate::Error; -const DEFAULT_EXEC_FUEL: u32 = 1_000_000; -const DEFAULT_RECURSION_FUEL: u32 = 4096; +#[derive(Debug)] +pub struct ParseResult { + pub root: Root, + pub diagnostics: Diagnostics, + pub exec_fuel_consumed: u32, +} -/// 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. Never replenishes. + exec_fuel_initial: Option, exec_fuel_remaining: Option, - - /// Recursion depth limit. recursion_fuel_limit: Option, - - /// Fatal error that stops parsing (fuel exhaustion). fatal_error: Option, } @@ -76,38 +56,52 @@ impl<'src> Parser<'src> { last_diagnostic_pos: None, delimiter_stack: Vec::with_capacity(8), debug_fuel: std::cell::Cell::new(256), - exec_fuel_remaining: Some(DEFAULT_EXEC_FUEL), - recursion_fuel_limit: Some(DEFAULT_RECURSION_FUEL), + exec_fuel_initial: None, + exec_fuel_remaining: None, + recursion_fuel_limit: None, 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 } - /// Set recursion depth limit. None = infinite. pub fn with_recursion_fuel(mut self, limit: Option) -> Self { self.recursion_fuel_limit = limit; self } - pub fn finish(mut self) -> Result<(GreenNode, Diagnostics), Error> { + pub fn parse(mut self) -> Result { + self.parse_root(); + 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, + exec_fuel_consumed, + }) + } + + fn finish(mut self) -> Result<(GreenNode, Diagnostics, u32), Error> { self.drain_trivia(); if let Some(err) = self.fatal_error { return Err(err); } - Ok((self.builder.finish(), self.diagnostics)) + 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, exec_fuel_consumed)) } - /// 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) } @@ -116,7 +110,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(); @@ -125,7 +118,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 { @@ -152,7 +144,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() } @@ -165,13 +156,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; @@ -208,14 +198,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()); } @@ -224,13 +212,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"); @@ -244,7 +230,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"); @@ -264,7 +249,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; @@ -283,8 +268,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() { @@ -294,9 +277,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); @@ -311,13 +292,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; @@ -337,8 +311,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 = @@ -369,7 +341,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, @@ -377,12 +348,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, @@ -401,8 +370,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() { @@ -412,7 +379,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/parser/mod.rs b/crates/plotnik-lib/src/parser/mod.rs index 1d9ef9a0..254bd9cc 100644 --- a/crates/plotnik-lib/src/parser/mod.rs +++ b/crates/plotnik-lib/src/parser/mod.rs @@ -38,49 +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::{ - Alt, AltKind, Anchor, Branch, Capture, Def, Expr, Field, NegatedField, Quantifier, Ref, Root, - Seq, Str, Tree, Type, Wildcard, + AltExpr, AltKind, Anchor, AnonymousNode, Branch, CapturedExpr, Def, Expr, FieldExpr, NamedNode, + NegatedField, QuantifiedExpr, Ref, Root, SeqExpr, Type, }; -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 { - 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 { - parser.parse_root(); - let (cst, diagnostics) = parser.finish()?; - Ok((Parse { cst }, diagnostics)) -} +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_kind.rs b/crates/plotnik-lib/src/query/alt_kind.rs deleted file mode 100644 index 2e6100a8..00000000 --- a/crates/plotnik-lib/src/query/alt_kind.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Semantic validation for the typed AST. -//! -//! Checks constraints that are easier to express after parsing: -//! - Mixed tagged/untagged alternations - -use rowan::TextRange; - -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::{Alt, AltKind, Branch, Expr, Root}; - -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); - } - } - - assert_root_no_bare_exprs(root); - - Ok(((), errors)) -} - -fn validate_expr(expr: &Expr, errors: &mut Diagnostics) { - match expr { - Expr::Alt(alt) => { - check_mixed_alternation(alt, errors); - for branch in alt.branches() { - if let Some(body) = branch.body() { - validate_expr(&body, errors); - } - } - assert_alt_no_bare_exprs(alt); - } - Expr::Tree(tree) => { - for child in tree.children() { - validate_expr(&child, errors); - } - } - Expr::Seq(seq) => { - for child in seq.children() { - validate_expr(&child, errors); - } - } - Expr::Capture(cap) => { - if let Some(inner) = cap.inner() { - validate_expr(&inner, errors); - } - } - Expr::Quantifier(q) => { - if let Some(inner) = q.inner() { - validate_expr(&inner, errors); - } - } - Expr::Field(f) => { - if let Some(value) = f.value() { - validate_expr(&value, errors); - } - } - Expr::Ref(_) - | Expr::Str(_) - | Expr::Wildcard(_) - | Expr::Anchor(_) - | Expr::NegatedField(_) => {} - } -} - -fn check_mixed_alternation(alt: &Alt, 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); - } - - if first_tagged.is_some() && first_untagged.is_some() { - break; - } - } - - 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 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(); -} - -fn branch_range(branch: &Branch) -> TextRange { - branch.syntax().text_range() -} diff --git a/crates/plotnik-lib/src/query/alt_kinds.rs b/crates/plotnik-lib/src/query/alt_kinds.rs new file mode 100644 index 00000000..af16df76 --- /dev/null +++ b/crates/plotnik-lib/src/query/alt_kinds.rs @@ -0,0 +1,92 @@ +//! Semantic validation for the typed AST. +//! +//! Checks constraints that are easier to express after parsing: +//! - Mixed tagged/untagged alternations + +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::parser::{AltExpr, AltKind, Branch, Expr}; + +impl Query<'_> { + pub(super) fn validate_alt_kinds(&mut self) { + 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_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); + } + Expr::NamedNode(node) => { + for child in node.children() { + self.validate_alt_expr(&child); + } + } + Expr::SeqExpr(seq) => { + for child in seq.children() { + self.validate_alt_expr(&child); + } + } + Expr::CapturedExpr(cap) => { + let Some(inner) = cap.inner() else { return }; + self.validate_alt_expr(&inner); + } + Expr::QuantifiedExpr(q) => { + let Some(inner) = q.inner() else { return }; + self.validate_alt_expr(&inner); + } + Expr::FieldExpr(f) => { + let Some(value) = f.value() else { return }; + self.validate_alt_expr(&value); + } + Expr::Ref(_) | Expr::AnonymousNode(_) => {} + } + } + + fn check_mixed_alternation(&mut self, alt: &AltExpr) { + if alt.kind() != AltKind::Mixed { + return; + } + + 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_range = tagged_branch + .label() + .map(|t| t.text_range()) + .unwrap_or_else(|| branch_range(tagged_branch)); + + let untagged_range = branch_range(untagged_branch); + + 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 { + branch.text_range() +} diff --git a/crates/plotnik-lib/src/query/alt_kind_tests.rs b/crates/plotnik-lib/src/query/alt_kinds_tests.rs similarity index 82% rename from crates/plotnik-lib/src/query/alt_kind_tests.rs rename to crates/plotnik-lib/src/query/alt_kinds_tests.rs index 4d1e7c95..4c1ee966 100644 --- a/crates/plotnik-lib/src/query/alt_kind_tests.rs +++ b/crates/plotnik-lib/src/query/alt_kinds_tests.rs @@ -2,37 +2,37 @@ 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 Def Alt Branch A: - Tree a + NamedNode a Branch B: - Tree b + NamedNode b "); } #[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 Def Alt Branch - Tree a + NamedNode a Branch - Tree b + NamedNode b "); } #[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,13 +102,13 @@ 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 Def Alt Branch A: - Tree a + NamedNode a "); } diff --git a/crates/plotnik-lib/src/query/dump.rs b/crates/plotnik-lib/src/query/dump.rs index 5e03811b..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; @@ -28,7 +30,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/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/mod.rs b/crates/plotnik-lib/src/query/mod.rs index 6e37fc96..6d4491ff 100644 --- a/crates/plotnik-lib/src/query/mod.rs +++ b/crates/plotnik-lib/src/query/mod.rs @@ -1,230 +1,226 @@ -//! 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; mod printer; pub use printer::QueryPrinter; -pub mod alt_kind; -pub mod named_defs; -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 named_defs_tests; -#[cfg(test)] mod printer_tests; #[cfg(test)] -mod ref_cycles_tests; +mod recursion_tests; +#[cfg(test)] +mod shapes_tests; #[cfg(test)] -mod shape_cardinalities_tests; +mod symbol_table_tests; use std::collections::HashMap; +use rowan::GreenNodeBuilder; + use crate::Result; use crate::diagnostics::Diagnostics; +use crate::parser::cst::SyntaxKind; use crate::parser::lexer::lex; -use crate::parser::{self, Parse, Parser, Root, SyntaxNode}; -use named_defs::SymbolTable; -use shape_cardinalities::ShapeCardinality; +use crate::parser::{ParseResult, Parser, Root, SyntaxNode, ast}; + +const DEFAULT_EXEC_FUEL: u32 = 1_000_000; +const DEFAULT_RECURSION_FUEL: u32 = 4096; + +use shapes::ShapeCardinality; +use symbol_table::SymbolTable; -/// Builder for configuring and creating a [`Query`]. -pub struct QueryBuilder<'a> { +/// A parsed and analyzed query. +/// +/// 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)] +pub struct Query<'a> { source: &'a str, - exec_fuel: Option>, - recursion_fuel: Option>, + ast: Root, + symbol_table: SymbolTable<'a>, + shape_cardinality_table: HashMap, + exec_fuel: Option, + recursion_fuel: Option, + exec_fuel_consumed: u32, + parse_diagnostics: Diagnostics, + alt_kind_diagnostics: Diagnostics, + resolve_diagnostics: Diagnostics, + recursion_diagnostics: Diagnostics, + shapes_diagnostics: Diagnostics, } -impl<'a> QueryBuilder<'a> { - /// Create a new builder for the given source. +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> { + /// Create a new query from source text. + /// + /// Call [`exec`](Self::exec) to run analysis passes. pub fn new(source: &'a str) -> Self { Self { source, - exec_fuel: None, - recursion_fuel: None, + ast: empty_root(), + symbol_table: SymbolTable::default(), + shape_cardinality_table: HashMap::new(), + 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(), + recursion_diagnostics: Diagnostics::new(), + shapes_diagnostics: Diagnostics::new(), } } /// Set execution fuel limit. None = infinite. /// /// Execution fuel never replenishes. It protects against large inputs. - /// Returns error when exhausted. + /// Returns error from [`exec`](Self::exec) when exhausted. pub fn with_exec_fuel(mut self, limit: Option) -> Self { - self.exec_fuel = Some(limit); + 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. + /// deeply nested input. Returns error from [`exec`](Self::exec) when exhausted. pub fn with_recursion_fuel(mut self, limit: Option) -> Self { - self.recursion_fuel = Some(limit); + self.recursion_fuel = limit; self } - /// Build the query, running all analysis passes. + /// Run all analysis passes. /// /// 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)) + /// 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) } -} -/// A parsed and analyzed query. -/// -/// Construction succeeds unless fuel limits are exceeded. -/// Check [`is_valid`](Self::is_valid) or [`diagnostics`](Self::diagnostics) -/// to determine if the query has syntax/semantic issues. -#[derive(Debug, Clone)] -pub struct Query<'a> { - source: &'a str, - parse: Parse, - symbols: SymbolTable, - shape_cardinalities: HashMap, - // Diagnostics per pass - parse_diagnostics: Diagnostics, - alt_kind_diagnostics: Diagnostics, - resolve_diagnostics: Diagnostics, - ref_cycle_diagnostics: Diagnostics, - shape_diagnostics: Diagnostics, -} + fn try_parse(&mut self) -> Result<()> { + let tokens = lex(self.source); + let parser = Parser::new(self.source, tokens) + .with_exec_fuel(self.exec_fuel) + .with_recursion_fuel(self.recursion_fuel); -impl<'a> Query<'a> { - /// Parse and analyze a 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() + let ParseResult { + root, + diagnostics, + exec_fuel_consumed, + } = parser.parse()?; + self.ast = root; + self.parse_diagnostics = diagnostics; + self.exec_fuel_consumed = exec_fuel_consumed; + Ok(()) } - /// Create a builder for configuring parser limits. - pub fn builder(source: &'a str) -> QueryBuilder<'a> { - QueryBuilder::new(source) + pub(crate) fn as_cst(&self) -> &SyntaxNode { + self.ast.as_cst() } - fn from_parse(source: &'a str, parse: Parse, parse_diagnostics: Diagnostics) -> Self { - let root = Root::cast(parse.syntax()).expect("parser always produces Root"); - - let ((), alt_kind_diagnostics) = - alt_kind::validate(&root).expect("alt_kind::validate is infallible"); - - let (symbols, resolve_diagnostics) = - named_defs::resolve(&root).expect("named_defs::resolve is infallible"); - - let ((), ref_cycle_diagnostics) = - ref_cycles::validate(&root, &symbols).expect("ref_cycles::validate is infallible"); - - let (shape_cardinalities, shape_diagnostics) = - shape_cardinalities::analyze(&root, &symbols) - .expect("shape_cardinalities::analyze is infallible"); - - Self { - source, - parse, - symbols, - shape_cardinalities, - parse_diagnostics, - alt_kind_diagnostics, - resolve_diagnostics, - ref_cycle_diagnostics, - shape_diagnostics, - } + pub(crate) fn root(&self) -> &Root { + &self.ast } - #[allow(dead_code)] - pub fn source(&self) -> &str { - self.source - } + pub(crate) fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { + // Error nodes are invalid + if node.kind() == SyntaxKind::Error { + return ShapeCardinality::Invalid; + } - pub fn syntax(&self) -> SyntaxNode { - self.parse.syntax() - } + // 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 + }; + } - pub fn root(&self) -> Root { - Root::cast(self.parse.syntax()).expect("parser always produces Root") - } + // 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); + } - pub fn symbols(&self) -> &SymbolTable { - &self.symbols - } + // 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); + } - pub fn shape_cardinality(&self, node: &SyntaxNode) -> ShapeCardinality { - self.shape_cardinalities - .get(node) - .copied() + // Expr: direct lookup + ast::Expr::cast(node.clone()) + .and_then(|e| self.shape_cardinality_table.get(&e).copied()) .unwrap_or(ShapeCardinality::One) } /// 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()); 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 } - 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() && !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() } +} + +impl<'a> TryFrom<&'a str> for Query<'a> { + type Error = crate::Error; - pub fn render_diagnostics(&self) -> String { - self.all_diagnostics().printer(self.source).render() + fn try_from(source: &'a str) -> Result { + Self::new(source).exec() } +} + +impl<'a> TryFrom<&'a String> for Query<'a> { + type Error = crate::Error; - pub fn render_diagnostics_colored(&self, colored: bool) -> String { - self.all_diagnostics() - .printer(self.source) - .colored(colored) - .render() + 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 56ce056c..50e2fd2c 100644 --- a/crates/plotnik-lib/src/query/mod_tests.rs +++ b/crates/plotnik-lib/src/query/mod_tests.rs @@ -2,28 +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()); - assert!(q.symbols().get("Expr").is_some()); } #[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/named_defs.rs b/crates/plotnik-lib/src/query/named_defs.rs deleted file mode 100644 index 18ed6cfe..00000000 --- a/crates/plotnik-lib/src/query/named_defs.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Name resolution: builds symbol table and checks references. -//! -//! Two-pass approach: -//! 1. Collect all `Name = expr` definitions -//! 2. Check that all `(UpperIdent)` references are defined - -use indexmap::{IndexMap, IndexSet}; -use rowan::TextRange; - -use crate::PassResult; -use crate::diagnostics::Diagnostics; -use crate::parser::{Expr, Ref, Root}; - -#[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 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(); - let mut diagnostics = Diagnostics::new(); - - // Pass 1: collect definitions - for def in root.defs() { - let Some(name_token) = def.name() else { - continue; - }; - - let name = name_token.text().to_string(); - let range = name_token.text_range(); - - if defs.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); - } - 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 }; - collect_reference_diagnostics(&body, &symbols, &mut diagnostics); - } - - // Parser wraps all top-level exprs in Def nodes, so this should be empty - assert!( - root.exprs().next().is_none(), - "named_defs: unexpected bare Expr in Root (parser should wrap in Def)" - ); - - 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::Tree(tree) => { - for child in tree.children() { - collect_refs(&child, refs); - } - } - Expr::Alt(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::Seq(seq) => { - for child in seq.children() { - collect_refs(&child, refs); - } - } - Expr::Capture(cap) => { - let Some(inner) = cap.inner() else { return }; - collect_refs(&inner, refs); - } - Expr::Quantifier(q) => { - let Some(inner) = q.inner() else { return }; - collect_refs(&inner, refs); - } - Expr::Field(f) => { - let Some(value) = f.value() else { return }; - collect_refs(&value, refs); - } - Expr::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => {} - } -} - -fn collect_reference_diagnostics( - expr: &Expr, - symbols: &SymbolTable, - diagnostics: &mut Diagnostics, -) { - match expr { - Expr::Ref(r) => { - check_ref_diagnostic(r, symbols, diagnostics); - } - Expr::Tree(tree) => { - for child in tree.children() { - collect_reference_diagnostics(&child, symbols, diagnostics); - } - } - Expr::Alt(alt) => { - for branch in alt.branches() { - let Some(body) = branch.body() else { continue }; - collect_reference_diagnostics(&body, symbols, diagnostics); - } - // 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::Seq(seq) => { - for child in seq.children() { - collect_reference_diagnostics(&child, symbols, diagnostics); - } - } - Expr::Capture(cap) => { - let Some(inner) = cap.inner() else { return }; - collect_reference_diagnostics(&inner, symbols, diagnostics); - } - Expr::Quantifier(q) => { - let Some(inner) = q.inner() else { return }; - collect_reference_diagnostics(&inner, symbols, diagnostics); - } - Expr::Field(f) => { - let Some(value) = f.value() else { return }; - collect_reference_diagnostics(&value, symbols, diagnostics); - } - Expr::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => {} - } -} - -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() { - return; - } - - diagnostics - .error( - format!("undefined reference: `{}`", name), - name_token.text_range(), - ) - .emit(); -} diff --git a/crates/plotnik-lib/src/query/printer.rs b/crates/plotnik-lib/src/query/printer.rs index fbb8581c..2f02712e 100644 --- a/crates/plotnik-lib/src/query/printer.rs +++ b/crates/plotnik-lib/src/query/printer.rs @@ -1,11 +1,14 @@ +//! AST/CST pretty-printer for debugging and test snapshots. + 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; +use super::shapes::ShapeCardinality; pub struct QueryPrinter<'q, 'src> { query: &'q Query<'src>, @@ -64,30 +67,29 @@ 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 { - use indexmap::IndexSet; use std::collections::HashMap; - let symbols = &self.query.symbols; + let symbols = &self.query.symbol_table; if symbols.is_empty() { 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() { 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()); } } - 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 +125,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.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(); for r in refs { self.format_symbol_tree(r, indent + 1, defined, body_nodes, visited, w)?; @@ -166,8 +169,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.text_range()); writeln!(w, "Root{}{}", card, span)?; for def in root.defs() { @@ -183,8 +186,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.text_range()); let name = def.name().map(|t| t.text().to_string()); match name { @@ -200,29 +203,35 @@ 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.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)?, - } - for child in t.children() { - self.format_expr(&child, indent + 1, w)?; + 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(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) => { + ast::Expr::AltExpr(a) => { writeln!(w, "{}Alt{}{}", prefix, card, span)?; for branch in a.branches() { self.format_branch(&branch, indent + 1, w)?; @@ -231,62 +240,88 @@ 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)?; - 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) => { + 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::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)?; - } - 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.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) { + self.format_expr(&expr, indent, w)?; } } Ok(()) } + fn mark_anchor(&self, indent: usize, w: &mut impl Write) -> std::fmt::Result { + let prefix = " ".repeat(indent); + writeln!(w, "{}.", prefix) + } + + 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, @@ -294,8 +329,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.text_range()); let label = branch.label().map(|t| t.text().to_string()); match label { @@ -337,3 +372,12 @@ impl Query<'_> { QueryPrinter::new(self) } } + +fn collect_refs(expr: &Expr) -> IndexSet { + expr.as_cst() + .descendants() + .filter_map(ast::Ref::cast) + .filter_map(|r| r.name()) + .map(|tok| tok.text().to_string()) + .collect() +} diff --git a/crates/plotnik-lib/src/query/printer_tests.rs b/crates/plotnik-lib/src/query/printer_tests.rs index 3ebd0ff3..be5b8f51 100644 --- a/crates/plotnik-lib/src/query/printer_tests.rs +++ b/crates/plotnik-lib/src/query/printer_tests.rs @@ -3,27 +3,27 @@ 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] - Tree [0..6] call + NamedNode [0..6] call "); } #[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¹ - Tree¹ call + NamedNode¹ call "); } #[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,90 +45,90 @@ 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 Alt Branch A: - Tree a + NamedNode a Branch B: - Tree b + NamedNode b "); } #[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 - Capture @x :: T - Tree call + CapturedExpr @x :: T + NamedNode call "); } #[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 - Quantifier * - Tree a + QuantifiedExpr * + NamedNode a Def - Quantifier + - Tree b + QuantifiedExpr + + NamedNode b Def - Quantifier ? - Tree c + QuantifiedExpr ? + NamedNode c "); } #[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 - Tree call - Field name: - Tree id + NamedNode call + FieldExpr name: + NamedNode id "); } #[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 - Tree call + NamedNode call NegatedField !name "); } #[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 - Tree call - Wildcard - Anchor - Tree arg + NamedNode call + AnonymousNode (any) + . + NamedNode arg "); } #[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 - Tree call - Str "foo" + NamedNode call + AnonymousNode "foo" "#); } @@ -138,13 +138,13 @@ 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 - Tree call + NamedNode call Def - Tree func + NamedNode func Ref 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,59 +219,59 @@ 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 - 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 "); } #[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] Seq [0..9] - Tree [1..4] a - Tree [5..8] b + NamedNode [1..4] a + NamedNode [5..8] b "); } #[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] - 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 "); } #[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] 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/recursion.rs b/crates/plotnik-lib/src/query/recursion.rs new file mode 100644 index 00000000..780ea9db --- /dev/null +++ b/crates/plotnik-lib/src/query/recursion.rs @@ -0,0 +1,336 @@ +//! Escape path analysis for recursive definitions. +//! +//! Detects patterns that can never match because they require +//! infinitely nested structures (recursion with no escape path). + +use indexmap::{IndexMap, IndexSet}; +use rowan::TextRange; + +use super::Query; +use crate::parser::{Def, Expr, SyntaxKind}; + +impl Query<'_> { + pub(super) fn validate_recursion(&mut self) { + let sccs = self.find_sccs(); + + 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) + }); + + 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); + } + } + + 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 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); + } + } + + 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 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 + } + + 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) => { + let Some(name_token) = r.name() else { + return true; + }; + !scc.contains(name_token.text()) + } + Expr::NamedNode(node) => { + 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() + .map(|body| expr_has_escape(&body, scc)) + .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 + | SyntaxKind::Star + | SyntaxKind::QuestionQuestion + | SyntaxKind::StarQuestion, + ) => true, + Some(SyntaxKind::Plus | SyntaxKind::PlusQuestion) => q + .inner() + .map(|inner| expr_has_escape(&inner, scc)) + .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, + } +} + +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_ref_in_expr(expr: &Expr, target: &str) -> Option { + match expr { + Expr::Ref(r) => { + let name_token = r.name()?; + if name_token.text() == target { + return Some(name_token.text_range()); + } + None + } + Expr::NamedNode(node) => node + .children() + .find_map(|child| find_ref_in_expr(&child, target)), + 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::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)), + 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)), + Expr::AnonymousNode(_) => None, + } +} diff --git a/crates/plotnik-lib/src/query/ref_cycles_tests.rs b/crates/plotnik-lib/src/query/recursion_tests.rs similarity index 78% rename from crates/plotnik-lib/src/query/ref_cycles_tests.rs rename to crates/plotnik-lib/src/query/recursion_tests.rs index 0cc79421..59b14b5b 100644 --- a/crates/plotnik-lib/src/query/ref_cycles_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/ref_cycles.rs b/crates/plotnik-lib/src/query/ref_cycles.rs deleted file mode 100644 index c8bd8da8..00000000 --- a/crates/plotnik-lib/src/query/ref_cycles.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Escape path analysis for recursive definitions. -//! -//! Detects patterns that can never match because they require -//! infinitely nested structures (recursion with no escape path). - -use indexmap::{IndexMap, IndexSet}; -use rowan::TextRange; - -use super::named_defs::SymbolTable; -use crate::PassResult; -use crate::diagnostics::Diagnostics; -use crate::parser::{Def, Expr, Root, SyntaxKind}; - -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 { - continue; - }; - - if !def_info.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) { - let chain = build_self_ref_chain(root, name); - emit_error(&mut errors, name, &scc, chain); - } - continue; - } - - 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; - } - } - - if !any_has_escape { - let chain = build_cycle_chain(root, symbols, &scc); - emit_error(&mut errors, &scc[0], &scc, chain); - } - } - - Ok(((), errors)) -} - -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; - }; - !scc.contains(name_token.text()) - } - Expr::Tree(tree) => { - let children: Vec<_> = tree.children().collect(); - children.is_empty() || children.iter().all(|c| expr_has_escape(c, scc)) - } - - Expr::Alt(alt) => { - alt.branches().any(|b| { - b.body() - .map(|body| expr_has_escape(&body, scc)) - .unwrap_or(true) - }) || alt.exprs().any(|e| expr_has_escape(&e, scc)) - } - - Expr::Seq(seq) => seq.children().all(|c| expr_has_escape(&c, scc)), - - Expr::Quantifier(q) => match q.operator().map(|op| op.kind()) { - Some( - SyntaxKind::Question - | SyntaxKind::Star - | SyntaxKind::QuestionQuestion - | SyntaxKind::StarQuestion, - ) => true, - Some(SyntaxKind::Plus | SyntaxKind::PlusQuestion) => q - .inner() - .map(|inner| expr_has_escape(&inner, scc)) - .unwrap_or(true), - _ => true, - }, - - Expr::Capture(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::Str(_) | Expr::Wildcard(_) | Expr::Anchor(_) | Expr::NegatedField(_) => true, - } -} - -fn find_sccs(symbols: &SymbolTable) -> Vec> { - struct State<'a> { - symbols: &'a SymbolTable, - 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(def_info) = state.symbols.get(name) { - for ref_name in &def_info.refs { - if state.symbols.get(ref_name).is_none() { - continue; - } - if !state.indices.contains_key(ref_name) { - strongconnect(ref_name, state); - let ref_lowlink = state.lowlinks[ref_name]; - 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]; - 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.names() { - if !state.indices.contains_key(name) { - strongconnect(name, &mut state); - } - } - - state - .sccs - .into_iter() - .filter(|scc| { - scc.len() > 1 - || symbols - .get(&scc[0]) - .map(|d| d.refs.contains(&scc[0])) - .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 - } - } - Expr::Tree(tree) => tree - .children() - .find_map(|child| find_ref_in_expr(&child, target)), - Expr::Alt(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::Capture(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)), - _ => 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(def_info) = symbols.get(current) { - for ref_name in &def_info.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); - } - - builder.emit(); -} diff --git a/crates/plotnik-lib/src/query/shape_cardinalities.rs b/crates/plotnik-lib/src/query/shape_cardinalities.rs deleted file mode 100644 index 8dc1749e..00000000 --- a/crates/plotnik-lib/src/query/shape_cardinalities.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Shape cardinality analysis for query expressions. -//! -//! Determines whether an expression matches a single node position (`One`) -//! or multiple sequential positions (`Many`). Used to validate field constraints: -//! `field: expr` requires `expr` to have `ShapeCardinality::One`. -//! -//! `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, -}; -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 std::collections::HashMap; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ShapeCardinality { - One, - Many, - Invalid, -} - -pub fn analyze( - root: &Root, - symbols: &SymbolTable, -) -> PassResult> { - let mut result = 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.syntax().clone()); - } - } - - compute_node_cardinality(&root.syntax().clone(), symbols, &def_bodies, &mut result); - validate_node(&root.syntax().clone(), &result, &mut errors); - - Ok((result, errors)) -} - -fn compute_node_cardinality( - node: &SyntaxNode, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - let card = get_or_compute(node, symbols, def_bodies, cache); - - for child in node.children() { - compute_node_cardinality(&child, symbols, def_bodies, cache); - } - - card -} - -fn compute_single( - node: &SyntaxNode, - symbols: &SymbolTable, - 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.syntax(), 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)) - .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::Tree(_) - | Expr::Str(_) - | Expr::Wildcard(_) - | Expr::Anchor(_) - | Expr::Field(_) - | Expr::NegatedField(_) - | Expr::Alt(_) => ShapeCardinality::One, - - Expr::Seq(ref seq) => seq_cardinality(seq, symbols, def_bodies, cache), - - Expr::Capture(ref cap) => { - let inner = ensure_capture_has_inner(cap.inner()); - get_or_compute(inner.syntax(), 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) - } - - Expr::Ref(ref r) => ref_cardinality(r, symbols, def_bodies, cache), - } -} - -fn get_or_compute( - node: &SyntaxNode, - symbols: &SymbolTable, - def_bodies: &HashMap, - cache: &mut HashMap, -) -> ShapeCardinality { - if let Some(&c) = cache.get(node) { - return c; - } - let c = compute_single(node, symbols, def_bodies, cache); - cache.insert(node.clone(), c); - c -} - -fn seq_cardinality( - seq: &Seq, - 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].syntax(), symbols, def_bodies, cache), - _ => 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; - } - - let Some(body_node) = def_bodies.get(name) else { - return ShapeCardinality::Invalid; - }; - - get_or_compute(body_node, symbols, def_bodies, cache) -} - -fn validate_node( - node: &SyntaxNode, - cardinalities: &HashMap, - errors: &mut Diagnostics, -) { - if let Some(field) = Field::cast(node.clone()) - && let Some(value) = field.value() - { - let card = cardinalities - .get(value.syntax()) - .copied() - .unwrap_or(ShapeCardinality::One); - - if card == ShapeCardinality::Many { - let field_name = field - .name() - .map(|t| t.text().to_string()) - .unwrap_or_else(|| "field".to_string()); - - errors - .error( - format!( - "field `{}` value must match a single node, not a sequence", - field_name - ), - value.syntax().text_range(), - ) - .emit(); - } - } - - for child in node.children() { - validate_node(&child, cardinalities, errors); - } -} diff --git a/crates/plotnik-lib/src/query/shapes.rs b/crates/plotnik-lib/src/query/shapes.rs new file mode 100644 index 00000000..8916d131 --- /dev/null +++ b/crates/plotnik-lib/src/query/shapes.rs @@ -0,0 +1,133 @@ +//! Shape cardinality analysis for query expressions. +//! +//! Determines whether an expression matches a single node position (`One`) +//! or multiple sequential positions (`Many`). Used to validate field constraints: +//! `field: expr` requires `expr` to have `ShapeCardinality::One`. +//! +//! `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 crate::parser::{Expr, FieldExpr, Ref, SeqExpr, SyntaxNode}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShapeCardinality { + One, + Many, + Invalid, +} + +impl Query<'_> { + pub(super) fn infer_shapes(&mut self) { + self.compute_all_cardinalities(self.ast.as_cst().clone()); + self.validate_shapes(self.ast.as_cst().clone()); + } + + fn compute_all_cardinalities(&mut self, node: SyntaxNode) { + if let Some(expr) = Expr::cast(node.clone()) { + self.get_or_compute(&expr); + } + + for child in node.children() { + self.compute_all_cardinalities(child); + } + } + + 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 + } + + fn compute_single(&mut self, expr: &Expr) -> ShapeCardinality { + match expr { + Expr::NamedNode(_) | Expr::AnonymousNode(_) | Expr::FieldExpr(_) | Expr::AltExpr(_) => { + ShapeCardinality::One + } + + Expr::SeqExpr(seq) => self.seq_cardinality(seq), + + Expr::CapturedExpr(cap) => { + let inner = ensure_capture_has_inner(cap.inner()); + self.get_or_compute(&inner) + } + + Expr::QuantifiedExpr(q) => { + let inner = ensure_quantifier_has_inner(q.inner()); + self.get_or_compute(&inner) + } + + Expr::Ref(r) => self.ref_cardinality(r), + } + } + + fn seq_cardinality(&mut self, seq: &SeqExpr) -> ShapeCardinality { + let children: Vec<_> = seq.children().collect(); + + match children.len() { + 0 => ShapeCardinality::One, + 1 => self.get_or_compute(&children[0]), + _ => ShapeCardinality::Many, + } + } + + 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) + } + + 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; + }; + + let Some(value) = field.value() else { + for child in node.children() { + self.validate_shapes(child); + } + return; + }; + + let card = self + .shape_cardinality_table + .get(&value) + .copied() + .unwrap_or(ShapeCardinality::One); + + if card == ShapeCardinality::Many { + let field_name = field + .name() + .map(|t| t.text().to_string()) + .unwrap_or_else(|| "field".to_string()); + + self.shapes_diagnostics + .error( + format!( + "field `{}` value must match a single node, not a sequence", + field_name + ), + value.text_range(), + ) + .emit(); + } + + for child in node.children() { + self.validate_shapes(child); + } + } +} diff --git a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs b/crates/plotnik-lib/src/query/shapes_tests.rs similarity index 66% rename from crates/plotnik-lib/src/query/shape_cardinalities_tests.rs rename to crates/plotnik-lib/src/query/shapes_tests.rs index b2817621..46f5964f 100644 --- a/crates/plotnik-lib/src/query/shape_cardinalities_tests.rs +++ b/crates/plotnik-lib/src/query/shapes_tests.rs @@ -3,30 +3,30 @@ 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¹ Def¹ - Tree¹ identifier + NamedNode¹ identifier "); } #[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¹ Def¹ Seq¹ - Tree¹ identifier + NamedNode¹ identifier "); } #[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¹ @@ -34,35 +34,35 @@ fn nested_singleton_seq_is_one() { Seq¹ Seq¹ Seq¹ - Tree¹ identifier + NamedNode¹ identifier "); } #[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¹ Def⁺ Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b "); } #[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¹ Def¹ Alt¹ Branch¹ - Tree¹ a + NamedNode¹ a Branch¹ - Tree¹ b + NamedNode¹ b "); } @@ -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¹ @@ -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 "); } @@ -92,14 +92,14 @@ 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⁺ Def¹ X - Tree¹ identifier + NamedNode¹ identifier Def¹ - Tree¹ call + NamedNode¹ call Ref¹ X "); } @@ -110,62 +110,62 @@ 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⁺ Def⁺ X Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b Def¹ - Tree¹ call + NamedNode¹ call Ref⁺ X "); } #[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¹ Def¹ - Tree¹ call - Field¹ name: - Tree¹ identifier + NamedNode¹ call + FieldExpr¹ name: + NamedNode¹ identifier "); } #[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¹ Def¹ - Tree¹ call - Field¹ name: + NamedNode¹ call + FieldExpr¹ name: Alt¹ Branch¹ - Tree¹ identifier + NamedNode¹ identifier Branch¹ - Tree¹ string + NamedNode¹ string "); } #[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¹ 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 @@ -181,17 +181,17 @@ 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⁺ 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" @@ -204,39 +204,39 @@ 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¹ Def¹ - Quantifier¹ * - Tree¹ identifier + QuantifiedExpr¹ * + NamedNode¹ identifier "); } #[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¹ Def¹ - Capture¹ @name - Tree¹ identifier + CapturedExpr¹ @name + NamedNode¹ identifier "); } #[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¹ Def⁺ - Capture⁺ @items + CapturedExpr⁺ @items Seq⁺ - Tree¹ a - Tree¹ b + NamedNode¹ a + NamedNode¹ b "); } @@ -248,25 +248,25 @@ 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⁺ 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 "); } @@ -276,69 +276,69 @@ 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¹ Def¹ Alt¹ Branch¹ Ident: - Tree¹ identifier + NamedNode¹ identifier Branch¹ Num: - Tree¹ number + NamedNode¹ number "); } #[test] -fn anchor_is_one() { - let query = Query::new("(block . (statement))").unwrap(); +fn anchor_has_no_cardinality() { + let query = Query::try_from("(block . (statement))").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ block - Anchor¹ - Tree¹ statement + NamedNode¹ block + . + NamedNode¹ statement "); } #[test] -fn negated_field_is_one() { - let query = Query::new("(function !async)").unwrap(); +fn negated_field_has_no_cardinality() { + let query = Query::try_from("(function !async)").unwrap(); assert!(query.is_valid()); insta::assert_snapshot!(query.dump_with_cardinalities(), @r" Root¹ Def¹ - Tree¹ function - NegatedField¹ !async + NamedNode¹ function + NegatedField !async "); } #[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¹ Def¹ - Tree¹ _ + NamedNode¹ (any) "); } #[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¹ Def¹ - Wildcard¹ + AnonymousNode¹ (any) "); } #[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,18 +349,18 @@ 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¹ Def¹ - Str¹ "if" + AnonymousNode¹ "if" "#); } #[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.rs b/crates/plotnik-lib/src/query/symbol_table.rs new file mode 100644 index 00000000..e1620e59 --- /dev/null +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -0,0 +1,110 @@ +//! Symbol table: name resolution and reference checking. +//! +//! Two-pass approach: +//! 1. Collect all `Name = expr` definitions +//! 2. Check that all `(UpperIdent)` references are defined + +use indexmap::IndexMap; + +use crate::parser::{Expr, Ref, ast}; + +use super::Query; + +pub type SymbolTable<'src> = IndexMap<&'src str, ast::Expr>; + +impl<'a> Query<'a> { + pub(super) fn resolve_names(&mut self) { + // 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; + } + + let Some(body) = def.body() else { + continue; + }; + self.symbol_table.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); + } + + // 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)" + ); + } + + fn collect_reference_diagnostics(&mut self, expr: &Expr) { + match expr { + Expr::Ref(r) => { + self.check_ref_diagnostic(r); + } + Expr::NamedNode(node) => { + for child in node.children() { + self.collect_reference_diagnostics(&child); + } + } + 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(_) => {} + } + } + + fn check_ref_diagnostic(&mut self, r: &Ref) { + let Some(name_token) = r.name() else { return }; + let name = name_token.text(); + + if self.symbol_table.contains_key(name) { + return; + } + + self.resolve_diagnostics + .error( + format!("undefined reference: `{}`", name), + name_token.text_range(), + ) + .emit(); + } +} diff --git a/crates/plotnik-lib/src/query/named_defs_tests.rs b/crates/plotnik-lib/src/query/symbol_table_tests.rs similarity index 85% rename from crates/plotnik-lib/src/query/named_defs_tests.rs rename to crates/plotnik-lib/src/query/symbol_table_tests.rs index cdaa5c32..e512f798 100644 --- a/crates/plotnik-lib/src/query/named_defs_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