fix(cpu): support single-load writeback register aliases

Allow ARM LDR/LDRB/LDRH/LDRSB/LDRSH to share non-PC base and destination registers with writeback. Keep the loaded value after computing addresses from incoming operands. Preserve PC restrictions, unsupported user transfers, store behavior, and atomic memory diagnostics.

Add original regressions for index modes, offsets, widths, alignment, sign extension, all active banks, timing, I/O/IRQ ordering, and failures. The unchanged pinned public ARM ROM progresses past tests 360 and 361, then fails at test 362 on an unmapped 0x80000000 read; no full-suite pass is claimed.

Verified on Darwin arm64: debug/release workspace and core suites, Python adapter tests, Clippy, rustdoc, native ROM-window checks, and all ten demo smoke tests. Document the new behavior and next memory-access blocker.
This commit is contained in:
0xrsydn 2026-10-05 20:15:18 +07:00
commit ab625dc55d
11 changed files with 367 additions and 12 deletions

View file

@ -20,6 +20,8 @@ mod exception_tests;
#[cfg(test)]
mod instruction_tests;
#[cfg(test)]
mod load_alias_tests;
#[cfg(test)]
mod status_tests;
#[cfg(test)]
mod tests;

View file

@ -142,7 +142,7 @@ impl Cpu {
let byte = instruction & (1 << 22) != 0;
let load = instruction & (1 << 20) != 0;
if (!pre && write_bit) // LDRT/STRT access semantics are not implemented.
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|| (write_back && base_register == 15)
|| (byte && destination == 15)
{
return Err(self.unsupported(instruction));
@ -186,6 +186,8 @@ impl Cpu {
if write_back {
self.registers[base_register] = adjusted;
}
// ARM7 load aliases keep the loaded value, not the updated base. All
// operands and the effective address were captured before either write.
if load && destination != 15 {
self.registers[destination] = value;
}
@ -210,7 +212,7 @@ impl Cpu {
|| (!load && kind != 1)
|| (!pre && write_bit)
|| destination == 15
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|| (write_back && base_register == 15)
{
return Err(self.unsupported(instruction));
}
@ -240,6 +242,7 @@ impl Cpu {
if write_back {
self.registers[base_register] = adjusted;
}
// Halfword and signed load aliases also let the loaded value win.
if load {
self.registers[destination] = value;
}

View file

@ -504,7 +504,6 @@ fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
for instruction in [
0xe1a0_0f11, // Rs=pc (register shift)
0xe3b0_f001, // status restore via MOVS pc
0xe490_0004, // LDR r0,[r0],#4 (base/destination overlap)
0xe5bf_1000, // LDR with PC writeback
0xe4b0_1004, // LDRT user-mode transfer
0xe5d0_f000, // LDRB pc

View file

@ -0,0 +1,319 @@
//! Original regressions for ARM7 single-load base/destination writeback aliases.
use super::*;
use crate::{
io::{IE, IF, IME, TIMER_BASE},
machine::{Machine, MachineError, StepKind},
memory::ROM_START,
timing::{bus_cycles, AccessKind, AccessWidth},
};
const RAM: u32 = 0x0200_0100;
const DATA: u32 = 0xf233_80a5;
#[derive(Clone, Copy, Debug)]
enum Kind {
Word,
Byte,
Half,
SignedByte,
SignedHalf,
}
const KINDS: [Kind; 5] = [
Kind::Word,
Kind::Byte,
Kind::Half,
Kind::SignedByte,
Kind::SignedHalf,
];
#[derive(Clone, Copy, Debug)]
enum Index {
Offset,
Pre,
Post,
}
fn load(kind: Kind, index: Index, up: bool, register_offset: bool, register: u32) -> u32 {
let p = u32::from(!matches!(index, Index::Post)) << 24;
let w = u32::from(matches!(index, Index::Pre)) << 21;
let common = 0xe010_0000 | p | w | u32::from(up) << 23 | register << 16 | register << 12;
match kind {
Kind::Word | Kind::Byte => {
common
| 0x0400_0000
| u32::from(matches!(kind, Kind::Byte)) << 22
| if register_offset { 0x0200_0101 } else { 4 }
} // r1 LSL #2, or #4
_ => {
common
| 0x90
| match kind {
Kind::Half => 0x20,
Kind::SignedByte => 0x40,
_ => 0x60,
}
| if register_offset { 1 } else { 0x0040_0004 }
} // r1, or split immediate #4
}
}
fn memory(instruction: u32) -> Memory {
let mut memory = Memory::new(instruction.to_le_bytes().to_vec()).unwrap();
memory.write32(RAM, DATA).unwrap();
memory
}
// Independent byte-lane expectations for [a5,80,33,f2], including odd ARM7 loads.
fn expected(kind: Kind, low: usize) -> u32 {
(match kind {
Kind::Word => [0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2],
Kind::Byte => [0xa5, 0x80, 0x33, 0xf2],
Kind::Half => [0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2],
Kind::SignedByte => [0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2],
Kind::SignedHalf => [0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2],
})[low]
}
#[test]
fn load_aliases_keep_loaded_values_for_all_widths_offsets_and_index_modes() {
for kind in KINDS {
for index in [Index::Offset, Index::Pre, Index::Post] {
for up in [false, true] {
for register_offset in [false, true] {
for low in 0..4 {
let address = RAM + low as u32;
let base = match index {
Index::Post => address,
_ if up => address - 4,
_ => address + 4,
};
let mut bus = memory(load(kind, index, up, register_offset, 0));
let mut cpu = Cpu::new(ROM_START);
cpu.apply_status(0xf000_00df, Mode::System);
cpu.registers[0] = base;
cpu.registers[1] = if matches!(kind, Kind::Word | Kind::Byte) {
1
} else {
4
};
let mut after = cpu.clone();
after.registers[0] = expected(kind, low);
after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}");
assert_eq!(bus.read32(RAM).unwrap(), DATA);
assert_eq!(bus.cycles(), 0);
assert_eq!(
timing.code_cycles,
bus_cycles(0, ROM_START, AccessWidth::Word, AccessKind::Sequential)
);
assert_eq!(
timing.data_cycles,
if matches!(kind, Kind::Word) { 6 } else { 3 }
);
assert_eq!(timing.internal_cycles, 1);
}
}
}
}
}
}
#[test]
fn load_aliases_modify_only_the_active_register_in_every_cpu_bank() {
let modes = [
Mode::User,
Mode::System,
Mode::Fiq,
Mode::Irq,
Mode::Supervisor,
Mode::Abort,
Mode::Undefined,
];
for kind in KINDS {
for mode in modes {
for register in 0..15 {
let mut cpu = Cpu::new(ROM_START);
for bank in modes {
cpu.switch_mode(bank);
for r in 8..15 {
cpu.registers[r] = 0x1000 * bank as u32 + r as u32;
}
}
cpu.apply_status(0x9000_00c0 | mode as u32, mode);
cpu.registers[register] = RAM - 4;
let mut after = cpu.clone();
after.registers[register] = expected(kind, 0);
after.registers[15] += 4;
cpu.step(&mut memory(load(
kind,
Index::Pre,
true,
false,
register as u32,
)))
.unwrap();
assert_eq!(cpu, after, "{kind:?} {mode:?} r{register}");
}
}
}
}
#[test]
fn register_offsets_use_incoming_values_and_wrapping_address_arithmetic() {
for kind in KINDS {
let mut instruction = load(kind, Index::Pre, true, true, 0);
if matches!(kind, Kind::Word | Kind::Byte) {
instruction &= !0x100;
} // unshifted r1
let mut bus = memory(instruction);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0xffff_fffc;
cpu.registers[1] = RAM + 4;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected(kind, 0));
assert_eq!(cpu.registers[1], RAM + 4);
}
// All three registers alias: address uses old r0 + (old r0 LSR #24).
let mut bus = memory(0xe7b0_0c20);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected(Kind::Word, 2));
}
#[test]
fn post_index_does_not_read_or_validate_the_discarded_writeback_address() {
for kind in KINDS {
let mut instruction = load(kind, Index::Post, true, true, 0);
if matches!(kind, Kind::Word | Kind::Byte) {
instruction &= !0x100;
}
let mut bus = memory(instruction);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
cpu.registers[1] = 0x7e00_0000; // adjusted address is unmapped 0x80000100
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], expected(kind, 0));
}
}
#[test]
fn failed_alias_reads_preserve_register_banks_device_state_and_clock() {
for kind in KINDS {
for index in [Index::Pre, Index::Post] {
let mut bus = memory(load(kind, index, true, false, 0));
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.apply_status(0x9000_00d1, Mode::Fiq);
cpu.registers[0] = 0x0100_0000;
let before = cpu.clone();
let display = bus.display_position();
let counter = bus.read16(TIMER_BASE).unwrap();
let mut machine = Machine::new(cpu, bus);
let address = if matches!(index, Index::Pre) {
0x0100_0004
} else {
0x0100_0000
};
assert_eq!(
machine.step(),
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
address
))))
);
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.last_timing().total(), 0);
assert_eq!(machine.memory().display_position(), display);
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter);
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
assert_eq!(machine.memory().read32(RAM).unwrap(), DATA);
}
}
}
#[test]
fn alias_io_load_reads_before_device_progress_and_irq_delivery() {
let mut bus = memory(0xe5b0_0004); // LDR r0,[r0,#4]!
bus.write32(TIMER_BASE, 0x00c0_fffa).unwrap();
bus.write16(IE, 8).unwrap();
bus.write16(IME, 1).unwrap();
let expected = bus.read32(TIMER_BASE).unwrap();
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = TIMER_BASE - 4;
let mut machine = Machine::new(cpu, bus);
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.cpu().registers()[0], expected);
assert_eq!(machine.cpu().pc(), ROM_START + 4);
assert_eq!(machine.cycles(), 10); // ROM boundary: 8 code + 1 I/O data + 1 internal
assert_ne!(machine.memory().read32(TIMER_BASE).unwrap(), expected);
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
assert_eq!(machine.cpu().registers()[0], expected);
assert_eq!(machine.cpu().pc(), 0x18);
}
#[test]
fn skipped_alias_loads_do_not_access_memory_or_apply_writeback() {
for kind in KINDS {
for index in [Index::Pre, Index::Post] {
let mut bus = memory(load(kind, index, true, false, 0) & 0x0fff_ffff); // EQ, Z clear
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = 0x0100_0000;
let mut after = cpu.clone();
after.registers[15] += 4;
let timing = cpu.step_timed(&mut bus).unwrap();
assert_eq!(cpu, after);
assert_eq!(timing.data_cycles, 0);
assert_eq!(timing.internal_cycles, 0);
}
}
}
#[test]
fn pc_writeback_and_user_transfer_encodings_remain_diagnostics() {
for kind in KINDS {
for instruction in [
load(kind, Index::Pre, true, false, 15),
load(kind, Index::Post, true, false, 0) | 1 << 21,
] {
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
let before = cpu.clone();
assert!(matches!(
cpu.step(&mut memory(instruction)),
Err(CpuError::UnsupportedInstruction { .. })
));
assert_eq!(cpu, before);
}
}
}
#[test]
fn alias_stores_still_store_the_original_base_before_writeback() {
for kind in [Kind::Word, Kind::Byte, Kind::Half] {
for index in [Index::Pre, Index::Post] {
for up in [false, true] {
let instruction = load(kind, index, up, false, 0) & !(1 << 20);
let mut bus = memory(instruction);
let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = RAM;
let adjusted = if up { RAM + 4 } else { RAM - 4 };
let address = if matches!(index, Index::Pre) {
adjusted
} else {
RAM
};
cpu.step(&mut bus).unwrap();
assert_eq!(cpu.registers[0], adjusted);
match kind {
Kind::Word => assert_eq!(bus.read32(address).unwrap(), RAM),
Kind::Byte => assert_eq!(bus.read8(address).unwrap(), RAM as u8),
Kind::Half => assert_eq!(bus.read16(address).unwrap(), RAM as u16),
_ => unreachable!(),
}
assert_eq!(cpu.cpsr(), 0x1f);
}
}
}
}