Files
LithosAnanake/docs/lithosananke/hosted-acceptance-test
Robert Allan JamesandClaude Sonnet 5 4485c3893b Fix riscv64 hosted acceptance-test doc; port 3 clang-surfaced bugs from punch list
Addresses items #1 (partial) and #2 of
docs/working/archive/session-logs/2026-07-24-punch-list.md.

docs/lithosananke/hosted-acceptance-test/README.md:
- riscv64 leg used riscv64-linux-gnu-gcc, which fails to build this tree
  (nanosleep visibility under -std=c99). Replaced with the working
  clang-18 --target=riscv64-linux-gnu --sysroot=/usr/riscv64-linux-gnu
  invocation, verified end-to-end.
- All three arch sections referenced a -c "<script>" flag that has never
  existed in cli.c/main.c. Corrected to the working
  `echo "..." | starforth -s` pattern, verified on all three architectures.
- Updated Prerequisites: qemu-user alone is sufficient (guest binaries are
  static; qemu-user-static provides static *emulators*, not required here).

Source fixes (ported from the old pre-split monorepo's master, commit
4db9946a, where they were made but never carried over to this line):
- src/math_portable.c: `-100LL << 16` is UB (shifting a negative value)
  under clang's -Wshift-negative-value; changed to `-(100LL << 16)`.
