// 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;

// MUST stay in lock-step with PATH_MAX in runtime/include/limits.h.
// Not pulled via `#include <limits.h>` because libc.c is built standalone
// (the smoke harness compiles it without -I runtime/include, which lets
// clang's own limits.h win the lookup and drag in glibc headers).
#define LIBC_PATH_MAX  256

// 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;
// SetMark ($2016) / SetEOF ($2018) take a positioning base word followed
// by a displacement long -- NOT the bare-position layout that GetMark
// ($2017) / GetEOF ($2019) return.  These calls require pCount=3; the
// Get side keeps the 3-field records above.  base=0 means "displacement
// measured from the start of the file".
typedef struct {
    u16            pCount;
    u16            refNum;
    u16            base;
    unsigned long  displacement;
} __GsosSetPosRecGS;
typedef struct {
    u16            pCount;
    void          *pathname;
    u16            access;
    u16            fileType;
    unsigned long  auxType;
    u16            storageType;
} __GsosCreateParm;
typedef struct {
    u16    pCount;
    void  *pathname;
} __GsosDestroyParm;
typedef struct {
    u16    pCount;
    void  *oldPathname;
    void  *newPathname;
} __GsosChangePathParm;
// GSString-like length-prefixed buffer + max length cap for the OS to
// observe.  Matches `ResultBuf` in iigs/gsos.h byte-for-byte (maxLen,
// then a GSString { length, text[1] }).
typedef struct {
    u16  maxLen;
    u16  length;
    char text[1];     // variable-length tail
} __GsosResultBuf;
typedef struct {
    u16    pCount;       // 2
    u16    prefixNum;
    void  *prefix;       // __GsosResultBuf *
} __GsosPrefixParm;
// Full Get_File_Info parm block.  We only set pCount=4 from realpath()
// (just enough to retrieve storageType), but the full struct is laid
// out so callers needing aux fields can use the same type.
typedef struct {
    u16            pCount;
    void          *pathname;     // GSString *
    u16            access;
    u16            fileType;
    unsigned long  auxType;
    u16            storageType;
    unsigned char  createDate[8];
    unsigned char  modDate[8];
    void          *optionList;
    unsigned long  eof;
    unsigned long  blocksUsed;
    unsigned long  resourceEOF;
    unsigned long  resourceBlocks;
} __GsosFileInfoParm;
typedef struct {
    u16            pCount;
    u16            refNum;
    u16            flags;
    u16            base;
    u16            displacement;
    void          *name;         // __GsosResultBuf *
    u16            entryNum;
    u16            fileType;
    unsigned long  eof;
    unsigned long  blockCount;
    unsigned char  createDate[8];
    unsigned char  modDate[8];
    u16            access;
    unsigned long  auxType;
    u16            fileSysID;
    void          *optionList;
    unsigned long  resourceEOF;
    unsigned long  resourceBlocks;
} __GsosDirEntryParm;
// Weak so programs that never call into the GS/OS file backend don't
// drag iigsGsos.o into the link.  fopen guards GSOS path through
// __gsosAvailable() below.
//
// `retain` + `used` on the weak-extern decl is the LTO survival policy
// (Phase 1.11): under LTO the inliner can decide an undefined weak is
// constant-0/NULL and propagate that through every caller, DCE-ing the
// dispatcher arms entirely.  `used` keeps the compiler from removing
// references to the symbol; `retain` survives linker GC.  In a non-LTO
// build the attributes are no-ops on a declaration (no body to retain).
extern u16 gsosOpen   (__GsosOpenParm    *p) __attribute__((weak, retain, used));
extern u16 gsosRead   (__GsosIORecGS     *p) __attribute__((weak, retain, used));
extern u16 gsosWrite  (__GsosIORecGS     *p) __attribute__((weak, retain, used));
extern u16 gsosClose  (__GsosRefNumRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosGetEOF (__GsosEOFRecGS    *p) __attribute__((weak, retain, used));
extern u16 gsosSetEOF (__GsosSetPosRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosSetMark(__GsosSetPosRecGS *p) __attribute__((weak, retain, used));
extern u16 gsosGetMark(__GsosMarkRecGS   *p) __attribute__((weak, retain, used));
extern u16 gsosCreate    (__GsosCreateParm    *p) __attribute__((weak, retain, used));
extern u16 gsosDestroy   (__GsosDestroyParm   *p) __attribute__((weak, retain, used));
extern u16 gsosChangePath(__GsosChangePathParm *p) __attribute__((weak, retain, used));
extern u16 gsosGetPrefix  (__GsosPrefixParm   *p) __attribute__((weak, retain, used));
extern u16 gsosGetFileInfo(__GsosFileInfoParm *p) __attribute__((weak, retain, used));
extern u16 gsosGetDirEntry(__GsosDirEntryParm *p) __attribute__((weak, retain, used));

// Stub-mode sentinel.  Defined in iigsGsos.s as 1 (real dispatch
// wrappers linked) and in iigsGsosStub.s as 0 (universal-success
// stub linked).  When neither is linked the weak-extern resolves to
// address 0, which we detect via &__gsosIsRealImpl == 0 — otherwise
// loading from address 0 dereferences NULL.  This is the single
// source of truth that distinguishes "real GS/OS available" from
// "stub linked" from "no GS/OS surface at all", so newly-added
// wrappers can refuse to silently lie about success.
extern int __gsosIsRealImpl __attribute__((weak, retain, used));

int __gsosAvailable(void) {
    if (&__gsosIsRealImpl == (int *)0) {
        return 0;
    }
    return __gsosIsRealImpl;
}

// ---- 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.
//
// NB: this is a weak DECLARATION (no definition here), NOT a weak
// default definition.  A weak default defined in this same TU would
// lose to itself: the strong override in libcGno.o gets gc'd / out-
// resolved (verified — `putchar`'s call bound to the local weak $E2
// default and the GNO console went silent).  Keeping it an undefined-
// weak ref forces link-time resolution to whatever strong def is
// linked (libcGno's console putByte under GNO, else nothing -> the
// $E2 fallback below).  The `&__putByte` footgun this creates is
// handled in the backend: W65816AsmPrinter emits `lda #0` (not the PBR
// `lda $BE`) for an external-weak symbol's bank half, and link816 skips
// recording a cRELOC for a sub-text-base (weak-null) target.
extern void __putByte(char c) __attribute__((weak, retain, used));

int putchar(int c) {
    if (__putByte)
        __putByte((char)c);
    else
        *(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.
// Console byte source.  Default polls the IIgs keyboard at $C000.  A
// hosted environment (GNO/ME) provides a strong __getByte that reads
// its console (returns -1 on EOF).  Weak DECLARATION, not a default
// definition — see __putByte for why.
extern int __getByte(void) __attribute__((weak, retain, used));

int getchar(void) {
    if (__getByte)
        return __getByte();
    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)

// vprintf / printf used to dispatch through their own small format
// helpers (writeUDec/writeDec/writeULong/writeHex/writeStr/writeSignedLong/
// writeDouble).  Once vprintf was rewritten to route through vsnprintf
// (so printf and snprintf share one format engine in snprintf.c), the
// helpers became dead weight and were removed.

extern int vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);


// vprintf used to carry its own format dispatcher (subset of vsnprintf's).
// That meant `%lld`, `%zu`, `%n`, flag/width/`*` etc. quietly worked in
// snprintf but NOT in printf — confusing for portable code.  Route
// through vsnprintf via a stack buffer; the buffer is sized to handle
// most lines in one shot.  Single-threaded use only.
int vprintf(const char *fmt, va_list ap) {
    char tmp[256];
    int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
    int emitted = n < (int)sizeof(tmp) ? n : (int)sizeof(tmp) - 1;
    for (int i = 0; i < emitted; i++) putchar(tmp[i]);
    return n;
}

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, retain, used));
extern char __heap_end[]   __attribute__((weak, retain, used));

#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;


// Point the allocator at a fresh [start, end) window: bump pointer at the
// bottom, ceiling at the top, empty free list.  Shared by mallocInitOnce
// (the link-time window) and __heapInitMM (the Memory-Manager-backed
// window) so the three heap-state fields are only ever set in one place.
static void heapSetWindow(char *start, char *end) {
    bumpPtr  = start;
    heapEnd  = end;
    freeList = (FreeBlk *)0;
}


static void mallocInitOnce(void) {
    if (bumpPtr) return;
    heapSetWindow(__heap_start ? __heap_start : HEAP_DEFAULT_START,
                  __heap_end   ? __heap_end   : HEAP_DEFAULT_END);
}

// GS/OS and GNO: back the malloc heap with a locked Memory-Manager handle
// so the heap span is owned by the MM and no later NewHandle (ours or the
// system's) can land on top of live malloc data.  crt0Gsos and crt0Gno both
// call this with our MM user ID in A, before .init_array -- crt0Gsos runs
// MMStartUp first to fetch the ID; crt0Gno reuses the ID GNO hands __start
// (GNO already started the Memory Manager).  Only crt0.s (bare-metal, no
// Memory Manager) does NOT call it, so -ffunction-sections GC drops this
// function -- and its NewHandle reference -- from bare-metal links alone.  A
// weak no-op __heapInitMM in libgcc.s resolves the `jsl` for GS/OS programs
// that don't link libc.o.
//
// This reservation is deliberately eager (done once at startup, before any
// malloc) so the Memory Manager cannot hand the heap span to another
// allocation first; it therefore costs the locked window's worth of RAM for
// the whole process lifetime even if the program never mallocs.  On any
// failure -- NewHandle returns 0, a null master pointer, or a block that
// straddles a bank boundary -- it returns without setting bumpPtr, so
// mallocInitOnce falls back to the link-time [__heap_start,__heap_end)
// window; on that fallback path a tight-memory machine is no worse off than
// before.
extern void *NewHandle(unsigned long size, unsigned short userID, unsigned short attr, void *loc);
extern void  DisposeHandle(void *h);
void __heapInitMM(unsigned short userID) {
    // Idempotency guard: keep the first window that was installed.  Each
    // entry crt (crt0Gsos for GS/OS, crt0Gno for GNO) invokes this once
    // before any malloc, but this keeps a second call -- or a future reorder
    // -- from stomping a heap that is already serving allocations.
    if (bumpPtr) {
        return;
    }
    // Heap size: keep the link-time window when it is in a sane range,
    // else a 32 KB default.  The window is bank-0-bounded, so it never
    // spans more than a single bank.
    unsigned long want = (unsigned long)(void *)__heap_end
                       - (unsigned long)(void *)__heap_start;
    if (want < 0x2000UL || want > 0xC000UL) {
        want = 0x8000UL;                    // 32 KB
    }
    // attr 0x8014 = attrLocked | attrNoCross | attrPage.  Every bit here is
    // set in the proven-good desktop.c DP-block value 0xC015, minus
    // attrFixed and attrBank -- so the MM is free to place the block in any
    // bank and then pin it.  attrLocked keeps the master pointer stable,
    // which malloc relies on (it hands out raw pointers into the block).
    void *h = NewHandle(want, userID, (unsigned short)0x8014, (void *)0);
    if (!h) {
        return;                             // fall back to link-time window
    }
    char *base = *(char **)h;
    if (!base) {
        DisposeHandle(h);
        return;
    }
    // The allocator does flat, intra-bank (16-bit) pointer arithmetic and
    // assumes a single bank; reject (rather than corrupt on) a straddling
    // block.  This also makes correctness independent of the attrNoCross
    // bit guess above.
    unsigned long lo = (unsigned long)(void *)base;
    unsigned long hi = lo + want - 1UL;
    if ((lo & 0xFF0000UL) != (hi & 0xFF0000UL)) {
        DisposeHandle(h);
        return;
    }
    // heapEnd is one-PAST-end.  A block flush against the bank top (last
    // byte 0xXXFFFF) passes the straddle check above (its last byte is
    // in-bank), but base+want is then 0xXX0000 of the NEXT bank -- and since
    // pointer arithmetic does not carry into the bank byte, heapEnd would
    // wrap to the bank base, below bumpPtr, wedging every malloc.  Give up
    // the top byte so the exclusive end stays inside base's bank.
    if (((lo + want) & 0xFFFFUL) == 0UL) {
        want -= 1UL;
    }
    heapSetWindow(base, base + want);
}

// Heap serialization hooks -- weak no-ops (libgcc.s) for single-threaded
// runtimes; libcGno.c overrides them with a GNO binary-semaphore mutex so
// malloc/free are safe across preemptive threads.
extern void __heapLock(void);
extern void __heapUnlock(void);

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, and 16-bit ops are cheaper than i32 on the 65816.  (An
    // earlier comment here blamed an "i32 umax miscompile" for staying
    // 16-bit; that was a misattribution.  The i32 path is correct, verified
    // in MAME; the real over-heap bug was a control-flow live-in staleness
    // bug in W65816BranchExpand, since fixed.)
    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.  __heapLock/__heapUnlock serialize this free-list
    // critical section against concurrent threads (a GNO semaphore mutex under
    // libcGno.c; a no-op single-threaded).
    __heapLock();
    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;
            }
            __heapUnlock();
            return (char *)cur + HDR_SZ;
        }
        link = &cur->next;
        cur  = cur->next;
    }
    // Bump-allocate from the high end.  This heap only serves small
    // allocations: the 0x7FF0 cap at the top of malloc() bounds n, and
    // callers needing big buffers (e.g. a 16 KB sprite scratch) get them
    // straight from the GS/OS Memory Manager, not malloc.  (There is no
    // "halBigAlloc" routing -- an older comment here described one that was
    // never implemented; oversized requests simply hit the 0x7FF0 cap and
    // return NULL.)
    //
    // Ceiling test compares bytes-needed against bytes-remaining rather than
    // the obvious `p + HDR_SZ + n > heapEnd`: that sum can wrap past a bank
    // boundary under the target's intra-bank (16-bit) pointer arithmetic
    // when the heap sits high in a bank (an MM-backed heap can land at
    // $xx8000..$xxFFFF), spuriously passing the test and running off the
    // heap.  heapEnd - p stays in-bank -- __heapInitMM guarantees heapEnd
    // and every p share a bank and heapEnd >= p -- so it is a small
    // non-negative remaining-byte count.  Covered by smoke check #176.
    char *p = bumpPtr;
    if ((size_t)(HDR_SZ + n) > (size_t)(heapEnd - p)) {
        __heapUnlock();
        return (void *)0;
    }
    *(u16 *)p = (u16)n;
    bumpPtr = p + HDR_SZ + n;
    __heapUnlock();
    return p + HDR_SZ;
}

