← board

The arm

compiler/ir_codegen_xtensa.inc:1419:

    IR_CONST_INT:
      if Is64BitXtensa(IntToTypeKind(IRTk[node])) then
        EmitLoadConst64Xtensa(IRIVal[node])
      else
        EmitLoadConstXtensa(reg_xtensa_a2, Low32(IRIVal[node]));

Is64BitXtensa is (tyInt64) or (tyUInt64) or (tyDouble)not tySingle, character-identical to Is64BitRISCV32. A float constant carries the DOUBLE bit pattern whatever its declared type says (defs.inc, AN_FLOAT_LIT), so Low32 of 0.5 is 0. riscv32 had this and printed 0.0 for every Single constant; frankC fixed it by narrowing (DoubleBitsToSingleBits) with test/test_single_const_value.pas.

arm32 is NOT affected — checked, not assumed: its arm loads the whole 64-bit pattern into d0 and converts on the way out (ir_codegen_arm32.inc:1273). i386 uses the x87 path. xtensa is the only sibling that shares riscv32's soft-float ILP32 value model, and it is the one that shares the bug.

Why it is filed and not fixed — measured

No float program compiles for xtensa at all:

$ ./compiler/pascal26 --target=xtensa --xtensa-soft-mulhigh xf.pas out
error: target xtensa: external (dynamic) symbols are not supported on this
       target (first one: calloc)
  in: ./compiler/builtin/softfloat.pas

Reduced to const S1: Single = 0.5; a := S1; if a > 0.25 ... with no Writeln of a float — same error. Soft float pulls softfloat.pas, which pulls calloc.

So the arm is unreachable, and a fix to it could not be run, only read. The same probe on riscv32 is the control and it works end to end: the pre-fix binary prints 0, the post-fix binary prints 1.

What to do, and when

One line, mirroring riscv32's:

      if IntToTypeKind(IRTk[node]) = tySingle then
        EmitLoadConstXtensa(reg_xtensa_a2, DoubleBitsToSingleBits(IRIVal[node]))
      else if Is64BitXtensa(...) then ...

Land it with the calloc fix, not before — the value of doing it then is that test_single_const_value.pas can be run on xtensa and the fix verified rather than asserted. Landing it now buys an unverifiable edit to a backend whose whole history is "nothing could run it, so every ticket ended do not land this on inspection" (testmgr.py's xtensa note). That history is the reason to wait.

Gate

Track A: make compiler/pascal26 + tools/gate.sh quick, and — the point of the whole ticket — test/test_single_const_value.pas run on xtensa against the native oracle, which requires the softfloat/calloc dependency resolved first.


Rejected — fixed by 4b6f21d68, and this ticket's limit was the wrong one

2026-08-30, frankA, same evening it was filed. The defect was real and the sibling analysis above holds (arm32 and i386 unaffected, xtensa the only backend sharing riscv32's soft-float ILP32 value model). frankC fixed it independently in 4b6f21d68, with test/test_single_const_bits.pas enrolled on the cross target.

What I got wrong is the part worth keeping. I wrote "the arm is unreachable, so a fix could be read but not run" and filed on that basis. It is false. I measured that a program which writes a Single cannot be built for xtensa — softfloat pulls calloc, the backend emits no dynamic segment — and then stated that as a fact about the target. A program that reads the constant's bits through a pointer needs no softfloat at all, builds, and runs under qemu-xtensa:

BEFORE   0            0
AFTER    1056964608   -1077936128     ($3F000000, $BFC00000)

"I could not construct a probe" is a statement about my probe. I had the emulator installed and had already used it in this session. The failure was accepting the first probe's error as the boundary of what the target can run, which is exactly the shape of a false limit: it reads like diligence, it gets believed, and it converted a fixable bug into a parked ticket. A limit deserves the same scepticism as a result — more, because nobody re-tests it.

Kept in rejected/ rather than deleted: the arm32/i386 exclusions were checked rather than assumed and are worth not re-deriving, and the ticket is the record of how a correct finding still produced the wrong action.