- src/physics_pipelining_metrics.c: removed dead q48_mul_q48()
  (-Wunused-function under clang; GCC doesn't flag this by default).
- src/word_source/editor_words.c: removed dead set_scr() (same reason).

These three were required just to get the documented clang build to
compile at all. The punch list's higher-severity item — a genuine
SIGSEGV-causing register-reuse hazard in vm_pop_asm/vm_rpop_asm
(include/vm_asm_opt_riscv64.h) — is intentionally NOT included here; it's
a separate, more careful change and isn't required for this build to
succeed (latent only under clang; this repo's kernel build uses GCC).

Verified: all three hosted builds compile and run correctly (amd64
native, aarch64 via qemu-aarch64, riscv64 via qemu-riscv64), each
printing "3 Goodbye!" for the piped `1 2 + . BYE` script. All three
Makefile.starkernel builds (amd64/aarch64/riscv64) still compile cleanly
with these shared vendored-source changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 07:25:40 -04:00
..

Hosted StarForth — 3-Architecture Acceptance Test

Date: 2026-07-24 (original), corrected 2026-08-02 Branch: master Commits: edced063 (original block-subsystem fix), this correction pass


What this verifies

The hosted StarForth VM (make, not Makefile.starkernel) is a static binary. This test confirms it starts cleanly — reaches the ok> REPL prompt with no errors — on all three architectures the project targets:

  • amd64 — native execution on the build host
  • aarch64 — cross-compiled, run under QEMU user-mode emulation
  • riscv64 — cross-compiled, run under QEMU user-mode emulation

This is distinct from the kernel (Makefile.starkernel) 3-arch acceptance test documented in ../amd64-isr-fix/README.md, which boots a full UEFI bare-metal image under system-mode QEMU. This test only exercises the hosted Linux binary, using QEMU user-mode emulation to run a foreign-architecture static binary directly on the x86_64 host — no kernel, no UEFI firmware, no system emulation.


Prerequisites

Cross toolchains, QEMU user-mode emulation, and clang (needed for the riscv64 leg — see below):

sudo apt-get install -y \
    gcc-aarch64-linux-gnu \
    gcc-riscv64-linux-gnu \
    clang-18 lld-18 \
    qemu-user

qemu-user alone is enough — the binaries built below are statically linked (-static), so the non-static qemu-<arch> emulators run them directly with no sysroot. (The qemu-user-static package, if you see it referenced elsewhere, provides statically-linked emulators; it's not required just because the guest binary is static.)


Invocation note

None of the three binaries accept a -c "<script>" flag — earlier revisions of this doc showed one, but src/cli.c/src/main.c have never had one in this repo's history. Pipe the script via stdin with -s (script/silent mode) instead. Startup always runs the full internal POST test suite before the REPL takes stdin, so expect substantial test-suite output before your script's result appears at the very end.


amd64 — native

make clean && make
echo "1 2 + . BYE" | ./build/amd64/standard/starforth -s

Expect 3 Goodbye! at the end of the output, then a clean exit. No emulation needed — this is the build host's native architecture.


aarch64 — cross-compile + QEMU user-mode

make rpi4-cross
echo "1 2 + . BYE" | qemu-aarch64 ./build/raspi/fastest/starforth -s

make rpi4-cross cross-compiles with aarch64-linux-gnu-gcc (ARCH=raspi, TARGET=fastest, -static); see Makefile:547-554. The output binary is statically linked, so qemu-aarch64 can execute it directly with no sysroot.


riscv64 — cross-compile + QEMU user-mode

There is no dedicated make target for this yet (unlike rpi4-cross for aarch64) — invoke the cross-compile directly. This leg must use clang, not GCC: riscv64-linux-gnu-gcc fails to build this tree (nanosleep visibility failure under -std=c99, unrelated to any project change).

make ARCH=riscv64 \
    CC="clang-18 --target=riscv64-linux-gnu --sysroot=/usr/riscv64-linux-gnu" \
    TARGET=fastest \
    CFLAGS="-std=c11 -pthread -Wall -Werror -Iinclude -Isrc/word_source -Isrc/test_runner/include -DSTRICT_PTR=1 -march=rv64gc -mabi=lp64d -mcmodel=medany -DARCH_RISCV64=1 -O3 -DUSE_ASM_OPT=1 -DUSE_DIRECT_THREADING=1 -DNDEBUG -flto -static" \
    LDFLAGS="-flto -s -static -fuse-ld=lld" \
    all

echo "1 2 + . BYE" | qemu-riscv64 ./build/riscv64/fastest/starforth -s

Note -fuse-ld=lld lives in LDFLAGS, not folded into CC — clang rejects it as an unused argument during -c (compile-only) invocations under -Werror.


Pass criteria

For each architecture:

  1. Build completes with no errors.
  2. blk_subsys_init succeeds — no "Failed to initialize block subsystem" error. (This was broken on every architecture, including amd64, prior to edced063 — see below.)
  3. The internal POST suite runs (substantial log output — expected, not a failure by itself) and the piped 1 2 + . BYE script prints 3 Goodbye! at the end.

Background: the block-subsystem bug this depends on

Before edced063, the hosted VM could not start on any architecture on this branch. src/main.c allocated a hardcoded blk_ram[1024 * 1024] buffer, stale relative to BLK_RAM_BLOCKS=2080 (include/block_subsystem.h) — a deliberate redesign that moved the RAM/ramdrive boundary to physical block 2080 (user-visible LBN 2048), per BLK_FORTH_SYS_RESERVED=32. The kernel-side buffer in src/starkernel/vm/bootstrap/sk_vm_bootstrap.c already derived its size correctly from the shared constant; main.c was the one remaining hardcoded holdout. The fix:

#include "block_subsystem.h"
...
static uint8_t blk_ram[BLK_RAM_BLOCKS * BLK_FORTH_SIZE];   /* was: blk_ram[1024 * 1024] */

This is why the acceptance test above is meaningful on this branch specifically: it did not pass before this fix, on any of the three architectures.


Background: the riscv64 clang build (2026-08-02 correction)

The riscv64 command above was broken in this doc from 2026-07-24 until this correction — docs/working/archive/session-logs/2026-07-24-punch-list.md (item #2) recorded that the working clang invocation had been found and fixed in the old pre-split monorepo's master branch's .claude/CLAUDE.md, but the correction was never copied into this doc. Verifying it end-to-end for this correction surfaced that the clang build also needs three small source fixes from that same punch list (item #1), previously ported to the old monorepo's master but not to this line:

  • src/math_portable.c-100LL << 16 is UB (shifting a negative value) under -Wshift-negative-value; changed to -(100LL << 16).
  • src/physics_pipelining_metrics.c — removed dead q48_mul_q48() (-Wunused-function under clang; GCC doesn't catch this by default).
  • src/word_source/editor_words.c — removed dead set_scr() (same reason).

The punch list's highest-priority item — a genuine SIGSEGV-causing register-reuse hazard in include/vm_asm_opt_riscv64.h's vm_pop_asm/ vm_rpop_asm (clang's register allocator can pick the same register for a memory operand referenced both before and after a write-back, corrupting the result; GCC happens not to hit it) — is a separate, not-yet-ported fix and is not required just to get this acceptance test's -O3 build to compile and pass; it's a latent runtime risk specifically under clang, tracked separately, not resolved by this correction.