/* flan_rt — the milestone-2 host ABI. * * This is the whole of it: argv, stdout, exit, and four text conversions * (plan.org, Milestone-2 primitives). Keeping the list this short is what * makes the wasm32 target cheap, because a primitive is the only thing * implemented twice. * * Every function here takes and returns scalars or an out-pointer. Nothing * returns a struct by value: the emitted .ll would then have to agree with the * platform's struct-return ABI, which is exactly the kind of thing that works * on x86-64 and silently does not on wasm32. */ #include #include #include #include /* ── Conditions, spec-conditions.md ────────────────────────────────── */ /* A handler stack, and nothing more. signal walks it, calls every frame whose * type matches, and returns; a handler that returns normally leaves the * signalling function to carry on, and with an empty stack signal is a null * check. Nothing here transfers control — restart-case is what will, and it * needs a calling convention this does not. * * Frames are allocated by the caller, on its own stack: establishing a handler * is two stores and a push. The condition crosses as a pointer because a * condition is a struct and the handler runs while the signalling frame is * still alive, so there is nothing to copy. * * A type is a number rather than a pointer to anything, so that a module * compiled later against a running program agrees with it: see Check.type_id. */ typedef struct flan_handler { struct flan_handler *prev; uint32_t type_id; void (*fn)(void *condition, void *xfer); } flan_handler; static flan_handler *handlers; void flan_handler_push(flan_handler *h) { h->prev = handlers; handlers = h; } void flan_handler_pop(flan_handler *h) { /* By frame, not by count: restoring what this frame displaced is correct * even if something below it got the stack out of step. */ handlers = h->prev; } /* [xfer] is the signalling function's own end of the transfer channel * (spec-conditions.md §6), threaded through so that a handler invoking a * restart can write its target into it. That makes this C frame transparent to * a transfer, which it has to be: a handler is always reached through here, so * the rule that a transfer cannot cross a foreign frame would otherwise make * restart-case useless. * * A handler that transfers stops the walk. The remaining handlers are for a * signal that is still looking for someone; this one has been answered. */ void flan_signal(uint32_t type_id, void *condition, void *xfer) { for (flan_handler *h = handlers; h != NULL; h = h->prev) if (h->type_id == type_id) { h->fn(condition, xfer); if (*(void **)xfer != NULL) return; } } /* A restart stack, the same shape and for the same reasons. What a transfer * carries is the *address* of one of these frames, not a number: the frame is * allocated by the restart-case that offers it, on its own stack, so the * address is unique against every module a running program may later load and * against every re-entry of the same restart-case. §4's "innermost frame * offering the name" is then just the order of the walk. */ typedef struct flan_restart { struct flan_restart *prev; uint32_t name_id; /* The name as written, beside the hash that matching uses. Matching never * needs it; a break loop does, because it has to show someone their choices * and nothing at run time can turn a hash back into a name. */ const uint8_t *name; int64_t namelen; } flan_restart; static flan_restart *restarts; void flan_restart_push(flan_restart *r) { r->prev = restarts; restarts = r; } void flan_restart_pop(flan_restart *r) { restarts = r->prev; } /* What is on offer, innermost first — spec-conditions.md §4's walk, without * committing to anything. This is [compute-restarts]' data; today its only * caller is the break loop. */ int32_t flan_restart_count(void) { int32_t n = 0; for (flan_restart *r = restarts; r != NULL; r = r->prev) n++; return n; } const uint8_t *flan_restart_name(int32_t i, int64_t *len) { for (flan_restart *r = restarts; r != NULL; r = r->prev) if (i-- == 0) { *len = r->namelen; return r->name; } *len = 0; return NULL; } void *flan_find_restart(uint32_t name_id) { for (flan_restart *r = restarts; r != NULL; r = r->prev) if (r->name_id == name_id) return r; return NULL; } /* The i'th frame itself, innermost first — the same walk as * [flan_restart_name] and the other half of it. A break loop that offers * someone a *list* has to be able to take the entry they picked, and §4's * by-name lookup cannot express "the second retry": it takes the first frame * offering the name, by definition, so a shadowed restart is on every list * and reachable from none of them. Identifying a restart positionally is the * only thing that fixes that, and it is why SBCL does the same. * * The address is the currency: a transfer carries the frame's address, so a * caller that holds one from before is holding the same frame the walk found, * whatever the walk finds today. */ void *flan_restart_frame(int32_t i) { for (flan_restart *r = restarts; r != NULL; r = r->prev) if (i-- == 0) return r; return NULL; } /* Aim the transfer channel at a frame obtained earlier. The same store * [flan_break_resume] makes and the same one an invoke-restart makes — this * only spells it without a lookup, for a caller that did its looking up when * the stack was worth reading. */ void flan_restart_take(void *frame, void *xfer) { *(void **)xfer = frame; } /* [T] and string are both ptr+len — see Emit.ll. */ typedef struct { const uint8_t *ptr; int64_t len; } flan_slice; static int rt_argc; static char **rt_argv; static flan_slice *rt_args; /* argv as [string], built once, never freed */ void flan_rt_init(int32_t argc, char **argv) { rt_argc = (int)argc; rt_argv = argv; /* Line buffered even when stdout is a file or a pipe, where the C default is * a 4K block. A Flan program can run for minutes with a REPL attached to it, * and output that only appears when it exits is output nobody can use. It is * also what makes a program's progress observable to a test that is driving * it. The cost is one write per line instead of per 4K. */ setvbuf(stdout, NULL, _IOLBF, 0); } void flan_argv(flan_slice *out) { if (rt_args == NULL && rt_argc > 0) { rt_args = (flan_slice *)malloc(sizeof(flan_slice) * (size_t)rt_argc); for (int i = 0; i < rt_argc; i++) { rt_args[i].ptr = (const uint8_t *)rt_argv[i]; rt_args[i].len = (int64_t)strlen(rt_argv[i]); } } out->ptr = (const uint8_t *)rt_args; out->len = (int64_t)rt_argc; } void flan_write_stdout(const uint8_t *p, int64_t n) { if (n > 0) fwrite(p, 1, (size_t)n, stdout); } /* The merged dev build's way out, and null everywhere else. * * A Flan [main] does not return: [Emit] ends it with a call to this and an * [unreachable], because stdout is a FILE* and the acceptance tests read it. * That is fine when the program is its own process and wrong when the compiler * is in the same one — [exit] would take the session down with the program. * The merged entry point installs a hook that flushes, tells the compiler the * program is done, and parks instead. See lib/dev.ml. */ void (*flan_exit_hook)(int32_t status) = 0; void flan_exit(int32_t status) { fflush(stdout); if (flan_exit_hook) flan_exit_hook(status); /* does not return */ exit((int)status); } /* The conversions are *text*: bytes->f64 parses "12.5", f64->bytes renders it. * calc-me's tokenizer needs the first, the prelude's printers the second. */ #define SCRATCH 64 static char scratch[SCRATCH]; /* rendered text lives here until the next call */ /* snprintf returns what it *would* have written, not what it did. The three * shims below hand the result back as a slice, so taking that number at face * value would publish a length past the end of the buffer and every reader of * that slice would run off it. No format here can reach 64 — %g is at most 13 * characters and %lld at most 20 — so this clamp cannot fire today; it is here * because the distance between "cannot fire" and "reads off the end of a * static buffer" is one format string, and nothing else in the file says so. * Found by reading, under a sanitizer sweep that could not have found it: * nothing in the corpus prints a number long enough. */ static int64_t fit(int n) { if (n < 0) return 0; return n < SCRATCH ? (int64_t)n : (int64_t)(SCRATCH - 1); } /* The length is clamped below *and* above. Above is obvious and was always * here. Below was not, and it was the real one: a slice's length is a signed * 64-bit count, (slice s 2 1) computes 2 - 1 - 2 = -1, and `(size_t)n` on a * negative n is 18446744073709551615, which is not less than 511, so k became * 511 and the memcpy read 511 bytes from wherever the slice pointed. A checked * build traps on the reversed slice before it gets here; an unchecked one does * not, and every other (ptr, len) entry point in this file — flan_write_stdout, * flan_escape_bytes, flan_dev_emit — already guards the negative case. These * two were the exceptions. */ static size_t clamp_len(int64_t n, size_t cap) { if (n <= 0) return 0; return (uint64_t)n < (uint64_t)cap ? (size_t)n : cap; } double flan_bytes_to_f64(const uint8_t *p, int64_t n) { char buf[512]; size_t k = clamp_len(n, sizeof buf - 1); memcpy(buf, p, k); buf[k] = '\0'; return strtod(buf, NULL); } int64_t flan_bytes_to_i64(const uint8_t *p, int64_t n) { char buf[64]; size_t k = clamp_len(n, sizeof buf - 1); memcpy(buf, p, k); buf[k] = '\0'; return (int64_t)strtoll(buf, NULL, 10); } /* %g so that 3.5 prints as "3.5" and not "3.500000" — calc-me's expected * output is a table of exact strings. */ void flan_f64_to_bytes(double x, flan_slice *out) { int n = snprintf(scratch, SCRATCH, "%g", x); out->ptr = (const uint8_t *)scratch; out->len = fit(n); } void flan_i64_to_bytes(int64_t x, flan_slice *out) { int n = snprintf(scratch, SCRATCH, "%lld", (long long)x); out->ptr = (const uint8_t *)scratch; out->len = fit(n); } /* u64 is not i64 with a flag: 0xFFFFFFFFFFFFFFFF is 18446744073709551615 and * not -1, and routing it through the signed printer is the only way println * could disagree with the REPL about a value both can hold. Hence a second * shim rather than a cast at the call site. */ void flan_u64_to_bytes(uint64_t x, flan_slice *out) { int n = snprintf(scratch, SCRATCH, "%llu", (unsigned long long)x); out->ptr = (const uint8_t *)scratch; out->len = fit(n); } /* A string *inside* a printed structure, quoted and escaped, so that the run * of bytes can be told from the punctuation around it — (S {:name "a b"}) has * two fields if the quotes are missing and one if they are there. * * This is the same escape table as flan_dev_emit_str in flan_dev.c, and * deliberately so: the REPL and println must not disagree about what a struct * looks like. It cannot be the *same function* because the dev one streams * into the result buffer and this one has to hand back a slice; if either * table changes, change both. * * Its own buffer, not `scratch`: escaping is the one conversion whose output * is not a bounded handful of characters. Over-long input is truncated with an * ellipsis rather than silently cut, because a value that prints as a shorter * value is the failure nobody notices. */ #define ESCAPE_MAX 1024 static char escaped[ESCAPE_MAX]; void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) { size_t len = n < 0 ? 0 : (size_t)n; size_t w = 0; int cut = 0; /* The guard reserves 9 bytes, and all 9 are spoken for: 4 for the longest * single escape (\xNN), 3 for the ellipsis, 1 for the closing quote, 1 * spare. So the loop never writes a partial escape and the three writes * after it never need a bound of their own. Swept over every length to 1300 * against \x01, '"', '\\' and 'a': the worst output is 1021 of 1024. If the * escape table ever grows a longer form, this 9 grows with it. */ escaped[w++] = '"'; for (size_t i = 0; i < len; i++) { if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; } unsigned char c = p[i]; switch (c) { case '"': escaped[w++] = '\\'; escaped[w++] = '"'; break; case '\\': escaped[w++] = '\\'; escaped[w++] = '\\'; break; case '\n': escaped[w++] = '\\'; escaped[w++] = 'n'; break; case '\t': escaped[w++] = '\\'; escaped[w++] = 't'; break; case '\r': escaped[w++] = '\\'; escaped[w++] = 'r'; break; default: if (c < 0x20) { w += (size_t)snprintf(escaped + w, 5, "\\x%02x", c); } else { escaped[w++] = (char)c; } } } if (cut) { escaped[w++] = '.'; escaped[w++] = '.'; escaped[w++] = '.'; } escaped[w++] = '"'; out->ptr = (const uint8_t *)escaped; out->len = (int64_t)w; } /* Bounds failures. The emitted code branches here and then falls off the end * with `unreachable`, so these must not return — the same explicit shape as * every other non-local exit, which is what keeps wasm32 free of unwinding. * * The location is passed as ptr+len because that is what a Flan string already * is; nothing here allocates. Exit 134 is abort()'s status without abort()'s * signal, so the same assertion should hold once wasm32 builds. * * stdout is flushed *before* the message: stderr is unbuffered and a * redirected stdout is not, so without this the error appears above the output * that led to it. */ static _Noreturn void rt_die(void) { fflush(stdout); fflush(stderr); exit(134); } _Noreturn void flan_bounds_fail(const uint8_t *loc, int64_t loclen, int64_t idx, int64_t len) { fflush(stdout); fprintf(stderr, "%.*s: index %lld is out of bounds for length %lld\n", (int)loclen, (const char *)loc, (long long)idx, (long long)len); rt_die(); } /* §2's diverging variant: the same walk, but a handler that returns normally * has not answered it. Only a transfer gets past here — the caller's guard * sees the channel and forwards it — so with nothing transferring the program * stops. In a dev build this is where the break loop will go; until it exists, * stopping is all there is, and it says which condition it was. * * [flan_signal] is not reused with a flag because the two differ in what they * do when the walk ends, which is the whole of §1 against §2. */ /* The dev-build break loop, spec-conditions.md §2. A hook rather than a direct * call because the loop lives in the *agent*, which is an optional package, and * this file is the release runtime — it must not depend on something a program * may not have imported. A program with no agent leaves this NULL and dies the * way it always did. * * The hook may resume by writing a restart into the transfer channel, which is * the same channel an invoke-restart writes and reaches the same guard. So * choosing a restart from the break loop and choosing one from a handler are * the same act, lowered the same way. */ void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition, void *xfer); /* Must agree with Check.type_id, byte for byte, or a name typed at the break * loop matches nothing. FNV-1a over the name, 32 bits. */ static uint32_t flan_name_id(const uint8_t *s, int64_t n) { uint32_t h = 0x811c9dc5u; for (int64_t i = 0; i < n; i++) { h ^= (uint32_t)s[i]; h *= 0x01000193u; } return h; } /* What the break loop calls to resume: look a restart up by the name someone * typed and aim the channel at it. 0 if no frame offers it, and then the loop * says so rather than resuming into nothing. */ int32_t flan_break_resume(const uint8_t *name, int64_t namelen, void *xfer) { void *r = flan_find_restart(flan_name_id(name, namelen)); if (r == NULL) return 0; *(void **)xfer = r; return 1; } void flan_error(uint32_t type_id, void *condition, void *xfer, const uint8_t *name, int64_t namelen) { flan_signal(type_id, condition, xfer); if (*(void **)xfer != NULL) return; /* Nothing handled it. In a dev build that is a place to stand, not the end * of the program — which is the whole of §2 and the reason it is worth * having. */ if (flan_break_hook != NULL) { flan_break_hook(name, namelen, condition, xfer); if (*(void **)xfer != NULL) return; } fflush(stdout); fprintf(stderr, "unhandled %.*s\n", (int)namelen, (const char *)name); rt_die(); } /* Nothing on the restart stack offers the name. It is reported where the * invoke was, because that is the only place that knows what was asked for; * there is nowhere to resume, so there is nothing else to do. */ _Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen, const uint8_t *name, int64_t namelen) { fflush(stdout); fprintf(stderr, "%.*s: no restart named %.*s is active\n", (int)loclen, (const char *)loc, (int)namelen, (const char *)name); rt_die(); } /* The frame the name found does not take these arguments — spec-conditions.md * §3's run-time check. It has to be at run time: a restart is resolved on a * dynamic stack, so the invoke site cannot see what it will find, and the * frame cannot see who will find it. What each end knows is its own parameter * list, so the message is both of them side by side. */ _Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen, const uint8_t *name, int64_t namelen, const uint8_t *want, int64_t wantlen, const uint8_t *got, int64_t gotlen) { fflush(stdout); fprintf(stderr, "%.*s: restart %.*s takes %.*s, given %.*s\n", (int)loclen, (const char *)loc, (int)namelen, (const char *)name, (int)wantlen, (const char *)want, (int)gotlen, (const char *)got); rt_die(); } /* A clause with parameters was reached by a transfer that filled none of them * in. No [invoke-restart] can do that — it writes the arguments before it aims * the channel — so this is the other way a transfer starts: the break loop, * which today takes a restart by position and has no way to supply a value. * Refused at the clause rather than run on a buffer nobody wrote. */ _Noreturn void flan_restart_unarmed(const uint8_t *loc, int64_t loclen, const uint8_t *name, int64_t namelen, const uint8_t *want, int64_t wantlen) { fflush(stdout); fprintf(stderr, "%.*s: restart %.*s takes %.*s, and whatever took it supplied no " "arguments — a restart with parameters cannot be taken from the " "break loop yet\n", (int)loclen, (const char *)loc, (int)namelen, (const char *)name, (int)wantlen, (const char *)want); rt_die(); } /* Something a defer called invoked a restart. A defer is the cleanup a * transfer runs on its way out (§5), so a transfer starting there would leave * this frame's defers half run with two targets and no way to choose. The * lexical case is refused by the checker; this is the one that reaches a * function through a call, where nothing static could see it. */ _Noreturn void flan_transfer_fail(const uint8_t *loc, int64_t loclen) { fflush(stdout); fprintf(stderr, "%.*s: a defer invoked a restart, which a defer may not do — it is " "the cleanup a transfer runs on its way out\n", (int)loclen, (const char *)loc); rt_die(); } _Noreturn void flan_slice_fail(const uint8_t *loc, int64_t loclen, int64_t lo, int64_t hi, int64_t len) { fflush(stdout); fprintf(stderr, "%.*s: slice [%lld %lld) is out of bounds for length %lld\n", (int)loclen, (const char *)loc, (long long)lo, (long long)hi, (long long)len); rt_die(); } /* ── An index out of range is a condition ────────────────────────────── * * The two functions above are still here and still die; what changed is that * they are no longer the *first* thing a bad index reaches. A bounds failure * now signals BoundsError with `error`, exactly as a failed allocation signals * StorageExhausted, and only reaches the message above if nothing answered. * * Why it had to change. `flan dev` runs the compiler inside the program, in * one process. exit(134) therefore took the session with it, and the session * is the thing the project is built around never having to restart. The * ordinary route into it is not exotic: a grid indexed from a mouse position * is out of bounds the first time the pointer leaves the window. * * **No restart is established here**, and that is a decision rather than an * omission. flan_alloc_* and the file guards offer `retry` because their * attempt is repeatable — a handler frees something, or supplies another * path, and the same operation then succeeds. Nothing a handler can do makes * index 51 valid for a length-50 array, so there is no attempt to re-run and * nothing for a site restart to resume into. `use-value` for the index is the * near miss: it would cost every indexing operation an alloca and a restart * frame, and what it buys is a *different element*, silently, which is the * class of answer this codebase refuses everywhere else. The restarts that * matter are the ones the program already established — a frame loop's * `continue` — and those are on the restart stack and reachable from the break * loop without anything being pushed here. * * With no break hook — a release build, or any program that did not import the * agent — flan_break_hook is NULL, nothing transfers, and this falls through * to the same message and the same status it always had. That is still the * right answer: there is nowhere to stand. * * The condition is three int64s on this frame and it must agree field for * field with the prelude's (defstruct BoundsError [low i64 high i64 length * i64]) — the same hand-kept agreement flan_name_id has with Check.type_id, * and for the same reason: a struct is a layout and a type is a number, and * neither side can see the other. `low` and `high` are the same index for an * `at`, and the two ends of the range for a `slice`, so one condition type * covers both and a handler writes one clause rather than two. */ typedef struct { int64_t low, high, length; } flan_bounds_cond; static const uint8_t flan_bounds_name[] = "BoundsError"; #define FLAN_BOUNDS_NAMELEN 11 /* Returns nonzero if something transferred, in which case the caller returns * and its caller's guard carries the transfer out. */ static int flan_bounds_signal(void *xfer, int64_t low, int64_t high, int64_t len) { flan_bounds_cond c; uint32_t id = flan_name_id(flan_bounds_name, FLAN_BOUNDS_NAMELEN); c.low = low; c.high = high; c.length = len; flan_signal(id, &c, xfer); if (*(void **)xfer != NULL) return 1; if (flan_break_hook != NULL) { flan_break_hook(flan_bounds_name, FLAN_BOUNDS_NAMELEN, &c, xfer); if (*(void **)xfer != NULL) return 1; } return 0; } void flan_bounds_error(const uint8_t *loc, int64_t loclen, int64_t idx, int64_t len, void *xfer) { if (flan_bounds_signal(xfer, idx, idx, len)) return; flan_bounds_fail(loc, loclen, idx, len); } void flan_slice_error(const uint8_t *loc, int64_t loclen, int64_t lo, int64_t hi, int64_t len, void *xfer) { if (flan_bounds_signal(xfer, lo, hi, len)) return; flan_slice_fail(loc, loclen, lo, hi, len); } /* ── Allocators, spec-memory.md ──────────────────────────────────────── * * One type-erased procedure plus an opaque data pointer, which is Odin's * shape (base/runtime/core.odin, Allocator_Proc), and every operation takes * size and align as parameters because the only place the concrete type is * known is the call site. * * A Flan `Allocator` value is a *pointer* to one of these, not a copy of it. * That is forced by two things in the spec and is not a convenience: the * capability set has to be readable at run time from wherever a container * landed, and `free-all` bumps an epoch that every container made from the * allocator has to observe. A copied-by-value allocator would give each copy * its own epoch and the dev trap would never fire. * * Nothing here returns a struct by value, per the file header. */ enum { FLAN_ALLOC_ALLOC = 0, FLAN_ALLOC_RESIZE = 1, FLAN_ALLOC_FREE = 2, FLAN_ALLOC_FREE_ALL = 3 }; /* The capability set. Odin reads its own back through the procedure * (Query_Features returning an Allocator_Mode_Set); a field is the same * information without the round trip, and `can-free` is the one that is * load-bearing — spec-memory.md refuses a drop-carrying container against an * allocator that lacks it. */ enum { FLAN_CAN_ALLOC = 1u << 0, FLAN_CAN_RESIZE = 1u << 1, FLAN_CAN_FREE = 1u << 2, FLAN_CAN_FREE_ALL = 1u << 3 }; typedef struct flan_allocator flan_allocator; /* Returns NULL on failure and never reports failure any other way. The * condition, the restart and the message are all the compiler's job; this * layer says yes or no. */ typedef void *(*flan_alloc_proc)(flan_allocator *a, int32_t mode, void *p, int64_t old_size, int64_t size, int64_t align); struct flan_allocator { flan_alloc_proc proc; void *data; uint32_t caps; /* Bumped on every free-all. A container records it and traps if it moved: * spec-memory.md, "Dev builds detect a released region". Separate from the * per-Vec generation word, which answers a different question. */ uint64_t epoch; /* Dev accounting for the general-purpose tier: "did you forget to free" is * an allocator-tier question and this is the allocator's answer. */ int64_t live_blocks; int64_t live_bytes; /* A cap on live bytes, or 0 for none. It is here because * spec-memory.md's retry restart is only answerable by a handler that can * make the *same* request succeed, and for a fixed backing buffer the only * such handler is one that raises the ceiling: releasing the region a * container lives in invalidates the container, which is what the epoch * check exists to catch. So "grow the arena and then invoke retry", which * the spec names as the handler that works, needs a ceiling to raise. It * doubles as the knob a test exhausts an allocator with on purpose. */ int64_t budget; }; /* Would this request put the allocator over its budget? */ static int flan_over_budget(flan_allocator *a, int64_t size) { return a->budget > 0 && a->live_bytes + size > a->budget; } /* -- The heap allocator: malloc, realloc, free. ---------------------- */ static void *flan_heap_proc(flan_allocator *a, int32_t mode, void *p, int64_t old_size, int64_t size, int64_t align) { switch (mode) { case FLAN_ALLOC_ALLOC: { void *q = NULL; size_t al, sz; if (size <= 0) return NULL; if (flan_over_budget(a, size)) return NULL; al = (size_t)(align < (int64_t)sizeof(void *) ? (int64_t)sizeof(void *) : align); sz = (size_t)size; /* aligned_alloc requires a size that is a multiple of the alignment. */ if (sz % al) sz += al - (sz % al); q = aligned_alloc(al, sz); if (q) { a->live_blocks++; a->live_bytes += size; } return q; } case FLAN_ALLOC_RESIZE: { /* aligned_alloc has no realloc, so growth is a new block and a copy. The * caller passes old_size for exactly this reason, and it is the one * number a wrong answer here would read off the end of. */ void *q; if (flan_over_budget(a, size - old_size)) return NULL; q = flan_heap_proc(a, FLAN_ALLOC_ALLOC, NULL, 0, size, align); if (!q) return NULL; if (p && old_size > 0) memcpy(q, p, (size_t)(old_size < size ? old_size : size)); if (p) { free(p); a->live_blocks--; a->live_bytes -= old_size; } return q; } case FLAN_ALLOC_FREE: if (p) { free(p); a->live_blocks--; a->live_bytes -= old_size; } return NULL; case FLAN_ALLOC_FREE_ALL: default: return NULL; } } static flan_allocator flan_heap = { flan_heap_proc, NULL, FLAN_CAN_ALLOC | FLAN_CAN_RESIZE | FLAN_CAN_FREE, 0, 0, 0, 0 }; /* -- The arena: one fixed backing buffer and a bump offset. ---------- * * `free-all` is retain-capacity: offset = 0, the pages stay. That is an * announced amendment to spec-memory.md's operation table (see BUILT.md) and * it is what Odin's arena_free_all already does in effect. Handing the pages * back is `arena-destroy`, a separate operation, because a frame arena reset * every frame must not return memory only to ask for it again. * * The epoch is bumped either way: the pages are the same but every container * made before the reset is invalid, which is the whole point of the trap. */ typedef struct flan_arena { uint8_t *base; int64_t cap; int64_t offset; int64_t peak; } flan_arena; static int64_t flan_align_up(int64_t x, int64_t a) { if (a <= 1) return x; return (x + a - 1) / a * a; } static void *flan_arena_proc(flan_allocator *a, int32_t mode, void *p, int64_t old_size, int64_t size, int64_t align) { flan_arena *ar = (flan_arena *)a->data; switch (mode) { case FLAN_ALLOC_ALLOC: { int64_t start, end; if (size <= 0) return NULL; if (flan_over_budget(a, size)) return NULL; if (align < 1) align = 1; start = flan_align_up(ar->offset, align); end = start + size; if (end > ar->cap || end < start) return NULL; /* exhausted, or overflow */ ar->offset = end; if (end > ar->peak) ar->peak = end; a->live_blocks++; a->live_bytes += size; return ar->base + start; } case FLAN_ALLOC_RESIZE: { void *q; /* Growing the most recent block in place is the one case worth special * casing: a Vec that is the only thing pushing into a frame arena grows * without copying, which is the common shape. */ if (p && (uint8_t *)p + old_size == ar->base + ar->offset) { int64_t end = (int64_t)((uint8_t *)p - ar->base) + size; if (end > ar->cap || end < 0) return NULL; ar->offset = end; if (end > ar->peak) ar->peak = end; a->live_bytes += size - old_size; return p; } q = flan_arena_proc(a, FLAN_ALLOC_ALLOC, NULL, 0, size, align); if (!q) return NULL; if (p && old_size > 0) memcpy(q, p, (size_t)(old_size < size ? old_size : size)); return q; /* the old block is not reclaimable */ } case FLAN_ALLOC_FREE: return NULL; /* refused by the capability set above */ case FLAN_ALLOC_FREE_ALL: ar->offset = 0; a->live_blocks = 0; a->live_bytes = 0; return NULL; default: return NULL; } } /* -- The context, spec-memory.md's context/allocator and context/temp ---- * * A dynamic variable with save and restore, not an extra parameter on every * signature. The spec calls it part of the calling convention; taking that * literally would touch every function signature, the FFI shim, the dev * trampolines and the reload ABI, for the same observable behaviour. The * literal reading is deferred and BUILT.md says so. * * There are no threads in Flan, so a plain global is the whole of it. */ static flan_allocator *flan_ctx_alloc = &flan_heap; static flan_allocator *flan_ctx_tmp = NULL; flan_allocator *flan_arena_new(int64_t cap); flan_allocator *flan_context_allocator(void) { return flan_ctx_alloc; } /* The default temp arena, made on first use. 1 MiB: big enough that the * per-frame tier does not fail on a toy program, small enough that a program * which never touches it has not paid for a heap. */ #define FLAN_TEMP_DEFAULT (1 << 20) flan_allocator *flan_context_temp(void) { if (!flan_ctx_tmp) flan_ctx_tmp = flan_arena_new(FLAN_TEMP_DEFAULT); return flan_ctx_tmp; } /* Returns the previous one, which is what with-allocator restores — on the * normal path and on the transfer path both. */ flan_allocator *flan_context_set(flan_allocator *a) { flan_allocator *prev = flan_ctx_alloc; if (a) flan_ctx_alloc = a; return prev; } void flan_context_restore(flan_allocator *a) { if (a) flan_ctx_alloc = a; } flan_allocator *flan_arena_new(int64_t cap) { flan_allocator *a; flan_arena *ar; if (cap <= 0) cap = FLAN_TEMP_DEFAULT; a = (flan_allocator *)calloc(1, sizeof *a); ar = (flan_arena *)calloc(1, sizeof *ar); if (!a || !ar) { free(a); free(ar); return NULL; } ar->base = (uint8_t *)malloc((size_t)cap); if (!ar->base) { free(a); free(ar); return NULL; } ar->cap = cap; a->proc = flan_arena_proc; a->data = ar; /* No FLAN_CAN_FREE: an arena cannot release one block, which is Odin's * answer too (allocators.odin returns Mode_Not_Implemented for .Free). */ a->caps = FLAN_CAN_ALLOC | FLAN_CAN_RESIZE | FLAN_CAN_FREE_ALL; return a; } void flan_arena_destroy(flan_allocator *a) { flan_arena *ar; if (!a || a->proc != flan_arena_proc) return; ar = (flan_arena *)a->data; if (a == flan_ctx_alloc) flan_ctx_alloc = &flan_heap; if (a == flan_ctx_tmp) flan_ctx_tmp = NULL; a->epoch++; free(ar->base); free(ar); free(a); } flan_allocator *flan_heap_allocator(void) { return &flan_heap; } int8_t flan_alloc_can_free(flan_allocator *a) { return (int8_t)(a && (a->caps & FLAN_CAN_FREE) ? 1 : 0); } int8_t flan_alloc_can_free_all(flan_allocator *a) { return (int8_t)(a && (a->caps & FLAN_CAN_FREE_ALL) ? 1 : 0); } int64_t flan_alloc_epoch(flan_allocator *a) { return a ? (int64_t)a->epoch : 0; } int64_t flan_alloc_live_blocks(flan_allocator *a) { return a ? a->live_blocks : 0; } int64_t flan_alloc_budget(flan_allocator *a) { return a ? a->budget : 0; } void flan_alloc_set_budget(flan_allocator *a, int64_t n) { if (a) a->budget = n < 0 ? 0 : n; } _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen); _Noreturn void flan_null_alloc_fail(const uint8_t *loc, int64_t loclen); /* free-all on an allocator that does not offer it is a trap, not a silent * no-op: "I released the region" and "I leaked the region" must not be the * same program text. */ void flan_alloc_free_all(flan_allocator *a, const uint8_t *loc, int64_t loclen) { /* A null allocator is a zeroed [defvar] nobody assigned yet. Silently doing * nothing would make "I released the region" and "I never made one" the same * program text, which is the thing this trap exists to prevent. */ if (!a) flan_null_alloc_fail(loc, loclen); if (!(a->caps & FLAN_CAN_FREE_ALL)) flan_free_all_fail(loc, loclen); a->proc(a, FLAN_ALLOC_FREE_ALL, NULL, 0, 0, 0); a->epoch++; } _Noreturn void flan_null_alloc_fail(const uint8_t *loc, int64_t loclen) { fflush(stdout); fprintf(stderr, "%.*s: this allocator is null — a zeroed Allocator was never given " "one\n", (int)loclen, (const char *)loc); rt_die(); } _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) { fflush(stdout); fprintf(stderr, "%.*s: this allocator does not offer free-all — it has no region to " "release, and releasing nothing is not the same as releasing " "everything\n", (int)loclen, (const char *)loc); rt_die(); } /* ── (Vec T), spec-memory.md ──────────────────────────────────────────── * * One type-erased runtime over (size, align), which is Odin's arrangement * (base/runtime/dynamic_array_internal.odin): the monomorphised wrapper is the * only place the concrete type is known, so it is the only place that can * produce the numbers, and it passes them in. There are no generics here and * none are needed. * * Header, and it is six words rather than the spec's four: * * ptr len cap allocator the release layout spec-memory.md fixes * gen bumped on every reallocation — the stale-slice * word. It has no reader yet; see BUILT.md. * epoch the allocator's epoch when this Vec last * touched it. Any operation on a container whose * recorded epoch has moved traps. * * The two dev words are present in every build, not only a dev one, and that * is not laziness: a redefinition module is built by llc and ld against a host * that was built separately, and nothing makes the two agree on a struct size. * A layout that changes with a build flag is a layout that can disagree across * that boundary silently. Dropping them in release is deferred and BUILT.md * says what it is blocked on. * * Every entry point returns int8_t 1/0 for "did it fit", and never reports * failure any other way: the condition, the restart and the message are the * compiler's job (see Check's alloc_guard). */ typedef struct flan_vec { void *ptr; int64_t len; int64_t cap; flan_allocator *alloc; int64_t gen; int64_t epoch; } flan_vec; /* The request that did not fit, for the condition the compiler builds at the * failing site. A pair of globals rather than out-parameters because the * condition is a value struct on the signalling frame's stack with fixed * numeric fields and no rendered message — spec-memory.md is explicit that * this is the one path that must not allocate, and reading two words is the * cheapest way to carry the numbers out. */ static int64_t flan_fail_bytes = 0; static int64_t flan_fail_align = 0; static int64_t flan_fail_id = 0; int64_t flan_alloc_fail_bytes(void) { return flan_fail_bytes; } int64_t flan_alloc_fail_align(void) { return flan_fail_align; } int64_t flan_alloc_fail_id(void) { return flan_fail_id; } /* The allocator's identity, for the condition's :allocator field. The pointer * is the identity — the same thing the epoch hangs off. */ int64_t flan_alloc_id(flan_allocator *a) { return (int64_t)(intptr_t)a; } _Noreturn void flan_vec_stale_fail(const uint8_t *loc, int64_t loclen, int64_t was, int64_t now) { fflush(stdout); fprintf(stderr, "%.*s: this container's allocator was released — it was made at " "epoch %lld and the allocator is at %lld now\n", (int)loclen, (const char *)loc, (long long)was, (long long)now); rt_die(); } _Noreturn void flan_vec_bounds_fail(const uint8_t *loc, int64_t loclen, int64_t i, int64_t len) { fflush(stdout); fprintf(stderr, "%.*s: index %lld is out of bounds for length %lld\n", (int)loclen, (const char *)loc, (long long)i, (long long)len); rt_die(); } /* spec-memory.md, "Dev builds detect a released region". This is the check * that makes the epoch word worth carrying, and it runs on every operation, * not only in a dev build — see the header on why the words are unconditional. * A Vec that never allocated has no allocator and nothing to check. */ static void flan_vec_check(flan_vec *v, const uint8_t *loc, int64_t loclen) { if (v->alloc) { int64_t now = (int64_t)v->alloc->epoch; if (now != v->epoch) flan_vec_stale_fail(loc, loclen, v->epoch, now); } } /* A zeroed Vec — a struct field nobody assigned, or a (defvar xs (Vec i32)) — * has a null allocator, and the first operation that needs storage adopts the * context allocator. That is Odin's behaviour, and the alternative was to * refuse a Vec-typed struct field outright until step 5. Shipping the null * silently was not an option: it is a null deref on the first push. */ static flan_allocator *flan_vec_adopt(flan_vec *v) { if (!v->alloc) { v->alloc = flan_context_allocator(); v->epoch = (int64_t)v->alloc->epoch; } return v->alloc; } static int8_t flan_vec_grow(flan_vec *v, int64_t want, int64_t size, int64_t align) { flan_allocator *a = flan_vec_adopt(v); int64_t cap = v->cap; void *p; if (want <= cap) return 1; /* Doubling, from four. Four rather than one because the three reallocations * a growing-from-one Vec does before it holds anything are pure cost, and * doubling because it is what makes n pushes amortised O(n). */ if (cap < 4) cap = 4; while (cap < want) { if (cap > (int64_t)1 << 40) { cap = want; break; } cap *= 2; } flan_fail_bytes = cap * size; flan_fail_align = align; flan_fail_id = (int64_t)(intptr_t)a; if (v->ptr) p = a->proc(a, FLAN_ALLOC_RESIZE, v->ptr, v->cap * size, cap * size, align); else p = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, cap * size, align); if (!p) return 0; v->ptr = p; v->cap = cap; /* Any slice taken before this points at storage that may have moved. The * word is bumped here and read nowhere yet; see BUILT.md. */ v->gen++; return 1; } int8_t flan_vec_init(flan_vec *v, flan_allocator *a, int64_t cap, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { /* [a] is NULL only when no allocator was named at the site and the context * is being used. An allocator *named* at the site and null is a zeroed * Allocator nobody assigned, and substituting the heap for it would be the * same "released the region / never made one" collapse flan_alloc_free_all * traps for — except silent, and discovered as a leak. The checker cannot * see it, because a null is a run-time value. * * The no-allocator-named case never arrives here as NULL: the checker passes * flan_context_allocator(), which always answers one. */ if (!a) flan_null_alloc_fail(loc, loclen); v->ptr = NULL; v->len = 0; v->cap = 0; v->gen = 0; v->alloc = a; v->epoch = (int64_t)v->alloc->epoch; if (cap <= 0) return 1; return flan_vec_grow(v, cap, size, align); } int8_t flan_vec_reserve(flan_vec *v, int64_t n, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { flan_vec_check(v, loc, loclen); if (n <= v->cap) return 1; return flan_vec_grow(v, n, size, align); } int8_t flan_vec_push(flan_vec *v, const void *elem, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { flan_vec_check(v, loc, loclen); if (v->len + 1 > v->cap && !flan_vec_grow(v, v->len + 1, size, align)) return 0; memcpy((uint8_t *)v->ptr + v->len * size, elem, (size_t)size); v->len++; return 1; } int64_t flan_vec_len(flan_vec *v, const uint8_t *loc, int64_t loclen) { flan_vec_check(v, loc, loclen); return v->len; } /* [xfer] is this operation's end of the transfer channel, so that an index out * of range signals BoundsError instead of ending the process — see the note * above flan_bounds_error. (at v i) and (at arr i) are the same form in the * source and shipping one of them signalling and the other exiting would read * as a bug, so the two are plumbed together. The channel is the last * parameter, as it is on every Flan signature; Emit appends it and guards the * call, and a transfer therefore leaves through the caller's pad with NULL * here never dereferenced. */ void *flan_vec_at(flan_vec *v, int32_t i, int64_t size, const uint8_t *loc, int64_t loclen, void *xfer) { flan_vec_check(v, loc, loclen); /* The same unsigned comparison the fixed-array bounds check uses: a negative * index sign-extends to a huge unsigned and is caught by the one test. */ if ((uint64_t)(int64_t)i >= (uint64_t)v->len) { if (flan_bounds_signal(xfer, (int64_t)i, (int64_t)i, v->len)) return NULL; flan_vec_bounds_fail(loc, loclen, (int64_t)i, v->len); } return (uint8_t *)v->ptr + (int64_t)i * size; } /* [hi] of -1 means "to the end": (as-slice v) has no static length to write. */ void flan_vec_as_slice(flan_vec *v, void *out, int32_t lo, int32_t hi, int64_t size, const uint8_t *loc, int64_t loclen, void *xfer) { struct { void *p; int64_t n; } s; int64_t l = lo, h = (hi < 0) ? v->len : hi; flan_vec_check(v, loc, loclen); if (l < 0 || h > v->len || l > h) { /* Both ends, because both are what went wrong — the fixed-array slice * check reports the same pair. [out] is left untouched on the transfer * path; the caller's guard branches before it reads the slice. */ if (flan_bounds_signal(xfer, l, h, v->len)) return; flan_vec_bounds_fail(loc, loclen, l, v->len); } s.p = (uint8_t *)v->ptr + l * size; s.n = h - l; memcpy(out, &s, sizeof s); } /* spec-memory.md's first release point. The Vec is left zeroed rather than * dangling — the checker has already made using it afterwards a compile error, * and zeroing costs nothing and makes a bug that slips past the checker a null * deref rather than a use-after-free. An allocator without can-free keeps the * block: releasing it is free-all's job, and pretending otherwise here is the * silent-no-op this file refuses elsewhere. */ void flan_vec_free(flan_vec *v, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { flan_vec_check(v, loc, loclen); if (v->ptr && v->alloc && (v->alloc->caps & FLAN_CAN_FREE)) v->alloc->proc(v->alloc, FLAN_ALLOC_FREE, v->ptr, v->cap * size, 0, align); (void)align; v->ptr = NULL; v->len = 0; v->cap = 0; v->alloc = NULL; v->gen++; v->epoch = 0; } int8_t flan_vec_clone(flan_vec *dst, flan_vec *src, flan_allocator *a, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { flan_vec_check(src, loc, loclen); if (!flan_vec_init(dst, a, src->len, size, align, loc, loclen)) return 0; if (src->len > 0) memcpy(dst->ptr, src->ptr, (size_t)(src->len * size)); dst->len = src->len; return 1; } /* ── (Pool T) and (Handle T), spec-memory.md ───────────────────────── * * A handle is a reference to something that can die, which reports that it * died rather than silently resolving to whatever reused its slot. That is * the whole design, and every decision below follows from it. * * THE PACKING. A handle is one int64_t: the slot index in the low 32 bits and * that slot's generation counter in the high 32. One word, so it copies, * zeroes and compares like the integer it is, and owns nothing — the pool is * the single owner. 32 bits of index because a Vec's index is an i32 here and * widening indices is one change across every container, not a pool question. * * LIVE IS ODD. A slot's generation starts at 0 and is bumped on every * allocation and on every release, so an odd generation means live and an * even one means dead. Two things fall out of that and both are load-bearing: * a zeroed handle is generation 0, which is even, so it resolves to nothing * rather than to slot 0 — ZII gives a handle field the right meaning for * free; and iteration can ask a slot whether it is live without a second * array or a spare bit. * * WRAPPING RETIRES THE SLOT. 32 bits is 2^31 allocate/release pairs on one * slot — every frame at 60fps for a year and a bit — but "rare" is not an * answer when the failure is the silent wrong one this type exists to * prevent. So a release from generation 0xFFFFFFFF bumps to 0 and does *not* * put the slot back on the free list. The slot is retired: dead forever, its * payload leaked, and no future handle can ever collide with an old one. * Leaking is defined behaviour here (spec-memory.md, "Leaking is defined * behaviour") and one slot is a bounded price for making the collision * unrepresentable rather than unlikely. * * TWO FAILURES, KEPT APART. A stale handle answers "gone" — it is an answer, * not an error. A pool whose allocator was released traps, through the same * epoch check a Vec gets. They answer different questions and must not be * conflated, exactly as the Vec's generation and epoch words must not be. * * GROWTH IS TRANSACTIONAL, and that is not tidiness. spec-memory.md's * StorageExhausted restart re-attempts *the same call*, so a failed grow has * to leave the pool byte for byte as it was — including a cap that still * agrees with the real block sizes, since the next attempt passes cap as the * allocator's old_size. Two blocks grow together, so a resize-in-place of the * first followed by a failure on the second would leave cap describing * neither. Allocate both, copy, then release the old pair: the only state * mutated after the last thing that can fail. */ typedef struct flan_pool_slot { uint32_t gen; /* odd: live. even: dead. 0: never allocated, or retired. */ int32_t next; /* free-list link, -1 for the end. Meaningless while live. */ } flan_pool_slot; typedef struct flan_pool { void *items; /* cap payloads, size bytes each */ flan_pool_slot *slots; /* cap slot headers, index-parallel with items */ int64_t len; /* slot high-water: 0..len have ever been handed out */ int64_t cap; int64_t live; /* how many of those are live now */ int64_t free; /* head of the free list, -1 when empty */ flan_allocator *alloc; int64_t epoch; } flan_pool; static int64_t flan_handle_pack(int64_t i, uint32_t gen) { return (int64_t)(((uint64_t)gen << 32) | (uint64_t)(uint32_t)i); } static int64_t flan_handle_index(int64_t h) { return (int64_t)(uint32_t)(uint64_t)h; } static uint32_t flan_handle_gen(int64_t h) { return (uint32_t)((uint64_t)h >> 32); } /* The same epoch check a Vec gets, and for the same reason. A pool that never * allocated has no allocator and nothing to check. */ static void flan_pool_check(flan_pool *p, const uint8_t *loc, int64_t loclen) { if (p->alloc) { int64_t now = (int64_t)p->alloc->epoch; if (now != p->epoch) flan_vec_stale_fail(loc, loclen, p->epoch, now); } } static flan_allocator *flan_pool_adopt(flan_pool *p) { if (!p->alloc) { p->alloc = flan_context_allocator(); p->epoch = (int64_t)p->alloc->epoch; } return p->alloc; } static int8_t flan_pool_grow(flan_pool *p, int64_t want, int64_t size, int64_t align) { flan_allocator *a = flan_pool_adopt(p); int64_t cap = p->cap, sslot = (int64_t)sizeof(flan_pool_slot); void *ni, *ns; if (want <= cap) return 1; /* Doubling from four, exactly as the Vec grows. */ if (cap < 4) cap = 4; while (cap < want) { if (cap > (int64_t)1 << 40) { cap = want; break; } cap *= 2; } flan_fail_bytes = cap * size + cap * sslot; flan_fail_align = align; flan_fail_id = (int64_t)(intptr_t)a; ni = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, cap * size, align); if (!ni) return 0; ns = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, cap * sslot, 8); if (!ns) { /* An allocator without can-free leaks the first block here. That is the * defined outcome and not a new one: the request failed because the * region is exhausted, and the region is about to be released whole or * the ceiling raised and the call re-attempted. */ if (a->caps & FLAN_CAN_FREE) a->proc(a, FLAN_ALLOC_FREE, ni, cap * size, 0, align); return 0; } if (p->len > 0) { memcpy(ni, p->items, (size_t)(p->len * size)); memcpy(ns, p->slots, (size_t)(p->len * sslot)); } if (p->items && (a->caps & FLAN_CAN_FREE)) { a->proc(a, FLAN_ALLOC_FREE, p->items, p->cap * size, 0, align); a->proc(a, FLAN_ALLOC_FREE, p->slots, p->cap * sslot, 0, 8); } p->items = ni; p->slots = ns; p->cap = cap; return 1; } int8_t flan_pool_init(flan_pool *p, flan_allocator *a, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { (void)size; (void)align; /* Null for the same reason and with the same answer flan_vec_init gives: * the no-allocator-named case never arrives here as NULL. */ if (!a) flan_null_alloc_fail(loc, loclen); p->items = NULL; p->slots = NULL; p->len = 0; p->cap = 0; p->live = 0; p->free = -1; p->alloc = a; p->epoch = (int64_t)a->epoch; return 1; } /* 1/0 for "did it fit", like every other allocating entry point. The handle * goes out through [out] rather than being returned, so that the compiler's * alloc_guard reads the answer and the handle separately. */ int8_t flan_pool_insert(flan_pool *p, const void *elem, int64_t *out, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { int64_t i; flan_pool_check(p, loc, loclen); if (p->free >= 0) { i = p->free; p->free = p->slots[i].next; } else { if (p->len + 1 > p->cap && !flan_pool_grow(p, p->len + 1, size, align)) return 0; i = p->len++; p->slots[i].gen = 0; p->slots[i].next = -1; } p->slots[i].gen++; /* even -> odd: this slot is live */ p->live++; memcpy((uint8_t *)p->items + i * size, elem, (size_t)size); *out = flan_handle_pack(i, p->slots[i].gen); return 1; } /* NULL when the handle names nothing, which the compiler turns into None. The * index is bounded with the unsigned comparison flan_vec_at uses, because the * low half of a handle can be any 32 bits at all. */ void *flan_pool_resolve(flan_pool *p, int64_t h, int64_t size, const uint8_t *loc, int64_t loclen) { int64_t i = flan_handle_index(h); uint32_t g = flan_handle_gen(h); flan_pool_check(p, loc, loclen); if (!(g & 1u)) return NULL; /* a zeroed or dead handle */ if ((uint64_t)i >= (uint64_t)p->len) return NULL; if (p->slots[i].gen != g) return NULL; /* the slot was reused */ return (uint8_t *)p->items + i * size; } /* 1 if this call released it, 0 if the handle was already gone. Releasing * twice is therefore an answer rather than undefined behaviour — which is the * generational scheme paying for itself a second time, since a pool is the * one place a double free is *detectable* rather than merely refused. */ int8_t flan_pool_release(flan_pool *p, int64_t h, const uint8_t *loc, int64_t loclen) { int64_t i = flan_handle_index(h); uint32_t g = flan_handle_gen(h), was; flan_pool_check(p, loc, loclen); if (!(g & 1u)) return 0; if ((uint64_t)i >= (uint64_t)p->len) return 0; if (p->slots[i].gen != g) return 0; was = p->slots[i].gen; p->slots[i].gen = was + 1; /* odd -> even: dead, and every old handle with it */ p->live--; /* The wrap. See the header: the slot is retired rather than reissued. */ if (was != 0xFFFFFFFFu) { p->slots[i].next = (int32_t)p->free; p->free = i; } return 1; } int64_t flan_pool_len(flan_pool *p, const uint8_t *loc, int64_t loclen) { flan_pool_check(p, loc, loclen); return p->len; } int64_t flan_pool_live(flan_pool *p, const uint8_t *loc, int64_t loclen) { flan_pool_check(p, loc, loclen); return p->live; } /* The handle of slot [i], or 0 — the never-valid handle — if that slot is * dead. This plus (len p) is the whole of enumeration, which is what * migrate-instances needs and what a Vec behind an index cannot give: a Vec's * indices shift under a removal and a pool's never do. Out of range traps * rather than answering 0, because an index is an index here and 0..len are * the valid ones. */ int64_t flan_pool_handle(flan_pool *p, int32_t i, const uint8_t *loc, int64_t loclen) { uint32_t g; flan_pool_check(p, loc, loclen); if ((uint64_t)(int64_t)i >= (uint64_t)p->len) flan_vec_bounds_fail(loc, loclen, (int64_t)i, p->len); g = p->slots[i].gen; if (!(g & 1u)) return 0; return flan_handle_pack((int64_t)i, g); } /* spec-memory.md's first release point, applied to the owner. Zeroed rather * than left dangling, for the reason flan_vec_free zeroes. Every handle into * it is stale afterwards and says so: len goes to 0, so the bound check * answers "gone" for all of them. */ void flan_pool_free(flan_pool *p, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen) { flan_pool_check(p, loc, loclen); if (p->items && p->alloc && (p->alloc->caps & FLAN_CAN_FREE)) { p->alloc->proc(p->alloc, FLAN_ALLOC_FREE, p->items, p->cap * size, 0, align); p->alloc->proc(p->alloc, FLAN_ALLOC_FREE, p->slots, p->cap * (int64_t)sizeof(flan_pool_slot), 0, 8); } p->items = NULL; p->slots = NULL; p->len = 0; p->cap = 0; p->live = 0; p->free = -1; p->alloc = NULL; p->epoch = 0; } /* ── (Map K V), spec-memory.md ────────────────────────────────────────── * * Odin's map, followed deliberately: open-addressed Robin Hood hashing at a * 75% load factor, cache-line cell packing, and pointer-width integers through * the probe loop (base/runtime/dynamic_map_internal.odin, whose header states * the same three). One type-erased runtime over (key size, value size) and a * compiler-emitted hash and equality pair, exactly as the Vec runtime is one * over (size, align). * * Why the shape matters, since the obvious question is whether this is another * Python dict. Python's algorithm is fine. What makes it slow is that every * key and every value is a separately allocated, reference-counted object, and * hashing goes through __hash__ and __eq__ calls that cannot be inlined. Here * a key is raw bytes inside the block and the hash and comparison are compiled * concretely per key type. That is most of the gap before any cleverness. * * Robin Hood, in one paragraph. Every occupied slot has a probe distance: how * far it sits from the slot its hash wanted. On insert, if the element already * in a slot is closer to its desired slot than the element being placed, the * two swap and the poorer one carries on down the run. Distances even out, the * worst case collapses towards the average, and a lookup may stop the moment * it is further from home than the occupant it is looking at — which is the * early exit in flan_map_find and is why a miss costs about what a hit does. * * Cache-line cells, in one more. A flat [capacity]K array lets one key straddle * two cache lines, so a probe that walks four slots can touch five lines. A * Map_Cell packs as many Ks as fit in 64 bytes and pads the remainder, so no * key ever straddles a line and a linear probe walks memory in the order the * prefetcher expects. Keys, values and hashes are three separate blocks, so a * probe — which reads hashes and only then one key — touches hash lines and * nothing else until it has a candidate. * * Header, six words, the same as flan_vec's and for the same reason (a layout * that changes with a build flag can disagree across the reload boundary): * * data one allocation: keys | values | hashes | scratch * len live entries * log2cap 0 until something is allocated; never 1 or 2 after * allocator gen epoch as on a Vec, and checked the same way * * Odin stuffs log2cap into the low six bits of the data pointer because its * Raw_Map must be three words. This header already carries an allocator, a * generation and an epoch, so the bit-stuffing would buy nothing and cost a * mask on every access — and, more usefully, not tagging means correctness * never depends on the block being 64-byte aligned. It is requested as 64, and * cell packing pays off when the request is honoured, but an arena whose base * is not cache-aligned gives a slower map rather than a wrong one. * * Every entry point returns int8_t 1/0 for "did it fit", never reporting * failure any other way — the condition, the restart and the message are the * compiler's job (Check's alloc_guard). */ #define FLAN_MAP_CACHE_LINE 64 #define FLAN_MAP_LOAD_FACTOR 75 #define FLAN_MAP_MIN_LOG2 3 /* 8 slots */ /* The hash word. Zero means the slot is empty, which is what makes a * zeroed hash block an empty map. There is no tombstone: removal is deferred * (spec-memory.md defers move-aware lookup, removal and owned entries), so the * only two states a slot has are empty and occupied. That deletes Odin's * backward-shift loop from this file entirely, and it is the single largest * reason this is shorter than the Odin original. * * The top bit is set on every stored hash so that a hasher answering 0 does * not read as an empty slot. It is the highest bit, so the desired slot and * the probe distance — which use only the low log2cap bits — are unchanged by * it, and no hash needs rewriting when the capacity changes. */ typedef uint64_t flan_map_hash; #define FLAN_MAP_OCCUPIED ((uint64_t)1 << 63) /* The pair the compiler emits per key type. [size] is the key's size, passed * so that the flat hasher and comparator below can serve every key whose * equality is bytewise and need no per-type function at all. * * The trailing pointer is the transfer channel. Every Flan function's emitted * signature ends with one (Emit's xfer_param), and a hash function emitted for * a struct key is an ordinary Flan function — so the typedef spells it out * rather than hoping nothing ever writes through it. Nothing does: neither a * hasher nor a comparator can signal, because the only things either can call * are the leaf C entry points below. It is passed as a real address, never * NULL, so that a store through it would be a store and not a crash. */ typedef uint64_t (*flan_hash_fn)(const void *key, uint64_t seed, int64_t size, void *xfer); typedef int8_t (*flan_eq_fn)(const void *a, const void *b, int64_t size, void *xfer); typedef struct flan_map { void *data; int64_t len; int64_t log2cap; flan_allocator *alloc; int64_t gen; int64_t epoch; } flan_map; /* ── Hashing ────────────────────────────────────────────────────────── * * FNV-1a over the bytes, then a final avalanche. FNV alone leaves the low bits * poorly mixed and the low bits are exactly what selects the slot, so the * splitmix64 finaliser is not decoration: without it consecutive small integer * keys collide in long runs. The seed is mixed in first so that two maps do not * agree on the same pathological ordering. */ static uint64_t flan_mix64(uint64_t x) { x ^= x >> 30; x *= 0xbf58476d1ce4e5b9ULL; x ^= x >> 27; x *= 0x94d049bb133111ebULL; x ^= x >> 31; return x; } static uint64_t flan_hash_mem(const uint8_t *p, int64_t n, uint64_t seed) { uint64_t h = 0xcbf29ce484222325ULL ^ seed; int64_t i = 0; /* Eight bytes at a time. Byte-at-a-time FNV is a serial chain of one * multiply per byte, and the multiply's latency is the whole cost — it was * a quarter of a lookup before this. The tail is the byte loop, which is * the original and is what any size not a multiple of eight still gets. */ for (; i + 8 <= n; i += 8) { uint64_t w; memcpy(&w, p + i, 8); h = (h ^ w) * 0x100000001b3ULL; } for (; i < n; i++) h = (h ^ (uint64_t)p[i]) * 0x100000001b3ULL; return flan_mix64(h); } /* The key is [size] bytes and its equality is bytewise. Every integer, enum, * bool, float and fixed array of those is served by this one pair, so the * compiler emits a function only for a key type that needs one. */ /* Each of the four comes in two spellings, and the split is not decoration. * * flan_key_hash_flat(k, seed, size) called directly * flan_hash_flat(k, seed, size, xfer) taken as a function pointer * * The pointer form has to match flan_hash_fn, whose last parameter exists * because a hash function emitted for a struct key is an ordinary Flan * function and every Flan function's signature ends with the transfer channel. * The direct form has to match what such an emitted function *calls*, and an * emitted function has no channel to hand on — it would be passing its own, * which is not the same thing and not something a leaf hasher should see. So * one is the implementation and the other is a thin wrapper, rather than one * function called two ways with an argument that is a lie in one of them. */ uint64_t flan_key_hash_flat(const void *key, uint64_t seed, int64_t size) { /* A key that is one machine word — which is every integer, every enum and * every bool, so very nearly every key — is one load and one mix. This is * where "the hash is compiled concretely per key type" stops being a * description of the arrangement and starts being the reason it is quick: * the general path is a loop over bytes with a multiply chain, and none of * these takes it. */ switch (size) { case 1: return flan_mix64((uint64_t)*(const uint8_t *)key + seed); case 2: { uint16_t x; memcpy(&x, key, 2); return flan_mix64((uint64_t)x + seed); } case 4: { uint32_t x; memcpy(&x, key, 4); return flan_mix64((uint64_t)x + seed); } case 8: { uint64_t x; memcpy(&x, key, 8); return flan_mix64(x + seed); } default: return flan_hash_mem((const uint8_t *)key, size, seed); } } int8_t flan_key_eq_flat(const void *a, const void *b, int64_t size) { /* Likewise, and for a sharper reason: memcmp on eight bytes is a *call* into * libc's vectorised implementation, which was an eighth of a lookup. These * four cases are a load and a compare. */ switch (size) { case 1: return (int8_t)(*(const uint8_t *)a == *(const uint8_t *)b); case 2: { uint16_t x, y; memcpy(&x, a, 2); memcpy(&y, b, 2); return (int8_t)(x == y); } case 4: { uint32_t x, y; memcpy(&x, a, 4); memcpy(&y, b, 4); return (int8_t)(x == y); } case 8: { uint64_t x, y; memcpy(&x, a, 8); memcpy(&y, b, 8); return (int8_t)(x == y); } default: return (int8_t)(memcmp(a, b, (size_t)size) == 0); } } /* Copying one entry's worth of bytes. Same reason again: memcpy of eight bytes * became a call into libc's memmove, which is pure overhead for a size the * switch resolves to a single load and store. */ static void flan_copy_small(void *dst, const void *src, int64_t n) { switch (n) { case 1: *(uint8_t *)dst = *(const uint8_t *)src; return; case 2: memcpy(dst, src, 2); return; case 4: memcpy(dst, src, 4); return; case 8: memcpy(dst, src, 8); return; case 16: memcpy(dst, src, 16); return; default: memcpy(dst, src, (size_t)n); return; } } uint64_t flan_hash_flat(const void *key, uint64_t seed, int64_t size, void *xfer) { (void)xfer; return flan_key_hash_flat(key, seed, size); } int8_t flan_eq_flat(const void *a, const void *b, int64_t size, void *xfer) { (void)xfer; return flan_key_eq_flat(a, b, size); } /* A string is ptr+len and its bytes are elsewhere, so neither the flat hasher * nor memcmp is correct for it: two equal strings at different addresses must * hash the same. [size] is ignored; the shape is fixed. */ uint64_t flan_key_hash_str(const void *key, uint64_t seed, int64_t size) { const flan_slice *s = (const flan_slice *)key; (void)size; return flan_hash_mem(s->ptr, s->len, seed); } int8_t flan_key_eq_str(const void *a, const void *b, int64_t size) { const flan_slice *x = (const flan_slice *)a, *y = (const flan_slice *)b; (void)size; if (x->len != y->len) return 0; if (x->len == 0) return 1; return (int8_t)(memcmp(x->ptr, y->ptr, (size_t)x->len) == 0); } uint64_t flan_hash_str(const void *key, uint64_t seed, int64_t size, void *xfer) { (void)xfer; return flan_key_hash_str(key, seed, size); } int8_t flan_eq_str(const void *a, const void *b, int64_t size, void *xfer) { (void)xfer; return flan_key_eq_str(a, b, size); } /* Combining, for a key type the compiler does emit a function for: a struct * with padding (whose padding bytes are indeterminate and must not be hashed) * or one with a string field (whose bytes are elsewhere). The emitted function * hashes each field with the right pair and folds the results through here. */ uint64_t flan_hash_combine(uint64_t acc, uint64_t h) { return flan_mix64(acc ^ (h + 0x9e3779b97f4a7c15ULL + (acc << 6) + (acc >> 2))); } /* ── Cell geometry ──────────────────────────────────────────────────── * * Odin precomputes these into a Map_Cell_Info so the probe loop never divides. * They are derived from the element size alone — alignment cannot matter, * because a cell starts on a 64-byte boundary and no Flan type is aligned * above that — so they are derived once on entry to each operation and kept in * locals, which is the same trade with one less thing for the checker to pass * and get wrong. */ /* 64/size for every size a cell can pack, as a table rather than a division. * * This is Odin's Map_Cell_Info by another route. Odin precomputes * elements_per_cell and size_of_cell into a static per-type record because the * probe loop must not divide; the same number is wanted here and the call site * cannot hand it over, because the sizes reach this runtime as ordinary i64 * arguments rather than as a compile-time record. A 64-entry table is one load * and needs nothing added to the calling convention. * * It is worth the lines: the geometry is recomputed on every lookup, three * times over (keys, values, hashes), and three divisions there measured as a * fifth of the whole operation. */ static const uint8_t flan_epc_table[64] = { 1, 64, 32, 21, 16, 12, 10, 9, 8, 7, 6, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, }; static int64_t flan_cell_epc(int64_t size) { if (size <= 0 || size >= FLAN_MAP_CACHE_LINE) return 1; return (int64_t)flan_epc_table[size]; } /* log2 of [epc] when it is a power of two, and -1 when it is not. * * epc is 64/size clamped to at least 1, so the only powers of two it can ever * be are these seven. A switch over them is a handful of compares the branch * predictor gets right every time; a loop looking for the bit measured *worse* * than the division it was replacing, which is why this is written out. */ static int flan_log2_epc(int64_t epc) { switch (epc) { case 1: return 0; case 2: return 1; case 4: return 2; case 8: return 3; case 16: return 4; case 32: return 5; case 64: return 6; default: return -1; } } /* Rounding to a cache line is a mask, not a division: the generic * flan_align_up divides, and this sits on the lookup path. */ static int64_t flan_map_round(int64_t x) { return (x + (FLAN_MAP_CACHE_LINE - 1)) & ~(int64_t)(FLAN_MAP_CACHE_LINE - 1); } static int64_t flan_cell_size(int64_t size) { return flan_map_round(flan_cell_epc(size) * size); } /* The bytes a [count]-element run of cells occupies, rounded to a cache line * so the next block starts on one too. * * Division-free in the common case, and that matters more here than anywhere * else in this file: flan_map_blocks calls this five times and is itself * called once per lookup, so a division here is five divisions on the hot * path — which measured as the difference between a 39ns lookup and a 12ns * one, far outweighing the per-slot indexing the cell shift covers. */ static int64_t flan_run_bytes(int64_t epc, int64_t cell, int64_t shift, int64_t count) { int64_t cells = (shift >= 0) ? ((count + epc - 1) >> shift) : ((count + epc - 1) / epc); return cells * cell; } static int64_t flan_cells_bytes(int64_t size, int64_t count) { int64_t epc = flan_cell_epc(size); return flan_run_bytes(epc, flan_cell_size(size), flan_log2_epc(epc), count); } /* log2 of [epc] when it is a power of two, and -1 when it is not. * * This is the difference between a probe that costs a shift and one that costs * two 64-bit integer divisions, and it is measurable: with the divisions in * place a cache-resident i64 lookup took 39ns, and without them 12ns. Odin * does not need it because its static path resolves elements_per_cell at * compile time and its dynamic path special-cases 1 and 2; here the number is * always a run-time value, so the compiler cannot turn the division into a * shift and something has to. * * It is a power of two whenever the element size is, which is every primitive, * every pointer, and a struct whose size rounds to one — so the fallback is * the rare path rather than the common one. */ /* Slot [i] of a cell-packed run. [epc], [cell] and [shift] are hoisted by * every caller that walks, which is why they are parameters rather than * recomputed here — recomputing [shift] per slot would cost more than the * division it removes. */ static uint8_t *flan_cell_at(uint8_t *base, int64_t size, int64_t epc, int64_t cell, int64_t shift, int64_t i) { if (epc == 1) return base + i * cell; if (shift >= 0) return base + (i >> shift) * cell + (i & (epc - 1)) * size; return base + (i / epc) * cell + (i % epc) * size; } /* The four blocks. Keys, values and hashes get one run each; the scratch is * two more keys and two more values, which is where the Robin Hood swap keeps * the element in flight. Odin allocates the same two, for the same reason: the * swap is a memcpy between type-erased buffers and there is no local of the * right type to hold one. */ static int64_t flan_map_block_size(int64_t ksize, int64_t vsize, int64_t cap) { return flan_cells_bytes(ksize, cap) + flan_cells_bytes(vsize, cap) + flan_cells_bytes((int64_t)sizeof(flan_map_hash), cap) + flan_cells_bytes(ksize, 2) + flan_cells_bytes(vsize, 2); } /* Everything an operation needs to walk the block, computed once on entry. * * It used to be five calls to flan_cells_bytes, each recomputing the element * geometry it had just been asked for, on a function called once per lookup. * Gathering it into one struct is the single largest win in this file after * the hash: the arithmetic is the same, it simply happens once. */ typedef struct flan_map_geom { int64_t kepc, kcell, vepc, vcell; int64_t kshift, vshift; uint8_t *ks; uint8_t *vs; flan_map_hash *hs; uint8_t *sk; uint8_t *sv; } flan_map_geom; static void flan_map_geometry(const flan_map *m, int64_t ksize, int64_t vsize, int64_t cap, flan_map_geom *g) { uint8_t *p = (uint8_t *)m->data; int64_t hsize = (int64_t)sizeof(flan_map_hash); int64_t hepc = flan_cell_epc(hsize), hcell = flan_cell_size(hsize); int hshift = flan_log2_epc(hepc); g->kepc = flan_cell_epc(ksize); g->kcell = flan_cell_size(ksize); g->vepc = flan_cell_epc(vsize); g->vcell = flan_cell_size(vsize); g->kshift = flan_log2_epc(g->kepc); g->vshift = flan_log2_epc(g->vepc); g->ks = p; p += flan_run_bytes(g->kepc, g->kcell, g->kshift, cap); g->vs = p; p += flan_run_bytes(g->vepc, g->vcell, g->vshift, cap); g->hs = (flan_map_hash *)p; p += flan_run_bytes(hepc, hcell, hshift, cap); g->sk = p; p += flan_run_bytes(g->kepc, g->kcell, g->kshift, 2); g->sv = p; } /* The same epoch check a Vec does, and it runs in every build for the same * reason. A map that never allocated has no allocator and nothing to check. */ static void flan_map_check(flan_map *m, const uint8_t *loc, int64_t loclen) { if (m->alloc) { int64_t now = (int64_t)m->alloc->epoch; if (now != m->epoch) flan_vec_stale_fail(loc, loclen, m->epoch, now); } } static flan_allocator *flan_map_adopt(flan_map *m) { if (!m->alloc) { m->alloc = flan_context_allocator(); m->epoch = (int64_t)m->alloc->epoch; } return m->alloc; } /* The seed, derived from the block address exactly as Odin derives it: two * maps with the same keys then disagree about which slot is which, so an * adversarial insertion order against one is not an insertion order against * the other. It changes on every grow, which is why hashes are recomputed * there rather than carried over. */ static uint64_t flan_map_seed(const flan_map *m) { /* One multiply, not a full avalanche. This is recomputed on every lookup and * all it has to do is decorrelate two maps from each other: whatever it * returns is fed to the hasher, which mixes properly. A splitmix here was * five dependent multiplies on the critical path of every probe, for * mixing that happens again immediately afterwards. * * The block is 64-byte aligned when the allocator honours the request, so * the low six bits carry nothing and are shifted out before multiplying. */ return (((uint64_t)(uintptr_t)m->data >> 6) * 0x9e3779b97f4a7c15ULL); } static int64_t flan_map_cap(const flan_map *m) { return m->data ? ((int64_t)1 << m->log2cap) : 0; } /* 75% of capacity, as fixed-point integer arithmetic. Robin Hood wants a * maximum load factor under 100% and 75% is where Odin sets it. */ static int64_t flan_map_threshold(const flan_map *m) { return (flan_map_cap(m) * FLAN_MAP_LOAD_FACTOR) / 100; } /* How far this element is from the slot its hash wanted. Odin's identity: * (slot - hash) & mask is the same number as (slot + cap - desired) & mask, * with fewer operations, because desired is hash & mask. */ static int64_t flan_map_distance(uint64_t hash, int64_t slot, int64_t mask) { return (int64_t)(((uint64_t)slot - hash) & (uint64_t)mask); } /* Place one element, already hashed, into a map known to have room. This is * Odin's swap_loop and nothing else: with no tombstones there is no second * loop, and the load factor guarantees an empty slot is reached. */ static void flan_map_place(flan_map *m, uint64_t h, const void *ikey, const void *ival, int64_t ksize, int64_t vsize) { flan_map_geom g; int64_t cap = flan_map_cap(m), mask = cap - 1; int64_t pos = (int64_t)(h & (uint64_t)mask), dist = 0; uint8_t *k, *v, *tk, *tv; flan_map_geometry(m, ksize, vsize, cap, &g); /* The element in flight lives in scratch slot 0; slot 1 is the swap * temporary. Both are inside the block, so nothing here touches the stack * with a size only known at run time. */ k = flan_cell_at(g.sk, ksize, g.kepc, g.kcell, g.kshift, 0); v = flan_cell_at(g.sv, vsize, g.vepc, g.vcell, g.vshift, 0); tk = flan_cell_at(g.sk, ksize, g.kepc, g.kcell, g.kshift, 1); tv = flan_cell_at(g.sv, vsize, g.vepc, g.vcell, g.vshift, 1); flan_copy_small(k, ikey, ksize); if (vsize > 0) flan_copy_small(v, ival, vsize); for (;;) { uint64_t eh = g.hs[pos]; if (eh == 0) { flan_copy_small(flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, pos), k, ksize); if (vsize > 0) flan_copy_small(flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, pos), v, vsize); g.hs[pos] = h; return; } /* The Robin Hood swap: the occupant is richer — closer to home — than the * element in flight, so the poorer one takes the slot and the richer one * carries on. This is what keeps the variance down. */ if (dist > flan_map_distance(eh, pos, mask)) { uint8_t *kp = flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, pos); uint8_t *vp = flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, pos); uint64_t th; flan_copy_small(tk, k, ksize); flan_copy_small(k, kp, ksize); flan_copy_small(kp, tk, ksize); if (vsize > 0) { flan_copy_small(tv, v, vsize); flan_copy_small(v, vp, vsize); flan_copy_small(vp, tv, vsize); } th = h; h = g.hs[pos]; g.hs[pos] = th; dist = flan_map_distance(h, pos, mask); } pos = (pos + 1) & mask; dist++; } } /* The slot holding [key], or -1. The middle test is the Robin Hood early exit: * this probe is further from home than the occupant is, and Robin Hood * maintains that no element is ever further from home than one it passed, so * the key cannot be further along. A miss therefore costs about what a hit * does, which is the property the ordering buys. */ static int64_t flan_map_find_g(flan_map *m, const void *key, int64_t ksize, int64_t vsize, flan_hash_fn hash, flan_eq_fn eq, flan_map_geom *gp) { flan_map_geom g; int64_t cap, mask, pos, dist = 0; uint64_t h; void *xfer = NULL; if (!m->data || m->len == 0) return -1; cap = flan_map_cap(m); mask = cap - 1; flan_map_geometry(m, ksize, vsize, cap, &g); if (gp) *gp = g; h = hash(key, flan_map_seed(m), ksize, &xfer) | FLAN_MAP_OCCUPIED; pos = (int64_t)(h & (uint64_t)mask); for (;;) { uint64_t eh = g.hs[pos]; if (eh == 0) return -1; if (dist > flan_map_distance(eh, pos, mask)) return -1; if (eh == h && eq(key, flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, pos), ksize, &xfer)) return pos; pos = (pos + 1) & mask; dist++; } } /* Allocate a block for 2^log2cap slots and zero the hashes. Only the hash run * needs zeroing — a key or value slot is never read without its hash saying it * is live — so the keys and values are left as the allocator returned them. */ static int8_t flan_map_alloc(flan_map *m, flan_allocator *a, int64_t log2cap, int64_t ksize, int64_t vsize) { int64_t cap = (int64_t)1 << log2cap; int64_t bytes = flan_map_block_size(ksize, vsize, cap); void *p; flan_fail_bytes = bytes; flan_fail_align = FLAN_MAP_CACHE_LINE; flan_fail_id = (int64_t)(intptr_t)a; p = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, bytes, FLAN_MAP_CACHE_LINE); if (!p) return 0; m->data = p; m->log2cap = log2cap; m->len = 0; { flan_map_geom g; flan_map_geometry(m, ksize, vsize, cap, &g); memset(g.hs, 0, (size_t)flan_cells_bytes((int64_t)sizeof(flan_map_hash), cap)); } return 1; } /* Double the capacity and reinsert. The seed moves with the block, so every * hash is recomputed rather than carried over — which is what a fresh seed per * block is for. The old block is released only after the last read of it. */ static int8_t flan_map_grow(flan_map *m, int64_t want, int64_t ksize, int64_t vsize, flan_hash_fn hash) { flan_allocator *a = flan_map_adopt(m); flan_map fresh; int64_t log2cap = FLAN_MAP_MIN_LOG2, old_cap = flan_map_cap(m); flan_map_geom g; int64_t i, moved; void *xfer = NULL; /* Smallest power of two whose 75% threshold still holds [want]. */ while ((((int64_t)1 << log2cap) * FLAN_MAP_LOAD_FACTOR) / 100 < want) { if (log2cap >= 40) return 0; log2cap++; } if (log2cap <= m->log2cap && m->data) return 1; fresh.data = NULL; fresh.len = 0; fresh.log2cap = 0; fresh.alloc = a; fresh.gen = 0; fresh.epoch = (int64_t)a->epoch; if (!flan_map_alloc(&fresh, a, log2cap, ksize, vsize)) return 0; if (m->data) { flan_map_geometry(m, ksize, vsize, old_cap, &g); moved = m->len; for (i = 0; i < old_cap && moved > 0; i++) { uint64_t h; if (g.hs[i] == 0) continue; h = hash(flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, i), flan_map_seed(&fresh), ksize, &xfer) | FLAN_MAP_OCCUPIED; flan_map_place(&fresh, h, flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, i), flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, i), ksize, vsize); fresh.len++; moved--; } if (a->caps & FLAN_CAN_FREE) a->proc(a, FLAN_ALLOC_FREE, m->data, flan_map_block_size(ksize, vsize, old_cap), 0, FLAN_MAP_CACHE_LINE); } m->data = fresh.data; m->log2cap = fresh.log2cap; m->len = fresh.len; /* Every key and value moved, so any pointer into the old block is stale — * the same word, bumped for the same reason, as a Vec's reallocation. */ m->gen++; return 1; } int8_t flan_map_init(flan_map *m, flan_allocator *a, int64_t ksize, int64_t vsize, const uint8_t *loc, int64_t loclen) { if (!a) flan_null_alloc_fail(loc, loclen); (void)ksize; (void)vsize; m->data = NULL; m->len = 0; m->log2cap = 0; m->gen = 0; m->alloc = a; m->epoch = (int64_t)a->epoch; /* No block until something is put in it: an empty map that is never written * costs nothing, which is what makes a (defvar m (Map string i32)) free. */ return 1; } /* The upsert. spec-memory.md: it either inserts or replaces, and returns Unit * — there is no Result and no ignorable error code, because a put that put * nothing and said nothing is the outcome the StorageExhausted rule exists to * make impossible. */ int8_t flan_map_put(flan_map *m, const void *key, const void *val, int64_t ksize, int64_t vsize, flan_hash_fn hash, flan_eq_fn eq, const uint8_t *loc, int64_t loclen) { int64_t at; flan_map_geom g; uint64_t h; flan_map_check(m, loc, loclen); at = flan_map_find_g(m, key, ksize, vsize, hash, eq, &g); if (at >= 0) { /* Replace. The key already in the block compares equal to the one handed * in, so it is left alone: overwriting it would be a no-op for every * bytewise key and a question nobody has asked for the others. */ if (vsize > 0) flan_copy_small( flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, at), val, vsize); return 1; } if (!m->data || m->len + 1 > flan_map_threshold(m)) if (!flan_map_grow(m, m->len + 1, ksize, vsize, hash)) return 0; { void *xfer = NULL; h = hash(key, flan_map_seed(m), ksize, &xfer) | FLAN_MAP_OCCUPIED; } flan_map_place(m, h, key, val, ksize, vsize); m->len++; return 1; } /* Lookup. The value is copied out into [out] — the first Map implementation * admits copyable keys and values only, so get returns a copy — and the answer * is 1/0 for found, which the compiler turns into Some/None. */ int8_t flan_map_get(flan_map *m, const void *key, void *out, int64_t ksize, int64_t vsize, flan_hash_fn hash, flan_eq_fn eq, const uint8_t *loc, int64_t loclen) { int64_t at; flan_map_geom g; flan_map_check(m, loc, loclen); /* The geometry the probe already built, rather than a second helping of the * same arithmetic: it was a fifth of the operation, computed twice. */ at = flan_map_find_g(m, key, ksize, vsize, hash, eq, &g); if (at < 0) return 0; if (vsize > 0) flan_copy_small( out, flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, at), vsize); return 1; } int8_t flan_map_has(flan_map *m, const void *key, int64_t ksize, int64_t vsize, flan_hash_fn hash, flan_eq_fn eq, const uint8_t *loc, int64_t loclen) { flan_map_check(m, loc, loclen); return (int8_t)(flan_map_find_g(m, key, ksize, vsize, hash, eq, NULL) >= 0); } int64_t flan_map_len(flan_map *m, const uint8_t *loc, int64_t loclen) { flan_map_check(m, loc, loclen); return m->len; } /* The cursor step, and the whole of iteration. * * Everything else in this file addresses *one* entry: get, put and has each * hash a key and probe. Nothing walked the block, so a map's keys and its * values could not be read out at all, and this is the one function that * changes it. * * The cursor is a slot index the caller owns, and the contract is the one a * slot index gives for free: it starts at 0, it is written back one past the * entry just answered, and a 0 answer leaves it at [cap] so calling again is * still 0. There is no iterator struct because there is nothing for one to * hold — a map has no tombstones (removal is deferred), so no state beyond the * position is needed to know where to resume. * * Invalidated by anything that moves the block, exactly as a Vec's slice is: * a put that grows rehashes into a new block and every index before it means a * different entry. The epoch check below catches a released arena and nothing * catches a resize, which is the same bargain [as-slice] already makes. * * The layout is the one the geometry describes and is worth restating because * it is the thing most likely to be got wrong here: [data] is *one* allocation * laid out keys | values | hashes | scratch, each run cell-packed, so a key is * reached through [flan_cell_at] and never by [ks + i * ksize]. The hashes are * the exception the clone loop already relies on — an 8-byte element packs 8 * to a 64-byte cell with nothing left over, so a flat index is the right * index. Order is block order, which is the hash's order and not the * insertion's; two maps holding the same entries may walk them differently. */ int8_t flan_map_next(flan_map *m, int64_t *cursor, void *kout, void *vout, int64_t ksize, int64_t vsize, const uint8_t *loc, int64_t loclen) { flan_map_geom g; int64_t cap, i; flan_map_check(m, loc, loclen); if (!m->data || m->len == 0) return 0; cap = flan_map_cap(m); i = *cursor; if (i < 0) i = 0; if (i >= cap) { *cursor = cap; return 0; } flan_map_geometry(m, ksize, vsize, cap, &g); for (; i < cap; i++) { if (g.hs[i] == 0) continue; memcpy(kout, flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, i), (size_t)ksize); memcpy(vout, flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, i), (size_t)vsize); *cursor = i + 1; return 1; } *cursor = cap; return 0; } /* Room for [n] entries without reallocating, which means a block whose 75% * threshold is at least n. */ int8_t flan_map_reserve(flan_map *m, int64_t n, int64_t ksize, int64_t vsize, flan_hash_fn hash, const uint8_t *loc, int64_t loclen) { flan_map_check(m, loc, loclen); if (n <= 0) return 1; if (m->data && n <= flan_map_threshold(m)) return 1; return flan_map_grow(m, n, ksize, vsize, hash); } /* spec-memory.md's first release point, and the same rules the Vec's free * follows: left zeroed rather than dangling, and an allocator without can-free * keeps the block because releasing it is free-all's job. */ void flan_map_free(flan_map *m, int64_t ksize, int64_t vsize, const uint8_t *loc, int64_t loclen) { flan_map_check(m, loc, loclen); if (m->data && m->alloc && (m->alloc->caps & FLAN_CAN_FREE)) m->alloc->proc(m->alloc, FLAN_ALLOC_FREE, m->data, flan_map_block_size(ksize, vsize, flan_map_cap(m)), 0, FLAN_MAP_CACHE_LINE); m->data = NULL; m->len = 0; m->log2cap = 0; m->alloc = NULL; m->gen++; m->epoch = 0; } /* A deep, independent copy. It reinserts rather than copying the block: the * seed is derived from the block address, so a bytewise copy would be a map * whose stored hashes disagree with its own seed and whose every lookup * missed. Reinserting is also what makes the copy's layout independent of the * original's insertion history. */ int8_t flan_map_clone(flan_map *dst, flan_map *src, flan_allocator *a, int64_t ksize, int64_t vsize, flan_hash_fn hash, const uint8_t *loc, int64_t loclen) { flan_map_geom g; int64_t cap, i, moved; void *xfer = NULL; flan_map_check(src, loc, loclen); if (!flan_map_init(dst, a, ksize, vsize, loc, loclen)) return 0; if (!src->data || src->len == 0) return 1; if (!flan_map_grow(dst, src->len, ksize, vsize, hash)) return 0; cap = flan_map_cap(src); flan_map_geometry(src, ksize, vsize, cap, &g); moved = src->len; for (i = 0; i < cap && moved > 0; i++) { uint64_t h; if (g.hs[i] == 0) continue; h = hash(flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, i), flan_map_seed(dst), ksize, &xfer) | FLAN_MAP_OCCUPIED; flan_map_place(dst, h, flan_cell_at(g.ks, ksize, g.kepc, g.kcell, g.kshift, i), flan_cell_at(g.vs, vsize, g.vepc, g.vcell, g.vshift, i), ksize, vsize); dst->len++; moved--; } return 1; } /* ── The filesystem, and the whole of what it adds to the host ABI ─── * * plan.org names the filesystem as the #1 portability risk — "pack assets, one * abstraction, never touch paths" — so the widening here is deliberately three * calls and one reader, and the reason each exists is written down: * * flan_file_size(path, n, &out) how many bytes are there * flan_file_read(path, n, buf, cap, &got) fill a buffer the caller owns * flan_file_write(path, n, buf, len) write a whole file * flan_file_fail_reason() which of the four reasons it was * * They are POSIX-shaped and know nothing about a Vec: no file handle crosses * the boundary, no descriptor is held between calls, and every one takes a * path and returns 1/0 the way every allocator entry point already does. The * Vec-aware part is flan_slurp below, which is *runtime glue* on this side of * the ABI rather than a fourth host call — so a second target implements three * functions and inherits the rest. * * These do touch paths, which is the widening plan.org warned about and which * decision 2 took knowingly. `embed` is the answer that does not: an asset * baked in at compile time needs none of this and works identically on both * targets. Reach for slurp when the bytes genuinely are not known until the * program runs. * * The reason is a global rather than an out-parameter for the same reason * flan_alloc_fail_bytes is: the condition the compiler builds at the failing * site is a value struct with fixed numeric fields and no rendered message, * and reading one word is the cheapest way to carry the number out. */ #include #define FLAN_FILE_OK 0 #define FLAN_FILE_MISSING 1 #define FLAN_FILE_DENIED 2 #define FLAN_FILE_IO 3 /* Decision 2: writing is desktop-only and *signals* on web. Not a build-time * refusal, because Flan has no conditional compilation and "isolate this to * desktop" is therefore not expressible in source; and not a silent no-op, * because that is how a save file disappears with nothing said. The program * gets a condition and decides. This is the language having something Odin * does not — Odin's core/os/file_js.odin stubs the whole API to .Unsupported * so that importing core:os "panics cleanly". */ #define FLAN_FILE_UNSUPPORTED 4 static int64_t flan_file_fail = FLAN_FILE_OK; int64_t flan_file_fail_reason(void) { return flan_file_fail; } /* A Flan string is ptr+len and never NUL-terminated, so every entry point here * makes a terminated copy on its own stack. PATH_MAX is not consulted: a path * too long for this buffer is reported as missing rather than truncated and * silently opened, which is the failure this exists to avoid. */ #define FLAN_PATH_MAX 4096 static int flan_path_cstr(const uint8_t *p, int64_t n, char *out) { if (n < 0 || n >= FLAN_PATH_MAX) return 0; if (n > 0) memcpy(out, p, (size_t)n); out[n] = '\0'; /* An embedded NUL would make the C string shorter than the Flan one, so the * file opened would not be the file named. Refuse rather than guess. */ if ((int64_t)strlen(out) != n) return 0; return 1; } static int64_t flan_errno_reason(void) { switch (errno) { case ENOENT: case ENOTDIR: return FLAN_FILE_MISSING; case EACCES: case EPERM: return FLAN_FILE_DENIED; default: return FLAN_FILE_IO; } } int8_t flan_file_size(const uint8_t *path, int64_t n, int64_t *out) { char buf[FLAN_PATH_MAX]; FILE *f; long end; *out = 0; if (!flan_path_cstr(path, n, buf)) { flan_file_fail = FLAN_FILE_MISSING; return 0; } errno = 0; f = fopen(buf, "rb"); if (!f) { flan_file_fail = flan_errno_reason(); return 0; } if (fseek(f, 0, SEEK_END) != 0 || (end = ftell(f)) < 0) { fclose(f); flan_file_fail = FLAN_FILE_IO; return 0; } fclose(f); *out = (int64_t)end; flan_file_fail = FLAN_FILE_OK; return 1; } /* Reads at most cap bytes and reports how many it got. The file may have * changed size since flan_file_size looked, so the count is an output and not * an assertion: a short read is a successful read of a shorter file, and a * longer file is truncated to the buffer the caller already allocated. */ int8_t flan_file_read(const uint8_t *path, int64_t n, void *dst, int64_t cap, int64_t *got) { char buf[FLAN_PATH_MAX]; FILE *f; size_t r; *got = 0; if (!flan_path_cstr(path, n, buf)) { flan_file_fail = FLAN_FILE_MISSING; return 0; } errno = 0; f = fopen(buf, "rb"); if (!f) { flan_file_fail = flan_errno_reason(); return 0; } r = cap > 0 ? fread(dst, 1, (size_t)cap, f) : 0; if (ferror(f)) { fclose(f); flan_file_fail = FLAN_FILE_IO; return 0; } fclose(f); *got = (int64_t)r; flan_file_fail = FLAN_FILE_OK; return 1; } int8_t flan_file_write(const uint8_t *path, int64_t n, const void *src, int64_t len) { #if defined(__EMSCRIPTEN__) /* The browser has no filesystem to write to that outlives the page, and * MEMFS would be the silent no-op decision 2 rules out by name. So the * answer is the condition, every time, with the path still in it so a * handler can say which write was refused. */ (void)path; (void)n; (void)src; (void)len; flan_file_fail = FLAN_FILE_UNSUPPORTED; return 0; #else char buf[FLAN_PATH_MAX]; FILE *f; size_t w; if (!flan_path_cstr(path, n, buf)) { flan_file_fail = FLAN_FILE_MISSING; return 0; } errno = 0; f = fopen(buf, "wb"); if (!f) { flan_file_fail = flan_errno_reason(); return 0; } w = len > 0 ? fwrite(src, 1, (size_t)len, f) : 0; if (w != (size_t)(len > 0 ? len : 0) || fclose(f) != 0) { flan_file_fail = FLAN_FILE_IO; return 0; } flan_file_fail = FLAN_FILE_OK; return 1; #endif } /* Runtime glue, not host ABI: the Vec-aware half of slurp, kept on this side * of the boundary so the three calls above stay POSIX-shaped and a second * target implements only them. * * It fills a Vec the *compiler* already initialised to the right capacity — * which is what keeps spec-memory.md's rule intact: the allocation went * through flan_vec_init under the compiler's alloc_guard, so a failure to * allocate is StorageExhausted with retry, and a failure to read is FileError * with retry and use-value. Two failures, two conditions, neither swallowing * the other. */ int8_t flan_slurp_into(flan_vec *v, const uint8_t *path, int64_t n) { int64_t got = 0; if (!flan_file_read(path, n, v->ptr, v->cap, &got)) return 0; v->len = got; return 1; }