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:
parent
a283f87b79
commit
a45944e0f9
8 changed files with 82 additions and 16 deletions
|
|
@ -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(_) => {}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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#"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue