mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
ir/codegen: signed comparison lowering for i8/i16
Closes the §A follow-up gap: ordering compares (`<`, `<=`, `>`, `>=`) on signed integer types now use the canonical 6502 `CMP / SBC / BVC / EOR #$80` overflow-correction idiom so the N flag reflects the true sign of the difference, instead of the previous BCC/BCS-based path that always treated `$FFxx` as greater than `$00yy`. The same change also fixes narrow-to-wide widening: assigning a runtime `i8` expression to an `i16` variable now sign-extends the high byte via a new `IrOp::SignExtend` op instead of zero-extending it, so `var w: i16 = some_i8_neg` round-trips negative values. The lowerer tracks signedness on each IR temp (analogous to the existing `wide_hi` map) and threads it onto the new `Signedness` field of `CmpLt`/`CmpGt`/`CmpLtEq`/`CmpGtEq` and their 16-bit variants. The optimizer's constant-folder uses the same flag to fold compares correctly under either signedness. Casts to `u8`/`u16` strip the signed flag so an explicit `as` opt-out stays unsigned. `examples/signed_compare.ne` exercises both bit widths through the emulator harness — the four pip sprites at the top of the screen show three lit (signed-correct) and one dark (would only light if the compare regressed to unsigned semantics).
This commit is contained in:
parent
3719b6c0dd
commit
9719dc4111
11 changed files with 878 additions and 81 deletions
1
tests/emulator/goldens/signed_compare.audio.hash
Normal file
1
tests/emulator/goldens/signed_compare.audio.hash
Normal file
|
|
@ -0,0 +1 @@
|
|||
a82b6ff5 132084
|
||||
BIN
tests/emulator/goldens/signed_compare.png
Normal file
BIN
tests/emulator/goldens/signed_compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -3850,6 +3850,110 @@ fn i16_compiles_with_arithmetic_and_compare() {
|
|||
assert_eq!(info.mapper, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_i16_lt_emits_overflow_corrected_branch() {
|
||||
// `i16` ordering compare must emit the `BVC / EOR #$80`
|
||||
// overflow-correction idiom that flips the N flag when SBC
|
||||
// signals signed overflow. Without it, comparing a negative
|
||||
// i16 against a positive one falls back to an unsigned BCC
|
||||
// path that always treats $FFxx as greater than $00yy. This
|
||||
// is the behavioural piece of the §A-follow-up: any i16 `<`
|
||||
// must lower to a code sequence that can correctly answer
|
||||
// "is -1 < 1?".
|
||||
let source = r#"
|
||||
game "SignedCmp" { mapper: NROM }
|
||||
var a: i16 = -1
|
||||
var b: i16 = 1
|
||||
var hit: u8 = 0
|
||||
on frame { if a < b { hit = 1 } }
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
// Look for the overflow-correction byte triple `BVC + EOR
|
||||
// #$80`. EOR #$80 = `49 80`, and BVC has opcode `50` with a
|
||||
// single-byte signed offset; the offset value is layout-
|
||||
// dependent so we just check for the `50 ?? 49 80` shape
|
||||
// anywhere in PRG.
|
||||
let prg = &rom[16..16 + 16384];
|
||||
let has_idiom = prg
|
||||
.windows(4)
|
||||
.any(|w| w[0] == 0x50 && w[2] == 0x49 && w[3] == 0x80);
|
||||
assert!(
|
||||
has_idiom,
|
||||
"expected `BVC ?? / EOR #$80` overflow-correction idiom in PRG ROM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_i8_lt_emits_overflow_corrected_branch() {
|
||||
// Same check as above, but for the 8-bit signed compare.
|
||||
let source = r#"
|
||||
game "SignedCmp8" { mapper: NROM }
|
||||
var a: i8 = -1
|
||||
var b: i8 = 1
|
||||
var hit: u8 = 0
|
||||
on frame { if a < b { hit = 1 } }
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
let prg = &rom[16..16 + 16384];
|
||||
let has_idiom = prg
|
||||
.windows(4)
|
||||
.any(|w| w[0] == 0x50 && w[2] == 0x49 && w[3] == 0x80);
|
||||
assert!(
|
||||
has_idiom,
|
||||
"expected `BVC ?? / EOR #$80` overflow-correction idiom in PRG ROM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsigned_u16_lt_does_not_emit_signed_idiom() {
|
||||
// u16 `<` should keep using the cheaper BCC-based unsigned
|
||||
// path. If the lowering accidentally promoted everything to
|
||||
// the signed path we'd waste two bytes per compare; the
|
||||
// overflow-correction idiom must NOT appear.
|
||||
let source = r#"
|
||||
game "UnsignedCmp" { mapper: NROM }
|
||||
var a: u16 = 0
|
||||
var b: u16 = 1
|
||||
var hit: u8 = 0
|
||||
on frame { if a < b { hit = 1 } }
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
let prg = &rom[16..16 + 16384];
|
||||
let has_idiom = prg
|
||||
.windows(4)
|
||||
.any(|w| w[0] == 0x50 && w[2] == 0x49 && w[3] == 0x80);
|
||||
assert!(
|
||||
!has_idiom,
|
||||
"u16 compare should stay unsigned; found `BVC / EOR #$80` idiom"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_signed_against_zero_is_negative_check() {
|
||||
// Negative analyzer test isn't applicable here (the analyzer
|
||||
// already accepts negative literals against i8/i16), but we
|
||||
// can still guard against the silent-drop shape: an i8
|
||||
// compare against zero should at minimum emit a CMP and a
|
||||
// signed-overflow guard. This is a smoke-level sibling of
|
||||
// the existing i16-arithmetic integration test.
|
||||
let source = r#"
|
||||
game "SignedZero" { mapper: NROM }
|
||||
var a: i8 = -5
|
||||
var hit: u8 = 0
|
||||
on frame { if a < 0 { hit = 1 } }
|
||||
start Main
|
||||
"#;
|
||||
let rom = compile(source);
|
||||
let info = rom::validate_ines(&rom).expect("should be valid iNES");
|
||||
assert_eq!(info.mapper, 0);
|
||||
let prg = &rom[16..16 + 16384];
|
||||
let has_sbc = prg.iter().any(|&b| b == 0xE9 || b == 0xE5 || b == 0xED);
|
||||
assert!(has_sbc, "signed compare should emit at least one SBC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fade_out_emits_jsr_and_forces_palette_bright() {
|
||||
// `fade_out(n)` should emit a JSR to `__fade_out` and also
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue