← board

Cross self-host: i386 generated compiler runs under Linux

Done

All four acceptance criteria met (commits 7756241, 68bef67, 1f93c42, e3b8866): the i386-hosted compiler compiles test/hello.pas to x86-64 byte-identically to native and the result runs, and the full self-fixedpoint compiler.pas -> i386 (native) -> compiler.pas -> i386 (self) is now BYTE-IDENTICAL ([code=1574514B data=43368B bss=131496552B procs=801], cmp clean). Walls cleared: Int64 edx:eax codegen, the PWord = ^Int64 machine-word landmine, the 64-bit ordered-compare sbb ModRM bug (which masqueraded as an Expected: unit lexer error via PXXStrLoadFile's broken if fd<0 guard), and full 8-byte Int64 by-value param passing (float-bit high dwords were truncated through MovRaxImm/EmitI64). The param-passing change moves runtime-helper size/len params to NativeInt and is bootstrapped via make bootstrap (FPC), not make compiler/pascal26, because the prior compiler has the old Int64 protos baked in.

Goal

Make the i386 compiler binary emitted by native pascal26 work as a compiler. Tackle this platform independently from AArch64 and ARM32, even if root causes turn out to overlap.

Probe

Repro from repo root:

./compiler/pascal26 -dPXX_MANAGED_STRING --target=i386 \
  compiler/compiler.pas /tmp/compiler_i386
./compiler/pascal26 -dPXX_MANAGED_STRING --target=x86_64 \
  test/hello.pas /tmp/hello_native_to_x64
tools/run_target.sh i386 /tmp/compiler_i386 -dPXX_MANAGED_STRING \
  --target=x86_64 test/hello.pas /tmp/hello_i386_to_x64

Observed 2026-06-13 after the i386 managed-string COW slice: the probe passes. The i386-generated compiler emits /tmp/hello_i386_to_x64, cmp matches the native x86-64 output byte-for-byte, and the result prints Hello, World!.

Acceptance

Cleared walls

2026-06-13 (later): no longer a crash. The i386-emitted compiler now starts, lexes, parses, and compiles ~118 lines of the heap RTL before failing with a semantic error:

pascal26:119: error: no overload of heapmmap matches these arguments

Root cause: i386 has no copy-on-write for managed-string writes (already flagged in the IR_INDEX comment in ir_codegen386.inc). LowerCase does res := s; res[i] := Chr(...), which shares s's handle and then mutates it in place. With no COW, the in-place write corrupts the shared original, so the call name HeapMmap is folded to heapmmap in a buffer that is still aliased by the case-preserved decl name — MatchProcCall's exact = then misses (Procs[40].Name = 'HeapMmap' vs lookup 'heapmmap').

Minimal repro:

function LowerCase(const s: ansistring): ansistring;
var i: integer; res: ansistring;
begin res := s; for i:=1 to Length(res) do if res[i] in ['A'..'Z'] then res[i]:=Chr(Ord(res[i])+32); LowerCase:=res; end;
// x:='HeapMmap'; LowerCase(x) leaves x='heapmmap' on i386, 'HeapMmap' on x86-64

2026-06-13 (latest): the heapmmap COW wall is cleared. i386 now has AnsiString index-write clone-if-shared handling via PXXStrUnique, and the byte-identical compiler.pas --target=i386 then hello.pas --target=x86_64 probe passes under -dPXX_MANAGED_STRING.

2026-06-13 (latest): the apparent endless loop was quadratic include expansion, not parser/codegen recursion. ExpandIncludes appended a 1.37 MB expanded compiler source one byte at a time with SetLength on each byte; the i386-generated compiler made that look like an endless CPU/memory-growth run. Bulk span/string append now gets past include expansion and completes the deeper fixed-point compile.

Current wall

ACCEPTANCE #1-3 MET (2026-06-13). The i386-hosted compiler now compiles test/hello.pas to x86-64 byte-identically to native and the result runs ("Hello, World!"). It also compiles empty.pas/compiler.pas to i386 without crashing. Walls cleared along the way:

REMAINING WALL (acceptance #4, full byte-identical self-fixedpoint). The deep compiler.pas -> i386 (native) -> compiler.pas -> i386 (self) probe now completes with IDENTICAL sizes [code=1570805B data=43368B bss=131496552B procs=801] but the binaries differ at byte 34168 (~5237 bytes, all 64-bit float/immediate constants where the self build emits a zeroed or sign-collapsed high dword). Root cause: Int64 by-value params are truncated to 32 bits at the i386 call boundary (the current ABI homes only the low dword + sign-extend). So MovRaxImm(v: Int64) / EmitI64(v: Int64) — fed the 64-bit double bits of a float literal — lose the high dword. Minimal repro: i386-hosted compiler compiling a double := 1e15 program to x86-64 emits 0.00.

Fix = full 8-byte Int64 by-value param passing. Attempted and reverted twice this session because it needs the hand-emitted runtime-helper call sites to agree:

Note: a bad git stash pop of an unrelated stash injected merge-conflict markers mid-session and made builds look non-deterministically broken; if builds suddenly fail with unresolved forward/syntax errors, check git status for UU files and re-make bootstrap.

Earlier note (the float-constant wall, now cleared by the Int64 work)

The deeper self-fixedpoint probe now terminates and produces matching code/data/BSS sizes, but the binaries are not byte-identical:

./compiler/pascal26 -dPXX_MANAGED_STRING --target=i386 \
  compiler/compiler.pas /tmp/compiler_i386_native
/tmp/compiler_i386_native -dPXX_MANAGED_STRING \
  --target=i386 compiler/compiler.pas /tmp/compiler_i386_self
cmp /tmp/compiler_i386_native /tmp/compiler_i386_self

Observed 2026-06-13: both compilers report [code=1467327B data=43304B bss=131496552B procs=794], but cmp first differs at byte 24194. The diff is in the emitted x86-64 float writer code embedded in the i386 compiler: native emits the double bits for 1000000000000000.0 (0x430c6bf526340000), while the i386-hosted compiler emits zero. This is the same underlying problem seen repeatedly in this burn-down: i386's scalar model only carries low dwords for many Int64 operations/stores, so shl/shr, large integer constants, float bit patterns, pointer serialization, and related codegen paths keep needing local workarounds. See feature-i386-int64-codegen.md; fixing that should be treated as the next foundation task before more self-host patching.

Log