// Minimal libc for the W65816 backend.  Provides:
//   string.h: memcpy, memset, memmove, memcmp, strlen, strcpy, strcmp,
//             strncpy, strncmp, strchr, strrchr
//   ctype.h:  isdigit, isalpha, isalnum, isspace, isupper, islower,
//             toupper, tolower, isxdigit, isprint, ispunct
//   stdlib.h: abs, labs, atoi
//
// All functions are straightforward implementations using only
// integer ops.  Each is short enough that internal conditional
// branches stay within 8-bit PCREL reach.
//
// Output goes (eventually) through a putchar stub that targets a
// memory-mapped IO port or a MAME-debug Lua hook; for now putchar
// is provided as a weak stub that does nothing.

typedef unsigned long  size_t;
typedef int            ssize_t;
typedef unsigned char  u8;
typedef unsigned short u16;

// GS/OS class-1 file-call hooks.  Resolved at link time by the
// iigsGsos.s wrappers (which themselves dispatch through $E100A8).
// Declared inline here to avoid pulling iigs/gsos.h's full type
// surface into libc.c.  The parm-block types are local matches of
// iigs/gsos.h's structs — kept layout-equivalent so callers in
// iigs/gsos.h can interoperate.
typedef struct {
    u16    pCount;
    u16    refNum;
    void  *pathname;
    u16    requestAccess;
} __GsosOpenParm;
typedef struct {
    u16            pCount;
    u16            refNum;
    void          *dataBuffer;
    unsigned long  requestCount;
    unsigned long  transferCount;
} __GsosIORecGS;
typedef struct {
    u16 pCount;
    u16 refNum;
} __GsosRefNumRecGS;
typedef struct {
    u16            pCount;
    u16            refNum;
    unsigned long  eof;
} __GsosEOFRecGS;
typedef struct {
    u16            pCount;
    u16            refNum;
    unsigned long  position;
} __GsosMarkRecGS;
// Weak so programs that never call into the GS/OS file backend don't
// drag iigsGsos.o into the link.  fopen guards GSOS path on a NULL
// check (see __gsosAvailable below).
extern u16 gsosOpen   (__GsosOpenParm    *p) __attribute__((weak));
extern u16 gsosRead   (__GsosIORecGS     *p) __attribute__((weak));
extern u16 gsosWrite  (__GsosIORecGS     *p) __attribute__((weak));
extern u16 gsosClose  (__GsosRefNumRecGS *p) __attribute__((weak));
extern u16 gsosGetEOF (__GsosEOFRecGS    *p) __attribute__((weak));
extern u16 gsosSetEOF (__GsosEOFRecGS    *p) __attribute__((weak));
extern u16 gsosSetMark(__GsosMarkRecGS   *p) __attribute__((weak));
extern u16 gsosGetMark(__GsosMarkRecGS   *p) __attribute__((weak));

static int __gsosAvailable(void) {
    // gsosOpen is the entry point — if iigsGsos.o is linked, all the
    // wrappers are present (they're all in one .s file).
    return gsosOpen != (u16 (*)(__GsosOpenParm *))0;
}

// ---- string.h ----

void *memcpy(void *dst, const void *src, size_t n) {
    char *d = (char *)dst;
    const char *s = (const char *)src;
    while (n--) *d++ = *s++;
    return dst;
}

void *memmove(void *dst, const void *src, size_t n) {
    char *d = (char *)dst;
    const char *s = (const char *)src;
    if (d < s) {
        while (n--) *d++ = *s++;
    } else {
        d += n; s += n;
        while (n--) *--d = *--s;
    }
    return dst;
}

void *memset(void *dst, int c, size_t n) {
    char *d = (char *)dst;
    while (n--) *d++ = (char)c;
    return dst;
}

int memcmp(const void *a, const void *b, size_t n) {
    const u8 *p = (const u8 *)a;
    const u8 *q = (const u8 *)b;
    while (n--) {
        if (*p != *q) return *p - *q;
        p++; q++;
    }
    return 0;
}

size_t strlen(const char *s) {
    size_t n = 0;
    while (*s++) n++;
    return n;
}

char *strcpy(char *dst, const char *src) {
    char *d = dst;
    while ((*d++ = *src++)) {}
    return dst;
}

char *strncpy(char *dst, const char *src, size_t n) {
    char *d = dst;
    while (n && (*d = *src)) { d++; src++; n--; }
    while (n--) *d++ = 0;
    return dst;
}

int strcmp(const char *a, const char *b) {
    while (*a && *a == *b) { a++; b++; }
    return (u8)*a - (u8)*b;
}

int strncmp(const char *a, const char *b, size_t n) {
    while (n && *a && *a == *b) { a++; b++; n--; }
    if (!n) return 0;
    return (u8)*a - (u8)*b;
}

char *strchr(const char *s, int c) {
    while (*s) {
        if (*s == (char)c) return (char *)s;
        s++;
    }
    if ((char)c == 0) return (char *)s;
    return 0;
}

char *strrchr(const char *s, int c) {
    const char *r = 0;
    while (*s) {
        if (*s == (char)c) r = s;
        s++;
    }
    if ((char)c == 0) return (char *)s;
    return (char *)r;
}

// ---- ctype.h ----

