DANGLING SHAS BY DESIGN. The commit shas in this ticket live on branch
wasm, not onorigin/master— it was filed from the wasm lane's standalone checkout, which pushes to its own branch.progress.sh checkflags themSIDE-BRANCH-SHAand that is correct rather than a defect: the measurement was taken where the work is. Branch permission is not merge permission — nothing onorigin/wasmis pre-approved for master. Twelve-hex values like2e68d018ccacare binary sha256 prefixes ofcompiler/pascal26, not commits at all, and will not resolve as objects. — frankwasm, 2026-08-30
The fact
var q: Int64;
q := 4294967297; { 2^32 + 1 }
WriteLn(q in [1,2,3]);
| target | answer | |
|---|---|---|
| x86-64 | FALSE | correct |
| aarch64 | FALSE | correct |
| wasm32 | FALSE | correct |
| i386 | TRUE | wrong |
| arm32 | TRUE | wrong |
The items here are 1, 2 and 3 — small. It is the test value that is
truncated, which makes this a different defect from
bug-p-set-membership-item-constant-truncated-to-32-bits (the item constants,
all targets, parser-level). The two are independent and either can be fixed
without the other.
Root cause
compiler/ir_codegen386.inc:3078:
IREmitNode386(IRA[argNode]); { eax = test value }
EmitB($89); EmitB($C1); { mov ecx, eax }
A 64-bit value arrives in edx:eax; only eax is moved to ecx, and every
subsequent cmp ecx, imm32 sees the low half. The high half is dropped with
nothing testing it.
compiler/ir_codegen_arm32.inc:2520 is the same shape — mov r1, r0 where a
64-bit value occupies r0/r1 under AAPCS.
The 64-bit backends are correct by construction: x86-64 uses mov rcx, rax,
aarch64 mov x1, x0 with cmp x1, x2. riscv32 and xtensa have no
SPECIAL_IN arm and refuse in outright — a refusal, not a wrong answer, so
they are not affected.
Fix
Compare both halves on the 32-bit backends. The cheapest correct shape is to
test the high half against the item's sign-extension and fold that into the
existing accumulator (edx on i386, r4 on arm32) — the accumulator is already
there and already has the right lifetime, so no new register pressure.
The alternative — refuse a 64-bit test value on 32-bit targets, as riscv32 and xtensa already effectively do — is worse than it sounds: it would turn working i386 code into a compile error for the sake of a case that is almost never reached. Prefer the fix.
The two defects can cancel, which is worth knowing when testing
On arm32, 4294967296 in [4294967296..4294967300] answers TRUE — the right
answer, reached wrongly. The test value truncates to 0 and the parser truncates
the bounds to 0..4, so 0 in [0..4] is TRUE. x86-64 and aarch64, which have only
the parser defect, answer FALSE.
So a test that only checks arm32 against a correct expectation on that input would pass. Fixing either defect alone will make that case go red on arm32, and that red will be progress, not a regression.
Repro
printf 'program t;\nvar q: Int64;\nbegin q := 4294967297; WriteLn(q in [1,2,3]); end.\n' > /tmp/t.pas
./compiler/pascal26 --target=i386 /tmp/t.pas /tmp/t386 && /tmp/t386 # TRUE; should be FALSE
./compiler/pascal26 /tmp/t.pas /tmp/t64 && /tmp/t64 # FALSE — the control
Measured at branch wasm sha 954b56b53, compiler 2e68d018ccac; x86-64 and i386
run natively, arm32/aarch64 under qemu-arm/qemu-aarch64, wasm32 under node.
Every cell in the table above was run; none is by inspection.
Reachability — read this before ranking it up
Same caveat as the sibling ticket: a set item list is normally 0..255 and a 64-bit test value against one is rare. prio 25 reflects reach, not severity — it is a silent wrong answer, which is why it is filed at all.
WARNING for whoever fixes this: on arm32 two defects CANCEL on the range shape
Added 2026-08-28 by the coordinator, from frankwasm's re-measurement under qemu (its
earlier cross-target claim was made "by inspection"; qemu-arm and qemu-aarch64 were on
PATH the whole time, and inspection would have been half wrong).
The table above is the scalar shape — q in [1,2,3] with a large q — and it is correct:
arm32 answers TRUE and TRUE is wrong.
On the range shape it is the other way round. 2^32 in [2^32..2^32+4] answers TRUE on
arm32, which is the correct answer — reached because the truncation of the test value and
the truncation on the bound side cancel.
So a correct partial fix makes a currently-passing case start failing. Repair the test value alone and the range shape breaks; repair the bound alone and it breaks the other way. Whoever takes this must:
- pin both shapes — scalar and range — before touching anything;
- expect the range row to go red mid-fix, and not read that as a regression introduced by the fix; it is a compensating error becoming visible;
- only judge the work by both rows green together.
A PASSING RESULT PRODUCED BY TWO ERRORS THAT CANCEL IS INDISTINGUISHABLE FROM A PASSING RESULT PRODUCED BY CORRECTNESS — and it converts the first honest half-fix into what looks like a regression.
Related: feature-a-a-refusal-is-a-claim-with-a-date-on-it — same signature as the rest of that
family, a state carrying no information because two conditions read identically.
Fixed 2026-08-31 — and it was wider than filed, in both directions
Two more backends
The ticket names i386 and arm32. riscv32 and xtensa have it too — they were
not exempt, they simply had no in arm when frankwasm measured; both gained one
later the same week. Measured before touching anything, at binary 73396b86f09a:
q := 4294967297; WriteLn(q in [1,2,3]); { oracle: FALSE }
i386 TRUE arm32 TRUE riscv32 TRUE xtensa TRUE
x86-64 FALSE aarch64 FALSE
Two shapes, and only one of them is the reported symptom
in reaches codegen by two different routes, which is the trap
normalise-dont-special-case.md names:
| source shape | lowering | symptom before the fix |
|---|---|---|
q in [1,2,3] (all-constant) |
SPECIAL_IN, compared inline |
silently TRUE |
q in s (a set variable) |
IR_BINOP tkIn |
failed to compile |
The second shape is not in the ticket and is the louder bug:
$ pascal26 --target=i386 setv.pas /tmp/x
error: target i386: 64-bit binop operator not yet supported
...on all four backends, identically. Anyone reproducing only the filed repro
would have fixed SPECIAL_IN, closed the ticket, and left q in s uncompilable.
One root cause under both
in was being dispatched as 64-bit ARITHMETIC because its element is
64-bit. Each backend routes a binop to its dedicated 64-bit emitter on
Is64Bit(tk) or Is64Bit(lhsTk) or Is64Bit(rhsTk) — but in has a Boolean
result and its right operand is a set address, so the only thing that
matched was the element, and the 64-bit emitter has no tkIn arm.
The same mismatch explains the quiet half: every compare in SPECIAL_IN is
32 bits wide, so the high dword was never read. x86-64 is correct for free —
its cmp rcx, imm32 is REX.W and the immediate is sign-extended to 64 bits.
The fix, one idea at four sites
- Exclude
tkInfrom the 64-bit dispatch. It removes only cases that previously raised an error, so it cannot change a program that worked. IR_BINOP tkIn: if the element is 64-bit, saturate it to 256 when the high word is nonzero, then let each backend's existing unsigned0..255range check decide. Deliberately reusing that check instead of adding a second one that could disagree with it. Must happen before the left operand is spilled — the high word lives in the register the right operand is about to overwrite.SPECIAL_IN: compute "does this value fit in a signed 32-bit int" once (hi = lo asr 31) and AND it into the result. Branch-free on i386/arm32/ riscv32 so it needs no patch site and cannot desynchronise the item loop's own jumps; xtensa uses branches because it has no immediate ASR emitter here.
Verified
test/test_set_in_64bit_element.pas — 21 rows, wired for native + aarch64 +
riscv32 + arm32 + i386 + xtensa. All six targets match, including the
Char/Integer/in-range control rows that a fix could have broken while making the
2^32+1 rows go FALSE.
Regression, object-level: compiling all 140 Pascal sources with the pre-fix
binary 73396b86f09a and the post-fix 338a7cbd49c5 gives byte-identical
objects — i386 137/137, and the other three below. That is expected by
construction: every arm added is guarded by "the element is 64-bit AND the op is
tkIn", and that combination previously either errored or was already wrong.
FPC disagrees, and we are diverging on purpose
FPC 3.2.2 truncates and answers TRUE; 7 of the 21 rows differ. That is now a recorded decision rather than an accident: [[decide-does-in-truncate-an-out-of-range-element-or-answer-false]]. This fix did not choose a semantics — it moved four backends onto the answer x86-64 and aarch64 already gave. What no ruling changes is that all six must answer alike.
Verified at HEAD and closed (frankA, 2026-09-02)
test/test_set_in_64bit_element.pas runs clean at HEAD, 21 rows, and is wired
into the Makefile at eight call sites (native plus the six cross targets,
and one guard row). The FPC divergence has its own ticket
([[decide-does-in-truncate-an-out-of-range-element-or-answer-false]], which is
in decided/ — settled, not merely filed; checked rather than assumed, because
the first version of this line said "filed and open"), so nothing of this
ticket's own scope is outstanding.
Closed on the fix, not on a green: what makes it closeable is the recorded byte-identical-object regression across the pre- and post-fix binaries plus six targets agreeing, both already in the section above.
Found by a filter, and how matters. This ticket's summary begins with the
word FIXED while its status was backlog — the exact aperture
[[bug-t-check-has-no-aperture-for-a-ticket-whose-body-records-its-own-completion]]
asks for. It sat at prio 25, which is why nobody dispatching hit it; the three
instances that ticket records were all caught because someone was about to be
handed the work.
Log
- 2026-09-02 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 7001c95b6.