diff --git a/Cargo.lock b/Cargo.lock index 683c9f7..34dc913 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,8 @@ dependencies = [ "compiler_core", "insta", "owo-colors", + "serde_json", + "tree-sitter", "wasmtime", ] @@ -334,27 +336,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c4ebb31662e2051dcc49b7342d222405a99e951720756cc4b93315972abd67" +checksum = "0bc293b86236abcc45f2f72e2d18e2bd636f2a08b75eb286bae31e71e1430c91" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dfc2ec96ec1c3a8a250602e936712e00a381df032f7a8ad175c8f768c03bb" +checksum = "b954c826eddaf1b001402cb8aecf1764c6f6d637ba69fb9e3311f1ebac965be6" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5694aa8a2eb2571a15b3feee38d16ccaf2712200e7b5c9ae0479069bdfb46949" +checksum = "4053fa2575ef4a5c35d2708533df2200400ae979226cea9cc92a578b811bd4e7" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -362,9 +364,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab349d30a5fad9699440ee7ccb435374e8a8735dcca26696a4245bcefcc47e" +checksum = "d216663191014aa63e1d2cffd058e609eaf207646d40b739d88250f65b2c4f69" dependencies = [ "serde", "serde_derive", @@ -373,9 +375,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e95970bdb51d145c828a114a1084cb8b63e65569a51600ad398cb49fa78b062" +checksum = "9a5e7e7aad6a425a51da1ad7ab9e5d280ea97eb7c7c4545fafb567915a75aadb" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -401,9 +403,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8414b8ecc81f89f8a3f2c5cbc785b9ed690200cd6f9d780e96a92f88879704" +checksum = "c421d80a9a85f806cb02a2983b5b5368a335c319795b1f1b4b771a24479af5b0" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -414,24 +416,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631c4e5db42e6a0f9e7a68f18f7faba3692862114870c7c598aee7e0e5677e59" +checksum = "78fdb83ab012d0ee6a44ced7ca8788a444f17cf821c62f95d6ef87c9f0262518" [[package]] name = "cranelift-control" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c6e92c825abfbb739a4beaa5db3988f98a96a68d6ea656f562098efc142976" +checksum = "1b75adc6eb7bb4ac6365106afb6cac4f12fe1ddfa02ddc9fd7015ca1469b471b" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e57cd185782abada9ab2606bfe88d0abc0d42d83a7d432dcf69991e17fe76e" +checksum = "668e56db75a54816cbdd7c7b7bfc558b08bf7b2cda9d0846491517e92f3b393b" dependencies = [ "cranelift-bitset", "serde", @@ -441,9 +443,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0f17e48d15e29552e2f264d302c31a661a830196d879bbdaf4d7b2bf2f7011" +checksum = "c63892dc1cc3ae48680183fa66997f60ffe7f1e200c8d390f8ee66edff4aef5a" dependencies = [ "cranelift-codegen", "log", @@ -453,15 +455,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "407b80b46934c9dce9a6581f0e80d079b7a69e11372fb03d7dce4c7ba3fee4e3" +checksum = "94eaf429c32a12715429c7c6ddfdd43c170f4cdd7e97bfa507bd68a652091087" [[package]] name = "cranelift-native" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a40e056e421d9a6c757983f2184f765ae1c28a51555d3877e98afc97ce5705b" +checksum = "cd77674904ae9be11c1e1efdba54788b59f3d6658d747b97534bfbba2909aacc" dependencies = [ "cranelift-codegen", "libc", @@ -470,9 +472,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.132.1" +version = "0.132.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cdbadda21e49798825a1ec795dab30bcb03235891f662b5ccf23fa45b39682f" +checksum = "cba7c0ff5941842c36653da155580ce41e675c204a67ac1b4e1c478a9347bbb7" [[package]] name = "crc32fast" @@ -1510,9 +1512,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6ccdd6fc70f6c33eb21cb8fe05a71a5f7ee4cbef7a093a8a87dd8a908697cd" +checksum = "2d9880c1985ccccaed3646b0ef793dc39a4b117403ed4afc6fa3ef6027c5200f" dependencies = [ "cranelift-bitset", "log", @@ -1522,9 +1524,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00101fdb6fcaf9ea98a1994be11861d7e0c910c718c41bae373c44179e3160c" +checksum = "ee249346855ad102580e474da5463f86f8a7d449e6d49e00fefb304e448e2983" dependencies = [ "proc-macro2", "quote", @@ -2628,9 +2630,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c191891081c3bf9f10cb579819b32b0e81695641dc639eef34b57cf10c59ad81" +checksum = "5c7ce9aa2c67f75fadcfdc6aa9097d03e7c39485dfe316f2ed6a7c0fd186c527" dependencies = [ "addr2line", "async-trait", @@ -2681,9 +2683,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f337d68a62d868f3c297517b46d20dc7e293f0da36bbee2f6ec3c30eab938bd" +checksum = "c8fb157bd1fbf689ac89d570433a700db6f33bdfcb5ffc30e3f1c49e4c70de71" dependencies = [ "anyhow", "cpp_demangle", @@ -2712,9 +2714,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f45a4fb2a6a269100b845404f2210d874eaee9ecd9efb6fc6c1e80896fab40f" +checksum = "e0d1a46c4a2360186b59c6ed7a74a1121ac97925ae9a18db1b2f146cc27ac0b7" dependencies = [ "base64", "directories-next", @@ -2732,9 +2734,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab1cc8df1940657960571d7bc9bc0eadf869c0eb95cff831a6554409f89b25b0" +checksum = "b96c17f35fae2ab574667aba0c58fd56349a6f788ac42541a2e543116d5cfb91" dependencies = [ "anyhow", "proc-macro2", @@ -2747,15 +2749,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73ccd2e0e6bd91fb0161a17ee5e2d7badbeba6eb7461d0e0e3991492bd3835d" +checksum = "9d2eeb9b53222859e6f5dc73d2ccfb33254d672469cac11b693a71912e2f3817" [[package]] name = "wasmtime-internal-core" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110bf85122cd451d3b9ff67f8911d428ec9b729208abe950a0333c3244660e88" +checksum = "4a1deaf6bc3430abd7497b00c64f06ca2b97ca0fe41af87836446ca30949965c" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -2765,9 +2767,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0236a21553c5b30ee953f413a8dc99729548b747219eef7e70f8288ed313ebe" +checksum = "b845f83b5b04b11bc48329b53eb4fa8cf9f28a43c71ed8e1203f68ffa9806d1b" dependencies = [ "cfg-if", "cranelift-codegen", @@ -2792,9 +2794,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ccd50931b61ad593ae91e62d41a96b997b26c9308305cae4523cfef9301d9e8" +checksum = "e10c8466f72965ae85c250f90aaa7992c089a2f8502009bd0d2c9e7d6409174a" dependencies = [ "cc", "cfg-if", @@ -2807,9 +2809,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a52ca7429272234b44f81fdf80d4793593e5c368b0f960d41fd401b1117bec" +checksum = "1d3adfecf5621b14d8f8871f4cb4ed9f844197b1ddefc702ef4c859552cd9551" dependencies = [ "cc", "object", @@ -2819,9 +2821,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6818a4864719772680694f4e4649a8600bb5efcf71111ebaf7419b266463e8" +checksum = "08d3c1e9fb618ec45c9b3477ea683cd37bee427273d7b13bba5c66a1caaf1dd6" dependencies = [ "cfg-if", "libc", @@ -2831,9 +2833,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdc6a69c42fbbf13104ccbf0d3c096d0392f00102e2c70b542d9fca402eb162" +checksum = "7aa91132b81f1e172ec7e7c3c114ac34209ee6b3524b3a8d6943af99803f66c5" dependencies = [ "cfg-if", "cranelift-codegen", @@ -2844,9 +2846,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0bd1cd696ec4b7e6f46357c0479e7739c3858e54afb69a15765402bbc1f9dd" +checksum = "6ea811ffe23f597cc7708327ea25d9eb018dcf760ffe15ccb7d0b27ad635de61" dependencies = [ "proc-macro2", "quote", @@ -2855,9 +2857,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2ca01234ef55acd10c0fa5a2f96c3fd422eba03b221e2e9572944d19a476a7" +checksum = "828b66175c54a0d00b4c1c1c76658d8aa73aeb9fa3553575c5eee56d40f2eb18" dependencies = [ "cranelift-codegen", "gimli", @@ -2872,9 +2874,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b3d8cfaad2d33a92ec4ed93d69e9836c04f7eb5b8dfe1dde9df47c41f20b7" +checksum = "4ae00896ad9bef1b3ca6401ae9a841daa6f357dd91541b6baf87082946d1bde1" dependencies = [ "anyhow", "bitflags", @@ -2967,9 +2969,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "45.0.1" +version = "45.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fdfab04a4d446c558a9ea994ded662d35580a17b15385b862002703aa43245" +checksum = "89c09acfdfa281b3340e1e94ef3cf6618d69eab975280f881e154c29f49419c1" dependencies = [ "cranelift-assembler-x64", "cranelift-codegen", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 2c1b8f3..8ec0c9a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,7 +16,9 @@ path = "src/bin/regulus.rs" clap = { version = "4.5.40", features = ["derive"] } compiler_core = { path = "../core" } owo-colors = "4.2.1" -wasmtime = "45.0.1" +serde_json = "1.0.145" +tree-sitter = "0.26.9" +wasmtime = "45.0.2" [dev-dependencies] insta = "1.43.2" diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index e559b65..ff2eec4 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::{Parser, Subcommand, ValueEnum}; +use clap::{Args as ClapArgs, Parser, Subcommand, ValueEnum}; use compiler_core::target::CompileTarget; #[derive(Debug, Parser)] @@ -90,14 +90,30 @@ pub enum Command { /// Inspect compiler-internal views of one Gleam source file. #[command(visible_alias = "dbg")] Debug { + #[command(subcommand)] + view: Option, /// Gleam source file to inspect. - input: PathBuf, + /// + /// Required when no debug subcommand is used. + input: Option, /// Print the raw tree-sitter concrete syntax tree as an S-expression. /// /// Use this to look at exact node kinds, field names, or the way Gleam /// syntax is split across adjacent tree-sitter nodes. #[arg(long = "ts", alias = "tree-sitter")] tree_sitter: bool, + /// Print the Regulus AST built from the tree-sitter tree. + #[arg(long)] + ast: bool, + /// Include tree-sitter byte spans, positions, and field names. + #[arg(long)] + spans: bool, + /// Print selected debug views as JSON. + #[arg(long)] + json: bool, + /// Disable ANSI colors in human-readable debug output. + #[arg(long)] + no_color: bool, }, /// Load a Gleam project and print discovered modules. List { @@ -106,6 +122,58 @@ pub enum Command { }, } +#[derive(Debug, Subcommand)] +pub enum DebugCommand { + /// Print the raw tree-sitter concrete syntax tree. + #[command(visible_alias = "tree-sitter")] + Ts(DebugTreeArgs), + /// Print tree-sitter nodes with byte spans, positions, and field names. + Spans(DebugTreeArgs), + /// Print the Regulus AST built from the tree-sitter tree. + Ast(DebugAstArgs), + /// Print selected debug views as JSON. + Json(DebugJsonArgs), +} + +#[derive(Debug, ClapArgs)] +pub struct DebugTreeArgs { + /// Gleam source file to inspect. + pub input: PathBuf, + /// Print this view as JSON. + #[arg(long)] + pub json: bool, + /// Disable ANSI colors in human-readable output. + #[arg(long)] + pub no_color: bool, +} + +#[derive(Debug, ClapArgs)] +pub struct DebugAstArgs { + /// Gleam source file to inspect. + pub input: PathBuf, + /// Print this view as JSON. + #[arg(long)] + pub json: bool, + /// Disable ANSI colors in human-readable output. + #[arg(long)] + pub no_color: bool, +} + +#[derive(Debug, ClapArgs)] +pub struct DebugJsonArgs { + /// Gleam source file to inspect. + pub input: PathBuf, + /// Include the tree-sitter S-expression. + #[arg(long = "ts", alias = "tree-sitter")] + pub tree_sitter: bool, + /// Include the Regulus AST. + #[arg(long)] + pub ast: bool, + /// Include tree-sitter span details instead of only the S-expression. + #[arg(long)] + pub spans: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum Emit { Wasm, diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index 68fe80a..93fb527 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -11,7 +11,7 @@ use std::process::ExitCode; use compiler_core::source::SourceFile; use compiler_core::{diagnostic::Diagnostics, target::CompileTarget}; -use super::args::Command; +use super::args::{Command, DebugCommand}; use super::echo; use builder::Builder; @@ -56,11 +56,36 @@ pub fn run(command: Command) -> ExitCode { let runner = Runner { input: &input, function: &function, args: &args, target, verbose, json }; runner.run() } - Command::Debug { input, tree_sitter } => Debugger::new(&input, tree_sitter).run(), + Command::Debug { view, input, tree_sitter, ast, spans, json, no_color } => { + run_debug(view, input.as_deref(), tree_sitter, ast, spans, json, no_color) + } Command::List { project } => list(project.as_deref().unwrap_or_else(|| Path::new("."))), } } +fn run_debug( + view: Option, input: Option<&Path>, ts: bool, ast: bool, spans: bool, json: bool, no_color: bool, +) -> ExitCode { + match view { + Some(DebugCommand::Ts(args)) => Debugger::new(&args.input, true, false, false, args.json, args.no_color).run(), + Some(DebugCommand::Spans(args)) => { + Debugger::new(&args.input, true, false, true, args.json, args.no_color).run() + } + Some(DebugCommand::Ast(args)) => Debugger::new(&args.input, false, true, false, args.json, args.no_color).run(), + Some(DebugCommand::Json(args)) => { + let tree_sitter = args.tree_sitter || args.spans || !args.ast; + Debugger::new(&args.input, tree_sitter, args.ast, args.spans, true, false).run() + } + None => { + let Some(input) = input else { + echo::error("debug requires a subcommand or a source file with at least one view flag"); + return ExitCode::FAILURE; + }; + Debugger::new(input, ts, ast, spans, json, no_color).run() + } + } +} + fn list(input: &Path) -> ExitCode { match compiler_core::project::load_project(input) { Ok(project) => { diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs index 1f32384..29d6925 100644 --- a/crates/cli/src/commands/debug.rs +++ b/crates/cli/src/commands/debug.rs @@ -3,6 +3,9 @@ use std::path::Path; use std::process::ExitCode; use compiler_core::source::{SourceFile, SourceFileId}; +use owo_colors::OwoColorize; +use serde_json::{Value, json}; +use tree_sitter::Node; use crate::echo; @@ -14,18 +17,26 @@ use crate::echo; pub struct Debugger<'a> { pub input: &'a Path, pub tree_sitter: bool, + pub ast: bool, + pub spans: bool, + pub json: bool, + pub no_color: bool, } impl<'a> Debugger<'a> { - pub fn new(input: &'a Path, ts: bool) -> Self { - Self { input, tree_sitter: ts } + pub fn new(input: &'a Path, ts: bool, ast: bool, spans: bool, json: bool, no_color: bool) -> Self { + Self { input, tree_sitter: ts, ast, spans, json, no_color } } } impl Debugger<'_> { pub fn run(&self) -> ExitCode { - if !self.tree_sitter { - echo::error("select at least one debug view, for example `--ts`"); + if !self.tree_sitter && !self.ast { + echo::error("select at least one debug view, for example `--ts` or `--ast`"); + return ExitCode::FAILURE; + } + if self.spans && !self.tree_sitter { + echo::error("`--spans` currently applies to `--ts`; pass both flags together"); return ExitCode::FAILURE; } @@ -39,17 +50,125 @@ impl Debugger<'_> { Err(diagnostics) => return echo::fail_with_diagnostics("parse", self.input.display(), &diagnostics), }; + if self.json { + return self.print_json(&cst); + } + if self.tree_sitter { - println!("{}", cst.tree.root_node().to_sexp()); + if self.spans { + println!("{}", self.heading("tree-sitter")); + print_tree(cst.tree.root_node(), 0, None, !self.no_color); + } else { + println!("{}", cst.tree.root_node().to_sexp()); + } } - // TODO: Add `--ast` output once the debug command has a stable shape - // for printing Regulus AST after target selection. - // TODO: Add `--spans` to include byte spans and field names for parser - // and diagnostic source-mapping work. - // TODO: Add `--json` for machine-readable debug output suitable for - // editor tooling and snapshot fixtures. + if self.ast { + let ast = match compiler_core::ast::build(&cst) { + Ok(ast) => ast, + Err(diagnostics) => { + return echo::fail_with_diagnostics("build AST", self.input.display(), &diagnostics); + } + }; + println!("{}", self.heading("ast")); + println!("{ast:#?}"); + } ExitCode::SUCCESS } + + fn print_json(&self, cst: &compiler_core::parse::ConcreteSyntaxTree) -> ExitCode { + let mut output = serde_json::Map::new(); + output.insert("file".to_string(), json!(self.input.to_string_lossy().to_string())); + + if self.tree_sitter { + let value = if self.spans { + tree_json(cst.tree.root_node(), None) + } else { + json!({ "sexp": cst.tree.root_node().to_sexp() }) + }; + output.insert("tree_sitter".to_string(), value); + } + + if self.ast { + let ast = match compiler_core::ast::build(cst) { + Ok(ast) => ast, + Err(diagnostics) => { + return echo::fail_with_diagnostics("build AST", self.input.display(), &diagnostics); + } + }; + output.insert("ast".to_string(), json!(format!("{ast:#?}"))); + } + + match serde_json::to_string_pretty(&Value::Object(output)) { + Ok(text) => { + println!("{text}"); + ExitCode::SUCCESS + } + Err(error) => echo::fail("debug", "JSON output", error), + } + } + + fn heading(&self, text: &str) -> String { + if self.no_color { + format!("{text}:") + } else { + format!("{}", format!("{text}:").bright_magenta().bold()) + } + } +} + +fn print_tree(node: Node<'_>, depth: usize, field: Option<&str>, color: bool) { + let indent = " ".repeat(depth); + let kind = if color { format!("{}", node.kind().bright_cyan()) } else { node.kind().to_string() }; + let field = field + .map(|field| { + if color { + format!(" {}={}", "field".bright_black(), field.bright_yellow()) + } else { + format!(" field={field}") + } + }) + .unwrap_or_default(); + println!( + "{indent}{kind}{field} [{}..{}] {}:{}..{}:{}", + node.start_byte(), + node.end_byte(), + node.start_position().row + 1, + node.start_position().column + 1, + node.end_position().row + 1, + node.end_position().column + 1 + ); + + let mut cursor = node.walk(); + for (index, child) in node.named_children(&mut cursor).enumerate() { + let field = node.field_name_for_named_child(index as u32); + print_tree(child, depth + 1, field, color); + } +} + +fn tree_json(node: Node<'_>, field: Option<&str>) -> Value { + let mut cursor = node.walk(); + let children = node + .named_children(&mut cursor) + .enumerate() + .map(|(index, child)| tree_json(child, node.field_name_for_named_child(index as u32))) + .collect::>(); + + json!({ + "kind": node.kind(), + "field": field, + "named": node.is_named(), + "start_byte": node.start_byte(), + "end_byte": node.end_byte(), + "start": { + "line": node.start_position().row + 1, + "column": node.start_position().column + 1, + }, + "end": { + "line": node.end_position().row + 1, + "column": node.end_position().column + 1, + }, + "children": children, + }) } diff --git a/crates/cli/tests/build.rs b/crates/cli/tests/build.rs index db01979..d7096e3 100644 --- a/crates/cli/tests/build.rs +++ b/crates/cli/tests/build.rs @@ -146,6 +146,178 @@ fn debug_alias_prints_tree_sitter_tree() { let _ = fs::remove_dir_all(temp); } +#[test] +fn debug_subcommands_select_views() { + let temp = unique_temp_dir("regulus_cli_debug_subcommands"); + fs::create_dir_all(&temp).expect("create temp dir"); + + let input = temp.join("app.gleam"); + fs::write(&input, "pub fn main() -> Int { 1 }\n").expect("write Gleam input"); + + let ts_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("dbg") + .arg("ts") + .arg(&input) + .output() + .expect("run reggie dbg ts"); + assert!( + ts_output.status.success(), + "dbg ts failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&ts_output.stdout), + String::from_utf8_lossy(&ts_output.stderr) + ); + assert!( + String::from_utf8_lossy(&ts_output.stdout).contains("(source_file"), + "missing tree output" + ); + + let ast_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("debug") + .arg("ast") + .arg(&input) + .arg("--no-color") + .output() + .expect("run reggie debug ast"); + assert!( + ast_output.status.success(), + "debug ast failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&ast_output.stdout), + String::from_utf8_lossy(&ast_output.stderr) + ); + assert!( + String::from_utf8_lossy(&ast_output.stdout).contains("Function("), + "missing AST output" + ); + + let json_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("debug") + .arg("json") + .arg(&input) + .arg("--ast") + .arg("--spans") + .output() + .expect("run reggie debug json"); + assert!( + json_output.status.success(), + "debug json failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&json_output.stdout), + String::from_utf8_lossy(&json_output.stderr) + ); + + let json_stdout = String::from_utf8_lossy(&json_output.stdout); + assert!( + json_stdout.contains("\"tree_sitter\""), + "missing tree JSON: {json_stdout}" + ); + assert!(json_stdout.contains("\"ast\""), "missing AST JSON: {json_stdout}"); + + let _ = fs::remove_dir_all(temp); +} + +#[test] +fn debug_without_subcommand_requires_view_flags() { + let output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("dbg") + .output() + .expect("run reggie dbg"); + + assert!(!output.status.success(), "plain dbg should fail"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("debug requires a subcommand"), + "unexpected stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn debug_prints_ast_spans_and_json_views() { + let temp = unique_temp_dir("regulus_cli_debug_views"); + fs::create_dir_all(&temp).expect("create temp dir"); + + let input = temp.join("app.gleam"); + fs::write(&input, "pub fn main() -> Int { 1 }\n").expect("write Gleam input"); + + let ast_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("debug") + .arg(&input) + .arg("--ast") + .arg("--no-color") + .output() + .expect("run reggie debug --ast"); + assert!( + ast_output.status.success(), + "debug --ast failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&ast_output.stdout), + String::from_utf8_lossy(&ast_output.stderr) + ); + + let ast_stdout = String::from_utf8_lossy(&ast_output.stdout); + assert!(ast_stdout.contains("ast:"), "missing AST heading: {ast_stdout}"); + assert!(ast_stdout.contains("Function("), "missing AST function: {ast_stdout}"); + assert!( + !ast_stdout.contains("\u{1b}["), + "--no-color should disable ANSI codes: {ast_stdout:?}" + ); + + let spans_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("debug") + .arg(&input) + .arg("--ts") + .arg("--spans") + .arg("--no-color") + .output() + .expect("run reggie debug --ts --spans"); + assert!( + spans_output.status.success(), + "debug --spans failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&spans_output.stdout), + String::from_utf8_lossy(&spans_output.stderr) + ); + + let spans_stdout = String::from_utf8_lossy(&spans_output.stdout); + assert!( + spans_stdout.contains("tree-sitter:"), + "missing tree heading: {spans_stdout}" + ); + assert!( + spans_stdout.contains("source_file [0.."), + "missing root span: {spans_stdout}" + ); + assert!( + spans_stdout.contains("field=name"), + "missing field names: {spans_stdout}" + ); + + let json_output = Command::new(env!("CARGO_BIN_EXE_reggie")) + .arg("debug") + .arg(&input) + .arg("--ts") + .arg("--ast") + .arg("--spans") + .arg("--json") + .output() + .expect("run reggie debug --json"); + assert!( + json_output.status.success(), + "debug --json failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&json_output.stdout), + String::from_utf8_lossy(&json_output.stderr) + ); + + let json_stdout = String::from_utf8_lossy(&json_output.stdout); + assert!( + json_stdout.contains("\"tree_sitter\""), + "missing tree JSON: {json_stdout}" + ); + assert!( + json_stdout.contains("\"start_byte\""), + "missing span JSON: {json_stdout}" + ); + assert!(json_stdout.contains("\"ast\""), "missing AST JSON: {json_stdout}"); + + let _ = fs::remove_dir_all(temp); +} + #[test] fn builds_package_owned_overlap_fixture() { let fixture = workspace_root().join("fixtures/projects/generated_names/dependency_module_overlap"); diff --git a/docs/website/guide/usage/cli.md b/docs/website/guide/usage/cli.md index 2c07475..f3c67cb 100644 --- a/docs/website/guide/usage/cli.md +++ b/docs/website/guide/usage/cli.md @@ -127,6 +127,33 @@ dumps include per-module AST, resolved AST, typed output, linked IR, and WAT. If compilation fails, Regulus does not write the final Wasm artifact. Debug artifacts are only written when the requested compiler phase completes. +## Inspect one source file + +Use `debug` when changing parser or AST support for one `.gleam` file. The +`dbg` command is an alias. + +```sh +reggie debug ts path/to/module.gleam +reggie debug spans path/to/module.gleam +reggie debug ast path/to/module.gleam +reggie debug json path/to/module.gleam --ast --spans +``` + +The debug views are: + +| View | Output | +| ------- | ---------------------------------------------------- | +| `ts` | Tree-sitter concrete syntax tree S-expression. | +| `spans` | Tree-sitter nodes with spans, positions, and fields. | +| `ast` | Regulus AST built from the tree-sitter tree. | +| `json` | Selected debug views as JSON. | + +`spans` is for tree-sitter nodes. `json --spans` includes tree-sitter span +details. If no `json` view flags are passed, `json` defaults to tree-sitter +output. + +By default, debug/dbg require a source file and at least one view flag. + ## List project modules Use `list` to inspect discovered modules without building artifacts.