From cbae131d0ccde1824b74a79eaf53076a5b98289d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 2 May 2026 19:59:35 -0500 Subject: [PATCH] Checkpoint --- STATUS.md | 83 +++++----- runtime/include/iigs/gsos.h | 94 +++++++++++ runtime/src/iigsGsos.s | 70 ++++++++ scripts/benchCycles.sh | 152 +++++++++++++++++ scripts/smokeTest.sh | 313 +++++++++++++++++++++++++++++++++++- 5 files changed, 671 insertions(+), 41 deletions(-) create mode 100644 runtime/include/iigs/gsos.h create mode 100644 runtime/src/iigsGsos.s create mode 100755 scripts/benchCycles.sh diff --git a/STATUS.md b/STATUS.md index f1130f7..de6f334 100644 --- a/STATUS.md +++ b/STATUS.md @@ -81,11 +81,16 @@ which runs correctly under MAME (apple2gs). abort(), SIGINT/SIGTERM call exit(128+sig), others ignored. - ``: setlocale always returns "C"; localeconv returns a fixed C-locale lconv struct. -- C++ subset: classes, single inheritance, virtual functions, - polymorphism via base-class pointer arrays, virtual dtors. - Compile with `clang++ -fno-exceptions -fno-rtti`. Multiple - inheritance with virtual bases, full RTTI, exceptions are - out of scope. +- C++ subset: classes, single inheritance, multiple inheritance + (Drawable+Movable through one Sprite), virtual base diamond + (A and B virtually derive Base; Diamond inherits from both + with one shared Base subobject), virtual functions, + 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:** @@ -105,21 +110,24 @@ which runs correctly under MAME (apple2gs). image addresses. - `runtime/build.sh` builds crt0, libc, soft-float, soft-double, 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 regressions, link816 bss-base safety + weak-symbol resolution + heap_end-vs-heap_start sanity, iigs/toolbox.h compile + link, - standalone runtime headers, AsmPrinter peepholes (STZ / PEA / - PEI — single-STA, shared-LDA-multi-STA, DPF0-forwarding), - malloc/free coalesce ordering, plus real-world coverage: - Conway's Game of Life blinker (2D loop + neighbour bounds), - binary search tree (recursive struct + malloc), function-pointer - dispatch table (indirect JSL via `__jsl_indir`), memory-backed - file I/O (mfsRegister + fopen/fread/fwrite/fseek/fprintf), C++ - polymorphism (single inheritance + virtual functions), wchar / - signal core APIs, hex dumper writing through fprintf, JSON - tokenizer state machine, scripts/bench.sh size-vs-Calypsi - harness. 100% pass. + iigs/gsos.h compile + link, standalone runtime headers, + AsmPrinter peepholes (STZ / PEA / PEI — single-STA, shared- + LDA-multi-STA, DPF0-forwarding), malloc/free coalesce ordering, + plus real-world coverage: Conway's Game of Life blinker + (2D loop + neighbour bounds), binary search tree (recursive + struct + malloc), function-pointer dispatch table (indirect + JSL via `__jsl_indir`), memory-backed file I/O (mfsRegister + + fopen/fread/fwrite/fseek/fprintf), C++ polymorphism (single + inheritance), C++ multiple inheritance (Drawable+Movable), + C++ virtual base diamond, wchar / signal core APIs, hex dumper + 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 clang (this toolchain) and Calypsi cc65816, comparing emitted @@ -197,27 +205,24 @@ RAM through $FFFF, gaining 8KB of bank-0 space.) ## Yet to come -- **GS/OS-backed `` file I/O** — current FS is - memory-backed (programs `mfsRegister` buffers as files). A - GS/OS backend would let programs see the real ProDOS volume - during MAME execution, but needs Tool Locator init in crt0 - and a class-1 parm-block dispatch wrapper around $E100A8. +- **C++ full RTTI + exceptions** — multi-inheritance and virtual + base diamonds work; `dynamic_cast` and `throw`/`try`/`catch` + do not. Both need libcxxabi (`__dynamic_cast` walks the + type_info hierarchy; `__cxa_throw`/_Unwind_*/personality + routine drive stack unwinding). Reasonable to defer until + someone wants exception-based code on the IIgs. -- **C++ exceptions / RTTI / multiple inheritance with virtual - bases** — only the `-fno-exceptions -fno-rtti` subset is - supported. `__cxa_throw` etc. would need an unwind ABI on - this target plus a personality routine. +- **Close the size gap to Calypsi further** — `scripts/bench.sh` + shows clang at ~2.2x Calypsi text size on the microbenchmarks, + sumOfSquares worst at 6.45x (__mulsi3 dispatch). Calypsi's + 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` - shows clang at ~2.2x Calypsi text size on the included - microbenchmarks, with sumOfSquares as the worst case (6.45x) - due to __mulsi3 dispatch overhead. Targeted improvements: - inline 16x16->32 multiply for small operands; widen the - 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. +- **GS/OS file I/O exercised under MAME** — wrappers + (`runtime/include/iigs/gsos.h` + `runtime/src/iigsGsos.s`) + compile and link, but the smoke harness can't drive them + (no ProDOS volume mounted). Validating end-to-end needs a + 2img/po/dsk launched as a MAME hard disk plus toolbox init. diff --git a/runtime/include/iigs/gsos.h b/runtime/include/iigs/gsos.h new file mode 100644 index 0000000..ea8a4fd --- /dev/null +++ b/runtime/include/iigs/gsos.h @@ -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 diff --git a/runtime/src/iigsGsos.s b/runtime/src/iigsGsos.s new file mode 100644 index 0000000..bf40129 --- /dev/null +++ b/runtime/src/iigsGsos.s @@ -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 ; push 16-bit ptr (low half of 32-bit +; PEA 0 ; long pointer; high half is 0) +; LDX # +; 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 diff --git a/scripts/benchCycles.sh b/scripts/benchCycles.sh new file mode 100755 index 0000000..ead9809 --- /dev/null +++ b/scripts/benchCycles.sh @@ -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" </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" diff --git a/scripts/smokeTest.sh b/scripts/smokeTest.sh index a8f54c9..75ac5ae 100755 --- a/scripts/smokeTest.sh +++ b/scripts/smokeTest.sh @@ -3752,8 +3752,9 @@ EOF # 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). + # — full RTTI dynamic_cast and exception machinery require + # 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)" cppFile="$(mktemp --suffix=.cpp)" oCppFile="$(mktemp --suffix=.o)" @@ -3818,6 +3819,108 @@ EOF fi 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 # 16 bytes from a registered "in" file, writes a hex+ASCII # dump to a registered "out" file via fprintf. Verifies the @@ -3976,6 +4079,162 @@ EOF fi 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" \ "$oExtrasF" "$oStrtokF" "$oMathF" "$oSfF" "$oSdF" "$oCrt0F" else @@ -4078,6 +4337,56 @@ EOF fi 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 +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 # replacements for clang's bundled versions (which try to include # glibc bits/* headers and break the build). Compile a small