Skip to content

Commit 4d1c04d

Browse files
committed
feat: Add lang list and lang dump commands
1 parent 39531f1 commit 4d1c04d

File tree

13 files changed

+797
-41
lines changed

13 files changed

+797
-41
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/plotnik-cli/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,7 @@ plotnik-lib = { version = "0.2", path = "../plotnik-lib" }
240240
arborium-tree-sitter = "2.5.0"
241241
serde = { version = "1.0", features = ["derive"] }
242242
serde_json = "1.0"
243-
thiserror = "2.0"
243+
thiserror = "2.0"
244+
245+
[dev-dependencies]
246+
insta = "=1.46.0"

crates/plotnik-cli/src/cli/commands.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub fn build_cli() -> Command {
5353
.subcommand(infer_command())
5454
.subcommand(exec_command())
5555
.subcommand(trace_command())
56-
.subcommand(langs_command())
56+
.subcommand(lang_command())
5757
}
5858

5959
/// Show AST of query and/or source file.
@@ -260,7 +260,29 @@ pub fn trace_command() -> Command {
260260
)
261261
}
262262

263+
/// Language information commands.
264+
pub fn lang_command() -> Command {
265+
Command::new("lang")
266+
.about("Language information and grammar dump")
267+
.subcommand_required(true)
268+
.arg_required_else_help(true)
269+
.subcommand(lang_list_command())
270+
.subcommand(lang_dump_command())
271+
}
272+
263273
/// List supported languages.
264-
pub fn langs_command() -> Command {
265-
Command::new("langs").about("List supported languages")
274+
fn lang_list_command() -> Command {
275+
Command::new("list").about("List supported languages with aliases")
276+
}
277+
278+
/// Dump grammar for a language.
279+
fn lang_dump_command() -> Command {
280+
Command::new("dump")
281+
.about("Dump grammar in Plotnik-like syntax")
282+
.arg(
283+
clap::Arg::new("lang")
284+
.help("Language name or alias")
285+
.required(true)
286+
.index(1),
287+
)
266288
}

crates/plotnik-cli/src/cli/dispatch.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,26 @@ impl From<TraceParams> for TraceArgs {
310310
}
311311
}
312312

313-
pub struct LangsParams;
313+
pub struct LangListParams;
314314

