diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b0b47473..3cd691c5 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 84ff3f43..88e92d15 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index feb4dac9..117f2fd9 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d47ac05d..4f0dc64b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 diff --git a/test/wcc/683_arr_strslice_elem.c b/test/wcc/683_arr_strslice_elem.c index f992c46e..b0460f1f 100644 --- a/test/wcc/683_arr_strslice_elem.c +++ b/test/wcc/683_arr_strslice_elem.c @@ -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"