From 451e2ebec9d34fe5152bd832e5ecab0f7cc29c9d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 25 May 2026 00:20:16 +0900 Subject: [PATCH] cgen: slice-elem store/read -> 3-word via kind-OR (both stages) The G-cluster gave str its 3-word {ptr,len,cap} store/read at indexed/field/chained sites, but each arm was gated on str only; the slice arm fell through to the 1-word fldstoreop default, dropping len+cap. A []T value stored through arr[i]=, arr[i].f=, *struct.f=, or value-spine o.i.f= (and read back via arr[i] / arr[i].f) silently lost length and capacity. Widen all six arms (4 stores + 2 read mirrors) with a kind-OR (TY_STR||TY_SLICE / typeisstr||typeisslice), never a size test: str and slice are both 24B, so a width gate would fire on both and mask the missing slice arm. The str kind stays distinct and nominal -- the arm is widened, the kinds are not collapsed. cstage and wwstage mirrored. Gate-blind class: store and read were both short, so byte-identity and cstage==wwstage stayed green on self-consistent garbage; only a runtime len/cap round-trip exposes it (test 941, table-driven, 4 shapes x 2 stages, fail-before/pass-after on both ww and ww_ww). Deref store (*p=) and tuple-elem store (N_MLET/N_MASSIGN, distinct DX,CX,R8 return-ABI) are the same bug class but separate folds. --- Makefile | 7 + cmd/w6c/cgen.c | 50 +++--- selfhost/cmd/w6c/main.combined.ww | 67 ++++---- selfhost/cmd/wcc/cgenexpr.ww | 67 ++++---- selfhost/cmd/wwdump/main.combined.ww | 67 ++++---- test/wcc/941_slice_store_cap_run.c | 244 +++++++++++++++++++++++++++ 6 files changed, 387 insertions(+), 115 deletions(-) create mode 100644 test/wcc/941_slice_store_cap_run.c diff --git a/Makefile b/Makefile index 30f2bbee..62d45662 100644 --- a/Makefile +++ b/Makefile @@ -262,6 +262,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_str_arrfield_store_cap_run \ $(BIN)/test_str_chainfield_store_cap_run \ $(BIN)/test_str_massign_store_cap_run \ + $(BIN)/test_slice_store_cap_run \ $(BIN)/test_str_forrange_loopvar_run \ $(BIN)/test_composite_call_arg \ $(BIN)/test_composite_call_arg_run \ @@ -683,6 +684,12 @@ $(BIN)/test_str_forrange_loopvar_run: test/wcc/940_str_forrange_loopvar_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_slice_store_cap_run: test/wcc/941_slice_store_cap_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_composite_call_arg: test/wcc/723_composite_call_arg.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index a480ff78..3476cccb 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2887,8 +2887,9 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } if (n->op == TK_ASSIGN - && fu && fu->kind == TY_STR) { - /* str IS []u8: rhs leaves + && fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + /* str/slice: rhs leaves * AX=ptr, BX=len, CX=cap * (#1/Phase 3). Spill all * three across the index/ @@ -3111,8 +3112,9 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BX, foff)); break; } - if (fu && fu->kind == TY_STR) { - /* str IS []u8: rhs leaves AX=ptr, + if (fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + /* str/slice: rhs leaves AX=ptr, * BX=len, CX=cap (#1/Phase 3). Spill * all three across the base-expr eval * (it may clobber any reg), stage the @@ -3310,8 +3312,9 @@ cgexpr(Cg *c, Node *n, Local *locals) int fsz = (int)(leaf_type ? leaf_type->size : 8); int store_op = fldstoreop(leaf_type, fsz); - if (fu && fu->kind == TY_STR) { - /* str IS []u8: store ptr/len/cap. cgexpr + if (fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + /* str/slice: store ptr/len/cap. cgexpr * leaves CX=cap, so the via_cx base goes in * DX (not CX) to avoid clobbering it — same * as the single-dot str field store @@ -3578,6 +3581,7 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *eff = idx_eff(bt); int esz = (eff && eff->sub) ? (int)eff->sub->size : 1; int elem_is_str = eff && eff->sub && type_isstr(eff->sub); + int elem_is_slice = eff && eff->sub && type_isslice(eff->sub); Type *esub = eff ? eff->sub : NULL; Type *esubu = (esub && esub->kind == TY_NAMED) ? esub->under : esub; @@ -3645,9 +3649,9 @@ cgexpr(Cg *c, Node *n, Local *locals) } if (is_arr || is_sl || is_ptr) { cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */ - /* str IS []u8: stash cap+len so all three store + /* str/slice: stash cap+len so all three store * (#1/Phase 3). */ - if (elem_is_str) { + if (elem_is_str || elem_is_slice) { ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ } @@ -3689,8 +3693,8 @@ cgexpr(Cg *c, Node *n, Local *locals) ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */ ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */ - if (elem_is_str) { - /* str IS []u8: store ptr/len/cap (#1/Phase 3). */ + if (elem_is_str || elem_is_slice) { + /* str/slice: store ptr/len/cap (#1/Phase 3, #7). */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0)); ins1(c, A_POPQ, areg(D_CX)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8)); @@ -6141,8 +6145,9 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; - if (fu && fu->kind == TY_STR) { - /* str IS the 3-word {ptr,len,cap} + if (fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + /* str/slice: the 3-word {ptr,len,cap} * slice header (#1). AX holds the * element base, so load .ptr (which * targets AX) LAST. Matches the @@ -6232,12 +6237,13 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); } ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); - /* str element: load the full (ptr, len, cap) header - * into (AX, BX, CX) — str is 24B since #1, so the cap - * word must survive. Kind-gate on type_isstr, never - * size==24 (slices are 24B too; a size gate bleeds into - * the general >16B struct path, #10). Base is BX. */ - if (u->sub && type_isstr(u->sub)) { + /* str/slice element: load the full (ptr, len, cap) header + * into (AX, BX, CX) — both are 24B since #1, so the cap + * word must survive. Kind-gate on type_isstr||type_isslice, + * never size==24: a >16B struct is 24B+ too but takes the + * struct-copy path, not this 3-word header load (#10). + * Base is BX. */ + if (u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) { cgslicehdr(c, D_BX); break; } @@ -6281,10 +6287,10 @@ cgexpr(Cg *c, Node *n, Local *locals) cgexpr(c, n->lhs, locals); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); - /* str element via fallback base: load the full (ptr, len, - * cap) header into (AX, BX, CX). Kind-gate on type_isstr, - * never size==24 (see Site A). Base is AX. */ - if (u && u->sub && type_isstr(u->sub)) { + /* str/slice element via fallback base: load the full (ptr, len, + * cap) header into (AX, BX, CX). Kind-gate on type_isstr|| + * type_isslice, never size==24 (see Site A). Base is AX. */ + if (u && u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) { cgslicehdr(c, D_AX); break; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 588bf88f..f4fa3027 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14556,12 +14556,15 @@ fn cgindex(c: *cgen, n: *node) void = { let idx: *node = n.rhs; let esz: i32 = 8; let signed_elem: bool = false; - // #1/Phase 3: str=24B collides with slice=24B, so the str-element - // branches below MUST gate on kind (mirroring cstage's elem_is_str), - // not a bare `esz == primtypesize("str")` size check — otherwise a - // []u8 element (also 24B) misfires into the str 2-word load and - // diverges from cstage (#60 collision class; sentinel 754). + // #1/Phase 3: str and slice are both 24B (and a >16B struct is + // 24B+ too), so the header branches below MUST gate on KIND + // (elemisstr/elemisslice, mirroring cstage's elem_is_str|| + // elem_is_slice), not a bare `esz == primtypesize("str")` size + // check — a size gate would route a plain >16B struct into the + // 3-word {ptr,len,cap} load and diverge from cstage (#60 collision + // class; sentinel 754). let elemisstr: bool = false; + let elemisslice: bool = false; let baselocal: *local = nil; // Global `[N]T` array or `*T` pointer used as an index base. // The local-ident lookup above misses it; we need LEAQ name(SB) @@ -14603,7 +14606,7 @@ fn cgindex(c: *cgen, n: *node) void = { // cgen.c:3517-18). esz-only — N_DOT-base signedness // stays unset, as before. let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); }; + if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); }; } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner @@ -14653,6 +14656,7 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; elemisstr = isstrtype(c, etn); + elemisslice = isslicetype(c, etn); }; }; cgexpr(c, idx); @@ -14686,10 +14690,11 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: load the full (ptr, len, cap) header into - // (AX, BX, CX) — str is 24B since #1, so cap must survive. - // Kind-gate on isstrtype, never size==24. Base is BX. - if (elemisstr) { + // str/slice element: load the full (ptr, len, cap) header into + // (AX, BX, CX) — both are 24B since #1, so cap must survive. + // Kind-gate on isstrtype||isslicetype, never size==24 (a >16B + // struct is 24B+ too but takes the struct-copy path). Base is BX. + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -14726,9 +14731,9 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: full (ptr, len, cap) header into (AX, BX, CX); + // str/slice element: full (ptr, len, cap) header into (AX, BX, CX); // cap must survive (#1). Kind-gate, never size==24. Base BX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -14756,10 +14761,10 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element via fallback base: full (ptr, len, cap) header + // str/slice element via fallback base: full (ptr, len, cap) header // into (AX, BX, CX); cap must survive (#1). Kind-gate, never // size==24. Base AX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "AX"); return; }; @@ -15610,8 +15615,8 @@ fn cgdot(c: *cgen, n: *node) void = { } else { emitline("\tMOVQ\tBX, AX\n"); }; - if (isstrtype(c, fi.tnode)) { - // str IS the 3-word {ptr,len,cap} + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + // str/slice: the 3-word {ptr,len,cap} // slice header (#1). AX holds the // element base, so load .ptr (which // targets AX) LAST. Matches the @@ -17718,16 +17723,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; cgexpr(c, n.rhs); // value → AX - // str IS []u8: spill cap (CX) + len (BX) before + // str/slice: spill cap (CX) + len (BX) before // computing the index so the post-index store can - // pop all three. #1/Phase 3: str=24B collides with + // pop all three. str=24B (#1/Phase 3) collides with // slice=24B, so this MUST gate on kind (cstage's - // elem_is_str, cmd/w6c/cgen.c:3576) — not a bare - // `esz == primtypesize("str")` — or a []u8 element - // (also 24B) misfires the str 3-word store and - // diverges from cstage. Write-side mirror of the + // elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581), + // never a bare esz==24: a >16B struct is also >=24B + // but takes the struct-copy path, not this 3-word + // {ptr,len,cap} store. Write-side mirror of the // cgindex read-path gate (#7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); }; @@ -17768,10 +17773,10 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); // value - // str IS []u8: pop the saved len + cap and store + // str/slice: pop the saved len + cap and store // all three words. Kind-gate, not size — see the // spill site above (#1/Phase 3, #7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); @@ -17875,7 +17880,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; - // str IS []u8: rhs leaves AX=ptr, + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the index/address // computation (IMULQ's CX scratch @@ -17883,7 +17888,7 @@ fn cgassign(c: *cgen, n: *node) void = { // off the str AX/BX/CX convention // (mirrors s.f=v), then store the full // triple at foff+0/+8/+16. - if (isstrtype(c, fi.tnode)) { + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); @@ -18784,8 +18789,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; if (n.op == tkind.TK_ASSIGN) { - if (typeisstr(ft)) { - // str IS []u8: rhs leaves AX=ptr, + if (typeisstr(ft) || typeisslice(ft)) { + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the base-expr eval // (it may clobber any reg), stage the @@ -18906,8 +18911,8 @@ fn cgassign(c: *cgen, n: *node) void = { }; return; }; - if (typeisstr(leaftype)) { - // str IS []u8: store ptr/len/cap. cgexpr leaves + if (typeisstr(leaftype) || typeisslice(leaftype)) { + // str/slice: store ptr/len/cap. cgexpr leaves // CX=cap, so the viacx base goes in DX (not CX) to // avoid clobbering it — same as the single-dot str // field store (#1/Phase 3). diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index b3740056..9eda10af 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -723,12 +723,15 @@ fn cgindex(c: *cgen, n: *node) void = { let idx: *node = n.rhs; let esz: i32 = 8; let signed_elem: bool = false; - // #1/Phase 3: str=24B collides with slice=24B, so the str-element - // branches below MUST gate on kind (mirroring cstage's elem_is_str), - // not a bare `esz == primtypesize("str")` size check — otherwise a - // []u8 element (also 24B) misfires into the str 2-word load and - // diverges from cstage (#60 collision class; sentinel 754). + // #1/Phase 3: str and slice are both 24B (and a >16B struct is + // 24B+ too), so the header branches below MUST gate on KIND + // (elemisstr/elemisslice, mirroring cstage's elem_is_str|| + // elem_is_slice), not a bare `esz == primtypesize("str")` size + // check — a size gate would route a plain >16B struct into the + // 3-word {ptr,len,cap} load and diverge from cstage (#60 collision + // class; sentinel 754). let elemisstr: bool = false; + let elemisslice: bool = false; let baselocal: *local = nil; // Global `[N]T` array or `*T` pointer used as an index base. // The local-ident lookup above misses it; we need LEAQ name(SB) @@ -770,7 +773,7 @@ fn cgindex(c: *cgen, n: *node) void = { // cgen.c:3517-18). esz-only — N_DOT-base signedness // stays unset, as before. let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); }; + if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); }; } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner @@ -820,6 +823,7 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; elemisstr = isstrtype(c, etn); + elemisslice = isslicetype(c, etn); }; }; cgexpr(c, idx); @@ -853,10 +857,11 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: load the full (ptr, len, cap) header into - // (AX, BX, CX) — str is 24B since #1, so cap must survive. - // Kind-gate on isstrtype, never size==24. Base is BX. - if (elemisstr) { + // str/slice element: load the full (ptr, len, cap) header into + // (AX, BX, CX) — both are 24B since #1, so cap must survive. + // Kind-gate on isstrtype||isslicetype, never size==24 (a >16B + // struct is 24B+ too but takes the struct-copy path). Base is BX. + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -893,9 +898,9 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: full (ptr, len, cap) header into (AX, BX, CX); + // str/slice element: full (ptr, len, cap) header into (AX, BX, CX); // cap must survive (#1). Kind-gate, never size==24. Base BX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -923,10 +928,10 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element via fallback base: full (ptr, len, cap) header + // str/slice element via fallback base: full (ptr, len, cap) header // into (AX, BX, CX); cap must survive (#1). Kind-gate, never // size==24. Base AX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "AX"); return; }; @@ -1777,8 +1782,8 @@ fn cgdot(c: *cgen, n: *node) void = { } else { emitline("\tMOVQ\tBX, AX\n"); }; - if (isstrtype(c, fi.tnode)) { - // str IS the 3-word {ptr,len,cap} + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + // str/slice: the 3-word {ptr,len,cap} // slice header (#1). AX holds the // element base, so load .ptr (which // targets AX) LAST. Matches the @@ -3885,16 +3890,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; cgexpr(c, n.rhs); // value → AX - // str IS []u8: spill cap (CX) + len (BX) before + // str/slice: spill cap (CX) + len (BX) before // computing the index so the post-index store can - // pop all three. #1/Phase 3: str=24B collides with + // pop all three. str=24B (#1/Phase 3) collides with // slice=24B, so this MUST gate on kind (cstage's - // elem_is_str, cmd/w6c/cgen.c:3576) — not a bare - // `esz == primtypesize("str")` — or a []u8 element - // (also 24B) misfires the str 3-word store and - // diverges from cstage. Write-side mirror of the + // elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581), + // never a bare esz==24: a >16B struct is also >=24B + // but takes the struct-copy path, not this 3-word + // {ptr,len,cap} store. Write-side mirror of the // cgindex read-path gate (#7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); }; @@ -3935,10 +3940,10 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); // value - // str IS []u8: pop the saved len + cap and store + // str/slice: pop the saved len + cap and store // all three words. Kind-gate, not size — see the // spill site above (#1/Phase 3, #7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); @@ -4042,7 +4047,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; - // str IS []u8: rhs leaves AX=ptr, + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the index/address // computation (IMULQ's CX scratch @@ -4050,7 +4055,7 @@ fn cgassign(c: *cgen, n: *node) void = { // off the str AX/BX/CX convention // (mirrors s.f=v), then store the full // triple at foff+0/+8/+16. - if (isstrtype(c, fi.tnode)) { + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); @@ -4951,8 +4956,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; if (n.op == tkind.TK_ASSIGN) { - if (typeisstr(ft)) { - // str IS []u8: rhs leaves AX=ptr, + if (typeisstr(ft) || typeisslice(ft)) { + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the base-expr eval // (it may clobber any reg), stage the @@ -5073,8 +5078,8 @@ fn cgassign(c: *cgen, n: *node) void = { }; return; }; - if (typeisstr(leaftype)) { - // str IS []u8: store ptr/len/cap. cgexpr leaves + if (typeisstr(leaftype) || typeisslice(leaftype)) { + // str/slice: store ptr/len/cap. cgexpr leaves // CX=cap, so the viacx base goes in DX (not CX) to // avoid clobbering it — same as the single-dot str // field store (#1/Phase 3). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e938c123..1c7af047 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14556,12 +14556,15 @@ fn cgindex(c: *cgen, n: *node) void = { let idx: *node = n.rhs; let esz: i32 = 8; let signed_elem: bool = false; - // #1/Phase 3: str=24B collides with slice=24B, so the str-element - // branches below MUST gate on kind (mirroring cstage's elem_is_str), - // not a bare `esz == primtypesize("str")` size check — otherwise a - // []u8 element (also 24B) misfires into the str 2-word load and - // diverges from cstage (#60 collision class; sentinel 754). + // #1/Phase 3: str and slice are both 24B (and a >16B struct is + // 24B+ too), so the header branches below MUST gate on KIND + // (elemisstr/elemisslice, mirroring cstage's elem_is_str|| + // elem_is_slice), not a bare `esz == primtypesize("str")` size + // check — a size gate would route a plain >16B struct into the + // 3-word {ptr,len,cap} load and diverge from cstage (#60 collision + // class; sentinel 754). let elemisstr: bool = false; + let elemisslice: bool = false; let baselocal: *local = nil; // Global `[N]T` array or `*T` pointer used as an index base. // The local-ident lookup above misses it; we need LEAQ name(SB) @@ -14603,7 +14606,7 @@ fn cgindex(c: *cgen, n: *node) void = { // cgen.c:3517-18). esz-only — N_DOT-base signedness // stays unset, as before. let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); }; + if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); }; } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner @@ -14653,6 +14656,7 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; elemisstr = isstrtype(c, etn); + elemisslice = isslicetype(c, etn); }; }; cgexpr(c, idx); @@ -14686,10 +14690,11 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: load the full (ptr, len, cap) header into - // (AX, BX, CX) — str is 24B since #1, so cap must survive. - // Kind-gate on isstrtype, never size==24. Base is BX. - if (elemisstr) { + // str/slice element: load the full (ptr, len, cap) header into + // (AX, BX, CX) — both are 24B since #1, so cap must survive. + // Kind-gate on isstrtype||isslicetype, never size==24 (a >16B + // struct is 24B+ too but takes the struct-copy path). Base is BX. + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -14726,9 +14731,9 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element: full (ptr, len, cap) header into (AX, BX, CX); + // str/slice element: full (ptr, len, cap) header into (AX, BX, CX); // cap must survive (#1). Kind-gate, never size==24. Base BX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; @@ -14756,10 +14761,10 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; - // str element via fallback base: full (ptr, len, cap) header + // str/slice element via fallback base: full (ptr, len, cap) header // into (AX, BX, CX); cap must survive (#1). Kind-gate, never // size==24. Base AX. - if (elemisstr) { + if (elemisstr || elemisslice) { cgslicehdr(c, "AX"); return; }; @@ -15610,8 +15615,8 @@ fn cgdot(c: *cgen, n: *node) void = { } else { emitline("\tMOVQ\tBX, AX\n"); }; - if (isstrtype(c, fi.tnode)) { - // str IS the 3-word {ptr,len,cap} + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + // str/slice: the 3-word {ptr,len,cap} // slice header (#1). AX holds the // element base, so load .ptr (which // targets AX) LAST. Matches the @@ -17718,16 +17723,16 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; cgexpr(c, n.rhs); // value → AX - // str IS []u8: spill cap (CX) + len (BX) before + // str/slice: spill cap (CX) + len (BX) before // computing the index so the post-index store can - // pop all three. #1/Phase 3: str=24B collides with + // pop all three. str=24B (#1/Phase 3) collides with // slice=24B, so this MUST gate on kind (cstage's - // elem_is_str, cmd/w6c/cgen.c:3576) — not a bare - // `esz == primtypesize("str")` — or a []u8 element - // (also 24B) misfires the str 3-word store and - // diverges from cstage. Write-side mirror of the + // elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581), + // never a bare esz==24: a >16B struct is also >=24B + // but takes the struct-copy path, not this 3-word + // {ptr,len,cap} store. Write-side mirror of the // cgindex read-path gate (#7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); }; @@ -17768,10 +17773,10 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); // value - // str IS []u8: pop the saved len + cap and store + // str/slice: pop the saved len + cap and store // all three words. Kind-gate, not size — see the // spill site above (#1/Phase 3, #7/754). - if (isstrtype(c, elemtn)) { + if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); @@ -17875,7 +17880,7 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; - // str IS []u8: rhs leaves AX=ptr, + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the index/address // computation (IMULQ's CX scratch @@ -17883,7 +17888,7 @@ fn cgassign(c: *cgen, n: *node) void = { // off the str AX/BX/CX convention // (mirrors s.f=v), then store the full // triple at foff+0/+8/+16. - if (isstrtype(c, fi.tnode)) { + if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); @@ -18784,8 +18789,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; if (n.op == tkind.TK_ASSIGN) { - if (typeisstr(ft)) { - // str IS []u8: rhs leaves AX=ptr, + if (typeisstr(ft) || typeisslice(ft)) { + // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the base-expr eval // (it may clobber any reg), stage the @@ -18906,8 +18911,8 @@ fn cgassign(c: *cgen, n: *node) void = { }; return; }; - if (typeisstr(leaftype)) { - // str IS []u8: store ptr/len/cap. cgexpr leaves + if (typeisstr(leaftype) || typeisslice(leaftype)) { + // str/slice: store ptr/len/cap. cgexpr leaves // CX=cap, so the viacx base goes in DX (not CX) to // avoid clobbering it — same as the single-dot str // field store (#1/Phase 3). diff --git a/test/wcc/941_slice_store_cap_run.c b/test/wcc/941_slice_store_cap_run.c new file mode 100644 index 00000000..9d71a4e3 --- /dev/null +++ b/test/wcc/941_slice_store_cap_run.c @@ -0,0 +1,244 @@ +/* + * 941_slice_store_cap_run — runtime coverage for task #7: a `[]T` (slice-typed) + * VALUE stored through an indexed / field / chained lhs, and read back, must + * move the full 24B {ptr,len,cap} header, not just {ptr}. Slices are 24B always; + * the G-cluster's store+read arms were kind-gated on str ONLY (the slice arm was + * never extended), so a slice value fell to the 1-word fldstoreop/fldloadop + * default and silently DROPPED len+cap. str IS []u8 since Phase 2 (#1), so the + * str 3-word machinery applies to slices verbatim — the fix widens each gate + * from `str` to `str || slice` (kind-OR, never a sz==24 test, which would also + * catch >16B structs). + * + * Sites exercised (cstage cmd/w6c/cgen.c + cgenexpr.ww twin), each store paired + * with its read mirror: + * A whole-element `xs[i] = sl` + read `xs[i]` (cgen.c store ~3650/3692; + * read N_INDEX cgslicehdr; ww cgassign elemtn + cgindex elemisslice). + * B arr[i].field `arr[i].f = sl` + read `arr[i].f` (G1-twin store; cgdot + * arrfield read). + * C chained *struct `r.sym.f = sl` + read `r.sym.f` (G2-twin store; the + * chained-*struct read was already 3-word). + * D value-spine `o.i.f = sl` + read `o.i.f` (value-struct-spine + * store; the value-spine read was already 3-word). + * + * UNLIKE the str store probes (937/938), here BOTH the store AND the read were + * 1-word before the fix, so each row is a store->read-back ROUNDTRIP: a 1-word + * store leaves the dst's len/cap at their prior value, and a 1-word read never + * loads them, so a broken stage yields a value whose len/cap are not p's. The + * full {ptr,len,cap} triple is asserted (ptr via s[0]=='h'=104, len=2, cap=8; + * cap!=len so a dropped len OR cap is caught). + * + * POISON: rows B/C/D seed the dst slot with a DIFFERENT slice q (len=4,cap=5) + * via a PROVEN 3-word slice store that is NOT the site under test — the single- + * dot field store, which was merged onto the slice arm pre-#7 (cgen.c C4.4): + * B via `pr.f = q` (pr=&arr[1], *struct field); + * C via `st.f = q` (direct field); + * D via `pi.f = q` (pi=&o.i, via-ptr field). + * So before the fix the dst keeps q's cap=5 while the read returns stale words; + * either way cap != 8. Row A is a bare roundtrip with no explicit poison: no + * proven 3-word store targets a bare slice array-element header (the same reason + * str has no standalone whole-element-store probe), so the store+read pair is + * itself the discriminator. + * + * Slices are built with explicit pseudo-field stores (`s.ptr=&buf[0]; s.len=N; + * s.cap=M`), the proven construction used by 691 — NOT sub-slicing, whose cap is + * the separate #20 fix that depends on this fold. + * + * Verified fail-before (all 4 rows exit 1, cap reads the poison/stale, not 8) / + * pass-after (exit 0) on BOTH the cstage `ww` and wwstage `ww_ww` drivers. + * NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling + * race does not apply (mirrors the 928/932 precedent). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + /* A — whole-element `xs[0] = p` into a [2][]u8 local array, read back + * via `xs[0]`. Exercises the N_INDEX store arm (elem_is_slice) and the + * N_INDEX read (cgslicehdr) together. No proven poison for a bare + * array-element header; the roundtrip is the probe. */ + { "slice_store_wholeelem", + "export fn main() i32 = {\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let xs: [2][]u8;\n" + " xs[0] = p;\n" + " let e: []u8 = xs[0];\n" + " if (e.cap: i32 != 8) { return 1; };\n" + " if (e.len: i32 != 2) { return 2; };\n" + " if (e[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, + /* B — `arr[i].f = p` into a [N]rec field (G1-twin). Poison arr[1].f + * (cap=5,len=4,'q') via &arr[1] + the proven single-dot *struct field + * store; then the field-of-indexed store lands p (cap=8,len=2,'h'). */ + { "slice_store_arrfield", + "type rec = struct { f: []u8 };\n" + "export fn main() i32 = {\n" + " let qb: [8]u8; qb[0] = 113u8;\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let q: []u8; q.ptr = &qb[0]; q.len = 4; q.cap = 5;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let arr: [3]rec;\n" + " let pr: *rec = &arr[1];\n" + " pr.f = q;\n" + " arr[1].f = p;\n" + " let s: []u8 = arr[1].f;\n" + " if (s.cap: i32 != 8) { return 1; };\n" + " if (s.len: i32 != 2) { return 2; };\n" + " if (s[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, + /* C — chained `r.sym.f = p` where r.sym is a *inner (G2-twin). Poison + * the pointee field via the proven direct field store (`st.f = q`); the + * chained store derefs r.sym and overwrites st.f. */ + { "slice_store_chained_ptr", + "type inner = struct { f: []u8 };\n" + "type outer = struct { sym: *inner };\n" + "export fn main() i32 = {\n" + " let qb: [8]u8; qb[0] = 113u8;\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let q: []u8; q.ptr = &qb[0]; q.len = 4; q.cap = 5;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let st: inner;\n" + " st.f = q;\n" + " let r: outer;\n" + " r.sym = &st;\n" + " r.sym.f = p;\n" + " let s: []u8 = r.sym.f;\n" + " if (s.cap: i32 != 8) { return 1; };\n" + " if (s.len: i32 != 2) { return 2; };\n" + " if (s[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, + /* D — value-spine `o.i.f = p` where o.i is a value-struct field (not a + * pointer). Poison o.i.f via &o.i + the proven via-ptr field store + * (`pi.f = q`); the value-spine store walks o.i and overwrites .f. */ + { "slice_store_valuespine", + "type inner = struct { f: []u8 };\n" + "type outer = struct { i: inner };\n" + "export fn main() i32 = {\n" + " let qb: [8]u8; qb[0] = 113u8;\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let q: []u8; q.ptr = &qb[0]; q.len = 4; q.cap = 5;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let o: outer;\n" + " let pi: *inner = &o.i;\n" + " pi.f = q;\n" + " o.i.f = p;\n" + " let s: []u8 = o.i.f;\n" + " if (s.cap: i32 != 8) { return 1; };\n" + " if (s.len: i32 != 2) { return 2; };\n" + " if (s[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/slicestore_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/slicestore_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[160]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "slice_store_cap_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "slice_store_cap_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "slice_store_cap_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("slice_store_cap_run: %d/%d ok\n", total, total); + return 0; +}