/* 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. That is the same standing * arrangement flan_escape_bytes and flan_dev_emit_str already live under — * "if either table changes, change both" — and it is made mechanical rather * than hopeful: test/dyn_ops.c includes this header and names every function * below, so a signature that drifts from the implementation is a link error in * `dune test` rather than a surprise at someone else's call site. * * 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; /* ── 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); /* ── 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. */ flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b); /* 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); flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b); flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b); /* 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); /* 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); /* ── 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); /* ── 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 root stack, emptied. 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. */ void flan_dyn_root_reset(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 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); #ifdef __cplusplus } #endif #endif /* FLAN_DYN_H */