/* flan_dyn — the dynamic-value runtime's ABI. * * This header is not compiled into a program. The build embeds the runtime's * .c files as strings and hands each one to clang on its own, with no include * path (see [Build.compile_c]), so runtime/flan_dyn.c declares everything it * defines and this file declares it a second time. What keeps that second * copy honest is partial, and the exact shape of it is worth writing down * rather than overclaiming: test/dyn_ops.c includes this header and calls * most of what it declares, so a name that is spelled one way here and * another way in flan_dyn.c fails to link in `dune test`. * * Two holes in that. A signature that drifts while the name stays is not * caught at all — C links on names and not on types, so a changed parameter * or return type compiles on both sides and goes wrong at the call site * instead of failing the build. And five of the names below are never * referenced by dyn_ops.c — [flan_dyn_map_get], [flan_dyn_map_set], * [flan_dyn_map_contains], [flan_dyn_is_nil] and [flan_dyn_need_not_nil] — * so not even the rename check reaches them. Calling those five from * dyn_ops.c would close the second hole; nothing available from here closes * the first. * * Who reads it: the compiler lane, which emits calls to these names, and the * C tests. The whole of the boundary is here. What is behind it — the value * representation, the heap layout, the collector — is flan_dyn.c's business * and is argued in docs/SPIKE-DYNAMIC.md. */ #ifndef FLAN_DYN_H #define FLAN_DYN_H #include #ifdef __cplusplus extern "C" { #endif /* A dynamic value. One machine word, always — the whole point of the type is * that a dyn local is a register or a stack slot and never a struct the ABI * has to agree about. NaN-boxed; see the design doc. */ typedef uint64_t flan_dyn; /* A type's dyn map — where the dyn words are inside one instance of it. * * The compiler emits one of these as static data for every type that holds a * dyn anywhere: a struct with a dyn field, a struct holding such a struct by * value, a fixed array of either. The offsets are flattened at compile time, * so nesting costs nothing here — an inner struct's dyn word appears at the * outer offset plus the inner one, and there is no walking of a type graph at * run time and no second descriptor to follow. * * [size] is the stride of one instance. The collector does not read it; the * typed-container view will, which is the reason it is here now rather than * being added later to data both lanes already emit. * * Nothing in this ABI ever writes a descriptor, and no value ever points at * one. See [flan_dyn_root_push_desc]. */ typedef struct flan_desc { int64_t size; int64_t n; const int64_t *offs; } flan_desc; /* ── Constructors ──────────────────────────────────────────────────── */ flan_dyn flan_dyn_nil(void); flan_dyn flan_dyn_from_i64(int64_t x); flan_dyn flan_dyn_from_f64(double x); flan_dyn flan_dyn_from_bool(uint8_t b); /* Copies the bytes into the GC heap. The result is a text value, immutable * from that moment: nothing in this ABI writes into one. [p] may point * anywhere — a literal in .rodata, a frame slot, a slice the caller is about * to drop — because the bytes are copied before this returns. */ flan_dyn flan_dyn_from_bytes(const uint8_t *p, int64_t n); flan_dyn flan_dyn_vec_new(void); flan_dyn flan_dyn_map_new(void); /* A map carrying a shape tag: what a (defclass point [x y]) constructor * builds. [k] is the class's name as a keyword and anything else traps. * * The tag lives in the object's header and not in an entry of the map, so it * is invisible to [get], [set], [contains] and [len] — an instance's length * is its slot count and no key a program can write collides with it. What can * see it is [flan_dyn_class_of], [flan_dyn_eq] (two values of different * classes are unequal, and an instance is never equal to a plain map) and * [flan_dyn_print] (an instance renders as #point{ :x 1 :y 2}). * * The tag is not traced and does not have to be: an interned keyword entry is * immortal and is not a collector object. */ flan_dyn flan_dyn_map_new_class(flan_dyn k); /* The class's name as a keyword, or nil for anything that is not an instance * — an ordinary map included. Never traps. */ flan_dyn flan_dyn_class_of(flan_dyn v); /* A keyword: :foo as a run-time value. Interned — the runtime keeps one entry * per distinct name forever, so two keywords with the same bytes are the same * word and equality is an identity compare, never a memcmp. The entries are * immortal by construction and the collector never traces or frees one. * [p] may point anywhere; the bytes are copied on the first interning. The * name is the bytes after the colon: flan_dyn_kw("a", 1) is :a. */ flan_dyn flan_dyn_kw(const uint8_t *p, int64_t n); /* ── Operations ──────────────────────────────────────────────────────── * * Every one of these may trap, and a trap does not return: it prints a * sentence naming the operation, the tags it was given and the values, and * then takes flan_rt.c's [flan_trap] — which parks the program for inspection * in a dev session and ends it in a standalone build. The three that cannot * trap say so on their own line. * * The nine below take the site as well: [loc]/[loclen] are the bytes of a * "file:line:col" string the emitter already has, and the trap prints them as * a GNU prefix so the failure is somewhere rather than nowhere. It is the same * pair flan_rt.c's bounds and arithmetic traps take, and the same pair * [flan_dyn_cast_kind] takes below. A caller with no site — the C tests, and * anything outside a compiled Flan program — passes (NULL, 0) and gets the * sentence with no prefix. */ flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); /* Answer a bool dyn. Numbers compare as numbers and text compares bytewise; * a mixture of the two, or anything else, traps. */ flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); /* Structural, and the one operation in this file that never traps: two values * of unrelated tags are not an error, they are unequal. */ flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b); /* Bytes of a text, elements of a vec. Anything else traps. */ flan_dyn flan_dyn_len(flan_dyn v); /* Element of a vec, or the byte of a text as an int. Out of range traps. */ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i); /* Vec only — a text is immutable and says so rather than being copied. */ void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x); void flan_dyn_push(flan_dyn v, flan_dyn x); /* Map only; anything else traps by name. Keys and values are both dyn and a * key is compared structurally, so a keyword, a text, an int, or a whole map * may key one. [get] on an absent key answers nil — absence is an answer, the * same line [eq] takes about unrelated tags — and [contains] is the question * to ask when nil might also be stored. [set] replaces the value of an equal * key in place, so a key occurs once and insertion order is print order. */ flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k); void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v); flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k); /* Structural, and per type it renders what typed [print] renders. Never * traps: every tag has a rendering, including nil. */ void flan_dyn_print(flan_dyn v); /* ── The typed boundary ──────────────────────────────────────────────── * * What an annotated parameter does with a dyn argument, and what a dyn * expression does where the checker wants a machine value. A tag that is not * the one asked for traps; there is no widening and no coercion here, which * is deliberate — see the doc's boundary section. */ int64_t flan_dyn_need_i64(flan_dyn v); double flan_dyn_need_f64(flan_dyn v); uint8_t flan_dyn_need_bool(flan_dyn v); /* A numeric cast written on a dyn — [(f64 d)], [(u32 d)] — FIX.org * 2026-09-20. Unlike the parameter boundary above this one coerces: it * answers which numeric tag the box holds (1 float, 0 int) and the caller * branches, so the conversion itself is the cast the compiler already emits * for a typed operand of that tag. A box holding anything else traps, bool * included, exactly as [flan_dyn_need_i64] refuses one. * * [want_float] says what the target type is, and a disagreement writes one * line to stderr — once per [loc], not once per value, because these casts * run inside frame loops. [loc] and [target] are Flan slices: pointer and * length, not NUL-terminated. */ 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); /* nil <-> None at an (Option T) boundary, and (Some nil)'s refusal — M2 item * 4. [flan_dyn_is_nil] is the tag test the boundary's runtime half needs and * does not want to build out of [flan_dyn_tag] and a comparison at every call * site; it answers 1 for nil and 0 for every other tag, and cannot trap. * [flan_dyn_need_not_nil] is the other half: it answers [v] unchanged when * [v] is not nil, and traps when it is — the run-time case of (Some nil), * for a dyn value that is not known to be nil until the program runs. */ int32_t flan_dyn_is_nil(flan_dyn v); flan_dyn flan_dyn_need_not_nil(flan_dyn v); /* Truthiness for a dyn used where a typed value would need a strict bool — * an [if]'s condition when the scrutinee's own type is dyn. Clojure's rule: * nil and false are falsey, every other value is truthy, including 0, 0.0, * "", an empty vec, an empty map, and any keyword. Never traps. */ uint8_t flan_dyn_truthy(flan_dyn v); /* ── Typed containers as views — M2 item 3 ───────────────────────────── * * A [(Vec T)], a [T] slice, or a fixed [n T] array crossing into dyn is a * VIEW, not a copy: the box holds a small heap record naming where the * elements live and what one of them is, and every read or write goes * straight through to the container's own storage. [flan_dyn_at] boxes an * element on the way out; [flan_dyn_set_at] tag-checks the dyn value it is * given against the element type on the way in and traps, by [flan_trap], * on a mismatch — never a silent coercion. * * T is restricted to i64, f64 and bool — exactly the set [flan_dyn_need_i64] * and friends already treat as crossing the typed boundary both ways. That * is not an arbitrary cut: the excluded case that matters is a string * element, whose dyn form is a pointer into this collector's heap, while a * typed container's storage is arena or stack memory the collector never * scans. Writing such a pointer into that memory would be a live reference * nothing ever traces — a use-after-free the collector cannot see coming, * not a bug in this file but a hazard the type admits. i64, f64 and bool * carry no such pointer, so a view restricted to them cannot manufacture * it. [box] in lib/check.ml keeps the "does not cross into dyn yet" refusal * for every other element type, and this paragraph is why. * * Two kinds, because the containers split exactly here: a [(Vec T)] can grow * and move (a push may reallocate), a slice and a fixed array cannot. * * [flan_dyn_view_vec] takes the address of the Vec's own header — the * struct [flan_vec] in flan_rt.c, restated in flan_dyn.c under the same * "if either table changes, change both" rule this whole boundary already * lives under. That address is the Vec's home, fixed for as long as the Vec * exists — but "as long as the Vec exists" is the whole of the guarantee, * which is why [permanent_root] in lib/check.ml admits only storage that * outlives every frame: a global, a field or an array element of one, or a * slice cut from one at the crossing. A local's slot is a home too, and it * is precisely the one that is refused. Every operation re-reads that * header's [ptr] and [len] fresh, so a push that grows and moves the Vec is * never seen as stale — [flan_vec_grow] overwrites the SAME header's [ptr] * field in place, and there is no snapshot anywhere to go stale. That is * what makes the failure the open design question worried about * (a push through dyn holding a dangling pointer) impossible rather than * merely unlikely: there is nothing captured at the crossing for a later * push to invalidate. * * [flan_dyn_view_flat] takes a data address and a length captured once, at * the crossing — sound for a slice and for a fixed array because neither * ever moves or grows. Note the asymmetry is not an oversight: pointing * *this* case at the value's own slot instead would be worse than a * snapshot, because a slot's lifetime is not the slice's, and a slice taken * from a Vec is already one push away from dangling on its own account * (flan_vec_grow's own comment says so) — the view is exactly as * stale-safe as the thing it is a view of, no more and no less. */ #define FLAN_VIEW_I64 0 #define FLAN_VIEW_F64 1 #define FLAN_VIEW_BOOL 2 flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem); flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem); /* ── The collector ───────────────────────────────────────────────────── * * Mark-sweep, precise, and never moving. [flan_gc_init] is idempotent, and the * first allocation calls it if nobody else has — deliberately, because * flan_rt_init calling it would be flan_rt.c naming a symbol in flan_dyn.c, * and the whole of the droppability argument is that the dependency runs one * way only. So an emitted program need not call it at all; it is exported * because a test that wants a heap in a known state wants to say so. * * [flan_gc_collect] is a full collection on demand, which nothing in an * emitted program needs — collection happens inside allocation — and which * the tests and a break loop want. [flan_gc_live_bytes] is what the heap holds * after the last sweep, counted the way the trigger counts it. */ void flan_gc_init(void); void flan_gc_collect(void); int64_t flan_gc_live_bytes(void); /* Roots, shadow-stack style, exactly as flan_dev.c's frame chain is: the * compiler emits a push per dyn local on entry and one pop for the lot on the * way out. The address is remembered, not the value, so a local that is * reassigned needs no second push. * * **The slot must hold a valid flan_dyn before it is pushed.** The collector * reads every registered address on every mark, and an uninitialised slot is a * word of stack garbage that will be decoded as a pointer. Storing nil first * is the whole of the contract; the compiler lane zeroes a slot at its * declaration anyway. * * A zeroed slot satisfies it, and this used to be the one thing in this header * decided by one side alone. It is checkable now that both sides exist: * flan_dyn.c's mark walks a value only when it is boxed, and boxed means the * quiet-NaN prefix is set, which the zero word does not have. So zero decodes * as the double 0.0 — an ordinary value, and never an address anything * follows. Both backends zero, and they are right to. * * Globals go through the same pair, pushed once at startup and never popped. * * [flan_dyn_root_pop] takes a count rather than an address because that is * what a function epilogue knows cheaply. Popping more than are pushed is * clamped at empty rather than being a second failure on top of the first. */ void flan_dyn_root_push(flan_dyn *slot); void flan_dyn_root_pop(int64_t n); /* The same stack, for a slot that holds an aggregate rather than a dyn word: * a struct with a dyn field, a struct holding one of those by value, an array * of either. [base] is the first byte of the instance and [d] says where the * dyn words are inside it. * * The question this answers, and it is the only interesting one about the * whole mechanism: how does the collector get from a run of bytes to the * descriptor for the type at those bytes? It does not. **The instance never * carries a pointer to its descriptor, and the collector never derives one.** * The pairing is made here, at the push, by the code that put the value there * and therefore knows its static type. That is what makes a bare struct on the * stack the easy case rather than the impossible one, and it is why no Flan * struct grows a header word: a header would change the layout C interop * agrees on, change the stride of an array, and change what embedding a struct * in another one costs. * * The same contract as the dyn form: **the dyn words named by [d] must hold * valid flan_dyn values before the push**, which zero satisfies. The compiler * zeroes those words and not the whole instance — the rest of the bytes are * never read through this stack. * * One entry, so one pop takes it off like any other, and a function's pop * count is still the number of pushes it made. [d] is static data with the * lifetime of the program; nothing copies it. */ void flan_dyn_root_push_desc(void *base, const flan_desc *d); /* ── Extensions ──────────────────────────────────────────────────────── * * Additions to the agreed ABI, none of which the compiler lane has to emit. * They are here because something in this repository needs them; each says * what. */ /* The *frames*' roots, dropped. The counterpart of flan_dev.c's * [flan_dev_frames_reset] and there for its one caller: the merged dev build's * [main] is re-entered by longjmp, which pops no frame, so every root the * finished run pushed still points into stack the next run is about to write * over. Marking through those addresses would decode whatever the new run put * there. Called between runs, on the thread that runs them. * * The globals' roots are not dropped with them, and that is the whole of the * distinction: a finished run's frames are gone but its globals are not — the * dev daemon parks with them readable, and runs evaluated thunks against them * that allocate and therefore collect. Emptying the stack outright unrooted * every dyn global for the whole of the park. This resets to the line * [flan_dyn_root_globals_end] recorded. */ void flan_dyn_root_reset(void); /* Where that line comes from. The emitted [main] brackets its global pushes * with these: [begin] immediately before the first, [end] immediately after * the last, with nothing but the pushes in between — nothing there may * allocate, because between the two the globals are unrooted and still hold * whatever a previous run left in them. [begin] empties the stack rather than * adding to it, so a [main] entered a second time re-roots the same globals * instead of pushing a second copy of each. * * A program with no dyn globals need not call either: the line starts at zero, * which is what an empty push list should leave it at. */ void flan_dyn_root_globals_begin(void); void flan_dyn_root_globals_end(void); /* The tag of a value, as a number and as the word that number is printed as. * The numbers are FLAN_DYN_TAG_* below. The break loop and the inspector want * both — a value's tag is the first thing anyone asks a stopped dyn program — * and the trap messages in flan_dyn.c are written from the same table, so a * message and an inspector cannot disagree about what to call a value. */ #define FLAN_DYN_TAG_NIL 0 #define FLAN_DYN_TAG_BOOL 1 #define FLAN_DYN_TAG_INT 2 #define FLAN_DYN_TAG_FLOAT 3 #define FLAN_DYN_TAG_TEXT 4 #define FLAN_DYN_TAG_VEC 5 #define FLAN_DYN_TAG_KEYWORD 6 #define FLAN_DYN_TAG_MAP 7 int32_t flan_dyn_tag(flan_dyn v); const char *flan_dyn_tag_name(int32_t tag); /* How many objects the heap holds, and the floor under the collection * trigger. Both are the tests': a live-bytes figure alone cannot tell a heap * that is collecting from one whose objects happen to be small, and a floor of * a megabyte would make the million-allocation case a megabyte of arithmetic * before it proved anything. Setting the floor takes effect at the next * allocation and never shrinks a heap by itself; pass 0 for the default. */ int64_t flan_gc_count(void); void flan_gc_set_floor(int64_t bytes); /* Reports flan_dyn.c's own mirror of flan_rt.c's [flan_vec] — [size, then * the offset of ptr, len, cap, alloc, epoch] — for test/dyn_ops.c's * "layout" mode to compare against flan_rt.c's [flan_vec_layout] and * against its own hand-built mirror. See [flan_vec_layout]'s comment in * flan_rt.c for what this ties together and why nothing at compile time * otherwise does. */ void flan_dyn_vec_hdr_layout(int64_t out[6]); #ifdef __cplusplus } #endif #endif /* FLAN_DYN_H */