/*
 * builtins.c — All built-in functions for the Scheme interpreter
 */
#include "lumbda.h"

/* ═══════════════════════════════════════════════════════════════════════════
 * Helper macros
 * ═══════════════════════════════════════════════════════════════════════════ */

#define CHECK_ARITY(name, expected) \
    if (n != (expected)) lisp_error(name ": need %d args, got %d", (expected), n)
#define CHECK_MIN_ARITY(name, expected) \
    if (n < (expected)) lisp_error(name ": need at least %d args, got %d", (expected), n)
#define AS_NUM(v) \
    (is_number(v) ? (v) : (lisp_error("not a number: %s", show(v, false)), VAL_NIL))

static void check_string(Value v) {
    if (!IS_STRING(v)) lisp_error("not a string: %s", show(v, false));
}

static void check_sym(Value v) {
    if (!IS_SYM(v)) lisp_error("not a symbol: %s", show(v, false));
}

static void check_pair(Value v) {
    if (!IS_PAIR(v)) lisp_error("not a pair: %s", show(v, false));
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Arithmetic builtins
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_add(Value *a, int n, Env *e) {
    (void)e;
    if (n == 0) return VAL_INT(0);
    Value result = AS_NUM(a[0]);
    for (int i = 1; i < n; i++) result = num_add(result, AS_NUM(a[i]));
    return result;
}

static Value bi_sub(Value *a, int n, Env *e) {
    (void)e;
    if (n == 0) lisp_error("-: no args");
    if (n == 1) return num_neg(AS_NUM(a[0]));
    Value result = AS_NUM(a[0]);
    for (int i = 1; i < n; i++) result = num_sub(result, AS_NUM(a[i]));
    return result;
}

static Value bi_mul(Value *a, int n, Env *e) {
    (void)e;
    Value result = VAL_INT(1);
    for (int i = 0; i < n; i++) result = num_mul(result, AS_NUM(a[i]));
    return result;
}

static Value bi_div(Value *a, int n, Env *e) {
    (void)e;
    if (n == 0) lisp_error("/: no args");
    if (n == 1) return num_div(VAL_INT(1), AS_NUM(a[0]));
    Value result = AS_NUM(a[0]);
    for (int i = 1; i < n; i++) result = num_div(result, AS_NUM(a[i]));
    return result;
}

#define MATH_1(name, fn) \
    static Value bi_##name(Value *a, int n, Env *e) { \
        (void)e; CHECK_ARITY(#name, 1); \
        return make_double(fn(as_number_double(AS_NUM(a[0])))); \
    }

MATH_1(sqrt, sqrt)

static Value bi_isqrt(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("isqrt", 1);
    int64_t v = as_number_int(AS_NUM(a[0]));
    if (v < 0) lisp_error("isqrt: negative argument: %lld", (long long)v);
    if (v < 2) return VAL_INT(v);
    uint64_t x = (uint64_t)v;
    uint64_t res = 0;
    uint64_t bit = (uint64_t)1 << 62;
    while (bit > x) bit >>= 2;
    while (bit != 0) {
        if (x >= res + bit) { x -= res + bit; res = (res >> 1) + bit; }
        else { res >>= 1; }
        bit >>= 2;
    }
    return VAL_INT((int64_t)res);
}

MATH_1(sin, sin)
MATH_1(cos, cos)
MATH_1(tan, tan)
MATH_1(asin, asin)
MATH_1(acos, acos)
MATH_1(exp, exp)

static Value bi_log(Value *a, int n, Env *e) {
    (void)e;
    if (n == 1) return make_double(log(as_number_double(a[0])));
    if (n == 2) return make_double(log(as_number_double(a[0])) / log(as_number_double(a[1])));
    lisp_error("log: need 1-2 args");
    return VAL_NIL;
}

static Value bi_atan(Value *a, int n, Env *e) {
    (void)e;
    if (n == 1) return make_double(atan(as_number_double(a[0])));
    if (n == 2) return make_double(atan2(as_number_double(a[0]), as_number_double(a[1])));
    lisp_error("atan: need 1-2 args");
    return VAL_NIL;
}

static Value bi_quotient(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("quotient", 2);
    if (IS_INTEGER(a[0]) && IS_INTEGER(a[1])) return big_quotient(a[0], a[1]);
    return VAL_INT(as_number_int(a[0]) / as_number_int(a[1]));
}

static Value bi_remainder(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("remainder", 2);
    if (IS_INTEGER(a[0]) && IS_INTEGER(a[1])) return big_remainder(a[0], a[1]);
    int64_t x = as_number_int(a[0]), y = as_number_int(a[1]);
    int64_t r = x % y;
    /* Remainder has sign of dividend */
    return VAL_INT(r);
}

static Value bi_modulo(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("modulo", 2);
    if (IS_INTEGER(a[0]) && IS_INTEGER(a[1])) return big_modulo(a[0], a[1]);
    int64_t x = as_number_int(a[0]), y = as_number_int(a[1]);
    int64_t r = x % y;
    if ((r > 0 && y < 0) || (r < 0 && y > 0)) r += y;
    return VAL_INT(r);
}

static Value bi_expt(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("expt", 2);
    /* Integer-base / non-negative integer-exp → bignum-aware path.
     * This catches (expt 2 256) and friends — fixnum * fixnum would
     * silently overflow inside the old int64 loop. */
    if (IS_INTEGER(a[0]) && IS_INTEGER(a[1])) {
        bool nonneg = (IS_INT(a[1]) && as_int(a[1]) >= 0) ||
                      (IS_BIGNUM(a[1]) && AS_BIGNUM(a[1])->sign >= 0);
        if (nonneg) return big_expt(a[0], a[1]);
    }
    return make_double(pow(as_number_double(a[0]), as_number_double(a[1])));
}

static Value bi_abs(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("abs", 1);
    if (IS_INT(a[0])) {
        int64_t v = as_int(a[0]);
        if (v == FIXNUM_MIN) return big_abs(a[0]);  /* avoid -INT_MIN overflow */
        return VAL_INT(v < 0 ? -v : v);
    }
    if (IS_BIGNUM(a[0])) return big_abs(a[0]);
    return make_double(fabs(as_number_double(a[0])));
}

static Value bi_floor(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("floor", 1);
    return VAL_INT((int64_t)floor(as_number_double(a[0])));
}

static Value bi_ceiling(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("ceiling", 1);
    return VAL_INT((int64_t)ceil(as_number_double(a[0])));
}

static Value bi_round(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("round", 1);
    return VAL_INT((int64_t)round(as_number_double(a[0])));
}

static Value bi_truncate(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("truncate", 1);
    return VAL_INT((int64_t)trunc(as_number_double(a[0])));
}

static Value bi_min(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("min", 1);
    Value r = a[0];
    for (int i = 1; i < n; i++) if (num_lt(a[i], r)) r = a[i];
    return r;
}

static Value bi_max(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("max", 1);
    Value r = a[0];
    for (int i = 1; i < n; i++) if (num_gt(a[i], r)) r = a[i];
    return r;
}

static Value bi_gcd(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("gcd", 2);
    int64_t x = as_number_int(a[0]), y = as_number_int(a[1]);
    if (x < 0) x = -x; if (y < 0) y = -y;
    while (y) { int64_t t = y; y = x % y; x = t; }
    return VAL_INT(x);
}

static Value bi_lcm(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("lcm", 2);
    int64_t x = as_number_int(a[0]), y = as_number_int(a[1]);
    if (x < 0) x = -x; if (y < 0) y = -y;
    int64_t g = x;
    { int64_t b = y; while (b) { int64_t t = b; b = g % b; g = t; } }
    return VAL_INT(g ? (x / g) * y : 0);
}

static Value bi_exact(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("exact", 1);
    if (IS_INT(a[0]) || IS_RATIONAL(a[0])) return a[0];
    /* Approximate double → fraction */
    double d = as_double(a[0]);
    return rational_normalize((int64_t)(d * 1000000), 1000000);
}

static Value bi_inexact(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("inexact", 1);
    return make_double(as_number_double(a[0]));
}

static Value bi_numerator(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("numerator", 1);
    if (IS_RATIONAL(a[0])) return VAL_INT(AS_RATIONAL(a[0])->num);
    if (IS_INT(a[0])) return a[0];
    return a[0];
}

static Value bi_denominator(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("denominator", 1);
    if (IS_RATIONAL(a[0])) return VAL_INT(AS_RATIONAL(a[0])->den);
    return VAL_INT(1);
}

static Value bi_number_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("number->string", 1);
    char buf[128];
    if (n > 1) {
        int base = (int)as_number_int(a[1]);
        /* Bignum decimal path uses our renderer; other bases fall through to
         * int64 — at our scale (256-bit secp256k1) base-10 covers every site
         * that actually needs precision. */
        if (IS_BIGNUM(a[0]) && base == 10) {
            char *s = bignum_to_str(AS_BIGNUM(a[0]));
            Value r = make_string_from_cstr(s);
            ul_free(s);
            return r;
        }
        int64_t val = as_number_int(a[0]);
        switch (base) {
            case 2: {
                char *p = buf + 127; *p = '\0';
                uint64_t uv = val < 0 ? -val : val;
                if (uv == 0) { *--p = '0'; }
                else { while (uv) { *--p = '0' + (uv & 1); uv >>= 1; } }
                if (val < 0) *--p = '-';
                return make_string_from_cstr(p);
            }
            case 8: snprintf(buf, sizeof(buf), "%llo", (long long)val); break;
            case 16: snprintf(buf, sizeof(buf), "%llx", (long long)val); break;
            default: snprintf(buf, sizeof(buf), "%lld", (long long)val); break;
        }
    } else {
        char *s = show(a[0], false);
        Value r = make_string_from_cstr(s);
        ul_free(s);
        return r;
    }
    return make_string_from_cstr(buf);
}

/* Numeric comparison builtins */
#define NUM_CMP(name, op) \
    static Value bi_##name(Value *a, int n, Env *e) { \
        (void)e; CHECK_MIN_ARITY(#name, 2); \
        for (int i = 0; i < n - 1; i++) { \
            if (!(op(AS_NUM(a[i]), AS_NUM(a[i+1])))) return VAL_FALSE; \
        } \
        return VAL_TRUE; \
    }

NUM_CMP(num_eq, num_eq)
NUM_CMP(num_lt, num_lt)
NUM_CMP(num_gt, num_gt)
NUM_CMP(num_le, num_le)
NUM_CMP(num_ge, num_ge)

/* Numeric predicates */
#define NUM_PRED(name, check) \
    static Value bi_##name(Value *a, int n, Env *e) { \
        (void)e; CHECK_ARITY(#name, 1); \
        return VAL_BOOL(check); \
    }

NUM_PRED(zero_p, is_number(a[0]) && num_eq(a[0], VAL_INT(0)))
NUM_PRED(positive_p, is_number(a[0]) && num_gt(a[0], VAL_INT(0)))
NUM_PRED(negative_p, is_number(a[0]) && num_lt(a[0], VAL_INT(0)))
NUM_PRED(odd_p, IS_INTEGER(a[0]) && big_is_odd(a[0]))
NUM_PRED(even_p, IS_INTEGER(a[0]) && !big_is_odd(a[0]))

static Value bi_nan_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("nan?", 1);
    return VAL_BOOL(IS_DOUBLE(a[0]) && isnan(as_double(a[0])));
}
static Value bi_infinite_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("infinite?", 1);
    return VAL_BOOL(IS_DOUBLE(a[0]) && isinf(as_double(a[0])));
}
static Value bi_finite_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("finite?", 1);
    return VAL_BOOL(is_number(a[0]) && (IS_INT(a[0]) || (IS_DOUBLE(a[0]) && isfinite(as_double(a[0])))));
}

static Value bi_square(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("square", 1);
    return num_mul(a[0], a[0]);
}

static Value bi_exact_integer_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("exact-integer?", 1);
    return VAL_BOOL(IS_INT(a[0]));
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Booleans & Equality
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_not(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("not", 1);
    return VAL_BOOL(!IS_TRUTHY(a[0]));
}

static Value bi_boolean_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("boolean?", 1);
    return VAL_BOOL(IS_TRUE(a[0]) || IS_FALSE(a[0]));
}

static Value bi_eq_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("eq?", 2);
    if (a[0] == a[1]) return VAL_TRUE;
    /* eq? on integers, booleans, symbols is value comparison */
    if (IS_INT(a[0]) && IS_INT(a[1])) return VAL_BOOL(as_int(a[0]) == as_int(a[1]));
    return VAL_FALSE;
}

static Value bi_eqv_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("eqv?", 2);
    if (a[0] == a[1]) return VAL_TRUE;
    if (IS_INT(a[0]) && IS_INT(a[1])) return VAL_BOOL(as_int(a[0]) == as_int(a[1]));
    if (IS_DOUBLE(a[0]) && IS_DOUBLE(a[1])) return VAL_BOOL(as_double(a[0]) == as_double(a[1]));
    if (IS_CHAR(a[0]) && IS_CHAR(a[1])) return VAL_BOOL(AS_CHAR(a[0]) == AS_CHAR(a[1]));
    if (IS_STRING(a[0]) && IS_STRING(a[1])) {
        ULString *s1 = AS_STRING(a[0]), *s2 = AS_STRING(a[1]);
        return VAL_BOOL(s1->len == s2->len && memcmp(s1->data, s2->data, s1->len) == 0);
    }
    return VAL_FALSE;
}