void free(void *p) {
    if (!p) return;
    __heapLock();
    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;
        }
    }
    __heapUnlock();
}

void *calloc(size_t nmemb, size_t size) {
    // size_t is 32-bit, so the multiply itself won't overflow for any
    // realistic input.  The 0xFFFF cap is a "fits in one 64KB bank"
    // sanity check: the heap lives in bank 0 below the IO window, so
    // any single allocation must fit there.  calloc(65536, 1) returns
    // null rather than silently truncating into the IO range.
    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;

// BRK $00 then spin -- halts a 65816 in BRK so MAME's debugger catches
// it; the spin loop guards against the (rare) case where BRK returns.
static void __halt(void) __attribute__((noreturn));
static void __halt(void) {
    __asm__ volatile (".byte 0x00, 0x00");
    while (1) {}
}


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();
    }
    __halt();
}

// ---- 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).
//
// Under crt0Gsos (GS/OS app) and crt0Gno (GNO shell command) the Tool
// Locator is already initialised by the host (the GS/OS Loader before
// __start, or by the GNO kernel which itself calls JSL $E10000), so
// iigsToolboxInit() is effectively a no-op there but harmless to call.
// Under crt0.s (bare metal, no GS/OS), this is the ONLY way to get TL
// up — and the explicit SEI is critical because TLStartUp's internal
// CLI re-enables the ROM VBL IRQ which runs in M=X=8 and would
// corrupt our in-progress M=16 register state.
//
// 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.
static 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);
}

