diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b9a6f20c..4c4f085f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -557,6 +557,36 @@ struct_arg_size(Type *t) return (int)t->size; } +/* agg_has_float — does an aggregate Type carry ANY float leaf + * (recursively through struct fields / array element / tuple + * positionals)? The #12 producer (N_TRYPROP/N_TRYUNW success shuffle) + * loud-stops a float-bearing aggregate success variant: the union + * return places a float eightbyte in the SSE class (X0/X1) which the GP + * AX/DX/CX payload shuffle cannot reach (mirror #11/#165). Conservative + * — ANY float, not a per-eightbyte SSE classify like struct_float_class + * — a loud-stop only needs to refuse, not transport. Mirrors wwstage + * tinfo_agg_float (cgenutil.ww). */ +static int +agg_has_float(Type *t) +{ + t = type_chase_named(t); + if (t == NULL) return 0; + if (cg_isfloat(t)) return 1; + if (t->kind == TY_STRUCT) { + for (Tfield *f = t->fields; f; f = f->next) + if (agg_has_float(f->type)) return 1; + return 0; + } + if (t->kind == TY_ARRAY) + return agg_has_float(t->sub); + if (t->kind == TY_TUPLE) { + for (Tparam *p = t->params; p; p = p->next) + if (agg_has_float(p->type)) return 1; + return 0; + } + return 0; +} + /* struct_float_class — SysV per-eightbyte classification for the #165 * float-bearing-struct param case (the param twin of #171's struct * return, classifying per-eightbyte rather than #163's per-element). @@ -5385,10 +5415,21 @@ cgexpr(Cg *c, Node *n, Local *locals) * store; for via_ptr/is_global the dst base addr * is reloaded into BX before each store so cgexpr * can clobber AX/BX between fields. */ + /* #12: an unwrap `b.f = mk()!` (direct) / `p.f = + * mk()!` (via-ptr) rides the same {AX,DX,CX} shape + * (producer shift) — admit it alongside N_CALL. Gated + * !is_global: the wwstage single-dot arms cover a direct + * / via-ptr base only; a global `g.f` unwrap stays on + * its pre-existing path in BOTH stages (out of scope, + * not regressed). Float/over-cap loud-stop at the + * producer. */ if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && (int)str_fu->size <= 24 - && n->rhs && n->rhs->kind == N_CALL + && n->rhs && (n->rhs->kind == N_CALL + || ((n->rhs->kind == N_TRYUNW + || n->rhs->kind == N_TRYPROP) + && !is_global)) && (str_fu->size % 8 == 0 || str_fu->size % 8 == 1 || str_fu->size % 8 == 2 @@ -5828,8 +5869,17 @@ cgexpr(Cg *c, Node *n, Local *locals) * float return eightbyte rides X0/X1 which the GP * AX/DX/CX cursor cannot read). Mirrors wwstage * cgenexpr.ww. */ + /* #12: an unwrap `arr[i].f = mk()!` rides the + * same {AX,DX,CX} shape (producer shift) — admit + * it alongside N_CALL; the #11b non-call arm below + * excludes the unwrap kinds so this arm is the + * sole handler. cg_sret_retsize keys off ft (the + * field = success variant type), so an over-cap + * unwrap stays loud here too. */ if (n->op == TK_ASSIGN && n->rhs - && n->rhs->kind == N_CALL) { + && (n->rhs->kind == N_CALL + || n->rhs->kind == N_TRYUNW + || n->rhs->kind == N_TRYPROP)) { if (cg_sret_retsize(ft) > 0) fatal("#11c/#234: over-cap " "(sret) aggregate field " @@ -5991,8 +6041,14 @@ cgexpr(Cg *c, Node *n, Local *locals) * cg_aggcopy — the ONE copy emitter the non- * indexed bases use (DRY). fsz natural * (ft->size). Mirrors wwstage cgenexpr.ww. */ + /* #12: exclude the unwrap kinds — the #11 arm + * above is their sole handler (they ride the + * {AX,DX,CX} register cursor, NOT a source + * address). */ if (n->op == TK_ASSIGN && n->rhs - && n->rhs->kind != N_CALL && fu + && n->rhs->kind != N_CALL + && n->rhs->kind != N_TRYUNW + && n->rhs->kind != N_TRYPROP && fu && (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY || fu->kind == TY_TUPLE) && fsz > 8) { cgexpr(c, idx, locals); @@ -6986,12 +7042,20 @@ cgexpr(Cg *c, Node *n, Local *locals) * copy scratch → dest. Scratch-first (not a dest spill across * the call) keeps the call at the frame's natural alignment. * In-cap only (cg_sret_retsize==0). */ + /* #12: an unwrap `mk()!` / `r?` whose success variant is + * an in-cap struct/array rides the SAME {AX,DX,CX} payload + * shape as the call return (the producer shift materialises + * it); admit it alongside N_CALL. cg_sret_retsize(esub)==0 + * holds for an in-cap variant; the float/over-cap loud- + * stops live at the producer. */ if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN && esubu && (esubu->kind == TY_STRUCT || esubu->kind == TY_ARRAY || esubu->kind == TY_TUPLE) && esz > 8 - && n->rhs->kind == N_CALL + && (n->rhs->kind == N_CALL + || n->rhs->kind == N_TRYUNW + || n->rhs->kind == N_TRYPROP) && cg_sret_retsize(esub) == 0) { int scr = cg_tagscr_slot(c, &locals, esz); cgexpr(c, n->rhs, locals); /* call → AX/DX/CX */ @@ -11113,8 +11177,24 @@ cgexpr(Cg *c, Node *n, Local *locals) * standard AX=tag cursor. The scalar MOVQ DX,AX * below carried only the inner tag and dropped the * payload (ken unw16). Nullable folds to one word - * and stays on the scalar move. */ - if (stu && stu->kind == TY_TAGGED && !stu->nullable) { + * and stays on the scalar move. + * + * #12: a general-aggregate (struct/array) success + * variant rides the SAME in-cap {AX,DX,CX} payload + * shuffle — the union return packs the payload as raw + * GP words past the outer tag. Pre-#12 it matched no + * arm and fell to the bare MOVQ DX,AX below, + * materialising only w0 (w1/w2 dropped) — a SILENT + * both-stage word-drop. A float-bearing aggregate rides + * X0/X1 (the SSE return-class) which this GP cursor + * cannot reach, so LOUD-STOP it (mirror #11/#165). */ + if (stu && ((stu->kind == TY_TAGGED && !stu->nullable) + || stu->kind == TY_STRUCT || stu->kind == TY_ARRAY)) { + if (agg_has_float(stu)) + fatal("#12/#165: float-bearing " + "aggregate success variant unwrap " + "(S|e)! rides SSE X0/X1; GP cursor " + "unwired"); ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); if (stu->size > 8) ins2(c, A_MOVQ, areg(D_CX), @@ -11225,8 +11305,17 @@ cgexpr(Cg *c, Node *n, Local *locals) } /* Family C (#35): TAGGED success = nested box on the * payload words — shift past the outer tag (see the - * N_TRYPROP twin). */ - if (stu && stu->kind == TY_TAGGED && !stu->nullable) { + * N_TRYPROP twin). #12: a struct/array success variant + * rides the same in-cap {AX,DX,CX} shuffle; a float- + * bearing aggregate LOUD-STOPS (X0/X1, GP cursor can't + * reach it). Full WHY at the N_TRYPROP twin. */ + if (stu && ((stu->kind == TY_TAGGED && !stu->nullable) + || stu->kind == TY_STRUCT || stu->kind == TY_ARRAY)) { + if (agg_has_float(stu)) + fatal("#12/#165: float-bearing " + "aggregate success variant unwrap " + "(S|e)! rides SSE X0/X1; GP cursor " + "unwired"); ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); if (stu->size > 8) ins2(c, A_MOVQ, areg(D_CX), diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 62e3fe39..34a66a3f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -240,8 +240,24 @@ fn cgtrytaggedshift(c: *cgen, n: *syntax.node) bool = { let sv: *syntax.tinfo = successvariant(ou); sv = tichase(sv); if (sv == nil) { return false; }; - if (sv.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (sv.nullable != 0) { return false; }; + // #12: a general-aggregate (struct/array) success variant rides the + // SAME in-cap {AX=w0,DX=w1,CX=w2} payload shuffle as the nested-TAGGED + // box — the union return packs the payload as raw GP words past the + // outer tag. Pre-#12 it matched no arm and fell to the bare MOVQ DX,AX + // below, materialising only w0 (w1/w2 dropped) — a SILENT both-stage + // word-drop. A float-bearing aggregate rides X0/X1 instead (the SSE + // return-class), which this GP cursor cannot reach, so LOUD-STOP it + // (mirror #11/#165). Mirrors cstage N_TRYPROP/N_TRYUNW. + let agg: bool = false; + if (sv.kind == syntax.tykind.TY_TAGGED && sv.nullable == 0) { agg = true; }; + if (sv.kind == syntax.tykind.TY_STRUCT) { agg = true; }; + if (sv.kind == syntax.tykind.TY_ARRAY) { agg = true; }; + if (!agg) { return false; }; + if (tinfoaggfloat(sv)) { + let m12f: str = "#12/#165: float-bearing aggregate success variant unwrap (S|e)! rides SSE X0/X1; GP cursor unwired\n"; + os.write(2, m12f.ptr, m12f.len: u64); + os.exit(1); + }; emitline("\tMOVQ\tDX, AX\n"); if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); }; if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); }; @@ -9106,7 +9122,16 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // alignment. esz>8 non-str/non-slice IS struct/array/tuple // here (tagged returned above). In-cap only // (callsretsize==0). Mirror of cstage cgen.c C2c arm. - if (n.rhs.kind == syntax.nkind.N_CALL && esz > 8 + // #12: an unwrap `mk()!` / `r?` whose success variant + // is an in-cap struct/array rides the SAME {AX,DX,CX} + // payload shape as the call return (the producer shift + // at cgtrytaggedshift materialises it); admit it + // alongside N_CALL so the materialise + copy just works. + // callsretsize==0 holds (non-call → 0); the float/over- + // cap loud-stops live at the producer. + if ((n.rhs.kind == syntax.nkind.N_CALL + || n.rhs.kind == syntax.nkind.N_TRYUNW + || n.rhs.kind == syntax.nkind.N_TRYPROP) && esz > 8 && !isstrtype(c, elemtn) && !isslicetype(c, elemtn) && callsretsize(c, n.rhs) == 0) { let scrc2: i32 = tagscradd(c, esz); @@ -10014,9 +10039,17 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // pure-float return eightbyte rides X0/X1 which // the GP AX/DX/CX cursor cannot read). Mirrors // cstage cgen.c. + // #12: an unwrap `arr[i].f = mk()!` rides the same + // {AX,DX,CX} shape (producer shift) — admit it + // alongside N_CALL; the #11b non-call arm below + // excludes the unwrap kinds so this arm is the SOLE + // handler. callsretsize==0 for a non-call, so an + // over-cap unwrap stays loud at the producer. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL) { + && (n.rhs.kind == syntax.nkind.N_CALL + || n.rhs.kind == syntax.nkind.N_TRYUNW + || n.rhs.kind == syntax.nkind.N_TRYPROP)) { if (callsretsize(c, n.rhs) > 0) { let m11o: str = "#11c/#234: over-cap (sret) aggregate field receive arr[i].f=mk() unwired (cs!=ww; task #8)\n"; os.write(2, m11o.ptr, m11o.len: u64); @@ -10179,9 +10212,16 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // through aggargsrcaddr (src -> SI) + aggcopy — the ONE // copy emitter the non-indexed bases use (DRY, rule 8). // tsz natural (fi.fsz, type table). Mirrors cstage cgen.c. + // #12: exclude the unwrap kinds — the #11 arm above is + // their sole handler (they ride the {AX,DX,CX} register + // cursor, NOT a source address, so aggargsrcaddr can't + // reach them). Pre-#12 an N_TRYUNW matched != N_CALL and + // loud-stopped here; now it materialises in the #11 arm. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind != syntax.nkind.N_CALL) { + && n.rhs.kind != syntax.nkind.N_CALL + && n.rhs.kind != syntax.nkind.N_TRYUNW + && n.rhs.kind != syntax.nkind.N_TRYPROP) { let fk11b: *syntax.tinfo = tichase(fi.tnode.type_: *syntax.tinfo); let isagg11b: bool = false; if (fk11b != nil) { @@ -10458,10 +10498,15 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize (cstage // SSoT lu->size, check.c:760; cgen.c:7720 - // sz=lu->size at the receive twin). + // sz=lu->size at the receive twin). #12: an + // unwrap `p.f = mk()!` rides the same {AX,DX,CX} + // shape (producer shift) — admit it alongside + // N_CALL; float/over-cap loud-stop at the producer. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL + && (n.rhs.kind == syntax.nkind.N_CALL + || n.rhs.kind == syntax.nkind.N_TRYUNW + || n.rhs.kind == syntax.nkind.N_TRYPROP) && fi.tnode != nil && fi.tnode.kind == syntax.nkind.N_TNAME && aliasprimsize(c, fi.tnode.str) == 0) { @@ -10702,10 +10747,15 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // directly at (lc.off+fi.foff)(BP). // N_STRUCTLIT: field-walk; each inner // field stored at +fi.foff+inner_foff(BP). - // BP-rel direct, no addr scratch needed. + // BP-rel direct, no addr scratch needed. #12: an + // unwrap `b.f = mk()!` rides the same {AX,DX,CX} + // shape (producer shift) — admit it alongside + // N_CALL; float/over-cap loud-stop at the producer. if (n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL + && (n.rhs.kind == syntax.nkind.N_CALL + || n.rhs.kind == syntax.nkind.N_TRYUNW + || n.rhs.kind == syntax.nkind.N_TRYPROP) && fi.tnode != nil && fi.tnode.kind == syntax.nkind.N_TNAME && aliasprimsize(c, fi.tnode.str) == 0) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 7a88d87b..a28cd715 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -3013,6 +3013,42 @@ fn aggargfloatstop(n: *syntax.node) bool = { return false; }; +// tinfoaggfloat — does an aggregate tinfo carry ANY float leaf +// (recursively through struct fields / array element / tuple +// positionals)? The #12 producer (cgtrytaggedshift) loud-stops a +// float-bearing aggregate success variant: the union return places a +// float eightbyte in the SSE class (X0/X1) which the GP {AX,DX,CX} +// payload shuffle cannot reach (mirror #11/#165). Conservative — ANY +// float, not a per-eightbyte SSE classify like structfloatclass — a +// loud-stop only needs to refuse, not transport. Mirrors cstage +// agg_has_float (cmd/w6c/cgen.c). +fn tinfoaggfloat(t: *syntax.tinfo) bool = { + if (t == nil) { return false; }; + let u: *syntax.tinfo = tichase(t); + if (u == nil) { return false; }; + if (syntax.typeisfloat(u)) { return true; }; + if (u.kind == syntax.tykind.TY_STRUCT) { + let fi: *syntax.tfield = u.fields; + for (fi != nil) { + if (tinfoaggfloat(fi.type_)) { return true; }; + fi = fi.tnext; + }; + return false; + }; + if (u.kind == syntax.tykind.TY_ARRAY) { + return tinfoaggfloat(u.sub); + }; + if (u.kind == syntax.tykind.TY_TUPLE) { + let pe: *syntax.ttupleelem = u.tupleelems; + for (pe != nil) { + if (tinfoaggfloat(pe.type_)) { return true; }; + pe = pe.tnext; + }; + return false; + }; + return false; +}; + // structfloatclass — SysV per-eightbyte classification for the #165 // float-bearing-struct param case (param twin of #171's struct return; // classifies per-eightbyte, not #163's per-element). Returns 0 when the diff --git a/test/lang/struct_unwrap_test.ww b/test/lang/struct_unwrap_test.ww new file mode 100644 index 00000000..7e78adf9 --- /dev/null +++ b/test/lang/struct_unwrap_test.ww @@ -0,0 +1,77 @@ +// struct_unwrap_test — #12: an aggregate (struct) success-variant unwrap +// `(S|e)!` / `r?` dropped payload words (SILENT, both stages, byte-id-BLIND). +// A struct success variant rides the in-cap {AX=w0,DX=w1,CX=w2} GP ABI — a +// DIFFERENT shuffle from #6's str/slice {ptr,len,cap} header. Two layers: +// L1 PRODUCER (cgtrytaggedshift / cgen.c N_TRYPROP+N_TRYUNW): a struct/array +// success variant matched no arm and fell to the bare `MOVQ DX,AX`, +// materialising only w0 — w1/w2 (in CX/R8) dropped. +// L2 CONSUMER (the aggregate-STORE arms): gated rhs.kind==N_CALL and stored +// one word for an N_TRYUNW/N_TRYPROP rhs — admitting the unwrap kinds +// reuses the #10-correct scratch materialise. +// Both layers are silent both-stage, so the byte-id gate is BLIND to the drop — +// these VALUE asserts are the sole tooth. Reverting either stage's L2 relax (or +// the shared L1 producer) reverts the dropped word to the poison sentinel 9 and +// reddens. POISON-SEED every dest member to 9 so a dropped w1/w2 reads 9 != the +// expected nonzero value (a fresh-frame 0 would also mismatch, but 9 is robust). +// +// Coverage = the three SILENT store shapes (arr-elem C2c, single-dot field +// direct + via-ptr, indexed-field #11) for a clean 2-eightbyte s2 (w1 tooth) +// plus a 3-eightbyte s24 (w2 tooth — exercises the producer's R8->CX shift). +// The sub-8-tail-THROUGH-unwrap case (#10's tail) is NOT pinnable: an ARRAY +// success variant is checker-rejected (`([7]i16|e)`, task #5/#60) and a sub-8- +// tail STRUCT success variant trips a SEPARATE pre-existing bug — the union- +// return struct-lit fill overruns saved BP when the variant ends at the frame +// edge (g's MOVQ writes past the 14B struct into [BP]); filed for the lead. + +package struct_unwrap_test; + +type e = !i32; +type s2 = struct { a: i64, b: i64 }; // 2 eightbytes: w0=a, w1=b +type s24 = struct { a: i64, b: i64, c: i64 }; // 3 eightbytes: w0,w1,w2 +type box = struct { f: s2, g: i64 }; + +fn mk2() (s2 | e) = { return s2 { a = 111i64, b = 222i64 }; }; +fn mk24() (s24 | e) = { return s24 { a = 111i64, b = 222i64, c = 333i64 }; }; + +@test fn arr_elem() void = { // L2 site: C2c whole-element arr[i]=mk()! + let arr: [2]s2; + arr[1].a = 9i64; arr[1].b = 9i64; // poison: w1 sentinel 9 != 222 + arr[1] = mk2()!; + assert(arr[1].a == 111i64); + assert(arr[1].b == 222i64); // w1 — the dropped-word tooth +}; + +@test fn arr_elem_w2() void = { // C2c, 3 eightbytes — w2 (R8->CX) tooth + let arr: [2]s24; + arr[1].a = 9i64; arr[1].b = 9i64; arr[1].c = 9i64; + arr[1] = mk24()!; + assert(arr[1].a == 111i64); + assert(arr[1].b == 222i64); + assert(arr[1].c == 333i64); // w2 tooth (>16B producer shift) +}; + +@test fn field_direct() void = { // L2 site: single-dot direct b.f=mk()! + let b: box = box { f = s2{a=9i64,b=9i64}, g = 7i64 }; + b.f = mk2()!; + assert(b.f.a == 111i64); + assert(b.f.b == 222i64); // w1 tooth + assert(b.g == 7i64); // neighbour field intact +}; + +@test fn field_via_ptr() void = { // L2 site: single-dot via *struct p.f=mk()! + let b: box = box { f = s2{a=9i64,b=9i64}, g = 7i64 }; + let p: *box = &b; + p.f = mk2()!; + assert(b.f.a == 111i64); + assert(b.f.b == 222i64); // w1 tooth + assert(b.g == 7i64); +}; + +@test fn field_of_indexed() void = { // L2 site: #11 indexed-field arr[i].f=mk()! + let arr: [2]box; + arr[1].g = 5i64; arr[1].f.a = 9i64; arr[1].f.b = 9i64; + arr[1].f = mk2()!; + assert(arr[1].f.a == 111i64); + assert(arr[1].f.b == 222i64); // w1 tooth + assert(arr[1].g == 5i64); // neighbour field intact +};