static Value bi_equal_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("equal?", 2);
    return VAL_BOOL(values_equal(a[0], a[1]));
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Type predicates
 * ═══════════════════════════════════════════════════════════════════════════ */

#define TYPE_PRED(name, check) \
    static Value bi_##name(Value *a, int n, Env *e) { \
        (void)e; CHECK_ARITY(#name, 1); \
        return VAL_BOOL(check); \
    }

TYPE_PRED(number_p, is_number(a[0]))
TYPE_PRED(integer_p, IS_INT(a[0]) || IS_BIGNUM(a[0]) || (IS_DOUBLE(a[0]) && as_double(a[0]) == floor(as_double(a[0]))))
TYPE_PRED(real_p, is_number(a[0]))
TYPE_PRED(rational_p, IS_INT(a[0]) || IS_BIGNUM(a[0]) || IS_RATIONAL(a[0]) || (IS_DOUBLE(a[0]) && isfinite(as_double(a[0]))))
TYPE_PRED(exact_p, IS_INT(a[0]) || IS_BIGNUM(a[0]) || IS_RATIONAL(a[0]))
TYPE_PRED(inexact_p, IS_DOUBLE(a[0]))
TYPE_PRED(pair_p, IS_PAIR(a[0]))
TYPE_PRED(null_p, IS_NIL(a[0]))
TYPE_PRED(list_p, is_proper_list(a[0]))
TYPE_PRED(symbol_p, IS_SYM(a[0]))
TYPE_PRED(string_p, IS_STRING(a[0]))
TYPE_PRED(char_p, IS_CHAR(a[0]))
TYPE_PRED(vector_p, IS_VECTOR(a[0]))
TYPE_PRED(procedure_p, is_callable(a[0]))
TYPE_PRED(void_p, IS_VOID(a[0]))
TYPE_PRED(eof_object_p, IS_EOF(a[0]))
TYPE_PRED(port_p, IS_PORT(a[0]))
TYPE_PRED(input_port_p, IS_PORT(a[0]) && AS_PORT(a[0])->dir == PORT_INPUT)
TYPE_PRED(output_port_p, IS_PORT(a[0]) && AS_PORT(a[0])->dir == PORT_OUTPUT)
TYPE_PRED(hash_table_p, IS_HASHTABLE(a[0]))

/* ═══════════════════════════════════════════════════════════════════════════
 * Pairs & Lists
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_cons(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("cons", 2);
    return cons(a[0], a[1]);
}
static Value bi_car(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("car", 1); check_pair(a[0]);
    return CAR(a[0]);
}
static Value bi_cdr(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("cdr", 1); check_pair(a[0]);
    return CDR(a[0]);
}
static Value bi_set_car(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("set-car!", 2); check_pair(a[0]);
    AS_PAIR(a[0])->car = a[1];
    return VAL_VOID;
}
static Value bi_set_cdr(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("set-cdr!", 2); check_pair(a[0]);
    AS_PAIR(a[0])->cdr = a[1];
    return VAL_VOID;
}
static Value bi_list(Value *a, int n, Env *e) {
    (void)e;
    return list_to_value(a, n);
}
static Value bi_length(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("length", 1);
    Value *items; int count = value_to_list(a[0], &items);
    ul_free(items);
    return VAL_INT(count);
}
static Value bi_append(Value *a, int n, Env *e) {
    (void)e;
    if (n == 0) return VAL_NIL;
    Value result = a[n - 1];
    for (int i = n - 2; i >= 0; i--) {
        Value *items; int ni = value_to_list(a[i], &items);
        for (int j = ni - 1; j >= 0; j--) result = cons(items[j], result);
        ul_free(items);
    }
    return result;
}
static Value bi_reverse(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("reverse", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    Value result = VAL_NIL;
    for (int i = 0; i < ni; i++) result = cons(items[i], result);
    ul_free(items);
    return result;
}

static Value bi_list_tail(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list-tail", 2);
    int k = (int)as_number_int(a[1]);
    Value lst = a[0];
    for (int i = 0; i < k; i++) { check_pair(lst); lst = CDR(lst); }
    return lst;
}
static Value bi_list_ref(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list-ref", 2);
    int k = (int)as_number_int(a[1]);
    Value lst = a[0];
    for (int i = 0; i < k; i++) { check_pair(lst); lst = CDR(lst); }
    check_pair(lst);
    return CAR(lst);
}
static Value bi_list_set(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list-set!", 3);
    int k = (int)as_number_int(a[1]);
    Value lst = a[0];
    for (int i = 0; i < k; i++) { check_pair(lst); lst = CDR(lst); }
    check_pair(lst);
    AS_PAIR(lst)->car = a[2];
    return VAL_VOID;
}
static Value bi_list_copy(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list-copy", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    Value r = list_to_value(items, ni);
    ul_free(items);
    return r;
}
static Value bi_make_list(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("make-list", 1);
    int len = (int)as_number_int(a[0]);
    Value fill = n > 1 ? a[1] : VAL_FALSE;
    Value r = VAL_NIL;
    for (int i = 0; i < len; i++) r = cons(fill, r);
    return r;
}

static Value bi_iota(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("iota", 1);
    int count = (int)as_number_int(a[0]);
    int start = n > 1 ? (int)as_number_int(a[1]) : 0;
    int step = n > 2 ? (int)as_number_int(a[2]) : 1;
    Value r = VAL_NIL;
    for (int i = count - 1; i >= 0; i--) r = cons(VAL_INT(start + i * step), r);
    return r;
}

static Value bi_memq(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("memq", 2);
    Value lst = a[1];
    while (IS_PAIR(lst)) {
        if (CAR(lst) == a[0] || (IS_INT(CAR(lst)) && IS_INT(a[0]) && as_int(CAR(lst)) == as_int(a[0])))
            return lst;
        lst = CDR(lst);
    }
    return VAL_FALSE;
}

static Value bi_memv(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("memv", 2);
    Value lst = a[1];
    while (IS_PAIR(lst)) {
        if (values_equal(CAR(lst), a[0])) return lst;
        lst = CDR(lst);
    }
    return VAL_FALSE;
}

static Value bi_member(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("member", 2);
    Value lst = a[1];
    while (IS_PAIR(lst)) {
        if (values_equal(CAR(lst), a[0])) return lst;
        lst = CDR(lst);
    }
    return VAL_FALSE;
}

static Value bi_assq(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("assq", 2);
    Value lst = a[1];
    while (IS_PAIR(lst)) {
        Value pair = CAR(lst);
        if (IS_PAIR(pair) && CAR(pair) == a[0]) return pair;
        lst = CDR(lst);
    }
    return VAL_FALSE;
}

static Value bi_assoc(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("assoc", 2);
    Value lst = a[1];
    while (IS_PAIR(lst)) {
        Value pair = CAR(lst);
        if (IS_PAIR(pair) && values_equal(CAR(pair), a[0])) return pair;
        lst = CDR(lst);
    }
    return VAL_FALSE;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Higher-order functions
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_map(Value *a, int n, Env *e) {
    CHECK_MIN_ARITY("map", 2);
    Value proc = a[0];
    /* Collect all lists */
    int nlists = n - 1;
    Value *lists = a + 1;
    Value **items_arr = (Value **)ul_malloc(sizeof(Value *) * nlists);
    int min_len = INT32_MAX;
    for (int i = 0; i < nlists; i++) {
        int ni;
        items_arr[i] = NULL;
        ni = value_to_list(lists[i], &items_arr[i]);
        if (ni < min_len) min_len = ni;
    }

    Value *results = (Value *)ul_malloc_values(sizeof(Value) * min_len);
    Value *call_args = (Value *)ul_malloc_values(sizeof(Value) * nlists);
    for (int j = 0; j < min_len; j++) {
        for (int i = 0; i < nlists; i++) call_args[i] = items_arr[i][j];
        results[j] = call_proc(proc, call_args, nlists, e);
    }
    Value r = list_to_value(results, min_len);

    for (int i = 0; i < nlists; i++) ul_free(items_arr[i]);
    ul_free(items_arr); ul_free(results); ul_free(call_args);
    return r;
}

static Value bi_for_each(Value *a, int n, Env *e) {
    CHECK_MIN_ARITY("for-each", 2);
    Value proc = a[0];
    int nlists = n - 1;
    Value *lists = a + 1;
    Value **items_arr = (Value **)ul_malloc(sizeof(Value *) * nlists);
    int min_len = INT32_MAX;
    for (int i = 0; i < nlists; i++) {
        int ni;
        ni = value_to_list(lists[i], &items_arr[i]);
        if (ni < min_len) min_len = ni;
    }
    Value *call_args = (Value *)ul_malloc_values(sizeof(Value) * nlists);
    for (int j = 0; j < min_len; j++) {
        for (int i = 0; i < nlists; i++) call_args[i] = items_arr[i][j];
        call_proc(proc, call_args, nlists, e);
    }
    for (int i = 0; i < nlists; i++) ul_free(items_arr[i]);
    ul_free(items_arr); ul_free(call_args);
    return VAL_VOID;
}

static Value bi_filter(Value *a, int n, Env *e) {
    CHECK_ARITY("filter", 2);
    Value proc = a[0];
    Value *items; int ni = value_to_list(a[1], &items);
    Value *results = (Value *)ul_malloc_values(sizeof(Value) * ni);
    int nr = 0;
    for (int i = 0; i < ni; i++) {
        Value v = call_proc(proc, &items[i], 1, e);
        if (IS_TRUTHY(v)) results[nr++] = items[i];
    }
    Value r = list_to_value(results, nr);
    ul_free(items); ul_free(results);
    return r;
}

static Value bi_fold_left(Value *a, int n, Env *e) {
    CHECK_ARITY("fold-left", 3);
    Value proc = a[0];
    Value acc = a[1];
    Value *items; int ni = value_to_list(a[2], &items);
    for (int i = 0; i < ni; i++) {
        Value args[2] = {items[i], acc};
        acc = call_proc(proc, args, 2, e);
    }
    ul_free(items);
    return acc;
}

static Value bi_fold_right(Value *a, int n, Env *e) {
    CHECK_ARITY("fold-right", 3);
    Value proc = a[0];
    Value acc = a[1];
    Value *items; int ni = value_to_list(a[2], &items);
    for (int i = ni - 1; i >= 0; i--) {
        Value args[2] = {items[i], acc};
        acc = call_proc(proc, args, 2, e);
    }
    ul_free(items);
    return acc;
}

static Value bi_any(Value *a, int n, Env *e) {
    CHECK_ARITY("any", 2);
    Value *items; int ni = value_to_list(a[1], &items);
    for (int i = 0; i < ni; i++) {
        Value v = call_proc(a[0], &items[i], 1, e);
        if (IS_TRUTHY(v)) { ul_free(items); return v; }
    }
    ul_free(items); return VAL_FALSE;
}

static Value bi_every(Value *a, int n, Env *e) {
    CHECK_ARITY("every", 2);
    Value *items; int ni = value_to_list(a[1], &items);
    for (int i = 0; i < ni; i++) {
        Value v = call_proc(a[0], &items[i], 1, e);
        if (!IS_TRUTHY(v)) { ul_free(items); return VAL_FALSE; }
    }
    ul_free(items); return VAL_TRUE;
}

static Value bi_find(Value *a, int n, Env *e) {
    CHECK_ARITY("find", 2);
    Value *items; int ni = value_to_list(a[1], &items);
    for (int i = 0; i < ni; i++) {
        Value v = call_proc(a[0], &items[i], 1, e);
        if (IS_TRUTHY(v)) { Value r = items[i]; ul_free(items); return r; }
    }
    ul_free(items); return VAL_FALSE;
}

