kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 8e9dc6c4c12726f18150535ec45a800b6659ee67
parent b073810bd67ba34a5f4f213fa3899d6df6f03429
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 21:56:27 -0700

perf(types): memoize ABI facts + record layouts; share one hashmap facility

Two per-TU O(1) memos collapse hot repeated type queries:
- ABI info (size/align/scalar-kind/signedness) per Type* in c_abi_type_info, the
  chokepoint behind every sizeof/alignof/scalar probe: body-size 2098->1971ms
  (-6%); a 1M-statement TU issued 7M lowering round-trips, now ~all cache hits.
- Completed record layout ids keyed by tag id, replacing a per-lookup list scan
  that was O(records^2): a 8000-distinct-struct TU 904ms->41ms (22x).

Consistency cleanup (per review): route everything through the shared hashmap
facility instead of bespoke open-addressed tables. Add KIT_HASHSET_DEFINE — an
open-addressed set with CUSTOM hash+eq — to kit/support/hashmap.h for the
structural type intern (whose identity is structural, not a scalar key, so the
existing scalar key->value KIT_HASHMAP_DEFINE can't express it). The ABI and
record memos are plain KIT_HASHMAP_DEFINE (Type* and tag-id keys). Give Pool a
single shared arena-heap facade (Pool.arena_heap) so all per-pool maps get arena
lifetime with no teardown; migrate the parser's scope/tag/extern indexes off
their duplicate local facade onto it.

A Pool is 1:1 with a compiler, so the former defensive (compiler, ...) keys are
dropped. Object output byte-identical across the toy/parse/derived/record
corpora; type-decl/body-size/recmemo wins all preserved. Verified: test-cg-api/
toy/parse/pp/libc/smoke-x64 all green.

Diffstat:
Minclude/kit/support/hashmap.h | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlang/c/abi/c_abi.c | 44++++++++++++++++++++++++++++++++++++++++++--
Mlang/c/parse/parse.c | 36++++--------------------------------
Mlang/c/parse/parse_priv.h | 9++++-----
Mlang/c/type/type.c | 249++++++++++++++++++++++++++++---------------------------------------------------
Mlang/cpp/cpp_support.h | 31++++++++++++++++++++++++++++++-
6 files changed, 253 insertions(+), 200 deletions(-)

diff --git a/include/kit/support/hashmap.h b/include/kit/support/hashmap.h @@ -175,4 +175,88 @@ static inline uint32_t kit_hash_u64(uint64_t x) { } \ struct NAME +/* Open-addressed hash SET over a pointer/scalar element type, with CUSTOM hash + * and equality. Where KIT_HASHMAP_DEFINE is a scalar key -> value map with + * built-in `==` equality, this is for interning / hash-consing: the stored + * element IS the canonical result, and membership is decided by a structural + * predicate (e.g. two independently built `int*` Types compare equal). A + * zero/NULL element marks an empty slot, so ET must be a pointer (or a scalar + * whose 0 value is never a real member). HASH_FN(ET) and EQ_FN(ET a, ET b) are + * function (or function-like macro) names. Lookup hashes/compares a probe of + * the same type ET; on a miss the caller allocates the canonical element and + * NAME##_add()s it. Heap-backed like KIT_HASHMAP_DEFINE (pair with an + * arena-heap facade for arena lifetime). */ +#define KIT_HASHSET_DEFINE(NAME, ET, HASH_FN, EQ_FN) \ + typedef struct NAME { \ + KitHeap* heap; \ + ET* slots; \ + uint32_t cap; \ + uint32_t used; \ + } NAME; \ + \ + KIT_HASHMAP_UNUSED static void NAME##_resize(NAME* s, uint32_t new_cap) { \ + ET* fresh; \ + uint32_t i, mask; \ + fresh = (ET*)s->heap->alloc(s->heap, sizeof(ET) * new_cap, _Alignof(ET)); \ + if (!fresh) return; \ + memset(fresh, 0, sizeof(ET) * new_cap); \ + mask = new_cap - 1u; \ + for (i = 0; i < s->cap; ++i) { \ + ET e = s->slots[i]; \ + uint32_t j; \ + if (!(e)) continue; \ + j = HASH_FN(e) & mask; \ + while (fresh[j]) j = (j + 1u) & mask; \ + fresh[j] = e; \ + } \ + if (s->slots) s->heap->free(s->heap, s->slots, sizeof(ET) * s->cap); \ + s->slots = fresh; \ + s->cap = new_cap; \ + } \ + \ + KIT_HASHMAP_UNUSED static inline void NAME##_init_cap(NAME* s, KitHeap* h, \ + uint32_t cap) { \ + s->heap = h; \ + s->slots = NULL; \ + s->cap = 0; \ + s->used = 0; \ + if (cap) NAME##_resize(s, cap); \ + } \ + \ + KIT_HASHMAP_UNUSED static inline void NAME##_init(NAME* s, KitHeap* h) { \ + NAME##_init_cap(s, h, KIT_HASHMAP_INIT_CAP); \ + } \ + \ + KIT_HASHMAP_UNUSED static inline void NAME##_fini(NAME* s) { \ + if (s->slots) s->heap->free(s->heap, s->slots, sizeof(ET) * s->cap); \ + s->slots = NULL; \ + s->cap = s->used = 0; \ + } \ + \ + KIT_HASHMAP_UNUSED static inline ET NAME##_find(const NAME* s, ET probe) { \ + uint32_t mask, j; \ + if (s->cap == 0) return (ET)0; \ + mask = s->cap - 1u; \ + j = HASH_FN(probe) & mask; \ + while (s->slots[j]) { \ + if (EQ_FN(s->slots[j], probe)) return s->slots[j]; \ + j = (j + 1u) & mask; \ + } \ + return (ET)0; \ + } \ + \ + KIT_HASHMAP_UNUSED static inline void NAME##_add(NAME* s, ET e) { \ + uint32_t mask, j; \ + if (s->cap == 0 || \ + s->used * KIT_HASHMAP_LOAD_DEN >= s->cap * KIT_HASHMAP_LOAD_NUM) \ + NAME##_resize(s, s->cap ? s->cap * 2u : KIT_HASHMAP_INIT_CAP); \ + if (s->cap == 0) return; \ + mask = s->cap - 1u; \ + j = HASH_FN(e) & mask; \ + while (s->slots[j]) j = (j + 1u) & mask; \ + s->slots[j] = e; \ + s->used++; \ + } \ + struct NAME + #endif diff --git a/lang/c/abi/c_abi.c b/lang/c/abi/c_abi.c @@ -2,10 +2,49 @@ #include <string.h> +/* Type -> ABITypeInfo memo. ABI facts (size/align/scalar kind/signedness) are a + * pure function of the C type + the fixed target, so once computed they never + * change — except a struct/union still incomplete, which we therefore never + * cache. The Pool is 1:1 with a compiler, so a plain Type* key suffices. + * Arena-backed (the pool arena-heap), in the pool's abi_cache slot, reclaimed + * wholesale at teardown. Collapses the per-query type_cg lowering + cg-API + * round-trips — a hot path: every sizeof/alignof/scalar probe — to one lookup. */ +static int abi_info_cacheable(const Type* t) { + /* Incomplete records have no stable layout yet; everything else (scalars, + * pointers, arrays, functions, enums, complete records) is fixed. */ + return !((t->kind == TY_STRUCT || t->kind == TY_UNION) && t->rec.incomplete); +} + +static inline u32 abi_type_ptr_hash(const Type* t) { + return kit_hash_u64((uint64_t)(uintptr_t)t); +} + +KIT_HASHMAP_DEFINE(AbiInfoMap, const Type*, ABITypeInfo, abi_type_ptr_hash); + +static AbiInfoMap* abi_cache_get(Pool* p) { + AbiInfoMap* m = (AbiInfoMap*)p->abi_cache; + if (m) return m; + m = arena_new(p->arena, AbiInfoMap); + if (!m) return NULL; + AbiInfoMap_init_cap(m, &p->arena_heap, 0); /* lazy: first insert allocates */ + p->abi_cache = m; + return m; +} + ABITypeInfo c_abi_type_info(KitCompiler* a, Pool* p, const Type* t) { - KitCgTypeId id = type_cg_id_in_pool(a, p, t); - KitCgTypeKind kind = kit_cg_type_kind(a, id); + AbiInfoMap* cache = NULL; + KitCgTypeId id; + KitCgTypeKind kind; ABITypeInfo r; + if (abi_info_cacheable(t)) { + cache = abi_cache_get(p); + if (cache) { + ABITypeInfo* hit = AbiInfoMap_get(cache, t); + if (hit) return *hit; + } + } + id = type_cg_id_in_pool(a, p, t); + kind = kit_cg_type_kind(a, id); memset(&r, 0, sizeof(r)); r.size = (u32)kit_cg_type_size(a, id); r.align = kit_cg_type_align(a, id); @@ -31,6 +70,7 @@ ABITypeInfo c_abi_type_info(KitCompiler* a, Pool* p, const Type* t) { break; } r.signed_ = type_is_signed_integer(t); + if (cache) AbiInfoMap_set(cache, t, r); return r; } diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -293,33 +293,6 @@ u32 count_recorded_top_level_items(const Tok* vec, u32 len) { * O(1) per lookup. */ #define SCOPE_INDEX_THRESHOLD 12u -/* KitHeap facade over the pool's arena: the scope/tag/external indexes allocate - * through this so all their storage lives in the per-TU arena and is reclaimed - * in bulk when the pool is freed. The file scope is never popped, so this also - * removes any per-scope teardown bookkeeping. `free` is a no-op (the arena owns - * the memory); resize orphans the old table in the arena, which is bounded by - * the final table size and negligible against the rest of the TU's arena use. - */ -static void* c_arena_heap_alloc(KitHeap* h, size_t n, size_t align) { - return kit_arena_alloc((KitArena*)h->user, n, align); -} -static void* c_arena_heap_realloc(KitHeap* h, void* old, size_t old_n, - size_t new_n, size_t align) { - void* q = kit_arena_alloc((KitArena*)h->user, new_n, align); - if (q && old && old_n) memcpy(q, old, old_n < new_n ? old_n : new_n); - return q; -} -static void c_arena_heap_free(KitHeap* h, void* p, size_t n) { - (void)h; - (void)p; - (void)n; -} -static void c_arena_heap_init(Parser* p) { - p->arena_heap.alloc = c_arena_heap_alloc; - p->arena_heap.realloc = c_arena_heap_realloc; - p->arena_heap.free = c_arena_heap_free; - p->arena_heap.user = p->pool->arena; -} Scope* scope_new(Parser* p, Scope* parent) { Scope* s = arena_new(p->pool->arena, Scope); @@ -344,7 +317,7 @@ void scope_pop(Parser* p) { * current binding a linear scan would return — wins on shadowing. */ static void scope_entries_index_build(Parser* p, Scope* s) { SymEntry* e; - SymEntryMap_init_cap(&s->emap, &p->arena_heap, 64u); + SymEntryMap_init_cap(&s->emap, &p->pool->arena_heap, 64u); for (e = s->entries; e; e = e->next) if (e->name) (void)SymEntryMap_try_insert(&s->emap, e->name, e, NULL); } @@ -438,7 +411,7 @@ static void reject_same_scope_redefinition(Parser* p, Sym name, static void tag_index_build(Parser* p, Scope* s) { TagEntry* e; - TagEntryMap_init_cap(&s->tmap, &p->arena_heap, 64u); + TagEntryMap_init_cap(&s->tmap, &p->pool->arena_heap, 64u); for (e = s->tags; e; e = e->next) if (e->name) (void)TagEntryMap_try_insert(&s->tmap, e->name, e, NULL); } @@ -1612,9 +1585,8 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, p.auto_var_init = (u8)auto_var_init; /* Index storage for the scope/tag tables and the external-function table all - * comes from the arena via this facade; the maps need it before first use. */ - c_arena_heap_init(&p); - ExternalFuncMap_init(&p.external_funcs, &p.arena_heap); + * comes from the arena via the pool's shared arena-heap facade. */ + ExternalFuncMap_init(&p.external_funcs, &p.pool->arena_heap); for (i = (CKw)1; i < KW_COUNT; ++i) { p.kw_sym[i] = kit_sym_intern(p.pool->c, kit_slice_cstr(kw_names[i])); diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -159,8 +159,8 @@ struct TagEntry { * SCOPE_INDEX_THRESHOLD, so the O(n^2) "scan the whole scope per declaration" * cost (file scopes with thousands of globals/typedefs, functions with * thousands of locals) collapses to O(1) average lookups while tiny block - * scopes stay on the cheap linear list. The maps allocate through the parser's - * arena-heap facade (see parse.c) so there is nothing to free. */ + * scopes stay on the cheap linear list. The maps allocate through the pool's + * shared arena-heap facade (Pool.arena_heap) so there is nothing to free. */ KIT_HASHMAP_DEFINE(SymEntryMap, Sym, SymEntry*, kit_hash_u32); KIT_HASHMAP_DEFINE(TagEntryMap, Sym, TagEntry*, kit_hash_u32); KIT_HASHMAP_DEFINE(ExternalFuncMap, Sym, SymEntry*, kit_hash_u32); @@ -319,10 +319,9 @@ typedef struct Parser { Scope* scope; /* name -> current file-scope function entry. Replaces what was an O(n) linear - * list walked on every function declaration/reference. */ + * list walked on every function declaration/reference. Storage comes from the + * pool's shared arena-heap facade (Pool.arena_heap). */ ExternalFuncMap external_funcs; - /* KitHeap facade over pool->arena backing the scope/tag/external indexes. */ - KitHeap arena_heap; CGLabel cur_break; CGLabel cur_continue; diff --git a/lang/c/type/type.c b/lang/c/type/type.c @@ -29,71 +29,6 @@ struct TypeListNode { Type ty; }; -typedef struct CgRecordMemo CgRecordMemo; -struct CgRecordMemo { - CgRecordMemo* next; - const KitCompiler* compiler; - const Type* type; - KitCgTypeId id; -}; - -/* Open-addressed structural intern table for derived types whose identity is - * STRUCTURAL — pointer / array / function, and qualified variants of any - * non-tagged type. Slots hold the canonical Type*; lookup hashes and compares - * only the kind-specific identity fields (never a raw memcmp), so structural - * nodes need no zeroing and their padding is irrelevant. Tagged types - * (struct/union/enum) are interned by declaration identity instead and live on - * `derived`. Replaces the former O(n) linear scan of the whole derived list — - * that scan was O(types * derivations), catastrophic on type-population-heavy - * TUs. The table backing array is arena-allocated; old arrays leak into the - * arena and are reclaimed wholesale at pool teardown (no fini hook). */ -typedef struct TypeInternTable { - const Type** slots; /* cap entries, power-of-two; NULL == empty */ - u32 cap; - u32 used; -} TypeInternTable; - -typedef struct PoolTypeCache { - /* Direct slots for void + primitive kinds (TY_VOID..TY_LDOUBLE). */ - const Type* prim[NUM_PRIM_KINDS]; - /* Structurally-interned ptr/array/func + qualified non-tagged types. */ - TypeInternTable structural; - /* Identity-interned tagged types (struct/union/enum, qualified or not) and - * the throwaway record-builder results: a list, scanned only by the (rare) - * tag-based type_qualified / type_unqual paths and never per derivation. */ - TypeListNode* derived; - /* Completed record layout ids are compiler-local. */ - CgRecordMemo* cg_record_memos; - /* Tag id allocator (1-based; TAG_NONE = 0). */ - u32 next_tag; -} PoolTypeCache; - -static PoolTypeCache* cache_get(Pool* p) { - PoolTypeCache* c = (PoolTypeCache*)p->type_cache; - if (c) return c; - c = arena_new(p->arena, PoolTypeCache); - if (!c) return NULL; - memset(c, 0, sizeof *c); - c->next_tag = 1; - p->type_cache = c; - return c; -} - -static Type* alloc_type_node(Pool* p, PoolTypeCache* c) { - TypeListNode* n = arena_new(p->arena, TypeListNode); - if (!n) return NULL; - memset(n, 0, sizeof *n); - n->next = c->derived; - c->derived = n; - return &n->ty; -} - -/* Bare structural-type node: arena-allocated, NOT linked into `derived`, NOT - * zeroed. Each constructor writes exactly the fields its kind reads; the - * structural table compares those fields (not raw bytes), so the union tail and - * padding may stay uninitialized. */ -static Type* alloc_struct_type(Pool* p) { return arena_new(p->arena, Type); } - static u32 type_fnv_ptr(u32 h, const void* q) { u64 u = (u64)(uintptr_t)q; h = (h ^ (u32)u) * 16777619u; @@ -101,7 +36,11 @@ static u32 type_fnv_ptr(u32 h, const void* q) { return h; } -/* Structural hash over a (non-tagged) derived type's identity fields. */ +/* Structural hash + equality over a (non-tagged) derived type's identity + * fields. These hash-cons TY_PTR / TY_ARRAY / TY_FUNC and qualified variants of + * non-tagged types: two independently built `int*` Types hash and compare equal + * and so collapse to one canonical node. They compare fields, never raw bytes, + * so a structural node's union tail and padding may stay uninitialized. */ static u32 type_struct_hash(const Type* t) { u32 h = 2166136261u; h = (h ^ (u32)t->kind) * 16777619u; @@ -151,52 +90,69 @@ static int type_struct_eq(const Type* a, const Type* b) { } } -static const Type* type_intern_find(const TypeInternTable* tab, - const Type* probe, u32 h) { - u32 mask, i; - const Type* s; - if (!tab->slots) return NULL; - mask = tab->cap - 1u; - i = h & mask; - while ((s = tab->slots[i]) != NULL) { - if (type_struct_eq(s, probe)) return s; - i = (i + 1u) & mask; - } - return NULL; -} - -/* Insert a freshly built canonical node. Best-effort under OOM: on a failed - * grow the node is simply not interned (a later identical query allocates a - * duplicate — correct, just unshared), matching the arena/buf failure policy. */ -static void type_intern_add(Pool* p, TypeInternTable* tab, const Type* t, - u32 h) { - u32 mask, i; - if (!tab->slots || (tab->used + 1u) * 4u >= tab->cap * 3u) { - u32 ncap = tab->cap ? tab->cap * 2u : 64u; - const Type** ns = arena_zarray(p->arena, const Type*, ncap); - if (!ns) return; - if (tab->slots) { - u32 nmask = ncap - 1u, k; - for (k = 0; k < tab->cap; ++k) { - const Type* s = tab->slots[k]; - u32 j; - if (!s) continue; - j = type_struct_hash(s) & nmask; - while (ns[j]) j = (j + 1u) & nmask; - ns[j] = s; - } - } - tab->slots = ns; - tab->cap = ncap; - } - if (!tab->slots) return; - mask = tab->cap - 1u; - i = h & mask; - while (tab->slots[i]) i = (i + 1u) & mask; - tab->slots[i] = t; - tab->used++; +/* Structural intern SET: the canonical Type* IS the stored element and + * membership is decided structurally (type_struct_eq) — which a scalar + * key->value map cannot express, hence KIT_HASHSET_DEFINE. Interns + * ptr/array/func + qualified non-tagged types; tagged types are interned by + * declaration identity on `derived` and never enter this set. Replaces the + * former O(types) linear scan of one flat derived list (which made each + * derivation O(types) → type construction O(types^2)). */ +KIT_HASHSET_DEFINE(TypeInternSet, const Type*, type_struct_hash, type_struct_eq); + +/* Completed record layout id, keyed by record identity. A record's identity is + * its tag id (same_record_type is tag-id equality), so a plain u32->id map + * captures it; the Pool is 1:1 with a compiler so no compiler key is needed. + * Replaces the former per-lookup list scan (O(records^2) for record-heavy TUs). + * Tag id 0 (TAG_NONE) never names a stored record, so it is safe as the empty + * key sentinel. */ +KIT_HASHMAP_DEFINE(CgRecordMap, u32, KitCgTypeId, kit_hash_u32); + +typedef struct PoolTypeCache { + /* Direct slots for void + primitive kinds (TY_VOID..TY_LDOUBLE). */ + const Type* prim[NUM_PRIM_KINDS]; + /* Structurally-interned ptr/array/func + qualified non-tagged types. */ + TypeInternSet structural; + /* Identity-interned tagged types (struct/union/enum, qualified or not) and + * the throwaway record-builder results: a list, scanned only by the (rare) + * tag-based type_qualified / type_unqual paths and never per derivation. */ + TypeListNode* derived; + /* Completed record layout ids, keyed by tag id. */ + CgRecordMap cg_records; + /* Tag id allocator (1-based; TAG_NONE = 0). */ + u32 next_tag; +} PoolTypeCache; + +static PoolTypeCache* cache_get(Pool* p) { + PoolTypeCache* c = (PoolTypeCache*)p->type_cache; + if (c) return c; + c = arena_new(p->arena, PoolTypeCache); + if (!c) return NULL; + memset(c, 0, sizeof *c); + c->next_tag = 1; + /* Both tables run on the pool's arena-heap facade: a resize orphans the old + * array into the arena (reclaimed wholesale at teardown), so no fini hook is + * needed. Lazy (cap 0): the first insert allocates. */ + TypeInternSet_init_cap(&c->structural, &p->arena_heap, 0); + CgRecordMap_init_cap(&c->cg_records, &p->arena_heap, 0); + p->type_cache = c; + return c; +} + +static Type* alloc_type_node(Pool* p, PoolTypeCache* c) { + TypeListNode* n = arena_new(p->arena, TypeListNode); + if (!n) return NULL; + memset(n, 0, sizeof *n); + n->next = c->derived; + c->derived = n; + return &n->ty; } +/* Bare structural-type node: arena-allocated, NOT linked into `derived`, NOT + * zeroed. Each constructor writes exactly the fields its kind reads; the + * structural set compares those fields (not raw bytes), so the union tail and + * padding may stay uninitialized. */ +static Type* alloc_struct_type(Pool* p) { return arena_new(p->arena, Type); } + const Type* type_void(Pool* p) { return type_prim(p, TY_VOID); } const Type* type_prim(Pool* p, TypeKind kind) { @@ -215,29 +171,26 @@ const Type* type_prim(Pool* p, TypeKind kind) { const Type* type_ptr(Pool* p, const Type* pointee) { PoolTypeCache* c = cache_get(p); Type probe; - u32 h; const Type* found; Type* t; if (!c) return NULL; probe.kind = TY_PTR; probe.qual = 0; probe.ptr.pointee = pointee; - h = type_struct_hash(&probe); - found = type_intern_find(&c->structural, &probe, h); + found = TypeInternSet_find(&c->structural, &probe); if (found) return found; t = alloc_struct_type(p); if (!t) return NULL; t->kind = TY_PTR; t->qual = 0; t->ptr.pointee = pointee; - type_intern_add(p, &c->structural, t, h); + TypeInternSet_add(&c->structural, t); return t; } const Type* type_array(Pool* p, const Type* elem, u32 count, int incomplete) { PoolTypeCache* c = cache_get(p); Type probe; - u32 h; const Type* found; Type* t; if (!c) return NULL; @@ -246,8 +199,7 @@ const Type* type_array(Pool* p, const Type* elem, u32 count, int incomplete) { probe.arr.elem = elem; probe.arr.count = count; probe.arr.incomplete = (u8)(incomplete ? 1 : 0); - h = type_struct_hash(&probe); - found = type_intern_find(&c->structural, &probe, h); + found = TypeInternSet_find(&c->structural, &probe); if (found) return found; t = alloc_struct_type(p); if (!t) return NULL; @@ -256,7 +208,7 @@ const Type* type_array(Pool* p, const Type* elem, u32 count, int incomplete) { t->arr.elem = elem; t->arr.count = count; t->arr.incomplete = (u8)(incomplete ? 1 : 0); - type_intern_add(p, &c->structural, t, h); + TypeInternSet_add(&c->structural, t); return t; } @@ -264,7 +216,6 @@ const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, int variadic) { PoolTypeCache* c = cache_get(p); Type probe; - u32 h; const Type* found; Type* t; if (!c) return NULL; @@ -276,8 +227,7 @@ const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, probe.fn.params = params; probe.fn.nparams = n; probe.fn.variadic = (u8)(variadic ? 1 : 0); - h = type_struct_hash(&probe); - found = type_intern_find(&c->structural, &probe, h); + found = TypeInternSet_find(&c->structural, &probe); if (found) return found; t = alloc_struct_type(p); if (!t) return NULL; @@ -294,7 +244,7 @@ const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, } else { t->fn.params = NULL; } - type_intern_add(p, &c->structural, t, h); + TypeInternSet_add(&c->structural, t); return t; } @@ -330,17 +280,15 @@ const Type* type_qualified(Pool* p, const Type* base, u16 qual) { /* Non-tagged: structurally interned (qualified prim / ptr / array / func). */ { Type probe = *base; - u32 h; const Type* found; probe.qual = qual; - h = type_struct_hash(&probe); - found = type_intern_find(&c->structural, &probe, h); + found = TypeInternSet_find(&c->structural, &probe); if (found) return found; t = alloc_struct_type(p); if (!t) return NULL; *t = *base; t->qual = qual; - type_intern_add(p, &c->structural, t, h); + TypeInternSet_add(&c->structural, t); return t; } } @@ -491,17 +439,15 @@ const Type* type_unqual(Pool* p, const Type* t) { * prim; here that leaves ptr / array / func). */ { Type probe = *t; - u32 h; const Type* found; probe.qual = 0; - h = type_struct_hash(&probe); - found = type_intern_find(&c->structural, &probe, h); + found = TypeInternSet_find(&c->structural, &probe); if (found) return found; nt = alloc_struct_type(p); if (!nt) return NULL; *nt = *t; nt->qual = 0; - type_intern_add(p, &c->structural, nt, h); + TypeInternSet_add(&c->structural, nt); return nt; } } @@ -740,14 +686,6 @@ static KitCgTypeId type_cg_builtin(KitCompiler* c, TypeKind kind) { return KIT_CG_TYPE_NONE; } -static int same_record_type(const Type* a, const Type* b) { - if (a == b) return 1; - if (!a || !b) return 0; - if (a->kind != b->kind) return 0; - if (a->kind != TY_STRUCT && a->kind != TY_UNION) return 0; - return a->rec.tag_id != TAG_NONE && a->rec.tag_id == b->rec.tag_id; -} - typedef enum TypeCgMode { TYPE_CG_VALUE, TYPE_CG_RECORD_FIELD, @@ -762,37 +700,28 @@ typedef struct TypeCgLower { static KitCgTypeId type_cg_lower(TypeCgLower* l, const Type* t, TypeCgMode mode); +/* Record identity is its tag id (records are never structurally interned), so + * the memo is a plain tag-id -> layout-id map. Only ever called with STRUCT/ + * UNION types, so t->rec is live; complete records always carry a non-zero tag, + * so tag 0 is free to serve as the map's empty-key sentinel. */ static KitCgTypeId type_cg_record_memo_get(PoolTypeCache* cache, KitCompiler* c, const Type* t) { + const KitCgTypeId* hit; + (void)c; if (!cache) return KIT_CG_TYPE_NONE; - for (CgRecordMemo* m = cache->cg_record_memos; m; m = m->next) { - if (m->compiler == c && (m->type == t || same_record_type(m->type, t))) { - return m->id; - } - } - return KIT_CG_TYPE_NONE; + hit = CgRecordMap_get(&cache->cg_records, t->rec.tag_id); + return hit ? *hit : KIT_CG_TYPE_NONE; } static void type_cg_record_memo_put(Pool* p, PoolTypeCache* cache, KitCompiler* c, const Type* t, KitCgTypeId id) { - CgRecordMemo* m; - if (!p || !cache || !c || !t || id == KIT_CG_TYPE_NONE) return; + (void)p; + (void)c; + if (!cache || !t || id == KIT_CG_TYPE_NONE) return; if (t->kind != TY_STRUCT && t->kind != TY_UNION) return; - if (t->rec.incomplete) return; - for (m = cache->cg_record_memos; m; m = m->next) { - if (m->compiler == c && (m->type == t || same_record_type(m->type, t))) { - m->id = id; - return; - } - } - m = arena_new(p->arena, CgRecordMemo); - if (!m) return; - m->compiler = c; - m->type = t; - m->id = id; - m->next = cache->cg_record_memos; - cache->cg_record_memos = m; + if (t->rec.incomplete || t->rec.tag_id == TAG_NONE) return; + CgRecordMap_set(&cache->cg_records, t->rec.tag_id, id); } static KitCgTypeId type_cg_record_layout(TypeCgLower* l, const Type* t) { diff --git a/lang/cpp/cpp_support.h b/lang/cpp/cpp_support.h @@ -12,6 +12,7 @@ #include <stdarg.h> #include <stddef.h> #include <stdint.h> +#include <string.h> typedef int8_t i8; typedef int16_t i16; @@ -32,9 +33,32 @@ typedef u32 BytesId; typedef struct Pool { Compiler* c; KitArena* arena; - void* type_cache; /* opaque slot owned by the C frontend; unused by cpp */ + /* A KitHeap facade over `arena`: alloc/realloc route to the arena (realloc + * copies into a fresh block), free is a no-op. Lets arena-lifetime hash maps + * (KIT_HASHMAP_DEFINE, which is otherwise heap-backed) avoid any teardown — + * resize orphans the old table into the arena, bounded by the final size and + * reclaimed wholesale at pool teardown. Shared by the parser scope indexes, + * the type-system record memo, and the ABI info cache. */ + KitHeap arena_heap; + void* type_cache; /* opaque slot owned by the C type system; unused by cpp */ + void* abi_cache; /* opaque slot owned by the C ABI layer; unused by cpp */ } Pool; +static inline void* kit_pool_heap_alloc(KitHeap* h, size_t n, size_t align) { + return kit_arena_alloc((KitArena*)h->user, n, align); +} +static inline void* kit_pool_heap_realloc(KitHeap* h, void* old, size_t old_n, + size_t new_n, size_t align) { + void* q = kit_arena_alloc((KitArena*)h->user, new_n, align); + if (q && old && old_n) memcpy(q, old, old_n < new_n ? old_n : new_n); + return q; +} +static inline void kit_pool_heap_free(KitHeap* h, void* p, size_t n) { + (void)h; + (void)p; + (void)n; +} + /* C data model for frontend-visible scalar spelling. kit currently uses LP64 * for 64-bit non-Windows targets, LLP64 for 64-bit Windows targets, and ILP32 * for 32-bit targets. The distinction is exactly sizeof(long): 8 for LP64, @@ -51,10 +75,15 @@ static inline Pool* c_pool_new(Compiler* c) { p->c = c; p->arena = NULL; p->type_cache = NULL; + p->abi_cache = NULL; if (kit_arena_new(h, 0, &p->arena) != KIT_OK || !p->arena) { h->free(h, p, sizeof(*p)); return NULL; } + p->arena_heap.alloc = kit_pool_heap_alloc; + p->arena_heap.realloc = kit_pool_heap_realloc; + p->arena_heap.free = kit_pool_heap_free; + p->arena_heap.user = p->arena; return p; }