← board

The RTTI blob is hard-sized to 64-bit fields, so half of it is padding on every 32-bit target

READ THE REACH BEFORE THE PRIO

Five targets carry this: i386, arm32, riscv32, xtensa, wasm32. It was found on ESP and it is not an ESP defect. ESP is where it is expensive — SRAM is the scarce resource there and this blob sits in it — but an i386 or arm32 image carries the same padding and nobody has measured what it costs them. A seat scanning the queue and filing this under "ESP memory" is skipping a defect in their own target.

It is also not a new class. [[bug-a-method-pointer-record-is-hard-sized-16-bytes-on-32-bit-targets]] is the same animal and is done/: a width hard-coded for 64 bits, correct on the host, wrong on all five 32-bit targets, and structurally invisible because the dev loop, gate.sh quick and the pin all run on x86-64. That one was found by falsifying a wasm32 IR arm; this one by an ESP SRAM census. Two unrelated instruments, one class, neither of them the one that would normally catch it — which is evidence the class is live and under-sampled, not that it was closed in August.

Found 2026-09-20 while categorising ESP SRAM under [[umbrella-an-esp32-image-is-as-small-as-it-can-be]]. It is on the owner's stated axis — "SRAM here is most relevant" — and unlike every code-removal rung, which measures at zero SRAM from here, this one is SRAM by construction.

The layout

compiler/defs.inc:

{ RTTI blob byte layouts (all fields 8 bytes) }
RTTI_CLS_SIZE    = 128; { name,parent,instSize,vmt,propCount,props,methCount,meths,
                          fieldCount,fields,ifaceCount,ifaces,unitName,classInfo,
                          flatCount,flatBases }
RTTI_PROP_SIZE   =  64; { 8 fields }
RTTI_IFACE_SIZE  =  32;

Sixteen fields, eight bytes each, one constant for all seven backends. Half of those fields are pointers, and a pointer on riscv32, xtensa, i386, arm32 or wasm32 is four bytes.

The measurement, and it is a differential rather than an inference

PXXDBG=a.datamap on examples/esp32/nilpy-c3/main/main.npy, compiler at 734c1df1e:

x86-64 hosted                     class RTTI headers  10240B
riscv32 --platform=esp --dce      class RTTI headers  10240B

Byte-identical across a 64-bit and a 32-bit target. 10,240 / 128 = 80 classes get a header (of 113 total; records are excluded). The size did not move because nothing about it is target-aware.

On ESP that 10,240 B is SRAM — 8.1% of our whole 125,832 B, measured on the chip with examples/esp32/nilpy-c3/build.sh sram.

Pascal VMTs have the same shape: pasparser_prog.inc reserves vmtN * 8 bytes per VMT, so a slot is eight bytes whatever a pointer costs. a.datamap reports 1,848 B of VMT on this program, ~924 of it padding on a 32-bit target.

NOT a correctness bug, and worth saying why

The emitter writes a 4-byte pointer into an 8-byte slot that DataPutZeros has already zeroed, and the targets are little-endian, so a reader taking eight bytes gets the pointer correctly zero-extended. Nothing reads garbage. This is purely bytes.

And no ISR hazard applies, which separates it from everything on [[feature-a-there-is-no-read-only-load-segment-so-nothing-can-be-flash-resident]]'s REMAINING list: the blob stays in .data exactly where it is, it just stops being twice the size it needs. No structure moves to flash, so the cache-off-in-an-interrupt question does not arise.

The real obstacle: three spellings of one layout

Three mechanisms serving one concept is the threshold devdocs/dev/root-cause-over-microfix.md calls a design flaw, and nothing forces the three to agree — a stride change that updates two of them produces a runtime that reads the wrong field and no compile error. That, not the arithmetic, is the work.

The bootstrap constraint the header documents still applies and is the reason this is not a free edit: the compiler must be able to read a blob the previous stage's emitter wrote. defs.inc's own note argues that class blobs are safe from this because the compiler declares no class type and calls no class-blob accessor — re-verify that by grep before relying on it, because it is exactly the kind of checkable claim that decays.

What would retire this

Fields sized from TARGET_PTR_SIZE in all three spellings, DERIVED from one place rather than restated, plus a test that a 32-bit and a 64-bit build report different a.datamap RTTI totals for the same program — the differential above, inverted into a guard. Today that guard would be red; after the fix the two numbers must differ, and asserting equality is what the current code would pass.

State the win as a ceiling: up to ~5,120 B on this program, less for any field that must stay 8 bytes (instSize and the counts are candidates to keep if anything relies on their width). Measure after, not before.