diff --git a/Makefile b/Makefile index 6666b8d6..a8bc1b6f 100644 --- a/Makefile +++ b/Makefile @@ -326,6 +326,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_in_union_run \ $(BIN)/test_nonlit_tuple_widen_run \ $(BIN)/test_const_slice_aggregate_run \ + $(BIN)/test_tuple_index_read_run \ $(BIN)/test_tuple_slot_layout_run \ $(BIN)/test_tagged_tuple_widen_run \ $(BIN)/test_tagged_structlit_payload_run \ @@ -1636,6 +1637,12 @@ $(BIN)/test_const_slice_aggregate_run: test/wcc/946_const_slice_aggregate_run.c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tuple_index_read_run: test/wcc/947_tuple_index_read_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_tagged_tuple_widen_run: test/wcc/936_tagged_tuple_widen_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f4e78088..5371fad9 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6235,6 +6235,80 @@ cgexpr(Cg *c, Node *n, Local *locals) cg_sret_dest_off = 0; break; } + /* #121 (write-face of leg-b): a tuple-LITERAL rhs into an + * indexed element `a[i] = (3,4)`. A literal has no source + * ADDRESS, so the ident/dot/deref copy arm below can't reach + * it — it fell to the 1-word scalar store tail (word0 only; + * the read-luck masked it until leg-b's correct read). The + * indexed-tuple-element STORE is the write-face of leg-b's + * read (one round-trip, #58/#135 precedent). Materialise the + * literal into a frame scratch via the cglet in-cap cursor + * fill (cg_tuple_lit_to_cursor + tuple_store), then word-copy + * scratch → &a[i]. NARROW: only the N_TUPLE-literal rhs (the + * rest of the #270/#31/#32 store family stays deferred). In- + * cap only (cg_sret_retsize==0); over-cap tuple stores keep + * the existing sret/loud paths. */ + if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN + && esubu && esubu->kind == TY_TUPLE && esz > 8 + && n->rhs->kind == N_TUPLE + && base->kind == N_IDENT + && cg_sret_retsize(esub) == 0) { + int scr = cg_tagscr_slot(c, &locals, esz); + cg_tuple_lit_to_cursor(c, &locals, n->rhs, esubu); + int gpcur = 0, ssecur = 0, eoff = 0, ef32; + for (Tparam *p = esubu->params; p; p = p->next) { + int isflt = fld_isfloat(p->type, &ef32); + tuple_store(c, p->type, gpcur, ssecur, + scr + eoff); + if (isflt) + ssecur++; + else + gpcur += tuple_eslot(p->type) / 8; + eoff += tuple_eslot(p->type); + } + /* dest &a[i] → BX (mirror the #270-1b resolve) */ + cgexpr(c, n->lhs->rhs, locals); /* idx → AX */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); + if (base->kind == N_IDENT) { + int off = localfind(locals, base->str); + int isglobal = (off == 0) + && let_islet(base->str); + if (isglobal && is_arr) + ins2(c, A_LEAQ, + masym(c, base->str), + areg(D_BX)); + else if (isglobal) + ins2(c, A_MOVQ, + masym(c, base->str), + areg(D_BX)); + else if (is_arr) + ins2(c, A_LEAQ, + amem(D_BP, off), areg(D_BX)); + else + ins2(c, A_MOVQ, + amem(D_BP, off), areg(D_BX)); + } else if (cg_dotbase_addr(c, base, D_BX, locals)) { + /* N_DOT array-field base resolved inline. */ + } else { + cgexpr(c, base, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + } + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + /* word-copy scratch → dest (tuple esz is 8-aligned, + * tuple_eslot 8B floor). */ + for (int k = 0; k < esz; k += 8) { + ins2(c, A_MOVQ, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, k)); + } + break; + } /* #270-1b: aggregate (struct/array/tuple >8B) element * STORE `a[i] = val`. The scalar store path below copies * only the first 8 bytes (fldstoreop MOVQ) — a silent @@ -11434,6 +11508,70 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } + /* #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed + * TUPLE element. The struct branch above handles struct / ptr-to- + * struct elements; a tuple element fell through to the read- + * resolver and died LOUD ("unsupported field-read shape"). + * Resolve &tbl[i] via the place-spine (cgplaceaddr, the #116 + * mechanism) into BX→AX, then read the field at addr+foff reusing + * the per-element-kind arms the N_IDENT tuple-field block wires + * (cgen.c TY_TUPLE arm): str-triple / float-X0 / fn-or-scalar + * fldloadop. Narrow (rob Q3): tagged / nested-aggregate fields + * stay LOUD (no fold-6 consumer; the #58/#117 untested-arm + * hazard). Mirrors selfhost cgenexpr.ww's leg-(a) twin. */ + if (n->lhs && n->lhs->kind == N_INDEX && n->str) { + Type *eu = type_chase_named(n->lhs->type); + if (eu && eu->kind == TY_TUPLE) { + int idx = 0; + for (const char *q = n->str; *q; q++) + idx = idx * 10 + (*q - '0'); + Tparam *tp = eu->params; + int foff = 0; + while (idx > 0 && tp) { + foff += tuple_eslot(tp->type); + tp = tp->next; + idx--; + } + if (tp != NULL) { + Type *fu = type_chase_named(tp->type); + if (fu && (fu->kind == TY_TAGGED + || fu->kind == TY_STRUCT + || fu->kind == TY_TUPLE + || fu->kind == TY_ARRAY)) + fatal("#121: aggregate/tagged tuple-" + "element field read off an " + "indexed base unwired"); + if (!cgplaceaddr(c, n->lhs, D_BX, locals)) + fatal("#121: indexed tuple base " + "not place-resolvable"); + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + int tf32 = 0; + if (fld_isfloat(tp->type, &tf32)) { + ins2(c, tf32 ? A_MOVSS : A_MOVSD, + amem(D_AX, foff), areg(D_X0)); + goto dot_done; + } + if (fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + ins2(c, A_MOVQ, + amem(D_AX, foff + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(D_AX, foff + 16), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_AX, foff + 0), + areg(D_AX)); + goto dot_done; + } + int fsz = (int)(tp->type + ? tp->type->size : 8); + int op = fldloadop(tp->type, fsz); + ins2(c, op, amem(D_AX, foff), areg(D_AX)); + goto dot_done; + } + } + } /* Nested module-qualified field where the chain didn't fold to * a known shape (typical when w6c runs on a single file with * `use mod;` but no driver concatenation — the body's enum / @@ -11632,6 +11770,30 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); break; } + /* #121 leg (b): whole TUPLE element `let e = tbl[i]`. + * The in-cap tuple receive (cglet) reads the SysV register + * cursor (tuple_rseq words L→R); without this arm the + * element fell to the scalar load below = word0 only + * (str.len→0, later words garbage — silent both-wrong- + * identical, #263). Fill gptotal cursor words from the + * element address (BX); descending so AX (= tuple_rseq[0]) + * loads last, mirroring the tagged arm. Over-cap (> the GP + * cursor) leaves the ADDRESS in AX (the sret / + * cg_tagged_memread convention) — the cglet over-cap arm + * louds it (no live consumer). */ + if (esubu && esubu->kind == TY_TUPLE) { + int nw = 0; + for (Tparam *p = esubu->params; p; p = p->next) + nw += tuple_eslot(p->type) / 8; + if (nw > TUPLE_GPCAP) { + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + break; + } + for (int k = nw - 1; k >= 0; k--) + ins2(c, A_MOVQ, amem(D_BX, k * 8), + areg(tuple_rseq[k])); + break; + } /* float element → MOVSS/MOVSD into X0: the consumer's * ADDSD/MOVSD spill machinery already expects X0, but the * integer fldloadop below would leave it in AX and the SSE @@ -11702,6 +11864,23 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); break; } + /* #121 leg (b) via fallback base: whole TUPLE element. AX holds + * the element address — copy to BX (the cursor fill into AX + * clobbers it), then fill the gptotal cursor words. Over-cap + * leaves the ADDRESS in AX (cglet louds it). Twin of the + * N_IDENT-base arm above. */ + if (esubu && esubu->kind == TY_TUPLE) { + int nw = 0; + for (Tparam *p = esubu->params; p; p = p->next) + nw += tuple_eslot(p->type) / 8; + if (nw > TUPLE_GPCAP) + break; /* AX already = element addr */ + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + for (int k = nw - 1; k >= 0; k--) + ins2(c, A_MOVQ, amem(D_BX, k * 8), + areg(tuple_rseq[k])); + break; + } /* float element via fallback base → X0 (see Site A, #119). The * base address is in AX; MOVSS/MOVSD reads the element into X0. */ if (type_isfloat(esub)) { @@ -13524,6 +13703,24 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) if (slc && slc->kind == N_UN && slc->op == TK_STAR && u && (u->kind == TY_SLICE || u->kind == TY_STR)) fatal("for-range over a deref base unwired (#11)"); + /* #121 leg (c): for-range over a module-GLOBAL slice/str/array + * base SEGV's today — the init + per-iteration base resolution + * below assume a frame-local slot (localfind), so a global + * let/def base reads saved-BP as the .ptr/.len (SEGV; divergent + * cs≠ww asm). LOUD-STOP symmetric both stages (byte-id-neutral — + * no asm; converges the divergence by rejecting; segfault→ + * compile-error is pure improvement). The fix is the N_INDEX + * isglobal base resolution (LEAQ/MOVQ name(SB)) ported into the + * for-range spine — a DISTINCT mechanism from leg (a)/(b)'s + * element-address machinery, with its own scalar/str/array/ + * destructure global-base test surface. Filed as a #121 sibling; + * off fold-6's path (fold-6 needs only leg (a)). */ + if (slc && slc->kind == N_IDENT + && localfind(*locals, slc->str) == 0 + && (let_islet(slc->str) || def_isarraydef(slc->str))) + fatal("#121: for-range over a module-global " + "slice/array base unwired (global-base " + "resolution gap)"); int baseoff = 0; if (slc && slc->kind != N_IDENT && !(u && u->kind == TY_ARRAY)) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d441347a..d048acda 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -23272,6 +23272,25 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; + // #121 leg (b): whole TUPLE element `let e = tbl[i]`. Classify off + // the chased index-result tinfo (n.type_, the same source the N_DOT/ + // N_INDEX arms read). gptotal = sum(tupeslot/8); the per-base arms + // below fill that many cursor words (tupreg) from the element + // address. Mirrors cstage cgen.c N_INDEX TY_TUPLE arms. + let elem_tuple: bool = false; + let tuple_nwords: i32 = 0; + { + let eti: *tinfo = tichase(n.type_: *tinfo); + if (eti != nil) { if (eti.kind == tykind.TY_TUPLE) { + elem_tuple = true; + // ww tuples store elements in .tupleelems, not .params. + let tpw: *ttupleelem = eti.tupleelems; + for (tpw != nil) { + tuple_nwords += tupeslot(tpw.type_) / 8; + tpw = tpw.tnext; + }; + };}; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -23316,6 +23335,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — fill gptotal cursor words + // from the element address (BX); descending so AX loads last, + // mirroring the tagged arm. Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 @@ -23388,6 +23426,24 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — twin of the global arm + // above (base in BX). Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 || elemisslice) { @@ -23449,6 +23505,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b) via fallback base: whole TUPLE element. AX holds the + // element address — copy to BX (the cursor fill into AX clobbers it), + // then fill the gptotal cursor words. Over-cap leaves AX as the addr. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + return; + }; + emitline("\tMOVQ\tAX, BX\n"); + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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. @@ -25071,6 +25146,90 @@ fn cgdot(c: *cgen, n: *node) void = { };}; }; }; + // #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed + // TUPLE element. The struct N_INDEX-lhs block above handles + // struct / ptr-to-struct elements; a tuple element fell through + // to the read-resolver and died LOUD. Resolve &tbl[i] via the + // place-spine (cgplaceaddr, the #116 mechanism) into BX→AX, then + // read the field at addr+foff reusing the per-element-kind arms + // the local N_TTUPLE block wires: str-triple / float-X0 / fn-or- + // scalar loadopsz. Narrow (rob Q3): tagged / nested-aggregate + // fields stay LOUD. Mirrors cstage cgen.c leg-(a) arm 1:1. + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + let eu: *tinfo = tichase(lhs.type_: *tinfo); + if (eu != nil) { if (eu.kind == tykind.TY_TUPLE) { + let idx: i32 = fldnumidx(fld); + if (idx >= 0) { + // ww tuples store elements in .tupleelems + // (ttupleelem chain), NOT .params (that is + // fn-only). foff via tupeslot accumulation = + // cstage tuple_eslot, byte-id. + let tp: *ttupleelem = eu.tupleelems; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += tupeslot(tp.type_); + tp = tp.tnext; + i += 1; + }; + }; + if (tp != nil) { + let ft: *tinfo = tp.type_; + let fu: *tinfo = tichase(ft); + if (fu != nil && (fu.kind == tykind.TY_TAGGED + || fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_TUPLE + || fu.kind == tykind.TY_ARRAY)) { + let mt: str = "#121: aggregate/tagged tuple-element field read off an indexed base unwired\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (!cgplaceaddr(c, lhs, "BX")) { + let mp: str = "#121: indexed tuple base not place-resolvable\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\tBX, AX\n"); + if (typeisfloat(ft)) { + let mov: str = "MOVSD"; + if (typeisf32(ft)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + if (fu != nil && (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE)) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + }; + };}; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case; @@ -29510,6 +29669,91 @@ fn cgassign(c: *cgen, n: *node) void = { c.sretdestoff = 0; return; }; + // #121 (write-face of leg-b): a tuple-LITERAL rhs into + // an indexed element `a[i] = (3,4)`. A literal has no + // source ADDRESS, so the ident/dot/deref copy arm below + // can't reach it — it fell to the 1-word scalar store + // tail (word0 only; the read-luck masked it until leg-b's + // correct read). Materialise the literal into a frame + // scratch via the cglet in-cap path (cgtuplelittocursor + + // tupstore), then word-copy scratch → &a[i]. NARROW: + // N_TTUPLE-literal rhs, N_IDENT base (idxelemtn gives the + // element N_TTUPLE), in-cap. Mirror of cstage cgen.c #121 + // store arm; the materialise + base-resolve are the + // cglet / #270-1b byte-id twins. + if (n.rhs.kind == nkind.N_TUPLE && esz > 8 + && base != nil && base.kind == nkind.N_IDENT + && elemtn != nil && elemtn.kind == nkind.N_TTUPLE + && sretretsize(c, elemtn) == 0) { + let scr121: i32 = tagscradd(c, esz); + cgtuplelittocursor(c, n.rhs, elemtn); + let gpc: i32 = 0; + let ssc: i32 = 0; + let eo: i32 = 0; + let q121: *node = elemtn.list; + for (q121 != nil) { + let qt: *node = q121.lhs; + let isflt: bool = isfloattype(c, qt); + let es: i32 = tupeslotn(qt); + tupstore(c, gpc, ssc, scr121 + eo, es, qt); + if (isflt) { ssc += 1; } + else { gpc += es / 8; }; + eo += es; + q121 = q121.next; + }; + // dest &a[i] → BX (#270-1b base resolve) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn2: *node = baselocal.tnode; + let isarr2: bool = false; + if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; }; + if (basealias) { + let bu60: *tinfo = tichase(base.type_: *tinfo); + if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; }; + }; + if (isarr2) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { if (dotbaseaddr(c, base, "BX")) { + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let kk2: i32 = 0; + for (kk2 < esz) { + emitline("\tMOVQ\t"); + emitoff((scr121 + kk2): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(kk2: i64); + emitline("(BX)\n"); + kk2 += 8; + }; + return; + }; // #270-1b: aggregate (struct/array/tuple >8B) element // STORE `a[i] = val`. The scalar store path below copies // only the first 8 bytes (tnodestoreop MOVQ) — a silent @@ -36568,6 +36812,32 @@ fn cgforrange(c: *cgen, n: *node) void = { baseoff = localalloc(c, bname, 8, nil); }; }; + // #121 leg (c): for-range over a module-GLOBAL slice/str/array base + // SEGV's today — the init + per-iteration base resolution below + // assume a frame-local slot (localfindnode), so a global let/def base + // reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage + // cgen.c (byte-id-neutral; segfault→compile-error is pure + // improvement). The fix (the N_INDEX isglobal base resolution ported + // into the for-range spine) is a DISTINCT mechanism — filed as a #121 + // sibling, off fold-6's path. + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + if (localfindnode(c, slc.str) == nil) { + let isglob: bool = isletvar(c, slc.str); + if (!isglob) { + let gdtn: *node = defvartnode(c, slc.str); + if (gdtn != nil) { + if (gdtn.kind == nkind.N_TARRAY) { isglob = true; }; + }; + }; + if (isglob) { + let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n"; + os.write(2, mc.ptr, mc.len: u64); + os.exit(1); + }; + }; + }; + }; // Per-binding (up to 8 — matches the C array). Parallel arrays so // we don't depend on local-struct cgen. diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index d954fc5b..e76084cc 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1919,6 +1919,25 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; + // #121 leg (b): whole TUPLE element `let e = tbl[i]`. Classify off + // the chased index-result tinfo (n.type_, the same source the N_DOT/ + // N_INDEX arms read). gptotal = sum(tupeslot/8); the per-base arms + // below fill that many cursor words (tupreg) from the element + // address. Mirrors cstage cgen.c N_INDEX TY_TUPLE arms. + let elem_tuple: bool = false; + let tuple_nwords: i32 = 0; + { + let eti: *tinfo = tichase(n.type_: *tinfo); + if (eti != nil) { if (eti.kind == tykind.TY_TUPLE) { + elem_tuple = true; + // ww tuples store elements in .tupleelems, not .params. + let tpw: *ttupleelem = eti.tupleelems; + for (tpw != nil) { + tuple_nwords += tupeslot(tpw.type_) / 8; + tpw = tpw.tnext; + }; + };}; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -1963,6 +1982,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — fill gptotal cursor words + // from the element address (BX); descending so AX loads last, + // mirroring the tagged arm. Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 @@ -2035,6 +2073,24 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — twin of the global arm + // above (base in BX). Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 || elemisslice) { @@ -2096,6 +2152,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b) via fallback base: whole TUPLE element. AX holds the + // element address — copy to BX (the cursor fill into AX clobbers it), + // then fill the gptotal cursor words. Over-cap leaves AX as the addr. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + return; + }; + emitline("\tMOVQ\tAX, BX\n"); + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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. @@ -3718,6 +3793,90 @@ fn cgdot(c: *cgen, n: *node) void = { };}; }; }; + // #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed + // TUPLE element. The struct N_INDEX-lhs block above handles + // struct / ptr-to-struct elements; a tuple element fell through + // to the read-resolver and died LOUD. Resolve &tbl[i] via the + // place-spine (cgplaceaddr, the #116 mechanism) into BX→AX, then + // read the field at addr+foff reusing the per-element-kind arms + // the local N_TTUPLE block wires: str-triple / float-X0 / fn-or- + // scalar loadopsz. Narrow (rob Q3): tagged / nested-aggregate + // fields stay LOUD. Mirrors cstage cgen.c leg-(a) arm 1:1. + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + let eu: *tinfo = tichase(lhs.type_: *tinfo); + if (eu != nil) { if (eu.kind == tykind.TY_TUPLE) { + let idx: i32 = fldnumidx(fld); + if (idx >= 0) { + // ww tuples store elements in .tupleelems + // (ttupleelem chain), NOT .params (that is + // fn-only). foff via tupeslot accumulation = + // cstage tuple_eslot, byte-id. + let tp: *ttupleelem = eu.tupleelems; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += tupeslot(tp.type_); + tp = tp.tnext; + i += 1; + }; + }; + if (tp != nil) { + let ft: *tinfo = tp.type_; + let fu: *tinfo = tichase(ft); + if (fu != nil && (fu.kind == tykind.TY_TAGGED + || fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_TUPLE + || fu.kind == tykind.TY_ARRAY)) { + let mt: str = "#121: aggregate/tagged tuple-element field read off an indexed base unwired\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (!cgplaceaddr(c, lhs, "BX")) { + let mp: str = "#121: indexed tuple base not place-resolvable\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\tBX, AX\n"); + if (typeisfloat(ft)) { + let mov: str = "MOVSD"; + if (typeisf32(ft)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + if (fu != nil && (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE)) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + }; + };}; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case; @@ -8157,6 +8316,91 @@ fn cgassign(c: *cgen, n: *node) void = { c.sretdestoff = 0; return; }; + // #121 (write-face of leg-b): a tuple-LITERAL rhs into + // an indexed element `a[i] = (3,4)`. A literal has no + // source ADDRESS, so the ident/dot/deref copy arm below + // can't reach it — it fell to the 1-word scalar store + // tail (word0 only; the read-luck masked it until leg-b's + // correct read). Materialise the literal into a frame + // scratch via the cglet in-cap path (cgtuplelittocursor + + // tupstore), then word-copy scratch → &a[i]. NARROW: + // N_TTUPLE-literal rhs, N_IDENT base (idxelemtn gives the + // element N_TTUPLE), in-cap. Mirror of cstage cgen.c #121 + // store arm; the materialise + base-resolve are the + // cglet / #270-1b byte-id twins. + if (n.rhs.kind == nkind.N_TUPLE && esz > 8 + && base != nil && base.kind == nkind.N_IDENT + && elemtn != nil && elemtn.kind == nkind.N_TTUPLE + && sretretsize(c, elemtn) == 0) { + let scr121: i32 = tagscradd(c, esz); + cgtuplelittocursor(c, n.rhs, elemtn); + let gpc: i32 = 0; + let ssc: i32 = 0; + let eo: i32 = 0; + let q121: *node = elemtn.list; + for (q121 != nil) { + let qt: *node = q121.lhs; + let isflt: bool = isfloattype(c, qt); + let es: i32 = tupeslotn(qt); + tupstore(c, gpc, ssc, scr121 + eo, es, qt); + if (isflt) { ssc += 1; } + else { gpc += es / 8; }; + eo += es; + q121 = q121.next; + }; + // dest &a[i] → BX (#270-1b base resolve) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn2: *node = baselocal.tnode; + let isarr2: bool = false; + if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; }; + if (basealias) { + let bu60: *tinfo = tichase(base.type_: *tinfo); + if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; }; + }; + if (isarr2) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { if (dotbaseaddr(c, base, "BX")) { + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let kk2: i32 = 0; + for (kk2 < esz) { + emitline("\tMOVQ\t"); + emitoff((scr121 + kk2): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(kk2: i64); + emitline("(BX)\n"); + kk2 += 8; + }; + return; + }; // #270-1b: aggregate (struct/array/tuple >8B) element // STORE `a[i] = val`. The scalar store path below copies // only the first 8 bytes (tnodestoreop MOVQ) — a silent diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 2387fb6e..c36aa1f8 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -3727,6 +3727,32 @@ fn cgforrange(c: *cgen, n: *node) void = { baseoff = localalloc(c, bname, 8, nil); }; }; + // #121 leg (c): for-range over a module-GLOBAL slice/str/array base + // SEGV's today — the init + per-iteration base resolution below + // assume a frame-local slot (localfindnode), so a global let/def base + // reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage + // cgen.c (byte-id-neutral; segfault→compile-error is pure + // improvement). The fix (the N_INDEX isglobal base resolution ported + // into the for-range spine) is a DISTINCT mechanism — filed as a #121 + // sibling, off fold-6's path. + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + if (localfindnode(c, slc.str) == nil) { + let isglob: bool = isletvar(c, slc.str); + if (!isglob) { + let gdtn: *node = defvartnode(c, slc.str); + if (gdtn != nil) { + if (gdtn.kind == nkind.N_TARRAY) { isglob = true; }; + }; + }; + if (isglob) { + let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n"; + os.write(2, mc.ptr, mc.len: u64); + os.exit(1); + }; + }; + }; + }; // Per-binding (up to 8 — matches the C array). Parallel arrays so // we don't depend on local-struct cgen. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 04f2efbb..fd807868 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -23272,6 +23272,25 @@ fn cgindex(c: *cgen, n: *node) void = { }; }; }; + // #121 leg (b): whole TUPLE element `let e = tbl[i]`. Classify off + // the chased index-result tinfo (n.type_, the same source the N_DOT/ + // N_INDEX arms read). gptotal = sum(tupeslot/8); the per-base arms + // below fill that many cursor words (tupreg) from the element + // address. Mirrors cstage cgen.c N_INDEX TY_TUPLE arms. + let elem_tuple: bool = false; + let tuple_nwords: i32 = 0; + { + let eti: *tinfo = tichase(n.type_: *tinfo); + if (eti != nil) { if (eti.kind == tykind.TY_TUPLE) { + elem_tuple = true; + // ww tuples store elements in .tupleelems, not .params. + let tpw: *ttupleelem = eti.tupleelems; + for (tpw != nil) { + tuple_nwords += tupeslot(tpw.type_) / 8; + tpw = tpw.tnext; + }; + };}; + }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -23316,6 +23335,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — fill gptotal cursor words + // from the element address (BX); descending so AX loads last, + // mirroring the tagged arm. Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 @@ -23388,6 +23426,24 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b): whole TUPLE element — twin of the global arm + // above (base in BX). Over-cap leaves the ADDRESS in AX. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + emitline("\tMOVQ\tBX, AX\n"); + return; + }; + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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 || elemisslice) { @@ -23449,6 +23505,25 @@ fn cgindex(c: *cgen, n: *node) void = { emitline("\tMOVQ\t(BX), AX\n"); return; }; + // #121 leg (b) via fallback base: whole TUPLE element. AX holds the + // element address — copy to BX (the cursor fill into AX clobbers it), + // then fill the gptotal cursor words. Over-cap leaves AX as the addr. + if (elem_tuple) { + if (tuple_nwords > TUPLE_GPCAP) { + return; + }; + emitline("\tMOVQ\tAX, BX\n"); + let kw: i32 = tuple_nwords - 1; + for (kw >= 0) { + emitline("\tMOVQ\t"); + emitdispreg((kw * 8): i64, "BX"); + emitline(", "); + emitline(tupreg(kw)); + emitline("\n"); + kw -= 1; + }; + return; + }; // 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. @@ -25071,6 +25146,90 @@ fn cgdot(c: *cgen, n: *node) void = { };}; }; }; + // #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed + // TUPLE element. The struct N_INDEX-lhs block above handles + // struct / ptr-to-struct elements; a tuple element fell through + // to the read-resolver and died LOUD. Resolve &tbl[i] via the + // place-spine (cgplaceaddr, the #116 mechanism) into BX→AX, then + // read the field at addr+foff reusing the per-element-kind arms + // the local N_TTUPLE block wires: str-triple / float-X0 / fn-or- + // scalar loadopsz. Narrow (rob Q3): tagged / nested-aggregate + // fields stay LOUD. Mirrors cstage cgen.c leg-(a) arm 1:1. + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + let eu: *tinfo = tichase(lhs.type_: *tinfo); + if (eu != nil) { if (eu.kind == tykind.TY_TUPLE) { + let idx: i32 = fldnumidx(fld); + if (idx >= 0) { + // ww tuples store elements in .tupleelems + // (ttupleelem chain), NOT .params (that is + // fn-only). foff via tupeslot accumulation = + // cstage tuple_eslot, byte-id. + let tp: *ttupleelem = eu.tupleelems; + let foff: i32 = 0; + let i: i32 = 0; + for (i < idx) { + if (tp == nil) { i = idx; } + else { + foff += tupeslot(tp.type_); + tp = tp.tnext; + i += 1; + }; + }; + if (tp != nil) { + let ft: *tinfo = tp.type_; + let fu: *tinfo = tichase(ft); + if (fu != nil && (fu.kind == tykind.TY_TAGGED + || fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_TUPLE + || fu.kind == tykind.TY_ARRAY)) { + let mt: str = "#121: aggregate/tagged tuple-element field read off an indexed base unwired\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (!cgplaceaddr(c, lhs, "BX")) { + let mp: str = "#121: indexed tuple base not place-resolvable\n"; + os.write(2, mp.ptr, mp.len: u64); + os.exit(1); + }; + emitline("\tMOVQ\tBX, AX\n"); + if (typeisfloat(ft)) { + let mov: str = "MOVSD"; + if (typeisf32(ft)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + if (fu != nil && (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE)) { + emitline("\tMOVQ\t"); + emitdispreg((foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg((foff + 16): i64, "AX"); + emitline(", CX\n"); + emitline("\tMOVQ\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + }; + };}; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case; @@ -29510,6 +29669,91 @@ fn cgassign(c: *cgen, n: *node) void = { c.sretdestoff = 0; return; }; + // #121 (write-face of leg-b): a tuple-LITERAL rhs into + // an indexed element `a[i] = (3,4)`. A literal has no + // source ADDRESS, so the ident/dot/deref copy arm below + // can't reach it — it fell to the 1-word scalar store + // tail (word0 only; the read-luck masked it until leg-b's + // correct read). Materialise the literal into a frame + // scratch via the cglet in-cap path (cgtuplelittocursor + + // tupstore), then word-copy scratch → &a[i]. NARROW: + // N_TTUPLE-literal rhs, N_IDENT base (idxelemtn gives the + // element N_TTUPLE), in-cap. Mirror of cstage cgen.c #121 + // store arm; the materialise + base-resolve are the + // cglet / #270-1b byte-id twins. + if (n.rhs.kind == nkind.N_TUPLE && esz > 8 + && base != nil && base.kind == nkind.N_IDENT + && elemtn != nil && elemtn.kind == nkind.N_TTUPLE + && sretretsize(c, elemtn) == 0) { + let scr121: i32 = tagscradd(c, esz); + cgtuplelittocursor(c, n.rhs, elemtn); + let gpc: i32 = 0; + let ssc: i32 = 0; + let eo: i32 = 0; + let q121: *node = elemtn.list; + for (q121 != nil) { + let qt: *node = q121.lhs; + let isflt: bool = isfloattype(c, qt); + let es: i32 = tupeslotn(qt); + tupstore(c, gpc, ssc, scr121 + eo, es, qt); + if (isflt) { ssc += 1; } + else { gpc += es / 8; }; + eo += es; + q121 = q121.next; + }; + // dest &a[i] → BX (#270-1b base resolve) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn2: *node = baselocal.tnode; + let isarr2: bool = false; + if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; }; + if (basealias) { + let bu60: *tinfo = tichase(base.type_: *tinfo); + if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; }; + }; + if (isarr2) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { if (dotbaseaddr(c, base, "BX")) { + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let kk2: i32 = 0; + for (kk2 < esz) { + emitline("\tMOVQ\t"); + emitoff((scr121 + kk2): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(kk2: i64); + emitline("(BX)\n"); + kk2 += 8; + }; + return; + }; // #270-1b: aggregate (struct/array/tuple >8B) element // STORE `a[i] = val`. The scalar store path below copies // only the first 8 bytes (tnodestoreop MOVQ) — a silent @@ -36568,6 +36812,32 @@ fn cgforrange(c: *cgen, n: *node) void = { baseoff = localalloc(c, bname, 8, nil); }; }; + // #121 leg (c): for-range over a module-GLOBAL slice/str/array base + // SEGV's today — the init + per-iteration base resolution below + // assume a frame-local slot (localfindnode), so a global let/def base + // reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage + // cgen.c (byte-id-neutral; segfault→compile-error is pure + // improvement). The fix (the N_INDEX isglobal base resolution ported + // into the for-range spine) is a DISTINCT mechanism — filed as a #121 + // sibling, off fold-6's path. + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + if (localfindnode(c, slc.str) == nil) { + let isglob: bool = isletvar(c, slc.str); + if (!isglob) { + let gdtn: *node = defvartnode(c, slc.str); + if (gdtn != nil) { + if (gdtn.kind == nkind.N_TARRAY) { isglob = true; }; + }; + }; + if (isglob) { + let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n"; + os.write(2, mc.ptr, mc.len: u64); + os.exit(1); + }; + }; + }; + }; // Per-binding (up to 8 — matches the C array). Parallel arrays so // we don't depend on local-struct cgen. diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 37d13b43..e88d251a 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -1382,9 +1382,11 @@ static const struct row rows[] = { " return 0;\n" "};\n", 0, K_BUILDERR, "tuple arg element kind unsupported" }, - /* variadic-of-tuples ((i64,i64)...) stays LOUD via the - * tuple-in-slice read surface — bounded, not silent (demand 2). */ - { "t2_reject_variadic_of_tuples", + /* variadic-of-tuples ((i64,i64)...): `ts[0].0` is an indexed-slice + * tuple-element FIELD read — #121 leg (a) now resolves it (was LOUD + * "unsupported field-read shape"; the dispatch fired only for an + * N_IDENT base). Runs correct both stages + byte-id. */ + { "t2_variadic_of_tuples", "package main;\n" "fn first(ts: (i64, i64)...) i64 = {\n" " if (len(ts) == 0) { return -1; };\n" @@ -1395,7 +1397,7 @@ static const struct row rows[] = { " if (first(t) != 3) { return 1; };\n" " return 0;\n" "};\n", 0, - K_BUILDERR, "unsupported field-read shape" }, + K_RUN, NULL }, /* rule-7: a tuple arg from a source whose cgexpr does NOT fill * the cursor (here a struct-field chain) dies loud — pre-C-t2 * it fell to the scalar single-PUSHQ default silently. diff --git a/test/wcc/947_tuple_index_read_run.c b/test/wcc/947_tuple_index_read_run.c new file mode 100644 index 00000000..ea113754 --- /dev/null +++ b/test/wcc/947_tuple_index_read_run.c @@ -0,0 +1,297 @@ +/* + * 947_tuple_index_read_run — #121: const-slice tuple-element READ. + * + * Three legs of indexed-tuple read off a `const [](str,*fn)` (fold-6's + * charclass_map shape), all sharing the &tbl[i] place-spine (#116): + * + * (a) FIELD `tbl[i].N` (bound) — was LOUD both stages ("unsupported + * field-read shape": the tuple-field dispatch fired only for an + * N_IDENT base; an N_INDEX base fell to the read-resolver fatal). + * Now resolved via cgplaceaddr + the per-element-kind arms + * (str-triple / fn-8B). align-loud→both-accept-correct. + * + * (b) WHOLE `let e = tbl[i]` then read e.N — was SILENT both-wrong- + * IDENTICAL (#263, word0-only: the in-cap tuple receive read a + * register cursor the N_INDEX load never filled past word0, so + * str.len→0 and the *fn word was garbage). Now fills all gptotal + * cursor words from &tbl[i]. align-both-correct. + * + * (c) for-range over a module-global slice — LOUD-STOP both stages + * (the for-range spine has no global-base resolution; pre-fix it + * read saved-BP → SEGV with divergent cs≠ww asm). Filed as a #121 + * sibling; loud is byte-id-neutral + a pure improvement. + * + * POLARITY: leg (a) cstage is the byte-id ref (align-loud→accept); leg + * (b) byte-id is BLIND (#263) — both stages were identically truncated, + * so the runtime READ-BACK with DISTINCT-per-row values (str lengths + * differ; fns fa↔'a'/fz↔'z' differ) is the only net: a wrong/truncated + * word is CAUGHT, not masked. + * + * MUST use `[](str,*fn)`, NOT `[](str,int)` — the latter is #120 + * wwstage-checker-rejected ("let: not assignable") BEFORE the read runs. + * + * NNN<950, self-contained (/tmp, no imports) — rule-14's selfhost-sibling + * race does not apply (941/944/945/946 precedent). Every K_RUN row also + * pins cstage/wwstage asm byte-id. + */ +#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; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +#define K_RUN 0 /* build+run both drivers, exit==want, + cs==ww byte-id */ +#define K_BUILDERR 1 /* build must FAIL with experr on BOTH drivers (rule 7) */ + +struct row { const char *label; const char *src; int want; + int kind; const char *experr; }; + +static int +errlog_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +static const struct row rows[] = { + /* leg (a): direct `tbl[i].N` field read, bound then observed. + * str field (distinct lengths 2 vs 4) + fn field (distinct fns, + * each called back). Was LOUD both stages at base 761cfa4. */ + { "field_read", + "package main;\n" + "fn fa(c: rune) bool = { return c == 'a'; };\n" + "fn fz(c: rune) bool = { return c == 'z'; };\n" + "const tbl: [](str, *fn(c: rune) bool) = " + "[(\"ab\", &fa), (\"cdef\", &fz)];\n" + "export fn main() i32 = {\n" + " let s1 = tbl[1].0;\n" + " let s0 = tbl[0].0;\n" + " if (len(s1) != 4) { return 1; };\n" + " if (len(s0) != 2) { return 2; };\n" + " let f1 = tbl[1].1;\n" + " if (!(*f1)('z')) { return 3; };\n" + " if ((*f1)('a')) { return 4; };\n" + " let f0 = tbl[0].1;\n" + " if (!(*f0)('a')) { return 5; };\n" + " if ((*f0)('z')) { return 6; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* leg (b): whole-element bind then read e.0 (str.len, catches the + * w1→0 truncation) + e.1 (the *fn word, catches w3 garbage). + * DISTINCT per row — a truncated/wrong word is caught not masked. + * Was SILENT both-wrong-identical (#263) at base 761cfa4. */ + { "whole_element", + "package main;\n" + "fn fa(c: rune) bool = { return c == 'a'; };\n" + "fn fz(c: rune) bool = { return c == 'z'; };\n" + "const tbl: [](str, *fn(c: rune) bool) = " + "[(\"ab\", &fa), (\"cdef\", &fz)];\n" + "export fn main() i32 = {\n" + " let e = tbl[1];\n" + " if (len(e.0) != 4) { return 1; };\n" + " if (!(*e.1)('z')) { return 2; };\n" + " if ((*e.1)('a')) { return 3; };\n" + " let e0 = tbl[0];\n" + " if (len(e0.0) != 2) { return 4; };\n" + " if (!(*e0.1)('a')) { return 5; };\n" + " if ((*e0.1)('z')) { return 6; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* write-face of leg (b): a tuple-LITERAL store into an indexed + * element `a[i] = (3,4)`, read back whole. DISTINCT words per row + * (3≠4, 7≠9) so the pre-fix word0-only store (cgen.c:6249 fell to + * the scalar tail) is CAUGHT — t.1 would read 0. Both faces (this + * store + the leg-b read) must be correct or the round-trip fails; + * the read alone was green by stale-register luck (809 tuple_elem). */ + { "store_roundtrip", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [2](u64, u64);\n" + " a[1] = (3u64, 4u64);\n" + " a[0] = (7u64, 9u64);\n" + " let t = a[1];\n" + " if (t.0 != 3) { return 1; };\n" + " if (t.1 != 4) { return 2; };\n" + " let s = a[0];\n" + " if (s.0 != 7) { return 3; };\n" + " if (s.1 != 9) { return 4; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* leg (c): for-range over a module-global slice LOUD-STOPS, + * symmetric both stages (segfault→compile-error, byte-id-neutral). + * `n += 1` (not len()) sidesteps the #26 len-vs-int checker reject + * so the cgen loud is what fires, on BOTH stages. */ + { "forrange_global_loud", + "package main;\n" + "fn fa(c: rune) bool = { return c == 'a'; };\n" + "const tbl: [](str, *fn(c: rune) bool) = " + "[(\"ab\", &fa), (\"cdef\", &fa)];\n" + "export fn main() i32 = {\n" + " let n: int = 0;\n" + " for (let e .. tbl) { n += 1; };\n" + " return n: i32;\n" + "};\n", 0, K_BUILDERR, + "for-range over a module-global" }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tir_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tir_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/tir_%d_e_%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 >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (r->kind == K_BUILDERR) { + int ok = (brc != 0) + && (r->experr == NULL || errlog_has(errf, r->experr)); + if (!ok) + fprintf(stderr, "row[%s]: %s expected loud builderr " + "\"%s\" (brc=%d)\n", r->label, driver, + r->experr ? r->experr : "", brc); + unlink(src); unlink(errf); rmdir(tmpdir); + return ok ? 0 : 1; + } + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + 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); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[96], cs[96], ws[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tir_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/tir_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/tir_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + int rc = slurp_eq(cs, ws); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[2120], wdrv[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + if (run_driver(cdrv, &rows[i], i) != 0) fail++; + } + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (run_driver(wdrv, &rows[i], i) != 0) fail++; + } + for (int i = 0; i < n; i++) { + if (rows[i].kind == K_BUILDERR) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "tuple_index_read: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("tuple_index_read: %d/%d ok\n", total, total); + return 0; +}