static Value bi_sort(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("sort", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    /* Simple insertion sort */
    for (int i = 1; i < ni; i++) {
        Value key = items[i];
        int j = i - 1;
        while (j >= 0 && num_gt(items[j], key)) {
            items[j + 1] = items[j]; j--;
        }
        items[j + 1] = key;
    }
    Value r = list_to_value(items, ni);
    ul_free(items);
    return r;
}

static Value bi_count(Value *a, int n, Env *e) {
    CHECK_ARITY("count", 2);
    Value *items; int ni = value_to_list(a[1], &items);
    int c = 0;
    for (int i = 0; i < ni; i++) {
        if (IS_TRUTHY(call_proc(a[0], &items[i], 1, e))) c++;
    }
    ul_free(items);
    return VAL_INT(c);
}

static Value bi_apply(Value *a, int n, Env *e) {
    CHECK_MIN_ARITY("apply", 2);
    Value proc = a[0];
    int npre = n - 2;
    Value last = a[n - 1];
    Value *lst; int nlst = value_to_list(last, &lst);
    int total = npre + nlst;
    Value *all = (Value *)ul_malloc_values(sizeof(Value) * total);
    for (int i = 0; i < npre; i++) all[i] = a[i + 1];
    memcpy(all + npre, lst, sizeof(Value) * nlst);
    Value r = call_proc(proc, all, total, e);
    ul_free(lst); ul_free(all);
    return r;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Strings
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_make_string(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("make-string", 1);
    int len = (int)as_number_int(a[0]);
    char fill = n > 1 && IS_CHAR(a[1]) ? (char)AS_CHAR(a[1]) : ' ';
    char *buf = (char *)ul_malloc(len + 1);
    memset(buf, fill, len); buf[len] = '\0';
    Value r = make_string(buf, len, true);
    ul_free(buf);
    return r;
}

static Value bi_string_length(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-length", 1); check_string(a[0]);
    return VAL_INT(AS_STRING(a[0])->len);
}

static Value bi_string_ref(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-ref", 2); check_string(a[0]);
    int idx = (int)as_number_int(a[1]);
    ULString *s = AS_STRING(a[0]);
    if (idx < 0 || idx >= (int)s->len) lisp_error("string-ref: index out of range");
    /* Cast through unsigned char so bytes 0x80..0xFF map to codepoints
     * 128..255 rather than sign-extending to negative ints — emit-stream
     * stores binary op bytes through (make-string 1 (integer->char b))
     * + (string-ref s 0); without this, char->integer of 0xFF returned
     * -1, so u64-le's serialization produced a sentinel-shaped giant. */
    return VAL_CHAR((unsigned char)s->data[idx]);
}

static Value bi_substring(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("substring", 2); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    int start = (int)as_number_int(a[1]);
    int end = n > 2 ? (int)as_number_int(a[2]) : (int)s->len;
    if (start < 0) start = 0;
    if (end > (int)s->len) end = (int)s->len;
    return make_string(s->data + start, end - start, false);
}

static Value bi_string_append(Value *a, int n, Env *e) {
    (void)e;
    size_t total = 0;
    for (int i = 0; i < n; i++) {
        check_string(a[i]);
        total += AS_STRING(a[i])->len;
    }
    char *buf = (char *)ul_malloc(total + 1);
    size_t pos = 0;
    for (int i = 0; i < n; i++) {
        ULString *s = AS_STRING(a[i]);
        memcpy(buf + pos, s->data, s->len);
        pos += s->len;
    }
    buf[pos] = '\0';
    Value r = make_string(buf, pos, false);
    ul_free(buf);
    return r;
}

static Value bi_string_to_list(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string->list", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    Value r = VAL_NIL;
    for (int i = (int)s->len - 1; i >= 0; i--) r = cons(VAL_CHAR(s->data[i]), r);
    return r;
}

static Value bi_list_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list->string", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    char *buf = (char *)ul_malloc(ni + 1);
    for (int i = 0; i < ni; i++) buf[i] = IS_CHAR(items[i]) ? (char)AS_CHAR(items[i]) : '?';
    buf[ni] = '\0';
    ul_free(items);
    Value r = make_string(buf, ni, false);
    ul_free(buf);
    return r;
}

static Value bi_string_to_symbol(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string->symbol", 1); check_string(a[0]);
    return intern(AS_STRING(a[0])->data);
}

static Value bi_symbol_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("symbol->string", 1); check_sym(a[0]);
    return make_string_from_cstr(sym_name(a[0]));
}

static Value bi_string_to_number(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("string->number", 1); check_string(a[0]);
    const char *s = AS_STRING(a[0])->data;
    int base = n > 1 ? (int)as_number_int(a[1]) : 10;
    /* Detect all-digit (optionally signed) shape → integer (fixnum or
     * bignum on overflow). */
    {
        const char *p = s;
        if (*p == '-' || *p == '+') p++;
        bool all_digits = (*p != '\0');
        for (const char *q = p; *q; q++) {
            char c = *q;
            int dval;
            if (c >= '0' && c <= '9') dval = c - '0';
            else if (base > 10 && c >= 'a' && c <= 'z') dval = c - 'a' + 10;
            else if (base > 10 && c >= 'A' && c <= 'Z') dval = c - 'A' + 10;
            else { all_digits = false; break; }
            if (dval >= base) { all_digits = false; break; }
        }
        if (all_digits) {
            char *end;
            errno = 0;
            long long val = strtoll(s, &end, base);
            if (*end == '\0' && errno == 0 && FITS_FIXNUM(val))
                return VAL_INT(val);
            return make_bignum_from_str(s, base);
        }
    }
    char *end;
    errno = 0;
    long long val = strtoll(s, &end, base);
    if (*end == '\0' && errno == 0) return VAL_INT(val);
    if (base == 10) {
        double d = strtod(s, &end);
        if (*end == '\0' && errno == 0) return make_double(d);
        /* Try rational */
        const char *slash = strchr(s, '/');
        if (slash) {
            int64_t num = strtoll(s, NULL, 10);
            int64_t den = strtoll(slash + 1, NULL, 10);
            if (den != 0) return rational_normalize(num, den);
        }
    }
    return VAL_FALSE;
}

static Value bi_string_upcase(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-upcase", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    char *buf = (char *)ul_malloc(s->len + 1);
    for (size_t i = 0; i < s->len; i++) buf[i] = toupper(s->data[i]);
    buf[s->len] = '\0';
    Value r = make_string(buf, s->len, false);
    ul_free(buf);
    return r;
}

static Value bi_string_downcase(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-downcase", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    char *buf = (char *)ul_malloc(s->len + 1);
    for (size_t i = 0; i < s->len; i++) buf[i] = tolower(s->data[i]);
    buf[s->len] = '\0';
    Value r = make_string(buf, s->len, false);
    ul_free(buf);
    return r;
}

static Value bi_string_eq(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string=?", 2); check_string(a[0]); check_string(a[1]);
    ULString *s1 = AS_STRING(a[0]), *s2 = AS_STRING(a[1]);
    return VAL_BOOL(s1->len == s2->len && memcmp(s1->data, s2->data, s1->len) == 0);
}
static Value bi_string_lt(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string<?", 2); check_string(a[0]); check_string(a[1]);
    return VAL_BOOL(strcmp(AS_STRING(a[0])->data, AS_STRING(a[1])->data) < 0);
}

static Value bi_format(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("format", 1);
    check_string(a[0]);
    const char *fmt = AS_STRING(a[0])->data;
    size_t fmtlen = AS_STRING(a[0])->len;
    char buf[4096];
    int pos = 0, ai = 1;
    for (size_t i = 0; i < fmtlen && pos < 4090; i++) {
        if (fmt[i] == '~' && i + 1 < fmtlen) {
            i++;
            switch (fmt[i]) {
                case 'a': { char *s = show(a[ai++], true); pos += snprintf(buf + pos, sizeof(buf) - pos, "%s", s); ul_free(s); break; }
                case 's': { char *s = show(a[ai++], false); pos += snprintf(buf + pos, sizeof(buf) - pos, "%s", s); ul_free(s); break; }
                case '%': buf[pos++] = '\n'; break;
                case '~': buf[pos++] = '~'; break;
                default: buf[pos++] = '~'; buf[pos++] = fmt[i]; break;
            }
        } else {
            buf[pos++] = fmt[i];
        }
    }
    buf[pos] = '\0';
    return make_string_from_cstr(buf);
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Characters
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_char_to_integer(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("char->integer", 1);
    return VAL_INT(AS_CHAR(a[0]));
}
static Value bi_integer_to_char(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("integer->char", 1);
    return VAL_CHAR((int)as_number_int(a[0]));
}
static Value bi_char_alphabetic(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("char-alphabetic?", 1);
    return VAL_BOOL(isalpha(AS_CHAR(a[0])));
}
static Value bi_char_numeric(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("char-numeric?", 1);
    return VAL_BOOL(isdigit(AS_CHAR(a[0])));
}
static Value bi_char_whitespace(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("char-whitespace?", 1);
    return VAL_BOOL(isspace(AS_CHAR(a[0])));
}
static Value bi_char_eq(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("char=?", 2);
    return VAL_BOOL(AS_CHAR(a[0]) == AS_CHAR(a[1]));
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Vectors
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_make_vector(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("make-vector", 1);
    int len = (int)as_number_int(a[0]);
    Value fill = n > 1 ? a[1] : VAL_INT(0);
    return make_vector(len, fill);
}
static Value bi_vector(Value *a, int n, Env *e) {
    (void)e;
    return make_vector_from(a, n);
}
static Value bi_vector_length(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("vector-length", 1);
    return VAL_INT(AS_VECTOR(a[0])->len);
}
static Value bi_vector_ref(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("vector-ref", 2);
    int idx = (int)as_number_int(a[1]);
    return AS_VECTOR(a[0])->data[idx];
}
static Value bi_vector_set(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("vector-set!", 3);
    int idx = (int)as_number_int(a[1]);
    AS_VECTOR(a[0])->data[idx] = a[2];
    return VAL_VOID;
}
static Value bi_vector_to_list(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("vector->list", 1);
    ULVector *v = AS_VECTOR(a[0]);
    return list_to_value(v->data, (int)v->len);
}
static Value bi_list_to_vector(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("list->vector", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    Value r = make_vector_from(items, ni);
    ul_free(items);
    return r;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Hash tables
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_make_hash_table(Value *a, int n, Env *e) {
    (void)e;
    return make_hashtable();
}
static Value bi_hash_table_set(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-set!", 3);
    ht_set(AS_HASHTABLE(a[0]), a[1], a[2]);
    return VAL_VOID;
}
static Value bi_hash_table_ref(Value *a, int n, Env *e) {
    CHECK_MIN_ARITY("hash-table-ref", 2);
    bool found;
    Value v = ht_ref(AS_HASHTABLE(a[0]), a[1], &found);
    if (!found) {
        if (n > 2) return call_proc(a[2], NULL, 0, e);
        lisp_error("hash-table-ref: missing key");
    }
    return v;
}
static Value bi_hash_table_ref_default(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-ref/default", 3);
    bool found;
    Value v = ht_ref(AS_HASHTABLE(a[0]), a[1], &found);
    return found ? v : a[2];
}
static Value bi_hash_table_delete(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-delete!", 2);
    ht_delete(AS_HASHTABLE(a[0]), a[1]);
    return VAL_VOID;
}
static Value bi_hash_table_exists(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-exists?", 2);
    bool found;
    ht_ref(AS_HASHTABLE(a[0]), a[1], &found);
    return VAL_BOOL(found);
}
static Value bi_hash_table_size(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-size", 1);
    return VAL_INT(ht_count(AS_HASHTABLE(a[0])));
}
static Value bi_hash_table_keys(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-keys", 1);
    ULHashTable *ht = AS_HASHTABLE(a[0]);
    Value r = VAL_NIL;
    for (size_t i = 0; i < ht->nbuckets; i++) {
        HTEntry *entry = ht->buckets[i];
        while (entry) { r = cons(entry->key, r); entry = entry->next; }
    }
    return r;
}
static Value bi_hash_table_values(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("hash-table-values", 1);
    ULHashTable *ht = AS_HASHTABLE(a[0]);
    Value r = VAL_NIL;
    for (size_t i = 0; i < ht->nbuckets; i++) {
        HTEntry *entry = ht->buckets[i];
        while (entry) { r = cons(entry->value, r); entry = entry->next; }
    }
    return r;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * I/O
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_display(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("display", 1);
    FILE *out = n > 1 && IS_PORT(a[1]) ? AS_PORT(a[1])->fp : stdout;
    if (!out) out = stdout;
    char *s = show(a[0], true);
    if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_STRING) {
        port_write_str(AS_PORT(a[1]), s, strlen(s));
    } else {
        fputs(s, out);
        fflush(out);
    }
    ul_free(s);
    return VAL_VOID;
}

static Value bi_write(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("write", 1);
    FILE *out = n > 1 && IS_PORT(a[1]) ? AS_PORT(a[1])->fp : stdout;
    if (!out) out = stdout;
    char *s = show(a[0], false);
    if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_STRING) {
        port_write_str(AS_PORT(a[1]), s, strlen(s));
    } else {
        fputs(s, out);
        fflush(out);
    }
    ul_free(s);
    return VAL_VOID;
}

static Value bi_newline(Value *a, int n, Env *e) {
    (void)e;
    FILE *out = n > 0 && IS_PORT(a[0]) ? AS_PORT(a[0])->fp : stdout;
    if (!out) out = stdout;
    fputc('\n', out);
    fflush(out);
    return VAL_VOID;
}

static Value bi_print(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("print", 1);
    char *s = show(a[0], true);
    printf("%s\n", s);
    fflush(stdout);
    ul_free(s);
    return VAL_VOID;
}

static Value bi_read_line(Value *a, int n, Env *e) {
    (void)e;
    if (n > 0 && IS_PORT(a[0])) {
        char *line = port_read_line(AS_PORT(a[0]));
        if (!line) return VAL_EOF;
        Value r = make_string_from_cstr(line);
        ul_free(line);
        return r;
    }
    char buf[4096];
    if (!fgets(buf, sizeof(buf), stdin)) return VAL_EOF;
    size_t len = strlen(buf);
    if (len > 0 && buf[len-1] == '\n') buf[--len] = '\0';
    return make_string(buf, len, false);
}

static Value bi_read_char(Value *a, int n, Env *e) {
    (void)e;
    int ch;
    if (n > 0 && IS_PORT(a[0])) {
        ch = port_read_char(AS_PORT(a[0]));
    } else {
        ch = fgetc(stdin);
    }
    if (ch == EOF) return VAL_EOF;
    return VAL_CHAR(ch);
}

static Value bi_open_input_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("open-input-file", 1); check_string(a[0]);
    FILE *f = fopen(AS_STRING(a[0])->data, "r");
    if (!f) lisp_error("open-input-file: cannot open: %s", AS_STRING(a[0])->data);
    return make_file_port(f, PORT_INPUT);
}
static Value bi_open_output_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("open-output-file", 1); check_string(a[0]);
    FILE *f = fopen(AS_STRING(a[0])->data, "w");
    if (!f) lisp_error("open-output-file: cannot open: %s", AS_STRING(a[0])->data);
    return make_file_port(f, PORT_OUTPUT);
}

/* open-binary-output-file: like open-output-file but opens in w+b mode
 * so emit-stream can stream gates directly to disk AND seek back to
 * rewrite the header once n_ops is known. Pairs with port-set-position!. */
static Value bi_open_binary_output_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("open-binary-output-file", 1); check_string(a[0]);
    FILE *f = fopen(AS_STRING(a[0])->data, "w+b");
    if (!f) lisp_error("open-binary-output-file: cannot open: %s", AS_STRING(a[0])->data);
    return make_file_port(f, PORT_OUTPUT);
}

