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:
+63
-15
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user