flan/runtime/flan_dyn.h
Joseph Ferano 7d4bec521e A value carries its own type, and the heap under it collects
Milestone 1 of dynamic-by-default, the runtime half: NaN-boxed values in one
machine word, a mark-sweep heap, and the operations over them.

A double is itself, which is what a language with a physics loop and a float
calculator in its corpus wants; everything else hides in the quiet-NaN space,
three tag bits and a 48-bit payload that is exactly an x86-64 user pointer.
The negative-NaN collision is answered by canonicalising every NaN on the way
in, which flan_rt.c had already decided was the right thing to print. An i64
past the payload goes on the heap rather than becoming a 48-bit integer with a
64-bit name.

The collector is mark-sweep and nothing else -- no generation, no barrier, no
free list -- because the answer to wanting it faster is to type the program.
Roots are pushed, not scanned: NaN-boxing makes a conservative guess wrong in
both directions, and flan_dev.c's frame chain is the precedent. A fixed ring
of the last sixty-four allocations is marked unconditionally, which closes the
window where an expression with two constructors in it can collect its own
first result before the compiler has rooted either.

A type mismatch traps rather than aborting, through a flan_trap exported from
flan_rt.c so it takes the same path the six existing traps take: parked for
inspection in a dev session, dead where it stands otherwise. The sentence
names the operation, both tags as words, and both values.

flan_dyn.c is its own translation unit and nothing in the release runtime
names a symbol in it, so a program with no dyn operation links no collector
and --no-gc can be file-level selection rather than an argument with the
linker.

docs/SPIKE-DYNAMIC.md carries the argument. test/dyn_ops.c drives every
operation and all twenty-four refusals from C, the way dev_limits.c does,
including a million allocations against a hundred live and the control that
says an unrooted object really is reclaimed.
2026-09-19 05:52:47 +07:00

178 lines
8.4 KiB
C

/* 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 <stdint.h>
#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.
*
* 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 */