Checkpoint

This commit is contained in:
Scott Duensing 2026-05-02 19:59:35 -05:00
parent 81694c5971
commit cbae131d0c
5 changed files with 671 additions and 41 deletions

View file

@ -81,11 +81,16 @@ which runs correctly under MAME (apple2gs).
abort(), SIGINT/SIGTERM call exit(128+sig), others ignored. abort(), SIGINT/SIGTERM call exit(128+sig), others ignored.
- `<locale.h>`: setlocale always returns "C"; localeconv returns - `<locale.h>`: setlocale always returns "C"; localeconv returns
a fixed C-locale lconv struct. a fixed C-locale lconv struct.
- C++ subset: classes, single inheritance, virtual functions, - C++ subset: classes, single inheritance, multiple inheritance
polymorphism via base-class pointer arrays, virtual dtors. (Drawable+Movable through one Sprite), virtual base diamond
Compile with `clang++ -fno-exceptions -fno-rtti`. Multiple (A and B virtually derive Base; Diamond inherits from both
inheritance with virtual bases, full RTTI, exceptions are with one shared Base subobject), virtual functions,
out of scope. polymorphism via base-class pointer arrays, virtual dtors,
this-pointer adjustment for non-leftmost bases, vbase offset
tables. Compile with `clang++ -fno-exceptions -fno-rtti`.
Full RTTI (`dynamic_cast`, `typeid`) and exceptions remain
out of scope — those need libcxxabi (`__dynamic_cast`,
`__cxa_throw`, unwind tables, personality routine).
**Toolchain:** **Toolchain:**
@ -105,21 +110,24 @@ which runs correctly under MAME (apple2gs).
image addresses. image addresses.
- `runtime/build.sh` builds crt0, libc, soft-float, soft-double, - `runtime/build.sh` builds crt0, libc, soft-float, soft-double,
libgcc into linkable objects. libgcc into linkable objects.
- `scripts/smokeTest.sh` runs 113 end-to-end checks at -O2: - `scripts/smokeTest.sh` runs 120 end-to-end checks at -O2:
scalar ops, control flow, calling conventions, MAME execution scalar ops, control flow, calling conventions, MAME execution
regressions, link816 bss-base safety + weak-symbol resolution + regressions, link816 bss-base safety + weak-symbol resolution +
heap_end-vs-heap_start sanity, iigs/toolbox.h compile + link, heap_end-vs-heap_start sanity, iigs/toolbox.h compile + link,
standalone runtime headers, AsmPrinter peepholes (STZ / PEA / iigs/gsos.h compile + link, standalone runtime headers,
PEI — single-STA, shared-LDA-multi-STA, DPF0-forwarding), AsmPrinter peepholes (STZ / PEA / PEI — single-STA, shared-
malloc/free coalesce ordering, plus real-world coverage: LDA-multi-STA, DPF0-forwarding), malloc/free coalesce ordering,
Conway's Game of Life blinker (2D loop + neighbour bounds), plus real-world coverage: Conway's Game of Life blinker
binary search tree (recursive struct + malloc), function-pointer (2D loop + neighbour bounds), binary search tree (recursive
dispatch table (indirect JSL via `__jsl_indir`), memory-backed struct + malloc), function-pointer dispatch table (indirect
file I/O (mfsRegister + fopen/fread/fwrite/fseek/fprintf), C++ JSL via `__jsl_indir`), memory-backed file I/O (mfsRegister +
polymorphism (single inheritance + virtual functions), wchar / fopen/fread/fwrite/fseek/fprintf), C++ polymorphism (single
signal core APIs, hex dumper writing through fprintf, JSON inheritance), C++ multiple inheritance (Drawable+Movable),
tokenizer state machine, scripts/bench.sh size-vs-Calypsi C++ virtual base diamond, wchar / signal core APIs, hex dumper
harness. 100% pass. writing through fprintf, JSON tokenizer state machine,
hash-table command shell (parser + dispatch + chained
collisions over fprintf-to-mfs), scripts/bench.sh size-vs-
Calypsi harness. 100% pass.
- `scripts/bench.sh` compiles a microbenchmark suite with both - `scripts/bench.sh` compiles a microbenchmark suite with both
clang (this toolchain) and Calypsi cc65816, comparing emitted clang (this toolchain) and Calypsi cc65816, comparing emitted
@ -197,27 +205,24 @@ RAM through $FFFF, gaining 8KB of bank-0 space.)
## Yet to come ## Yet to come
- **GS/OS-backed `<stdio.h>` file I/O** — current FS is - **C++ full RTTI + exceptions** — multi-inheritance and virtual
memory-backed (programs `mfsRegister` buffers as files). A base diamonds work; `dynamic_cast` and `throw`/`try`/`catch`
GS/OS backend would let programs see the real ProDOS volume do not. Both need libcxxabi (`__dynamic_cast` walks the
during MAME execution, but needs Tool Locator init in crt0 type_info hierarchy; `__cxa_throw`/_Unwind_*/personality
and a class-1 parm-block dispatch wrapper around $E100A8. routine drive stack unwinding). Reasonable to defer until
someone wants exception-based code on the IIgs.
- **C++ exceptions / RTTI / multiple inheritance with virtual - **Close the size gap to Calypsi further**`scripts/bench.sh`
bases** — only the `-fno-exceptions -fno-rtti` subset is shows clang at ~2.2x Calypsi text size on the microbenchmarks,
supported. `__cxa_throw` etc. would need an unwind ABI on sumOfSquares worst at 6.45x (__mulsi3 dispatch). Calypsi's
this target plus a personality routine. edge is structural: it uses `(sr,s),Y` for stack-relative
indirection where we route through DP $E0 indirect-long for
bank safety. Targeted opportunities: inline 16x16→32
multiply for small operands; widen IMG-slot heuristic so
greedy reaches further before spilling.
- **Close the size gap to Calypsi**`scripts/bench.sh` - **GS/OS file I/O exercised under MAME** — wrappers
shows clang at ~2.2x Calypsi text size on the included (`runtime/include/iigs/gsos.h` + `runtime/src/iigsGsos.s`)
microbenchmarks, with sumOfSquares as the worst case (6.45x) compile and link, but the smoke harness can't drive them
due to __mulsi3 dispatch overhead. Targeted improvements: (no ProDOS volume mounted). Validating end-to-end needs a
inline 16x16->32 multiply for small operands; widen the 2img/po/dsk launched as a MAME hard disk plus toolbox init.
IMG slot heuristic so greedy uses them more aggressively;
cycle-time benchmark harness (separate from size).
- **Larger/real-world end-to-end programs** — current real-world
smoke (Game of Life, BST, dispatch, hex dumper, JSON tokenizer)
exercises core idioms. A multi-thousand-line program (e.g.
a small interactive shell, a text editor command loop) would
catch issues no smaller test reaches.

