2.5 KiB
§NNN — A LOOP CURSOR'S C TYPE (POINTER vs PLAIN INTEGER) SELECTS sltu vs slt FOR ITS BOUND TEST, INDEPENDENT OF THE VALUES INVOLVED (P31 S60; func_800CAE74, md_MAIN_031, byte-proven; cross-confirmed same wave by func_8017F2A4, ov_SC03_096)
THE LAW. C mandates unsigned comparison semantics for T* relational operators (<, >=, …), so
a loop cursor DECLARED as a pointer (u8 *p; ... while (p < end)) always lowers to sltu, while the
identical byte-address arithmetic done through a PLAIN INTEGER cursor
(s32 p; ... while (p < base+K)) lowers to slt — even though the underlying values, the loop body,
and the generated addresses are byte-identical either way. This is a pure declared-TYPE dial with zero
effect on the address arithmetic itself (both spellings emit the same addiu/addu for the bump and
the same base computation); it only flips the one comparison opcode.
ASM EVIDENCE. func_800CAE74: cursor p/end base+0x1F declared s32, target emits
slt $v0,$v1,$a3 (signed) at func_800CAE74.s:D8, bound hoisted at addiu $a3,$a1,0x1F (line B8).
Independent same-wave confirmation, func_8017F2A4 (ov_SC03_096): the submitting agent independently
reports "sltu forces pointer-typed rec/end (s32 arithmetic emits signed slt)" for a different
record-scan loop in a different overlay — same law, opposite direction, unrelated function.
WHEN IT APPLIES. Any bottom-tested or top-tested loop whose residual is a slt/sltu MISMATCH on
the bound test with everything else (bump, base, body) already byte-identical. Check the target opcode
first: sltu/sltiu ⇒ declare the cursor and its bound as a pointer type; slt/slti ⇒ declare both
as a plain signed integer (s32) and do the pointer arithmetic in integer space (*(u8*)p/*(u8*)s
for the dereference, p += 1 for the bump). Do not reach for a cast-only fix ((s32)(p < end) does
not change the opcode — the DECLARED type of the compared operands is what the signed/unsigned dispatch
reads, not a cast at the compare site) — this needs the variable's own declaration changed, not a
use-site cast.
(The candidate's other claims — a "parameter reassignment" narrative for arg1, and a two-cursor
split — do not survive checking against the actual banked C: arg1 is untouched and lives in $s2
only because it is read across a later call, ordinary call-crossing liveness already covered by
§167-21/§76; not promoted. See the "could not verify" section.)