int isdigit(int c) { return c >= '0' && c <= '9'; }
int isupper(int c) { return c >= 'A' && c <= 'Z'; }
int islower(int c) { return c >= 'a' && c <= 'z'; }
int isalpha(int c) { return isupper(c) || islower(c); }
int isalnum(int c) { return isalpha(c) || isdigit(c); }
int isspace(int c) {
    return c == ' ' || c == '\t' || c == '\n' ||
           c == '\r' || c == '\v' || c == '\f';
}
int isxdigit(int c) {
    return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
int isprint(int c)  { return c >= 0x20 && c < 0x7f; }
int ispunct(int c)  { return isprint(c) && !isalnum(c) && c != ' '; }
int iscntrl(int c)  { return (c >= 0 && c < 0x20) || c == 0x7f; }
int isgraph(int c)  { return isprint(c) && c != ' '; }
int isblank(int c)  { return c == ' ' || c == '\t'; }

int toupper(int c) { return islower(c) ? c - 32 : c; }
int tolower(int c) { return isupper(c) ? c + 32 : c; }

// ---- stdlib.h ----

int abs(int n)        { return n < 0 ? -n : n; }
long labs(long n)     { return n < 0 ? -n : n; }

// div/ldiv/lldiv: return both quotient and remainder in one struct.
// Useful for code that wants a single libcall instead of paired / and %.
// Per C99: quot is integer division truncated toward zero; rem has the
// same sign as the numerator.

typedef struct { int       quot, rem; } div_t;
typedef struct { long      quot, rem; } ldiv_t;
typedef struct { long long quot, rem; } lldiv_t;

div_t   div  (int       n, int       d) { div_t   r; r.quot = n/d; r.rem = n%d; return r; }
ldiv_t  ldiv (long      n, long      d) { ldiv_t  r; r.quot = n/d; r.rem = n%d; return r; }
lldiv_t lldiv(long long n, long long d) { lldiv_t r; r.quot = n/d; r.rem = n%d; return r; }

int atoi(const char *s) {
    int sign = 1;
    while (isspace(*s)) s++;
    if (*s == '-') { sign = -1; s++; }
    else if (*s == '+') { s++; }
    // Parse magnitude as unsigned to dodge signed-overflow UB on
    // values like "32768" (parsing INT_MAX+1 as signed int).
    unsigned int u = 0;
    while (isdigit(*s)) {
        u = u * 10 + (unsigned int)(*s - '0');
        s++;
    }
    return sign < 0 ? (int)(0u - u) : (int)u;
}


// ---- stdio.h essentials (stubs) ----

// putchar: by default, writes to direct-page slot $E2 (which the
// emulator harness can poll).  Real targets (MAME with our IIgs
// glue, or a console emulator) override this with a strong
// definition.  Marked `weak` so users can replace it.
__attribute__((weak))
int putchar(int c) {
    *(volatile char *)0xE2 = (char)c;
    return c;
}

int puts(const char *s) {
    while (*s) { putchar(*s); s++; }
    putchar('\n');
    return 0;
}

// ---- input ----
//
// getchar polls the IIgs hardware keyboard register at $C000.  Bit 7
// is the "key ready" strobe; bits 0..6 are the ASCII code.  Reading
// $C010 clears the strobe so the next keypress can be detected.  This
// blocks until a key is pressed and returns the (7-bit) ASCII value.
// It works in any execution context (raw boot, ProDOS-16, GS/OS app)
// because $C000 is hardware I/O, not toolbox-mediated.
//
// Callers wanting non-blocking input or Event Manager integration
// should call ReadCh/GetNextEvent directly via iigs/toolbox.h.
int getchar(void) {
    volatile unsigned char *kbd  = (volatile unsigned char *)0xC000;
    volatile unsigned char *strb = (volatile unsigned char *)0xC010;
    while (((*kbd) & 0x80) == 0) {
        // Spin until a key is ready.  No yield — single-threaded.
    }
    int c = (*kbd) & 0x7F;
    (void)*strb;  // clear strobe
    return c;
}

// ---- minimal printf ----

// Re-declare va_list / va_* locally rather than including stdarg.h —
// clang's built-ins work directly and skip a header pull-in that's
// otherwise harmless but adds compile time on every TU.
typedef __builtin_va_list  va_list;
#define va_start(ap, last) __builtin_va_start(ap, last)
#define va_arg(ap, ty)     __builtin_va_arg(ap, ty)
#define va_end(ap)         __builtin_va_end(ap)

static void writeUDec(unsigned int n) {
    char buf[6];   // 16-bit: max 5 digits + null
    int i = 0;
    if (n == 0) { putchar('0'); return; }
    while (n > 0) { buf[i++] = '0' + (n % 10); n /= 10; }
    while (i > 0) putchar(buf[--i]);
}

static void writeDec(int n) {
    // For INT_MIN, `-n` overflows signed int (UB).  Negate as unsigned
    // — well-defined (two's-complement wrap), and the magnitude is
    // identical for the print path.
    if (n < 0) { putchar('-'); writeUDec((unsigned int)(0u - (unsigned int)n)); }
    else        writeUDec((unsigned int)n);
}

static void writeULong(unsigned long n) {
    char buf[11];  // 32-bit: max 10 digits + null
    int i = 0;
    if (n == 0) { putchar('0'); return; }
    while (n > 0) { buf[i++] = '0' + (n % 10); n /= 10; }
    while (i > 0) putchar(buf[--i]);
}

static void writeHex(unsigned int n, int width) {
    static const char digits[] = "0123456789abcdef";
    // unsigned int is 16-bit on this target -> at most 4 hex digits.
    // Cap width to that; without it `printf("%08x", ...)` blew past
    // the buf[] tail and corrupted the stack.
    char buf[4];
    if (width > 4) width = 4;
    int i = 0;
    if (n == 0) { buf[i++] = '0'; }
    while (n > 0 && i < 4) { buf[i++] = digits[n & 0xF]; n >>= 4; }
    while (i < width) buf[i++] = '0';
    while (i > 0) putchar(buf[--i]);
}

static void writeStr(const char *s) {
    if (!s) s = "(null)";
    while (*s) { putchar(*s); s++; }
}

// Format-spec handlers used to be marked noinline to keep vprintf's
// main loop small for the long-branch limitation; now W65816BranchExpand
// reliably promotes Bxx to BRL when needed, so the inliner is free to
// merge them when it wants.
static void writeSignedLong(long n) {
    // See writeDec: avoid the signed-overflow UB on LONG_MIN.
    if (n < 0) { putchar('-'); writeULong(0ul - (unsigned long)n); }
    else        writeULong((unsigned long)n);
}

// Minimal %f / %g support.  Uses double soft-float; precision capped
// at 6 fractional digits (the C default).  Doesn't handle Inf/NaN
// specially — prints the integer extraction, which will be 0 for
// non-finite values.  Not IEEE-precise (intermediate truncation in
// the soft-double mul/div), but good enough for typical formatted
// numeric output.
static void writeDouble(double v, int prec) {
    if (prec < 0) prec = 6;
    if (prec > 9) prec = 9;
    // Test the IEEE-754 sign bit (so -0.0 prints with the sign per
    // C99) and avoid the soft-float __ltdf2 comparison, which has
    // historically miscompiled for negative inputs (see snprintf.c
    // banner for the same workaround).
    unsigned long long vbits;
    __builtin_memcpy(&vbits, &v, 8);
    if (vbits & ((unsigned long long)1 << 63)) {
        putchar('-');
        vbits &= ~((unsigned long long)1 << 63);
        __builtin_memcpy(&v, &vbits, 8);
    }
    long ipart = (long)v;
    writeULong((unsigned long)ipart);
    if (prec == 0) return;
    putchar('.');
    double frac = v - (double)ipart;
    // Multiply fraction by 10^prec, then print as integer with leading zeros.
    long mul = 1;
    for (int i = 0; i < prec; i++) mul *= 10;
    long fdigits = (long)(frac * (double)mul);
    if (fdigits < 0) fdigits = -fdigits;
    char buf[10];
    int n = 0;
    long scale = mul / 10;
    while (n < prec) {
        if (scale == 0) scale = 1;
        long d = fdigits / scale;
        buf[n++] = '0' + (char)(d % 10);
        scale /= 10;
        if (scale == 0) break;
    }
    while (n < prec) buf[n++] = '0';
    for (int i = 0; i < n; i++) putchar(buf[i]);
}

int vprintf(const char *fmt, va_list ap) {
    int count = 0;
    while (*fmt) {
        char c = *fmt++;
        if (c != '%') { putchar(c); count++; continue; }
        // Optional width (honoured for %x and %f).
        int width = 0;
        while (*fmt >= '0' && *fmt <= '9') {
            width = width * 10 + (*fmt - '0');
            fmt++;
        }
        // Optional precision (.N) — used by %f.
        int prec = -1;
        if (*fmt == '.') {
            fmt++;
            prec = 0;
            while (*fmt >= '0' && *fmt <= '9') {
                prec = prec * 10 + (*fmt - '0');
                fmt++;
            }
        }
        int isLong = 0;
        if (*fmt == 'l') { isLong = 1; fmt++; }
        char spec = *fmt++;
        if (spec == 'd' || spec == 'i') {
            if (isLong) writeSignedLong(va_arg(ap, long));
            else        writeDec(va_arg(ap, int));
        } else if (spec == 'u') {
            if (isLong) writeULong(va_arg(ap, unsigned long));
            else        writeUDec(va_arg(ap, unsigned int));
        } else if (spec == 'x' || spec == 'X') {
            writeHex(va_arg(ap, unsigned int), width);
        } else if (spec == 'c') {
            putchar(va_arg(ap, int));
        } else if (spec == 's') {
            writeStr(va_arg(ap, const char *));
        } else if (spec == 'f' || spec == 'F' ||
                   spec == 'g' || spec == 'G' ||
                   spec == 'e' || spec == 'E') {
            writeDouble(va_arg(ap, double), prec);
        } else if (spec == 'p') {
            putchar('0'); putchar('x');
            writeHex(va_arg(ap, unsigned int), 4);
        } else if (spec == '%') {
            putchar('%');
        } else {
            putchar('%'); putchar(spec);
        }
        count++;
    }
    return count;
}

int printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    int r = vprintf(fmt, ap);
    va_end(ap);
    return r;
}