View file

@ -0,0 +1,94 @@
// IIgs GS/OS file I/O wrappers.
//
// GS/OS calls dispatch through $E100A8 with X holding the call number
// and a 16-bit pointer to a class-1 parameter block pushed on the
// stack. The parm block layout is per-call but always begins with a
// pCount field giving the number of parameters used.
//
// These wrappers are STUBS — they construct the parm blocks and call
// the dispatcher, but require the caller to have a real ProDOS volume
// mounted (or equivalent GS/OS volume) for the calls to succeed.
// Without that, the dispatcher returns an error code or hangs.
//
// To use these in MAME smoke tests you'd need:
// - A 2img / po / dsk image containing a ProDOS volume
// - MAME launched with the image as floppy or hard-disk
// - Tool Locator + GS/OS init via iigsToolboxInit()
// None of which the current smoke harness provides — these wrappers
// are infrastructure for a future GS/OS-aware test rig.
//
// Class-1 GS/OS calls (pCount-prefixed):
// $2010 Open
// $2012 Read
// $2013 Write
// $2014 Close
// $2026 GetEOF
// $2027 SetEOF
// $2029 Quit (special — no return)
// See "GS/OS Reference" for the full ~50 calls and parm-block layouts.
#ifndef IIGS_GSOS_H
#define IIGS_GSOS_H
#ifdef __cplusplus
extern "C" {
#endif
// GS/OS string descriptor: 2-byte length + char data (no NUL).
// Use with caller-allocated storage:
// struct { unsigned short len; char text[14]; } pname = { 6, "MYFILE" };
typedef struct {
unsigned short length;
char text[1]; // variable-length; total size = length + 2
} GSString;
// Class-1 Open parm block.
typedef struct {
unsigned short pCount; // 2 (or up to 12 for full options)
unsigned short refNum; // [out] file reference number
void *pathname; // [in] GSString *
// Optional fields (if pCount > 2): requestAccess, resourceNumber,
// accessRequested, optionList, ... — left out for the basic open.
} OpenParm;
// Class-1 Read/Write parm block.
typedef struct {
unsigned short pCount; // 4
unsigned short refNum; // [in] file reference
void *dataBuffer; // [in] in-bank pointer to buffer
unsigned long requestCount; // [in] bytes requested
unsigned long transferCount;// [out] bytes actually transferred
} IORecGS;
// Class-1 Close / GetEOF / SetEOF / etc. — simple refNum-only blocks.
typedef struct {
unsigned short pCount; // 1
unsigned short refNum;
} RefNumRecGS;
typedef struct {
unsigned short pCount; // 2
unsigned short refNum;
unsigned long eof;
} EOFRecGS;
// Open / Read / Write / Close wrappers. Each returns 0 on success or
// a non-zero GS/OS error code (see gsos.h reference for codes). The
// parm block lives on the caller's stack; you set the input fields
// before the call and read output fields after.
//
// Implementation lives in runtime/src/iigsGsos.s — needed because the
// W65816 backend's inline asm can't take memory operands for the
// parm-block address.
extern unsigned short gsosOpen (OpenParm *p);
extern unsigned short gsosRead (IORecGS *p);
extern unsigned short gsosWrite (IORecGS *p);
extern unsigned short gsosClose (RefNumRecGS *p);
extern unsigned short gsosGetEOF (EOFRecGS *p);
extern unsigned short gsosSetEOF (EOFRecGS *p);
#ifdef __cplusplus
}
#endif
#endif // IIGS_GSOS_H

