← board

{$Q+} overflow checking has no runtime helper on arm32 and riscv32

Repro

$ pascal26 --target=arm32   test/test_a64_leafsym_binops.pas /tmp/x
pascal26:86: error: {$Q+}: PXXOverflow runtime helper not found (builtinheap not loaded)
$ pascal26 --target=riscv32 test/test_a64_leafsym_binops.pas /tmp/x
pascal26:86: error: {$Q+}: PXXOverflow runtime helper not found (builtinheap not loaded)

Still reproduces at bfd6e7f0e with a self-hosted 8444cdce006a. x86-64 and aarch64 compile the same file. The error is raised while compiling builtin/softfloat.pas, not the test program, so it is not specific to this test — it fires for anything that reaches {$Q+} code on those two targets.

Why it is filed separately rather than left where it was found

It shares nothing with the aggregate-result bug but the session that saw it: a different diagnostic, different targets, different mechanism (a missing runtime helper rather than an ABI lowering gap). Keeping the two in one ticket meant the aggregate-result fix would close a ticket that still contained a live, unfixed second defect — the shape that turns a done/ entry into a thing nobody rereads.

What is NOT yet known

The root cause is unverified. The message says builtinheap not loaded, which reads as a unit-loading/ordering question rather than a missing implementation, but nobody has checked whether PXXOverflow exists at all for these targets, whether it exists and is not linked, or whether {$Q+} should be lowering to an inline check on 32-bit instead of a helper call. That is the first thing to establish, and it decides whether this is a five-line wiring fix or a codegen gap.

Cost

{$Q+} is how a program asks for arithmetic overflow to be detected rather than silently wrapped, which makes an unbuildable {$Q+} a safety feature that is unavailable on two of six targets — and unavailable loudly, at compile time, so nothing silently mis-runs. That is the good version of this failure: it refuses rather than pretending. It still means 32-bit code cannot be built with the checking its author asked for.

Gate

make compiler/pascal26 + test/test_a64_leafsym_binops.pas compiles and runs for arm32 and riscv32 with output matching the x86-64 oracle, plus a {$Q+} program that actually overflows being caught on both targets — the point of the directive is the trap firing, and a build that merely compiles proves only half.


Fixed 2026-09-01 — the message names the wrong thing

It is not a missing helper

uses sysutils loads builtinheap and the error does not change. That single observation kills the ticket's framing: PXXOverflow is present, declared unconditionally in builtinheap.pas, on every target. The problem is order, not absence, and the diagnostic's "(builtinheap not loaded)" sends the reader somewhere there is nothing to find.

What it actually is

The error is raised while compiling the compiler's own source:

pascal26:86: error: {$Q+}: PXXOverflow runtime helper not found (builtinheap not loaded)
  in: ./compiler/builtin/softfloat.pas
  near: Result := Result or 1 ; >>> end ; function

QChecksVal is a lexer GLOBAL, reset exactly once per compilation in PasInitDefines. A program that leaves {$Q+} on at end. — the ordinary way to write it — leaves it on for everything the compiler lexes NEXT, which is its own ambient runtime units. pasparser_prog.inc pulls softfloat before builtinheap, so softfloat compiled with overflow checks it never asked for, and those checks called a helper that had not been declared yet.

Only arm32 and riscv32 pull softfloat, which is the whole of why the ticket reads as a 32-bit-backend problem. It is not one; it is a switch-scoping bug that those two targets are the only ones positioned to trip.

The observation that identified it

Moving {$Q-} above end. made both targets compile and trap correctly:

{$Q+} c := a + b; {$Q-}     arm32 -> Runtime error 215      riscv32 -> Runtime error 215

A missing helper cannot be fixed by turning the switch off later in the file. That is what said "leak", not "absence".

The fix

ParseUsesUnitAmbient now saves the lexical switch state, resets it to the compiler defaults, parses the injected unit, and restores. Reset as a block rather than switch by switch, because the rule is a unit the program never asked for is not governed by the program's switches — stating it once is what stops the next switch reopening it. {$R+} leaks identically today and merely happens not to fail; that is the version of this bug nobody would have found, and it is now closed too.

Scoped to AMBIENT units — ones the compiler injects. Whether a user's own uses should also stop propagating {$Q+} (FPC treats $Q as a local switch, so arguably yes) is left alone deliberately: it is a visible language-semantics change with no bug behind it today.

Verified, with a control

test/test_qplus_survives_ambient_units.pas, wired native + aarch64 + riscv32 + arm32 + i386.

binary arm32 riscv32 i386 aarch64
pre-fix c4a89282faa6 CFAIL CFAIL pass pass
post-fix fc7668c096b2 pass pass pass pass

The test discriminates — it fails on the old binary, which is the only thing that makes a green meaningful. The i386 and aarch64 rows pass BOTH ways: they are controls, not evidence, and are reported as such rather than counted.

The .pas deliberately leaves {$Q+} on at end. and the Makefile says not to tidy it: adding {$Q-} is exactly what made the OLD compiler pass.

Xtensa is a third, quieter defect and is NOT fixed here

xtensa compiles this and silently wraps (c=-2147483648, no trap) because it is the one backend with no overflow-check emitter at all — the only target absent from the FindProc('PXXOverflow') grep. Filed as [[bug-a-xtensa-has-no-q-plus-overflow-check-emitter-so-it-wraps-silently]], and it is the same job as [[bug-a-the-div-by-zero-check-is-still-missing-on-xtensa]].

Log