/* port-set-position!: fseek to absolute byte offset on a file port.
 * Errors on string ports. Returns VOID. */
static Value bi_port_set_position(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("port-set-position!", 2);
    ULPort *p = AS_PORT(a[0]);
    if (p->kind != PORT_FILE) {
        lisp_error("port-set-position!: port must be a file port");
    }
    long offset = (long)as_int(a[1]);
    if (fseek(p->fp, offset, SEEK_SET) != 0) {
        lisp_error("port-set-position!: fseek failed");
    }
    return VAL_VOID;
}
static Value bi_write_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("write-file", 2); check_string(a[0]); check_string(a[1]);
    FILE *f = fopen(AS_STRING(a[0])->data, "w");
    if (!f) return VAL_FALSE;
    ULString *s = AS_STRING(a[1]);
    size_t wrote = fwrite(s->data, 1, s->len, f);
    fclose(f);
    return wrote == (size_t)s->len ? VAL_TRUE : VAL_FALSE;
}
static Value bi_file_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("file->string", 1); check_string(a[0]);
    FILE *f = fopen(AS_STRING(a[0])->data, "rb");
    if (!f) return VAL_FALSE;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    char *buf = ul_malloc(size + 1);
    size_t got = fread(buf, 1, size, f);
    fclose(f);
    buf[got] = '\0';
    Value r = make_string(buf, got, false);
    ul_free(buf);
    return r;
}
static Value bi_open_input_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("open-input-string", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    return make_string_input_port(s->data, s->len);
}

/* ── TCP sockets: fd wrapped in a FILE* via fdopen, unbuffered ─ */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>

static Value wrap_fd_as_port(int fd, PortDir dir) {
    FILE *fp = fdopen(fd, dir == PORT_INPUT ? "rb" : "r+b");
    if (!fp) { close(fd); return VAL_FALSE; }
    setvbuf(fp, NULL, _IONBF, 0);
    return make_file_port(fp, dir);
}

static Value bi_tcp_listen(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-listen", 1);
    int port = (int)as_int(a[0]);
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) return VAL_FALSE;
    int one = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    /* SO_REUSEPORT lets N processes bind() the same port. Kernel hashes
       incoming connections across the listening sockets so each accept()
       returns a fresh client to whichever process is ready. Used by bend
       to run multiple workers behind one port without an external
       distributor. */
    setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons((uint16_t)port);
    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return VAL_FALSE; }
    if (listen(fd, 128) < 0) { close(fd); return VAL_FALSE; }
    return wrap_fd_as_port(fd, PORT_INPUT);
}

static Value bi_tcp_accept(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-accept", 1);
    ULPort *p = AS_PORT(a[0]);
    int server_fd = fileno(p->fp);
    struct sockaddr_in caddr;
    socklen_t clen = sizeof(caddr);
    int cfd = accept(server_fd, (struct sockaddr *)&caddr, &clen);
    if (cfd < 0) return VAL_FALSE;
    return wrap_fd_as_port(cfd, PORT_OUTPUT);
}

static Value bi_tcp_connect(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-connect", 2); check_string(a[0]);
    const char *host = AS_STRING(a[0])->data;
    int port = (int)as_int(a[1]);
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) return VAL_FALSE;
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons((uint16_t)port);
    if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
        struct hostent *he = gethostbyname(host);
        if (!he) { close(fd); return VAL_FALSE; }
        memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
    }
    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return VAL_FALSE; }
    return wrap_fd_as_port(fd, PORT_OUTPUT);
}

static Value bi_tcp_recv(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-recv", 2);
    ULPort *p = AS_PORT(a[0]);
    size_t cap = (size_t)as_int(a[1]);
    if (cap > (1u << 20)) cap = (1u << 20);
    char *buf = ul_malloc(cap + 1);
    int fd = fileno(p->fp);
    ssize_t got = read(fd, buf, cap);
    if (got < 0) { ul_free(buf); return VAL_FALSE; }
    buf[got] = '\0';
    Value r = make_string(buf, got, false);
    ul_free(buf);
    return r;
}

static Value bi_tcp_send(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-send", 2); check_string(a[1]);
    ULPort *p = AS_PORT(a[0]);
    ULString *s = AS_STRING(a[1]);
    int fd = fileno(p->fp);
    ssize_t wrote = write(fd, s->data, s->len);
    if (wrote < 0) return VAL_FALSE;
    return VAL_INT(wrote);
}

static Value bi_tcp_close(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("tcp-close", 1);
    ULPort *p = AS_PORT(a[0]);
    if (p->fp && p->fp != stdin && p->fp != stdout && p->fp != stderr)
        fclose(p->fp);
    p->closed = true;
    return VAL_VOID;
}

#include <sys/wait.h>
/* spawn-process-stdio: fork+exec a child with its stdin & stdout piped
 * back to us; return (stdin-port . stdout-port). Lets gpu-worker.lsp
 * (and any other lumbda code) hold a long-lived subprocess across many
 * request cycles without spawning per request.
 *
 *   (spawn-process-stdio "path/to/binary" '("arg1" "arg2"))
 *     ⇒ (#<port> . #<port>)
 */
static Value bi_spawn_process_stdio(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("spawn-process-stdio", 2); check_string(a[0]);
    const char *path = AS_STRING(a[0])->data;

    /* count args */
    int argc = 1;
    Value lst = a[1];
    while (IS_PAIR(lst)) { argc++; lst = CDR(lst); }
    char **argv = (char**)ul_malloc(sizeof(char*) * (argc + 1));
    argv[0] = (char*)path;
    int i = 1; lst = a[1];
    while (IS_PAIR(lst)) {
        Value v = CAR(lst);
        if (IS_STRING(v)) {
            argv[i++] = AS_STRING(v)->data;
        } else if (IS_SYM(v)) {
            argv[i++] = (char*)sym_name(v);
        } else {
            ul_free(argv);
            lisp_error("spawn-process-stdio: arg must be string or symbol");
        }
        lst = CDR(lst);
    }
    argv[argc] = NULL;

    int in_pipe[2];   /* parent writes → child reads (child's stdin)  */
    int out_pipe[2];  /* child writes  → parent reads (child's stdout)*/
    if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) {
        ul_free(argv); return VAL_FALSE;
    }
    pid_t pid = fork();
    if (pid < 0) {
        close(in_pipe[0]); close(in_pipe[1]);
        close(out_pipe[0]); close(out_pipe[1]);
        ul_free(argv); return VAL_FALSE;
    }
    if (pid == 0) {
        /* child */
        dup2(in_pipe[0], STDIN_FILENO);
        dup2(out_pipe[1], STDOUT_FILENO);
        close(in_pipe[0]); close(in_pipe[1]);
        close(out_pipe[0]); close(out_pipe[1]);
        execvp(path, argv);
        _exit(127);
    }
    /* parent */
    close(in_pipe[0]);
    close(out_pipe[1]);
    ul_free(argv);

    /* line-buffer the parent's write end so daemon sees newlines promptly */
    FILE *win = fdopen(in_pipe[1], "w");
    FILE *rout = fdopen(out_pipe[0], "r");
    if (!win || !rout) {
        if (win) fclose(win); else close(in_pipe[1]);
        if (rout) fclose(rout); else close(out_pipe[0]);
        return VAL_FALSE;
    }
    setvbuf(win, NULL, _IOLBF, 0);
    Value pin  = make_file_port(win,  PORT_OUTPUT);
    Value pout = make_file_port(rout, PORT_INPUT);
    return cons(pin, pout);
}

/* sleep: pause execution for N seconds (integer). Wraps libc sleep(3).
 * Returns void. Real wall-clock wait — yields CPU.
 *
 * Needed for fork-per-accept admission loops (waitpid-nonblock between
 * sleeps) and any cooperative pacing. Without this, scripts busy-loop
 * with (let loop () ... (loop)) which (a) burns CPU and (b) hangs in
 * resource-constrained VMs / fork contexts (SIGKILL after a few sec
 * under cgroup limits — observed 2026-06-11 vm-runner.sh testing).
 */
static Value bi_sleep(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("sleep", 1);
    unsigned int secs = (unsigned int)as_number_int(a[0]);
    sleep(secs);
    return VAL_VOID;
}

/* exit-immediate: _exit() wrapper. No atexit handlers, no stdio
 * flush, no GC cleanup. REQUIRED in fork-self children — calling
 * regular exit() in a forked child runs libc atexit + stdio
 * flush against parent's already-shared state, which can hang
 * or corrupt (seen empirically: minimal `(let ((pid (fork-self)))
 * (exit 0))` hangs the child until SIGTERM).
 *
 * Use (exit-immediate 0) in fork-self child branches. */
static Value bi_exit_immediate(Value *a, int n, Env *e) {
    (void)e;
    _exit(n > 0 ? (int)as_number_int(a[0]) : 0);
    return VAL_VOID;
}

/* fork-self: fork() wrapper.
 *
 *   (fork-self) ⇒ 0 in child, pid in parent, #f on failure
 *
 * Used by gpu-worker.lsp to fork-per-accept — parent immediately
 * returns to accept while child handles the request and exits.
 * Single-PID-many-handler pattern: parent PID persists, ephemeral
 * children carry the per-request work + bend-cuda subprocess.
 *
 * Caller's responsibility: reap children via (waitpid-nonblock) in
 * the accept loop, or set SIGCHLD handler. We do NOT install a
 * default reaper because that would interfere with spawn-process-stdio
 * which expects synchronous waitpid in handle-request.
 */
static Value bi_fork_self(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e; CHECK_ARITY("fork-self", 0);
    pid_t pid = fork();
    if (pid < 0) return VAL_FALSE;
    return VAL_INT((int64_t)pid);
}

/* waitpid-nonblock: reap one completed child (WNOHANG).
 *
 *   (waitpid-nonblock) ⇒ pid (int) if child reaped, 0 if none ready
 *
 * Reaps zombies left by fork-self. Call from accept loop before each
 * accept to keep zombie count bounded.
 */
static Value bi_waitpid_nonblock(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e; CHECK_ARITY("waitpid-nonblock", 0);
    int status;
    pid_t pid = waitpid(-1, &status, WNOHANG);
    if (pid <= 0) return VAL_INT(0);
    return VAL_INT((int64_t)pid);
}

static Value bi_flush_port(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("flush-port", 1);
    ULPort *p = AS_PORT(a[0]);
    if (p->fp) fflush(p->fp);
    return VAL_VOID;
}

/* write-binary-file / read-binary-file: byte-for-byte file I/O,
 * Latin-1 encoded in our string representation (1:1 byte mapping).
 * Used by gpu-worker.lsp's binary wire mode to bypass S-expression
 * serialization on huge payloads. */
static Value bi_write_binary_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("write-binary-file", 2);
    check_string(a[0]); check_string(a[1]);
    const char *path = AS_STRING(a[0])->data;
    ULString *data = AS_STRING(a[1]);
    FILE *f = fopen(path, "wb");
    if (!f) return VAL_FALSE;
    size_t wrote = fwrite(data->data, 1, data->len, f);
    fclose(f);
    return wrote == data->len ? VAL_VOID : VAL_FALSE;
}

static Value bi_append_binary_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("append-binary-file", 2);
    check_string(a[0]); check_string(a[1]);
    const char *path = AS_STRING(a[0])->data;
    ULString *data = AS_STRING(a[1]);
    FILE *f = fopen(path, "ab");
    if (!f) return VAL_FALSE;
    size_t wrote = fwrite(data->data, 1, data->len, f);
    fclose(f);
    return wrote == data->len ? VAL_VOID : VAL_FALSE;
}

/* append-port-to-binary-file: stream a string-output port's buffer
 * directly to a file via fwrite, skipping the Scheme-side string
 * materialization (get-output-string + append-binary-file would copy
 * the buffer twice — at multi-GB body sizes this blows past any VM RAM
 * budget). The port stays usable after; caller closes it. */
static Value bi_append_port_to_binary_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("append-port-to-binary-file", 2);
    check_string(a[0]);
    const char *path = AS_STRING(a[0])->data;
    ULPort *p = AS_PORT(a[1]);
    if (p->kind != PORT_STRING || p->dir != PORT_OUTPUT) {
        lisp_error("append-port-to-binary-file: port must be a string output port");
    }
    FILE *f = fopen(path, "ab");
    if (!f) return VAL_FALSE;
    size_t wrote = fwrite(p->str_buf, 1, p->str_len, f);
    fclose(f);
    return wrote == p->str_len ? VAL_VOID : VAL_FALSE;
}

