From 38ce132051b12a854f513c9b73012821560c92a4 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Fri, 9 Jan 2026 15:42:07 -0300 Subject: [PATCH] feat: Add `lang list` and `lang dump` commands --- AGENTS.md | 27 +- Cargo.lock | 1 + crates/plotnik-cli/Cargo.toml | 5 +- crates/plotnik-cli/src/cli/commands.rs | 28 +- crates/plotnik-cli/src/cli/dispatch.rs | 16 +- crates/plotnik-cli/src/cli/mod.rs | 3 +- crates/plotnik-cli/src/commands/lang.rs | 269 ++++++++++++++++++ crates/plotnik-cli/src/commands/lang_tests.rs | 240 ++++++++++++++++ crates/plotnik-cli/src/commands/langs.rs | 7 - crates/plotnik-cli/src/commands/mod.rs | 4 +- ...mmands__lang_tests__grammar_dump_json.snap | 156 ++++++++++ crates/plotnik-cli/src/main.rs | 19 +- crates/plotnik-langs/src/builtin.rs | 25 ++ docs/binary-format/07-dump-format.md | 4 +- docs/binary-format/08-trace-format.md | 16 +- docs/cli.md | 67 +++-- 16 files changed, 824 insertions(+), 63 deletions(-) create mode 100644 crates/plotnik-cli/src/commands/lang.rs create mode 100644 crates/plotnik-cli/src/commands/lang_tests.rs delete mode 100644 crates/plotnik-cli/src/commands/langs.rs create mode 100644 crates/plotnik-cli/src/commands/snapshots/plotnik__commands__lang_tests__grammar_dump_json.snap diff --git a/AGENTS.md b/AGENTS.md index b03e5d0c..c10c0cc1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,15 +172,16 @@ docs/ Run: `cargo run -p plotnik -- ` -| Command | Purpose | -| ------- | ------------------------------- | -| `ast` | Show AST of query and/or source | -| `check` | Validate query | -| `dump` | Show compiled bytecode | -| `infer` | Generate TypeScript types | -| `exec` | Execute query, output JSON | -| `trace` | Trace execution for debugging | -| `langs` | List supported languages | +| Command | Purpose | +| ----------- | ------------------------------- | +| `ast` | Show AST of query and/or source | +| `check` | Validate query | +| `dump` | Show compiled bytecode | +| `infer` | Generate TypeScript types | +| `exec` | Execute query, output JSON | +| `trace` | Trace execution for debugging | +| `lang list` | List supported languages | +| `lang dump` | Dump grammar for a language | ## ast @@ -265,12 +266,14 @@ cargo run -p plotnik -- trace query.ptk app.ts --no-result -vv Options: `-v` (verbose), `-vv` (very verbose), `--no-result`, `--fuel ` -## langs +## lang -List supported tree-sitter languages. +Language information and grammar tools. ```sh -cargo run -p plotnik -- langs +cargo run -p plotnik -- lang list # List languages with aliases +cargo run -p plotnik -- lang dump json # Dump JSON grammar +cargo run -p plotnik -- lang dump typescript # Dump TypeScript grammar ``` # Coding Rules diff --git a/Cargo.lock b/Cargo.lock index a7e1c7ed..15ba56ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,6 +1614,7 @@ version = "0.2.0" dependencies = [ "arborium-tree-sitter", "clap", + "insta", "plotnik-core", "plotnik-langs", "plotnik-lib", diff --git a/crates/plotnik-cli/Cargo.toml b/crates/plotnik-cli/Cargo.toml index ec61bc98..146dffc3 100644 --- a/crates/plotnik-cli/Cargo.toml +++ b/crates/plotnik-cli/Cargo.toml @@ -240,4 +240,7 @@ plotnik-lib = { version = "0.2", path = "../plotnik-lib" } arborium-tree-sitter = "2.5.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -thiserror = "2.0" \ No newline at end of file +thiserror = "2.0" + +[dev-dependencies] +insta = "=1.46.0" \ No newline at end of file diff --git a/crates/plotnik-cli/src/cli/commands.rs b/crates/plotnik-cli/src/cli/commands.rs index 0e1d504a..bcede551 100644 --- a/crates/plotnik-cli/src/cli/commands.rs +++ b/crates/plotnik-cli/src/cli/commands.rs @@ -53,7 +53,7 @@ pub fn build_cli() -> Command { .subcommand(infer_command()) .subcommand(exec_command()) .subcommand(trace_command()) - .subcommand(langs_command()) + .subcommand(lang_command()) } /// Show AST of query and/or source file. @@ -260,7 +260,29 @@ pub fn trace_command() -> Command { ) } +/// Language information commands. +pub fn lang_command() -> Command { + Command::new("lang") + .about("Language information and grammar dump") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand(lang_list_command()) + .subcommand(lang_dump_command()) +} + /// List supported languages. -pub fn langs_command() -> Command { - Command::new("langs").about("List supported languages") +fn lang_list_command() -> Command { + Command::new("list").about("List supported languages with aliases") +} + +/// Dump grammar for a language. +fn lang_dump_command() -> Command { + Command::new("dump") + .about("Dump grammar in Plotnik-like syntax") + .arg( + clap::Arg::new("lang") + .help("Language name or alias") + .required(true) + .index(1), + ) } diff --git a/crates/plotnik-cli/src/cli/dispatch.rs b/crates/plotnik-cli/src/cli/dispatch.rs index 5b6e7b44..42e8cced 100644 --- a/crates/plotnik-cli/src/cli/dispatch.rs +++ b/crates/plotnik-cli/src/cli/dispatch.rs @@ -310,14 +310,26 @@ impl From for TraceArgs { } } -pub struct LangsParams; +pub struct LangListParams; -impl LangsParams { +impl LangListParams { pub fn from_matches(_m: &ArgMatches) -> Self { Self } } +pub struct LangDumpParams { + pub lang: String, +} + +impl LangDumpParams { + pub fn from_matches(m: &ArgMatches) -> Self { + Self { + lang: m.get_one::("lang").cloned().unwrap(), + } + } +} + /// Parse --color flag into ColorChoice. fn parse_color(m: &ArgMatches) -> ColorChoice { match m.get_one::("color").map(|s| s.as_str()) { diff --git a/crates/plotnik-cli/src/cli/mod.rs b/crates/plotnik-cli/src/cli/mod.rs index efe475fa..7e8ef287 100644 --- a/crates/plotnik-cli/src/cli/mod.rs +++ b/crates/plotnik-cli/src/cli/mod.rs @@ -7,7 +7,8 @@ mod dispatch_tests; pub use commands::build_cli; pub use dispatch::{ - AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, + AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangDumpParams, LangListParams, + TraceParams, }; /// Color output mode for CLI commands. diff --git a/crates/plotnik-cli/src/commands/lang.rs b/crates/plotnik-cli/src/commands/lang.rs new file mode 100644 index 00000000..c31748d5 --- /dev/null +++ b/crates/plotnik-cli/src/commands/lang.rs @@ -0,0 +1,269 @@ +use std::collections::HashSet; +use std::process::exit; + +use plotnik_core::grammar::{Grammar, Rule}; + +/// List all supported languages with aliases. +pub fn run_list() { + let infos = plotnik_langs::all_info(); + for info in infos { + let aliases: Vec<_> = info.aliases.iter().skip(1).copied().collect(); + if aliases.is_empty() { + println!("{}", info.name); + } else { + println!("{} ({})", info.name, aliases.join(", ")); + } + } +} + +/// Dump grammar for a language. +pub fn run_dump(lang_name: &str) { + let Some(lang) = plotnik_langs::from_name(lang_name) else { + eprintln!("error: unknown language '{lang_name}'"); + eprintln!(); + eprintln!("Run 'plotnik lang list' to see available languages."); + exit(1); + }; + + let grammar = lang.grammar(); + let renderer = GrammarRenderer::new(grammar); + print!("{}", renderer.render()); +} + +pub struct GrammarRenderer<'a> { + grammar: &'a Grammar, + hidden_rules: HashSet<&'a str>, +} + +impl<'a> GrammarRenderer<'a> { + pub fn new(grammar: &'a Grammar) -> Self { + let hidden_rules: HashSet<_> = grammar + .rules + .iter() + .filter(|(name, _)| name.starts_with('_')) + .map(|(name, _)| name.as_str()) + .collect(); + + Self { + grammar, + hidden_rules, + } + } + + pub fn render(&self) -> String { + let mut out = String::new(); + + self.render_header(&mut out); + self.render_extras(&mut out); + self.render_externals(&mut out); + self.render_supertypes(&mut out); + self.render_rules(&mut out); + + out + } + + fn render_header(&self, out: &mut String) { + out.push_str( + r#"/* + * Grammar Dump + * + * Syntax: + * (node_kind) named node (queryable) + * "literal" anonymous node (queryable) + * (_hidden ...) hidden rule (not queryable, children inline) + * {...} sequence (ordered children) + * [...] alternation (first match) + * ? * + quantifiers (0-1, 0+, 1+) + * "x"! immediate token (no preceding whitespace) + * field: ... named field + * T :: supertype supertype declaration + */ + +"#, + ); + } + + fn render_extras(&self, out: &mut String) { + self.render_rule_list("extras", &self.grammar.extras, out); + } + + fn render_externals(&self, out: &mut String) { + self.render_rule_list("externals", &self.grammar.externals, out); + } + + fn render_rule_list(&self, label: &str, rules: &[Rule], out: &mut String) { + if rules.is_empty() { + return; + } + + out.push_str(label); + out.push_str(" = [\n"); + for rule in rules { + out.push_str(" "); + self.render_rule(rule, out, 1); + out.push('\n'); + } + out.push_str("]\n\n"); + } + + fn render_supertypes(&self, out: &mut String) { + for supertype in &self.grammar.supertypes { + if let Some((_, rule)) = self.grammar.rules.iter().find(|(n, _)| n == supertype) { + out.push_str(supertype); + out.push_str(" :: supertype = "); + self.render_rule(rule, out, 0); + out.push_str("\n\n"); + } + } + } + + fn render_rules(&self, out: &mut String) { + let supertypes_set: HashSet<_> = self.grammar.supertypes.iter().collect(); + + for (name, rule) in &self.grammar.rules { + if supertypes_set.contains(name) { + continue; + } + + out.push_str(name); + out.push_str(" = "); + self.render_rule(rule, out, 0); + out.push_str("\n\n"); + } + } + + fn render_rule(&self, rule: &Rule, out: &mut String, indent: usize) { + match rule { + Rule::Blank => out.push_str("()"), + + Rule::String(s) => { + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(c), + } + } + out.push('"'); + } + + Rule::Pattern { value, flags } => { + out.push('/'); + out.push_str(value); + out.push('/'); + if let Some(f) = flags { + out.push_str(f); + } + } + + Rule::Symbol(name) => { + out.push('('); + out.push_str(name); + if self.hidden_rules.contains(name.as_str()) { + out.push_str(" ...)"); + } else { + out.push(')'); + } + } + + Rule::Seq(children) => { + self.render_block(children, '{', '}', indent, out); + } + + Rule::Choice(children) => { + if let Some(simplified) = self.simplify_optional(children) { + self.render_rule(&simplified, out, indent); + out.push('?'); + } else { + self.render_block(children, '[', ']', indent, out); + } + } + + Rule::Repeat(inner) => { + self.render_rule(inner, out, indent); + out.push('*'); + } + + Rule::Repeat1(inner) => { + self.render_rule(inner, out, indent); + out.push('+'); + } + + Rule::Field { name, content } => { + out.push_str(name); + out.push_str(": "); + self.render_rule(content, out, indent); + } + + Rule::Alias { + content: _, + value, + named, + } => { + let (open, close) = if *named { ('(', ')') } else { ('"', '"') }; + out.push(open); + out.push_str(value); + out.push(close); + } + + Rule::Token(inner) => { + self.render_rule(inner, out, indent); + } + + Rule::ImmediateToken(inner) => { + self.render_rule(inner, out, indent); + out.push('!'); + } + + Rule::Prec { content, .. } + | Rule::PrecLeft { content, .. } + | Rule::PrecRight { content, .. } + | Rule::PrecDynamic { content, .. } => { + self.render_rule(content, out, indent); + } + + Rule::Reserved { content, .. } => { + self.render_rule(content, out, indent); + } + } + } + + fn render_block( + &self, + children: &[Rule], + open: char, + close: char, + indent: usize, + out: &mut String, + ) { + out.push(open); + out.push('\n'); + + let child_indent = indent + 1; + let prefix = " ".repeat(child_indent); + + for child in children { + out.push_str(&prefix); + self.render_rule(child, out, child_indent); + out.push('\n'); + } + + out.push_str(&" ".repeat(indent)); + out.push(close); + } + + fn simplify_optional(&self, children: &[Rule]) -> Option { + if children.len() != 2 { + return None; + } + + match (&children[0], &children[1]) { + (Rule::Blank, other) | (other, Rule::Blank) => Some(other.clone()), + _ => None, + } + } +} diff --git a/crates/plotnik-cli/src/commands/lang_tests.rs b/crates/plotnik-cli/src/commands/lang_tests.rs new file mode 100644 index 00000000..2e50a01f --- /dev/null +++ b/crates/plotnik-cli/src/commands/lang_tests.rs @@ -0,0 +1,240 @@ +use plotnik_langs::Lang; + +use super::lang::GrammarRenderer; + +fn smoke_test(lang: Lang, source: &str, expected_root: &str) { + let tree = lang.parse(source); + let root = tree.root_node(); + assert_eq!(root.kind(), expected_root); + assert!(!root.has_error()); +} + +#[test] +#[cfg(feature = "lang-bash")] +fn smoke_parse_bash() { + smoke_test(plotnik_langs::bash(), "echo hello", "program"); +} + +#[test] +#[cfg(feature = "lang-c")] +fn smoke_parse_c() { + smoke_test( + plotnik_langs::c(), + "int main() { return 0; }", + "translation_unit", + ); +} + +#[test] +#[cfg(feature = "lang-cpp")] +fn smoke_parse_cpp() { + smoke_test( + plotnik_langs::cpp(), + "int main() { return 0; }", + "translation_unit", + ); +} + +#[test] +#[cfg(feature = "lang-c-sharp")] +fn smoke_parse_csharp() { + smoke_test(plotnik_langs::csharp(), "class Foo { }", "compilation_unit"); +} + +#[test] +#[cfg(feature = "lang-css")] +fn smoke_parse_css() { + smoke_test(plotnik_langs::css(), "body { color: red; }", "stylesheet"); +} + +#[test] +#[cfg(feature = "lang-elixir")] +fn smoke_parse_elixir() { + smoke_test(plotnik_langs::elixir(), "defmodule Foo do end", "source"); +} + +#[test] +#[cfg(feature = "lang-go")] +fn smoke_parse_go() { + smoke_test(plotnik_langs::go(), "package main", "source_file"); +} + +#[test] +#[cfg(feature = "lang-haskell")] +fn smoke_parse_haskell() { + smoke_test( + plotnik_langs::haskell(), + "main = putStrLn \"hello\"", + "haskell", + ); +} + +#[test] +#[cfg(feature = "lang-hcl")] +fn smoke_parse_hcl() { + smoke_test( + plotnik_langs::hcl(), + "resource \"aws_instance\" \"x\" {}", + "config_file", + ); +} + +#[test] +#[cfg(feature = "lang-html")] +fn smoke_parse_html() { + smoke_test(plotnik_langs::html(), "", "document"); +} + +#[test] +#[cfg(feature = "lang-java")] +fn smoke_parse_java() { + smoke_test(plotnik_langs::java(), "class Foo {}", "program"); +} + +#[test] +#[cfg(feature = "lang-javascript")] +fn smoke_parse_javascript() { + smoke_test( + plotnik_langs::javascript(), + "function hello() { return 42; }", + "program", + ); +} + +#[test] +#[cfg(feature = "lang-json")] +fn smoke_parse_json() { + smoke_test(plotnik_langs::json(), r#"{"key": "value"}"#, "document"); +} + +#[test] +#[cfg(feature = "lang-kotlin")] +fn smoke_parse_kotlin() { + smoke_test(plotnik_langs::kotlin(), "fun main() {}", "source_file"); +} + +#[test] +#[cfg(feature = "lang-lua")] +fn smoke_parse_lua() { + smoke_test(plotnik_langs::lua(), "print('hello')", "chunk"); +} + +#[test] +#[cfg(feature = "lang-nix")] +fn smoke_parse_nix() { + smoke_test(plotnik_langs::nix(), "{ x = 1; }", "source_code"); +} + +#[test] +#[cfg(feature = "lang-php")] +fn smoke_parse_php() { + smoke_test(plotnik_langs::php(), ";", "program"); +} + +#[test] +#[cfg(feature = "lang-yaml")] +fn smoke_parse_yaml() { + smoke_test(plotnik_langs::yaml(), "key: value", "stream"); +} + +#[test] +#[cfg(feature = "lang-json")] +fn grammar_dump_json() { + let lang = plotnik_langs::json(); + let grammar = lang.grammar(); + let renderer = GrammarRenderer::new(grammar); + let output = renderer.render(); + + insta::assert_snapshot!(output); +} + +#[test] +fn lang_info_has_aliases() { + let infos = plotnik_langs::all_info(); + assert!(!infos.is_empty()); + + for info in &infos { + assert!(!info.name.is_empty(), "name should not be empty"); + assert!( + !info.aliases.is_empty(), + "aliases should not be empty for {}", + info.name + ); + } +} + +#[test] +fn lang_from_name_canonical() { + let infos = plotnik_langs::all_info(); + + for info in &infos { + let lang = plotnik_langs::from_name(info.name); + assert!( + lang.is_some(), + "canonical name '{}' should resolve", + info.name + ); + } +} + +#[test] +fn lang_from_name_aliases() { + let infos = plotnik_langs::all_info(); + + for info in &infos { + for alias in info.aliases { + let lang = plotnik_langs::from_name(alias); + assert!(lang.is_some(), "alias '{}' should resolve", alias); + } + } +} diff --git a/crates/plotnik-cli/src/commands/langs.rs b/crates/plotnik-cli/src/commands/langs.rs deleted file mode 100644 index 608f2377..00000000 --- a/crates/plotnik-cli/src/commands/langs.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub fn run() { - let langs = plotnik_langs::all(); - println!("Supported languages ({}):", langs.len()); - for lang in langs { - println!(" {}", lang.name()); - } -} diff --git a/crates/plotnik-cli/src/commands/mod.rs b/crates/plotnik-cli/src/commands/mod.rs index f0644b3d..31fae701 100644 --- a/crates/plotnik-cli/src/commands/mod.rs +++ b/crates/plotnik-cli/src/commands/mod.rs @@ -3,11 +3,11 @@ pub mod check; pub mod dump; pub mod exec; pub mod infer; +pub mod lang; pub mod lang_resolver; -pub mod langs; pub mod query_loader; pub mod run_common; pub mod trace; #[cfg(test)] -mod langs_tests; +mod lang_tests; diff --git a/crates/plotnik-cli/src/commands/snapshots/plotnik__commands__lang_tests__grammar_dump_json.snap b/crates/plotnik-cli/src/commands/snapshots/plotnik__commands__lang_tests__grammar_dump_json.snap new file mode 100644 index 00000000..0658b324 --- /dev/null +++ b/crates/plotnik-cli/src/commands/snapshots/plotnik__commands__lang_tests__grammar_dump_json.snap @@ -0,0 +1,156 @@ +--- +source: crates/plotnik-cli/src/commands/lang_tests.rs +expression: output +--- +/* + * Grammar Dump + * + * Syntax: + * (node_kind) named node (queryable) + * "literal" anonymous node (queryable) + * (_hidden ...) hidden rule (not queryable, children inline) + * {...} sequence (ordered children) + * [...] alternation (first match) + * ? * + quantifiers (0-1, 0+, 1+) + * "x"! immediate token (no preceding whitespace) + * field: ... named field + * T :: supertype supertype declaration + */ + +extras = [ + /\s/ + (comment) +] + +_value :: supertype = [ + (object) + (array) + (number) + (string) + (true) + (false) + (null) +] + +document = (_value ...)* + +object = { + "{" + { + (pair) + { + "," + (pair) + }* + }? + "}" +} + +pair = { + key: (string) + ":" + value: (_value ...) +} + +array = { + "[" + { + (_value ...) + { + "," + (_value ...) + }* + }? + "]" +} + +string = [ + { + "\"" + "\"" + } + { + "\"" + (_string_content ...) + "\"" + } +] + +_string_content = [ + (string_content) + (escape_sequence) +]+ + +string_content = /[^\\"\n]+/! + +escape_sequence = { + "\\" + /(\"|\\|\/|b|f|n|r|t|u)/ +}! + +number = [ + { + { + "-"? + [ + "0" + { + /[1-9]/ + /\d+/? + } + ] + } + "." + /\d+/? + { + [ + "e" + "E" + ] + { + "-"? + /\d+/ + } + }? + } + { + { + "-"? + [ + "0" + { + /[1-9]/ + /\d+/? + } + ] + } + { + [ + "e" + "E" + ] + { + "-"? + /\d+/ + } + }? + } +] + +true = "true" + +false = "false" + +null = "null" + +comment = [ + { + "//" + /.*/ + } + { + "/*" + /[^*]*\*+([^/*][^*]*\*+)*/ + "/" + } +] diff --git a/crates/plotnik-cli/src/main.rs b/crates/plotnik-cli/src/main.rs index 54c8b7e1..c2c8f98d 100644 --- a/crates/plotnik-cli/src/main.rs +++ b/crates/plotnik-cli/src/main.rs @@ -2,8 +2,8 @@ mod cli; mod commands; use cli::{ - AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, - build_cli, + AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangDumpParams, LangListParams, + TraceParams, build_cli, }; fn main() { @@ -34,10 +34,17 @@ fn main() { let params = TraceParams::from_matches(m); commands::trace::run(params.into()); } - Some(("langs", m)) => { - let _params = LangsParams::from_matches(m); - commands::langs::run(); - } + Some(("lang", m)) => match m.subcommand() { + Some(("list", sub_m)) => { + let _params = LangListParams::from_matches(sub_m); + commands::lang::run_list(); + } + Some(("dump", sub_m)) => { + let params = LangDumpParams::from_matches(sub_m); + commands::lang::run_dump(¶ms.lang); + } + _ => unreachable!("clap should have caught this"), + }, _ => unreachable!("clap should have caught this"), } } diff --git a/crates/plotnik-langs/src/builtin.rs b/crates/plotnik-langs/src/builtin.rs index 23721bf6..fb23f6e2 100644 --- a/crates/plotnik-langs/src/builtin.rs +++ b/crates/plotnik-langs/src/builtin.rs @@ -2,6 +2,17 @@ use std::sync::{Arc, LazyLock}; use crate::{Lang, LangInner}; +/// Language metadata for listing. +#[derive(Debug, Clone)] +pub struct LangInfo { + /// Canonical name (first in names list). + pub name: &'static str, + /// All name aliases (includes canonical name). + pub aliases: &'static [&'static str], + /// File extensions. + pub extensions: &'static [&'static str], +} + macro_rules! define_langs { ( $( @@ -76,6 +87,20 @@ macro_rules! define_langs { )* ] } + + /// Get metadata for all available languages. + pub fn all_info() -> Vec { + vec![ + $( + #[cfg(feature = $feature)] + LangInfo { + name: $name, + aliases: &[$($alias),*], + extensions: &[$($ext),*], + }, + )* + ] + } }; } diff --git a/docs/binary-format/07-dump-format.md b/docs/binary-format/07-dump-format.md index 6bf20c6f..fc749f9b 100644 --- a/docs/binary-format/07-dump-format.md +++ b/docs/binary-format/07-dump-format.md @@ -69,8 +69,8 @@ Value: 08 ε 11, 16 10 ▶ 11 !!▽ [Enum(M2)] (number) [Node Set(M0) EndEnum] 19 - 14 ... - 15 ... + 14 ... + 15 ... 16 !!▽ [Enum(M3)] (string) [Node Set(M1) EndEnum] 19 19 △ _ 10 ``` diff --git a/docs/binary-format/08-trace-format.md b/docs/binary-format/08-trace-format.md index a698e23b..985d811e 100644 --- a/docs/binary-format/08-trace-format.md +++ b/docs/binary-format/08-trace-format.md @@ -146,8 +146,8 @@ Value: 08 ε 11, 16 10 ▶ 11 !!▽ [Enum(M2)] (number) [Node Set(M0) EndEnum] 19 - 14 ... - 15 ... + 14 ... + 15 ... 16 !!▽ [Enum(M3)] (string) [Node Set(M1) EndEnum] 19 19 △ _ 10 ``` @@ -191,7 +191,7 @@ Value: △ document ● document 42 -------------------------------------------- - 10 ◀ (Value) + 10 ◀ (Value) _ObjWrap: -------------------------------------------- @@ -233,7 +233,7 @@ Value: ⬥ Enum "Num" ▽ string ○ string "hello" - 08 ❮❮❮ + 08 ❮❮❮ -------------------------------------------- 16 [Enum(M3)] (string) [Node Set(M1) EndEnum] 19 ⬥ Enum "Str" @@ -246,7 +246,7 @@ Value: △ document ● document "hello" -------------------------------------------- - 10 ◀ (Value) + 10 ◀ (Value) _ObjWrap: -------------------------------------------- @@ -298,7 +298,7 @@ Value: ⬥ Enum "Num" ▽ true ○ true true - 08 ❮❮❮ + 08 ❮❮❮ -------------------------------------------- 16 [Enum(M3)] (string) [Node Set(M1) EndEnum] 19 ⬥ Enum "Str" @@ -327,12 +327,12 @@ Value: 08 ε 11, 16 11 [Enum(M2)] (number) [Node Set(M0) EndEnum] 19 ○ string - 08 ❮❮❮ + 08 ❮❮❮ 16 [Enum(M3)] (string) [Node Set(M1) EndEnum] 19 ● string 19 _ 10 ● document - 10 ◀ (Value) + 10 ◀ (Value) _ObjWrap: 03 ε [EndObj] 05 diff --git a/docs/cli.md b/docs/cli.md index fab2b251..786f4e58 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -15,22 +15,26 @@ plotnik check -q 'Func = (function_declaration) @fn' -l typescript plotnik infer -q 'Func = (function_declaration) @fn' -l typescript # List supported languages -plotnik langs +plotnik lang list + +# Dump grammar for a language +plotnik lang dump typescript ``` --- ## Commands -| Command | Input | Purpose | `-l` flag | -| ------- | ----- | ------------------------------- | ------------------------------------- | -| `ast` | both | Show AST of query and/or source | Inferred from extension | -| `dump` | query | Show bytecode | Optional (enables linking) | -| `check` | query | Validate query | Optional (enables grammar validation) | -| `infer` | query | Generate type definitions | Required | -| `exec` | both | Run query, output JSON | Inferred from extension | -| `trace` | both | Trace query execution | Inferred from extension | -| `langs` | — | List supported languages | — | +| Command | Input | Purpose | `-l` flag | +| ----------- | ----- | ------------------------------- | ------------------------------------- | +| `ast` | both | Show AST of query and/or source | Inferred from extension | +| `dump` | query | Show bytecode | Optional (enables linking) | +| `check` | query | Validate query | Optional (enables grammar validation) | +| `infer` | query | Generate type definitions | Required | +| `exec` | both | Run query, output JSON | Inferred from extension | +| `trace` | both | Trace query execution | Inferred from extension | +| `lang list` | — | List supported languages | — | +| `lang dump` | — | Dump grammar for a language | — | --- @@ -134,23 +138,48 @@ plotnik infer -q 'Q = (identifier) @id' -l js --no-node-type --no-export | `--no-export` | Don't add `export` keyword | | `--void-type TYPE` | Type for void results (`undefined` or `null`) | -### langs +### lang + +Language information and grammar tools. + +#### lang list -List all supported tree-sitter languages. +List all supported tree-sitter languages with their aliases. ```sh -plotnik langs +plotnik lang list ``` ``` -Supported languages (107): - bash - c - cpp - css - ... +bash (sh, shell) +c +cpp (c++, cxx, cc) +javascript (js, jsx, ecmascript, es) +typescript (ts) +... ``` +#### lang dump + +Dump a language's grammar in Plotnik-like syntax. Useful for learning how to write queries against a grammar. + +```sh +plotnik lang dump json +plotnik lang dump typescript +``` + +The output uses a syntax similar to Plotnik queries: + +- `(node_kind)` — named node (queryable) +- `"literal"` — anonymous node (queryable) +- `(_hidden ...)` — hidden rule (not queryable, children inline) +- `{...}` — sequence (ordered children) +- `[...]` — alternation (first match) +- `? * +` — quantifiers +- `"x"!` — immediate token (no whitespace before) +- `field: ...` — named field +- `T :: supertype` — supertype declaration + ### exec Execute a query against source code and output JSON matches.