[iterate-4A] jit: coverage — update-form loads/stores + mfspr/mtspr(LR/CTR) → 89.6% native

Second coverage batch, data-driven from the histogram. Diff harness validates
every op against the interpreter (MISMATCHES=0 over 47.96M blocks).

Update-form load/stores (24 ops: lbzu..stdux, lfsu..stfdux and their x-forms):
same access as the base form but with EA = gpr[ra] + offset (ra used directly —
update forms are illegal with ra==0) and rA := EA written back (zero-extended)
after the access. New ea_d_update/ea_x_update/write_ea_back helpers; loads write
rD then rA (so rA wins if rd==ra, matching the interpreter). stwu especially is
the standard stack-frame push in every function prologue.

mfspr/mtspr for LR and CTR only (64-bit field copies): the ubiquitous mflr/mtlr
prologue-epilogue pair and mtctr. covered() gates on the compile-time SPR number
so the offset always resolves; XER (packs CA/OV/SO) and the modelled SPRs stay
uncovered. This is the biggest single jump — mflr/mtlr gate almost every
non-leaf function's prologue and epilogue blocks.

Coverage (single-block, diff mode): 80.6% -> 82.8% (update forms) -> 89.6%
(mfspr/mtspr). 7/7 jit unit tests; foreground-validated per the bg-SIGTERM
finding. Carry-setting shifts (sraw*/srad*), rotate-double (rldic*), and XER
mfspr/mtspr remain for later batches; VMX128 is the long pole.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-08 07:06:42 +02:00
parent 0becc9185a
commit 5950dc86d5

View File