70
runtime/src/iigsGsos.s Normal file
View file

@ -0,0 +1,70 @@
; iigsGsos.s — GS/OS class-1 dispatch wrappers.
;
; Each wrapper takes a 16-bit pointer to a class-1 parm block in A
; (the C ABI). The dispatcher convention is:
; PEA <parm-block-addr-low> ; push 16-bit ptr (low half of 32-bit
; PEA 0 ; long pointer; high half is 0)
; LDX #<call-number>
; JSL $E100A8
; Returns the call status in A (0 = success, non-zero = error code).
;
; All wrappers preserve nothing — the GS/OS dispatcher clobbers A,
; X, Y, P. Each takes the parm-block pointer in A (i16) and pushes
; it as a 32-bit pointer (low half = the in-bank ptr, high half = 0
; for bank-0 parm blocks, which is what we always use).
.text
.globl gsosOpen
.globl gsosRead
.globl gsosWrite
.globl gsosClose
.globl gsosGetEOF
.globl gsosSetEOF
; Common dispatch helper macro: arg in A, call number in X.
; Pushes the 32-bit parm-block pointer, JSLs the dispatcher, returns
; status in A. All wrappers below follow the same shape — copy/paste
; rather than macro because the assembler doesn't have a portable
; macro syntax we rely on.
gsosOpen:
pha ; push parm-block low
pea 0 ; push parm-block high (0 for bank 0)
ldx #0x2010
jsl 0xe100a8
rtl
gsosRead:
pha
pea 0
ldx #0x2012
jsl 0xe100a8
rtl
gsosWrite:
pha
pea 0
ldx #0x2013
jsl 0xe100a8
rtl
gsosClose:
pha
pea 0
ldx #0x2014
jsl 0xe100a8
rtl
gsosGetEOF:
pha
pea 0
ldx #0x2019
jsl 0xe100a8
rtl
gsosSetEOF:
pha
pea 0
ldx #0x2018
jsl 0xe100a8
rtl

152
scripts/benchCycles.sh Executable file
View file

