← board

No program declaring a class can build for --esp-profile=bare

The boundary, measured

Compiler fe1e9c37d322. Three programs, --esp-profile=bare:

program result
begin writeln(1) end. builds
five lines declaring a class with one AnsiString field FAILS, both chips
that same class program, --platform=posix builds
pascal26:2: error: unresolved forward: PXXClassFinalize
  in: ./compiler/builtin/builtinheap.pas

So it is the profile, not the architecture, and it is classes, not exceptions — which is what the row that exposed it looked like.

Cause

PXXClassFinalize's body is inside the {$ifndef PXX_ESP} block that opens at builtinheap.pas:3671 and closes at 4356. Its forward declaration was in the interface list, which is not inside any guard. The ESP profile therefore declared a procedure whose body it never compiled.

The file already carried a comment predicting this exactly. Above PXXRecordZeroManaged:

Forward only where the BODY exists — PXXClassFinalize is itself inside {$ifndef PXX_ESP}, so an unconditional forward left it unresolved on the ESP profile (test-emit-obj: "unresolved forward: PXXClassFinalize").

A later change moved the forward into the interface — "it is declared in the interface now, with the rest of the code generator's entry points" — and re-introduced the defect the comment existed to prevent, because the interface list is outside the guard. A comment that names its own failure mode, names the error string, and is tripped anyway is an argument for a guard, not for better prose.

Fix

Guard the declaration to match the body. Every caller was already guarded (PXXObjFree's call is inside {$ifndef PXX_ESP}), so nothing under PXX_ESP loses a symbol it uses.

Inertness proof for the host compiler, which is the part worth quoting: make compiler/pascal26 printed converged after 1 round(s) — a real recompute, not the stamp path — and produced a byte-identical binary, sha256 unchanged at fe1e9c37d322. The compiler is not built with PXX_ESP, so the change provably cannot affect it.

Reaches $(PXX_STABLE) at the NEXT pin, not this one

Pin v404 (8844c8c42) landed ~20 minutes before this fix, and make pin freezes compiler/builtin/**. The pinned compiler still carries the unguarded declaration, so Track B and E builds — which use $(PXX_STABLE) by rule — keep hitting this until the next pin. Not a reason to pin: the exposed path is one where no class program could build at all, so there is no working behaviour being regressed. Recorded so a later reader is not confused by a fix that works at HEAD and not in a B-lane build.

Why it was invisible, which is the durable half

test-esp-bare is in zero testmgr tiers and referenced by no script — see bug-t-the-esp-bare-suite-is-in-no-tier-so-nothing-ever-runs-it. This defect was found on that suite's first ever execution. The suite is 27 assertions and test-esp-softfloat another 24, and on a box with the Espressif qemu builds installed none of them skip.

A second defect made the diagnosis harder and is fixed alongside: tools/esp_run_bare.sh sent the compile's stdout to /dev/null while pascal26 writes diagnostics there, so a build failure and a program that ran and printed nothing were the same observation — empty output, nonzero rc. The row survived a repro at ESP_RUN_TIMEOUT=40 and a second chip before a by-hand compile showed the real error. The runner now prints the compiler's diagnostic on failure and is unchanged on success.

If you are hitting this RIGHT NOW, read this first

Symptom: error: unresolved forward: PXXClassFinalize when building any program that declares a class for --esp-profile=bare.

Discriminator, and it takes one command: build the same program with compiler/pascal26 at HEAD instead of $(PXX_STABLE). If HEAD builds it and the pinned compiler does not, you are seeing the pin, not a regression.

Why: fixed at HEAD in 6758c7ce7, which lands in compiler/builtin/ builtinheap.pas. make pin freezes compiler/builtin/**, and pin v404 (8844c8c42) was cut ~20 minutes before the fix. So the pinned compiler still carries the unguarded declaration and will until the next pin.

This is not a regression in either direction. Nothing that used to build stopped building: no class program could build for this profile at all, on either chip, for as long as the profile has existed. The pinned compiler is correctly older — the same shape as the __GNUC__ case in CLAUDE.md's Track B notes, where a green under the pin was correct about a different compiler.

Do not work around it by reshaping the source. Build that program at HEAD, or wait for the next pin.