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:
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;
}