315-
impl LangsParams {
315+
impl LangListParams {
316316
pub fn from_matches(_m: &ArgMatches) -> Self {
317317
Self
318318
}
319319
}
320320

321+
pub struct LangDumpParams {
322+
pub lang: String,
323+
}
324+
325+
impl LangDumpParams {
326+
pub fn from_matches(m: &ArgMatches) -> Self {
327+
Self {
328+
lang: m.get_one::<String>("lang").cloned().unwrap(),
329+
}
330+
}
331+
}
332+
321333
/// Parse --color flag into ColorChoice.
322334
fn parse_color(m: &ArgMatches) -> ColorChoice {
323335
match m.get_one::<String>("color").map(|s| s.as_str()) {

crates/plotnik-cli/src/cli/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ mod dispatch_tests;
77

88
pub use commands::build_cli;
99
pub use dispatch::{
10-
AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams,
10+
AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangDumpParams, LangListParams,
11+
TraceParams,
1112
};
1213

1314
/// Color output mode for CLI commands.
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
use std::collections::HashSet;
2+
use std::process::exit;
3+
4+
use plotnik_core::grammar::{Grammar, Rule};
5+
6+
/// List all supported languages with aliases.
7+
pub fn run_list() {
8+
let infos = plotnik_langs::all_info();
9+
for info in infos {
10+
let aliases: Vec<_> = info.aliases.iter().skip(1).copied().collect();
11+
if aliases.is_empty() {
12+
println!("{}", info.name);
13+
} else {
14+
println!("{} ({})", info.name, aliases.join(", "));
15+
}
16+
}
17+
}
18+
19+
/// Dump grammar for a language.
20+
pub fn run_dump(lang_name: &str) {
21+
let Some(lang) = plotnik_langs::from_name(lang_name) else {
22+
eprintln!("error: unknown language '{lang_name}'");
23+
eprintln!();
24+
eprintln!("Run 'plotnik lang list' to see available languages.");
25+
exit(1);
26+
};
27+
28+
let grammar = lang.grammar();
29+
let renderer = GrammarRenderer::new(grammar);
30+
print!("{}", renderer.render());
31+
}
32+
33+
pub struct GrammarRenderer<'a> {
34+
grammar: &'a Grammar,
35+
hidden_rules: HashSet<&'a str>,
36+
}
37+
38+
impl<'a> GrammarRenderer<'a> {
39+
pub fn new(grammar: &'a Grammar) -> Self {
40+
let hidden_rules: HashSet<_> = grammar
41+
.rules
42+
.iter()
43+
.filter(|(name, _)| name.starts_with('_'))
44+
.map(|(name, _)| name.as_str())
45+
.collect();
46+
47+
Self {
48+
grammar,
49+
hidden_rules,
50+
}
51+
}
52+
53+
pub fn render(&self) -> String {
54+
let mut out = String::new();
55+
56+
self.render_header(&mut out);
57+
self.render_extras(&mut out);
58+
self.render_externals(&mut out);
59+
self.render_supertypes(&mut out);
60+
self.render_rules(&mut out);
61+
62+
out
63+
}
64+
65+
fn render_header(&self, out: &mut String) {
66+
out.push_str(
67+
r#"/*
68+
* Plotnik Grammar Dump
69+
*
70+
* Syntax:
71+
* (node_kind) named node (queryable)
72+
* "literal" anonymous node (queryable)
73+
* (_hidden ...) hidden rule (not queryable, children inline)
74+
* {...} sequence (ordered children)
75+
* [...] alternation (first match)
76+
* ? * + quantifiers (0-1, 0+, 1+)
77+
* "x"! immediate token (no preceding whitespace)
78+
* field: ... named field
79+
* T :: supertype supertype declaration
80+
*/
81+
82+
"#,
83+
);
84+
}
85+
86+
fn render_extras(&self, out: &mut String) {
87+
if self.grammar.extras.is_empty() {
88+
return;
89+
}
90+
91+
out.push_str("extras = [\n");
92+
for extra in &self.grammar.extras {
93+
out.push_str(" ");
94+
self.render_rule(extra, out, 1);
95+
out.push('\n');
96+
}
97+
out.push_str("]\n\n");
98+
}
99+
100+
fn render_externals(&self, out: &mut String) {
101+
if self.grammar.externals.is_empty() {
102+
return;
103+
}
104+
105+
out.push_str("externals = [\n");
106+
for external in &self.grammar.externals {
107+
out.push_str(" ");
108+
self.render_rule(external, out, 1);
109+
out.push('\n');
110+
}
111+
out.push_str("]\n\n");
112+
}
113+
114+
fn render_supertypes(&self, out: &mut String) {
115+
for supertype in &self.grammar.supertypes {
116+
if let Some((_, rule)) = self.grammar.rules.iter().find(|(n, _)| n == supertype) {
117+
out.push_str(supertype);
118+
out.push_str(" :: supertype = ");
119+
self.render_rule(rule, out, 0);
120+
out.push_str("\n\n");
121+
}
122+
}
123+
}
124+
125+
fn render_rules(&self, out: &mut String) {
126+
let supertypes_set: HashSet<_> = self.grammar.supertypes.iter().collect();
127+
128+
for (name, rule) in &self.grammar.rules {
129+
if supertypes_set.contains(name) {
130+
continue;
131+
}
132+
133+
out.push_str(name);
134+
out.push_str(" = ");
135+
self.render_rule(rule, out, 0);
136+
out.push_str("\n\n");
137+
}
138+
}
139+
140+
fn render_rule(&self, rule: &Rule, out: &mut String, indent: usize) {
141+
match rule {
142+
Rule::Blank => out.push_str("()"),
143+
144+
Rule::String(s) => {
145+
out.push('"');
146+
for c in s.chars() {
147+
match c {
148+
'"' => out.push_str("\\\""),
149+
'\\' => out.push_str("\\\\"),
150+
'\n' => out.push_str("\\n"),
151+
'\r' => out.push_str("\\r"),
152+
'\t' => out.push_str("\\t"),
153+
_ => out.push(c),
154+
}
155+
}
156+
out.push('"');
157+
}
158+
159+
Rule::Pattern { value, flags } => {
160+
out.push('/');
161+
out.push_str(value);
162+
out.push('/');
163+
if let Some(f) = flags {
164+
out.push_str(f);
165+
}
166+
}
167+
168+
Rule::Symbol(name) => {
169+
if self.hidden_rules.contains(name.as_str()) {
170+
out.push('(');
171+
out.push_str(name);
172+
out.push_str(" ...)");
173+
} else {
174+
out.push('(');
175+
out.push_str(name);
176+
out.push(')');
177+
}
178+
}
179+
180+
Rule::Seq(children) => {
181+
self.render_block(children, '{', '}', indent, out);
182+
}
183+
184+
Rule::Choice(children) => {
185+
if let Some(simplified) = self.simplify_optional(children) {
186+
self.render_rule(&simplified, out, indent);
187+
out.push('?');
188+
} else {
189+
self.render_block(children, '[', ']', indent, out);
190+
}
191+
}
192+
193+
Rule::Repeat(inner) => {
194+
self.render_rule(inner, out, indent);
195+
out.push('*');
196+
}
197+
198+
Rule::Repeat1(inner) => {
199+
self.render_rule(inner, out, indent);
200+
out.push('+');
201+
}
202+
203+
Rule::Field { name, content } => {
204+
out.push_str(name);
205+
out.push_str(": ");
206+
self.render_rule(content, out, indent);
207+
}
208+
209+
Rule::Alias {
210+
content: _,
211+
value,
212+
named,
213+
} => {
214+
if *named {
215+
out.push('(');
216+
out.push_str(value);
217+
out.push(')');
218+
} else {
219+
out.push('"');
220+
out.push_str(value);
221+
out.push('"');
222+
}
223+
}
224+
225+
Rule::Token(inner) => {
226+
self.render_rule(inner, out, indent);
227+
}
228+
229+
Rule::ImmediateToken(inner) => {
230+
self.render_rule(inner, out, indent);
231+
out.push('!');
232+
}
233+
234+
Rule::Prec { content, .. }
235+
| Rule::PrecLeft { content, .. }
236+
| Rule::PrecRight { content, .. }
237+
| Rule::PrecDynamic { content, .. } => {
238+
self.render_rule(content, out, indent);
239+
}
240+
241+
Rule::Reserved { content, .. } => {
242+
self.render_rule(content, out, indent);
243+
}
244+
}
245+
}
246+
247+
fn render_block(&self, children: &[Rule], open: char, close: char, indent: usize, out: &mut String) {
248+
out.push(open);
249+
out.push('\n');
250+
251+
let child_indent = indent + 1;
252+
let prefix = " ".repeat(child_indent);
253+
254+
for child in children {
255+
out.push_str(&prefix);
256+
self.render_rule(child, out, child_indent);
257+
out.push('\n');
258+
}
259+
260+
out.push_str(&" ".repeat(indent));
261+
out.push(close);
262+
}
263+
264+
fn simplify_optional(&self, children: &[Rule]) -> Option<Rule> {
265+
if children.len() != 2 {
266+
return None;
267+
}
268+
269+
match (&children[0], &children[1]) {
270+
(Rule::Blank, other) | (other, Rule::Blank) => Some(other.clone()),
271+
_ => None,
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)