diff --git a/Makefile b/Makefile index 2671dce6..e735fe06 100644 --- a/Makefile +++ b/Makefile @@ -595,6 +595,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_valstruct_subsize_run \ $(BIN)/test_aggret_source_run \ $(BIN)/test_oddstruct_byval_ret_run \ + $(BIN)/test_chained_dot_struct_copy_run \ $(BIN)/test_continue_run \ $(BIN)/test_callret_unsigned_arith_run \ $(BIN)/test_sar_shr_run \ @@ -3425,6 +3426,12 @@ $(BIN)/test_oddstruct_byval_ret_run: test/wcc/949_oddstruct_byval_ret_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_chained_dot_struct_copy_run: test/wcc/949_chained_dot_struct_copy_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_continue_run: test/wcc/911_continue_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 318d43a7..7d5c2ce8 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6365,19 +6365,57 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BP, base_disp + total_off + k)); k += 8; } - if (k < ssz) { - int tail = ssz - k; - int lop = (tail == 4) ? A_MOVL - : (tail == 1 ? A_MOVB : A_MOVQ); - ins2(c, lop, + /* Descending 4/2/1 sized tail, not a + * 4/1-then-MOVQ: ssz is the field's + * natural (non-slot-padded) struct size, + * so a tail of {2,3,5,6,7} packs at its + * own stride (struct{u16,[8]u8}=10 has + * tail 2). The old `tail==1?MOVB:MOVQ` + * fell every other tail to an 8B MOVQ that + * over-writes past the destination FIELD — + * a @packed neighbour gets clobbered (the + * depth-≥2 chained `t.m.l = s` wrote s's + * slop over t.m.z, exit 0 not 222) and + * cstage diverged from wwstage's sized + * MOVW (gate-blind cs≠ww). Aligns cstage + * UP to the wwstage 8/4/2/1 ladder + * (cgenexpr.ww:10083) and cg_aggcopy. + * #107 class. */ + if (k + 4 <= ssz) { + ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) - ins2(c, lop, areg(D_AX), + ins2(c, A_MOVL, areg(D_AX), amem(D_CX, total_off + k)); else - ins2(c, lop, areg(D_AX), + ins2(c, A_MOVL, areg(D_AX), amem(D_BP, base_disp + total_off + k)); + k += 4; + } + if (k + 2 <= ssz) { + ins2(c, A_MOVW, + amem(D_BP, soff + k), + areg(D_AX)); + if (via_cx) + ins2(c, A_MOVW, areg(D_AX), + amem(D_CX, total_off + k)); + else + ins2(c, A_MOVW, areg(D_AX), + amem(D_BP, base_disp + total_off + k)); + k += 2; + } + if (k + 1 <= ssz) { + ins2(c, A_MOVB, + amem(D_BP, soff + k), + areg(D_AX)); + if (via_cx) + ins2(c, A_MOVB, areg(D_AX), + amem(D_CX, total_off + k)); + else + ins2(c, A_MOVB, areg(D_AX), + amem(D_BP, base_disp + total_off + k)); + k += 1; } break; } diff --git a/test/wcc/949_chained_dot_struct_copy_run.c b/test/wcc/949_chained_dot_struct_copy_run.c new file mode 100644 index 00000000..0273072f --- /dev/null +++ b/test/wcc/949_chained_dot_struct_copy_run.c @@ -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 +#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; +} + +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; +}