From 147cb4b4be4b20b308a3d4d7244e97eaa0d7a6f1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 27 Jun 2026 17:19:43 +0900 Subject: [PATCH] wwstage: marshal full slice/str header for deref-source call arg (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nodeisslice/nodeisstr lacked an N_UN(TK_STAR) arm, so a deref-source slice/str call arg (f(*h), h:*[]T) fell to the scalar single-PUSHQ default — marshalling only .ptr and dropping .len/.cap. Add the type-keyed arm (read checker-stamped n.type_, mirror cstage node_isslice/node_isstr and the sibling N_DOT/N_INDEX arms). cgen already loads the full 24B header (C1b c67f362); this fixes only the call-arg push/pop count. wwstage-only align-up; cstage was always correct. --- selfhost/cmd/wcc/cgenutil.ww | 26 +++++++++++--- test/lang/deref_callarg_test.ww | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 test/lang/deref_callarg_test.ww diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index c237ed33..7a175a1d 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1371,6 +1371,18 @@ fn nodeisslice(c: *cgen, n: *syntax.node) bool = { if (k == syntax.nkind.N_INDEX) { return syntax.typeisslice(n.type_: *syntax.tinfo); }; + // #9 (C1c): `*h` where h:*[]T — an N_UN TK_STAR deref whose pointee is + // a slice. C1b (c67f362) fixed the 24B header LOAD; this arm fixes the + // call-arg push COUNT (pushargsrev's slice arm :1194 and cgcall's pop + // sizer cgenexpr.ww:8098 both key off this recognizer). Without it the + // deref fell through to the scalar single-PUSHQ default, marshalling + // .ptr and dropping .len/.cap. Read the checker-stamped n.type_ + // (unoptype TK_STAR returns the pointee, check.ww:2967-2981), mirroring + // cstage node_isslice = type_isslice(n->type) (cmd/w6c/cgen.c:226-228) + // and the N_DOT/N_INDEX arms above. + if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) { + return syntax.typeisslice(n.type_: *syntax.tinfo); + }; return false; }; @@ -1385,11 +1397,7 @@ fn nodeisslice(c: *cgen, n: *syntax.node) bool = { // recognizer because the stamp is on a sub-node (callee return / cast // target), not on `n` itself. Covered: N_STRLIT, N_IDENT (local stamp / // def stamp), N_CALL (return type), N_INDEX (element stamp — #46 F7-c2), -// N_DOT (n.type_ — #56 A.6.3h), N_CAST. -// Not covered (separate bugs / out of scope): -// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX` -// and never loads .len into BX; fixing the recognizer alone won't -// help. Tracked alongside the broader cgun-load-shape gap. +// N_DOT (n.type_ — #56 A.6.3h), N_CAST, N_UN(TK_STAR) (#9 C1c). fn nodeisstr(c: *cgen, n: *syntax.node) bool = { if (n == nil) { return false; }; let k: syntax.nkind = n.kind; @@ -1470,6 +1478,14 @@ fn nodeisstr(c: *cgen, n: *syntax.node) bool = { if (k == syntax.nkind.N_CAST) { return isstrtype(c, n.rhs); }; + // #9 (C1c): `*sp` where sp:*str — the str twin of the N_UN TK_STAR + // slice arm in nodeisslice. The same recognizer drives the call-arg + // push count and cgcall's pop sizer; reading the checker-stamped + // n.type_ (pointee per unoptype TK_STAR, check.ww:2967-2981) mirrors + // cstage node_isstr = type_isstr(n->type) (cmd/w6c/cgen.c:213-216). + if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) { + return syntax.typeisstr(n.type_: *syntax.tinfo); + }; return false; }; diff --git a/test/lang/deref_callarg_test.ww b/test/lang/deref_callarg_test.ww new file mode 100644 index 00000000..897a2a87 --- /dev/null +++ b/test/lang/deref_callarg_test.ww @@ -0,0 +1,64 @@ +// deref_callarg_test — runtime contract for a whole 24B str/slice header +// passed BY VALUE as a call argument whose SOURCE is a deref (`f(*h)` with +// h:*str / h:*[]T). #9 (C1c): cgenutil.ww's nodeisslice/nodeisstr had no +// N_UN(TK_STAR) arm, so `*h` fell through to the scalar single-PUSHQ default +// — pushargsrev marshalled only .ptr (1 word) and cgcall's pop sizer drained +// one word, so the callee read .len/.cap from stale arg registers. C1b +// (c67f362) already fixed the 24B header LOAD; this pins the call-arg push +// COUNT. cstage (cmd/w6c/cgen.c node_isslice/node_isstr) was always type- +// keyed and correct; this aligns wwstage UP. +// +// WHY the poison call: a pre-fix `f(*h)` pushes only .ptr, so the callee +// reads .len from whatever arg register the caller left — NOT from the +// deref load (which lands .len in BX, a reg the callee never reads). A bare +// decoy header does not survive into that register, so the bug can hide +// behind a residue coincidence (observed: a naive str case falsely PASSED). +// The preceding NORMAL-arg call f(decoy) deterministically leaves the +// callee's .len register holding decoy's len (≠ backing's), so a pre-fix +// f(*h) reads the decoy's len and the assert REDDENS. Verified +// reddens-on-revert: with the N_UN arm removed both fns RED, with it +// restored both GREEN. byte-id (990-996) proves the two stages AGREE, not +// that the code is correct — this gap was byte-id-identical on both stages. + +package deref_callarg_test; + +fn slclen(s: []i32) i32 = { return s.len: i32; }; + +fn slccap(s: []i32) i32 = { return s.cap: i32; }; + +fn slcsum(s: []i32) i32 = { return s[0] + s[1]; }; + +fn strlen(s: str) i32 = { return s.len: i32; }; + +fn strcap(s: str) i32 = { return s.cap: i32; }; + +fn strfirst(s: str) i32 = { return s[0]: i32; }; + +@test fn slice_deref_callarg() void = { + let backing: []i32 = [10i32, 20i32, 30i32, 40i32]; + let h: *[]i32 = &backing; + let decoy: []i32 = [1i32]; + // Poison the callee's .len register with decoy's 1 via a normal-arg + // call, then the deref-call must overwrite it with backing's 4. + assert(slclen(decoy) == 1); + assert(slclen(*h) == 4); + // .cap is the 3rd dropped word; decoy ([1]) seeds cap 1, so a pre-fix + // 1-word push leaves the callee reading 1 here, not backing's 4. + assert(slccap(*h) == 4); + // slcsum pins .ptr survived the push (10 + 20). + assert(slcsum(*h) == 30); +}; + +@test fn str_deref_callarg() void = { + // str IS []u8 — same 3-word arg. Decoy ("xy", len 2) poisons the + // callee's .len register so a pre-fix strlen(*sp) reads 2, not 5. + let backing: str = "abcde"; + let sp: *str = &backing; + let decoy: str = "xy"; + assert(strlen(decoy) == 2); + assert(strlen(*sp) == 5); + // .cap twin of slccap; decoy ("xy") seeds cap 2, not backing's 5. + assert(strcap(*sp) == 5); + // strfirst pins .ptr survived the push ('a' == 97). + assert(strfirst(*sp) == 97); +};