← board

riscv32 codegen has no Variant support

Found 2026-08-23 while cross-checking [[bug-a-a-variant-comparison-does-not-coerce-a-stringy-operand]] across the targets that share the PXXVarBinOpPas helper.

Repro

program tiny; var v: Variant; begin v := 1; writeln(v); end.
$ ./compiler/pascal26 --target=riscv32 -Fulib/rtl tiny.pas out
pascal26:1: error: target riscv32: unsupported node in IR codegen: var_store

x86-64, i386, aarch64 and arm32 all compile it and print 1. Reproduces with the PINNED binary as well as HEAD, so it is pre-existing, not a regression.

Why it is worth a ticket rather than a shrug

Low priority — riscv32 is a bare-metal profile and variants are not what an MCU program reaches for. But it is worth recording because two tickets already describe riscv32 as one of the four targets whose variant binops "route through PXXVarBinOpPas". They do not route anywhere: the program never gets past codegen. Any cross-target agreement claim about variants covers four targets, not five, and a session that assumes otherwise will look for a bug that cannot exist.

var_store is likely not the only missing node — it is just the first one a one-line program hits. Whoever takes this should enumerate the variant IR ops against ir_codegen_riscv.inc rather than fixing the one the error names.

Gate

Track A's, plus tiny.pas above compiling and running under qemu-riscv32, and the variant differential in test/test_variant_comparison_coerces_a_stringy_operand.pas producing ALL OK there as it already does on the other four targets.


Resolution (2026-08-27)

Enumerated rather than fixing the one op the error named, as the ticket asked. var_store was the first of four missing pieces, not the only one — each found by re-running the repro after the previous one landed:

missing symptom
IR_VAR_STORE unsupported node in IR codegen: var_store — the filed one
IR_LOAD_SYM for a tyVariant symbol a variant in value position tried to LOAD a 16-byte slot into one register instead of yielding its address
the write dispatch write of this type not supported (hosted) on writeln(v)
IR_VAR_BOX / IR_VAR_BINOP reached only once a store compiled

What landed

Six helpers plus three arms in ir_codegen_riscv32.inc, modelled on arm32 — the 32-bit peer with the same 16-byte slot (tag +0, zero word +4, 8-byte payload +8) — and not on aarch64, whose payload is one register.

The 32-bit-specific hazards, each handled explicitly because each has been a real bug on a sibling target:

PXXVarBinOp's five arguments all land in a0-a4: riscv32 pushes call arguments and then loads a0..a7 from the block, and five is under the eight-word spill line, so no stack argument is needed the way arm32 needs one.

Verification — against qemu, not by inspection

The ticket's own repro runs and prints 1. Beyond it: test_cross_variant and test_cross_variant_single were SKIPped on riscv32 in the Makefile with the comment "backend feature gap" — this bug was that gap, and both are un-skipped and now differential-green against x86-64. The gate's named file, test_variant_comparison_coerces_a_stringy_operand.pas, produces ALL OK under qemu-riscv32, which is what proves PXXVarBinOpPas is genuinely reached here for the first time.

New: test/test_cross_variant_payload_widths.pas, one row per hazard above, run as a differential so the expectations are a 64-bit target's answers rather than a hand table. Verified identical on i386, arm32, aarch64 and riscv32, so it guards the whole 32-bit family, not just this fix.

Filed, not chased

[[bug-a-a-variant-assigned-to-itself-becomes-empty]] — v := v EMPTIES the variant on every target (pinned included; FPC leaves the value). riscv32 matched the x86-64 oracle on that row because the oracle is wrong too. Root cause is pinned down in that ticket: the retain-before-clear guards the payload's refcount, PXXVarClear then PXXMemZeros the 16 slot bytes it is about to be the source of, and the copy copies the zeros. Six backends plus the runtime, and no business riding along here.

Log