w6c_ww: exclude bare str-literal from indexed .cap shuffle (fix #13 symmetry break)

The #13 .cap read-fix gated the wwstage CX→AX shuffle on the base's
type being TY_SLICE/TY_STR, on the assumption that a bare string literal
types as untyped_str and so misses the gate (matching cstage, whose
cap-shuffle lives only in the typed pseudo-field branch). That assumption
is false on wwstage: its checker stamps N_STRLIT as `str` (check.ww:2322),
not untyped_str as cstage does (check.c:1079). So `"abc".cap` passed the
TY_STR gate and emitted a stray `MOVQ CX, AX` on wwstage only — while
cstage's untyped catch-all never shuffles it — a rule-10 byte-id break.

A string literal's cgexpr loads only AX=ptr/BX=len (cgen.ww N_STRLIT),
never a CX cap, so the shuffle was garbage on top of divergent. Exclude
N_STRLIT from the gate: `"abc".cap` now returns AX unshuffled on both
stages, byte-identical. The typed `t[i].cap` path (lhs N_INDEX) is
unaffected.

The underlying N_STRLIT type divergence (cstage untyped_str vs wwstage
str) is a separate latent checker issue, filed for follow-up; this commit
keeps the cgen byte-identical regardless.

683: new BYTEID_ONLY row str_lit_cap_symmetry pins the edge (asm-byte-id
asserted; runtime value is a link-time address). 39/39 ok; test-unit
251/251; smoke cs==ww; sizelint clean. combined.ww (w6c + wwdump) regen'd.
This commit is contained in:
2026-06-03 18:03:13 +09:00
parent 84ed2ab15a
commit 63cb39e45b
4 changed files with 57 additions and 19 deletions

View File

@@ -22614,14 +22614,18 @@ fn cgdot(c: *cgen, n: *node) void = {
// .cap on a non-ident base (indexed element `t[i].cap`, call,
// dot-slice): cgexpr leaves the full {ptr,len,cap} header via
// cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped
// str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return
// AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the
// typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the
// untyped catch-all. #13 read-fix, sibling of the #20 store.
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT
// a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len
// (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled.
// cstage reaches that outcome by typing N_STRLIT as untyped_str
// (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types
// N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 —
// divergence filed separately), so the TY_STR kind-gate alone would
// wrongly fire. The N_STRLIT exclusion keeps this byte-identical with
// cstage. #13 read-fix, sibling of the #20 store.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
if (lhs != nil && lhs.kind != nkind.N_STRLIT) {
let lu: *tinfo = lhs.type_: *tinfo;
for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; };
if (lu != nil && (lu.kind == tykind.TY_SLICE

View File

@@ -2890,14 +2890,18 @@ fn cgdot(c: *cgen, n: *node) void = {
// .cap on a non-ident base (indexed element `t[i].cap`, call,
// dot-slice): cgexpr leaves the full {ptr,len,cap} header via
// cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped
// str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return
// AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the
// typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the
// untyped catch-all. #13 read-fix, sibling of the #20 store.
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT
// a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len
// (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled.
// cstage reaches that outcome by typing N_STRLIT as untyped_str
// (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types
// N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 —
// divergence filed separately), so the TY_STR kind-gate alone would
// wrongly fire. The N_STRLIT exclusion keeps this byte-identical with
// cstage. #13 read-fix, sibling of the #20 store.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
if (lhs != nil && lhs.kind != nkind.N_STRLIT) {
let lu: *tinfo = lhs.type_: *tinfo;
for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; };
if (lu != nil && (lu.kind == tykind.TY_SLICE

View File

@@ -22614,14 +22614,18 @@ fn cgdot(c: *cgen, n: *node) void = {
// .cap on a non-ident base (indexed element `t[i].cap`, call,
// dot-slice): cgexpr leaves the full {ptr,len,cap} header via
// cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped
// str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return
// AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the
// typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the
// untyped catch-all. #13 read-fix, sibling of the #20 store.
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT
// a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len
// (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled.
// cstage reaches that outcome by typing N_STRLIT as untyped_str
// (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types
// N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 —
// divergence filed separately), so the TY_STR kind-gate alone would
// wrongly fire. The N_STRLIT exclusion keeps this byte-identical with
// cstage. #13 read-fix, sibling of the #20 store.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
if (lhs != nil && lhs.kind != nkind.N_STRLIT) {
let lu: *tinfo = lhs.type_: *tinfo;
for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; };
if (lu != nil && (lu.kind == tykind.TY_SLICE