static Value bi_read_binary_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("read-binary-file", 1);
    check_string(a[0]);
    const char *path = AS_STRING(a[0])->data;
    FILE *f = fopen(path, "rb");
    if (!f) return VAL_FALSE;
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    fseek(f, 0, SEEK_SET);
    char *buf = ul_malloc(sz + 1);
    if (fread(buf, 1, sz, f) != (size_t)sz) { fclose(f); ul_free(buf); return VAL_FALSE; }
    fclose(f);
    buf[sz] = 0;
    Value r = make_string(buf, sz, false);
    ul_free(buf);
    return r;
}

/* walk-circuit-ops / op-specs->bytes / count-lumbda-ops
 *
 * C-tier ports of the three Python primitives at lumbda.py
 * _walk_circuit_ops / _op_specs_to_bytes / _count_lumbda_ops. Each
 * lifts foxhop ecdsa's emit-ops-bin pipeline out of our Scheme tree-
 * walker — same algorithms, native C dispatch.
 *
 * NO_SLOT representation differs across tiers: Python tier stores the
 * literal 18446744073709551615 fixnum (Python big-int). C tier fixnums
 * cap at 48 bits, so we use VAL_FALSE as our slot sentinel inside the
 * op-spec vector; op-specs->bytes packs 0xFFFFFFFFFFFFFFFF when it
 * sees VAL_FALSE. Both tiers produce byte-identical QECCOPS1 output.
 *
 * Kind enum (matches ecdsa/lumbda/emit-ops-bin.lsp):
 *   1 Register, 2 AppendToRegister, 6 X, 7 Z, 8 CX, 9 CZ, 10 Swap,
 *   13 CCX, 14 CCZ.
 */

/* Cached symbol interns — populated lazily on first call; symbol
 * interning is idempotent so repeated calls cost nothing. */
static Value g_sym_alloc, g_sym_free;
static Value g_sym_x, g_sym_z;
static Value g_sym_cx, g_sym_cz;
static Value g_sym_ccx, g_sym_ccz;
static Value g_sym_swap;
static int g_walk_syms_init = 0;

static void walk_syms_init(void) {
    if (g_walk_syms_init) return;
    g_sym_alloc = intern("alloc");
    g_sym_free  = intern("free");
    g_sym_x     = intern("x");
    g_sym_z     = intern("z");
    g_sym_cx    = intern("cx");
    g_sym_cz    = intern("cz");
    g_sym_ccx   = intern("ccx");
    g_sym_ccz   = intern("ccz");
    g_sym_swap  = intern("swap");
    g_walk_syms_init = 1;
}

/* Build a fresh op-spec Vector (7 elements: kind q2 q1 qt ct cc rt).
 * Unused slots take VAL_FALSE as sentinel. */
static Value mk_op_spec(int64_t kind, Value q2, Value q1, Value qt,
                        Value ct, Value cc, Value rt) {
    Value items[7];
    items[0] = VAL_INT(kind);
    items[1] = q2; items[2] = q1; items[3] = qt;
    items[4] = ct; items[5] = cc; items[6] = rt;
    return make_vector_from(items, 7);
}

/* Layout lookup: hash-table from Symbol → fixnum base.
 * Returns the base as int64_t. Caller must ensure name exists. */
static int64_t layout_base(ULHashTable *layout, Value name) {
    bool found;
    Value v = ht_ref(layout, name, &found);
    if (!found) lisp_error("walk-circuit-ops: unknown register");
    return as_int(v);
}

/* Append Register + width × AppendToRegister to result list (in
 * reverse so we can reverse at end). next_q & reg_id updated as
 * declared registers grow the layout. Returns the new next_q. */
static int64_t emit_register_boilerplate(Value *result_head,
                                         Value name, int64_t width,
                                         int64_t next_q,
                                         ULHashTable *layout) {
    int64_t base = next_q;
    int64_t reg_id = base;
    ht_set(layout, name, VAL_INT(base));
    /* Register record. */
    *result_head = cons(mk_op_spec(1,
                                   VAL_FALSE, VAL_FALSE, VAL_FALSE,
                                   VAL_FALSE, VAL_FALSE,
                                   VAL_INT(reg_id)),
                        *result_head);
    /* width × AppendToRegister. */
    for (int64_t i = 0; i < width; i++) {
        *result_head = cons(mk_op_spec(2,
                                       VAL_FALSE, VAL_FALSE,
                                       VAL_INT(base + i),
                                       VAL_FALSE, VAL_FALSE,
                                       VAL_INT(reg_id)),
                            *result_head);
    }
    return next_q + width;
}

/* Reverse a Scheme list in place via cons rebuild. */
static Value list_reverse(Value lst) {
    Value r = VAL_NIL;
    while (IS_PAIR(lst)) {
        r = cons(CAR(lst), r);
        lst = CDR(lst);
    }
    return r;
}

