← board

A BASIC program is an illegal instruction on aarch64 and arm32

Repro

compiler/pascal26 --target=aarch64 test/test_basic_comprehensive.bas /tmp/b64
qemu-aarch64 /tmp/b64
  qemu: uncaught target signal 4 (Illegal instruction) - core dumped

Same for --target=arm32 under qemu-arm. Both COMPILE cleanly and die at runtime, before the first line of output. --target=i386 is fine (21 lines, correct). x86-64 is fine.

Not a regression: stable_linux_amd64/default/pinned produces the same SIGILL on both targets. This is how BASIC cross-compilation has always been.

Why nothing caught it

The .bas jobs run in the native tier only — tstate shows job_tier/test-core#src:test/test_basic_comprehensive.bas = native — so no cross-target verdict has ever been published for this frontend. The failure is old, silent and complete.

Where to look first

The BASIC driver open-codes its program prologue, and its entry stub is emitted as raw x86-64 bytes in at least one place (EmitB($E9) + a 32-bit displacement patch) rather than through EmitProgramEntryForTarget, which is the routine that exists precisely because "every other frontend open-coded the x86-64 one and nothing else, and a NilPy arm32 binary began with x86-64 bytes" — that comment is in pasparser_prog.inc today, describing the identical bug in a different frontend.

So the likely shape is: the ELF entry point of a .bas binary on aarch64/arm32 contains x86-64 instruction bytes. Check that before anything else — decode the first bytes at the entry point and compare with a Pascal binary for the same target.

This is one more instance of the checklist-in-five-copies problem; [[refactor-a-one-program-driver-prologue-for-every-frontend]] is the systematic fix, and would likely close this ticket as a side effect.

Gate

.bas tests pass under qemu on aarch64 and arm32, native and i386 unchanged, self-host byte-identical.

Fixed 2026-08-24 (claude-A) — and the "where to look first" note was right twice over

All three .bas tests now produce byte-identical output on x86-64, i386, aarch64 and arm32. It took two fixes, and the second one is the interesting half.

1. The entry stub, exactly as predicted

The ELF entry point of an aarch64 .bas binary held x86-64 bytes:

entry=0x4000b0: 48 89 24 25 78 ad 43 00 e9 60 9d 03 00 | 62 00 00 14 ...
                ^--------- x86-64 mov [abs], rsp -----^ ^-- the real aarch64 code

ParseBProgram open-coded mov [BSS_INITIAL_RSP], rsp + jmp rel32 and Patch32(jmpPatch, CodeLen - (jmpPatch + 4)) for every target. Replaced with EmitProgramEntryForTarget / PatchProgramEntryJump, which is what that pair exists for.

Why BASIC and only BASIC. Nine drivers open-code this stub — Ada, Erlang, Fortran, BASIC, Algol, LOLCODE, Whitespace, Rust, Zig. Eight of them refuse a non-x86-64 target up front ("only the x86-64 target is supported by the skeleton"). BASIC alone had no guard, so it was the one that produced a broken binary instead of a diagnostic. That is the whole reason this ticket exists and the others do not.

2. A placeholder branch that is inert on x86-64 and destructive everywhere else

With the entry fixed, aarch64 passed all three tests and arm32 passed two — test_basic_goto_gosub ran correctly to the end and then fell through its END into the subroutine below and died with "RETURN without GOSUB". Minimal repro, arm32 only: 10 GOSUB 100 / 20 END / 100 PRINT "sub" / 110 RETURN.

Found by decoding the emitted words rather than by reading the lowering:

0x08048374: EB000000   bl +8      <-- lands at 0x37C, SKIPPING 0x378
0x08048378: E3A070F8   mov r7, #248   (sys_exit_group)
0x0804837c: E3A00000   mov r0, #0
0x08048380: EF000000   svc #0

so the svc ran with r7 still holding 4 (write) from the preceding PRINT, and the program did not exit at all.

EB000000 is EmitCallProc's forward-reference placeholder. BASIC's END lowers to a Halt, a Halt calls __pxx_run_finalizers, and bparser never called EmitFinalizerRunnerBody — the FIFTH step this driver has been caught missing ([[refactor-a-one-program-driver-prologue-for-every-frontend]]). So the proc stayed at BodyAddr = -1 and the call kept its stand-in forever, because nothing resolves this driver's forward calls: ApplyCallFixups is called by six other drivers and by DceRun, and DceRun is off unless --dce and x86-64 and the Pascal frontend.

The stand-in itself was the bug, and it was a bug on four targets:

target placeholder what an unpatched one does
x86-64 call rel32=0 targets the next instruction — falls through, inert
arm32 bl imm=0 PC reads 8 ahead — skips an instruction
aarch64 bl imm=0 branches to itself — infinite loop (measured: the binary hung)
riscv32 auipc t0,0; jalr ra,t0,0 targets the auipc — self-loop
xtensa call0/call8 rel 0 likewise

Only x86-64's was harmless, and only x86-64 ever ran these frontends. All five are now a NOP of the same size, so an unpatched call is silently skipped exactly as it has always been silently skipped on x86-64, and a patched one is overwritten identically.

EmitFinalizerRunnerBody was also added to bparser and to the four other drivers that lacked it (Ada, Fortran, Algol, LOLCODE, Whitespace); every skeleton frontend's test output is unchanged, verified program by program.

What was deliberately NOT done

ApplyCallFixups is still not called by the BASIC driver. Adding it turns a unit-free .bas program with a string literal into unresolved forward: PXXStrFromLit — a real latent hole, filed as [[bug-a-a-unit-free-basic-program-calls-a-helper-it-never-emits]]. Making the placeholder inert is what this fix needed; making the pass run is that ticket's scoping question.

--target=riscv32 still cannot compile a .bas program at all — compiler error: PXXWriteNL not found, identical under pinned, so pre-existing and the same "this driver does not pull the unit" family as the ticket above.

Gate

make compiler/pascal26 fixedpoint converged in one round; tools/gate.sh quick GREEN; the three .bas tests wired into test-core as CROSS rows on i386 / aarch64 / arm32 (guarded on qemu-user being present, the same shape lib-test's cross net block uses); every skeleton frontend's output re-measured identical.

Log