Three different PXXDynSetLen bodies are visible at once on every ESP ISA
Measured
At HEAD (compiler/pascal26 = 1a31826169bc) and identically under pin v413
(f94c2a7e2396), compiling test/test_esp_bare.pas:
| target | duplicate definition of 'PXXDynSetLen' |
|---|---|
--target=xtensa --esp-profile=bare |
1 |
--target=riscv32 --esp-profile=bare |
1 |
--target=xtensa |
1 |
--target=riscv32 |
1 |
--target=arm32 |
0 |
--target=i386 |
0 |
hosted x86-64 (no --target) |
0 |
CORRECTED 2026-09-21, and the first table measured the FIXTURE as well as the
compiler. The table above was taken with test_esp_bare.pas, which opens with
{$ifdef CPU_XTENSA}{$define PXX_ESP}{$endif} — so the program supplies the
define, and the rows that fire without --esp-profile=bare fire because of the
fixture, not the target. Re-measured with an empty program and with a program
whose only content is those two {$define} lines:
| program | flags | duplicates |
|---|---|---|
| empty | --target=xtensa |
0 |
| empty | --target=xtensa --esp-profile=bare |
1 |
defines PXX_ESP |
--target=xtensa |
1 |
defines PXX_ESP |
--target=xtensa --esp-profile=bare |
1 |
The trigger is PXX_ESP being defined — by the profile or by the source —
not the ISA. An ESP-class ISA is merely where that normally happens. Stated
because the difference decides who can hit it: any program that defines
PXX_ESP itself pays this, on any target where those arms compile.
Census, same day: compiling an empty program across xtensa, riscv32 (each
with and without --esp-profile=bare), arm32, aarch64, i386, wasm32 and hosted
x86-64 yields exactly one duplicated name tree-wide — PXXDynSetLen. So the
class has one live instance today; the mechanism below is what makes it worth a
guard anyway. The compiler's own text names the
hazard:
duplicate definition of 'PXXDynSetLen' with the same parameter types; the later body wins, but calls written between the two bind to the earlier one
Why this is not a harmless duplicate
The three bodies are pairwise different, not copies:
| definition | size |
|---|---|
builtinheap.pas:533 |
235 lines |
builtinheap.pas:2271 |
34 lines |
builtinheap.pas:5238 |
59 lines |
So "the later body wins" is not a tie-break between identical texts — it selects
between three different implementations of dynamic-array SetLength, and a call
site's lexical position inside the builtin decides which it gets.
What is NOT established
- Which two of the three are simultaneously active, and under exactly which define combination. See the warning below.
- Whether any call site currently sits between two definitions. If none does, today's images are correct by luck of layout, which is precisely what makes this latent rather than academic.
- No wrong behaviour is observed: the bare qemu boot rows still diff UART clean against the x86-64 oracle on both chips.
Do not read the conditionals — ask the compiler
builtinheap.pas lines 13-14 are comment prose quoting
{$ifdef CPU_XTENSA}{$define PXX_ESP} in order to describe a past bug. A
directive scanner that does not skip comments treats those as real and reports a
nesting stack that never unwinds, yielding impossible conditions such as
ifdef PXX_ESP AND ifndef PXX_ESP at one line. Three separate static attempts
produced three contradictory answers here before the approach was abandoned.
The reliable instruments are the ones used above: vary one flag at a time and
count the warning, and ask git log -S when each copy appeared.
What would retire this
Either one definition per ISA with the others removed or renamed, or — if all three are genuinely wanted — a build-time refusal when two same-signature bodies are visible at once, so the binding can never be decided by layout.
2026-09-21 — MEASURED: THIS IS 92% OF AN EMPTY BARE ESP IMAGE
Filed earlier the same day as a latent hazard on the strength of the compiler's warning. It is not latent. It is the dominant cost of every bare ESP image.
The mechanism is the ORPHAN, not the binding
The warning describes a binding ambiguity. The expensive half is what happens to the body that loses:
- it is still emitted into the image;
- it is not registered as a proc body, so
DceOwnerOfanswers< 0for every call site inside it; - the pass therefore labels those calls
called from unowned code— and that is a root.
DCE only drops registered bodies, so the orphan's bytes are undroppable, and
every routine it calls is pinned. PXXDynSetLen's orphan calls
PXXDynArrayReleaseEsp (builtinheap.pas:2282, :2303), which is how the whole
allocator core ends up live in a program that does nothing.
The tell is visible in --dce-why without any experiment: the registered
body is reported PXXDynSetLen <- DROPPED while PXXDynArrayReleaseEsp is
simultaneously live <- [called from unowned code]. A dropped caller and a live
callee is only possible if the real caller is not a body.
Measured, by renaming ONE definition in an isolated sandbox
The repo was never modified: a copy of compiler/pascal26 plus
compiler/builtin/** in a scratch directory, --where confirming it resolved
the sandbox builtin, and the pristine baseline reproducing 11348 B exactly
before any edit.
| program | target | as-is | de-duplicated |
|---|---|---|---|
| empty program | xtensa | 11348 B | 864 B |
test_esp_bare |
xtensa | 13788 B | 4968 B |
test_esp_bare |
riscv32 | 16988 B | 5380 B |
test_esp_exception |
xtensa | 28928 B | 20108 B |
test_esp_exception |
riscv32 | 37028 B | 25420 B |
SetLength program |
xtensa | 34724 B | 32576 B |
SetLength program |
riscv32 | 44020 B | 41172 B |
Empty program live bodies go from 12 (8872 B) to 2 (150 B).
Behaviour is unchanged: the SetLength program boots under Espressif qemu on
both esp32s3 and esp32c3 in both arms and prints the same answer, so the
orphan was contributing nothing but bytes.
THE SRAM DELTA IS ZERO, AND SRAM IS THE RESOURCE THAT COUNTS
Every number above is an IMAGE size, and the owner ruled on 2026-09-20 that
image size is the lesser issue: "SRAM here is most relevant, ESP's have
'plenty' flash memory so that's a lesser issue." So the headline needed the
column I had not taken. Taken 2026-09-21, data + bss, same eight rows:
| program | target | SRAM as-is | SRAM de-duplicated | delta |
|---|---|---|---|---|
| empty | xtensa / riscv32 | 67464 B | 67464 B | 0 |
test_esp_bare |
xtensa / riscv32 | 67524 B | 67524 B | 0 |
test_esp_exception |
xtensa / riscv32 | 67496 B | 67496 B | 0 |
SetLength program |
xtensa / riscv32 | 67500 B | 67500 B | 0 |
Exactly zero, not merely small. Dropping the orphan removes CODE, and the routines it rooted carry no static data, so nothing leaves SRAM.
So this must not travel as an ESP memory win. It is worth doing — 13x on the image, flash-constrained parts, flashing time — and it does not, on its own, outrank whatever else competes for the first hardware day.
(Deterministic, for the record: code/data/bss are outputs of the compiler
and its sources, so these rows do not depend on box load, and the sandbox holds
its own copy of pascal26 and compiler/builtin/**, so a rebuild elsewhere
cannot move them either.)
WHERE THE ESP SRAM ACTUALLY IS, found by taking that column
An empty bare program reserves 67,464 B, and HEAP_ARENA is 65,536 of
it — 97.1% of an empty program's SRAM, and ~23% of the 276,832 B free DRAM
pool, reserved unconditionally under {$ifdef PXX_ESP} (builtinheap.pas:1155,
EspArena : array[0..(HEAP_ARENA div 8) - 1] of Int64).
That is the same shape as bug-a-the-signal-alt-stack-is-32768-bytes-of- unconditional-bss, fixed on 2026-09-18 by reserving the alt stack iff a
handler can exist. Second unconditional BSS reservation to dominate ESP SRAM in
four days.
And the de-duplication above is what makes the condition decidable: once the
orphan is gone, DCE proves PXXAlloc dead in a non-allocating program — it is
dropped, measured — so the arena provably has no reader. Reserved-iff-reachable
is then the same one-line predicate 16ebf18ce used. Belongs under
umbrella-an-esp32-image-is-as-small-as-it-can-be; not filed separately here
because it wants a measurement against the pin now landing.
It reconciles the logbook's two size rows
Makefile:test-esp-bare records 47872 -> 4836 for this fixture on 2026-09-19.
The de-duplicated build today measures 4968 B. So the ~2.85x growth logged as
unreconciled was this collision, not RTL drift — and the two rows can now be
collapsed, with this as the reason.
Why it is not fixed in this commit
The rename is a diagnostic, not a fix. Which of the three bodies ESP ought to
bind is a semantic question about builtinheap, and the answer is not obvious:
today ESP binds the non-ESP body, and the ESP-specific path through
PXXDynArrayReleaseEsp is dead. That may itself be the wrong outcome, in which
case the fix changes behaviour rather than only size.
The class, which is worth more than this routine
Any duplicate same-signature definition in a builtin produces an undroppable orphan that roots its callees. The general remedy is to make the compiler refuse it instead of warning — a warning nobody reads has been costing 10 KB per ESP image since June.
2026-09-21 — FIXED (2c59f8326), and the deciding column is the null one
Deleted rather than guarded: the shared body is a strict superset (unmanaged
elements fall through to baseRecDesc := nil and the retain/release become
no-ops), and the stale comment claiming ESP used the lean arm is corrected in
the same commit.
Condition on every number below: pin v414, binary aeadb1754b80, pinned
source b109703344ea, --esp-profile=bare, empty program.
| target | code as-pinned | code fixed | data+bss as-pinned | data+bss fixed |
|---|---|---|---|---|
| xtensa | 11348 B | 848 B | 67464 B | 67464 B |
| riscv32 | 14044 B | 336 B | 67464 B | 67464 B |
SRAM delta: zero. The orphan lived entirely in flash. Per the owner's 2026-09-20 ruling this is therefore the lesser axis, and the finding is smaller than the image numbers imply.
What this column does NOT show, which is the point: every per-feature SRAM
delta on an ESP image is measured with HEAP_ARENA present — 65,536 B reserved
unconditionally, 97.1% of an empty program's SRAM. A table of small SRAM deltas
is small for a reason that has nothing to do with the features being measured.
That is the blocker-in-reverse for the arena ticket, which is now unblocked.
Verified: fixedpoint converged (binary byte-identical to the pin — builtinheap
compiles into user programs, not the compiler); duplicate warning 0 on both ESP
ISAs with and without the bare profile; hosted output identical;
i386/arm32/aarch64/wasm32/riscv32 still compile; and 6/6 qemu boots on both
esp32s3 and esp32c3 (test_esp_bare, test_esp_exception, a SetLength
program) match their x86-64 oracles.
Log
- 2026-09-21 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 2876138e6.