wcc: tuple slice-element read loads the full 24B header, both stages

Reading a slice-typed tuple element (t.0) loaded only the pointer
word; len and cap took whatever was left in BX/CX — silent garbage in
BOTH stages once anything clobbered the registers between build and
read. Load all three header words at the tuple-element arm. Review
item #28.

Both stages move in one commit: one emission contract; splitting
would leave the byte-id gates red between the halves.
This commit is contained in:
2026-06-12 21:04:35 +09:00
parent 850746cfa8
commit 0a6f500b8c
5 changed files with 73 additions and 12 deletions

View File

@@ -11319,15 +11319,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_X0)); areg(D_X0));
break; break;
} }
/* str IS []u8 — load (ptr, len, cap) into /* str IS []u8, and a slice is the same 24B
* (AX, BX, CX), the canonical slice-header ABI, * {ptr,len,cap} header — load all three words into
* so chains like `t.1.len` propagate through the * (AX, BX, CX), the canonical slice-header ABI, so
* slice-rhs convention (#1/Phase 3 collapse). * chains like `t.1.len` propagate through the
* UNLIKE the field arms there is no slice-element * slice-rhs convention (#1/Phase 3 collapse). #28:
* sibling here, so the triple is hand-authored; * pre-fix the gate was str-only, so a SLICE tuple
* base is BP (frame) or CX (global base — CX is * element fell to the fldloadop tail (one ptr word;
* written LAST so it survives the +0/+8 reads). */ * len/cap stale). base is BP (frame) or CX (global
if (fu && fu->kind == TY_STR) { * base — CX written LAST so it survives the +0/+8). */
if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) {
ins2(c, A_MOVQ, ins2(c, A_MOVQ,
amem(base_reg, base_disp + foff + 0), amem(base_reg, base_disp + foff + 0),
areg(D_AX)); areg(D_AX));

View File

@@ -26646,7 +26646,14 @@ fn cgdot(c: *cgen, n: *node) void = {
}; };
if (tp != nil) { if (tp != nil) {
let tpt: *node = tp.lhs; let tpt: *node = tp.lhs;
if (isstrtype(c, tpt)) { // str IS []u8, and a slice is the same 24B
// {ptr,len,cap} header — load all three words
// into (AX, BX, CX). #28: pre-fix the gate was
// str-only, so a SLICE tuple element fell to
// the scalar tail below (one ptr word; len/cap
// took stale registers). base is BP (frame),
// so the triple order has no clobber risk.
if (isstrtype(c, tpt) || isslicetype(c, tpt)) {
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64); emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n"); emitline("(BP), AX\n");

View File

@@ -3495,7 +3495,14 @@ fn cgdot(c: *cgen, n: *node) void = {
}; };
if (tp != nil) { if (tp != nil) {
let tpt: *node = tp.lhs; let tpt: *node = tp.lhs;
if (isstrtype(c, tpt)) { // str IS []u8, and a slice is the same 24B
// {ptr,len,cap} header — load all three words
// into (AX, BX, CX). #28: pre-fix the gate was
// str-only, so a SLICE tuple element fell to
// the scalar tail below (one ptr word; len/cap
// took stale registers). base is BP (frame),
// so the triple order has no clobber risk.
if (isstrtype(c, tpt) || isslicetype(c, tpt)) {
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64); emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n"); emitline("(BP), AX\n");

View File

@@ -26646,7 +26646,14 @@ fn cgdot(c: *cgen, n: *node) void = {
}; };
if (tp != nil) { if (tp != nil) {
let tpt: *node = tp.lhs; let tpt: *node = tp.lhs;
if (isstrtype(c, tpt)) { // str IS []u8, and a slice is the same 24B
// {ptr,len,cap} header — load all three words
// into (AX, BX, CX). #28: pre-fix the gate was
// str-only, so a SLICE tuple element fell to
// the scalar tail below (one ptr word; len/cap
// took stale registers). base is BP (frame),
// so the triple order has no clobber risk.
if (isstrtype(c, tpt) || isslicetype(c, tpt)) {
emitline("\tMOVQ\t"); emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64); emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n"); emitline("(BP), AX\n");

View File

@@ -100,6 +100,45 @@ static const struct row rows[] = {
" let v: []u8 = s: []u8;\n" " let v: []u8 = s: []u8;\n"
" return v.cap: i32;\n" " return v.cap: i32;\n"
"};\n", 4, K_RUN, NULL }, "};\n", 4, K_RUN, NULL },
/* #28 (#263 both-stages): tuple slice-element read `t.0` over a
* ([]u8, i64). A clobber() call between the tuple build and the
* read leaves junk in BX/CX; pre-fix the str-only arm fell to the
* scalar tail (ptr word only), so ys.len read stale BX. Reads len
* == 5 (distinct from any stale value) → 0. */
{ "tuple_slice_elem",
"package main;\n"
"fn clobber() i64 = {\n"
" let a: i64 = 111;\n"
" let b: i64 = 222;\n"
" return a + b;\n"
"};\n"
"export fn main() i32 = {\n"
" let xs: []u8 = [1u8, 2u8, 3u8, 4u8, 5u8];\n"
" let t: ([]u8, i64) = (xs, 7);\n"
" let junk: i64 = clobber();\n"
" let ys: []u8 = t.0;\n"
" if (ys.len == 5) { return 0; };\n"
" return 1;\n"
"};\n", 0, K_RUN, NULL },
/* #28 sibling: the slice element sits at tuple position 1 (foff != 0:
* an i64 occupies slot 0, the []u8 header starts at +8). Exercises the
* widened arm's offset arithmetic, which the position-0 row leaves
* untested. Same clobber()-between-build-and-read trigger; len == 6. */
{ "tuple_slice_elem_pos1",
"package main;\n"
"fn clobber() i64 = {\n"
" let a: i64 = 111;\n"
" let b: i64 = 222;\n"
" return a + b;\n"
"};\n"
"export fn main() i32 = {\n"
" let xs: []u8 = [1u8, 2u8, 3u8, 4u8, 5u8, 6u8];\n"
" let t: (i64, []u8) = (9, xs);\n"
" let junk: i64 = clobber();\n"
" let ys: []u8 = t.1;\n"
" if (ys.len == 6) { return 0; };\n"
" return 1;\n"
"};\n", 0, K_RUN, NULL },
}; };
static int static int