diff --git a/Makefile b/Makefile index 49190e1c..edeee030 100644 --- a/Makefile +++ b/Makefile @@ -302,6 +302,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_subslice_cap_run \ $(BIN)/test_subslice_ptresz_run \ $(BIN)/test_deref_slice_store_run \ + $(BIN)/test_alias_decl_order_size_run \ $(BIN)/test_tuple_nary_destructure_run \ $(BIN)/test_rvalue_tuple_destructure_run \ $(BIN)/test_overcap_tuple_field_store_run \ @@ -1446,6 +1447,12 @@ $(BIN)/test_deref_slice_store_run: test/wcc/944_deref_slice_store_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_alias_decl_order_size_run: test/wcc/944_alias_decl_order_size_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_tuple_nary_destructure_run: test/wcc/945_tuple_nary_destructure_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index f2b755a1..749f3998 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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 ;` 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) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 8fa15776..cd2d3886 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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 diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 09412079..8bc04909 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -124,6 +124,14 @@ type tinfo = struct { // cstage Type.nullable (cmd/wcc/ww.h:430-433). name: str, under: *tinfo, + resolving: i32, // #62/#69: TY_NAMED demand-resolution cycle guard. + // Mirrors cstage Type.resolving (cmd/wcc/ww.h) + // and harec idecl->in_progress (ref/harec/src/ + // check.c:4767): set while the alias body + // resolves; a VALUE-position read of an + // in-progress named is a true type cycle and + // loud-rejects. Pointer positions never read + // size, so legal self-refs stay accepted. slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`. // `size` stays natural (Hare-faithful); // `slotsize` carries the slot-padded width diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a52c52d9..4a52c997 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9510,6 +9510,14 @@ type tinfo = struct { // cstage Type.nullable (cmd/wcc/ww.h:430-433). name: str, under: *tinfo, + resolving: i32, // #62/#69: TY_NAMED demand-resolution cycle guard. + // Mirrors cstage Type.resolving (cmd/wcc/ww.h) + // and harec idecl->in_progress (ref/harec/src/ + // check.c:4767): set while the alias body + // resolves; a VALUE-position read of an + // in-progress named is a true type cycle and + // loud-rejects. Pointer positions never read + // size, so legal self-refs stay accepted. slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`. // `size` stays natural (Hare-faithful); // `slotsize` carries the slot-padded width @@ -10321,6 +10329,36 @@ fn cerr(m: str) void = { os.write(2, m.ptr, m.len: u64); }; +// circularnamed — #62/#69 cycle guard, cstage circular_named twin +// (cmd/wcc/check.c): a VALUE-position read of a TY_NAMED whose body is +// still resolving is a true type cycle (infinite size) — loud, per +// 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 `type node = struct { next: *node }` stays legal. +// Pre-#62 a pure alias cycle left a CYCLIC under-chain in the table +// and every NAMED-chain chase loop downstream spun forever (the #69 +// compiler hang); a struct-value cycle recursed the slot walkers to +// stack overflow. +fn circularnamed(c: *checker, t: *tinfo, n: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != tykind.TY_NAMED) { return false; }; + if (t.resolving == 0) { return false; }; + if (n != nil) { cerr(n.file); cerr(": "); }; + cerr("error: circular type dependency: '"); + cerr(t.name); + cerr("'\n"); + c.errs += 1; + // Loud-STOP, not accumulate: wwstage's AST-level alias walkers + // (resolvealias, cgenutil aliaslookup chains) follow TNAME->TNAME + // by NAME, blind to the tinfo table — on a cyclic alias graph they + // spin forever even after the table edge is cut to tyerr (measured: + // error printed once, then hang). cstage accumulates instead — its + // single-peel ternaries can't loop. Asymmetry is deliberate; both + // stages reject with the same message + non-zero exit. + os.exit(1); +}; + // seedprimitives — install the built-in type names so `i32`, `str`, // etc. can be looked up like ordinary symbols. fn seedprimitives(c: *checker) void = { @@ -12022,7 +12060,20 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // TTAGGED tinfocachebind cycle-break. let named: *tinfo = typenamed(s.name, nil); s.type_ = named; + named.resolving = 1; let under: *tinfo = tinfofornode(c, body); + // #62/#69: alias-root cycle (`type a = b; + // type b = a` / `type a = a`) — checked + // BEFORE clearing the flag so self-aliases + // trip on their own mark. tyerr instead of + // the cyclic under keeps the table ACYCLIC + // by construction: every NAMED-chain chase + // loop stays terminating. Mirrors cstage + // resolve_typedecl. + if (circularnamed(c, under, n)) { + under = c.tc.tyerr; + }; + named.resolving = 0; named.under = under; if (under != nil) { named.size = under.size; @@ -12063,6 +12114,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; let sub: *tinfo = tinfofornode(c, n.lhs); + // #62/#69: `type a = [2]a` value cycle — loud, cstage twin. + if (circularnamed(c, sub, n)) { sub = c.tc.tyerr; }; r = typearray(sub, elen); case nkind.N_TFN: // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B @@ -12126,6 +12179,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let p: *node = n.list; for (p != nil) { let pt: *tinfo = tinfofornode(c, p.lhs); + // #62/#69: tuple-member value cycle — loud, cstage twin. + if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; }; let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!; if (teh == nil) { teh = te; } else { tet.tnext = te; }; tet = te; @@ -12180,6 +12235,11 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { for (f != nil) { if (f.kind == nkind.N_TFIELD) { let ft: *tinfo = tinfofornode(c, f.lhs); + // #62/#69: struct-field value cycle (`type s1 = + // struct { x: s2 }; type s2 = struct { x: s1 }`) + // — loud; pre-#62 this stack-overflowed the slot + // walkers. cstage twin. + if (circularnamed(c, ft, f)) { ft = c.tc.tyerr; }; if (ft != nil) { if (ft.align > maxalign) { maxalign = ft.align; }; if (ft.align > 0u64) { @@ -12241,6 +12301,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); + // #62/#69: union-member value cycle — loud, cstage twin. + if (circularnamed(c, vt, v)) { vt = c.tc.tyerr; }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 0a8d2767..174764e5 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -57,6 +57,36 @@ fn cerr(m: str) void = { os.write(2, m.ptr, m.len: u64); }; +// circularnamed — #62/#69 cycle guard, cstage circular_named twin +// (cmd/wcc/check.c): a VALUE-position read of a TY_NAMED whose body is +// still resolving is a true type cycle (infinite size) — loud, per +// 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 `type node = struct { next: *node }` stays legal. +// Pre-#62 a pure alias cycle left a CYCLIC under-chain in the table +// and every NAMED-chain chase loop downstream spun forever (the #69 +// compiler hang); a struct-value cycle recursed the slot walkers to +// stack overflow. +fn circularnamed(c: *checker, t: *tinfo, n: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != tykind.TY_NAMED) { return false; }; + if (t.resolving == 0) { return false; }; + if (n != nil) { cerr(n.file); cerr(": "); }; + cerr("error: circular type dependency: '"); + cerr(t.name); + cerr("'\n"); + c.errs += 1; + // Loud-STOP, not accumulate: wwstage's AST-level alias walkers + // (resolvealias, cgenutil aliaslookup chains) follow TNAME->TNAME + // by NAME, blind to the tinfo table — on a cyclic alias graph they + // spin forever even after the table edge is cut to tyerr (measured: + // error printed once, then hang). cstage accumulates instead — its + // single-peel ternaries can't loop. Asymmetry is deliberate; both + // stages reject with the same message + non-zero exit. + os.exit(1); +}; + // seedprimitives — install the built-in type names so `i32`, `str`, // etc. can be looked up like ordinary symbols. fn seedprimitives(c: *checker) void = { @@ -1758,7 +1788,20 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // TTAGGED tinfocachebind cycle-break. let named: *tinfo = typenamed(s.name, nil); s.type_ = named; + named.resolving = 1; let under: *tinfo = tinfofornode(c, body); + // #62/#69: alias-root cycle (`type a = b; + // type b = a` / `type a = a`) — checked + // BEFORE clearing the flag so self-aliases + // trip on their own mark. tyerr instead of + // the cyclic under keeps the table ACYCLIC + // by construction: every NAMED-chain chase + // loop stays terminating. Mirrors cstage + // resolve_typedecl. + if (circularnamed(c, under, n)) { + under = c.tc.tyerr; + }; + named.resolving = 0; named.under = under; if (under != nil) { named.size = under.size; @@ -1799,6 +1842,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; let sub: *tinfo = tinfofornode(c, n.lhs); + // #62/#69: `type a = [2]a` value cycle — loud, cstage twin. + if (circularnamed(c, sub, n)) { sub = c.tc.tyerr; }; r = typearray(sub, elen); case nkind.N_TFN: // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B @@ -1862,6 +1907,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let p: *node = n.list; for (p != nil) { let pt: *tinfo = tinfofornode(c, p.lhs); + // #62/#69: tuple-member value cycle — loud, cstage twin. + if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; }; let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!; if (teh == nil) { teh = te; } else { tet.tnext = te; }; tet = te; @@ -1916,6 +1963,11 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { for (f != nil) { if (f.kind == nkind.N_TFIELD) { let ft: *tinfo = tinfofornode(c, f.lhs); + // #62/#69: struct-field value cycle (`type s1 = + // struct { x: s2 }; type s2 = struct { x: s1 }`) + // — loud; pre-#62 this stack-overflowed the slot + // walkers. cstage twin. + if (circularnamed(c, ft, f)) { ft = c.tc.tyerr; }; if (ft != nil) { if (ft.align > maxalign) { maxalign = ft.align; }; if (ft.align > 0u64) { @@ -1977,6 +2029,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); + // #62/#69: union-member value cycle — loud, cstage twin. + if (circularnamed(c, vt, v)) { vt = c.tc.tyerr; }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ba587b81..24ff7a73 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9510,6 +9510,14 @@ type tinfo = struct { // cstage Type.nullable (cmd/wcc/ww.h:430-433). name: str, under: *tinfo, + resolving: i32, // #62/#69: TY_NAMED demand-resolution cycle guard. + // Mirrors cstage Type.resolving (cmd/wcc/ww.h) + // and harec idecl->in_progress (ref/harec/src/ + // check.c:4767): set while the alias body + // resolves; a VALUE-position read of an + // in-progress named is a true type cycle and + // loud-rejects. Pointer positions never read + // size, so legal self-refs stay accepted. slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`. // `size` stays natural (Hare-faithful); // `slotsize` carries the slot-padded width @@ -10321,6 +10329,36 @@ fn cerr(m: str) void = { os.write(2, m.ptr, m.len: u64); }; +// circularnamed — #62/#69 cycle guard, cstage circular_named twin +// (cmd/wcc/check.c): a VALUE-position read of a TY_NAMED whose body is +// still resolving is a true type cycle (infinite size) — loud, per +// 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 `type node = struct { next: *node }` stays legal. +// Pre-#62 a pure alias cycle left a CYCLIC under-chain in the table +// and every NAMED-chain chase loop downstream spun forever (the #69 +// compiler hang); a struct-value cycle recursed the slot walkers to +// stack overflow. +fn circularnamed(c: *checker, t: *tinfo, n: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != tykind.TY_NAMED) { return false; }; + if (t.resolving == 0) { return false; }; + if (n != nil) { cerr(n.file); cerr(": "); }; + cerr("error: circular type dependency: '"); + cerr(t.name); + cerr("'\n"); + c.errs += 1; + // Loud-STOP, not accumulate: wwstage's AST-level alias walkers + // (resolvealias, cgenutil aliaslookup chains) follow TNAME->TNAME + // by NAME, blind to the tinfo table — on a cyclic alias graph they + // spin forever even after the table edge is cut to tyerr (measured: + // error printed once, then hang). cstage accumulates instead — its + // single-peel ternaries can't loop. Asymmetry is deliberate; both + // stages reject with the same message + non-zero exit. + os.exit(1); +}; + // seedprimitives — install the built-in type names so `i32`, `str`, // etc. can be looked up like ordinary symbols. fn seedprimitives(c: *checker) void = { @@ -12022,7 +12060,20 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // TTAGGED tinfocachebind cycle-break. let named: *tinfo = typenamed(s.name, nil); s.type_ = named; + named.resolving = 1; let under: *tinfo = tinfofornode(c, body); + // #62/#69: alias-root cycle (`type a = b; + // type b = a` / `type a = a`) — checked + // BEFORE clearing the flag so self-aliases + // trip on their own mark. tyerr instead of + // the cyclic under keeps the table ACYCLIC + // by construction: every NAMED-chain chase + // loop stays terminating. Mirrors cstage + // resolve_typedecl. + if (circularnamed(c, under, n)) { + under = c.tc.tyerr; + }; + named.resolving = 0; named.under = under; if (under != nil) { named.size = under.size; @@ -12063,6 +12114,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; let sub: *tinfo = tinfofornode(c, n.lhs); + // #62/#69: `type a = [2]a` value cycle — loud, cstage twin. + if (circularnamed(c, sub, n)) { sub = c.tc.tyerr; }; r = typearray(sub, elen); case nkind.N_TFN: // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B @@ -12126,6 +12179,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let p: *node = n.list; for (p != nil) { let pt: *tinfo = tinfofornode(c, p.lhs); + // #62/#69: tuple-member value cycle — loud, cstage twin. + if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; }; let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!; if (teh == nil) { teh = te; } else { tet.tnext = te; }; tet = te; @@ -12180,6 +12235,11 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { for (f != nil) { if (f.kind == nkind.N_TFIELD) { let ft: *tinfo = tinfofornode(c, f.lhs); + // #62/#69: struct-field value cycle (`type s1 = + // struct { x: s2 }; type s2 = struct { x: s1 }`) + // — loud; pre-#62 this stack-overflowed the slot + // walkers. cstage twin. + if (circularnamed(c, ft, f)) { ft = c.tc.tyerr; }; if (ft != nil) { if (ft.align > maxalign) { maxalign = ft.align; }; if (ft.align > 0u64) { @@ -12241,6 +12301,8 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); + // #62/#69: union-member value cycle — loud, cstage twin. + if (circularnamed(c, vt, v)) { vt = c.tc.tyerr; }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { diff --git a/test/wcc/944_alias_decl_order_size_run.c b/test/wcc/944_alias_decl_order_size_run.c new file mode 100644 index 00000000..c464850b --- /dev/null +++ b/test/wcc/944_alias_decl_order_size_run.c @@ -0,0 +1,430 @@ +/* + * 944_alias_decl_order_size_run — #62 rider (alias arc pre-F1): type- + * table layout must be DECL-ORDER-INDEPENDENT. cstage check_file used + * to resolve 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. wwstage (demand- + * driven tinfofornode) was order-independent on every row — the + * measured-sound side cstage now mirrors via resolve_typedecl's + * resolve-on-first-reference. Oracle: ken /tmp/ken_62_oracle.md + * (size((void|ali)) = 8B tag + roundup8(chased member size); every + * behavior indistinguishable from the (void|base) spelling). + * + * row | shape | want + * -----------------+-----------------------------------------+----- + * sizes_norm | base,ali decl order; size(base/ali/ | + * | (void|ali)) = 16/16/24 | 0 + * sizes_fwd | ali BEFORE base (fwd-ref); same asserts | 0 + * union_decl_norm | named u=(void|ali) declared LAST | 0 + * union_decl_fwd | named u=(void|ali) declared FIRST | 0 + * field_fwd | outer{i,j:inner} before inner — total | + * | size AND j.b offset readback (4/9/7/3 | + * | break prefix-luck; last word checked) | 0 + * field_norm | inner before outer; same asserts | 0 + * arrelem_fwd | arr=[2]base before base — stride + | + * | a[1].b last-word readback | 0 + * arrelem_norm | base before arr; same asserts | 0 + * chain2_fwd | a2=a1=base full fwd chain; 16/16 | 0 + * chain2_norm | base,a1,a2 decl order; same asserts | 0 + * union_base_ctl | (void|base) match payload readback — | + * | the m5b_match0 no-regress control | 0 + * cycle_alias | type a=b; type b=a — loud BUILDERR | + * | both stages (#69: pre-fix cs silent-0, | + * | ww HANG) | err + * cycle_self | type a=a — loud BUILDERR both stages | err + * cycle_struct_value | s1{x:s2}/s2{x:s1} — loud BUILDERR | + * | both stages (pre-fix ww stack overflow) | err + * ptr_selfref_ok | node{v,next:*node} legal self-ref stays | + * | accepted + byte-id (guard no-over-fire) | 0 + * + * NOT pinned here: the alias-in-union match payload readback + * (m5b_match1) — its word0-only box STORE is the Layer-2 cgen family + * (cstage cg_widen_tagged_store single NAMED peel; wwstage + * rhsstructpayload name-keyed structlookup without alias chase), + * EXPECTED-FAIL until the F1/F2 copy-width fix lands. See task #62. + * + * Every row also asserts cstage/wwstage asm byte-id, except rows + * flagged nobyteid (arrelem_fwd — the pre-existing task-#60 index- + * over-alias divergence; see the row comment). NNN<950, + * self-contained (/tmp, no imports) — rule-14's selfhost-sibling + * race does not apply (941 precedent). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +#define K_RUN 0 /* build+run both drivers, exit==want, + cs==ww byte-id */ +#define K_BUILDERR 1 /* build must FAIL with experr on BOTH drivers (rule 7) */ + +/* nobyteid: the row's RUNTIME behavior is pinned on both stages but + * its asm is excluded from the byte-id sweep — cite the filed task at + * the row. */ +struct row { const char *label; const char *src; int want; int nobyteid; + int kind; const char *experr; }; + +/* errlog_has — a BUILDERR row must fail WITH its diagnostic; any other + * failure (parse error, crash, hang-kill) is a vacuous reject (940 + * precedent). */ +static int +errlog_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +static const struct row rows[] = { + { "sizes_norm", + "package main;\n" + "type base = struct { a: size, b: size };\n" + "type ali = base;\n" + "export fn main() i32 = {\n" + " if (size(base) != 16) { return 1; };\n" + " if (size(ali) != 16) { return 2; };\n" + " if (size((void | ali)) != 24) { return 3; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + { "sizes_fwd", + "package main;\n" + "type ali = base;\n" + "type base = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " if (size(base) != 16) { return 1; };\n" + " if (size(ali) != 16) { return 2; };\n" + " if (size((void | ali)) != 24) { return 3; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + { "union_decl_norm", + "package main;\n" + "type base = struct { a: size, b: size };\n" + "type ali = base;\n" + "type u = (void | ali);\n" + "export fn main() i32 = {\n" + " if (size(u) != 24) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + { "union_decl_fwd", + "package main;\n" + "type u = (void | ali);\n" + "type ali = base;\n" + "type base = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " if (size(u) != 24) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + /* fwd field: a size-0 inner collapsed outer's offsets too — pin + * the LAST word of the SECOND field, values all distinct. */ + { "field_fwd", + "package main;\n" + "type outer = struct { i: inner, j: inner };\n" + "type inner = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " if (size(inner) != 16) { return 1; };\n" + " if (size(outer) != 32) { return 2; };\n" + " let o: outer;\n" + " o.i.a = 4; o.i.b = 9; o.j.a = 7; o.j.b = 3;\n" + " if (o.i.a != 4) { return 3; };\n" + " if (o.i.b != 9) { return 4; };\n" + " if (o.j.a != 7) { return 5; };\n" + " if (o.j.b != 3) { return 6; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + { "field_norm", + "package main;\n" + "type inner = struct { a: size, b: size };\n" + "type outer = struct { i: inner, j: inner };\n" + "export fn main() i32 = {\n" + " if (size(inner) != 16) { return 1; };\n" + " if (size(outer) != 32) { return 2; };\n" + " let o: outer;\n" + " o.i.a = 4; o.i.b = 9; o.j.a = 7; o.j.b = 3;\n" + " if (o.i.a != 4) { return 3; };\n" + " if (o.i.b != 9) { return 4; };\n" + " if (o.j.a != 7) { return 5; };\n" + " if (o.j.b != 3) { return 6; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + /* nobyteid: indexing an alias-NAMED array local is the task-#60 + * family — wwstage emits a ptr-load/ADDQ spine vs cstage's direct + * 8(BX); PRE-EXISTING on master, order-independent, runtime- + * correct here (8B-multiple elements). The size assert + element + * readback below stay pinned on both stages; flip this flag when + * #60 lands. */ + { "arrelem_fwd", + "package main;\n" + "type arr = [2]base;\n" + "type base = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " if (size(arr) != 32) { return 1; };\n" + " let a: arr;\n" + " a[0].a = 4; a[0].b = 9; a[1].a = 7; a[1].b = 3;\n" + " if (a[0].b != 9) { return 2; };\n" + " if (a[1].a != 7) { return 3; };\n" + " if (a[1].b != 3) { return 4; };\n" + " return 0;\n" + "};\n", 0, 1, K_RUN, NULL }, + /* nobyteid: same #60 cite as arrelem_fwd — the index-over-alias + * divergence is order-INDEPENDENT (verified on master). */ + { "arrelem_norm", + "package main;\n" + "type base = struct { a: size, b: size };\n" + "type arr = [2]base;\n" + "export fn main() i32 = {\n" + " if (size(arr) != 32) { return 1; };\n" + " let a: arr;\n" + " a[0].a = 4; a[0].b = 9; a[1].a = 7; a[1].b = 3;\n" + " if (a[0].b != 9) { return 2; };\n" + " if (a[1].a != 7) { return 3; };\n" + " if (a[1].b != 3) { return 4; };\n" + " return 0;\n" + "};\n", 0, 1, K_RUN, NULL }, + { "chain2_fwd", + "package main;\n" + "type a2 = a1;\n" + "type a1 = base;\n" + "type base = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " if (size(a1) != 16) { return 1; };\n" + " if (size(a2) != 16) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + { "chain2_norm", + "package main;\n" + "type base = struct { a: size, b: size };\n" + "type a1 = base;\n" + "type a2 = a1;\n" + "export fn main() i32 = {\n" + " if (size(a1) != 16) { return 1; };\n" + " if (size(a2) != 16) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + /* m5b_match0 no-regress control: direct base member, full payload + * readback through the box, last word checked. */ + { "union_base_ctl", + "package main;\n" + "type base = struct { a: size, b: size };\n" + "export fn main() i32 = {\n" + " let x: base;\n" + " x.a = 4; x.b = 9;\n" + " let v: (void | base) = x;\n" + " match (v) {\n" + " case let s: base => {\n" + " if (s.a != 4) { return 1; };\n" + " if (s.b != 9) { return 2; };\n" + " };\n" + " case void => { return 3; };\n" + " };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, + /* #62/#69 cycle guard (rob's rider condition): TRUE typedecl + * cycles loud-reject on BOTH stages — pre-guard cs silently sized + * them 0 and wwstage HUNG (alias cycle) / stack-overflowed (struct + * value cycle). harec cite: check.c:4767 "Circular dependency". */ + { "cycle_alias", + "package main;\n" + "type a = b;\n" + "type b = a;\n" + "export fn main() i32 = {\n" + " if (size(a) != 8) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_BUILDERR, "circular type dependency" }, + { "cycle_self", + "package main;\n" + "type a = a;\n" + "export fn main() i32 = {\n" + " if (size(a) != 8) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_BUILDERR, "circular type dependency" }, + { "cycle_struct_value", + "package main;\n" + "type s1 = struct { x: s2 };\n" + "type s2 = struct { x: s1 };\n" + "export fn main() i32 = {\n" + " if (size(s1) != 8) { return 1; };\n" + " return 0;\n" + "};\n", 0, 0, K_BUILDERR, "circular type dependency" }, + /* The LEGAL self-reference (pointer field never reads the + * target's size) must stay accepted — the io.stream / list-node + * shape the cycle guard is forbidden from breaking. */ + { "ptr_selfref_ok", + "package main;\n" + "type node = struct { v: size, next: *node };\n" + "export fn main() i32 = {\n" + " let n: node;\n" + " n.v = 7;\n" + " n.next = &n;\n" + " if (size(node) != 16) { return 1; };\n" + " if (n.v != 7) { return 2; };\n" + " return 0;\n" + "};\n", 0, 0, K_RUN, NULL }, +}; + +/* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */ +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ados_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/ados_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/ados_%d_e_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + /* timeout: the pre-#69 wwstage HANG on a cycle must fail the row, + * not wedge the suite. */ + snprintf(cmd, sizeof cmd, + "cd %s && timeout 20 %s build %s >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (r->kind == K_BUILDERR) { + int ok = (brc != 0) + && (r->experr == NULL || errlog_has(errf, r->experr)); + if (!ok) + fprintf(stderr, "row[%s]: %s expected loud builderr " + "\"%s\" (brc=%d)\n", r->label, driver, + r->experr ? r->experr : "", brc); + unlink(src); unlink(errf); rmdir(tmpdir); + return ok ? 0 : 1; + } + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +/* cs==ww .s byte-id (rule 10). */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[96], cs[96], ws[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ados_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/ados_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/ados_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + int rc = slurp_eq(cs, ws); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[2120], wdrv[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + if (run_driver(cdrv, &rows[i], i) != 0) fail++; + } + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (run_driver(wdrv, &rows[i], i) != 0) fail++; + } + for (int i = 0; i < n; i++) { + if (rows[i].nobyteid || rows[i].kind == K_BUILDERR) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "alias_decl_order_size: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("alias_decl_order_size: %d/%d ok\n", total, total); + return 0; +}