static Value bi_walk_circuit_ops(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("walk-circuit-ops", 2);
    walk_syms_init();

    Value registers_lst = a[0];
    Value ops_lst       = a[1];

    Value layout_v = make_hashtable();
    ULHashTable *layout = AS_HASHTABLE(layout_v);
    int64_t next_q = 0;
    Value result_head = VAL_NIL;   /* built in reverse */

    /* Declared registers first (boilerplate). */
    while (IS_PAIR(registers_lst)) {
        Value rec = CAR(registers_lst);
        /* rec = (name width) */
        Value name = CAR(rec);
        int64_t width = as_int(CADR(rec));
        next_q = emit_register_boilerplate(&result_head, name, width,
                                           next_q, layout);
        registers_lst = CDR(registers_lst);
    }

    /* Walk ops. */
    while (IS_PAIR(ops_lst)) {
        Value op = CAR(ops_lst);
        Value tag = CAR(op);
        Value rest = CDR(op);

        if (tag == g_sym_ccx) {
            Value c1 = CAR(rest);
            Value c2 = CAR(CDR(rest));
            Value tgt = CAR(CDR(CDR(rest)));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(13,
                                          VAL_INT(q2), VAL_INT(q1), VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_cx) {
            Value c1 = CAR(rest);
            Value tgt = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(8,
                                          VAL_FALSE, VAL_INT(q1), VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_x) {
            Value tgt = CAR(rest);
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(6,
                                          VAL_FALSE, VAL_FALSE, VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_alloc) {
            Value name = CAR(rest);
            int64_t width = as_int(CADR(rest));
            next_q = emit_register_boilerplate(&result_head, name, width,
                                               next_q, layout);
        } else if (tag == g_sym_free) {
            Value name = CAR(rest);
            ht_delete(layout, name);
        } else if (tag == g_sym_z) {
            Value tgt = CAR(rest);
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(7,
                                          VAL_FALSE, VAL_FALSE, VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_cz) {
            Value c1 = CAR(rest);
            Value tgt = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(9,
                                          VAL_FALSE, VAL_INT(q1), VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_swap) {
            Value aa = CAR(rest);
            Value bb = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(aa)) + as_int(CADR(aa));
            int64_t qt = layout_base(layout, CAR(bb)) + as_int(CADR(bb));
            result_head = cons(mk_op_spec(10,
                                          VAL_FALSE, VAL_INT(q1), VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else if (tag == g_sym_ccz) {
            Value c1 = CAR(rest);
            Value c2 = CAR(CDR(rest));
            Value tgt = CAR(CDR(CDR(rest)));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            result_head = cons(mk_op_spec(14,
                                          VAL_INT(q2), VAL_INT(q1), VAL_INT(qt),
                                          VAL_FALSE, VAL_FALSE, VAL_FALSE),
                               result_head);
        } else {
            lisp_error("walk-circuit-ops: unknown op tag");
        }

        ops_lst = CDR(ops_lst);
    }

    return list_reverse(result_head);
}

/* Pack a single u64 into 8 LE bytes at buf. VAL_FALSE → NO_SLOT
 * (0xFFFFFFFFFFFFFFFF). Otherwise the fixnum value as unsigned. */
static void pack_u64_slot(char *buf, Value v) {
    uint64_t u;
    if (v == VAL_FALSE) {
        u = 0xFFFFFFFFFFFFFFFFULL;
    } else if (IS_BIGNUM(v)) {
        /* Bignum sentinel (e.g. *no-slot* = 2^64-1) — extract low 64
         * bits of magnitude. Python tier passes the literal big-int so
         * cross-tier emit-stream code stays unchanged. */
        Bignum *b = AS_BIGNUM(v);
        u = b->n_limbs > 0 ? b->limbs[0] : 0;
        if (b->sign < 0) u = ~u + 1;
    } else {
        u = (uint64_t)as_int(v);
    }
    buf[0] = (char)(u & 0xFF);
    buf[1] = (char)((u >> 8) & 0xFF);
    buf[2] = (char)((u >> 16) & 0xFF);
    buf[3] = (char)((u >> 24) & 0xFF);
    buf[4] = (char)((u >> 32) & 0xFF);
    buf[5] = (char)((u >> 40) & 0xFF);
    buf[6] = (char)((u >> 48) & 0xFF);
    buf[7] = (char)((u >> 56) & 0xFF);
}

static Value bi_op_specs_to_bytes(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("op-specs->bytes", 1);

    /* Two-pass: count first to size the buffer exactly. */
    size_t count = 0;
    {
        Value lst = a[0];
        while (IS_PAIR(lst)) { count++; lst = CDR(lst); }
    }
    size_t total = count * 56;
    char *buf = (char *)ul_malloc(total + 1);

    Value lst = a[0];
    size_t pos = 0;
    while (IS_PAIR(lst)) {
        Value v = CAR(lst);
        if (!IS_VECTOR(v)) lisp_error("op-specs->bytes: not a vector");
        ULVector *vec = AS_VECTOR(v);
        if (vec->len < 7) lisp_error("op-specs->bytes: short vector");
        /* u32 kind */
        uint32_t kind = (uint32_t)as_int(vec->data[0]);
        buf[pos++] = (char)(kind & 0xFF);
        buf[pos++] = (char)((kind >> 8) & 0xFF);
        buf[pos++] = (char)((kind >> 16) & 0xFF);
        buf[pos++] = (char)((kind >> 24) & 0xFF);
        /* u32 pad (zero) */
        buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0;
        /* 6× u64 slots */
        for (int i = 1; i <= 6; i++) {
            pack_u64_slot(buf + pos, vec->data[i]);
            pos += 8;
        }
        lst = CDR(lst);
    }
    buf[pos] = 0;
    Value r = make_string(buf, total, false);
    ul_free(buf);
    return r;
}

static Value bi_count_lumbda_ops(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("count-lumbda-ops", 1);
    walk_syms_init();

    int64_t tof = 0, cli = 0, tot = 0;
    Value lst = a[0];
    while (IS_PAIR(lst)) {
        Value op = CAR(lst);
        Value tag = IS_PAIR(op) ? CAR(op) : op;
        tot++;
        if (tag == g_sym_ccx) tof++;
        else if (tag == g_sym_x || tag == g_sym_cx) cli++;
        lst = CDR(lst);
    }
    Value items[3];
    items[0] = VAL_INT(tof);
    items[1] = VAL_INT(cli);
    items[2] = VAL_INT(tot);
    return make_vector_from(items, 3);
}

/* emit-circuit-to-ops-bin-stream — streaming-emit walker that writes
 * each 56-byte QECCOPS1 op record directly to a file as it is produced.
 * O(1) host memory regardless of n_ops. C-tier port of lumbda.py
 * _emit_circuit_to_ops_bin_stream. Byte-identical output across tiers.
 *
 * Calling shape (matches Python tier):
 *   (emit-circuit-to-ops-bin-stream out-path registers ops)
 *     out-path   string  — destination file
 *     registers  list of (name width) pairs (declared registers)
 *     ops        list of (tag …) op records (lumbda ops, same shape as
 *                walk-circuit-ops input)
 *   → returns n_ops written (fixnum)
 *
 * File format (QECCOPS1):
 *   magic   8B   "QECCOPS1"
 *   n_ops   8B   u64 LE (placeholder zero, patched at tail via fseek)
 *   body    n_ops × 56B  (matches op-specs->bytes layout exactly)
 *
 * Per-op record layout (56 B little-endian):
 *   u32 kind | u32 pad=0 | u64 q2 | u64 q1 | u64 qt | u64 ct | u64 cc | u64 rt
 *
 * Kind dispatch & layout mirror bi_walk_circuit_ops (above) — single
 * source of truth. NO_SLOT representation: 0xFFFFFFFFFFFFFFFF, written
 * directly into u64 slots that the Scheme side did not populate.
 *
 * Buffering: libc's default fwrite buffer (~4 KB) plus the 56-byte
 * records means each disk write covers ~73 ops — same throughput class
 * as the Python tier's 8 KiB manual batch. fflush + fclose at the tail
 * commit everything before the function returns.
 *
 * Layout map: ULHashTable Symbol → fixnum base (same as walk-circuit-ops).
 * Returning the symbol's qubit base; we never need width after the
 * Register/Append boilerplate is emitted, so we store just the base. */
static void pack_op_record(char *buf, uint32_t kind,
                           uint64_t q2, uint64_t q1, uint64_t qt,
                           uint64_t ct, uint64_t cc, uint64_t rt) {
    /* u32 kind */
    buf[0] = (char)(kind & 0xFF);
    buf[1] = (char)((kind >> 8) & 0xFF);
    buf[2] = (char)((kind >> 16) & 0xFF);
    buf[3] = (char)((kind >> 24) & 0xFF);
    /* u32 pad */
    buf[4] = 0; buf[5] = 0; buf[6] = 0; buf[7] = 0;
    /* 6× u64 LE */
    uint64_t u[6] = { q2, q1, qt, ct, cc, rt };
    for (int i = 0; i < 6; i++) {
        size_t off = 8 + i * 8;
        uint64_t x = u[i];
        buf[off + 0] = (char)(x & 0xFF);
        buf[off + 1] = (char)((x >> 8) & 0xFF);
        buf[off + 2] = (char)((x >> 16) & 0xFF);
        buf[off + 3] = (char)((x >> 24) & 0xFF);
        buf[off + 4] = (char)((x >> 32) & 0xFF);
        buf[off + 5] = (char)((x >> 40) & 0xFF);
        buf[off + 6] = (char)((x >> 48) & 0xFF);
        buf[off + 7] = (char)((x >> 56) & 0xFF);
    }
}

/* Write one op record to f, bumping count. Returns 0 on success, -1 on
 * fwrite failure (caller closes & errors). */
#define NO_SLOT_U64 0xFFFFFFFFFFFFFFFFULL
static int emit_one(FILE *f, uint64_t *count,
                    uint32_t kind,
                    uint64_t q2, uint64_t q1, uint64_t qt,
                    uint64_t ct, uint64_t cc, uint64_t rt) {
    char rec[56];
    pack_op_record(rec, kind, q2, q1, qt, ct, cc, rt);
    if (fwrite(rec, 1, 56, f) != 56) return -1;
    (*count)++;
    return 0;
}

/* Emit Register + width × AppendToRegister boilerplate.
 * Reg-id = base = next_q at time of emit (matches Python tier semantics:
 * reg_id = base in _emit_circuit_to_ops_bin_stream::emit_register).
 * Updates layout (name → base) and returns new next_q.
 * Returns -1 on fwrite failure, 0 on success. */
static int emit_register_stream(FILE *f, uint64_t *count,
                                ULHashTable *layout,
                                Value name, int64_t width,
                                int64_t *next_q) {
    int64_t base = *next_q;
    uint64_t reg_id = (uint64_t)base;
    ht_set(layout, name, VAL_INT(base));
    /* Register record. */
    if (emit_one(f, count, 1,
                 NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64,
                 NO_SLOT_U64, NO_SLOT_U64, reg_id) < 0) return -1;
    /* width × AppendToRegister. */
    for (int64_t i = 0; i < width; i++) {
        if (emit_one(f, count, 2,
                     NO_SLOT_U64, NO_SLOT_U64,
                     (uint64_t)(base + i),
                     NO_SLOT_U64, NO_SLOT_U64, reg_id) < 0) return -1;
    }
    *next_q += width;
    return 0;
}

static Value bi_emit_circuit_to_ops_bin_stream(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("emit-circuit-to-ops-bin-stream", 3);
    check_string(a[0]);
    walk_syms_init();

    const char *out_path = AS_STRING(a[0])->data;
    Value registers_lst = a[1];
    Value ops_lst       = a[2];

    FILE *f = fopen(out_path, "wb");
    if (!f) lisp_error("emit-circuit-to-ops-bin-stream: cannot open %s", out_path);

    /* Header: magic + zero placeholder for n_ops (patched at tail). */
    if (fwrite("QECCOPS1", 1, 8, f) != 8) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: write magic failed");
    }
    char zero8[8] = {0,0,0,0,0,0,0,0};
    if (fwrite(zero8, 1, 8, f) != 8) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: write header n_ops placeholder failed");
    }

    Value layout_v = make_hashtable();
    ULHashTable *layout = AS_HASHTABLE(layout_v);
    int64_t next_q = 0;
    uint64_t count = 0;
    int err = 0;

    /* Declared registers first (boilerplate before ops). */
    while (IS_PAIR(registers_lst)) {
        Value rec = CAR(registers_lst);
        Value name = CAR(rec);
        int64_t width = as_int(CADR(rec));
        if (emit_register_stream(f, &count, layout, name, width, &next_q) < 0) {
            err = 1; break;
        }
        registers_lst = CDR(registers_lst);
    }

    /* Walk ops. */
    while (!err && IS_PAIR(ops_lst)) {
        Value op = CAR(ops_lst);
        Value tag = CAR(op);
        Value rest = CDR(op);

        if (tag == g_sym_ccx) {
            Value c1 = CAR(rest);
            Value c2 = CAR(CDR(rest));
            Value tgt = CAR(CDR(CDR(rest)));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 13,
                         (uint64_t)q2, (uint64_t)q1, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_cx) {
            Value c1 = CAR(rest);
            Value tgt = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 8,
                         NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_x) {
            Value tgt = CAR(rest);
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 6,
                         NO_SLOT_U64, NO_SLOT_U64, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_alloc) {
            Value name = CAR(rest);
            int64_t width = as_int(CADR(rest));
            if (emit_register_stream(f, &count, layout, name, width, &next_q) < 0) err = 1;
        } else if (tag == g_sym_free) {
            Value name = CAR(rest);
            ht_delete(layout, name);
        } else if (tag == g_sym_z) {
            Value tgt = CAR(rest);
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 7,
                         NO_SLOT_U64, NO_SLOT_U64, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_cz) {
            Value c1 = CAR(rest);
            Value tgt = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 9,
                         NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_swap) {
            Value aa = CAR(rest);
            Value bb = CAR(CDR(rest));
            int64_t q1 = layout_base(layout, CAR(aa)) + as_int(CADR(aa));
            int64_t qt = layout_base(layout, CAR(bb)) + as_int(CADR(bb));
            if (emit_one(f, &count, 10,
                         NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else if (tag == g_sym_ccz) {
            Value c1 = CAR(rest);
            Value c2 = CAR(CDR(rest));
            Value tgt = CAR(CDR(CDR(rest)));
            int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
            int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
            int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
            if (emit_one(f, &count, 14,
                         (uint64_t)q2, (uint64_t)q1, (uint64_t)qt,
                         NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
        } else {
            fclose(f);
            lisp_error("emit-circuit-to-ops-bin-stream: unknown op tag");
        }
        ops_lst = CDR(ops_lst);
    }

    if (err) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: fwrite failed");
    }

    /* Patch n_ops header. u64 LE at offset 8. */
    if (fflush(f) != 0) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: fflush failed");
    }
    if (fseek(f, 8, SEEK_SET) != 0) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: fseek failed");
    }
    char nbuf[8];
    uint64_t nu = count;
    nbuf[0] = (char)(nu & 0xFF);
    nbuf[1] = (char)((nu >> 8) & 0xFF);
    nbuf[2] = (char)((nu >> 16) & 0xFF);
    nbuf[3] = (char)((nu >> 24) & 0xFF);
    nbuf[4] = (char)((nu >> 32) & 0xFF);
    nbuf[5] = (char)((nu >> 40) & 0xFF);
    nbuf[6] = (char)((nu >> 48) & 0xFF);
    nbuf[7] = (char)((nu >> 56) & 0xFF);
    if (fwrite(nbuf, 1, 8, f) != 8) {
        fclose(f);
        lisp_error("emit-circuit-to-ops-bin-stream: header patch write failed");
    }
    if (fclose(f) != 0) {
        lisp_error("emit-circuit-to-ops-bin-stream: fclose failed");
    }

    return VAL_INT((int64_t)count);
}
#undef NO_SLOT_U64

/* heap-snapshot / heap-restore: asm-only arena primitives. The asm impl
 * has no GC; these let a server rewind its bump allocator between
 * requests. Python + C have real GCs — no-ops here so portable .lsp
 * code can call them unconditionally. */
static Value bi_heap_snapshot(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e;
    return VAL_FALSE;
}
static Value bi_heap_restore(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e;
    return VAL_VOID;
}

#include <time.h>
static Value bi_current_time_ms(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e;
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    int64_t ms = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
    return VAL_INT(ms);
}

static Value bi_read_from_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("read-from-string", 1); check_string(a[0]);
    int count = 0;
    Value *exprs = read_all(AS_STRING(a[0])->data, &count, false);
    Value r = (count > 0) ? exprs[0] : VAL_FALSE;
    ul_free(exprs);
    return r;
}
static Value bi_open_output_string(Value *a, int n, Env *e) {
    (void)e;
    return make_string_output_port();
}
static Value bi_get_output_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("get-output-string", 1);
    /* Use port's known str_len, not strlen — emitter writes binary
     * (op bytes with embedded 0x00). strlen truncates at first null. */
    ULPort *p = AS_PORT(a[0]);
    if (p->kind != PORT_STRING || p->dir != PORT_OUTPUT) {
        return make_string("", 0, false);
    }
    return make_string(p->str_buf, p->str_len, false);
}
static Value bi_close_port(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("close-port", 1);
    ULPort *p = AS_PORT(a[0]);
    if (p->fp && p->fp != stdin && p->fp != stdout && p->fp != stderr)
        fclose(p->fp);
    p->closed = true;
    return VAL_VOID;
}
static Value bi_eof_object(Value *a, int n, Env *e) {
    (void)e; return VAL_EOF;
}
static Value bi_void(Value *a, int n, Env *e) {
    (void)e; return VAL_VOID;
}

static Value bi_current_input_port(Value *a, int n, Env *e) {
    (void)e; return make_file_port(stdin, PORT_INPUT);
}
static Value bi_current_output_port(Value *a, int n, Env *e) {
    (void)e; return make_file_port(stdout, PORT_OUTPUT);
}
static Value bi_current_error_port(Value *a, int n, Env *e) {
    (void)e; return make_file_port(stderr, PORT_OUTPUT);
}
static Value bi_flush_output_port(Value *a, int n, Env *e) {
    (void)e;
    if (n > 0 && IS_PORT(a[0]) && AS_PORT(a[0])->fp) fflush(AS_PORT(a[0])->fp);
    else fflush(stdout);
    return VAL_VOID;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * File system
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_file_exists(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("file-exists?", 1); check_string(a[0]);
    struct stat st;
    return VAL_BOOL(stat(AS_STRING(a[0])->data, &st) == 0);
}
static Value bi_current_directory(Value *a, int n, Env *e) {
    (void)e;
    char buf[4096];
    if (!getcwd(buf, sizeof(buf))) return make_string_from_cstr(".");
    return make_string_from_cstr(buf);
}
static Value bi_rename_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("rename-file", 2);
    check_string(a[0]); check_string(a[1]);
    if (rename(AS_STRING(a[0])->data, AS_STRING(a[1])->data) != 0) {
        lisp_error("rename-file: %s", strerror(errno));
    }
    return VAL_VOID;
}
static Value bi_delete_file(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("delete-file", 1);
    check_string(a[0]);
    if (unlink(AS_STRING(a[0])->data) != 0) {
        lisp_error("delete-file: %s", strerror(errno));
    }
    return VAL_VOID;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * System
 * ═══════════════════════════════════════════════════════════════════════════ */

static Value bi_exit(Value *a, int n, Env *e) {
    (void)e;
    exit(n > 0 ? (int)as_number_int(a[0]) : 0);
    return VAL_VOID;
}

static Value bi_error(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("error", 1);
    char *msg = show(a[0], true);
    Value *irr = n > 1 ? a + 1 : NULL;
    int nirr = n - 1;
    Value obj = make_error_object(msg, irr, nirr);
    ErrorObject *eo = AS_ERROR(obj);
    char full_msg[MAX_ERROR_MSG];
    int off = snprintf(full_msg, sizeof(full_msg), "%s", eo->message);
    for (int i = 0; i < nirr && off < MAX_ERROR_MSG - 2; i++) {
        char *s = show(irr[i], false);
        off += snprintf(full_msg + off, sizeof(full_msg) - off, ": %s", s);
        ul_free(s);
    }
    ul_free(msg);
    lisp_error_with_obj(obj, "%s", full_msg);
    return VAL_NIL;
}

static Value bi_error_object_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("error-object?", 1);
    return VAL_BOOL(IS_ERROR_OBJ(a[0]));
}

static Value bi_error_object_message(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("error-object-message", 1);
    if (IS_ERROR_OBJ(a[0])) return make_string_from_cstr(AS_ERROR(a[0])->message);
    char *s = show(a[0], true);
    Value r = make_string_from_cstr(s);
    ul_free(s);
    return r;
}

static Value bi_error_object_irritants(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("error-object-irritants", 1);
    if (IS_ERROR_OBJ(a[0])) {
        ErrorObject *eo = AS_ERROR(a[0]);
        return list_to_value(eo->irritants, eo->nirritants);
    }
    return VAL_NIL;
}

static Value bi_current_time(Value *a, int n, Env *e) {
    (void)e;
    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    return make_double(ts.tv_sec + ts.tv_nsec / 1e9);
}

static Value bi_current_jiffy(Value *a, int n, Env *e) {
    (void)e;
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return VAL_INT(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}

static Value bi_jiffies_per_second(Value *a, int n, Env *e) {
    (void)e; return VAL_INT(1000);
}

static Value bi_command_line(Value *a, int n, Env *e) {
    (void)a; (void)n; (void)e;
    return VAL_NIL; /* will be set by main */
}

static Value bi_get_environment_variable(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("get-environment-variable", 1); check_string(a[0]);
    const char *val = getenv(AS_STRING(a[0])->data);
    if (!val) return VAL_FALSE;
    return make_string_from_cstr(val);
}

static Value bi_gensym(Value *a, int n, Env *e) {
    (void)e;
    static int counter = 0;
    char buf[32];
    snprintf(buf, sizeof(buf), "g%d", counter++);
    return intern(buf);
}

static Value bi_make_parameter(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("make-parameter", 1);
    /* We store the parameter value in a vector of size 1 */
    Value box = make_vector(1, a[0]);
    /* If converter provided, apply it */
    if (n > 1) {
        Value args[1] = {a[0]};
        AS_VECTOR(box)->data[0] = call_proc(a[1], args, 1, e);
    }
    /* Return a closure: 0 args → get, 1 arg → set */
    /* We need to create a Scheme closure that captures the box */
    /* Use a builtin with captured state via a trick: store box in an env */
    Env *closure_env = make_env(e);
    env_define(closure_env, intern("__param_box__"), box);
    if (n > 1) env_define(closure_env, intern("__param_converter__"), a[1]);
    else env_define(closure_env, intern("__param_converter__"), VAL_FALSE);

    char src[256];
    snprintf(src, sizeof(src),
        "(lambda args "
        "  (if (null? args) "
        "    (vector-ref __param_box__ 0) "
        "    (begin (vector-set! __param_box__ 0 "
        "             (if __param_converter__ (__param_converter__ (car args)) (car args))) "
        "           (void))))");
    int nc;
    Value *exprs = read_all(src, &nc, false);
    Value proc = leval(exprs[0], closure_env);
    ul_free(exprs);
    return proc;
}

static Value bi_values(Value *a, int n, Env *e) {
    (void)e;
    if (n == 1) return a[0];
    return make_vector_from(a, n);
}

static Value bi_call_with_values(Value *a, int n, Env *e) {
    CHECK_ARITY("call-with-values", 2);
    Value r = call_proc(a[0], NULL, 0, e);
    if (IS_VECTOR(r)) {
        ULVector *v = AS_VECTOR(r);
        return call_proc(a[1], v->data, (int)v->len, e);
    }
    Value args[1] = {r};
    return call_proc(a[1], args, 1, e);
}

static Value bi_object_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("object->string", 1);
    char *s = show(a[0], false);
    Value r = make_string_from_cstr(s);
    ul_free(s);
    return r;
}

static Value bi_display_to_string(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("display-to-string", 1);
    char *s = show(a[0], true);
    Value r = make_string_from_cstr(s);
    ul_free(s);
    return r;
}

static Value bi_string_copy(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-copy", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    return make_string(s->data, s->len, true);
}

static Value bi_string_set(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-set!", 3); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    if (!s->mutable) lisp_error("string-set!: immutable string");
    int idx = (int)as_number_int(a[1]);
    s->data[idx] = (char)AS_CHAR(a[2]);
    return VAL_VOID;
}

static Value bi_string_contains(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-contains", 2);
    check_string(a[0]); check_string(a[1]);
    return VAL_BOOL(strstr(AS_STRING(a[0])->data, AS_STRING(a[1])->data) != NULL);
}

static Value bi_string_join(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("string-join", 1);
    Value *items; int ni = value_to_list(a[0], &items);
    const char *sep = n > 1 ? AS_STRING(a[1])->data : " ";
    size_t seplen = strlen(sep);
    size_t total = 0;
    for (int i = 0; i < ni; i++) {
        check_string(items[i]);
        total += AS_STRING(items[i])->len;
        if (i > 0) total += seplen;
    }
    char *buf = (char *)ul_malloc(total + 1);
    size_t pos = 0;
    for (int i = 0; i < ni; i++) {
        if (i > 0) { memcpy(buf + pos, sep, seplen); pos += seplen; }
        ULString *s = AS_STRING(items[i]);
        memcpy(buf + pos, s->data, s->len);
        pos += s->len;
    }
    buf[pos] = '\0';
    ul_free(items);
    Value r = make_string(buf, pos, false);
    ul_free(buf);
    return r;
}

static Value bi_string_split(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("string-split", 1);
    check_string(a[0]);
    const char *str = AS_STRING(a[0])->data;
    const char *sep = n > 1 ? AS_STRING(a[1])->data : " ";
    size_t seplen = strlen(sep);

    Value result = VAL_NIL;
    Value *items = NULL;
    int nitems = 0, cap = 16;
    items = (Value *)ul_malloc_values(sizeof(Value) * cap);

    const char *p = str;
    while (*p) {
        const char *found = strstr(p, sep);
        if (!found) {
            if (nitems >= cap) { cap *= 2; items = (Value *)ul_realloc_values(items, sizeof(Value) * cap); }
            items[nitems++] = make_string(p, strlen(p), false);
            break;
        }
        if (nitems >= cap) { cap *= 2; items = (Value *)ul_realloc_values(items, sizeof(Value) * cap); }
        items[nitems++] = make_string(p, found - p, false);
        p = found + seplen;
    }
    result = list_to_value(items, nitems);
    ul_free(items);
    return result;
}

/* Record type subtype check — used by define-record-type predicates */
static Value bi_is_subtype_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("__is-subtype?__", 2);
    check_string(a[0]); check_string(a[1]);
    return VAL_BOOL(is_subtype(AS_STRING(a[0])->data, AS_STRING(a[1])->data));
}

static Value bi_identity(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("identity", 1);
    return a[0];
}

static Value bi_string_trim(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-trim", 1); check_string(a[0]);
    const char *s = AS_STRING(a[0])->data;
    size_t len = AS_STRING(a[0])->len;
    while (len > 0 && isspace(s[0])) { s++; len--; }
    while (len > 0 && isspace(s[len-1])) { len--; }
    return make_string(s, len, false);
}

static Value bi_string_index(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-index", 2); check_string(a[0]); check_string(a[1]);
    const char *found = strstr(AS_STRING(a[0])->data, AS_STRING(a[1])->data);
    if (!found) return VAL_INT(-1);
    return VAL_INT(found - AS_STRING(a[0])->data);
}

static Value bi_string_replace(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("string-replace", 3);
    check_string(a[0]); check_string(a[1]); check_string(a[2]);
    const char *src = AS_STRING(a[0])->data;
    const char *from = AS_STRING(a[1])->data;
    const char *to = AS_STRING(a[2])->data;
    size_t from_len = strlen(from), to_len = strlen(to);
    if (from_len == 0) return a[0];

    /* Use strstr (libc-tuned, often Boyer-Moore-Horspool) to skip the
     * O(N*k) hand-rolled strncmp-at-every-position sediment. */
    char buf[8192];
    size_t pos = 0;
    const char *cur = src;
    const char *hit;
    while ((hit = strstr(cur, from)) != NULL) {
        size_t chunk = (size_t)(hit - cur);
        if (pos + chunk + to_len >= sizeof(buf) - 1) break;
        memcpy(buf + pos, cur, chunk); pos += chunk;
        memcpy(buf + pos, to, to_len); pos += to_len;
        cur = hit + from_len;
    }
    size_t tail = strlen(cur);
    if (pos + tail >= sizeof(buf)) tail = sizeof(buf) - pos - 1;
    memcpy(buf + pos, cur, tail); pos += tail;
    buf[pos] = '\0';
    return make_string_from_cstr(buf);
}

static Value bi_auto_compile(Value *a, int n, Env *e) {
    (void)e;
    if (n == 0) return VAL_BOOL(g_auto_compile);
    g_auto_compile = IS_TRUTHY(a[0]);
    return VAL_VOID;
}

static Value bi_compile(Value *a, int n, Env *e) {
    CHECK_ARITY("compile", 1);
    if (!IS_PROC(a[0])) lisp_error("compile: not a procedure");
    CompiledProc *cp = compile_proc(AS_PROC(a[0]), e);
    return VAL_PTR(cp);
}

static Value bi_compiled_p(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("compiled?", 1);
    return VAL_BOOL(IS_COMPILED_PROC(a[0]));
}

static Value bi_procedure_name(Value *a, int n, Env *e) {
    (void)e; CHECK_ARITY("procedure-name", 1);
    if (IS_PROC(a[0])) {
        const char *name = AS_PROC(a[0])->name;
        return name ? make_string_from_cstr(name) : VAL_FALSE;
    }
    if (IS_COMPILED_PROC(a[0])) {
        const char *name = AS_COMPILED_PROC(a[0])->name;
        return name ? make_string_from_cstr(name) : VAL_FALSE;
    }
    return VAL_FALSE;
}

static Value bi_write_string(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("write-string", 1); check_string(a[0]);
    ULString *s = AS_STRING(a[0]);
    if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_STRING) {
        port_write_str(AS_PORT(a[1]), s->data, s->len);
    } else if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_FILE) {
        /* Use fwrite with known s->len — fputs strlen-truncates and
         * loses any 0x00 in the payload (breaks binary emit through
         * a file port). */
        FILE *out = AS_PORT(a[1])->fp;
        fwrite(s->data, 1, s->len, out);
    } else {
        FILE *out = (n > 1 && IS_PORT(a[1])) ? AS_PORT(a[1])->fp : stdout;
        fwrite(s->data, 1, s->len, out ? out : stdout);
        fflush(out ? out : stdout);
    }
    return VAL_VOID;
}

static Value bi_write_char(Value *a, int n, Env *e) {
    (void)e; CHECK_MIN_ARITY("write-char", 1);
    /* Route to string-port buffer when target is a string port — without
     * this, write-char ignored string ports and wrote to stdout, breaking
     * binary emit through (open-output-string) accumulators. */
    if (n > 1 && IS_PORT(a[1]) && AS_PORT(a[1])->kind == PORT_STRING) {
        char ch = (char)AS_CHAR(a[0]);
        port_write_str(AS_PORT(a[1]), &ch, 1);
        return VAL_VOID;
    }
    FILE *out = n > 1 && IS_PORT(a[1]) ? AS_PORT(a[1])->fp : stdout;
    fputc(AS_CHAR(a[0]), out ? out : stdout);
    /* Per-byte fflush only when writing to stdout for interactive REPL
     * feedback. File ports buffer until close (or explicit flush-port)
     * — emit-stream calls write-char millions of times per second and
     * per-write fflush would tank throughput. */
    if (!(n > 1 && IS_PORT(a[1]))) fflush(stdout);
    return VAL_VOID;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * Prelude — standard macros defined in Scheme
 * ═══════════════════════════════════════════════════════════════════════════ */

const char *PRELUDE =
"(define-macro (when test . body)\n"
"  `(if ,test (begin ,@body) (void)))\n"
"\n"
"(define-macro (unless test . body)\n"
"  `(if ,test (void) (begin ,@body)))\n"
"\n"
"(define-macro (and . args)\n"
"  (cond ((null? args) #t)\n"
"        ((null? (cdr args)) (car args))\n"
"        (else `(if ,(car args) (and ,@(cdr args)) #f))))\n"
"\n"
"(define-macro (or . args)\n"
"  (cond ((null? args) #f)\n"
"        ((null? (cdr args)) (car args))\n"
"        (else (let ((v (gensym)))\n"
"                `(let ((,v ,(car args)))\n"
"                   (if ,v ,v (or ,@(cdr args))))))))\n"
"\n"
"(define-macro (case key . clauses)\n"
"  (let ((k (gensym)))\n"
"    `(let ((,k ,key))\n"
"       (cond ,@(map (lambda (c)\n"
"                      (if (eq? (car c) 'else)\n"
"                          c\n"
"                          `((memv ,k ',(car c)) ,@(cdr c))))\n"
"                    clauses)))))\n"
"\n"
"(define-macro (while test . body)\n"
"  (let ((loop (gensym)))\n"
"    `(let ,loop ()\n"
"       (when ,test ,@body (,loop)))))\n"
"\n"
"(define-macro (for var lst . body)\n"
"  `(for-each (lambda (,var) ,@body) ,lst))\n"
"\n"
"(define (1+ n) (+ n 1))\n"
"(define (1- n) (- n 1))\n"
"(define (-1+ n) (- n 1))\n"
"(define (add1 n) (+ n 1))\n"
"(define (sub1 n) (- n 1))\n"
"\n"
"(define (square x) (* x x))\n"
"(define (cube x) (* x x x))\n"
"\n"
"(define (atom? x) (not (pair? x)))\n"
"\n"
"(define (flatten lst)\n"
"  (cond ((null? lst) '())\n"
"        ((pair? (car lst)) (append (flatten (car lst)) (flatten (cdr lst))))\n"
"        (else (cons (car lst) (flatten (cdr lst))))))\n"
"\n"
"(define (range . args)\n"
"  (cond ((= (length args) 1) (iota (car args)))\n"
"        ((= (length args) 2) (iota (- (cadr args) (car args)) (car args)))\n"
"        ((= (length args) 3) (iota (ceiling (/ (- (cadr args) (car args)) (caddr args)))\n"
"                                   (car args) (caddr args)))\n"
"        (else (error \"range: wrong number of args\"))))\n"
"\n"
"(define (char-list->string chars) (apply string chars))\n"
"(define (string-for-each f s) (for-each f (string->list s)))\n"
"(define (string-map f s) (list->string (map f (string->list s))))\n"
"(define (char->string c) (string c))\n"
"(define (boolean->string b) (if b \"#t\" \"#f\"))\n"
"\n"
"(define (caar x) (car (car x)))\n"
"(define (cadr x) (car (cdr x)))\n"
"(define (cdar x) (cdr (car x)))\n"
"(define (cddr x) (cdr (cdr x)))\n"
"(define (caaar x) (car (car (car x))))\n"
"(define (caadr x) (car (car (cdr x))))\n"
"(define (cadar x) (car (cdr (car x))))\n"
"(define (caddr x) (car (cdr (cdr x))))\n"
"(define (cdaar x) (cdr (car (car x))))\n"
"(define (cdadr x) (cdr (car (cdr x))))\n"
"(define (cddar x) (cdr (cdr (car x))))\n"
"(define (cdddr x) (cdr (cdr (cdr x))))\n"
"(define (caaaar x) (car (car (car (car x)))))\n"
"(define (caaadr x) (car (car (car (cdr x)))))\n"
"(define (caaddr x) (car (car (cdr (cdr x)))))\n"
"(define (caddar x) (car (cdr (cdr (car x)))))\n"
"(define (cadddr x) (car (cdr (cdr (cdr x)))))\n"
"\n"
"(define (list* . args)\n"
"  (if (null? (cdr args)) (car args)\n"
"      (cons (car args) (apply list* (cdr args)))))\n"
"(define cons* list*)\n"
"\n"
"(define (last-pair lst)\n"
"  (if (pair? (cdr lst)) (last-pair (cdr lst)) lst))\n"
"\n"
"(define-macro (trace name)\n"
"  `(set! ,name (make-traced ,name ',name)))\n"
"\n"
"(define-macro (untrace name)\n"
"  `(set! ,name (untrace-proc ',name)))\n"
;

/* ═══════════════════════════════════════════════════════════════════════════
 * Build the global environment
 * ═══════════════════════════════════════════════════════════════════════════ */

#define DEF(name, fn) env_define(g, intern(name), VAL_BUILTIN(fn))

Env *make_global_env(void) {
    Env *g = make_env(NULL);
    g->global = g;

    /* Arithmetic */
    DEF("+", bi_add); DEF("-", bi_sub); DEF("*", bi_mul); DEF("/", bi_div);
    DEF("quotient", bi_quotient); DEF("remainder", bi_remainder); DEF("modulo", bi_modulo);
    DEF("expt", bi_expt); DEF("abs", bi_abs);
    DEF("floor", bi_floor); DEF("ceiling", bi_ceiling); DEF("round", bi_round);
    DEF("truncate", bi_truncate);
    DEF("sqrt", bi_sqrt); DEF("isqrt", bi_isqrt); DEF("log", bi_log); DEF("exp", bi_exp);
    DEF("sin", bi_sin); DEF("cos", bi_cos); DEF("tan", bi_tan);
    DEF("asin", bi_asin); DEF("acos", bi_acos); DEF("atan", bi_atan);
    DEF("min", bi_min); DEF("max", bi_max);
    DEF("gcd", bi_gcd); DEF("lcm", bi_lcm);
    DEF("exact", bi_exact); DEF("inexact", bi_inexact);
    DEF("exact->inexact", bi_inexact); DEF("inexact->exact", bi_exact);
    DEF("numerator", bi_numerator); DEF("denominator", bi_denominator);
    DEF("number->string", bi_number_to_string);
    DEF("square", bi_square); DEF("exact-integer?", bi_exact_integer_p);

    /* Numeric comparison */
    DEF("=", bi_num_eq); DEF("<", bi_num_lt); DEF(">", bi_num_gt);
    DEF("<=", bi_num_le); DEF(">=", bi_num_ge);

    /* Numeric predicates */
    DEF("zero?", bi_zero_p); DEF("positive?", bi_positive_p); DEF("negative?", bi_negative_p);
    DEF("odd?", bi_odd_p); DEF("even?", bi_even_p);
    DEF("nan?", bi_nan_p); DEF("infinite?", bi_infinite_p); DEF("finite?", bi_finite_p);

    /* Booleans & equality */
    DEF("not", bi_not); DEF("boolean?", bi_boolean_p);
    DEF("eq?", bi_eq_p); DEF("eqv?", bi_eqv_p); DEF("equal?", bi_equal_p);

    /* Type predicates */
    DEF("number?", bi_number_p); DEF("integer?", bi_integer_p);
    DEF("real?", bi_real_p); DEF("rational?", bi_rational_p);
    DEF("exact?", bi_exact_p); DEF("inexact?", bi_inexact_p);
    DEF("pair?", bi_pair_p); DEF("null?", bi_null_p); DEF("list?", bi_list_p);
    DEF("symbol?", bi_symbol_p); DEF("string?", bi_string_p);
    DEF("char?", bi_char_p); DEF("vector?", bi_vector_p);
    DEF("procedure?", bi_procedure_p);
    DEF("void?", bi_void_p); DEF("eof-object?", bi_eof_object_p);
    DEF("port?", bi_port_p); DEF("input-port?", bi_input_port_p);
    DEF("output-port?", bi_output_port_p);
    DEF("hash-table?", bi_hash_table_p);

    /* Pairs & lists */
    DEF("cons", bi_cons); DEF("car", bi_car); DEF("cdr", bi_cdr);
    DEF("set-car!", bi_set_car); DEF("set-cdr!", bi_set_cdr);
    DEF("list", bi_list); DEF("length", bi_length);
    DEF("append", bi_append); DEF("reverse", bi_reverse);
    DEF("list-tail", bi_list_tail); DEF("list-ref", bi_list_ref);
    DEF("list-set!", bi_list_set); DEF("list-copy", bi_list_copy);
    DEF("make-list", bi_make_list); DEF("iota", bi_iota);
    DEF("memq", bi_memq); DEF("memv", bi_memv); DEF("member", bi_member);
    DEF("assq", bi_assq); DEF("assoc", bi_assoc); DEF("assv", bi_assoc);

    /* Higher-order */
    DEF("map", bi_map); DEF("for-each", bi_for_each);
    DEF("filter", bi_filter);
    DEF("fold-left", bi_fold_left); DEF("fold-right", bi_fold_right);
    DEF("foldl", bi_fold_left); DEF("foldr", bi_fold_right);
    DEF("reduce", bi_fold_left);
    DEF("any", bi_any); DEF("every", bi_every);
    DEF("find", bi_find); DEF("sort", bi_sort); DEF("count", bi_count);
    DEF("apply", bi_apply);
    DEF("filter-map", bi_filter); /* simplified */
    DEF("identity", bi_identity);

    /* Strings */
    DEF("make-string", bi_make_string);
    DEF("string-length", bi_string_length); DEF("string-ref", bi_string_ref);
    DEF("substring", bi_substring); DEF("string-append", bi_string_append);
    DEF("string-copy", bi_string_copy); DEF("string-set!", bi_string_set);
    DEF("string->list", bi_string_to_list); DEF("list->string", bi_list_to_string);
    DEF("string->symbol", bi_string_to_symbol); DEF("symbol->string", bi_symbol_to_string);
    DEF("string->number", bi_string_to_number);
    DEF("string-upcase", bi_string_upcase); DEF("string-downcase", bi_string_downcase);
    DEF("string=?", bi_string_eq); DEF("string<?", bi_string_lt);
    DEF("string-contains", bi_string_contains);
    DEF("string-join", bi_string_join); DEF("string-split", bi_string_split);
    DEF("string-trim", bi_string_trim); DEF("string-index", bi_string_index);
    DEF("string-replace", bi_string_replace);
    DEF("format", bi_format); DEF("string-format", bi_format);

    /* Characters */
    DEF("char->integer", bi_char_to_integer); DEF("integer->char", bi_integer_to_char);
    DEF("char-alphabetic?", bi_char_alphabetic);
    DEF("char-numeric?", bi_char_numeric);
    DEF("char-whitespace?", bi_char_whitespace);
    DEF("char=?", bi_char_eq);

    /* Vectors */
    DEF("make-vector", bi_make_vector); DEF("vector", bi_vector);
    DEF("vector-length", bi_vector_length);
    DEF("vector-ref", bi_vector_ref); DEF("vector-set!", bi_vector_set);
    DEF("vector->list", bi_vector_to_list); DEF("list->vector", bi_list_to_vector);

    /* Hash tables */
    DEF("make-hash-table", bi_make_hash_table);
    DEF("make-equal-hash-table", bi_make_hash_table);
    DEF("hash-table-set!", bi_hash_table_set);
    DEF("hash-table/put!", bi_hash_table_set);
    DEF("hash-table-ref", bi_hash_table_ref);
    DEF("hash-table-ref/default", bi_hash_table_ref_default);
    DEF("hash-table/get", bi_hash_table_ref_default);
    DEF("hash-table-delete!", bi_hash_table_delete);
    DEF("hash-table-exists?", bi_hash_table_exists);
    DEF("hash-table-size", bi_hash_table_size);
    DEF("hash-table/count", bi_hash_table_size);
    DEF("hash-table-keys", bi_hash_table_keys);
    DEF("hash-table-values", bi_hash_table_values);

    /* I/O */
    DEF("display", bi_display); DEF("write", bi_write);
    DEF("newline", bi_newline); DEF("print", bi_print);
    DEF("println", bi_print); DEF("writeln", bi_write);
    DEF("write-string", bi_write_string); DEF("write-char", bi_write_char);
    DEF("read-line", bi_read_line); DEF("read-char", bi_read_char);
    DEF("open-input-file", bi_open_input_file);
    DEF("open-output-file", bi_open_output_file);
    DEF("open-binary-output-file", bi_open_binary_output_file);
    DEF("port-set-position!", bi_port_set_position);
    DEF("write-file", bi_write_file);
    DEF("file->string", bi_file_to_string);
    DEF("tcp-listen", bi_tcp_listen);
    DEF("tcp-accept", bi_tcp_accept);
    DEF("tcp-connect", bi_tcp_connect);
    DEF("tcp-recv", bi_tcp_recv);
    DEF("tcp-send", bi_tcp_send);
    DEF("tcp-close", bi_tcp_close);
    DEF("spawn-process-stdio", bi_spawn_process_stdio);
    DEF("fork-self", bi_fork_self);
    DEF("waitpid-nonblock", bi_waitpid_nonblock);
    DEF("exit-immediate", bi_exit_immediate);
    DEF("sleep", bi_sleep);
    DEF("flush-port", bi_flush_port);
    DEF("write-binary-file", bi_write_binary_file);
    DEF("append-binary-file", bi_append_binary_file);
    DEF("append-port-to-binary-file", bi_append_port_to_binary_file);
    DEF("read-binary-file", bi_read_binary_file);
    DEF("walk-circuit-ops", bi_walk_circuit_ops);
    DEF("op-specs->bytes", bi_op_specs_to_bytes);
    DEF("count-lumbda-ops", bi_count_lumbda_ops);
    DEF("emit-circuit-to-ops-bin-stream", bi_emit_circuit_to_ops_bin_stream);
    DEF("heap-snapshot", bi_heap_snapshot);
    DEF("heap-restore", bi_heap_restore);
    DEF("current-time-ms", bi_current_time_ms);
    DEF("read-from-string", bi_read_from_string);
    DEF("open-input-string", bi_open_input_string);
    DEF("open-output-string", bi_open_output_string);
    DEF("get-output-string", bi_get_output_string);
    DEF("close-port", bi_close_port);
    DEF("close-input-port", bi_close_port);
    DEF("close-output-port", bi_close_port);
    DEF("current-input-port", bi_current_input_port);
    DEF("current-output-port", bi_current_output_port);
    DEF("current-error-port", bi_current_error_port);
    DEF("flush-output-port", bi_flush_output_port);
    DEF("eof-object", bi_eof_object); DEF("void", bi_void);

    /* File system */
    DEF("file-exists?", bi_file_exists);
    DEF("rename-file", bi_rename_file);
    DEF("delete-file", bi_delete_file);
    DEF("current-directory", bi_current_directory);

    /* System */
    DEF("exit", bi_exit); DEF("error", bi_error);
    DEF("error-object?", bi_error_object_p);
    DEF("error?", bi_error_object_p);
    DEF("error-object-message", bi_error_object_message);
    DEF("error-object-irritants", bi_error_object_irritants);
    DEF("error-message", bi_error_object_message);
    DEF("current-time", bi_current_time);
    DEF("current-jiffy", bi_current_jiffy);
    DEF("jiffies-per-second", bi_jiffies_per_second);
    DEF("command-line", bi_command_line);
    DEF("get-environment-variable", bi_get_environment_variable);
    DEF("gensym", bi_gensym);
    DEF("make-parameter", bi_make_parameter);
    DEF("values", bi_values);
    DEF("call-with-values", bi_call_with_values);
    DEF("object->string", bi_object_to_string);
    DEF("write-to-string", bi_object_to_string);
    DEF("display-to-string", bi_display_to_string);
    DEF("raise", bi_error);
    DEF("procedure-name", bi_procedure_name);
    DEF("auto-compile!", bi_auto_compile);
    DEF("compile", bi_compile);
    DEF("compiled?", bi_compiled_p);

    /* Internal */
    DEF("__is-subtype?__", bi_is_subtype_p);

    /* Constants */
    env_define(g, intern("pi"), make_double(M_PI));
    env_define(g, intern("e"), make_double(M_E));
    env_define(g, intern("else"), VAL_TRUE);
    env_define(g, intern("..."), intern("..."));
    env_define(g, intern("*version*"), make_string_from_cstr("1.0.0"));
    env_define(g, intern("*name*"), make_string_from_cstr("lumbda"));

    /* Portal builtins */
    register_portal_builtins(g);

    return g;
}
