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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user