A pool slot that remembers how many times it has been reused
(Handle T) and (Pool T) land as types and as a runtime. A handle is one int64_t — slot index low, generation high — so it copies, zeroes and compares like the integer it is and owns nothing. A live slot's generation is odd, which makes a zeroed handle resolve to nothing rather than to slot zero, and makes iteration free. Wrapping retires the slot rather than reissuing it: 2^31 reuses is rare, and rare is not an answer when the failure is the silent wrong one the type exists to prevent. No surface yet — the checker still has no names for any of it.
This commit is contained in:
parent
54027ca942
commit
8f429bcd5d
29
lib/check.ml
29
lib/check.ml
@ -429,7 +429,23 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
|
||||
map_type loc (resolve env ~seen k) (resolve env ~seen v)
|
||||
| "Map", _ -> fail loc "(Map K V) takes exactly two types"
|
||||
| "Result", _ -> unimplemented loc "(Result T E)" 6
|
||||
| "Handle", _ -> unimplemented loc "(Handle T)" 6
|
||||
| "Pool", [ a ] ->
|
||||
let e = resolve env ~seen a in
|
||||
(* The same refusal (Vec T) makes, for the same reason: the pool's
|
||||
runtime is type-erased and copies and releases slots bytewise, so a
|
||||
release would drop what an owning element owns. Recursive teardown
|
||||
arrives with drop. *)
|
||||
if Types.is_move_only e then
|
||||
fail loc
|
||||
"(Pool %s) holds a move-only element, and the type-erased runtime \
|
||||
copies and releases slots bytewise — so releasing a slot would \
|
||||
leak what it owns. Recursive teardown arrives with drop (step 5 \
|
||||
in NEXT.md)"
|
||||
(Types.to_string e);
|
||||
Types.Pool e
|
||||
| "Pool", _ -> fail loc "(Pool T) takes exactly one type"
|
||||
| "Handle", [ a ] -> Types.Handle (resolve env ~seen a)
|
||||
| "Handle", _ -> fail loc "(Handle T) takes exactly one type"
|
||||
| _ ->
|
||||
fail loc
|
||||
"%s takes no type arguments — generics are milestone 5" name)
|
||||
@ -2507,7 +2523,16 @@ and named_call ctx ~want loc name args =
|
||||
in
|
||||
arity loc name 2 args;
|
||||
let a, b = binary ctx name loc ~want:None args in
|
||||
if not (Types.is_comparable a.Tast.ty) then
|
||||
(* [=] and [!=] admit one type [<] does not: a handle, which is a pair of
|
||||
numbers in one word and where "the same entity" is the question the
|
||||
type exists to answer. Ordering handles would order a slot index, which
|
||||
is a free-list artefact and means nothing. *)
|
||||
let ok =
|
||||
match name with
|
||||
| "=" | "!=" -> Types.is_equatable a.Tast.ty
|
||||
| _ -> Types.is_comparable a.Tast.ty
|
||||
in
|
||||
if not ok then
|
||||
fail loc
|
||||
"%s compares machine numbers; %s has no built-in comparison \
|
||||
(plan.org, Types)" name (Types.to_string a.Tast.ty);
|
||||
|
||||
35
lib/emit.ml
35
lib/emit.ml
@ -113,6 +113,15 @@ let rec ll (t : Types.t) =
|
||||
map's address — so the shape exists only so that a slot, a struct field
|
||||
and a copy in the IR are the right number of bytes. *)
|
||||
| Types.Map _ -> "%map"
|
||||
(* items + slots + len + cap + live + free + allocator + epoch. Nothing here
|
||||
reads a field of one either — every operation is a runtime call taking
|
||||
the pool's address. *)
|
||||
| Types.Pool _ -> "%pool"
|
||||
(* A handle is one 64-bit number: the slot index in the low half and that
|
||||
slot's generation in the high half. Packed rather than a two-field struct
|
||||
so that copying, zeroing and [=] are what they are for an integer, with no
|
||||
backend arm anywhere except the one comparison below. *)
|
||||
| Types.Handle _ -> "i64"
|
||||
| Types.Option e -> Printf.sprintf "{ i8, %s }" (ll e)
|
||||
| Types.Var _ ->
|
||||
(* The checker rejects it by name — nothing reaches here. *)
|
||||
@ -267,6 +276,8 @@ let rec lay m (t : Types.t) : int * int =
|
||||
| Types.Alloc -> 8, 8
|
||||
| Types.Fn _ -> 8, 8
|
||||
| Types.Vec _ | Types.Map _ -> 48, 8
|
||||
| Types.Pool _ -> 64, 8
|
||||
| Types.Handle _ -> 8, 8
|
||||
(* [n x T] adds no padding of its own: T's size already carries its tail. *)
|
||||
| Types.Array (n, e) -> let s, a = lay m e in Int64.to_int n * s, a
|
||||
| Types.Option e -> let s, a, _ = lay_fields m [ Types.Int Types.I8; e ] in s, a
|
||||
@ -460,6 +471,19 @@ let rec dty m d (t : Types.t) : int =
|
||||
("allocator", Types.Alloc); ("gen", Types.Int Types.I64);
|
||||
("epoch", Types.Int Types.I64) ]
|
||||
|> fun n -> ignore k; ignore v; n
|
||||
(* Eight fields, shown as eight, for the reason the two above are. *)
|
||||
| Types.Pool e ->
|
||||
composite (Types.to_string t)
|
||||
[ ("items", Types.Ptr e);
|
||||
("slots", Types.Ptr (Types.Int Types.U8));
|
||||
("len", Types.Int Types.I64); ("cap", Types.Int Types.I64);
|
||||
("live", Types.Int Types.I64); ("free", Types.Int Types.I64);
|
||||
("allocator", Types.Alloc); ("epoch", Types.Int Types.I64) ]
|
||||
(* An i64 under lldb, which is what it is. Splitting it into a two-field
|
||||
composite would be describing a struct that is not there: the packing
|
||||
is the runtime's, and [p h] answering with the number is honest. *)
|
||||
| Types.Handle _ ->
|
||||
basic (Types.to_string t) 64 "DW_ATE_unsigned"
|
||||
(* A pointer to code, and lldb is told exactly that and no more. DWARF
|
||||
has DW_TAG_subroutine_type for the signature behind it, and spelling
|
||||
one out here would buy a reader nothing they cannot get from the
|
||||
@ -1684,6 +1708,11 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
location. Signed, because a member may be declared negative. *)
|
||||
| Types.Enum _ ->
|
||||
ins f "%s = icmp %s %s %s, %s" t (icmp_op true p) (ll x.Tast.ty) a b
|
||||
(* [Types.is_equatable] admits a handle and [is_comparable] does not, so
|
||||
only [Eq]/[Ne] arrive here — one unsigned integer compare over the
|
||||
packed (index, generation) pair. *)
|
||||
| Types.Handle _ ->
|
||||
ins f "%s = icmp %s i64 %s, %s" t (icmp_op false p) a b
|
||||
| t' -> failwith ("comparison on " ^ Types.to_string t'));
|
||||
t
|
||||
| (Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr), [ x; y ] ->
|
||||
@ -1822,7 +1851,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
lets an operation mutate the caller's container in place.
|
||||
Passing the header by value here would hand the runtime a
|
||||
copy to grow and leave the caller's untouched. *)
|
||||
| Types.Vec _ | Types.Map _ -> [ "ptr " ^ addr f a ]
|
||||
| Types.Vec _ | Types.Map _ | Types.Pool _ -> [ "ptr " ^ addr f a ]
|
||||
| t -> [ ll t ^ " " ^ value f a ])
|
||||
args)
|
||||
in
|
||||
@ -2242,6 +2271,10 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
|
||||
; nor value type appears in it, for the same reason: one type-erased runtime,
|
||||
; handed the two sizes and a hash/equality pair at each call site.
|
||||
%map = type { ptr, i64, i64, ptr, i64, i64 }
|
||||
; (Pool T) — slab storage handed out behind (Handle T). Type-erased in exactly
|
||||
; the same way; the element type is nowhere in it. items and slots are grown
|
||||
; together and share one cap, so a slot index is an index into both.
|
||||
%pool = type { ptr, ptr, i64, i64, i64, i64, ptr, i64 }
|
||||
; A handler frame: the one it displaced, the condition type it matches, and
|
||||
; the lifted function that runs. Allocated on the establishing frame's stack.
|
||||
%handler = type { ptr, i32, ptr }
|
||||
|
||||
42
lib/types.ml
42
lib/types.ml
@ -44,6 +44,25 @@ type t =
|
||||
so this is a container without generics — the concrete type is known only
|
||||
at the call site, which is exactly where the two numbers are produced. *)
|
||||
| Vec of t
|
||||
(* [(Pool T)]: slab storage handed out behind [(Handle T)]. Owning and
|
||||
move-only exactly as a [Vec] is, and built on the same type-erased
|
||||
runtime over (size, align). It is not a second [Vec]: a [Vec]'s indices
|
||||
shift when something is removed and a [Pool]'s slot index never moves,
|
||||
which is the whole reason a handle into one stays meaningful. *)
|
||||
| Pool of t
|
||||
(* [(Handle T)]: a reference to something that can die, which reports that
|
||||
it died rather than silently resolving to whatever reused its slot
|
||||
(spec-memory.md, "Borrowing" — "Cross-referencing long-lived objects uses
|
||||
(Handle a) into a pool, never a raw pointer or slice. A stale handle is
|
||||
detectable").
|
||||
|
||||
It is a plain 64-bit number — a slot index in the low 32 bits and that
|
||||
slot's generation counter in the high 32 — so it copies, compares and
|
||||
zeroes like an integer and owns nothing. A zeroed handle is generation 0,
|
||||
and a live slot's generation is always odd, so [Zero] of a handle is a
|
||||
handle that resolves to nothing rather than one that resolves to slot 0.
|
||||
See runtime/flan_rt.c's pool section for the packing. *)
|
||||
| Handle of t
|
||||
| Option of t (* (Option T) *)
|
||||
| Fn of t list * t (* (Fn [T ...] R) *)
|
||||
| Var of string (* a type variable — milestone 5 *)
|
||||
@ -94,6 +113,8 @@ let rec equal a b =
|
||||
| Ptr x, Ptr y -> equal x y
|
||||
| Alloc, Alloc -> true
|
||||
| Vec x, Vec y -> equal x y
|
||||
| Pool x, Pool y -> equal x y
|
||||
| Handle x, Handle y -> equal x y
|
||||
| Option x, Option y -> equal x y
|
||||
| Fn (ps, r), Fn (ps', r') ->
|
||||
List.length ps = List.length ps'
|
||||
@ -116,6 +137,8 @@ let rec to_string = function
|
||||
| Ptr t -> "(Ptr " ^ to_string t ^ ")"
|
||||
| Alloc -> "Allocator"
|
||||
| Vec t -> "(Vec " ^ to_string t ^ ")"
|
||||
| Pool t -> "(Pool " ^ to_string t ^ ")"
|
||||
| Handle t -> "(Handle " ^ to_string t ^ ")"
|
||||
| Option t -> "(Option " ^ to_string t ^ ")"
|
||||
| Fn (ps, r) ->
|
||||
Printf.sprintf "(Fn [%s] %s)"
|
||||
@ -130,7 +153,10 @@ let is_numeric = function Int _ | Float _ -> true | _ -> false
|
||||
[free] needs no analysis of its own. A struct that owns one is move-only
|
||||
too; that arrives with [drop], which is the step after this one. *)
|
||||
let rec is_move_only = function
|
||||
| Vec _ | Map _ -> true
|
||||
(* A [Pool] owns its storage; a [Handle] into one owns nothing, which is the
|
||||
point of it — handles are copied freely, and the pool is the single
|
||||
owner that [free] applies to. *)
|
||||
| Vec _ | Map _ | Pool _ -> true
|
||||
| Option t -> is_move_only t
|
||||
| Array (_, t) -> is_move_only t
|
||||
| _ -> false
|
||||
@ -153,6 +179,11 @@ let rec keyable = function
|
||||
| Float _ -> false (* NaN /= NaN, and 0.0 and -0.0 differ bytewise *)
|
||||
| Array (_, t) -> keyable t
|
||||
| Named _ -> true (* [Check] decides, by walking the fields *)
|
||||
(* A [Handle] is not a map key, for the reason a [Ptr] is not: hashing an
|
||||
identity is a different operation from hashing what it names, and a
|
||||
handle whose slot has been reused hashes the same as it always did while
|
||||
naming nothing. The type exists to make that difference visible, so
|
||||
burying it under a key is the one thing it must not do. *)
|
||||
| _ -> false
|
||||
|
||||
(* Ordering and equality are defined on machine types and on nothing else at
|
||||
@ -160,6 +191,15 @@ let rec keyable = function
|
||||
unconstrained type supports only what every type supports (plan.org, Types). *)
|
||||
let is_comparable = function Enum _ -> true | t -> is_numeric t
|
||||
|
||||
(* [=] and [!=] admit one more type than [<] does. A [Handle] is a pair of
|
||||
numbers in a 64-bit word, so "is this the same entity" is one integer
|
||||
compare and is worth having — two handles are equal exactly when they name
|
||||
the same slot at the same generation, so a stale handle is never equal to
|
||||
the live one that replaced it. Ordering handles would compare a slot index,
|
||||
which means nothing: allocation order is a free-list artefact. Hence two
|
||||
predicates rather than one. *)
|
||||
let is_equatable = function Handle _ -> true | t -> is_comparable t
|
||||
|
||||
(* [Never] is the type of an expression that does not produce a value: return,
|
||||
an early-returning `some`, exit. It fits anywhere, and that is the only
|
||||
place anything resembling subtyping exists. *)
|
||||
|
||||
@ -1007,6 +1007,265 @@ int8_t flan_vec_clone(flan_vec *dst, flan_vec *src, flan_allocator *a,
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user