From 6e007774e4abc0283f929f0cc2538ae3f90b0632 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 17:46:13 +0000 Subject: [PATCH] CLI: --call-graph flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prints a tree view of every function/handler and its direct callees, plus the max call depth reached from each state-handler entry point. Useful for stack-budget investigation since the NES has only 256 bytes of stack. === Call Graph (max depth: 2 / 8) === Main::frame (max depth 2) ├── clamp ├── clamp └── check_collision check_collision ├── abs_diff └── abs_diff abs_diff └── (leaf) clamp └── (leaf) Max-depth labels are only shown on entry points where the analyzer has computed a depth; transitive callees print without a label so the output isn't confusing. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3 --- docs/language-guide.md | 1 + src/main.rs | 52 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/docs/language-guide.md b/docs/language-guide.md index fe03453..8ab8dfb 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -944,6 +944,7 @@ nescript build game.ne --dump-ir | `--asm-dump` | Dump generated 6502 assembly to stdout | | `--dump-ir` | Dump the lowered IR program (after optimization) to stdout | | `--memory-map` | Dump a memory map of variable allocations to stdout | +| `--call-graph` | Dump a call graph (which handler/function calls which) to stdout | | `--use-ast` | Use the legacy AST-based codegen (default is the IR codegen) | ### Check diff --git a/src/main.rs b/src/main.rs index ec12442..90d607b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,10 @@ enum Cli { #[arg(long)] memory_map: bool, + /// Dump a call graph showing which functions call which. + #[arg(long)] + call_graph: bool, + /// Use the legacy AST-based codegen. The default is the IR-based /// codegen, which runs the optimizer passes before emitting 6502. #[arg(long)] @@ -61,6 +65,7 @@ fn main() { asm_dump, dump_ir, memory_map, + call_graph, use_ast, } => { let output = output.unwrap_or_else(|| input.with_extension("nes")); @@ -71,6 +76,7 @@ fn main() { asm_dump, dump_ir, memory_map, + call_graph, use_ast, }, ) { @@ -155,6 +161,46 @@ fn print_memory_map(analysis: &nescript::analyzer::AnalysisResult) { println!("Main RAM: {ram_used}/1280 bytes used"); } +/// Print a human-readable call graph of the analyzed program. +/// Entries show the max call depth reached from each entry point +/// (state handler) and the transitive callees. +fn print_call_graph(analysis: &nescript::analyzer::AnalysisResult) { + use std::collections::BTreeMap; + + let sorted: BTreeMap<_, _> = analysis + .call_graph + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let max_depth = analysis.max_depths.values().copied().max().unwrap_or(0); + + println!("=== Call Graph (max depth: {max_depth} / 8) ==="); + if sorted.is_empty() { + println!(" (no functions or handlers)"); + return; + } + for (caller, callees) in &sorted { + if let Some(depth) = analysis.max_depths.get(caller) { + println!("{caller} (max depth {depth})"); + } else { + println!("{caller}"); + } + if callees.is_empty() { + println!(" └── (leaf)"); + } else { + let count = callees.len(); + for (i, callee) in callees.iter().enumerate() { + let branch = if i + 1 == count { + "└──" + } else { + "├──" + }; + println!(" {branch} {callee}"); + } + } + } +} + fn dump_asm(instructions: &[nescript::asm::Instruction]) { use nescript::asm::{AddressingMode, Opcode}; for inst in instructions { @@ -179,6 +225,7 @@ struct CompileOptions { asm_dump: bool, dump_ir: bool, memory_map: bool, + call_graph: bool, use_ast: bool, } @@ -187,6 +234,7 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result, ()> { let asm_dump = opts.asm_dump; let dump_ir = opts.dump_ir; let memory_map = opts.memory_map; + let call_graph = opts.call_graph; let use_ast = opts.use_ast; let raw_source = std::fs::read_to_string(input).map_err(|e| { eprintln!("error: failed to read {}: {e}", input.display()); @@ -237,6 +285,10 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result, ()> { print_memory_map(&analysis); } + if call_graph { + print_call_graph(&analysis); + } + // Resolve sprite assets (CHR data + tile indices) relative to the // source file's directory, so `@binary` / `@chr` paths work naturally. let source_dir = input.parent().unwrap_or_else(|| Path::new("."));