kit

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

commit 00b2d7e9dc31fd67c6fd1f2e6728ecbebfb0df21
parent 8406232ec0dc65db7ff231a12bffff91975c3ab7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 29 May 2026 12:53:16 -0700

type_qualified: intern via assignment so memcmp is padding-stable

The -O0 3-stage bootstrap was non-reproducible (stage2 != stage3): ~20 object
files differed by a frame-size off-by-16. Root cause was a self-miscompile in
type_qualified().

It built its memcmp comparison template with an aggregate *initializer*
(Type tmpl = *base;). cfree lowers aggregate initialization field-by-field
(emit_struct_copy_into_slot -> emit_walk_copy) and does NOT copy inter-field
padding, whereas aggregate *assignment* uses a full-width copy_bytes. So the
template's padding held stack garbage and memcmp(&n->ty, &tmpl, sizeof(Type))
missed the cached node, interning a duplicate const-qualified Type. Two distinct
Type* for the same logical pointer then made pcg_convert's 'if (src == dst)'
identity check fail, emitting a redundant cfree_cg_bitcast -> an extra frame
slot -> +16 frame. Only const array-decay-as-call-arg triggered it (only const
goes through type_qualified).

Fix: build the template via plain assignment (decl + 'tmpl = *base;'), matching
the already-correct type_unqual idiom, so the full-width copy makes the padding
deterministic and the memcmp byte-stable.

-O0 bootstrap-debug now reproduces (stage2 == stage3, identical sha256);
toy suite 1034 pass / 0 fail.

Diffstat:
Mlang/c/type/type.c | 27++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/lang/c/type/type.c b/lang/c/type/type.c @@ -158,21 +158,30 @@ const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, } const Type* type_qualified(Pool* p, const Type* base, u16 qual) { + PoolTypeCache* c; + Type tmpl; + Type* t; if (!base || qual == 0) return base; - PoolTypeCache* c = cache_get(p); + c = cache_get(p); if (!c) return NULL; + /* Build the comparison template once, via assignment (not aggregate + * initialization). Struct assignment copies the full object representation, + * including padding bytes, so the memcmp below is byte-stable against the + * interned nodes (which are likewise produced by `*t = tmpl`). An aggregate + * *initializer* (`Type tmpl = *base;`) is lowered as a field-by-field copy + * that leaves padding unspecified, which would make this memcmp miss and + * intern a duplicate qualified type. See type_unqual for the same idiom. */ + tmpl = *base; + tmpl.qual = qual; for (TypeListNode* n = c->derived; n; n = n->next) { - if (n->ty.kind == base->kind && n->ty.qual == qual) { - /* Compare body bytes other than qual. Cheap: types are POD. */ - Type tmpl = *base; - tmpl.qual = qual; - if (memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) return &n->ty; + if (n->ty.kind == base->kind && n->ty.qual == qual && + memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) { + return &n->ty; } } - Type* t = alloc_type_node(p, c); + t = alloc_type_node(p, c); if (!t) return NULL; - *t = *base; - t->qual = qual; + *t = tmpl; return t; }