// ---- additional string.h ----

void *memchr(const void *s, int c, size_t n) {
    const u8 *p = (const u8 *)s;
    while (n--) {
        if (*p == (u8)c) return (void *)p;
        p++;
    }
    return 0;
}

// strstr: index-based scan rather than pointer-increment.
char *strstr(const char *haystack, const char *needle) {
    if (!needle[0]) return (char *)haystack;
    unsigned int i = 0;
    while (haystack[i]) {
        unsigned int j = 0;
        while (needle[j] && haystack[i + j] == needle[j]) j++;
        if (!needle[j]) return (char *)(haystack + i);
        i++;
    }
    return 0;
}

// Forward declarations for strdup/strndup; the actual definitions
// live further down in the file (malloc and strnlen are below).
extern void *malloc(size_t);
extern size_t strnlen(const char *, size_t);

// strdup/strndup — POSIX, super common in real-world code.
// Both allocate via malloc; caller frees.
char *strdup(const char *s) {
    size_t n = strlen(s);
    char *r = (char *)malloc(n + 1);
    if (!r) return 0;
    memcpy(r, s, n + 1);
    return r;
}

char *strndup(const char *s, size_t maxlen) {
    size_t n = strnlen(s, maxlen);
    char *r = (char *)malloc(n + 1);
    if (!r) return 0;
    memcpy(r, s, n);
    r[n] = 0;
    return r;
}

// memccpy — copy until either `n` bytes done OR byte `c` was copied.
// Returns ptr to byte after the copied `c`, or NULL if `c` not found.
void *memccpy(void *dst, const void *src, int c, size_t n) {
    unsigned char *d = (unsigned char *)dst;
    const unsigned char *s = (const unsigned char *)src;
    while (n--) {
        unsigned char v = *s++;
        *d++ = v;
        if (v == (unsigned char)c) return d;
    }
    return 0;
}

// stpcpy — like strcpy but returns ptr to terminating NUL of dst.
char *stpcpy(char *dst, const char *src) {
    while ((*dst = *src) != 0) { dst++; src++; }
    return dst;
}

// stpncpy — like strncpy but returns ptr to terminator (or to dst+n
// if no terminator was copied).
char *stpncpy(char *dst, const char *src, size_t n) {
    char *p = dst;
    while (n && (*p = *src) != 0) { p++; src++; n--; }
    char *end = p;
    while (n--) *p++ = 0;
    return end;
}

// ---- malloc/free — first-fit allocator with coalescing-on-free ----
//
// Heap lives between the static-data top (linker-supplied __heap_start)
// and a soft cap.  Each allocated block is preceded by a 2-byte header
// holding the block's payload size in bytes.  Free blocks add a 2-byte
// "next" pointer after the size, forming a singly-linked free list.
//
// malloc: first-fit walk of the free list; split the chosen block when
// the remainder is large enough to host its own header+next.
// free: insert onto the head of the free list, then coalesce with any
// adjacent free blocks (forward and backward via free-list scan).
//
// The bump fallback (top of heap) is used when the free list has no
// suitable block.

// Linker-supplied weak symbols; fallback to fixed defaults so a static
// link without crt0 still has SOMETHING.
extern char __heap_start[] __attribute__((weak));
extern char __heap_end[]   __attribute__((weak));

#define HEAP_DEFAULT_START  ((char *)0x4000)
#define HEAP_DEFAULT_END    ((char *)0xBF00)

// Heap is bounded to <32KB so the size field stays uint16_t even
// under 32-bit size_t (saves 2 bytes/header).  next-pointer width
// follows the data layout (2 bytes under p:16, 4 under p:32) — bake
// it into FREE_NODE_SZ via sizeof.
typedef struct FreeBlk {
    u16              size;       // payload size, NOT including header
    struct FreeBlk  *next;       // valid only while in the free list
} FreeBlk;

#define HDR_SZ          ((size_t)sizeof(u16))
#define FREE_NODE_SZ    ((size_t)(sizeof(u16) + sizeof(struct FreeBlk *)))
#define MIN_SPLIT       ((size_t)(FREE_NODE_SZ + 2))

static FreeBlk      *freeList = (FreeBlk *)0;
static char         *bumpPtr  = (char *)0;
static char         *heapEnd  = (char *)0;
// Use the bumpPtr nonzero-ness as the "initialized" flag — sidesteps
// an i1-narrowing isel bug on a dedicated bool flag.
static void mallocInitOnce(void) {
    if (bumpPtr) return;
    bumpPtr = __heap_start ? __heap_start : HEAP_DEFAULT_START;
    heapEnd = __heap_end   ? __heap_end   : HEAP_DEFAULT_END;
    freeList = (FreeBlk *)0;
}

void *malloc(size_t n0) {
    mallocInitOnce();
    // Heap ceiling is ~32KB so anything > 0x7FF0 is unsatisfiable.
    if (n0 > (size_t)0x7FF0) return (void *)0;
    // Round up to 2-byte alignment, with a minimum of FREE_NODE_SZ-HDR_SZ.
    // Keep this in 16-bit arithmetic — the 0x7FF0 cap above guarantees the
    // value fits.  Going through `unsigned long` here triggers an i32 umax
    // pattern that our backend currently miscompiles; staying 16-bit dodges
    // that path entirely.
    u16 n = (u16)n0;
    if (n == 0) n = 1;
    n = (u16)((n + 1) & ~(u16)1);
    if (n < (u16)(FREE_NODE_SZ - HDR_SZ))
        n = (u16)(FREE_NODE_SZ - HDR_SZ);
    // First-fit on free list.
    FreeBlk **link = &freeList;
    FreeBlk  *cur  = freeList;
    while (cur) {
        if (cur->size >= n) {
            // Split if there's room for a separate free block.
            if (cur->size >= n + MIN_SPLIT) {
                u16 rem = (u16)(cur->size - n - HDR_SZ);
                FreeBlk *tail = (FreeBlk *)((char *)cur + HDR_SZ + n);
                tail->size = rem;
                tail->next = cur->next;
                cur->size  = (u16)n;
                *link = tail;
            } else {
                *link = cur->next;
            }
            return (char *)cur + HDR_SZ;
        }
        link = &cur->next;
        cur  = cur->next;
    }
    // Bump-allocate from the high end.
    char *p = bumpPtr;
    if (p + HDR_SZ + n > heapEnd) return (void *)0;
    *(u16 *)p = (u16)n;
    bumpPtr = p + HDR_SZ + n;
    return p + HDR_SZ;
}

