w6c+cgen: read .cap of an indexed str/slice array element (fix #13, #20 read-sibling)

`t[i].cap` (t a `[N][]u8` / `[N]str`) miscompiled in BOTH stages,
divergently — the read-side sibling of #20's store fix. cgexpr on the
indexed element leaves the full {ptr,len,cap} header (AX/BX/CX via
cgslicehdr), but the `.cap` field-selector never shuffled CX→AX:
cstage's typed pseudo-field else-branch handled only .ptr/.len, so
`.cap` fell through returning AX=.ptr; wwstage's cgdot non-ident
catch-all likewise handled only .ptr/.len, emitting no read (stale AX).
`t[i].len` already worked (BX→AX shuffle) — only `.cap` was missing.

Fix mirrors the .len shuffle: add the .cap CX→AX arm in both stages.
The shuffle fires ONLY for a typed slice/str base (TY_SLICE/TY_STR
after NAMED-chase); an untyped str literal (`"abc".cap`) leaves only
AX=ptr/BX=len and must return AX unshuffled — keeping the wwstage
catch-all byte-identical with cstage, whose cap-shuffle lives in the
typed branch, not the untyped catch-all.

Validated direct `t[i].cap` (slice + str, elements 0/1) against the
whole-element-copy oracle (`let q=t[i]; q.cap`, made correct by #20),
plus .len-after-index regression pins, in test 683; dual-stage runtime
+ byte-id (36/36 ok). combined.ww regenerated.
This commit is contained in:
2026-06-03 17:50:54 +09:00
parent b3d4d2df32
commit 84ed2ab15a
5 changed files with 126 additions and 9 deletions

View File

@@ -7991,12 +7991,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_AX));
} else {
/* Evaluate the str/slice expression — leaves
* (AX=ptr, BX=len) for str. .ptr returns AX,
* .len shuffles BX→AX. */
* the full (AX=ptr, BX=len, CX=cap) header
* (cgslicehdr) for an indexed element / non-ident
* base. .ptr returns AX, .len shuffles BX→AX,
* .cap shuffles CX→AX. The .cap shuffle is the
* #13 read-fix (sibling of the #20 store): pre-fix
* the else-arm handled only .len, so `t[i].cap`
* fell through returning AX=.ptr. */
cgexpr(c, n->lhs, locals);
if (lenfld)
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
else if (capfld)
ins2(c, A_MOVQ, areg(D_CX),
areg(D_AX));
}
break;
}

View File

@@ -22611,6 +22611,26 @@ fn cgdot(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, AX\n");
return;
};
// .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.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
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
|| lu.kind == tykind.TY_STR)) {
emitline("\tMOVQ\tCX, AX\n");
};
};
return;
};
// Chained struct-field-via-ptr-via-ptr access:
// r.sym.val where r: *lrel, .sym: *lsym, .val: u64
// Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct

View File

@@ -2887,6 +2887,26 @@ fn cgdot(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, AX\n");
return;
};
// .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.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
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
|| lu.kind == tykind.TY_STR)) {
emitline("\tMOVQ\tCX, AX\n");
};
};
return;
};
// Chained struct-field-via-ptr-via-ptr access:
// r.sym.val where r: *lrel, .sym: *lsym, .val: u64
// Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct

View File

@@ -22611,6 +22611,26 @@ fn cgdot(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, AX\n");
return;
};
// .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.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil) {
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
|| lu.kind == tykind.TY_STR)) {
emitline("\tMOVQ\tCX, AX\n");
};
};
return;
};
// Chained struct-field-via-ptr-via-ptr access:
// r.sym.val where r: *lrel, .sym: *lsym, .val: u64
// Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct

View File

@@ -21,13 +21,16 @@
* for `is_str_el || is_slice_el`; wwstage adds isslicel (TY_SLICE ->
* esz = esubti.size, fixing the stride) and the matching 3-word store.
*
* Cap is validated via a WHOLE-ELEMENT COPY (`let q = t[i]; q.cap`), NOT
* a direct `t[i].cap` read: the `.cap` field-extract on an INDEXED slice/
* str element is a SEPARATE, still-open bug (returns .ptr on cstage,
* emits no read on wwstage — divergent), the "cgindex truncating-to-ptr"
* sibling cited at cgen.c:9039-9040, filed as task #13. A whole-element
* copy reads the stored cap through the (correct) ident-load path, so it
* exercises THIS fix's stored cap word without hitting #13.
* Cap is validated BOTH ways: a WHOLE-ELEMENT COPY (`let q = t[i]; q.cap`)
* AND a DIRECT `t[i].cap` read. The direct read was a SEPARATE bug (task
* #13, the #20 store's read-sibling): the `.cap` field-extract on an
* INDEXED slice/str element returned .ptr on cstage (it shuffled only
* .len BX→AX, leaving AX=.ptr for cap) and emitted NO read on wwstage
* (the cgdot non-ident catch-all handled only .ptr/.len) — divergent.
* Fix (BOTH stages, byte-id): cgexpr leaves the full {ptr,len,cap} header
* via cgslicehdr for an indexed element, so .cap shuffles CX→AX, the twin
* of the .len BX→AX shuffle. The direct-cap rows must equal the
* whole-element-copy oracle (which #20 made correct).
*
* Mutation-sanity (the per-word coverage): the .len rows fail if the
* store drops the .len word (the pre-fix slice 1-word store), and the
@@ -128,6 +131,52 @@ static const struct row rows[] = {
"};\n",
7 },
/* #13 direct-read rows: `t[i].cap` straight (no copy via a let).
* Must equal the slice_cap_copy / str_cap_copy oracle. Pre-fix:
* cstage returned .ptr (a heap/stack address, != 7/6), wwstage
* emitted no read (stale AX) — and the two diverged. */
{ "slice_cap_direct0",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 7;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 2; b.cap = 6;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\treturn t[0].cap: i32;\n"
"};\n",
7 },
{ "slice_cap_direct1",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 7;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 2; b.cap = 6;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\treturn t[1].cap: i32;\n"
"};\n",
6 },
{ "str_cap_direct0",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: str = \"abcde\";\n"
"\tlet b: str = \"xy\";\n"
"\tlet t: [2]str = [a, b];\n"
"\treturn t[0].cap: i32;\n"
"};\n",
5 },
{ "str_cap_direct1",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: str = \"abcde\";\n"
"\tlet b: str = \"xy\";\n"
"\tlet t: [2]str = [a, b];\n"
"\treturn t[1].cap: i32;\n"
"};\n",
2 },
{ "slice_ptr",
"package main;\n"
"export fn main() i32 = {\n"