__pxxcoswitch and __pxxclone have no riscv32 arm
compiler/ir_codegen_riscv32.inc handles 58 IR node kinds; aarch64 handles 65.
Six of the seven-way difference are IR_CLONE, IR_COSWITCH, IR_IMTADDR,
IR_IO_LOCK, IR_IO_UNLOCK, IR_MULHI. (The seventh, IR_WRITELN, was a
false positive — it is the trailing label of IR_WRITE, IR_WRITELN:.)
Now five, not six: IR_IMTADDR was RETIRED on 2026-09-09 — it is not an
absent node kind any more, it is not a node kind. This audit reached that
conclusion from the opposite direction to the grep that retired it (a
cross-target arm census, not an identifier search), which is why the two count
as two readings and not one.
Three of the remaining five cannot be reached on riscv32, so they are not bugs:
IR_MULHI—ir.inc:7781errors at the emission site for any non-64-bit target and points atMulHiU64in lib/rtl.IR_IO_LOCK/IR_IO_UNLOCK—ir.inc:13213gates them to x86-64/i386/aarch64/arm32 under--threadsafe. (IR_IMTADDRwas the fourth: no emitter anywhere in the frontend, and interface dispatch through aTInterfacedObjectrunning correctly on riscv32 today. Retired outright, so riscv32's missing arm is now missing nothing.)
Two are reachable. AN_COSWITCH (pasparser_expr.inc:4132,
pasparser_stmt.inc:5056, pyparser.inc:46140) and AN_CLONE
(pasparser_expr.inc:4170, pyparser.inc:46178) are ungated builtins.
Repro
program cosw;
var a, b: Pointer;
begin
a := nil; b := nil;
__pxxcoswitch(@a, @b);
WriteLn('back');
end.
$ pascal26 cosw.pas cn # ok
$ pascal26 --target=arm32 cosw.pas ca # ok
$ pascal26 --target=riscv32 cosw.pas cr
pascal26:7: error: target riscv32: unsupported node in IR codegen: coswitch
Suspected site
compiler/ir_codegen_riscv32.inc. Donor arms: ir_codegen_arm32.inc handles
both, and it is the closer model than aarch64 — same 32-bit pointer width, same
IR_ARG chain shape (ir.inc:7692 for coswitch, ir.inc:7711 for clone).
IR_CLONE's comment names the SysV arg regs, so the register mapping is the
part that has to be re-derived for the RISC-V calling convention rather than
copied.
Low priority: both are low-level builtins with no in-tree riscv32 caller. The failure is loud and at compile time.
palthread.pas landmine — read this before you lift the __pxxclone guard
Found by frank-coordinator grepping for the sibling of the PalBackendMmapAnon
MAP_ANONYMOUS fix (97e96fc1b); scope corrected by frankS; the flag value
below is measured by frankA rather than cited.
Not a bug today. lib/rtl/palthread.pas defines MAP_ANON_PRIV = $22 at
:84, used at :161 to mmap every thread stack. That constant sits outside
the arch split, which starts at :87, while the syscall numbers sit inside
it — and both xtensa and riscv32 fall to the {$else} at :120, where
SYS_mmap = -1 and the __pxxclone compile-error fires first. So nothing is
silently wrong right now.
It becomes wrong the moment this ticket lands, because lifting the guard removes the thing that is currently saving it.
The two targets are NOT symmetric — this is the part to get right:
SYS_mmap |
MAP_PRIVATE|MAP_ANONYMOUS |
|
|---|---|---|
| riscv32 | 222 (generic ABI), placeholder is -1 |
$22 = 34 — already correct, same as x86-64/i386/aarch64/arm |
| xtensa | 80 (its own numbering; generic 222 is Unknown syscall 222) |
$802 = 2050 — the sole outlier |
So: when moving MAP_ANON_PRIV inside the arch split, xtensa takes $802 and
every other arch takes $22 (frankS's wording, and the reason for it is that
a note grouping the two targets invites someone to "fix" riscv32's already-correct
$22 to $802 and reproduce the EBADF that 97e96fc1b just removed).
riscv32 needs the syscall block only; xtensa needs the syscall block and the
flags constant.
$800 is MEASURED, not read off a table or taken from a comment. Under
qemu-xtensa -strace, mmap2 with flags $800 alone is decoded by qemu as
MAP_ANONYMOUS and returns EINVAL (no MAP_PRIVATE/MAP_SHARED); $802 is
decoded as MAP_PRIVATE|MAP_ANONYMOUS and maps; $22 is decoded as
MAP_PRIVATE|0x20 — 0x20 is not a named flag on this target — and returns
EBADF, mapping fd -1. That is qemu's own flag decoder naming the bit,
independent of builtinheap.pas:971's comment, which had been the only source.
Scope of that measurement (frankS): qemu's decoder is qemu's, not the
kernel's — but for this claim qemu is the right authority rather than a weaker
one, because hosted xtensa runs under qemu-user, so it is the execution target
for the profile where PalBackendMmapAnon and the thread-stack mmap actually
run. Read it as measured under qemu-xtensa 10.2.1, the execution target for
the hosted profile — not as a claim about silicon. The bare/ESP profile never
reaches mmap, so nothing there depends on it.
RE-MEASURED 2026-09-19 (frankS) — and it is NOT in the per-thread-state group
This was handed to me as the third member of a group about per-thread state that is not actually per-thread, with the grouping explicitly flagged as a lead to check and with the discriminator supplied: does the defect need more than one thread to exist? It does not, and the answer took two compiles.
__pxxcoswitch — the named error is gone
program cosw2; { zero threads created }
var a, b: Pointer;
begin a := nil; b := nil; __pxxcoswitch(@a, @b); end.
./compiler/pascal26 --target=riscv32 ... -> ok: ... rc=0
Gained at fc70d0cbe, 2026-09-02, "riscv32 stackful coroutines — httpdemo
crosses". The ticket has been carrying a quoted error string that the compiler
stopped producing 17 days ago.
__pxxclone — refused, but a layer above the one this ticket names
(plain) error: __pxxclone (thread creation) requires --threadsafe or
{$threadsafe on}: the default heap/ARC/console-I/O runtime is
not thread-safe
(--threadsafe) --threadsafe is x86-64/i386/aarch64/arm32 only: the heap/ARC/I-O
locks are not implemented on this target yet
There is no "missing arm" to reach. The gap is the threadsafe runtime on
riscv32; a codegen arm for clone would be unreachable until that exists, so
implementing one against this ticket would be work with no observable.
Why it is not in the group
Both observables are compile-time refusals, with zero threads running. The group's other two members are silent wrong ANSWERS at runtime that require at least two threads to exist at all — one thread reading another's errno, one thread reading another's TLS block. This is a plain porting gap that announces itself, and it is the good kind: it stops and names what is missing.
Saying so rather than making it fit, which is what I was asked for.