std1.0.0-beta-7 SafeC docs

Source wasm/wasm_rt.sc

View in the repository

// SafeC Standard Library — freestanding wasm32 runtime (see wasm_rt.h).
// Top-level (non-namespaced) definitions on purpose: these must link
// against the *bare*, unmangled 'malloc'/'free'/'strlen'/etc. symbols
// that std's 'extern' declarations reference (SafeC only mangles function
// *definitions* inside a namespace — 'extern' declarations and top-level
// definitions both keep the bare C-ABI name, which is how this file's
// malloc() satisfies std::malloc's extern declaration at link time
// without either side needing to know about the other).
// safeguard: runtime wasm                (malloc & co. for a WebAssembly module)
#pragma once
#include <std/wasm/wasm_rt.h>
//
// NOT part of the general stdlib archive (libsafec_std.a): this file
// defines bare libc-shaped symbols (malloc, free, memcpy, strlen, ...) —
// linking those same names into a *hosted* build's archive collides with
// the real libc there (confirmed in CI: Windows' libucrt raised LNK2005
// "already defined" once this file made it into libsafec_std.a).
// safeguard's Builder.cpp excludes 'std/wasm/' from the stdlib archive
// build specifically because of this file — see the 'kExcludedStdSubdirs'
// comment there. A wasm32 build never links against that archive anyway
// (it's a host-arch .a); it reaches this file through the 'runtime wasm'
// directive above.

// ── Heap: size classes over linear memory ────────────────────────────────────
//
// Every block is a power of two from 16 bytes up (header included), so a
// freed block goes on its class's free list and is handed out again by
// the next allocation of that class — O(1) malloc/free, at most 2x
// internal fragmentation, and no coalescing (a page's allocations are
// dominated by short strings and small vectors). Fresh blocks are carved
// from the end of the heap, which starts at the linker's __heap_base and
// grows the linear memory (memory.grow) on demand; memory is never handed
// back to the host.
//
// Header (8 bytes, keeps payloads 8-aligned): the class (log2 of the block
// size) and a magic word, checked by free()/realloc() so a pointer this
// heap didn't hand out is ignored rather than corrupting a free list.

extern unsigned char __heap_base;

// Literals in pointer-width arithmetic (size_t and uintptr_t are the same
// type on every target: 32-bit on wasm32, 64-bit when std's compile sweep
// builds this file for the host).
#define WRT_U(x) ((uintptr_t)(x))
   // defined by wasm-ld: first byte past static data

#define WASM_RT_MIN_CLASS 4U          // 16-byte blocks
#define WASM_RT_MAX_CLASS 31U         // 2 GiB blocks
#define WASM_RT_HEADER    WRT_U(8)
#define WASM_RT_MAGIC     0x5AFEC0DEU
#define WASM_RT_PAGE      WRT_U(65536)

static uintptr_t gWasmRtBrk = WRT_U(0);       // next fresh byte (0 = not set up yet)
static uintptr_t gWasmRtEnd = WRT_U(0);       // end of the linear memory
static uintptr_t gWasmRtFree[32];       // free-list heads, by class (0 = empty)

static int wasm_rt_memory_pages_() {
    int pages;
    unsafe { asm volatile ("memory.size 0\n\tlocal.set %0" : "=r"(pages)); }
    return pages;
}

// Grows the memory by 'pages' 64 KiB pages; the old page count, or -1.
static int wasm_rt_memory_grow_(int pages) {
    int old;
    unsafe { asm volatile ("local.get %1\n\tmemory.grow 0\n\tlocal.set %0" : "=r"(old) : "r"(pages)); }
    return old;
}

static void wasm_rt_init_() {
    unsafe {
        uintptr_t base = (uintptr_t)&__heap_base;
        gWasmRtBrk = (base + WRT_U(15)) & ~(uintptr_t)WRT_U(15);
        gWasmRtEnd = (uintptr_t)wasm_rt_memory_pages_() * WASM_RT_PAGE;
        for (unsigned int i = 0U; i < 32U; i = i + 1U) { gWasmRtFree[i] = WRT_U(0); }
    }
}

