← board

Assert is undefined on --esp-profile=bare

Measured

compiler/pascal26 = c9de36a3754e at d6de711d1, converged after 1 round(s).

program a;
var i: Integer;
begin
  i := 1;
  Assert(i = 1);
end.
build result
--target=esp32s3 --esp-profile=bare pascal26:5: error: undefined variable (Assert)
--target=esp32c3 --esp-profile=bare same error
--target=riscv32 --platform=esp ok, procs=603
--target=xtensa --platform=esp ok, procs=606
--target=xtensa --xtensa-abi=windowed --platform=esp ok, procs=606
--target=riscv32 (hosted) ok, procs=580
x86-64 ok, procs=554

So this is the BARE profile only, not ESP and not xtensa. Remove the Assert line and the same program builds for bare on both chips.

Cause

pasparser_stmt.inc:6443 recognises a bare Assert( only when every clause holds, and the last one is (FindProc('__pxxAssert') >= 0). __pxxAssert lives in compiler/builtin/builtin.pas, and pasparser_prog.inc guards the needsBuiltin injections with not TargetIsEspClass. Bare therefore has no __pxxAssert, the alias does not fire, and the name reaches the ordinary identifier path — which is why the diagnostic calls a procedure a variable.

The soft-alias discipline is right and is not the bug. It exists so a user's own Assert still wins (same shape as Move/FillChar), and a guard that falls through silently is the correct behaviour when a user routine is what was meant. What is missing is the helper, not the guard.

Why the obvious fix is the wrong one

Not "pull builtin on bare". Verified rather than assumed — uses builtin under --esp-profile=bare fails on both chips:

pascal26:1161: error: undefined variable (PXXVarBinOp)     in builtin.pas
pascal26:1724: error: undefined variable (PxxSciDigits17)  in builtin.pas

pasparser_prog.inc:1039 says of builtin that ESP "cannot compile [it] at all", and that comment is TRUE — checked, because a comment and the code disagreeing is the case where one of them is wrong and you do not know which. PxxSciDigits17 is declared in builtinheap.pas, so the two are companion-unit breakage rather than anything about the ISA, but the effect stands.

Nor is the size policy wrong: bare deliberately carries no RTL because softfloat alone is ~54–64KB of flash and the typical MCU program has no float in it. See [[feature-a-complete-the-builtin-unit-on-the-esp-class-targets]] (done, and about the IDF profile, which is why it did not reach this).

The fix, and what makes it cheap

A minimal __pxxAssert needs AnsiString, +, WriteLn and Halt, and all four already work on bare — measured, not assumed:

program s; var x: AnsiString;
begin x := 'a'; x := x + 'b'; WriteLn(x); end.
--target=esp32s3 --esp-profile=bare  ->  ok, code=46516B (empty: 46436B)

Eighty bytes. So a small dedicated unit carrying only __pxxAssert (with AssertErrorProc and FPC's message shape — the message REPLACES Assertion failed, position appended, Halt(227)), injected on demand from the existing ESP-class token scan exactly the way needsSoftFloat is (pasparser_prog.inc:1067 scan, :1739 injection), costs nothing to a program with no assertion in it.

Whatever fixes this must land a row that RUNS

test_esp_bare.pas contains no Assert, which is why 29 green assertions on both chips did not see this. The row belongs in test-esp-bare — but note that target is in ZERO tiers ([[bug-t-the-esp-bare-suite-is-in-no-tier-so-nothing-ever-runs-it]]), so a row added there today is watched by nothing. Assert the OUTPUT under qemu (tools/esp_run_bare.sh), not just that it compiles: Assert(False) must print the composed message and stop, and a passing Assert must print nothing.

CORRECTION, same evening, by the author — "WriteLn works on bare" was a COMPILE, not a run

The section above said "a minimal __pxxAssert needs AnsiString, +, WriteLn and Halt, and all four already work on bare — measured, not assumed", and cited an 80-byte size delta as the evidence. Three of the four work. writeln compiles and emits nothing, and the 80 bytes were the string, not the output.

What I actually measured was ok: from the compiler. On a profile whose only evidence of having run is what appears on a wire, that is not a result — and the word "measured" beside it made the claim read as the checked part. This is the same shape as the size ticket I corrected an hour earlier, pointed at myself.

The device says it plainly. A program that writes MMIO-A, then WriteLn, then MMIO-C prints MMIO-A and MMIO-C on both chips and nothing between them: the program RUNS, execution continues past the writeln, and the output is simply gone. So it is a silent no-op and not a crash — which is why a build check could never have separated them.

And it is INTENDED and already written down, which I should have read before claiming the opposite: docs/targets/esp32.md:70"writeln/readln are intentionally no-ops — there is no console. Output goes through your own UART writes." test/test_esp_bare.pas has hand-rolled PByte($60000000)^ := since the day it was written, and the whole suite does the same.

Consequence for the fix, and it is not cosmetic. A helper that reuses builtin's body compiles on both chips and prints NOTHING when the assertion fires — a silent Halt(227). That is the worst available outcome for an assertion: strictly less informative than the compile error this ticket is about, because at least the compile error was loud. The helper has to write the composed message to the UART0 TX FIFO itself (defs.inc:1847: "UART0 FIFO is MMIO at 0x60000000 on both"), which is also what the docs tell every bare user to do by hand.

I know both of those because I wrote the naive version first and ran it. The compile-only version passed every check I had — both chips built, the no-assert program stayed byte-identical to the canary baseline — and produced no output at all on the device.

RESOLVED 2026-09-06 (frankF)

compiler/builtin/espassert.pas carries __pxxAssert for this profile; pasparser_prog.inc pulls it on demand from the ESP-class token scan, gated on EspBareBoot and never ambient. test/test_esp_bare_assert.pas plus two test-esp-bare rows diff the device serial against the x86-64 oracle.

Measured on both chips under Espressif qemu, compiler c756f893d9b4, converged after 1 round(s):

boom (test_esp_bare_assert.pas, line 50).    byte-identical to the oracle
NOT REACHED does not print                   so Halt(227) genuinely stops
Assertion failed (a4.pas, line 5).           the no-message form, also exact

Zero cost to a program without one: the empty program stays 46436B/75 procs on esp32s3 and 57900B/72 on esp32c3, which are the size-canary baseline numbers, and var assert: Integer does not pull the unit.

gate.sh quick GREEN run before committing, which is the only way the FPC seed canary executes at all — it is skipped on a clean tree, and it PASSED.

Not tier-verified. Tonight's landing hold expired rather than a full tier publishing, and the one requested full published a verdict with no manifest. Newest full remains 6d04b14cd88d. This rests on gate.sh quick, the fixedpoint, and 31 executed qemu assertions — not on a tier.

Log