diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 7a175a1d..7a88d87b 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1383,6 +1383,16 @@ fn nodeisslice(c: *cgen, n: *syntax.node) bool = { if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) { return syntax.typeisslice(n.type_: *syntax.tinfo); }; + // #6 (Mech A): an unwrap source (`f()!` N_TRYUNW, `r!`/`r?` N_TRYPROP) + // whose success variant is a slice. Without this arm the unwrap fell + // through to the scalar single-PUSHQ default, marshalling .ptr and + // dropping .len/.cap (cgcall's pop sizer under-drains). n.type_ is + // checker-stamped to the success variant (check.ww N_TRYPROP/N_TRYUNW), + // mirroring cstage node_isslice = type_isslice(n->type). Twin of the + // N_UN(TK_STAR) #9 arm above. + if (k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TRYPROP) { + return syntax.typeisslice(n.type_: *syntax.tinfo); + }; return false; }; @@ -1486,6 +1496,13 @@ fn nodeisstr(c: *cgen, n: *syntax.node) bool = { if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) { return syntax.typeisstr(n.type_: *syntax.tinfo); }; + // #6 (Mech A): str twin of the N_TRYUNW/N_TRYPROP slice arm in + // nodeisslice — an unwrap source whose success variant is a str. + // n.type_ is checker-stamped to the success variant; mirrors cstage + // node_isstr = type_isstr(n->type). + if (k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TRYPROP) { + return syntax.typeisstr(n.type_: *syntax.tinfo); + }; return false; }; diff --git a/test/lang/unwrap_callarg_str_test.ww b/test/lang/unwrap_callarg_str_test.ww new file mode 100644 index 00000000..30ae2e21 --- /dev/null +++ b/test/lang/unwrap_callarg_str_test.ww @@ -0,0 +1,43 @@ +// unwrap_callarg_str_test — runtime contract for a whole 24B str header +// produced by an UNWRAP source (`mk_se()!` N_TRYUNW) passed BY VALUE as a +// call argument. #6 (Mechanism A): cgenutil.ww's nodeisstr had no +// N_TRYUNW/N_TRYPROP arm, so the unwrap 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. cstage (cmd/w6c/cgen.c node_isstr) was always +// type-keyed and correct; this aligns wwstage UP. Twin of #9's +// deref_callarg str pin — same masking mechanism, different node kind. +// +// WHY the poison call: a pre-fix `strlen(mk_se()!)` pushes only .ptr, so +// the callee reads .len from whatever arg register the caller left. The +// preceding NORMAL-arg call strlen(decoy) deterministically leaves the +// callee's .len register holding decoy's len 2 (≠ backing's 5), so a +// pre-fix 1-word push reads 2 and the assert REDDENS. This is a wwstage- +// only gap (cs≠ww), so the tooth is byte-id (cstage already pushed 3) AND +// value-under-the-wwstage-frontend; byte-id (990-996) proves the stages +// AGREE, not that the code is correct. + +package unwrap_callarg_str_test; + +type e = !i32; + +fn mk_se() (str | e) = { return "abcde"; }; + +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 str_unwrap_callarg() void = { + // Decoy ("xy", len 2) poisons the callee's .len register via a + // normal-arg call; the unwrap-source call must overwrite it with 5. + let decoy: str = "xy"; + assert(strlen(decoy) == 2); + assert(strlen(mk_se()!) == 5); + // .cap is the 3rd dropped word; decoy ("xy") seeds cap 2, so a + // pre-fix 1-word push leaves the callee reading 2 here, not 5. + assert(strcap(mk_se()!) == 5); + // strfirst pins .ptr survived the push ('a' == 97). + assert(strfirst(mk_se()!) == 97); +};