// Smallest class whose block holds 'size' payload bytes, or 0 if too big.
static unsigned int wasm_rt_class_(size_t size) {
    uintptr_t need = (uintptr_t)size + WASM_RT_HEADER;
    if (need < (uintptr_t)size) { return 0U; }   // overflow
    unsigned int c = WASM_RT_MIN_CLASS;
    while (c <= WASM_RT_MAX_CLASS && (WRT_U(1) << (uintptr_t)c) < need) { c = c + 1U; }
    return c > WASM_RT_MAX_CLASS ? 0U : c;
}

// A fresh block of 2^c bytes from the end of the heap; 0 when memory can't grow.
static uintptr_t wasm_rt_carve_(unsigned int c) {
    uintptr_t bytes = WRT_U(1) << (uintptr_t)c;
    // Align big blocks a little (to 16) — the heap start already is.
    if (gWasmRtBrk + bytes < gWasmRtBrk) { return WRT_U(0); }
    if (gWasmRtBrk + bytes > gWasmRtEnd) {
        uintptr_t short_by = gWasmRtBrk + bytes - gWasmRtEnd;
        uintptr_t pages = (short_by + WASM_RT_PAGE - WRT_U(1)) / WASM_RT_PAGE;
        if (pages < WRT_U(16)) { pages = WRT_U(16); }   // grow 1 MiB at a time at least
        if (wasm_rt_memory_grow_((int)pages) < 0) {
            pages = (short_by + WASM_RT_PAGE - WRT_U(1)) / WASM_RT_PAGE;   // retry with just enough
            if (wasm_rt_memory_grow_((int)pages) < 0) { return WRT_U(0); }
        }
        gWasmRtEnd = (uintptr_t)wasm_rt_memory_pages_() * WASM_RT_PAGE;
    }
    uintptr_t b = gWasmRtBrk;
    gWasmRtBrk = gWasmRtBrk + bytes;
    return b;
}

// The block header of payload 'p' if this heap handed it out, else 0.
static uintptr_t wasm_rt_block_(unsigned char* p) {
    if (p == (unsigned char*)0) { return WRT_U(0); }
    unsafe {
        uintptr_t b = (uintptr_t)p - WASM_RT_HEADER;
        if (gWasmRtBrk == WRT_U(0) || b < (uintptr_t)&__heap_base || b >= gWasmRtBrk) { return WRT_U(0); }
        unsigned int* h = (unsigned int*)b;
        if (h[1] != WASM_RT_MAGIC || h[0] < WASM_RT_MIN_CLASS || h[0] > WASM_RT_MAX_CLASS) { return WRT_U(0); }
        return b;
    }
}

unsigned char* malloc(size_t size) {
    if (gWasmRtBrk == WRT_U(0)) { wasm_rt_init_(); }
    unsigned int c = wasm_rt_class_(size);
    if (c == 0U) { return (unsigned char*)0; }
    uintptr_t b = gWasmRtFree[c];
    unsafe {
        if (b != WRT_U(0)) {
            gWasmRtFree[c] = *(uintptr_t*)(b + WASM_RT_HEADER);   // next link lives in the payload
        } else {
            b = wasm_rt_carve_(c);
            if (b == WRT_U(0)) { return (unsigned char*)0; }
        }
        unsigned int* h = (unsigned int*)b;
        h[0] = c;
        h[1] = WASM_RT_MAGIC;
        return (unsigned char*)(b + WASM_RT_HEADER);
    }
}

void free(unsigned char* ptr) {
    uintptr_t b = wasm_rt_block_(ptr);
    if (b == WRT_U(0)) { return; }
    unsafe {
        unsigned int* h = (unsigned int*)b;
        unsigned int c = h[0];
        h[1] = 0U;   // a second free() of the same block is now ignored
        *(uintptr_t*)(b + WASM_RT_HEADER) = gWasmRtFree[c];
        gWasmRtFree[c] = b;
    }
}

unsigned char* calloc(size_t count, size_t size) {
    size_t total = count * size;
    if (size != WRT_U(0) && total / size != count) { return (unsigned char*)0; }
    unsigned char* p = malloc(total);
    if (p != (unsigned char*)0) {
        unsafe { memset(p, 0, total); }
    }
    return p;
}

// Usable payload bytes of a block from malloc (0 for anything else).
size_t malloc_usable_size(unsigned char* ptr) {
    uintptr_t b = wasm_rt_block_(ptr);
    if (b == WRT_U(0)) { return WRT_U(0); }
    unsafe { return (size_t)((WRT_U(1) << (uintptr_t)((unsigned int*)b)[0]) - WASM_RT_HEADER); }
}

unsigned char* realloc(unsigned char* ptr, size_t new_size) {
    if (ptr == (unsigned char*)0) { return malloc(new_size); }
    size_t have = malloc_usable_size(ptr);
    if (have == WRT_U(0)) { return (unsigned char*)0; }
    if (new_size <= have && new_size > have / WRT_U(4)) { return ptr; }   // still fits, not wastefully
    unsigned char* q = malloc(new_size);
    if (q == (unsigned char*)0) { return (unsigned char*)0; }
    unsafe { memcpy(q, ptr, have < new_size ? have : new_size); }
    free(ptr);
    return q;
}

// Alignment up to 16 comes for free (payloads are 8-aligned; blocks of
// 32+ bytes start 16-aligned); larger alignments over-allocate.
int posix_memalign(unsigned char** memptr, size_t alignment, size_t size) {
    if (alignment <= WRT_U(8)) {
        unsigned char* p = malloc(size);
        unsafe { *memptr = p; }
        return p == (unsigned char*)0 ? 12 : 0;   // ENOMEM
    }
    // Over-allocate and leak the slack before the aligned address: the
    // aligned pointer can't be free()d through this heap's header check,
    // so free() ignores it (std uses this for long-lived slabs only).
    unsigned char* raw = malloc(size + alignment);
    if (raw == (unsigned char*)0) { return 12; }
    unsafe {
        uintptr_t a = ((uintptr_t)raw + alignment - WRT_U(1)) & ~((uintptr_t)alignment - WRT_U(1));
        *memptr = (unsigned char*)a;
    }
    return 0;
}

// ── memory ───────────────────────────────────────────────────────────────────

unsigned char* memcpy(unsigned char* dst, const unsigned char* src, size_t n) {
    unsafe {
        unsigned char* d = (unsigned char*)dst;
        const unsigned char* s = (const unsigned char*)src;
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) { d[i] = s[i]; }
    }
    return dst;
}

unsigned char* memmove(unsigned char* dst, const unsigned char* src, size_t n) {
    unsafe {
        unsigned char* d = (unsigned char*)dst;
        const unsigned char* s = (const unsigned char*)src;
        if (d < s) {
            for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) { d[i] = s[i]; }
        } else {
            size_t i = n;
            while (i > WRT_U(0)) { i = i - WRT_U(1); d[i] = s[i]; }
        }
    }
    return dst;
}

unsigned char* memset(unsigned char* ptr, int val, size_t n) {
    unsafe {
        unsigned char* p = (unsigned char*)ptr;
        unsigned char b = (unsigned char)val;
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) { p[i] = b; }
    }
    return ptr;
}

int memcmp(const unsigned char* a, const unsigned char* b, size_t n) {
    unsafe {
        const unsigned char* pa = (const unsigned char*)a;
        const unsigned char* pb = (const unsigned char*)b;
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) {
            if (pa[i] != pb[i]) { return (int)pa[i] - (int)pb[i]; }
        }
    }
    return 0;
}

unsigned char* memchr(const unsigned char* s, int c, size_t n) {
    unsafe {
        const unsigned char* p = (const unsigned char*)s;
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) {
            if (p[i] == (unsigned char)c) { return (unsigned char*)(p + i); }
        }
    }
    return (unsigned char*)0;
}

// ── strings ──────────────────────────────────────────────────────────────────

size_t strlen(const char* s) {
    size_t n = WRT_U(0);
    unsafe { while (s[n] != '\0') { n = n + WRT_U(1); } }
    return n;
}

int strcmp(const char* a, const char* b) {
    unsafe {
        size_t i = WRT_U(0);
        while (a[i] != '\0' && a[i] == b[i]) { i = i + WRT_U(1); }
        return (int)(unsigned char)a[i] - (int)(unsigned char)b[i];
    }
}

int strncmp(const char* a, const char* b, size_t n) {
    unsafe {
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) {
            if (a[i] != b[i] || a[i] == '\0') { return (int)(unsigned char)a[i] - (int)(unsigned char)b[i]; }
        }
    }
    return 0;
}

