mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
Inline asm: {var} placeholder substitution
Within \`asm { ... }\` blocks, \`{name}\` is replaced with the
resolved hex address of the variable at codegen time. The lexer's
asm-body capture now balances nested braces so it doesn't cut off
at the first \`{x}\`. Both IR and AST codegen paths preprocess the
body before passing to the inline parser:
var counter: u8 = 0
on frame {
asm {
LDA {counter}
CLC
ADC #\$01
STA {counter}
}
}
Zero-page addresses become \`\$XX\`, absolute addresses become
\`\$XXXX\`. Unknown names pass through unchanged so the asm parser
can surface the "unknown mnemonic" / "unexpected token" error.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
f95130346a
commit
a283f87b79
5 changed files with 176 additions and 16 deletions
|
|
@ -909,6 +909,44 @@ error[E0402]: recursion is not allowed
|
|||
|
||||
### Build
|
||||
|
||||
## Inline Assembly
|
||||
|
||||
`asm { ... }` blocks contain raw 6502 assembly that the compiler
|
||||
parses and splices directly into the output:
|
||||
|
||||
```
|
||||
fun fast_add() -> u8 {
|
||||
var x: u8 = 5
|
||||
var y: u8 = 3
|
||||
asm {
|
||||
LDA {x}
|
||||
CLC
|
||||
ADC {y}
|
||||
STA {x}
|
||||
}
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
Within an asm block, `{name}` is replaced with the resolved
|
||||
zero-page or absolute address of the variable `name`. This lets
|
||||
handwritten assembly reference NEScript variables without knowing
|
||||
where the analyzer allocated them.
|
||||
|
||||
Supported addressing modes (mirroring the generated codegen):
|
||||
|
||||
- `LDA #$10` / `LDA #42` — immediate
|
||||
- `LDA $10` / `STA $20,X` — zero-page (+ indexed)
|
||||
- `LDA $2000` / `LDA $0200,Y` — absolute (+ indexed)
|
||||
- `LDA ($10,X)` / `LDA ($10),Y` — indirect-X / indirect-Y
|
||||
- `JMP ($FFFC)` — indirect
|
||||
- `CLC`, `SEC`, `NOP`, ... — implied
|
||||
- `LSR A` — accumulator
|
||||
- Labels: `loop_start:` defines a label; `BNE loop_start` branches
|
||||
to it. Labels are local to the surrounding asm block.
|
||||
|
||||
## Command Line
|
||||
|
||||
Compile a `.ne` source file into a `.nes` ROM:
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ pub struct IrCodeGen<'a> {
|
|||
/// When true, emit code for `debug.log` / `debug.assert`.
|
||||
/// When false, these ops are stripped entirely.
|
||||
debug_mode: bool,
|
||||
_allocations: &'a [VarAllocation],
|
||||
allocations: &'a [VarAllocation],
|
||||
}
|
||||
|
||||
impl<'a> IrCodeGen<'a> {
|
||||
|
|
@ -115,7 +115,7 @@ impl<'a> IrCodeGen<'a> {
|
|||
next_oam_slot: 0,
|
||||
in_frame_handler: false,
|
||||
debug_mode: false,
|
||||
_allocations: allocations,
|
||||
allocations,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -616,12 +616,19 @@ impl<'a> IrCodeGen<'a> {
|
|||
}
|
||||
}
|
||||
IrOp::InlineAsm(body) => {
|
||||
// Parse the asm body with the shared inline parser and
|
||||
// splice the resulting instructions directly into our
|
||||
// output stream. Parse errors are emitted as `BRK` so
|
||||
// the ROM still links — codegen is too late to return
|
||||
// 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.
|
||||
match crate::asm::parse_inline(body) {
|
||||
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) {
|
||||
Ok(parsed) => self.instructions.extend(parsed),
|
||||
Err(msg) => {
|
||||
eprintln!("inline asm error: {msg}");
|
||||
|
|
@ -710,6 +717,52 @@ enum CmpKind {
|
|||
GtEq,
|
||||
}
|
||||
|
||||
/// Replace `{name}` tokens in an inline-asm body with the resolved
|
||||
/// hex address from the given resolver. Unknown names and malformed
|
||||
/// placeholders are passed through unchanged (the asm parser will
|
||||
/// surface a clearer error than we could at this stage).
|
||||
///
|
||||
/// Addresses that fit in a byte become zero-page syntax (`$XX`),
|
||||
/// otherwise absolute (`$XXXX`). This lets users write
|
||||
/// `lda {score}` and have it resolve to `lda $10` or similar.
|
||||
fn substitute_asm_vars<F: Fn(&str) -> Option<u16>>(body: &str, resolver: F) -> String {
|
||||
let mut out = String::with_capacity(body.len());
|
||||
let bytes = body.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'{' {
|
||||
// Find the closing `}`.
|
||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
||||
let name_start = i + 1;
|
||||
let name_end = i + 1 + end;
|
||||
let name = &body[name_start..name_end];
|
||||
// Only substitute bare identifiers.
|
||||
let is_ident = !name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
|
||||
&& name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric());
|
||||
if is_ident {
|
||||
if let Some(addr) = resolver(name) {
|
||||
use std::fmt::Write;
|
||||
if addr < 0x100 {
|
||||
let _ = write!(out, "${addr:02X}");
|
||||
} else {
|
||||
let _ = write!(out, "${addr:04X}");
|
||||
}
|
||||
i = name_end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Scan the IR functions for all scanline handlers (named
|
||||
/// `{state}_scanline_{line}`) and return them in IR function order.
|
||||
fn collect_scanline_handlers(ir: &IrProgram) -> Vec<(String, u8)> {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,40 @@ impl CodeGen {
|
|||
self
|
||||
}
|
||||
|
||||
/// Replace `{name}` placeholders in an inline-asm body with the
|
||||
/// resolved zero-page or absolute hex address. Unknown names
|
||||
/// pass through unchanged so the asm parser can surface a clear
|
||||
/// error.
|
||||
fn substitute_asm_vars(&self, body: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut out = String::with_capacity(body.len());
|
||||
let bytes = body.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'{' {
|
||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
||||
let name_start = i + 1;
|
||||
let name_end = i + 1 + end;
|
||||
let name = &body[name_start..name_end];
|
||||
if !name.is_empty() {
|
||||
if let Some(&addr) = self.var_addrs.get(name) {
|
||||
if addr < 0x100 {
|
||||
let _ = write!(out, "${addr:02X}");
|
||||
} else {
|
||||
let _ = write!(out, "${addr:04X}");
|
||||
}
|
||||
i = name_end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn fresh_label(&mut self, prefix: &str) -> String {
|
||||
self.label_counter += 1;
|
||||
format!("__{prefix}_{}", self.label_counter)
|
||||
|
|
@ -369,13 +403,16 @@ impl CodeGen {
|
|||
self.emit_label(&pass_label);
|
||||
}
|
||||
}
|
||||
Statement::InlineAsm(body, _) => 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::InlineAsm(body, _) => {
|
||||
let substituted = self.substitute_asm_vars(body);
|
||||
match crate::asm::parse_inline(&substituted) {
|
||||
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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -498,11 +498,23 @@ impl<'a> Lexer<'a> {
|
|||
|
||||
impl Lexer<'_> {
|
||||
/// Capture everything between the `{` we just consumed and the
|
||||
/// matching `}` as an `AsmBody` token. 6502 assembly has no
|
||||
/// `{`/`}` in its syntax, so we can simply scan for the next `}`.
|
||||
/// matching `}` as an `AsmBody` token. Nested braces (e.g.
|
||||
/// `{variable}` substitution placeholders) are balanced by
|
||||
/// depth counting, so the body isn't cut short.
|
||||
fn capture_asm_body(&mut self, start: usize) -> Token {
|
||||
let body_start = self.pos;
|
||||
while self.pos < self.source.len() && self.source[self.pos] != b'}' {
|
||||
let mut depth = 1u32;
|
||||
while self.pos < self.source.len() && depth > 0 {
|
||||
match self.source[self.pos] {
|
||||
b'{' => depth += 1,
|
||||
b'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
let body = String::from_utf8_lossy(&self.source[body_start..self.pos]).into_owned();
|
||||
|
|
|
|||
|
|
@ -322,6 +322,26 @@ fn program_with_enums() {
|
|||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_with_inline_asm_variable_substitution() {
|
||||
let source = r#"
|
||||
game "AsmVar" { mapper: NROM }
|
||||
var counter: u8 = 0
|
||||
on frame {
|
||||
asm {
|
||||
LDA {counter}
|
||||
CLC
|
||||
ADC #$01
|
||||
STA {counter}
|
||||
}
|
||||
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() {
|
||||
let source = r#"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue