RISC-V does not propagate NaN payloads. Any operation that produces a NaN
returns exactly 0x7FF8000000000000. Both hosts propagate an operand's
payload instead, and both routes inherited that, so `fadd.d` of a
payload-carrying NaN handed the payload straight back.
Both routes were wrong in the same way, which is why the differential
fuzzer reported clean runs over this for a hundred thousand sequences: it
compares the two implementations against each other and they agreed. It
took an external oracle to see it, and the survey against qemu-riscv64 --
6 ops x 81 operand pairs -- found 222 of 486 cases wrong.
The interpreter canonicalises explicitly, in the one place a host double
becomes a guest register value: fadd, fsub, fmul, fdiv, fsqrt and the four
FMA forms.
The backend sets FPCR.DN for the duration of translated code. That is the
ARM control bit for exactly this -- with DN set the hardware returns its
default NaN, which is bit-identical to the RISC-V canonical NaN -- so one
bit covers the whole arithmetic surface and costs nothing per instruction.
The alternative, testing and rewriting the result of every FP instruction,
would have been three extra instructions on each one.
It is scoped to the trampoline rather than set once around the dispatch
loop, so it applies to exactly the translated code and never leaks into the
ECALL handlers, which run host FP. X26 carries the saved FPCR across the
call: callee-saved, already pushed by the trampoline prologue, and not
touched by translated blocks, which use X9-X12 and X22-X25 for the register
cache and X0/X1/X2/X16/X17 as scratch.
After this the arithmetic surface is exact against qemu: fadd, fsub, fmul
and fdiv all go from 41-45 wrong out of 81 to zero, in both routes.
FMIN/FMAX are *not* fixed here and are filed separately. DN improves them
-- 23 wrong to 10 -- but the residue is the case where one operand is a
signalling NaN and the other is a number: RISC-V returns the number, ARM
returns a NaN whatever DN says. The interpreter already gets that right,
so it is a backend-only defect with a different shape, and folding it in
here would have mixed a one-bit change with a per-instruction one.
Sixteen regression assertions covering both routes, both operand positions
and all four arithmetic ops; all sixteen fail against the unfixed tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every float-to-integer convert went through a plain C cast, which is
round-toward-zero. The instruction's rm field was never read, so RNE --
the default, and what the compiler emits unless told otherwise -- silently
behaved as RTZ, and RDN/RUP/RMM did too.
This is not an edge case: 1.5 converted to 1, 2.6 to 2, 3.5 to 3, -2.6 to
-2. Any guest converting a fractional double to an integer got the wrong
answer whenever the fraction reached one half.
Rounding also has to happen *before* the range check, because the valid
domain is defined in terms of the value after rounding: 2147483647.5 under
RNE becomes 2^31, which is out of range for FCVT.W.D even though the
unrounded value is not. Checking first and rounding never gave the wrong
answer at that boundary in three of the four converts.
rv_round_to_int deliberately avoids nearbyint()/rint(). Those follow the
host floating point environment, which would make the guest's rounding
mode whatever the host happened to be set to, and would differ between the
interpreter and a JIT that encodes the mode into the instruction. The
arithmetic it does instead is exact: the fractional part is obtained by
subtracting an exactly representable truncation, and above 2^52 a double
has no fractional part at all.
Dynamic rounding (rm=7) now resolves through fcsr.frm, which the CSR
instructions already maintained but nothing consumed.
Found by conformance-testing against qemu-riscv64 rather than by the
differential fuzzer: the DBT truncates too, so the two routes agreed and
the fuzzer reported a clean run. The DBT side is #1313.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects, one instruction family. RISC-V requires NaN to convert to the
destination type's MAXIMUM and saturation at both ends; the host ISAs do
neither, and RV64 sign-extends every 32-bit result including the unsigned
form.
#1313 -- a64 backend. FCVTZS/FCVTZU were used bare:
* The W forms converted at 64-bit width and then narrowed, so
fcvt.w.d(+inf) gave -1 (0x7FFF_FFFF_FFFF_FFFF truncated then
sign-extended) rather than 0x7FFFFFFF, and fcvt.w.d(-inf) gave 0 rather
than 0x80000000. Now uses the 32-bit destination forms so ARM saturates
where RISC-V does; new emit_fcvtzs_w32_d / emit_fcvtzu_w32_d.
* fcvt.wu.d zero-extended via MOV Wd,Wd. RV64 sign-extends W results even
for the unsigned form, so +inf must be 0xFFFFFFFF_FFFFFFFF, not
0x00000000_FFFFFFFF. Now SXTW.
* ARM returns 0 for NaN. All four forms now test unordered (FCMP of the
operand against itself) and CSEL in the destination maximum, using X17,
the documented IP1 scratch already used by the guest-address clamp.
#1314 -- interpreter. FCVT.WU.D and FCVT.LU.D folded the NaN and negative
cases together as `if (isnan(a) || a < 0.0) v = 0`, but only *negative*
gives 0; NaN gives the maximum. Split.
Adds test_fp_cvt_nan_inf to dbt_test.cpp: 12 cases x both routes, with the
expected values taken from the RISC-V unprivileged spec rather than from
either implementation here -- which matters, because for the two NaN
unsigned rows both routes agreed and were both wrong, so the differential
fuzzer structurally could not see them (#1314).
Verified against the unfixed build, which reproduces the qemu table in
#1313 exactly -- 7 wrong DBT cases plus the 2 rows where the interpreter is
wrong too:
FAIL fcvt.w.d(+inf) DBT 0xFFFFFFFFFFFFFFFF want 0x7FFFFFFF
FAIL fcvt.w.d(-inf) DBT 0x0 want 0xFFFFFFFF80000000
FAIL fcvt.w.d(NaN) DBT 0x0 want 0x7FFFFFFF
FAIL fcvt.wu.d(+inf) DBT 0xFFFFFFFF want 0xFFFFFFFFFFFFFFFF
FAIL fcvt.wu.d(NaN) interp+DBT 0x0 want 0xFFFFFFFFFFFFFFFF
FAIL fcvt.l.d(NaN) DBT 0x0 want 0x7FFFFFFFFFFFFFFF
FAIL fcvt.lu.d(NaN) interp+DBT 0x0 want 0xFFFFFFFFFFFFFFFF
Two harness bugs found and fixed while writing that test, both of which
made it pass or fail for the wrong reason: ADDI's immediate is 12-bit
SIGNED so the guest constant had to be built a byte at a time, and the
W/WU/L/LU variant lives in the rs2 field, not funct3 -- all four forms were
assembling as fcvt.w.d.
Not addressed here: the x86-64 backends almost certainly have the same
class of bug in a different direction (CVTTSD2SI yields the "integer
indefinite" value on invalid, which is not the RISC-V answer either). This
host cannot test them; filed as a note on #1313 for an x86 box.
dbt 196/196 hand-assembled + both ELF legs; smoke 1427/1427, 0 crashes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`addr + len <= mem->size` is unsigned 64-bit arithmetic. For an addr
within len of UINT64_MAX the sum wraps to a small number, compares below
mem->size, and the check passes -- after which the caller indexes
mem->data with the unwrapped addr. Every guest read and write in
dbt_interp.cpp routes through this one predicate, so all ten share it.
Testing the operands separately cannot wrap: the first term establishes
addr <= mem->size, so the subtraction in the second is always defined.
This is the interpreter-route counterpart to #1151 (DBT-side intrinsic
bounds, PR #1277). Same footing: defense-in-depth on a path a correctly
generated blob does not reach, fixed for the same reason.
New tests/dbt_interp island, wired into `make test`. mem_check and the
accessors are file-static, so the test #includes the translation unit
rather than linking it -- dbt_interp.cpp depends only on dbt_interp.h,
dbt_decoder.h and the standard library, so there are no stubs and no skip
path.
The WRAP cases are the only ones that discriminate; the other eight
behave identically under both forms. Negative control, reverting
mem_check to `addr + len <= mem->size`:
FAIL: WRAP addr=2^64-1 len=2 must be refused
FAIL: WRAP addr=2^64-4 len=8 must be refused
FAIL: WRAP addr near 2^64 with a large len must be refused
FAIL: WRAP read64 returns 0 rather than indexing out of bounds
FAIL: WRAP write64 must not modify guest memory
=== dbt interp: 8 passed, 5 failed ===
Full make test green: GANL 16, netaddr 61, dbt chain 48, dbt cache 14,
dbt interp 13, alarm 8, smoke 1426/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decoder (dbt_decoder.h) and interpreter (dbt_interp.cpp) for the
RV64IMD instruction set — the execution foundation for the JIT
compiler. D-only (no single-precision F); ECALL dispatches through
a caller-provided callback for future EngineAPI integration.
Standalone test harness (dbt_test.cpp): 20 tests, 40 assertions
covering integer, W-suffix, shift, multiply, branch, load/store,
FP arithmetic, FP conversion, and loop execution.
Design doc updated: RV64 decision locked, ~/riscv as reference only,
file locations in mux/modules/engine/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>