void free(void *p) {
    if (!p) return;
    FreeBlk *blk = (FreeBlk *)((char *)p - HDR_SZ);
    blk->next = freeList;
    freeList = blk;
    // Coalesce: walk the free list and merge adjacent blocks.  Outer
    // loop tracks a's predecessor (a_link) so we can excise `a` when
    // it gets absorbed into a lower-address neighbour.  Without that,
    // an `aEnd == b` from b's perspective (i.e. b precedes a in
    // memory) would extend b but leave a in the list — a future malloc
    // could then hand out a's range as a "free" block while the
    // expanded b overlaps it.  O(n^2) in the worst case; n is small.
    FreeBlk **a_link = &freeList;
    FreeBlk  *a      = freeList;
    while (a) {
        int a_absorbed = 0;
        FreeBlk **link = &a->next;
        FreeBlk  *b    = a->next;
        while (b) {
            char *aEnd = (char *)a + HDR_SZ + a->size;
            char *bEnd = (char *)b + HDR_SZ + b->size;
            if (aEnd == (char *)b) {
                // a immediately precedes b — extend a, drop b.
                a->size = (u16)(a->size + HDR_SZ + b->size);
                *link = b->next;
                b = *link;
                continue;
            }
            if (bEnd == (char *)a) {
                // b immediately precedes a — extend b, drop a from
                // the outer list.  We can't continue the inner walk
                // (a is gone), so break out and let the outer loop
                // restart from a's successor.
                b->size = (u16)(b->size + HDR_SZ + a->size);
                *a_link = a->next;
                a_absorbed = 1;
                break;
            }
            link = &b->next;
            b    = b->next;
        }
        if (a_absorbed) {
            a = *a_link;  // already advanced by the excise
        } else {
            a_link = &a->next;
            a      = a->next;
        }
    }
}

void *calloc(size_t nmemb, size_t size) {
    // size_t is 16-bit on this target; nmemb*size can overflow and
    // wrap to a small value (e.g. calloc(65536, 1) -> 0 -> 2-byte
    // alloc), then the caller writes way past the returned region.
    // Bail when the multiplication would overflow.
    if (size != 0 && nmemb > (size_t)0xFFFF / size) return (void *)0;
    size_t total = nmemb * size;
    void *p = malloc(total);
    if (p) memset(p, 0, total);
    return p;
}

void *realloc(void *ptr, size_t n) {
    if (!ptr) return malloc(n);
    if (n == 0) { free(ptr); return (void *)0; }
    size_t old = *(u16 *)((char *)ptr - HDR_SZ);
    if (n <= old) return ptr;
    void *q = malloc(n);
    if (!q) return (void *)0;
    memcpy(q, ptr, old);
    free(ptr);
    return q;
}

// ---- atexit / exit ----
//
// Standard exit() halts via BRK after running any registered atexit
// handler.  Programs running under the IIgs runtime typically would
// call back into GS/OS Quit; here we just wedge the CPU.  Single-slot
// atexit (the storage and registration function are below).

typedef void (*AtexitFn)(void);
static AtexitFn __atexitFn = (AtexitFn)0;

void exit(int code) {
    (void)code;
    // C99 7.20.4.3: exit() must invoke registered atexit handlers in
    // reverse-registration order before terminating.
    if (__atexitFn) {
        AtexitFn fn = __atexitFn;
        __atexitFn = (AtexitFn)0;   // prevent re-entry if fn calls exit
        fn();
    }
    // BRK $00 — halts a 65816 in BRK, MAME's debugger catches.
    __asm__ volatile (".byte 0x00, 0x00");
    while (1) {}  // unreachable
}

// ---- errno ----
//
// Single global errno cell.  Library functions that want to report a
// failure code write here.  The `errno` macro in <errno.h> expands to
// `(*__errno_location())` — we provide that for source compatibility,
// but most code can just touch `errno` directly.
int errno = 0;
int *__errno_location(void) { return &errno; }

char *strerror(int err) {
    switch (err) {
    case 0:  return (char *)"Success";
    case 1:  return (char *)"Operation not permitted";
    case 2:  return (char *)"No such file or directory";
    case 3:  return (char *)"No such process";
    case 4:  return (char *)"Interrupted system call";
    case 5:  return (char *)"Input/output error";
    case 6:  return (char *)"No such device or address";
    case 7:  return (char *)"Argument list too long";
    case 8:  return (char *)"Exec format error";
    case 9:  return (char *)"Bad file descriptor";
    case 10: return (char *)"No child processes";
    case 11: return (char *)"Resource temporarily unavailable";
    case 12: return (char *)"Out of memory";
    case 13: return (char *)"Permission denied";
    case 14: return (char *)"Bad address";
    case 16: return (char *)"Device or resource busy";
    case 17: return (char *)"File exists";
    case 18: return (char *)"Cross-device link";
    case 19: return (char *)"No such device";
    case 20: return (char *)"Not a directory";
    case 21: return (char *)"Is a directory";
    case 22: return (char *)"Invalid argument";
    case 23: return (char *)"Too many open files in system";
    case 24: return (char *)"Too many open files";
    case 25: return (char *)"Inappropriate I/O control operation";
    case 26: return (char *)"Text file busy";
    case 27: return (char *)"File too large";
    case 28: return (char *)"No space left on device";
    case 29: return (char *)"Illegal seek";
    case 30: return (char *)"Read-only file system";
    case 31: return (char *)"Too many links";
    case 32: return (char *)"Broken pipe";
    case 33: return (char *)"Numerical argument out of domain";
    case 34: return (char *)"Numerical result out of range";
    case 36: return (char *)"File name too long";
    case 38: return (char *)"Function not implemented";
    case 39: return (char *)"Directory not empty";
    case 40: return (char *)"Too many levels of symbolic links";
    case 84: return (char *)"Invalid or incomplete multibyte or wide character";
    default: return (char *)"Unknown error";
    }
}

// perror — write `prefix: errno-string\n` to stderr.  Common pattern in
// portable programs that report I/O failures.
void perror(const char *prefix) {
    if (prefix && *prefix) {
        const char *p = prefix;
        while (*p) { putchar(*p); p++; }
        putchar(':');
        putchar(' ');
    }
    const char *m = strerror(errno);
    while (*m) { putchar(*m); m++; }
    putchar('\n');
}

// ---- time.h ----
//
// time() reads the IIgs RTC via ReadTimeHex (Misc Tool $0D03) and
// converts the broken-down date/time to seconds since 1970-01-01.
// Requires `iigsToolboxInit()` to have run at least once — without
// the Tool Locator initialised, JSL $E10000 crashes.  Programs
// that need real time() should call iigsToolboxInit() early from
// main; otherwise time() returns 0 (no crash, but no clock).
//
// clock() reads the IIgs vertical-blank counter at $00/E1/006B (1
// byte that increments every VBL ~= 60 Hz) via inline asm with a
// 24-bit absolute load — works with or without toolbox init since
// the VBL counter is just a memory location updated by the IRQ
// handler.  Wraparound tracked in a u32 static so the counter can
// span days.  CLOCKS_PER_SEC is 60 (defined in time.h).

// Toolbox-init flag, set by iigsToolboxInit().  time() guards on it.
// volatile to dodge the i1-narrowing isel bug on bool flag globals.
static volatile unsigned short __toolboxInited = 0;

void iigsToolboxInit(void) {
    if (__toolboxInited) return;
    __asm__ volatile (
        "rep #0x30\n"
        "ldx #0x0201\n"           // TLStartUp
        "jsl 0xe10000\n"
        "sei\n"                   // re-disable IRQ that the dispatcher may re-enable
        "rep #0x30\n"
        :
        :
        : "a", "x", "y", "memory"
    );
    __toolboxInited = 1;
}

typedef long          time_t;
typedef unsigned long clock_t;

