← board

xtensa has no {$Q+} overflow-check emitter, so it wraps silently

Found 2026-09-01 while fixing [[bug-a-q-plus-overflow-checking-has-no-runtime-helper-on-arm32-and-riscv32]], which is a different defect with the same trigger. That one was a switch LEAK and is fixed. This one is a MISSING FEATURE and is not.

The fact

program qC; {$Q+}
var a, b, c: Integer;
begin a := 2147483647; b := 1; WriteLn('before'); c := a + b; WriteLn('after c=', c); end.
target result
x86-64, i386, aarch64, arm32, riscv32 Runtime error 215 (arithmetic overflow)
xtensa after c=-2147483648no trap

Why it is the quiet one

The arm32/riscv32 defect it was found beside failed to compile. This one compiles clean and produces a wrong value, which is the failure mode CLAUDE.md names as the expensive kind. Anyone checking "does {$Q+} work on my target" by whether it builds gets a yes.

The cause is not subtle — it is simply absent

$ grep -rn "'PXXOverflow'" compiler/*.inc
ir_codegen_arm32.inc  ir_codegen_riscv32.inc  ir_codegen386.inc (x2)
ir_codegen.inc (x2)   ir_codegen_aarch64.inc

Six sites, five backends, no xtensa. There is no EmitOvfCheckXtensa, so IRIVal[node] = 1 (the {$Q+} marker on the binop) has nothing to dispatch to and is dropped.

This is the same job as the div-by-zero gap

[[bug-a-the-div-by-zero-check-is-still-missing-on-xtensa]] is the identical shape — the last target without a pre-divide zero check, left out for the same reasons — and its write-up already lists the four things that make xtensa a genuinely different job rather than a sixth copy of the edit: it cannot be RUN under the bare profile, its branches carry only an 8-bit displacement, the windowed ABI rotates the register window on a call, and there are two shapes depending on XtensaSoftDivide. All four apply here. Take them together.

The trap call itself is the easy half: PXXOverflow takes no arguments and never returns, and hosted xtensa CAN be run (qemu-xtensa, --platform=posix), which is how the table above was measured — so unlike the div-zero ticket's bare-profile problem, this one has a working oracle today.


Fixed 2026-09-01 — and this ticket pointed at the wrong place

I filed this yesterday saying xtensa "is the only target absent from the FindProc('PXXOverflow') grep, so there is no EmitOvfCheckXtensa to dispatch to". The grep was right and the conclusion did not follow.

Pascal widens Q-tagged arithmetic to Int64 in the frontend. The IR for a + b with two Integers is identical on xtensa and riscv32:

2: binop a=0 b=1 c=70 ival=1 tk=13        tk=13 is tyInt64

So the add is a 64-bit operation that cannot overflow, and the wrap happens when that value is stored into a 4-byte slot. The check belongs at the narrowing store, which is where x86-64 (EmitOvfCheckNarrowX64), riscv32 and arm32 all put it. Their binop-level {$Q+} arms exist for genuinely 32-bit ops and are not what makes Integer arithmetic trap.

The canary, because "I think this arm is unreachable" is not a measurement

I had already written checked add/sub arms into the xtensa binop emitter and they did nothing. Rather than reason about why, I planted Error('CANARY: ... IS reachable') inside one and recompiled every {$Q+} program to hand:

q6: not reached   q7: not reached   qC: not reached   qA: not reached
qplus: not reached   test_qplus_survives_ambient_units: not reached

Unreachable for every program that could exercise it. Those arms were removed rather than shipped — untestable code that looks like a fix is worse than the gap, because the next person greps, finds a checked-add arm, and concludes the target is covered. The comment left in the binop emitter says so explicitly.

What landed

EmitOvfTrapXtensa(condR, s, t) — caller states the SKIP condition, so each site can build its own predicate (xtensa has no flags and no slt). Then the narrowing check in IR_STORE_SYM: re-extend the value to the destination width, compare with the full a2:a3 pair, trap on any difference. Same branch-patch and register-lifetime discipline as the div-zero check landed an hour earlier.

Verified

test/test_qplus_narrowing_store.pas — shape by argument count, six targets.

shape before (12907805ca46) after (070b0d10db75)
Integer add -2147483648 RE 215
Integer sub 2147483647 RE 215
Byte 144 RE 215
ShortInt -56 RE 215
Word 54464 RE 215
control ok k=8 b=6 ok k=8 b=6

All five discriminate. The control is load-bearing: a check that traps unconditionally passes the first five rows and fails only this one.

32-bit * overflow is covered too, for free — it narrows through the same store, so the mul-high reconstruction this ticket worried about was never needed.

Residual, and it is NOT xtensa-specific

A 64-bit × 64-bit product that overflows Int64 still wraps silently on xtensa AND riscv32 (x86-64 catches it). Pre-existing, shared, and now filed separately as [[bug-a-64-bit-multiply-overflow-is-unchecked-under-q-plus-on-riscv32-and-xtensa]]. xtensa is at parity with its closest sibling, which was the bar.

Log