// ---- ETL chrono clock hooks (Phase 5.3 cxxchrono) ----
//
// `etl::chrono::{system,steady,high_resolution}_clock::now()` calls
// these `extern "C"` hooks; each is expected to return the current
// time as a count of the clock's duration::rep units.  We configure
// all three clocks to `duration<int32_t, etl::milli>` via
// runtime/include/c++/etl_profile.h, so the rep is int32_t and the
// unit is milliseconds.
//
// Source of truth is the IIgs $E1:006B VBL counter (60 Hz on NTSC),
// already maintained by clock() above.  Conversion factor:
//   1 VBL tick = 1000/60 ms = 50/3 ms
// We multiply by 50 in u32, then divide by 3.  Intermediate (ticks*50)
// fits in u32 as long as ticks < UINT32_MAX/50 ≈ 85.9M, which at 60 Hz
// is ~16.5 days of wall time — well beyond any smoke or demo run.
// Beyond that the u32 wraps cleanly (unsigned defined-overflow).
//
// `system_clock` and `high_resolution_clock` share the same monotonic
// source; the IIgs has no walltime tick faster than VBL, and no
// monotonic-vs-realtime distinction in hardware.  All three are
// "steady" in the standard sense (never decreases, no leap-second or
// wall-clock adjustment).  The header documents this.
//
// Returning a strictly-i32 value is load-bearing: an i64 return would
// drag __addsi3 / __ashlsi3 / etc. into every now() comparison.
// Static-assert the contract before chrono callers ever see it.
// libc.c is built standalone (no -I runtime/include — see PATH_MAX
// note above), so we don't pull <stdint.h>; assert directly on the
// underlying scalar `long` we hand to the chrono surface.
typedef char __etl_chrono_rep_must_be_i32[
    (sizeof(int) == 2 && sizeof(long) == 4) ? 1 : -1
];


static long __vblToMillis(clock_t ticks) {
    // ticks is unsigned long (32-bit); the (50*ticks)/3 path stays in
    // u32 below ~4 hours of wall time, then wraps cleanly.  Cast to
    // signed long at the end — chrono::steady_clock uses signed rep
    // (`int32_t` on this target == `long`).
    unsigned long ms = (ticks * 50UL) / 3UL;
    return (long)ms;
}


long etl_get_steady_clock(void) {
    return __vblToMillis(clock());
}


long etl_get_high_resolution_clock(void) {
    return __vblToMillis(clock());
}


long etl_get_system_clock(void) {
    return __vblToMillis(clock());
}

// ---- 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

// Console byte sink for stderr.  A hosted environment (GNO/ME) provides
// a strong __putByteErr that targets the stderr stream (GNO fd 3); when
// absent, stderr just shares stdout's sink (the historical behavior).
// Weak DECLARATION, not a default definition — see __putByte for why.
extern void __putByteErr(char c) __attribute__((weak, retain, used));

// Write one byte to the stdout (kind 1) or stderr (kind 2) console
// stream.  Single dispatch point so every stderr write path routes to
// the right backend hook without duplicating the test.
static int putcharStd(int kind, int c) {
    if (kind == FILE_KIND_STDERR && __putByteErr) {
        __putByteErr((char)c);
        return c;
    }
    return putchar(c);
}

typedef struct __sFILE {
    u8           kind;
    u8           writable;
    u8           eof;
    u8           err;
    u8           autoDelete;    // 1 = remove(path) on fclose (tmpfile)
    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; for autoDelete files
                               // points into __tmpNames[slot].
    unsigned short refNum;     // GS/OS file reference (kind=GSOS only)
} FILE;

// Slots 0-2 are stdin/stdout/stderr; 3..MFS_MAX_FILES-1 are usable FILE*
// slots (both GS/OS and memory files draw from this table -- see fopen).
// At 8 that left only 5 concurrent opens, which starved apps that keep
// several files open at once (e.g. AGI holds one FILE* per game VOL.x
// archive). Each slot is a ~30-byte FILE plus a 24-byte tmpfile-name row,
// so 24 slots (21 usable) costs well under 1.5 KB of BSS.
#define MFS_MAX_FILES 24

// Per-FILE-slot tmpfile name storage.  Parallel to __mfs[] so an
// auto-delete FILE can own its name without the caller having to pass
// (and keep alive) a path string.  Only slots 3..MFS_MAX_FILES-1 are
// ever populated by tmpfile(); 0..2 are stdin/stdout/stderr.  Each
// entry is L_tmpnam bytes -- matches stdio.h's macro so portable
// callers passing a buffer of exactly L_tmpnam can pass it to tmpnam()
// and we'll fill in <= that many chars (incl. terminating NUL).
//
// L_tmpnam is 24 here -- big enough for our canonical "/RAM5/Txxxxxxxx.TMP"
// shape (19 chars + NUL = 20) plus headroom for prefix tuning.  Must
// stay in lock-step with the value in <stdio.h>.
#define LIBC_L_TMPNAM  24
static char __tmpNames[MFS_MAX_FILES][LIBC_L_TMPNAM];

