feat: Add compact AST dump

Introduce a new `dump_ast_compact` function for the environment and
Dumper. This function provides a simplified, human-readable
representation of the AST, focusing on inferred types and purity markers
instead of detailed debug information.

This change adds:
- A `compact` flag to the `Dumper` struct.
- The `dump_compact` method to the `Dumper`.
- The `dump_ast_compact` method to the `Environment`.
- A new `dump_ast_compact` command-line argument for the `ast` binary.
- A new MCP server endpoint `dump_ast_compact`.
- A `CompactMetadata` trait to abstract over different metadata types
  for compact display.
- Updates to documentation and CLI help messages.
This commit is contained in:
2026-03-29 18:28:10 +02:00
parent 10a7fcb576
commit 0e806b9e5d
6 changed files with 156 additions and 23 deletions
+63 -15
View File
@@ -1,17 +1,33 @@
use crate::ast::nodes::{CompilerPhase, Node, NodeKind}; use crate::ast::nodes::{CompactMetadata, CompilerPhase, Node, NodeKind};
/// Human-readable AST dumper for the bound AST. /// Human-readable AST dumper for the bound AST.
pub struct Dumper { pub struct Dumper {
output: String, output: String,
indent: usize, indent: usize,
/// When `true`, emits a compact format suitable for GUI display:
/// type annotations via `Display` and purity symbols instead of raw debug structs.
compact: bool,
} }
impl Dumper { impl Dumper {
/// Produces a formatted string representation of the given bound AST node and its children. /// Produces a verbose debug representation of the given AST node and its children.
pub fn dump<P: CompilerPhase>(node: &Node<P>) -> String { pub fn dump<P: CompilerPhase>(node: &Node<P>) -> String {
let mut dumper = Self { let mut dumper = Self {
output: String::new(), output: String::new(),
indent: 0, indent: 0,
compact: false,
};
dumper.visit(node);
dumper.output
}
/// Produces a compact, human-readable representation suitable for GUI display.
/// Shows inferred types and purity (`!` / `~`) instead of raw debug structs.
pub fn dump_compact<P: CompilerPhase>(node: &Node<P>) -> String {
let mut dumper = Self {
output: String::new(),
indent: 0,
compact: true,
}; };
dumper.visit(node); dumper.visit(node);
dumper.output dumper.output
@@ -26,8 +42,16 @@ impl Dumper {
fn log<P: CompilerPhase>(&mut self, label: &str, node: &Node<P>) { fn log<P: CompilerPhase>(&mut self, label: &str, node: &Node<P>) {
self.write_indent(); self.write_indent();
self.output.push_str(label); self.output.push_str(label);
if self.compact {
if let Some(annotation) = node.ty.compact_label() {
self.output.push_str("");
self.output.push_str(&annotation);
}
} else {
self.output self.output
.push_str(&format!(" <Metadata: {:?}>\n", node.ty)); .push_str(&format!(" <Metadata: {:?}>", node.ty));
}
self.output.push('\n');
} }
fn visit<P: CompilerPhase>(&mut self, node: &Node<P>) { fn visit<P: CompilerPhase>(&mut self, node: &Node<P>) {
@@ -37,7 +61,12 @@ impl Dumper {
self.log(&format!("Constant: {}", v), node); self.log(&format!("Constant: {}", v), node);
} }
NodeKind::Identifier { symbol, binding } => { NodeKind::Identifier { symbol, binding } => {
self.log(&format!("Identifier: {} ({:?})", symbol.name, binding), node) let label = if self.compact {
format!("Identifier: {}", symbol.name)
} else {
format!("Identifier: {} ({:?})", symbol.name, binding)
};
self.log(&label, node);
} }
NodeKind::FieldAccessor(k) => self.log(&format!("FieldAccessor: .{}", k.name()), node), NodeKind::FieldAccessor(k) => self.log(&format!("FieldAccessor: .{}", k.name()), node),
@@ -50,7 +79,12 @@ impl Dumper {
} }
NodeKind::Assign { target, value, info } => { NodeKind::Assign { target, value, info } => {
self.log(&format!("Assign: {:?}", info), node); let label = if self.compact {
"Assign".to_string()
} else {
format!("Assign: {:?}", info)
};
self.log(&label, node);
self.indent += 1; self.indent += 1;
self.write_indent(); self.write_indent();
self.output.push_str("Target:\n"); self.output.push_str("Target:\n");
@@ -66,7 +100,12 @@ impl Dumper {
value, value,
info, info,
} => { } => {
self.log(&format!("Def ({:?})", info), node); let label = if self.compact {
"Def".to_string()
} else {
format!("Def ({:?})", info)
};
self.log(&label, node);
self.indent += 1; self.indent += 1;
self.write_indent(); self.write_indent();
self.output.push_str("Pattern:\n"); self.output.push_str("Pattern:\n");
@@ -106,7 +145,12 @@ impl Dumper {
body, body,
info, info,
} => { } => {
self.log(&format!("Lambda ({:?})", info), node); let label = if self.compact {
"Lambda".to_string()
} else {
format!("Lambda ({:?})", info)
};
self.log(&label, node);
self.indent += 1; self.indent += 1;
self.write_indent(); self.write_indent();
@@ -157,10 +201,12 @@ impl Dumper {
} }
NodeKind::Record { fields, layout } => { NodeKind::Record { fields, layout } => {
self.log( let label = if self.compact {
&format!("Record (Layout: {:?})", layout), "Record".to_string()
node, } else {
); format!("Record (Layout: {:?})", layout)
};
self.log(&label, node);
self.indent += 1; self.indent += 1;
for (key, val) in fields { for (key, val) in fields {
self.write_indent(); self.write_indent();
@@ -181,10 +227,12 @@ impl Dumper {
original_call, original_call,
expanded, expanded,
} => { } => {
self.log( let label = if self.compact {
&format!("Expansion (Original: {:?})", original_call.kind), "Expansion".to_string()
node, } else {
); format!("Expansion (Original: {:?})", original_call.kind)
};
self.log(&label, node);
self.indent += 1; self.indent += 1;
self.visit(expanded); self.visit(expanded);
self.indent -= 1; self.indent -= 1;
+9
View File
@@ -725,6 +725,15 @@ impl Environment {
Ok(Dumper::dump(&linked)) Ok(Dumper::dump(&linked))
} }
/// Like [`dump_ast`] but produces a compact, GUI-friendly representation:
/// inferred types via `Display` and purity symbols (`!` / `~`) instead of raw debug output.
pub fn dump_ast_compact(&self, source: &str) -> Result<String, String> {
self.preload_dependencies(source, None)?;
let compiled = self.compile(source).into_result()?;
let linked = self.link(compiled);
Ok(Dumper::dump_compact(&linked))
}
pub fn compile(&self, source: &str) -> CompilationResult { pub fn compile(&self, source: &str) -> CompilationResult {
if let Err(e) = self.preload_dependencies(source, None) { if let Err(e) = self.preload_dependencies(source, None) {
return CompilationResult::error(format!("Dependency Error: {}", e)); return CompilationResult::error(format!("Dependency Error: {}", e));
+18 -2
View File
@@ -70,10 +70,12 @@ impl MycMcpServer {
} }
} }
/// Dumps the compiled and optimized AST for a Myc expression. /// Dumps the compiled and optimized AST for a Myc expression (verbose debug format).
#[tool(description = "Compile a Myc expression and return a human-readable dump of \ #[tool(description = "Compile a Myc expression and return a human-readable dump of \
the compiled AST. Useful for understanding how the compiler transforms code \ the compiled AST. Useful for understanding how the compiler transforms code \
through its optimization and lowering passes.")] through its optimization and lowering passes. \
Returns the full debug format including binding addresses and stack offsets. \
Use dump_ast_compact for a cleaner view focused on types and purity.")]
fn dump_ast(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String { fn dump_ast(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String {
let env = Self::make_env(); let env = Self::make_env();
match env.dump_ast(&code) { match env.dump_ast(&code) {
@@ -82,6 +84,20 @@ impl MycMcpServer {
} }
} }
/// Dumps the compiled AST in compact, human-readable format.
#[tool(description = "Compile a Myc expression and return a compact AST dump showing \
inferred types (e.g. 'fn([int]) -> bool') and purity markers ('!' for impure, \
'~' for side-effect-free). '[tail]' marks tail-call positions. \
Omits internal details like stack offsets and binding addresses. \
Prefer this over dump_ast when you want to understand the type structure of an expression.")]
fn dump_ast_compact(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String {
let env = Self::make_env();
match env.dump_ast_compact(&code) {
Ok(dump) => dump,
Err(e) => format!("Error: {}", e),
}
}
/// Checks the syntax and types of a Myc expression without executing it. /// Checks the syntax and types of a Myc expression without executing it.
#[tool(description = "Check the syntax and types of a Myc expression without running it. \ #[tool(description = "Check the syntax and types of a Myc expression without running it. \
Returns 'OK' if the code is valid, or a list of compiler diagnostics on error. \ Returns 'OK' if the code is valid, or a list of compiler diagnostics on error. \
+52 -1
View File
@@ -95,10 +95,61 @@ pub enum DeclarationKind {
Parameter, Parameter,
} }
// ── CompactMetadata trait ──────────────────────────────────────────────────────
/// Provides a compact, human-readable annotation for node metadata.
/// Used by the GUI AST dump to show type and purity without verbose debug output.
pub trait CompactMetadata {
/// Returns a short annotation such as `"int"`, `"fn([]) -> bool !"`.
/// Returns `None` for phases that carry no type information.
fn compact_label(&self) -> Option<String>;
}
impl CompactMetadata for () {
fn compact_label(&self) -> Option<String> {
None
}
}
impl CompactMetadata for StaticType {
fn compact_label(&self) -> Option<String> {
Some(format!("{self}"))
}
}
impl CompactMetadata for NodeMetrics {
fn compact_label(&self) -> Option<String> {
Some(format!(
"{}{}",
self.original.ty,
purity_suffix(self.purity)
))
}
}
impl CompactMetadata for RuntimeMetadata {
fn compact_label(&self) -> Option<String> {
Some(format!(
"{}{}{}",
self.ty,
if self.is_tail { " [tail]" } else { "" },
purity_suffix(self.original.ty.purity)
))
}
}
fn purity_suffix(p: Purity) -> &'static str {
match p {
Purity::Pure => "",
Purity::SideEffectFree => " ~",
Purity::Impure => " !",
}
}
// ── CompilerPhase trait ──────────────────────────────────────────────────────── // ── CompilerPhase trait ────────────────────────────────────────────────────────
pub trait CompilerPhase: std::fmt::Debug + 'static { pub trait CompilerPhase: std::fmt::Debug + 'static {
type Metadata: std::fmt::Debug + Clone + PartialEq; type Metadata: std::fmt::Debug + Clone + PartialEq + CompactMetadata;
type LocalAddress: std::fmt::Debug + Clone + Copy + PartialEq + Eq + std::hash::Hash; type LocalAddress: std::fmt::Debug + Clone + Copy + PartialEq + Eq + std::hash::Hash;
/// Semantic role and address of an identifier (reference vs. declaration). /// Semantic role and address of an identifier (reference vs. declaration).
+10 -1
View File
@@ -26,10 +26,14 @@ struct Cli {
#[arg(short, long)] #[arg(short, long)]
update_bench: bool, update_bench: bool,
/// Dump the compiled AST /// Dump the compiled AST (verbose debug format)
#[arg(short, long)] #[arg(short, long)]
dump: bool, dump: bool,
/// Dump the compiled AST in compact, human-readable format (types + purity)
#[arg(long)]
dump_compact: bool,
/// Run with TracingObserver enabled /// Run with TracingObserver enabled
#[arg(short, long)] #[arg(short, long)]
trace: bool, trace: bool,
@@ -129,6 +133,11 @@ fn main() {
Ok(dump) => println!("{}", dump), Ok(dump) => println!("{}", dump),
Err(e) => eprintln!("Error dumping AST: {}", e), Err(e) => eprintln!("Error dumping AST: {}", e),
} }
} else if cli.dump_compact {
match env.dump_ast_compact(&content) {
Ok(dump) => println!("{}", dump),
Err(e) => eprintln!("Error dumping AST: {}", e),
}
} else if cli.trace { } else if cli.trace {
execute_trace(&env, &content); execute_trace(&env, &content);
} else { } else {
+2 -2
View File
@@ -163,9 +163,9 @@ impl CompilerApp {
self.env = Environment::new(); self.env = Environment::new();
self.env.optimization = true; // Enable optimization for AST dump self.env.optimization = true; // Enable optimization for AST dump
match self.env.dump_ast(&self.source_code) { match self.env.dump_ast_compact(&self.source_code) {
Ok(dump) => { Ok(dump) => {
self.output_log = format!("--- BOUND AST DUMP ---\n\n{}", dump); self.output_log = format!("--- AST DUMP ---\n\n{}", dump);
} }
Err(e) => { Err(e) => {
self.output_log = format!("Error during AST Binding: {}", e); self.output_log = format!("Error during AST Binding: {}", e);