4812 lines
197 KiB
C
4812 lines
197 KiB
C
/* flan_dyn — tagged values, a mark-sweep heap, and the operations over them.
|
|
*
|
|
* Milestone 1 of dynamic-by-default: code nobody annotated computes with
|
|
* values that carry their type at run time, code that is fully annotated
|
|
* compiles to exactly what it compiled to before, and a build that asks for
|
|
* neither a collector nor a tag can be told that it has one.
|
|
*
|
|
* The argument for every decision in here — why NaN-boxing rather than low-bit
|
|
* tagging, why mark-sweep rather than anything cleverer, why the roots are
|
|
* pushed rather than found — is docs/SPIKE-DYNAMIC.md. This file carries the
|
|
* parts of it a reader needs *while reading the code*, and points at the doc
|
|
* for the rest.
|
|
*
|
|
* ── What this file may depend on ──────────────────────────────────────
|
|
*
|
|
* flan_rt.c, and nothing else in the tree. The dependency does not run the
|
|
* other way: no line of flan_rt.c or flan_dev.c names anything defined here.
|
|
* That is the whole of what makes a `--no-gc` build possible — not because the
|
|
* linker drops this object (it does not; a named object is linked whole, and
|
|
* `nm` on any corpus program finds flan_dyn_add in it), but because nothing
|
|
* else needs it, so *not compiling it* is a change at the three sites that
|
|
* name it and nowhere else. A single back-reference from the release runtime
|
|
* would make the collector unconditional and the refusal a lie. See the doc's
|
|
* "Dropping the collector".
|
|
*
|
|
* ── Threads ───────────────────────────────────────────────────────────
|
|
*
|
|
* There are none, and the globals below are plain globals for the reason the
|
|
* handler stack, the restart stack and the frame chain in the other two files
|
|
* are: one thread runs Flan. The dev agent's listener thread runs C and the
|
|
* loader and never enters a Flan body, so it never allocates and never marks.
|
|
* If the language grows threads, the heap needs a lock and the roots need to
|
|
* be thread-local, and that is one change in two places rather than a rewrite.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ── What we borrow from flan_rt.c ─────────────────────────────────────
|
|
*
|
|
* Declared rather than included: the build hands clang each runtime .c on its
|
|
* own with no include path (see [Build.compile_c]), so a #include of
|
|
* flan_dyn.h would not resolve. runtime/flan_dyn.h says the same things a
|
|
* second time and test/dyn_ops.c includes it, which is what keeps the two
|
|
* copies honest. */
|
|
|
|
void flan_write_stdout(const uint8_t *p, int64_t n);
|
|
/* The two sinks in flan_dev.c a dyn value can be rendered into instead of
|
|
* stdout: an evaluated expression's value, and a watch slot. flan_dev.c is
|
|
* linked into every build, so these resolve whether or not the build is a dev
|
|
* one; the reference runs this way round so that flan_dev.c names nothing in
|
|
* this file and the dyn runtime stays droppable at the file level. */
|
|
void flan_dev_emit(const uint8_t *bytes, int64_t len);
|
|
void flan_dev_watch_emit(const uint8_t *bytes, int64_t len);
|
|
|
|
/* The one non-local exit a dyn operation can take. flan_rt.c's [rt_trap] is
|
|
* static, and re-implementing what it does — the break-loop hook, the flush,
|
|
* the socket, [_exit(134)] — would be a second answer to "how does a Flan
|
|
* program die where it stands", which that file went to some trouble to have
|
|
* only one of. So flan_rt.c exports a thin wrapper and this calls it. */
|
|
_Noreturn void flan_trap(const uint8_t *name, int64_t namelen);
|
|
|
|
/* The site and operation a walk over a view reports a trap at — a print, an
|
|
* equality, a length — set by the entry point that has them and read by the
|
|
* element readers the walk calls. NULL when the entry point has no site.
|
|
* Every trap in this file goes through [dyn_trap], which clears them first:
|
|
* a trap does not return to the [walk_leave] that would have, and a later
|
|
* walk must not name the site of one that was abandoned. */
|
|
static const uint8_t *walk_loc;
|
|
static int64_t walk_len;
|
|
static const char *walk_op = "print";
|
|
|
|
typedef struct { const uint8_t *loc; int64_t len; const char *op; } walk_site;
|
|
|
|
static walk_site walk_enter(const uint8_t *loc, int64_t len, const char *op) {
|
|
walk_site was;
|
|
was.loc = walk_loc; was.len = walk_len; was.op = walk_op;
|
|
walk_loc = loc; walk_len = loc != NULL ? len : 0; walk_op = op;
|
|
return was;
|
|
}
|
|
|
|
static void walk_leave(walk_site was) {
|
|
walk_loc = was.loc; walk_len = was.len; walk_op = was.op;
|
|
}
|
|
|
|
int flan_dev_reg_enabled(void);
|
|
/* runtime/flan_dev.c: set with the registry, so a view's crossing reads a
|
|
* word rather than making a call to learn it is in a release build. */
|
|
extern int flan_dev_views_checked;
|
|
|
|
static _Noreturn void dyn_trap(const uint8_t *name, int64_t namelen) {
|
|
walk_loc = NULL;
|
|
walk_len = 0;
|
|
walk_op = "print";
|
|
flan_trap(name, namelen);
|
|
}
|
|
/* A trap's sentence, printed after its site and kept for the break loop, which
|
|
* shows it beside the trap's name (flan_rt.c). */
|
|
void flan_say(const uint8_t *loc, int64_t loclen, const char *fmt, ...);
|
|
|
|
/* Growing a Vec through a dyn view borrows flan_rt.c's own growth: doubling,
|
|
* allocator adoption and the epoch check all live in [flan_vec_push], and
|
|
* re-implementing any of that here would be a second copy of logic the
|
|
* duplicity doctrine (docs/SPIKE-DUPLICITY.md) says belongs on one side only.
|
|
* [v] is declared [void *] rather than [flan_vec *] so this file need not
|
|
* name flan_rt.c's type; the two structs' layouts must agree, which is
|
|
* [flan_dyn_vec_hdr] below, restated for the same reason [flan_desc] is. */
|
|
int8_t flan_vec_push(void *v, const void *elem, int64_t size, int64_t align,
|
|
const uint8_t *loc, int64_t loclen);
|
|
|
|
/* ── The representation ────────────────────────────────────────────────
|
|
*
|
|
* NaN-boxed, in a word. A double is *itself*: the 2^64 minus a NaN's worth of
|
|
* bit patterns that are not quiet NaNs are read straight back as f64, at no
|
|
* cost, which is what a language where f64 is first class and where sand.flan
|
|
* runs a physics loop wants. Everything else hides inside the quiet-NaN space.
|
|
*
|
|
* The box is sign bit + all-ones exponent + quiet bit, which is
|
|
* 0xFFF8000000000000. Bits 50..48 are three tag bits; bits 47..0 are the
|
|
* payload, which is exactly the width of an x86-64 user-space pointer.
|
|
*
|
|
* 63 62..52 51 50..48 47..0
|
|
* 1 1...1 1 tag payload
|
|
*
|
|
* The collision this scheme always has to answer for is a real f64 that is
|
|
* already a *negative* quiet NaN: those bits are indistinguishable from a box.
|
|
* [flan_dyn_from_f64] answers it by canonicalising every NaN to the positive
|
|
* quiet NaN on the way in. That is not a new rule invented here — flan_rt.c's
|
|
* [flan_f64_to_bytes] already renders every NaN as "nan" with no sign, and
|
|
* carries three paragraphs on why the sign bit of a NaN is not a fact about
|
|
* the arithmetic and should not be shown. A dyn value takes the same line one
|
|
* step further and does not *store* it. Nothing observable changes: NaN is not
|
|
* equal to itself, so no comparison can see which NaN it is, and the printer
|
|
* was already refusing to say.
|
|
*
|
|
* Integers. i64 is first class here and 48 bits is not 64, so an int that fits
|
|
* the payload is inline and one that does not is a heap box. The inline range
|
|
* is ±2^47, which is every array index, every counter and every timestamp in
|
|
* milliseconds until the year 6429; the box is what keeps the other end of the
|
|
* type honest rather than quietly wrapping. See the doc.
|
|
*
|
|
* Tags 5, 6 and 7 are unspoken for — keywords and maps took 4 and BOX_OBJ's
|
|
* [kind] field, not new top-level tags — and that is where a typed handle
|
|
* goes when interop arrives — a (Vec i64) crossing into dyn without being
|
|
* copied. Again, the doc. */
|
|
|
|
typedef uint64_t flan_dyn;
|
|
|
|
/* A type's map of the words the collector follows: where they are inside one
|
|
* instance of it. The compiler emits one of these per type that has any, as
|
|
* static data, and hands a pointer to it to [flan_dyn_root_push_desc] or
|
|
* [flan_dyn_env_new]. Nothing here ever writes one.
|
|
*
|
|
* Three kinds of word, one table each:
|
|
*
|
|
* - [offs]: a dyn word, marked by [mark_value].
|
|
* - [envs]: the environment half of an (Fn ...) value, pointer-sized. It holds
|
|
* a collector-allocated environment, null, or — for a named function widened
|
|
* into an Fn — that function's code address. [mark_env] follows only the
|
|
* first, and tells them apart by asking [envset] rather than by reading the
|
|
* word, so a code address is never dereferenced.
|
|
* - [vecs]: a (Vec T) header whose elements hold words of their own, with the
|
|
* element's descriptor. The marker reads the header's pointer and length
|
|
* where they are, so a push that reallocated is seen.
|
|
* - [maps]: a (Map K V) header whose values hold words of their own, with the
|
|
* value's descriptor. Read the same way, and walked over the full slots.
|
|
*
|
|
* [size] is the stride of one instance as the compiler's element-size
|
|
* arithmetic counts it, which is what a Vec's elements are laid out at. The
|
|
* collector reads it only through a [vecs] entry's element descriptor. */
|
|
struct flan_desc;
|
|
typedef struct flan_desc_vec {
|
|
int64_t off;
|
|
const struct flan_desc *elem;
|
|
} flan_desc_vec;
|
|
|
|
typedef struct flan_desc {
|
|
int64_t size;
|
|
int64_t n;
|
|
const int64_t *offs;
|
|
int64_t nenv;
|
|
const int64_t *envs;
|
|
int64_t nvec;
|
|
const flan_desc_vec *vecs;
|
|
int64_t nmap;
|
|
const flan_desc_vec *maps;
|
|
} flan_desc;
|
|
|
|
#define DYN_QNAN 0xFFF8000000000000ULL
|
|
#define DYN_TAGMASK 0x0007000000000000ULL
|
|
#define DYN_PAYMASK 0x0000FFFFFFFFFFFFULL
|
|
#define DYN_TAGSHIFT 48
|
|
|
|
/* The four box tags. Not the same numbers as FLAN_DYN_TAG_* in the header:
|
|
* those are what a *reader* is told (float and int are two answers), these are
|
|
* how the word is laid out (a float is not boxed at all, and a big int is a
|
|
* pointer). [flan_dyn_tag] is the translation. */
|
|
#define BOX_NIL 0u
|
|
#define BOX_BOOL 1u
|
|
#define BOX_INT 2u
|
|
#define BOX_OBJ 3u
|
|
/* A keyword. The payload is a pointer to an interned entry that is not a GC
|
|
* object at all: keywords are immortal by construction — the intern table
|
|
* below holds the only copy of each name, nothing ever removes one, and the
|
|
* collector never sees the tag ([mark_value] walks BOX_OBJ and nothing else).
|
|
* Interning is what buys the Lisp symbol model: two keywords with the same
|
|
* name are the same word, so equality is the identity compare [dyn_equal]
|
|
* already opens with, never a memcmp. */
|
|
#define BOX_KW 4u
|
|
|
|
/* Restated from flan_dyn.h — a view's element kind. */
|
|
#define FLAN_VIEW_I64 0
|
|
#define FLAN_VIEW_F64 1
|
|
#define FLAN_VIEW_BOOL 2
|
|
|
|
/* Spelled as a negated positive rather than as a shift of -1: shifting a
|
|
* negative value left is undefined, and this file is swept by UBSan. */
|
|
#define DYN_INT_MAX (((int64_t)1 << 47) - 1)
|
|
#define DYN_INT_MIN (-DYN_INT_MAX - 1)
|
|
|
|
static inline int dyn_boxed(flan_dyn v) { return (v & DYN_QNAN) == DYN_QNAN; }
|
|
static inline unsigned dyn_box(flan_dyn v) {
|
|
return (unsigned)((v & DYN_TAGMASK) >> DYN_TAGSHIFT);
|
|
}
|
|
static inline uint64_t dyn_payload(flan_dyn v) { return v & DYN_PAYMASK; }
|
|
|
|
static inline flan_dyn dyn_make(unsigned tag, uint64_t payload) {
|
|
return DYN_QNAN | ((uint64_t)tag << DYN_TAGSHIFT) | (payload & DYN_PAYMASK);
|
|
}
|
|
|
|
/* ── The heap ──────────────────────────────────────────────────────────
|
|
*
|
|
* One header, three kinds, and a singly-linked list of everything ever
|
|
* allocated. The list is the sweep's; there is no other index, no free list
|
|
* and no size class, because the collector's stated job is to be small enough
|
|
* to read in one sitting. A heap that wants to be faster than this wants the
|
|
* program to be typed instead.
|
|
*
|
|
* [mark] is a byte and not a bit in a side table for the same reason. A side
|
|
* table is the right answer when the sweep is the cost, and the sweep is never
|
|
* going to be the cost here.
|
|
*
|
|
* A vec's elements live in a plain malloc block hanging off the header rather
|
|
* than in a GC object of their own. Two reasons: a growth is then a [realloc]
|
|
* and not a copy this file writes, and the elements are never reachable except
|
|
* through their vec, so giving them an identity would buy nothing and cost a
|
|
* header. Their bytes are counted in [gc_bytes] and freed when the vec is
|
|
* swept, which is the whole of their lifetime. */
|
|
|
|
#define OBJ_TEXT 0
|
|
#define OBJ_VEC 1
|
|
#define OBJ_INT 2 /* an i64 too wide for the payload */
|
|
#define OBJ_MAP 3 /* keys and values interleaved: k0 v0 k1 v1 ... */
|
|
#define OBJ_VIEW 4 /* a typed container crossing into dyn as a view */
|
|
#define OBJ_ENV 5 /* a closure's environment: bytes after the header,
|
|
marked through the descriptor it was made with.
|
|
Never a dyn value — nothing boxes one — so no tag
|
|
word, printer or operation ever sees this kind. */
|
|
|
|
/* flan_vec, restated. This file must not name flan_rt.c's [flan_vec] — see
|
|
* the "if either table changes, change both" note above [flan_vec_push] —
|
|
* so a view over a [(Vec T)] is built from an address whose first five words
|
|
* this mirrors exactly. Only [ptr], [len] and [epoch]/[alloc] are ever read
|
|
* through it; nothing here writes one. */
|
|
typedef struct flan_dyn_vec_hdr {
|
|
void *ptr;
|
|
int64_t len;
|
|
int64_t cap;
|
|
void *alloc;
|
|
int64_t epoch;
|
|
} flan_dyn_vec_hdr;
|
|
|
|
/* This mirror's own layout, reported the same way flan_rt.c's
|
|
* [flan_vec_layout] reports the original's — see that function's comment
|
|
* for what ties the two together and why nothing at compile time otherwise
|
|
* does. */
|
|
void flan_dyn_vec_hdr_layout(int64_t out[6]) {
|
|
out[0] = (int64_t)sizeof(flan_dyn_vec_hdr);
|
|
out[1] = (int64_t)offsetof(flan_dyn_vec_hdr, ptr);
|
|
out[2] = (int64_t)offsetof(flan_dyn_vec_hdr, len);
|
|
out[3] = (int64_t)offsetof(flan_dyn_vec_hdr, cap);
|
|
out[4] = (int64_t)offsetof(flan_dyn_vec_hdr, alloc);
|
|
out[5] = (int64_t)offsetof(flan_dyn_vec_hdr, epoch);
|
|
}
|
|
|
|
/* flan_map, restated for the same reason and read the same way: only
|
|
* [data], [log2cap], [alloc] and [epoch]. With it, the three numbers of the
|
|
* block's geometry the marker needs — flan_rt.c's FLAN_MAP_HEAD, _GROUP and
|
|
* _ALIGN. If either file's table changes, change both. */
|
|
typedef struct flan_dyn_map_hdr {
|
|
void *data;
|
|
int64_t len;
|
|
int64_t log2cap;
|
|
void *alloc;
|
|
int64_t epoch;
|
|
} flan_dyn_map_hdr;
|
|
|
|
#define DYN_MAP_HEAD 24
|
|
#define DYN_MAP_GROUP 8
|
|
#define DYN_MAP_ALIGN 64
|
|
#define DYN_MAP_FULL 0x80
|
|
|
|
/* This mirror's numbers, compared against flan_rt.c's [flan_map_layout] by
|
|
* test/dyn_ops.c's "layout" mode. */
|
|
void flan_dyn_map_hdr_layout(int64_t out[10]) {
|
|
out[0] = (int64_t)sizeof(flan_dyn_map_hdr);
|
|
out[1] = (int64_t)offsetof(flan_dyn_map_hdr, data);
|
|
out[2] = (int64_t)offsetof(flan_dyn_map_hdr, len);
|
|
out[3] = (int64_t)offsetof(flan_dyn_map_hdr, log2cap);
|
|
out[4] = (int64_t)offsetof(flan_dyn_map_hdr, alloc);
|
|
out[5] = (int64_t)offsetof(flan_dyn_map_hdr, epoch);
|
|
out[6] = DYN_MAP_HEAD;
|
|
out[7] = DYN_MAP_GROUP;
|
|
out[8] = DYN_MAP_ALIGN;
|
|
out[9] = DYN_MAP_FULL;
|
|
}
|
|
|
|
/* flan_allocator's prefix, far enough to read the one word a stale-container
|
|
* check needs. The struct has more fields after [epoch]; this file never
|
|
* touches them; and the alignment of a leading same-typed prefix is the same
|
|
* in any translation unit that agrees on the field order, which is the
|
|
* "change both" this comment is the other half of. */
|
|
typedef struct flan_dyn_alloc_hdr {
|
|
void *proc;
|
|
void *data;
|
|
uint32_t caps;
|
|
uint64_t epoch;
|
|
} flan_dyn_alloc_hdr;
|
|
|
|
/* A keyword's interned entry, declared here because a map's shape tag is one.
|
|
* The definition, the table and the argument for interning are further down,
|
|
* under "Keywords". */
|
|
struct kw_entry;
|
|
|
|
typedef struct flan_obj {
|
|
struct flan_obj *next; /* every object ever allocated, newest first */
|
|
uint8_t kind;
|
|
uint8_t mark;
|
|
/* OBJ_MAP with a [klass] only: the generation of the class definition this
|
|
instance was built against. Compared against the registry's current
|
|
generation on every access that observes the slot set, and a mismatch is
|
|
a lazy migration — see [class_sync] and "Classes" below.
|
|
|
|
It lives *here*, in the padding that [kind] and [mark] leave in front of
|
|
[len]'s alignment, and that placement is the whole reason the field is
|
|
free: [sizeof(flan_obj)] is 48 with it and was 48 without it. The union
|
|
is exactly 24 bytes — [items], [cap], [klass] fill it — so there is no
|
|
spare word inside the arm, and a field after it would have cost every
|
|
dyn object in the heap eight bytes for a word only class instances read.
|
|
[flan_dyn_obj_size] answers the number and dyn_ops.c's [classes] mode
|
|
asserts it, so a later field that pushes it past 48 fails a test rather
|
|
than costing that silently.
|
|
|
|
Zero means "built before any class definition was registered", which is
|
|
also the answer for every map that is not an instance. The registry's
|
|
first registration of a name lands on 1, so a gen-0 instance of a
|
|
registered class migrates once, which is what makes a program built
|
|
before this existed correct rather than merely unbroken. */
|
|
uint32_t gen;
|
|
int64_t len; /* bytes of a text, elements of a vec or entries
|
|
of a map */
|
|
union {
|
|
int64_t i; /* OBJ_INT */
|
|
struct { flan_dyn *items; int64_t cap;
|
|
/* OBJ_MAP only, and NULL for every map that is not a defclass
|
|
instance: the shape tag. It is the interned entry of the
|
|
class's name — :point for (defclass point [x y]) — so the
|
|
identity compare that makes keyword equality cheap is also
|
|
what makes a class check cheap, and the tag needs no marking
|
|
because an interned entry is immortal and is not a GC object
|
|
(see [mark_value], which follows BOX_OBJ and nothing else).
|
|
|
|
It lives in the header rather than in a reserved entry of the
|
|
map itself, which is the one place this departs from the
|
|
queue's note: an entry would be counted by [len], walked by
|
|
[render], and compared by [dyn_equal]'s key loop, so every
|
|
instance would answer a length one larger than its slot count
|
|
and print a key nobody wrote. A field cannot be reached by
|
|
[get] or [put] at all, so no user key can collide with it.
|
|
|
|
A vec leaves it NULL. The arm is shared, so the field exists
|
|
for both kinds; nothing reads it for an OBJ_VEC. */
|
|
struct kw_entry *klass; } v; /* OBJ_VEC and OBJ_MAP —
|
|
a map shares the vec's arm on purpose: its entries are the same malloc
|
|
block of dyn words, interleaved key then value, with [len] counting
|
|
entries and [cap] counting entries too. Sharing the arm is what lets the
|
|
marker and the sweep treat the two kinds with one load and a doubled
|
|
count rather than a second field to keep in step. */
|
|
/* OBJ_VIEW: typed storage this file did not allocate and does not own.
|
|
The shape is in the header's [gen], which only a class instance
|
|
otherwise uses: VIEW_VEC means [base] is a [flan_dyn_vec_hdr *] whose
|
|
length is read live, which is why a Vec growing through the view
|
|
cannot go stale; VIEW_FLAT means [base] is the first element and the
|
|
header's [len] the count taken at the crossing, for a slice or a fixed
|
|
array; VIEW_STRUCT means [base] is one struct. [desc] is the element's
|
|
descriptor, or the struct's. [nul] is always NULL and sits where a
|
|
map's [klass] does, so a struct view answers "no class" to every class
|
|
question without a branch. A [view_guard] trails the header. */
|
|
struct { void *base; const uint8_t *desc; void *nul; } view;
|
|
/* OBJ_ENV: the descriptor the environment's bytes are marked through,
|
|
or NULL when it holds nothing the collector follows. [len] is the
|
|
byte count, and the bytes trail the header as a text's do. */
|
|
struct { const struct flan_desc *desc; } env;
|
|
/* OBJ_TEXT's bytes trail the header; see [obj_text_bytes]. */
|
|
} u;
|
|
} flan_obj;
|
|
|
|
/* How many dyn words hang off an object's items block — the count the marker
|
|
* walks and the sweep charges. A map holds two per entry.
|
|
*
|
|
* OBJ_VIEW answers 0 explicitly rather than falling into the [o->len] arm.
|
|
* [mark_push] never puts a view on the mark stack — it traces only
|
|
* OBJ_VEC/OBJ_MAP — so this is not reachable today, but [o->u.view.base]
|
|
* aliases [o->u.v.items] in the union, and a native array of i64 or f64
|
|
* reinterpreted as dyn words is exactly the kind of thing this file's
|
|
* roots contract exists to prevent happening by accident. Answering 0 here
|
|
* is what keeps a future change to the marking gate from silently trusting
|
|
* this function's default arm instead of failing loudly. */
|
|
static inline int64_t obj_words(flan_obj *o) {
|
|
if (o->kind == OBJ_VIEW || o->kind == OBJ_ENV) return 0;
|
|
return o->kind == OBJ_MAP ? o->len * 2 : o->len;
|
|
}
|
|
|
|
static inline uint8_t *obj_text_bytes(flan_obj *o) { return (uint8_t *)(o + 1); }
|
|
|
|
/* What trails an OBJ_VIEW's header: the dev check's record of its storage,
|
|
* explained with the view helpers below. Allocated with every view, and
|
|
* charged to the heap and taken back by the sweep with it. */
|
|
typedef struct view_guard {
|
|
const void *frame; /* NULL when no frame is checked */
|
|
uint64_t serial;
|
|
const char *fname; /* whose frame, for the sentence */
|
|
int64_t fnamelen;
|
|
uintptr_t rbase; /* 0 when no block is checked */
|
|
int64_t rseq;
|
|
const char *rtype;
|
|
int64_t rtypelen;
|
|
} view_guard;
|
|
|
|
static flan_obj *gc_all; /* the sweep list */
|
|
static int64_t gc_bytes; /* what the live objects hold, headers included */
|
|
static int64_t gc_count;
|
|
static int64_t gc_next; /* collect when an allocation would pass this */
|
|
static int64_t gc_floor = 1 << 20;
|
|
static int gc_ready;
|
|
|
|
/* ── Roots ─────────────────────────────────────────────────────────────
|
|
*
|
|
* Addresses of slots, pushed by the code that owns them. Not a conservative
|
|
* scan of the C stack, and the reason is worth stating once here rather than
|
|
* only in the doc: a conservative scan has to decide whether an arbitrary word
|
|
* is a pointer, and NaN-boxing makes that decision *wrong* in both directions
|
|
* — a live double is bit-identical to a boxed pointer often enough to retain
|
|
* garbage, and a payload with the box stripped is not the pointer the scanner
|
|
* would look for. Precision here is cheaper than the arguments about it.
|
|
*
|
|
* Growable, because a deep recursion over dyn locals is an ordinary program
|
|
* and a fixed table would be a limit nobody could predict. The array holds the
|
|
* addresses, so growing it moves the array and not the slots.
|
|
*
|
|
* A root is an address and a shape. The shape is NULL for the common case —
|
|
* the address is a dyn word and marking it is one call — and a [flan_desc] for
|
|
* an aggregate, which is a struct or an array of them with dyn fields
|
|
* somewhere inside. The descriptor is static data the compiler emitted for
|
|
* that type, and the pairing of address with descriptor is made at the *push*,
|
|
* by the code that knows what is at that address, which is why nothing in the
|
|
* heap or on the stack needs a header word for the collector to read. See
|
|
* flan_dyn.h's [flan_dyn_root_push_desc] for the whole of that argument. */
|
|
|
|
typedef struct {
|
|
void *base;
|
|
const flan_desc *desc; /* NULL: [base] is one flan_dyn */
|
|
} flan_root;
|
|
|
|
static flan_root *roots;
|
|
static int64_t roots_n, roots_cap;
|
|
|
|
/* How much of the bottom of that stack belongs to the globals rather than to
|
|
* any frame. The globals are pushed once, before the first frame runs, and
|
|
* never popped — so they are exactly the entries below this line, and every
|
|
* frame's roots are exactly the entries above it. Zero until a [main] says
|
|
* otherwise, which is also the right answer for a program that has no dyn
|
|
* globals to push. See [flan_dyn_root_globals_begin]. */
|
|
static int64_t roots_base;
|
|
|
|
/* ── The temporaries ring ──────────────────────────────────────────────
|
|
*
|
|
* The hazard this exists for, plainly: mark-sweep frees what is unreachable,
|
|
* and a freshly allocated object is unreachable until somebody roots it. So
|
|
*
|
|
* flan_dyn_push(v, flan_dyn_add(flan_dyn_from_bytes(p, n), ...));
|
|
*
|
|
* — or any expression with two allocating calls in it — can have the second
|
|
* allocation collect the result of the first, in the window before the
|
|
* compiler has stored either into a rooted slot. C's argument evaluation order
|
|
* is unspecified, so this is not even a window a careful emitter could close
|
|
* by ordering its calls.
|
|
*
|
|
* The answer is the smallest one that does not need the other lane to have
|
|
* read a document: every object this file allocates is written into a fixed
|
|
* ring, and the marker roots the whole ring unconditionally. Any expression
|
|
* making at most RING allocations before rooting its result is then safe, with
|
|
* no ABI change and no contract for anybody to get wrong. The cost is a store
|
|
* and a masked increment per allocation, and up to RING objects' worth of
|
|
* float in the heap — which the trigger absorbs, because the trigger is a
|
|
* fraction of live bytes and not a count.
|
|
*
|
|
* 64 slots. An expression with 65 allocating calls in it and no intervening
|
|
* root would be a single Flan form with 65 constructors in it, which is not a
|
|
* form anybody writes; if it ever is, the compiler roots its intermediates and
|
|
* this ring is belt on top of braces. */
|
|
|
|
#define RING 64
|
|
static flan_obj *ring[RING];
|
|
static unsigned ring_at;
|
|
|
|
/* ── Tag words ─────────────────────────────────────────────────────────
|
|
*
|
|
* One table. The trap messages below and [flan_dyn_tag_name] read it, so a
|
|
* sentence a program dies with and a name an inspector shows cannot drift
|
|
* apart. Words and never numbers: "cannot add int and text" is a sentence
|
|
* somebody can act on and "tag 2 and tag 4" is a puzzle. */
|
|
|
|
static const char *const tag_words[] = { "nil", "bool", "int", "float",
|
|
"text", "vec", "keyword", "map" };
|
|
|
|
#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
|
|
|
|
static inline flan_obj *dyn_obj(flan_dyn v) {
|
|
return (flan_obj *)(uintptr_t)dyn_payload(v);
|
|
}
|
|
|
|
/* An interned keyword's entry: the name's bytes trail the length, one malloc
|
|
* per distinct name, never freed. Not a flan_obj — the collector has no
|
|
* business with something immortal — and the tag alone says which it is. */
|
|
typedef struct kw_entry {
|
|
int64_t len;
|
|
/* bytes trail */
|
|
} kw_entry;
|
|
|
|
static inline kw_entry *dyn_kw(flan_dyn v) {
|
|
return (kw_entry *)(uintptr_t)dyn_payload(v);
|
|
}
|
|
|
|
static inline uint8_t *kw_bytes(kw_entry *k) { return (uint8_t *)(k + 1); }
|
|
|
|
int32_t flan_dyn_tag(flan_dyn v) {
|
|
if (!dyn_boxed(v)) return FLAN_DYN_TAG_FLOAT;
|
|
switch (dyn_box(v)) {
|
|
case BOX_NIL: return FLAN_DYN_TAG_NIL;
|
|
case BOX_BOOL: return FLAN_DYN_TAG_BOOL;
|
|
case BOX_INT: return FLAN_DYN_TAG_INT;
|
|
case BOX_KW: return FLAN_DYN_TAG_KEYWORD;
|
|
default: {
|
|
flan_obj *o = dyn_obj(v);
|
|
if (o == NULL) return FLAN_DYN_TAG_NIL;
|
|
switch (o->kind) {
|
|
case OBJ_TEXT: return FLAN_DYN_TAG_TEXT;
|
|
/* A view answers the same tag a heap vec does: from a dyn program's
|
|
side there is nothing to tell them apart by, which is the point of a
|
|
view being indistinguishable rather than a fourth kind of vec. */
|
|
case OBJ_VEC: return FLAN_DYN_TAG_VEC;
|
|
/* ...and a struct's view answers a map's, [gen] being its shape
|
|
(VIEW_STRUCT, below). */
|
|
case OBJ_VIEW: return (o->gen & 0xff) == 2 ? FLAN_DYN_TAG_MAP : FLAN_DYN_TAG_VEC;
|
|
case OBJ_MAP: return FLAN_DYN_TAG_MAP;
|
|
default: return FLAN_DYN_TAG_INT;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const char *flan_dyn_tag_name(int32_t tag) {
|
|
if (tag < 0 || tag > FLAN_DYN_TAG_MAP) return "?";
|
|
return tag_words[tag];
|
|
}
|
|
|
|
static inline const char *tag_of(flan_dyn v) {
|
|
return flan_dyn_tag_name(flan_dyn_tag(v));
|
|
}
|
|
|
|
/* ── Rendering, for messages and for print ─────────────────────────────
|
|
*
|
|
* One walk, two callers. [flan_dyn_print] writes to stdout through
|
|
* [flan_write_stdout], so a dyn print and a typed print interleave correctly
|
|
* in the one buffer; a trap message renders into a small buffer and puts the
|
|
* values in the sentence. [flan_dyn_emit_dev] and [flan_dyn_emit_watch] are
|
|
* the print walk aimed at flan_dev.c's buffers instead of stdout.
|
|
*
|
|
* What it renders, per tag, is what typed [print] renders for the
|
|
* corresponding type — captured from a running program rather than read off
|
|
* lib/render.ml, because that file is the REPL's inspector and not necessarily
|
|
* println's expansion:
|
|
*
|
|
* int %lld 42
|
|
* float %g, and "nan" unsigned 3.5, 1, nan
|
|
* bool the word true / false
|
|
* text bare at the top level, hi / "a b"
|
|
* quoted and escaped inside
|
|
* vec a slice's spelling [1 2 3]
|
|
*
|
|
* A space between elements and none inside the brackets, the same as
|
|
* lib/render.ml's slice loop: an acceptance test compares the two.
|
|
*
|
|
* nil is the one tag with no typed counterpart, and it renders as `nil`.
|
|
*
|
|
* A typed Vec prints as `<vec>` rather than structurally, and a dyn vec does
|
|
* not: it prints the way a *slice* does. That is deliberate and is argued in
|
|
* the doc — the typed refusal is about borrowing storage the printer does not
|
|
* own, and a dyn vec's storage is the collector's, so there is nothing to
|
|
* borrow and nobody to ask.
|
|
*
|
|
* DEPTH is a cycle stop and nothing else. A typed value cannot contain itself,
|
|
* so the typed printer needs no run-time cap; [flan_dyn_set_at] makes a dyn
|
|
* vec that can, so this one does. Past the cap it prints render.ml's "...",
|
|
* which is the same mark that file uses for the same idea. */
|
|
|
|
#define PRINT_DEPTH 16
|
|
|
|
/* Where the rendering goes. stdout for [print], or one of flan_dev.c's
|
|
* buffers when the value is an evaluated expression's or a watched one: a
|
|
* value written to stdout arrives on the program's output rather than as the
|
|
* value the editor asked for. */
|
|
typedef void (*dyn_sink)(const uint8_t *p, int64_t n);
|
|
|
|
static void emit(dyn_sink w, const char *s) {
|
|
w((const uint8_t *)s, (int64_t)strlen(s));
|
|
}
|
|
|
|
static void emit_n(dyn_sink w, const uint8_t *p, int64_t n) { w(p, n); }
|
|
|
|
/* A text inside a structure, quoted and escaped. The same table as
|
|
* flan_rt.c's [flan_escape_char], which is where the typed side's printers —
|
|
* [flan_escape_bytes] and flan_dev.c's emitters — now share their one copy of
|
|
* it: printers that disagree about what a string looks like are that many
|
|
* wire formats.
|
|
*
|
|
* This copy is deliberate, and the argument for it is docs/SPIKE-DUPLICITY.md
|
|
* §9's: the dyn printer lives inside the runtime that owns the storage it
|
|
* walks, which is why it prints a dyn vec structurally where the typed
|
|
* printer answers <vec>. The whole printer is this side's; the table it
|
|
* shares with the other side is the part that must not drift. If that table
|
|
* changes, change this one. Streamed rather than built, so there is no buffer
|
|
* to overrun and no length to cap. */
|
|
static void emit_escaped(dyn_sink w, const uint8_t *p, int64_t n) {
|
|
int64_t i;
|
|
emit(w, "\"");
|
|
for (i = 0; i < n; i++) {
|
|
unsigned char c = p[i];
|
|
switch (c) {
|
|
case '"': emit(w, "\\\""); break;
|
|
case '\\': emit(w, "\\\\"); break;
|
|
case '\n': emit(w, "\\n"); break;
|
|
case '\t': emit(w, "\\t"); break;
|
|
case '\r': emit(w, "\\r"); break;
|
|
default:
|
|
if (c < 0x20) {
|
|
char b[5];
|
|
snprintf(b, sizeof b, "\\x%02x", c);
|
|
emit(w, b);
|
|
} else {
|
|
emit_n(w, &c, 1);
|
|
}
|
|
}
|
|
}
|
|
emit(w, "\"");
|
|
}
|
|
|
|
static int64_t dyn_int_value(flan_dyn v); /* forward: both int shapes */
|
|
static double dyn_num_value(flan_dyn v);
|
|
|
|
/* forward: the view helpers, needed by [render] and [say_render] above where
|
|
* they are defined, alongside the container operations below */
|
|
static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_obj *o);
|
|
static inline void view_guard_check(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o);
|
|
/* A struct view's fields, for the map arms of the printers. */
|
|
static int64_t view_nfields(flan_obj *o);
|
|
static flan_dyn view_field_key(flan_obj *o, int64_t i);
|
|
static flan_dyn view_field_val(flan_obj *o, int64_t i);
|
|
static void view_struct_name(flan_obj *o, const char **name, int64_t *len);
|
|
/* 1 when element (or, with [field], field) [i] of view [o] is a u64 above
|
|
* the largest dyn int: it has no dyn value, and the printers write its
|
|
* digits instead of reading it. */
|
|
static int view_big_u64(flan_obj *o, int64_t i, int field,
|
|
unsigned long long *out);
|
|
|
|
/* forward: needed by [dyn_equal] below, defined alongside the view helpers
|
|
* further down — a length and an element reader that answer correctly
|
|
* whether [o] is an ordinary heap vec or a view over a typed container. */
|
|
static int64_t vecish_len(flan_obj *o);
|
|
static flan_dyn vecish_at(flan_obj *o, int64_t i);
|
|
|
|
|
|
static void render(dyn_sink w, flan_dyn v, int depth, int nested) {
|
|
char buf[64];
|
|
int32_t t = flan_dyn_tag(v);
|
|
if (depth > PRINT_DEPTH) { emit(w, "..."); return; }
|
|
switch (t) {
|
|
case FLAN_DYN_TAG_NIL:
|
|
emit(w, "nil");
|
|
return;
|
|
case FLAN_DYN_TAG_BOOL:
|
|
emit(w, dyn_payload(v) ? "true" : "false");
|
|
return;
|
|
case FLAN_DYN_TAG_INT:
|
|
snprintf(buf, sizeof buf, "%lld", (long long)dyn_int_value(v));
|
|
emit(w, buf);
|
|
return;
|
|
case FLAN_DYN_TAG_FLOAT: {
|
|
double d;
|
|
memcpy(&d, &v, sizeof d);
|
|
/* x != x rather than isnan, which keeps math.h out of this file and is
|
|
* the comparison flan_rt.c and the prelude both use. */
|
|
if (d != d) snprintf(buf, sizeof buf, "nan");
|
|
else snprintf(buf, sizeof buf, "%g", d);
|
|
emit(w, buf);
|
|
return;
|
|
}
|
|
case FLAN_DYN_TAG_TEXT: {
|
|
flan_obj *o = dyn_obj(v);
|
|
if (nested) emit_escaped(w, obj_text_bytes(o), o->len);
|
|
else emit_n(w, obj_text_bytes(o), o->len);
|
|
return;
|
|
}
|
|
/* A keyword prints with its colon, bare, at every depth: :a is its own
|
|
* spelling the way true is, and quoting it would make it a text. */
|
|
case FLAN_DYN_TAG_KEYWORD: {
|
|
kw_entry *k = dyn_kw(v);
|
|
emit(w, ":");
|
|
emit_n(w, kw_bytes(k), k->len);
|
|
return;
|
|
}
|
|
/* The map prints in edn's shape with the vec's spacing: a space between
|
|
* elements, key and value alike, so {:a 1 :b 2} sits beside the vec's
|
|
* [1 2 3]. Entries come out in insertion order, which is the only order
|
|
* the representation has. */
|
|
case FLAN_DYN_TAG_MAP: {
|
|
flan_obj *o = dyn_obj(v);
|
|
int64_t i;
|
|
/* A struct's view prints as the class instance it most resembles: its
|
|
* type's name as the shape tag, then its fields in declaration order. */
|
|
if (o->kind == OBJ_VIEW) {
|
|
const char *nm;
|
|
int64_t nl, n = view_nfields(o);
|
|
view_struct_name(o, &nm, &nl);
|
|
emit(w, "#");
|
|
emit_n(w, (const uint8_t *)nm, nl);
|
|
emit(w, "{");
|
|
for (i = 0; i < n; i++) {
|
|
if (i > 0) emit(w, " ");
|
|
render(w, view_field_key(o, i), depth + 1, 1);
|
|
emit(w, " ");
|
|
unsigned long long big;
|
|
if (view_big_u64(o, i, 1, &big)) {
|
|
snprintf(buf, sizeof buf, "%llu", big);
|
|
emit(w, buf);
|
|
} else
|
|
render(w, view_field_val(o, i), depth + 1, 1);
|
|
}
|
|
emit(w, "}");
|
|
return;
|
|
}
|
|
/* A class instance prints its shape tag in front, Clojure's own spelling
|
|
* for a record: #point{:x 1 :y 2}. The tag is not an entry, so it is
|
|
* written here or it is not written at all. */
|
|
if (o->u.v.klass != NULL) {
|
|
emit(w, "#");
|
|
emit_n(w, kw_bytes(o->u.v.klass), o->u.v.klass->len);
|
|
}
|
|
emit(w, "{");
|
|
for (i = 0; i < o->len; i++) {
|
|
if (i > 0) emit(w, " ");
|
|
render(w, o->u.v.items[i * 2], depth + 1, 1);
|
|
emit(w, " ");
|
|
render(w, o->u.v.items[i * 2 + 1], depth + 1, 1);
|
|
}
|
|
emit(w, "}");
|
|
return;
|
|
}
|
|
default: {
|
|
flan_obj *o = dyn_obj(v);
|
|
int64_t i, n = vecish_len(o);
|
|
emit(w, "[");
|
|
for (i = 0; i < n; i++) {
|
|
if (i > 0) emit(w, " ");
|
|
unsigned long long big;
|
|
if (o->kind == OBJ_VIEW && view_big_u64(o, i, 0, &big)) {
|
|
snprintf(buf, sizeof buf, "%llu", big);
|
|
emit(w, buf);
|
|
} else
|
|
render(w, vecish_at(o, i), depth + 1, 1);
|
|
}
|
|
emit(w, "]");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void print_walk(flan_dyn v) { render(flan_write_stdout, v, 0, 0); }
|
|
|
|
/* The same rendering into an evaluated expression's value, and into the watch
|
|
* slot [flan_dev_watch_begin] opened. lib/render.ml's dyn arm calls these on
|
|
* the inspecting side and [flan_dyn_print] on [println]'s. A text is quoted
|
|
* even at the top, because the typed side's renderer quotes a string there:
|
|
* the value "5" and the value 5 must not read alike. */
|
|
void flan_dyn_emit_dev(flan_dyn v) {
|
|
walk_site was = walk_enter(NULL, 0, "print");
|
|
render(flan_dev_emit, v, 0, 1);
|
|
walk_leave(was);
|
|
}
|
|
void flan_dyn_emit_watch(flan_dyn v) {
|
|
walk_site was = walk_enter(NULL, 0, "print");
|
|
render(flan_dev_watch_emit, v, 0, 1);
|
|
walk_leave(was);
|
|
}
|
|
|
|
/* And into a condition's message, which flan_rt.c's sink bounds. */
|
|
void flan_msg_emit(const uint8_t *p, int64_t n);
|
|
void flan_dyn_emit_msg(flan_dyn v) {
|
|
walk_site was = walk_enter(NULL, 0, "print");
|
|
render(flan_msg_emit, v, 0, 1);
|
|
walk_leave(was);
|
|
}
|
|
|
|
/* The same walk into a buffer, for a trap's sentence. Bounded and truncated
|
|
* rather than allocating: a trap is the one moment when allocating would be a
|
|
* second thing to go wrong, and the message's job is to name the value, not to
|
|
* reproduce it. The depth is 2 rather than PRINT_DEPTH for the same reason. */
|
|
|
|
#define SAY_MAX 96
|
|
|
|
typedef struct { char *p; int64_t n, cap; } sayer;
|
|
|
|
static void say_puts(sayer *s, const char *t) {
|
|
while (*t && s->n < s->cap - 1) s->p[s->n++] = *t++;
|
|
s->p[s->n] = '\0';
|
|
}
|
|
|
|
static void say_render(sayer *s, flan_dyn v, int depth) {
|
|
char buf[64];
|
|
int32_t t = flan_dyn_tag(v);
|
|
if (s->n >= s->cap - 4) return;
|
|
switch (t) {
|
|
case FLAN_DYN_TAG_NIL: say_puts(s, "nil"); return;
|
|
case FLAN_DYN_TAG_BOOL: say_puts(s, dyn_payload(v) ? "true" : "false"); return;
|
|
case FLAN_DYN_TAG_INT:
|
|
snprintf(buf, sizeof buf, "%lld", (long long)dyn_int_value(v));
|
|
say_puts(s, buf);
|
|
return;
|
|
case FLAN_DYN_TAG_FLOAT: {
|
|
double d;
|
|
memcpy(&d, &v, sizeof d);
|
|
if (d != d) snprintf(buf, sizeof buf, "nan");
|
|
else snprintf(buf, sizeof buf, "%g", d);
|
|
say_puts(s, buf);
|
|
return;
|
|
}
|
|
case FLAN_DYN_TAG_TEXT: {
|
|
flan_obj *o = dyn_obj(v);
|
|
int64_t i;
|
|
say_puts(s, "\"");
|
|
for (i = 0; i < o->len && s->n < s->cap - 6; i++) {
|
|
char c[2];
|
|
uint8_t b = obj_text_bytes(o)[i];
|
|
c[0] = b >= 0x20 ? (char)b : '.';
|
|
c[1] = '\0';
|
|
say_puts(s, c);
|
|
}
|
|
say_puts(s, i < o->len ? "...\"" : "\"");
|
|
return;
|
|
}
|
|
case FLAN_DYN_TAG_KEYWORD: {
|
|
kw_entry *k = dyn_kw(v);
|
|
int64_t i;
|
|
say_puts(s, ":");
|
|
for (i = 0; i < k->len && s->n < s->cap - 6; i++) {
|
|
char c[2];
|
|
c[0] = (char)kw_bytes(k)[i];
|
|
c[1] = '\0';
|
|
say_puts(s, c);
|
|
}
|
|
if (i < k->len) say_puts(s, "...");
|
|
return;
|
|
}
|
|
case FLAN_DYN_TAG_MAP: {
|
|
flan_obj *o = dyn_obj(v);
|
|
int64_t i;
|
|
if (depth >= 2) { say_puts(s, "{...}"); return; }
|
|
if (o->kind == OBJ_VIEW) {
|
|
const char *nm;
|
|
int64_t nl, n = view_nfields(o), j;
|
|
view_struct_name(o, &nm, &nl);
|
|
say_puts(s, "#");
|
|
for (j = 0; j < nl && s->n < s->cap - 8; j++) {
|
|
char c[2];
|
|
c[0] = nm[j];
|
|
c[1] = '\0';
|
|
say_puts(s, c);
|
|
}
|
|
say_puts(s, "{");
|
|
for (i = 0; i < n && s->n < s->cap - 8; i++) {
|
|
if (i > 0) say_puts(s, " ");
|
|
say_render(s, view_field_key(o, i), depth + 1);
|
|
say_puts(s, " ");
|
|
unsigned long long big;
|
|
if (view_big_u64(o, i, 1, &big)) {
|
|
char nb[32];
|
|
snprintf(nb, sizeof nb, "%llu", big);
|
|
say_puts(s, nb);
|
|
} else
|
|
say_render(s, view_field_val(o, i), depth + 1);
|
|
}
|
|
say_puts(s, i == n ? "}" : i > 0 ? " ...}" : "...}");
|
|
return;
|
|
}
|
|
/* The same tag [render] writes, so a trap sentence naming an instance
|
|
* says which class it was. Truncated with the rest when the buffer is
|
|
* short: [say] is a 96-byte sentence, not a printer. */
|
|
if (o->u.v.klass != NULL && s->n < s->cap - 8) {
|
|
int64_t j;
|
|
say_puts(s, "#");
|
|
for (j = 0; j < o->u.v.klass->len && s->n < s->cap - 8; j++) {
|
|
char c[2];
|
|
c[0] = (char)kw_bytes(o->u.v.klass)[j];
|
|
c[1] = '\0';
|
|
say_puts(s, c);
|
|
}
|
|
}
|
|
say_puts(s, "{");
|
|
for (i = 0; i < o->len && s->n < s->cap - 8; i++) {
|
|
if (i > 0) say_puts(s, " ");
|
|
say_render(s, o->u.v.items[i * 2], depth + 1);
|
|
say_puts(s, " ");
|
|
say_render(s, o->u.v.items[i * 2 + 1], depth + 1);
|
|
}
|
|
say_puts(s, i == o->len ? "}" : i > 0 ? " ...}" : "...}");
|
|
return;
|
|
}
|
|
default: {
|
|
flan_obj *o = dyn_obj(v);
|
|
int64_t i, n = vecish_len(o);
|
|
if (depth >= 2) { say_puts(s, "[...]"); return; }
|
|
say_puts(s, "[");
|
|
for (i = 0; i < n && s->n < s->cap - 8; i++) {
|
|
if (i > 0) say_puts(s, " ");
|
|
unsigned long long big;
|
|
if (o->kind == OBJ_VIEW && view_big_u64(o, i, 0, &big)) {
|
|
char nb[32];
|
|
snprintf(nb, sizeof nb, "%llu", big);
|
|
say_puts(s, nb);
|
|
} else
|
|
say_render(s, vecish_at(o, i), depth + 1);
|
|
}
|
|
say_puts(s, i == n ? "]" : i > 0 ? " ...]" : "...]");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void say(char *buf, int64_t cap, flan_dyn v) {
|
|
sayer s;
|
|
s.p = buf;
|
|
s.n = 0;
|
|
s.cap = cap;
|
|
buf[0] = '\0';
|
|
say_render(&s, v, 0);
|
|
}
|
|
|
|
/* ── Traps ─────────────────────────────────────────────────────────────
|
|
*
|
|
* The shape of every message: the operation, then what was wrong in words,
|
|
* then the call as it would have been written. So a program that adds a number
|
|
* to a string stops with
|
|
*
|
|
* dyn +: int and text, and + wants two numbers — (+ 3 "hi")
|
|
*
|
|
* which names the operation, both tags, and both values, in that order,
|
|
* because that is the order somebody reads it in. [flan_trap] then parks the
|
|
* program in a dev session and ends it standing up in a standalone build; the
|
|
* sentence is the same either way, which is the point of routing through the
|
|
* hook rather than calling abort here.
|
|
*
|
|
* The trap *name* — what the break loop shows and what a `layout` op will say
|
|
* it cannot place — is "DynType" for a tag that was not what the operation
|
|
* wanted, "DynRange" for an index outside a vec or a text, "DynArith" for a
|
|
* division by zero or the one quotient that overflows, and "DynHeap" for an
|
|
* allocation the host refused. Four names rather than one because they are
|
|
* four different mistakes and a person stopped in one of them wants to know
|
|
* which without reading the sentence twice — and because the break loop lists
|
|
* them by name. */
|
|
|
|
/* Where the operation was written, printed as flan_rt.c's traps print it
|
|
* ([flan_say] writes both): the GNU "file:line:col: " prefix, so `next-error`
|
|
* walks to the dyn failure the same way it walks to a bounds failure. The pair is what an emitted string
|
|
* literal already is — a pointer and a length, not a C string — and the
|
|
* emitter hands it over exactly as [flan_dyn_cast_kind]'s site does.
|
|
*
|
|
* A NULL [loc] prints nothing at all and the sentence after it is byte for
|
|
* byte the one this file printed before: the entry points that have not been
|
|
* given a site (everything but the arithmetic, the ordering, [at], [set-at]
|
|
* and [push]) pass NULL, and so does test/dyn_ops.c, which calls the runtime
|
|
* directly and has no source position to offer. */
|
|
|
|
/* A trap's sentence written in pieces, then said in one through [flan_say],
|
|
* so it reaches the break loop like every other. */
|
|
static char said_buf[2048];
|
|
static size_t said_len;
|
|
|
|
static void said_add(const char *fmt, ...) {
|
|
va_list ap;
|
|
int n;
|
|
if (said_len >= sizeof said_buf) return;
|
|
va_start(ap, fmt);
|
|
n = vsnprintf(said_buf + said_len, sizeof said_buf - said_len, fmt, ap);
|
|
va_end(ap);
|
|
if (n > 0) said_len += (size_t)n;
|
|
if (said_len >= sizeof said_buf) said_len = sizeof said_buf - 1;
|
|
}
|
|
|
|
static _Noreturn void trap2(const uint8_t *loc, int64_t loclen,
|
|
const char *name, int64_t namelen, const char *op,
|
|
const char *why, flan_dyn a, flan_dyn b) {
|
|
char sa[SAY_MAX], sb[SAY_MAX];
|
|
say(sa, SAY_MAX, a);
|
|
say(sb, SAY_MAX, b);
|
|
flan_say(loc, loclen, "dyn %s: %s and %s, and %s — (%s %s %s)", op,
|
|
tag_of(a), tag_of(b), why, op, sa, sb);
|
|
dyn_trap((const uint8_t *)name, namelen);
|
|
}
|
|
|
|
static _Noreturn void trap1(const uint8_t *loc, int64_t loclen,
|
|
const char *name, int64_t namelen, const char *op,
|
|
const char *why, flan_dyn a) {
|
|
char sa[SAY_MAX];
|
|
say(sa, SAY_MAX, a);
|
|
flan_say(loc, loclen, "dyn %s: %s, and %s — (%s %s)", op, tag_of(a), why, op,
|
|
sa);
|
|
dyn_trap((const uint8_t *)name, namelen);
|
|
}
|
|
|
|
#define TYPE_TRAP "DynType", 7
|
|
#define ARITH_TRAP "DynArith", 8
|
|
|
|
/* [at] and [set-at]'s, with the site their call was written at. */
|
|
static _Noreturn void trap_range(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_dyn v, int64_t i,
|
|
int64_t len) {
|
|
char sv[SAY_MAX];
|
|
say(sv, SAY_MAX, v);
|
|
flan_say(loc, loclen,
|
|
"dyn %s: index %lld is out of bounds for %s of length %lld — %s", op,
|
|
(long long)i, tag_of(v), (long long)len, sv);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
|
|
/* ── Allocation and collection ─────────────────────────────────────────
|
|
*
|
|
* Collection happens here and nowhere else, which is the fact the roots
|
|
* contract rests on: between two allocations nothing is swept, so a
|
|
* temporary living only in a C local survives the operation it was made in.
|
|
* A growing vec's element array is a [realloc] and not an allocation in this
|
|
* sense — it cannot collect, because the value being pushed may not be rooted
|
|
* yet. That means a program that only ever pushes can hold more than the
|
|
* trigger says before the next real allocation catches up, which is fine: what
|
|
* it is holding is the vec, and the vec is live.
|
|
*
|
|
* The trigger is the plainest one that works: collect when this allocation
|
|
* would carry the heap past a limit, then set the limit to twice what survived
|
|
* — with a floor, so a program with a tiny live set does not collect on every
|
|
* other allocation. That gives amortised O(1) collections per byte allocated
|
|
* and a heap bounded at twice the live set plus the floor, which is the
|
|
* property the million-allocation test asserts.
|
|
*
|
|
* "The answer to 'I need more performance' will never be a faster GC, it will
|
|
* be to type the whole program" — so there is no generation, no card table, no
|
|
* incremental phase, and no free list. */
|
|
|
|
static void gc_mark_all(void);
|
|
static void gc_sweep(void);
|
|
|
|
void flan_gc_init(void) {
|
|
if (gc_ready) return;
|
|
gc_ready = 1;
|
|
gc_all = NULL;
|
|
gc_bytes = 0;
|
|
gc_count = 0;
|
|
gc_next = gc_floor;
|
|
}
|
|
|
|
/* The trigger is recomputed from the new floor by the same formula the sweep
|
|
* uses, rather than only being raised to meet it. Raising alone left a heap
|
|
* that had been given a *lower* floor still running to the old one — the first
|
|
* collection then happened a megabyte in, and a test that had asked for 64K
|
|
* measured a megabyte. */
|
|
void flan_gc_set_floor(int64_t bytes) {
|
|
gc_floor = bytes > 0 ? bytes : (1 << 20);
|
|
gc_next = gc_bytes * 2;
|
|
if (gc_next < gc_floor) gc_next = gc_floor;
|
|
}
|
|
|
|
int64_t flan_gc_live_bytes(void) { return gc_bytes; }
|
|
int64_t flan_gc_count(void) { return gc_count; }
|
|
|
|
void flan_gc_collect(void) {
|
|
gc_mark_all();
|
|
gc_sweep();
|
|
gc_next = gc_bytes * 2;
|
|
if (gc_next < gc_floor) gc_next = gc_floor;
|
|
}
|
|
|
|
/* Out of memory is the one failure in here that is not the program's fault and
|
|
* not recoverable by anything this file can do. It takes the trap path like
|
|
* everything else, so a dev session parks on it and can be read, rather than
|
|
* the allocation quietly answering NULL and every caller below growing a null
|
|
* check for a case none of them can handle.
|
|
*
|
|
* [push] is the one caller with a site to give: its growth is the allocation
|
|
* a program's own line asked for. Every other caller is the collector's own
|
|
* bookkeeping — an object, a class table, the keyword table — and passes
|
|
* NULL, which prints no prefix. */
|
|
static _Noreturn void trap_oom(const uint8_t *loc, int64_t loclen,
|
|
int64_t want) {
|
|
flan_say(loc, loclen,
|
|
"dyn heap: %lld bytes could not be allocated, with %lld live",
|
|
(long long)want, (long long)gc_bytes);
|
|
dyn_trap((const uint8_t *)"DynHeap", 7);
|
|
}
|
|
|
|
static flan_obj *gc_alloc(uint8_t kind, int64_t extra) {
|
|
int64_t need = (int64_t)sizeof(flan_obj) + extra;
|
|
flan_obj *o;
|
|
if (!gc_ready) flan_gc_init();
|
|
if (gc_bytes + need > gc_next) flan_gc_collect();
|
|
o = (flan_obj *)malloc((size_t)need);
|
|
if (o == NULL) trap_oom(NULL, 0, need);
|
|
o->next = gc_all;
|
|
o->kind = kind;
|
|
o->mark = 0;
|
|
o->gen = 0;
|
|
o->len = 0;
|
|
memset(&o->u, 0, sizeof o->u);
|
|
gc_all = o;
|
|
gc_bytes += need;
|
|
gc_count++;
|
|
/* Into the ring before anything else can allocate. See the ring's comment:
|
|
* this is the one line that makes an expression with two constructors in it
|
|
* safe without the other lane having agreed to anything. */
|
|
ring[ring_at] = o;
|
|
ring_at = (ring_at + 1) % RING;
|
|
return o;
|
|
}
|
|
|
|
/* The mark stack. Explicit rather than recursive, because a vec of a vec of a
|
|
* vec is an ordinary dyn value and its depth is the program's, not this
|
|
* file's: a recursive marker would put the heap's depth on the C stack and a
|
|
* long enough chain would overflow it during a collection, which is the worst
|
|
* possible moment. Grown on demand and kept between collections, so a steady
|
|
* program stops paying for it after the first one. */
|
|
static flan_obj **mstack;
|
|
static int64_t mstack_n, mstack_cap;
|
|
|
|
static void mark_push(flan_obj *o) {
|
|
if (o == NULL || o->mark) return;
|
|
o->mark = 1;
|
|
/* Only a vec, a map and an environment with a descriptor have anything to
|
|
* trace. A text and a boxed int are leaves, and marking them is the whole
|
|
* of their visit. */
|
|
if (o->kind != OBJ_VEC && o->kind != OBJ_MAP
|
|
&& !(o->kind == OBJ_ENV && o->u.env.desc != NULL)) return;
|
|
if (mstack_n == mstack_cap) {
|
|
int64_t cap = mstack_cap ? mstack_cap * 2 : 64;
|
|
flan_obj **m = (flan_obj **)realloc(mstack, (size_t)cap * sizeof *m);
|
|
if (m == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *m);
|
|
mstack = m;
|
|
mstack_cap = cap;
|
|
}
|
|
mstack[mstack_n++] = o;
|
|
}
|
|
|
|
static void mark_value(flan_dyn v) {
|
|
if (dyn_boxed(v) && dyn_box(v) == BOX_OBJ) mark_push(dyn_obj(v));
|
|
}
|
|
|
|
/* ── Environments ──────────────────────────────────────────────────────
|
|
*
|
|
* The second word of an (Fn ...) value is one of three things: null, for a
|
|
* value made from a name or from an fn that captured nothing; the address of
|
|
* an environment this file allocated; or, for a named function widened into
|
|
* an Fn, that function's code address, which the widening thunk reads back
|
|
* and calls. The marker meets all three at the same offset and must follow
|
|
* only the second.
|
|
*
|
|
* Nothing in the word says which. A code address can have any low bits — on
|
|
* wasm32 it is a table index, a small integer — so no tag bit is free on that
|
|
* side, and dereferencing one to look for a header would read code or trap.
|
|
* So the collector keeps the set of environment addresses it has handed out
|
|
* and follows a word only when the set has it. An address that is not an
|
|
* environment is never read through, which also makes a stale or unwritten
|
|
* word harmless: at worst it keeps a live environment alive a little longer.
|
|
*
|
|
* Open addressing with linear probing. The sweep deletes each environment it
|
|
* frees, and shrinks the table once it is mostly empty. */
|
|
|
|
static uintptr_t *envset;
|
|
static int64_t envset_cap, envset_n;
|
|
|
|
static inline uint64_t ptr_hash(uintptr_t p) {
|
|
uint64_t x = (uint64_t)p;
|
|
x ^= x >> 33;
|
|
x *= 0xff51afd7ed558ccdULL;
|
|
x ^= x >> 33;
|
|
return x;
|
|
}
|
|
|
|
static void envset_put(uintptr_t p);
|
|
|
|
/* A fresh table of [cap] slots, a power of two, filled from [old]. */
|
|
static void envset_resize(int64_t cap) {
|
|
uintptr_t *old = envset;
|
|
int64_t oldcap = envset_cap, i;
|
|
envset = (uintptr_t *)calloc((size_t)cap, sizeof *envset);
|
|
if (envset == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *envset);
|
|
envset_cap = cap;
|
|
envset_n = 0;
|
|
for (i = 0; i < oldcap; i++)
|
|
if (old[i] != 0) envset_put(old[i]);
|
|
free(old);
|
|
}
|
|
|
|
static void envset_put(uintptr_t p) {
|
|
uint64_t h;
|
|
if ((envset_n + 1) * 2 > envset_cap)
|
|
envset_resize(envset_cap ? envset_cap * 2 : 64);
|
|
h = ptr_hash(p) & (uint64_t)(envset_cap - 1);
|
|
while (envset[h] != 0) {
|
|
if (envset[h] == p) return;
|
|
h = (h + 1) & (uint64_t)(envset_cap - 1);
|
|
}
|
|
envset[h] = p;
|
|
envset_n++;
|
|
}
|
|
|
|
static int envset_has(uintptr_t p) {
|
|
uint64_t h;
|
|
if (envset_cap == 0 || p == 0) return 0;
|
|
h = ptr_hash(p) & (uint64_t)(envset_cap - 1);
|
|
while (envset[h] != 0) {
|
|
if (envset[h] == p) return 1;
|
|
h = (h + 1) & (uint64_t)(envset_cap - 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* One environment freed. Backward-shift deletion, so the table needs no
|
|
* tombstones: every entry after the hole that could have been placed in it
|
|
* moves back. */
|
|
static void envset_del(uintptr_t p) {
|
|
uint64_t mask, i, j, k;
|
|
if (envset_cap == 0) return;
|
|
mask = (uint64_t)(envset_cap - 1);
|
|
i = ptr_hash(p) & mask;
|
|
while (envset[i] != p) {
|
|
if (envset[i] == 0) return;
|
|
i = (i + 1) & mask;
|
|
}
|
|
j = i;
|
|
for (;;) {
|
|
j = (j + 1) & mask;
|
|
if (envset[j] == 0) break;
|
|
k = ptr_hash(envset[j]) & mask;
|
|
/* Move [j] back into the hole at [i] unless its home lies cyclically
|
|
in (i, j]. */
|
|
if ((i <= j) ? (i < k && k <= j) : (i < k || k <= j)) continue;
|
|
envset[i] = envset[j];
|
|
i = j;
|
|
}
|
|
envset[i] = 0;
|
|
envset_n--;
|
|
}
|
|
|
|
/* After a sweep: a table that has emptied to an eighth of its size is
|
|
* rebuilt at a size for what is left, so a program that once held a million
|
|
* environments does not keep a million-slot table. */
|
|
static int64_t envs_made; /* environments allocated since the last sweep */
|
|
|
|
static void envset_shrink(void) {
|
|
int64_t cap = 64, want = envset_n * 4;
|
|
/* Room for as many as the last cycle made, so a program that makes and
|
|
drops closures at a steady rate does not shrink and regrow the table
|
|
every cycle. */
|
|
if (envs_made * 2 > want) want = envs_made * 2;
|
|
envs_made = 0;
|
|
if (envset_cap <= 64 || envset_n * 8 > envset_cap) return;
|
|
while (cap < want) cap *= 2;
|
|
if (cap < envset_cap) envset_resize(cap);
|
|
}
|
|
|
|
static void mark_env(uintptr_t w) {
|
|
if (envset_has(w)) mark_push((flan_obj *)w - 1);
|
|
}
|
|
|
|
/* ── The Vec blocks a marker may read ──────────────────────────────────
|
|
*
|
|
* A (Vec T) header is copied by value, so the one the marker is handed may be
|
|
* a stale copy whose block another copy's push has since reallocated and
|
|
* freed. Reading its elements would read freed memory — and a block large
|
|
* enough for malloc to have unmapped it faults. So the marker never trusts a
|
|
* header's pointer: flan_rt.c reports every Vec block it allocates, moves or
|
|
* frees through [flan_vec_block_hook], this table keeps the live ones with
|
|
* their byte size and the allocator epoch they were made at, and a header is
|
|
* followed only when its pointer is a live block — for no more elements than
|
|
* the block holds, and not after its allocator has been reset past that
|
|
* epoch. A stale header whose pointer malloc has since reused for another Vec
|
|
* is bounded by that Vec's block and its words go through the same checks as
|
|
* any other, so nothing outside a live block is ever read.
|
|
*
|
|
* The hook is installed by [flan_dyn_track_vecs], which a program that can
|
|
* make a collector-owned environment calls first thing in main; blocks made
|
|
* before it cannot hold one. Allocator headers are never freed (flan_rt.c's
|
|
* [flan_arena_destroy]), so reading one's epoch is always safe.
|
|
*
|
|
* Open addressing with tombstones, since blocks come and go all the time. */
|
|
|
|
typedef struct {
|
|
uintptr_t ptr; /* 0 empty, 1 deleted */
|
|
int64_t bytes;
|
|
void *alloc;
|
|
int64_t epoch;
|
|
} vblock;
|
|
|
|
static vblock *vblocks;
|
|
static int64_t vblocks_cap, vblocks_n, vblocks_used;
|
|
|
|
extern void (*flan_vec_block_hook)(void *old, void *fresh, int64_t bytes,
|
|
void *alloc, int64_t epoch);
|
|
|
|
static void vblock_put(uintptr_t p, int64_t bytes, void *alloc, int64_t epoch);
|
|
|
|
static void vblock_resize(int64_t cap) {
|
|
vblock *old = vblocks;
|
|
int64_t oldcap = vblocks_cap, i;
|
|
vblocks = (vblock *)calloc((size_t)cap, sizeof *vblocks);
|
|
if (vblocks == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *vblocks);
|
|
vblocks_cap = cap;
|
|
vblocks_n = 0;
|
|
vblocks_used = 0;
|
|
for (i = 0; i < oldcap; i++)
|
|
if (old[i].ptr > 1)
|
|
vblock_put(old[i].ptr, old[i].bytes, old[i].alloc, old[i].epoch);
|
|
free(old);
|
|
}
|
|
|
|
static vblock *vblock_find(uintptr_t p) {
|
|
uint64_t h;
|
|
if (vblocks_cap == 0 || p <= 1) return NULL;
|
|
h = ptr_hash(p) & (uint64_t)(vblocks_cap - 1);
|
|
while (vblocks[h].ptr != 0) {
|
|
if (vblocks[h].ptr == p) return &vblocks[h];
|
|
h = (h + 1) & (uint64_t)(vblocks_cap - 1);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void vblock_put(uintptr_t p, int64_t bytes, void *alloc, int64_t epoch) {
|
|
uint64_t h;
|
|
vblock *hit = vblock_find(p);
|
|
if (hit != NULL) {
|
|
hit->bytes = bytes; hit->alloc = alloc; hit->epoch = epoch;
|
|
return;
|
|
}
|
|
if ((vblocks_used + 1) * 2 > vblocks_cap) {
|
|
int64_t cap = 64;
|
|
while (cap < (vblocks_n + 1) * 4) cap *= 2;
|
|
vblock_resize(cap);
|
|
}
|
|
h = ptr_hash(p) & (uint64_t)(vblocks_cap - 1);
|
|
while (vblocks[h].ptr > 1) h = (h + 1) & (uint64_t)(vblocks_cap - 1);
|
|
if (vblocks[h].ptr == 0) vblocks_used++;
|
|
vblocks[h].ptr = p;
|
|
vblocks[h].bytes = bytes;
|
|
vblocks[h].alloc = alloc;
|
|
vblocks[h].epoch = epoch;
|
|
vblocks_n++;
|
|
}
|
|
|
|
static void vblock_drop(uintptr_t p) {
|
|
vblock *hit = vblock_find(p);
|
|
if (hit != NULL) { hit->ptr = 1; vblocks_n--; }
|
|
}
|
|
|
|
static void vblock_hook(void *old, void *fresh, int64_t bytes, void *alloc,
|
|
int64_t epoch) {
|
|
if (old != NULL) vblock_drop((uintptr_t)old);
|
|
if (fresh != NULL) vblock_put((uintptr_t)fresh, bytes, alloc, epoch);
|
|
}
|
|
|
|
void flan_dyn_track_vecs(void) { flan_vec_block_hook = vblock_hook; }
|
|
|
|
/* Vecs still to walk, as (live block, element count, element descriptor).
|
|
* Explicit for the mark stack's reason: a data type can hold a Vec of itself,
|
|
* so how deep Vecs nest is the data's and not the type's, and recursion would
|
|
* put it on the C stack. */
|
|
typedef struct { char *p; int64_t n; const flan_desc *e; } vec_work;
|
|
static vec_work *vstack;
|
|
static int64_t vstack_n, vstack_cap;
|
|
|
|
/* Maps still to walk: a live block's control run, its slots, the slot count,
|
|
* the stride and value offset the block's own head records, and the value's
|
|
* descriptor. Queued for the Vec queue's reason. */
|
|
typedef struct {
|
|
const uint8_t *ctrl; char *slots; int64_t cap, stride, voff;
|
|
const flan_desc *e;
|
|
} map_work;
|
|
static map_work *mapstack;
|
|
static int64_t mapstack_n, mapstack_cap;
|
|
|
|
/* A live block, not reset since it was made: the checks a Vec's block and a
|
|
* Map's share. The allocator header is never freed, so its epoch is always
|
|
* readable. */
|
|
static vblock *live_block(void *p) {
|
|
vblock *b = vblock_find((uintptr_t)p);
|
|
if (b == NULL) return NULL;
|
|
if (b->alloc != NULL
|
|
&& (int64_t)((flan_dyn_alloc_hdr *)b->alloc)->epoch != b->epoch)
|
|
return NULL;
|
|
return b;
|
|
}
|
|
|
|
/* Queue the map whose header is at [h]. The slot count comes from the
|
|
* header and everything else from the block, and the walk is bounded by the
|
|
* block's recorded size, so a stale header copy naming a block another map
|
|
* now owns reads nothing outside that block. */
|
|
static void queue_map(const flan_dyn_map_hdr *h, const flan_desc *e) {
|
|
vblock *b;
|
|
const int64_t *head;
|
|
int64_t cap, ctrl, stride, voff;
|
|
if (e == NULL || h->data == NULL || h->log2cap <= 0 || h->log2cap > 40)
|
|
return;
|
|
b = live_block(h->data);
|
|
if (b == NULL || b->bytes < DYN_MAP_HEAD) return;
|
|
cap = (int64_t)1 << h->log2cap;
|
|
ctrl = (DYN_MAP_HEAD + cap + (DYN_MAP_GROUP - 1) + (DYN_MAP_ALIGN - 1))
|
|
& ~(int64_t)(DYN_MAP_ALIGN - 1);
|
|
head = (const int64_t *)h->data;
|
|
stride = head[1];
|
|
voff = head[2];
|
|
if (stride <= 0 || voff < 0 || voff + e->size > stride) return;
|
|
if (ctrl > b->bytes || (b->bytes - ctrl) / stride < cap) return;
|
|
if (mapstack_n == mapstack_cap) {
|
|
int64_t c = mapstack_cap ? mapstack_cap * 2 : 16;
|
|
map_work *m = (map_work *)realloc(mapstack, (size_t)c * sizeof *m);
|
|
if (m == NULL) trap_oom(NULL, 0, c * (int64_t)sizeof *m);
|
|
mapstack = m;
|
|
mapstack_cap = c;
|
|
}
|
|
mapstack[mapstack_n].ctrl = (const uint8_t *)h->data + DYN_MAP_HEAD;
|
|
mapstack[mapstack_n].slots = (char *)h->data + ctrl;
|
|
mapstack[mapstack_n].cap = cap;
|
|
mapstack[mapstack_n].stride = stride;
|
|
mapstack[mapstack_n].voff = voff;
|
|
mapstack[mapstack_n].e = e;
|
|
mapstack_n++;
|
|
}
|
|
|
|
/* The words [d] names inside the instance at [base]. A Vec entry is checked
|
|
* against the live blocks above and queued; [mark_desc] drains the queue
|
|
* before it returns. */
|
|
static void mark_words(char *base, const flan_desc *d) {
|
|
int64_t j;
|
|
for (j = 0; j < d->n; j++) mark_value(*(flan_dyn *)(base + d->offs[j]));
|
|
for (j = 0; j < d->nenv; j++) mark_env(*(uintptr_t *)(base + d->envs[j]));
|
|
for (j = 0; j < d->nmap; j++)
|
|
queue_map((const flan_dyn_map_hdr *)(base + d->maps[j].off),
|
|
d->maps[j].elem);
|
|
for (j = 0; j < d->nvec; j++) {
|
|
flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)(base + d->vecs[j].off);
|
|
const flan_desc *e = d->vecs[j].elem;
|
|
vblock *b;
|
|
int64_t n;
|
|
if (e == NULL || e->size <= 0 || h->len <= 0) continue;
|
|
b = live_block(h->ptr);
|
|
if (b == NULL) continue;
|
|
n = b->bytes / e->size;
|
|
if (h->len < n) n = h->len;
|
|
if (vstack_n == vstack_cap) {
|
|
int64_t cap = vstack_cap ? vstack_cap * 2 : 16;
|
|
vec_work *v = (vec_work *)realloc(vstack, (size_t)cap * sizeof *v);
|
|
if (v == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *v);
|
|
vstack = v;
|
|
vstack_cap = cap;
|
|
}
|
|
vstack[vstack_n].p = (char *)h->ptr;
|
|
vstack[vstack_n].n = n;
|
|
vstack[vstack_n].e = e;
|
|
vstack_n++;
|
|
}
|
|
}
|
|
|
|
static void mark_desc(char *base, const flan_desc *d) {
|
|
mark_words(base, d);
|
|
while (vstack_n > 0 || mapstack_n > 0) {
|
|
int64_t i;
|
|
if (vstack_n > 0) {
|
|
vec_work w = vstack[--vstack_n];
|
|
for (i = 0; i < w.n; i++) mark_words(w.p + i * w.e->size, w.e);
|
|
} else {
|
|
map_work w = mapstack[--mapstack_n];
|
|
for (i = 0; i < w.cap; i++)
|
|
if (w.ctrl[i] & DYN_MAP_FULL)
|
|
mark_words(w.slots + i * w.stride + w.voff, w.e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── Texts a str was taken from ────────────────────────────────────────
|
|
*
|
|
* A dyn text crossing into a str ([flan_dyn_need_as]) is not copied: the str
|
|
* is the text's own bytes, which never move, since this collector never
|
|
* moves anything. What could happen is a free — a str in a typed local,
|
|
* field or Vec is not a root, and typed storage is never scanned — so the
|
|
* crossing pins the text here, keyed by the temp arena's stamp, and every
|
|
* collection marks the pins whose stamp is still live. The str then lasts
|
|
* as long as the text is reachable from dyn or until the next free-temp,
|
|
* whichever is later: the lifetime i64->bytes's text already has, and text
|
|
* kept longer is cloned, as there.
|
|
*
|
|
* Rooting the text in the crossing's frame would be shorter than that and
|
|
* wrong for a str returned or stored, and a pin for good would keep every
|
|
* text a frame loop ever crossed. A copy into the temp arena would be sound
|
|
* too; this is the same lifetime without the copy. */
|
|
const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch);
|
|
int32_t flan_temp_stamp_live(const void *a, uint64_t inc, uint64_t epoch);
|
|
|
|
typedef struct text_pin {
|
|
flan_obj *o;
|
|
const void *a;
|
|
uint64_t inc, epoch;
|
|
} text_pin;
|
|
|
|
static text_pin *pins;
|
|
static int64_t pins_n, pins_cap;
|
|
|
|
static void pins_prune(void) {
|
|
int64_t i, k = 0;
|
|
for (i = 0; i < pins_n; i++)
|
|
if (flan_temp_stamp_live(pins[i].a, pins[i].inc, pins[i].epoch))
|
|
pins[k++] = pins[i];
|
|
pins_n = k;
|
|
}
|
|
|
|
static void pin_text(flan_obj *o) {
|
|
uint64_t inc, epoch;
|
|
const void *a = flan_temp_stamp(&inc, &epoch);
|
|
text_pin *last = pins_n > 0 ? &pins[pins_n - 1] : NULL;
|
|
if (last != NULL && last->o == o && last->a == a && last->inc == inc
|
|
&& last->epoch == epoch)
|
|
return;
|
|
if (pins_n == pins_cap) {
|
|
pins_prune();
|
|
if (pins_n == pins_cap) {
|
|
int64_t cap = pins_cap ? pins_cap * 2 : 64;
|
|
text_pin *p = (text_pin *)realloc(pins, (size_t)cap * sizeof *p);
|
|
if (p == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *p);
|
|
pins = p;
|
|
pins_cap = cap;
|
|
}
|
|
}
|
|
pins[pins_n].o = o;
|
|
pins[pins_n].a = a;
|
|
pins[pins_n].inc = inc;
|
|
pins[pins_n].epoch = epoch;
|
|
pins_n++;
|
|
}
|
|
|
|
static void gc_mark_all(void) {
|
|
int64_t i;
|
|
unsigned k;
|
|
pins_prune();
|
|
for (i = 0; i < pins_n; i++) mark_push(pins[i].o);
|
|
for (i = 0; i < roots_n; i++) {
|
|
const flan_desc *d = roots[i].desc;
|
|
if (d == NULL) mark_value(*(flan_dyn *)roots[i].base);
|
|
else mark_desc((char *)roots[i].base, d);
|
|
}
|
|
for (k = 0; k < RING; k++) mark_push(ring[k]);
|
|
while (mstack_n > 0) {
|
|
flan_obj *o = mstack[--mstack_n];
|
|
int64_t n;
|
|
if (o->kind == OBJ_ENV) {
|
|
mark_desc((char *)(o + 1), o->u.env.desc);
|
|
continue;
|
|
}
|
|
n = obj_words(o);
|
|
for (i = 0; i < n; i++) mark_value(o->u.v.items[i]);
|
|
}
|
|
}
|
|
|
|
static void gc_sweep(void) {
|
|
flan_obj **link = &gc_all;
|
|
flan_obj *o = gc_all;
|
|
while (o != NULL) {
|
|
flan_obj *next = o->next;
|
|
if (o->mark) {
|
|
o->mark = 0;
|
|
link = &o->next;
|
|
} else {
|
|
int64_t held = (int64_t)sizeof(flan_obj);
|
|
if (o->kind == OBJ_TEXT || o->kind == OBJ_ENV) held += o->len;
|
|
if (o->kind == OBJ_VIEW && (o->gen & 0x100))
|
|
held += (int64_t)sizeof(view_guard);
|
|
if (o->kind == OBJ_ENV) envset_del((uintptr_t)(o + 1));
|
|
if (o->kind == OBJ_VEC || o->kind == OBJ_MAP) {
|
|
int64_t per = o->kind == OBJ_MAP ? 2 : 1;
|
|
held += o->u.v.cap * per * (int64_t)sizeof(flan_dyn);
|
|
free(o->u.v.items);
|
|
}
|
|
gc_bytes -= held;
|
|
gc_count--;
|
|
*link = next;
|
|
free(o);
|
|
}
|
|
o = next;
|
|
}
|
|
/* A freed environment's address left the set above, before malloc can hand
|
|
it out again as something else. */
|
|
envset_shrink();
|
|
}
|
|
|
|
void *flan_dyn_env_new(int64_t size, const flan_desc *d) {
|
|
flan_obj *o = gc_alloc(OBJ_ENV, size);
|
|
o->len = size;
|
|
o->u.env.desc = (d != NULL && (d->n > 0 || d->nenv > 0 || d->nvec > 0
|
|
|| d->nmap > 0))
|
|
? d : NULL;
|
|
memset(o + 1, 0, (size_t)size);
|
|
envset_put((uintptr_t)(o + 1));
|
|
envs_made++;
|
|
return (void *)(o + 1);
|
|
}
|
|
|
|
static void root_add(void *base, const flan_desc *d) {
|
|
if (roots_n == roots_cap) {
|
|
int64_t cap = roots_cap ? roots_cap * 2 : 64;
|
|
flan_root *r = (flan_root *)realloc(roots, (size_t)cap * sizeof *r);
|
|
if (r == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *r);
|
|
roots = r;
|
|
roots_cap = cap;
|
|
}
|
|
roots[roots_n].base = base;
|
|
roots[roots_n].desc = d;
|
|
roots_n++;
|
|
}
|
|
|
|
void flan_dyn_root_push(flan_dyn *slot) { root_add(slot, NULL); }
|
|
|
|
/* The aggregate form. One entry on the same stack, so one [flan_dyn_root_pop]
|
|
* takes off a mixture of the two and a function's pop count stays the number
|
|
* of pushes it made. A NULL descriptor is not an error — it is a type the
|
|
* compiler found no dyn in — but it still occupies an entry, because the count
|
|
* is what the epilogue knows, and it is turned into an empty descriptor rather
|
|
* than stored as NULL, which on this stack means something else. */
|
|
static const flan_desc desc_empty = { 0, 0, NULL, 0, NULL, 0, NULL, 0, NULL };
|
|
|
|
void flan_dyn_root_push_desc(void *base, const flan_desc *d) {
|
|
root_add(base, d == NULL ? &desc_empty : d);
|
|
}
|
|
|
|
/* Clamped at the globals rather than refused. A pop that outruns its pushes
|
|
* means the frame machinery is already out of step, and the useful thing at
|
|
* that point is a heap that still collects, not a second failure on top of the
|
|
* first. flan_rt.c's [flan_handler_pop] takes the same line for the same
|
|
* reason, by frame rather than by count. The floor is [roots_base] and not
|
|
* zero because the globals under it were never any frame's to pop: an
|
|
* over-popping frame taking them with it is the one way this clamp could turn
|
|
* a miscount into a use-after-free. */
|
|
void flan_dyn_root_pop(int64_t n) {
|
|
if (n <= 0) return;
|
|
roots_n = roots_n - n > roots_base ? roots_n - n : roots_base;
|
|
}
|
|
|
|
/* The two halves of "the globals are the bottom of this stack".
|
|
*
|
|
* [begin] empties it outright, because a re-entered [main] is about to push
|
|
* the same globals again and the entries the previous run left are the ones
|
|
* that would be duplicated. [end] records how many of them there are.
|
|
*
|
|
* Nothing between the two may allocate: between them the globals hold whatever
|
|
* the previous run left in them and are not rooted, so a collection there
|
|
* would sweep values the slots still point at. The emitted [main] calls
|
|
* [begin] immediately before the pushes and [end] immediately after, with only
|
|
* the pushes in between, which is what makes that hold. */
|
|
void flan_dyn_root_globals_begin(void) { roots_n = 0; roots_base = 0; }
|
|
void flan_dyn_root_globals_end(void) { roots_base = roots_n; }
|
|
|
|
void flan_dyn_root_reset(void) { roots_n = roots_base; }
|
|
|
|
/* The same for one evaluation that trapped: the roots its frames pushed are
|
|
* dropped, and the ones below it kept. */
|
|
int64_t flan_dyn_root_mark(void) { return roots_n; }
|
|
void flan_dyn_root_restore(int64_t n) { if (n <= roots_n) roots_n = n; }
|
|
|
|
/* ── Constructors ──────────────────────────────────────────────────────*/
|
|
|
|
flan_dyn flan_dyn_nil(void) { return dyn_make(BOX_NIL, 0); }
|
|
|
|
flan_dyn flan_dyn_from_bool(uint8_t b) {
|
|
return dyn_make(BOX_BOOL, b ? 1u : 0u);
|
|
}
|
|
|
|
flan_dyn flan_dyn_from_i64(int64_t x) {
|
|
flan_obj *o;
|
|
if (x >= DYN_INT_MIN && x <= DYN_INT_MAX)
|
|
return dyn_make(BOX_INT, (uint64_t)x);
|
|
/* Wider than the payload, so it goes on the heap. Rare by construction —
|
|
* see the representation note — and it is the case that keeps i64 an i64
|
|
* rather than a 48-bit integer with a different name. */
|
|
o = gc_alloc(OBJ_INT, 0);
|
|
o->u.i = x;
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
flan_dyn flan_dyn_from_f64(double x) {
|
|
flan_dyn v;
|
|
/* Every NaN becomes the one positive quiet NaN, which is what keeps a
|
|
* negative quiet NaN from being read back as a box. The argument that this
|
|
* loses nothing is in the representation note above and in flan_rt.c's
|
|
* [flan_f64_to_bytes]. */
|
|
if (x != x) return 0x7FF8000000000000ULL;
|
|
memcpy(&v, &x, sizeof v);
|
|
return v;
|
|
}
|
|
|
|
flan_dyn flan_dyn_from_bytes(const uint8_t *p, int64_t n) {
|
|
flan_obj *o;
|
|
if (n < 0) n = 0;
|
|
o = gc_alloc(OBJ_TEXT, n);
|
|
o->len = n;
|
|
if (n > 0) memcpy(obj_text_bytes(o), p, (size_t)n);
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
flan_dyn flan_dyn_vec_new(void) {
|
|
flan_obj *o = gc_alloc(OBJ_VEC, 0);
|
|
o->len = 0;
|
|
o->u.v.items = NULL;
|
|
o->u.v.cap = 0;
|
|
/* Shared arm, and nothing reads this for a vec; written anyway so that the
|
|
field's value is never whatever [gc_alloc] happened to leave. */
|
|
o->u.v.klass = NULL;
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
flan_dyn flan_dyn_map_new(void) {
|
|
flan_obj *o = gc_alloc(OBJ_MAP, 0);
|
|
o->len = 0;
|
|
o->u.v.items = NULL;
|
|
o->u.v.cap = 0;
|
|
o->u.v.klass = NULL;
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
/* ── Classes ───────────────────────────────────────────────────────────
|
|
*
|
|
* The registry a redefined (defclass ...) updates, and the lazy migration
|
|
* that makes the instances built against the old definition answer the new
|
|
* one. This is CLHS 4.3.6, [update-instance-for-redefined-class] included —
|
|
* see [class_hook]; docs/SBCL-REDEFINITION-NOTES.md is where the protocol
|
|
* was read off and candidate C is this.
|
|
*
|
|
* **Why a registry at all, when a class instance is already just a map.**
|
|
* Because a map cannot be asked what it is *supposed* to hold. The instance
|
|
* knows the keys it has; only the class knows the keys it ought to have, and
|
|
* "ought to have" is the whole content of a redefinition. So one entry per
|
|
* class name, holding the current slot list and a generation, and an
|
|
* instance holds the generation it was built against.
|
|
*
|
|
* **Why nothing here is a GC object.** A class's name and its slots are
|
|
* *names*, and [flan_dyn_kw]'s entries are interned, immortal and not on the
|
|
* collector's heap — the same argument the [klass] field makes one screen up.
|
|
* The table below is [malloc]ed, append-only and never freed, so the marker
|
|
* has nothing to trace here and no root has to be pushed for it. A registry
|
|
* of dyn vectors would have needed both, and would have needed them to
|
|
* survive a collection triggered from inside a migration.
|
|
*
|
|
* **What the registry constrains.** A store into a slot the class declares
|
|
* — the constructor's, [put]'s, [set]'s — is checked against the slot's
|
|
* type. A key the class does not declare is refused by [get], [put] and
|
|
* [set] alike ([trap_no_slot]), so an instance's keys are its class's slots
|
|
* and the migration below, which keeps only those, drops nothing a program
|
|
* wrote.
|
|
*
|
|
* **Where a migration happens.** [want_map], so every [get], [put] and
|
|
* [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances
|
|
* of different generations are compared as the class currently defines them
|
|
* rather than by the shapes they happen to be carrying. CLHS asks for "no
|
|
* later than the next time a slot is read or written" and those are the
|
|
* three places that read or write the slot *set*.
|
|
*
|
|
* **The two printers are deliberately not among them**, and the consequence
|
|
* is visible to whoever is sitting in front of the editor, so it is written
|
|
* out rather than left as a footnote. [render] — which [print] and every
|
|
* value the editor renders go through — and [say_render] — the 96-byte
|
|
* sentence a trap prints — both walk [items] raw and neither syncs.
|
|
*
|
|
* The reason is the same for both: [say_render] runs inside trap reporting,
|
|
* where the heap is whatever the trap left, and a printer that frees an
|
|
* object's entry block and installs another is not something to have on
|
|
* that path; [render] is the same function's sibling and is called from it
|
|
* for nested values, so splitting them would put a mutation one recursion
|
|
* below a trap anyway.
|
|
*
|
|
* What that costs: **a stale instance shows its OLD slots to the editor
|
|
* until something touches it.** A watch expression, the value [C-x C-e]
|
|
* answers, and the inspector's render of a dyn all reach a class instance
|
|
* through [render], so immediately after a [defclass] is redefined the
|
|
* inspector can show a slot the class no longer has, and not show one it
|
|
* has gained — while [(get p :z)] typed at the same instant answers the new
|
|
* definition and migrates it, after which the inspector agrees. CLHS's
|
|
* "implementation-dependent time" permits it and it is the price of the
|
|
* printer staying a printer; it is not a bug report waiting to happen only
|
|
* because it is written down here, in TODO.org, "A redefined defclass
|
|
* migrates its instances lazily", and nowhere else. */
|
|
|
|
/* Interning, which is under "Keywords" further down. This file includes no
|
|
* header of its own — every entry point is written out in flan_dyn.h and
|
|
* defined here in the order the sections read best — so the one call that
|
|
* runs ahead of its definition declares itself. */
|
|
flan_dyn flan_dyn_kw(const uint8_t *p, int64_t n);
|
|
|
|
/* What a slot may hold. A dyn value's tag is the whole of what can be asked
|
|
* of it, so these are the tags — plus a range on top of the int tag for a
|
|
* narrower integer type, the significand a float slot holds exactly, a
|
|
* class for a slot declared with one, and whether nil is admitted, which is
|
|
* what (Option T) says. [word] is a scalar type's name, for the sentence a
|
|
* refusal prints. */
|
|
enum { ST_ANY, ST_BOOL, ST_INT, ST_FLOAT, ST_TEXT, ST_CLASS };
|
|
|
|
typedef struct slot_type {
|
|
uint8_t kind;
|
|
uint8_t opt; /* nil admitted: (Option T) */
|
|
uint8_t fbits; /* ST_FLOAT: 53 for f64, 24 for f32 */
|
|
int64_t lo, hi; /* ST_INT only */
|
|
kw_entry *cls; /* ST_CLASS only */
|
|
const char *word; /* static; NULL for ST_ANY and ST_CLASS */
|
|
} slot_type;
|
|
|
|
typedef struct class_entry {
|
|
kw_entry *name;
|
|
kw_entry **slots; /* interned, immortal, in declaration order */
|
|
slot_type *types; /* one per slot, same order */
|
|
/* Per slot, the generation a migration last warned about a value that no
|
|
* longer fits the slot's type. One warning per slot per redefinition,
|
|
* however many instances carry such a value. */
|
|
uint32_t *warned;
|
|
int64_t nslots;
|
|
uint32_t gen;
|
|
/* Whether any slot has a type. A class with none pays nothing at a store
|
|
* beyond reading this. */
|
|
int typed;
|
|
} class_entry;
|
|
|
|
static class_entry *classes;
|
|
static int64_t classes_n, classes_cap;
|
|
|
|
static class_entry *class_find(kw_entry *name) {
|
|
int64_t i;
|
|
for (i = 0; i < classes_n; i++)
|
|
if (classes[i].name == name) return &classes[i];
|
|
return NULL;
|
|
}
|
|
|
|
/* The generation a new instance of [name] is stamped with. Zero for a class
|
|
* no definition has been registered for — which, now that the constructor
|
|
* registers its class, is only an instance built by something other than a
|
|
* constructor: test/dyn_ops.c, calling the runtime directly. */
|
|
static uint32_t class_gen(kw_entry *name) {
|
|
class_entry *e = class_find(name);
|
|
return e == NULL ? 0u : e->gen;
|
|
}
|
|
|
|
/* One slot's type, as [Check.class_spec_of] writes it: a scalar type's name,
|
|
* [#name] for a class, and a leading [?] for an (Option T). */
|
|
static slot_type slot_type_of(const uint8_t *w, int64_t n) {
|
|
static const struct {
|
|
const char *w; uint8_t kind, fbits; int64_t lo, hi;
|
|
} known[] = {
|
|
{ "bool", ST_BOOL, 0, 0, 0 },
|
|
{ "str", ST_TEXT, 0, 0, 0 },
|
|
{ "f32", ST_FLOAT, 24, 0, 0 },
|
|
{ "f64", ST_FLOAT, 53, 0, 0 },
|
|
{ "i8", ST_INT, 0, INT8_MIN, INT8_MAX },
|
|
{ "i16", ST_INT, 0, INT16_MIN, INT16_MAX },
|
|
{ "i32", ST_INT, 0, INT32_MIN, INT32_MAX },
|
|
{ "i64", ST_INT, 0, INT64_MIN, INT64_MAX },
|
|
{ "u8", ST_INT, 0, 0, UINT8_MAX },
|
|
{ "u16", ST_INT, 0, 0, UINT16_MAX },
|
|
{ "u32", ST_INT, 0, 0, UINT32_MAX },
|
|
{ "u64", ST_INT, 0, 0, INT64_MAX },
|
|
};
|
|
slot_type t = { ST_ANY, 0, 0, 0, 0, NULL, NULL };
|
|
size_t i;
|
|
if (n > 0 && w[0] == '?') {
|
|
t = slot_type_of(w + 1, n - 1);
|
|
if (t.kind != ST_ANY) t.opt = 1;
|
|
return t;
|
|
}
|
|
if (n > 1 && w[0] == '#') {
|
|
t.kind = ST_CLASS;
|
|
t.cls = dyn_kw(flan_dyn_kw(w + 1, n - 1));
|
|
return t;
|
|
}
|
|
for (i = 0; i < sizeof known / sizeof known[0]; i++)
|
|
if ((int64_t)strlen(known[i].w) == n && memcmp(known[i].w, w, (size_t)n) == 0) {
|
|
t.kind = known[i].kind;
|
|
t.fbits = known[i].fbits;
|
|
t.lo = known[i].lo;
|
|
t.hi = known[i].hi;
|
|
t.word = known[i].w;
|
|
return t;
|
|
}
|
|
/* A word this table does not know is a compiler newer than this runtime.
|
|
Holding anything is the answer that loses no data. */
|
|
return t;
|
|
}
|
|
|
|
static int slot_type_eq(const slot_type *a, const slot_type *b) {
|
|
return a->kind == b->kind && a->opt == b->opt && a->fbits == b->fbits
|
|
&& a->lo == b->lo && a->hi == b->hi && a->cls == b->cls;
|
|
}
|
|
|
|
/* The type as it was written, for a sentence. */
|
|
static void slot_type_text(const slot_type *t, char *buf, size_t cap) {
|
|
char base[96];
|
|
if (t->kind == ST_CLASS)
|
|
snprintf(base, sizeof base, "%.*s", (int)t->cls->len,
|
|
(const char *)(t->cls + 1));
|
|
else
|
|
snprintf(base, sizeof base, "%s", t->word != NULL ? t->word : "dyn");
|
|
if (t->opt) snprintf(buf, cap, "(Option %s)", base);
|
|
else snprintf(buf, cap, "%s", base);
|
|
}
|
|
|
|
/* Whether [v] may be stored in a slot of type [t], and what is stored: [v]
|
|
* itself, or — for an int into a float slot — the float it widens to. The
|
|
* widening is the typed side's rule read off the value rather than off a
|
|
* static type: an integer the float holds exactly is admitted as that float,
|
|
* and one it does not is refused, as (f64 x) would be for the
|
|
* type that could hold it. A float into an f32 slot has to be one an f32
|
|
* holds, which is the typed side refusing f64 into f32. */
|
|
static int slot_admit(const slot_type *t, flan_dyn v, flan_dyn *out) {
|
|
int tag = flan_dyn_tag(v);
|
|
*out = v;
|
|
if (t->kind == ST_ANY) return 1;
|
|
if (tag == FLAN_DYN_TAG_NIL) return t->opt;
|
|
switch (t->kind) {
|
|
case ST_BOOL: return tag == FLAN_DYN_TAG_BOOL;
|
|
case ST_TEXT: return tag == FLAN_DYN_TAG_TEXT;
|
|
case ST_CLASS:
|
|
return tag == FLAN_DYN_TAG_MAP && dyn_obj(v)->u.v.klass == t->cls;
|
|
case ST_FLOAT:
|
|
if (tag == FLAN_DYN_TAG_FLOAT) {
|
|
double d = dyn_num_value(v);
|
|
return t->fbits == 53 || d != d || (double)(float)d == d;
|
|
}
|
|
if (tag == FLAN_DYN_TAG_INT) {
|
|
/* Exact is a round trip, not a range: 2^54 is an f64 exactly and
|
|
2^53+1 is not. The range test before the cast back is what keeps
|
|
that cast defined, since INT64_MAX rounds up to 2^63. */
|
|
int64_t x = dyn_int_value(v);
|
|
double d = t->fbits == 53 ? (double)x : (double)(float)x;
|
|
if (!(d >= -9223372036854775808.0 && d < 9223372036854775808.0)
|
|
|| (int64_t)d != x)
|
|
return 0;
|
|
*out = flan_dyn_from_f64(d);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
case ST_INT: {
|
|
int64_t x;
|
|
if (tag != FLAN_DYN_TAG_INT) return 0;
|
|
x = dyn_int_value(v);
|
|
return x >= t->lo && x <= t->hi;
|
|
}
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
static int slot_fits(const slot_type *t, flan_dyn v) {
|
|
flan_dyn ignored;
|
|
return slot_admit(t, v, &ignored);
|
|
}
|
|
|
|
/* A class's slots as the compiler hands them over: one line per slot, the
|
|
* slot's name and then, after a space, its type's name — nothing for a slot
|
|
* written with no type. The same string comes from a constructor and from a
|
|
* reload, so it is read in one place. The count is returned; both arrays are
|
|
* NULL for a class with no slots, which allocates nothing. */
|
|
static int64_t class_spec(const uint8_t *spec, int64_t n, kw_entry ***names,
|
|
slot_type **types) {
|
|
int64_t count = 0, i, start;
|
|
if (n < 0) n = 0;
|
|
*names = NULL;
|
|
*types = NULL;
|
|
for (i = 0, start = 0; i <= n; i++)
|
|
if (i == n ? i > start : spec[i] == '\n') {
|
|
if (i > start) count++;
|
|
start = i + 1;
|
|
}
|
|
if (count == 0) return 0;
|
|
*names = (kw_entry **)malloc((size_t)count * sizeof **names);
|
|
*types = (slot_type *)malloc((size_t)count * sizeof **types);
|
|
if (*names == NULL || *types == NULL)
|
|
trap_oom(NULL, 0, count * (int64_t)(sizeof **names + sizeof **types));
|
|
count = 0;
|
|
for (i = 0, start = 0; i <= n; i++)
|
|
if (i == n ? i > start : spec[i] == '\n') {
|
|
if (i > start) {
|
|
int64_t sp = start;
|
|
while (sp < i && spec[sp] != ' ') sp++;
|
|
(*names)[count] = dyn_kw(flan_dyn_kw(spec + start, sp - start));
|
|
(*types)[count] = sp < i ? slot_type_of(spec + sp + 1, i - sp - 1)
|
|
: slot_type_of(NULL, 0);
|
|
count++;
|
|
}
|
|
start = i + 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void class_add(kw_entry *k, kw_entry **list, slot_type *types,
|
|
int64_t count) {
|
|
if (classes_n == classes_cap) {
|
|
int64_t cap = classes_cap ? classes_cap * 2 : 8;
|
|
class_entry *t =
|
|
(class_entry *)realloc(classes, (size_t)cap * sizeof *t);
|
|
if (t == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *t);
|
|
classes = t;
|
|
classes_cap = cap;
|
|
}
|
|
classes[classes_n].name = k;
|
|
classes[classes_n].slots = list;
|
|
classes[classes_n].types = types;
|
|
classes[classes_n].warned =
|
|
count > 0 ? (uint32_t *)calloc((size_t)count, sizeof(uint32_t)) : NULL;
|
|
if (count > 0 && classes[classes_n].warned == NULL)
|
|
trap_oom(NULL, 0, count * (int64_t)sizeof(uint32_t));
|
|
classes[classes_n].nslots = count;
|
|
classes[classes_n].typed = 0;
|
|
{
|
|
int64_t j;
|
|
for (j = 0; j < count; j++)
|
|
if (types[j].kind != ST_ANY) classes[classes_n].typed = 1;
|
|
}
|
|
/* One, never zero: an instance built before this registration carries zero
|
|
* and has to be seen as stale, because the definition it was built from is
|
|
* exactly the one nobody recorded. */
|
|
classes[classes_n].gen = 1u;
|
|
classes_n++;
|
|
}
|
|
|
|
/* One class's current definition, as the compiler's per-reload thunk hands
|
|
* it over: the class's name as a keyword, and [class_spec]'s string.
|
|
*
|
|
* The generation is bumped only when the definition actually differs — a
|
|
* slot's name or its type. That is what makes C-c C-k idempotent: reloading
|
|
* a file re-runs every one of its class definitions, and a bump per reload
|
|
* would migrate every instance in the program every time anybody saved, for
|
|
* no change. */
|
|
void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) {
|
|
kw_entry *k;
|
|
kw_entry **list;
|
|
slot_type *types;
|
|
int64_t count, i;
|
|
class_entry *e;
|
|
if (flan_dyn_tag(name) != FLAN_DYN_TAG_KEYWORD)
|
|
/* No location: the caller is the thunk a reload runs, which has no
|
|
source position of its own — the class's own [defclass] is where a
|
|
reader would look, and it is not on any stack by the time this runs.
|
|
Unreachable from written Flan in any case; only the compiler emits
|
|
this call, and it emits a keyword. */
|
|
trap1(NULL, 0, TYPE_TRAP, "class definition",
|
|
"a class name is a keyword", name);
|
|
k = dyn_kw(name);
|
|
count = class_spec(slots, n, &list, &types);
|
|
e = class_find(k);
|
|
if (e != NULL) {
|
|
int same = e->nslots == count;
|
|
if (same)
|
|
for (i = 0; i < count; i++)
|
|
if (e->slots[i] != list[i] || !slot_type_eq(&e->types[i], &types[i])) {
|
|
same = 0;
|
|
break;
|
|
}
|
|
if (same) { free(list); free(types); return; }
|
|
free(e->slots);
|
|
free(e->types);
|
|
free(e->warned);
|
|
e->slots = list;
|
|
e->types = types;
|
|
e->warned =
|
|
count > 0 ? (uint32_t *)calloc((size_t)count, sizeof(uint32_t)) : NULL;
|
|
if (count > 0 && e->warned == NULL)
|
|
trap_oom(NULL, 0, count * (int64_t)sizeof(uint32_t));
|
|
e->nslots = count;
|
|
e->typed = 0;
|
|
for (i = 0; i < count; i++)
|
|
if (types[i].kind != ST_ANY) e->typed = 1;
|
|
/* Wrapping is not a correctness question — what matters is that the new
|
|
* generation differs from the one the live instances carry — but zero is
|
|
* reserved for "no definition registered", so it is stepped over. */
|
|
e->gen = e->gen + 1u;
|
|
if (e->gen == 0u) e->gen = 1u;
|
|
return;
|
|
}
|
|
class_add(k, list, types, count);
|
|
}
|
|
|
|
/* update-instance-for-redefined-class's dispatcher, as the last reload that
|
|
* installed a class or one of its methods left it; NULL until then. Set by a
|
|
* thunk and not found by name, because the name is a Flan symbol this file
|
|
* cannot spell and the body behind it moves with every method added. */
|
|
static void *migrate_fn;
|
|
|
|
void flan_dyn_class_hook(void *fn) { migrate_fn = fn; }
|
|
|
|
extern int (*flan_dyn_migrate_hook)(void *fn, uint64_t instance,
|
|
uint64_t added, uint64_t discarded);
|
|
|
|
/* Allocation and the map operations are further down, under their own
|
|
* headings; the hook's arguments are built with them. */
|
|
flan_dyn flan_dyn_vec_new(void);
|
|
flan_dyn flan_dyn_map_new(void);
|
|
void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen);
|
|
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v);
|
|
flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
|
|
int64_t loclen);
|
|
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
|
|
int64_t loclen);
|
|
|
|
/* The user hook, run on an instance the name-matching has just brought up to
|
|
* date. [inst], [added] and [gone] are rooted by the caller.
|
|
*
|
|
* Two things are kept for the length of the call, because the call is
|
|
* arbitrary Flan and may allocate as much as it likes:
|
|
*
|
|
* - the name-matched entries, rooted, so that taking the restart puts the
|
|
* instance back exactly as name-matching left it, whatever the method did
|
|
* to it before it signalled. That is the restart's whole meaning, and it
|
|
* is SBCL's choice of what a failed update leaves (std-class.lisp, the
|
|
* nlx-protect around the call) moved one step: SBCL restores the obsolete
|
|
* instance and retries at the next access, where here the name-matched
|
|
* one is kept and nothing is retried.
|
|
* - the temporaries ring, saved and put back. A migration starts inside
|
|
* [get] or [put], whose caller may be holding an object only the ring
|
|
* keeps alive — the result of the call beside it in the same expression.
|
|
* A method that allocates more than the ring holds would push it out and
|
|
* let the next collection free it, so the ring is rooted for the call and
|
|
* restored after it, and the caller sees the ring it left. */
|
|
static void class_hook(flan_obj *o, flan_dyn inst, flan_dyn added,
|
|
flan_dyn gone, int64_t n) {
|
|
flan_dyn *snap = NULL;
|
|
flan_obj *ring_was[RING];
|
|
flan_dyn ring_rooted[RING];
|
|
unsigned ring_at_was = ring_at, k;
|
|
int64_t j, roots_at = roots_n;
|
|
int r;
|
|
if (n > 0) {
|
|
snap = (flan_dyn *)malloc((size_t)n * 2 * sizeof *snap);
|
|
if (snap == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *snap);
|
|
memcpy(snap, o->u.v.items, (size_t)n * 2 * sizeof *snap);
|
|
for (j = 0; j < n; j++) root_add(&snap[j * 2 + 1], NULL);
|
|
}
|
|
memcpy(ring_was, ring, sizeof ring);
|
|
for (k = 0; k < RING; k++) {
|
|
ring_rooted[k] = ring[k] == NULL
|
|
? dyn_make(BOX_NIL, 0)
|
|
: dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)ring[k]);
|
|
root_add(&ring_rooted[k], NULL);
|
|
}
|
|
r = flan_dyn_migrate_hook(migrate_fn, inst, added, gone);
|
|
memcpy(ring, ring_was, sizeof ring);
|
|
ring_at = ring_at_was;
|
|
/* Nothing below allocates on the collector's heap, so the roots into
|
|
[snap] and this frame can go before either does. */
|
|
roots_n = roots_at;
|
|
if (r == 1) {
|
|
/* The restart: the entries name-matching left, in a block of their own,
|
|
* whatever the method grew or shrank the instance to. */
|
|
flan_dyn *back = NULL;
|
|
if (n > 0) {
|
|
back = (flan_dyn *)malloc((size_t)n * 2 * sizeof *back);
|
|
if (back == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *back);
|
|
memcpy(back, snap, (size_t)n * 2 * sizeof *back);
|
|
}
|
|
gc_bytes += (n - o->u.v.cap) * 2 * (int64_t)sizeof(flan_dyn);
|
|
free(o->u.v.items);
|
|
o->u.v.items = back;
|
|
o->u.v.cap = n;
|
|
o->len = n;
|
|
}
|
|
free(snap);
|
|
if (r == 2) {
|
|
kw_entry *c = o->u.v.klass;
|
|
flan_say(NULL, 0,
|
|
"dyn migrate: update-instance-for-redefined-class, migrating an "
|
|
"instance of %.*s, was left for a restart established outside "
|
|
"it. A migration runs inside get, put or set, and cannot be "
|
|
"left for one of their callers; the instance is kept as its "
|
|
"slots matched by name. Take migrate-by-name, or handle the "
|
|
"condition inside the method",
|
|
(int)c->len, (const char *)(c + 1));
|
|
dyn_trap((const uint8_t *)"DynMigrate", 10);
|
|
}
|
|
}
|
|
|
|
/* An entry at the end of a map, with no lookup first: for a map whose keys
|
|
* are known to be distinct already. A lookup compares keys with [dyn_equal],
|
|
* which migrates any stale instance it meets and runs that instance's hook —
|
|
* and a migration building its own hook's arguments must not start another
|
|
* one, or the instance it is migrating is migrated again inside itself. */
|
|
static void map_append(flan_obj *m, flan_dyn k, flan_dyn v) {
|
|
if (m->len == m->u.v.cap) {
|
|
int64_t cap = m->u.v.cap ? m->u.v.cap * 2 : 8;
|
|
flan_dyn *items =
|
|
(flan_dyn *)realloc(m->u.v.items, (size_t)cap * 2 * sizeof *items);
|
|
if (items == NULL) trap_oom(NULL, 0, cap * 2 * (int64_t)sizeof *items);
|
|
gc_bytes += (cap - m->u.v.cap) * 2 * (int64_t)sizeof *items;
|
|
m->u.v.items = items;
|
|
m->u.v.cap = cap;
|
|
}
|
|
m->u.v.items[m->len * 2] = k;
|
|
m->u.v.items[m->len * 2 + 1] = v;
|
|
m->len++;
|
|
}
|
|
|
|
/* Which of [o]'s entries is the slot [s], or -1. The interned identity
|
|
* compare, never [dyn_equal]: see [map_append]. [flan_dyn_tag] and not a
|
|
* bare [dyn_box]: a float is not boxed at all, so its payload bits can read
|
|
* as any box tag, and reading a non-keyword's payload as a [kw_entry *] is a
|
|
* wild pointer. A raw [put] can have left a float — or anything else — as a
|
|
* key. */
|
|
static int64_t entry_of(flan_obj *o, kw_entry *s) {
|
|
int64_t i;
|
|
for (i = 0; i < o->len; i++) {
|
|
flan_dyn key = o->u.v.items[i * 2];
|
|
if (flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD && dyn_kw(key) == s)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* The migration. [o] is left holding exactly the class's current slots, in
|
|
* the class's order, with the values it already had for the ones it still
|
|
* has — which is the property CLHS 4.3.6 guarantees, matched by name, with
|
|
* the instance's identity preserved because none of this allocates a new
|
|
* object. A slot it has just gained holds its type's zero value, Flan's
|
|
* zero-is-initialisation — false, 0, 0.0, the empty string — or nil where
|
|
* the type admits nil or has no zero: a dyn slot, an (Option T), a class.
|
|
*
|
|
* Rebuilt into a fresh block rather than compacted in place, and the order is
|
|
* the class's rather than the instance's, so that a migrated instance is
|
|
* indistinguishable from one the constructor has just built. [dyn_equal]
|
|
* compares maps by lookup and would not have cared; [render] and [len] print
|
|
* and count in insertion order and would have. One malloc per instance per
|
|
* redefinition is the price, and a migration happens once.
|
|
*
|
|
* In three steps, and the order is what keeps it sound.
|
|
*
|
|
* First, everything that allocates on the collector's heap: the hook's
|
|
* arguments, and an empty string for a gained string slot. A collection may
|
|
* run here, while [o] still holds its old entries whole. Nothing in this
|
|
* step compares a key with [dyn_equal] — see [map_append] — so nothing in it
|
|
* can migrate another instance and run a hook inside this migration.
|
|
*
|
|
* Second, the name-matching, which allocates nothing on the collector's
|
|
* heap, calls nothing that can migrate, and finishes by stamping [o]
|
|
* current. [e] is read only up to here: a hook may build an instance of a
|
|
* class the registry has not seen, and adding it moves [classes].
|
|
*
|
|
* Third, the hook, on an instance that is already current, so a method that
|
|
* reads or writes it finds it migrated and does not start a second
|
|
* migration. A method may touch anything, including a map something above
|
|
* this frame is walking; the migration of [o] itself is finished before it
|
|
* runs. */
|
|
/* Kept out of line: inlined into [class_sync], its frame and saved
|
|
* registers were paid on every [get] and [put] of every map, current or not
|
|
* — measured at about a tenth of an untyped [put]'s instructions. */
|
|
__attribute__((noinline))
|
|
static void class_migrate(flan_obj *o, class_entry *e) {
|
|
flan_dyn *fresh = NULL;
|
|
int64_t i, j, n;
|
|
/* Rooted by address for as long as they may be needed: each is a
|
|
* collector object held nowhere else. */
|
|
flan_dyn inst, added, gone, empty;
|
|
int64_t roots_at = roots_n;
|
|
int hook, need_empty = 0;
|
|
n = e->nslots;
|
|
/* CLHS 4.3.6: the method runs on every instance a redefinition reaches,
|
|
* whether or not the slot names moved — a changed type is a change a
|
|
* method may want to convert for. */
|
|
hook = migrate_fn != NULL && flan_dyn_migrate_hook != NULL;
|
|
for (j = 0; j < n; j++)
|
|
if (e->types[j].kind == ST_TEXT && !e->types[j].opt
|
|
&& entry_of(o, e->slots[j]) < 0)
|
|
need_empty = 1;
|
|
inst = dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
added = gone = empty = dyn_make(BOX_NIL, 0);
|
|
if (hook || need_empty) root_add(&inst, NULL);
|
|
if (need_empty) {
|
|
empty = flan_dyn_from_bytes((const uint8_t *)"", 0);
|
|
root_add(&empty, NULL);
|
|
}
|
|
if (hook) {
|
|
added = flan_dyn_vec_new();
|
|
root_add(&added, NULL);
|
|
gone = flan_dyn_map_new();
|
|
root_add(&gone, NULL);
|
|
for (j = 0; j < n; j++)
|
|
if (entry_of(o, e->slots[j]) < 0)
|
|
flan_dyn_push(added,
|
|
dyn_make(BOX_KW, (uint64_t)(uintptr_t)e->slots[j]),
|
|
NULL, 0);
|
|
/* Every key the class no longer declares, a raw [put]'s included:
|
|
* CLHS's discarded slots and their property list, as one map. [o]'s
|
|
* keys are distinct, so these are, and they are appended as they are. */
|
|
for (i = 0; i < o->len; i++) {
|
|
flan_dyn key = o->u.v.items[i * 2];
|
|
int kept = 0;
|
|
if (flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD)
|
|
for (j = 0; j < n; j++)
|
|
if (dyn_kw(key) == e->slots[j]) { kept = 1; break; }
|
|
if (!kept) map_append(dyn_obj(gone), key, o->u.v.items[i * 2 + 1]);
|
|
}
|
|
}
|
|
if (n > 0) {
|
|
fresh = (flan_dyn *)malloc((size_t)n * 2 * sizeof *fresh);
|
|
if (fresh == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *fresh);
|
|
}
|
|
for (j = 0; j < n; j++) {
|
|
const slot_type *t = &e->types[j];
|
|
flan_dyn v;
|
|
i = entry_of(o, e->slots[j]);
|
|
if (i >= 0) {
|
|
v = o->u.v.items[i * 2 + 1];
|
|
/* A kept value that the slot's new type does not admit is kept
|
|
anyway: throwing it away would be the data loss a redefinition
|
|
exists to avoid, and there is nothing to convert it to. What it
|
|
gets is a warning, once per slot per redefinition, and the next
|
|
write to the slot is checked like any other. */
|
|
if (!slot_fits(t, v) && e->warned[j] != e->gen) {
|
|
char sv[SAY_MAX], st[128];
|
|
kw_entry *c = o->u.v.klass, *sl = e->slots[j];
|
|
e->warned[j] = e->gen;
|
|
say(sv, SAY_MAX, v);
|
|
slot_type_text(t, st, sizeof st);
|
|
fflush(stdout);
|
|
fprintf(stderr,
|
|
"warning: %.*s was redefined, and its slot :%.*s is now "
|
|
"declared %s. An instance holds %s there, which is %s; it "
|
|
"keeps that value, and the next write to :%.*s is checked\n",
|
|
(int)c->len, (const char *)(c + 1),
|
|
(int)sl->len, (const char *)(sl + 1), st, sv,
|
|
tag_of(v), (int)sl->len, (const char *)(sl + 1));
|
|
}
|
|
}
|
|
else if (t->opt) v = dyn_make(BOX_NIL, 0);
|
|
else
|
|
switch (t->kind) {
|
|
case ST_BOOL: v = flan_dyn_from_bool(0); break;
|
|
case ST_INT: v = flan_dyn_from_i64(0); break;
|
|
case ST_FLOAT: v = flan_dyn_from_f64(0.0); break;
|
|
case ST_TEXT: v = empty; break;
|
|
default: v = dyn_make(BOX_NIL, 0); break;
|
|
}
|
|
fresh[j * 2] = dyn_make(BOX_KW, (uint64_t)(uintptr_t)e->slots[j]);
|
|
fresh[j * 2 + 1] = v;
|
|
}
|
|
/* Charged the way [map_set]'s growth is, in both directions: a class that
|
|
* lost slots gives the bytes back, or the trigger drifts up by whatever
|
|
* every migration in the program ever released. */
|
|
gc_bytes += (n - o->u.v.cap) * 2 * (int64_t)sizeof(flan_dyn);
|
|
free(o->u.v.items);
|
|
o->u.v.items = fresh;
|
|
o->u.v.cap = n;
|
|
o->len = n;
|
|
o->gen = e->gen;
|
|
e = NULL;
|
|
if (hook) class_hook(o, inst, added, gone, n);
|
|
roots_n = roots_at;
|
|
}
|
|
|
|
/* Every read or write of an instance comes through here first: the class's
|
|
* entry, with [o] migrated to it if it was stale, or NULL for a map with no
|
|
* class. The common case — current — is a lookup and a compare, and the
|
|
* migration is a call of its own so that it stays out of the way. The entry
|
|
* is looked up again after one, because a hook may have moved the table. */
|
|
static class_entry *class_sync(flan_obj *o) {
|
|
class_entry *e;
|
|
if (o->kind != OBJ_MAP || o->u.v.klass == NULL) return NULL;
|
|
e = class_find(o->u.v.klass);
|
|
if (e == NULL || e->gen == o->gen) return e;
|
|
class_migrate(o, e);
|
|
return class_find(o->u.v.klass);
|
|
}
|
|
|
|
/* The same map with a shape tag on it: what a (defclass ...) constructor
|
|
* calls. [k] is a keyword and anything else traps by name — the compiler
|
|
* hands it the class's own name and nothing else can reach this.
|
|
*
|
|
* [spec] is the class's definition, [class_spec]'s string, and it registers
|
|
* the class the first time any instance of it is built. That is what makes
|
|
* a slot's type checked in a program that is never reloaded — the registry
|
|
* used to be filled only by a reload. A class already registered keeps
|
|
* what it has, and has to: a constructor compiled before a redefinition may
|
|
* still be on some stack, and letting its definition win would put the
|
|
* class back the way it was. Redefining is [flan_dyn_class_def]'s alone. */
|
|
/* A constructor call's site: [pending] from the caller, moved to [building]
|
|
* by the constructor's first act, so a call through a function value — which
|
|
* sets nothing — finds none rather than an earlier call's. Nothing between
|
|
* the caller setting it and the constructor taking it can construct: the
|
|
* caller evaluated every argument first. */
|
|
static const uint8_t *site_pending, *site_building;
|
|
static int64_t site_pending_len, site_building_len;
|
|
|
|
void flan_dyn_ctor_site(const uint8_t *loc, int64_t loclen) {
|
|
site_pending = loc;
|
|
site_pending_len = loclen;
|
|
}
|
|
|
|
flan_dyn flan_dyn_map_new_class(flan_dyn k, const uint8_t *spec, int64_t n) {
|
|
flan_obj *o;
|
|
site_building = site_pending;
|
|
site_building_len = site_pending_len;
|
|
site_pending = NULL;
|
|
site_pending_len = 0;
|
|
if (flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD)
|
|
trap1(NULL, 0, TYPE_TRAP, "class instance", "a class tag is a keyword", k);
|
|
if (class_find(dyn_kw(k)) == NULL) {
|
|
kw_entry **list;
|
|
slot_type *types;
|
|
int64_t count = class_spec(spec, n, &list, &types);
|
|
class_add(dyn_kw(k), list, types, count);
|
|
}
|
|
o = gc_alloc(OBJ_MAP, 0);
|
|
o->len = 0;
|
|
o->u.v.items = NULL;
|
|
o->u.v.cap = 0;
|
|
o->u.v.klass = dyn_kw(k);
|
|
/* Stamped at construction against whatever the registry currently says, so
|
|
* an instance built by the constructor this reload just installed is
|
|
* already current and never migrates. */
|
|
o->gen = class_gen(o->u.v.klass);
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
/* The shape tag, as a value: the class's name as a keyword, or nil. Never
|
|
* traps. Absence is an answer here for the reason it is one in [map_get] —
|
|
* asking what class a value is, is a question every value can be asked, and
|
|
* an ordinary map, a number and nil all truthfully answer "none". */
|
|
int64_t flan_dyn_obj_size(void) { return (int64_t)sizeof(flan_obj); }
|
|
|
|
flan_dyn flan_dyn_class_of(flan_dyn v) {
|
|
flan_obj *o;
|
|
if (flan_dyn_tag(v) != FLAN_DYN_TAG_MAP) return flan_dyn_nil();
|
|
o = dyn_obj(v);
|
|
if (o->u.v.klass == NULL) return flan_dyn_nil();
|
|
return dyn_make(BOX_KW, (uint64_t)(uintptr_t)o->u.v.klass);
|
|
}
|
|
|
|
/* The kind of a value as a keyword named by [tag_words], or a class instance's
|
|
* class name as [flan_dyn_class_of] answers it. A class may not be named like
|
|
* a kind (the parser refuses it), so :map always means a plain map. Keywords
|
|
* are immortal, so each kind's keyword is interned once and kept. When dyn
|
|
* gains a char, its tag gets a word in [tag_words] and :char falls out here
|
|
* with no change to this function. */
|
|
flan_dyn flan_dyn_type_of(flan_dyn v) {
|
|
static flan_dyn kinds[FLAN_DYN_TAG_MAP + 1];
|
|
static int interned;
|
|
int32_t t = flan_dyn_tag(v);
|
|
if (t == FLAN_DYN_TAG_MAP && dyn_obj(v)->u.v.klass != NULL)
|
|
return flan_dyn_class_of(v);
|
|
if (!interned) {
|
|
for (int i = 0; i <= FLAN_DYN_TAG_MAP; i++)
|
|
kinds[i] = flan_dyn_kw((const uint8_t *)tag_words[i],
|
|
(int64_t)strlen(tag_words[i]));
|
|
interned = 1;
|
|
}
|
|
return kinds[t];
|
|
}
|
|
|
|
/* ── Keywords ──────────────────────────────────────────────────────────
|
|
*
|
|
* One global table, append-only, never freed: a keyword is a *name*, the set
|
|
* of names a program uses is written in its source (plus whatever an edn file
|
|
* contributes), and a name is not something the collector should be asked to
|
|
* prove liveness of. Interning here rather than at each site is what makes
|
|
* two spellings of :a one word — the constructor scans for the bytes and
|
|
* answers the entry that already holds them, so keyword equality upstream is
|
|
* the identity compare and never touches the bytes again.
|
|
*
|
|
* The scan is linear. A structural hash would repay itself on a program with
|
|
* thousands of distinct keywords; a config file has dozens, every literal in
|
|
* compiled code could be hoisted to one construction the day it matters, and
|
|
* a table this simple has nothing in it to get wrong. */
|
|
|
|
static kw_entry **kws;
|
|
static int64_t kws_n, kws_cap;
|
|
|
|
flan_dyn flan_dyn_kw(const uint8_t *p, int64_t n) {
|
|
int64_t i;
|
|
kw_entry *k;
|
|
if (n < 0) n = 0;
|
|
for (i = 0; i < kws_n; i++) {
|
|
k = kws[i];
|
|
if (k->len == n && (n == 0 || memcmp(kw_bytes(k), p, (size_t)n) == 0))
|
|
return dyn_make(BOX_KW, (uint64_t)(uintptr_t)k);
|
|
}
|
|
if (kws_n == kws_cap) {
|
|
int64_t cap = kws_cap ? kws_cap * 2 : 32;
|
|
kw_entry **t = (kw_entry **)realloc(kws, (size_t)cap * sizeof *t);
|
|
if (t == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *t);
|
|
kws = t;
|
|
kws_cap = cap;
|
|
}
|
|
k = (kw_entry *)malloc(sizeof(kw_entry) + (size_t)n);
|
|
if (k == NULL) trap_oom(NULL, 0, (int64_t)sizeof(kw_entry) + n);
|
|
k->len = n;
|
|
if (n > 0) memcpy(kw_bytes(k), p, (size_t)n);
|
|
kws[kws_n++] = k;
|
|
return dyn_make(BOX_KW, (uint64_t)(uintptr_t)k);
|
|
}
|
|
|
|
/* ── Reading a value back ──────────────────────────────────────────────*/
|
|
|
|
static int64_t dyn_int_value(flan_dyn v) {
|
|
if (dyn_box(v) == BOX_INT) {
|
|
/* Sign-extend from 48 bits. The shift pair is the portable spelling; a
|
|
* bitfield would be one line shorter and implementation-defined. */
|
|
uint64_t p = dyn_payload(v);
|
|
return (int64_t)(p << 16) >> 16;
|
|
}
|
|
return dyn_obj(v)->u.i;
|
|
}
|
|
|
|
static double dyn_num_value(flan_dyn v) {
|
|
double d;
|
|
if (flan_dyn_tag(v) == FLAN_DYN_TAG_INT) return (double)dyn_int_value(v);
|
|
memcpy(&d, &v, sizeof d);
|
|
return d;
|
|
}
|
|
|
|
static inline int is_num(flan_dyn v) {
|
|
int32_t t = flan_dyn_tag(v);
|
|
return t == FLAN_DYN_TAG_INT || t == FLAN_DYN_TAG_FLOAT;
|
|
}
|
|
|
|
static inline int is_text(flan_dyn v) {
|
|
return flan_dyn_tag(v) == FLAN_DYN_TAG_TEXT;
|
|
}
|
|
|
|
static inline int is_vec(flan_dyn v) {
|
|
return flan_dyn_tag(v) == FLAN_DYN_TAG_VEC;
|
|
}
|
|
|
|
int64_t flan_dyn_need_i64(flan_dyn v) {
|
|
if (flan_dyn_tag(v) != FLAN_DYN_TAG_INT)
|
|
trap1(NULL, 0, TYPE_TRAP, "i64", "an int was wanted", v);
|
|
return dyn_int_value(v);
|
|
}
|
|
|
|
/* A float, and an int is not one. Refusing the widening is the decision, not
|
|
* an omission. The typed language does widen an integer into a float, but only
|
|
* where the float holds every value of it exactly — an i32 into an f64, never
|
|
* an i64 (TODO.org, "Implicit numeric widening is legal; narrowing stays a
|
|
* hard error"). This boundary has no such guarantee to offer:
|
|
* the box carries one integer width and it is i64, so "an int here" means the
|
|
* widest one, which is exactly the conversion the typed lattice refuses. The
|
|
* dyn
|
|
* *operators* promote, because arithmetic between a 2 and a 2.5 has an obvious
|
|
* answer and refusing it makes dynamic code worse; the boundary into a typed
|
|
* f64 parameter does not, because there the annotation is somebody's stated
|
|
* expectation and a mismatch is worth hearing about.
|
|
*
|
|
* Where this is sharper than the typed language is a *literal*. `(g 1)`
|
|
* against `(defn g [x f64] ...)` compiles, because the checker gives the
|
|
* literal the type the parameter asks for; a dyn value written as `1` has
|
|
* already been through flan_dyn_from_i64 and cannot remember that it was a
|
|
* literal. So the same source read as dyn traps where read as typed it does
|
|
* not. That is a real divergence, it is the compiler's to close — by tagging
|
|
* such a literal as a float where it can see the context — and it is written
|
|
* down in the doc's boundary section so that closing it is a decision somebody
|
|
* makes rather than a surprise somebody meets. Softening the check here is the
|
|
* option not to take: this function sees a tag and nothing else, so it could
|
|
* not tell (g 1) from (g (length xs)). */
|
|
double flan_dyn_need_f64(flan_dyn v) {
|
|
if (flan_dyn_tag(v) != FLAN_DYN_TAG_FLOAT)
|
|
trap1(NULL, 0, TYPE_TRAP, "f64", "a float was wanted", v);
|
|
return dyn_num_value(v);
|
|
}
|
|
|
|
uint8_t flan_dyn_need_bool(flan_dyn v) {
|
|
if (flan_dyn_tag(v) != FLAN_DYN_TAG_BOOL)
|
|
trap1(NULL, 0, TYPE_TRAP, "bool", "a bool was wanted", v);
|
|
return (uint8_t)(dyn_payload(v) ? 1 : 0);
|
|
}
|
|
|
|
/* ── A numeric cast opening a box ───────────────────────────────────────
|
|
*
|
|
* TODO.org, "A numeric cast opens a dyn box".
|
|
*
|
|
* [(f64 d)] on a dyn. The *conversion* is not done here: [check.ml] lowers
|
|
* such a cast to a branch on this function's answer, and each arm is the
|
|
* ordinary [flan_dyn_need_i64]/[flan_dyn_need_f64] followed by the cast the
|
|
* emitter already emits for a typed argument of that type. So this function
|
|
* decides one thing — which of the two numeric tags the box holds — and
|
|
* everything about the arithmetic (the fptosi range check, NaN, the
|
|
* narrowing rule) stays where it already was, identical in both backends and
|
|
* identical to the typed spelling of the same cast.
|
|
*
|
|
* Answers 1 for a float box, 0 for an int box. Every other tag traps, with
|
|
* the same [trap1] sentence the typed boundary's own refusals use — bool
|
|
* included, which mirrors [flan_dyn_need_i64] refusing a bool today rather
|
|
* than inventing a new rule for casts.
|
|
*
|
|
* [want_float] is what the *target* type is: 1 for f32/f64, 0 for the
|
|
* integer widths. When it disagrees with the box, the cast still happens —
|
|
* the author's call, "just coerce it with a warning" — and the warning below
|
|
* is the whole of what the disagreement costs. A cast is already a
|
|
* conversion operator, [(f64 5)] converts a typed integer, so converting
|
|
* across the box is the cast doing its job; the warning exists because the
|
|
* box's kind was not what the program apparently expected.
|
|
*
|
|
* Once per *site*, not per value. These casts sit in per-cell-per-frame
|
|
* loops — sand.flan runs at 120fps — and a per-occurrence line would be a
|
|
* flood rather than a diagnostic. The site is the [loc] text [check.ml]
|
|
* passes in, and the table below is keyed on its *bytes* rather than its
|
|
* address: the two backends emit their own constants for it and neither
|
|
* promises that two mentions of one site share one pointer.
|
|
*
|
|
* The table is fixed and small because it is only ever as large as the
|
|
* number of cross-kind cast sites a program has, which is a handful in the
|
|
* programs this was written for. A program with more than [SITE_MAX] of them
|
|
* stops deduplicating for the overflow — it still warns, every time, which
|
|
* is the noisy failure rather than the silent one.
|
|
*
|
|
* What the table stores is the loc's pointer, never a copy — the same licence
|
|
* the allocation registry takes for its type names (flan_dev.c:1073-1075).
|
|
* The bytes are a string constant in the image of whatever module emitted the
|
|
* cast, and a module that emitted a string constant is never unloaded: the
|
|
* cell table forbids dlclose outright (flan_dev.c:27), and the one path that
|
|
* does unload — an expression thunk — is gated on the module having emitted
|
|
* no string literal at all ([nstr] in lib/emit.ml:383-391). A cast site's loc
|
|
* IS a string literal, emitted through [string_const] like any other, so a
|
|
* module holding one of these sites is on the never-unloaded side of that
|
|
* test. If that ever stops being true the table has to copy.
|
|
*
|
|
* Not thread-safe, and that is a statement about who runs this rather than a
|
|
* shrug: [flan_dyn_cast_kind] runs on the program's own thread and Flan has
|
|
* no second one today. The dev agent's listener thread reads flan_dev.c's
|
|
* tables — which is why *those* carry a seqlock — and never touches this one.
|
|
* Were a second thread ever to reach a cast, the cost would not be a line
|
|
* printed twice: {ptr, len} is a pair only meaningful together, and a reader
|
|
* that took the new pointer with the old length would read off the end of a
|
|
* string literal, exactly the hazard flan_dev.c:1126-1129 describes. The fix
|
|
* then is the registry's, not a lock on a path that runs per cast in a frame
|
|
* loop. */
|
|
|
|
#define SITE_MAX 64
|
|
|
|
static struct { const uint8_t *ptr; int64_t len; } warned_sites[SITE_MAX];
|
|
static int warned_count;
|
|
|
|
static int site_first_time(const uint8_t *loc, int64_t loc_len) {
|
|
for (int i = 0; i < warned_count; i++)
|
|
if (warned_sites[i].len == loc_len &&
|
|
memcmp(warned_sites[i].ptr, loc, (size_t)loc_len) == 0)
|
|
return 0;
|
|
if (warned_count < SITE_MAX) {
|
|
warned_sites[warned_count].ptr = loc;
|
|
warned_sites[warned_count].len = loc_len;
|
|
warned_count++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
|
|
const uint8_t *target, int64_t target_len,
|
|
int32_t want_float) {
|
|
int32_t tag = flan_dyn_tag(v);
|
|
if (tag != FLAN_DYN_TAG_INT && tag != FLAN_DYN_TAG_FLOAT) {
|
|
/* [trap1] takes the operation as a C string and the target is a Flan
|
|
* slice, so it is copied out. Every cast name is two or three bytes; the
|
|
* clamp is for a caller this file cannot see. */
|
|
char name[8];
|
|
size_t n = (size_t)target_len < sizeof name - 1 ? (size_t)target_len
|
|
: sizeof name - 1;
|
|
memcpy(name, target, n);
|
|
name[n] = '\0';
|
|
/* This one has a site: the cast's own, which the emitter already hands
|
|
* over for the cross-kind warning below. It was the first entry point on
|
|
* this side to take a location and it was not passing it on. */
|
|
trap1(loc, loc_len, TYPE_TRAP, name, "a number was wanted", v);
|
|
}
|
|
int32_t is_float = tag == FLAN_DYN_TAG_FLOAT ? 1 : 0;
|
|
if (is_float != (want_float ? 1 : 0) && site_first_time(loc, loc_len)) {
|
|
fflush(stdout);
|
|
fprintf(stderr,
|
|
"%.*s: (%.*s x) found a dyn holding %s, and converted it to "
|
|
"%.*s — warned once for this site\n",
|
|
(int)loc_len, (const char *)loc, (int)target_len,
|
|
(const char *)target, is_float ? "a float" : "an int",
|
|
(int)target_len, (const char *)target);
|
|
}
|
|
return is_float;
|
|
}
|
|
|
|
/* nil <-> None at an (Option T) boundary. Cannot trap — every dyn value
|
|
* answers this one way or the other. */
|
|
int32_t flan_dyn_is_nil(flan_dyn v) {
|
|
return flan_dyn_tag(v) == FLAN_DYN_TAG_NIL ? 1 : 0;
|
|
}
|
|
|
|
/* (Some nil)'s run-time half: a dyn value the checker could not see was nil
|
|
* at compile time, reaching Some anyway. [op] is "some" rather than a Flan
|
|
* spelling of the call, matching how every other dyn trap here names the
|
|
* operation that refused. */
|
|
flan_dyn flan_dyn_need_not_nil(flan_dyn v) {
|
|
if (flan_dyn_tag(v) == FLAN_DYN_TAG_NIL)
|
|
trap1(NULL, 0, TYPE_TRAP, "some",
|
|
"Some cannot hold nil -- nil and None would become the same case "
|
|
"of an (Option dyn)", v);
|
|
return v;
|
|
}
|
|
|
|
/* Clojure's truthiness, not C's or Python's: nil and false are the only
|
|
* falsey values, and everything else — 0, 0.0, "", an empty vec, an empty
|
|
* map, any keyword — is truthy. Never traps; every tag answers. */
|
|
uint8_t flan_dyn_truthy(flan_dyn v) {
|
|
int32_t t = flan_dyn_tag(v);
|
|
if (t == FLAN_DYN_TAG_NIL) return 0;
|
|
if (t == FLAN_DYN_TAG_BOOL) return (uint8_t)(dyn_payload(v) ? 1 : 0);
|
|
return 1;
|
|
}
|
|
|
|
/* ── Arithmetic ────────────────────────────────────────────────────────
|
|
*
|
|
* Two ints answer an int; anything else numeric answers a float. The promotion
|
|
* is the one place dyn is more permissive than the typed language, and the
|
|
* case for it is that (+ 1 2.5) has exactly one sensible answer and a language
|
|
* that refuses it is not dynamic in any useful sense. A program that wants the
|
|
* refusal annotates, which is the whole bargain.
|
|
*
|
|
* Integer division and remainder by zero trap rather than answering. That
|
|
* matches typed Flan, which signals ArithError and dies with "divide by zero"
|
|
* if nothing handles it; the condition is not signalled here because a dyn
|
|
* operation has no [loc] to report and no transfer channel in its hands, which
|
|
* is the same reason the six traps in flan_rt.c park rather than signal. The
|
|
* float case is left to IEEE — 1.0/0.0 is inf and that is an answer, not a
|
|
* failure.
|
|
*
|
|
* The INT64_MIN / -1 pair overflows, and is the only pair that does. It gets
|
|
* its own sentence for the reason flan_rt.c's gives it one: somebody meeting
|
|
* it has probably never had to think about it. */
|
|
|
|
static void want_nums(const uint8_t *loc, int64_t loclen, const char *op,
|
|
const char *why, flan_dyn a, flan_dyn b) {
|
|
if (!is_num(a) || !is_num(b)) trap2(loc, loclen, TYPE_TRAP, op, why, a, b);
|
|
}
|
|
|
|
#define ARITH_NUM "it takes two numbers"
|
|
|
|
static flan_dyn arith(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn a, flan_dyn b) {
|
|
int64_t x, y;
|
|
want_nums(loc, loclen, op, ARITH_NUM, a, b);
|
|
if (flan_dyn_tag(a) == FLAN_DYN_TAG_INT &&
|
|
flan_dyn_tag(b) == FLAN_DYN_TAG_INT) {
|
|
x = dyn_int_value(a);
|
|
y = dyn_int_value(b);
|
|
switch (op[0]) {
|
|
case '+': return flan_dyn_from_i64((int64_t)((uint64_t)x + (uint64_t)y));
|
|
case '-': return flan_dyn_from_i64((int64_t)((uint64_t)x - (uint64_t)y));
|
|
case '*': return flan_dyn_from_i64((int64_t)((uint64_t)x * (uint64_t)y));
|
|
case '/':
|
|
if (y == 0) trap2(loc, loclen, ARITH_TRAP, op, "it does not divide by zero", a, b);
|
|
if (x == INT64_MIN && y == -1)
|
|
trap2(loc, loclen, ARITH_TRAP, op,
|
|
"the quotient is one past the largest i64, which is true of "
|
|
"this pair of operands and no other", a, b);
|
|
return flan_dyn_from_i64(x / y);
|
|
default:
|
|
if (y == 0) trap2(loc, loclen, ARITH_TRAP, op, "it does not divide by zero", a, b);
|
|
if (x == INT64_MIN && y == -1) return flan_dyn_from_i64(0);
|
|
return flan_dyn_from_i64(x % y);
|
|
}
|
|
}
|
|
{
|
|
double p = dyn_num_value(a), q = dyn_num_value(b);
|
|
switch (op[0]) {
|
|
case '+': return flan_dyn_from_f64(p + q);
|
|
case '-': return flan_dyn_from_f64(p - q);
|
|
case '*': return flan_dyn_from_f64(p * q);
|
|
case '/': return flan_dyn_from_f64(p / q);
|
|
default:
|
|
/* No fmod, which would drag math.h in for one operator. The identity is
|
|
* the definition of the remainder, and the trunc is what C's [%] does
|
|
* for integers, so the two operators agree about sign. */
|
|
if (q == 0.0) return flan_dyn_from_f64(p - p); /* nan, by 0/0 */
|
|
{
|
|
double t = p / q;
|
|
double k;
|
|
/* A quotient past 2^63 has no integer part this can name, and the
|
|
* cast would be undefined rather than merely wrong. Every such
|
|
* remainder is zero to the precision a double has left, so that is
|
|
* what it answers — which is also fmod's answer. */
|
|
if (!(t > -9.2233720368547758e18 && t < 9.2233720368547758e18))
|
|
return flan_dyn_from_f64(t == t ? 0.0 : t);
|
|
k = (t < 0) ? -(double)(int64_t)(-t) : (double)(int64_t)t;
|
|
return flan_dyn_from_f64(p - k * q);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return arith(loc, loclen, "+", a, b);
|
|
}
|
|
flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return arith(loc, loclen, "-", a, b);
|
|
}
|
|
/* (- x): an int wraps, as (- 0 x) does, and a float flips its sign, so the
|
|
* negation of 0.0 is -0.0 and not the 0.0 a subtraction from zero gives. */
|
|
flan_dyn flan_dyn_neg(flan_dyn a, const uint8_t *loc, int64_t loclen) {
|
|
if (!is_num(a)) trap1(loc, loclen, TYPE_TRAP, "-", "it takes a number", a);
|
|
if (flan_dyn_tag(a) == FLAN_DYN_TAG_INT)
|
|
return flan_dyn_from_i64((int64_t)(0 - (uint64_t)dyn_int_value(a)));
|
|
return flan_dyn_from_f64(-dyn_num_value(a));
|
|
}
|
|
flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return arith(loc, loclen, "*", a, b);
|
|
}
|
|
flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return arith(loc, loclen, "/", a, b);
|
|
}
|
|
flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return arith(loc, loclen, "%", a, b);
|
|
}
|
|
|
|
/* ── Ordering ──────────────────────────────────────────────────────────
|
|
*
|
|
* Numbers against numbers, text against text, and nothing else. Text orders
|
|
* bytewise, which is [memcmp] with the shorter one first on a tie — the same
|
|
* order a sort of a [(Vec str)] would want and the only order that needs no
|
|
* locale, no collation table and no argument.
|
|
*
|
|
* A number against a text traps rather than answering. The temptation is to
|
|
* order by tag so that every value is comparable and sorting never fails; the
|
|
* reason not to is that the resulting order is an artefact of this file's tag
|
|
* numbering, and a program that sorted a mixed vec would get a stable answer
|
|
* that means nothing. */
|
|
|
|
static int order(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn a, flan_dyn b) {
|
|
if (is_num(a) && is_num(b)) {
|
|
if (flan_dyn_tag(a) == FLAN_DYN_TAG_INT &&
|
|
flan_dyn_tag(b) == FLAN_DYN_TAG_INT) {
|
|
int64_t x = dyn_int_value(a), y = dyn_int_value(b);
|
|
return x < y ? -1 : (x > y ? 1 : 0);
|
|
}
|
|
{
|
|
double p = dyn_num_value(a), q = dyn_num_value(b);
|
|
/* NaN is unordered, and the honest answer is that it is neither less
|
|
* than nor greater than anything. Reported as "greater" would make a
|
|
* sort loop; reported as 2 lets each operator below answer false, which
|
|
* is what IEEE says every one of them answers. */
|
|
if (p != p || q != q) return 2;
|
|
return p < q ? -1 : (p > q ? 1 : 0);
|
|
}
|
|
}
|
|
if (is_text(a) && is_text(b)) {
|
|
flan_obj *x = dyn_obj(a), *y = dyn_obj(b);
|
|
int64_t n = x->len < y->len ? x->len : y->len;
|
|
int c = n > 0 ? memcmp(obj_text_bytes(x), obj_text_bytes(y), (size_t)n) : 0;
|
|
if (c != 0) return c < 0 ? -1 : 1;
|
|
return x->len < y->len ? -1 : (x->len > y->len ? 1 : 0);
|
|
}
|
|
trap2(loc, loclen, TYPE_TRAP, op,
|
|
"it compares two numbers or two texts, and these are neither", a, b);
|
|
}
|
|
|
|
flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return flan_dyn_from_bool(order(loc, loclen, "<", a, b) == -1);
|
|
}
|
|
flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
int c = order(loc, loclen, "<=", a, b);
|
|
return flan_dyn_from_bool(c == -1 || c == 0);
|
|
}
|
|
flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
return flan_dyn_from_bool(order(loc, loclen, ">", a, b) == 1);
|
|
}
|
|
flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
int c = order(loc, loclen, ">=", a, b);
|
|
return flan_dyn_from_bool(c == 1 || c == 0);
|
|
}
|
|
|
|
/* ── Equality ──────────────────────────────────────────────────────────
|
|
*
|
|
* Structural, and the only operation here that never traps: two values of
|
|
* unrelated tags are unequal, which is an answer, and making it an error would
|
|
* mean a dyn program could not ask "is this the string I expected" without
|
|
* first checking that it is a string at all.
|
|
*
|
|
* A number equals a number by value across the two tags — (= 1 1.0) is true —
|
|
* which is the same promotion the operators do and for the same reason. Text
|
|
* is bytewise and not by identity: two separately built texts with the same
|
|
* bytes are equal, and the doc's "string identity" section says why that is
|
|
* the only defensible choice when a text is immutable.
|
|
*
|
|
* A vec is equal element by element, with an identity shortcut first. The
|
|
* depth cap is the cycle stop: [set_at] lets a vec contain itself, and past
|
|
* the cap two vecs are equal only if they are the same vec, which terminates
|
|
* and answers correctly for the case that actually arises (a cycle compared
|
|
* against itself). Two *distinct* cyclic vecs with the same shape answer
|
|
* false, which is a wrong answer to a question nobody has asked yet; the
|
|
* honest fix is a visited set and it can be added the day somebody needs it. */
|
|
|
|
#define EQ_DEPTH 64
|
|
|
|
/* A container compared with itself is equal without reading an element, but
|
|
* in a dev build that shortcut would let a view inside it that has gone
|
|
* stale pass unremarked, where comparing any other container holding it
|
|
* traps. So a dev build walks the one container, as deep as equality would,
|
|
* and checks every view it holds — the same rule [eq_walk] applies at the
|
|
* top. A release build keeps no guards, and takes the shortcut. */
|
|
/* The containers one scan has already walked. A container reached twice —
|
|
* shared, or holding itself — is walked once, so a scan is linear in what it
|
|
* can reach rather than exponential, and a cycle ends. Open addressing over
|
|
* the object's address, each slot stamped with the scan that filled it. */
|
|
typedef struct { flan_obj *o; uint64_t stamp; } scan_slot;
|
|
static scan_slot *scan_seen;
|
|
static size_t scan_cap, scan_n;
|
|
/* The scan a slot was filled by. A slot from an earlier scan reads as empty,
|
|
* so starting a scan costs a counter bump rather than clearing a table that
|
|
* one large scan left large. */
|
|
static uint64_t scan_stamp;
|
|
|
|
static int scan_first_visit(flan_obj *o) {
|
|
size_t i, mask;
|
|
if (scan_n * 2 >= scan_cap) {
|
|
size_t ncap = scan_cap ? scan_cap * 2 : 64, j;
|
|
scan_slot *n = (scan_slot *)calloc(ncap, sizeof *n);
|
|
if (n == NULL) return 0; /* no room to remember: stop descending */
|
|
for (j = 0; j < scan_cap; j++) {
|
|
size_t k;
|
|
if (scan_seen[j].stamp != scan_stamp) continue;
|
|
for (k = ((uintptr_t)scan_seen[j].o >> 4) & (ncap - 1);
|
|
n[k].stamp == scan_stamp; k = (k + 1) & (ncap - 1)) {}
|
|
n[k] = scan_seen[j];
|
|
}
|
|
free(scan_seen);
|
|
scan_seen = n;
|
|
scan_cap = ncap;
|
|
}
|
|
mask = scan_cap - 1;
|
|
for (i = ((uintptr_t)o >> 4) & mask; scan_seen[i].stamp == scan_stamp;
|
|
i = (i + 1) & mask)
|
|
if (scan_seen[i].o == o) return 0;
|
|
scan_seen[i].o = o;
|
|
scan_seen[i].stamp = scan_stamp;
|
|
scan_n++;
|
|
return 1;
|
|
}
|
|
|
|
static void stale_walk(flan_dyn v, int depth) {
|
|
flan_obj *o;
|
|
int64_t i, n;
|
|
if (!dyn_boxed(v) || dyn_box(v) != BOX_OBJ || depth >= EQ_DEPTH) return;
|
|
o = dyn_obj(v);
|
|
if (o == NULL) return;
|
|
if (o->kind == OBJ_VIEW) {
|
|
view_guard_check(walk_loc, walk_len, walk_op, o);
|
|
return;
|
|
}
|
|
if (o->kind != OBJ_VEC && o->kind != OBJ_MAP) return;
|
|
if (!scan_first_visit(o)) return;
|
|
n = o->kind == OBJ_MAP ? o->len * 2 : o->len;
|
|
for (i = 0; i < n; i++) stale_walk(o->u.v.items[i], depth + 1);
|
|
}
|
|
|
|
/* Only when some guarded view has ever been made: until then there is
|
|
* nothing a scan could find, and a program that never crosses a typed value
|
|
* into dyn pays nothing for it. */
|
|
static int64_t views_guarded;
|
|
|
|
static void stale_scan(flan_dyn v, int depth) {
|
|
if (views_guarded == 0) return;
|
|
scan_stamp++; /* never 0, which is what a fresh slot holds */
|
|
scan_n = 0;
|
|
stale_walk(v, depth);
|
|
}
|
|
|
|
static int dyn_equal(flan_dyn a, flan_dyn b, int depth) {
|
|
int32_t ta = flan_dyn_tag(a), tb = flan_dyn_tag(b);
|
|
if (a == b && ta != FLAN_DYN_TAG_FLOAT) {
|
|
if (flan_dev_views_checked) stale_scan(a, depth);
|
|
return 1;
|
|
}
|
|
if (is_num(a) && is_num(b)) {
|
|
if (ta == FLAN_DYN_TAG_INT && tb == FLAN_DYN_TAG_INT)
|
|
return dyn_int_value(a) == dyn_int_value(b);
|
|
return dyn_num_value(a) == dyn_num_value(b);
|
|
}
|
|
if (ta != tb) return 0;
|
|
if (ta == FLAN_DYN_TAG_TEXT) {
|
|
flan_obj *x = dyn_obj(a), *y = dyn_obj(b);
|
|
if (x->len != y->len) return 0;
|
|
return x->len == 0 ||
|
|
memcmp(obj_text_bytes(x), obj_text_bytes(y), (size_t)x->len) == 0;
|
|
}
|
|
if (ta == FLAN_DYN_TAG_VEC) {
|
|
flan_obj *x = dyn_obj(a), *y = dyn_obj(b);
|
|
int64_t i, xn, yn;
|
|
if (x == y) {
|
|
if (flan_dev_views_checked) stale_scan(a, depth);
|
|
return 1;
|
|
}
|
|
if (depth >= EQ_DEPTH) return 0;
|
|
/* [x]/[y] may each be an ordinary heap vec or a view (M2 item 3) — the
|
|
tag does not say which, so [vecish_len]/[vecish_at] below read either
|
|
shape correctly. Reading raw through [x->u.v.items] the way this arm
|
|
used to is wrong for a view: nothing sets [len] for OBJ_VIEW, so it
|
|
reads back 0, and the elements alias [u.view.base] reinterpreted as
|
|
dyn words — two views with different contents would compare equal, a
|
|
view and an equal heap vec would compare unequal, and a map keyed by
|
|
any view would collide with every other view, silently, with nothing
|
|
to crash. */
|
|
xn = vecish_len(x);
|
|
yn = vecish_len(y);
|
|
if (xn != yn) return 0;
|
|
for (i = 0; i < xn; i++)
|
|
if (!dyn_equal(vecish_at(x, i), vecish_at(y, i), depth + 1)) return 0;
|
|
return 1;
|
|
}
|
|
/* Two maps are equal when they hold the same keys and each key answers an
|
|
* equal value — by lookup and never by position, because two maps built by
|
|
* inserting the same pairs in different orders are the same map. Sizes are
|
|
* compared first, so one lookup per entry of x is the whole walk: every key
|
|
* of x found in y at equal size means every key of y was found. Quadratic,
|
|
* like everything else about this map, and wrong to be clever about before
|
|
* the linear scan itself is. */
|
|
if (ta == FLAN_DYN_TAG_MAP) {
|
|
flan_obj *x = dyn_obj(a), *y = dyn_obj(b);
|
|
int64_t i, j;
|
|
if (x == y) {
|
|
if (flan_dev_views_checked) stale_scan(a, depth);
|
|
return 1;
|
|
}
|
|
if (depth >= EQ_DEPTH) return 0;
|
|
/* A struct's view is equal to another view of the same struct type with
|
|
equal fields, and to nothing else — the answer an instance gets beside
|
|
a plain map, for the same reason: the type's name is its shape tag. */
|
|
if (x->kind == OBJ_VIEW || y->kind == OBJ_VIEW) {
|
|
const char *xn, *yn;
|
|
int64_t xl, yl, n;
|
|
if (x->kind != OBJ_VIEW || y->kind != OBJ_VIEW) return 0;
|
|
view_struct_name(x, &xn, &xl);
|
|
view_struct_name(y, &yn, &yl);
|
|
if (xl != yl || memcmp(xn, yn, (size_t)xl) != 0) return 0;
|
|
n = view_nfields(x);
|
|
if (n != view_nfields(y)) return 0;
|
|
for (i = 0; i < n; i++)
|
|
if (!dyn_equal(view_field_val(x, i), view_field_val(y, i), depth + 1))
|
|
return 0;
|
|
return 1;
|
|
}
|
|
/* Two instances of one class built either side of a redefinition hold
|
|
different key sets, and comparing those key sets would answer "not
|
|
equal" about a difference the class no longer has. So both are brought
|
|
to the current definition first and the comparison is then the ordinary
|
|
one. The decision this records: equality is over the class as it is
|
|
now, and not over the shapes the two values were born with. */
|
|
class_sync(x);
|
|
class_sync(y);
|
|
/* The shape tag is part of the value. Two instances of one class compare
|
|
* by their entries as any two maps do; an instance and a plain map with
|
|
* the same entries do not, which is Clojure's answer for a record beside
|
|
* a map and is the only answer a tag can have if it means anything. An
|
|
* identity compare, because both sides are interned entries. */
|
|
if (x->u.v.klass != y->u.v.klass) return 0;
|
|
if (x->len != y->len) return 0;
|
|
for (i = 0; i < x->len; i++) {
|
|
flan_dyn k = x->u.v.items[i * 2];
|
|
int found = 0;
|
|
for (j = 0; j < y->len; j++) {
|
|
if (dyn_equal(k, y->u.v.items[j * 2], depth + 1)) {
|
|
if (!dyn_equal(x->u.v.items[i * 2 + 1], y->u.v.items[j * 2 + 1],
|
|
depth + 1))
|
|
return 0;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
/* nil, bool and keyword, whose whole content the identity test above
|
|
* already compared — a keyword's bytes were interned into exactly one
|
|
* entry, so two keywords are equal iff they are the same word. Reached only
|
|
* when that test said no. */
|
|
return 0;
|
|
}
|
|
|
|
static flan_dyn eq_walk(flan_dyn a, flan_dyn b) {
|
|
/* A view is checked before the identity shortcut, so a gone one traps
|
|
even compared with itself. */
|
|
if (dyn_boxed(a) && dyn_box(a) == BOX_OBJ && dyn_obj(a) != NULL
|
|
&& dyn_obj(a)->kind == OBJ_VIEW)
|
|
view_guard_check(walk_loc, walk_len, walk_op, dyn_obj(a));
|
|
if (dyn_boxed(b) && dyn_box(b) == BOX_OBJ && dyn_obj(b) != NULL
|
|
&& dyn_obj(b)->kind == OBJ_VIEW)
|
|
view_guard_check(walk_loc, walk_len, walk_op, dyn_obj(b));
|
|
return flan_dyn_from_bool((uint8_t)dyn_equal(a, b, 0));
|
|
}
|
|
|
|
/* ── Containers ────────────────────────────────────────────────────────*/
|
|
|
|
static inline int is_map(flan_dyn v) {
|
|
return flan_dyn_tag(v) == FLAN_DYN_TAG_MAP;
|
|
}
|
|
|
|
/* ── Typed containers as views ─────────────────────────────────────────
|
|
*
|
|
* Every entry point below already dispatches on [flan_dyn_tag]. A view over
|
|
* a Vec, a slice or a fixed array answers the vec tag and a view over a
|
|
* struct answers the map tag, so [flan_dyn_len], [flan_dyn_at],
|
|
* [flan_dyn_set_at], [flan_dyn_push], the map operations and the printers
|
|
* each add one branch for [OBJ_VIEW]. What follows is that branch's
|
|
* machinery.
|
|
*
|
|
* What one element is comes from a descriptor the compiler writes into
|
|
* read-only data (lib/check.ml, [view_desc]), a prefix code:
|
|
*
|
|
* b B h H i I l L i8 u8 i16 u16 i32 u32 i64 u64
|
|
* f d ? f32 f64 bool
|
|
* t str (read as a copy; never written from here)
|
|
* a<n>;T a fixed [n T]
|
|
* sT a slice [T]
|
|
* cT a [const T]: only in what a crossing *into* a
|
|
* written type wants ([flan_dyn_need_as]); no view
|
|
* is ever of one
|
|
* vT a (Vec T)
|
|
* {Name;f1;T1f2;T2} a struct, its fields in declaration order
|
|
*
|
|
* Offsets are computed here by C's rule, the one Emit.lay spells: every
|
|
* scalar aligned to its size, a struct to its strictest field and padded to
|
|
* it, an array adding no padding of its own. test/programs/dyn-view-any.flan
|
|
* reads back a struct whose fields sit at offsets only that rule gets right,
|
|
* on both backends.
|
|
*
|
|
* A view never stores a collector pointer into typed storage: a read boxes
|
|
* (or copies, for a str), and a write unboxes a number or a bool. That is
|
|
* why a str element is read-only from here, and why an aggregate element is
|
|
* written through its own view rather than replaced whole. */
|
|
|
|
static int64_t desc_int(const uint8_t **p) {
|
|
int64_t n = 0;
|
|
while (**p >= '0' && **p <= '9') { n = n * 10 + (**p - '0'); (*p)++; }
|
|
if (**p == ';') (*p)++;
|
|
return n;
|
|
}
|
|
|
|
/* Past a name and its ';'. */
|
|
static const uint8_t *desc_name_end(const uint8_t *d) {
|
|
while (*d != ';') d++;
|
|
return d + 1;
|
|
}
|
|
|
|
static const uint8_t *desc_skip(const uint8_t *d) {
|
|
switch (*d) {
|
|
case 'a': d++; desc_int(&d); return desc_skip(d);
|
|
case 's': case 'c': case 'v': return desc_skip(d + 1);
|
|
case '{':
|
|
d = desc_name_end(d + 1);
|
|
while (*d != '}') d = desc_skip(desc_name_end(d));
|
|
return d + 1;
|
|
default: return d + 1;
|
|
}
|
|
}
|
|
|
|
static int64_t align_to(int64_t n, int64_t a) { return (n + a - 1) / a * a; }
|
|
|
|
static void desc_lay(const uint8_t *d, int64_t *size, int64_t *align) {
|
|
switch (*d) {
|
|
case 'b': case 'B': case '?': *size = 1; *align = 1; return;
|
|
case 'h': case 'H': *size = 2; *align = 2; return;
|
|
case 'i': case 'I': case 'f': *size = 4; *align = 4; return;
|
|
case 't': case 's': case 'c': *size = 16; *align = 8; return;
|
|
case 'v': *size = 40; *align = 8; return;
|
|
case 'a': {
|
|
int64_t n, s, a;
|
|
d++;
|
|
n = desc_int(&d);
|
|
desc_lay(d, &s, &a);
|
|
*size = n * s;
|
|
*align = a;
|
|
return;
|
|
}
|
|
case '{': {
|
|
int64_t off = 0, al = 1, s, a;
|
|
d = desc_name_end(d + 1);
|
|
while (*d != '}') {
|
|
d = desc_name_end(d);
|
|
desc_lay(d, &s, &a);
|
|
if (a < 1) a = 1;
|
|
off = align_to(off, a) + s;
|
|
if (a > al) al = a;
|
|
d = desc_skip(d);
|
|
}
|
|
*size = align_to(off, al);
|
|
*align = al;
|
|
return;
|
|
}
|
|
default: *size = 8; *align = 8; return;
|
|
}
|
|
}
|
|
|
|
static inline int64_t desc_size(const uint8_t *d) {
|
|
int64_t s, a;
|
|
switch (*d) { /* the scalars, without the walk */
|
|
case 'b': case 'B': case '?': return 1;
|
|
case 'h': case 'H': return 2;
|
|
case 'i': case 'I': case 'f': return 4;
|
|
case 'l': case 'L': case 'd': return 8;
|
|
default: break;
|
|
}
|
|
desc_lay(d, &s, &a);
|
|
return s;
|
|
}
|
|
|
|
/* A struct descriptor's fields, one at a time: [desc_fields] answers where
|
|
* the first one starts, and each [desc_next] answers a field's name, offset
|
|
* and type and steps past it, 0 at the closing brace. [*off] is the running
|
|
* offset. */
|
|
static const uint8_t *desc_fields(const uint8_t *d, int64_t *off) {
|
|
*off = 0;
|
|
return desc_name_end(d + 1);
|
|
}
|
|
|
|
static int desc_next(const uint8_t **at, int64_t *off, const uint8_t **name,
|
|
int64_t *namelen, int64_t *foff, const uint8_t **fty) {
|
|
const uint8_t *d = *at;
|
|
int64_t s, a;
|
|
if (*d == '}') return 0;
|
|
*name = d;
|
|
while (*d != ';') d++;
|
|
*namelen = (int64_t)(d - *name);
|
|
d++;
|
|
desc_lay(d, &s, &a);
|
|
if (a < 1) a = 1;
|
|
*foff = align_to(*off, a);
|
|
*off = *foff + s;
|
|
*fty = d;
|
|
*at = desc_skip(d);
|
|
return 1;
|
|
}
|
|
|
|
/* The field keyword [k] names, or NULL. */
|
|
static const uint8_t *desc_field(const uint8_t *d, flan_dyn k, int64_t *foff) {
|
|
const uint8_t *at, *name, *fty;
|
|
int64_t off, namelen;
|
|
kw_entry *kw;
|
|
if (flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD) return NULL;
|
|
kw = dyn_kw(k);
|
|
at = desc_fields(d, &off);
|
|
while (desc_next(&at, &off, &name, &namelen, foff, &fty))
|
|
if (namelen == kw->len && memcmp(name, kw_bytes(kw), (size_t)namelen) == 0)
|
|
return fty;
|
|
return NULL;
|
|
}
|
|
|
|
static int64_t desc_nfields(const uint8_t *d) {
|
|
const uint8_t *at, *name, *fty;
|
|
int64_t off, namelen, foff, n = 0;
|
|
at = desc_fields(d, &off);
|
|
while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) n++;
|
|
return n;
|
|
}
|
|
|
|
/* The Flan spelling of a descriptor's type, for a sentence. */
|
|
static void desc_spell(const uint8_t *d, char *buf, size_t cap) {
|
|
static const char scalars[] = "bBhHiIlLfd?t";
|
|
static const char *const words[] = { "i8", "u8", "i16", "u16", "i32", "u32",
|
|
"i64", "u64", "f32", "f64", "bool",
|
|
"str" };
|
|
const char *w;
|
|
char inner[96];
|
|
if (cap == 0) return;
|
|
buf[0] = '\0';
|
|
if (*d != '\0' && (w = strchr(scalars, *d)) != NULL) {
|
|
snprintf(buf, cap, "%s", words[w - scalars]);
|
|
return;
|
|
}
|
|
switch (*d) {
|
|
case 'a': {
|
|
int64_t n;
|
|
d++;
|
|
n = desc_int(&d);
|
|
desc_spell(d, inner, sizeof inner);
|
|
snprintf(buf, cap, "[%lld %s]", (long long)n, inner);
|
|
return;
|
|
}
|
|
case 's':
|
|
desc_spell(d + 1, inner, sizeof inner);
|
|
snprintf(buf, cap, "[%s]", inner);
|
|
return;
|
|
case 'c':
|
|
desc_spell(d + 1, inner, sizeof inner);
|
|
snprintf(buf, cap, "[const %s]", inner);
|
|
return;
|
|
case 'v':
|
|
desc_spell(d + 1, inner, sizeof inner);
|
|
snprintf(buf, cap, "(Vec %s)", inner);
|
|
return;
|
|
case '{': {
|
|
const uint8_t *e = desc_name_end(d + 1);
|
|
snprintf(buf, cap, "%.*s", (int)(e - d - 2), (const char *)d + 1);
|
|
return;
|
|
}
|
|
default: snprintf(buf, cap, "?"); return;
|
|
}
|
|
}
|
|
|
|
/* What a dev build remembers of a view's storage at the crossing, and checks
|
|
* before every read or write through it (runtime/flan_dev.c keeps both
|
|
* tables):
|
|
*
|
|
* a frame the shadow frame the storage belongs to and the serial that
|
|
* activation was given. Live while that frame is still on the
|
|
* chain with the same serial: a later call landing at the same
|
|
* address gets a different one.
|
|
* a block the registry entry of the heap or arena block holding the
|
|
* storage, by base and note sequence. Live while the entry is,
|
|
* which a free, a free-all, an arena's destroy and a Vec's
|
|
* growth (for the block it left) all end.
|
|
*
|
|
* A frame is found by the compiler's word ([here]) or, for a stack address
|
|
* it could not tie to the calling frame, by address (flan_dev.c,
|
|
* [flan_dev_frame_owner]). A release build keeps neither table, so a view
|
|
* there records nothing and checks nothing. Storage neither table knows — a
|
|
* global, rodata, C memory — is not checked. */
|
|
/* [view_guard] is defined beside [flan_obj], since the sweep charges it. */
|
|
|
|
uint64_t flan_dev_frame_claim(void *frame, const char **name, int64_t *namelen);
|
|
int32_t flan_dev_frame_alive(const void *frame, uint64_t serial);
|
|
int32_t flan_dev_reg_claim(const void *p, uintptr_t *base, int64_t *seq,
|
|
const char **type, int64_t *typelen);
|
|
int32_t flan_dev_reg_alive(uintptr_t base, int64_t seq);
|
|
struct flan_frame;
|
|
extern struct flan_frame *flan_frame_head; /* runtime/flan_dev.c */
|
|
|
|
#define VIEW_FLAT 0 /* [base] is the first element, [o->len] the count */
|
|
#define VIEW_VEC 1 /* [base] is a Vec's header, read live */
|
|
#define VIEW_STRUCT 2 /* [base] is the struct, [desc] the struct's own */
|
|
|
|
/* A view carries a guard only when the dev registry is on: a release build
|
|
* would allocate and zero it for nothing, and a crossing is on the hot path.
|
|
* VIEW_GUARDED in [gen] says the guard is there. */
|
|
#define VIEW_GUARDED 0x100 /* bits 16-31 hold the element size */
|
|
/* Bits 9-10: an element kind [flan_dyn_at] boxes inline. */
|
|
#define VIEW_FAST_I64 1
|
|
#define VIEW_FAST_F64 2
|
|
#define VIEW_FAST_BOOL 3
|
|
static inline view_guard *view_g(flan_obj *o) {
|
|
return (o->gen & VIEW_GUARDED) ? (view_guard *)(o + 1) : NULL;
|
|
}
|
|
static inline int view_shape(flan_obj *o) { return (int)(o->gen & 0xff); }
|
|
|
|
void *flan_dev_frame_owner(const void *p);
|
|
|
|
/* What a dev build records of storage the compiler could not tie to the
|
|
* calling frame: the frame that owns it when it is on the stack (a slice of
|
|
* a local, a slice parameter over a caller's), else the registry block that
|
|
* holds it. Neither, and nothing is checked. */
|
|
static void guard_storage(view_guard *g, const void *p) {
|
|
void *f;
|
|
g->frame = NULL;
|
|
g->rbase = 0;
|
|
if (p == NULL) return;
|
|
if ((f = flan_dev_frame_owner(p)) != NULL) {
|
|
g->frame = f;
|
|
g->serial = flan_dev_frame_claim(f, &g->fname, &g->fnamelen);
|
|
return;
|
|
}
|
|
flan_dev_reg_claim(p, &g->rbase, &g->rseq, &g->rtype, &g->rtypelen);
|
|
}
|
|
|
|
|
|
/* A new view record, its guard empty. */
|
|
static inline __attribute__((always_inline)) flan_obj *
|
|
view_new(void *base, int64_t len, const uint8_t *desc,
|
|
int shape) {
|
|
int dev = flan_dev_views_checked;
|
|
flan_obj *o =
|
|
gc_alloc(OBJ_VIEW, dev ? (int64_t)sizeof(view_guard) : 0);
|
|
o->u.view.base = base;
|
|
o->u.view.desc = desc;
|
|
o->u.view.nul = NULL;
|
|
o->gen = (uint32_t)shape | (dev ? VIEW_GUARDED : 0);
|
|
/* A flat or Vec view's element size, kept so an access does not read the
|
|
descriptor again; 0 when it does not fit the 16 bits, and then it does. */
|
|
if (shape != VIEW_STRUCT) {
|
|
int64_t sz = desc_size(desc);
|
|
if (sz > 0 && sz < 0x10000) o->gen |= (uint32_t)sz << 16;
|
|
/* The three element kinds [flan_dyn_at] reads without a call. */
|
|
o->gen |= (uint32_t)(*desc == 'l' ? VIEW_FAST_I64
|
|
: *desc == 'd' ? VIEW_FAST_F64
|
|
: *desc == '?' ? VIEW_FAST_BOOL : 0) << 9;
|
|
}
|
|
o->len = len;
|
|
if (dev) {
|
|
memset(view_g(o), 0, sizeof(view_guard));
|
|
views_guarded++;
|
|
}
|
|
return o;
|
|
}
|
|
|
|
/* The stale-storage check. The sentence never renders the view: it has just
|
|
* been found to point at storage that is gone, and rendering reads it. */
|
|
static __attribute__((noinline)) void view_guard_slow(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o);
|
|
static inline __attribute__((always_inline)) void view_guard_check(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o) {
|
|
if (o->gen & VIEW_GUARDED) view_guard_slow(loc, loclen, op, o);
|
|
}
|
|
|
|
static __attribute__((noinline)) void view_guard_slow(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o) {
|
|
view_guard *g = view_g(o);
|
|
if (g->frame != NULL && !flan_dev_frame_alive(g->frame, g->serial)) {
|
|
flan_say(loc, loclen,
|
|
"dyn %s: this view points into a local of %.*s, and that call "
|
|
"has returned. A view of a local lasts as long as the call that "
|
|
"made it",
|
|
op, (int)g->fnamelen, g->fname);
|
|
dyn_trap((const uint8_t *)"DynStale", 8);
|
|
}
|
|
if (g->rbase != 0 && !flan_dev_reg_alive(g->rbase, g->rseq)) {
|
|
flan_say(loc, loclen,
|
|
"dyn %s: this view's storage, a block of %.*s, has been "
|
|
"released — freed, cleared by free-all, or left behind when a "
|
|
"Vec grew. Take the view again after the change",
|
|
op, (int)g->rtypelen, g->rtype);
|
|
dyn_trap((const uint8_t *)"DynStale", 8);
|
|
}
|
|
}
|
|
|
|
/* The stale-container check flan_rt.c's [flan_vec_check] runs for a typed
|
|
* Vec, restated for a view's own trap rather than reused: the duplicity
|
|
* doctrine's dyn side gets its own spelling (docs/SPIKE-DUPLICITY.md), and a
|
|
* dyn program that hits this wants the same park-and-inspect [flan_trap]
|
|
* gives every other dyn mistake, not the typed side's [rt_die]. A Vec with
|
|
* no allocator yet — one nobody has pushed to — has nothing to check. Like
|
|
* the guard's, the sentence never renders the view. */
|
|
static void view_vec_check(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn_vec_hdr *h) {
|
|
if (h->alloc) {
|
|
flan_dyn_alloc_hdr *a = (flan_dyn_alloc_hdr *)h->alloc;
|
|
if ((int64_t)a->epoch != h->epoch) {
|
|
flan_say(loc, loclen,
|
|
"dyn %s: this view's container's allocator was released — the "
|
|
"Vec was made at epoch %lld and the allocator is at %lld now",
|
|
op, (long long)h->epoch, (long long)(int64_t)a->epoch);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* [len] and [base], read live for a Vec view (so a push that grows and
|
|
* moves the underlying Vec is seen the very next operation) and read from
|
|
* the snapshot for a flat one. [view_len] checks the guard first. */
|
|
static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_obj *o) {
|
|
view_guard_check(loc, loclen, op, o);
|
|
if (view_shape(o) == VIEW_VEC) {
|
|
flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)o->u.view.base;
|
|
view_vec_check(loc, loclen, op, h);
|
|
return h->len;
|
|
}
|
|
return o->len;
|
|
}
|
|
|
|
static void *view_base(flan_obj *o) {
|
|
if (view_shape(o) == VIEW_VEC) return ((flan_dyn_vec_hdr *)o->u.view.base)->ptr;
|
|
return o->u.view.base;
|
|
}
|
|
|
|
/* A view of the aggregate element at [p], inside [parent]'s storage. An
|
|
* element of a Vec is checked against the Vec's block, since the Vec's
|
|
* growth is what would leave it behind; any other inline element shares its
|
|
* parent's guard. A slice element's data is somewhere else, so it is looked
|
|
* up afresh. */
|
|
static flan_dyn view_child(flan_obj *parent, const uint8_t *d, uint8_t *p) {
|
|
flan_obj *c;
|
|
switch (*d) {
|
|
case 's': {
|
|
void *data;
|
|
int64_t n;
|
|
memcpy(&data, p, 8);
|
|
memcpy(&n, p + 8, 8);
|
|
c = view_new(data, n, d + 1, VIEW_FLAT);
|
|
if (view_g(c) != NULL) guard_storage(view_g(c), data);
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)c);
|
|
}
|
|
case 'a': {
|
|
const uint8_t *e = d + 1;
|
|
int64_t n = desc_int(&e);
|
|
c = view_new(p, n, e, VIEW_FLAT);
|
|
break;
|
|
}
|
|
case 'v': c = view_new(p, 0, d + 1, VIEW_VEC); break;
|
|
default: c = view_new(p, 0, d, VIEW_STRUCT); break;
|
|
}
|
|
if (view_g(c) == NULL) {}
|
|
else if (view_shape(parent) == VIEW_VEC) guard_storage(view_g(c), p);
|
|
else if (view_g(parent) != NULL) *view_g(c) = *view_g(parent);
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)c);
|
|
}
|
|
|
|
/* One element, boxed on the way out. A number widens to a dyn int or float,
|
|
* except a u64 above the largest i64, which has no dyn int to become. */
|
|
static flan_dyn view_read(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_obj *parent, const uint8_t *d, uint8_t *p) {
|
|
switch (*d) {
|
|
case 'b': { int8_t x; memcpy(&x, p, 1); return flan_dyn_from_i64(x); }
|
|
case 'B': { uint8_t x; memcpy(&x, p, 1); return flan_dyn_from_i64(x); }
|
|
case 'h': { int16_t x; memcpy(&x, p, 2); return flan_dyn_from_i64(x); }
|
|
case 'H': { uint16_t x; memcpy(&x, p, 2); return flan_dyn_from_i64(x); }
|
|
case 'i': { int32_t x; memcpy(&x, p, 4); return flan_dyn_from_i64(x); }
|
|
case 'I': { uint32_t x; memcpy(&x, p, 4); return flan_dyn_from_i64(x); }
|
|
case 'l': { int64_t x; memcpy(&x, p, 8); return flan_dyn_from_i64(x); }
|
|
case 'L': {
|
|
uint64_t x;
|
|
memcpy(&x, p, 8);
|
|
if (x > (uint64_t)INT64_MAX) {
|
|
flan_say(loc, loclen,
|
|
"dyn %s: this u64 element is %llu, above the largest dyn int "
|
|
"(9223372036854775807), so it has no dyn value",
|
|
op, (unsigned long long)x);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
return flan_dyn_from_i64((int64_t)x);
|
|
}
|
|
case 'f': { float x; memcpy(&x, p, 4); return flan_dyn_from_f64((double)x); }
|
|
case 'd': { double x; memcpy(&x, p, 8); return flan_dyn_from_f64(x); }
|
|
case '?': return flan_dyn_from_bool(*p ? 1 : 0);
|
|
case 't': {
|
|
const uint8_t *s;
|
|
int64_t n;
|
|
memcpy(&s, p, 8);
|
|
memcpy(&n, p + 8, 8);
|
|
return flan_dyn_from_bytes(s, n);
|
|
}
|
|
default: return view_child(parent, d, p);
|
|
}
|
|
}
|
|
|
|
/* The range each integer element holds. */
|
|
static int int_range(uint8_t c, int64_t *lo, int64_t *hi) {
|
|
switch (c) {
|
|
case 'b': *lo = INT8_MIN; *hi = INT8_MAX; return 1;
|
|
case 'B': *lo = 0; *hi = UINT8_MAX; return 1;
|
|
case 'h': *lo = INT16_MIN; *hi = INT16_MAX; return 1;
|
|
case 'H': *lo = 0; *hi = UINT16_MAX; return 1;
|
|
case 'i': *lo = INT32_MIN; *hi = INT32_MAX; return 1;
|
|
case 'I': *lo = 0; *hi = UINT32_MAX; return 1;
|
|
case 'l': *lo = INT64_MIN; *hi = INT64_MAX; return 1;
|
|
case 'L': *lo = 0; *hi = INT64_MAX; return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
/* One element, unboxed on the way in. The dyn value's tag must be the one
|
|
* the element's type wants, or this traps by name and never coerces a
|
|
* mismatched value into the slot; an int the element's width cannot hold
|
|
* traps too, naming both. An int goes into a float element when the float
|
|
* holds it exactly, the rule a class's typed float slot follows, and traps
|
|
* when it does not. A float into an f32 narrows, as (f32 x) does. [v] is the
|
|
* view and [x] the value, for the sentence. */
|
|
/* "an" before a word said with a vowel sound: an i8, an f32, an Item; a
|
|
* u8, a bool, a [3 i32]. */
|
|
static const char *an(const char *w) {
|
|
if (w[0] == 'u' && w[1] >= '0' && w[1] <= '9') return "a";
|
|
if (w[0] != '\0' && strchr("aeiouAEIOU", w[0]) != NULL) return "an";
|
|
if (w[0] == 'f' && w[1] >= '0' && w[1] <= '9') return "an";
|
|
return "a";
|
|
}
|
|
|
|
/* A write into a struct field that the field refuses: the field by name, its
|
|
* type, what was wrong, and the call with the key in it. [why] finishes the
|
|
* sentence after the field's type. */
|
|
static _Noreturn void field_refuse(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_dyn v, flan_dyn key,
|
|
const uint8_t *d, flan_dyn x,
|
|
const char *trap, const char *why) {
|
|
char ty[128], sn[96], sv[SAY_MAX], sx[SAY_MAX];
|
|
const char *nm;
|
|
int64_t nl;
|
|
kw_entry *k = dyn_kw(key);
|
|
desc_spell(d, ty, sizeof ty);
|
|
view_struct_name(dyn_obj(v), &nm, &nl);
|
|
snprintf(sn, sizeof sn, "%.*s", (int)nl, nm);
|
|
say(sv, SAY_MAX, v);
|
|
say(sx, SAY_MAX, x);
|
|
said_len = 0;
|
|
said_add("dyn %s: field :%.*s of %s %s is %s %s%s — ", op, (int)k->len,
|
|
(const char *)kw_bytes(k), an(sn), sn, an(ty), ty, why);
|
|
if (strcmp(op, "set") == 0)
|
|
said_add("(set (get %s :%.*s) %s)", sv, (int)k->len,
|
|
(const char *)kw_bytes(k), sx);
|
|
else
|
|
said_add("(put %s :%.*s %s)", sv, (int)k->len, (const char *)kw_bytes(k),
|
|
sx);
|
|
flan_say(loc, loclen, "%s", said_buf);
|
|
dyn_trap((const uint8_t *)trap, (int64_t)strlen(trap));
|
|
}
|
|
|
|
/* ", and 1.5 is a float": what the refused value is, for a field's sentence. */
|
|
static void value_is(char *buf, size_t cap, flan_dyn x) {
|
|
char sx[SAY_MAX];
|
|
const char *t = tag_of(x);
|
|
if (flan_dyn_tag(x) == FLAN_DYN_TAG_NIL) {
|
|
snprintf(buf, cap, ", and the value is nil");
|
|
return;
|
|
}
|
|
say(sx, SAY_MAX, x);
|
|
snprintf(buf, cap, ", and %s is %s %s", sx, an(t), t);
|
|
}
|
|
|
|
/* One element or field, unboxed on the way in. The dyn value's tag must be
|
|
* the one the element's type wants, or this traps by name and never coerces a
|
|
* mismatched value into the slot; an int the element's width cannot hold
|
|
* traps too, naming both. An int goes into a float element when the float
|
|
* holds it exactly, the rule a class's typed float slot follows, and traps
|
|
* when it does not. A float into an f32 narrows, as (f32 x) does. [v] is the
|
|
* view and [x] the value, for the sentence; [key] is the field's keyword
|
|
* when [v] is a struct's view and nil for an element, and a field's refusal
|
|
* names the field. */
|
|
static void view_write(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn v, flan_dyn key, const uint8_t *d, flan_dyn x,
|
|
uint8_t *p) {
|
|
char ty[128], why[160];
|
|
int64_t lo, hi;
|
|
int field = flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD;
|
|
desc_spell(d, ty, sizeof ty);
|
|
if (int_range(*d, &lo, &hi)) {
|
|
int64_t n;
|
|
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT) {
|
|
if (field) {
|
|
value_is(why, sizeof why, x);
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynType", why);
|
|
}
|
|
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are int", v, x);
|
|
}
|
|
n = dyn_int_value(x);
|
|
if (n < lo || n > hi) {
|
|
if (field) {
|
|
if (*d == 'L')
|
|
snprintf(why, sizeof why,
|
|
", which holds no negative number, and %lld does not fit",
|
|
(long long)n);
|
|
else
|
|
snprintf(why, sizeof why,
|
|
", which holds %lld to %lld, and %lld does not fit",
|
|
(long long)lo, (long long)hi, (long long)n);
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynRange", why);
|
|
}
|
|
if (*d == 'L')
|
|
flan_say(loc, loclen,
|
|
"dyn %s: %lld does not fit a u64 element, which holds no "
|
|
"negative number", op, (long long)n);
|
|
else
|
|
flan_say(loc, loclen,
|
|
"dyn %s: %lld does not fit %s %s element, which holds %lld "
|
|
"to %lld", op, (long long)n, an(ty), ty, (long long)lo,
|
|
(long long)hi);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
switch (*d) {
|
|
case 'b': case 'B': { uint8_t b = (uint8_t)n; memcpy(p, &b, 1); return; }
|
|
case 'h': case 'H': { uint16_t h = (uint16_t)n; memcpy(p, &h, 2); return; }
|
|
case 'i': case 'I': { uint32_t w = (uint32_t)n; memcpy(p, &w, 4); return; }
|
|
default: memcpy(p, &n, 8); return;
|
|
}
|
|
}
|
|
switch (*d) {
|
|
case 'f': case 'd': {
|
|
double f;
|
|
if (flan_dyn_tag(x) == FLAN_DYN_TAG_INT) {
|
|
/* [slot_admit]'s rule for a class's float slot: exact is a round
|
|
trip, and the range test keeps the cast back defined. */
|
|
int64_t n = dyn_int_value(x);
|
|
f = *d == 'f' ? (double)(float)n : (double)n;
|
|
if (!(f >= -9223372036854775808.0 && f < 9223372036854775808.0)
|
|
|| (int64_t)f != n) {
|
|
if (field) {
|
|
snprintf(why, sizeof why,
|
|
", and %lld has no exact %s. Write it as a float, as in "
|
|
"%lld.0", (long long)n, ty, (long long)n);
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynRange", why);
|
|
}
|
|
flan_say(loc, loclen,
|
|
"dyn %s: %lld has no exact %s, so it does not go into this "
|
|
"element. Write it as a float, as in %lld.0",
|
|
op, (long long)n, ty, (long long)n);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
} else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT)
|
|
f = dyn_num_value(x);
|
|
else {
|
|
if (field) {
|
|
value_is(why, sizeof why, x);
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynType", why);
|
|
}
|
|
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are float", v, x);
|
|
}
|
|
if (*d == 'f') { float g = (float)f; memcpy(p, &g, 4); }
|
|
else memcpy(p, &f, 8);
|
|
return;
|
|
}
|
|
case '?':
|
|
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL) {
|
|
if (field) {
|
|
value_is(why, sizeof why, x);
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynType", why);
|
|
}
|
|
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are bool", v, x);
|
|
}
|
|
*p = dyn_payload(x) ? 1 : 0;
|
|
return;
|
|
case 't':
|
|
if (field)
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynType",
|
|
", which is read-only through a dyn view");
|
|
trap2(loc, loclen, TYPE_TRAP, op,
|
|
"a str element is read-only through a dyn view", v, x);
|
|
default: {
|
|
char sx[SAY_MAX];
|
|
if (field)
|
|
field_refuse(loc, loclen, op, v, key, d, x, "DynType",
|
|
", which a dyn view does not replace whole. Write into "
|
|
"its own elements or fields instead");
|
|
say(sx, SAY_MAX, x);
|
|
flan_say(loc, loclen,
|
|
"dyn %s: this element is %s %s, and a dyn view does not replace "
|
|
"it whole — write into its own elements or fields instead of "
|
|
"storing %s",
|
|
op, an(ty), ty, sx);
|
|
dyn_trap((const uint8_t *)"DynType", 7);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Element [i] of a vec-shaped view. The caller has checked the bounds. */
|
|
static inline uint8_t *view_elem_at(flan_obj *o, int64_t i) {
|
|
int64_t sz = (int64_t)(o->gen >> 16);
|
|
if (sz == 0) sz = desc_size(o->u.view.desc);
|
|
return (uint8_t *)view_base(o) + i * sz;
|
|
}
|
|
|
|
/* A length and an element reader that answer correctly whether [o] is an
|
|
* ordinary heap vec (OBJ_VEC, elements are dyn words) or a view over a
|
|
* typed container (OBJ_VIEW, native bytes boxed on the way out) — the pair
|
|
* [dyn_equal]'s VEC arm and the printers need. */
|
|
static int64_t vecish_len(flan_obj *o) {
|
|
return o->kind == OBJ_VIEW ? view_len(walk_loc, walk_len, walk_op, o) : o->len;
|
|
}
|
|
|
|
static flan_dyn vecish_at(flan_obj *o, int64_t i) {
|
|
if (o->kind == OBJ_VIEW)
|
|
return view_read(walk_loc, walk_len, walk_op, o, o->u.view.desc,
|
|
view_elem_at(o, i));
|
|
return o->u.v.items[i];
|
|
}
|
|
|
|
/* A struct view's field [k], or a trap naming the fields there are: a
|
|
* struct's shape is fixed, so a key it does not have is a mistake rather
|
|
* than an absence. */
|
|
static uint8_t *view_field(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_obj *o, flan_dyn k, const uint8_t **fty) {
|
|
int64_t off;
|
|
const uint8_t *d = o->u.view.desc;
|
|
view_guard_check(loc, loclen, op, o);
|
|
*fty = desc_field(d, k, &off);
|
|
if (*fty == NULL) {
|
|
char sk[SAY_MAX], nm[96];
|
|
const uint8_t *at, *name, *t;
|
|
int64_t o2, namelen, foff;
|
|
say(sk, SAY_MAX, k);
|
|
desc_spell(d, nm, sizeof nm);
|
|
said_len = 0;
|
|
said_add("dyn %s: a %s has no field %s. Its fields are", op, nm, sk);
|
|
at = desc_fields(d, &o2);
|
|
while (desc_next(&at, &o2, &name, &namelen, &foff, &t))
|
|
said_add(" :%.*s", (int)namelen, (const char *)name);
|
|
flan_say(loc, loclen, "%s", said_buf);
|
|
dyn_trap((const uint8_t *)"DynType", 7);
|
|
}
|
|
return (uint8_t *)o->u.view.base + off;
|
|
}
|
|
|
|
/* The crossing. [base] is the container's address (a Vec's header, the
|
|
* array's or the struct's first byte) or a slice's data, [len] a flat
|
|
* view's element count, [desc] the element's descriptor (the struct's own,
|
|
* for a struct view). [here] is the checker's word that the storage is the
|
|
* calling function's own frame; otherwise a dev build finds the frame that
|
|
* owns a stack address, or the registry block that holds a heap one. */
|
|
static inline __attribute__((always_inline)) flan_dyn
|
|
view_make(void *base, int64_t len, const uint8_t *desc,
|
|
int shape, int32_t here) {
|
|
flan_obj *o = view_new(base, len, desc, shape);
|
|
view_guard *g = view_g(o);
|
|
if (g == NULL) {}
|
|
else if (here) {
|
|
if (flan_frame_head != NULL) {
|
|
g->frame = flan_frame_head;
|
|
g->serial =
|
|
flan_dev_frame_claim(flan_frame_head, &g->fname, &g->fnamelen);
|
|
}
|
|
} else
|
|
guard_storage(g, base);
|
|
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
|
|
}
|
|
|
|
flan_dyn flan_dyn_view_slice(void *data, int64_t len, const uint8_t *desc,
|
|
int64_t desclen, int32_t here) {
|
|
(void)desclen;
|
|
return view_make(data, len, desc, VIEW_FLAT, here);
|
|
}
|
|
|
|
flan_dyn flan_dyn_view_at(void *addr, int64_t len, const uint8_t *desc,
|
|
int64_t desclen, int32_t shape, int32_t here) {
|
|
(void)desclen;
|
|
return view_make(addr, len, desc, shape, here);
|
|
}
|
|
|
|
/* A struct view's fields by position, for the printers and equality. */
|
|
static int64_t view_nfields(flan_obj *o) {
|
|
view_guard_check(walk_loc, walk_len, walk_op, o);
|
|
return desc_nfields(o->u.view.desc);
|
|
}
|
|
|
|
static int view_nth(flan_obj *o, int64_t i, const uint8_t **name,
|
|
int64_t *namelen, int64_t *foff, const uint8_t **fty) {
|
|
int64_t off;
|
|
const uint8_t *at = desc_fields(o->u.view.desc, &off);
|
|
while (desc_next(&at, &off, name, namelen, foff, fty))
|
|
if (i-- == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static flan_dyn view_field_key(flan_obj *o, int64_t i) {
|
|
const uint8_t *name, *fty;
|
|
int64_t namelen, foff;
|
|
if (!view_nth(o, i, &name, &namelen, &foff, &fty)) return flan_dyn_nil();
|
|
return flan_dyn_kw(name, namelen);
|
|
}
|
|
|
|
static flan_dyn view_field_val(flan_obj *o, int64_t i) {
|
|
const uint8_t *name, *fty;
|
|
int64_t namelen, foff;
|
|
if (!view_nth(o, i, &name, &namelen, &foff, &fty)) return flan_dyn_nil();
|
|
return view_read(walk_loc, walk_len, walk_op, o, fty,
|
|
(uint8_t *)o->u.view.base + foff);
|
|
}
|
|
|
|
static int view_big_u64(flan_obj *o, int64_t i, int field,
|
|
unsigned long long *out) {
|
|
const uint8_t *d, *p;
|
|
uint64_t x;
|
|
if (field) {
|
|
const uint8_t *name;
|
|
int64_t namelen, foff;
|
|
if (!view_nth(o, i, &name, &namelen, &foff, &d)) return 0;
|
|
p = (const uint8_t *)o->u.view.base + foff;
|
|
} else {
|
|
d = o->u.view.desc;
|
|
p = view_elem_at(o, i);
|
|
}
|
|
if (*d != 'L') return 0;
|
|
memcpy(&x, p, 8);
|
|
if (x <= (uint64_t)INT64_MAX) return 0;
|
|
*out = (unsigned long long)x;
|
|
return 1;
|
|
}
|
|
|
|
static void view_struct_name(flan_obj *o, const char **name, int64_t *len) {
|
|
const uint8_t *d = o->u.view.desc;
|
|
const uint8_t *e = desc_name_end(d + 1);
|
|
*name = (const char *)d + 1;
|
|
*len = (int64_t)(e - d - 2);
|
|
}
|
|
|
|
/* print, =, length and has-key? with the site they were written at, so a
|
|
* view that traps inside one — gone, or a u64 too wide to compare — says
|
|
* where, and names the operation. The compiler calls these; the site-less
|
|
* ones stay for test/dyn_ops.c and the runtime's own callers. */
|
|
static flan_dyn len_walk(flan_dyn v);
|
|
static flan_dyn contains_walk(flan_dyn m, flan_dyn k);
|
|
|
|
void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen) {
|
|
walk_site was = walk_enter(loc, loclen, "print");
|
|
print_walk(v);
|
|
walk_leave(was);
|
|
}
|
|
|
|
void flan_dyn_print(flan_dyn v) { flan_dyn_print_at(v, NULL, 0); }
|
|
|
|
flan_dyn flan_dyn_eq_at(flan_dyn a, flan_dyn b, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
walk_site was = walk_enter(loc, loclen, "=");
|
|
flan_dyn r = eq_walk(a, b);
|
|
walk_leave(was);
|
|
return r;
|
|
}
|
|
|
|
flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b) {
|
|
return flan_dyn_eq_at(a, b, NULL, 0);
|
|
}
|
|
|
|
flan_dyn flan_dyn_len_at(flan_dyn v, const uint8_t *loc, int64_t loclen) {
|
|
walk_site was = walk_enter(loc, loclen, "length");
|
|
flan_dyn r = len_walk(v);
|
|
walk_leave(was);
|
|
return r;
|
|
}
|
|
|
|
flan_dyn flan_dyn_len(flan_dyn v) { return flan_dyn_len_at(v, NULL, 0); }
|
|
|
|
flan_dyn flan_dyn_map_contains_at(flan_dyn m, flan_dyn k, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
walk_site was = walk_enter(loc, loclen, "has-key?");
|
|
flan_dyn r = contains_walk(m, k);
|
|
walk_leave(was);
|
|
return r;
|
|
}
|
|
|
|
flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) {
|
|
return flan_dyn_map_contains_at(m, k, NULL, 0);
|
|
}
|
|
|
|
/* The first ABI, kept for test/dyn_ops.c: FLAN_VIEW_I64/F64/BOOL. */
|
|
static const uint8_t *old_elem_desc(int32_t elem) {
|
|
return (const uint8_t *)(elem == FLAN_VIEW_I64 ? "l"
|
|
: elem == FLAN_VIEW_F64 ? "d" : "?");
|
|
}
|
|
|
|
flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem) {
|
|
return view_make(hdr, 0, old_elem_desc(elem), VIEW_VEC, 0);
|
|
}
|
|
|
|
flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem) {
|
|
return view_make(data, len, old_elem_desc(elem), VIEW_FLAT, 0);
|
|
}
|
|
|
|
/* ── Dyn into a written type ───────────────────────────────────────────
|
|
*
|
|
* The reverse of a view: a dyn value reaching typed code that wrote a str, a
|
|
* slice, a fixed array or a struct (lib/check.ml, [into_typed]). [want] is
|
|
* the written type's descriptor, the prefix code above, with [c] for a
|
|
* [const T]; [out] is where the typed value goes — a str's or slice's two
|
|
* words, or the array's or struct's bytes. Four answers, in order:
|
|
*
|
|
* a text its own bytes, for a str or a [const u8], pinned
|
|
* ([pin_text]) and never copied;
|
|
* a view of typed storage whose element type is the one wanted: that
|
|
* storage, checked for staleness in a dev build, never copied;
|
|
* a vec, map copied and unboxed element by element, each checked, for a
|
|
* [const T], a fixed array or a struct — a [const T]'s block
|
|
* in the temp arena ([flan_temp_block]);
|
|
* anything else traps, naming the element and what it is.
|
|
*
|
|
* A [T] that can be written through is never a copy. A write through a copy
|
|
* would not reach the dyn vec, and the same program would then answer
|
|
* differently with the vec typed or dyn; so a plain dyn vec, or a view of
|
|
* other elements, into a [T] traps and names [const T]. A fixed array and a
|
|
* struct are values, copied on the typed side as well, so a copy there
|
|
* changes nothing. */
|
|
|
|
void *flan_temp_block(int64_t bytes, int64_t align, int64_t elem,
|
|
const char *type, int64_t typelen);
|
|
|
|
typedef struct into_site {
|
|
const uint8_t *loc;
|
|
int64_t loclen;
|
|
char op[140]; /* "into [const i64]" */
|
|
char where[192]; /* "element 2", "field :x of element 2"; "" */
|
|
} into_site;
|
|
|
|
static _Noreturn void into_trap(into_site *s, const char *trap,
|
|
const char *fmt, ...) {
|
|
char msg[640];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(msg, sizeof msg, fmt, ap);
|
|
va_end(ap);
|
|
flan_say(s->loc, s->loclen, "dyn %s: %s", s->op, msg);
|
|
dyn_trap((const uint8_t *)trap, (int64_t)strlen(trap));
|
|
}
|
|
|
|
static const char *into_who(into_site *s) {
|
|
return s->where[0] ? s->where : "this";
|
|
}
|
|
|
|
/* "element 2 is a text, "a", and an i64 is wanted there". A view is checked
|
|
* for staleness before it is rendered, since rendering reads it. */
|
|
static _Noreturn void into_wrong(into_site *s, flan_dyn x, const char *why) {
|
|
char sx[SAY_MAX];
|
|
const char *t = tag_of(x);
|
|
if (dyn_boxed(x) && dyn_box(x) == BOX_OBJ && dyn_obj(x) != NULL
|
|
&& dyn_obj(x)->kind == OBJ_VIEW)
|
|
view_guard_check(s->loc, s->loclen, s->op, dyn_obj(x));
|
|
if (flan_dyn_tag(x) == FLAN_DYN_TAG_NIL)
|
|
into_trap(s, "DynType", "%s is nil, and %s", into_who(s), why);
|
|
say(sx, SAY_MAX, x);
|
|
into_trap(s, "DynType", "%s is %s %s, %s, and %s", into_who(s), an(t), t,
|
|
sx, why);
|
|
}
|
|
|
|
/* "an i64 is wanted there". */
|
|
static void into_wanted(char *buf, size_t cap, const uint8_t *d) {
|
|
char ty[128];
|
|
desc_spell(d, ty, sizeof ty);
|
|
snprintf(buf, cap, "%s %s is wanted there", an(ty), ty);
|
|
}
|
|
|
|
/* Whether a view's descriptor [a] starts with the whole of the code [b].
|
|
* The code is prefix-free, so a view's (inside a longer one) matches exactly
|
|
* when these bytes do. */
|
|
static int desc_same(const uint8_t *a, const uint8_t *b) {
|
|
size_t n = (size_t)(desc_skip(b) - b);
|
|
return memcmp(a, b, n) == 0;
|
|
}
|
|
|
|
/* A vec's length and element [i], a plain one's word or a view's element
|
|
* boxed. [o] is a vec-tagged object whose guard has been checked. */
|
|
static int64_t into_len(into_site *s, flan_obj *o) {
|
|
return o->kind == OBJ_VIEW ? view_len(s->loc, s->loclen, s->op, o) : o->len;
|
|
}
|
|
|
|
static flan_dyn into_at(into_site *s, flan_obj *o, int64_t i) {
|
|
if (o->kind == OBJ_VIEW)
|
|
return view_read(s->loc, s->loclen, s->op, o, o->u.view.desc,
|
|
view_elem_at(o, i));
|
|
return o->u.v.items[i];
|
|
}
|
|
|
|
/* A view object, or NULL; checked for staleness when it is one. */
|
|
static flan_obj *into_view(into_site *s, flan_dyn x) {
|
|
flan_obj *o;
|
|
if (!dyn_boxed(x) || dyn_box(x) != BOX_OBJ) return NULL;
|
|
o = dyn_obj(x);
|
|
if (o == NULL || o->kind != OBJ_VIEW) return NULL;
|
|
view_guard_check(s->loc, s->loclen, s->op, o);
|
|
return o;
|
|
}
|
|
|
|
/* Steps [s->where] into a part of what it names; [into_leave] steps back. */
|
|
static void into_enter(into_site *s, const char *fmt, ...) {
|
|
char part[96], rest[192];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(part, sizeof part, fmt, ap);
|
|
va_end(ap);
|
|
memcpy(rest, s->where, sizeof rest);
|
|
if (rest[0] == '\0') snprintf(s->where, sizeof s->where, "%s", part);
|
|
else snprintf(s->where, sizeof s->where, "%s of %s", part, rest);
|
|
}
|
|
|
|
static void into_leave(into_site *s, const char *saved) {
|
|
memcpy(s->where, saved, sizeof s->where);
|
|
}
|
|
|
|
static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p);
|
|
static void into_slice(into_site *s, const uint8_t *d, flan_dyn x,
|
|
uint8_t *p);
|
|
|
|
/* A text's bytes as a str's two words, pinned. */
|
|
static void into_text(flan_dyn x, uint8_t *p) {
|
|
flan_obj *o = dyn_obj(x);
|
|
const uint8_t *b = obj_text_bytes(o);
|
|
pin_text(o);
|
|
memcpy(p, &b, 8);
|
|
memcpy(p + 8, &o->len, 8);
|
|
}
|
|
|
|
/* [n] elements of [e] from the vec-tagged [o] into [p]. */
|
|
static void into_elems(into_site *s, const uint8_t *e, flan_obj *o, int64_t n,
|
|
uint8_t *p) {
|
|
int64_t i, sz = desc_size(e);
|
|
char saved[192];
|
|
memcpy(saved, s->where, sizeof saved);
|
|
for (i = 0; i < n; i++) {
|
|
flan_dyn x;
|
|
into_enter(s, "element %lld", (long long)i);
|
|
x = into_at(s, o, i);
|
|
into_put(s, e, x, p + i * sz);
|
|
into_leave(s, saved);
|
|
}
|
|
}
|
|
|
|
static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p) {
|
|
char why[256], ty[128];
|
|
int64_t lo, hi;
|
|
if (int_range(*d, &lo, &hi)) {
|
|
int64_t n;
|
|
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT) {
|
|
into_wanted(why, sizeof why, d);
|
|
into_wrong(s, x, why);
|
|
}
|
|
n = dyn_int_value(x);
|
|
if (n < lo || n > hi) {
|
|
desc_spell(d, ty, sizeof ty);
|
|
if (*d == 'L')
|
|
into_trap(s, "DynRange", "%s is %lld, and a u64 holds no negative "
|
|
"number", into_who(s), (long long)n);
|
|
into_trap(s, "DynRange", "%s is %lld, and %s %s holds %lld to %lld",
|
|
into_who(s), (long long)n, an(ty), ty, (long long)lo,
|
|
(long long)hi);
|
|
}
|
|
switch (*d) {
|
|
case 'b': case 'B': { uint8_t b = (uint8_t)n; memcpy(p, &b, 1); return; }
|
|
case 'h': case 'H': { uint16_t h = (uint16_t)n; memcpy(p, &h, 2); return; }
|
|
case 'i': case 'I': { uint32_t w = (uint32_t)n; memcpy(p, &w, 4); return; }
|
|
default: memcpy(p, &n, 8); return;
|
|
}
|
|
}
|
|
switch (*d) {
|
|
case 'f': case 'd': {
|
|
double f = 0;
|
|
/* An int goes into a float when the float holds it exactly: a view's
|
|
element write and a class's float slot take the same rule. */
|
|
if (flan_dyn_tag(x) == FLAN_DYN_TAG_INT) {
|
|
int64_t n = dyn_int_value(x);
|
|
f = *d == 'f' ? (double)(float)n : (double)n;
|
|
if (!(f >= -9223372036854775808.0 && f < 9223372036854775808.0)
|
|
|| (int64_t)f != n)
|
|
into_trap(s, "DynRange", "%s is %lld, which has no exact %s. Write "
|
|
"it as a float, as in %lld.0", into_who(s), (long long)n,
|
|
*d == 'f' ? "f32" : "f64", (long long)n);
|
|
} else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT)
|
|
f = dyn_num_value(x);
|
|
else {
|
|
into_wanted(why, sizeof why, d);
|
|
into_wrong(s, x, why);
|
|
}
|
|
if (*d == 'f') { float g = (float)f; memcpy(p, &g, 4); }
|
|
else memcpy(p, &f, 8);
|
|
return;
|
|
}
|
|
case '?':
|
|
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL) {
|
|
into_wanted(why, sizeof why, d);
|
|
into_wrong(s, x, why);
|
|
}
|
|
*p = dyn_payload(x) ? 1 : 0;
|
|
return;
|
|
case 't':
|
|
if (!is_text(x))
|
|
into_wrong(s, x, "a str is wanted there, which only a text becomes");
|
|
into_text(x, p);
|
|
return;
|
|
case 'a': {
|
|
const uint8_t *e = d + 1;
|
|
int64_t n = desc_int(&e), len;
|
|
flan_obj *o;
|
|
if (!is_vec(x)) {
|
|
desc_spell(d, ty, sizeof ty);
|
|
snprintf(why, sizeof why, "%s %s is made from a vec", an(ty), ty);
|
|
into_wrong(s, x, why);
|
|
}
|
|
o = into_view(s, x);
|
|
if (o == NULL) o = dyn_obj(x);
|
|
len = into_len(s, o);
|
|
if (len != n) {
|
|
desc_spell(d, ty, sizeof ty);
|
|
into_trap(s, "DynRange", "%s has %lld element%s, and %s %s holds "
|
|
"exactly %lld", s->where[0] ? s->where : "this vec",
|
|
(long long)len, len == 1 ? "" : "s", an(ty), ty,
|
|
(long long)n);
|
|
}
|
|
/* A view of the same elements is the same bytes: an array is a value,
|
|
copied on the typed side too. */
|
|
if (o->kind == OBJ_VIEW && desc_same(o->u.view.desc, e)) {
|
|
if (n > 0) memcpy(p, view_base(o), (size_t)(n * desc_size(e)));
|
|
return;
|
|
}
|
|
into_elems(s, e, o, n, p);
|
|
return;
|
|
}
|
|
case '{': {
|
|
const uint8_t *at, *name, *fty;
|
|
int64_t off, namelen, foff;
|
|
flan_obj *o;
|
|
char saved[192];
|
|
if (!is_map(x)) {
|
|
desc_spell(d, ty, sizeof ty);
|
|
snprintf(why, sizeof why, "%s %s is made from a map", an(ty), ty);
|
|
into_wrong(s, x, why);
|
|
}
|
|
o = into_view(s, x);
|
|
if (o != NULL && desc_same(o->u.view.desc, d)) {
|
|
memcpy(p, o->u.view.base, (size_t)desc_size(d));
|
|
return;
|
|
}
|
|
memcpy(saved, s->where, sizeof saved);
|
|
at = desc_fields(d, &off);
|
|
while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) {
|
|
flan_dyn k = flan_dyn_kw(name, namelen), v;
|
|
if (!flan_dyn_truthy(flan_dyn_map_contains_at(x, k, s->loc,
|
|
s->loclen))) {
|
|
desc_spell(d, ty, sizeof ty);
|
|
into_trap(s, "DynType", "%s has no :%.*s, and %s %s needs every "
|
|
"field", s->where[0] ? s->where : "this map",
|
|
(int)namelen, (const char *)name, an(ty), ty);
|
|
}
|
|
v = flan_dyn_get(x, k, s->loc, s->loclen);
|
|
into_enter(s, "field :%.*s", (int)namelen, (const char *)name);
|
|
into_put(s, fty, v, p + foff);
|
|
into_leave(s, saved);
|
|
}
|
|
return;
|
|
}
|
|
case 's': case 'c':
|
|
into_slice(s, d, x, p);
|
|
return;
|
|
default:
|
|
desc_spell(d, ty, sizeof ty);
|
|
snprintf(why, sizeof why, "%s %s is not made from a dyn value", an(ty), ty);
|
|
into_wrong(s, x, why);
|
|
}
|
|
}
|
|
|
|
/* The copy a [const T] reads, of a plain vec or a view of other elements. */
|
|
static void into_copy(into_site *s, const uint8_t *e, flan_obj *o,
|
|
uint8_t *out) {
|
|
static const char scalars[] = "bBhHiIlLfd?t";
|
|
static const char *const words[] = { "i8", "u8", "i16", "u16", "i32", "u32",
|
|
"i64", "u64", "f32", "f64", "bool",
|
|
"str" };
|
|
const char *w = *e != '\0' ? strchr(scalars, *e) : NULL;
|
|
/* The registry keeps the name by pointer, so it is static text. */
|
|
const char *type = w != NULL ? words[w - scalars] : "element";
|
|
int64_t n = into_len(s, o), size, align;
|
|
uint8_t *block = NULL;
|
|
desc_lay(e, &size, &align);
|
|
if (n > 0 && size > 0) {
|
|
block = (uint8_t *)flan_temp_block(n * size, align, size, type,
|
|
(int64_t)strlen(type));
|
|
if (block == NULL) trap_oom(s->loc, s->loclen, n * size);
|
|
memset(block, 0, (size_t)(n * size));
|
|
into_elems(s, e, o, n, block);
|
|
}
|
|
memcpy(out, &block, 8);
|
|
memcpy(out + 8, &n, 8);
|
|
}
|
|
|
|
/* A [T] or a [const T], at the top or inside an array or a struct. A text
|
|
* is a [const u8]'s bytes, a view of the very elements wanted is its own
|
|
* storage, and a [const T] of anything else vec-shaped is a copy; a [T]
|
|
* never is, since a write through it would not reach the vec. */
|
|
static void into_slice(into_site *s, const uint8_t *d, flan_dyn x,
|
|
uint8_t *p) {
|
|
const uint8_t *e = d + 1;
|
|
int mut = *d == 's';
|
|
const char *who = s->where[0] ? s->where : "this vec";
|
|
char ty[128], ety[128], why[256];
|
|
flan_obj *o;
|
|
desc_spell(d, ty, sizeof ty);
|
|
desc_spell(e, ety, sizeof ety);
|
|
if (is_text(x) && *e == 'B') {
|
|
if (mut)
|
|
into_wrong(s, x, "a text is read-only, so it becomes a str or a "
|
|
"[const u8] and never a [u8]");
|
|
into_text(x, p);
|
|
return;
|
|
}
|
|
if (!is_vec(x)) {
|
|
snprintf(why, sizeof why, "only a vec becomes %s %s", an(ty), ty);
|
|
into_wrong(s, x, why);
|
|
}
|
|
o = into_view(s, x);
|
|
if (o != NULL && desc_same(o->u.view.desc, e)) {
|
|
void *b = view_base(o);
|
|
int64_t n = view_len(s->loc, s->loclen, s->op, o);
|
|
memcpy(p, &b, 8);
|
|
memcpy(p + 8, &n, 8);
|
|
return;
|
|
}
|
|
if (mut) {
|
|
char have[128];
|
|
if (o != NULL) {
|
|
desc_spell(o->u.view.desc, have, sizeof have);
|
|
into_trap(s, "DynType", "%s is a view of %s elements, so %s %s of it "
|
|
"would be a copy, and a write through the copy would never "
|
|
"reach the vec. Take it as [const %s], which reads a copy",
|
|
who, have, an(ty), ty, ety);
|
|
}
|
|
into_trap(s, "DynType", "%s is a dyn vec, so %s %s of it would be a "
|
|
"copy, and a write through the copy would never reach the vec. "
|
|
"Take it as [const %s], which reads a copy", who, an(ty), ty,
|
|
ety);
|
|
}
|
|
into_copy(s, e, o != NULL ? o : dyn_obj(x), p);
|
|
}
|
|
|
|
void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen,
|
|
void *out, const uint8_t *loc, int64_t loclen) {
|
|
into_site s;
|
|
char ty[128];
|
|
uint8_t *p = (uint8_t *)out;
|
|
(void)wantlen;
|
|
s.loc = loc;
|
|
s.loclen = loclen;
|
|
s.where[0] = '\0';
|
|
desc_spell(want, ty, sizeof ty);
|
|
snprintf(s.op, sizeof s.op, "into %s", ty);
|
|
switch (*want) {
|
|
case 't':
|
|
if (!is_text(v)) into_wrong(&s, v, "only a text becomes a str");
|
|
into_text(v, p);
|
|
return;
|
|
default:
|
|
into_put(&s, want, v, p);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static flan_dyn len_walk(flan_dyn v) {
|
|
if (is_text(v)) return flan_dyn_from_i64(dyn_obj(v)->len);
|
|
/* A map's length is its slot count, so a stale instance would answer the
|
|
count of a definition that no longer exists. Migrated first for the same
|
|
reason [get] is. */
|
|
if (is_map(v)) {
|
|
flan_obj *o = dyn_obj(v);
|
|
if (o->kind == OBJ_VIEW) return flan_dyn_from_i64(view_nfields(o));
|
|
class_sync(o);
|
|
return flan_dyn_from_i64(o->len);
|
|
}
|
|
if (is_vec(v)) {
|
|
flan_obj *o = dyn_obj(v);
|
|
if (o->kind == OBJ_VIEW)
|
|
return flan_dyn_from_i64(view_len(walk_loc, walk_len, walk_op, o));
|
|
return flan_dyn_from_i64(o->len);
|
|
}
|
|
trap1(NULL, 0, TYPE_TRAP, "length", "only a text, a vec or a map has one", v);
|
|
}
|
|
|
|
/* The index has to be an int, and that is a separate sentence from the
|
|
* container being wrong: (at v "1") and (at 3 1) are two different mistakes
|
|
* and telling somebody "these are the wrong types" names neither. */
|
|
static int64_t need_index(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn v, flan_dyn i) {
|
|
if (flan_dyn_tag(i) != FLAN_DYN_TAG_INT)
|
|
trap2(loc, loclen, TYPE_TRAP, op, "an index must be an int", v, i);
|
|
return dyn_int_value(i);
|
|
}
|
|
|
|
/* A text answers a byte, as an int. That is what [(at s i)] on a
|
|
* [(Slice u8)] does in the typed language, and a text is a run of bytes in
|
|
* both. Codepoints are utf8's job and stay there. */
|
|
flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
int64_t k;
|
|
flan_obj *o;
|
|
/* m[:k] on a map is (get m :k), whatever the key: get's rule, nil when
|
|
* absent. */
|
|
if (is_map(v)) return flan_dyn_get(v, i, loc, loclen);
|
|
if (!is_text(v) && !is_vec(v))
|
|
trap2(loc, loclen, TYPE_TRAP, "at", "only a text, a vec or a map is indexed",
|
|
v, i);
|
|
k = need_index(loc, loclen, "at", v, i);
|
|
o = dyn_obj(v);
|
|
if (o->kind == OBJ_VIEW) {
|
|
int64_t len = view_len(loc, loclen, "at", o);
|
|
if (k < 0 || k >= len) trap_range(loc, loclen, "at", v, k, len);
|
|
{
|
|
uint8_t *p = view_elem_at(o, k);
|
|
switch ((o->gen >> 9) & 3) {
|
|
case VIEW_FAST_I64: { int64_t x; memcpy(&x, p, 8); return flan_dyn_from_i64(x); }
|
|
case VIEW_FAST_F64: { double x; memcpy(&x, p, 8); return flan_dyn_from_f64(x); }
|
|
case VIEW_FAST_BOOL: return flan_dyn_from_bool(*p ? 1 : 0);
|
|
default: return view_read(loc, loclen, "at", o, o->u.view.desc, p);
|
|
}
|
|
}
|
|
}
|
|
if (k < 0 || k >= o->len) trap_range(loc, loclen, "at", v, k, o->len);
|
|
if (o->kind == OBJ_TEXT) return flan_dyn_from_i64(obj_text_bytes(o)[k]);
|
|
return o->u.v.items[k];
|
|
}
|
|
|
|
/* (slice s lo) and (slice s lo hi) over a text; nil for [hi] is the length.
|
|
* The typed slice of a string is a view, and this is a copy: a text is
|
|
* immutable, so no program can tell the two apart. A vec's slice would have
|
|
* to share its elements with the vec to mean what the typed one means, which
|
|
* a copy does not, so a vec traps by type rather than answering differently. */
|
|
flan_dyn flan_dyn_slice(flan_dyn v, flan_dyn lo, flan_dyn hi,
|
|
const uint8_t *loc, int64_t loclen) {
|
|
int64_t a, b, len;
|
|
flan_obj *o;
|
|
if (!is_text(v))
|
|
trap2(loc, loclen, TYPE_TRAP, "slice", "only a text is sliced", v, lo);
|
|
o = dyn_obj(v);
|
|
len = o->len;
|
|
a = need_index(loc, loclen, "slice", v, lo);
|
|
b = flan_dyn_tag(hi) == FLAN_DYN_TAG_NIL
|
|
? len : need_index(loc, loclen, "slice", v, hi);
|
|
if (a < 0 || b < a || b > len) {
|
|
char sv[SAY_MAX];
|
|
say(sv, SAY_MAX, v);
|
|
flan_say(loc, loclen,
|
|
"dyn slice: [%lld %lld) is out of bounds for text of length %lld "
|
|
"— %s", (long long)a, (long long)b, (long long)len, sv);
|
|
dyn_trap((const uint8_t *)"DynRange", 8);
|
|
}
|
|
return flan_dyn_from_bytes(obj_text_bytes(o) + a, b - a);
|
|
}
|
|
|
|
void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
int64_t k;
|
|
flan_obj *o;
|
|
if (is_text(v))
|
|
trap2(loc, loclen, TYPE_TRAP, "set-at",
|
|
"a text is immutable — build another one", v, i);
|
|
/* Assigning m[:k] is (put m :k x), a class slot's type check with it. */
|
|
if (is_map(v)) {
|
|
flan_dyn_map_put(v, i, x, loc, loclen);
|
|
return;
|
|
}
|
|
if (!is_vec(v))
|
|
trap2(loc, loclen, TYPE_TRAP, "set-at",
|
|
"only a vec or a map is assigned into", v, i);
|
|
k = need_index(loc, loclen, "set-at", v, i);
|
|
o = dyn_obj(v);
|
|
if (o->kind == OBJ_VIEW) {
|
|
int64_t len = view_len(loc, loclen, "set-at", o);
|
|
if (k < 0 || k >= len) trap_range(loc, loclen, "set-at", v, k, len);
|
|
view_write(loc, loclen, "set-at", v, flan_dyn_nil(), o->u.view.desc, x,
|
|
view_elem_at(o, k));
|
|
return;
|
|
}
|
|
if (k < 0 || k >= o->len) trap_range(loc, loclen, "set-at", v, k, o->len);
|
|
o->u.v.items[k] = x;
|
|
}
|
|
|
|
void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen) {
|
|
flan_obj *o;
|
|
if (!is_vec(v)) {
|
|
/* The value is in the sentence rather than the vec, because the vec is the
|
|
* thing that is wrong and the value is what says which push it was. */
|
|
trap2(loc, loclen, TYPE_TRAP, "push", "only a vec is pushed to", v, x);
|
|
}
|
|
o = dyn_obj(v);
|
|
if (o->kind == OBJ_VIEW) {
|
|
uint8_t buf[16];
|
|
/* The typed Vec's own traps print a site too, and without one from the
|
|
* caller the best this can name is the operation. */
|
|
static const uint8_t push_loc[] = "(dyn push)";
|
|
const uint8_t *site = loc != NULL && loclen > 0 ? loc : push_loc;
|
|
int64_t sitelen = loc != NULL && loclen > 0 ? loclen
|
|
: (int64_t)sizeof(push_loc) - 1;
|
|
int64_t size, align;
|
|
if (view_shape(o) != VIEW_VEC)
|
|
trap2(loc, loclen, TYPE_TRAP, "push",
|
|
"this view is a slice or an array and cannot grow", v, x);
|
|
view_len(loc, loclen, "push", o);
|
|
desc_lay(o->u.view.desc, &size, &align);
|
|
/* Only a number or a bool is ever written, and [view_write] refuses the
|
|
rest before a byte of [buf] is used. */
|
|
memset(buf, 0, sizeof buf);
|
|
view_write(loc, loclen, "push", v, flan_dyn_nil(), o->u.view.desc, x, buf);
|
|
if (!flan_vec_push(o->u.view.base, buf, size, align, site, sitelen))
|
|
trap_oom(loc, loclen, size);
|
|
return;
|
|
}
|
|
if (o->len == o->u.v.cap) {
|
|
int64_t cap = o->u.v.cap ? o->u.v.cap * 2 : 8;
|
|
flan_dyn *items =
|
|
(flan_dyn *)realloc(o->u.v.items, (size_t)cap * sizeof *items);
|
|
if (items == NULL) trap_oom(loc, loclen, cap * (int64_t)sizeof *items);
|
|
/* The growth is charged to the heap so the trigger sees it, and it is
|
|
* charged *here* rather than at the next collection because a vec that
|
|
* doubles a dozen times between allocations would otherwise be invisible
|
|
* to the trigger until it was already large. */
|
|
gc_bytes += (cap - o->u.v.cap) * (int64_t)sizeof *items;
|
|
o->u.v.items = items;
|
|
o->u.v.cap = cap;
|
|
}
|
|
o->u.v.items[o->len++] = x;
|
|
}
|
|
|
|
/* ── Maps ──────────────────────────────────────────────────────────────
|
|
*
|
|
* Association pairs in one block, scanned linearly with the structural
|
|
* equality above. Not a hash table, and that is a decision rather than a
|
|
* shortcut deferred: hashing dyn values structurally means a hash function
|
|
* over every tag kept in step with [dyn_equal] forever — the exact same-side
|
|
* duplication the duplicity audit warns the typed side's printers into — and
|
|
* the maps this exists for are documents read from files, tens of entries.
|
|
* "The answer to 'I need more performance' will never be a faster GC"; nor
|
|
* will it be a faster dyn map. Type the program.
|
|
*
|
|
* A key occurs once: [set] replaces the value of an equal key in place, which
|
|
* is what makes a map keyed by anything — including another map — a set with
|
|
* dedup for free. Insertion order is preserved and is the print order.
|
|
*
|
|
* Absence answers nil rather than trapping. A key that is not in a map is an
|
|
* answer to a question the caller was allowed to ask — the same line [eq]
|
|
* takes about unrelated tags — and nil is Clojure's answer, which is the rule
|
|
* on the dyn side: TODO.org, "The dynamic paths mimic Clojure, the static
|
|
* paths mimic Odin". [contains] is the question to ask when nil might also be
|
|
* *stored*, and both are here so neither has to be guessed from the other. */
|
|
|
|
static int64_t map_find(flan_obj *o, flan_dyn k) {
|
|
int64_t i;
|
|
for (i = 0; i < o->len; i++)
|
|
if (dyn_equal(o->u.v.items[i * 2], k, 0)) return i;
|
|
return -1;
|
|
}
|
|
|
|
static flan_obj *want_map(const uint8_t *loc, int64_t loclen, const char *op,
|
|
flan_dyn m, flan_dyn k) {
|
|
flan_obj *o;
|
|
if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, op, "only a map answers it", m, k);
|
|
o = dyn_obj(m);
|
|
/* The lazy half of the redefinition protocol: [get], [put] and [has-key?]
|
|
all arrive here, and CLHS 4.3.6 asks for the update to happen no later
|
|
than the next read or write of a slot. A plain map returns from the
|
|
first line of [class_sync] untouched. */
|
|
class_sync(o);
|
|
return o;
|
|
}
|
|
|
|
flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) {
|
|
flan_obj *o = want_map(NULL, 0, "get", m, k);
|
|
if (o->kind == OBJ_VIEW) {
|
|
const uint8_t *fty;
|
|
uint8_t *p = view_field(NULL, 0, "get", o, k, &fty);
|
|
return view_read(NULL, 0, "get", o, fty, p);
|
|
}
|
|
int64_t i = map_find(o, k);
|
|
return i < 0 ? flan_dyn_nil() : o->u.v.items[i * 2 + 1];
|
|
}
|
|
|
|
/* A program's (get m k) and (.k m): the same, with the site a value that is
|
|
* not a map is refused at, and a key an instance's class does not declare
|
|
* refused rather than answered nil — [trap_no_slot]. */
|
|
static class_entry *class_sync(flan_obj *o);
|
|
static int64_t class_slot(class_entry *e, flan_dyn k);
|
|
static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o,
|
|
class_entry *e, flan_dyn k);
|
|
flan_dyn flan_dyn_get(flan_dyn m, flan_dyn k, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
class_entry *e;
|
|
if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "get", "only a map answers it", m, k);
|
|
/* A struct's view: its field, or a trap at this site naming the fields. */
|
|
if (dyn_obj(m)->kind == OBJ_VIEW) {
|
|
const uint8_t *fty;
|
|
uint8_t *p = view_field(loc, loclen, "get", dyn_obj(m), k, &fty);
|
|
return view_read(loc, loclen, "get", dyn_obj(m), fty, p);
|
|
}
|
|
e = class_sync(dyn_obj(m));
|
|
if (e != NULL && class_slot(e, k) < 0)
|
|
trap_no_slot(loc, loclen, "get", dyn_obj(m), e, k);
|
|
return flan_dyn_map_get(m, k);
|
|
}
|
|
|
|
static flan_dyn contains_walk(flan_dyn m, flan_dyn k) {
|
|
flan_obj *o = want_map(walk_loc, walk_len, walk_op, m, k);
|
|
if (o->kind == OBJ_VIEW) {
|
|
int64_t off;
|
|
view_guard_check(walk_loc, walk_len, walk_op, o);
|
|
return flan_dyn_from_bool(desc_field(o->u.view.desc, k, &off) != NULL);
|
|
}
|
|
return flan_dyn_from_bool(map_find(o, k) >= 0);
|
|
}
|
|
|
|
/* A class slot's type, checked at the store — SBCL's place for it
|
|
* (src/pcl/slots.lisp, [set-slot-value]'s typecheck before the write),
|
|
* because the store is where the wrong value is. -1 when [k] is not a slot
|
|
* the class declares. */
|
|
static int64_t class_slot(class_entry *e, flan_dyn k) {
|
|
int64_t j;
|
|
if (e == NULL || flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD) return -1;
|
|
for (j = 0; j < e->nslots; j++)
|
|
if (e->slots[j] == dyn_kw(k)) return j;
|
|
return -1;
|
|
}
|
|
|
|
/* The three stores that reach a declared slot, for the sentence a refusal
|
|
* prints: the call as it would have been written. */
|
|
enum { BY_PUT, BY_SET, BY_NEW };
|
|
|
|
static _Noreturn void trap_slot_type(const uint8_t *loc, int64_t loclen,
|
|
int by, flan_obj *o, class_entry *e,
|
|
int64_t j, flan_dyn m, flan_dyn v) {
|
|
char sm[SAY_MAX], sv[SAY_MAX], st[128];
|
|
const slot_type *t = &e->types[j];
|
|
kw_entry *sl = e->slots[j], *c = o->u.v.klass;
|
|
int sn = (int)sl->len, cn = (int)c->len;
|
|
const char *ss = (const char *)(sl + 1), *cs = (const char *)(c + 1);
|
|
say(sm, SAY_MAX, m);
|
|
say(sv, SAY_MAX, v);
|
|
slot_type_text(t, st, sizeof st);
|
|
said_len = 0;
|
|
said_add("dyn %s: the slot :%.*s of %.*s is declared %s, and ",
|
|
by == BY_PUT ? "put" : by == BY_SET ? "set" : "construct", sn, ss,
|
|
cn, cs, st);
|
|
/* A number of the right kind that does not fit is not news about its tag. */
|
|
if ((t->kind == ST_INT && flan_dyn_tag(v) == FLAN_DYN_TAG_INT)
|
|
|| (t->kind == ST_FLOAT
|
|
&& (flan_dyn_tag(v) == FLAN_DYN_TAG_INT
|
|
|| flan_dyn_tag(v) == FLAN_DYN_TAG_FLOAT)))
|
|
said_add("%s is not a value it holds exactly — ", sv);
|
|
else if (t->kind == ST_CLASS && flan_dyn_tag(v) == FLAN_DYN_TAG_MAP)
|
|
said_add("this is not an instance of it — ");
|
|
else
|
|
said_add("this is %s — ", tag_of(v));
|
|
if (by == BY_PUT)
|
|
said_add("(put %s :%.*s %s)", sm, sn, ss, sv);
|
|
else if (by == BY_SET)
|
|
said_add("(set (get %s :%.*s) %s)", sm, sn, ss, sv);
|
|
else if (site_building != NULL && loc != NULL)
|
|
said_add("(%.*s ...) with :%.*s %s; the slot is declared at %.*s",
|
|
cn, cs, sn, ss, sv, (int)loclen, (const char *)loc);
|
|
else
|
|
said_add("(%.*s ...) with :%.*s %s", cn, cs, sn, ss, sv);
|
|
/* A constructor's refusal is placed at the call that was wrong, when the
|
|
call said where it was, and names the slot's declaration after it. */
|
|
flan_say(by == BY_NEW && site_building != NULL ? site_building : loc,
|
|
by == BY_NEW && site_building != NULL ? site_building_len : loclen,
|
|
"%s", said_buf);
|
|
dyn_trap((const uint8_t *)"DynType", 7);
|
|
}
|
|
|
|
/* The value a store into [o] under [k] actually stores: [v], or the float an
|
|
* int widens to in a float slot. A class with no typed slot answers at its
|
|
* flag, and a map with no class before that. */
|
|
static flan_dyn check_slot(const uint8_t *loc, int64_t loclen, int by,
|
|
flan_obj *o, class_entry *e, flan_dyn m,
|
|
flan_dyn k, flan_dyn v) {
|
|
int64_t j;
|
|
flan_dyn out;
|
|
if (e == NULL || !e->typed) return v;
|
|
j = class_slot(e, k);
|
|
if (j < 0) return v;
|
|
if (!slot_admit(&e->types[j], v, &out))
|
|
trap_slot_type(loc, loclen, by, o, e, j, m, v);
|
|
return out;
|
|
}
|
|
|
|
static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v);
|
|
|
|
/* A key an instance's class does not declare, read or written. An instance
|
|
* has exactly its class's slots — a typo in a slot name is an error at the
|
|
* access and not a new key — so get, put and set all refuse one; a plain map
|
|
* takes any key. */
|
|
static _Noreturn void trap_no_slot(const uint8_t *loc, int64_t loclen,
|
|
const char *op, flan_obj *o,
|
|
class_entry *e, flan_dyn k) {
|
|
char sk[SAY_MAX];
|
|
kw_entry *c = o->u.v.klass;
|
|
int64_t i;
|
|
say(sk, SAY_MAX, k);
|
|
said_len = 0;
|
|
said_add("dyn %s: %.*s has no slot %s. Its slots are", op, (int)c->len,
|
|
(const char *)(c + 1), sk);
|
|
if (e == NULL || e->nslots == 0) said_add(" none");
|
|
else
|
|
for (i = 0; i < e->nslots; i++)
|
|
said_add(" :%.*s", (int)e->slots[i]->len,
|
|
(const char *)(e->slots[i] + 1));
|
|
flan_say(loc, loclen, "%s", said_buf);
|
|
dyn_trap((const uint8_t *)"DynType", 7);
|
|
}
|
|
|
|
/* A constructor's stores: [flan_dyn_map_set]'s, with the refusal worded for
|
|
* the constructor call it happened inside rather than for a [put] nobody
|
|
* wrote, and placed at the slot's declaration. */
|
|
void flan_dyn_slot_init(flan_dyn m, flan_dyn k, flan_dyn v,
|
|
const uint8_t *loc, int64_t loclen) {
|
|
flan_obj *o = want_map(loc, loclen, "construct", m, k);
|
|
class_entry *e = o->u.v.klass == NULL ? NULL : class_find(o->u.v.klass);
|
|
map_store(o, k, check_slot(loc, loclen, BY_NEW, o, e, m, k, v));
|
|
}
|
|
|
|
/* (set (get inst :slot) v). Three refusals, each its own sentence, because
|
|
* they are three different mistakes: the value is not a class instance at
|
|
* all (a map's entries are written with [put], which is where inserting a
|
|
* key is real); the key is not a slot the class declares; the value does not
|
|
* fit the slot's type. The first is why this is not [put]. */
|
|
void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
|
|
const uint8_t *loc, int64_t loclen) {
|
|
flan_obj *o;
|
|
class_entry *e;
|
|
int64_t j;
|
|
flan_dyn out;
|
|
if (is_map(m) && dyn_obj(m)->kind == OBJ_VIEW) {
|
|
const uint8_t *fty;
|
|
uint8_t *p = view_field(loc, loclen, "set", dyn_obj(m), k, &fty);
|
|
view_write(loc, loclen, "set", m, k, fty, v, p);
|
|
return;
|
|
}
|
|
if (!is_map(m) || dyn_obj(m)->u.v.klass == NULL) {
|
|
char sm[SAY_MAX];
|
|
say(sm, SAY_MAX, m);
|
|
flan_say(loc, loclen,
|
|
"dyn set: (get m k) is a place only on a class instance, and "
|
|
"this is %s%s — %s. A map's entries are written with put",
|
|
is_map(m) ? "a map with no class" : "a ",
|
|
is_map(m) ? "" : tag_of(m), sm);
|
|
dyn_trap((const uint8_t *)"DynType", 7);
|
|
}
|
|
o = dyn_obj(m);
|
|
e = class_sync(o);
|
|
j = class_slot(e, k);
|
|
if (j < 0) trap_no_slot(loc, loclen, "set", o, e, k);
|
|
if (!slot_admit(&e->types[j], v, &out))
|
|
trap_slot_type(loc, loclen, BY_SET, o, e, j, m, v);
|
|
map_store(o, k, out);
|
|
}
|
|
|
|
static void map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
|
|
int64_t loclen, int any_key) {
|
|
flan_obj *o;
|
|
class_entry *e;
|
|
if (!is_map(m)) trap2(loc, loclen, TYPE_TRAP, "put", "only a map answers it", m, k);
|
|
o = dyn_obj(m);
|
|
if (o->kind == OBJ_VIEW) {
|
|
const uint8_t *fty;
|
|
uint8_t *p = view_field(loc, loclen, "put", o, k, &fty);
|
|
view_write(loc, loclen, "put", m, k, fty, v, p);
|
|
return;
|
|
}
|
|
e = class_sync(o);
|
|
if (!any_key && e != NULL && class_slot(e, k) < 0)
|
|
trap_no_slot(loc, loclen, "put", o, e, k);
|
|
/* A map with no class, and a class with no typed slot, stop at the test. */
|
|
if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v);
|
|
map_store(o, k, v);
|
|
}
|
|
|
|
/* A program's put, and the store under a dyn's [.k] and [:k]. */
|
|
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
|
|
int64_t loclen) {
|
|
map_put(m, k, v, loc, loclen, 0);
|
|
}
|
|
|
|
/* With no site and any key: an untagged map literal's stores, and
|
|
* test/dyn_ops.c, which builds instances' odd states by hand. No program
|
|
* reaches an instance through it. */
|
|
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) {
|
|
map_put(m, k, v, NULL, 0, 1);
|
|
}
|
|
|
|
/* The store under all three, with the instance already brought up to date. */
|
|
static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v) {
|
|
int64_t i = map_find(o, k);
|
|
if (i >= 0) {
|
|
o->u.v.items[i * 2 + 1] = v;
|
|
return;
|
|
}
|
|
if (o->len == o->u.v.cap) {
|
|
int64_t cap = o->u.v.cap ? o->u.v.cap * 2 : 8;
|
|
flan_dyn *items =
|
|
(flan_dyn *)realloc(o->u.v.items, (size_t)cap * 2 * sizeof *items);
|
|
if (items == NULL) trap_oom(NULL, 0, cap * 2 * (int64_t)sizeof *items);
|
|
/* Charged now for the reason push's growth is: the trigger has to see
|
|
* the block while it is growing, not after. */
|
|
gc_bytes += (cap - o->u.v.cap) * 2 * (int64_t)sizeof *items;
|
|
o->u.v.items = items;
|
|
o->u.v.cap = cap;
|
|
}
|
|
o->u.v.items[o->len * 2] = k;
|
|
o->u.v.items[o->len * 2 + 1] = v;
|
|
o->len++;
|
|
}
|