static FILE __mfs[MFS_MAX_FILES] = {
    { .kind = FILE_KIND_STDIN,                .unget = -1 },
    { .kind = FILE_KIND_STDOUT, .writable = 1, .unget = -1 },
    { .kind = FILE_KIND_STDERR, .writable = 1, .unget = -1 },
};

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 putcharStd(stream->kind, 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) { putcharStd(stream->kind, *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; }

// Indirect hook for tmpfile-auto-delete on fclose.  When `tmpfile()`
// is called it installs `remove` here; programs that never call
// tmpfile leave this NULL and the entire remove / __renameCopyDelete
// / __isGsosPath / gsosDestroy-wrapper machinery is dead-stripped
// by --gc-sections.  (A direct `if (autoDel) remove(path)` in fclose
// would create a hard static edge fclose -> remove, dragging the
// full file-deletion surface into every link that uses fopen/fclose
// -- a >20 KB cost for programs that never touch temp files.)
typedef int (*__AutoDeleteFn)(const char *path);
// `volatile` is load-bearing: without it the optimizer proves the
// only assignment to __autoDeleteFn is `remove` (from inside tmpfile)
// and inlines a direct call to remove from fclose, defeating the
// dead-stripping that lets non-tmpfile programs avoid pulling the
// full remove/rename/__renameCopyDelete tree into the link.
static __AutoDeleteFn volatile __autoDeleteFn = (__AutoDeleteFn)0;

int fclose(FILE *stream) {
    if (!stream) return -1;
    // Don't close stdin/stdout/stderr — they're long-lived statics.
    u8 autoDel       = stream->autoDelete;
    const char *path = stream->path;
    if (stream->kind == FILE_KIND_GSOS) {
        __GsosRefNumRecGS c = { 1, stream->refNum };
        gsosClose(&c);
        stream->kind   = 0;
        stream->refNum = 0;
        stream->path   = (const char *)0;
        stream->autoDelete = 0;
        if (autoDel && path && __autoDeleteFn) {
            (void)__autoDeleteFn(path);
        }
        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;
    stream->autoDelete = 0;
    if (autoDel && path && __autoDeleteFn) {
        (void)__autoDeleteFn(path);
    }
    return 0;
}

// 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)
        return vprintf(fmt, ap);
    if (stream->kind == FILE_KIND_STDERR) {
        // Route formatted stderr output to the stderr console hook (GNO
        // fd 3) rather than vprintf's stdout putchar.  Format into a
        // stack buffer, then emit byte-by-byte via putcharStd.
        char tmp[256];
        int n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
        if (n < 0) return -1;
        size_t outLen = ((size_t)n < sizeof(tmp) - 1)
                          ? (size_t)n : sizeof(tmp) - 1;
        for (size_t i = 0; i < outLen; i++)
            putcharStd(FILE_KIND_STDERR, tmp[i]);
        return n;
    }
    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 ----
// Weak: a GNO link (libcGno.o) overrides with a raise(SIGABRT)-based abort.
__attribute__((weak))
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;
    __halt();
}

void quick_exit(int code) {
    (void)code;
    if (__quickFn) {
        AtexitFn fn = __quickFn;
        __quickFn = (AtexitFn)0;
        fn();
    }
    __halt();
}

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

// ---- getenv / system ----
//
// Bare GS/OS has no environment.  This WEAK getenv returns NULL; a GNO link
// (libcGno.o) overrides it with a shell-variable-table-backed getenv.  system
// always returns 0 (no command shell available).  These keep portable code
// compiling.
char *getenv(const char *name) __attribute__((weak));
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->autoDelete = 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.  Sized to
// LIBC_PATH_MAX (kept in sync with limits.h's PATH_MAX) so user code
// that bounds-checks against PATH_MAX stays consistent with what fopen
// will accept.
typedef struct __GsosPathBufT {
    u16  length;
    char text[LIBC_PATH_MAX];
} __GsosPathBufT;

static __GsosPathBufT __gsosPathBuf;

static int __fillGSString(__GsosPathBufT *buf, const char *path) {
    size_t n = 0;
    while (path[n] && n < LIBC_PATH_MAX) n++;
    if (path[n]) return -1;  // path > PATH_MAX chars
    buf->length = (u16)n;
    for (size_t i = 0; i < n; i++) buf->text[i] = path[i];
    return 0;
}

static int __buildGSString(const char *path) {
    return __fillGSString(&__gsosPathBuf, path);
}

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);
        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;
    // For write/append modes, Create the file first (GS/OS Open does
    // not create).  A "duplicate filename" ($47) result is fine — the
    // file already exists.  Guarded on the weak gsosCreate so bare
    // builds without it keep the old write-existing-only behavior.
    if (wantWrite && gsosCreate) {
        // access $C3 (destroy/rename/write/read), fileType $04 (TXT),
        // auxType 0, storageType 1 (seedling — GS/OS grows as needed).
        __GsosCreateParm cp = { 5, &__gsosPathBuf, 0xC3, 0x04, 0, 1 };
        (void)gsosCreate(&cp);   // ignore result; Open reports real errors
    }
    // 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->autoDelete = 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.
        __GsosSetPosRecGS e = { 3, op.refNum, 0, 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) {
            __GsosSetPosRecGS m = { 3, op.refNum, 0, e.eof };
            gsosSetMark(&m);
        }
    }
    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;
    // size_t is u32 here, so the multiply itself can't overflow.  The
    // 0xFFFE cap is a "single 64KB bank" limit -- the underlying
    // mem/GSOS backends address by 16-bit offset, so any single fread
    // must fit in one bank.
    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;
    // size_t is u32 here, so the multiply itself can't overflow.  The
    // 0xFFFE cap is a "single 64KB bank" limit -- the underlying
    // mem/GSOS backends address by 16-bit offset, so any single fwrite
    // must fit in one bank.
    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++) putcharStd(stream->kind, *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;
        __GsosSetPosRecGS m = { 3, stream->refNum, 0, (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 / tmpfile / tmpnam — promoted from stubs (Phase 2.3
// of docs/GAP_CLOSURE_PLAN.md).
//
// Layered fallback strategy:
//   1. mfs path (memory-backed FS staged via mfsRegister): no path
//      separator → try mfs first.  remove() → mfsUnregister; rename()
//      → swap mfs registration name when both sides are mfs.
//   2. GS/OS class-1 calls when __gsosAvailable():
//        remove → Destroy ($2002)
//        rename same-dir → ChangePath ($2004)
//        rename cross-dir → Open(src,R) + Create(dst) + chunked
//                           Read/Write loop + Close + Destroy(src).
// The mfs-vs-GS/OS detection is "does the path contain a separator
// (`/` or `:`)?".  Pure-name strings hit mfs; volume-rooted paths hit
// GS/OS.  This matches both ProDOS `/VOL/FILE` and HFS `:Vol:File:`
// conventions without forcing the caller to declare which.

extern int rand(void);

// True when `path` looks like a GS/OS volume path (contains `/` or
// `:`).  Pure-name strings ("greet", "out.tmp") are treated as mfs
// keys; volume-rooted paths route through GS/OS class-1 calls.
static int __isGsosPath(const char *path) {
    if (!path) return 0;
    for (const char *p = path; *p; p++) {
        if (*p == '/' || *p == ':') return 1;
    }
    return 0;
}

// Locate the index of the last path-separator (`/` or `:`).  Returns
// -1 if the path has none.  Used by rename() to decide between the
// same-dir fast path (ChangePath) and the cross-dir copy+delete
// fallback: same-dir == both inputs have identical "parent" substring
// up to and including their last separator.
static int __lastSepIdx(const char *path) {
    int last = -1;
    int i = 0;
    while (path[i]) {
        if (path[i] == '/' || path[i] == ':') last = i;
        i++;
    }
    return last;
}

// True when `a` and `b` share the same parent directory — i.e. the
// substrings up to and including the last separator are identical.
// Both inputs must be GS/OS paths (have at least one separator); a
// pure-name string is treated as "no parent" and matches another
// pure-name string.
static int __sameParentDir(const char *a, const char *b) {
    int la = __lastSepIdx(a);
    int lb = __lastSepIdx(b);
    if (la != lb) return 0;
    if (la < 0) return 1;       // both pure-name
    for (int i = 0; i <= la; i++) {
        if (a[i] != b[i]) return 0;
    }
    return 1;
}

// Second GSString scratch — rename() needs two parm-block path slots
// simultaneously (old+new for ChangePath), and Destroy of the source
// at the end of the cross-dir fallback can reuse __gsosPathBuf for the
// source name.  Keeps the destination name alive across all calls.
static __GsosPathBufT __gsosPathBuf2;

static int __buildGSString2(const char *path) {
    return __fillGSString(&__gsosPathBuf2, path);
}

int remove(const char *path) {
    if (!path) return -1;
    // Try mfs first — backwards-compatible with the staged-buffer FS.
    if (mfsUnregister(path) == 0) return 0;
    // Fall through to GS/OS only when a real dispatcher is linked AND
    // the path looks like a volume path; otherwise honour the mfs
    // miss with -1 (mfsUnregister already set the right answer).
    if (!__isGsosPath(path)) return -1;
    if (!__gsosAvailable() || !gsosDestroy) return -1;
    if (__buildGSString(path) < 0) {
        errno = 36;        // ENAMETOOLONG
        return -1;
    }
    __GsosDestroyParm dp = { 1, &__gsosPathBuf };
    if (gsosDestroy(&dp) != 0) {
        errno = 2;         // ENOENT (path not found / access denied)
        return -1;
    }
    return 0;
}

// Cross-directory copy+delete fallback for rename().  Open both
// paths directly via the GS/OS dispatcher (NOT through fopen — that
// would pressurise the FILE-slot table during rename), stream the
// bytes through a fixed 2KB scratch buffer, close, and Destroy the
// source.  Buffer is intentionally small (2 KB) because the
// per-iteration cost is dominated by the Read+Write JSL overhead,
// not the buffer-byte count -- the smaller buffer trades ~150 cyc
// of dispatch per chunk against several KB of single-bank text
// budget that the rest of the runtime needs.  The plan's 8 KB spec
// was an upper bound on chunking; smaller chunks correctness-equivalent.
//
// Error-recovery sequencing (consolidated into __renameCleanup):
//   - Write fails mid-loop      -> Destroy partial dst + return -1
//   - Final Destroy(source)     -> leave dst in place + return -1 +
//     fails (data preserved)      log "destination written, source
//                                 not removed" to stderr.
//   - Source vanished mid-op    -> best-effort; same as final-Destroy
//                                 failure path (rare under GS/OS).
#define RENAME_COPY_BUF_SZ  2048
static unsigned char __renameCopyBuf[RENAME_COPY_BUF_SZ];

// Shared parm blocks (BSS).  Reused across the open/read/write/close
// flow so we don't pay the per-block init code cost in every error
// branch.  __renameCopyDelete is the only caller; non-rename paths
// use stack-resident parm blocks (smaller scope, less BSS).
static __GsosIORecGS      __rcIORec;
static __GsosRefNumRecGS  __rcRefRec;
static __GsosSetPosRecGS  __rcEofRec;
static __GsosDestroyParm  __rcDestroy;

// Tear-down on any mid-flow failure.  Closes both refs (best-effort)
// and Destroys the partial destination so the on-disk state matches
// "rename never happened".  The source name in __gsosPathBuf may
// already have been overwritten by a Read-side helper, so the caller
// supplies dst once more for the Destroy parm.
static void __renameCleanup(u16 srcRef, u16 dstRef, const char *dst) {
    __rcRefRec.pCount = 1; __rcRefRec.refNum = srcRef; gsosClose(&__rcRefRec);
    __rcRefRec.refNum = dstRef;                        gsosClose(&__rcRefRec);
    if (__buildGSString2(dst) == 0) {
        __rcDestroy.pCount   = 1;
        __rcDestroy.pathname = &__gsosPathBuf2;
        (void)gsosDestroy(&__rcDestroy);
    }
}

static int __renameCopyDelete(const char *src, const char *dst) {
    if (__buildGSString(src) < 0)  { errno = 36; return -1; }
    if (__buildGSString2(dst) < 0) { errno = 36; return -1; }
    __GsosOpenParm srcOpen = { 3, 0, &__gsosPathBuf, 1 };
    if (gsosOpen(&srcOpen) != 0) { errno = 2; return -1; }
    u16 srcRef = srcOpen.refNum;
    __GsosCreateParm cp = { 5, &__gsosPathBuf2, 0xC3, 0x04, 0, 1 };
    (void)gsosCreate(&cp);
    __GsosOpenParm dstOpen = { 3, 0, &__gsosPathBuf2, 3 };
    if (gsosOpen(&dstOpen) != 0) {
        __rcRefRec.pCount = 1; __rcRefRec.refNum = srcRef;
        gsosClose(&__rcRefRec);
        errno = 5;
        return -1;
    }
    u16 dstRef = dstOpen.refNum;
    __rcEofRec.pCount = 3; __rcEofRec.refNum = dstRef; __rcEofRec.base = 0; __rcEofRec.displacement = 0;
    if (gsosSetEOF(&__rcEofRec) != 0) {
        __renameCleanup(srcRef, dstRef, dst);
        errno = 5;
        return -1;
    }
    for (;;) {
        __rcIORec.pCount        = 4;
        __rcIORec.refNum        = srcRef;
        __rcIORec.dataBuffer    = __renameCopyBuf;
        __rcIORec.requestCount  = RENAME_COPY_BUF_SZ;
        __rcIORec.transferCount = 0;
        u16 rc = gsosRead(&__rcIORec);
        if (rc != 0 && rc != 0x4C) {
            __renameCleanup(srcRef, dstRef, dst);
            errno = 5;
            return -1;
        }
        unsigned long got = __rcIORec.transferCount;
        if (got == 0) break;
        __rcIORec.refNum        = dstRef;
        __rcIORec.requestCount  = got;
        __rcIORec.transferCount = 0;
        u16 wrc = gsosWrite(&__rcIORec);
        if (wrc != 0 || __rcIORec.transferCount != got) {
            __renameCleanup(srcRef, dstRef, dst);
            errno = 28;
            return -1;
        }
        if (rc == 0x4C) break;
    }
    __rcRefRec.pCount = 1;
    __rcRefRec.refNum = srcRef; gsosClose(&__rcRefRec);
    __rcRefRec.refNum = dstRef; gsosClose(&__rcRefRec);
    if (__buildGSString(src) < 0) { errno = 36; return -1; }
    __rcDestroy.pCount   = 1;
    __rcDestroy.pathname = &__gsosPathBuf;
    if (gsosDestroy(&__rcDestroy) != 0) {
        const char *msg =
            "rename: destination written, source not removed\n";
        const char *p = msg;
        while (*p) {
            if (__putByteErr) __putByteErr(*p);
            else              putchar(*p);
            p++;
        }
        errno = 5;
        return -1;
    }
    return 0;
}

int rename(const char *old, const char *neu) {
    if (!old || !neu) return -1;
    // Both mfs-name shapes: swap the registration in place.
    int oldIsGsos = __isGsosPath(old);
    int neuIsGsos = __isGsosPath(neu);
    if (!oldIsGsos && !neuIsGsos) {
        for (int i = 0; i < MFS_MAX_REG; i++) {
            if (__mfsReg[i].inUse && strcmp(__mfsReg[i].path, old) == 0) {
                // Refuse if neu is already taken by another entry.
                for (int j = 0; j < MFS_MAX_REG; j++) {
                    if (j != i && __mfsReg[j].inUse
                        && strcmp(__mfsReg[j].path, neu) == 0) {
                        errno = 17;     // EEXIST
                        return -1;
                    }
                }
                __mfsReg[i].path = neu;
                return 0;
            }
        }
        // No mfs entry for `old`; no GS/OS surface to fall back to
        // (no separators in either path).
        errno = 2;
        return -1;
    }
    // GS/OS-path rename.  Both must be GS/OS-shape for a coherent
    // rename — mixing mfs-name and GS/OS-path is rejected up front.
    if (oldIsGsos != neuIsGsos) {
        errno = 18;       // EXDEV (cross-device link)
        return -1;
    }
    if (!__gsosAvailable()) {
        errno = 38;       // ENOSYS
        return -1;
    }
    // Same-dir fast path: ChangePath.
    if (__sameParentDir(old, neu)) {
        if (!gsosChangePath) { errno = 38; return -1; }
        if (__buildGSString(old) < 0)  { errno = 36; return -1; }
        if (__buildGSString2(neu) < 0) { errno = 36; return -1; }
        __GsosChangePathParm cp = { 2, &__gsosPathBuf, &__gsosPathBuf2 };
        if (gsosChangePath(&cp) != 0) {
            errno = 2;
            return -1;
        }
        return 0;
    }
    // Cross-dir fallback: copy+delete.
    if (!gsosOpen || !gsosCreate || !gsosRead || !gsosWrite
        || !gsosClose || !gsosDestroy || !gsosSetEOF) {
        errno = 38;
        return -1;
    }
    return __renameCopyDelete(old, neu);
}

// tmpnam — generate a unique temporary filename.  If `s` is non-NULL,
// fill it (must be L_tmpnam bytes or larger); else return a pointer
// to a static buffer overwritten on each call.  Format:
//   /RAM5/Txxxxxxxx.TMP  (19 chars + NUL = 20)
// The 8 hex chars come from two rand() calls (15 bits each); since
// rand() is seeded from ReadTimeHex via __srandInitFromTime in
// crt0Gsos.s / crt0Gno.s, distinct invocations of the same program
// produce distinct names.  Within a single program the LCG advance
// of `rand() << 16 | rand()` cycles long enough for practical use
// (>2^30 calls before a repeat at the 8-hex-digit resolution).
static char __tmpnamStatic[LIBC_L_TMPNAM];

char *tmpnam(char *s) {
    char *dst = s ? s : __tmpnamStatic;
    // Prefix: "/RAM5/T"
    dst[0]  = '/';
    dst[1]  = 'R';
    dst[2]  = 'A';
    dst[3]  = 'M';
    dst[4]  = '5';
    dst[5]  = '/';
    dst[6]  = 'T';
    // 8 hex digits from 32 bits of entropy.
    unsigned long r1 = (unsigned long)rand();
    unsigned long r2 = (unsigned long)rand();
    unsigned long bits = (r1 << 16) | r2;
    static const char hex[] = "0123456789ABCDEF";
    for (int i = 0; i < 8; i++) {
        dst[7 + i] = hex[(bits >> ((7 - i) * 4)) & 0xF];
    }
    // Suffix: ".TMP\0"  (4 chars + NUL)
    dst[15] = '.';
    dst[16] = 'T';
    dst[17] = 'M';
    dst[18] = 'P';
    dst[19] = 0;
    return dst;
}

FILE *tmpfile(void) {
    // Install the auto-delete hook in fclose so a subsequent fclose
    // of *this* FILE routes through remove().  fclose intentionally
    // refers to the hook only through a function pointer so the
    // remove()/rename()/__renameCopyDelete machinery is dead-stripped
    // from programs that never call tmpfile.
    __autoDeleteFn = remove;
    // Pre-allocate a FILE slot so we can park its owned tmpName
    // before fopen runs (fopen scans for slots itself; we observe the
    // first free index here so the parallel __tmpNames[] entry is
    // the right one).
    int slot = -1;
    for (int i = 3; i < MFS_MAX_FILES; i++) {
        if (__mfs[i].kind == 0) { slot = i; break; }
    }
    if (slot < 0) return (FILE *)0;
    // Build a fresh name in the slot's owned buffer.  Multiple
    // tmpfile() calls in a row each get their own slot-keyed name,
    // so collisions between concurrently-open temp FILEs are
    // structurally impossible (each slot has its own buffer).
    char *nameBuf = __tmpNames[slot];
    (void)tmpnam(nameBuf);
    // Attempt to open the (likely-not-yet-existing) GS/OS path for
    // read+write+truncate.  fopen("w+") routes through gsosCreate
    // first, then Open with r+w access.
    FILE *f = fopen(nameBuf, "w+");
    if (!f) return (FILE *)0;
    // f must be the slot we observed above (fopen scans in the same
    // order).  Set the auto-delete flag and point path at our owned
    // buffer so fclose can route to remove() on close.
    f->autoDelete = 1;
    f->path       = nameBuf;
    return f;
}

// ---- 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];

// Weak: a GNO link (libcGno.o) overrides with a kernel-backed signal() that
// routes real handlers through __sigTrampoline.
__attribute__((weak))
__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;
}

__attribute__((weak))
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;
}


// ---- POSIX file helpers (Phase 3.3) ----
//
// dirname/basename/fnmatch/mkstemp/realpath/glob.  All accept either
// ProDOS-style "/VOL/FILE" or HFS-style ":Vol:File:" paths; the
// separator is auto-detected per call (first one of `/` or `:` seen).
// Pure-name paths (no separator) are treated as basename-equivalent
// inputs.  realpath() and glob() require a real GS/OS dispatcher
// (`__gsosAvailable() == 1`); without one they fail cleanly with
// errno = 38 (ENOSYS) instead of pretending success.

// Auto-detect the separator used by `p`.  Returns '/', ':', or 0 if
// the path is pure-name (no separator).  '/' wins when both appear
// (matches the GS/OS-preferred convention).
static char __pathSep(const char *p) {
    if (!p) return 0;
    int sawColon = 0;
    while (*p) {
        if (*p == '/') return '/';
        if (*p == ':') sawColon = 1;
        p++;
    }
    return sawColon ? ':' : 0;
}

// dirname — return the parent directory portion of `path`.  Writes
// to a static scratch buffer; result valid until the next dirname()
// call.  Mirrors POSIX semantics:
//   "/usr/lib"   -> "/usr"
//   "/usr/"      -> "/"
//   "usr"        -> "."
//   "/"          -> "/"
//   ""           -> "."
// HFS form is symmetrical with `:` as the separator.
static char __dirnameBuf[LIBC_PATH_MAX];
char *dirname(char *path) {
    if (!path || !*path) {
        __dirnameBuf[0] = '.';
        __dirnameBuf[1] = 0;
        return __dirnameBuf;
    }
    char sep = __pathSep(path);
    if (!sep) {
        __dirnameBuf[0] = '.';
        __dirnameBuf[1] = 0;
        return __dirnameBuf;
    }
    // Find last separator that is not the trailing one.  Strip
    // trailing separators first.
    int end = 0;
    while (path[end]) end++;
    while (end > 1 && path[end - 1] == sep) end--;
    int lastSep = -1;
    for (int i = 0; i < end; i++) {
        if (path[i] == sep) lastSep = i;
    }
    if (lastSep < 0) {
        __dirnameBuf[0] = '.';
        __dirnameBuf[1] = 0;
        return __dirnameBuf;
    }
    if (lastSep == 0) {
        __dirnameBuf[0] = sep;
        __dirnameBuf[1] = 0;
        return __dirnameBuf;
    }
    int n = lastSep;
    if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
    for (int i = 0; i < n; i++) __dirnameBuf[i] = path[i];
    __dirnameBuf[n] = 0;
    return __dirnameBuf;
}

// basename — return the file-name portion of `path`.  Same
// scratch-buffer semantics as dirname.
//   "/usr/lib"   -> "lib"
//   "/usr/"      -> "usr"
//   "/"          -> "/"
//   ""           -> "."
static char __basenameBuf[LIBC_PATH_MAX];
char *basename(char *path) {
    if (!path || !*path) {
        __basenameBuf[0] = '.';
        __basenameBuf[1] = 0;
        return __basenameBuf;
    }
    char sep = __pathSep(path);
    int end = 0;
    while (path[end]) end++;
    // Strip trailing separators (but preserve "/" itself).
    while (end > 1 && sep && path[end - 1] == sep) end--;
    if (end == 1 && sep && path[0] == sep) {
        __basenameBuf[0] = sep;
        __basenameBuf[1] = 0;
        return __basenameBuf;
    }
    int start = 0;
    if (sep) {
        for (int i = 0; i < end; i++) {
            if (path[i] == sep) start = i + 1;
        }
    }
    int n = end - start;
    if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
    for (int i = 0; i < n; i++) __basenameBuf[i] = path[start + i];
    __basenameBuf[n] = 0;
    return __basenameBuf;
}

// fnmatch — POSIX glob-style pattern match.  Implements:
//   *           any-string wildcard
//   ?           any-single-char wildcard
//   [abc]       character class
//   [a-z]       character range
//   [!abc]      negated class (POSIX) — `[^abc]` also accepted
//   \c          literal escape (when FNM_NOESCAPE not in flags)
// Returns 0 on match, FNM_NOMATCH (1) otherwise.  Flags:
//   FNM_NOESCAPE (1) — disable backslash escape
//   FNM_PATHNAME (2) — `*` and `?` do not match `/`
//   FNM_PERIOD   (4) — leading `.` only matches an explicit `.`
//   FNM_CASEFOLD (16) — case-insensitive
#define FNM_NOMATCH    1
#define FNM_NOESCAPE   0x01
#define FNM_PATHNAME   0x02
#define FNM_PERIOD     0x04
#define FNM_CASEFOLD   0x10

static int __fnmCharEq(char a, char b, int flags) {
    if (flags & FNM_CASEFOLD) {
        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
        if (b >= 'A' && b <= 'Z') b = (char)(b + 32);
    }
    return a == b;
}

// Match a single bracket expression starting at pat[*pi] == '['.
// On match advances *pi past the ']' and returns 1; on no-match
// returns 0 with *pi advanced past the ']'; on malformed bracket
// (no closing ']') returns -1 and leaves *pi alone (caller treats
// '[' as literal).
static int __fnmBracket(const char *pat, int *pi, char c, int flags) {
    int i = *pi + 1;          // past '['
    int negate = 0;
    if (pat[i] == '!' || pat[i] == '^') { negate = 1; i++; }
    // Find closing bracket — fail if missing.
    int close = i;
    if (pat[close] == ']') close++;
    while (pat[close] && pat[close] != ']') close++;
    if (!pat[close]) return -1;
    int matched = 0;
    while (i < close) {
        char lo = pat[i];
        if (!(flags & FNM_NOESCAPE) && lo == '\\' && pat[i+1]) {
            lo = pat[i+1]; i += 2;
        } else {
            i++;
        }
        if (pat[i] == '-' && i + 1 < close) {
            char hi = pat[i+1];
            if (!(flags & FNM_NOESCAPE) && hi == '\\' && pat[i+2]) {
                hi = pat[i+2]; i += 3;
            } else {
                i += 2;
            }
            char lc = c, ll = lo, lh = hi;
            if (flags & FNM_CASEFOLD) {
                if (lc >= 'A' && lc <= 'Z') lc = (char)(lc + 32);
                if (ll >= 'A' && ll <= 'Z') ll = (char)(ll + 32);
                if (lh >= 'A' && lh <= 'Z') lh = (char)(lh + 32);
            }
            if (lc >= ll && lc <= lh) matched = 1;
        } else {
            if (__fnmCharEq(lo, c, flags)) matched = 1;
        }
    }
    *pi = close + 1;
    if (negate) matched = !matched;
    return matched;
}

// Recursive fnmatch core — needed because `*` requires backtracking.
// Depth is bounded by the pattern length (each `*` consumes one frame
// per non-* segment), well under the IIgs stack budget for typical
// 256-char patterns.
static int __fnmMatch(const char *pat, const char *str, int flags) {
    int pi = 0;
    int si = 0;
    while (pat[pi]) {
        char pc = pat[pi];
        if (pc == '*') {
            while (pat[pi] == '*') pi++;
            if (!pat[pi]) {
                if (flags & FNM_PATHNAME) {
                    // `*` may not cross a separator; bail if any
                    // remaining input contains one.
                    while (str[si]) {
                        if (str[si] == '/') return FNM_NOMATCH;
                        si++;
                    }
                }
                return 0;
            }
            // Try each remaining position in str.
            while (str[si]) {
                if (__fnmMatch(pat + pi, str + si, flags) == 0) return 0;
                if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
                si++;
            }
            return __fnmMatch(pat + pi, str + si, flags);
        }
        if (!str[si]) return FNM_NOMATCH;
        if (pc == '?') {
            if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
            if ((flags & FNM_PERIOD) && si == 0 && str[si] == '.') return FNM_NOMATCH;
            pi++; si++;
            continue;
        }
        if (pc == '[') {
            int saved = pi;
            int r = __fnmBracket(pat, &pi, str[si], flags);
            if (r < 0) {
                // Malformed; treat '[' as literal.
                if (!__fnmCharEq('[', str[si], flags)) return FNM_NOMATCH;
                pi = saved + 1; si++;
                continue;
            }
            if (!r) return FNM_NOMATCH;
            if ((flags & FNM_PATHNAME) && str[si] == '/') return FNM_NOMATCH;
            si++;
            continue;
        }
        if (!(flags & FNM_NOESCAPE) && pc == '\\' && pat[pi+1]) {
            pc = pat[pi+1]; pi += 2;
        } else {
            pi++;
        }
        if (!__fnmCharEq(pc, str[si], flags)) return FNM_NOMATCH;
        si++;
    }
    return str[si] ? FNM_NOMATCH : 0;
}

int fnmatch(const char *pattern, const char *string, int flags) {
    if (!pattern || !string) return FNM_NOMATCH;
    if ((flags & FNM_PERIOD) && string[0] == '.' && pattern[0] != '.') {
        return FNM_NOMATCH;
    }
    return __fnmMatch(pattern, string, flags);
}

// mkstemp — create a unique temp file from a template ending in
// `XXXXXX`.  The X's are replaced with random hex from rand() (which
// crt0 seeds from ReadTimeHex).  Returns an int "fd" — we model fds
// 3..MFS_MAX_FILES-1 as 1:1 with FILE* slots, so a subsequent
// fdopen()/close()/etc. can manipulate the same slot.  On error
// returns -1 and leaves the template untouched.
//
// Reject paths that are not writable:
//   - mfs paths are accepted (mfs is always writable when registered).
//   - GS/OS paths require a real dispatcher.
//   - Pure-name paths (no separator) without an mfs registration are
//     treated as GS/OS paths under the default prefix (cwd).
int mkstemp(char *template_) {
    if (!template_) { errno = 22; return -1; }
    int n = 0;
    while (template_[n]) n++;
    if (n < 6) { errno = 22; return -1; }
    int xStart = n - 6;
    for (int i = 0; i < 6; i++) {
        if (template_[xStart + i] != 'X') { errno = 22; return -1; }
    }
    // Decide which backend will get used.  We do NOT actually accept
    // a non-writable target here (it makes mkstemp's "the file is
    // yours to write" contract honest): a GS/OS path on a stub-only
    // build is rejected with EROFS.
    int gsosPath = __isGsosPath(template_);
    if (gsosPath && !__gsosAvailable()) { errno = 30; return -1; }
    static const char hex[] = "0123456789ABCDEF";
    // Try up to TRIES distinct names before giving up.
    enum { MKSTEMP_TRIES = 64 };
    for (int t = 0; t < MKSTEMP_TRIES; t++) {
        unsigned long r1 = (unsigned long)rand();
        unsigned long r2 = (unsigned long)rand();
        unsigned long bits = (r1 << 16) ^ r2 ^ ((unsigned long)t * 2654435761UL);
        for (int i = 0; i < 6; i++) {
            template_[xStart + i] = hex[(bits >> (i * 4)) & 0xF];
        }
        // Existence check + create+open are not atomic on GS/OS (no
        // O_EXCL).  Use fopen("rb") to probe -- if it succeeds, the
        // file exists and we try a different name.
        FILE *probe = fopen(template_, "rb");
        if (probe) { fclose(probe); continue; }
        FILE *f = fopen(template_, "wb+");
        if (!f) {
            // No collision but creation failed (permissions / disk
            // full / bad path); short-circuit without retrying.
            errno = 13;     // EACCES
            return -1;
        }
        // Return the slot index as an fd-equivalent.  Slots 3.. are
        // user files; slots 0..2 are stdin/stdout/stderr.
        for (int i = 3; i < MFS_MAX_FILES; i++) {
            if (&__mfs[i] == f) return i;
        }
        // Should never happen — but if the slot table changes shape
        // we close cleanly rather than leak.
        fclose(f);
        errno = 24;       // EMFILE
        return -1;
    }
    errno = 17;           // EEXIST
    return -1;
}

// realpath — resolve `path` to an absolute, canonical pathname.  On
// IIgs this means:
//   - if path is already absolute (starts with '/', ':', or matches
//     a GS/OS volume root), copy verbatim into resolved (or malloc
//     a fresh buffer when resolved == NULL).
//   - else prepend the default GS/OS prefix ($0) from gsosGetPrefix.
//   - verify the resulting path exists via gsosGetFileInfo.
// Returns resolved (or the allocated buffer) on success; NULL +
// errno on failure.  resolved must be at least PATH_MAX bytes when
// non-NULL.
char *realpath(const char *path, char *resolved) {
    if (!path) { errno = 22; return (char *)0; }
    if (!__gsosAvailable() || !gsosGetPrefix || !gsosGetFileInfo) {
        // Without a real dispatcher we can still canonicalize an
        // already-absolute path by string-copying it.  Relative paths
        // are unresolvable.
        char sep0 = path[0];
        if (sep0 != '/' && sep0 != ':') { errno = 38; return (char *)0; }
        char *out = resolved ? resolved : (char *)malloc(LIBC_PATH_MAX);
        if (!out) { errno = 12; return (char *)0; }
        int i = 0;
        while (path[i] && i < LIBC_PATH_MAX - 1) { out[i] = path[i]; i++; }
        out[i] = 0;
        if (path[i]) {
            if (!resolved) free(out);
            errno = 36; return (char *)0;
        }
        return out;
    }
    // Build absolute path.
    char abs[LIBC_PATH_MAX];
    int outLen = 0;
    if (path[0] == '/' || path[0] == ':') {
        while (path[outLen] && outLen < LIBC_PATH_MAX - 1) {
            abs[outLen] = path[outLen]; outLen++;
        }
    } else {
        // Get default prefix.  We reuse __gsosPathBuf as a ResultBuf
        // since it is sized to LIBC_PATH_MAX and has the same layout
        // (u16 length + char[]).  The ResultBuf has an extra leading
        // maxLen field, so we use a small dedicated buffer here.
        struct {
            u16 maxLen;
            u16 length;
            char text[LIBC_PATH_MAX];
        } pref;
        pref.maxLen = LIBC_PATH_MAX;
        pref.length = 0;
        __GsosPrefixParm pp = { 2, 0, &pref };
        if (gsosGetPrefix(&pp) != 0) { errno = 2; return (char *)0; }
        for (int i = 0; i < pref.length && outLen < LIBC_PATH_MAX - 1; i++) {
            abs[outLen++] = pref.text[i];
        }
        // Ensure trailing separator before appending the relative
        // remainder.  Pick the separator already in use, falling
        // back to '/'.
        char sep = (outLen > 0) ? abs[outLen - 1] : '/';
        if (sep != '/' && sep != ':') sep = '/';
        if (outLen == 0 || (abs[outLen - 1] != '/' && abs[outLen - 1] != ':')) {
            if (outLen < LIBC_PATH_MAX - 1) abs[outLen++] = sep;
        }
        int i = 0;
        while (path[i] && outLen < LIBC_PATH_MAX - 1) {
            abs[outLen++] = path[i++];
        }
    }
    abs[outLen] = 0;
    if (outLen >= LIBC_PATH_MAX - 1) { errno = 36; return (char *)0; }
    // Canonicalize: strip duplicate separators, resolve "." and ".."
    // segments.  This is structural; no GS/OS calls beyond the
    // existence check below.
    char canon[LIBC_PATH_MAX];
    char sep = (abs[0] == ':') ? ':' : '/';
    int ci = 0;
    int i = 0;
    while (abs[i]) {
        // Skip duplicate separators.
        if (abs[i] == sep && ci > 0 && canon[ci - 1] == sep) { i++; continue; }
        canon[ci++] = abs[i++];
        if (ci >= LIBC_PATH_MAX - 1) { errno = 36; return (char *)0; }
    }
    canon[ci] = 0;
    // Verify existence.
    if (__buildGSString(canon) < 0) { errno = 36; return (char *)0; }
    __GsosFileInfoParm fi;
    fi.pCount   = 4;
    fi.pathname = &__gsosPathBuf;
    if (gsosGetFileInfo(&fi) != 0) { errno = 2; return (char *)0; }
    char *out = resolved ? resolved : (char *)malloc(LIBC_PATH_MAX);
    if (!out) { errno = 12; return (char *)0; }
    for (int j = 0; j <= ci; j++) out[j] = canon[j];
    return out;
}

// glob — POSIX directory iterator returning paths matching `pattern`.
// Minimal implementation: pattern is split into "dir-prefix" +
// "leaf-glob".  Open the dir via gsosOpen, iterate via
// gsosGetDirEntry, fnmatch each entry against the leaf-glob, and
// stash matches into glob_t.gl_pathv (a malloc'd char** with malloc'd
// entries).
typedef struct {
    size_t gl_pathc;
    char **gl_pathv;
    size_t gl_offs;
} glob_t;

#define GLOB_NOSPACE   1
#define GLOB_ABORTED   2
#define GLOB_NOMATCH   3

#define GLOB_ERR       0x01
#define GLOB_MARK      0x02
#define GLOB_NOSORT    0x04
#define GLOB_NOCHECK   0x10
#define GLOB_NOESCAPE  0x40

static int __glob_addMatch(glob_t *g, const char *s) {
    size_t newC = g->gl_pathc + 1;
    char **nv = (char **)malloc(sizeof(char *) * (newC + 1));
    if (!nv) return GLOB_NOSPACE;
    for (size_t i = 0; i < g->gl_pathc; i++) nv[i] = g->gl_pathv[i];
    size_t n = 0;
    while (s[n]) n++;
    char *copy = (char *)malloc(n + 1);
    if (!copy) { free(nv); return GLOB_NOSPACE; }
    for (size_t i = 0; i <= n; i++) copy[i] = s[i];
    nv[newC - 1] = copy;
    nv[newC]     = (char *)0;
    if (g->gl_pathv) free(g->gl_pathv);
    g->gl_pathv = nv;
    g->gl_pathc = newC;
    return 0;
}

int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob) {
    (void)errfunc;
    if (!pattern || !pglob) return GLOB_ABORTED;
    if (!(flags & 0)) {     // GLOB_APPEND not modelled
        pglob->gl_pathc = 0;
        pglob->gl_pathv = (char **)0;
        pglob->gl_offs  = 0;
    }
    if (!__gsosAvailable() || !gsosGetDirEntry || !gsosOpen || !gsosClose) {
        // Fall back to NOCHECK semantics: the pattern itself is the
        // single result when GLOB_NOCHECK is set, else NOMATCH.
        if (flags & GLOB_NOCHECK) {
            return __glob_addMatch(pglob, pattern) == 0 ? 0 : GLOB_NOSPACE;
        }
        errno = 38;
        return GLOB_NOMATCH;
    }
    // Split pattern at the last separator.
    char sep = __pathSep(pattern) ? __pathSep(pattern) : '/';
    int patLen = 0;
    while (pattern[patLen]) patLen++;
    int lastSep = -1;
    for (int i = 0; i < patLen; i++) {
        if (pattern[i] == sep) lastSep = i;
    }
    char dirPath[LIBC_PATH_MAX];
    const char *leaf;
    if (lastSep < 0) {
        // No dir component — iterate cwd.  Use prefix #0.
        dirPath[0] = 0;
        leaf = pattern;
    } else {
        int nd = (lastSep == 0) ? 1 : lastSep;
        if (nd >= LIBC_PATH_MAX) return GLOB_NOSPACE;
        for (int i = 0; i < nd; i++) dirPath[i] = pattern[i];
        dirPath[nd] = 0;
        leaf = pattern + lastSep + 1;
    }
    // Open the directory.  When dirPath is empty, resolve "" via
    // gsosGetPrefix (default cwd).
    char dirBuf[LIBC_PATH_MAX];
    const char *openName = dirPath;
    if (!dirPath[0]) {
        struct {
            u16 maxLen;
            u16 length;
            char text[LIBC_PATH_MAX];
        } pref;
        pref.maxLen = LIBC_PATH_MAX;
        pref.length = 0;
        __GsosPrefixParm pp = { 2, 0, &pref };
        if (!gsosGetPrefix || gsosGetPrefix(&pp) != 0) {
            errno = 2;
            return GLOB_NOMATCH;
        }
        int n = pref.length;
        if (n >= LIBC_PATH_MAX) n = LIBC_PATH_MAX - 1;
        // Strip trailing separator to please GS/OS Open.
        while (n > 1 && (pref.text[n-1] == '/' || pref.text[n-1] == ':')) n--;
        for (int i = 0; i < n; i++) dirBuf[i] = pref.text[i];
        dirBuf[n] = 0;
        openName = dirBuf;
    }
    if (__buildGSString(openName) < 0) { errno = 36; return GLOB_NOSPACE; }
    __GsosOpenParm dirOpen = { 3, 0, &__gsosPathBuf, 1 };
    if (gsosOpen(&dirOpen) != 0) { errno = 2; return GLOB_NOMATCH; }
    u16 refNum = dirOpen.refNum;
    // Iterate.
    int rc = 0;
    int matches = 0;
    enum { LEAF_BUF = LIBC_PATH_MAX };
    while (rc == 0) {
        struct {
            u16  maxLen;
            u16  length;
            char text[LEAF_BUF];
        } nb;
        nb.maxLen = LEAF_BUF;
        nb.length = 0;
        __GsosDirEntryParm de;
        de.pCount       = 6;
        de.refNum       = refNum;
        de.flags        = 0;
        de.base         = 0;
        de.displacement = 1;     // next entry
        de.name         = &nb;
        u16 gerr = gsosGetDirEntry(&de);
        if (gerr != 0) {
            // $61 endOfDir is the normal termination.
            break;
        }
        // Build a NUL-terminated leaf name.
        char leafBuf[LEAF_BUF + 1];
        int ln = nb.length;
        if (ln >= LEAF_BUF) ln = LEAF_BUF - 1;
        for (int i = 0; i < ln; i++) leafBuf[i] = nb.text[i];
        leafBuf[ln] = 0;
        int fflags = (flags & GLOB_NOESCAPE) ? FNM_NOESCAPE : 0;
        if (fnmatch(leaf, leafBuf, fflags) == 0) {
            // Re-assemble full path: dirPath + sep + leaf
            char full[LIBC_PATH_MAX];
            int fi = 0;
            int dp = 0;
            while (dirPath[dp] && fi < LIBC_PATH_MAX - 1) full[fi++] = dirPath[dp++];
            if (fi > 0 && full[fi-1] != sep && fi < LIBC_PATH_MAX - 1) full[fi++] = sep;
            for (int i = 0; i < ln && fi < LIBC_PATH_MAX - 1; i++) full[fi++] = leafBuf[i];
            full[fi] = 0;
            if (__glob_addMatch(pglob, full) != 0) {
                rc = GLOB_NOSPACE;
                break;
            }
            matches++;
        }
    }
    __GsosRefNumRecGS cr = { 1, refNum };
    if (gsosClose) gsosClose(&cr);
    if (rc != 0) return rc;
    if (matches == 0) {
        if (flags & GLOB_NOCHECK) {
            return __glob_addMatch(pglob, pattern) == 0 ? 0 : GLOB_NOSPACE;
        }
        return GLOB_NOMATCH;
    }
    return 0;
}

void globfree(glob_t *pglob) {
    if (!pglob) return;
    if (pglob->gl_pathv) {
        for (size_t i = 0; i < pglob->gl_pathc; i++) {
            if (pglob->gl_pathv[i]) free(pglob->gl_pathv[i]);
        }
        free(pglob->gl_pathv);
    }
    pglob->gl_pathc = 0;
    pglob->gl_pathv = (char **)0;
}
