From 0f2587d294ee444654f34b22e2def56100b5c3ae Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 04:38:41 +0900 Subject: [PATCH] w6c+wwstage: struct-array-field slice as call-arg via dotbaseaddr (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inline slice of a struct `[N]T`-field passed DIRECTLY as a call argument (`rd(x.o[lo:hi])`) materialized the slice .ptr from the field VALUE, not its ADDRESS: the pushargs/pushargsrev N_SLICE inline builder's non-ident else-arm did plain cgexpr(base), so the N_DOT field auto-derefs (MOVL field,AX used as .ptr) -> callee derefs garbage -> SEGFAULT. The let-init / assign-rhs / return / hoist-to-local contexts already routed through the cgslice #252 choke-point; only this call-arg builder kept a private duplicate. cs==ww both segfaulted identically pre-fix (gate-blind). Fix (symmetric both stages): - route the else-arm through cg_dotbase_addr / dotbaseaddr (the cgslice #252 choke-point: array-field-gated, so `[]T`/str/`*T` fields fall through to cgexpr; chained inner `o.p.m` handled via its #253 arm); - extend the N_IDENT-only esz gate to N_DOT bases, taking the element width from the checker-stamped base->type (rule-13 type table), so non-u8 call-arg slices scale stride. Before: `MOVL -8(BP),AX; PUSHQ AX` (field value as .ptr). After: `LEAQ -8(BP),AX; PUSHQ AX` (field address). cs==ww byte-identical. Helper note: used dotbaseaddr (not dotchainaddr as first scoped) — it is the established cgslice choke-point and is array-field-gated, so a slice/ str-typed field base keeps the correct cgexpr header-ptr load; bare dotchainaddr lacks that gate and would mis-emit the field address for those. dotbaseaddr already handles the chained `o.p.m` inner via #253. Tests: test/wcc/949 gains 6 call-arg rows (u8, i32-esz-stride, via-*struct, chained, + hoist-to-local and bare-local-array controls), each run- correctness AND cs==ww byte-id. PROOF-GREP residual: the tagged-union-element indexed-STORE arm (cgen.c:~4972 / cgenexpr.ww:~5024) is the same N_DOT-base auto-deref shape, still unrouted in BOTH stages (symmetric, segfaults) — a distinct consumption axis filed separately; NOT fixed here. --- cmd/w6c/cgen.c | 20 +++++-- selfhost/cmd/w6c/main.combined.ww | 28 ++++++++-- selfhost/cmd/wcc/cgenutil.ww | 28 ++++++++-- selfhost/cmd/wwdump/main.combined.ww | 28 ++++++++-- test/wcc/949_dotbase_addr_slice_run.c | 80 ++++++++++++++++++++++++++- 5 files changed, 164 insertions(+), 20 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e1226ca8..93f317b1 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6167,11 +6167,14 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *bt = base ? base->type : NULL; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; - /* N_IDENT-gated: non-ident bases stay esz=1 - * (unscaled), byte-id with wwstage which has no - * tnode there (rule 10) -- #76 residual, non- - * ident cluster #74. */ - int esz = (base && base->kind == N_IDENT + /* esz from the type table for an N_IDENT base + * (#76) or an N_DOT array/slice-field base + * (#257: scale by the field's element width via + * the checker-stamped base->type, not esz=1 -- + * silently wrong for non-u8). Other non-ident + * bases stay esz=1 (unscaled). */ + int esz = (base && (base->kind == N_IDENT + || base->kind == N_DOT) && bu && bu->sub) ? (int)bu->sub->size : 1; /* base addr → push */ @@ -6192,6 +6195,13 @@ cgexpr(Cg *c, Node *n, Local *locals) } else { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); } + } else if (cg_dotbase_addr(c, base, D_AX, locals)) { + /* #257: N_DOT `[N]T`-field base as a call + * arg → field ADDRESS (LEAQ), not the + * auto-deref VALUE load cgexpr emits. Same + * choke-point as the cgslice #252 site; + * `[]T`/str/`*T` fields fall through to + * cgexpr (correct header/ptr load). */ } else { cgexpr(c, base, locals); } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d3d4055d..d1df0bb7 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15624,15 +15624,30 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; }; }; + // #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call + // arg) carries no tnode — resolve esz / base-address from the + // checker-stamped element tinfo on base.type_ instead. Cstage + // twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror + // of the cgslice #252 site. + let dotbu: *tinfo = nil; + if (base != nil) { if (base.kind == nkind.N_DOT) { + dotbu = base.type_: *tinfo; + for (dotbu != nil && dotbu.kind == tykind.TY_NAMED) { + dotbu = dotbu.under; + }; + };}; // esz from the type table for an N_IDENT base (#76; mirrors - // the cgindex idiom). Non-ident base stays esz=1 -> ptr - // unscaled, matching cstage's base->kind==N_IDENT gate. + // the cgindex idiom) or an N_DOT array/slice-field base (#257: + // scale by the field's element width, not esz=1 -> silently + // wrong for non-u8). Other non-ident bases stay esz=1. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); - };}; + } else { if (dotbu != nil && dotbu.sub != nil) { + esz = dotbu.sub.size: i32; + };};}; // base address → push if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -15661,9 +15676,14 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (dotbaseaddr(c, base, "AX")) { + // #257: N_DOT `[N]T`-field base as a call arg → field + // ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr + // emits. Same choke-point as the cgslice #252 site; + // `[]T`/str/`*T` fields fall through to cgexpr. } else { cgexpr(c, base); - };}; + };};}; emitline("\tPUSHQ\tAX\n"); // hi (default base length) → push if (hi != nil) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 14b9b037..552248de 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -274,15 +274,30 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; }; }; + // #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call + // arg) carries no tnode — resolve esz / base-address from the + // checker-stamped element tinfo on base.type_ instead. Cstage + // twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror + // of the cgslice #252 site. + let dotbu: *tinfo = nil; + if (base != nil) { if (base.kind == nkind.N_DOT) { + dotbu = base.type_: *tinfo; + for (dotbu != nil && dotbu.kind == tykind.TY_NAMED) { + dotbu = dotbu.under; + }; + };}; // esz from the type table for an N_IDENT base (#76; mirrors - // the cgindex idiom). Non-ident base stays esz=1 -> ptr - // unscaled, matching cstage's base->kind==N_IDENT gate. + // the cgindex idiom) or an N_DOT array/slice-field base (#257: + // scale by the field's element width, not esz=1 -> silently + // wrong for non-u8). Other non-ident bases stay esz=1. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); - };}; + } else { if (dotbu != nil && dotbu.sub != nil) { + esz = dotbu.sub.size: i32; + };};}; // base address → push if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -311,9 +326,14 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (dotbaseaddr(c, base, "AX")) { + // #257: N_DOT `[N]T`-field base as a call arg → field + // ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr + // emits. Same choke-point as the cgslice #252 site; + // `[]T`/str/`*T` fields fall through to cgexpr. } else { cgexpr(c, base); - };}; + };};}; emitline("\tPUSHQ\tAX\n"); // hi (default base length) → push if (hi != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bc1837bc..b90faf74 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15624,15 +15624,30 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; }; }; + // #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call + // arg) carries no tnode — resolve esz / base-address from the + // checker-stamped element tinfo on base.type_ instead. Cstage + // twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror + // of the cgslice #252 site. + let dotbu: *tinfo = nil; + if (base != nil) { if (base.kind == nkind.N_DOT) { + dotbu = base.type_: *tinfo; + for (dotbu != nil && dotbu.kind == tykind.TY_NAMED) { + dotbu = dotbu.under; + }; + };}; // esz from the type table for an N_IDENT base (#76; mirrors - // the cgindex idiom). Non-ident base stays esz=1 -> ptr - // unscaled, matching cstage's base->kind==N_IDENT gate. + // the cgindex idiom) or an N_DOT array/slice-field base (#257: + // scale by the field's element width, not esz=1 -> silently + // wrong for non-u8). Other non-ident bases stay esz=1. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); - };}; + } else { if (dotbu != nil && dotbu.sub != nil) { + esz = dotbu.sub.size: i32; + };};}; // base address → push if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -15661,9 +15676,14 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (dotbaseaddr(c, base, "AX")) { + // #257: N_DOT `[N]T`-field base as a call arg → field + // ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr + // emits. Same choke-point as the cgslice #252 site; + // `[]T`/str/`*T` fields fall through to cgexpr. } else { cgexpr(c, base); - };}; + };};}; emitline("\tPUSHQ\tAX\n"); // hi (default base length) → push if (hi != nil) { diff --git a/test/wcc/949_dotbase_addr_slice_run.c b/test/wcc/949_dotbase_addr_slice_run.c index b6d19918..f63521aa 100644 --- a/test/wcc/949_dotbase_addr_slice_run.c +++ b/test/wcc/949_dotbase_addr_slice_run.c @@ -1,8 +1,10 @@ /* * 949_dotbase_addr_slice_run — runtime + byte-id net for the array- - * field-base-address family: #252 (the addr-of + slice SIBLING of #135) - * and #253 (the CHAINED-base close-out). 949_dotbase_arr_run covers the - * single-level read/write index path. + * field-base-address family: #252 (the addr-of + slice SIBLING of #135), + * #253 (the CHAINED-base close-out), and #257 (the CALL-ARG consumption + * axis — an inline struct-array-field slice passed straight as a call + * argument). 949_dotbase_arr_run covers the single-level read/write + * index path. * * #252: taking `&x.o[i]` (address-of an element) or slicing * `x.o[lo:hi]` / `x.o[lo:]` of a struct's `[N]T`-typed FIELD computed @@ -377,6 +379,78 @@ static const struct row rows[] = { " let o: top; o.a.b.m[1] = 66u8;\n" " return o.a.b.m[1]: i32;\n" "};\n", 66, 0 }, + /* #257 call-arg consumption axis. An INLINE slice of a struct + * `[N]T`-field passed DIRECTLY as a call argument materialized the + * slice .ptr from the field VALUE, not its ADDRESS: the pushargs + * N_SLICE inline builder's non-ident else-arm did plain cgexpr(base) + * -> the N_DOT field auto-derefs (MOVL field,AX used as .ptr) -> + * callee derefs garbage -> SEGFAULT. The let-init / assign-rhs / + * return / hoist-to-local contexts already routed through the cgslice + * #252 choke-point; only this call-arg builder kept a private + * duplicate. Fix routes the else-arm through cg_dotbase_addr / + * dotbaseaddr (array-field-gated; chained inner via #253) + extends + * the N_IDENT-only esz gate to N_DOT bases (element width from the + * checker-stamped base->type). cs==ww both segfaulted identically + * pre-fix (gate-blind). */ + { "callarg_u8", + "package main;\n" + "type e = struct { o: [4]u8 };\n" + "fn rd(b: []u8) i32 = { return b[0]: i32; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 66u8;\n" + " return rd(x.o[1:4]);\n" + "};\n", 66, 1 }, + { "callarg_i32", + "package main;\n" + "type e = struct { o: [4]i32 };\n" + "fn rd(b: []i32) i32 = { return b[0]; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 88;\n" + " return rd(x.o[1:3]);\n" + "};\n", 88, 1 }, + { "callarg_ptr_u8", + "package main;\n" + "type e = struct { o: [4]u8 };\n" + "fn rd(b: []u8) i32 = { return b[0]: i32; };\n" + "fn f(p: *e) i32 = { return rd(p.o[1:4]); };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 66u8;\n" + " return f(&x);\n" + "};\n", 66, 1 }, + { "callarg_chain", + "package main;\n" + "type inner = struct { m: [4]u8 };\n" + "type outer = struct { p: *inner };\n" + "fn rd(b: []u8) i32 = { return b[0]: i32; };\n" + "export fn main() i32 = {\n" + " let a: inner; a.m[1] = 66u8;\n" + " let o: outer; o.p = &a;\n" + " return rd(o.p.m[1:4]);\n" + "};\n", 66, 1 }, + /* Controls: an N_IDENT slice arg (hoist-to-local) and a bare-local- + * array slice arg take the N_IDENT fast-paths, NOT the N_DOT else-arm + * — assert they still emit correct code. */ + { "callarg_ctrl_local", + "package main;\n" + "type e = struct { o: [4]u8 };\n" + "fn rd(b: []u8) i32 = { return b[0]: i32; };\n" + "export fn main() i32 = {\n" + " let x: e;\n" + " x.o[1] = 66u8;\n" + " let sl: []u8 = x.o[1:4];\n" + " return rd(sl);\n" + "};\n", 66, 1 }, + { "callarg_ctrl_arr", + "package main;\n" + "fn rd(b: []u8) i32 = { return b[0]: i32; };\n" + "export fn main() i32 = {\n" + " let a: [4]u8;\n" + " a[1] = 66u8;\n" + " return rd(a[1:4]);\n" + "};\n", 66, 1 }, { NULL, NULL, 0, 0 } };