@ -0,0 +1,152 @@
#!/usr/bin/env bash
# benchCycles.sh — measure benchmark cycle counts in MAME.
#
# For each benchmark in benchmarks/, build a wrapper that calls the
# benchmark function in a loop with fixed input, records the IIgs CPU
# cycle counter before/after via MAME's Lua interface, and writes the
# delta to a known memory address. Output is a markdown table: per
# benchmark, the cycles per call.
#
# This is a separate harness from bench.sh (which measures only code
# size). Cycle measurement requires a full MAME run per benchmark
# (~5 seconds each) so don't run on every smoke pass.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BENCH_DIR="$PROJECT_ROOT/benchmarks"
CLANG="$PROJECT_ROOT/tools/llvm-mos-build/bin/clang"
LLVM_MC="$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$PROJECT_ROOT/tools/link816"
oCrt0=$(mktemp --suffix=.o)
oLibgcc=$(mktemp --suffix=.o)
"$LLVM_MC" -arch=w65816 -filetype=obj "$PROJECT_ROOT/runtime/src/crt0.s" -o "$oCrt0"
"$LLVM_MC" -arch=w65816 -filetype=obj "$PROJECT_ROOT/runtime/src/libgcc.s" -o "$oLibgcc"
# Per-benchmark wrapper template. The C wrapper calls each benchmark
# with appropriate inputs, then writes the iteration count and cycle
# delta to bank 2. We use clock() (VBL counter, 60 Hz) as a coarse
# timer — enough to compare relative speeds.
benchInputs() {
case "$1" in
sumOfSquares) echo 'sumOfSquares(50)';;
fib) echo 'fib(10)';;
strcpy) echo 'mystrcpy(dst, "hello world!")';;
memcmp) echo 'mymemcmp("hello", "hello", 5)';;
bsearch) echo 'bsearch(arr, 8, 5)';;
dotProduct) echo 'dotProduct(va, vb, 4)';;
popcount) echo 'popcount(0x12345678UL)';;
crc32) echo 'crc32((const unsigned char *)"hello", 5)';;
*) echo "/* unknown */";;
esac
}
benchExtern() {
case "$1" in
sumOfSquares) echo 'extern unsigned long sumOfSquares(unsigned short n);';;
fib) echo 'extern unsigned short fib(unsigned short n);';;
strcpy) echo 'extern char *mystrcpy(char *d, const char *s); static char dst[16];';;
memcmp) echo 'extern int mymemcmp(const void *a, const void *b, unsigned int n);';;
bsearch) echo 'extern int bsearch(const int *arr, int n, int key); static const int arr[] = {1,2,3,4,5,6,7,8};';;
dotProduct) echo 'extern long dotProduct(const short *a, const short *b, unsigned int n); static const short va[] = {1,2,3,4}; static const short vb[] = {5,6,7,8};';;
popcount) echo 'extern int popcount(unsigned long x);';;
crc32) echo 'extern unsigned long crc32(const unsigned char *p, unsigned int n);';;
*) echo '';;
esac
}
# Run one benchmark in MAME with cycle measurement.
runOneBench() {
local name="$1"
local extern_decl
local call_expr
extern_decl=$(benchExtern "$name")
call_expr=$(benchInputs "$name")
if [ -z "$extern_decl" ] || [ "$call_expr" = "/* unknown */" ]; then
echo "(no input config)"
return
fi
local cwrap=$(mktemp --suffix=.c)
local owrap=$(mktemp --suffix=.o)
local obench=$(mktemp --suffix=.o)
local bin=$(mktemp --suffix=.bin)
cat > "$cwrap" <<EOF
$extern_decl
__attribute__((noinline)) static void switchToBank2(void) {
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
}
// Read VBL bit + scan-line position from the IIgs Mega II registers.
// \$C02E (VertCnt low) increments at HBL rate (~15.7 kHz), wrapping at
// 256. Higher resolution than the soft-VBL counter at \$E1006B; works
// without ROM IRQ handling.
__attribute__((noinline)) static unsigned char readVbl(void) {
unsigned char r;
__asm__ volatile ("sep #0x20\nlda 0xc02e\nrep #0x20\nand #0x00ff\n"
: "=a"(r) : : "memory");
return r;
}
volatile unsigned long sink;
#define ITERS 100
int main(void) {
// Re-enable IRQs so the IIgs ROM's VBL handler runs and the
// VBL counter at \$E1006B actually ticks. crt0 disables IRQs
// for safety; the cycle bench needs them on for the timer.
__asm__ volatile ("cli\n" ::: "memory");
unsigned char t0 = readVbl();
for (int i = 0; i < ITERS; i++) {
sink = (unsigned long)($call_expr);
}
unsigned char t1 = readVbl();
__asm__ volatile ("sei\n" ::: "memory");
unsigned char dt = t1 - t0; // VBL ticks; wraps at 256
switchToBank2();
*(volatile unsigned short *)0x5000 = (unsigned short)dt;
*(volatile unsigned short *)0x5002 = (unsigned short)(sink & 0xFFFF);
while (1) {}
}
EOF
"$CLANG" --target=w65816 -O2 -ffunction-sections -c "$cwrap" -o "$owrap" 2>/dev/null \
|| { echo "compile-fail"; rm -f "$cwrap" "$owrap"; return; }
"$CLANG" --target=w65816 -O2 -ffunction-sections -c "$BENCH_DIR/$name.c" -o "$obench" 2>/dev/null \
|| { echo "compile-fail"; rm -f "$cwrap" "$owrap" "$obench"; return; }
"$LINK" -o "$bin" --text-base 0x1000 "$oCrt0" "$oLibgcc" "$owrap" "$obench" 2>/dev/null \
|| { echo "link-fail"; rm -f "$cwrap" "$owrap" "$obench" "$bin"; return; }
# Read VBL delta at $025000.
local val
val=$(bash "$PROJECT_ROOT/scripts/runInMame.sh" "$bin" 0x025000 0000 2>&1 \
| grep -oE 'val=0x[0-9a-f]+' | head -1 | sed 's/val=0x//')
rm -f "$cwrap" "$owrap" "$obench" "$bin"
if [ -z "$val" ]; then
echo "(no read)"
else
# \$C02E ticks at HBL rate. IIgs has ~65 cycles per HBL at
# native 2.6 MHz, so each tick ≈ 65 cycles. We ran 100
# iterations, so per-iter cycles ≈ ticks * 65 / 100. For
# very fast benches, 100 iters may not cross a tick — bump
# the constant in the C wrapper if you need finer resolution.
local ticks=$((16#$val))
if [ "$ticks" -eq 0 ]; then
echo "<65 cyc/iter (under timer resolution)"
else
local cycles=$((ticks * 65 / 100))
printf "%d hbl-ticks (~%d cyc/iter)" "$ticks" "$cycles"
fi
fi
}
printf '| Benchmark | Per-iteration cycles |\n'
printf '|-----------|---------------------:|\n'
for src in "$BENCH_DIR"/*.c; do
name=$(basename "$src" .c)
result=$(runOneBench "$name")
printf '| %s | %s |\n' "$name" "$result"
done
rm -f "$oCrt0" "$oLibgcc"

View file

@ -3752,8 +3752,9 @@ EOF
# C++ subset: classes, single inheritance, virtual functions, # C++ subset: classes, single inheritance, virtual functions,
# polymorphism via base-class pointer arrays, virtual dtors. # polymorphism via base-class pointer arrays, virtual dtors.
# Compiled with -fno-exceptions -fno-rtti (the supported subset # Compiled with -fno-exceptions -fno-rtti (the supported subset
# — full RTTI / exceptions / multi-inheritance with virtual # — full RTTI dynamic_cast and exception machinery require
# bases are not supported). # libcxxabi which we don't ship; multi-inheritance and virtual
# base diamonds DO work and are exercised below).
log "check: MAME runs C++ polymorphism (virtuals + single inheritance)" log "check: MAME runs C++ polymorphism (virtuals + single inheritance)"
cppFile="$(mktemp --suffix=.cpp)" cppFile="$(mktemp --suffix=.cpp)"
oCppFile="$(mktemp --suffix=.o)" oCppFile="$(mktemp --suffix=.o)"
@ -3818,6 +3819,108 @@ EOF
fi fi
rm -f "$cppFile" "$oCppFile" "$binCppFile" rm -f "$cppFile" "$oCppFile" "$binCppFile"
# C++ multiple inheritance (no virtual bases): two abstract
# interfaces, one concrete class implementing both. Calls via
# both base pointers must dispatch correctly through their
# respective vtable slices (the second base lives at non-zero
# offset from the object's start; the cast adjusts the ptr).
log "check: MAME runs C++ multiple inheritance (Drawable+Movable)"
cppMiFile="$(mktemp --suffix=.cpp)"
oCppMiFile="$(mktemp --suffix=.o)"
binCppMiFile="$(mktemp --suffix=.bin)"
cat > "$cppMiFile" <<'EOF'
extern "C" __attribute__((noinline)) void switchToBank2(void) {
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
}
class Drawable { public: virtual int draw() const = 0; virtual ~Drawable() {} };
class Movable { public: virtual int move(int dx) const = 0; virtual ~Movable() {} };
class Sprite : public Drawable, public Movable {
int x;
public:
Sprite(int x_) : x(x_) {}
int draw() const override { return x * 100; }
int move(int dx) const override { return x + dx; }
};
extern "C" int main(void) {
Sprite s(7);
Drawable *d = &s;
Movable *m = &s;
int ok = 0;
if (d->draw() == 700) ok |= 1;
if (m->move(5) == 12) ok |= 2;
if (s.draw() == 700) ok |= 4;
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 "$cppMiFile" -o "$oCppMiFile"
"$PROJECT_ROOT/tools/link816" -o "$binCppMiFile" --text-base 0x1000 \
"$oCrt0F" "$oLibgccFile" "$oCppMiFile" \
>/dev/null 2>&1
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCppMiFile" --check \
0x025000=0007 >/dev/null 2>&1; then
die "MAME: C++ multiple inheritance != 0x07 (this-adjustment regression)"
fi
rm -f "$cppMiFile" "$oCppMiFile" "$binCppMiFile"
# C++ virtual base diamond: A and B both virtually inherit from
# Base; Diamond inherits from both. There must be exactly one
# Base subobject in Diamond (`d.b == 42`), and calls through
# either A* or B* must reach Diamond's override (vbase offset
# tables in the vtables resolve the shared subobject).
log "check: MAME runs C++ virtual base diamond"
cppVbFile="$(mktemp --suffix=.cpp)"
oCppVbFile="$(mktemp --suffix=.o)"
binCppVbFile="$(mktemp --suffix=.bin)"
cat > "$cppVbFile" <<'EOF'
extern "C" __attribute__((noinline)) void switchToBank2(void) {
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
}
class Base {
public:
int b;
Base(int x) : b(x) {}
virtual int kind() const = 0;
virtual ~Base() {}
};
class A : public virtual Base { public: A(int x) : Base(x) {} int kind() const override { return 1; } };
class B : public virtual Base { public: B(int x) : Base(x) {} int kind() const override { return 2; } };
class Diamond : public A, public B {
public:
Diamond(int x) : Base(x), A(x), B(x) {}
int kind() const override { return 99; }
};
extern "C" int main(void) {
Diamond d(42);
int ok = 0;
if (d.kind() == 99) ok |= 1;
if (d.b == 42) ok |= 2;
A *a = &d;
B *b = &d;
if (a->kind() == 99) ok |= 4;
if (b->kind() == 99) ok |= 8;
if (a->b == 42) ok |= 0x10;
if (b->b == 42) ok |= 0x20;
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 "$cppVbFile" -o "$oCppVbFile"
"$PROJECT_ROOT/tools/link816" -o "$binCppVbFile" --text-base 0x1000 \
"$oCrt0F" "$oLibgccFile" "$oCppVbFile" \
>/dev/null 2>&1
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binCppVbFile" --check \
0x025000=003f >/dev/null 2>&1; then
die "MAME: C++ virtual base diamond != 0x3F (vbase offset regression)"
fi
rm -f "$cppVbFile" "$oCppVbFile" "$binCppVbFile"
# Real-world: hex dumper using memory-backed file I/O. Reads # Real-world: hex dumper using memory-backed file I/O. Reads
# 16 bytes from a registered "in" file, writes a hex+ASCII # 16 bytes from a registered "in" file, writes a hex+ASCII
# dump to a registered "out" file via fprintf. Verifies the # dump to a registered "out" file via fprintf. Verifies the
@ -3976,6 +4079,162 @@ EOF
fi fi
rm -f "$cJsFile" "$oJsFile" "$binJsFile" rm -f "$cJsFile" "$oJsFile" "$binJsFile"
# Real-world: command-driven hash-table shell. ~250 lines
# of C exercising malloc/free, hash table with chaining,
# tokenizer state machine, mfsRegister + fopen/fprintf,
# and ~10 sequential commands processed against the table.
# Catches integration bugs that microbenchmarks miss.
log "check: MAME runs hash-table shell (script + memory I/O)"
cShFile="$(mktemp --suffix=.c)"
oShFile="$(mktemp --suffix=.o)"
binShFile="$(mktemp --suffix=.bin)"
cat > "$cShFile" <<'EOF'
extern void *malloc(unsigned int n);
extern void free(void *p);
extern unsigned int strlen(const char *s);
extern int strcmp(const char *a, const char *b);
extern char *strchr(const char *s, int c);
extern char *strstr(const char *h, const char *n);
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 fprintf(struct __sFILE *f, const char *fmt, ...);
__attribute__((noinline)) static void switchToBank2(void) {
__asm__ volatile ("sep #0x20\n.byte 0xa9,0x02\npha\nplb\nrep #0x20\n");
}
__attribute__((noinline)) static char *strdup_(const char *s) {
unsigned int n = strlen(s) + 1;
char *r = (char *)malloc(n);
if (r) for (unsigned int i = 0; i < n; i++) r[i] = s[i];
return r;
}
__attribute__((noinline)) static unsigned short hashKey(const char *s) {
unsigned short h = 5381;
while (*s) { h = ((h << 5) + h) + (unsigned char)*s; s++; }
return h;
}
#define HASH_BUCKETS 4
typedef struct Entry { char *key; char *val; struct Entry *next; } Entry;
static Entry *table[HASH_BUCKETS];
static unsigned short totalEntries = 0;
__attribute__((noinline)) static int dbInsert(const char *k, const char *v) {
unsigned short h = hashKey(k) % HASH_BUCKETS;
Entry *e = table[h];
while (e) { if (strcmp(e->key, k) == 0) { free(e->val); e->val = strdup_(v); return 0; } e = e->next; }
Entry *n = (Entry *)malloc(sizeof(Entry));
if (!n) return -1;
n->key = strdup_(k); n->val = strdup_(v); n->next = table[h]; table[h] = n;
totalEntries++; return 1;
}
__attribute__((noinline)) static const char *dbGet(const char *k) {
Entry *e = table[hashKey(k) % HASH_BUCKETS];
while (e) { if (strcmp(e->key, k) == 0) return e->val; e = e->next; }
return (const char *)0;
}
__attribute__((noinline)) static int dbDelete(const char *k) {
unsigned short h = hashKey(k) % HASH_BUCKETS;
Entry *e = table[h]; Entry **pp = &table[h];
while (e) {
if (strcmp(e->key, k) == 0) { *pp = e->next; free(e->key); free(e->val); free(e); totalEntries--; return 1; }
pp = &e->next; e = e->next;
}
return 0;
}
static char *skipWs(char *s) { while (*s == ' ' || *s == '\t') s++; return s; }
__attribute__((noinline)) static char *takeToken(char *s, char **out) {
s = skipWs(s);
if (!*s) { *out = (char *)0; return s; }
*out = s;
while (*s && *s != ' ' && *s != '\t') s++;
if (*s) { *s = 0; s++; }
return s;
}
__attribute__((noinline)) static char *takeRest(char *s) {
s = skipWs(s);
char *end = s + strlen(s);
while (end > s && (end[-1] == ' ' || end[-1] == '\t')) end--;
*end = 0;
return *s ? s : (char *)0;
}
__attribute__((noinline)) static int dispatch(char *line, struct __sFILE *out) {
char *cmd; char *rest = takeToken(line, &cmd);
if (!cmd) return 0;
if (strcmp(cmd, "INSERT") == 0) {
char *key; char *r2 = takeToken(rest, &key); char *val = takeRest(r2);
if (key && val) {
int rc = dbInsert(key, val);
fprintf(out, "INSERT %s = %s -> %s\n", key, val,
rc == 1 ? "added" : (rc == 0 ? "updated" : "fail"));
}
return 1;
}
if (strcmp(cmd, "GET") == 0) {
char *key = takeRest(rest);
if (key) { const char *v = dbGet(key); fprintf(out, "GET %s = %s\n", key, v ? v : "(none)"); }
return 1;
}
if (strcmp(cmd, "DELETE") == 0) {
char *key = takeRest(rest);
if (key) { int rc = dbDelete(key); fprintf(out, "DELETE %s -> %s\n", key, rc ? "removed" : "not found"); }
return 1;
}
if (strcmp(cmd, "COUNT") == 0) { fprintf(out, "COUNT = %u\n", (unsigned)totalEntries); return 1; }
return 0;
}
__attribute__((noinline)) static int runScript(const char *script, struct __sFILE *out) {
int n = 0;
char buf[64];
const char *p = script;
while (*p) {
const char *eol = strchr(p, '\n');
unsigned int len = eol ? (unsigned int)(eol - p) : strlen(p);
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
for (unsigned int i = 0; i < len; i++) buf[i] = p[i];
buf[len] = 0;
n += dispatch(buf, out);
p += len; if (*p == '\n') p++;
}
return n;
}
static char outbuf[1024];
static const char SCRIPT[] =
"INSERT name alice\n" "INSERT age 30\n"
"GET name\n" "INSERT name bob\n" "GET name\n"
"GET nope\n" "COUNT\n" "DELETE age\n"
"DELETE age\n" "COUNT\n";
int main(void) {
mfsRegister("out", outbuf, 0, 1024, 1);
struct __sFILE *out = fopen("out", "w");
int cmds = runScript(SCRIPT, out);
fprintf(out, "ran %d cmds\n", cmds);
fclose(out);
int ok = 0;
if (strstr(outbuf, "INSERT name = alice -> added")) ok |= 0x001;
if (strstr(outbuf, "INSERT name = bob -> updated")) ok |= 0x002;
if (strstr(outbuf, "GET name = bob")) ok |= 0x004;
if (strstr(outbuf, "GET nope = (none)")) ok |= 0x008;
if (strstr(outbuf, "DELETE age -> removed")) ok |= 0x010;
if (strstr(outbuf, "DELETE age -> not found")) ok |= 0x020;
if (strstr(outbuf, "COUNT = 2")) ok |= 0x040;
if (strstr(outbuf, "COUNT = 1")) ok |= 0x080;
if (strstr(outbuf, "ran 10 cmds")) ok |= 0x100;
switchToBank2();
*(volatile unsigned short *)0x5000 = (unsigned short)ok;
while (1) {}
}
EOF
"$CLANG" --target=w65816 -O2 -ffunction-sections -c \
"$cShFile" -o "$oShFile"
"$PROJECT_ROOT/tools/link816" -o "$binShFile" --text-base 0x1000 \
"$oCrt0F" "$oLibcF" "$oExtrasF" "$oSnprintfF" \
"$oSfF" "$oSdF" "$oLibgccFile" "$oShFile" \
>/dev/null 2>&1
if ! bash "$PROJECT_ROOT/scripts/runInMame.sh" "$binShFile" --check \
0x025000=01ff >/dev/null 2>&1; then
die "MAME: hash-table shell bitmap != 0x1FF"
fi
rm -f "$cShFile" "$oShFile" "$binShFile"
rm -f "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \ rm -f "$oLibcF" "$oStrtolF" "$oSnprintfF" "$oQsortF" \
"$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F" "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F"
else else
@ -4078,6 +4337,56 @@ EOF
fi fi
rm -f "$oToolFile" "$oToolboxAsm" "$binTbx" rm -f "$oToolFile" "$oToolboxAsm" "$binTbx"
# iigs/gsos.h — GS/OS class-1 dispatch wrappers for fopen/etc.
# The wrappers can't be exercised in MAME (no ProDOS volume in
# the smoke harness), but we verify the header compiles and
# the .s wrappers link. Build runtime objs locally since the
# MAME-block ones above were already rm'd.
log "check: iigs/gsos.h + iigsGsos.s compile and link"
cGsFile="$(mktemp --suffix=.c)"
oGsFile="$(mktemp --suffix=.o)"
oGsAsm="$(mktemp --suffix=.o)"
oGsLibc="$(mktemp --suffix=.o)"
oGsSnp="$(mktemp --suffix=.o)"
oGsSf="$(mktemp --suffix=.o)"
oGsSd="$(mktemp --suffix=.o)"
binGs="$(mktemp --suffix=.bin)"
cat > "$cGsFile" <<'EOF'
#include <iigs/gsos.h>
int main(void) {
GSString *p = (GSString *)0x4000;
OpenParm op = { 2, 0, p };
if (gsosOpen(&op) != 0) return 1;
static char buf[64];
IORecGS r = { 4, op.refNum, buf, 64, 0 };
if (gsosRead(&r) != 0) return 2;
RefNumRecGS c = { 1, op.refNum };
return gsosClose(&c);
}
EOF
"$CLANG" --target=w65816 -O2 -I"$PROJECT_ROOT/runtime/include" -ffunction-sections \
-c "$cGsFile" -o "$oGsFile"
"$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 -filetype=obj \
"$PROJECT_ROOT/runtime/src/iigsGsos.s" -o "$oGsAsm"
"$CLANG" --target=w65816 -O2 -ffunction-sections \
-c "$PROJECT_ROOT/runtime/src/libc.c" -o "$oGsLibc" 2>/dev/null
"$CLANG" --target=w65816 -O2 -ffunction-sections \
-c "$PROJECT_ROOT/runtime/src/snprintf.c" -o "$oGsSnp" 2>/dev/null
"$CLANG" --target=w65816 -O2 -ffunction-sections \
-c "$PROJECT_ROOT/runtime/src/softFloat.c" -o "$oGsSf" 2>/dev/null
"$CLANG" --target=w65816 -O2 -ffunction-sections \
-c "$PROJECT_ROOT/runtime/src/softDouble.c" -o "$oGsSd" 2>/dev/null
oGsCrt0="$(mktemp --suffix=.o)"
"$PROJECT_ROOT/tools/llvm-mos-build/bin/llvm-mc" -arch=w65816 -filetype=obj \
"$PROJECT_ROOT/runtime/src/crt0.s" -o "$oGsCrt0"
if ! "$PROJECT_ROOT/tools/link816" -o "$binGs" --text-base 0x1000 \
"$oGsCrt0" "$oGsLibc" "$oGsSnp" "$oGsSf" "$oGsSd" \
"$oGsFile" "$oGsAsm" "$oLibgccFile" \
--no-gc-sections 2>&1; then
die "iigs/gsos.h + iigsGsos.s failed to link"
fi
rm -f "$cGsFile" "$oGsFile" "$oGsAsm" "$oGsLibc" "$oGsSnp" "$oGsSf" "$oGsSd" "$oGsCrt0" "$binGs"
# stdint.h / stddef.h / limits.h / inttypes.h: standalone # stdint.h / stddef.h / limits.h / inttypes.h: standalone
# replacements for clang's bundled versions (which try to include # replacements for clang's bundled versions (which try to include
# glibc bits/* headers and break the build). Compile a small # glibc bits/* headers and break the build). Compile a small