char* strchr(const char* s, int c) {
    unsafe {
        size_t i = WRT_U(0);
        for (;;) {
            if (s[i] == (char)c) { return (char*)(s + i); }
            if (s[i] == '\0') { return (char*)0; }
            i = i + WRT_U(1);
        }
    }
    return (char*)0;
}

char* strrchr(const char* s, int c) {
    char* last = (char*)0;
    unsafe {
        size_t i = WRT_U(0);
        for (;;) {
            if (s[i] == (char)c) { last = (char*)(s + i); }
            if (s[i] == '\0') { break; }
            i = i + WRT_U(1);
        }
    }
    return last;
}

char* strstr(const char* h, const char* n) {
    unsafe {
        if (n[0] == '\0') { return (char*)h; }
        for (size_t i = WRT_U(0); h[i] != '\0'; i = i + WRT_U(1)) {
            size_t k = WRT_U(0);
            while (n[k] != '\0' && h[i + k] == n[k]) { k = k + WRT_U(1); }
            if (n[k] == '\0') { return (char*)(h + i); }
        }
    }
    return (char*)0;
}

static int wasm_rt_in_set_(char c, const char* set) {
    unsafe {
        for (size_t i = WRT_U(0); set[i] != '\0'; i = i + WRT_U(1)) { if (set[i] == c) { return 1; } }
    }
    return 0;
}

size_t strspn(const char* s, const char* accept) {
    size_t n = WRT_U(0);
    unsafe { while (s[n] != '\0' && wasm_rt_in_set_(s[n], accept)) { n = n + WRT_U(1); } }
    return n;
}

size_t strcspn(const char* s, const char* reject) {
    size_t n = WRT_U(0);
    unsafe { while (s[n] != '\0' && !wasm_rt_in_set_(s[n], reject)) { n = n + WRT_U(1); } }
    return n;
}

char* strpbrk(const char* s, const char* accept) {
    unsafe {
        size_t n = strcspn(s, accept);
        return s[n] == '\0' ? (char*)0 : (char*)(s + n);
    }
}

char* strncpy(char* dst, const char* src, size_t n) {
    unsafe {
        size_t i = WRT_U(0);
        while (i < n && src[i] != '\0') { dst[i] = src[i]; i = i + WRT_U(1); }
        while (i < n) { dst[i] = '\0'; i = i + WRT_U(1); }
    }
    return dst;
}

char* strcat(char* dst, const char* src) {
    unsafe {
        size_t d = strlen(dst);
        size_t i = WRT_U(0);
        while (src[i] != '\0') { dst[d + i] = src[i]; i = i + WRT_U(1); }
        dst[d + i] = '\0';
    }
    return dst;
}

char* strncat(char* dst, const char* src, size_t n) {
    unsafe {
        size_t d = strlen(dst);
        size_t i = WRT_U(0);
        while (i < n && src[i] != '\0') { dst[d + i] = src[i]; i = i + WRT_U(1); }
        dst[d + i] = '\0';
    }
    return dst;
}

char* strtok_r(char* s, const char* delim, char** saveptr) {
    unsafe {
        char* p = s != (char*)0 ? s : *saveptr;
        if (p == (char*)0) { return (char*)0; }
        p = p + strspn(p, delim);
        if (*p == '\0') { *saveptr = p; return (char*)0; }
        char* end = p + strcspn(p, delim);
        if (*end != '\0') { *end = '\0'; *saveptr = end + 1; }
        else { *saveptr = end; }
        return p;
    }
}

// ── number parsing ───────────────────────────────────────────────────────────

static int wasm_rt_digit_(char c, int base) {
    int d = 99;
    if (c >= '0' && c <= '9') { d = (int)(c - '0'); }
    else if (c >= 'a' && c <= 'z') { d = (int)(c - 'a') + 10; }
    else if (c >= 'A' && c <= 'Z') { d = (int)(c - 'A') + 10; }
    return d < base ? d : -1;
}

