Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,12 @@ Inspect query AST/CST or parse source files with tree-sitter.
cargo run -p plotnik-cli -- debug -q 'Test = (identifier) @id'
cargo run -p plotnik-cli -- debug -q 'Test = (identifier) @id' --only-symbols
cargo run -p plotnik-cli -- debug -q 'Test = (identifier) @id' --types
cargo run -p plotnik-cli -- debug -q 'Test = (identifier) @id' --bytecode
cargo run -p plotnik-cli -- debug -s app.ts
cargo run -p plotnik-cli -- debug -s app.ts --raw
```

Options: `--only-symbols`, `--cst`, `--raw`, `--spans`, `--arities`, `--types`
Options: `--only-symbols`, `--cst`, `--raw`, `--spans`, `--arities`, `--types`, `--bytecode`

## types

Expand Down
27 changes: 16 additions & 11 deletions crates/plotnik-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ pub struct Cli {
pub enum Command {
/// Debug and inspect queries and source files
#[command(after_help = r#"EXAMPLES:
plotnik debug -q '(identifier) @id'
plotnik debug -q '(identifier) @id' --only-symbols
plotnik debug -q 'Q = (identifier) @id'
plotnik debug -q 'Q = (identifier) @id' --only-symbols
plotnik debug -q 'Q = (identifier) @id' --bytecode
plotnik debug -s app.ts
plotnik debug -s app.ts --raw
plotnik debug -q '(function_declaration) @fn' -s app.ts -l typescript"#)]
plotnik debug -q 'Q = (function_declaration) @fn' -s app.ts -l typescript"#)]
Debug {
#[command(flatten)]
query: QueryArgs,
Expand All @@ -57,10 +58,10 @@ pub enum Command {

/// Execute a query against source code and output JSON
#[command(after_help = r#"EXAMPLES:
plotnik exec -q '(identifier) @id' -s app.js
plotnik exec -q '(identifier) @id' -s app.js --pretty
plotnik exec -q '(function_declaration) @fn' -s app.ts -l typescript --verbose-nodes
plotnik exec -q '(identifier) @id' -s app.js --check
plotnik exec -q 'Q = (identifier) @id' -s app.js
plotnik exec -q 'Q = (identifier) @id' -s app.js --pretty
plotnik exec -q 'Q = (function_declaration) @fn' -s app.ts -l typescript --verbose-nodes
plotnik exec -q 'Q = (identifier) @id' -s app.js --check
plotnik exec --query-file query.ptk -s app.js --entry FunctionDef"#)]
Exec {
#[command(flatten)]
Expand All @@ -79,11 +80,11 @@ pub enum Command {

/// Generate type definitions from a query
#[command(after_help = r#"EXAMPLES:
plotnik types -q '(identifier) @id' -l javascript
plotnik types -q 'Q = (identifier) @id' -l javascript
plotnik types --query-file query.ptk -l typescript
plotnik types -q '(function_declaration) @fn' -l js --format ts
plotnik types -q '(identifier) @id' -l js --verbose-nodes
plotnik types -q '(identifier) @id' -l js -o types.d.ts
plotnik types -q 'Q = (function_declaration) @fn' -l js --format ts
plotnik types -q 'Q = (identifier) @id' -l js --verbose-nodes
plotnik types -q 'Q = (identifier) @id' -l js -o types.d.ts

NOTE: Use --verbose-nodes to match `exec --verbose-nodes` output shape."#)]
Types {
Expand Down Expand Up @@ -202,4 +203,8 @@ pub struct OutputArgs {
/// Show inferred types
#[arg(long)]
pub types: bool,

/// Show bytecode dump
#[arg(long)]
pub bytecode: bool,
}
37 changes: 28 additions & 9 deletions crates/plotnik-cli/src/commands/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct DebugArgs {
pub graph: bool,
pub graph_raw: bool,
pub types: bool,
pub bytecode: bool,
pub color: bool,
}

Expand All @@ -47,7 +48,7 @@ pub fn run(args: DebugArgs) {
})
});

let show_query = has_query_input && !args.symbols && !args.graph && !args.types;
let show_query = has_query_input && !args.symbols && !args.graph && !args.types && !args.bytecode;
let show_source = has_source_input;

if show_query && let Some(ref q) = query {
Expand Down Expand Up @@ -82,13 +83,25 @@ pub fn run(args: DebugArgs) {
if args.types
&& let Some(ref q) = query
{
ensure_valid(q, args.color);
let bytecode = q.emit().expect("bytecode emission failed");
let module =
plotnik_lib::bytecode::Module::from_bytes(bytecode).expect("module loading failed");
let output = plotnik_lib::bytecode::emit::emit_typescript(&module);
print!("{}", output);
}

if args.bytecode
&& let Some(ref q) = query
{
ensure_valid(q, args.color);
let bytecode = q.emit().expect("bytecode emission failed");
let module =
plotnik_lib::bytecode::Module::from_bytes(bytecode).expect("module loading failed");
let output = plotnik_lib::bytecode::dump(&module);
print!("{}", output);
}

if show_source {
if show_query || args.symbols {
println!();
Expand All @@ -99,15 +112,21 @@ pub fn run(args: DebugArgs) {
print!("{}", dump_source(&tree, &source_code, args.raw));
}

if let Some(ref q) = query
&& !q.is_valid()
{
eprint!(
"{}",
q.diagnostics().render_colored(q.source_map(), args.color)
);
std::process::exit(1);
if let Some(ref q) = query {
ensure_valid(q, args.color);
}
}

/// Ensure query is valid, exiting with diagnostics if not.
fn ensure_valid(q: &Query, color: bool) {
if q.is_valid() {
return;
}
eprint!(
"{}",
q.diagnostics().render_colored(q.source_map(), color)
);
std::process::exit(1);
}

fn load_query(args: &DebugArgs) -> String {
Expand Down
1 change: 1 addition & 0 deletions crates/plotnik-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn main() {
graph: output.graph,
graph_raw: output.graph_raw,
types: output.types,
bytecode: output.bytecode,
color: output.color.should_colorize(),
});
}
Expand Down
131 changes: 131 additions & 0 deletions crates/plotnik-lib/src/bytecode/dump_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Tests for bytecode dump functionality.

use crate::Query;
use indoc::indoc;

#[test]
fn dump_minimal() {
let input = "Test = (identifier) @id";

let res = Query::expect_valid_linked_bytecode(input);

insta::assert_snapshot!(res, @r#"
[header]
linked = true

[strings]
S00 "Beauty will save the world"
S01 "id"
S02 "Test"
S03 "identifier"

[types.defs]
T00 = void
T01 = Node
T02 = str
T03 = Struct(M0, 1) ; { id }

[types.members]
M0 = (S01, T01) ; id: Node

[types.names]
N0 = (S02, T03) ; Test

[entry]
Test = 01 :: T03

[code]
00 𝜀 ◼

Test:
01 𝜀 02
02 *↓ (identifier) 03
03 𝜀 [Node Set(M0)] ◼
"#);
}

#[test]
fn dump_multiple_entrypoints() {
let input = indoc! {r#"
Expression = [(identifier) @name (number) @value]
Root = (function_declaration name: (identifier) @name)
"#};

let res = Query::expect_valid_linked_bytecode(input);

// Verify key sections exist
assert!(res.contains("[header]"));
assert!(res.contains("[strings]"));
assert!(res.contains("[types.defs]"));
assert!(res.contains("[types.members]"));
assert!(res.contains("[types.names]"));
assert!(res.contains("[entry]"));
assert!(res.contains("[code]"));

// Verify both entrypoints appear
assert!(res.contains("Expression"));
assert!(res.contains("Root"));

// Verify code section has entrypoint labels
assert!(res.contains("Expression:"));
assert!(res.contains("Root:"));
}

#[test]
fn dump_with_field_constraints() {
let input = indoc! {r#"
Test = (binary_expression
left: (_) @left
right: (_) @right)
"#};

let res = Query::expect_valid_linked_bytecode(input);

// Should have field references in code section
assert!(res.contains("left:"));
assert!(res.contains("right:"));
}

#[test]
fn dump_with_quantifier() {
let input = "Test = (identifier)* @items";

let res = Query::expect_valid_linked_bytecode(input);

// Should have array type
assert!(res.contains("Array") || res.contains("[]"));
}

#[test]
fn dump_with_alternation() {
let input = "Test = [(identifier) @id (string) @str]";

let res = Query::expect_valid_linked_bytecode(input);

// Should have code section with branching
assert!(res.contains("[code]"));
}

#[test]
fn dump_comprehensive() {
// A query that exercises most features:
// - Multiple definitions (entrypoints)
// - Field constraints (node_fields)
// - Multiple node types (node_types)
// - Captures with types (type_defs, type_members)
// - Alternation (branching in code)
let input = indoc! {r#"
Ident = (identifier) @name :: string
Expression = [
Literal: (number) @value
Variable: (identifier) @name
]
Assignment = (assignment_expression
left: (identifier) @target
right: (Expression) @value)
"#};

let res = Query::expect_valid_linked_bytecode(input);

insta::assert_snapshot!(res);
}
2 changes: 2 additions & 0 deletions crates/plotnik-lib/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub use module::{

pub use dump::dump;

#[cfg(test)]
mod dump_tests;
#[cfg(test)]
mod instructions_tests;
#[cfg(test)]
Expand Down
Loading