The HW-entropy intrinsics are unreachable on every ESP target
- Type: bug (compiler core; ESP campaign) — Track A file ownership, S tag.
- Filed: 2026-08-30 by frankB, re-verifying [[feature-random-library]]'s blockers at HEAD on the coordinator's request.
- Measured against pin v395 (
stable_linux_amd64/default/pinned).
Repro — five lines, no library involved
program hw;
var b: Boolean;
begin
b := __pxxCpuHasHwRandom;
if b then b := False;
end.
| target | result |
|---|---|
| x86-64 (native) | builds |
| aarch64 | builds |
| arm32 | builds |
| i386 | builds |
--target=xtensa --platform=esp --esp-profile=bare |
error: undefined variable (__pxxCpuHasHwRandom) |
--target=riscv32 --platform=esp --esp-profile=bare |
error: undefined variable (__pxxCpuHasHwRandom) |
Cause — one guard, and it is measured, not inferred
compiler/pasparser_prog.inc:1056 decides whether a program pulls the builtin
unit by scanning its tokens for names that need it:
if (Tokens[i].Kind = tkIdent) and
(CaseEqual(GetTokenStr(i), '__pxxcpuhashwrandom') or
CaseEqual(GetTokenStr(i), '__pxxhwrandom64')) and (not TargetIsEspClass) then
needsBuiltin := True;
On an ESP-class target the arm never fires, the builtin unit is not pulled, and the name resolves to nothing. The comment directly above it states the mandate this breaks:
"lib/rtl/random.pas is required to call them with no per-arch anything of its own — that mandate is the whole reason they are compiler-side."
random.pas honours that mandate exactly — HWEntropyAvailable is a bare
__pxxCpuHasHwRandom with no {$ifdef} — and its own comment says
"__pxxCpuHasHwRandom answers truthfully on x86-64 and False everywhere else,
which routes those targets to tier 2 on its own." On ESP it does not answer
False; it fails to exist, and the program does not build at all.
(not TargetIsEspClass) appears on 22 arms of this function, so it is the
file's established pattern rather than a slip on this one line — which is why
this is filed for Track A to decide rather than guessed at from Track B.
Consequence
lib/rtl/random.pas is uncompilable on xtensa and riscv32. Measured with a
driver program that only does v := Random64:
| target | result |
|---|---|
| native / aarch64 / arm32 / i386 | builds |
| xtensa + esp + bare | random.pas:321: undefined variable (__pxxCpuHasHwRandom) |
| riscv32 + esp + bare | same |
| riscv32 hosted | atomics need machine-mode CSR access (mstatus) — the separate, known limitation |
xtensa is the primary ESP target (the owner's S2/S3 hardware), so this is not a corner.
Control — the ESP failures above are NOT this bug
Run first, because without it the measurement is worthless. An empty
program, no uses random:
| target | result |
|---|---|
--target=xtensa --platform=esp (no --esp-profile=bare) |
fails: external (dynamic) symbols are not supported ... (first one: calloc) |
--target=xtensa --platform=esp --esp-profile=bare |
builds |
--target=riscv32 --platform=esp --esp-profile=bare |
builds |
--target=riscv32 hosted |
builds |
So the calloc error on the non-bare ESP profile belongs to any program on
that profile and has nothing to do with random; and hosted riscv32 builds an
empty program fine, which is what makes its random atomics failure real. Only
the bare rows isolate this ticket's defect.
Relationship to the two tickets that closed
Both of [[feature-random-library]]'s recorded blockers are in done/, and
both fixes hold — this is not a regression of either:
- [[bug-a-xtensa-refuses-to-lower-an-unreachable-syscall]] — the
unsupported node in IR codegen: syscallerror is gone. Confirmed by its absence at HEAD; xtensa now stops later, on a different symbol. - [[feature-a-rdrand-cpuid-compiler-builtins]] — the intrinsics exist and work on all four non-ESP targets.
What happened is that a new wall stood behind the old one. That ticket's
summary put "the ESP RNG register" in its stated scope; the x86-64 half landed
and the ESP half did not, and the guard makes the gap present as an undefined
symbol rather than as a missing feature. A ticket moving to done/ does not
mean the thing it was blocking now builds — which is the same lesson
feature-random-library already carries in its own words: "a ranked queue says
a ticket is UNBLOCKED, not that it has WORK LEFT IN IT."
Options, for whoever takes it
- Provide the intrinsics on ESP.
__pxxCpuHasHwRandomreturns True and__pxxHwRandom64reads the ESP RNG register — the tier-1 support the parent ticket named. Most work, most value: it is the only tier-1 entropy these targets could have. - Drop the guard on this arm only, so the builtin unit is pulled and the
existing "False everywhere else" body answers False on ESP.
randomthen compiles and routes to tier 2, exactly as its comment predicts. Cheapest, and it makes the library's stated design true. Needs someone who knows why the other 21 guards are there — if the builtin unit cannot be pulled on ESP at all, this option does not exist and (3) is the fallback. - A stub that answers False, reachable on ESP without the full builtin unit, if (2) is impossible for unit-size or startup reasons.
Not chosen here: this is a Track A judgement about the builtin unit on ESP, and
Track B has no standing to pick. Not worked around in random.pas — an
{$ifdef} there is precisely the compiler-appeasement workaround the platonic
-code rule forbids, and it would silently delete the mandate the intrinsics
exist to serve.
Gate
make test + self-host byte-identical, plus a cross build of a program using
lib/rtl/random.pas for xtensa and riscv32 under --esp-profile=bare. The
five-line repro above is the regression test.
FALSIFIER RUN 2026-08-30 (frankS) — the 22 arms encode a REAL constraint, so option (2) is wrong and (3) is right
The coordinator's read was that (2) drop the guard on this one arm is right,
and asked to be overturned if the code says otherwise. It does. Measured, not
reasoned: compiler cf30672a934e, HEAD 252e9539d.
The one experiment that settles it
needsBuiltin does exactly one thing — ParseUsesUnitAmbient('builtin')
(pasparser_prog.inc:1340). So the question "is the guard structural or a copied
habit?" reduces to can the builtin unit be pulled on bare ESP at all?
| target / profile | empty program | uses builtin; |
|---|---|---|
| bare xtensa | ok | FAILS — undefined variable (PxxSciDigits17) in ./compiler/builtin/builtin.pas |
| bare riscv32 | ok | FAILS — same error, same file |
| hosted xtensa | ok | ok |
builtin.pas does not compile on bare ESP. The empty-program control rules
out a general bare-profile breakage — the failure is specifically pulling this
unit.
So the guard on those 22 arms is a real structural constraint, not a copied
habit: it exists because the builtin unit is uncompilable on a bare-metal
target, which is exactly what TargetIsEspClass's own header in util.inc says
it is for ("guarding 'may I pull this RTL unit'", and being wrong "silently
drags an uncompilable unit into a bare-metal build").
Consequence for the three options
- (2) drop the guard — WRONG. It converts a clean
undefined variablein user code into a compile failure insidebuiltin.pas, which is strictly worse: the error now names a compiler-shipped file and a symbol (PxxSciDigits17) that has nothing to do with entropy. - (3) a False stub that does NOT pull the builtin unit — CORRECT, and it is
not "(2) with extra machinery": the machinery is the whole point, because the
pull is the thing that cannot happen here. The body's "False everywhere else"
answer is still what we want; it just cannot arrive via
builtin.pason bare. - (1) real ESP intrinsics — unchanged, its own S ticket, not a prerequisite.
SCOPE CORRECTION — this is bare-ESP-only, not "xtensa and riscv32 at all"
program ur; uses random; begin end. with -Fulib/rtl:
| config | result |
|---|---|
| hosted xtensa | ok — compiles fine |
| bare xtensa | FAILS at __pxxHwRandom64 (this bug) |
| bare riscv32 | FAILS at __pxxHwRandom64 (this bug) |
| hosted riscv32 | FAILS — but elsewhere, the separate atomics/CSR defect |
| host x86-64 | ok |
TargetIsEspClass is (xtensa or riscv32) and EspBareBoot, so on hosted
xtensa the guard never fires and random.pas builds. The ticket's framing —
"random.pas does not compile on xtensa or riscv32 at all" — is too broad, and
the "xtensa is the primary target" urgency argument does not apply to the hosted
profile that the S campaign's oracle work actually runs on.
Two distinct defects are in these rows and they should not be conflated: the hw-entropy guard (bare only, this ticket) and hosted riscv32's atomics failure (a different point in the file, not this ticket).
Not done here
The fix itself is not written — this is the falsifier the dispatch asked for, and it changed the answer. Whoever implements (3) should confirm how a False stub reaches a bare target without the builtin pull, since that mechanism is precisely what the measurement above shows is unavailable.
Follow-on, same day: the constraint is real but SMALLER than it looked
[[bug-a-builtin-pas-calls-a-declaration-that-esp-compiles-out]] (A p50) was filed
off the falsifier's error message. Its DISPROOF RUN narrows what that ticket can
do for this one: the bare-ESP builtin failure is two unguarded call sites
(PXXVarBinOp, PxxSciDigits17), not a root cause standing behind all 22 arms —
15 of the 17 declarations in the same guarded block have no caller at all and are
cleanly excluded.
Nothing here changes. Option (3) — a False stub that reaches a bare target
without the builtin pull — was already chosen on the ground that it must hold
whether or not the pull is ever repaired, and that ground is untouched. What does
change is the expectation someone might have carried in: do not wait for the
builtin.pas fix hoping it retires this arm. It will not. Repairing those two
call sites makes uses builtin; compile on bare ESP; it says nothing about
whether pulling the whole builtin unit is the right way to reach
__pxxHwRandom64 on a bare target, which is the actual question here.
The open question for whoever implements (3) is unchanged and is still the ticket's real content: how does a False stub reach a bare target without the builtin pull? That route is precisely what the falsifier shows is unavailable.
OPEN DISAGREEMENT: the SCOPE CORRECTION's hosted xtensa: ok row does not reproduce
Not resolved here, deliberately. Recorded so neither framing gets leaned on until someone says what "hosted xtensa" names. This row matters because it was used to argue the ticket's original framing was too broad and to lower its urgency.
frankS measured hosted xtensa → ok, compiles fine. I get calloc, in four
independent runs — at the pin (which predates everything I touched) and at
HEAD, with unmodified random.pas, on both program shapes.
Exact invocations, not labels, since the labels are the suspected problem:
./stable_linux_amd64/default/pinned --target=xtensa -Fulib/rtl ur.pas out
./compiler/pascal26 --target=xtensa -Fulib/rtl ur.pas out
where ur.pas is BOTH
program ur; uses random; begin end. (frankS's shape)
program ur; uses random; var v: UInt64; begin v := Random64; ... end.
All four:
pascal26:2: error: target xtensa: external (dynamic) symbols are not supported
on this target (first one: calloc)
A third source agrees with my reading. This ticket's own CONTROL table
already reports that same calloc error for --target=xtensa --platform=esp
(non-bare), and
[[bug-a-hosted-xtensa-diverges-from-the-oracle-on-21-cross-programs]] classifies
"external (dynamic) symbols" on hosted xtensa as by design. If that
classification is right, uses random cannot build on hosted xtensa at all, and
the ok row cannot be describing the configuration its label names.
No ticket filed for the calloc limitation — it is already recorded as by-design in the ticket above. Checked before filing rather than after.
What is NOT concluded: that frankS was wrong. Two sessions measured different things under one name, which is the likeliest reading, and neither the scope correction nor the original framing should carry weight until the configurations are pinned down.
Log
- 2026-08-30 — resolved, commit ba14f5f56.