diff --git a/Makefile b/Makefile index 3fc0ba8b..5c525bfa 100644 --- a/Makefile +++ b/Makefile @@ -326,6 +326,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_structlit_slice_field \ $(BIN)/test_arrglob_nodup_run \ $(BIN)/test_globptr_field_store_run \ + $(BIN)/test_globptr_field_read_run \ $(BIN)/test_dot_str_chained_arg \ $(BIN)/test_dot_slice_arg \ $(BIN)/test_dot_tagged_source \ @@ -1458,6 +1459,16 @@ $(BIN)/test_globptr_field_store_run: test/wcc/689_globptr_field_store_run.c $(BI $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 689_globptr_field_read_run (#15): reading a field through a module-global +# *struct ptr base (single-dot `gp.f`) no longer SEGVs in cstage / collapses +# to a bare symbol in wwstage; both load the ptr value via SB then the field +# offset, byte-identically. Read twin of 689_globptr_field_store_run. +$(BIN)/test_globptr_field_read_run: test/wcc/689_globptr_field_read_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_dot_str_chained_arg: test/wcc/692_dot_str_chained_arg.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b7e7348a..e82227a3 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -11856,7 +11856,17 @@ cgexpr(Cg *c, Node *n, Local *locals) && (lenfld || capfld || ptrfld) && 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)); + /* #15 — a module-GLOBAL ptr has no local + * slot (off==0); load its pointer VALUE from + * the data slot via SB, else amem(BP,0) derefs + * the saved BP (read twin of the #6 store fix). */ + if (off == 0 && let_islet(dot_lhs->str)) + ins2(c, A_MOVQ, + mafn(c, dot_lhs->str, c->cur_mod), + areg(D_BX)); + else + 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)); break; @@ -11878,7 +11888,17 @@ cgexpr(Cg *c, Node *n, Local *locals) if (inner && inner->kind == TY_STRUCT && 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)); + /* #15 — module-GLOBAL ptr base: load its pointer + * VALUE from the data slot via SB (off==0 = no + * local slot), else amem(BP,0) derefs the saved + * BP (read twin of the #6 store fix). */ + if (off == 0 && let_islet(dot_lhs->str)) + ins2(c, A_MOVQ, + mafn(c, dot_lhs->str, c->cur_mod), + areg(D_BX)); + else + 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; /* tagged-union field through *struct: BX diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index bb4137d7..c8998889 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3248,6 +3248,51 @@ fn cgmatch(c: *cgen, n: *syntax.node) void = { return; }; +fn cgptrfieldload(c: *cgen, fi: *fieldinfo) void = { + // #15 — load struct field `fi` from a *struct base already in BX + // (a local ptr's MOVQ off(BP) value, or a module-global ptr's + // MOVQ name(SB) value). Shared by the local and global *struct + // field-read arms in cgdot; mirrors cstage cgen.c N_DOT *struct + // field tail. BX is never a load target, so order is harmless. + if (istaggedtype(c, fi.tnode)) { + let tsz: i32 = slotsize(c, fi.tnode); + cgloadtaggedfield(c, "BX", fi.foff, tsz); + return; + }; + // str IS []u8 — same 3-word {ptr,len,cap} as a slice field: load + // (ptr, len, cap) into (AX, BX, CX); .len LAST so the earlier reads + // still index off BX (#1/Phase 3 collapse). + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", AX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 16): i64, "BX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "BX"); + emitline(", BX\n"); + return; + }; + if (isfloattype(c, fi.tnode)) { + // f64/f32 via *struct: route through X0. + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", X0\n"); + return; + }; + let op: str = fieldloadop(c, fi); + emitline("\t"); + emitline(op); + emitline("\t"); + emitdispreg(fi.foff: i64, "BX"); + emitline(", AX\n"); +}; + fn cgdot(c: *cgen, n: *syntax.node) void = { let lhs: *syntax.node = n.lhs; let fld: str = n.str; @@ -3370,66 +3415,16 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { for (fi != nil) { let fn_: str = fi.fname; if (syntax.streq(fn_, fld)) { - // tagged-union field via *struct: stage - // the *struct in BX, then load the four - // payload regs via cgloadtaggedfield. - // BX isn't a target (AX/DX/CX/R8), so - // load order doesn't matter. Mirrors - // the direct-local branch above so the - // match / let-init / call-arg consumer - // shape is identical regardless of - // pointer rooting. - if (istaggedtype(c, fi.tnode)) { - let tsz: i32 = slotsize(c, fi.tnode); - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); - cgloadtaggedfield(c, "BX", - fi.foff, tsz); - return; - }; - // str IS []u8 — same 3-word {ptr,len,cap} - // as a slice field via *struct: load - // (ptr, len, cap) into (AX, BX, CX). BX - // holds the *struct pointer, so load .len - // LAST so the earlier reads still index - // off the base. str folds onto the slice - // arm (#1/Phase 3 collapse; cite cstage - // cgen.c N_DOT *struct S2). + // Stage the *struct base in BX, then field + // load via cgptrfieldload (shared with the + // #15 global-ptr arm below). BX isn't a load + // target (AX/DX/CX/R8/X0), so order is + // harmless. Mirrors cstage cgen.c N_DOT + // *struct field arm. emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { - emitline("\tMOVQ\t"); - emitdispreg(fi.foff: i64, "BX"); - emitline(", AX\n"); - emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 16): i64, "BX"); - emitline(", CX\n"); - emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 8): i64, "BX"); - emitline(", BX\n"); - } else { if (isfloattype(c, fi.tnode)) { - // f64/f32 via *struct: route through X0. - // MOVQ into AX leaves the SSE reg stale - // and any downstream consumer (arg - // pass, return, arithmetic) reads - // garbage. - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(fi.foff: i64, "BX"); - emitline(", X0\n"); - } else { - let op: str = fieldloadop(c, fi); - emitline("\t"); - emitline(op); - emitline("\t"); - emitdispreg(fi.foff: i64, "BX"); - emitline(", AX\n"); - }; }; + cgptrfieldload(c, fi); return; }; fi = fi.finext; @@ -3684,6 +3679,89 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { }; }; }; + // #15 — module-GLOBAL ptr receiver `gp.f` (no local slot, so the + // local arm above is skipped): load the pointer VALUE from name(SB) + // into BX, then field load. Read twin of the #6 store fix; mirrors + // cstage cgen.c N_DOT pointer-to-{struct,slice/str} SB base load. A + // global VALUE struct/slice/array is served by the dedicated global + // arms below, so only a *T receiver lands here. + if (dotlhs != nil) { + if (dotlhs.kind == syntax.nkind.N_IDENT) { + // localfindnode==nil mirrors cstage's off==0 guard: a + // LOCAL ptr (incl. one shadowing a global let) stays on + // the BP-relative local arm above; only a true module + // global lands here. + if (localfindnode(c, dotlhs.str) == nil + && isletvar(c, dotlhs.str)) { + let gnm: str = dotlhs.str; + let gtn: *syntax.node = letvartnode(c, gnm); + // Peel a NAMED alias chain to expose N_TPTR, the + // same module-aware peel as the local arm (#191/#223). + for (gtn != nil && gtn.kind == syntax.nkind.N_TNAME) { + if (structsamemod(c, gtn.str) != nil) { break; }; + let nx: *syntax.node = aliassamemod(c, gtn.str); + if (nx == nil) { + if (structlookup(c, gtn.str) != nil) { break; }; + nx = aliaslookup(c, gtn.str); + if (nx == nil) { break; }; + }; + gtn = nx; + }; + if (gtn != nil) { + if (gtn.kind == syntax.nkind.N_TPTR) { + let inner: *syntax.node = gtn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == syntax.nkind.N_TNAME) { + sname = inner.str; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookupchain(c, inner); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (syntax.streq(fi.fname, fld)) { + emitline("\tMOVQ\t"); + emitsymname(c, gnm); + emitline("(SB), BX\n"); + cgptrfieldload(c, fi); + return; + }; + fi = fi.finext; + }; + }; + }; + // Pointer to str/slice (`*[]u8`, `*str`): deref + // name(SB), then load at delta within the header. + let delta: i32 = -1; + if (syntax.streq(fld, "ptr")) { delta = 0; }; + if (syntax.streq(fld, "len")) { delta = 8; }; + if (syntax.streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + let innerkind: syntax.nkind = syntax.nkind.N_NONE; + if (inner != nil) { innerkind = inner.kind; }; + let innerstr: bool = false; + if (innerkind == syntax.nkind.N_TNAME) { + if (syntax.streq(inner.str, "str")) { innerstr = true; }; + }; + if (innerkind == syntax.nkind.N_TSLICE) { innerstr = true; }; + if (innerstr) { + emitline("\tMOVQ\t"); + emitsymname(c, gnm); + emitline("(SB), BX\n"); + emitline("\tMOVQ\t"); + emitdispreg(delta: i64, "BX"); + emitline(", AX\n"); + return; + }; + }; + }; + }; + }; + }; + }; // `def NAME: str = "..."` field access — inline the literal. // Sdef-backed strs aren't laid out in memory, so falling // through to the SB-load fallback below would mis-emit diff --git a/test/wcc/689_globptr_field_read_run.c b/test/wcc/689_globptr_field_read_run.c new file mode 100644 index 00000000..682abb1c --- /dev/null +++ b/test/wcc/689_globptr_field_read_run.c @@ -0,0 +1,231 @@ +/* + * 689_globptr_field_read_run (#15) — reading a field through a module- + * GLOBAL `*struct` pointer base (`gp.f`, single-dot) loads correctly AND + * cstage / wwstage agree byte-for-byte. + * + * THE BUG (both stages, two different wrong ways — no byte-id oracle until + * one side was fixed, but they DID diverge): a global `*struct` ptr base + * carries via_ptr=1 but has NO local slot (localfind/localfindnode = 0), + * so the pointer-to-struct field-READ arm never loaded the pointer VALUE + * from the global's data slot before applying the field offset: + * cstage : `MOVQ (BP),BX; MOVQ 8(BX),AX` — derefed the saved BP → SEGV. + * wwstage: `MOVQ f(SB),AX` — collapsed gp.f to a bare + * undefined symbol `f`. + * Read twin of the #6 STORE fix (689_globptr_field_store_run). + * + * THE FIX (both stages converge): load the pointer value via SB first — + * `MOVQ gp(SB),BX; MOVQ off(BX),AX` + * cstage: base load is now `off==0 && let_islet → MOVQ mafn(gp)(SB),BX`. + * wwstage: a dedicated global-ptr arm (lc==nil + isletvar + N_TPTR) emits + * `MOVQ name(SB),BX` and shares the field tail (cgptrfieldload). + * + * shape | want + * -------------------------------+------ + * gp.f (scalar, offset 8) | 170 + * gp.f+gp.g (scalar, offset 0/8) | 14 + * let x=gp.sf; x.len (str field) | 3 + * gp.d : i32 (f64 field) | 7 + * match gp.t (tagged field, 16B) | 5 + * gp.len (*[]u8 pseudo-field) | 3 + * lp.f (LOCAL ptr control) | 42 + * bump param p.f (param control) | 10 + * Each row also asserts w6c .s == w6c_ww .s byte-identical. + * Pre-fix: cstage SEGV(139) on every global-ptr row; wwstage ran wrong; + * cs vs ww .s differed. + * + * SCOPE: single-dot field reads only. Chained-through-global-ptr-root + * (`gp.x.y`, `gp.sf.len` inline) is a sibling — both stages run correct + * after this fix, but the chained-N_DOT spines still diverge in idiom + * (cstage offset-folds; wwstage inner-loads + shuffles), filed separately. + */ +#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 *name; + const char *src; + int want; +}; + +static const struct row ROWS[] = { +{ "scalar_off8", + "package main;\n" + "type S = struct { a: i64, f: i64 };\n" + "let gp: *S = nil;\n" + "export fn main() i32 = { let s: S = S{a=0,f=0}; gp = &s;" + " s.f = 170; return gp.f: i32; };\n", 170 }, +{ "scalar_off0_off8", + "package main;\n" + "type S = struct { f: i64, g: i64 };\n" + "let gp: *S = nil;\n" + "export fn main() i32 = { let s: S = S{f=0,g=0}; gp = &s;" + " s.f = 5; s.g = 9; return (gp.f + gp.g): i32; };\n", 14 }, +{ "str_field", + "package main;\n" + "type S = struct { a: i64, sf: str };\n" + "let gp: *S = nil;\n" + "export fn main() i32 = { let s: S = S{a=0,sf=\"abc\"}; gp = &s;" + " let x: str = gp.sf; return x.len: i32; };\n", 3 }, +{ "float_field", + "package main;\n" + "type S = struct { a: i64, d: f64 };\n" + "let gp: *S = nil;\n" + "export fn main() i32 = { let s: S = S{a=0,d=0.0}; gp = &s;" + " s.d = 7.0; return (gp.d): i32; };\n", 7 }, +{ "tagged_field", + "package main;\n" + "type S = struct { a: i64, t: (i64 | i32) };\n" + "let gp: *S = nil;\n" + "export fn main() i32 = { let s: S = S{a=0,t=5i32}; gp = &s;" + " let v: i32 = 0; match (gp.t) { case let x: i32 => v = x;" + " case let y: i64 => v = y: i32; }; return v; };\n", 5 }, +{ "pslice_len", + "package main;\n" + "let gp: *[]u8 = nil;\n" + "export fn main() i32 = { let b: []u8 = [1u8,2u8,3u8]; gp = &b;" + " return (gp.len): i32; };\n", 3 }, +{ "local_ptr_ctrl", + "package main;\n" + "type S = struct { a: i64, f: i64 };\n" + "export fn main() i32 = { let s: S = S{a=0,f=0}; let lp: *S = &s;" + " s.f = 42; return lp.f: i32; };\n", 42 }, +{ "param_ctrl", + "package main;\n" + "type S = struct { f: i64, g: i64 };\n" + "fn rd(p: *S) i32 = { return (p.f + p.g): i32; };\n" + "export fn main() i32 = { let s: S = S{f=4,g=6}; return rd(&s); };\n", 10 }, +}; + +#define NROWS ((int)(sizeof ROWS / sizeof ROWS[0])) + +/* build+run one row through `driver`; return exit code or -1. */ +static int +run_driver(const char *driver, const char *src, int idx) +{ + char tmpdir[64], srcf[128], outbin[160], rmcmd[192], cmd[2400]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/gpfr_%d_%d_d", getpid(), idx); + mkdir(tmpdir, 0755); + snprintf(srcf, sizeof srcf, "%s/t.ww", tmpdir); + snprintf(outbin, sizeof outbin, "%s/t", tmpdir); + snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); + + FILE *f = fopen(srcf, "wb"); + if (!f) { runwait(rmcmd); return -1; } + fputs(src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null", + driver, outbin, srcf); + if (runwait(cmd) != 0) { runwait(rmcmd); return -1; } + int got = runwait(outbin); + runwait(rmcmd); + return got; +} + +/* emit .s for `tool` (w6c / w6c_ww) into asmf; 0 ok, -1 on compile fail. */ +static int +emit_asm(const char *bin, const char *tool, const char *src, int idx, + const char *asmf) +{ + char srcf[80], cmd[2400]; + snprintf(srcf, sizeof srcf, "/tmp/gpfr_asm_%d_%d.ww", getpid(), idx); + FILE *f = fopen(srcf, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + snprintf(cmd, sizeof cmd, "%s/%s -o %s %s 2>/dev/null", + bin, tool, asmf, srcf); + int rc = runwait(cmd); + unlink(srcf); + return rc == 0 ? 0 : -1; +} + +static int +files_identical(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"), *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return 0; } + int ca, cb, same = 1; + do { + ca = fgetc(fa); cb = fgetc(fb); + if (ca != cb) { same = 0; break; } + } while (ca != EOF); + fclose(fa); fclose(fb); + return same; +} + +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); + int have_ww = (access(wdrv, X_OK) == 0); + + int fail = 0, total = 0; + for (int i = 0; i < NROWS; i++) { + const struct row *r = &ROWS[i]; + + total++; + int gc = run_driver(cdrv, r->src, i); + if (gc != r->want) { + fprintf(stderr, "globptr_field_read[%s,cstage]: run=%d want=%d\n", + r->name, gc, r->want); + fail++; + } + if (have_ww) { + total++; + int gw = run_driver(wdrv, r->src, i); + if (gw != r->want) { + fprintf(stderr, "globptr_field_read[%s,wwstage]: run=%d want=%d\n", + r->name, gw, r->want); + fail++; + } + } + + /* byte-id: w6c .s == w6c_ww .s */ + if (have_ww) { + char csf[80], wwf[80]; + snprintf(csf, sizeof csf, "/tmp/gpfr_cs_%d_%d.s", getpid(), i); + snprintf(wwf, sizeof wwf, "/tmp/gpfr_ww_%d_%d.s", getpid(), i); + total++; + int ec = emit_asm(bin, "w6c", r->src, i, csf); + int ew = emit_asm(bin, "w6c_ww", r->src, i, wwf); + if (ec != 0 || ew != 0 || !files_identical(csf, wwf)) { + fprintf(stderr, "globptr_field_read[%s]: cs/ww .s NOT " + "byte-identical (ec=%d ew=%d)\n", r->name, ec, ew); + fail++; + } + unlink(csf); unlink(wwf); + } + } + + if (fail) { + fprintf(stderr, "globptr_field_read_run: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("globptr_field_read_run: %d/%d ok\n", total, total); + return 0; +}