← board

Shrink the managed-block header on 32-bit targets

The waste

PWord is a pointer to a machine word — 8 bytes on 64-bit, 4 on 32-bit — while the header's field offsets are fixed 8-byte strides on every target:

[meta:8][refcount:8][length:8][data...]      handle = block + 24

So on ILP32 each slot carries a 4-byte value in its low half and 4 bytes of padding: 12 of the 24 header bytes are dead, on every string, dynamic array and object. Packing to 4-byte slots gives a 12-byte header (16 with alignment), roughly halving per-block overhead.

That matters most exactly where memory is scarcest. Under PXX_ESP the heap is a single 64 KiB static arena (vs a 256 MiB mmap chunk on a host), and xtensa and riscv32 are both ILP32 — so this is an ESP-capacity feature more than a desktop one. Frozen string[N]/shortstring already sidestep the header entirely and remain the primary ESP lever.

The part that is time-critical

Packing makes the meta word 32 bits wide on ILP32. Whatever phase 2 (feature-nilpy-text-string-kind) puts in bits 32–63 would have nowhere to live there — so either the layouts diverge per target (bad: the field means different things on different machines) or the upper half must be permanently unused.

So the constraint is already recorded in the design doc and phase 2 must obey it: every meaningful field lives in the low 32 bits. That is BlockKind(8) | Flags(8) | KindData0(8) | KindData1(8), with 32–63 reserved. It also forces KindData0 to hold a small encoding enum rather than a raw codepage (CP_UTF8 = 65001 does not fit in 8 bits) — which is the better field anyway.

If this ticket is never done, nothing is lost. If phase 2 spends the upper 32 bits, this ticket becomes impossible. That asymmetry is the whole reason it is filed now at a low priority rather than later.

Sketch

Introduce the stride as a constant (PXX_HDR_SLOT = SizeOf(NativeInt)) and derive PXX_HDR_SIZE/_META/_RC/_LEN from it, rather than the current fixed 8s. The Pascal side in builtinheap.pas already routes everything through those names since phase 1, so most of it follows. The work is the x86-64 emitter (which hardcodes [rax-16], [rsi-32], sub rax, 24 and the inline allocation sequences) — but note x86-64 is LP64 and would not change; it is the 32-bit backends that would need their own offsets, and today they hardcode almost nothing because they delegate managed release to the Pascal runtime. Check that claim before costing the work: it is what made phase 1 much smaller than expected.

Gate

Track A, and the same shape as phase 1 — but not via FPC. Seed the self-host from pinned (which carries its own frozen RTL); make compiler/pascal26 seeds from a binary with no versioned RTL and will silently produce a core-dumping compiler ([[bug-a-self-host-seed-has-no-versioned-rtl]]). Then testmgr --tier full with the 32-bit targets specifically, -dPXX_HEAP_DEBUG for free-base errors, then stabilize + pin. An ESP/xtensa build under --platform=esp --esp-profile=bare is the point of the exercise, so measure the arena headroom before and after.