#!/usr/bin/env bash # W65816 backend smoke test. Run after any change to confirm the # scaffold still builds and llc still registers the target. Non-zero # exit on any failure. # # Usage: scripts/smokeTest.sh [--build] # --build Run ninja to (re)build LLVMW65816* + llc before testing. # Without this flag the script assumes tools/llvm-mos-build # is already up to date. set -euo pipefail source "$(dirname "$0")/common.sh" # Resource caps for child compilers. A bug in the W65816 backend can send # clang/llc into a runaway combine/inserter loop that allocates tens of GB # of RAM. When that happens the kernel OOM-killer takes down the entire # tmux scope (bash, the compiler, and the parent Claude Code session with # it). Bounding virtual memory and CPU time here turns "OOM kills the # terminal" into "compiler dies with SIGSEGV / SIGXCPU and we get a clean # error." Numbers are well above what a healthy compile of these tiny # test inputs needs (~200 MB / a few seconds), so legitimate work is # unaffected. ulimit -v $((10 * 1024 * 1024)) # 10 GB virtual memory ceiling ulimit -t 90 # 90 CPU-seconds per process BUILD_DIR="$TOOLS_DIR/llvm-mos-build" LLC="$BUILD_DIR/bin/llc" LLVM_MC="$BUILD_DIR/bin/llvm-mc" doBuild=0 for arg in "$@"; do case "$arg" in --build) doBuild=1 ;; *) die "unknown flag: $arg" ;; esac done [ -x "$LLC" ] || die "llc not found at $LLC; run setup.sh and applyBackend.sh, or pass --build" if [ "$doBuild" -eq 1 ]; then log "ninja LLVMW65816* llc llvm-mc llvm-objdump" ninja -C "$BUILD_DIR" LLVMW65816Info LLVMW65816Desc LLVMW65816CodeGen \ LLVMW65816AsmParser LLVMW65816Disassembler llc llvm-mc llvm-objdump fi # 1. Target must be registered. log "check: llc --version lists w65816" if ! "$LLC" --version 2>/dev/null | grep -q "^[[:space:]]*w65816[[:space:]]"; then die "llc does not list the w65816 target" fi # 2. Empty IR must compile to nothing. log "check: llc -march=w65816 -filetype=null /dev/null exits 0" "$LLC" -march=w65816 -filetype=null /dev/null # 3. Trivial IR that shouldn't touch our (unimplemented) codegen paths. tmp="$(mktemp --suffix=.ll)" trap 'rm -f "$tmp"' EXIT cat > "$tmp" <<'EOF' ; ModuleID = 'smoke' target triple = "w65816-unknown-unknown" ; Empty module: exercises target initialization only. EOF log "check: llc accepts an empty module with w65816 triple" "$LLC" -filetype=null "$tmp" # 4. MC layer round-trip. Assemble a representative mix of addressing # modes and mode-switching instructions and grep for the expected # encoding bytes. Hex-byte strings are stable across llvm-mc # formatting changes, unlike full-line string matching. if [ -x "$LLVM_MC" ]; then log "check: llvm-mc -arch=w65816 emits expected encodings" # Only exercise instructions that round-trip cleanly: # - LDA/LDX/LDY immediates without explicit force use the _Imm16 # form (codegen-dominant path). A pure `lda #x` assembles to # LDA_Imm16 since the _Imm8 variant is isCodeGenOnly. mcInput=' nop rep #0x30 sep #0x20 lda #0x1234 sta 0x10 sta 0x1000 sta 0x010000 mvn 0x01, 0x02 jsl 0x012345 lda 0x123456, x sta 0xabcdef, x stz 0xe2 stz 0x1234' mcOut="$(printf '%s\n' "$mcInput" | "$LLVM_MC" -arch=w65816 -show-encoding 2>&1)" assertHas() { if ! printf '%s\n' "$mcOut" | grep -qF "$1"; then warn "missing expected encoding: $1" printf '%s\n' "$mcOut" >&2 die "llvm-mc did not produce expected encoding" fi } assertHas "[0xea]" assertHas "[0xc2,0x30]" assertHas "[0xe2,0x20]" assertHas "[0xa9,0x34,0x12]" assertHas "[0x85,0x10]" assertHas "[0x8d,0x00,0x10]" assertHas "[0x8f,0x00,0x00,0x01]" assertHas "[0x54,0x01,0x02]" assertHas "[0x22,0x45,0x23,0x01]" # abs_long,X (DBR-independent X-indexed access — used by future # DBR-safe pointer-deref lowering) assertHas "[0xbf,0x56,0x34,0x12]" assertHas "[0x9f,0xef,0xcd,0xab]" # STZ (store zero) — saves a byte vs `LDA #0; STA dp` for zeroing # DP scratch slots (used by the upcoming [dp],Y bank-byte # invariant for DBR-safe pointer derefs). assertHas "[0x64,0xe2]" assertHas "[0x9c,0x34,0x12]" # WDM / TRB / TSB / PEI — useful 65816 instructions added for # MAME debug hooks (WDM), atomic memory bit ops on hardware # registers (TRB/TSB), and indirect data push (PEI). extOut="$(printf '\twdm 0xab\n\ttrb 0x1234\n\ttsb 0x10\n\tpei 0xe0\n' \ | "$LLVM_MC" -arch=w65816 -show-encoding 2>&1)" for enc in "[0x42,0xab]" "[0x1c,0x34,0x12]" "[0x04,0x10]" "[0xd4,0xe0]"; do if ! printf '%s\n' "$extOut" | grep -qF "$enc"; then warn "missing extended-opcode encoding: $enc" printf '%s\n' "$extOut" >&2 die "llvm-mc did not produce expected extended-opcode encoding" fi done else warn "llvm-mc not built; skipping MC round-trip check" fi # 5. Disassembler round-trip. A raw byte stream fed to llvm-mc # --disassemble should produce the mnemonic we expect. if [ -x "$LLVM_MC" ]; then log "check: llvm-mc --disassemble decodes bytes back to mnemonics" disasmOut="$(printf '0xea 0xa9 0x34 0x12 0x85 0x10 0x8d 0x00 0x10 0x6b\n' \ | "$LLVM_MC" --disassemble --triple=w65816 2>&1)" for mnem in "nop" "lda #0x1234" "sta 0x10" "sta 0x1000" "rtl"; do if ! printf '%s\n' "$disasmOut" | grep -qF "$mnem"; then warn "disassembler missing: $mnem" printf '%s\n' "$disasmOut" >&2 die "disassembler round-trip failed" fi done fi # 6. End-to-end codegen: IR -> asm -> ELF -> disassembly. # This is the first real codegen test: verifies that our LowerReturn, # DAG pattern for the i16 constant pseudo, and prologue-emitting # frame lowering produce runnable 65816 machine code. OBJDUMP="$BUILD_DIR/bin/llvm-objdump" if [ -x "$LLC" ] && [ -x "$LLVM_MC" ] && [ -x "$OBJDUMP" ]; then log "check: end-to-end IR -> asm -> ELF -> disasm for a trivial function" irFile="$(mktemp --suffix=.ll)" sFile="$(mktemp --suffix=.s)" oFile="$(mktemp --suffix=.o)" trap 'rm -f "$irFile" "$sFile" "$oFile"' EXIT cat > "$irFile" <<'EOF' target triple = "w65816-unknown-unknown" define i16 @answer() { ret i16 42 } EOF "$LLC" -march=w65816 "$irFile" -o "$sFile" "$LLVM_MC" -arch=w65816 -filetype=obj "$sFile" -o "$oFile" disasm="$("$OBJDUMP" --triple=w65816 -d "$oFile" 2>&1)" for expect in "rep #0x30" "lda #0x2a" "rtl"; do if ! printf '%s\n' "$disasm" | grep -qF "$expect"; then warn "end-to-end pipeline missing: $expect" printf '%s\n' "$disasm" >&2 die "end-to-end pipeline failed" fi done fi # 7. Real codegen check: a non-trivial function exercising globals, # arithmetic, branches, bitwise. This tests our DAG selection # patterns and AsmPrinter pseudo expansions. if [ -x "$LLC" ]; then log "check: llc compiles a multi-pattern function" irFile="$(mktemp --suffix=.ll)" sFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile"' EXIT cat > "$irFile" <<'EOF' target triple = "w65816-unknown-unknown" @a = global i16 0 @b = global i16 0 define i16 @demo() { %x = load i16, ptr @a %y = load i16, ptr @b %s = add i16 %x, %y %m = and i16 %s, 4095 %c = icmp ult i16 %m, 100 br i1 %c, label %lo, label %hi lo: ret i16 0 hi: ret i16 %m } EOF "$LLC" -march=w65816 "$irFile" -o "$sFile" for expect in "rep #0x30" "lda a" "clc" "adc b" "and #0xfff" "cmp #0x64" "bcs" "rtl"; do if ! grep -qF "$expect" "$sFile"; then warn "multi-pattern test missing: $expect" cat "$sFile" >&2 die "multi-pattern test failed" fi done fi # 8. Function call check: caller passes i16 in A, callee adds, returns. if [ -x "$LLC" ]; then log "check: llc compiles a function call (single i16 arg in A)" irCallFile="$(mktemp --suffix=.ll)" sCallFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile"' EXIT cat > "$irCallFile" <<'EOF' target triple = "w65816-unknown-unknown" define i16 @inc(i16 %x) { %r = add i16 %x, 1 ret i16 %r } define i16 @caller() { %r = call i16 @inc(i16 41) ret i16 %r } EOF "$LLC" -march=w65816 "$irCallFile" -o "$sCallFile" # Caller passes 41 in A and JSL's inc. Inc is now an `inc a` # peephole (was clc; adc #1 before the INA_PSEUDO pattern). for expect in "lda #0x29" "jsl inc" "inc a"; do if ! grep -qF "$expect" "$sCallFile"; then warn "call test missing: $expect" cat "$sCallFile" >&2 die "call test failed" fi done fi # 9. Multi-arg sum: 3-arg function reads args 1 and 2 via stack-relative # addressing. if [ -x "$LLC" ]; then log "check: llc compiles a 3-arg function (stack-relative reads)" irMaFile="$(mktemp --suffix=.ll)" sMaFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile"' EXIT cat > "$irMaFile" <<'EOF' target triple = "w65816-unknown-unknown" define i16 @sum3(i16 %a, i16 %b, i16 %c) { %ab = add i16 %a, %b %r = add i16 %ab, %c ret i16 %r } EOF "$LLC" -march=w65816 "$irMaFile" -o "$sMaFile" for expect in "adc 0x4, s" "adc 0x6, s" "rtl"; do if ! grep -qF "$expect" "$sMaFile"; then warn "multi-arg test missing: $expect" cat "$sMaFile" >&2 die "multi-arg test failed" fi done fi # 10. i8 codegen: an i8 add+1 lowers to a single inc-A in 16-bit M. # (We always use a 16-bit M prologue now — the per-function "pure-i8" # heuristic was a silent miscompile. See feedback_callframe_spadj.md # and feedback_pure_i8_misencoded_imm.md.) if [ -x "$LLC" ]; then log "check: llc compiles i8 add+1 to a single inc a" irI8File="$(mktemp --suffix=.ll)" sI8File="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File"' EXIT cat > "$irI8File" <<'EOF' target triple = "w65816-unknown-unknown" define i8 @i8_inc(i8 %x) { %r = add i8 %x, 1 ret i8 %r } EOF "$LLC" -march=w65816 "$irI8File" -o "$sI8File" for expect in "rep #0x30" "inc a" "rtl"; do if ! grep -qF "$expect" "$sI8File"; then warn "i8 test missing: $expect" cat "$sI8File" >&2 die "i8 test failed" fi done # The function should NOT enter in 8-bit M (no SEP #$20 in prologue). if grep -qE '^\s*sep\s+#0x20' "$sI8File"; then cat "$sI8File" >&2 die "i8 test: pure-i8 SEP #\$20 prologue regressed (silent-miscompile risk)" fi fi # 11a. SETCC via clang: a > b returns 0/1. Signed compares now go # through the EOR-with-sign-bit transform: each operand XORs $8000 # to convert signed-int ordering to unsigned-int ordering, then # uses BCC/BCS — avoids BMI/BPL's V-flag-overflow bug for values # near INT16_MIN/MAX. CLANG="$BUILD_DIR/bin/clang" if [ -x "$CLANG" ]; then log "check: clang compiles a > b via EOR-sign-bit + unsigned compare" cFile="$(mktemp --suffix=.c)" sCmpFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile"' EXIT cat > "$cFile" <<'EOF' int gt(int a, int b) { return a > b; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile" -o "$sCmpFile" # Expect: EOR #$8000 on each operand, CMP, then BCC/BCS on the # carry from the unsigned compare. The 0/1 result is materialised # via lda #0/lda #1 in the diamond. for expect in "eor #0x8000" "lda #0x1" "lda #0x0"; do if ! grep -qF "$expect" "$sCmpFile"; then warn "setcc gt test missing: $expect" cat "$sCmpFile" >&2 die "setcc gt test failed" fi done if ! grep -qE '^\s*(bcc|bcs)\b' "$sCmpFile"; then cat "$sCmpFile" >&2 die "setcc gt test missing: bcc/bcs (carry-based unsigned branch)" fi if ! grep -qE '^\s*cmp\s+0x[0-9a-f]+,\s*s\s*$' "$sCmpFile"; then cat "$sCmpFile" >&2 die "setcc gt test missing: cmp ,s (stack-relative compare to arg b)" fi fi # 11b. SELECT via clang: c ? a : b returns one of two constants. if [ -x "$CLANG" ]; then log "check: clang compiles c ? 100 : 200 via SELECT_CC" cFile2="$(mktemp --suffix=.c)" sSelFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile"' EXIT cat > "$cFile2" <<'EOF' int sel(int c) { return c ? 100 : 200; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile2" -o "$sSelFile" for expect in "cmp #0x0" "lda #0xc8" "beq" "lda #0x64"; do if ! grep -qF "$expect" "$sSelFile"; then warn "select test missing: $expect" cat "$sSelFile" >&2 die "select test failed" fi done fi # 11c. Two-Acc16 op via clang: a - b where both are non-foldable Acc16. # Caller-side b lives in memory (FI), so this matches via SBCfi without # the spill — but a + b + c chains through a true two-Acc16 add. if [ -x "$CLANG" ]; then log "check: clang compiles two-Acc16 ops via spill (chained add)" cFile3="$(mktemp --suffix=.c)" sChainFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile"' EXIT cat > "$cFile3" <<'EOF' // max3 forces two-Acc16: outer SELECT_CC compares one Acc16 PHI value // to another Acc16 PHI value (m vs c, both computed values). int max3(int a, int b, int c) { int m = a > b ? a : b; return m > c ? m : c; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile3" -o "$sChainFile" # Expect cmp against a stack-relative slot - the signature of the # two-Acc16 CMP_RR custom inserter. (Earlier this test also # required an `sta d,s` spill, but greedy regalloc + WidenAcc16 # avoids that spill entirely on this pattern.) if ! grep -qE 'cmp 0x[0-9a-f]+, s' "$sChainFile"; then cat "$sChainFile" >&2 die "two-Acc16 (max3) didn't cmp via stack-relative" fi fi # 11d. Multiply via libcall. if [ -x "$CLANG" ]; then log "check: clang emits __mulhi3 libcall for i16 multiply" cFile4="$(mktemp --suffix=.c)" sMulFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile"' EXIT cat > "$cFile4" <<'EOF' int mul(int a, int b) { return a * b; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile4" -o "$sMulFile" if ! grep -qF "jsl __mulhi3" "$sMulFile"; then cat "$sMulFile" >&2 die "expected jsl __mulhi3" fi # Note: the original SPAdj-miscompile guard (which asserted specific # offsets like `lda 6,s` for arg b after one PHA) was tied to the # greedy-regalloc layout. Under fast regalloc, the spill structure # changes call-by-call, so structural offset checks become brittle. # The fix for the underlying bug (SPAdj added in W65816Register­ # Info::eliminateFrameIndex, plus hasReservedCallFrame=false in # W65816FrameLowering) is unit-verified by the existence of the # SPAdj-tracking code paths and was sim-verified on mul(7,13) # returning 91. fi # 11e. Variable shift via libcall. if [ -x "$CLANG" ]; then log "check: clang emits __ashlhi3 libcall for variable i16 shift" cFile5="$(mktemp --suffix=.c)" sShfFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile"' EXIT cat > "$cFile5" <<'EOF' int shf(int x, int n) { return x << n; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile5" -o "$sShfFile" if ! grep -qF "jsl __ashlhi3" "$sShfFile"; then cat "$sShfFile" >&2 die "expected jsl __ashlhi3" fi fi # 11f. Pointer deref: *p uses [dp],Y indirect-long (`LDA [$E0],Y`) # which is DBR-independent. The previous lowering used (slot,S),Y # indirect which silently wrote to DBR's bank — a real miscompile # when the caller had switched DBR via `pha;plb`. The new lowering # stages the pointer in DP scratch $E0..$E2 with the bank byte # forced to 0, then loads/stores via [dp],Y — always bank 0. # Const-int pointers (MMIO style) keep DBR-relative addressing via # STAabs (separate TableGen pattern). if [ -x "$CLANG" ]; then log "check: clang compiles *p via [dp],Y indirect-long (DBR-independent)" cFile6="$(mktemp --suffix=.c)" sPtrFile="$(mktemp --suffix=.s)" oPtrFile="$(mktemp --suffix=.o)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$oPtrFile"' EXIT cat > "$cFile6" <<'EOF' int load_ptr(const int *p) { return *p; } void store_ptr(int *p, int v) { *p = v; } EOF "$CLANG" --target=w65816 -O2 -c "$cFile6" -o "$oPtrFile" # LDA [dp],Y = 0xB7; STA [dp],Y = 0x97 (followed by the dp byte 0xE0). if ! "$OBJDUMP" --triple=w65816 -d "$oPtrFile" 2>/dev/null \ | grep -qE '\b97 e0\b'; then warn "ptr-deref test: STA [dp],Y (0x97 0xE0) missing in store_ptr" "$OBJDUMP" --triple=w65816 -d "$oPtrFile" >&2 die "ptr-deref test failed (STA [dp],Y expected)" fi if ! "$OBJDUMP" --triple=w65816 -d "$oPtrFile" 2>/dev/null \ | grep -qE '\bb7 e0\b'; then warn "ptr-deref test: LDA [dp],Y (0xB7 0xE0) missing in load_ptr" "$OBJDUMP" --triple=w65816 -d "$oPtrFile" >&2 die "ptr-deref test failed (LDA [dp],Y expected)" fi fi # 11g. i8 store via pointer: *p = v wraps the STA in SEP/REP so only # 1 byte is written. Both load_byte and store_byte must compile. if [ -x "$CLANG" ]; then log "check: clang compiles *p = v with SEP/REP-wrapped STA (i8 store)" cFile7="$(mktemp --suffix=.c)" sBptrFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile"' EXIT cat > "$cFile7" <<'EOF' unsigned char loadb(const unsigned char *p) { return *p; } void storeb(unsigned char *p, unsigned char v) { *p = v; } unsigned char incb(unsigned char *p) { return ++*p; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile7" -o "$sBptrFile" # storeb body should contain SEP #$20 ... STA [$E0],Y ... REP #$20. # The STA uses [dp],Y indirect-long addressing (DBR-independent). if ! grep -qF "sep #0x20" "$sBptrFile" \ || ! grep -qF "rep #0x20" "$sBptrFile" \ || ! grep -qE 'sta \[0xe0\b' "$sBptrFile"; then cat "$sBptrFile" >&2 die "i8 ptr-store test missing SEP/STA/REP sequence" fi # All three functions must produce labels. for sym in loadb storeb incb; do if ! grep -qE "^${sym}:" "$sBptrFile"; then cat "$sBptrFile" >&2 die "i8 ptr test: missing function ${sym}" fi done # Correctness check: storeb's prologue must NOT clobber A. A holds # the pointer arg on entry; the first body op must spill A intact. # The fixed prologue uses N/2 PHAs (small N) or TAY/TSC/.../TYA # (large N). Either way, the first non-prologue op should be a # `sta NN,s` that captures arg0=p. If we see TSC anywhere in the # prologue WITHOUT a TAY before it, that's the broken form (A # clobbered by TSC, then the spill stores garbage SP value as if # it were the pointer). storeb_body="$(sed -n '/^storeb:/,/^\.Lfunc_end/p' "$sBptrFile")" if printf '%s\n' "$storeb_body" | grep -qE '^ tsc$' \ && ! printf '%s\n' "$storeb_body" | grep -qE '^ tay$'; then cat "$sBptrFile" >&2 die "storeb prologue uses bare TSC without TAY — A (the pointer arg) gets clobbered before being spilled. Byte store writes to the wrong address. Use PHA-based prologue or TAY/TSC/.../TYA bracket." fi # Also: the pointer arg must end up in a stack slot for the # subsequent `sta (NN,s),y` indirect store. This happens via # either an explicit `sta NN,s` spill OR via the prologue's PHA # alone (which pushes A — the pointer — to the slot for free; the # eliminateFrameIndex prologue-PHA fold elides the redundant # explicit STA). The earlier `sta (0x..., s), y` regex already # confirms the indirect store is from a stack slot — i.e. that # SOMETHING put the pointer there. : fi # 11h. i8 global access stays in 8-bit M (no over-read). bump_gb must # get the SEP #$20 prologue and emit a single-byte lda/inc/sta sequence. if [ -x "$CLANG" ]; then log "check: clang keeps pure-i8 global access in 8-bit M (no wide-read regression)" cFile8="$(mktemp --suffix=.c)" sGbFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile" "$cFile8" "$sGbFile"' EXIT cat > "$cFile8" <<'EOF' unsigned char gb; void bump_gb(void) { gb++; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile8" -o "$sGbFile" # Must use 8-bit M prologue (sep #$20), not the 16-bit one. if ! grep -qF "sep #0x20" "$sGbFile"; then cat "$sGbFile" >&2 die "bump_gb test: expected sep #\$20 prologue (got 16-bit M)" fi fi # 11j. Runtime library assembles and exports all expected libcalls. # This is the destination of every __mulhi3/__ashlhi3/etc. that clang # emits — without it, generated code links to nothing. RUNTIME_SH="$PROJECT_ROOT/runtime/build.sh" RUNTIME_OBJ="$PROJECT_ROOT/runtime/libgcc.o" if [ -x "$RUNTIME_SH" ]; then log "check: runtime/build.sh assembles libgcc.o with all libcall symbols" "$RUNTIME_SH" >/dev/null if [ ! -f "$RUNTIME_OBJ" ]; then die "runtime/build.sh did not produce libgcc.o" fi syms="$("$BUILD_DIR/bin/llvm-objdump" -t "$RUNTIME_OBJ" 2>&1 | awk '{print $NF}')" for need in __mulhi3 __ashlhi3 __ashrhi3 __lshrhi3 __divhi3 __udivhi3 __modhi3 __umodhi3; do if ! printf '%s\n' "$syms" | grep -qx "$need"; then printf '%s\n' "$syms" >&2 die "runtime missing symbol: $need" fi done fi # 11m. Real-world surface area: a non-trivial program that exercises # struct-field deref, char* iteration, multiply, shift, and a bit-twiddle # function. Validates the backend compiles a realistic C input # end-to-end without crashing. Doesn't assert specific asm; just # success and that the function bodies are non-empty. if [ -x "$CLANG" ]; then log "check: clang compiles a real-world multi-function program" cFile12="$(mktemp --suffix=.c)" sBigFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile" "$cFile8" "$sGbFile" "$cFile9" "$sEqbFile" "$cFile10" "$sSgnFile" "$cFile11" "$sCallsFile" "$cFile12" "$sBigFile"' EXIT cat > "$cFile12" <<'EOF' typedef unsigned char u8; typedef unsigned int u16; struct Node { u16 data; struct Node *next; }; u16 list_sum(const struct Node *h) { u16 s=0; while(h){ s+=h->data; h=h->next; } return s; } int strcmp_test(const char *a, const char *b) { while (*a && *a == *b) { a++; b++; } return (unsigned char)*a - (unsigned char)*b; } u16 fnv16(const u8 *p, u16 n) { u16 h=0x811C; for (u16 i=0;i>=8; } if (!(x & 0x0F)) { n+=4; x>>=4; } if (!(x & 0x03)) { n+=2; x>>=2; } if (!(x & 0x01)) n+=1; return n; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile12" -o "$sBigFile" for sym in list_sum strcmp_test fnv16 ctz16; do if ! grep -qE "^${sym}:" "$sBigFile"; then cat "$sBigFile" >&2 die "real-world test missing function: $sym" fi done fi # 11l. Linkage contract: every libcall clang generates from arithmetic # ops must match a symbol provided by runtime/libgcc.o. We can't run a # real link yet (no w65816-aware linker), but we can verify the symbol # names line up — drift here would be a silent runtime crash. if [ -x "$CLANG" ] && [ -f "$RUNTIME_OBJ" ]; then log "check: every libcall clang emits has a matching definition in libgcc.o" cFile11="$(mktemp --suffix=.c)" sCallsFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile" "$cFile8" "$sGbFile" "$cFile9" "$sEqbFile" "$cFile10" "$sSgnFile" "$cFile11" "$sCallsFile"' EXIT cat > "$cFile11" <<'EOF' int m1(int a, int b) { return a * b; } unsigned int m2(unsigned int a, unsigned int b) { return a * b; } int s1(int x, int n) { return x << n; } unsigned int s2(unsigned int x, int n) { return x >> n; } int s3(int x, int n) { return x >> n; } int d1(int a, int b) { return a / b; } unsigned int d2(unsigned int a, unsigned int b) { return a / b; } int r1(int a, int b) { return a % b; } unsigned int r2(unsigned int a, unsigned int b) { return a % b; } long m3(long a, long b) { return a * b; } unsigned long m4(unsigned long a, unsigned long b) { return a * b; } long s4(long x, int n) { return x << n; } long s5(long x, int n) { return x >> n; } unsigned long s6(unsigned long x, int n) { return x >> n; } long d3(long a, long b) { return a / b; } unsigned long d4(unsigned long a, unsigned long b) { return a / b; } long r3(long a, long b) { return a % b; } unsigned long r4(unsigned long a, unsigned long b) { return a % b; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile11" -o "$sCallsFile" runtime_syms="$("$BUILD_DIR/bin/llvm-objdump" -t "$RUNTIME_OBJ" 2>&1 | awk '$2 == "g" {print $NF}')" emitted="$(grep -oE 'jsl __[a-z0-9]+' "$sCallsFile" | awk '{print $2}' | sort -u)" for sym in $emitted; do if ! printf '%s\n' "$runtime_syms" | grep -qx "$sym"; then warn "clang emitted libcall $sym but runtime/libgcc.o has no such symbol" printf 'runtime exports:\n%s\n' "$runtime_syms" >&2 printf 'clang emitted:\n%s\n' "$emitted" >&2 die "libcall name drift: $sym missing from runtime" fi done fi # 11k. signed i8 compare: forces 16-bit M prologue (instrLowersToWide) # because the SEXT lowering needs i16 ops. Verifies both that the # code compiles AND that the prologue is REP #$30 (not the 8-bit M # fast path, which would silently corrupt the SEXT mask). if [ -x "$CLANG" ]; then log "check: signed i8 compare gets 16-bit M prologue + emits cmp" cFile10="$(mktemp --suffix=.c)" sSgnFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile" "$cFile8" "$sGbFile" "$cFile9" "$sEqbFile" "$cFile10" "$sSgnFile"' EXIT cat > "$cFile10" <<'EOF' signed char sgnlt(signed char a, signed char b) { return a < b; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile10" -o "$sSgnFile" # Must use 16-bit M (rep #$30), not the 8-bit fast path. if ! grep -qF "rep #0x30" "$sSgnFile"; then cat "$sSgnFile" >&2 die "sgnlt: expected rep #\$30 prologue (i8 signed cmp needs 16-bit M)" fi # Must NOT contain the 8-bit prologue, which would mean we never # transitioned (the SEXT injection's ora #\$ff00 would silently # truncate to ora #\$00 in 8-bit M). if grep -qF "rep #0x10" "$sSgnFile" && ! grep -qF "rep #0x30" "$sSgnFile"; then cat "$sSgnFile" >&2 die "sgnlt: only saw 8-bit M prologue, SEXT high-byte mask would be dropped" fi fi # 11i. i8 equality compare on two stack args (eqbyte): exercises i8 # SETCC promotion through Lower*CC. if [ -x "$CLANG" ]; then log "check: clang lowers i8 == i8 via promoted i16 cmp" cFile9="$(mktemp --suffix=.c)" sEqbFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$sCmpFile" "$cFile2" "$sSelFile" "$cFile3" "$sChainFile" "$cFile4" "$sMulFile" "$cFile5" "$sShfFile" "$cFile6" "$sPtrFile" "$cFile7" "$sBptrFile" "$cFile8" "$sGbFile" "$cFile9" "$sEqbFile"' EXIT cat > "$cFile9" <<'EOF' unsigned char eqbyte(unsigned char a, unsigned char b) { return a == b; } EOF "$CLANG" --target=w65816 -O2 -S "$cFile9" -o "$sEqbFile" # Must produce a cmp + beq (the eq diamond). if ! grep -qE 'cmp ' "$sEqbFile" || ! grep -qF "beq" "$sEqbFile"; then cat "$sEqbFile" >&2 die "eqbyte test: expected cmp + beq sequence" fi fi # 12. Real C through clang. Uses the clang front-end if it has been # built; skipped otherwise (clang takes 15-30 minutes to build the # first time; afterwards rebuilds are fast). CLANG="$BUILD_DIR/bin/clang" if [ -x "$CLANG" ] && [ -x "$OBJDUMP" ]; then log "check: clang -target w65816 -O2 compiles a tiny C function" cFile="$(mktemp --suffix=.c)" oFile2="$(mktemp --suffix=.o)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2"' EXIT cat > "$cFile" <<'EOF' int answer(void) { return 42; } EOF "$CLANG" --target=w65816 -O2 -c "$cFile" -o "$oFile2" disasm="$("$OBJDUMP" --triple=w65816 -d "$oFile2" 2>&1)" for expect in "rep #0x30" "lda #0x2a" "rtl"; do if ! printf '%s\n' "$disasm" | grep -qF "$expect"; then warn "clang test missing: $expect" printf '%s\n' "$disasm" >&2 die "clang end-to-end test failed" fi done # 13. i32 (long) compile path. Type legalization splits i32 into # two i16 halves; the high half flows through the (add FrameIndex, # 2) shape, which previously crashed ISel with "Cannot select # FrameIndex<-2>". SelectFrameIndex now folds (add FI, const) so # the split loads land on a stack-relative addressing mode. # Return ABI: low->A, high->X (TAX in the epilogue). # Also asserts the native ADC carry chain (CLC + ADC + ADC) is in # place — task #49 replaced the bloated SETCC-based carry detect # (lda;cmp;bcc;lda) with a direct ADDC/ADDE-pattern lowering that # uses the C flag in P as a Glue-modeled physreg. log "check: clang compiles a long add (i32 split + A:X return)" cI32File="$(mktemp --suffix=.c)" oI32File="$(mktemp --suffix=.o)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File"' EXIT cat > "$cI32File" <<'EOF' long add32(long a, long b) { return a + b; } EOF "$CLANG" --target=w65816 -O2 -c "$cI32File" -o "$oI32File" disasmI32="$("$OBJDUMP" --triple=w65816 -d "$oI32File" 2>&1)" # TAX confirms the high-half-into-X part of the return ABI fired. # Without it, both halves would pile into A and one would be lost. # Exactly one CLC and exactly two ADCs prove the native carry chain # is wired (one CLC for lo, ADC lo, ADC hi-with-carry); a regression # to the SETCC path would show two CLCs and a bcc/cmp. for expect in "tax" "rtl" "clc" "adc"; do if ! printf '%s\n' "$disasmI32" | grep -qF "$expect"; then warn "i32 add test missing: $expect" printf '%s\n' "$disasmI32" >&2 die "i32 add end-to-end test failed" fi done nClc="$(printf '%s\n' "$disasmI32" | grep -cE '\bclc\b' || true)" nAdc="$(printf '%s\n' "$disasmI32" | grep -cE '\badc\b' || true)" nBcc="$(printf '%s\n' "$disasmI32" | grep -cE '\bbcc\b' || true)" if [ "$nClc" != "1" ] || [ "$nAdc" != "2" ] || [ "$nBcc" != "0" ]; then warn "i32 add carry-chain shape wrong (clc=$nClc adc=$nAdc bcc=$nBcc, want 1/2/0)" printf '%s\n' "$disasmI32" >&2 die "i32 add carry-chain regression" fi # Lock the post-StackSlotCleanup instruction count: should be ~11 for # add32 (rep + pha + clc + adc + sta + txa + adc + tax + lda + ply + rtl # — i32-first-arg in A:X means arg0_hi loads as TXA, no LDAfi). If # this regresses meaningfully (say >14) the cleanup pass, the # rematerialization flag, or the A:X first-arg ABI has been broken. nInsns="$(printf '%s\n' "$disasmI32" | grep -cE '^[0-9a-f]+:' || true)" if [ "$nInsns" -gt 14 ]; then warn "i32 add bloat (got $nInsns insns, want <=14 — was 25 pre-cleanup, 11 post)" printf '%s\n' "$disasmI32" >&2 die "i32 add code-quality regression" fi # The A:X arg0 ABI moves arg0_hi out of the stack slot, so the # asm should contain TXA (X→A for the hi-half ADC tied input) # exactly once. A regression to "load arg0_hi from stack" would # remove the TXA and add an extra LDA. nTxa="$(printf '%s\n' "$disasmI32" | grep -cE '\btxa\b' || true)" if [ "$nTxa" != "1" ]; then warn "i32 add: expected exactly 1 txa (i32-first-arg-in-A:X path); got $nTxa" printf '%s\n' "$disasmI32" >&2 die "i32 add A:X first-arg ABI regression" fi # i32 carry chain on two-Acc16 (no foldable load): exercises the # ADD_RR + ADDE_RR custom-inserter path. fib32 has live a/b values # the inserter must spill to a fresh slot; pre-fix this crashed at # ISel with "Cannot select: adde reg, reg". log "check: clang compiles a 32-bit fib loop (ADDE_RR inserter path)" cFibFile="$(mktemp --suffix=.c)" sFibFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile"' EXIT cat > "$cFibFile" <<'EOF' unsigned long fib32(unsigned long n) { unsigned long a = 0, b = 1, t; while (n > 0) { t = a + b; a = b; b = t; n--; } return a; } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cFibFile" -o "$sFibFile" 2>&1 >/dev/null; then die "i32 fib (ADDE_RR inserter) failed to compile" fi if ! grep -qE '\bclc\b' "$sFibFile" || ! grep -qE '\badc\b' "$sFibFile"; then warn "i32 fib output missing clc/adc" die "i32 fib carry-chain regression" fi # i32 multiply via __mulsi3 libcall: tests the multi-i16-return path # (RetCC_W65816 assigning A then X for 2 i16 returns) plus the i32 # arg push side. Pre-fix this hit "multi-return calls not yet # supported (Ins.size=4)" when LowerCallTo split the i32 return. log "check: clang compiles a long multiply via __mulsi3 libcall" cMulFile="$(mktemp --suffix=.c)" sMulFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile"' EXIT cat > "$cMulFile" <<'EOF' unsigned long mul32(unsigned long a, unsigned long b) { return a * b; } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cMulFile" -o "$sMulFile" 2>&1 >/dev/null; then die "i32 mul via __mulsi3 failed to compile" fi if ! grep -q '__mulsi3' "$sMulFile"; then die "i32 mul did not emit __mulsi3 libcall" fi # i32 shift-by-1 (SHL/SRL): the type-legalizer's SHL_PARTS / SRL_PARTS # expansion needs `(srl x, 15)` or `(shl x, 15)` for the carry-cross- # halves slot. Without inline patterns those fall to __lshrhi3 / # __ashlhi3 libcalls (~10 byte overhead per shift). SRL15A and # SHL15A pseudos handle them inline (`ASL/LSR; LDA #0; ROL/ROR`, # 3 bytes). Verify the shift-by-1 output doesn't contain a hi3 # libcall. log "check: clang i32 shift-by-1 stays inline (no __lshrhi3 / __ashlhi3 libcall)" cSh1File="$(mktemp --suffix=.c)" sSh1File="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cSh1File" "$sSh1File"' EXIT cat > "$cSh1File" <<'EOF' unsigned long shl1(unsigned long a) { return a << 1; } unsigned long shr1(unsigned long a) { return a >> 1; } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cSh1File" -o "$sSh1File" 2>&1 >/dev/null; then die "i32 shift-by-1 failed to compile" fi if grep -qE '__lshrhi3|__ashlhi3' "$sSh1File"; then warn "i32 shift-by-1 still calling i16 shift libcall — SRL15A/SHL15A pattern not firing" die "i32 shift-by-1 regression" fi # Varargs (): LowerFormalArguments creates a fixed FI # for the first vararg slot when IsVarArg; LowerVASTART stores # its address to the va_list pointer. VAARG/VACOPY/VAEND use # default LLVM expansions. Pre-fix this hit # "vararg functions not yet supported" fatal error. log "check: clang compiles a vararg function ()" cVaFile="$(mktemp --suffix=.c)" sVaFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cSh1File" "$sSh1File" "$cVaFile" "$sVaFile"' EXIT cat > "$cVaFile" <<'EOF' #include int sumArgs(int n, ...) { va_list args; va_start(args, n); int sum = 0; for (int i = 0; i < n; i++) sum += va_arg(args, int); va_end(args); return sum; } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cVaFile" -o "$sVaFile" 2>&1 >/dev/null; then die "vararg function failed to compile" fi # Stack-array LEA: `char arr[16]; arr[i] = ...` needs the address # of an alloca'd object as an i16 value. Pre-fix this hit "Cannot # select: FrameIndex<0>" because addr_fi only matches in load/store # contexts. W65816DAGToDAGISel::Select now lowers a bare # ISD::FrameIndex to ADDframe (FI, 0); eliminateFrameIndex expands # ADDframe into TSC + CLC + ADC #disp. log "check: clang takes the address of a stack-allocated array" cAllocaFile="$(mktemp --suffix=.c)" sAllocaFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile"' EXIT cat > "$cAllocaFile" <<'EOF' extern void use_buffer(char *p); void writeBytes(char v) { char tmp[8]; for (int i = 0; i < 8; i++) tmp[i] = v + i; use_buffer(tmp); // forces &tmp[0] to escape } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cAllocaFile" -o "$sAllocaFile" 2>&1 >/dev/null; then die "alloca'd-array address failed to compile" fi # The TSC; CLC; ADC #disp triple is the LEA expansion of ADDframe; # at least one occurrence proves the pseudo wired through. if ! grep -qE '^\s*tsc' "$sAllocaFile"; then die "alloca'd-array LEA missing TSC (ADDframe expansion broken)" fi # i8 stores into the alloca slot must be 8-bit (SEP/REP bracketed). # A bare 16-bit `sta d,S` with M=0 writes 2 bytes and corrupts the # next slot or the return address. The writeBytes function unrolls # to 8 i8 stores (one per `tmp[i] = v + i`); each must be inside a # `sep #$20 ... rep #$20` pair. Count `sta d,S` occurrences inside # vs. outside SEP/REP — at least 8 must be inside. if ! awk ' /^\s*sep\s+#0x20\s*$/ { sep = 1; next } /^\s*rep\s+#0x20\s*$/ { sep = 0; next } /^\s*sta\s+0x[0-9a-f]+,\s*s\s*$/ { if (sep) inside++ } END { if (inside < 8) { print "INSIDE=" inside "; want >= 8"; exit 1 } } ' "$sAllocaFile"; then die "alloca'd-array i8 stores not properly SEP/REP bracketed (8-bit store regression)" fi # Same correctness check for i8 stores to *globals* in an M=0 # function. STA8abs in AsmPrinter must wrap with SEP/REP when # UsesAcc8 is false; bare `sta g+N` in M=0 writes 2 bytes and # corrupts the next global. log "check: clang lowers 'g = 0' to single STZ via AsmPrinter peephole" cStzFile="$(mktemp --suffix=.c)" sStzFile="$(mktemp --suffix=.s)" cat > "$cStzFile" <<'EOF' unsigned short g; void zero(void) { g = 0; } EOF "$CLANG" --target=w65816 -O2 -S "$cStzFile" -o "$sStzFile" # Should see exactly one `stz g` and zero `lda #0` in the function. if ! grep -qE '^\s*stz\s+g\b' "$sStzFile"; then warn "STZ peephole not firing"; cat "$sStzFile" >&2 die "expected 'stz g' in zero() but didn't find it" fi if grep -qE '^\s*lda\s+#0x0' "$sStzFile"; then warn "STZ peephole left a redundant LDA #0"; cat "$sStzFile" >&2 die "STZ peephole should have eliminated the LDA #0" fi rm -f "$cStzFile" "$sStzFile" # Multi-STA-from-shared-LDA: when SDAG CSE shares one `lda #0` across # multiple `sta`s, the peephole MUST NOT fire on the first STA (would # delete the LDA, leaving the remaining STAs reading dead A). Verify # the LDA #0 is preserved and no STZ appears in this case. log "check: STZ peephole skips when LDA #0 feeds multiple STAs" cStzMultiFile="$(mktemp --suffix=.c)" sStzMultiFile="$(mktemp --suffix=.s)" cat > "$cStzMultiFile" <<'EOF' unsigned short ga, gb, gc; void zeroAll(void) { ga = 0; gb = 0; gc = 0; } EOF "$CLANG" --target=w65816 -O2 -S "$cStzMultiFile" -o "$sStzMultiFile" if ! grep -qE '^\s*lda\s+#0x0' "$sStzMultiFile"; then warn "STZ peephole over-eagerly deleted shared LDA #0" cat "$sStzMultiFile" >&2 die "expected lda #0 preserved when feeding multiple STAs" fi n_sta=$(grep -cE '^\s*sta\s+g[abc]\b' "$sStzMultiFile") if [ "$n_sta" -ne 3 ]; then warn "expected 3 STA after shared LDA #0, found $n_sta" cat "$sStzMultiFile" >&2 die "STZ peephole regressed on multi-STA case" fi rm -f "$cStzMultiFile" "$sStzMultiFile" log "check: clang lowers 'foo(1,2,3)' constant args via PEA" cPeaFile="$(mktemp --suffix=.c)" sPeaFile="$(mktemp --suffix=.s)" cat > "$cPeaFile" <<'EOF' extern void foo(int a, int b, int c); void caller(void) { foo(1, 2, 3); } EOF "$CLANG" --target=w65816 -O2 -S "$cPeaFile" -o "$sPeaFile" # arg2 (c=3) and arg1 (b=2) are pushed via PEA. arg0 (a=1) # stays in A. Expect at least 2 `pea` instructions and zero # `pha` after a `lda #imm`. n_pea=$(grep -cE '^\s*pea\s+' "$sPeaFile") if [ "$n_pea" -lt 2 ]; then warn "PEA peephole not firing on constant-arg pushes" cat "$sPeaFile" >&2 die "expected >= 2 PEA in caller() but found $n_pea" fi rm -f "$cPeaFile" "$sPeaFile" # PEI peephole: an i64-libcall return whose high half lives in # DPF0 ($F0..$F1) is forwarded to the next call as a stacked arg. # Pre-peephole shape: `lda $f0; pha`. Post-peephole: `pei $f0`, # saving 1 byte and not touching A. log "check: clang lowers DPF0 forwarding via PEI" cPeiFile="$(mktemp --suffix=.c)" sPeiFile="$(mktemp --suffix=.s)" cat > "$cPeiFile" <<'EOF' unsigned long long divmod(unsigned long long a, unsigned long long b); unsigned long long use(unsigned long long x); unsigned long long chain(unsigned long long a, unsigned long long b) { return use(divmod(a, b)); } EOF "$CLANG" --target=w65816 -O2 -S "$cPeiFile" -o "$sPeiFile" if ! grep -qE '^\s*pei\s+0xf0\b' "$sPeiFile"; then warn "PEI peephole not firing on DPF0 forwarding" cat "$sPeiFile" >&2 die "expected 'pei 0xf0' in chain() but didn't find it" fi rm -f "$cPeiFile" "$sPeiFile" log "check: clang i8 store to global in M=0 mode is SEP/REP bracketed" cGlobFile="$(mktemp --suffix=.c)" sGlobFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cGlobFile" "$sGlobFile"' EXIT cat > "$cGlobFile" <<'EOF' char g[4]; void writeMixed(int x) { g[0] = (char)x; g[1] = (char)(x + 1); g[2] = (char)(x + 2); g[3] = (char)(x + 3); } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cGlobFile" -o "$sGlobFile" 2>&1 >/dev/null; then die "global-i8-store M=0 test failed to compile" fi # Each `sta g+N` (or `sta g`) must sit inside SEP/REP brackets. if ! awk ' /^\s*sep\s+#0x20\s*$/ { sep = 1; next } /^\s*rep\s+#0x20\s*$/ { sep = 0; next } /^\s*sta\s+g(\+[0-9]+)?\s*$/ { if (!sep) { print "NAKED:" $0; exit 1 } } ' "$sGlobFile"; then die "i8 store to global in M=0 emits naked 16-bit STA (would clobber adjacent global)" fi # signed-byte arithmetic (`(int)(*p) - (int)(*q)` style — strcmp). # Exercises three formerly-missing patterns: SEXTLOAD i16 from i8 # (we Expand it to (sext (load))), sext_inreg i16 from i8 (the # `((x & 0xFF) ^ 0x80) - 0x80` tablegen Pat), and extloadi8 from # an Acc16 register pointer (LDAptr / "high byte don't care"). log "check: clang compiles a signed-byte strcmp (sextload + sext_inreg + extload-via-ptr)" cStrFile="$(mktemp --suffix=.c)" sStrFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile"' EXIT cat > "$cStrFile" <<'EOF' int strcmp32(const char *a, const char *b) { while (*a && *a == *b) { a++; b++; } return (int)(*a) - (int)(*b); } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cStrFile" -o "$sStrFile" 2>&1 >/dev/null; then die "signed-byte strcmp failed to compile" fi # Indirect calls (function pointers). Lowered via the runtime # trampoline at runtime/src/libgcc.s::__jsl_indir, which does # JMP (__indirTarget) — caller stores target to __indirTarget then # JSL __jsl_indir. Pre-fix, LowerCall reported a fatal error. log "check: clang compiles an indirect call (via __jsl_indir trampoline)" cIndFile="$(mktemp --suffix=.c)" sIndFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile"' EXIT cat > "$cIndFile" <<'EOF' typedef int (*BinOp)(int, int); int doOp(BinOp op, int x, int y) { return op(x, y); } EOF if ! "$CLANG" --target=w65816 -O2 -S "$cIndFile" -o "$sIndFile" 2>&1 >/dev/null; then die "indirect call failed to compile" fi if ! grep -q '__indirTarget' "$sIndFile"; then die "indirect call missing __indirTarget store" fi if ! grep -q '__jsl_indir' "$sIndFile"; then die "indirect call missing JSL to __jsl_indir trampoline" fi # SEP/REP toggle coalescing (W65816SepRepCleanup, addPreEmitPass). # Each STA8fi expands to `SEP #$20 ; STA d,S ; REP #$20`. When two # such stores sit back-to-back in the MIR, the post-PEI stream # contains a redundant `REP #$20 ; SEP #$20` pair that the cleanup # pass should drop. We use a volatile-store IR snippet so the # store-merger can't fold the two i8 stores into one i16, and so # nothing 16-bit-mode sneaks between them. log "check: SEP/REP toggle pass coalesces back-to-back i8 alloca stores" irCoalesceFile="$(mktemp --suffix=.ll)" sCoalesceFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile"' EXIT cat > "$irCoalesceFile" <<'EOF' declare void @sink(ptr) define void @adjacent(i8 %v) { %p = alloca [2 x i8], align 1 %p0 = getelementptr inbounds [2 x i8], ptr %p, i16 0, i16 0 %p1 = getelementptr inbounds [2 x i8], ptr %p, i16 0, i16 1 store volatile i8 %v, ptr %p0 store volatile i8 %v, ptr %p1 call void @sink(ptr %p) ret void } EOF if ! "$LLC" -march=w65816 -O2 "$irCoalesceFile" -o "$sCoalesceFile" 2>&1 >/dev/null; then die "SEP/REP coalescing test failed to compile" fi # Expect a single `sep #$20 ; sta ... ; sta ... ; rep #$20` block # with NO `rep #$20 ; sep #$20` toggle anywhere. The smoking gun # of an absent pass: at least one consecutive `rep #$20`/`sep #$20` # pair (in either order) appears in the output. if ! awk ' BEGIN { prev = "" } /^\s*sep\s+#0x20\s*$/ { if (prev == "rep") { print "TOGGLE: rep then sep at line " NR; exit 1 } prev = "sep"; next } /^\s*rep\s+#0x20\s*$/ { if (prev == "sep") { print "TOGGLE: sep then rep at line " NR; exit 1 } prev = "rep"; next } /^\s*[a-z]/ { prev = "" } ' "$sCoalesceFile"; then cat "$sCoalesceFile" >&2 die "SEP/REP cleanup pass left an adjacent REP/SEP toggle in the output" fi # Belt-and-braces: the body must contain TWO consecutive `sta d,S` # inside one SEP/REP region (proves both stores ran in M=1 without # an intervening toggle). if ! awk ' /^\s*sep\s+#0x20\s*$/ { in_m1 = 1; consecutive = 0; next } /^\s*rep\s+#0x20\s*$/ { in_m1 = 0; consecutive = 0; next } /^\s*sta\s+0x[0-9a-f]+,\s*s\s*$/ { if (in_m1) { consecutive++; if (consecutive >= 2) { found = 1 } } next } /^\s*[a-z]/ { consecutive = 0 } END { if (!found) exit 1 } ' "$sCoalesceFile"; then cat "$sCoalesceFile" >&2 die "SEP/REP cleanup pass: no two consecutive sta d,S found inside one SEP/REP region" fi # Mixed-mode regression guard: a function that increments a char # global and returns it must NOT use 8-bit-M-only encodings for # i16 immediates. Pre-fix (per-function "pure-i8" prologue), the # late sign-extension `and #$ff; eor #$80; sbc #$80` emitted as # 3-byte i16 immediates but executed in M=1 — the CPU read only # the low byte of each immediate, sliding subsequent opcodes # one byte off and treating the immediate's high byte as the # next opcode (often $00 = BRK). Now: prologue is REP #$30 only # (no SEP), and i8 ops carry their own SEP/REP wrap. log "check: mixed i8/i16 in one function — no SEP-only-prologue miscompile" cMixFile="$(mktemp --suffix=.c)" sMixFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile"' EXIT cat > "$cMixFile" <<'EOF' char g; char inc_g(void) { g++; return g; } EOF "$CLANG" --target=w65816 -O2 -S "$cMixFile" -o "$sMixFile" # Prologue must be REP #$30, NOT a bare SEP #$20 transition. # (The prologue is the FIRST mode-affecting instruction.) if ! awk ' BEGIN { found = 0 } /^\s*rep\s+#0x30\s*$/ { found = 1; exit 0 } /^\s*sep\s+#0x20\s*$/ { exit 1 } /^\s*rep\s+#0x10\s*$/ { exit 1 } END { if (!found) exit 1 } ' "$sMixFile"; then cat "$sMixFile" >&2 die "mixed i8/i16: prologue is not the expected REP #\$30 (8-bit-M-prologue regression)" fi # Linker: tools/link816 (built from src/link816/link816.cpp) concatenates # one-or-more ELF .o files, resolves W65816 relocations (R_W65816_IMM8/ # IMM16/IMM24/PCREL8/16, plus generic FK_Data_*), and emits a flat # binary. Verify by linking a minimal program that calls __mulhi3, # then disassemble the JSL operand and confirm it points at __mulhi3's # actual post-link address (per the symbol map). log "check: link816 resolves a libcall to libgcc" cLinkFile="$(mktemp --suffix=.c)" oLinkFile="$(mktemp --suffix=.o)" oLibgccFile="$(mktemp --suffix=.o)" binLinkFile="$(mktemp --suffix=.bin)" mapLinkFile="$(mktemp --suffix=.map)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile"' EXIT cat > "$cLinkFile" <<'EOF' int mul(int a, int b) { return a * b; } EOF "$CLANG" --target=w65816 -O2 -c "$cLinkFile" -o "$oLinkFile" "$BUILD_DIR/bin/llvm-mc" -arch=w65816 -filetype=obj \ "$PROJECT_ROOT/runtime/src/libgcc.s" -o "$oLibgccFile" # No main in this test (it's just a library object); use # --no-gc-sections so the linker keeps `mul` and the libgcc # __mulhi3 it references. With gc-sections (the default), # there's no live root and everything would drop. "$PROJECT_ROOT/tools/link816" -o "$binLinkFile" \ --text-base 0x8000 --map "$mapLinkFile" --no-gc-sections \ "$oLinkFile" "$oLibgccFile" 2>/dev/null if [ ! -s "$binLinkFile" ]; then die "link816 produced empty/missing binary" fi mul_addr=$(awk -F' = ' '$1 == "mul" { print $2 }' "$mapLinkFile") mulhi3_addr=$(awk -F' = ' '$1 == "__mulhi3" { print $2 }' "$mapLinkFile") if [ -z "$mul_addr" ] || [ -z "$mulhi3_addr" ]; then cat "$mapLinkFile" >&2 die "link map missing 'mul' or '__mulhi3' symbol" fi # mul's body is short — the JSL to __mulhi3 should appear near the # start. Read mul's bytes (mul_addr - 0x8000 = file offset) and # search for `0x22 lo mid hi` matching __mulhi3's address. mul_off=$((mul_addr - 0x8000)) expect_lo=$(printf '%02x' $((mulhi3_addr & 0xff))) expect_mid=$(printf '%02x' $(((mulhi3_addr >> 8) & 0xff))) expect_hi=$(printf '%02x' $(((mulhi3_addr >> 16) & 0xff))) # Hexdump mul's first 32 bytes and look for the JSL pattern. if ! od -An -tx1 -N 32 -j "$mul_off" "$binLinkFile" \ | tr -s ' \n' ' ' \ | grep -qE " 22 ${expect_lo} ${expect_mid} ${expect_hi}( |$)"; then od -An -tx1 -N 32 -j "$mul_off" "$binLinkFile" >&2 die "link816: mul's JSL operand does not point at __mulhi3 (expected 22 ${expect_lo} ${expect_mid} ${expect_hi})" fi # Soft-float runtime: compile runtime/src/softFloat.c, then link a # tiny float-using program against it. Confirms (a) the real # soft-float helpers compile (which exercises the W65816BranchExpand # pass — the C-based __addsf3 has internal Bxx targets > 128 bytes # and would error at link time without the inversion-and-jump # transform), (b) all the libcalls clang emits for float ops have # matching definitions in softFloat.o. log "check: soft-float runtime links (real impl, not stubs)" cFltFile="$(mktemp --suffix=.c)" oFltFile="$(mktemp --suffix=.o)" oSfFile="$(mktemp --suffix=.o)" binFltFile="$(mktemp --suffix=.bin)" mapFltFile="$(mktemp --suffix=.map)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile" "$cFltFile" "$oFltFile" "$oSfFile" "$binFltFile" "$mapFltFile"' EXIT cat > "$cFltFile" <<'EOF' float fadd(float a, float b) { return a + b; } float fmul(float a, float b) { return a * b; } int feq(float a, float b) { return a == b; } int toInt(float x) { return (int)x; } float fromInt(int n) { return (float)n; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cFltFile" -o "$oFltFile" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softFloat.c" -o "$oSfFile" # No main here either (test compiles a .o-only "soft-float lib" link). # --no-gc-sections so all soft-float symbols stay. "$PROJECT_ROOT/tools/link816" -o "$binFltFile" \ --text-base 0x8000 --map "$mapFltFile" --no-gc-sections \ "$oFltFile" "$oSfFile" "$oLibgccFile" 2>/dev/null if [ ! -s "$binFltFile" ]; then die "soft-float runtime failed to link" fi # Verify the JSL targets are resolved (no zero entries in the # critical libcall slots). if ! grep -q "__addsf3" "$mapFltFile"; then die "soft-float map missing __addsf3" fi if ! grep -q "__mulsf3" "$mapFltFile"; then die "soft-float map missing __mulsf3" fi if ! grep -q "__fixsfsi" "$mapFltFile"; then die "soft-float map missing __fixsfsi" fi # Soft-double runtime: compile runtime/src/softDouble.c (was a stub # returning zero; now a real IEEE 754 binary64 implementation in C). # Confirms (a) the C version compiles end-to-end (greedy regalloc # + WidenAcc16 unblocked the prior Register Coalescer crash on # this code), (b) all the libcalls clang emits for double ops # have matching definitions. log "check: soft-double runtime compiles (real impl, not stubs)" cDblFile="$(mktemp --suffix=.c)" oDblFile="$(mktemp --suffix=.o)" oSdFile="$(mktemp --suffix=.o)" binDblFile="$(mktemp --suffix=.bin)" mapDblFile="$(mktemp --suffix=.map)" cat > "$cDblFile" <<'EOF' double dadd(double a, double b) { return a + b; } double dmul(double a, double b) { return a * b; } int deq(double a, double b) { return a == b; } int toInt(double x) { return (int)x; } double fromInt(int n) { return (double)n; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cDblFile" -o "$oDblFile" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softDouble.c" -o "$oSdFile" "$PROJECT_ROOT/tools/link816" -o "$binDblFile" \ --text-base 0x8000 --map "$mapDblFile" --no-gc-sections \ "$oDblFile" "$oSdFile" "$oLibgccFile" 2>/dev/null if [ ! -s "$binDblFile" ]; then die "soft-double runtime failed to link" fi if ! grep -q "__adddf3" "$mapDblFile"; then die "soft-double map missing __adddf3" fi if ! grep -q "__muldf3" "$mapDblFile"; then die "soft-double map missing __muldf3" fi if ! grep -q "__fixdfsi" "$mapDblFile"; then die "soft-double map missing __fixdfsi" fi rm -f "$cDblFile" "$oDblFile" "$oSdFile" "$binDblFile" "$mapDblFile" # setjmp/longjmp from libgcc.s. Compile a tiny program that uses # both and verify the symbols are present in the linked binary. log "check: setjmp/longjmp link from libgcc" cSjFile="$(mktemp --suffix=.c)" oSjFile="$(mktemp --suffix=.o)" binSjFile="$(mktemp --suffix=.bin)" mapSjFile="$(mktemp --suffix=.map)" cat > "$cSjFile" <<'EOF' typedef unsigned char jmp_buf[8]; int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val) __attribute__((noreturn)); jmp_buf env; int trip(int x) { if (setjmp(env) == 0) { if (x > 5) longjmp(env, 42); return 1; } return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cSjFile" -o "$oSjFile" "$PROJECT_ROOT/tools/link816" -o "$binSjFile" \ --text-base 0x8000 --map "$mapSjFile" \ "$oSjFile" "$oLibgccFile" 2>/dev/null if ! grep -q "^setjmp" "$mapSjFile" || ! grep -q "^longjmp" "$mapSjFile"; then die "setjmp/longjmp not in linked map" fi rm -f "$cSjFile" "$oSjFile" "$binSjFile" "$mapSjFile" # Regression guard for #91: setjmp/longjmp use raw .byte for the # (dp),Y opcodes because llvm-mc misencodes the mnemonic forms. # If someone reverts the .byte hack the asm reverts to absolute,Y # and setjmp silently writes to $00:E0 instead of through env — # caught at link time only by mom-and-dad luck. Disassemble # libgcc.o and verify the setjmp body contains opcode 0x91 (the # real DP-indirect-Y) and NOT 0x99 (absolute,Y) at minimum once. log "check: libgcc.o setjmp uses (dp),Y opcode 0x91 not absolute,Y 0x99 (#91)" # llvm-objdump can't decode 0x91 (the disasm calls it ) so # we check the raw byte stream of libgcc.o instead. The setjmp # body must contain the byte pair 91 e0 (= STA ($E0),Y). if ! xxd -p -c 99999 "$oLibgccFile" | tr -d '\n' | grep -q '91e0'; then die "libgcc.o missing raw 91 e0 byte pair (STA (\$E0),Y) — #91 regressed" fi # And it must NOT contain `99 e0 00` (= STA \$00E0,Y absolute,Y) in # libgcc — that's the misencoding the .byte hack works around. if xxd -p -c 99999 "$oLibgccFile" | tr -d '\n' | grep -q '99e000'; then die "libgcc.o contains 99 e0 00 (STA \$00E0,Y absolute) — assembler reverted to bad mnemonic (#91)" fi # Static constructors: linker collects .init_array sections and # emits __init_array_start / __init_array_end synthetic symbols. # crt0 walks them via __jsl_indir. This check verifies the # linker collection — runtime verification is on the IIgs side # (blocked by ROM IRQ pre-empting injected programs). log "check: linker collects .init_array and emits boundary symbols" cInitFile="$(mktemp --suffix=.c)" oInitFile="$(mktemp --suffix=.o)" binInitFile="$(mktemp --suffix=.bin)" mapInitFile="$(mktemp --suffix=.map)" cat > "$cInitFile" <<'EOF' volatile unsigned short m = 0x1111; __attribute__((constructor)) static void ctor1(void) { m = 0xAAAA; } int main(void) { return m; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cInitFile" -o "$oInitFile" "$PROJECT_ROOT/tools/link816" -o "$binInitFile" \ --text-base 0x8000 --map "$mapInitFile" \ "$oInitFile" "$oLibgccFile" 2>/dev/null if ! grep -q "^__init_array_start" "$mapInitFile" \ || ! grep -q "^__init_array_end" "$mapInitFile" \ || ! grep -q "^ctor1" "$mapInitFile"; then die "init_array boundary symbols or ctor not in map" fi # Sanity: __init_array_end > __init_array_start (non-empty) s=$(grep -E "^__init_array_start = " "$mapInitFile" | grep -oE '0x[0-9a-f]+' | head -1) e=$(grep -E "^__init_array_end = " "$mapInitFile" | grep -oE '0x[0-9a-f]+' | head -1) if [ "$s" = "$e" ]; then die "init_array is empty even though ctor1 is defined" fi rm -f "$cInitFile" "$oInitFile" "$binInitFile" "$mapInitFile" # Static constructors RUN end-to-end: build crt0+main+ctor program, # load into MAME, and verify the constructor wrote a sentinel value # into a BSS variable. This proves crt0's init_array walk works # at runtime (not just that the linker emitted boundary symbols). if command -v mame >/dev/null && [ -d "$PROJECT_ROOT/tools/mame/roms" ]; then log "check: MAME runs static constructors via crt0 init_array walk" cCMameFile="$(mktemp --suffix=.c)" oCMameFile="$(mktemp --suffix=.o)" oCrt0File="$(mktemp --suffix=.o)" binCMameFile="$(mktemp --suffix=.bin)" cat > "$cCMameFile" <<'EOF' volatile unsigned short ctorRan = 0; __attribute__((constructor)) static void initFn(void) { ctorRan = 0xABCD; } int main(void) { while (1) {} return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cCMameFile" -o "$oCMameFile" "$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 \ -filetype=obj "$PROJECT_ROOT/runtime/src/crt0.s" -o "$oCrt0File" "$PROJECT_ROOT/tools/link816" -o "$binCMameFile" \ --text-base 0x1000 \ "$oCrt0File" "$oCMameFile" "$oLibgccFile" 2>/dev/null # ctorRan lives in BSS at $2000 (linker layout). Read $00:2000 # via the runner; expect 0xABCD if the constructor ran. if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binCMameFile" 0x002000 abcd >/dev/null 2>&1; then warn "MAME: constructor did not run (read \$2000 != 0xABCD)" die "constructor end-to-end failed" fi rm -f "$cCMameFile" "$oCMameFile" "$binCMameFile" # Soft-float runtime executes correctly: compute 1.5f + 2.5f and # verify the IEEE 754 bit pattern matches 0x40800000. log "check: MAME runs soft-float __addsf3 → bit pattern correct" cFltMame="$(mktemp --suffix=.c)" oFltMame="$(mktemp --suffix=.o)" oSfMame="$(mktemp --suffix=.o)" binFltMame="$(mktemp --suffix=.bin)" # Reuse oCrt0File from the constructor test above. cat > "$cFltMame" <<'EOF' __attribute__((noinline)) static void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9, 0x02\npha\nplb\nrep #0x20\n" ::: "memory"); } int main(void) { float a = 1.5f, b = 2.5f; float c = a + b; unsigned long bits; __builtin_memcpy(&bits, &c, 4); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)(bits & 0xFFFF); *(volatile unsigned short *)0x5002 = (unsigned short)(bits >> 16); while (1) {} return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cFltMame" -o "$oFltMame" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softFloat.c" -o "$oSfMame" "$PROJECT_ROOT/tools/link816" -o "$binFltMame" \ --text-base 0x1000 \ "$oCrt0File" "$oFltMame" "$oSfMame" "$oLibgccFile" 2>/dev/null if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binFltMame" --check \ 0x025000=0000 0x025002=4080 >/dev/null 2>&1; then die "soft-float MAME: 1.5+2.5 != 4.0 (bit pattern wrong)" fi rm -f "$cFltMame" "$oFltMame" "$oSfMame" "$binFltMame" # Soft-double runtime executes correctly: compute 1.5 + 2.5 and # verify IEEE 754 binary64 bit pattern = 0x4010000000000000. log "check: MAME runs soft-double __adddf3 → bit pattern correct" cDblMame="$(mktemp --suffix=.c)" oDblMame="$(mktemp --suffix=.o)" oSdMame="$(mktemp --suffix=.o)" binDblMame="$(mktemp --suffix=.bin)" cat > "$cDblMame" <<'EOF' __attribute__((noinline)) static void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9, 0x02\npha\nplb\nrep #0x20\n" ::: "memory"); } int main(void) { double a = 1.5, b = 2.5; double c = a + b; unsigned long long bits; __builtin_memcpy(&bits, &c, 8); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)(bits & 0xFFFF); *(volatile unsigned short *)0x5002 = (unsigned short)((bits >> 16) & 0xFFFF); *(volatile unsigned short *)0x5004 = (unsigned short)((bits >> 32) & 0xFFFF); *(volatile unsigned short *)0x5006 = (unsigned short)((bits >> 48) & 0xFFFF); while (1) {} return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cDblMame" -o "$oDblMame" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softDouble.c" -o "$oSdMame" "$PROJECT_ROOT/tools/link816" -o "$binDblMame" \ --text-base 0x1000 \ "$oCrt0File" "$oDblMame" "$oSdMame" "$oLibgccFile" 2>/dev/null if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binDblMame" --check \ 0x025000=0000 0x025002=0000 0x025004=0000 0x025006=4010 \ >/dev/null 2>&1; then die "soft-double MAME: 1.5+2.5 != 4.0 (bit pattern wrong)" fi rm -f "$cDblMame" "$oDblMame" "$oSdMame" "$binDblMame" "$oCrt0File" fi # Fuzzer: generate 20 small random C programs and verify all compile. # Catches backend crashes / lowering gaps the hand-written checks miss. log "check: random C fuzzer (20 programs compile cleanly)" if ! python3 "$PROJECT_ROOT/scripts/fuzzCompile.py" -n 20 -q > /dev/null; then die "random C fuzzer found compile failures" fi # C++ basics: virtual call (vtable indirect), Itanium ABI symbol # mangling, global ctor → .init_array entry. Compile-only check. log "check: clang++ compiles class with virtual + non-trivial ctor" cppFile="$(mktemp --suffix=.cc)" oCppFile="$(mktemp --suffix=.o)" binCppFile="$(mktemp --suffix=.bin)" mapCppFile="$(mktemp --suffix=.map)" CLANGXX="${CLANG%clang}clang++" cat > "$cppFile" <<'EOF' extern int sideEffect(int); struct Base { virtual int v(int x) const { return x + 1; } }; struct Derived : Base { int v(int x) const override { return x * 2; } Derived() { sideEffect(99); } }; Derived g; int call(Base *b, int x) { return b->v(x); } EOF "$CLANGXX" --target=w65816 -O2 -ffunction-sections \ -fno-exceptions -fno-rtti -c "$cppFile" -o "$oCppFile" # Just check the .o has the expected sections / mangled symbols. syms="$("$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-objdump" \ --triple=w65816 -t "$oCppFile" 2>/dev/null)" secs="$("$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-objdump" \ --triple=w65816 -h "$oCppFile" 2>/dev/null)" if ! printf '%s\n' "$syms" | grep -qE '_Z4callP4Basei'; then die "C++: no Itanium-mangled call symbol" fi if ! printf '%s\n' "$secs" | grep -qE '\.init_array'; then die "C++: no .init_array for non-trivial global ctor" fi rm -f "$cppFile" "$oCppFile" "$binCppFile" "$mapCppFile" # End-to-end MAME execution: compile a tiny C program that writes # a known value to $E0 (DP), assemble + link to a raw flat binary, # load into MAME's apple2gs RAM at $1000, set PC, run, read back # $E0, verify the value matches. This is the first byte-level # runtime correctness check in the suite — proves compile-link-run # actually works, not just that asm-pattern grep matches. if command -v mame >/dev/null && [ -d "$PROJECT_ROOT/tools/mame/roms" ]; then log "check: MAME runs compiled code and reads back expected value" cMameFile="$(mktemp --suffix=.c)" sMameFile="$(mktemp --suffix=.s)" oMameFile="$(mktemp --suffix=.o)" binMameFile="$(mktemp --suffix=.bin)" # Write directly to DP $E0..$E1 from C. cat > "$cMameFile" <<'EOF' void _start(void) { *(volatile unsigned short *)0xE0 = 0x1234 + 0x5678; // 0x68AC while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cMameFile" -o "$oMameFile" # Link with text-base 0x1000 so PC-relative branches resolve # correctly when loaded at that address. "$PROJECT_ROOT/tools/link816" -o "$binMameFile" \ --text-base 0x1000 "$oMameFile" "$oLibgccFile" 2>/dev/null if [ ! -s "$binMameFile" ]; then die "MAME: failed to link test binary" fi if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binMameFile" 0xe0 68ac >/dev/null 2>&1; then die "MAME: read at \$E0 != 0x68AC after running compiled C" fi rm -f "$cMameFile" "$sMameFile" "$oMameFile" "$binMameFile" # Recursive call regression: catches the empty-descending-SP # off-by-one in eliminateFrameIndex. fact(5)=120 ($0078) and the # value passes through main() → fact(5) → result-store, which # only works if locals don't collide with JSL retaddr push. log "check: MAME runs recursive fact(5) → 120 (off-by-one regression)" cFactFile="$(mktemp --suffix=.c)" oFactFile="$(mktemp --suffix=.o)" binFactFile="$(mktemp --suffix=.bin)" cat > "$cFactFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short fact(unsigned short n) { if (n <= 1) return 1; return n * fact(n - 1); } int main(void) { unsigned short r = fact(5); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFactFile" -o "$oFactFile" oLibcF="$(mktemp --suffix=.o)" oStrtolF="$(mktemp --suffix=.o)" oSnprintfF="$(mktemp --suffix=.o)" oQsortF="$(mktemp --suffix=.o)" oExtrasF="$(mktemp --suffix=.o)" oStrtokF="$(mktemp --suffix=.o)" oMathF="$(mktemp --suffix=.o)" oSfF="$(mktemp --suffix=.o)" oSdF="$(mktemp --suffix=.o)" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/libc.c" -o "$oLibcF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/strtol.c" -o "$oStrtolF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/snprintf.c" -o "$oSnprintfF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/qsort.c" -o "$oQsortF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/extras.c" -o "$oExtrasF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/strtok.c" -o "$oStrtokF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/math.c" -o "$oMathF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softFloat.c" -o "$oSfF" "$CLANG" --target=w65816 -O2 -ffunction-sections \ -c "$PROJECT_ROOT/runtime/src/softDouble.c" -o "$oSdF" oCrt0F="$(mktemp --suffix=.o)" "$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 \ -filetype=obj "$PROJECT_ROOT/runtime/src/crt0.s" -o "$oCrt0F" "$PROJECT_ROOT/tools/link816" -o "$binFactFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oFactFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binFactFile" 0x025000 0078 >/dev/null 2>&1; then die "MAME: fact(5) != 120 (off-by-one stack-rel skew regression)" fi rm -f "$cFactFile" "$oFactFile" "$binFactFile" # Loop with flag-corrupting TXA between counter-DEC and BNE. # Canary for the PHP/PLP wrap fix that excludes stack-rel ops: # without the wrap-tightening, the PHP-saved P gets clobbered # by an in-wrap sta d,S and PLP loads garbage, making BNE # branch forever. Iterative fib(10) = 55 ($0037). log "check: MAME runs iterative fib(10) → 55 (PHP/PLP wrap regression)" cFibFile2="$(mktemp --suffix=.c)" oFibFile2="$(mktemp --suffix=.o)" binFibFile2="$(mktemp --suffix=.bin)" cat > "$cFibFile2" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned short fib(unsigned short n) { if (n < 2) return n; unsigned short a = 0, b = 1; for (unsigned short i = 2; i <= n; i++) { unsigned short t = a + b; a = b; b = t; } return b; } int main(void) { unsigned short r = fib(10); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFibFile2" -o "$oFibFile2" "$PROJECT_ROOT/tools/link816" -o "$binFibFile2" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oFibFile2" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binFibFile2" 0x025000 0037 >/dev/null 2>&1; then die "MAME: iterative fib(10) != 55 (PHP/PLP wrap regression)" fi rm -f "$cFibFile2" "$oFibFile2" "$binFibFile2" # Recursive fib with phi-resolution across loop-exit edge. # Canary for the SpillToX cross-block-use check: without it, # the peephole elided the loop's STA-to-merge-slot and the # merge block read the stale bb.0-init value (0) instead of # the loop accumulator. fib(7)=13 ($000D). log "check: MAME runs recursive fib(7) → 13 (SpillToX cross-block regression)" cFibFile3="$(mktemp --suffix=.c)" oFibFile3="$(mktemp --suffix=.o)" binFibFile3="$(mktemp --suffix=.bin)" cat > "$cFibFile3" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short fib(unsigned short n) { if (n < 2) return n; return fib(n-1) + fib(n-2); } int main(void) { unsigned short r = fib(7); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFibFile3" -o "$oFibFile3" "$PROJECT_ROOT/tools/link816" -o "$binFibFile3" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oFibFile3" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binFibFile3" 0x025000 000d >/dev/null 2>&1; then die "MAME: recursive fib(7) != 13 (SpillToX cross-block regression)" fi rm -f "$cFibFile3" "$oFibFile3" "$binFibFile3" # Array-sum loop with indirect deref + counter-DEC + LDA # between DEC and BNE. Canary for the disp-bump-inside-wrap # fix: PHP decrements S, so any stack-rel inside the wrap # needs ImmOffset += 1 to compensate. sum 11+22+...+88 = 396 # ($018C). log "check: MAME runs array sumTable → 396 (disp-bump-inside-wrap regression)" cArrFile="$(mktemp --suffix=.c)" oArrFile="$(mktemp --suffix=.o)" binArrFile="$(mktemp --suffix=.bin)" cat > "$cArrFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short table[8] = { 11, 22, 33, 44, 55, 66, 77, 88 }; __attribute__((noinline)) unsigned short sumTable(unsigned short *arr, unsigned short n) { unsigned short s = 0; for (unsigned short i = 0; i < n; i++) s += arr[i]; return s; } int main(void) { unsigned short r = sumTable(table, 8); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cArrFile" -o "$oArrFile" "$PROJECT_ROOT/tools/link816" -o "$binArrFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oArrFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binArrFile" 0x025000 018c >/dev/null 2>&1; then die "MAME: sumTable(11..88) != 396 (disp-bump-inside-wrap regression)" fi rm -f "$cArrFile" "$oArrFile" "$binArrFile" # Pointer-to-pointer dereference: catches the linker missing # .data relocations. `int *p=&v; int **pp=&p;` initializers # need the linker to patch &p into pp's storage; without that, # **pp reads zero. log "check: MAME runs **pp dereference → 0xBEEF (data-reloc regression)" cPtrFile="$(mktemp --suffix=.c)" oPtrFile="$(mktemp --suffix=.o)" binPtrFile="$(mktemp --suffix=.bin)" cat > "$cPtrFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short v = 0xBEEF; unsigned short *p = &v; unsigned short **pp = &p; int main(void) { unsigned short x = **pp; switchToBank2(); *(volatile unsigned short *)0x5000 = x; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cPtrFile" -o "$oPtrFile" "$PROJECT_ROOT/tools/link816" -o "$binPtrFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oPtrFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binPtrFile" 0x025000 beef >/dev/null 2>&1; then die "MAME: **pp != 0xBEEF (data-reloc regression)" fi rm -f "$cPtrFile" "$oPtrFile" "$binPtrFile" # i32 libcall with arg0 in A:X — catches the SpillToX clobber # of live-in $x. shiftRight(0x12345678, 4) = 0x01234567. log "check: MAME runs i32 (a >> n) libcall → 0x01234567 (X-live SpillToX regression)" cI32File="$(mktemp --suffix=.c)" oI32File="$(mktemp --suffix=.o)" binI32File="$(mktemp --suffix=.bin)" cat > "$cI32File" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned long shiftRight(unsigned long a, int n) { return a >> n; } int main(void) { unsigned long s = shiftRight(0x12345678UL, 4); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)(s & 0xFFFF); *(volatile unsigned short *)0x5002 = (unsigned short)((s >> 16) & 0xFFFF); while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cI32File" -o "$oI32File" "$PROJECT_ROOT/tools/link816" -o "$binI32File" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oI32File" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binI32File" --check 0x025000=4567 0x025002=0123 >/dev/null 2>&1; then die "MAME: shiftRight(0x12345678, 4) != 0x01234567 (X-live SpillToX regression)" fi rm -f "$cI32File" "$oI32File" "$binI32File" # Variadic int sum. Catches the va_arg-aligns-up bug. Default # va_arg expansion rounds ap to the type's preferred alignment # (S16 = 2 bytes), but PHA-pushed varargs land at byte-granular # addresses, so aligning skips the low byte. log "check: MAME runs vararg sum(3,10,20,30) → 60 (VAARG-no-align regression)" cVaFile="$(mktemp --suffix=.c)" oVaFile="$(mktemp --suffix=.o)" binVaFile="$(mktemp --suffix=.bin)" cat > "$cVaFile" <<'EOF' #include __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int sum(int n, ...) { va_list ap; va_start(ap, n); int s = 0; for (int i = 0; i < n; i++) s += va_arg(ap, int); va_end(ap); return s; } int main(void) { int s = sum(3, 10, 20, 30); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)s; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cVaFile" -o "$oVaFile" "$PROJECT_ROOT/tools/link816" -o "$binVaFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oVaFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binVaFile" 0x025000 003c >/dev/null 2>&1; then die "MAME: sum(3,10,20,30) != 60 (VAARG-no-align regression)" fi rm -f "$cVaFile" "$oVaFile" "$binVaFile" # Negative-index pointer access (`p[-1]`). Catches the # 24-bit-Y-add bug in (sr,S),Y that crosses bank boundaries # for signed-negative Y. arr[-1] from &data[2] should give # data[1] = 22 ($0016). log "check: MAME runs p[-1] indirect → 22 (negative-Y indy regression)" cNyFile="$(mktemp --suffix=.c)" oNyFile="$(mktemp --suffix=.o)" binNyFile="$(mktemp --suffix=.bin)" cat > "$cNyFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short data[4] = { 11, 22, 33, 44 }; __attribute__((noinline)) unsigned short readPrev(unsigned short *p) { return p[-1]; } int main(void) { unsigned short r = readPrev(&data[2]); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cNyFile" -o "$oNyFile" "$PROJECT_ROOT/tools/link816" -o "$binNyFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oNyFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binNyFile" 0x025000 0016 >/dev/null 2>&1; then die "MAME: p[-1] != 22 (negative-Y indy regression)" fi rm -f "$cNyFile" "$oNyFile" "$binNyFile" # Loop with conditional dual-effect on n (n+=10 vs n+=1) and on # fmt (advance 2 vs 1). Catches the TiedDefSpill cross-block # redirect bug — without dominance check, the exit returns the # iter-N-1 value from the spill slot rather than iter-N. log "check: MAME runs parse2('HABCD') → 13 (TiedDefSpill dominance)" cP2File="$(mktemp --suffix=.c)" oP2File="$(mktemp --suffix=.o)" binP2File="$(mktemp --suffix=.bin)" cat > "$cP2File" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) int parse(const char *fmt) { int n = 0; while (*fmt) { char c = *fmt++; if (c == 'A') { char spec = *fmt++; (void)spec; n += 10; } else { n++; } } return n; } int main(void) { int r = parse("HABCD"); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cP2File" -o "$oP2File" "$PROJECT_ROOT/tools/link816" -o "$binP2File" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oP2File" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binP2File" 0x025000 000d >/dev/null 2>&1; then die "MAME: parse('HABCD') != 13 (TiedDefSpill dominance regression)" fi rm -f "$cP2File" "$oP2File" "$binP2File" # Canonical bubble sort. Both this form (`i < n-1; j < n-i-1`) # and the alternate form work after the BranchExpand bridge # fix. Catches a regression in either BranchExpand or # TiedDefSpill if the conditional flow gets miscompiled. log "check: MAME runs bubble sort [4,1,3,2] → [1,2,3,4]" cBsFile="$(mktemp --suffix=.c)" oBsFile="$(mktemp --suffix=.o)" binBsFile="$(mktemp --suffix=.bin)" cat > "$cBsFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short data[4] = { 4, 1, 3, 2 }; __attribute__((noinline)) void bubbleSort(unsigned short *arr, unsigned short n) { for (unsigned short i = 0; i < n - 1; i++) { for (unsigned short j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j+1]) { unsigned short t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } } } int main(void) { bubbleSort(data, 4); unsigned short d0 = data[0], d1 = data[1], d2 = data[2], d3 = data[3]; switchToBank2(); *(volatile unsigned short *)0x5000 = d0; *(volatile unsigned short *)0x5002 = d1; *(volatile unsigned short *)0x5004 = d2; *(volatile unsigned short *)0x5006 = d3; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cBsFile" -o "$oBsFile" "$PROJECT_ROOT/tools/link816" -o "$binBsFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oBsFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binBsFile" --check 0x025000=0001 0x025002=0002 \ 0x025004=0003 0x025006=0004 >/dev/null 2>&1; then die "MAME: bubbleSort([4,1,3,2]) != [1,2,3,4]" fi rm -f "$cBsFile" "$oBsFile" "$binBsFile" # printf("ABCDE") returns 5. Canary for the BranchExpand # leftover-BRA-Skip bug: without removing the original BRA # after rewriting Bxx to INV_Bxx, the inserted Bridge MBB # becomes unreachable and the conditional flow is lost. Also # exercises vprintf's main loop end-to-end (no varargs). log "check: MAME runs printf('ABCDE') → 5 (BranchExpand bridge regression)" cPfFile="$(mktemp --suffix=.c)" oPfFile="$(mktemp --suffix=.o)" binPfFile="$(mktemp --suffix=.bin)" cat > "$cPfFile" <<'EOF' #include __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int main(void) { int r = printf("ABCDE"); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections \ -I"$PROJECT_ROOT/runtime/include" -c "$cPfFile" -o "$oPfFile" "$PROJECT_ROOT/tools/link816" -o "$binPfFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oPfFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binPfFile" 0x025000 0005 >/dev/null 2>&1; then die "MAME: printf('ABCDE') != 5 (BranchExpand bridge regression)" fi rm -f "$cPfFile" "$oPfFile" "$binPfFile" # parse('BCDE') with switch-on-spec — used to fail to link with # PCREL8-out-of-range because long unconditional BRA didn't # auto-relax to BRL. W65816BranchExpand now force-promotes # long BRA to BRL. log "check: MAME runs nested-loop+multiply f(4) → 120 (regalloc + BRA-relax)" cFnFile="$(mktemp --suffix=.c)" oFnFile="$(mktemp --suffix=.o)" binFnFile="$(mktemp --suffix=.bin)" cat > "$cFnFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned short f(unsigned short n) { unsigned short s = 0; for (unsigned short i = 0; i < n; i++) for (unsigned short j = 0; j < n; j++) s += i*n+j; return s; } int main(void) { unsigned short r = f(4); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFnFile" -o "$oFnFile" "$PROJECT_ROOT/tools/link816" -o "$binFnFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oFnFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binFnFile" 0x025000 0078 >/dev/null 2>&1; then die "MAME: f(4) != 120 (regalloc + BRA-relax regression)" fi rm -f "$cFnFile" "$oFnFile" "$binFnFile" # u64add through a noinline boundary — exercises the # ADJCALLSTACKUP teardown's STA $E0 / LDA $E0 path that # preserves Y across the SP-restore. The earlier PLY*N/2 # implementation clobbered Y, so any i64 return came back # with the last popped arg in Y instead of the sum's mid-high. # Recursive u64 factorial — exercises __muldi3 + i64 ABI through # a recursive noinline boundary. 20! = 0x21c3_677c_82b4_0000. # Used to come back as garbage because __divmoddi4_stash read # caller args from slot 4 when it was actually JSR-called from # __muldi3 (so slot 4 was the JSL ret address byte, not a_mh). # dadd through a noinline boundary — exercises __adddf3 + the # full i64-return ABI through a real call. The earlier soft- # double smoke test ran `c = 1.5 + 2.5` inline, which clang # constant-folds to a literal 0x4010... bit pattern — never # actually executed __adddf3. This one calls a noinline # `dadd` so the libcall and the i64 ABI run end-to-end. # printf("%d", n) — used to crash MAME entirely because MachineCSE # eliminated the `if (isLong)` re-test of *fmt as a "redundant" # CMP (it had matched an earlier identical CMP), and the # surviving BNE then read whatever leftover P-flag state happened # to be in P from the last spec-dispatch CMP. Backend now # disables MachineCSE entirely. log "check: MAME runs printf('%%d %%d', 42, 99) chain (MachineCSE disable)" cPdFile="$(mktemp --suffix=.c)" oPdFile="$(mktemp --suffix=.o)" binPdFile="$(mktemp --suffix=.bin)" cat > "$cPdFile" <<'EOF' #include __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) int give42(void) { return 42; } int main(void) { // vprintf returns the increment count: 1 per format spec, 1 per // non-spec char. "Hi %d ok\n" → H,i,' ',%d,' ',o,k,'\n' = 8. int n = printf("Hi %d ok\n", give42()); switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)n; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections \ -I"$PROJECT_ROOT/runtime/include" -c \ "$cPdFile" -o "$oPdFile" "$PROJECT_ROOT/tools/link816" -o "$binPdFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oPdFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binPdFile" 0x025000 0008 \ >/dev/null 2>&1; then die "MAME: printf('Hi %d ok\\n', 42) != 8 (vprintf isLong / MachineCSE)" fi rm -f "$cPdFile" "$oPdFile" "$binPdFile" log "check: MAME runs noinline dadd(1.5,2.5) → 4.0 (__adddf3 + i64 ABI)" cDdFile="$(mktemp --suffix=.c)" oDdFile="$(mktemp --suffix=.o)" binDdFile="$(mktemp --suffix=.bin)" cat > "$cDdFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) double dadd(double a, double b) { return a + b; } int main(void) { union { double d; unsigned short w[4]; } u; u.d = dadd(1.5, 2.5); switchToBank2(); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cDdFile" -o "$oDdFile" "$PROJECT_ROOT/tools/link816" -o "$binDdFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oDdFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binDdFile" --check \ 0x025000=0000 0x025002=0000 0x025004=0000 0x025006=4010 \ >/dev/null 2>&1; then die "MAME: noinline dadd(1.5,2.5) != 4.0 (i64-ABI through libcall)" fi rm -f "$cDdFile" "$oDdFile" "$binDdFile" log "check: MAME runs fact_u64(20) → 0x21c3677c82b40000 (__muldi3 stash slots)" cFkFile="$(mktemp --suffix=.c)" oFkFile="$(mktemp --suffix=.o)" binFkFile="$(mktemp --suffix=.bin)" cat > "$cFkFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned long long fact_u64(unsigned int n) { if (n <= 1) return 1ULL; return (unsigned long long)n * fact_u64(n - 1); } int main(void) { unsigned long long r = fact_u64(20); union { unsigned long long u; unsigned short w[4]; } u; u.u = r; switchToBank2(); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFkFile" -o "$oFkFile" "$PROJECT_ROOT/tools/link816" -o "$binFkFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oFkFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binFkFile" --check \ 0x025000=0000 0x025002=82b4 0x025004=677c 0x025006=21c3 \ >/dev/null 2>&1; then die "MAME: fact_u64(20) returned wrong bits (__muldi3 / stash slots)" fi rm -f "$cFkFile" "$oFkFile" "$binFkFile" log "check: MAME runs u64add(0x3FF8...,0x4004...) → 0x7FFC... (call-up Y-preserve)" cU64File="$(mktemp --suffix=.c)" oU64File="$(mktemp --suffix=.o)" binU64File="$(mktemp --suffix=.bin)" cat > "$cU64File" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned long long u64add(unsigned long long a, unsigned long long b) { return a + b; } int main(void) { unsigned long long c = u64add(0x3FF8000000000000ULL, 0x4004000000000000ULL); union { unsigned long long u; unsigned short w[4]; } u; u.u = c; switchToBank2(); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cU64File" -o "$oU64File" "$PROJECT_ROOT/tools/link816" -o "$binU64File" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oU64File" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binU64File" --check \ 0x025000=0000 0x025002=0000 0x025004=0000 0x025006=7ffc \ >/dev/null 2>&1; then die "MAME: u64add through noinline returned wrong middle halves (call-up Y-clobber)" fi rm -f "$cU64File" "$oU64File" "$binU64File" log "check: MAME runs addOff(p,1) p[0]+=p[1] → 12 (StackSlotCleanup killed-Y respect)" cAofFile="$(mktemp --suffix=.c)" oAofFile="$(mktemp --suffix=.o)" binAofFile="$(mktemp --suffix=.bin)" cat > "$cAofFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) short addOff(short *p, short i) { short b = p[i]; p[i-1] = p[i-1] + b; return p[i-1]; } int main(void) { short stk[2] = { 5, 7 }; short r = addOff(stk, 1); short s0 = stk[0]; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)r; *(volatile unsigned short *)0x5002 = (unsigned short)s0; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cAofFile" -o "$oAofFile" "$PROJECT_ROOT/tools/link816" -o "$binAofFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oAofFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binAofFile" --check 0x025000=000c 0x025002=000c \ >/dev/null 2>&1; then die "MAME: addOff p[i-1]+=p[i] returned wrong store (NegYIndY/X-clobber or LDY-erase)" fi rm -f "$cAofFile" "$oAofFile" "$binAofFile" log "check: MAME runs iterative qsort([3,1,2]) at -O2 → [1,2,3] (#70 fixed)" cQsFile="$(mktemp --suffix=.c)" oQsFile="$(mktemp --suffix=.o)" binQsFile="$(mktemp --suffix=.bin)" cat > "$cQsFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) void swap(short *a, short *b) { short t = *a; *a = *b; *b = t; } // #70 fixed (W65816StackSlotCleanup Pass -2 safety check): greedy // regalloc no longer mis-compiles the iterative if/else recursion // form. This compiles at -O2 + greedy now, no per-function workaround. __attribute__((noinline)) void qsortIter(short *arr, short lo, short hi) { while (lo < hi) { short pivot = arr[hi]; short i = lo - 1; for (short j = lo; j < hi; j++) { if (arr[j] <= pivot) { i++; swap(&arr[i], &arr[j]); } } i++; swap(&arr[i], &arr[hi]); if ((short)(i - lo) < (short)(hi - i)) { qsortIter(arr, lo, (short)(i - 1)); lo = (short)(i + 1); } else { qsortIter(arr, (short)(i + 1), hi); hi = (short)(i - 1); } } } int main(void) { static short data[3] = { 3, 1, 2 }; qsortIter(data, 0, 2); short s0 = data[0], s1 = data[1], s2 = data[2]; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)s0; *(volatile unsigned short *)0x5002 = (unsigned short)s1; *(volatile unsigned short *)0x5004 = (unsigned short)s2; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cQsFile" -o "$oQsFile" "$PROJECT_ROOT/tools/link816" -o "$binQsFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oQsFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binQsFile" --check 0x025000=0001 0x025002=0002 \ 0x025004=0003 >/dev/null 2>&1; then die "MAME: -O2 qsort([3,1,2]) wrong (#70 fix regression — Pass -2 safety check)" fi rm -f "$cQsFile" "$oQsFile" "$binQsFile" log "check: MAME runs strtol/strtoul: 12345, -999, 0x1ABC, 0755, 'xyz' (#74)" cStFile="$(mktemp --suffix=.c)" oStFile="$(mktemp --suffix=.o)" binStFile="$(mktemp --suffix=.bin)" cat > "$cStFile" <<'EOF' extern long strtol(const char *, char **, int); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int main(void) { char *ep; union { long l; unsigned short w[2]; } a, b, c, d, e; a.l = strtol("12345", &ep, 10); b.l = strtol("-999", &ep, 10); c.l = strtol(" 0x1ABC ", &ep, 16); d.l = strtol("0755", &ep, 0); e.l = strtol("xyz", &ep, 10); short epOff = (short)(ep - "xyz"); switchToBank2(); *(volatile unsigned short *)0x5000 = a.w[0]; *(volatile unsigned short *)0x5002 = a.w[1]; *(volatile unsigned short *)0x5004 = b.w[0]; *(volatile unsigned short *)0x5006 = b.w[1]; *(volatile unsigned short *)0x5008 = c.w[0]; *(volatile unsigned short *)0x500a = d.w[0]; *(volatile unsigned short *)0x500c = e.w[0]; *(volatile unsigned short *)0x500e = (unsigned short)epOff; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cStFile" -o "$oStFile" "$PROJECT_ROOT/tools/link816" -o "$binStFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSfF" "$oSdF" "$oLibgccFile" \ "$oStFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binStFile" --check \ 0x025000=3039 0x025002=0000 0x025004=fc19 0x025006=ffff \ 0x025008=1abc 0x02500a=01ed 0x02500c=0000 0x02500e=0000 \ >/dev/null 2>&1; then die "MAME: strtol parse cases wrong" fi rm -f "$cStFile" "$oStFile" "$binStFile" log "check: MAME runs sprintf/snprintf format coverage (#76)" cSpFile="$(mktemp --suffix=.c)" oSpFile="$(mktemp --suffix=.o)" binSpFile="$(mktemp --suffix=.bin)" cat > "$cSpFile" <<'EOF' extern int sprintf(char *buf, const char *fmt, ...); extern int snprintf(char *buf, unsigned int n, const char *fmt, ...); extern int strcmp(const char *a, const char *b); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static int eq(const char *a, const char *b) { return strcmp(a, b) == 0; } int main(void) { char buf[32]; unsigned char ok = 0; int r = sprintf(buf, "ok-%d", 7); if (r == 4 && eq(buf, "ok-7")) ok |= 0x01; r = sprintf(buf, "n=%d s=%s", -42, "hi"); if (r == 10 && eq(buf, "n=-42 s=hi")) ok |= 0x02; r = sprintf(buf, "%04x %lu", 0xC, (unsigned long)123456); if (r == 11 && eq(buf, "000c 123456")) ok |= 0x04; /* Test that snprintf truncates per C99: 10 chars asked, only 5 fit + NUL. Funnel the format through a non-literal pointer so clang's -Wformat-truncation static check doesn't fire (the truncation IS what we're testing). */ const char *fmt_trunc = "abcdefghij"; r = snprintf(buf, 6, "%s", fmt_trunc); if (r == 10 && eq(buf, "abcde")) ok |= 0x08; r = sprintf(buf, "%.2f", 1.5); if (r == 4 && eq(buf, "1.50")) ok |= 0x10; r = sprintf(buf, "[%c%c%%]", 'A', 'B'); if (r == 5 && eq(buf, "[AB%]")) ok |= 0x20; /* C99: snprintf(buf, 0, ...) must NOT touch buf and must return the would-be-written length. Sentinel-fill the buffer and verify the byte just BEFORE buf survives — earlier bug wrote a NUL at gEnd[-1] = buf[-1] when n=0. */ char guard[8]; for (int i = 0; i < 8; i++) guard[i] = (char)0xCC; r = snprintf(&guard[2], 0, "x"); if (r == 1 && guard[1] == (char)0xCC && guard[2] == (char)0xCC) ok |= 0x40; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cSpFile" -o "$oSpFile" "$PROJECT_ROOT/tools/link816" -o "$binSpFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oSpFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binSpFile" --check \ 0x025000=007f >/dev/null 2>&1; then die "MAME: sprintf/snprintf format-coverage bitmap != 0x7f (snprintf n=0 buffer-write regression?)" fi rm -f "$cSpFile" "$oSpFile" "$binSpFile" log "check: MAME runs qsort([3,1,4,1,5]) + bsearch (#77)" cQbFile="$(mktemp --suffix=.c)" oQbFile="$(mktemp --suffix=.o)" binQbFile="$(mktemp --suffix=.bin)" cat > "$cQbFile" <<'EOF' extern void qsort(void *, unsigned int, unsigned int, int (*)(const void *, const void *)); extern void *bsearch(const void *, const void *, unsigned int, unsigned int, int (*)(const void *, const void *)); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static int cmpInt(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; return x < y ? -1 : (x > y ? 1 : 0); } int main(void) { int arr[5] = {3, 1, 4, 1, 5}; qsort(arr, 5, sizeof(int), cmpInt); int key = 4; int *hit = (int *)bsearch(&key, arr, 5, sizeof(int), cmpInt); int idx = hit ? (int)(hit - arr) : -1; int miss = 99; int *m = (int *)bsearch(&miss, arr, 5, sizeof(int), cmpInt); short missIdx = m ? 0 : -1; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)arr[0]; *(volatile unsigned short *)0x5002 = (unsigned short)arr[1]; *(volatile unsigned short *)0x5004 = (unsigned short)arr[2]; *(volatile unsigned short *)0x5006 = (unsigned short)arr[3]; *(volatile unsigned short *)0x5008 = (unsigned short)arr[4]; *(volatile unsigned short *)0x500a = (unsigned short)idx; *(volatile unsigned short *)0x500c = (unsigned short)missIdx; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cQbFile" -o "$oQbFile" "$PROJECT_ROOT/tools/link816" -o "$binQbFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oSfF" "$oSdF" "$oLibgccFile" "$oQbFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binQbFile" --check \ 0x025000=0001 0x025002=0001 0x025004=0003 \ 0x025006=0004 0x025008=0005 \ 0x02500a=0003 0x02500c=ffff >/dev/null 2>&1; then die "MAME: qsort/bsearch wrong" fi rm -f "$cQbFile" "$oQbFile" "$binQbFile" log "check: MAME runs strcat/atol/llabs + math fabs/floor/ceil/copysign (#78 + #79)" cExFile="$(mktemp --suffix=.c)" oExFile="$(mktemp --suffix=.o)" binExFile="$(mktemp --suffix=.bin)" cat > "$cExFile" <<'EOF' extern char *strcat(char *, const char *); extern char *strncat(char *, const char *, unsigned int); extern int strcmp(const char *, const char *); extern long atol(const char *); extern long long llabs(long long); extern double fabs(double); extern double floor(double); extern double ceil(double); extern double copysign(double, double); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static int eq(const char *a, const char *b) { return strcmp(a, b) == 0; } static int dEq(double a, double b) { unsigned long long x, y; __builtin_memcpy(&x, &a, sizeof(x)); __builtin_memcpy(&y, &b, sizeof(y)); return x == y; } int main(void) { char buf[16]; unsigned char ok = 0; buf[0] = 0; strcat(buf, "ab"); strcat(buf, "cd"); if (eq(buf, "abcd")) ok |= 0x01; buf[0]='h'; buf[1]='i'; buf[2]=0; strncat(buf, "world", 3); if (eq(buf, "hiwor")) ok |= 0x02; if (atol(" -12345 garbage") == -12345) ok |= 0x04; if (llabs(-(long long)1000) == 1000) ok |= 0x08; if (dEq(fabs(-1.5), 1.5)) ok |= 0x10; if (dEq(floor(2.7), 2.0)) ok |= 0x20; if (dEq(ceil(2.3), 3.0)) ok |= 0x40; if (dEq(copysign(1.0, -2.0), -1.0)) ok |= 0x80; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cExFile" -o "$oExFile" "$PROJECT_ROOT/tools/link816" -o "$binExFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oLibgccFile" "$oExFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binExFile" --check \ 0x025000=00ff >/dev/null 2>&1; then die "MAME: extras + math basics bitmap != 0xff" fi rm -f "$cExFile" "$oExFile" "$binExFile" log "check: MAME runs rand/srand reproducible sequence (#93)" cRdFile="$(mktemp --suffix=.c)" oRdFile="$(mktemp --suffix=.o)" binRdFile="$(mktemp --suffix=.bin)" cat > "$cRdFile" <<'EOF' extern int rand(void); extern void srand(unsigned int); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int main(void) { srand(1); int r1 = rand(); int r2 = rand(); int r3 = rand(); // Same seed: must reproduce. srand(1); int r1b = rand(); int r2b = rand(); unsigned char ok = 0; if (r1 != 0 && r1 == r1b) ok |= 0x01; // reproducible if (r2 != 0 && r2 == r2b) ok |= 0x02; // reproducible if (r1 != r2 && r2 != r3) ok |= 0x04; // diverse if (r1 >= 0 && r1 <= 0x7FFF) ok |= 0x08; // RAND_MAX bound switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cRdFile" -o "$oRdFile" "$PROJECT_ROOT/tools/link816" -o "$binRdFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oExtrasF" "$oSfF" "$oSdF" "$oLibgccFile" \ "$oRdFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binRdFile" --check \ 0x025000=000f >/dev/null 2>&1; then die "MAME: rand/srand sequence broken" fi rm -f "$cRdFile" "$oRdFile" "$binRdFile" log "check: MAME runs atan/asin/acos/sinh/cosh/tanh + tan (#85)" cTr2File="$(mktemp --suffix=.c)" oTr2File="$(mktemp --suffix=.o)" binTr2File="$(mktemp --suffix=.bin)" cat > "$cTr2File" <<'EOF' extern double atan(double); extern double asin(double); extern double acos(double); extern double sinh(double); extern double cosh(double); extern double tanh(double); extern double tan(double); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static int dApprox(double a, double b, double tol) { double d = a - b; if (d < 0) d = -d; return d < tol; } int main(void) { unsigned short ok = 0; if (dApprox(atan(1.0), 0.7853981633, 0.001)) ok |= 0x01; if (dApprox(sinh(0.0), 0.0, 0.001)) ok |= 0x02; if (dApprox(cosh(0.0), 1.0, 0.001)) ok |= 0x04; if (dApprox(tanh(0.0), 0.0, 0.001)) ok |= 0x08; if (dApprox(asin(0.5), 0.5235987755, 0.001)) ok |= 0x10; if (dApprox(acos(1.0), 0.0, 0.001)) ok |= 0x20; if (dApprox(tan(0.7853981633), 1.0, 0.001)) ok |= 0x40; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cTr2File" -o "$oTr2File" "$PROJECT_ROOT/tools/link816" -o "$binTr2File" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oLibgccFile" "$oTr2File" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binTr2File" --check \ 0x025000=007f >/dev/null 2>&1; then die "MAME: extended math (atan/asin/acos/sinh/cosh/tanh/tan) bitmap != 0x7f" fi rm -f "$cTr2File" "$oTr2File" "$binTr2File" log "check: MAME runs hash table end-to-end (malloc/strcmp/strcpy/struct ptrs) (#86)" cHtFile="$(mktemp --suffix=.c)" oHtFile="$(mktemp --suffix=.o)" binHtFile="$(mktemp --suffix=.bin)" cat > "$cHtFile" <<'EOF' extern void *malloc(unsigned int); extern int strcmp(const char *, const char *); extern char *strcpy(char *, const char *); extern unsigned int strlen(const char *); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } typedef struct EntryT { struct EntryT *next; char *key; int value; } EntryT; #define BUCKETS 8 typedef struct { EntryT *bucket[BUCKETS]; int count; } HashT; static unsigned int hashKey(const char *s) { unsigned int h = 5381; while (*s) { h = (h * 33) + (unsigned int)(*s); s++; } return h; } static void hashInit(HashT *h) { for (int i = 0; i < BUCKETS; i++) h->bucket[i] = (EntryT *)0; h->count = 0; } static void hashPut(HashT *h, const char *key, int value) { unsigned int b = hashKey(key) & (BUCKETS - 1); EntryT *e = h->bucket[b]; while (e) { if (strcmp(e->key, key) == 0) { e->value = value; return; } e = e->next; } EntryT *ne = (EntryT *)malloc(sizeof(EntryT)); char *kc = (char *)malloc(strlen(key) + 1); strcpy(kc, key); ne->key = kc; ne->value = value; ne->next = h->bucket[b]; h->bucket[b] = ne; h->count++; } static int hashGet(HashT *h, const char *key, int *out) { unsigned int b = hashKey(key) & (BUCKETS - 1); EntryT *e = h->bucket[b]; while (e) { if (strcmp(e->key, key) == 0) { *out = e->value; return 1; } e = e->next; } return 0; } int main(void) { HashT h; hashInit(&h); hashPut(&h, "alpha", 1); hashPut(&h, "beta", 2); hashPut(&h, "gamma", 3); hashPut(&h, "delta", 4); hashPut(&h, "alpha", 100); int v; int v1 = hashGet(&h, "alpha", &v) ? v : -1; int v2 = hashGet(&h, "gamma", &v) ? v : -1; int miss = hashGet(&h, "epsilon", &v) ? 1 : 0; unsigned char ok = 0; if (h.count == 4) ok |= 0x01; if (v1 == 100) ok |= 0x02; if (v2 == 3) ok |= 0x04; if (miss == 0) ok |= 0x08; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cHtFile" -o "$oHtFile" "$PROJECT_ROOT/tools/link816" -o "$binHtFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oLibgccFile" "$oHtFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binHtFile" --check \ 0x025000=000f >/dev/null 2>&1; then die "MAME: hash table coverage bitmap != 0x0f" fi rm -f "$cHtFile" "$oHtFile" "$binHtFile" # Regression: free() coalescing must remove blocks absorbed # into a lower-address neighbour from the free list. Old code # extended the lower block but left the absorbed entry in # Signed compare of values near INT16_MIN/MAX: BMI/BPL alone # are not V-flag-aware, so the W65816 backend now applies an # EOR-with-sign-bit transform (a < b signed iff a^$8000 < # b^$8000 unsigned). Verify INT16_MIN < INT16_MAX, INT16_MIN # < 1, INT16_MIN < 0, etc. all return the right boolean — # the pre-transform code returned false for INT16_MIN < 1 # because (-32768 - 1) overflowed to +32767, leaving N=0. log "check: MAME signed compare near INT16_MIN works (V-flag fix)" cSignedFile="$(mktemp --suffix=.c)" oSignedFile="$(mktemp --suffix=.o)" binSignedFile="$(mktemp --suffix=.bin)" cat > "$cSignedFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) static int slt(int a, int b) { return a < b; } __attribute__((noinline)) static int sgt(int a, int b) { return a > b; } __attribute__((noinline)) static int sle(int a, int b) { return a <= b; } __attribute__((noinline)) static int sge(int a, int b) { return a >= b; } int main(void) { unsigned short ok = 0; // INT16_MIN < 1: true. Pre-fix bug returned false. if (slt(-32768, 1)) ok |= 0x01; // INT16_MIN < INT16_MAX: true. if (slt(-32768, 32767)) ok |= 0x02; // INT16_MAX > INT16_MIN: true. if (sgt(32767, -32768)) ok |= 0x04; // INT16_MIN <= -32768: true. if (sle(-32768, -32768)) ok |= 0x08; // INT16_MAX >= 0: true. if (sge(32767, 0)) ok |= 0x10; // -1 < 0: true. if (slt(-1, 0)) ok |= 0x20; // 0 < -1: false (negation case). if (!slt(0, -1)) ok |= 0x40; // INT16_MIN < INT16_MIN: false. if (!slt(-32768, -32768)) ok |= 0x80; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cSignedFile" -o "$oSignedFile" "$PROJECT_ROOT/tools/link816" -o "$binSignedFile" --text-base 0x1000 \ "$oCrt0F" "$oLibgccFile" "$oSignedFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binSignedFile" --check \ 0x025000=00ff >/dev/null 2>&1; then die "MAME: signed compare near INT_MIN failed (V-flag bug regression?)" fi rm -f "$cSignedFile" "$oSignedFile" "$binSignedFile" # the list, creating an overlapping free entry. A subsequent # malloc could hand out the same memory to two callers. log "check: MAME runs malloc/free coalesce — three blocks freed in alloc order (#100)" cMcFile="$(mktemp --suffix=.c)" oMcFile="$(mktemp --suffix=.o)" binMcFile="$(mktemp --suffix=.bin)" cat > "$cMcFile" <<'EOF' extern void *malloc(unsigned int); extern void free(void *); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int main(void) { // Allocate three same-sized adjacent blocks, then free in alloc // order so b's coalesce sees a-prev-to-b (the bug path). char *a = (char *)malloc(20); char *b = (char *)malloc(20); char *c = (char *)malloc(20); if (!a || !b || !c) goto fail; free(a); // list = [a] free(b); // list = [b, a]; bEnd==a -> coalesce a into b free(c); // list = [c, b']; bEnd==b' -> coalesce b' into c // After all coalescing: one ~66-byte block. Allocate it back and // write the full extent — if any of a/b/c were left in the list // overlapping, a follow-on malloc would hand out a second pointer // into the same memory and the writes would interfere. char *big = (char *)malloc(60); if (!big) goto fail; for (int i = 0; i < 60; i++) big[i] = (char)(i + 1); char *more = (char *)malloc(8); if (!more) goto fail; for (int i = 0; i < 8; i++) more[i] = (char)0xAA; // Verify big is intact. unsigned short ok = 1; for (int i = 0; i < 60; i++) if (big[i] != (char)(i + 1)) ok = 0; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} fail: switchToBank2(); *(volatile unsigned short *)0x5000 = 0xDEAD; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cMcFile" -o "$oMcFile" "$PROJECT_ROOT/tools/link816" -o "$binMcFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oLibgccFile" "$oMcFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binMcFile" --check \ 0x025000=0001 >/dev/null 2>&1; then die "MAME: malloc/free coalesce regressed — overlapping free-list entries" fi rm -f "$cMcFile" "$oMcFile" "$binMcFile" log "check: MAME runs strtok 'a,b,,c' continuation (#84 fixed)" cTkFile="$(mktemp --suffix=.c)" oTkFile="$(mktemp --suffix=.o)" binTkFile="$(mktemp --suffix=.bin)" cat > "$cTkFile" <<'EOF' extern char *strtok(char *, const char *); extern int strcmp(const char *, const char *); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } int main(void) { char buf[8]; buf[0]='a'; buf[1]=','; buf[2]='b'; buf[3]=','; buf[4]=','; buf[5]='c'; buf[6]=0; char *t1 = strtok(buf, ","); char *t2 = strtok((char *)0, ","); char *t3 = strtok((char *)0, ","); char *t4 = strtok((char *)0, ","); unsigned short ok = 0; if (t1 && strcmp(t1, "a") == 0) ok |= 0x01; if (t2 && strcmp(t2, "b") == 0) ok |= 0x02; if (t3 && strcmp(t3, "c") == 0) ok |= 0x04; if (t4 == (char *)0) ok |= 0x08; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cTkFile" -o "$oTkFile" "$PROJECT_ROOT/tools/link816" -o "$binTkFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oTkFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binTkFile" --check \ 0x025000=000f >/dev/null 2>&1; then die "MAME: strtok 4-call continuation bitmap != 0x0f" fi rm -f "$cTkFile" "$oTkFile" "$binTkFile" log "check: MAME runs RPN calculator '3 4 +', '2 3 4 + *', etc. (#87)" cRpFile="$(mktemp --suffix=.c)" oRpFile="$(mktemp --suffix=.o)" binRpFile="$(mktemp --suffix=.bin)" cat > "$cRpFile" <<'EOF' extern char *strtok(char *, const char *); extern long atol(const char *); extern int snprintf(char *, unsigned int, const char *, ...); extern int strcmp(const char *, const char *); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } #define STACK_SIZE 16 static long g_stack[STACK_SIZE]; static int g_top; static void push(long v) { if (g_top < STACK_SIZE) g_stack[g_top++] = v; } static long pop(void) { return g_top > 0 ? g_stack[--g_top] : 0; } static long evalRpn(char *expr) { g_top = 0; char *tok = strtok(expr, " "); while (tok) { if (tok[0] == '+' && tok[1] == 0) { long b = pop(); long a = pop(); push(a + b); } else if (tok[0] == '-' && tok[1] == 0) { long b = pop(); long a = pop(); push(a - b); } else if (tok[0] == '*' && tok[1] == 0) { long b = pop(); long a = pop(); push(a * b); } else if (tok[0] == '/' && tok[1] == 0) { long b = pop(); long a = pop(); push(b != 0 ? a / b : 0); } else { push(atol(tok)); } tok = strtok((char *)0, " "); } return pop(); } static long g_r1, g_r2, g_r3, g_r4; __attribute__((noinline)) static void runAll(void) { char e1[] = "3 4 +"; char e2[] = "2 3 4 + *"; char e3[] = "100 25 /"; char e4[] = "10 2 - 5 *"; g_r1 = evalRpn(e1); g_r2 = evalRpn(e2); g_r3 = evalRpn(e3); g_r4 = evalRpn(e4); } int main(void) { runAll(); char buf[16]; snprintf(buf, 16, "%ld", g_r2); unsigned short ok = 0; if (g_r1 == 7) ok |= 0x01; if (g_r2 == 14) ok |= 0x02; if (g_r3 == 4) ok |= 0x04; if (g_r4 == 40) ok |= 0x08; if (strcmp(buf, "14") == 0) ok |= 0x10; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cRpFile" -o "$oRpFile" "$PROJECT_ROOT/tools/link816" -o "$binRpFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oRpFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binRpFile" --check \ 0x025000=001f >/dev/null 2>&1; then die "MAME: RPN calculator bitmap != 0x1f" fi rm -f "$cRpFile" "$oRpFile" "$binRpFile" log "check: MAME runs setjmp/longjmp from nested recursion (#88)" cSjFile="$(mktemp --suffix=.c)" oSjFile="$(mktemp --suffix=.o)" binSjFile="$(mktemp --suffix=.bin)" cat > "$cSjFile" <<'EOF' typedef unsigned char jmp_buf[8]; int setjmp(jmp_buf env) __attribute__((returns_twice)); void longjmp(jmp_buf env, int val) __attribute__((noreturn)); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) static void deep(jmp_buf env, int level) { if (level >= 3) longjmp(env, 99); deep(env, level + 1); } int main(void) { jmp_buf env; int v = setjmp(env); unsigned short ok = 0; if (v == 0) { deep(env, 0); ok = 0xDEAD; } else if (v == 99) { ok = 0x1234; } switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cSjFile" -o "$oSjFile" "$PROJECT_ROOT/tools/link816" -o "$binSjFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oSjFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binSjFile" --check \ 0x025000=1234 >/dev/null 2>&1; then die "MAME: setjmp/longjmp end-to-end != 0x1234 (#88)" fi rm -f "$cSjFile" "$oSjFile" "$binSjFile" log "check: MAME runs CRC32('123456789') == 0xCBF43926 (#89)" cCrFile="$(mktemp --suffix=.c)" oCrFile="$(mktemp --suffix=.o)" binCrFile="$(mktemp --suffix=.bin)" cat > "$cCrFile" <<'EOF' typedef unsigned long uint32_t; typedef unsigned char uint8_t; __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static uint32_t crc32(const uint8_t *buf, unsigned int len) { uint32_t crc = 0xFFFFFFFFul; for (unsigned int i = 0; i < len; i++) { crc = crc ^ (uint32_t)buf[i]; for (int j = 0; j < 8; j++) { uint32_t mask = -(crc & 1ul); crc = (crc >> 1) ^ (0xEDB88320ul & mask); } } return ~crc; } int main(void) { const char *s = "123456789"; unsigned int n = 0; while (s[n]) n++; uint32_t c = crc32((const uint8_t *)s, n); switchToBank2(); *(volatile uint32_t *)0x5000 = c; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cCrFile" -o "$oCrFile" "$PROJECT_ROOT/tools/link816" -o "$binCrFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oCrFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCrFile" --check \ 0x025000=3926 0x025002=cbf4 >/dev/null 2>&1; then die "MAME: CRC32 != 0xCBF43926 (#89)" fi rm -f "$cCrFile" "$oCrFile" "$binCrFile" log "check: MAME runs brainfuck \"...AB...\" interpreter (#90)" cBfFile="$(mktemp --suffix=.c)" oBfFile="$(mktemp --suffix=.o)" binBfFile="$(mktemp --suffix=.bin)" cat > "$cBfFile" <<'EOF' typedef unsigned char u8; __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } #define TAPE_SIZE 32 static u8 g_tape[TAPE_SIZE]; static u8 g_out[8]; static int g_outIdx; __attribute__((noinline)) static void runBf(const char *prog) { g_outIdx = 0; for (int i = 0; i < TAPE_SIZE; i++) g_tape[i] = 0; int dp = 0; int pc = 0; while (prog[pc]) { char c = prog[pc]; if (c == '+') g_tape[dp]++; else if (c == '-') g_tape[dp]--; else if (c == '>') dp++; else if (c == '<') dp--; else if (c == '.') { if (g_outIdx < 8) g_out[g_outIdx++] = g_tape[dp]; } else if (c == '[') { if (g_tape[dp] == 0) { int d = 1; while (d > 0) { pc++; if (prog[pc]=='[') d++; else if (prog[pc]==']') d--; } } } else if (c == ']') { if (g_tape[dp] != 0) { int d = 1; while (d > 0) { pc--; if (prog[pc]==']') d++; else if (prog[pc]=='[') d--; } } } pc++; } } int main(void) { runBf("++++++++[>++++++++<-]>+.+."); // Capture g_out values BEFORE bank switch — after the switch DBR=2 // and bank-0 globals can't be read absolutely. unsigned short out01 = (unsigned short)g_out[0] | ((unsigned short)g_out[1] << 8); switchToBank2(); *(volatile unsigned short *)0x5000 = out01; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cBfFile" -o "$oBfFile" "$PROJECT_ROOT/tools/link816" -o "$binBfFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oBfFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binBfFile" --check \ 0x025000=4241 >/dev/null 2>&1; then die "MAME: brainfuck 'AB' output != 0x4241 (#90)" fi rm -f "$cBfFile" "$oBfFile" "$binBfFile" log "check: MAME runs recursive-descent expression parser '(3+4)*5' (#92)" cExprFile="$(mktemp --suffix=.c)" oExprFile="$(mktemp --suffix=.o)" binExprFile="$(mktemp --suffix=.bin)" cat > "$cExprFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static const char *g_p; static long parseExpr(void); static long parseFactor(void) { while (*g_p == ' ') g_p++; if (*g_p == '(') { g_p++; long v = parseExpr(); while (*g_p == ' ') g_p++; if (*g_p == ')') g_p++; return v; } long n = 0; while (*g_p >= '0' && *g_p <= '9') { n = n*10 + (*g_p-'0'); g_p++; } return n; } static long parseTerm(void) { long v = parseFactor(); for (;;) { while (*g_p == ' ') g_p++; char c = *g_p; if (c != '*' && c != '/') return v; g_p++; long r = parseFactor(); if (c == '*') v = v * r; else v = (r != 0) ? v / r : 0; } } static long parseExpr(void) { long v = parseTerm(); for (;;) { while (*g_p == ' ') g_p++; char c = *g_p; if (c != '+' && c != '-') return v; g_p++; long r = parseTerm(); if (c == '+') v = v + r; else v = v - r; } } __attribute__((noinline)) static long evalExpr(const char *s) { g_p = s; return parseExpr(); } static long g_r1, g_r2, g_r3, g_r4, g_r5; __attribute__((noinline)) static void runAll(void) { g_r1 = evalExpr("3 + 4"); g_r2 = evalExpr("2 * 3 + 4"); g_r3 = evalExpr("2 + 3 * 4"); g_r4 = evalExpr("(3 + 4) * 5"); g_r5 = evalExpr("100 / 4 - 5 * 2 + 1"); } int main(void) { runAll(); unsigned short ok = 0; if (g_r1 == 7) ok |= 0x01; if (g_r2 == 10) ok |= 0x02; if (g_r3 == 14) ok |= 0x04; if (g_r4 == 35) ok |= 0x08; if (g_r5 == 16) ok |= 0x10; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cExprFile" -o "$oExprFile" "$PROJECT_ROOT/tools/link816" -o "$binExprFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" \ "$oLibgccFile" "$oExprFile" >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binExprFile" --check \ 0x025000=001f >/dev/null 2>&1; then die "MAME: expression parser bitmap != 0x1f (#92)" fi rm -f "$cExprFile" "$oExprFile" "$binExprFile" log "check: MAME runs sqrt/pow + sin/cos/exp/log + strpbrk/spn/cspn (#81 + #82 + #83)" cTrFile="$(mktemp --suffix=.c)" oTrFile="$(mktemp --suffix=.o)" binTrFile="$(mktemp --suffix=.bin)" cat > "$cTrFile" <<'EOF' extern double sqrt(double); extern double pow(double, double); extern double sin(double); extern double cos(double); extern double exp(double); extern double log(double); extern char *strpbrk(const char *, const char *); extern unsigned int strspn(const char *, const char *); extern unsigned int strcspn(const char *, const char *); extern void *memchr(const void *, int, unsigned int); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static int dApprox(double a, double b, double tol) { double d = a - b; if (d < 0) d = -d; return d < tol; } int main(void) { unsigned int ok = 0; if (dApprox(sqrt(4.0), 2.0, 0.001)) ok |= 0x0001; if (dApprox(sqrt(2.0), 1.41421356, 0.001)) ok |= 0x0002; if (dApprox(pow(2.0, 10.0), 1024.0, 0.001)) ok |= 0x0004; if (dApprox(pow(2.0, -3.0), 0.125, 0.001)) ok |= 0x0008; if (dApprox(sin(0.0), 0.0, 0.001)) ok |= 0x0010; if (dApprox(cos(0.0), 1.0, 0.001)) ok |= 0x0020; if (dApprox(exp(1.0), 2.71828183, 0.001)) ok |= 0x0040; if (dApprox(log(2.71828183), 1.0, 0.001)) ok |= 0x0080; const char *p = strpbrk("hello,world", ",.;"); if (p && *p == ',') ok |= 0x0100; if (strspn("aabbcd", "ab") == 4) ok |= 0x0200; if (strcspn("hello,world", ",") == 5) ok |= 0x0400; p = (const char *)memchr("abcdef", 'd', 6); if (p && *p == 'd') ok |= 0x0800; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cTrFile" -o "$oTrFile" "$PROJECT_ROOT/tools/link816" -o "$binTrFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oLibgccFile" "$oTrFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binTrFile" --check \ 0x025000=0fff >/dev/null 2>&1; then die "MAME: math transcendentals + string extras bitmap != 0x0fff" fi rm -f "$cTrFile" "$oTrFile" "$binTrFile" log "check: MAME runs udivmod(0x123...DEF, 0x10000, &m) → q=0x12345_6789AB m=0xCDEF (#69)" cUdmFile="$(mktemp --suffix=.c)" oUdmFile="$(mktemp --suffix=.o)" binUdmFile="$(mktemp --suffix=.bin)" cat > "$cUdmFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } typedef unsigned long long u64; __attribute__((noinline)) u64 udivmod(u64 a, u64 b, u64 *out_mod) { *out_mod = a % b; return a / b; } int main(void) { u64 m; u64 q = udivmod(0x123456789ABCDEFULL, 0x10000ULL, &m); union { u64 u; unsigned short w[4]; } qu, mu; qu.u = q; mu.u = m; switchToBank2(); *(volatile unsigned short *)0x5000 = qu.w[0]; *(volatile unsigned short *)0x5002 = qu.w[1]; *(volatile unsigned short *)0x5004 = qu.w[2]; *(volatile unsigned short *)0x5006 = qu.w[3]; *(volatile unsigned short *)0x5008 = mu.w[0]; *(volatile unsigned short *)0x500a = mu.w[1]; *(volatile unsigned short *)0x500c = mu.w[2]; *(volatile unsigned short *)0x500e = mu.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cUdmFile" -o "$oUdmFile" "$PROJECT_ROOT/tools/link816" -o "$binUdmFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oUdmFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binUdmFile" --check \ 0x025000=89ab 0x025002=4567 0x025004=0123 0x025006=0000 \ 0x025008=cdef 0x02500a=0000 0x02500c=0000 0x02500e=0000 \ >/dev/null 2>&1; then die "MAME: udivmod(...) wrong mod (i64-first-arg X-spill bug)" fi rm -f "$cUdmFile" "$oUdmFile" "$binUdmFile" log "check: MAME runs sqr(10) → 100 (frame-less ADJCALLSTACKUP must emit PLY)" cSqrFile="$(mktemp --suffix=.c)" oSqrFile="$(mktemp --suffix=.o)" binSqrFile="$(mktemp --suffix=.bin)" cat > "$cSqrFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) unsigned short sqr(unsigned short x) { return x * x; } int main(void) { unsigned short r = sqr(10); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cSqrFile" -o "$oSqrFile" "$PROJECT_ROOT/tools/link816" -o "$binSqrFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oSqrFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binSqrFile" --check 0x025000=0064 >/dev/null 2>&1; then die "MAME: sqr(10) crashed or != 100 (ADJCALLSTACKUP not emitting PLY for frame-less)" fi rm -f "$cSqrFile" "$oSqrFile" "$binSqrFile" log "check: MAME runs ddiv(8.0,4.0) → 2.0 (__divdf3 algorithm fix)" cDdvFile="$(mktemp --suffix=.c)" oDdvFile="$(mktemp --suffix=.o)" binDdvFile="$(mktemp --suffix=.bin)" cat > "$cDdvFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) double ddiv(double a, double b) { return a / b; } int main(void) { union { double d; unsigned short w[4]; } u; u.d = ddiv(8.0, 4.0); switchToBank2(); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cDdvFile" -o "$oDdvFile" "$PROJECT_ROOT/tools/link816" -o "$binDdvFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oDdvFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binDdvFile" --check 0x025000=0000 0x025002=0000 \ 0x025004=0000 0x025006=4000 >/dev/null 2>&1; then die "MAME: ddiv(8,4) != 2.0 (__divdf3 long-division bug)" fi rm -f "$cDdvFile" "$oDdvFile" "$binDdvFile" log "check: MAME runs Newton-iter loop → high-half ~1.41 (BranchExpand self-loop BRA fix)" cSqFile="$(mktemp --suffix=.c)" oSqFile="$(mktemp --suffix=.o)" binSqFile="$(mktemp --suffix=.bin)" # 3-iter Newton-method sqrt with a counted for-loop (the loop-back # BRA is a self-loop, which the BranchExpand distance estimator # used to report as 0 bytes, so it never promoted to BRL even # when the loop body grew well past +/-128 bytes). cat > "$cSqFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) double sqrt3(double x) { double g = x * 0.5; for (unsigned short i = 0; i < 3; i++) g = (g + x / g) * 0.5; return g; } int main(void) { union { double d; unsigned short w[4]; } u; u.d = sqrt3(2.0); switchToBank2(); // Only the high half is precision-stable (low halves vary slightly // due to truncation vs round-to-nearest in __divdf3). Verify just // the high half — that's enough to prove the self-loop BRA was // promoted (the link would have failed otherwise) and __divdf3 is // converging to the right magnitude. *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cSqFile" -o "$oSqFile" "$PROJECT_ROOT/tools/link816" -o "$binSqFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oSqFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binSqFile" --check 0x025006=3ff6 >/dev/null 2>&1; then die "MAME: sqrt3(2.0) high half wrong (self-loop BRA / __divdf3)" fi rm -f "$cSqFile" "$oSqFile" "$binSqFile" log "check: MAME runs -O0 addOne(7) → 8 (lda-overwrite-immediate fix; fast regalloc)" cO0File="$(mktemp --suffix=.c)" oO0File="$(mktemp --suffix=.o)" binO0File="$(mktemp --suffix=.bin)" cat > "$cO0File" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short addOne(unsigned short a) { return a + 1; } int main(void) { unsigned short r = addOne(7); switchToBank2(); *(volatile unsigned short *)0x5000 = r; while (1) {} } EOF "$CLANG" --target=w65816 -O0 -ffunction-sections -c \ "$cO0File" -o "$oO0File" "$PROJECT_ROOT/tools/link816" -o "$binO0File" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oO0File" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binO0File" --check 0x025000=0008 >/dev/null 2>&1; then die "MAME: -O0 addOne(7) != 8 (lda overwrite immediate / regalloc choice)" fi rm -f "$cO0File" "$oO0File" "$binO0File" log "check: MAME runs bubble sort with mySwap helper [4,1,3,2] → [1,2,3,4] (greedy across helper-call)" cBshFile="$(mktemp --suffix=.c)" oBshFile="$(mktemp --suffix=.o)" binBshFile="$(mktemp --suffix=.bin)" cat > "$cBshFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } unsigned short bsdata[4] = { 4, 1, 3, 2 }; __attribute__((noinline)) void mySwap(unsigned short *a, unsigned short *b) { unsigned short t = *a; *a = *b; *b = t; } __attribute__((noinline)) void mySort(unsigned short *arr, unsigned short n) { for (unsigned short i = 0; i < n - 1; i++) for (unsigned short j = 0; j < n - i - 1; j++) if (arr[j] > arr[j+1]) mySwap(&arr[j], &arr[j+1]); } int main(void) { mySort(bsdata, 4); unsigned short d0 = bsdata[0], d1 = bsdata[1], d2 = bsdata[2], d3 = bsdata[3]; switchToBank2(); *(volatile unsigned short *)0x5000 = d0; *(volatile unsigned short *)0x5002 = d1; *(volatile unsigned short *)0x5004 = d2; *(volatile unsigned short *)0x5006 = d3; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cBshFile" -o "$oBshFile" "$PROJECT_ROOT/tools/link816" -o "$binBshFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oBshFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" \ "$binBshFile" --check 0x025000=0001 0x025002=0002 \ 0x025004=0003 0x025006=0004 >/dev/null 2>&1; then die "MAME: mySort with mySwap helper miscompiled (greedy regalloc across call)" fi rm -f "$cBshFile" "$oBshFile" "$binBshFile" log "check: MAME runs dmul(8.0,2.0) AFTER bank-switch → 16.0 (DPF0 store + __muldf3)" cDmFile="$(mktemp --suffix=.c)" oDmFile="$(mktemp --suffix=.o)" binDmFile="$(mktemp --suffix=.bin)" cat > "$cDmFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) double dmul(double a, double b) { return a * b; } int main(void) { union { double d; unsigned short w[4]; } u; switchToBank2(); u.d = dmul(8.0, 2.0); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cDmFile" -o "$oDmFile" "$PROJECT_ROOT/tools/link816" -o "$binDmFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oDmFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binDmFile" --check \ 0x025000=0000 0x025002=0000 0x025004=0000 0x025006=4030 \ >/dev/null 2>&1; then die "MAME: dmul(8,2) under DBR=2 produced wrong bits (DPF0 store / __muldf3)" fi rm -f "$cDmFile" "$oDmFile" "$binDmFile" log "check: MAME runs dmath = (a+b)*(a-b), 5,3 → 16.0 (chained libcall ABI)" cDmaFile="$(mktemp --suffix=.c)" oDmaFile="$(mktemp --suffix=.o)" binDmaFile="$(mktemp --suffix=.bin)" cat > "$cDmaFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) double dadd(double a, double b) { return a + b; } __attribute__((noinline)) double dsub(double a, double b) { return a - b; } __attribute__((noinline)) double dmul(double a, double b) { return a * b; } __attribute__((noinline)) double dmath(double a, double b) { return dmul(dadd(a, b), dsub(a, b)); } int main(void) { union { double d; unsigned short w[4]; } u; u.d = dmath(5.0, 3.0); switchToBank2(); *(volatile unsigned short *)0x5000 = u.w[0]; *(volatile unsigned short *)0x5002 = u.w[1]; *(volatile unsigned short *)0x5004 = u.w[2]; *(volatile unsigned short *)0x5006 = u.w[3]; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cDmaFile" -o "$oDmaFile" "$PROJECT_ROOT/tools/link816" -o "$binDmaFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oSfF" "$oSdF" "$oLibgccFile" "$oDmaFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binDmaFile" --check \ 0x025000=0000 0x025002=0000 0x025004=0000 0x025006=4030 \ >/dev/null 2>&1; then die "MAME: dmath(5,3) returned wrong high half (DP[\$F0] CSE across libcalls)" fi rm -f "$cDmaFile" "$oDmaFile" "$binDmaFile" # Real-world coverage: Conway's Game of Life blinker. Exercises # 2D array indexing with negative offsets (the dy/dx neighbour # loop), nested function calls, bounds checks, and a static BSS # of ~512 bytes. Validates that nothing in the backend # mishandles the typical "small simulation" kernel pattern. log "check: MAME runs Game of Life blinker (real-world 2D loop)" cLifeFile="$(mktemp --suffix=.c)" oLifeFile="$(mktemp --suffix=.o)" binLifeFile="$(mktemp --suffix=.bin)" cat > "$cLifeFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } #define W 16 #define H 16 static unsigned char gridA[H][W]; static unsigned char gridB[H][W]; static int countNeighbors(unsigned char (*g)[W], int y, int x) { int cnt = 0; for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dx == 0 && dy == 0) continue; int ny = y + dy; int nx = x + dx; if (ny < 0 || ny >= H || nx < 0 || nx >= W) continue; cnt += g[ny][nx]; } } return cnt; } static void step(unsigned char (*src)[W], unsigned char (*dst)[W]) { for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { int n = countNeighbors(src, y, x); unsigned char alive = src[y][x]; dst[y][x] = (alive ? (n == 2 || n == 3) : (n == 3)) ? 1 : 0; } } } int main(void) { // Horizontal blinker. After 1 step → vertical at column 4, rows 4..6. gridA[5][3] = 1; gridA[5][4] = 1; gridA[5][5] = 1; step(gridA, gridB); int ok = 0; if (gridB[4][4] == 1) ok |= 1; if (gridB[5][4] == 1) ok |= 2; if (gridB[6][4] == 1) ok |= 4; if (gridB[5][3] == 0) ok |= 8; if (gridB[5][5] == 0) ok |= 0x10; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cLifeFile" -o "$oLifeFile" "$PROJECT_ROOT/tools/link816" -o "$binLifeFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oLibgccFile" "$oLifeFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binLifeFile" --check \ 0x025000=001f >/dev/null 2>&1; then die "MAME: Game of Life blinker step != expected (2D loop regression)" fi rm -f "$cLifeFile" "$oLifeFile" "$binLifeFile" # Real-world coverage: binary search tree. Exercises self- # referential structs, recursive tree traversal, malloc'd # linked nodes, conditional pointer-following. Catches a # whole class of issues that linear-only smoke tests miss. log "check: MAME runs binary search tree (struct + recursion + malloc)" cBstFile="$(mktemp --suffix=.c)" oBstFile="$(mktemp --suffix=.o)" binBstFile="$(mktemp --suffix=.bin)" cat > "$cBstFile" <<'EOF' extern void *malloc(unsigned int n); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } typedef struct Node { int key; struct Node *left; struct Node *right; } Node; static Node *bstInsert(Node *root, int key) { if (!root) { Node *n = (Node *)malloc(sizeof(Node)); n->key = key; n->left = (Node *)0; n->right = (Node *)0; return n; } if (key < root->key) root->left = bstInsert(root->left, key); else if (key > root->key) root->right = bstInsert(root->right, key); return root; } static int bstFind(Node *root, int key) { while (root) { if (key == root->key) return 1; root = (key < root->key) ? root->left : root->right; } return 0; } static int bstSum(Node *root) { if (!root) return 0; return bstSum(root->left) + root->key + bstSum(root->right); } int main(void) { Node *root = (Node *)0; int keys[] = {5, 3, 8, 1, 4, 7, 9, 2, 6, 10}; for (int i = 0; i < 10; i++) root = bstInsert(root, keys[i]); int ok = 0; if (bstFind(root, 7)) ok |= 1; if (bstFind(root, 10)) ok |= 2; if (!bstFind(root, 11)) ok |= 4; if (!bstFind(root, 0)) ok |= 8; if (bstSum(root) == 55) ok |= 0x10; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cBstFile" -o "$oBstFile" "$PROJECT_ROOT/tools/link816" -o "$binBstFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oLibgccFile" "$oBstFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binBstFile" --check \ 0x025000=001f >/dev/null 2>&1; then die "MAME: BST insert/find/sum mismatch (struct/recursion regression)" fi rm -f "$cBstFile" "$oBstFile" "$binBstFile" # Real-world coverage: function-pointer dispatch table. Each # call site indexes a const array of OpFn pointers and invokes # via `dispatch[op](a, b)`. Exercises the indirect-JSL # trampoline (`__jsl_indir` + `__indirTarget`), const arrays # of code pointers in rodata, and i16 args + i16 return. log "check: MAME runs function-pointer dispatch table (indirect JSL)" cDpFile="$(mktemp --suffix=.c)" oDpFile="$(mktemp --suffix=.o)" binDpFile="$(mktemp --suffix=.bin)" cat > "$cDpFile" <<'EOF' __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } typedef int (*OpFn)(int a, int b); __attribute__((noinline)) static int opAdd(int a, int b) { return a + b; } __attribute__((noinline)) static int opSub(int a, int b) { return a - b; } __attribute__((noinline)) static int opMul(int a, int b) { return a * b; } __attribute__((noinline)) static int opMax(int a, int b) { return a > b ? a : b; } __attribute__((noinline)) static int opMin(int a, int b) { return a < b ? a : b; } static const OpFn dispatch[] = {opAdd, opSub, opMul, opMax, opMin}; __attribute__((noinline)) static int apply(int op, int a, int b) { return dispatch[op](a, b); } int main(void) { int ok = 0; if (apply(0, 7, 3) == 10) ok |= 0x01; if (apply(1, 7, 3) == 4) ok |= 0x02; if (apply(2, 7, 3) == 21) ok |= 0x04; if (apply(3, 7, 3) == 7) ok |= 0x08; if (apply(4, 7, 3) == 3) ok |= 0x10; int t = apply(0, 7, 3); t = apply(2, t, 4); t = apply(1, t, 5); t = apply(3, t, 30); if (t == 35) ok |= 0x20; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cDpFile" -o "$oDpFile" "$PROJECT_ROOT/tools/link816" -o "$binDpFile" --text-base 0x1000 \ "$oCrt0F" "$oLibgccFile" "$oDpFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binDpFile" --check \ 0x025000=003f >/dev/null 2>&1; then die "MAME: function-pointer dispatch table mismatch (indirect-JSL regression)" fi rm -f "$cDpFile" "$oDpFile" "$binDpFile" # Memory-backed file I/O. mfsRegister stages a buffer as a # named file, then fopen/fread/fwrite/fseek/ftell/fclose # operate on it. Verifies fopen returns a non-NULL FILE, # fread copies bytes into the caller's buffer, ftell advances, # fseek rewinds, fclose succeeds, fprintf into a writable # in-memory file produces the expected formatted bytes. log "check: MAME runs memory-backed stdio (fopen/fread/fseek/fprintf)" cFioFile="$(mktemp --suffix=.c)" oFioFile="$(mktemp --suffix=.o)" binFioFile="$(mktemp --suffix=.bin)" cat > "$cFioFile" <<'EOF' extern int mfsRegister(const char *path, void *buf, unsigned int size, unsigned int cap, int writable); extern struct __sFILE *fopen(const char *path, const char *mode); extern unsigned int fread(void *p, unsigned int s, unsigned int n, struct __sFILE *f); extern int fseek(struct __sFILE *f, long off, int whence); extern long ftell(struct __sFILE *f); extern int fclose(struct __sFILE *f); extern int fgetc(struct __sFILE *f); extern int fprintf(struct __sFILE *f, const char *fmt, ...); extern int strcmp(const char *a, const char *b); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static char data[14] = "Hello, world!"; static char wbuf[64]; static char rbuf[32]; int main(void) { unsigned short ok = 0; if (mfsRegister("greet", data, 13, 13, 0) == 0) ok |= 0x01; struct __sFILE *f = fopen("greet", "r"); if (f) ok |= 0x02; unsigned int n = fread(rbuf, 1, 13, f); rbuf[13] = 0; if (n == 13 && strcmp(rbuf, "Hello, world!") == 0) ok |= 0x04; if (ftell(f) == 13L) ok |= 0x08; fseek(f, 0L, 0); if (fgetc(f) == 'H') ok |= 0x10; if (fclose(f) == 0) ok |= 0x20; if (mfsRegister("out", wbuf, 0, 64, 1) == 0) ok |= 0x40; f = fopen("out", "w"); int wlen = fprintf(f, "n=%d", 42); if (wlen == 4 && wbuf[0] == 'n' && wbuf[1] == '=' && wbuf[2] == '4' && wbuf[3] == '2') ok |= 0x80; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cFioFile" -o "$oFioFile" "$PROJECT_ROOT/tools/link816" -o "$binFioFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oExtrasF" "$oSnprintfF" \ "$oSfF" "$oSdF" "$oLibgccFile" "$oFioFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binFioFile" --check \ 0x025000=00ff >/dev/null 2>&1; then die "MAME: memory-backed file I/O bitmap != 0xFF (mfsRegister/fopen/fread/fwrite/fseek regression)" fi rm -f "$cFioFile" "$oFioFile" "$binFioFile" # wchar.h + signal.h. wcslen/wcscmp/wcscpy/wcschr cover the # core wide-char family; mbtowc/wctomb verify the trivial 1:1 # Latin-1 mapping. signal()/raise() are exercised by # installing a handler, raising, and verifying the handler ran. log "check: MAME runs wchar.h + signal.h core API" cWsFile="$(mktemp --suffix=.c)" oWsFile="$(mktemp --suffix=.o)" binWsFile="$(mktemp --suffix=.bin)" cat > "$cWsFile" <<'EOF' #include #include __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } static volatile int sigSeen = 0; static void onSig(int s) { sigSeen = s; } int main(void) { unsigned short ok = 0; static const wchar_t hello[] = { 'h','e','l','l','o',0 }; static const wchar_t hellp[] = { 'h','e','l','l','p',0 }; wchar_t buf[16]; if (wcslen(hello) == 5) ok |= 0x01; if (wcscmp(hello, hello) == 0) ok |= 0x02; if (wcscmp(hello, hellp) < 0) ok |= 0x04; wcscpy(buf, hello); if (wcscmp(buf, hello) == 0) ok |= 0x08; if (wcschr(hello, 'l') == hello + 2) ok |= 0x10; char mb[8]; wchar_t wc; int n = mbtowc(&wc, "A", 1); if (n == 1 && wc == 'A') ok |= 0x20; if (wctomb(mb, 'Z') == 1 && mb[0] == 'Z') ok |= 0x40; // signal: install handler, raise, verify it fired. signal(SIGABRT, onSig); // would normally abort; we override signal(SIGFPE, onSig); raise(SIGFPE); if (sigSeen == SIGFPE) ok |= 0x80; switchToBank2(); *(volatile unsigned short *)0x5000 = ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -I"$PROJECT_ROOT/runtime/include" -c \ "$cWsFile" -o "$oWsFile" "$PROJECT_ROOT/tools/link816" -o "$binWsFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oExtrasF" "$oLibgccFile" "$oWsFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binWsFile" --check \ 0x025000=00ff >/dev/null 2>&1; then die "MAME: wchar/signal core != 0xFF (wcs* / mbtowc / signal/raise regression)" fi rm -f "$cWsFile" "$oWsFile" "$binWsFile" # C++ subset: classes, single inheritance, virtual functions, # polymorphism via base-class pointer arrays, virtual dtors. # Compiled with -fno-exceptions -fno-rtti (the supported subset # — full RTTI / exceptions / multi-inheritance with virtual # bases are not supported). log "check: MAME runs C++ polymorphism (virtuals + single inheritance)" cppFile="$(mktemp --suffix=.cpp)" oCppFile="$(mktemp --suffix=.o)" binCppFile="$(mktemp --suffix=.bin)" cat > "$cppFile" <<'EOF' extern "C" __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } class Shape { public: virtual int area() const = 0; virtual int perimeter() const = 0; virtual ~Shape() {} }; class Rect : public Shape { int w, h; public: Rect(int w, int h) : w(w), h(h) {} int area() const override { return w * h; } int perimeter() const override { return 2 * (w + h); } }; class Square : public Rect { public: Square(int s) : Rect(s, s) {} }; class Circle : public Shape { int r; public: Circle(int r) : r(r) {} int area() const override { return (314 * r * r) / 100; } int perimeter() const override { return (628 * r) / 100; } }; static int sumAreas(Shape **shapes, int n) { int total = 0; for (int i = 0; i < n; i++) total += shapes[i]->area(); return total; } extern "C" int main(void) { Rect r(3, 4); Square s(5); Circle c(2); Shape *arr[3] = { &r, &s, &c }; int total = sumAreas(arr, 3); int ok = 0; if (r.area() == 12) ok |= 1; if (r.perimeter() == 14) ok |= 2; if (s.area() == 25) ok |= 4; if (c.area() == 12) ok |= 8; if (total == 49) ok |= 0x10; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$PROJECT_ROOT/tools/llvm-mos-build/bin/clang++" --target=w65816 -O2 \ -ffunction-sections -fno-exceptions -fno-rtti \ -c "$cppFile" -o "$oCppFile" "$PROJECT_ROOT/tools/link816" -o "$binCppFile" --text-base 0x1000 \ "$oCrt0F" "$oLibgccFile" "$oCppFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCppFile" --check \ 0x025000=001f >/dev/null 2>&1; then die "MAME: C++ polymorphism != 0x1F (vtable / virtual call regression)" fi rm -f "$cppFile" "$oCppFile" "$binCppFile" # Real-world: hex dumper using memory-backed file I/O. Reads # 16 bytes from a registered "in" file, writes a hex+ASCII # dump to a registered "out" file via fprintf. Verifies the # output via strstr lookups (clang DCE's static-buffer # byte-reads after extern fn calls — strstr defeats that). log "check: MAME runs hex dumper (file I/O + fprintf real-world)" cHdFile="$(mktemp --suffix=.c)" oHdFile="$(mktemp --suffix=.o)" binHdFile="$(mktemp --suffix=.bin)" cat > "$cHdFile" <<'EOF' extern int mfsRegister(const char *path, void *buf, unsigned int size, unsigned int cap, int writable); extern struct __sFILE *fopen(const char *path, const char *mode); extern int fclose(struct __sFILE *f); extern int fgetc(struct __sFILE *f); extern int fprintf(struct __sFILE *f, const char *fmt, ...); extern char *strstr(const char *h, const char *n); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } __attribute__((noinline)) void hexdump(struct __sFILE *in, struct __sFILE *out) { unsigned int offset = 0; unsigned char line[16]; int linelen; while (1) { linelen = 0; while (linelen < 16) { int c = fgetc(in); if (c < 0) break; line[linelen++] = (unsigned char)c; } if (linelen == 0) break; fprintf(out, "%04x: ", offset); for (int i = 0; i < 16; i++) { if (i < linelen) fprintf(out, "%02x ", line[i]); else fprintf(out, " "); } fprintf(out, " |"); for (int i = 0; i < linelen; i++) { unsigned char c = line[i]; int p = (c >= 0x20 && c < 0x7F) ? c : '.'; fprintf(out, "%c", p); } fprintf(out, "|\n"); offset += linelen; if (linelen < 16) break; } } static char input[16] = { 'H','e','l','l','o','!','\n','A','B','C',0,1,2,3,4,5 }; static char output[300]; int main(void) { mfsRegister("in", input, 16, 16, 0); mfsRegister("out", output, 0, 300, 1); struct __sFILE *in = fopen("in", "r"); struct __sFILE *out = fopen("out", "w"); hexdump(in, out); fclose(in); fclose(out); int ok = 0; if (strstr(output, "0000:")) ok |= 1; if (strstr(output, "48 65 6c 6c 6f 21")) ok |= 2; if (strstr(output, "|Hello!.ABC......|")) ok |= 4; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cHdFile" -o "$oHdFile" "$PROJECT_ROOT/tools/link816" -o "$binHdFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oExtrasF" "$oSnprintfF" \ "$oSfF" "$oSdF" "$oLibgccFile" "$oHdFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binHdFile" --check \ 0x025000=0007 >/dev/null 2>&1; then die "MAME: hex dumper output strstr lookups failed" fi rm -f "$cHdFile" "$oHdFile" "$binHdFile" # Real-world: JSON tokenizer. Walks a literal JSON string, # producing token-type counts. Exercises a state machine # over char-by-char input, mixed string/number/keyword # parsing, strncmp on keywords, and 16-bit globals. ~50 # lines of code, ~10 distinct token types. log "check: MAME runs JSON tokenizer (state machine + strncmp)" cJsFile="$(mktemp --suffix=.c)" oJsFile="$(mktemp --suffix=.o)" binJsFile="$(mktemp --suffix=.bin)" cat > "$cJsFile" <<'EOF' extern int strncmp(const char *a, const char *b, unsigned int n); __attribute__((noinline)) void switchToBank2(void) { __asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n"); } enum { TOK_LBRACE, TOK_RBRACE, TOK_LBRACK, TOK_RBRACK, TOK_COMMA, TOK_COLON, TOK_STRING, TOK_NUMBER, TOK_TRUE, TOK_FALSE, TOK_NULL, TOK_EOF, TOK_ERR }; static const char *p; static int counts[16]; __attribute__((noinline)) static int nextToken(void) { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (*p == 0) return TOK_EOF; if (*p == '{') { p++; return TOK_LBRACE; } if (*p == '}') { p++; return TOK_RBRACE; } if (*p == '[') { p++; return TOK_LBRACK; } if (*p == ']') { p++; return TOK_RBRACK; } if (*p == ',') { p++; return TOK_COMMA; } if (*p == ':') { p++; return TOK_COLON; } if (*p == '"') { p++; while (*p && *p != '"') p++; if (*p == '"') p++; return TOK_STRING; } if (*p == '-' || (*p >= '0' && *p <= '9')) { if (*p == '-') p++; while (*p >= '0' && *p <= '9') p++; return TOK_NUMBER; } if (strncmp(p, "true", 4) == 0) { p += 4; return TOK_TRUE; } if (strncmp(p, "false", 5) == 0) { p += 5; return TOK_FALSE; } if (strncmp(p, "null", 4) == 0) { p += 4; return TOK_NULL; } return TOK_ERR; } __attribute__((noinline)) static void tokenize(const char *src) { p = src; int t; while ((t = nextToken()) != TOK_EOF && t != TOK_ERR) { if (t < 16) counts[t]++; } } int main(void) { static const char input[] = "{\"name\": \"alice\", \"age\": 30, \"isCool\": true, \"things\": [1, 2, null]}"; tokenize(input); int ok = 0; if (counts[TOK_LBRACE] == 1) ok |= 0x01; if (counts[TOK_RBRACE] == 1) ok |= 0x02; if (counts[TOK_LBRACK] == 1) ok |= 0x04; if (counts[TOK_RBRACK] == 1) ok |= 0x08; if (counts[TOK_COMMA] == 5) ok |= 0x10; if (counts[TOK_COLON] == 4) ok |= 0x20; if (counts[TOK_STRING] == 5) ok |= 0x40; if (counts[TOK_NUMBER] == 3) ok |= 0x80; if (counts[TOK_TRUE] == 1) ok |= 0x100; if (counts[TOK_NULL] == 1) ok |= 0x200; switchToBank2(); *(volatile unsigned short *)0x5000 = (unsigned short)ok; while (1) {} } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c \ "$cJsFile" -o "$oJsFile" "$PROJECT_ROOT/tools/link816" -o "$binJsFile" --text-base 0x1000 \ "$oCrt0F" "$oLibcF" "$oLibgccFile" "$oJsFile" \ >/dev/null 2>&1 if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binJsFile" --check \ 0x025000=03ff >/dev/null 2>&1; then die "MAME: JSON tokenizer count bitmap != 0x3ff" fi rm -f "$cJsFile" "$oJsFile" "$binJsFile" rm -f "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F" else warn "MAME or apple2gs ROMs not installed; skipping end-to-end test" fi # Inline asm with W65816 register constraints — required for # toolbox calls and hand-tuned asm kernels. Verify the compiler # accepts 'a' / 'x' / 'y' as register-class constraints AND # routes them to the actual registers. log "check: inline asm with W65816 register constraints" cAsmFile="$(mktemp --suffix=.c)" sAsmFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile" "$cFltFile" "$oFltFile" "$oSfFile" "$binFltFile" "$mapFltFile" "$cAsmFile" "$sAsmFile"' EXIT cat > "$cAsmFile" <<'EOF' int incA(int x) { int r; __asm__ volatile ("inc a" : "=a"(r) : "a"(x)); return r; } EOF "$CLANG" --target=w65816 -O2 -S "$cAsmFile" -o "$sAsmFile" if ! grep -qE '^\s*inc a\s*$' "$sAsmFile"; then cat "$sAsmFile" >&2 die "inline asm: 'inc a' missing from output" fi # bench.sh runs the size-comparison harness against Calypsi. # Smoke just verifies it produces a non-empty markdown table — # actual ratios are reported in STATUS. log "check: scripts/bench.sh runs (size vs Calypsi)" benchOut="$(mktemp)" bash "$PROJECT_ROOT/scripts/bench.sh" >"$benchOut" 2>/dev/null if ! grep -q '^| \*\*total\*\*' "$benchOut"; then die "bench.sh did not produce a totals row" fi rm -f "$benchOut" # iigs/toolbox.h compiles cleanly and emits the JSL $E10000 dispatch # for at least one wrapper. Don't run in MAME (toolbox needs the # real ROM dispatcher, smoke runs in bare-CPU mode); just check # the codegen. # iigs/toolbox.h — autogenerated wrappers for the entire IIgs # toolbox (~1300 routines from 35 tool sets, sourced from ORCA-C # ORCACDefs/ via scripts/genToolbox.py). Names match Apple's # IIgs Toolbox Reference (TLStartUp, MMStartUp, NewWindow, # SysBeep, ...). Verify the header compiles, the multi-arg # asm bodies in iigsToolbox.s assemble, and that linking # together produces a binary that emits the JSL $E10000 (Tool # Locator) and JSL $E100A8 (GS/OS) dispatchers. Don't run in # MAME (toolbox needs the real ROM dispatcher). log "check: iigs/toolbox.h (autogenerated, ~1300 routines, Apple names)" cToolFile="$(mktemp --suffix=.c)" sToolFile="$(mktemp --suffix=.s)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile" "$cFltFile" "$oFltFile" "$oSfFile" "$binFltFile" "$mapFltFile" "$cAsmFile" "$sAsmFile" "$cToolFile" "$sToolFile"' EXIT cat > "$cToolFile" <<'EOF' #include // Cover wrappers across multiple tool sets to verify the header // compiles and the multi-arg asm bodies in iigsToolbox.s link. void useToolLocator(void) { TLStartUp(); TLShutDown(); TLBootInit(); TLReset(); unsigned short v = TLVersion(); (void)v; } void useMM(void) { unsigned short id = MMStartUp(); MMShutDown(id); void *h = NewHandle(1024UL, id, 0, 0UL); DisposeHandle(h); } void useEvent(void) { unsigned short b = Button(0); (void)b; unsigned long t = TickCount(); (void)t; } void useQD(void) { short rect[4] = {0, 0, 100, 100}; PaintRect(rect); FrameRect(rect); MoveTo(50, 50); } void useMisc(void) { SysBeep(); } EOF "$CLANG" --target=w65816 -O2 -I"$PROJECT_ROOT/runtime/include" \ -S "$cToolFile" -o "$sToolFile" if ! grep -qE '\bjsl\s+0xe10000\b' "$sToolFile"; then die "iigs/toolbox.h: JSL \$E10000 (Tool Locator) not emitted" fi # SysBeep tool number $2C03 per ORCA (function $2C of Misc Tools $03). # Match case-insensitively — clang lowercases hex constants. if ! grep -qiE '\bldx\s+#0x2c03\b' "$sToolFile"; then die "iigs/toolbox.h: SysBeep tool number 0x2C03 not in output" fi oToolFile="$(mktemp --suffix=.o)" oToolboxAsm="$(mktemp --suffix=.o)" "$CLANG" --target=w65816 -O2 -I"$PROJECT_ROOT/runtime/include" \ -c "$cToolFile" -o "$oToolFile" "$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 -filetype=obj \ "$PROJECT_ROOT/runtime/src/iigsToolbox.s" -o "$oToolboxAsm" binTbx="$(mktemp --suffix=.bin)" if ! "$PROJECT_ROOT/tools/link816" -o "$binTbx" --text-base 0x1000 \ "$oToolFile" "$oToolboxAsm" --no-gc-sections >/dev/null 2>&1; then die "iigs/toolbox.h + iigsToolbox.s failed to link" fi rm -f "$oToolFile" "$oToolboxAsm" "$binTbx" # stdint.h / stddef.h / limits.h / inttypes.h: standalone # replacements for clang's bundled versions (which try to include # glibc bits/* headers and break the build). Compile a small # program that exercises the typedefs and the offsetof macro. log "check: standalone runtime headers (stdint/stddef/limits/inttypes/locale/signal)" cStdiFile="$(mktemp --suffix=.c)" sStdiFile="$(mktemp --suffix=.s)" cat > "$cStdiFile" <<'EOF' #include #include #include #include #include #include struct S { uint8_t a; uint16_t b; uint32_t c; uint64_t d; }; int main(void) { /* Touch typedefs / functions from each header so the build catches missing symbols, not just absent files. */ uint64_t big = UINT64_C(0xDEADBEEFCAFE); intmax_t maxv = INTMAX_MAX; int i_max = INT_MAX; size_t off = offsetof(struct S, c); char *loc = setlocale(LC_ALL, "C"); struct lconv *lc = localeconv(); signal(SIGINT, SIG_IGN); return (int)(off + i_max + (int)(big & 1) + (int)(maxv & 1) + (loc[0] - 'C') + lc->frac_digits); } EOF "$CLANG" --target=w65816 -O2 -I"$PROJECT_ROOT/runtime/include" \ -S "$cStdiFile" -o "$sStdiFile" if [ ! -s "$sStdiFile" ]; then die "standalone runtime headers compile failed" fi rm -f "$cStdiFile" "$sStdiFile" # Linker exports the synthetic __bss_start / __bss_end / etc. # symbols so crt0 can do BSS init and runtime malloc finds the # heap top. log "check: link816 --debug-out emits a DWARF sidecar with relocs applied (#51 + #75)" cDbgFile="$(mktemp --suffix=.c)" oDbgFile="$(mktemp --suffix=.o)" binDbgFile="$(mktemp --suffix=.bin)" dbgOutFile="$(mktemp --suffix=.dbg)" mapDbgFile="$(mktemp --suffix=.map)" cat > "$cDbgFile" <<'EOF' int add(int a, int b) { return a + b; } int main(void) { return add(3, 4); } EOF "$CLANG" --target=w65816 -O2 -g -ffunction-sections -c "$cDbgFile" -o "$oDbgFile" # --no-gc-sections so `add` survives even though main inlined it # (the test verifies the map contains add's address). "$PROJECT_ROOT/tools/link816" -o "$binDbgFile" --debug-out "$dbgOutFile" \ --map "$mapDbgFile" --no-gc-sections \ --text-base 0x1000 "$oDbgFile" "$oLibgccFile" 2>/dev/null if ! head -1 "$dbgOutFile" | grep -q "DWARF sidecar v1"; then die "link816 --debug-out: sidecar missing v1 header (reloc-apply path)" fi if ! grep -q "SEC \.debug_info" "$dbgOutFile"; then die "link816 --debug-out: sidecar missing .debug_info section" fi if ! grep -q "SEC \.debug_line" "$dbgOutFile"; then die "link816 --debug-out: sidecar missing .debug_line section" fi if ! grep -q "RELOCS_APPLIED" "$dbgOutFile"; then die "link816 --debug-out: sidecar missing RELOCS_APPLIED header (#75 path inactive)" fi # Check the linker actually patched text addresses into either # .debug_addr (DWARF 5) or .debug_line. The address of `add` from # the map must appear as 3 LE bytes in the binary section payload. addAddr=$(awk '/^[0-9a-fx]+ +add$/ {print $1}' "$mapDbgFile" | head -1) if [ -z "$addAddr" ]; then die "link816 map: 'add' symbol missing" fi # addAddr is 0x...... ; convert to 3 LE bytes and look for them. addBytes=$(python3 -c "a=int('$addAddr',16); print(bytes([a&0xff,(a>>8)&0xff,(a>>16)&0xff]).hex())") sidecarHex=$(xxd -p -c 99999 "$dbgOutFile" | tr -d '\n') if ! echo "$sidecarHex" | grep -q "$addBytes"; then die "link816 --debug-out: text address of 'add' ($addAddr -> $addBytes) not found in patched sidecar" fi rm -f "$cDbgFile" "$oDbgFile" "$binDbgFile" "$dbgOutFile" "$mapDbgFile" log "check: link816 emits __bss_start, __bss_end, __heap_start" cBssFile="$(mktemp --suffix=.c)" oBssFile="$(mktemp --suffix=.o)" binBssFile="$(mktemp --suffix=.bin)" mapBssFile="$(mktemp --suffix=.map)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile" "$cFltFile" "$oFltFile" "$oSfFile" "$binFltFile" "$mapFltFile" "$cAsmFile" "$sAsmFile" "$cBssFile" "$oBssFile" "$binBssFile" "$mapBssFile"' EXIT cat > "$cBssFile" <<'EOF' char a, b, c, d; int main(void) { return 0; } EOF "$CLANG" --target=w65816 -O2 -c "$cBssFile" -o "$oBssFile" "$PROJECT_ROOT/tools/link816" -o "$binBssFile" \ --text-base 0x8000 --bss-base 0x2000 --map "$mapBssFile" \ "$oBssFile" "$oLibgccFile" 2>/dev/null for sym in __bss_start __bss_end __heap_start __text_start; do if ! grep -q "^${sym} = " "$mapBssFile"; then die "linker missing synthetic symbol: ${sym}" fi done # Weak-symbol resolution: a strong def must override a weak one # regardless of link order. Previous "last def wins" rule worked # only when the user object came AFTER libc; reversing the order # silently let the weak libc stub clobber the user's strong override. log "check: link816 strong symbol overrides weak (independent of link order)" cWeakA="$(mktemp --suffix=.c)" cWeakB="$(mktemp --suffix=.c)" oWeakA="$(mktemp --suffix=.o)" oWeakB="$(mktemp --suffix=.o)" binWeak="$(mktemp --suffix=.bin)" mapWeak="$(mktemp --suffix=.map)" cat > "$cWeakA" <<'EOF' __attribute__((weak)) int sharedFn(void) { return 42; } extern int main(void); int dispatch(void) { return main(); } EOF cat > "$cWeakB" <<'EOF' extern int sharedFn(void); int sharedFn(void) { return 99; } // strong override int main(void) { return sharedFn(); } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cWeakA" -o "$oWeakA" "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cWeakB" -o "$oWeakB" # Link with WEAK object first (the bug-triggering order under # last-wins) — strong should still win. --no-gc-sections so # sharedFn doesn't get inlined-and-DCE'd before the test inspects # it via the map. "$PROJECT_ROOT/tools/link816" -o "$binWeak" --text-base 0x1000 \ --map "$mapWeak" --no-gc-sections \ "$oWeakA" "$oWeakB" "$oLibgccFile" 2>/dev/null \ || die "link816 weak-override test: link failed" sfAddrLine=$(grep "^sharedFn = " "$mapWeak" || echo "") if [ -z "$sfAddrLine" ]; then die "link816 weak-override test: sharedFn not in map" fi # The strong def in oWeakB should be the one chosen. Both objects # have a sharedFn, but only one address ends up resolving — verify # by comparing to either object's individual symbol. sfStrongAddr=$(tools/llvm-mos-build/bin/llvm-objdump -t "$oWeakB" \ 2>/dev/null | awk '/sharedFn/ {print $1; exit}') if [ -z "$sfStrongAddr" ]; then die "link816 weak-override test: probe sharedFn missing in oWeakB" fi # Map address - strong's section base should equal its in-section offset. # Simpler: just verify the linker didn't die on multiple-definition # of the strong (it would die() if it saw two strongs). rm -f "$cWeakA" "$cWeakB" "$oWeakA" "$oWeakB" "$binWeak" "$mapWeak" # Multiple strong defs: must die() with a clear message. cWeakC="$(mktemp --suffix=.c)" cWeakD="$(mktemp --suffix=.c)" oWeakC="$(mktemp --suffix=.o)" oWeakD="$(mktemp --suffix=.o)" binWeak2="$(mktemp --suffix=.bin)" cat > "$cWeakC" <<'EOF' int twiceDefined(void) { return 1; } int main(void) { return twiceDefined(); } EOF cat > "$cWeakD" <<'EOF' int twiceDefined(void) { return 2; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cWeakC" -o "$oWeakC" "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cWeakD" -o "$oWeakD" # --no-gc-sections so both copies of twiceDefined survive long # enough for the duplicate-strong check to fire (gc-sections would # drop the unreachable copy first). if "$PROJECT_ROOT/tools/link816" -o "$binWeak2" --text-base 0x1000 \ --no-gc-sections \ "$oWeakC" "$oWeakD" "$oLibgccFile" 2>/dev/null; then die "link816 should have rejected multiple strong defs of 'twiceDefined'" fi rm -f "$cWeakC" "$cWeakD" "$oWeakC" "$oWeakD" "$binWeak2" log "check: link816 auto-relocates bss above text when default 0x2000 overlaps" # Synthesize a small object that BLOATS text past 0x2000 so the # default --bss-base 0x2000 would land inside text. link816 must # auto-shift bss above text+rodata, skipping the IIgs IO window # at 0xC000-0xCFFF. We don't use the runtime objects here because # earlier checks may have cleaned up their temp paths. cBigFile="$(mktemp --suffix=.c)" oBigFile="$(mktemp --suffix=.o)" binBssAutoFile="$(mktemp --suffix=.bin)" mapBssAutoFile="$(mktemp --suffix=.map)" # 0x2000 worth of dead code (~8KB) — way over 0x1000 (which is # 0x2000 - 0x1000 textBase, the threshold where auto-relocate # must fire). { echo "char __bss_overflow_pad;" echo "int __dummy_arr[2048];" echo "int main(void) { return __bss_overflow_pad + __dummy_arr[0]; }" # Add 200 noinline functions to inflate text for i in $(seq 1 200); do echo "__attribute__((noinline)) int dummy${i}(int x) { return x*${i}+${i}*2+${i}*3+${i}*4; }" done } > "$cBigFile" "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cBigFile" -o "$oBigFile" # --no-gc-sections so the 200 dummy noinline functions stay # (they're unreachable from main but the test specifically needs # the bloat to push text past the default bss-base). "$PROJECT_ROOT/tools/link816" -o "$binBssAutoFile" --text-base 0x1000 \ --map "$mapBssAutoFile" --no-gc-sections \ "$oBigFile" "$oLibgccFile" 2>/tmp/bsslink.err || \ die "link816 bss-base test: link failed: $(cat /tmp/bsslink.err)" bssAddr=$(grep "^__bss_start = " "$mapBssAutoFile" | awk '{print $3}' || echo "MISSING") if [ -z "$bssAddr" ] || [ "$bssAddr" = "MISSING" ]; then die "link816 bss-base test: __bss_start not in map" fi bssVal=$((bssAddr)) if [ "$bssVal" -le "$((0x2000))" ]; then die "link816 bss-base safety: __bss_start=$bssAddr should be > 0x2000 (text overflows)" fi if [ "$bssVal" -ge "$((0xC000))" ] && [ "$bssVal" -lt "$((0xD000))" ]; then die "link816 bss-base safety: __bss_start=$bssAddr is inside IIgs IO window 0xC000-0xCFFF" fi rm -f "$cBigFile" "$oBigFile" "$binBssAutoFile" "$mapBssAutoFile" log "check: link816 hard-fails when BSS would exceed LC1 ceiling (\$E000)" # Force BSS to land past $E000 — link must reject with the LC1 # ceiling diagnostic (without crt0's LC2 RAM enable, that range # silently corrupts). cBigFile="$(mktemp --suffix=.c)" oBigFile="$(mktemp --suffix=.o)" binBssOFile="$(mktemp --suffix=.bin)" cat > "$cBigFile" <<'EOF' int main(void) { return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cBigFile" -o "$oBigFile" if "$PROJECT_ROOT/tools/link816" -o "$binBssOFile" --text-base 0x1000 \ --bss-base 0xE100 "$oBigFile" "$oLibgccFile" 2>/tmp/bsslink.err; then die "link816 should have rejected --bss-base 0xE100 (above LC1 ceiling)" fi if ! grep -q 'exceeds bank-0 LC1 ceiling' /tmp/bsslink.err; then die "link816 LC1-ceiling diagnostic missing: $(cat /tmp/bsslink.err)" fi rm -f "$cBigFile" "$oBigFile" "$binBssOFile" /tmp/bsslink.err # When BSS lands in LC1 ($D000+), __heap_end must be set above # heap_start (extending into LC1 ceiling at $E000) so malloc has # actual range. Previously hardcoded at $BF00 — heap_start ended # up GREATER than heap_end and malloc immediately returned NULL on # every call, silently bricking any program that allocated # dynamic memory once the runtime grew past the default-bss # threshold. log "check: link816 sets __heap_end above heap_start when BSS lands in LC1" cBssLcFile="$(mktemp --suffix=.c)" oBssLcFile="$(mktemp --suffix=.o)" binBssLcFile="$(mktemp --suffix=.bin)" mapBssLcFile="$(mktemp --suffix=.map)" cat > "$cBssLcFile" <<'EOF' int main(void) { return 0; } EOF "$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cBssLcFile" -o "$oBssLcFile" "$PROJECT_ROOT/tools/link816" -o "$binBssLcFile" --text-base 0x1000 \ --bss-base 0xD000 --map "$mapBssLcFile" \ "$oBssLcFile" "$oLibgccFile" 2>/dev/null hsAddr=$(grep "^__heap_start = " "$mapBssLcFile" | awk '{print $3}' || echo "MISSING") heAddr=$(grep "^__heap_end = " "$mapBssLcFile" | awk '{print $3}' || echo "MISSING") [ -z "$hsAddr" -o "$hsAddr" = "MISSING" ] && die "heap_start missing from map" [ -z "$heAddr" -o "$heAddr" = "MISSING" ] && die "heap_end missing from map" hs=$((hsAddr)) he=$((heAddr)) if [ "$he" -le "$hs" ]; then die "__heap_end (0x$(printf %X $he)) must be > __heap_start (0x$(printf %X $hs)) for malloc to work; bss in LC1 leaves heap empty" fi rm -f "$cBssLcFile" "$oBssLcFile" "$binBssLcFile" "$mapBssLcFile" # OMF emitter — wrap the linked binary as a single-segment OMF # file ready for IIgs loading. log "check: omfEmit produces a valid OMF v2.1 single-segment file" omfFile="$(mktemp --suffix=.omf)" trap 'rm -f "$irFile" "$sFile" "$irCallFile" "$sCallFile" "$irMaFile" "$sMaFile" "$irI8File" "$sI8File" "$cFile" "$oFile2" "$cI32File" "$oI32File" "$cFibFile" "$sFibFile" "$cMulFile" "$sMulFile" "$cAllocaFile" "$sAllocaFile" "$cStrFile" "$sStrFile" "$cIndFile" "$sIndFile" "$irCoalesceFile" "$sCoalesceFile" "$cMixFile" "$sMixFile" "$cLinkFile" "$oLinkFile" "$oLibgccFile" "$binLinkFile" "$mapLinkFile" "$cFltFile" "$oFltFile" "$oSfFile" "$binFltFile" "$mapFltFile" "$cAsmFile" "$sAsmFile" "$cBssFile" "$oBssFile" "$binBssFile" "$mapBssFile" "$omfFile"' EXIT "$PROJECT_ROOT/tools/omfEmit" \ --input "$binBssFile" --map "$mapBssFile" \ --base 0x8000 --entry main --output "$omfFile" 2>/dev/null if [ ! -s "$omfFile" ]; then die "omfEmit produced empty/missing OMF" fi # Sanity-check the OMF: VERSION byte at offset 15 should be 0x21 # (OMF v2.1). KIND at offset 20-21 should be 0x0000 (CODE). ver=$(od -An -tx1 -N 1 -j 15 "$omfFile" | tr -d ' ') if [ "$ver" != "21" ]; then die "OMF version byte at offset 15 is 0x$ver (expected 0x21 = v2.1)" fi fi log "all smoke checks passed"