unsigned long long strtoull(const char* s, char** end, int base) {
    unsafe {
        size_t i = WRT_U(0);
        while (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') { i = i + WRT_U(1); }
        int neg = 0;
        if (s[i] == '+' || s[i] == '-') { neg = s[i] == '-'; i = i + WRT_U(1); }
        if ((base == 0 || base == 16) && s[i] == '0' && (s[i + WRT_U(1)] == 'x' || s[i + WRT_U(1)] == 'X') &&
            wasm_rt_digit_(s[i + WRT_U(2)], 16) >= 0) { base = 16; i = i + WRT_U(2); }
        else if (base == 0 && s[i] == '0') { base = 8; }
        else if (base == 0) { base = 10; }
        size_t start = i;
        unsigned long long v = 0ULL;
        for (;;) {
            int d = wasm_rt_digit_(s[i], base);
            if (d < 0) { break; }
            v = v * (unsigned long long)base + (unsigned long long)d;
            i = i + WRT_U(1);
        }
        if (end != (char**)0) { *end = (char*)(i == start ? s : s + i); }
        return neg ? (unsigned long long)0 - v : v;
    }
}

long long strtoll(const char* s, char** end, int base) {
    return (long long)strtoull(s, end, base);
}

int atoi(const char* s) { return (int)strtoll(s, (char**)0, 10); }
long long atoll(const char* s) { return strtoll(s, (char**)0, 10); }

// Decimal floating point ('-1.5e3', 'inf', 'nan' not supported): exact for
// the integer part, then scaled; good to ~15 significant digits.
double strtod(const char* s, char** end) {
    unsafe {
        size_t i = WRT_U(0);
        while (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') { i = i + WRT_U(1); }
        double sign = 1.0;
        if (s[i] == '+' || s[i] == '-') { if (s[i] == '-') { sign = -1.0; } i = i + WRT_U(1); }
        size_t start = i;
        double v = 0.0;
        while (s[i] >= '0' && s[i] <= '9') { v = v * 10.0 + (double)(s[i] - '0'); i = i + WRT_U(1); }
        if (s[i] == '.') {
            i = i + WRT_U(1);
            double scale = 0.1;
            while (s[i] >= '0' && s[i] <= '9') { v = v + (double)(s[i] - '0') * scale; scale = scale / 10.0; i = i + WRT_U(1); }
        }
        if (i == start || (i == start + WRT_U(1) && s[start] == '.')) {
            if (end != (char**)0) { *end = (char*)s; }
            return 0.0;
        }
        if (s[i] == 'e' || s[i] == 'E') {
            size_t e = i + WRT_U(1);
            int eneg = 0;
            if (s[e] == '+' || s[e] == '-') { eneg = s[e] == '-'; e = e + WRT_U(1); }
            if (s[e] >= '0' && s[e] <= '9') {
                int ex = 0;
                while (s[e] >= '0' && s[e] <= '9') { if (ex < 400) { ex = ex * 10 + (int)(s[e] - '0'); } e = e + WRT_U(1); }
                double f = 1.0;
                for (int k = 0; k < ex; k = k + 1) { f = f * 10.0; }
                v = eneg ? v / f : v * f;
                i = e;
            }
        }
        if (end != (char**)0) { *end = (char*)(s + i); }
        return sign * v;
    }
}

// ── sorting ──────────────────────────────────────────────────────────────────

static void wasm_rt_swap_(unsigned char* a, unsigned char* b, size_t n) {
    unsafe {
        for (size_t i = WRT_U(0); i < n; i = i + WRT_U(1)) { unsigned char t = a[i]; a[i] = b[i]; b[i] = t; }
    }
}


// Heapsort: in place, no recursion, O(n log n).
static void wasm_rt_sift_(unsigned char* base, size_t root, size_t n, size_t sz, WasmRtCmp_ cmp) {
    unsafe {
        for (;;) {
            size_t child = WRT_U(2) * root + WRT_U(1);
            if (child >= n) { return; }
            if (child + WRT_U(1) < n && cmp((const unsigned char*)(base + child * sz), (const unsigned char*)(base + (child + WRT_U(1)) * sz)) < 0) {
                child = child + WRT_U(1);
            }
            if (cmp((const unsigned char*)(base + root * sz), (const unsigned char*)(base + child * sz)) >= 0) { return; }
            wasm_rt_swap_(base + root * sz, base + child * sz, sz);
            root = child;
        }
    }
}

void qsort(unsigned char* base, size_t n, size_t sz, WasmRtCmp_ cmp) {
    if (n < WRT_U(2)) { return; }
    unsafe {
        unsigned char* b = (unsigned char*)base;
        size_t i = n / WRT_U(2);
        while (i > WRT_U(0)) { i = i - WRT_U(1); wasm_rt_sift_(b, i, n, sz, cmp); }
        size_t end = n;
        while (end > WRT_U(1)) {
            end = end - WRT_U(1);
            wasm_rt_swap_(b, b + end * sz, sz);
            wasm_rt_sift_(b, WRT_U(0), end, sz, cmp);
        }
    }
}

// ── compiler-rt builtins ─────────────────────────────────────────────────────
// 128-bit helpers LLVM emits calls to on wasm32 (no native i128): __multi3
// for i64 multiplies with overflow checks (std's checked_mul_size), and the
// 128-bit shifts that expansions use. A 128-bit value travels as (lo, hi)
// i64 halves; the result is written through the hidden sret pointer, which
// is exactly how these signatures lower on wasm32.

void __multi3(unsigned long long* ret, unsigned long long alo, unsigned long long ahi,
              unsigned long long blo, unsigned long long bhi) {
    unsigned long long mask = 0xFFFFFFFFULL;
    unsigned long long a0 = alo & mask;
    unsigned long long a1 = alo >> 32;
    unsigned long long b0 = blo & mask;
    unsigned long long b1 = blo >> 32;
    unsigned long long p00 = a0 * b0;
    unsigned long long p01 = a0 * b1;
    unsigned long long p10 = a1 * b0;
    unsigned long long p11 = a1 * b1;
    unsigned long long mid = (p00 >> 32) + (p01 & mask) + (p10 & mask);
    unsigned long long lo = (p00 & mask) | (mid << 32);
    unsigned long long hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32) + alo * bhi + ahi * blo;
    unsafe { ret[0] = lo; ret[1] = hi; }
}

void __ashlti3(unsigned long long* ret, unsigned long long lo, unsigned long long hi, int b) {
    unsigned long long rl = lo;
    unsigned long long rh = hi;
    if (b >= 128) { rl = 0ULL; rh = 0ULL; }
    else if (b >= 64) { rh = lo << (unsigned long long)(b - 64); rl = 0ULL; }
    else if (b > 0) { rh = (hi << (unsigned long long)b) | (lo >> (unsigned long long)(64 - b)); rl = lo << (unsigned long long)b; }
    unsafe { ret[0] = rl; ret[1] = rh; }
}

void __lshrti3(unsigned long long* ret, unsigned long long lo, unsigned long long hi, int b) {
    unsigned long long rl = lo;
    unsigned long long rh = hi;
    if (b >= 128) { rl = 0ULL; rh = 0ULL; }
    else if (b >= 64) { rl = hi >> (unsigned long long)(b - 64); rh = 0ULL; }
    else if (b > 0) { rl = (lo >> (unsigned long long)b) | (hi << (unsigned long long)(64 - b)); rh = hi >> (unsigned long long)b; }
    unsafe { ret[0] = rl; ret[1] = rh; }
}

void __ashrti3(unsigned long long* ret, unsigned long long lo, unsigned long long hi, int b) {
    long long shi = (long long)hi;
    unsigned long long fill = shi < 0LL ? 0xFFFFFFFFFFFFFFFFULL : 0ULL;
    unsigned long long rl = lo;
    unsigned long long rh = hi;
    if (b >= 128) { rl = fill; rh = fill; }
    else if (b >= 64) { rl = (unsigned long long)(shi >> (long long)(b - 64)); rh = fill; }
    else if (b > 0) { rl = (lo >> (unsigned long long)b) | (hi << (unsigned long long)(64 - b)); rh = (unsigned long long)(shi >> (long long)b); }
    unsafe { ret[0] = rl; ret[1] = rh; }
}

// ── the rest ─────────────────────────────────────────────────────────────────

void abort() {
    unsafe { asm volatile ("unreachable"); }
}

int fprintf(CFile_* stream, const char* fmt, ...) { return 0; }
int printf(const char* fmt, ...) { return 0; }