← board

--dce --emit-obj emits an ESP IRAM object that segfaults the linker

Repro

./compiler/pascal26 --dce -Fulib/rtl --emit-obj --target=riscv32 \
    --platform=esp test/esp_obj_rodata_iram.pas /tmp/o.o
tools/emit_obj_stub_shim.sh /tmp/o.o > /tmp/o.c
riscv32-esp-elf-gcc -fno-builtin -nostartfiles -Wl,-e,main /tmp/o.c /tmp/o.o -o /tmp/o.elf
# collect2: fatal error: ld terminated with signal 11 [Segmentation fault]

Drop --dce and it links. This is the iram arm of test-emit-obj's .rodata block, which runs without --dce today, which is why nothing saw it.

The matrix, measured 2026-09-21 at 497489e8a723

Two targets x three variants of the same fixture x the pass on and off. The variants are the tier's own: ro is test/esp_obj_rodata.pas, ctl is the same source with --no-ro-data, iram is test/esp_obj_rodata_iram.pas.

target variant --dce --no-dce
riscv32 ro link ok link ok
riscv32 ctl link ok link ok
riscv32 iram ld SEGFAULT link ok
xtensa ro link ok link ok
xtensa ctl link ok link ok
xtensa iram ld SEGFAULT link ok

Four clean rows per target are the control. They hold the pass, the platform, the writer and the shim fixed and vary only the IRAM section, so "--dce breaks ESP objects" is refuted by the same run that finds this.

Not a regression, and not caused by the fix that uncovered it

pin v415  (binary 94fddf62ee6a)   ld SEGFAULT
pin v416  (binary fddc21e7e661)   ld SEGFAULT   riscv32 AND xtensa
HEAD      (497489e8a723)          ld SEGFAULT

Two pins, and the second is the CURRENT one. v415 was the pin when this was found; v416 landed the same evening and was re-measured against rather than letting the v415 row go unquotable. Both reproduce on both ESP targets, so the pin that ships today carries this.

Pin v415 predates 30898eef7 (the ProcAddrFix/DynCall compaction fix), so this is older than that work and independent of it. It also reproduces with an explicit --dce, so no default is implicated.

Where to look first

dce.inc already treats IRAM as a distinct population — IramCallFix is its own fixup table and CodePosToIramOff decides, per site, whether a relocation belongs to .rela.text or .rela.iram1.text (elfwriter.inc, around the ProcAddrFixCount loops near 3030 and 3157). A code offset that moves under compaction while its IRAM mapping does not is the shape to suspect. Note CodePosToIramOff is asked about a post-compaction CodePos in those loops.

A linker segfault means the ELF is malformed, so read the object before reading the pass: readelf -SW, readelf -rW and a relocation whose offset falls outside its own section are the cheap first checks, and they say which table is wrong without any theory about why.

Why it matters beyond the tier

It is the standing blocker for defaulting --dce on under --emit-obj, which is item (1) of bug-a-emit-obj-retains-pxxassert-so-one-ansistring-in-it-imports-the-whole-esp-pal. IRAM is not exotic on ESP — it is where anything latency-sensitive or flash-cache-independent has to live — so this is a real path, not a fixture curiosity.

Positive control when a fix lands

The iram row must link on BOTH targets with --dce, and the four ro and ctl rows must still link, so a fix that disables the IRAM path or the pass wholesale is caught. Wire the iram variant into test-emit-obj with --dce at that point; it runs without the pass today.


2026-09-22 (frankh-c0) — THIS IS NOT AN --emit-obj FOOTNOTE: IT IS THE GATE ON A MEASURED -66% ACROSS EVERY PROGRAM

What I was doing. Working the emitted-size/DCE group from its next entry point and testing the one live question the 63KB-hello-world ticket leaves open — "whether the DEFAULT -O level should enable the pass." The way to answer that is to turn it on and run the tier, so I did.

PROMISE, measured at 19768262e, compiler c24a11f2beb3, plexus. One-line change, OptLevel >= 3 to >= 2 in compiler.pas:

program            --no-dce     default      delta
hello                 74192       25040       -66%
calcdemo             386456      112024       -71%
maze                 379140      104708       -72%
lispdemo             399188      124756       -68%
mathdemo             482012      187100       -61%
satdemo              394388      119956       -69%
console_2048         404716      126188       -68%
mandelbrot           414140      139708       -66%
raytracer           1490596      536228       -64%
                   --------    --------
TOTAL (9 programs)  4424828     1475708       -66%

Population: the nine examples/** programs that build unattended at HEAD. examples/parallel/collatz.pas is excluded and is NOT a DCE failure — it needs --threadsafe and fails identically under --no-dce and under the baseline binary, which is the control that says so.

AND THE SELF-HOST FIXEDPOINT CONVERGES WITH THE PASS ON BY DEFAULT — two rounds, 4ae692b547e5, on the largest Pascal program we have (4978 procs). The compiler itself only shrinks 1.9% (7910257 -> 7759281 code), which is the expected shape: a hello world is ~99% unreachable RTL, a compiler uses most of itself.

THE WALL, MEASURED RATHER THAN REASONED

--emit-obj has no DCE-defaulting line of its own — that was tried and reverted — but it does not need one. The -O rule is unconditional on output mode, so promoting the pass to -O2 turns it on for --emit-obj by the back door, and a plain no-flags ESP object build goes from linking to crashing the linker:

pascal26 -Fulib/rtl --emit-obj --target=riscv32 --platform=esp \
         test/esp_obj_rodata_iram.pas o.o
riscv32-esp-elf-gcc -fno-builtin -nostartfiles -Wl,-e,main o.c o.o -o o.elf

  DEFAULT  link rc=1  collect2: fatal error: ld terminated with signal 11
  NODCE    link rc=0

rc is the LINKER's own exit status, not a tail's — the first reading of this took the rc of the pipeline's last stage and got rc=0 for both arms, which is the wrapper-versus-job error in miniature and is why the rows above were re-run.

The object sizes say the same thing from the other side: 34696 B of code under the new default against 262000 B with --no-dce and 262000 B from the baseline binary — so the pass really is running on a path nobody asked it to run on.

WHY THIS CHANGES THE RANKING AND NOT JUST THE RECORD

This ticket reads as one broken path on one platform under one flag. It is the single thing standing between every pxx program and a two-thirds size cut, and that is invisible from its own summary, which is why the summary now says it. bug-a-a-pascal-hello-world-is-63kb-after-emission-size-dce is now wired blocked-by: this, so the ranker carries that instead of a reader having to notice it.

WHAT THIS DOES NOT SHOW

The tree was reverted and rebuilt byte-identical to c24a11f2beb3, and no full tier was run at -O2-with-DCE. So this names ONE wall, found by the cheapest available probe; it does not establish that the wall is the only one. The quick gate found a second thing first and it is paperwork rather than a defect: test_dce_riscv32_stub_calls fails because its control arm is the DEFAULT invocation, which under the promotion is no longer "without the pass" (235052 >= 235052). Five Makefile rows share that shape — riscv32, i386, xtensa, arm, and the NilPy ESP object row — and every one of them wants its off arm respelled --no-dce when the promotion eventually lands. Note they fail LOUDLY only because they assert a strict shrink; an <= would have passed while measuring nothing.

WHAT WOULD RETIRE THIS AS A BLOCKER: the IRAM object linking cleanly, then the five control arms respelled, then a full tier at -O2 with the pass on, green, with skip_holes == 0. That is the O-lane's PROOF gate and it is satisfiable on plexus now that the corpora are installed.

Log