← board

DCE drops a called body on the riscv32 IDF profile

Repro

printf 'class B:\n    def __init__(self, n):\n        self.n = n\ndef main():\n    print(B(3).n)\nmain()\n' > /tmp/d.npy
./compiler/pascal26 --dce --target=riscv32 --platform=esp -Fu$PWD/lib/rtl \
  -Fu$PWD/lib/rtl/platform/esp /tmp/d.npy /tmp/d.o
# pascal26:..: error: unresolved forward: PyUserObjGetattrTry
./compiler/pascal26 --dce /tmp/d.npy /tmp/d_x64            # builds
./compiler/pascal26 --dce --target=arm32 /tmp/d.npy /tmp/d_arm   # builds
./compiler/pascal26 --target=riscv32 --platform=esp -Fu$PWD/lib/rtl \
  -Fu$PWD/lib/rtl/platform/esp /tmp/d.npy /tmp/d.o       # builds (no --dce)

Measured on the full class/list demo (examples/esp32/nilpy-c3/main/main.npy) 2026-09-19; the reduced repro above is the same shape and is NOT separately re-run -- run it before trusting it.

Where to look first

PyUserObjGetattrTry's body is a try ... except on AttributeError do block. The two things that differ between the passing and failing cells are the target (riscv32) and --emit-obj/--platform=esp, so the suspects are an edge DCE's riscv32 reachability scan does not record (a call emitted through a path that bypasses RecordInternalCall), or an exception-frame edge only this profile emits. bug-a-dce-refuses-every-target-except-x86-64 is the ticket that turned DCE on for riscv32.

Resolution (frankS, 2026-09-19)

The two callers are pydynattr_hasattr and pydynattr_has_any_v, and --dce-report names them itself: kept (holds a stub target). Both were dead, both were kept whole, and both call PyUserObjGetattrTry, which nothing live reaches. 71 bodies were kept that way on riscv32 in this program.

POSITIVE CONTROL, and NOT the pinned compiler: pin v412 predates the IDF heap arena and refuses this program before DCE runs, so it fails for an unrelated reason. The control is HEAD with the DceMark loop reverted -- riscv32 then says unresolved forward: PyUserObjGetattrTry and xtensa still builds.

SIZES, same program, examples/esp32/nilpy-{c3,s3}/main/main.npy, measured through PXX_EXTRA_FLAGS=--dce ./build.sh qemu-assert, flashed image (the .bin esptool writes) and pxx's own code=:

chip image off image on code off code on
ESP32-C3 (riscv32) 3,326,224 B 2,307,648 B 2,995,036 B 2,074,596 B
ESP32-S3 (xtensa) 3,246,288 B 1,996,848 B 2,901,167 B 1,741,723 B

Both demos still RUN with --dce: output == CPython, one boot, on both chips. Neither fits the stock 1 MB factory partition even so, so the 4 MB partition table stays and the demos are still built WITHOUT --dce -- the flag is now measurable against them (PXX_EXTRA_FLAGS), not default. What would change that is the next row: DCE is worth 31% on riscv32 and 40% on xtensa here, and 1 MB needs 66%.

NOT closed by this ticket, banked as its own: riscv32 keeps 870 bodies live against xtensa's 735 on the identical program, ~340 KB of code -- bug-a-riscv32-dce-keeps-135-more-bodies-than-xtensa-on-one-program.

Log