1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

Language: raw asm { ... } blocks

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
This commit is contained in:
Claude 2026-04-12 17:34:17 +00:00
parent a283f87b79
commit a45944e0f9
No known key found for this signature in database
8 changed files with 82 additions and 16 deletions

View file

@ -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<String>) {
| Statement::LoadBackground(_, _)
| Statement::SetPalette(_, _)
| Statement::InlineAsm(_, _)
| Statement::RawAsm(_, _)
| Statement::Play(_, _)
| Statement::StartMusic(_, _)
| Statement::StopMusic(_) => {}

View file

@ -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}");

View file

@ -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.
}

View file

@ -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.

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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#"