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.
178 lines
8.4 KiB
C
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 */
|