w6c: chained-DOT struct field-copy tail uses 4/2/1 ladder, not over-MOVQ (#9)

The depth->=2 chained `t.m.l = s` struct-ident field copy selected its tail with (tail==4)?MOVL:(tail==1?MOVB:MOVQ), so every tail in {2,3,5,6,7} fell to an 8-byte MOVQ that over-wrote past the destination field — clobbering a @packed neighbour (t.m.l=s wrote s's slop over t.m.z: cstage exit 0 vs wwstage's correct 222) and diverging cs!=ww gate-blind. Replace with a descending 4/2/1 MOVL/MOVW/MOVB ladder comparing to ssz (the field's natural type-table size), aligning cstage UP to wwstage's sized ladder + cg_aggcopy. New table-driven gate 949_chained_dot_struct_copy_run (7 sizes x runtime-exit oracle on both drivers + cs==ww byte-id; negative-control proven).
This commit is contained in:
2026-06-19 13:41:44 +09:00
parent 1466f50a09
commit 801d105c0f
3 changed files with 386 additions and 7 deletions

View File

@@ -0,0 +1,334 @@
/*
* 949_chained_dot_struct_copy_run — #107-class sibling. A depth-≥2
* chained-DOT struct-field store from a struct IDENT source
* (`t.m.l = s;`, lhs.lhs is itself an N_DOT) copies the source slot
* word-by-word into the destination field. The cstage walker
* (cgen.c, TY_STRUCT N_IDENT arm of the chained-DOT N_ASSIGN branch)
* ran a MOVQ run then a SINGLE sized-tail word, but its tail selector
* was `tail==4 ? MOVL : tail==1 ? MOVB : MOVQ` — every OTHER tail
* width {2,3,5,6,7} fell to an 8-byte MOVQ.
*
* The copy length is the field's NATURAL (non-slot-padded) struct
* size: a `@packed` or sub-8-aligned leaf packs at its own stride
* (struct @packed {u8,u8,u8}=3, {u16,u32}=6, …). When that stride is
* not a {1,4}+8k width, the over-MOVQ wrote 8 bytes into the
* destination FIELD and clobbered the @packed neighbour that follows
* it within 8 bytes — silently. Pre-fix `t.m.l = s` wrote s's slop
* over t.m.z (read-back 0, not the planted value), AND cstage diverged
* from wwstage, which already emitted a descending 8/4/2/1 sized tail
* (cgenexpr.ww:10083) — a gate-blind cs≠ww divergence. The fix aligns
* cstage UP to that ladder (mirrors cg_aggcopy and #107).
*
* GATE POLARITY: must stay GREEN. Two independent oracles per row:
* - the runtime exit code = the value of the @packed NEIGHBOUR field
* t.m.z, read back AFTER the `t.m.l = s` copy. A wrong copy width
* over-writes it; byte-id is BLIND to a both-wrong pair, and the
* pre-fix cstage drop was exit 0 (the planted value silently lost).
* - w6c vs w6c_ww .s byte-identity (rule 10).
*
* Leaf size coverage = the tail widths the bug lived in {2,3,5,6,7}
* plus boundaries {1,4,8} the old selector already handled (MOVB / MOVL
* / full MOVQ run, no tail) — those lock that the new descending ladder
* did NOT perturb the paths that were already correct, including the
* new ladder's own k+1<=ssz MOVB arm reached when tail is 1. The
* neighbour value is the oracle, computed in the row table (no derived
* size literal).
*/
#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[] = {
/* tail 2: Leaf = struct{u16,[8]u8} = 10B, over-MOVQ at offset 8
* (tail 2) clobbers the @packed neighbour z at offset 10. */
{ "tail2_leaf10",
"package main;\n"
"type Leaf = struct { a: u16, b: [8]u8 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 0u16, b = [9u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8] };\n"
" let t = Top { m = Mid { l = Leaf { a=0u16, b=[0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8] }, z = 222u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
222 },
/* tail 3: Leaf = @packed{u8,u8,u8} = 3B, whole copy is a 3-byte
* stride; over-MOVQ at offset 0 wrote 8 bytes over z at offset 3. */
{ "tail3_leaf3",
"package main;\n"
"type Leaf = struct @packed { a: u8, b: u8, c: u8 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 1u8, b = 2u8, c = 3u8 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u8, b=0u8, c=0u8 }, z = 88u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
88 },
/* tail 4: Leaf = @packed{u32} = 4B — MOVL (already correct
* pre-fix); locks the new ladder leaves this path identical. */
{ "tail4_leaf4",
"package main;\n"
"type Leaf = struct @packed { a: u32 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 7u32 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u32 }, z = 123u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
123 },
/* tail 5: Leaf = @packed{u8,u32} = 5B, over-MOVQ at offset 0 wrote
* 8 bytes over z at offset 5. */
{ "tail5_leaf5",
"package main;\n"
"type Leaf = struct @packed { a: u8, b: u32 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 1u8, b = 0u32 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u8, b=0u32 }, z = 177u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
177 },
/* tail 6: Leaf = @packed{u16,u32} = 6B. */
{ "tail6_leaf6",
"package main;\n"
"type Leaf = struct @packed { a: u16, b: u32 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 1u16, b = 0u32 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u16, b=0u32 }, z = 99u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
99 },
/* tail 7: Leaf = @packed{u8,u16,u32} = 7B. */
{ "tail7_leaf7",
"package main;\n"
"type Leaf = struct @packed { a: u8, b: u16, c: u32 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 1u8, b = 2u16, c = 0u32 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u8, b=0u16, c=0u32 }, z = 111u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
111 },
/* via_cx tail 2: GLOBAL destination Top. A global (or ptr-root)
* chain stores CX-relative ((CX)/8(CX)) rather than BP-relative;
* the same 4/2/1 ladder feeds the other dest mode, so this row
* locks the via_cx arm a BP-local `t` never reaches. */
{ "viacx_global_tail2",
"package main;\n"
"type Leaf = struct { a: u16, b: [8]u8 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"let g: Top = Top { m = Mid { l = Leaf { a=0u16, b=[0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8] }, z = 222u8 }, tail = 0u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 0u16, b = [9u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8] };\n"
" g.m.l = s;\n"
" return g.m.z: i32;\n"
"};\n",
222 },
/* tail 1: Leaf = @packed{u8} = 1B — single MOVB, no MOVQ run;
* always correct (old MOVB == new ladder's k+1<=ssz MOVB), so this
* row locks the new descending ladder's 1-byte arm against a future
* regression. */
{ "tail1_leaf1",
"package main;\n"
"type Leaf = struct @packed { a: u8 };\n"
"type Mid = struct @packed { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 5u8 };\n"
" let t = Top { m = Mid { l = Leaf { a=0u8 }, z = 144u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
144 },
/* tail 0: Leaf = struct{i64,i64} = 16B — pure MOVQ run, no tail;
* locks the run path is undisturbed. Natural (non-packed) Mid so z
* is 8-aligned at offset 16 anyway. */
{ "tail0_leaf16",
"package main;\n"
"type Leaf = struct { a: i64, b: i64 };\n"
"type Mid = struct { l: Leaf, z: u8 };\n"
"type Top = struct { m: Mid, tail: u64 };\n"
"fn main() i32 = {\n"
" let s = Leaf { a = 5i64, b = 6i64 };\n"
" let t = Top { m = Mid { l = Leaf { a=0i64, b=0i64 }, z = 200u8 }, tail = 0u64 };\n"
" t.m.l = s;\n"
" return t.m.z: i32;\n"
"};\n",
200 },
};
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/chdotcp_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/chdotcp_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -2;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %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 -2;
}
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;
}
/* w6c (cstage) vs w6c_ww (wwstage) .s byte-id (rule 10). */
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/chdotcp_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/chdotcp_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/chdotcp_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, "timeout 180 %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, "timeout 180 %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], wdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated; }
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 && access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "chained_dot_struct_copy: 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,
"chained_dot_struct_copy[%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,
"chained_dot_struct_copy: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("chained_dot_struct_copy: %d/%d ok\n", total, total);
return 0;
}