From a45944e0f9822029e4262e80ad025f1875b48e72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 17:34:17 +0000 Subject: [PATCH] Language: raw asm { ... } blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit raw asm { LDA #\$42 STA \$00 } Unlike regular \`asm\`, \`raw asm\` does not perform \`{var}\` substitution — the body is passed to the inline parser verbatim. Useful for writing completely unmanaged bytes that don't rely on the analyzer's variable allocations, e.g. mapper init snippets. Implementation: - Parser: \`KwRaw\` followed by \`KwAsm\` emits \`Statement::RawAsm(body, span)\`. - IR lowering: prepends a \`\\0RAW\\0\` marker to the body when emitting \`IrOp::InlineAsm\` so the codegen can distinguish raw from regular without adding a second op variant. - IR codegen: strips the marker and skips substitution when present. - AST codegen: same, handling \`Statement::RawAsm\` directly. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3 --- src/analyzer/mod.rs | 3 ++- src/codegen/ir_codegen.rs | 29 ++++++++++++++++------------- src/codegen/mod.rs | 10 ++++++++++ src/ir/lowering.rs | 12 ++++++++++++ src/ir/mod.rs | 2 +- src/parser/ast.rs | 5 ++++- src/parser/mod.rs | 17 +++++++++++++++++ tests/integration_test.rs | 20 ++++++++++++++++++++ 8 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index b1dd1c5..b238ba9 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -1041,7 +1041,7 @@ impl Analyzer { self.walk_expr_reads(cond); self.check_expr_type(cond, &NesType::Bool); } - Statement::InlineAsm(_, _) => { + Statement::InlineAsm(_, _) | Statement::RawAsm(_, _) => { // Inline assembly is treated as an opaque block. The // codegen parses and validates the body; analysis has // nothing to check. @@ -1308,6 +1308,7 @@ fn collect_calls_stmt(stmt: &Statement, calls: &mut Vec) { | Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) | Statement::InlineAsm(_, _) + | Statement::RawAsm(_, _) | Statement::Play(_, _) | Statement::StartMusic(_, _) | Statement::StopMusic(_) => {} diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 202f1ab..ef97238 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -616,19 +616,22 @@ impl<'a> IrCodeGen<'a> { } } IrOp::InlineAsm(body) => { - // Preprocess `{var}` substitutions, then parse with - // the shared inline parser and splice the resulting - // instructions. Parse errors emit `BRK` so the ROM - // still links — codegen is too late to return - // user-facing diagnostics cleanly. - let substituted = substitute_asm_vars(body, |name| { - // Look up by allocated name. Prefer exact match. - self.allocations - .iter() - .find(|a| a.name == name) - .map(|a| a.address) - }); - match crate::asm::parse_inline(&substituted) { + // Preprocess `{var}` substitutions (unless this is a + // `raw asm` block, flagged by the lowering with a + // magic prefix), then parse with the shared inline + // parser and splice the resulting instructions. + let raw = body.strip_prefix(crate::ir::RAW_ASM_PREFIX); + let to_parse: std::borrow::Cow<'_, str> = if let Some(raw_body) = raw { + std::borrow::Cow::Borrowed(raw_body) + } else { + std::borrow::Cow::Owned(substitute_asm_vars(body, |name| { + self.allocations + .iter() + .find(|a| a.name == name) + .map(|a| a.address) + })) + }; + match crate::asm::parse_inline(&to_parse) { Ok(parsed) => self.instructions.extend(parsed), Err(msg) => { eprintln!("inline asm error: {msg}"); diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 6c9c342..af05194 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -413,6 +413,16 @@ impl CodeGen { } } } + Statement::RawAsm(body, _) => { + // Raw asm bypasses variable substitution. + match crate::asm::parse_inline(body) { + Ok(parsed) => self.instructions.extend(parsed), + Err(msg) => { + eprintln!("inline asm error: {msg}"); + self.emit(BRK, AM::Implied); + } + } + } Statement::Play(_, _) | Statement::StartMusic(_, _) | Statement::StopMusic(_) => { // Audio statements compile to no-ops for now. } diff --git a/src/ir/lowering.rs b/src/ir/lowering.rs index fdd196a..30a6a27 100644 --- a/src/ir/lowering.rs +++ b/src/ir/lowering.rs @@ -4,6 +4,11 @@ use super::*; use crate::analyzer::AnalysisResult; use crate::parser::ast::*; +/// Marker prefix the lowering prepends to the body of a `raw asm` +/// block, telling the codegen to skip `{var}` substitution. Uses +/// NUL characters so no normal source text can spoof it. +pub const RAW_ASM_PREFIX: &str = "\0RAW\0"; + /// Lower a parsed & analyzed program into IR. pub fn lower(program: &Program, analysis: &AnalysisResult) -> IrProgram { let mut ctx = LoweringContext::new(analysis); @@ -428,6 +433,13 @@ impl LoweringContext { Statement::InlineAsm(body, _) => { self.emit(IrOp::InlineAsm(body.clone())); } + Statement::RawAsm(body, _) => { + // Raw asm skips `{var}` substitution. We reuse the + // same IR op variant but mark the body with a magic + // prefix the codegen can detect — simpler than + // adding a separate IrOp. + self.emit(IrOp::InlineAsm(format!("{RAW_ASM_PREFIX}{body}"))); + } Statement::Play(_, _) | Statement::StartMusic(_, _) | Statement::StopMusic(_) => { // No audio driver yet — these parse but produce no // IR. diff --git a/src/ir/mod.rs b/src/ir/mod.rs index b99cfe4..d60f450 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -2,7 +2,7 @@ mod lowering; #[cfg(test)] mod tests; -pub use lowering::lower; +pub use lowering::{lower, RAW_ASM_PREFIX}; use crate::lexer::Span; use std::fmt; diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 8406bf9..66ff1b7 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -299,8 +299,10 @@ pub enum Statement { /// Stripped in release mode. DebugAssert(Expr, Span), /// Inline 6502 assembly captured verbatim. The body is parsed by - /// the codegen stage using `asm::parse_inline`. + /// the codegen stage using `asm::parse_inline`. `raw` variants + /// skip variable substitution for completely unmanaged bytes. InlineAsm(String, Span), + RawAsm(String, Span), /// Audio: `play SfxName` — trigger a one-shot sound effect. /// Currently a no-op at codegen time; no audio driver exists. Play(String, Span), @@ -334,6 +336,7 @@ impl Statement { | Self::DebugLog(_, s) | Self::DebugAssert(_, s) | Self::InlineAsm(_, s) + | Self::RawAsm(_, s) | Self::Play(_, s) | Self::StartMusic(_, s) | Self::StopMusic(s) => *s, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6294f96..2704f8b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -947,6 +947,23 @@ impl Parser { )) } } + TokenKind::KwRaw => { + // `raw asm { ... }` — verbatim bytes, no `{var}` + // substitution. + let span = self.current_span(); + self.advance(); // KwRaw + self.expect(&TokenKind::KwAsm)?; + if let TokenKind::AsmBody(body) = self.peek().clone() { + self.advance(); + Ok(Statement::RawAsm(body, span)) + } else { + Err(Diagnostic::error( + ErrorCode::E0201, + "expected `{` after `raw asm`", + self.current_span(), + )) + } + } TokenKind::Ident(_) => self.parse_assign_or_call(), _ => Err(Diagnostic::error( ErrorCode::E0201, diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2e2d7b8..4aa6621 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -322,6 +322,26 @@ fn program_with_enums() { rom::validate_ines(&rom_data).expect("should be valid iNES"); } +#[test] +fn program_with_raw_asm_block() { + // `raw asm` bypasses `{var}` substitution so the body is passed + // to the inline parser unchanged. + let source = r#" + game "RawAsm" { mapper: NROM } + var x: u8 = 0 + on frame { + raw asm { + LDA #$42 + STA $00 + } + wait_frame + } + start Main + "#; + let rom_data = compile(source); + rom::validate_ines(&rom_data).expect("should be valid iNES"); +} + #[test] fn program_with_inline_asm_variable_substitution() { let source = r#"