// ReadTimeHex returns 8 bytes via a parameter block: second, minute,
// hour, (unused), year-1900, day, month, weekday.  Push a 4-word
// result-area on the stack, JSL X=$0D03, pop the words back into
// DP scratch ($E0..$E7), then memcpy out.  We can't use "=g"
// constraints (W65816 backend rejects memory operands in inline
// asm), so the data path runs through known DP addresses.
//
// "memory" clobber on the asm tells the scheduler we touch arbitrary
// memory, so it can't reorder the asm against the volatile DP reads
// below.  That permits inlining without losing the read ordering.
static void readTimeHex(unsigned char buf[8]) {
    __asm__ volatile (
        "pea 0\n"
        "pea 0\n"
        "pea 0\n"
        "pea 0\n"
        "ldx #0x0D03\n"
        "jsl 0xe10000\n"
        "pla\n"
        "sta 0xe0\n"
        "pla\n"
        "sta 0xe2\n"
        "pla\n"
        "sta 0xe4\n"
        "pla\n"
        "sta 0xe6\n"
        :
        :
        : "a", "x", "y", "memory"
    );
    // Read DP $E0..$E7 via known-good direct page accesses.  We're
    // in M=16 by ABI so each `lda` reads 2 bytes — split into bytes.
    volatile unsigned char *dp = (volatile unsigned char *)0xE0;
    for (int i = 0; i < 8; i++) buf[i] = dp[i];
}

// Days at start of each month (non-leap).
static const unsigned short __monthDays[12] = {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};

static int __isLeap(int y) {
    return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}

time_t time(time_t *t) {
    if (!__toolboxInited) {
        if (t) *t = 0;
        return 0;
    }
    unsigned char b[8];
    readTimeHex(b);
    int sec   = b[0];
    int min   = b[1];
    int hour  = b[2];
    int year  = 1900 + b[4];
    int day   = b[5];
    int month = b[6];
    if (year < 1970 || month > 11) {
        if (t) *t = 0;
        return 0;
    }
    long days = 0;
    for (int y = 1970; y < year; y++) {
        days += __isLeap(y) ? 366 : 365;
    }
    days += __monthDays[month];
    if (month > 1 && __isLeap(year)) days++;
    days += day;
    long secs = days * 86400L + (long)hour * 3600 + (long)min * 60 + sec;
    if (t) *t = secs;
    return secs;
}

// VBL counter at $00/E1/006B (1 byte).  C `*p` deref where p is a
// 16-bit pointer can't reach $E1006B (would truncate to $006B in
// zero page), so we use inline asm with `lda 0xe1006b` (4-byte
// absolute-long, opcode 0xAF).
static unsigned long __vblBase = 0;
static unsigned char __vblPrev = 0;

clock_t clock(void) {
    unsigned char now;
    __asm__ volatile (
        "sep #0x20\n"
        "lda 0xe1006b\n"     // 24-bit absolute
        "rep #0x20\n"
        "and #0x00ff\n"
        : "=a"(now)
        :
        : "memory"
    );
    if (now < __vblPrev) {
        __vblBase += 256;
    }
    __vblPrev = now;
    return (clock_t)(__vblBase + now);
}

// ---- FILE* abstraction (memory-backed FS + GS/OS pass-through) ----
//
// stdin / stdout / stderr are tagged as kind=STDIO and route through
// putchar / fgetc-from-keyboard; opening a regular file allocates a
// FILE slot.  Two backends:
//
//   kind=MEM   — backed by an mfsRegister'd in-memory buffer.  Used by
//                smoke tests that don't have a real disk; staged via
//                mfsRegister(name, ptr, size, cap, writable) at startup.
//   kind=GSOS  — backed by a real GS/OS file.  fopen falls through to
//                gsosOpen for any path not in the mfs table, so callers
//                with a mounted ProDOS volume get true file I/O via
//                the GS/OS class-1 dispatcher (Open/Read/Write/Close/
//                SetMark/GetMark/SetEOF/GetEOF).  Requires a GS/OS-
//                hosted environment; in a bare MAME boot (no ProDOS
//                volume) gsosOpen fails and fopen returns NULL.
//
// FILE-table layout: 8 entries.  Slot 0..2 are stdin/stdout/stderr
// (immutable); 3..7 are user-allocated by fopen.  Each entry has:
//   kind (0=stdin, 1=stdout, 2=stderr, 3=memory, 4=GS/OS)
//   buf  (memory buffer base; unused for GS/OS)
//   size (logical size in bytes; unused for GS/OS — read on demand)
//   cap  (allocated capacity — for write-grow; unused for GS/OS)
//   pos  (current seek position; unused for GS/OS — Mark is authoritative)
//   eof, err flags
//   writable (1 if opened for "w" or "r+" or "a")
//   ungetc holding cell (-1 = empty)
//   refNum (GS/OS file reference; only valid when kind=GSOS)

#define FILE_KIND_STDIN  0
#define FILE_KIND_STDOUT 1
#define FILE_KIND_STDERR 2
#define FILE_KIND_MEM    3
#define FILE_KIND_GSOS   4

typedef struct __sFILE {
    u8           kind;
    u8           writable;
    u8           eof;
    u8           err;
    char        *buf;
    size_t       size;
    size_t       cap;
    size_t       pos;
    int          unget;        // -1 if no pushed-back char
    const char  *path;         // borrowed from caller, NULL for stdio
    unsigned short refNum;     // GS/OS file reference (kind=GSOS only)
} FILE;

#define MFS_MAX_FILES 8
static FILE __mfs[MFS_MAX_FILES] = {
    { FILE_KIND_STDIN,  0, 0, 0, 0, 0, 0, 0, -1, 0, 0 },
    { FILE_KIND_STDOUT, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0 },
    { FILE_KIND_STDERR, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0 },
};

FILE *stdin  = &__mfs[0];
FILE *stdout = &__mfs[1];
FILE *stderr = &__mfs[2];

// Registered "files" available to fopen.  Each registration is
// (path, buf, size, writable).  Order doesn't matter — fopen scans
// linearly.
typedef struct {
    const char *path;
    char       *buf;
    size_t      size;
    size_t      cap;
    u8          writable;
    u8          inUse;
} MfsEntry;

#define MFS_MAX_REG 16
static MfsEntry __mfsReg[MFS_MAX_REG];

// Register a memory region as a named file.  Returns 0 on success,
// -1 if the table is full or a duplicate name exists.  `cap` may be
// larger than `size` to allow appends without reallocation; pass
// cap=size if writes must not grow the file.
int mfsRegister(const char *path, void *buf, size_t size, size_t cap,
                int writable) {
    if (cap < size) cap = size;
    for (int i = 0; i < MFS_MAX_REG; i++) {
        if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0)
            return -1;
    }
    for (int i = 0; i < MFS_MAX_REG; i++) {
        if (!__mfsReg[i].inUse) {
            __mfsReg[i].path     = path;
            __mfsReg[i].buf      = (char *)buf;
            __mfsReg[i].size     = size;
            __mfsReg[i].cap      = cap;
            __mfsReg[i].writable = (u8)(writable != 0);
            __mfsReg[i].inUse    = 1;
            return 0;
        }
    }
    return -1;
}

// Drop a registration.  Returns 0 on success, -1 if not found.
int mfsUnregister(const char *path) {
    for (int i = 0; i < MFS_MAX_REG; i++) {
        if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0) {
            __mfsReg[i].inUse = 0;
            __mfsReg[i].path  = (const char *)0;
            return 0;
        }
    }
    return -1;
}

