The dyn runtime gets a map object and an interned keyword, alongside the
vec it already had. {:a 1 :b s} is a map literal wherever a struct
literal isn't — the parser tells the two apart by whether the first form
in the braces is a .field symbol — and a bracket literal builds the
runtime's own vec rather than a typed array wherever a dyn is wanted, which
is what lets a map literal's values nest arrays and maps freely. get, put,
len and has-key? all learn a dyn-map arm alongside the typed-map one they
already had, and (keyword s) builds the same interned value a :foo literal
does, for a name that only exists at run time. nil is now a literal, the
dyn absence value that get answers for a key a map does not hold.
On the runtime side, flan_dyn.c gets an OBJ_MAP that shares the vec's
storage arm and doubles its accounting, a linear-scan intern table for
keywords that makes equality an identity compare, and structural map
equality by lookup rather than position. The marker traces a map's
interleaved keys and values the same way it already traced a vec.
edn/read and its callers move off the old (Option Value) union entirely:
a document is plain dyn now, sets are dyn maps to true, and arena-edn.flan
is retired along with the union it demonstrated. The acceptance suite's
edn-read and json rows were recaptured against the new shape, and a new
dyn-map.flan program exercises the map and keyword operations end to end,
including a 200k-iteration churn loop against a rooted map that runs
GC for real, across the LLVM, -O0 and x86 rows, and under the sanitizer.
Keywords are dyn everywhere an enum isn't expected, which changed what a
couple of existing checker tests actually see refused; both were updated
to the sentence the checker gives now rather than the one it used to.
206 lines
10 KiB
C
206 lines
10 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);
|
|
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);
|
|
|
|
/* ── 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
|
|
#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);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* FLAN_DYN_H */
|