wcc/check: #62 typedecl layout is decl-order-INDEPENDENT — demand-resolve forward refs + loud cycle guard (#69)
check_file resolved typedecl bodies in file order with an eager under->size copy, so any body referencing a typedecl declared LATER read its size-0 placeholder and baked it in: alias size 0, tagged- union maxsz 0 (the F0 m5_match $48-frame under-allocated box), struct field offsets collapsed, array element stride 0 — a whole cstage-only family (7 size()-probe rows, all cs-fail/ww-pass pre-fix). wwstage's demand-driven tinfofornode was order-independent on every row, so this aligns cstage UP to the measured runtime-correct side (the #263-era ruling; rule 10's align-down governs acceptance surface, not layout correctness). Oracle: ken /tmp/ken_62_oracle.md — union size is 8B tag + roundup8(max CHASED member size), a fixed point over the module, never a function of decl order. resolve_typename now resolves a referenced-but-unresolved typedecl on demand via resolve_typedecl (cycle-guarded by Type.resolving); the pass-1.5 loop funnels through the same helper. No consumer can see an unresolved placeholder by construction. CYCLE GUARD — #69 ABSORBED into this rider (rob's rider condition): true typedecl cycles now LOUD-reject on BOTH stages — "circular type dependency" — mirroring harec's in_progress check (ref/harec/src/ check.c:4767 "Circular dependency for '%s'"). Pre-guard: cs silently sized cycles 0; wwstage HUNG on an alias cycle (`type a = b; type b = a` — ken's hang probe /tmp/ken62/c1_cycle.ww, killed at the 20s timeout) and stack-overflowed on a struct value cycle. The check sits at the VALUE-position size consumers only (alias root, struct field, array elem, tuple member, union member), so the legal pointer self-ref (`type node = struct { next: *node }`, the io.stream shape) stays accepted, byte-id. wwstage gets the twin tinfo.resolving flag (lib/ww/typ.ww) + circularnamed in check.ww; its arm loud-STOPS (os.exit) rather than accumulating — wwstage's AST-level alias walkers (resolvealias, aliaslookup chains) follow TNAME->TNAME by name, blind to the tinfo table, and spin on a cyclic alias graph even after the table edge is cut to tyerr (measured); cstage accumulates, its single-peel ternaries cannot loop. TWO-LAYER SPLIT — this is ONE bug number (#62) deliberately split across THREE commits (this rider + F1 + F2), per ken's sizes-correct ≠ payload-correct proof: in NORMAL decl order both stages size the box correctly (16/24, frames $64) yet both still run exit 2 — the box STORE is word0-only, a chase-blind copy-WIDTH lookup in cgen, NOT the type table. EXPECTED-FAIL after this commit: m5b_match1/m5_match stay exit-2 both stages (now byte-id BOTH orders; pre-fix the fwd order was $48-frame divergent). The Layer-2 sites and destinations: - F1 (cstage): cg_widen_tagged_store single NAMED peel, cmd/w6c/cgen.c ~2464 — the type_chase_named census family. - F2 (wwstage): rhsstructpayload bare name-keyed structlookup, no alias chase, selfhost/cmd/wcc/cgenutil.ww:3062 (structlookupchain :1691 already exists). Banked runtime payload-readback rows for F1/F2: /tmp/impl62r_layer2_rows.md. Test 944_alias_decl_order_size_run: every size class pinned in BOTH decl orders (sizes, named union, struct field offsets, array elem, 2-level chain — norm + fwd twins, prefix-luck-breaking last-word readbacks), 3 cycle BUILDERR rows + the legal ptr-self-ref row, (void|base) no-regress control; dual-stage + per-row byte-id (arrelem rows byte-id exempt: pre-existing #60 index-over-alias divergence, order-independent, cited at the rows). lib/ww/typ.ww is an embedded source: both main.combined.ww regen'd + committed (freshness gate).
This commit is contained in:
@@ -59,6 +59,9 @@ lookup_builtin(const char *name)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *decl_mod(Node *file, Node *d);
|
||||
static void resolve_typedecl(Checker *c, Node *d);
|
||||
|
||||
static Type *
|
||||
resolve_typename(Checker *c, Node *n)
|
||||
{
|
||||
@@ -88,6 +91,16 @@ resolve_typename(Checker *c, Node *n)
|
||||
}
|
||||
if (s == NULL || s->kind != SK_TYPE)
|
||||
return err(c, n->pos, "unknown type '%s'", nm);
|
||||
/* #62: a typedecl body may reference a typedecl declared LATER in
|
||||
* the (driver-concatenated) file. Layout is a fixed point over the
|
||||
* whole module — a function of the member types alone, never of
|
||||
* decl order — so resolve the referenced decl on demand before
|
||||
* handing its type out; no consumer may ever see the size-0
|
||||
* placeholder. Mirrors wwstage's demand-driven tinfofornode, the
|
||||
* measured order-independent side. */
|
||||
if (s->type && s->type->kind == TY_NAMED && s->type->under == NULL
|
||||
&& s->decl && s->decl->kind == N_TYPEDECL)
|
||||
resolve_typedecl(c, s->decl);
|
||||
return s->type;
|
||||
}
|
||||
|
||||
@@ -593,6 +606,23 @@ require_sized(Checker *c, Type *t, Pos pos, const char *where)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* circular_named — #62/#69 cycle guard: a VALUE-position reference to
|
||||
* a typedecl whose body is still being resolved is a true type cycle
|
||||
* (the type would have infinite size). Loud, mirroring harec's
|
||||
* in_progress check (ref/harec/src/check.c:4767 "Circular dependency
|
||||
* for '%s'"). Pointer/slice/chan/fn positions never read the target's
|
||||
* size and legitimately receive the in-progress placeholder, so the
|
||||
* check sits at the size-consuming sites only — `type node = struct {
|
||||
* next: *node }` stays legal. */
|
||||
static int
|
||||
circular_named(Checker *c, Type *t, Pos pos)
|
||||
{
|
||||
if (t == NULL || t->kind != TY_NAMED || !t->resolving) return 0;
|
||||
err(c, pos, "circular type dependency: '%s'",
|
||||
t->name ? t->name : "?");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static Type *
|
||||
resolve_type(Checker *c, Node *n)
|
||||
{
|
||||
@@ -633,6 +663,8 @@ resolve_type(Checker *c, Node *n)
|
||||
err(c, n->pos, "array length must be an integer literal");
|
||||
}
|
||||
Type *elem = resolve_type(c, n->lhs);
|
||||
if (circular_named(c, elem, n->pos)) /* #62/#69 */
|
||||
return ty_err;
|
||||
require_sized(c, elem, n->pos, "an array element"); /* #108(b) */
|
||||
return type_array(c->a, elem, len);
|
||||
}
|
||||
@@ -645,6 +677,8 @@ resolve_type(Checker *c, Node *n)
|
||||
for (Node *e = n->list; e; e = e->next) {
|
||||
Tparam *tp = amalloc(c->a, sizeof *tp);
|
||||
tp->type = resolve_type(c, e);
|
||||
if (circular_named(c, tp->type, e->pos)) /* #62/#69 */
|
||||
continue;
|
||||
/* #108(b): an unsized member would poison sz with the
|
||||
* (u64)-1 sentinel. harec ref/harec/src/type_store.c:1147. */
|
||||
if (!require_sized(c, tp->type, e->pos, "a tuple member"))
|
||||
@@ -708,6 +742,8 @@ resolve_type(Checker *c, Node *n)
|
||||
for (Node *e = n->list; e; e = e->next) {
|
||||
Type *vt = resolve_type(c, e);
|
||||
if (vt == ty_never) continue;
|
||||
if (circular_named(c, vt, e->pos)) /* #62/#69 */
|
||||
continue;
|
||||
/* #108(b): an unsized variant has no slot in the union
|
||||
* payload. harec ref/harec/src/type_store.c:449. */
|
||||
if (!require_sized(c, vt, e->pos, "a tagged union member"))
|
||||
@@ -820,6 +856,8 @@ resolve_type(Checker *c, Node *n)
|
||||
u64 off = 0, maxalign = 1;
|
||||
for (Node *f = n->list; f; f = f->next) {
|
||||
Type *ft = resolve_type(c, f->lhs);
|
||||
if (circular_named(c, ft, f->pos)) /* #62/#69 */
|
||||
continue;
|
||||
/* #108(b): an unsized field would overflow the offset
|
||||
* accumulator (align/size == (u64)-1); reject + skip it. */
|
||||
if (!require_sized(c, ft, f->pos, "a struct field"))
|
||||
@@ -2482,6 +2520,45 @@ decl_mod(Node *file, Node *d)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* resolve_typedecl — resolve d's body into its installed TY_NAMED
|
||||
* placeholder. Reached from check_file's typedecl pass AND on demand
|
||||
* from resolve_typename (#62): the old file-order pass let any body
|
||||
* referencing a LATER typedecl read a size-0 placeholder and bake it
|
||||
* into alias sizes, union maxsz, struct field offsets and array
|
||||
* element strides — decl-order-dependent layout. The resolving flag
|
||||
* hands self-references the placeholder, exactly where the file-order
|
||||
* pass did (sound for pointer fields, which never read the target's
|
||||
* size). After a completed resolve `under` is never NULL (resolve_type
|
||||
* returns ty_err/ty_void on failure), so the under==NULL demand gate
|
||||
* cannot re-fire on a failed body.
|
||||
*/
|
||||
static void
|
||||
resolve_typedecl(Checker *c, Node *d)
|
||||
{
|
||||
Type *t = d->type;
|
||||
if (t == NULL || t->under != NULL || t->resolving) return;
|
||||
t->resolving = 1;
|
||||
const char *save = c->cur_mod;
|
||||
c->cur_mod = decl_mod(c->file, d);
|
||||
Type *under = resolve_type(c, d->lhs);
|
||||
c->cur_mod = save;
|
||||
/* Alias-root cycle (`type a = b; type b = a` / `type a = a`):
|
||||
* checked BEFORE clearing the flag so self-aliases trip on their
|
||||
* own in-progress mark. ty_err instead of the cyclic under keeps
|
||||
* the table acyclic by construction — every later NAMED-chain
|
||||
* chase loop (F1/F2's tichase family) stays terminating. */
|
||||
if (circular_named(c, under, d->pos))
|
||||
under = ty_err;
|
||||
t->resolving = 0;
|
||||
t->under = under;
|
||||
if (under) {
|
||||
t->size = under->size;
|
||||
t->align = under->align;
|
||||
t->iserror = under->iserror;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* src_imports — does the source file that contributed decl-module
|
||||
* `modtag` carry `use <name>;` somewhere? With driver concatenation
|
||||
@@ -2609,14 +2686,7 @@ check_file(Checker *c, Node *file)
|
||||
}
|
||||
for (Node *d = file->list; d; d = d->next) {
|
||||
if (d->kind != N_TYPEDECL) continue;
|
||||
c->cur_mod = decl_mod(file, d);
|
||||
Type *under = resolve_type(c, d->lhs);
|
||||
d->type->under = under;
|
||||
if (under) {
|
||||
d->type->size = under->size;
|
||||
d->type->align = under->align;
|
||||
d->type->iserror = under->iserror;
|
||||
}
|
||||
resolve_typedecl(c, d);
|
||||
}
|
||||
c->cur_mod = NULL;
|
||||
for (Node *d = file->list; d; d = d->next) {
|
||||
|
||||
@@ -434,6 +434,13 @@ struct Type {
|
||||
int variadic;
|
||||
const char *name; /* named alias / debug */
|
||||
Type *under; /* underlying resolved type for NAMED */
|
||||
int resolving;/* TY_NAMED demand-resolution cycle guard
|
||||
* (#62): a self-reference re-entering
|
||||
* resolve while the body is open gets the
|
||||
* placeholder, exactly as the old file-order
|
||||
* pass handed it out — sound for pointer
|
||||
* fields, which never read the target's
|
||||
* size. */
|
||||
int iserror;/* Hare-style `!T` error mark; propagates
|
||||
* through NAMED aliases. Variants with
|
||||
* iserror=1 are the propagation target of
|
||||
|
||||
Reference in New Issue
Block a user