int fputc(int c, FILE *stream) {
    if (!stream) return -1;
    if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR)
        return putchar(c);
    if (stream->kind == FILE_KIND_MEM) {
        if (!stream->writable) { stream->err = 1; return -1; }
        if (stream->pos >= stream->cap) { stream->err = 1; return -1; }
        stream->buf[stream->pos++] = (char)c;
        if (stream->pos > stream->size) stream->size = stream->pos;
        return (int)(unsigned char)c;
    }
    if (stream->kind == FILE_KIND_GSOS) {
        if (!stream->writable) { stream->err = 1; return -1; }
        unsigned char b = (unsigned char)c;
        __GsosIORecGS r = { 4, stream->refNum, &b, 1, 0 };
        if (gsosWrite(&r) != 0 || r.transferCount != 1) {
            stream->err = 1;
            return -1;
        }
        return (int)b;
    }
    return -1;
}

int fputs(const char *s, FILE *stream) {
    if (!stream || !s) return -1;
    if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) {
        while (*s) { putchar(*s); s++; }
        return 0;
    }
    if (stream->kind == FILE_KIND_MEM || stream->kind == FILE_KIND_GSOS) {
        while (*s) {
            if (fputc(*s, stream) == -1) return -1;
            s++;
        }
        return 0;
    }
    return -1;
}

int fflush(FILE *stream) { (void)stream; return 0; }

int fclose(FILE *stream) {
    if (!stream) return -1;
    // Don't close stdin/stdout/stderr — they're long-lived statics.
    if (stream->kind == FILE_KIND_GSOS) {
        __GsosRefNumRecGS c = { 1, stream->refNum };
        gsosClose(&c);
        stream->kind   = 0;
        stream->refNum = 0;
        stream->path   = (const char *)0;
        return 0;
    }
    if (stream->kind != FILE_KIND_MEM) return 0;
    stream->kind = 0;
    stream->buf  = (char *)0;
    stream->size = 0;
    stream->cap  = 0;
    stream->pos  = 0;
    stream->path = (const char *)0;
    return 0;
}

// Forward decls for routines that live in snprintf.c.
extern int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);

// Forward decl for vfprintf so fprintf can call it.
int vfprintf(FILE *stream, const char *fmt, va_list ap);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

int fprintf(FILE *stream, const char *fmt, ...) {
    va_list ap;
    __builtin_va_start(ap, fmt);
    int r = vfprintf(stream, fmt, ap);
    __builtin_va_end(ap);
    return r;
}

int vfprintf(FILE *stream, const char *fmt, va_list ap) {
    if (!stream) return -1;
    if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR)
        return vprintf(fmt, ap);
    if (stream->kind == FILE_KIND_GSOS) {
        // Format into a stack buffer, then push to GS/OS via fwrite.
        // 256 bytes covers most format-string outputs; longer strings
        // get truncated (caller can break up the format if needed).
        if (!stream->writable) { stream->err = 1; return -1; }
        char tmp[256];
        int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
        if (n < 0) { stream->err = 1; return -1; }
        size_t outLen = ((size_t)n < sizeof(tmp) - 1)
                          ? (size_t)n : sizeof(tmp) - 1;
        size_t w = fwrite(tmp, 1, outLen, stream);
        if (w != outLen) return -1;
        return n;
    }
    if (stream->kind == FILE_KIND_MEM) {
        // Format into the file's tail.  Use the memory buffer that
        // remains as a snprintf target.  Caller is responsible for
        // sizing the file's buffer.
        if (!stream->writable) { stream->err = 1; return -1; }
        size_t remain = (stream->cap > stream->pos)
                            ? stream->cap - stream->pos : 0;
        if (remain == 0) { stream->err = 1; return -1; }
        int n = vsnprintf(stream->buf + stream->pos, remain, fmt, ap);
        if (n < 0) { stream->err = 1; return -1; }
        size_t written = ((size_t)n < remain) ? (size_t)n : remain - 1;
        stream->pos += written;
        if (stream->pos > stream->size) stream->size = stream->pos;
        return n;
    }
    return -1;
}

// ---- assert ----
//
// __assert_fail is what most assert() macros call.  Print a message
// (if we have stderr) and exit.

void __assert_fail(const char *expr, const char *file, unsigned int line,
                   const char *func) {
    fprintf(stderr, "%s:%u: %s: Assertion `%s' failed.\n",
            file, line, func, expr);
    exit(1);
}

// ---- abort ----
void abort(void) {
    exit(127);
}

// ---- atexit (single slot; storage + exit() invocation above) ----
int atexit(AtexitFn fn) {
    if (__atexitFn) return -1;
    __atexitFn = fn;
    return 0;
}

// ---- C99 _Exit + C11 quick_exit / at_quick_exit ----
//
// _Exit terminates without invoking atexit handlers (unlike exit).
// quick_exit terminates after invoking at_quick_exit handlers (a
// separate chain from atexit).  We share the single-slot pattern
// with atexit — single-shot handler, second registration fails.

static AtexitFn __quickFn = (AtexitFn)0;

void _Exit(int code) {
    (void)code;
    __asm__ volatile (".byte 0x00, 0x00");
    while (1) {}  // unreachable
}

void quick_exit(int code) {
    (void)code;
    if (__quickFn) {
        AtexitFn fn = __quickFn;
        __quickFn = (AtexitFn)0;
        fn();
    }
    __asm__ volatile (".byte 0x00, 0x00");
    while (1) {}  // unreachable
}

int at_quick_exit(AtexitFn fn) {
    if (__quickFn) return -1;
    __quickFn = fn;
    return 0;
}

// ---- getenv / system ----
//
// GS/OS has no environment.  getenv always returns NULL.  system
// always returns 0 (no command shell available).  These exist to
// keep portable code compiling.
char *getenv(const char *name) { (void)name; return (char *)0; }
int   system(const char *cmd)  { (void)cmd;  return 0;          }

// ---- File I/O (memory-backed) ----
//
// Backed by mfsRegister'd entries.  Mode strings:
//   "r"  read only
//   "w"  write, truncate to zero on open
//   "a"  write, position at end on open
//   "r+" read+write
//   "w+" read+write, truncate
// Plus optional "b" (no-op since we're memory-backed).
//
// Returns NULL if no registration matches `path` (or the requested
// mode isn't compatible with the registration's writable flag).

static void initFileMem(FILE *f, const MfsEntry *reg, int wantWrite) {
    f->kind = FILE_KIND_MEM;
    f->writable = (u8)(wantWrite ? 1 : 0);
    f->eof = 0;
    f->err = 0;
    f->buf = reg->buf;
    f->size = reg->size;
    f->cap = reg->cap;
    f->pos = 0;
    f->unget = -1;
    f->path = reg->path;
}

// Scratch GSString for fopen's gsosOpen call.  Single static buffer is
// fine — fopen is non-reentrant on a single-threaded target.
static struct {
    u16  length;
    char text[256];
} __gsosPathBuf;

static int __buildGSString(const char *path) {
    size_t n = 0;
    while (path[n] && n < 256) n++;
    if (path[n]) return -1;  // path > 256 chars
    __gsosPathBuf.length = (u16)n;
    for (size_t i = 0; i < n; i++) __gsosPathBuf.text[i] = path[i];
    return 0;
}

