Files
ww/test/wcc/812_agg_assign_width.c
Hojun-Cho 4c46d3afde cgen: #49 aggregate-ASSIGN word0-only family — one mem-to-mem funnel (cg_aggcopy), both stages
Whole-aggregate reassignment `b = a` fell to the N_ASSIGN scalar tail
and copied ONE MOVQ — word 0 of any struct/array/tuple — in BOTH
stages, byte-identical, gate-blind (ken f49_min asm proof; latent
because lib style is let-init, whose #265/#268 copy is full-width).
Same class at three more positions: struct-lit FIELD init from an
ident source (`outer{.., r = r}`, the #38 non-tagged half), the deref
place `*p = s` (#31-A), and the module-let global `g = a` / `g = pt{..}`.

Fix: extract the C1.25 assign-resolver word-copy tail verbatim into
cg_aggcopy/aggcopy — the ONE place-resolved (SI)->(BX) aggregate copy
— and wire it at the N_ASSIGN ident-aggregate arm (local + global),
the deref-place divert into the existing resolver aggregate arm, and
the structlit-fill aggregate-field arm, all fed by aggarg_srcaddr
(the closed #265/#268 dispatch). The new arms key on the FULL alias
chase (type_chase_named / chased stamped tinfo, the #22 precedent) in
BOTH stages — the region's single-peel `lu`/`fu` would miss
`type b = a; type a = struct` on cstage while the wwstage twin fired
(ken R1, gA3b: master cs ran the word0 corruption, exit 2; now 0).
Non-addressable aggregate rhs (tuple-lit, unhandled call shapes) dies
LOUD (rule 7) instead of silently truncating: #31-E `*p = (3,4)` and
#31-G's deref flavor `*p = mk()` are now loud both stages (the INDEX
flavor `a[i] = mk()` stays in the legacy INDEX arm — receive
machinery, not this funnel; still filed under #31). #31-B rides: the
cstage-only <=24B gate before cg_structlit_fill_bp is lifted (the
wwstage twin never gated — a >24B literal reassign was
cs-zero/ww-filled, rule-10 break). Global structlit reassign rides
the existing DST_GLOBAL fill machinery.

Unsplit (rule 11): the assign arm, fill arm and deref divert all
route through the one new funnel (cg_aggcopy + aggarg_srcaddr) in
both stages; splitting by site or by stage would ship a transient
cs!=ww (gate-red) or a funnel with no consumer.

941 t2_reject_chain_arg: the row's tuple-LITERAL field fill now louds
at the #49 fill arm before reaching the pinned ARG-site reject; the
fill switches to an ident source (newly working via the fill arm) so
the original arg-site pin still fires.

test/wcc/812_agg_assign_width.c: 17 runtime-readback rows (the only
oracle for a gate-blind class) + per-row asm byte-id; every row fails
at 7545bf7 (ken matrix f49_min/f49b/f49c/fA_16b/f38b + gA3b/gA6 +
impl-A probes; reviewer-A re-probed 5 rows + the gA2 12B shape at a
master git-archive scratch). Alias rows use FIELD-WISE init: the
struct-LIT spelling louds earlier at the pre-existing task-#7
aggregate-let bound on wwstage (the #5 alias-arc's hole, not this
funnel's). Reviewer-A amendment (test-only, K5 self-certify): add
the ken-gA2 odd-size row (12B {u32,u32,u32}, maxalign 4 — pins the
MOVL tail; master both stages exit 3) and gA4's neighbor guards on
the deref row, completing ken's validated matrix in the committed
suite.
2026-06-05 06:18:45 +09:00

499 lines
17 KiB
C

/*
* 812_agg_assign_width — cstage and wwstage agree, byte-for-byte and
* at runtime, that an aggregate ASSIGN copies the WHOLE value (task
* #49, Family A of ken's silent-set triage, /tmp/ken_silent_triage.md).
*
* The bug: plain whole-aggregate reassignment `b = a` fell to the
* N_ASSIGN scalar tail in BOTH stages and copied ONE MOVQ — word 0 of
* a 16/24/48B struct (ken f49_min cs.s asm proof; fA_16b shows even a
* 16B {size,size} fails). Same class at three more positions: a
* struct-lit FIELD initialised from an ident source (`outer{.., r=r}`,
* ken f38b / x5f-h) word0-copied inside cg_structlit_fill; a deref
* place `*p = s` truncated to 8 bytes (#31-A); a module-let global
* `g = a` / `g = pt{...}` stored one word (scalar `MOVQ AX, g(SB)`).
* Tagged/match context was INCIDENTAL — the hole is tagged-independent
* and gate-blind (byte-identical wrongness), latent only because lib
* style is let-init (`let b = a` runs the #265/#268 full-width copy —
* the working sibling these fixes mirror).
*
* The fix (BOTH stages, converged byte-identical): ONE place-resolved
* mem-to-mem copy funnel — cg_aggcopy/aggcopy, extracted verbatim from
* the C1.25 assign-resolver tail — fed by aggarg_srcaddr (source addr →
* SI) and a LEAQ of the destination (BP slot or g(SB) symbol → BX),
* wired at: the N_ASSIGN ident-aggregate arm (local + global), the
* deref-place divert into the existing resolver aggregate arm, and the
* structlit-fill aggregate-field arm. Non-addressable aggregate rhs
* shapes die LOUD (rule 7) instead of silently truncating.
*
* MUTATION: every row below fails at master 7545bf7 (probed 2026-06-05,
* ken's matrix f49_min/f49b/f49c/fA_16b/f38b + impl-A probes pA_*: all
* non-zero exits, byte-id both stages — the class is gate-blind, so
* runtime readback is the only oracle).
*
* row | shape | want
* ---------------------+-------------------------------------+------
* assign_16b | {size,size} b = a, x*10+y readback | 39
* assign_24b_plain | {size,size,size} b = a, sum | 6
* assign_48b | 6-field b = a, sum | 21
* assign_odd_12b | {u32,u32,u32} 12B maxalign-4 (gA2) | 0
* assign_match_bind | dst = s from a match binding over | 0
* | (st|void), st has a tagged member |
* | (ken f49_min — the ORIGINAL #49 |
* | context, regression pin) |
* lit_field_from_ident | outer{tag, r = r} field fill (f38b) | 21
* assign_str_field | {id, name:str} b = a, ladder | 0
* assign_nested | {tag, inner{x,y}} b = a, sum | 6
* arrelem_roundtrip | a[1] = b (kin, pre-wired #270-1b) | 15
* | then cc = a[1] (the new read side) |
* assign_deref | *p = s, 24B struct (#31-A fold) w/ | 27
* | guards either side of the pointee |
* assign_global | g = a then b = g (module-let dst | 36
* | and src, both new arms) |
* assign_dot_source | b = o.i (N_DOT source via | 6
* | aggarg_srcaddr) |
* assign_tuple | (size,size) u = t slot copy | 13
* global_structlit | g = pt{...} (DST_GLOBAL fill wire) | 18
* alias_named_assign | type row = st; b = a (ken R1/gA3b — | 0
* | full alias chase, field-wise init) |
* alias_twolevel_assign| ali -> base -> struct chain (gA6) | 0
* neighbor_guard | b = a with live guards either side | 0
* | of b — over-copy smashes them |
*/
#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;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
{ "assign_16b",
"package main;\n"
"type p2 = struct { x: size, y: size };\n"
"export fn main() i32 = {\n"
"\tlet a: p2 = p2 { x = 3: size, y = 9: size };\n"
"\tlet b: p2 = p2 { x = 0: size, y = 0: size };\n"
"\tb = a;\n"
"\treturn (b.x * 10 + b.y): i32;\n"
"};\n",
39 },
{ "assign_24b_plain",
"package main;\n"
"type st = struct { id: size, x: size, y: size };\n"
"export fn main() i32 = {\n"
"\tlet a: st = st { id = 1: size, x = 2: size, y = 3: size };\n"
"\tlet b: st = st { id = 0: size, x = 0: size, y = 0: size };\n"
"\tb = a;\n"
"\treturn (b.id + b.x + b.y): i32;\n"
"};\n",
6 },
{ "assign_48b",
"package main;\n"
"type big = struct { a: size, b: size, c: size, d: size, e: size, f: size };\n"
"export fn main() i32 = {\n"
"\tlet x: big = big { a = 1: size, b = 2: size, c = 3: size, d = 4: size, e = 5: size, f = 6: size };\n"
"\tlet y: big = big { a = 0: size, b = 0: size, c = 0: size, d = 0: size, e = 0: size, f = 0: size };\n"
"\ty = x;\n"
"\treturn (y.a + y.b + y.c + y.d + y.e + y.f): i32;\n"
"};\n",
21 },
/* ken gA2 — odd size: 12B {u32,u32,u32}, maxalign 4. Pins the
* MOVQ-run + MOVL tail of the funnel (the fi.fsz slot-padded
* skew worry) — NOT a multiple of 8. Pre-fix at master: word0
* copies a+b in one MOVQ, c lost → exit 3 (probed both stages). */
{ "assign_odd_12b",
"package main;\n"
"type tri = struct { a: u32, b: u32, c: u32 };\n"
"export fn main() i32 = {\n"
"\tlet x: tri = tri { a = 7: u32, b = 8: u32, c = 9: u32 };\n"
"\tlet y: tri = tri { a = 0: u32, b = 0: u32, c = 0: u32 };\n"
"\ty = x;\n"
"\tif (y.a != 7) { return 1; };\n"
"\tif (y.b != 8) { return 2; };\n"
"\tif (y.c != 9) { return 3; };\n"
"\treturn 0;\n"
"};\n",
0 },
/* ken f49_min verbatim — the ORIGINAL #49 filing context:
* assign from a match binding, struct with a tagged member.
* Pre-fix exit 4 (dst.m's tag word never copied). */
{ "assign_match_bind",
"package main;\n"
"type st = struct { id: size, m: (void | size) };\n"
"type un = (st | void);\n"
"export fn main() i32 = {\n"
"\tlet src: un = st { id = 6: size, m = 8: size };\n"
"\tlet dst: st = st { id = 0: size, m = void };\n"
"\tmatch (src) {\n"
"\tcase let s: st => {\n"
"\t\tif (!(s.m is size)) { return 1; };\n"
"\t\tdst = s;\n"
"\t};\n"
"\tcase void => { return 2; };\n"
"\t};\n"
"\tif (dst.id != 6) { return 3; };\n"
"\tif (!(dst.m is size)) { return 4; };\n"
"\tif (dst.m as size != 8) { return 5; };\n"
"\treturn 0;\n"
"};\n",
0 },
/* ken f38b shape — struct-lit FIELD from an ident source. Pre-fix
* exit: o.r.id copied (word0), x/y zero → 6. */
{ "lit_field_from_ident",
"package main;\n"
"type rep = struct { id: size, x: size, y: size };\n"
"type outer = struct { tag: size, r: rep };\n"
"export fn main() i32 = {\n"
"\tlet r: rep = rep { id = 6: size, x = 7: size, y = 8: size };\n"
"\tlet o: outer = outer { tag = 4: size, r = r };\n"
"\tif (o.tag != 4) { return 99; };\n"
"\treturn (o.r.id + o.r.x + o.r.y): i32;\n"
"};\n",
21 },
/* 3-word str header inside the copied struct: the header words
* past .ptr are exactly what a word0 copy drops. Ladder form —
* `.len` in mixed arithmetic trips the PRE-EXISTING task-#26
* typing divergence (cstage rejects), which is not this class. */
{ "assign_str_field",
"package main;\n"
"type named = struct { id: size, name: str };\n"
"export fn main() i32 = {\n"
"\tlet a: named = named { id = 7: size, name = \"hello\" };\n"
"\tlet b: named = named { id = 0: size, name = \"\" };\n"
"\tb = a;\n"
"\tif (b.id != 7) { return 1; };\n"
"\tif (b.name.len != 5) { return 2; };\n"
"\treturn 0;\n"
"};\n",
0 },
{ "assign_nested",
"package main;\n"
"type inner = struct { x: size, y: size };\n"
"type outer = struct { tag: size, i: inner };\n"
"export fn main() i32 = {\n"
"\tlet a: outer = outer { tag = 1: size, i = inner { x = 2: size, y = 3: size } };\n"
"\tlet b: outer = outer { tag = 0: size, i = inner { x = 0: size, y = 0: size } };\n"
"\tb = a;\n"
"\treturn (b.tag + b.i.x + b.i.y): i32;\n"
"};\n",
6 },
/* a[i] = b was already wired (#270-1b INDEX-place word-copy) —
* this row pins the KIN pair: the pre-existing store plus the
* NEW ident-from-index read (`cc = a[1]` word0-copied pre-fix). */
{ "arrelem_roundtrip",
"package main;\n"
"type pt = struct { x: size, y: size, z: size };\n"
"export fn main() i32 = {\n"
"\tlet a: [2]pt = [pt { x = 0: size, y = 0: size, z = 0: size }, pt { x = 0: size, y = 0: size, z = 0: size }];\n"
"\tlet b: pt = pt { x = 4: size, y = 5: size, z = 6: size };\n"
"\ta[1] = b;\n"
"\tif (a[0].x != 0) { return 99; };\n"
"\tlet cc: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"\tcc = a[1];\n"
"\treturn (cc.x + cc.y + cc.z): i32;\n"
"};\n",
15 },
/* #31-A fold: deref place from an addressable aggregate ident —
* pre-fix stored 8 bytes (probe pA_deref exit 2). Guards either
* side of the pointee slot per ken gA4: an over-wide copy through
* the resolved place smashes one. */
{ "assign_deref",
"package main;\n"
"type pt = struct { x: size, y: size, z: size };\n"
"export fn main() i32 = {\n"
"\tlet guard1: size = 111: size;\n"
"\tlet d: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"\tlet guard2: size = 222: size;\n"
"\tlet s: pt = pt { x = 8: size, y = 9: size, z = 10: size };\n"
"\tlet p: *pt = &d;\n"
"\t*p = s;\n"
"\tif (guard1 != 111) { return 98; };\n"
"\tif (guard2 != 222) { return 97; };\n"
"\treturn (d.x + d.y + d.z): i32;\n"
"};\n",
27 },
/* Module-let global as BOTH destination (`g = a`, LEAQ g(SB)
* dest) and source (`b = g`, aggarg_srcaddr global arm). */
{ "assign_global",
"package main;\n"
"type pt = struct { x: size, y: size, z: size };\n"
"let g: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"export fn main() i32 = {\n"
"\tlet a: pt = pt { x = 11: size, y = 12: size, z = 13: size };\n"
"\tg = a;\n"
"\tif (g.z != 13) { return 99; };\n"
"\tlet b: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"\tb = g;\n"
"\treturn (b.x + b.y + b.z): i32;\n"
"};\n",
36 },
{ "assign_dot_source",
"package main;\n"
"type inner = struct { x: size, y: size, z: size };\n"
"type outer = struct { tag: size, i: inner };\n"
"export fn main() i32 = {\n"
"\tlet o: outer = outer { tag = 1: size, i = inner { x = 2: size, y = 3: size, z = 4: size } };\n"
"\tlet b: inner = inner { x = 0: size, y = 0: size, z = 0: size };\n"
"\tb = o.i;\n"
"\treturn (b.x + b.z): i32;\n"
"};\n",
6 },
/* Tuple ident reassign rides the same funnel (8B/elem slots). */
{ "assign_tuple",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (size, size) = (4: size, 9: size);\n"
"\tlet u: (size, size) = (0: size, 0: size);\n"
"\tu = t;\n"
"\treturn (u.0 + u.1): i32;\n"
"};\n",
13 },
/* Global structlit reassign — newly wired through the DST_GLOBAL
* fill (pre-fix: one scalar MOVQ of AX≈0 to g(SB)). */
{ "global_structlit",
"package main;\n"
"type pt = struct { x: size, y: size, z: size };\n"
"let g: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"export fn main() i32 = {\n"
"\tg = pt { x = 5: size, y = 6: size, z = 7: size };\n"
"\treturn (g.x + g.y + g.z): i32;\n"
"};\n",
18 },
/* ken R1 (gA3b): ALIAS-named aggregate — the funnel dispatch must
* FULL-chase the alias (type_chase_named / chased tinfo); a
* single peel left cstage at TY_NAMED, fell to the scalar tail
* and word0-copied while wwstage copied full-width (live cs≠ww).
* FIELD-WISE init: the struct-LIT spelling can't pin the ww side
* (it louds earlier at the pre-existing task-#7 aggregate-let
* bound — the #5 alias-arc's hole, not this funnel's). */
{ "alias_named_assign",
"package main;\n"
"type st = struct { a: size, b: size };\n"
"type row = st;\n"
"export fn main() i32 = {\n"
"\tlet a: row;\n"
"\ta.a = 4; a.b = 9;\n"
"\tlet b: row;\n"
"\tb.a = 0; b.b = 0;\n"
"\tb = a;\n"
"\tif (b.a != 4) { return 1; };\n"
"\tif (b.b != 9) { return 2; };\n"
"\treturn 0;\n"
"};\n",
0 },
/* ken gA6: TWO-level alias chain (`ali -> base -> struct`) — pins
* that the dispatch is a full CHASE, not a deeper fixed peel. */
{ "alias_twolevel_assign",
"package main;\n"
"type ali = base;\n"
"type base = struct { a: size, b: size, c: size };\n"
"export fn main() i32 = {\n"
"\tlet a: ali;\n"
"\ta.a = 4; a.b = 9; a.c = 13;\n"
"\tlet b: ali;\n"
"\tb.a = 0; b.b = 0; b.c = 0;\n"
"\tb = a;\n"
"\tif (b.a != 4) { return 1; };\n"
"\tif (b.b != 9) { return 2; };\n"
"\tif (b.c != 13) { return 3; };\n"
"\treturn 0;\n"
"};\n",
0 },
/* Neighbor guards on both sides of the destination slot: a copy
* WIDER than size(T) (the inverse regression) smashes one. */
{ "neighbor_guard",
"package main;\n"
"type pt = struct { x: size, y: size, z: size };\n"
"export fn main() i32 = {\n"
"\tlet guard1: size = 111: size;\n"
"\tlet b: pt = pt { x = 0: size, y = 0: size, z = 0: size };\n"
"\tlet guard2: size = 222: size;\n"
"\tlet a: pt = pt { x = 1: size, y = 2: size, z = 3: size };\n"
"\tb = a;\n"
"\tif (guard1 != 111) { return 1; };\n"
"\tif (guard2 != 222) { return 2; };\n"
"\tif (b.x + b.y + b.z != 6) { return 3; };\n"
"\treturn 0;\n"
"};\n",
0 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/aggas_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/aggas_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
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); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. The class is gate-blind (both stages were wrong
* identically), so byte-id here pins that the CONVERGED fix stays
* converged. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/aggas_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/aggas_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/aggas_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;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
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[1024];
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[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "agg_assign_width: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"agg_assign_width[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"agg_assign_width: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("agg_assign_width: %d/%d ok\n", total, total);
return 0;
}