← board

riscv32 cannot reach a far call, so the compiler will not link

Measured

$ compiler/pascal26 --target=riscv32 compiler/compiler.pas /tmp/out
pascal26:8307: error: target riscv32: jal displacement 2197196 is outside the
  encodable range -1048576..1048574; the code is too large for this branch form

2.20 MB against JAL's ±1 MB. Ordinary programs cross-build and run on riscv32 fine; it is specifically the compiler's size that exceeds the form.

The shape of the fix already exists in-tree

xtensa hit the identical problem and solved it: EmitXtensaLongCall (symtab.inc) materialises the target address into a register and does an indirect call, and EmitXtensaCallToCode picks the short or long form by asking XtensaCallReaches. riscv32 needs the same pair — auipc+jalr is the natural long form there, and it is a two-instruction sequence with ±2 GB reach.

The reach test must be the thing that chooses, not a heuristic about program size: a build that is nearly over the line must still emit the cheap form for the calls that fit.

Sibling

[[bug-a-xtensa-cannot-widen-a-forward-call-so-a-big-image-still-refuses-to-build]] is the same family and is about xtensa's remaining forward-reference case. This one is riscv32 and is about JAL specifically.

Gate

make compiler/pascal26, then pascal26 --target=riscv32 compiler/compiler.pas must produce an artifact; plus the riscv32 cross battery, since every call in it now goes through whichever form the new chooser picks.

What it actually was (measured, not reasoned)

Two hypotheses were wrong before the third was measured, and both were wrong in the same way -- they were about CALLS, because the ticket's title says call.

  1. The signal-install site. Ruled out with --no-signals: still fails.

  2. The runtime-helper calls (ExcSetJmpAddr, ExcRaiseAddr, SigInstallAddr, ExcLongJmpAddr, SigSetHookAddr). Six sites routed through a new reach-choosing EmitRiscv32CallToCode; the error came back byte-identical, same displacement 2197196. That is what retired the hypothesis. The routing was kept anyway -- the helpers sit at the front of the image and nothing was reach-checking them -- but it fixed nothing here.

  3. The answer, from tagging every EncodeRISCVJAL call site with an id and printing it beside CodeLen and the current proc:

    jal displacement 2197196 ... [CodeLen=6397364 site=16
      proc=ParseFactorCore@4199288->6396484 lbl2 nfix2267]
    

    Site 16 is the label fixup loop -- a body jumping to its own label. ParseFactorCore occupies 4199288..6396484: 2.20 MB in one procedure, with 2267 forward jumps in it.

    The x86-64 map says this is a narrow wall and a real one: two procedures exceed 1 MB (ParseFactorCore 1.15 MB, PyParseFactorCore 1.24 MB) and the third largest is 0.40 MB.

Behind it sat a second, larger one that only appeared once the first was fixed: the program entry jump (EmitProgramEntryForTarget / PatchProgramEntryJump), which reserved one JAL to reach the main body past every proc body in the image, and asked for 20089124.

The fix

A forward jump cannot be widened at patch time unless the space was reserved, so:

Five copies of the same if-known-else-record-a-fixup block collapsed into EmitRv32JumpToLabel.

Evidence

Regression test

make test-riscv32 grew one: a GENERATED bigbody.pas -- one procedure with a 4000-arm if-chain, 1146732 B of rv32 code, compiled and run against the x86-64 oracle in 0.9 s. The compiler itself is the only other program that crosses the wall and it is a terrible regression test (the whole self-build has to fail first). Generated rather than checked in: the source has to be ~260 KB to produce >1 MB of code.

It carries its own POSITIVE CONTROL -- an assertion that the body really does exceed 1048576 B -- because a generator, a backend change or an optimisation could quietly bring it back under the line and the test would go on passing while covering nothing. Verified to reject both a small code= and a missing one; the missing case is also what makes the | tee safe, since a pipeline's status is tee's and a failed compile would otherwise exit 0.

Pinned refuses that file today (jal displacement 1106292 ...); the new compiler builds it and it prints the oracle's output under qemu.

What this does NOT close

asmtext_rv32.inc's own forward-reference patch (inline-asm labels) is still a bare 4-byte JAL. It was left alone deliberately: those labels are inside one hand-written asm block, and no block in the tree is anywhere near 1 MB. If one ever is, it fails loudly with the same message and this is the shape to copy.

Log