FILE *fopen(const char *path, const char *mode) {
    if (!path || !mode) return (FILE *)0;
    int wantWrite = 0;
    int wantRead  = 1;
    int truncate  = 0;
    int append    = 0;
    if (mode[0] == 'r') { wantRead = 1; wantWrite = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
    else if (mode[0] == 'w') { wantWrite = 1; truncate = 1; wantRead = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
    else if (mode[0] == 'a') { wantWrite = 1; append = 1; wantRead = (mode[1] == '+' || (mode[1] == 'b' && mode[2] == '+')); }
    else return (FILE *)0;

    // Locate mfs registration first.  Backwards-compat: any path
    // staged via mfsRegister(path, ...) routes to memory backend.
    MfsEntry *reg = (MfsEntry *)0;
    for (int i = 0; i < MFS_MAX_REG; i++) {
        if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, path) == 0) {
            reg = &__mfsReg[i];
            break;
        }
    }
    if (reg && wantWrite && !reg->writable) return (FILE *)0;

    // Allocate a FILE slot (3..MAX-1 — 0..2 are stdin/out/err).
    FILE *f = (FILE *)0;
    for (int i = 3; i < MFS_MAX_FILES; i++) {
        if (__mfs[i].kind == 0) {
            f = &__mfs[i];
            break;
        }
    }
    if (!f) return (FILE *)0;

    if (reg) {
        initFileMem(f, reg, wantWrite);
        (void)wantRead;
        if (truncate) f->size = 0;
        if (append)   f->pos  = f->size;
        return f;
    }

    // No mfs registration — try GS/OS.  Requires iigsGsos.o linked
    // (weak references; absent in some smoke tests) AND a mounted
    // ProDOS volume + Tool Locator init at run time.  Either missing
    // → NULL.
    if (!__gsosAvailable()) return (FILE *)0;
    if (__buildGSString(path) < 0) return (FILE *)0;
    // pCount=3 covers refNum + pathname + requestAccess.  GS/OS 6.0.2
    // Open ($2010) requires requestAccess to be non-zero for any actual
    // open; passing only pCount=2 (refNum + pathname) leaves the access
    // word indeterminate and the Open silently fails with a "bad access"
    // status.  requestAccess values: $0001 = read, $0002 = write,
    // $0003 = read+write.  We pick read or read+write based on the
    // mode string.  The remaining optional params (resourceNumber,
    // accessMode, fileType, auxType, storageType, createDate, modDate,
    // optionList) are left out — GS/OS treats their absence as
    // "default values".
    u16 access = (u16)(wantWrite ? (wantRead ? 3 : 2) : 1);
    __GsosOpenParm op = { 3, 0, &__gsosPathBuf, access };
    if (gsosOpen(&op) != 0) return (FILE *)0;

    f->kind     = FILE_KIND_GSOS;
    f->writable = (u8)(wantWrite ? 1 : 0);
    f->eof      = 0;
    f->err      = 0;
    f->buf      = (char *)0;
    f->size     = 0;
    f->cap      = 0;
    f->pos      = 0;
    f->unget    = -1;
    f->path     = path;
    f->refNum   = op.refNum;
    if (truncate) {
        // "w" / "w+" — truncate to zero length.
        __GsosEOFRecGS e = { 2, op.refNum, 0 };
        if (gsosSetEOF(&e) != 0) f->err = 1;
    }
    if (append) {
        // "a" / "a+" — position at end-of-file.
        __GsosEOFRecGS e = { 2, op.refNum, 0 };
        if (gsosGetEOF(&e) == 0) {
            __GsosMarkRecGS m = { 2, op.refNum, e.eof };
            gsosSetMark(&m);
        }
    }
    (void)wantRead;
    return f;
}

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
    if (!stream) return 0;
    if (size == 0 || nmemb == 0) return 0;
    // Avoid 32-bit overflow on size * nmemb: cap nmemb so each item
    // (size bytes) fits in remaining 16-bit address space.
    if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
    if (stream->kind == FILE_KIND_GSOS) {
        // Drain unget byte first if present.
        char *out = (char *)ptr;
        unsigned long total = (unsigned long)size * (unsigned long)nmemb;
        unsigned long offset = 0;
        if (stream->unget >= 0 && total > 0) {
            *out++ = (char)stream->unget;
            stream->unget = -1;
            offset = 1;
        }
        if (offset >= total) return offset / size;
        __GsosIORecGS r = {
            4, stream->refNum, out, total - offset, 0
        };
        u16 rc = gsosRead(&r);
        unsigned long got = offset + r.transferCount;
        if (rc != 0 || r.transferCount < total - offset) {
            stream->eof = 1;
            if (rc != 0 && rc != 0x4C) stream->err = 1;  // 0x4C = eofErr
        }
        return (size_t)(got / size);
    }
    if (stream->kind != FILE_KIND_MEM) return 0;
    char *out = (char *)ptr;
    size_t items = 0;
    while (items < nmemb) {
        size_t b;
        // Each item: size bytes.
        for (b = 0; b < size; b++) {
            if (stream->unget >= 0) {
                *out++ = (char)stream->unget;
                stream->unget = -1;
                continue;
            }
            if (stream->pos >= stream->size) {
                stream->eof = 1;
                return items;
            }
            *out++ = stream->buf[stream->pos++];
        }
        items++;
    }
    return items;
}

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
    if (!stream) return 0;
    if (size == 0 || nmemb == 0) return 0;
    // Cap nmemb so each item (size bytes) fits in the address space
    // — avoids 32-bit `size * nmemb` that the i32 multiply path triggers.
    if (nmemb > (size_t)0xFFFE / size) nmemb = (size_t)0xFFFE / size;
    const char *in = (const char *)ptr;
    if (stream->kind == FILE_KIND_STDOUT || stream->kind == FILE_KIND_STDERR) {
        size_t items = 0;
        while (items < nmemb) {
            for (size_t b = 0; b < size; b++) putchar(*in++);
            items++;
        }
        return items;
    }
    if (stream->kind == FILE_KIND_GSOS) {
        if (!stream->writable) { stream->err = 1; return 0; }
        unsigned long total = (unsigned long)size * (unsigned long)nmemb;
        __GsosIORecGS r = { 4, stream->refNum, (void *)in, total, 0 };
        if (gsosWrite(&r) != 0) {
            stream->err = 1;
            return (size_t)(r.transferCount / size);
        }
        return (size_t)(r.transferCount / size);
    }
    if (stream->kind != FILE_KIND_MEM) return 0;
    if (!stream->writable) { stream->err = 1; return 0; }
    size_t items = 0;
    while (items < nmemb) {
        size_t b;
        for (b = 0; b < size; b++) {
            if (stream->pos >= stream->cap) {
                stream->err = 1;
                if (stream->pos > stream->size) stream->size = stream->pos;
                return items;
            }
            stream->buf[stream->pos++] = *in++;
        }
        items++;
    }
    if (stream->pos > stream->size) stream->size = stream->pos;
    return items;
}

#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2

int fseek(FILE *stream, long offset, int whence) {
    if (!stream) return -1;
    if (stream->kind == FILE_KIND_GSOS) {
        long base;
        if (whence == SEEK_SET) {
            base = 0;
        } else if (whence == SEEK_CUR) {
            __GsosMarkRecGS m = { 2, stream->refNum, 0 };
            if (gsosGetMark(&m) != 0) return -1;
            base = (long)m.position;
        } else if (whence == SEEK_END) {
            __GsosEOFRecGS e = { 2, stream->refNum, 0 };
            if (gsosGetEOF(&e) != 0) return -1;
            base = (long)e.eof;
        } else {
            return -1;
        }
        long target = base + offset;
        if (target < 0) return -1;
        __GsosMarkRecGS m = { 2, stream->refNum, (unsigned long)target };
        if (gsosSetMark(&m) != 0) return -1;
        stream->eof = 0;
        stream->unget = -1;
        return 0;
    }
    if (stream->kind != FILE_KIND_MEM) return -1;
    long base;
    if (whence == SEEK_SET)      base = 0;
    else if (whence == SEEK_CUR) base = (long)stream->pos;
    else if (whence == SEEK_END) base = (long)stream->size;
    else return -1;
    long target = base + offset;
    if (target < 0 || target > (long)stream->size) return -1;
    stream->pos = (size_t)target;
    stream->eof = 0;
    stream->unget = -1;
    return 0;
}

