← board

i386 / arm32 ignore the field width when writing an Int64

Measured

var a: Int64; i: Integer;
begin
  a := 12345;  i := 12345;
  WriteLn('int64_w =[', a:12, ']');
  WriteLn('int32_w =[', i:12, ']');
end.
target int64_w int32_w
x86-64 [ 12345] [ 12345]
aarch64 [ 12345] [ 12345]
i386 [12345] [ 12345]
arm32 [12345] [ 12345]
FPC 3.2.2 [ 12345] [ 12345]

Same for a negative value (-12345:12). The 32-bit Integer pads correctly on the very same targets and in the very same statement, so this is not the width plumbing in general — it is what the 64-bit operand path does with it.

Why it is worth more than it looks

Padding is what makes columnar output line up, so the failure is a table that silently loses its shape on two targets and nowhere else — no crash, no diagnostic, and a green gate, because nothing in the suite prints a width-formatted Int64 and compares across targets.

Where to look

compiler/emit.inc: EmitwriteIntWArm32 / EmitwriteUIntWArm32 and the i386 equivalents. The aarch64 pair (EmitwriteIntWA64) is a working reference for the shape — it computes padding = width - digits - sign, writes the spaces, then the sign, then the digits. Confirm what the 32-bit IR_WRITE arm actually CALLS for a 64-bit operand before editing: it may be reaching a non-W writer entirely (which would explain a width that vanishes rather than one computed wrongly), and that is a one-line dispatch fix rather than an emitter rewrite.

Gate

The table above reads padded on all four targets, matching FPC, for positive, negative, Low(Int64) and High(Int64), plus gate.sh quick + self-host fixedpoint.

Resolution

The ticket's own guess was right and worth checking first: the 32-bit IR_WRITE arms were reaching a non-W writer entirely. The 64-bit branch (Is64BitArm32(tk) / Is64Bit386(tk)) called EmitwriteInt64Arm32 / EmitwriteInt64_386, neither of which takes a width — so wid was not computed wrongly, it was never passed. A dispatch fix, not an emitter rewrite.

Routed through the writer that already does it

riscv32 — the third 32-bit target — was already correct, and for exactly this reason: it routes EVERY width write, 64-bit and ordinary, through builtinheap's PXXWriteDecW(v: Int64; uns, wid), which computes padding = width - digits - sign, is INT_MIN-safe by construction (u := UInt64(-v) on a two's-complement negate), and already existed. arm32 and i386 now do the same for their 64-bit width case. No new assembly: the third sibling was the fix, the same way arm32's udiv was the fix for [[bug-a-aarch64-writeln-of-low-int64-prints-negated-digit-bytes]] an hour earlier.

Argument marshalling per each target's own convention, not assumed — read off the existing 64-bit call-argument paths: arm32 r0:r1 = value, r2 = unsigned flag, r3 = width; i386 pushes left-to-right with a 64-bit argument as hi then lo, caller cleans 16 bytes.

Measured

target Int64:12 Integer:12
x86-64 [ 12345] [ 12345]
aarch64 [ 12345] [ 12345]
i386 was [12345][ 12345] [ 12345]
arm32 was [12345][ 12345] [ 12345]
riscv32 [ 12345] [ 12345]
FPC 3.2.2 [ 12345] [ 12345]

Swept over positive, negative, Low(Int64), High(Int64), a QWord at High(UInt64), a width EQUAL to the digit count, and a width SMALLER than it (no truncation — the number wins, as in FPC). All five targets byte-identical to each other and to FPC 3.2.2.

Test

test/test_cross_int64.pas extended with the width cases, on top of the Low(Int64) cases added by the aarch64 ticket. It runs as a differential against the x86-64 oracle on arm32, riscv32 and aarch64, so both defects are now pinned by the same test on the targets that had them. The whole file is also byte-identical to FPC's build of it, which is a stronger oracle than the x86-64 self-comparison alone.

Gate: gate.sh quick GREEN (self-host fixedpoint + --tier quick + FPC seed canary) plus the five-target sweep. Backend code only, no frozen builtin touched, so no re-pin.

Log