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:
2026-06-05 10:13:06 +09:00
parent d14a23b85e
commit 738d7f481c
8 changed files with 708 additions and 8 deletions

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}