long ftell(FILE *stream) {
    if (!stream) return -1L;
    if (stream->kind == FILE_KIND_GSOS) {
        __GsosMarkRecGS m = { 2, stream->refNum, 0 };
        if (gsosGetMark(&m) != 0) return -1L;
        return (long)m.position;
    }
    if (stream->kind != FILE_KIND_MEM) return -1L;
    return (long)stream->pos;
}

int fgetc(FILE *stream) {
    if (!stream) return -1;
    if (stream->unget >= 0) {
        int c = stream->unget;
        stream->unget = -1;
        return c;
    }
    if (stream->kind == FILE_KIND_MEM) {
        if (stream->pos >= stream->size) { stream->eof = 1; return -1; }
        return (int)(unsigned char)stream->buf[stream->pos++];
    }
    if (stream->kind == FILE_KIND_STDIN) return getchar();
    if (stream->kind == FILE_KIND_GSOS) {
        unsigned char b = 0;
        __GsosIORecGS r = { 4, stream->refNum, &b, 1, 0 };
        u16 rc = gsosRead(&r);
        if (rc != 0 || r.transferCount != 1) {
            stream->eof = 1;
            if (rc != 0 && rc != 0x4C) stream->err = 1;
            return -1;
        }
        return (int)b;
    }
    return -1;
}

char *fgets(char *buf, int n, FILE *stream) {
    if (!buf || n <= 0 || !stream) return (char *)0;
    int i = 0;
    while (i < n - 1) {
        int c = fgetc(stream);
        if (c < 0) {
            if (i == 0) return (char *)0;
            break;
        }
        buf[i++] = (char)c;
        if (c == '\n') break;
    }
    buf[i] = 0;
    return buf;
}

int ungetc(int c, FILE *stream) {
    if (!stream || c < 0) return -1;
    if (stream->unget >= 0) return -1;  // only one slot
    stream->unget = c & 0xFF;
    stream->eof = 0;
    return c & 0xFF;
}

int feof(FILE *stream) {
    return stream ? (int)stream->eof : 1;
}

int ferror(FILE *stream) {
    return stream ? (int)stream->err : 0;
}

void clearerr(FILE *stream) {
    if (stream) { stream->eof = 0; stream->err = 0; }
}

// rewind — convenience wrapper: seek to start + clear error/EOF.
void rewind(FILE *stream) {
    if (!stream) return;
    fseek(stream, 0L, 0 /* SEEK_SET */);
    stream->eof = 0;
    stream->err = 0;
}

// fgetpos / fsetpos — thin wrappers over ftell / fseek.  fpos_t holds
// a single long (byte offset) on this target.
int fgetpos(FILE *stream, long *pos) {
    if (!stream || !pos) return -1;
    long t = ftell(stream);
    if (t < 0) return -1;
    *pos = t;
    return 0;
}

int fsetpos(FILE *stream, const long *pos) {
    if (!stream || !pos) return -1;
    return fseek(stream, *pos, 0 /* SEEK_SET */);
}

// setvbuf / setbuf — no-ops in our buffer-less model.  Return 0 to
// indicate success; portable code that checks the return value will
// keep working.
int setvbuf(FILE *stream, char *buf, int mode, unsigned long size) {
    (void)stream; (void)buf; (void)mode; (void)size;
    return 0;
}

void setbuf(FILE *stream, char *buf) {
    (void)stream; (void)buf;
}

// remove / rename — route through mfsUnregister for the memory-backed
// FS.  Plain rename always fails since mfs entries are name-keyed and
// we'd need a rename primitive we don't have.
int mfsUnregister(const char *path);
int remove(const char *path) {
    if (!path) return -1;
    return mfsUnregister(path);
}

int rename(const char *old, const char *neu) {
    (void)old; (void)neu;
    return -1;   // unsupported
}

// tmpfile / tmpnam — return NULL / 0 always.  We have no writable
// temp storage by default.
FILE *tmpfile(void) { return (FILE *)0; }
char *tmpnam(char *s) { (void)s; return (char *)0; }

// ---- locale.h stubs ----
//
// No real locale support — IIgs is single-locale.  setlocale always
// returns "C", localeconv returns a fixed C-locale struct.  These
// are stubs so portable code that calls setlocale("") for diagnostic
// purposes compiles and runs.

struct lconv {
    char *decimal_point;
    char *thousands_sep;
    char *grouping;
    char *int_curr_symbol;
    char *currency_symbol;
    char *mon_decimal_point;
    char *mon_thousands_sep;
    char *mon_grouping;
    char *positive_sign;
    char *negative_sign;
    char  int_frac_digits;
    char  frac_digits;
    char  p_cs_precedes;
    char  p_sep_by_space;
    char  n_cs_precedes;
    char  n_sep_by_space;
    char  p_sign_posn;
    char  n_sign_posn;
};

static struct lconv __c_lconv = {
    (char *)".",  // decimal_point
    (char *)"",   // thousands_sep
    (char *)"",   // grouping
    (char *)"",   // int_curr_symbol
    (char *)"",   // currency_symbol
    (char *)"",   // mon_decimal_point
    (char *)"",   // mon_thousands_sep
    (char *)"",   // mon_grouping
    (char *)"",   // positive_sign
    (char *)"",   // negative_sign
    (char)127,    // int_frac_digits   (CHAR_MAX = "unspecified")
    (char)127,    // frac_digits
    (char)127, (char)127, (char)127, (char)127, (char)127, (char)127,
};

char *setlocale(int category, const char *locale) {
    (void)category; (void)locale;
    return (char *)"C";
}

struct lconv *localeconv(void) {
    return &__c_lconv;
}

// ---- signal.h ----
//
// IIgs has no POSIX-style signal source (no kernel-delivered signals
// from external events), but a small in-process signal table makes
// signal()/raise() work for synchronous diagnostic use: a program
// can install SIGABRT/SIGINT/etc. handlers and abort()-equivalent
// code can raise(SIGABRT) to invoke them.  No async signal delivery.
//
// Table indexed by signal number 0..15; raise() looks up the
// installed handler and calls it.  SIG_DFL falls through to a
// per-signal default (SIGABRT calls abort(); others ignore).

typedef void (*__sighandler_t)(int);
#define _SIG_DFL ((__sighandler_t)0)
#define _SIG_IGN ((__sighandler_t)1)
#define _SIG_ERR ((__sighandler_t)-1)

#define _NSIG 16
static __sighandler_t __sigHandlers[_NSIG];

__sighandler_t signal(int sig, __sighandler_t handler) {
    if (sig < 0 || sig >= _NSIG) return _SIG_ERR;
    __sighandler_t prev = __sigHandlers[sig];
    if (!prev) prev = _SIG_DFL;
    __sigHandlers[sig] = handler;
    return prev;
}

int raise(int sig) {
    if (sig < 0 || sig >= _NSIG) return -1;
    __sighandler_t h = __sigHandlers[sig];
    if (h == _SIG_IGN) return 0;
    if (!h || h == _SIG_DFL) {
        // Default action: SIGABRT -> abort(); SIGTERM/SIGINT -> exit;
        // others -> ignore.
        if (sig == 6) abort();          // SIGABRT
        if (sig == 2 || sig == 15)      // SIGINT, SIGTERM
            exit(128 + sig);
        return 0;
    }
    h(sig);
    return 0;
}


