diff --git a/Makefile b/Makefile index d91299fa..596e77ec 100644 --- a/Makefile +++ b/Makefile @@ -236,6 +236,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_cgreturn_variant_zero \ $(BIN)/test_arrlit_str_full \ $(BIN)/test_redecl \ + $(BIN)/test_struct_field_index \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -463,6 +464,12 @@ $(BIN)/test_redecl: test/wcc/712_redecl.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_struct_field_index: test/wcc/713_struct_field_index.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/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f0158582..0d9651b8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6798,6 +6798,60 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; }; + // N_INDEX through a struct field: e.g. cmd.argsptr[i] + // where argsptr: *str. cgindex correctly loads the + // (ptr, len) pair via indexbaseesz; without this arm + // pushargsrev would only push AX and lose the .len. + if (base.kind == nkind.N_DOT) { + let fld: str = base.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + let inner: *node = base.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == nkind.N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == nkind.N_TNAME) { + sname = pinner.str; + }; + }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + let elem: *node = nil; + let fk: nkind = ft.kind; + if (fk == nkind.N_TPTR) { elem = ft.lhs; }; + if (fk == nkind.N_TSLICE) { elem = ft.lhs; }; + if (fk == nkind.N_TARRAY) { elem = ft.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; }; return false; }; @@ -7155,7 +7209,10 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (innert.kind == nkind.N_TNAME) { if (streq(innert.str, "str")) { return 1; }; }; - if (innert.kind == nkind.N_TSLICE) { return elemsizeof(innert); }; + // Slice element: resolve through elemsizeofc so a slice of a + // named struct (e.g. *[]option) returns the struct stride + // instead of falling through to elemsizeof's default 8. + if (innert.kind == nkind.N_TSLICE) { return elemsizeofc(c, innert); }; return 8; }; @@ -7186,6 +7243,13 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (streq(elem.str, "str")) { return 16; }; let ps: i32 = primsize(elem.str); if (ps > 0) { return ps; }; + // Pointer to named struct: indexing + // stride is the struct slot size. + // Without this, &p.ptr[i] for p.ptr: + // *S falls through to 8 and reads + // the wrong element. + let si: *structinfo = structlookup(c, elem.str); + if (si != nil) { return si.totsize; }; }; }; return 8; @@ -11942,7 +12006,13 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - }; + } else { if (base.kind == nkind.N_DOT) { + // `&p.ptr[i]` shape: stride is the element + // of the slice/struct-pointer field, not + // the default 8. Mirrors cgindex's N_DOT + // arm so &p.ptr[i] and p.ptr[i] agree. + esz = indexbaseesz(c, base); + };}; }; cgexpr(c, idx); if (esz > 1) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1c6a5e7d..11240a3a 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2403,7 +2403,13 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - }; + } else { if (base.kind == nkind.N_DOT) { + // `&p.ptr[i]` shape: stride is the element + // of the slice/struct-pointer field, not + // the default 8. Mirrors cgindex's N_DOT + // arm so &p.ptr[i] and p.ptr[i] agree. + esz = indexbaseesz(c, base); + };}; }; cgexpr(c, idx); if (esz > 1) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 841f7f5f..4244ed0f 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -555,6 +555,60 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; }; + // N_INDEX through a struct field: e.g. cmd.argsptr[i] + // where argsptr: *str. cgindex correctly loads the + // (ptr, len) pair via indexbaseesz; without this arm + // pushargsrev would only push AX and lose the .len. + if (base.kind == nkind.N_DOT) { + let fld: str = base.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + let inner: *node = base.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == nkind.N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == nkind.N_TNAME) { + sname = pinner.str; + }; + }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + let elem: *node = nil; + let fk: nkind = ft.kind; + if (fk == nkind.N_TPTR) { elem = ft.lhs; }; + if (fk == nkind.N_TSLICE) { elem = ft.lhs; }; + if (fk == nkind.N_TARRAY) { elem = ft.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; }; return false; }; @@ -912,7 +966,10 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (innert.kind == nkind.N_TNAME) { if (streq(innert.str, "str")) { return 1; }; }; - if (innert.kind == nkind.N_TSLICE) { return elemsizeof(innert); }; + // Slice element: resolve through elemsizeofc so a slice of a + // named struct (e.g. *[]option) returns the struct stride + // instead of falling through to elemsizeof's default 8. + if (innert.kind == nkind.N_TSLICE) { return elemsizeofc(c, innert); }; return 8; }; @@ -943,6 +1000,13 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (streq(elem.str, "str")) { return 16; }; let ps: i32 = primsize(elem.str); if (ps > 0) { return ps; }; + // Pointer to named struct: indexing + // stride is the struct slot size. + // Without this, &p.ptr[i] for p.ptr: + // *S falls through to 8 and reads + // the wrong element. + let si: *structinfo = structlookup(c, elem.str); + if (si != nil) { return si.totsize; }; }; }; return 8; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1743b780..9956b64d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6798,6 +6798,60 @@ fn nodeisstr(c: *cgen, n: *node) bool = { }; }; }; + // N_INDEX through a struct field: e.g. cmd.argsptr[i] + // where argsptr: *str. cgindex correctly loads the + // (ptr, len) pair via indexbaseesz; without this arm + // pushargsrev would only push AX and lose the .len. + if (base.kind == nkind.N_DOT) { + let fld: str = base.str; + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + let inner: *node = base.lhs; + if (inner != nil) { + if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == nkind.N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == nkind.N_TNAME) { + sname = pinner.str; + }; + }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + let elem: *node = nil; + let fk: nkind = ft.kind; + if (fk == nkind.N_TPTR) { elem = ft.lhs; }; + if (fk == nkind.N_TSLICE) { elem = ft.lhs; }; + if (fk == nkind.N_TARRAY) { elem = ft.lhs; }; + if (elem != nil) { + return isstrtype(c, elem); + }; + }; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; }; return false; }; @@ -7155,7 +7209,10 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (innert.kind == nkind.N_TNAME) { if (streq(innert.str, "str")) { return 1; }; }; - if (innert.kind == nkind.N_TSLICE) { return elemsizeof(innert); }; + // Slice element: resolve through elemsizeofc so a slice of a + // named struct (e.g. *[]option) returns the struct stride + // instead of falling through to elemsizeof's default 8. + if (innert.kind == nkind.N_TSLICE) { return elemsizeofc(c, innert); }; return 8; }; @@ -7186,6 +7243,13 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = { if (streq(elem.str, "str")) { return 16; }; let ps: i32 = primsize(elem.str); if (ps > 0) { return ps; }; + // Pointer to named struct: indexing + // stride is the struct slot size. + // Without this, &p.ptr[i] for p.ptr: + // *S falls through to 8 and reads + // the wrong element. + let si: *structinfo = structlookup(c, elem.str); + if (si != nil) { return si.totsize; }; }; }; return 8; @@ -11942,7 +12006,13 @@ fn cgun(c: *cgen, n: *node) void = { }; }; }; - }; + } else { if (base.kind == nkind.N_DOT) { + // `&p.ptr[i]` shape: stride is the element + // of the slice/struct-pointer field, not + // the default 8. Mirrors cgindex's N_DOT + // arm so &p.ptr[i] and p.ptr[i] agree. + esz = indexbaseesz(c, base); + };}; }; cgexpr(c, idx); if (esz > 1) { diff --git a/test/wcc/713_struct_field_index.c b/test/wcc/713_struct_field_index.c new file mode 100644 index 00000000..96e4af72 --- /dev/null +++ b/test/wcc/713_struct_field_index.c @@ -0,0 +1,229 @@ +/* + * 713_struct_field_index — wwstage N_INDEX through a struct-field base + * carries the element type. Three sister gaps in wwstage cgen's untyped + * AST defaulted to esz=8 / scalar-push when the base was N_DOT with the + * field a *T or []T. Surfaced by getopttest wwstage build (#37 + * investigation, originally misattributed as a symbol-mangling bug). + * + * Pre-fix sites (selfhost/cmd/wcc/): + * + * 1. cgenutil.ww indexbaseesz `.ptr` pseudo-field on a slice field: + * `&p.ptr[i]` where p: *[]S fell through `elemsizeof(innert)` + * which returned 8 (named-struct element), not sizeof(S). So + * `IMULQ $8, AX` instead of `IMULQ $24, AX` — every cross-element + * addressing landed inside element 0. Fix: route slice case + * through elemsizeofc so structlookup resolves the stride. + * + * 2. cgenexpr.ww cgun TK_AMP N_INDEX with N_DOT base: only + * base.kind == N_IDENT was handled; N_DOT base left esz=8 from + * the function-top default. `&p.ptr[i]` for any p.ptr (slice ptr + * or *T field) hit this. Fix: add the N_DOT arm calling + * indexbaseesz. + * + * 3. cgenutil.ww nodeisstr N_INDEX with N_DOT base: only N_IDENT + * base was handled; `cmd.argsptr[i]` (argsptr: *str) returned + * false, so pushargsrev pushed only AX and the call's str arg + * lost its .len half to stack residue. Sister to #34's + * [N]str-through-N_IDENT fix. Fix: walk the struct field's + * element type and return isstrtype. + * + * All three are the same root pattern as #34 / #36: the untyped AST + * loses the type through a struct field, so the per-shape recognizer + * has to walk N_DOT → struct → field → element manually. A typed AST + * (#11 wwstage check pass) would replace all three with one query. + * + * Rows are runtime-only (no asm byte-id): wwstage's local layout + * legitimately differs from cstage's (slot offsets shift by 8), and + * 995_self_rebuild already covers cross-stage drift on driver corpora. + * The semantic invariant — values round-trip correctly — is what we + * pin here. + * + * row | what it pins + * -----------------------------+---------------------------------- + * amp_ptr_index_struct_stride | &p.ptr[i] esz = sizeof(S)=24, + * | not 8 (fix 1+2) + * dot_ptr_index_str_arg | cmd.argsptr[i] as str arg pushes + * | both halves (fix 3) + * amp_ptr_index_str_pair | &p.ptr[i] esz=16 for *[]str — + * | sister regression-pin to fix 1 + */ +#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[] = { + /* 1. `&p.ptr[i]` where p: *[]S, S size 24 (i32 + str). Pre-fix + * wwstage: IMULQ $8 → store landed inside element 0. Post-fix: + * IMULQ $24 → store lands in element i. Reads back element 1's + * `a` = 22 (mod 256). */ + { "amp_ptr_index_struct_stride", + "type S = struct { a: i32, b: str };\n" + "fn main() i32 = {\n" + " let buf: [3]S;\n" + " buf[0].a = 10i32; buf[0].b = \"x\";\n" + " buf[1].a = 22i32; buf[1].b = \"yy\";\n" + " buf[2].a = 30i32; buf[2].b = \"zzz\";\n" + " let xs: []S;\n" + " xs.ptr = &buf[0];\n" + " xs.len = 3;\n" + " xs.cap = 3;\n" + " let p: *[]S = &xs;\n" + " let q: *S = &p.ptr[1];\n" + " return q.a;\n" + "};\n", + 22 }, + /* 2. `cmd.argsptr[i]` where argsptr: *str passed as str arg. + * Pre-fix wwstage: nodeisstr returned false → call-arg push + * dropped .len → streq received .len from stack residue → no + * match. Post-fix: full str round-trip. */ + { "dot_ptr_index_str_arg", + "fn streq(a: str, b: str) i32 = {\n" + " if (a.len != b.len) { return 0i32; };\n" + " let i: i32 = 0;\n" + " for (i < a.len) {\n" + " if (a[i] != b[i]) { return 0i32; };\n" + " i += 1;\n" + " };\n" + " return 1i32;\n" + "};\n" + "type cmd = struct { argsptr: *str, argslen: i32 };\n" + "fn main() i32 = {\n" + " let xs: [2]str;\n" + " xs[0] = \"first\";\n" + " xs[1] = \"second\";\n" + " let c: cmd;\n" + " c.argsptr = &xs[0];\n" + " c.argslen = 2;\n" + " if (streq(c.argsptr[0], \"first\") == 0i32) { return 1; };\n" + " if (streq(c.argsptr[1], \"second\") == 0i32) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 3. `&p.ptr[i]` for *[]str — sister to row 1 with str (16B) + * elements. Reads the .len half of element 1 to assert the + * stride lands on the right slot. Pre-fix cgun N_DOT-base + * (fix 2) left esz=8 even though elemsizeof would have returned + * 16 for str directly, so `&p.ptr[1]` pointed at element 0's + * `.len` half instead of element 1's `.ptr`. element 1's .len + * = 2 ("yy"). */ + { "amp_ptr_index_str_pair", + "fn main() i32 = {\n" + " let buf: [3]str;\n" + " buf[0] = \"a\";\n" + " buf[1] = \"yy\";\n" + " buf[2] = \"zzz\";\n" + " let xs: []str;\n" + " xs.ptr = &buf[0];\n" + " xs.len = 3;\n" + " xs.cap = 3;\n" + " let p: *[]str = &xs;\n" + " let q: *str = &p.ptr[1];\n" + " return q.len: i32;\n" + "};\n", + 2 }, +}; + +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/wcrsfi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcrsfi_%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; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + 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, "struct_field_index: 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, + "struct_field_index[%s] row[%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, got, + rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "struct_field_index: %d/%d row(s) failed\n", fail, total); + return 1; + } + printf("struct_field_index: %d/%d ok\n", total, total); + return 0; +}