@@ -424,9 +424,14 @@ pub fn covered(instr: &DecodedInstr) -> bool {
// (sign-extending) loads are not lowered yet.
lbz | lbzx | lhz | lhzx | lwz | lwzx | ld | ldx => true,
stb | stbx | sth | sthx | stw | stwx | std | stdx => true,
// Update (`u`) forms: base load/store + write the EA back to rA (zero-
// extended). Algebraic (sign-extending) loads remain uncovered.
lbzu | lbzux | lhzu | lhzux | lwzu | lwzux | ldu | ldux => true,
stbu | stbux | sthu | sthux | stwu | stwux | stdu | stdux => true,
// FP loads/stores are pure moves (no fpscr). Single-precision forms
// widen/narrow via IEEE round-to-nearest, matching the interpreter.
lfs | lfsx | lfd | lfdx | stfs | stfsx | stfd | stfdx => true,
lfsu | lfsux | lfdu | lfdux | stfsu | stfsux | stfdu | stfdux => true,
// FP register moves: only the Rc=0 forms (the `.` forms update CR1 from
// fpscr). fmr/fabs/fneg are sign-bit ops with no fpscr side effect.
// FP *arithmetic* (fadds/fmuls/fmadds…) is NOT covered — it updates
@@ -434,6 +439,13 @@ pub fn covered(instr: &DecodedInstr) -> bool {
fmrx | fabsx | fnegx => !instr.rc_bit(),
// FP arithmetic/compare — lowered by punting to the interpreter.
op if is_fp_punt(op) => true,
// mfspr/mtspr for LR and CTR only — trivial 64-bit field copies (the
// ubiquitous mflr/mtlr prologue pair and mtctr). XER (packs the CA/OV/SO
// flags) and the modelled SPRs (timebase, DEC, VRSAVE, …) stay uncovered.
mfspr | mtspr => matches!(
instr.spr(),
crate::context::spr::LR | crate::context::spr::CTR
),
// Branch terminators.
bx | bcx | bclrx => true,
_ => false,
@@ -1247,6 +1259,31 @@ fn ea_x(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, rb: usize) -> Value {
b.ins().ireduce(types::I32, ea64)
}
/// EA for an **update-form** D load/store: `gpr[ra] + disp` truncated to 32
/// bits. Unlike [`ea_d`], `ra` is used directly (update forms are illegal with
/// `ra==0`). The returned EA is also written back to `rA` by [`write_ea_back`].
fn ea_d_update(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, disp: i32) -> Value {
let base = cache_read_gpr(b, ec, ra);
let ea64 = b.ins().iadd_imm(base, disp as i64);
b.ins().ireduce(types::I32, ea64)
}
/// EA for an update-form X (indexed) load/store: `gpr[ra] + gpr[rb]` truncated
/// to 32 bits (`ra` used directly).
fn ea_x_update(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, rb: usize) -> Value {
let base = cache_read_gpr(b, ec, ra);
let rbv = cache_read_gpr(b, ec, rb);
let ea64 = b.ins().iadd(base, rbv);
b.ins().ireduce(types::I32, ea64)
}
/// Write an update-form EA (I32) back to `rA`, zero-extended to 64 bits — the
/// register writeback the `u` forms perform after the access.
fn write_ea_back(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, ea: Value) {
let ea64 = b.ins().uextend(types::I64, ea);
cache_write_gpr(ec, ra, ea64);
}
/// Emit a load trampoline call `fref(memenv, ea)` and return its result value.
fn call_load(b: &mut FunctionBuilder, ec: &EmitCtx, fref: FuncRef, ea: Value) -> Value {
let call = b.ins().call(fref, &[ec.memenv, ea]);
@@ -1896,6 +1933,168 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) {
call_store(b, ec, ec.tr.write64, ea, bits);
}
// ---- update-form integer loads: base load into rD, then rA = EA. ----
PpcOpcode::lbzu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = emit_inline_load(b, ec, ea, 1, ec.tr.read8);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lbzux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = emit_inline_load(b, ec, ea, 1, ec.tr.read8);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lhzu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = emit_inline_load(b, ec, ea, 2, ec.tr.read16);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lhzux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = emit_inline_load(b, ec, ea, 2, ec.tr.read16);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lwzu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = emit_inline_load(b, ec, ea, 4, ec.tr.read32);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lwzux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = emit_inline_load(b, ec, ea, 4, ec.tr.read32);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::ldu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.ds());
let v = emit_inline_load(b, ec, ea, 8, ec.tr.read64);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::ldux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = emit_inline_load(b, ec, ea, 8, ec.tr.read64);
cache_write_gpr(ec, instr.rd(), v);
write_ea_back(b, ec, instr.ra(), ea);
}
// ---- update-form integer stores: base store, then rA = EA. ----
PpcOpcode::stbu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write8, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stbux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write8, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::sthu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write16, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::sthux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write16, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stwu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write32, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stwux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = gpr32(b, ec, instr.rs());
call_store(b, ec, ec.tr.write32, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stdu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.ds());
let v = cache_read_gpr(b, ec, instr.rs());
call_store(b, ec, ec.tr.write64, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stdux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let v = cache_read_gpr(b, ec, instr.rs());
call_store(b, ec, ec.tr.write64, ea, v);
write_ea_back(b, ec, instr.ra(), ea);
}
// ---- update-form FP loads: base load into fpr[rD], then rA = EA. ----
PpcOpcode::lfsu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let bits64 = emit_inline_load(b, ec, ea, 4, ec.tr.read32);
let bits = b.ins().ireduce(types::I32, bits64);
let f32v = b.ins().bitcast(types::F32, MemFlags::new(), bits);
let f64v = b.ins().fpromote(types::F64, f32v);
b.ins().store(MemFlags::trusted(), f64v, ctxp, fpr_off(instr.rd()));
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lfsux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let bits64 = emit_inline_load(b, ec, ea, 4, ec.tr.read32);
let bits = b.ins().ireduce(types::I32, bits64);
let f32v = b.ins().bitcast(types::F32, MemFlags::new(), bits);
let f64v = b.ins().fpromote(types::F64, f32v);
b.ins().store(MemFlags::trusted(), f64v, ctxp, fpr_off(instr.rd()));
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lfdu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let bits = emit_inline_load(b, ec, ea, 8, ec.tr.read64);
b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd()));
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::lfdux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let bits = emit_inline_load(b, ec, ea, 8, ec.tr.read64);
b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd()));
write_ea_back(b, ec, instr.ra(), ea);
}
// ---- update-form FP stores: base store, then rA = EA. ----
PpcOpcode::stfsu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let f64v = b.ins().load(types::F64, MemFlags::trusted(), ctxp, fpr_off(instr.rs()));
let f32v = b.ins().fdemote(types::F32, f64v);
let bits = b.ins().bitcast(types::I32, MemFlags::new(), f32v);
call_store(b, ec, ec.tr.write32, ea, bits);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stfsux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let f64v = b.ins().load(types::F64, MemFlags::trusted(), ctxp, fpr_off(instr.rs()));
let f32v = b.ins().fdemote(types::F32, f64v);
let bits = b.ins().bitcast(types::I32, MemFlags::new(), f32v);
call_store(b, ec, ec.tr.write32, ea, bits);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stfdu => {
let ea = ea_d_update(b, ec, instr.ra(), instr.d());
let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rs()));
call_store(b, ec, ec.tr.write64, ea, bits);
write_ea_back(b, ec, instr.ra(), ea);
}
PpcOpcode::stfdux => {
let ea = ea_x_update(b, ec, instr.ra(), instr.rb());
let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rs()));
call_store(b, ec, ec.tr.write64, ea, bits);
write_ea_back(b, ec, instr.ra(), ea);
}
// ---- FP register moves (Rc=0; the `.` forms read fpscr → uncovered).
// Done as bit ops on the 64-bit pattern so they exactly match Rust's
// f64 copy / sign-bit abs / sign-bit negate. ----
@@ -1914,6 +2113,27 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) {
b.ins().store(MemFlags::trusted(), res, ctxp, fpr_off(instr.rd()));
}
// ---- mfspr/mtspr for LR and CTR: 64-bit field copies. `covered()`
// gates the SPR to LR/CTR, so the offset is always resolved. ----
PpcOpcode::mfspr => {
let field = match instr.spr() {
crate::context::spr::LR => offset_of!(PpcContext, lr),
crate::context::spr::CTR => offset_of!(PpcContext, ctr),
_ => unreachable!("mfspr covered only for LR/CTR"),
};
let v = b.ins().load(types::I64, MemFlags::trusted(), ctxp, off(field));
cache_write_gpr(ec, instr.rd(), v);
}
PpcOpcode::mtspr => {
let field = match instr.spr() {
crate::context::spr::LR => offset_of!(PpcContext, lr),
crate::context::spr::CTR => offset_of!(PpcContext, ctr),
_ => unreachable!("mtspr covered only for LR/CTR"),
};
let v = cache_read_gpr(b, ec, instr.rs());
b.ins().store(MemFlags::trusted(), v, ctxp, off(field));
}
// Unconditional branch. Target is an immediate; `pc` (= this instr's
// address in the interpreter) is `instr.addr` at emit time.
PpcOpcode::bx => {