wwstage: marshal full slice/str header for deref-source call arg (#9)

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.
This commit is contained in:
2026-06-27 17:19:43 +09:00
parent 597a6ba7ff
commit 147cb4b4be
2 changed files with 85 additions and 5 deletions

View File

@@ -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;
};

View File

@@ -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);
};