From c4347499eedabf2054f3d0958e98c6ac57a0f76a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 18:13:02 +0900 Subject: [PATCH] cstage+selfhost+test: fix (*p).f silent drop in N_DOT lhs (read+write) Pre-existing landmine surfaced by #5 (whole-STRUCT N_ASSIGN). Both stages' N_DOT dispatch gated on `lhs->lhs->kind == N_IDENT`; the parser produces N_UN(STAR, IDENT(p)) for `(*p).f`, so both sides fell off: - Write side (cgassign N_DOT base): emitted nothing, store dropped. - Read side (case N_DOT pointer-auto-deref): cgexpr derefed the pointer as a scalar, AX = first qword of struct, field offset dropped. Fix: retarget base / dot_lhs to the inner IDENT when shape is N_UN(STAR, IDENT). The existing via_ptr branch fires identically to `p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` (non-IDENT pointer expression) is tracked separately as task #19. 702 covers 7 rows: write_i64/i32/str, read_i64/i32/str_len, roundtrip --- Makefile | 7 + cmd/w6c/cgen.c | 53 ++++- selfhost/cmd/w6c/main.combined.ww | 40 +++- selfhost/cmd/wcc/cgenexpr.ww | 40 +++- selfhost/cmd/wwdump/main.combined.ww | 40 +++- test/wcc/702_dot_explicit_deref.c | 297 +++++++++++++++++++++++++++ 6 files changed, 456 insertions(+), 21 deletions(-) create mode 100644 test/wcc/702_dot_explicit_deref.c diff --git a/Makefile b/Makefile index c8f14c68..089e7c9b 100644 --- a/Makefile +++ b/Makefile @@ -228,6 +228,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_samemod_prefer \ $(BIN)/test_cgreturn_struct \ $(BIN)/test_cgassign_struct \ + $(BIN)/test_dot_explicit_deref \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -381,6 +382,12 @@ $(BIN)/test_cgassign_struct: test/wcc/701_cgassign_struct.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dot_explicit_deref: test/wcc/702_dot_explicit_deref.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_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f0a03089..5b5a1dd0 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1834,10 +1834,22 @@ cgexpr(Cg *c, Node *n, Local *locals) } /* p.x = v or p.x += v where p.x is a struct field * (direct or via *struct). For compound ops we read-modify- - * write the field; for plain `=` we just write. */ + * write the field; for plain `=` we just write. The base + * accepts two parser shapes: a bare IDENT (auto-deref when + * the IDENT's type is *T, value-struct otherwise) and the + * explicit-deref form `(*p).f = ...` where the parser emits + * N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the + * inner IDENT so the via_ptr branch fires identically to + * `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f + * (non-IDENT inner) falls through to the existing drop + * behaviour pending follow-up task. */ if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && - n->lhs->lhs->kind == N_IDENT) { + (n->lhs->lhs->kind == N_IDENT || + (n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR + && n->lhs->lhs->lhs + && n->lhs->lhs->lhs->kind == N_IDENT))) { Node *base = n->lhs->lhs; + if (base->kind == N_UN) base = base->lhs; Type *bt = base->type; Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int via_ptr = 0; @@ -3480,6 +3492,11 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } } + /* Tagged-union field: delegate to the + * shared widening writer. Handles + * str, scalar, struct literal/ident + * payload, and tagged-subset + * forwarding (with tag remap). */ Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; @@ -4808,7 +4825,19 @@ cgexpr(Cg *c, Node *n, Local *locals) /* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16). * Arrays don't carry a header; .len uses the static size and * .ptr is the address of the first element. */ - Type *bt = n->lhs ? n->lhs->type : NULL; + /* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR, + * IDENT(p)) with type T (post-deref struct). Pull the inner + * IDENT in as dot_lhs so bt resolves to *T and the pointer- + * auto-deref branch below fires (mirror of the N_ASSIGN + * N_DOT lhs retarget). v1 scope: N_IDENT inner only; + * (*expr).f follow-up task pending. Branches that gate on + * `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw + * n->lhs since (*p) isn't either of those shapes. */ + Node *dot_lhs = n->lhs; + if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR + && dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT) + dot_lhs = dot_lhs->lhs; + Type *bt = dot_lhs ? dot_lhs->type : NULL; Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; /* Module-qualified value reference: `mod.name`. The checker * leaves SK_USE idents untyped (NULL/ty_err); detect that and @@ -5224,14 +5253,17 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } /* pointer-to-slice/str field: deref and read pseudo-field. - * Used by helpers like rt_appendu8(s: *[]u8, v: u8). */ + * Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs + * gates the N_IDENT check so `(*p).len` (parser N_UN(STAR, + * IDENT)) emits the same load as `p.len` after the case-top + * retarget. */ if (u && u->kind == TY_PTR && u->sub) { Type *inner = u->sub; if (inner->kind == TY_NAMED) inner = inner->under; if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR) && (lenfld || capfld || ptrfld) - && n->lhs->kind == N_IDENT) { - int off = localfind(locals, n->lhs->str); + && dot_lhs && dot_lhs->kind == N_IDENT) { + int off = localfind(locals, dot_lhs->str); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX)); @@ -5239,13 +5271,16 @@ cgexpr(Cg *c, Node *n, Local *locals) } } /* pointer-to-struct field: deref and load. Common pattern: - * fn move(p: *point) ... { p.x += dx; ... } */ + * fn move(p: *point) ... { p.x += dx; ... } + * dot_lhs gates this branch so both `p.f` (n->lhs is IDENT) + * and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to + * inner IDENT at case-top) emit the same load sequence. */ if (u && u->kind == TY_PTR && u->sub) { Type *inner = u->sub; if (inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT - && n->lhs->kind == N_IDENT) { - int off = localfind(locals, n->lhs->str); + && dot_lhs && dot_lhs->kind == N_IDENT) { + int off = localfind(locals, dot_lhs->str); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); for (Tfield *f = inner->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f452ef14..7bce6030 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9825,6 +9825,26 @@ fn cgmatch(c: *cgen, n: *node) void = { fn cgdot(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; let fld: str = n.str; + // `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR, + // IDENT(p)). Substitute the inner IDENT as dotlhs so the + // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR + // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT + // lhs retarget in cgassign. v1 scope: N_IDENT inner only; + // (*expr).f follow-up task pending. Enum-leaf lookup above and + // chained-N_DOT branches below keep checking raw lhs since + // (*p) is neither shape. + let dotlhs: *node = lhs; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_UN) { + if (dotlhs.op == tkind.TK_STAR) { + if (dotlhs.lhs != nil) { + if (dotlhs.lhs.kind == nkind.N_IDENT) { + dotlhs = dotlhs.lhs; + }; + }; + }; + }; + }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` // → inline the pre-computed constant. With driver-side // concatenation, both forms key off the leaf type name. @@ -9854,9 +9874,9 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - if (lhs != nil) { - if (lhs.kind == nkind.N_IDENT) { - let nm: str = lhs.str; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_IDENT) { + let nm: str = dotlhs.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; @@ -12362,12 +12382,24 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Struct/ptr-to-struct field assignment: `s.f = expr;` or // `p.f = expr;`. Only plain `=` is wired (compound on field - // is rare and not yet needed by our fixtures). + // is rare and not yet needed by our fixtures). Base accepts the + // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) + // by retargeting to the inner IDENT so the via_ptr branch fires + // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { + if (base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + base = base.lhs; + }; + }; + }; + }; if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 557c6a19..b2f38bba 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1166,6 +1166,26 @@ fn cgmatch(c: *cgen, n: *node) void = { fn cgdot(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; let fld: str = n.str; + // `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR, + // IDENT(p)). Substitute the inner IDENT as dotlhs so the + // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR + // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT + // lhs retarget in cgassign. v1 scope: N_IDENT inner only; + // (*expr).f follow-up task pending. Enum-leaf lookup above and + // chained-N_DOT branches below keep checking raw lhs since + // (*p) is neither shape. + let dotlhs: *node = lhs; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_UN) { + if (dotlhs.op == tkind.TK_STAR) { + if (dotlhs.lhs != nil) { + if (dotlhs.lhs.kind == nkind.N_IDENT) { + dotlhs = dotlhs.lhs; + }; + }; + }; + }; + }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` // → inline the pre-computed constant. With driver-side // concatenation, both forms key off the leaf type name. @@ -1195,9 +1215,9 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - if (lhs != nil) { - if (lhs.kind == nkind.N_IDENT) { - let nm: str = lhs.str; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_IDENT) { + let nm: str = dotlhs.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; @@ -3703,12 +3723,24 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Struct/ptr-to-struct field assignment: `s.f = expr;` or // `p.f = expr;`. Only plain `=` is wired (compound on field - // is rare and not yet needed by our fixtures). + // is rare and not yet needed by our fixtures). Base accepts the + // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) + // by retargeting to the inner IDENT so the via_ptr branch fires + // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { + if (base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + base = base.lhs; + }; + }; + }; + }; if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7962cfbd..a10af267 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9825,6 +9825,26 @@ fn cgmatch(c: *cgen, n: *node) void = { fn cgdot(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; let fld: str = n.str; + // `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR, + // IDENT(p)). Substitute the inner IDENT as dotlhs so the + // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR + // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT + // lhs retarget in cgassign. v1 scope: N_IDENT inner only; + // (*expr).f follow-up task pending. Enum-leaf lookup above and + // chained-N_DOT branches below keep checking raw lhs since + // (*p) is neither shape. + let dotlhs: *node = lhs; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_UN) { + if (dotlhs.op == tkind.TK_STAR) { + if (dotlhs.lhs != nil) { + if (dotlhs.lhs.kind == nkind.N_IDENT) { + dotlhs = dotlhs.lhs; + }; + }; + }; + }; + }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` // → inline the pre-computed constant. With driver-side // concatenation, both forms key off the leaf type name. @@ -9854,9 +9874,9 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - if (lhs != nil) { - if (lhs.kind == nkind.N_IDENT) { - let nm: str = lhs.str; + if (dotlhs != nil) { + if (dotlhs.kind == nkind.N_IDENT) { + let nm: str = dotlhs.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; @@ -12362,12 +12382,24 @@ fn cgassign(c: *cgen, n: *node) void = { }; // Struct/ptr-to-struct field assignment: `s.f = expr;` or // `p.f = expr;`. Only plain `=` is wired (compound on field - // is rare and not yet needed by our fixtures). + // is rare and not yet needed by our fixtures). Base accepts the + // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) + // by retargeting to the inner IDENT so the via_ptr branch fires + // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { + if (base.kind == nkind.N_UN) { + if (base.op == tkind.TK_STAR) { + if (base.lhs != nil) { + if (base.lhs.kind == nkind.N_IDENT) { + base = base.lhs; + }; + }; + }; + }; if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); diff --git a/test/wcc/702_dot_explicit_deref.c b/test/wcc/702_dot_explicit_deref.c new file mode 100644 index 00000000..b3b3b536 --- /dev/null +++ b/test/wcc/702_dot_explicit_deref.c @@ -0,0 +1,297 @@ +/* + * 702_dot_explicit_deref — silent miscompile of `(*p).f` (task #16). + * + * Pre-existing landmine surfaced by worker-cgnassign during #5 + * (whole-struct N_ASSIGN). Both cstage cgen.c N_ASSIGN N_DOT and + * wwstage cgenexpr.ww cgassign single-dot dispatch gated on + * `n->lhs->lhs->kind == N_IDENT`. The parser produces N_UN(STAR, + * IDENT(p)) for `(*p).f`, so the store fell off the dispatch and + * silently emitted nothing. Read-side N_DOT had the same gap — + * the pointer-auto-deref load branch required N_IDENT and fell + * through to cgexpr on the N_UN, which derefs the pointer as a + * scalar (load 8B from p into AX, then the field part is dropped). + * + * Not exercised by current selfhost source (auto-deref `p.f` was + * used throughout) so 995_self_rebuild didn't catch it. Latent for + * any future user code that types out the explicit deref. + * + * The fix in both stages is a base/lhs retarget: when the parser + * shape is N_UN(STAR, IDENT(p)), substitute the inner IDENT so + * the existing via_ptr / pointer-auto-deref branch fires the same + * as `p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` with a + * non-IDENT pointer expression (`(*arr[i]).f`, `(*g.p).f`) still + * drops silently, tracked as a follow-up — needs cgexpr(inner) → + * reg + RHS-spill scheduling, which is a different design. + * + * Each row pins: + * - cstage value correctness (process exit code). + * - wwstage value correctness (when ww_ww exists). + * - cstage vs wwstage byte-identical .s output (catches drift). + */ +#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[] = { + /* Write side, scalar i64 field via explicit deref. + * Pre-fix: cgassign single-dot dispatch fell off (base.kind + * was N_UN, not N_IDENT) and emitted no store. f stayed 0. */ + { "write_i64", + "type pt = struct { x: i64, y: i64 };\n" + "fn main() i32 = {\n" + " let s: pt = pt { x = 0i64, y = 0i64 };\n" + " let p: *pt = &s;\n" + " (*p).x = 7i64;\n" + " (*p).y = 35i64;\n" + " return (s.x + s.y): i32;\n" + "};\n", + 42 }, + /* Write side, narrow i32 field. Pins fldstoreop sizing on + * the via_ptr path — MOVL not MOVQ (which would stomp 4 bytes + * past the field). */ + { "write_i32", + "type box = struct { a: i32, b: i32, c: i32 };\n" + "fn main() i32 = {\n" + " let s: box = box { a = 0, b = 0, c = 0 };\n" + " let p: *box = &s;\n" + " (*p).a = 5;\n" + " (*p).b = 11;\n" + " (*p).c = 19;\n" + " return s.a + s.b + s.c;\n" + "};\n", + 35 }, + /* Write side, str field. Pins the str-typed field branch in + * the via_ptr path (cgexpr → AX=ptr/BX=len, store both halves + * at +0/+8 — not just AX). Initialiser uses `let v: box;` + * (zero-fill) instead of a struct literal because N_LET + * N_STRUCTLIT silently drops the .len half of a str-typed + * field — a separate pre-existing bug, out of scope here. */ + { "write_str", + "type box = struct { s: str, n: i64 };\n" + "fn main() i32 = {\n" + " let v: box;\n" + " let p: *box = &v;\n" + " (*p).s = \"hi\";\n" + " (*p).n = 40i64;\n" + " return (v.s.len: i64 + v.n): i32;\n" + "};\n", + 42 }, + /* Read side, scalar i64 field via explicit deref. + * Pre-fix: case N_DOT's pointer-auto-deref branch required + * N_IDENT and fell through to `cgexpr(c, n->lhs, ...)` — + * derefed the pointer as if loading a scalar, so AX held the + * first qword of the struct (s.x = 50). Field offset for .y + * (+8) was dropped → returned 50 instead of 80. */ + { "read_i64", + "type pt = struct { x: i64, y: i64 };\n" + "fn main() i32 = {\n" + " let s: pt = pt { x = 50i64, y = 80i64 };\n" + " let p: *pt = &s;\n" + " return (*p).y: i32;\n" + "};\n", + 80 }, + /* Read side, narrow i32 field. Pins fldloadop sizing — MOVL + * with sign-extend, not raw MOVQ. */ + { "read_i32", + "type box = struct { a: i32, b: i32, c: i32 };\n" + "fn main() i32 = {\n" + " let s: box = box { a = 1, b = 2, c = 3 };\n" + " let p: *box = &s;\n" + " return (*p).a + (*p).b + (*p).c;\n" + "};\n", + 6 }, + /* Read side, str field. Pins the str-typed field load path — + * AX=ptr, BX=len. .len is the receive-side check. Initialiser + * uses write-by-field (same str-in-structlit-init bug avoided + * as in write_str). */ + { "read_str_len", + "type box = struct { s: str };\n" + "fn main() i32 = {\n" + " let v: box;\n" + " v.s = \"hello\";\n" + " let p: *box = &v;\n" + " return (*p).s.len: i32;\n" + "};\n", + 5 }, + /* Mixed: write via (*p).f then read via (*p).f. End-to-end + * round-trip pins both sides agreeing on the same offset and + * width. */ + { "roundtrip", + "type pt = struct { x: i32, y: i32, z: i32 };\n" + "fn main() i32 = {\n" + " let s: pt = pt { x = 0, y = 0, z = 0 };\n" + " let p: *pt = &s;\n" + " (*p).x = 4;\n" + " (*p).y = 7;\n" + " (*p).z = 13;\n" + " return (*p).x + (*p).y + (*p).z;\n" + "};\n", + 24 }, +}; + +/* run_driver — compile r->src via the given driver and exec; return + * the process exit code. Mirror of 701's helper. */ +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/wded_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wded_%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", + 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. Mirror of 701. */ +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/wded_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wded_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wded_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, "dot_explicit_deref: 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, + "dot_explicit_deref[%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, + "dot_explicit_deref: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("dot_explicit_deref: %d/%d ok\n", total, total); + return 0; +}