diff --git a/TODO.org b/TODO.org index a76e8535..7b8e242a 100644 --- a/TODO.org +++ b/TODO.org @@ -1422,13 +1422,11 @@ The opposite of what the escaping-alloca argument predicts, and the measurement that first said otherwise was comparing a 40-frame binary with a 600-frame one. That is why every number in =docs/BUILT.md= is a minimum of nine runs. -** NEXT runtime/flan_dyn_stub.c is dead -Decided 2026-09-25: delete it, as part of a sweep for dead code across the repository, each removal checked unused first. -No dune rule mentions it, no module refers to it, no test links it, and it does -not compile — two conflicting-type errors against its own header. It is maintained -by accident: one lane added a function to it, which is duplicity on the same side -of the same capability. The recommendation is delete, and the author added the -file, so it is his call. +** DONE runtime/flan_dyn_stub.c is dead +CLOSED: [2026-09-25] +Deleted, in a sweep for dead code across the repository in which each removal +was first shown unused. flan_dyn.c is the one implementation of the flan_dyn.h +ABI; a stand-in beside it is not to come back. * Dev loop diff --git a/runtime/flan_dyn_stub.c b/runtime/flan_dyn_stub.c deleted file mode 100644 index e8dfd0a5..00000000 --- a/runtime/flan_dyn_stub.c +++ /dev/null @@ -1,410 +0,0 @@ -/* flan_dyn_stub — a standing-in implementation of the flan_dyn.h ABI. - * - * THE MERGE REPLACES THIS FILE WITH runtime/flan_dyn.c. It exists so that the - * compiler side of dynamic-by-default can be built and run against the fixed - * ABI before the real runtime lands; the real one is being written in parallel - * against the same header, and flan_dyn.h is the contract the two are diffed - * against. - * - * What it is not: it mallocs and never frees, it collects nothing, and - * flan_dyn_root_push / flan_dyn_root_pop record their arguments and do nothing - * with them. That last point matters for anyone reading a passing test here — - * root emission is *not* exercised by this file. A program with entirely wrong - * root discipline passes every test that runs against this stub. The check - * that does bite is the one over the emitted IR, counting pushes against pops - * per function; see the acceptance tests. - * - * The representation is the simplest thing that satisfies the header's rule - * that the word is opaque: every value is a pointer to a heap cell, including - * the small ones. The real runtime will not do this. - */ - -#include -#include -#include -#include -#include - -/* The compiler carries this file as one string with flan_dyn.h pasted in front - * of it (lib/dune), and in that form there is no header on disk to find. The - * probe keeps the file compilable both ways: standalone against the real - * header, and concatenated, where the declarations are already above. The - * header's own include guard makes the two agree. */ -#if defined(__has_include) -# if __has_include("flan_dyn.h") -# include "flan_dyn.h" -# endif -#endif - -/* flan_rt.c's own [rt_trap] is static, so this mirrors it rather than calling - * it: print the sentence, offer the name to the dev daemon's hook, and leave - * with flan_rt's exit code so that a dyn trap is indistinguishable from any - * other trap to whoever is watching. The hook is flan_rt.c's global, and a - * program links both files. */ -extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen); - -static _Noreturn void dyn_trap(const char *name, const char *sentence) { - fflush(stdout); - fprintf(stderr, "%s\n", sentence); - fflush(stderr); - if (flan_trap_hook != NULL) - flan_trap_hook((const uint8_t *)name, (int64_t)strlen(name)); - _exit(134); -} - -enum tag { T_NIL, T_I64, T_F64, T_BOOL, T_STR, T_VEC }; - -typedef struct cell { - enum tag tag; - union { - int64_t i; - double f; - int32_t b; - struct { uint8_t *ptr; int64_t len; } s; - struct { struct cell **items; int64_t len, cap; } v; - } u; -} cell; - -static cell *alloc(enum tag t) { - cell *c = calloc(1, sizeof *c); - if (c == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); - c->tag = t; - return c; -} - -static cell *as(flan_dyn d) { return (cell *)(uintptr_t)d; } -static flan_dyn word(cell *c) { return (flan_dyn)(uintptr_t)c; } - -/* ── Construction ──────────────────────────────────────────────────── */ - -flan_dyn flan_dyn_nil(void) { return word(alloc(T_NIL)); } - -flan_dyn flan_dyn_from_i64(int64_t v) { - cell *c = alloc(T_I64); c->u.i = v; return word(c); -} - -flan_dyn flan_dyn_from_f64(double v) { - cell *c = alloc(T_F64); c->u.f = v; return word(c); -} - -flan_dyn flan_dyn_from_bool(int32_t v) { - cell *c = alloc(T_BOOL); c->u.b = (v != 0); return word(c); -} - -flan_dyn flan_dyn_from_bytes(const uint8_t *ptr, int64_t len) { - cell *c = alloc(T_STR); - c->u.s.ptr = malloc((size_t)len + 1); - if (c->u.s.ptr == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); - if (len > 0) memcpy(c->u.s.ptr, ptr, (size_t)len); - c->u.s.ptr[len] = 0; - c->u.s.len = len; - return word(c); -} - -flan_dyn flan_dyn_vec_new(void) { - cell *c = alloc(T_VEC); - c->u.v.cap = 8; - c->u.v.items = calloc((size_t)c->u.v.cap, sizeof(cell *)); - if (c->u.v.items == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); - return word(c); -} - -/* ── Arithmetic ────────────────────────────────────────────────────── */ - -/* Two numbers promote to f64 when either is one, which is the rule a reader - * expects of a dynamic language and is still not the rule the typed language - * uses. The typed side widens only where nothing can be lost, and an i64 into - * an f64 can — TODO.org, "Implicit numeric widening is legal; narrowing stays - * a hard error" — so (+ i64-x 2.5) is written there and is promoted here. The - * difference is not an oversight on either side: here there - * is no annotation to have been written, so refusing would leave (+ 1 2.5) - * with no spelling that works. */ -static int numeric(cell *c) { return c->tag == T_I64 || c->tag == T_F64; } -static double as_f(cell *c) { return c->tag == T_I64 ? (double)c->u.i : c->u.f; } - -static flan_dyn arith(flan_dyn a, flan_dyn b, char op) { - cell *x = as(a), *y = as(b); - if (!numeric(x) || !numeric(y)) dyn_trap("DynArithType", "this arithmetic needs two numbers, and one of the two values is not one"); - if (x->tag == T_I64 && y->tag == T_I64) { - int64_t p = x->u.i, q = y->u.i, r = 0; - switch (op) { - case '+': r = p + q; break; - case '-': r = p - q; break; - case '*': r = p * q; break; - case '/': if (q == 0) dyn_trap("DivideByZero", "division by zero"); r = p / q; break; - case '%': if (q == 0) dyn_trap("DivideByZero", "division by zero"); r = p % q; break; - } - return flan_dyn_from_i64(r); - } - { - double p = as_f(x), q = as_f(y), r = 0; - switch (op) { - case '+': r = p + q; break; - case '-': r = p - q; break; - case '*': r = p * q; break; - case '/': r = p / q; break; - /* fmod without math.h, to keep the stub's link line as short as the - * real runtime's is meant to be. */ - case '%': r = p - q * (double)(int64_t)(p / q); break; - } - return flan_dyn_from_f64(r); - } -} - -flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return arith(a, b, '+'); -} -flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return arith(a, b, '-'); -} -flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return arith(a, b, '*'); -} -flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return arith(a, b, '/'); -} -flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return arith(a, b, '%'); -} - -/* ── Ordering and equality ─────────────────────────────────────────── */ - -static int cmp(flan_dyn a, flan_dyn b) { - cell *x = as(a), *y = as(b); - if (x->tag == T_STR && y->tag == T_STR) { - int64_t n = x->u.s.len < y->u.s.len ? x->u.s.len : y->u.s.len; - int r = memcmp(x->u.s.ptr, y->u.s.ptr, (size_t)n); - if (r != 0) return r < 0 ? -1 : 1; - return x->u.s.len == y->u.s.len ? 0 : (x->u.s.len < y->u.s.len ? -1 : 1); - } - if (!numeric(x) || !numeric(y)) dyn_trap("DynCompareType", "these two values have no ordering between them"); - if (x->tag == T_I64 && y->tag == T_I64) - return x->u.i == y->u.i ? 0 : (x->u.i < y->u.i ? -1 : 1); - { - double p = as_f(x), q = as_f(y); - return p == q ? 0 : (p < q ? -1 : 1); - } -} - -flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return flan_dyn_from_bool(cmp(a, b) < 0); -} -flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return flan_dyn_from_bool(cmp(a, b) <= 0); -} -flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return flan_dyn_from_bool(cmp(a, b) > 0); -} -flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b, const uint8_t *loc, - int64_t loclen) { - (void)loc; (void)loclen; - return flan_dyn_from_bool(cmp(a, b) >= 0); -} - -/* Structural, and never traps — the header's one exception. */ -static int eq(cell *x, cell *y) { - if (numeric(x) && numeric(y)) { - if (x->tag == T_I64 && y->tag == T_I64) return x->u.i == y->u.i; - return as_f(x) == as_f(y); - } - if (x->tag != y->tag) return 0; - switch (x->tag) { - case T_NIL: return 1; - case T_BOOL: return x->u.b == y->u.b; - case T_STR: return x->u.s.len == y->u.s.len - && memcmp(x->u.s.ptr, y->u.s.ptr, (size_t)x->u.s.len) == 0; - case T_VEC: { - if (x->u.v.len != y->u.v.len) return 0; - for (int64_t i = 0; i < x->u.v.len; i++) - if (!eq(x->u.v.items[i], y->u.v.items[i])) return 0; - return 1; - } - default: return 0; - } -} - -flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b) { - return flan_dyn_from_bool(eq(as(a), as(b))); -} - -/* ── Containers ────────────────────────────────────────────────────── */ - -static cell *need_vec(flan_dyn v) { - cell *c = as(v); - if (c->tag != T_VEC) dyn_trap("DynNotAVec", "this value is not a vector, so it has no elements"); - return c; -} - -static int64_t need_index(flan_dyn i) { - cell *c = as(i); - if (c->tag != T_I64) dyn_trap("DynIndexType", "an index must be an integer"); - return c->u.i; -} - -flan_dyn flan_dyn_len(flan_dyn v) { - cell *c = as(v); - if (c->tag == T_STR) return flan_dyn_from_i64(c->u.s.len); - return flan_dyn_from_i64(need_vec(v)->u.v.len); -} - -flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i) { - cell *c = need_vec(v); - int64_t k = need_index(i); - if (k < 0 || k >= c->u.v.len) dyn_trap("Bounds", "index out of bounds"); - return word(c->u.v.items[k]); -} - -void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x) { - cell *c = need_vec(v); - int64_t k = need_index(i); - if (k < 0 || k >= c->u.v.len) dyn_trap("Bounds", "index out of bounds"); - c->u.v.items[k] = as(x); -} - -void flan_dyn_push(flan_dyn v, flan_dyn x) { - cell *c = need_vec(v); - if (c->u.v.len == c->u.v.cap) { - int64_t cap = c->u.v.cap * 2; - cell **items = realloc(c->u.v.items, (size_t)cap * sizeof(cell *)); - if (items == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); - c->u.v.items = items; - c->u.v.cap = cap; - } - c->u.v.items[c->u.v.len++] = as(x); -} - -static void print_cell(cell *c) { - switch (c->tag) { - case T_NIL: fputs("nil", stdout); break; - case T_I64: printf("%lld", (long long)c->u.i); break; - /* %g, so that a whole-numbered f64 does not print as an i64 would and - * the two remain distinguishable in a test's expected output. */ - case T_F64: printf("%g", c->u.f); break; - case T_BOOL: fputs(c->u.b ? "true" : "false", stdout); break; - case T_STR: printf("%.*s", (int)c->u.s.len, (const char *)c->u.s.ptr); break; - case T_VEC: - fputc('[', stdout); - for (int64_t i = 0; i < c->u.v.len; i++) { - if (i > 0) fputc(' ', stdout); - print_cell(c->u.v.items[i]); - } - fputc(']', stdout); - break; - } -} - -void flan_dyn_print(flan_dyn v) { print_cell(as(v)); } - -/* ── Extraction ────────────────────────────────────────────────────── */ - -int64_t flan_dyn_need_i64(flan_dyn v) { - cell *c = as(v); - if (c->tag != T_I64) dyn_trap("DynExpectedI64", "this value was required to be an i64 and is not"); - return c->u.i; -} - -double flan_dyn_need_f64(flan_dyn v) { - cell *c = as(v); - /* An i64 satisfies an f64 slot, because a dyn integer literal is an i64 by - * the header's rule and (defonce x f64 (f 1)) would otherwise be unwritable - * for any f returning dyn. The reverse is not true: f64 to i64 loses. */ - if (c->tag == T_I64) return (double)c->u.i; - if (c->tag != T_F64) dyn_trap("DynExpectedF64", "this value was required to be an f64 and is not"); - return c->u.f; -} - -int32_t flan_dyn_need_bool(flan_dyn v) { - cell *c = as(v); - if (c->tag != T_BOOL) dyn_trap("DynExpectedBool", "this value was required to be a bool and is not"); - return c->u.b; -} - -/* The cast boundary's tag question — see flan_dyn.h. The stub keeps its own - * trap vocabulary, as every function above it does; what it must agree with - * the real runtime about is the *answer*, 1 for a float box and 0 for an int - * one, because that is what the compiler branches on. The once-per-site - * table is the real runtime's word for word: a program built against the - * stub that warns twice for one line would be a difference in the - * diagnostic, which is the thing this pair exists to keep identical. */ - -#define STUB_SITE_MAX 64 - -static struct { const uint8_t *ptr; int64_t len; } stub_warned[STUB_SITE_MAX]; -static int stub_warned_count; - -int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len, - const uint8_t *target, int64_t target_len, - int32_t want_float) { - cell *c = as(v); - if (c->tag != T_I64 && c->tag != T_F64) - dyn_trap("DynExpectedNumber", - "a numeric cast was written on this value and it is not a number"); - int32_t is_float = c->tag == T_F64 ? 1 : 0; - if (is_float != (want_float ? 1 : 0)) { - int first = 1; - for (int i = 0; i < stub_warned_count; i++) - if (stub_warned[i].len == loc_len && - memcmp(stub_warned[i].ptr, loc, (size_t)loc_len) == 0) - first = 0; - if (first) { - if (stub_warned_count < STUB_SITE_MAX) { - stub_warned[stub_warned_count].ptr = loc; - stub_warned[stub_warned_count].len = loc_len; - stub_warned_count++; - } - fflush(stdout); - fprintf(stderr, - "flan %.*s: (%.*s x) found a dyn holding %s, and converted it " - "to %.*s — warned once for this site\n", - (int)loc_len, (const char *)loc, (int)target_len, - (const char *)target, is_float ? "a float" : "an int", - (int)target_len, (const char *)target); - } - } - return is_float; -} - -/* ── Roots ───────────────────────────────────────────────────────────── - * - * Recorded and otherwise ignored. The shadow stack is kept, and its depth - * checked against the pops, only so that a badly unbalanced emission fails - * loudly here rather than silently: an over-pop is a compiler bug worth - * dying on even in a stub that collects nothing. Under-pushing is invisible, - * and stays invisible until the real collector lands. */ - -static flan_dyn **roots = NULL; -static int64_t roots_len = 0, roots_cap = 0; - -void flan_dyn_root_push(flan_dyn *slot) { - if (roots_len == roots_cap) { - int64_t cap = roots_cap == 0 ? 64 : roots_cap * 2; - flan_dyn **r = realloc(roots, (size_t)cap * sizeof(flan_dyn *)); - if (r == NULL) dyn_trap("OutOfMemory", "the dyn runtime could not allocate"); - roots = r; - roots_cap = cap; - } - roots[roots_len++] = slot; -} - -void flan_dyn_root_pop(int64_t n) { - if (n < 0 || n > roots_len) dyn_trap("DynRootUnderflow", "the dyn root stack was popped further than it was pushed - a compiler bug"); - roots_len -= n; -} - -void flan_gc_init(void) { /* nothing to initialise: this stub never collects */ }