355 lines
18 KiB
C
355 lines
18 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;
|
|
|
|
/* 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 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. */
|
|
|
|
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);
|
|
|
|
/* 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);
|
|
|
|
/* 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 */
|