selfhost+test: route composite CALL return through nodeisslice (#24)

Wwstage call-arg-emit recognized slice args only when source was
IDENT/SLICE/CAST/DOT. For N_CALL returning []T the natural-push
fallthrough emitted one PUSHQ AX (lost .len/.cap) and cgcall's
pop-count under-drained by 2 words — corrupting R8/R9 and every
subsequent arg. Class A runtime miscompile with stack misalignment
and 3-POPs-of-garbage at the receiving call. Sister to #21
(tagged-CALL arg) but for plain []u8 slice, not tagged-variant —
wwstage's pushargsrev grew the tagged-CALL arm at #21 and never
grew the plain-composite arm.

Surfaced by lib/strings landing's `bytes.X(toutf8(in), p)` call
sites: 995_self_rebuild's wwstage rebuild tripped on byte-id
divergence at w6c_ww + wwdump_ww emit. 967_bytes_run was green
because `ww run` exercises the cstage path. Corpus-coverage-blind
on the wwstage side until lib/strings pulled the chain through
wwstage compilation.

Fix is minimal: `nodeisslice` (selfhost/cmd/wcc/cgenutil.ww) gains
an N_CALL arm structurally identical to the existing N_CALL arm in
`nodeisstr` (only swap: isslicetype for isstrtype). The downstream
natural-push slice path (PUSHQ CX/BX/AX, extra=2 pop-count) was
already correct — it just needed the N_CALL-of-slice-return shape
to be recognized as a slice. pushargsrev and cgcall untouched.

Polarity catalog: wwstage UNDER — missing N_CALL arm in slice
shape recognition. Convergence wwstage → cstage per rule 10
(cstage reads typed-AST `type_isslice` natively).

Tests:
  - 723_composite_call_arg pins the 3-PUSH order (CX, BX, AX)
    between `CALL view` and next CALL on canonical `f(g())` shape,
    plus cstage vs wwstage cmp -s byte-id.
  - 927_composite_call_arg_run runtime-pins 7 rows × 2 stages =
    14 fixtures: canonical, slice-CALL + let-slice (hasprefix
    shape), two composite-CALL args (arg-shift collision),
    middle-argpos, nested composite-in-composite, slice + scalar
    pop-count mix, tagged-CALL regression alongside (confirms
    #21 still holds).

95/95 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
2026-05-18 02:20:42 +09:00
parent 53c9e46c21
commit 0e2c6cd893
6 changed files with 570 additions and 0 deletions

View File

@@ -481,6 +481,22 @@ fn nodeisslice(c: *cgen, n: *node) bool = {
};
if (k == nkind.N_SLICE) { return true; };
if (k == nkind.N_CAST) { return isslicetype(c, n.rhs); };
// #24: N_CALL returning a slice — cgexpr leaves (AX=ptr,
// BX=len, CX=cap); pushargsrev's slice arm pushes CX/BX/AX
// and cgcall pops 3 words. Without this arm the natural-push
// fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop
// side under-drains by 2 words, leaving R8/R9 unset for the
// receiver. Mirrors nodeisstr's N_CALL arm just below.
if (k == nkind.N_CALL) {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rt: *node = fnretlookup(c, callee.str);
return isslicetype(c, rt);
};
};
return false;
};
// N_DOT to a slice field: resolve the field through the struct
// (or *struct) the base ident / inner chain lands on, then check
// the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg