/* * 681_arr_elem_field_write — `arr[i].field = v` (write-side counterpart * of 680_arr_elem_field). Both stages had a silent store-drop: * * cstage's chained-pointer-field-write branch (cgen.c near 1986) caught * `[N]*Struct` writes via its `!= N_IDENT` guard but skipped `[N]Struct` * value-arrays (TY_STRUCT element fails the TY_PTR guard). * * wwstage had no N_DOT(N_INDEX,...) lhs branch at all in cgassign * (cgenexpr.ww). Both shapes silently emitted no store. * * Closed in task #16 by mirroring task #8's read-side N_INDEX-lhs branch * onto the write side (fldstoreop, str/float leaf coverage, deref-or-not * for `*Struct` vs `Struct` element). * * Coverage — both array shapes, scalar/sub-word/str/float leaves, dyn * idx, and read-modify-write compound (`arr[i].x += 5`). Cstage and * wwstage on every fixture; wwstage gated on access(X_OK). */ #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[] = { /* [N]*Struct, i32 field write. Sole write into stk[0].x; read it * back. Returns 42. */ { "ptr_arr_i32_write", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let v: nd; v.name = \"hi\"; v.x = 0;\n" " let stk: [16]*nd; stk[0] = &v;\n" " stk[0].x = 42;\n" " return stk[0].x;\n" "};\n", 42 }, /* [N]*Struct, u8 field write. Sub-word store via MOVB through * fieldstoreop. Returns 9. */ { "ptr_arr_u8_write", "type nd = struct { tag: u8, pad: u8, x: i32 };\n" "fn main() i32 = {\n" " let v: nd; v.tag = 0u8; v.pad = 0u8; v.x = 0;\n" " let stk: [4]*nd; stk[0] = &v;\n" " stk[0].tag = 9u8;\n" " return stk[0].tag: i32;\n" "};\n", 9 }, /* [N]*Struct, str field write — 16B store of both ptr+len. * Returns len("hello") = 5. */ { "ptr_arr_str_write", "type nd = struct { a: i32, b: i32, name: str };\n" "fn main() i32 = {\n" " let v: nd; v.a = 0; v.b = 0; v.name = \"old\";\n" " let stk: [16]*nd; stk[0] = &v;\n" " stk[0].name = \"hello\";\n" " return stk[0].name.len: i32;\n" "};\n", 5 }, /* [N]Struct, i32 field write (value-array). The cstage chained- * pointer-field branch's TY_PTR guard skips this; without the new * value-array path the store silently drops. Returns 50. */ { "val_arr_i32_write", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[2].x = 50;\n" " return arr[2].x;\n" "};\n", 50 }, /* [N]Struct, u8 field write — value-array sub-word store. Returns 7. */ { "val_arr_u8_write", "type nd = struct { tag: u8, pad: u8, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[1].tag = 7u8;\n" " return arr[1].tag: i32;\n" "};\n", 7 }, /* [N]Struct, str field write — value-array 16B store. Returns * len("world") = 5. */ { "val_arr_str_write", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[1].name = \"world\";\n" " return arr[1].name.len: i32;\n" "};\n", 5 }, /* Sub-word signed: write -3i8 then read back as i32. If MOVB * stored the truncated low byte and the field's fldloadop sign- * extends correctly, the i32 read returns -3. Returns 42 when * the equality check passes, 0 otherwise. */ { "ptr_arr_i8_signed_write", "type nd = struct { tag: i8, pad: u8, x: i32 };\n" "fn main() i32 = {\n" " let v: nd; v.tag = 0i8; v.pad = 0u8; v.x = 0;\n" " let stk: [4]*nd; stk[0] = &v;\n" " stk[0].tag = -3i8;\n" " let t: i32 = stk[0].tag: i32;\n" " if (t == -3) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* Value-array sub-word signed: distinct from ptr_arr_i8_signed — * this path takes LEAQ &arr[i] (no MOVQ-to-deref) and stores the * truncated byte via fldstoreop MOVB. Read back via fldloadop * MOVSBQ sign-extends to i32; -3 round-trips. Returns 42. */ { "val_arr_i8_signed_write", "type nd = struct { tag: i8, pad: u8, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[1].tag = -3i8;\n" " let t: i32 = arr[1].tag: i32;\n" " if (t == -3) { return 42; };\n" " return 0;\n" "};\n", 42 }, /* f64 field write through value-array — pins MOVSD from X0 into * field offset of &arr[i]. Returns 23. */ { "val_arr_f64_write", "type nd = struct { x: i32, d: f64 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[1].d = 23.0f64;\n" " let v: f64 = arr[1].d;\n" " return v: i32;\n" "};\n", 23 }, /* Dyn idx through [N]*Struct write — index isn't a literal, so * the IMULQ path fires. Returns 99. */ { "ptr_arr_dyn_idx_write", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let a: nd; a.name = \"a\"; a.x = 0;\n" " let b: nd; b.name = \"b\"; b.x = 0;\n" " let c: nd; c.name = \"c\"; c.x = 0;\n" " let stk: [4]*nd;\n" " stk[0] = &a; stk[1] = &b; stk[2] = &c;\n" " let i: i32 = 2;\n" " stk[i].x = 99;\n" " return stk[2].x;\n" "};\n", 99 }, /* Read-modify-write compound on [N]Struct — exercises BOTH the * read (load old field) and the write (store combined). For * `arr[1].x += 5` with arr[1].x = 37 → 42. */ { "val_arr_compound_plus", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[1].x = 37;\n" " arr[1].x += 5;\n" " return arr[1].x;\n" "};\n", 42 }, /* Non-PLUSEQ compound (CARETEQ) on [N]Struct — exercises the XORQ * arm of the compound switch. Worker wired all six integer * compound ops; this pins one of the non-PLUSEQ arms so a future * regression in the switch table is caught. 0x2A ^ 0x14 = 0x3E * (62), then return 62 - 20 = 42. */ { "val_arr_compound_xor", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let arr: [4]nd;\n" " arr[2].x = 42;\n" " arr[2].x ^= 20;\n" " return arr[2].x - 20;\n" "};\n", 42 }, /* Compound through [N]*Struct — same shape but viaptr is true, * so the address compute deref-loads (BX), and the load_op picks * MOVSXD for i32. 10 += 22 → 32. */ { "ptr_arr_compound_plus", "type nd = struct { name: str, x: i32 };\n" "fn main() i32 = {\n" " let v: nd; v.name = \"a\"; v.x = 10;\n" " let stk: [4]*nd; stk[0] = &v;\n" " stk[0].x += 22;\n" " return stk[0].x;\n" "};\n", 32 }, /* Whole-slice rhs to struct field, direct struct local (task #24). * cgen N_ASSIGN had a TY_STR branch that stored ptr+len at +0/+8 * but no TY_SLICE branch; the fallthrough generic store wrote only * AX (ptr), silently dropping .len and .cap. Fixed by a parallel * TY_SLICE branch that stores AX/BX/CX at +0/+8/+16. The bug bit * bufio.init's `b.rbuf = rbuf` (first stdlib struct with a []u8 * field). Returns 42 when len, cap, and a trailing scalar field * all survived the store. */ { "slice_field_value_write", "type bs = struct { rbuf: []u8, mark: i32 };\n" "fn main() i32 = {\n" " let raw: [8]u8; raw[0] = 0u8;\n" " let rbuf: []u8 = raw[0:5];\n" " let b: bs;\n" " b.mark = 7;\n" " b.rbuf = rbuf;\n" " if (b.rbuf.len != 5) { return 1; };\n" " if (b.rbuf.cap != 8) { return 2; };\n" " if (b.mark != 7) { return 3; };\n" " return 42;\n" "};\n", 42 }, /* Same bug via pointer to struct — the bufio scenario exactly * (`init(b: *bstream, rbuf: []u8) { b.rbuf = rbuf; }`). The via_ptr * arm of the new TY_SLICE branch stages the struct addr in DX to * avoid clobbering CX (cap). */ { "slice_field_ptr_write", "type bs = struct { rbuf: []u8, mark: i32 };\n" "fn init(b: *bs, rbuf: []u8) void = {\n" " b.rbuf = rbuf;\n" " b.mark = 9;\n" "};\n" "fn main() i32 = {\n" " let raw: [8]u8; raw[0] = 0u8;\n" " let b: bs;\n" " init(&b, raw[0:5]);\n" " if (b.rbuf.len != 5) { return 1; };\n" " if (b.rbuf.cap != 8) { return 2; };\n" " if (b.mark != 9) { return 3; };\n" " return 42;\n" "};\n", 42 }, /* Distinct byte-pattern check — independent verification that * the AX/BX/CX MOVQs landed at +0/+8/+16 rather than passing by * coincidence with neighbouring zero memory. Distinct byte * patterns at raw[0] (0xAA) and raw[5] (0xFF) are read back * through b.rbuf[i], which only succeeds if .ptr (AX) survived * at +0. Trailing mark uses a distinctive value (0x33) so a * stray CX store one slot too far is caught separately. Here * .len=6 (hi-lo) and .cap=16 (base_cap-lo; raw is [16]u8) per * project #20, so this row pins .ptr survival and additionally * confirms .len and .cap land as distinct words (they no longer * coincide), while the other rows pin .len/.cap distinctness * from the zero default. */ { "slice_field_distinct_bytes", "type bs = struct { rbuf: []u8, mark: i32 };\n" "fn main() i32 = {\n" " let raw: [16]u8;\n" " raw[0] = 0xAAu8; raw[1] = 0xBBu8; raw[2] = 0xCCu8;\n" " raw[3] = 0xDDu8; raw[4] = 0xEEu8; raw[5] = 0xFFu8;\n" " let b: bs;\n" " b.mark = 0x33;\n" " b.rbuf = raw[0:6];\n" " if (b.rbuf.len != 6) { return 1; };\n" " if (b.rbuf.cap != 16) { return 2; };\n" " if (b.mark != 0x33) { return 3; };\n" " if (b.rbuf[0] != 0xAAu8) { return 4; };\n" " if (b.rbuf[5] != 0xFFu8) { return 5; };\n" " return 42;\n" "};\n", 42 }, /* Str-field regression for the parallel TY_STR branch right above * the new TY_SLICE branch — pin that store path so a future refactor * doesn't drop it. Direct struct local; `b.s = "hello"` stores * AX/BX at +0/+8, and the trailing i32 mark survives. */ { "str_field_value_write_regression", "type bs = struct { s: str, mark: i32 };\n" "fn main() i32 = {\n" " let b: bs;\n" " b.mark = 11;\n" " b.s = \"hello\";\n" " if (b.s.len != 5) { return 1; };\n" " if (b.mark != 11) { return 2; };\n" " return 42;\n" "};\n", 42 }, /* Whole-struct rhs to struct field (task #25). cgen N_ASSIGN had * special-cases for TY_STR (2 words) and TY_SLICE (3 words) but no * TY_STRUCT branch — the fallthrough fldstoreop wrote only AX, * dropping every trailing word. Fixed by a word-copy branch that * runs MOVQ from rhs slot to dest field, plus a 4/1-byte ragged * tail. The 16B row is the BORDER case: same total size as TY_STR * but must NOT take the str branch (str field has 16B header; this * is a 2-word struct value, structurally identical but type- * dispatched). Distinct byte values per word so a stray store at * the wrong offset surfaces as a misvalue, not coincidental zero. */ { "struct_field_value_write_16B", "type inner = struct { a: i64, b: i64 };\n" "type outer = struct { i: inner, mark: i32 };\n" "fn main() i32 = {\n" " let o: inner;\n" " o.a = 0x1122334455667788i64;\n" " o.b = 0x99aabbccddeeff00i64;\n" " let x: outer;\n" " x.mark = 0x55;\n" " x.i = o;\n" " if (x.i.a != 0x1122334455667788i64) { return 1; };\n" " if (x.i.b != 0x99aabbccddeeff00i64) { return 2; };\n" " if (x.mark != 0x55) { return 3; };\n" " return 42;\n" "};\n", 42 }, /* 24B struct rhs (the original #25 probe case). Three words at +0/+8/+16 * exercise the word-copy loop with no ragged tail. Distinct byte * patterns pin each word independently. */ { "struct_field_value_write_24B", "type inner = struct { a: i64, b: i64, c: i64 };\n" "type outer = struct { i: inner, mark: i32 };\n" "fn main() i32 = {\n" " let o: inner;\n" " o.a = 0x0a0a0a0a0a0a0a0ai64;\n" " o.b = 0x1b1b1b1b1b1b1b1bi64;\n" " o.c = 0x2c2c2c2c2c2c2c2ci64;\n" " let x: outer;\n" " x.mark = 7;\n" " x.i = o;\n" " if (x.i.a != 0x0a0a0a0a0a0a0a0ai64) { return 1; };\n" " if (x.i.b != 0x1b1b1b1b1b1b1b1bi64) { return 2; };\n" " if (x.i.c != 0x2c2c2c2c2c2c2c2ci64) { return 3; };\n" " if (x.mark != 7) { return 4; };\n" " return 42;\n" "};\n", 42 }, /* 32B struct rhs — four-word copy, no ragged tail. Trailing mark in * the outer struct catches a stray fifth-word store. */ { "struct_field_value_write_32B", "type inner = struct { a: i64, b: i64, c: i64, d: i64 };\n" "type outer = struct { i: inner, mark: i32 };\n" "fn main() i32 = {\n" " let o: inner;\n" " o.a = 0x0a0a0a0a0a0a0a0ai64;\n" " o.b = 0x1b1b1b1b1b1b1b1bi64;\n" " o.c = 0x2c2c2c2c2c2c2c2ci64;\n" " o.d = 0x3d3d3d3d3d3d3d3di64;\n" " let x: outer;\n" " x.mark = 0x33;\n" " x.i = o;\n" " if (x.i.a != 0x0a0a0a0a0a0a0a0ai64) { return 1; };\n" " if (x.i.b != 0x1b1b1b1b1b1b1b1bi64) { return 2; };\n" " if (x.i.c != 0x2c2c2c2c2c2c2c2ci64) { return 3; };\n" " if (x.i.d != 0x3d3d3d3d3d3d3d3di64) { return 4; };\n" " if (x.mark != 0x33) { return 5; };\n" " return 42;\n" "};\n", 42 }, /* Nested struct field write through chained N_DOT spine * (`b.inner.deep = other_deep`). Confirms #22's spine walker * handles a TY_STRUCT terminal, not just scalar/str/slice. The * 24B inner struct payload must survive both the spine walk and * the word-copy. */ { "struct_field_value_write_nested_chain", "type deep = struct { a: i64, b: i64, c: i64 };\n" "type mid = struct { d: deep, mark: i32 };\n" "type outer = struct { m: mid, tag: i32 };\n" "fn main() i32 = {\n" " let other: deep;\n" " other.a = 0x0a0a0a0a0a0a0a0ai64;\n" " other.b = 0x1b1b1b1b1b1b1b1bi64;\n" " other.c = 0x2c2c2c2c2c2c2c2ci64;\n" " let v: outer;\n" " v.m.mark = 5;\n" " v.tag = 9;\n" " v.m.d = other;\n" " if (v.m.d.a != 0x0a0a0a0a0a0a0a0ai64) { return 1; };\n" " if (v.m.d.b != 0x1b1b1b1b1b1b1b1bi64) { return 2; };\n" " if (v.m.d.c != 0x2c2c2c2c2c2c2c2ci64) { return 3; };\n" " if (v.m.mark != 5) { return 4; };\n" " if (v.tag != 9) { return 5; };\n" " return 42;\n" "};\n", 42 }, /* Whole-struct rhs to a struct field via a *struct base — exercises * the via_ptr arm of the new TY_STRUCT branch. Stages the dest addr * in BX (MOVQ off(BP),BX) before iterating, then writes at fi.foff+k(BX). */ { "struct_field_value_write_via_ptr", "type inner = struct { a: i64, b: i64, c: i64 };\n" "type outer = struct { i: inner, mark: i32 };\n" "fn fill(x: *outer) void = {\n" " let o: inner;\n" " o.a = 0x0a0a0a0a0a0a0a0ai64;\n" " o.b = 0x1b1b1b1b1b1b1b1bi64;\n" " o.c = 0x2c2c2c2c2c2c2c2ci64;\n" " x.mark = 7;\n" " x.i = o;\n" "};\n" "fn main() i32 = {\n" " let x: outer;\n" " fill(&x);\n" " if (x.i.a != 0x0a0a0a0a0a0a0a0ai64) { return 1; };\n" " if (x.i.b != 0x1b1b1b1b1b1b1b1bi64) { return 2; };\n" " if (x.i.c != 0x2c2c2c2c2c2c2c2ci64) { return 3; };\n" " if (x.mark != 7) { return 4; };\n" " return 42;\n" "};\n", 42 }, /* Ragged tail — 12B inner struct (3 i32 fields, maxalign=4, no 8B * round-up) lands the word-copy loop at k=8 with 4 bytes left and * pins the MOVL tail branch. Tail-only structs are reachable for * any TY_STRUCT whose maxalign < 8: this is the smallest such * shape. The trailing i32 mark in the outer struct catches a stray * MOVQ tail (which would overwrite +4 of the mark slot). The 5/3/2 * byte tails (struct of just i8 fields) fall back to MOVQ in cgen * and would overwrite past the field boundary; the language permits * align==1 structs but they're not exercised here — filed as a * sub-followup if a real callsite surfaces. */ { "struct_field_value_write_ragged_tail_12B", "type inner = struct { a: i32, b: i32, c: i32 };\n" "type outer = struct { i: inner, mark: i32 };\n" "fn main() i32 = {\n" " let o: inner;\n" " o.a = 0x11223344;\n" " o.b = 0x55667788;\n" " o.c = 0x29aabbcc;\n" " let x: outer;\n" " x.mark = 0x33;\n" " x.i = o;\n" " if (x.i.a != 0x11223344) { return 1; };\n" " if (x.i.b != 0x55667788) { return 2; };\n" " if (x.i.c != 0x29aabbcc) { return 3; };\n" " if (x.mark != 0x33) { return 4; };\n" " return 42;\n" "};\n", 42 }, /* Whole-tagged rhs to struct tagged-union field (task #26). cgen * N_ASSIGN had a TY_TAGGED branch that called cg_tag_for_variant * with the rhs type — when the rhs was the tagged union itself * (not a concrete variant), the function returned -1 and the * branch wrote literal 0 as the tag plus only AX as the payload, * dropping the original tag and trailing payload words. Fixed * by routing through cg_widen_tagged_store, which copies all slot * words from the source's tagged slot and runs cg_widen_tag_remap. * * Verification: the write must not bleed into the trailing `mark` * field. Direct struct local. Reading x.e back via match is a * separate code path (gated on a wwstage read-side bug for * tagged N_DOT — filed below as a follow-up); these rows pin the * write-side bug and let mark act as a canary for stray stores * past the field boundary. */ { "tagged_field_value_write_local_mark_canary", "type ev = (i64 | i32);\n" "type holder = struct { e: ev, mark: i32 };\n" "fn main() i32 = {\n" " let v: ev = 33: i32;\n" " let x: holder;\n" " x.mark = 0x3a3a3a3a;\n" " x.e = v;\n" " if (x.mark != 0x3a3a3a3a) { return 1; };\n" " return 42;\n" "};\n", 42 }, /* Same shape, concrete-variant str widening. The str payload * occupies +8/+16 (ptr/len); a missing store to +16 (the pre-#26 * shape that wrote only AX) would leave .len at whatever the * neighbouring memory held. mark canary at +24 catches a stray * AX-only path that wrote past 16B into the mark slot. */ { "tagged_field_value_write_str_variant_mark_canary", "type ev = (i32 | str);\n" "type holder = struct { e: ev, mark: i32 };\n" "fn main() i32 = {\n" " let x: holder;\n" " x.mark = 0x5e5e5e5e;\n" " x.e = (\"hello\": ev);\n" " if (x.mark != 0x5e5e5e5e) { return 1; };\n" " return 42;\n" "};\n", 42 }, /* Pointer-rooted dst — `(*p).e = v` exercises the base-reg * parameterisation added to cg_widen_tagged_store in #26. The * wrapper spills BX (the *struct pointer), routes the body * through a BP-rooted scratch, reloads BX, and word-copies the * scratch to (BX, foff). Without that path the via_ptr arm of * the new TY_TAGGED branch would either trample BX during cgexpr * or write to a stale slot address. mark canary at +24 catches * a stray spill that wrote past the e field. */ { "tagged_field_value_write_via_ptr_mark_canary", "type ev = (i64 | i32);\n" "type holder = struct { e: ev, mark: i32 };\n" "fn fill(h: *holder) void = {\n" " let v: ev = 33: i32;\n" " h.mark = 0x77777777;\n" " h.e = v;\n" "};\n" "fn main() i32 = {\n" " let x: holder;\n" " fill(&x);\n" " if (x.mark != 0x77777777) { return 1; };\n" " return 42;\n" "};\n", 42 }, }; static int run_driver(const char *driver, const struct row *r, int i) { char tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/waew_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/waew_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/waew_%d_%d", tmpdir, getpid(), i); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(src, "wb"); if (!f) { runwait(rmcmd); return -1; } fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s", driver, outbin, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); runwait(rmcmd); return -1; } int got = runwait(outbin); runwait(rmcmd); return got; } 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, "arr_elem_field_write: 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, "arr_elem_field_write[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (fail) { fprintf(stderr, "arr_elem_field_write: %d/%d fixtures failed\n", fail, total); return 1; } printf("arr_elem_field_write: %d/%d ok\n", total, total); return 0; }