Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
69a0527
refactor: Enhance query AST handling
zharinov Dec 5, 2025
24128f7
Add text_range method to AST nodes
zharinov Dec 5, 2025
8ea7a87
Remove Anchor from Expr and handle Anchor separately in printer
zharinov Dec 5, 2025
54fc341
Refactor NegatedField to be a predicate, not an expression
zharinov Dec 5, 2025
be18a02
Anchor printing
zharinov Dec 5, 2025
a584aa5
Restructure AST
zharinov Dec 5, 2025
4dbaacd
Update AGENTS.md
zharinov Dec 5, 2025
8febe01
Rename Alt and Seq AST types to AltExpr and SeqExpr
zharinov Dec 5, 2025
9a022c0
Refactor SymbolTable to be more lightweight and efficient
zharinov Dec 5, 2025
4e917b2
Refactor collect_refs to use SyntaxNode traversal
zharinov Dec 5, 2025
0a6d68a
Rename `named_defs` module to `symbol_table`
zharinov Dec 5, 2025
dceded4
Refactor shape cardinality to work with AST nodes
zharinov Dec 5, 2025
13f41b2
Refactor validation steps in Query to use method calls
zharinov Dec 5, 2025
8adc9aa
Refactor Query parsing into multiple steps
zharinov Dec 5, 2025
8fb17b6
Update diagnostics methods for better usability
zharinov Dec 5, 2025
732cac3
Refactor module names for clarity and consistency
zharinov Dec 5, 2025
53816fd
Refactor recursion detection using Tarjan's SCC algorithm
zharinov Dec 5, 2025
0369dc0
Add fuel state tracking to parser and query
zharinov Dec 5, 2025
8b2c49a
Refactor parser to use ParseResult and simplify parsing
zharinov Dec 5, 2025
b203b1e
Simplify docstrings and remove verbose comments
zharinov Dec 5, 2025
b09fd49
Refactor `Query` construction and analysis pipeline
zharinov Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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".
Expand All @@ -66,14 +66,14 @@ Run: `cargo run -p plotnik-cli -- <command>`

Inputs: `-q/--query <Q>`, `--query-file <F>`, `--source <S>`, `-s/--source-file <F>`, `-l/--lang <L>`

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
Expand All @@ -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

Expand Down
6 changes: 4 additions & 2 deletions crates/plotnik-cli/src/commands/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand Down Expand Up @@ -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);
}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/plotnik-lib/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
11 changes: 5 additions & 6 deletions crates/plotnik-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand All @@ -28,7 +27,7 @@ pub mod query;
pub type PassResult<T> = 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)]
Expand Down
171 changes: 95 additions & 76 deletions crates/plotnik-lib/src/parser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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<Self> {
$(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<Self> {
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<SyntaxToken> {
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 {
Expand All @@ -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<Self> {
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<Item = Def> + '_ {
Expand All @@ -125,7 +146,7 @@ impl Def {
}
}

impl Tree {
impl NamedNode {
pub fn node_type(&self) -> Option<SyntaxToken> {
self.0
.children_with_tokens()
Expand All @@ -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<Item = Expr> + '_ {
self.0.children().filter_map(Expr::cast)
}
Expand All @@ -155,7 +183,7 @@ impl Ref {
}
}

impl Alt {
impl AltExpr {
pub fn kind(&self) -> AltKind {
let mut tagged = false;
let mut untagged = false;
Expand Down Expand Up @@ -202,13 +230,13 @@ impl Branch {
}
}

impl Seq {
impl SeqExpr {
pub fn children(&self) -> impl Iterator<Item = Expr> + '_ {
self.0.children().filter_map(Expr::cast)
}
}

impl Capture {
impl CapturedExpr {
pub fn name(&self) -> Option<SyntaxToken> {
self.0
.children_with_tokens()
Expand All @@ -234,7 +262,7 @@ impl Type {
}
}

impl Quantifier {
impl QuantifiedExpr {
pub fn inner(&self) -> Option<Expr> {
self.0.children().find_map(Expr::cast)
}
Expand All @@ -257,7 +285,7 @@ impl Quantifier {
}
}

impl Field {
impl FieldExpr {
pub fn name(&self) -> Option<SyntaxToken> {
self.0
.children_with_tokens()
Expand All @@ -278,12 +306,3 @@ impl NegatedField {
.find(|t| t.kind() == SyntaxKind::Id)
}
}

impl Str {
pub fn value(&self) -> Option<SyntaxToken> {
self.0
.children_with_tokens()
.filter_map(|it| it.into_token())
.find(|t| t.kind() == SyntaxKind::StrVal)
}
}
Loading