mirror of
https://github.com/imjasonh/nescript
synced 2026-07-18 06:36:57 +00:00
Add debug.log and debug.assert statements
Parser: - debug.log(expr, ...) and debug.assert(cond) syntax - parse_debug_statement() handles both methods - New AST variants: Statement::DebugLog and Statement::DebugAssert Codegen: - CodeGen::with_debug(bool) to toggle debug instrumentation - DebugLog writes each expression to $4800 (Mesen debug port) - DebugAssert evaluates condition; on false, writes $FF to $4800 + BRK - Both are stripped entirely when debug_mode = false (release builds) CLI: - --debug flag now wired through to CodeGen - Without --debug, all debug statements produce zero bytes Analyzer: - Type-checks DebugAssert condition as bool - Walks DebugLog args for reads (used_vars tracking) IR lowering: - DebugLog/DebugAssert are no-ops (codegen handles them directly) 254 tests (5 new: 3 parser + 2 codegen). https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
6efef6c4c5
commit
45b2b2a279
8 changed files with 199 additions and 3 deletions
|
|
@ -606,6 +606,15 @@ impl Analyzer {
|
||||||
| Statement::Return(None, _)
|
| Statement::Return(None, _)
|
||||||
| Statement::LoadBackground(_, _)
|
| Statement::LoadBackground(_, _)
|
||||||
| Statement::SetPalette(_, _) => {}
|
| Statement::SetPalette(_, _) => {}
|
||||||
|
Statement::DebugLog(args, _) => {
|
||||||
|
for arg in args {
|
||||||
|
self.walk_expr_reads(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Statement::DebugAssert(cond, _) => {
|
||||||
|
self.walk_expr_reads(cond);
|
||||||
|
self.check_expr_type(cond, &NesType::Bool);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -804,6 +813,14 @@ fn collect_calls_stmt(stmt: &Statement, calls: &mut Vec<String>) {
|
||||||
collect_calls_expr(x, calls);
|
collect_calls_expr(x, calls);
|
||||||
collect_calls_expr(y, calls);
|
collect_calls_expr(y, calls);
|
||||||
}
|
}
|
||||||
|
Statement::DebugLog(args, _) => {
|
||||||
|
for arg in args {
|
||||||
|
collect_calls_expr(arg, calls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Statement::DebugAssert(cond, _) => {
|
||||||
|
collect_calls_expr(cond, calls);
|
||||||
|
}
|
||||||
Statement::Return(None, _)
|
Statement::Return(None, _)
|
||||||
| Statement::Transition(_, _)
|
| Statement::Transition(_, _)
|
||||||
| Statement::WaitFrame(_)
|
| Statement::WaitFrame(_)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ pub const ZP_CURRENT_STATE: u8 = 0x03;
|
||||||
/// Zero-page addresses for function call parameter passing ($04-$07, up to 4 params).
|
/// Zero-page addresses for function call parameter passing ($04-$07, up to 4 params).
|
||||||
pub const ZP_PARAM_BASE: u8 = 0x04;
|
pub const ZP_PARAM_BASE: u8 = 0x04;
|
||||||
|
|
||||||
|
/// Debug output port address used by Mesen and some other emulators.
|
||||||
|
pub const DEBUG_PORT: u16 = 0x4800;
|
||||||
|
|
||||||
/// Code generator: translates AST directly to 6502 instructions.
|
/// Code generator: translates AST directly to 6502 instructions.
|
||||||
/// For Milestone 1, we skip the IR and go AST → 6502 directly.
|
/// For Milestone 1, we skip the IR and go AST → 6502 directly.
|
||||||
pub struct CodeGen {
|
pub struct CodeGen {
|
||||||
|
|
@ -20,6 +23,9 @@ pub struct CodeGen {
|
||||||
var_addrs: HashMap<String, u16>,
|
var_addrs: HashMap<String, u16>,
|
||||||
const_values: HashMap<String, u16>,
|
const_values: HashMap<String, u16>,
|
||||||
label_counter: u32,
|
label_counter: u32,
|
||||||
|
/// When true, debug.log/assert statements emit runtime code.
|
||||||
|
/// When false, they are stripped entirely.
|
||||||
|
debug_mode: bool,
|
||||||
/// Address of the NMI-signaled "frame ready" flag in zero page
|
/// Address of the NMI-signaled "frame ready" flag in zero page
|
||||||
pub frame_flag_addr: u8,
|
pub frame_flag_addr: u8,
|
||||||
/// Address of controller state byte in zero page
|
/// Address of controller state byte in zero page
|
||||||
|
|
@ -53,6 +59,7 @@ impl CodeGen {
|
||||||
var_addrs,
|
var_addrs,
|
||||||
const_values,
|
const_values,
|
||||||
label_counter: 0,
|
label_counter: 0,
|
||||||
|
debug_mode: false,
|
||||||
frame_flag_addr: 0x00,
|
frame_flag_addr: 0x00,
|
||||||
input_addr: 0x01,
|
input_addr: 0x01,
|
||||||
state_indices: HashMap::new(),
|
state_indices: HashMap::new(),
|
||||||
|
|
@ -62,6 +69,14 @@ impl CodeGen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable debug mode: debug.log/debug.assert statements will emit runtime code.
|
||||||
|
/// When disabled (the default), debug statements are stripped.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_debug(mut self, enabled: bool) -> Self {
|
||||||
|
self.debug_mode = enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Register sprite-to-tile-index mappings so that `draw SpriteName` can
|
/// Register sprite-to-tile-index mappings so that `draw SpriteName` can
|
||||||
/// emit the correct CHR tile index instead of defaulting to 0.
|
/// emit the correct CHR tile index instead of defaulting to 0.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -306,6 +321,29 @@ impl CodeGen {
|
||||||
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
||||||
// TODO: implement in asset pipeline
|
// TODO: implement in asset pipeline
|
||||||
}
|
}
|
||||||
|
Statement::DebugLog(args, _) => {
|
||||||
|
if self.debug_mode {
|
||||||
|
for arg in args {
|
||||||
|
self.gen_expr(arg);
|
||||||
|
// Write A to debug port $4800
|
||||||
|
self.emit(STA, AM::Absolute(DEBUG_PORT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In release mode, stripped entirely
|
||||||
|
}
|
||||||
|
Statement::DebugAssert(cond, _) => {
|
||||||
|
if self.debug_mode {
|
||||||
|
// Evaluate condition; if zero (false), halt
|
||||||
|
self.gen_condition(cond);
|
||||||
|
let pass_label = self.fresh_label("assert_pass");
|
||||||
|
self.emit(BNE, AM::LabelRelative(pass_label.clone()));
|
||||||
|
// Assertion failed: write marker to debug port and BRK
|
||||||
|
self.emit(LDA, AM::Immediate(0xFF));
|
||||||
|
self.emit(STA, AM::Absolute(DEBUG_PORT));
|
||||||
|
self.emit(BRK, AM::Implied);
|
||||||
|
self.emit_label(&pass_label);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,3 +232,47 @@ fn codegen_shift_left() {
|
||||||
"shift left should generate ASL A"
|
"shift left should generate ASL A"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_debug_log_stripped_in_release() {
|
||||||
|
// Without --debug, debug.log should not emit any $4800 writes
|
||||||
|
let src = r#"
|
||||||
|
game "T" { mapper: NROM }
|
||||||
|
var x: u8 = 42
|
||||||
|
on frame {
|
||||||
|
debug.log(x)
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let insts = compile_to_instructions(src);
|
||||||
|
// Should NOT write to $4800
|
||||||
|
let has_debug = insts
|
||||||
|
.iter()
|
||||||
|
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
|
||||||
|
assert!(!has_debug, "debug.log should be stripped in release mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_debug_log_emits_in_debug_mode() {
|
||||||
|
use crate::analyzer;
|
||||||
|
use crate::parser;
|
||||||
|
|
||||||
|
let src = r#"
|
||||||
|
game "T" { mapper: NROM }
|
||||||
|
var x: u8 = 42
|
||||||
|
on frame {
|
||||||
|
debug.log(x)
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let (prog, _) = parser::parse(src);
|
||||||
|
let prog = prog.unwrap();
|
||||||
|
let analysis = analyzer::analyze(&prog);
|
||||||
|
let codegen = CodeGen::new(&analysis.var_allocations, &prog.constants).with_debug(true);
|
||||||
|
let insts = codegen.generate(&prog);
|
||||||
|
// Should write to $4800 at least once
|
||||||
|
let has_debug = insts
|
||||||
|
.iter()
|
||||||
|
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
|
||||||
|
assert!(has_debug, "debug.log should write to $4800 in debug mode");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,11 @@ impl LoweringContext {
|
||||||
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
||||||
// TODO: implement in asset pipeline
|
// TODO: implement in asset pipeline
|
||||||
}
|
}
|
||||||
|
Statement::DebugLog(_, _) | Statement::DebugAssert(_, _) => {
|
||||||
|
// Debug statements don't produce IR ops in release mode.
|
||||||
|
// In debug mode, the AST-based codegen handles them directly
|
||||||
|
// (IR codegen path for debug is a future enhancement).
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ fn dump_asm(instructions: &[nescript::asm::Instruction]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile(input: &PathBuf, _debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()> {
|
fn compile(input: &PathBuf, debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()> {
|
||||||
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
||||||
eprintln!("error: failed to read {}: {e}", input.display());
|
eprintln!("error: failed to read {}: {e}", input.display());
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -130,8 +130,9 @@ fn compile(input: &PathBuf, _debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()>
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Code generation (still AST-based for M2; IR codegen comes in M3)
|
// Code generation (still AST-based for M2; IR codegen comes in M3)
|
||||||
let codegen =
|
let codegen = CodeGen::new(&analysis.var_allocations, &program.constants)
|
||||||
CodeGen::new(&analysis.var_allocations, &program.constants).with_sprites(&sprites);
|
.with_sprites(&sprites)
|
||||||
|
.with_debug(debug);
|
||||||
let instructions = codegen.generate(&program);
|
let instructions = codegen.generate(&program);
|
||||||
|
|
||||||
if asm_dump {
|
if asm_dump {
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,12 @@ pub enum Statement {
|
||||||
LoadBackground(String, Span),
|
LoadBackground(String, Span),
|
||||||
SetPalette(String, Span),
|
SetPalette(String, Span),
|
||||||
Scroll(Expr, Expr, Span),
|
Scroll(Expr, Expr, Span),
|
||||||
|
/// debug.log(expr, ...) — writes values to the emulator debug port.
|
||||||
|
/// Stripped in release mode.
|
||||||
|
DebugLog(Vec<Expr>, Span),
|
||||||
|
/// debug.assert(cond) — runtime check, halts on failure.
|
||||||
|
/// Stripped in release mode.
|
||||||
|
DebugAssert(Expr, Span),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -787,6 +787,7 @@ impl Parser {
|
||||||
self.expect(&TokenKind::RParen)?;
|
self.expect(&TokenKind::RParen)?;
|
||||||
Ok(Statement::Scroll(x, y, span))
|
Ok(Statement::Scroll(x, y, span))
|
||||||
}
|
}
|
||||||
|
TokenKind::KwDebug => self.parse_debug_statement(),
|
||||||
TokenKind::Ident(_) => self.parse_assign_or_call(),
|
TokenKind::Ident(_) => self.parse_assign_or_call(),
|
||||||
_ => Err(Diagnostic::error(
|
_ => Err(Diagnostic::error(
|
||||||
ErrorCode::E0201,
|
ErrorCode::E0201,
|
||||||
|
|
@ -891,6 +892,38 @@ impl Parser {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse debug.log(...) or debug.assert(...)
|
||||||
|
fn parse_debug_statement(&mut self) -> Result<Statement, Diagnostic> {
|
||||||
|
let start = self.current_span();
|
||||||
|
self.expect(&TokenKind::KwDebug)?;
|
||||||
|
self.expect(&TokenKind::Dot)?;
|
||||||
|
let (method, _) = self.expect_ident()?;
|
||||||
|
self.expect(&TokenKind::LParen)?;
|
||||||
|
match method.as_str() {
|
||||||
|
"log" => {
|
||||||
|
let mut args = Vec::new();
|
||||||
|
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
|
||||||
|
args.push(self.parse_expr()?);
|
||||||
|
if *self.peek() == TokenKind::Comma {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.expect(&TokenKind::RParen)?;
|
||||||
|
Ok(Statement::DebugLog(args, start))
|
||||||
|
}
|
||||||
|
"assert" => {
|
||||||
|
let cond = self.parse_expr()?;
|
||||||
|
self.expect(&TokenKind::RParen)?;
|
||||||
|
Ok(Statement::DebugAssert(cond, start))
|
||||||
|
}
|
||||||
|
_ => Err(Diagnostic::error(
|
||||||
|
ErrorCode::E0201,
|
||||||
|
format!("unknown debug method '{method}' (expected 'log' or 'assert')"),
|
||||||
|
start,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_assign_or_call(&mut self) -> Result<Statement, Diagnostic> {
|
fn parse_assign_or_call(&mut self) -> Result<Statement, Diagnostic> {
|
||||||
let start = self.current_span();
|
let start = self.current_span();
|
||||||
let (name, _) = self.expect_ident()?;
|
let (name, _) = self.expect_ident()?;
|
||||||
|
|
|
||||||
|
|
@ -999,3 +999,55 @@ fn parse_shift_assign_operators() {
|
||||||
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
||||||
assert_eq!(frame.statements.len(), 2);
|
assert_eq!(frame.statements.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_debug_log() {
|
||||||
|
let src = r#"
|
||||||
|
game "T" { mapper: NROM }
|
||||||
|
var x: u8 = 0
|
||||||
|
on frame {
|
||||||
|
debug.log(x)
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let prog = parse_ok(src);
|
||||||
|
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
||||||
|
match &frame.statements[0] {
|
||||||
|
Statement::DebugLog(args, _) => assert_eq!(args.len(), 1),
|
||||||
|
other => panic!("expected DebugLog, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_debug_log_multiple_args() {
|
||||||
|
let src = r#"
|
||||||
|
game "T" { mapper: NROM }
|
||||||
|
var x: u8 = 0
|
||||||
|
var y: u8 = 0
|
||||||
|
on frame {
|
||||||
|
debug.log(x, y, 42)
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let prog = parse_ok(src);
|
||||||
|
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
||||||
|
match &frame.statements[0] {
|
||||||
|
Statement::DebugLog(args, _) => assert_eq!(args.len(), 3),
|
||||||
|
other => panic!("expected DebugLog, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_debug_assert() {
|
||||||
|
let src = r#"
|
||||||
|
game "T" { mapper: NROM }
|
||||||
|
var x: u8 = 0
|
||||||
|
on frame {
|
||||||
|
debug.assert(x == 0)
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let prog = parse_ok(src);
|
||||||
|
let frame = prog.states[0].on_frame.as_ref().unwrap();
|
||||||
|
assert!(matches!(frame.statements[0], Statement::DebugAssert(..)));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue