From bf1037d8c43f0238e7301b3ca235f75043561a86 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 01:32:27 +0900 Subject: [PATCH] wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A one-step `let xs: []T = [e0,e1,..]` had two faults. #31 (silent, cs!=ww): the #258 array→slice borrow wrapped the un-addressable N_ARRLIT directly as the N_SLICE base and cgen never spilled it to a stack slot, so .ptr dangled (`let xs:[]i32=[10,20,30]; xs[1]` returned the un-stored header 1; []u8/[]str segfaulted). #25 (over-strict): a slice target fell through to the exact- element type_eq borrow gate, rejecting bare-int-width ([]u8=[1,2,3]) and str elements the array-init path coerces. Fix (re-stamp + per-borrow scratch; both stages byte-identical asm): - Checker re-stamps the slice arrlit as [count]T, reusing the array-init per-element coercion + range-check (#25): in-range accepts, out-of-range loud-rejects. cstage arrlit_init_fits gains a TY_SLICE arm; wwstage checkletassign mirrors it and stashes the synthesized [count]T tnode on arrlit.lhs (free for N_ARRLIT) so cgen can size the backing NODE-wise (elemsizeofc) and count from the tnode's .rhs intlit — the arrlit's own value tinfo carries the literal's untyped element (unsized), so node-first sizing is required (a cstage/wwstage representation divergence; cstage's Type IS sized and reads base->type). - cgen materialises the N_ARRLIT borrow base into a FRESH per-borrow @slicescr stack slot (distinct slot per borrow: a borrow's backing must outlive the lowering, so it can't share a cached @aggargscr/@tagscr-style slot — two live borrows would alias one backing; localalloc/local_alloc is always-fresh), filled by REUSING the array-init element fill extracted from the N_LET path (cstage cg_arrlit_fill_bp, wwstage cgarrlitfillbp — same store sequence the byte-id-green `let a:[N]T=[..]` uses, the frame-order + store-op guarantee), then LEAQ'd as the base. Supported ONLY at a `let` init. In call-arg / return / assign position there is no addressable backing, so both stages LOUD-REJECT ("bind it to a `let` first") — aligning cstage DOWN to wwstage (which already refused the untyped arrlit element) per rule-10; this closes #31's silent call-arg segfault as a compile error. Full non-let support is deferred (#33). Escape (rule-8 WHY): a `let xs:[]T=[..]; return xs;` returns a slice into a freed frame slot = dangling, IDENTICAL to the pre-existing named-array borrow and Hare-consistent (no escape analysis / GC / heap promotion). Test 953_arrlit_slice_run: 8 accept rows (cstage runtime readback + cs==ww byte-id, frame-size canary incl.) covering the #31 i32 pin, bare-int→u8 coercion, str readback, the multi-live soundness pin (xs[0]+ys[0]=5, not 8 — proves fresh-per-borrow), and a mutate-through-borrow proof; 4 reject rows (out-of-range element + the three non-let contexts, loud in both stages). Tuple-element slices stay blocked by the pre-existing #30 array-init FATAL. --- cmd/w6c/cgen.c | 369 +++++++++-------- cmd/wcc/check.c | 57 ++- selfhost/cmd/w6c/main.combined.ww | 592 ++++++++++++++++----------- selfhost/cmd/wcc/cgenexpr.ww | 57 ++- selfhost/cmd/wcc/cgenstmt.ww | 455 ++++++++++---------- selfhost/cmd/wcc/cgenutil.ww | 2 + selfhost/cmd/wcc/check.ww | 78 +++- selfhost/cmd/wwdump/main.combined.ww | 592 ++++++++++++++++----------- test/wcc/953_arrlit_slice_run.c | 280 +++++++++++++ 9 files changed, 1621 insertions(+), 861 deletions(-) create mode 100644 test/wcc/953_arrlit_slice_run.c diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 6953c2a5..f41a62b8 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2984,6 +2984,175 @@ cg_tagged_tuple_payload_shift(Cg *c, Type *tup) ins2(c, A_MOVQ, areg(seq[i + 1]), areg(seq[i])); } +/* cg_arrlit_fill_bp — #31: fill the [count]T destination at BP-relative + * `off` from an N_ARRLIT, extracted verbatim from the N_LET array-init + * path so the slice-borrow base materialisation (the N_SLICE-over- + * N_ARRLIT arm) reuses the identical element-store sequence. `lu` is the + * [count]T array type the checker re-stamped (#25); `arrlit` the literal. */ +static void +cg_arrlit_fill_bp(Cg *c, Local **locals, Type *lu, Node *arrlit, int off) +{ + Type *esub = lu->sub; + int esz = esub ? (int)esub->size : 1; + /* #270-1c: an AGGREGATE (struct/array/tuple) element + * of an array literal — the scalar per-element MOVQ + * below stores only the first 8 bytes (unpopulated + * tail). Fill each element slot from its literal + * (cg_structlit_fill_bp) or source ident (word-copy). */ + Type *esubu = type_chase_named(esub); + int is_agg = esubu && (esubu->kind == TY_STRUCT + || esubu->kind == TY_ARRAY + || esubu->kind == TY_TUPLE); + /* #12: a tagged-union element. NOT folded into is_agg — + * is_agg's body does N_STRUCTLIT/N_IDENT word-copy and + * FATALs on the literal/scalar case, never boxing the + * tag+payload. Route each element through the same + * cg_widen_tagged_store choke-point every other tagged + * store uses (let-init, vararg gather, struct-field). */ + int is_tagged_el = esubu && esubu->kind == TY_TAGGED; + int is_str_el = type_isstr(esub); + /* #20/#270 str-slice arm: a slice element is a 24B + * {ptr,len,cap} header just like str; cgexpr lowers it + * into AX/BX/CX. Both must store all three words — the + * scalar 1-word MOVQ below drops .len and .cap. */ + int is_slice_el = type_isslice(esub); + /* float element → store FROM X0; the AX path stores + * raw double low-bits, garbage for f32 (#122, twin of + * the arr[i]= store fix and the cgen.c:6423 read). */ + int is_float_el = type_isfloat(esub); + int fmov = type_isf32(esub) ? A_MOVSS : A_MOVSD; + int op = A_MOVQ; + if (!is_str_el) { + if (esz == 1) op = A_MOVB; + else if (esz == 2) op = A_MOVW; + else if (esz == 4) op = A_MOVL; + /* #128a: esz==2 routes to MOVW (A_MOVW landed in + * both stages' w6a). Pre-fix the 2-byte case fell + * through to MOVQ, over-writing 6B into the next + * element's slot; sequential adjacent writes + * accident-corrected fully-init arrays but + * partial inits clobbered neighbours. */ + } + int idx = 0; + Node *last = NULL; + int repeat = 0; + for (Node *e = arrlit->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str && + strcmp(e->str, "...") == 0) { + repeat = 1; + break; + } + int base = off + idx * esz; + if (is_agg) { + if (e->kind == N_STRUCTLIT) { + cg_structlit_fill_bp(c, locals, + esubu, e, base); + } else if (e->kind == N_IDENT) { + int soff = localfind(*locals, + e->str); + int k = 0; + for (; k + 8 <= esz; k += 8) { + ins2(c, A_MOVQ, + amem(D_BP, soff + k), + areg(D_AX)); + ins2(c, A_MOVQ, + areg(D_AX), + amem(D_BP, base + k)); + } + if (k + 4 <= esz) { + ins2(c, A_MOVL, + amem(D_BP, soff + k), + areg(D_AX)); + ins2(c, A_MOVL, + areg(D_AX), + amem(D_BP, base + k)); + k += 4; + } + if (k + 2 <= esz) { + ins2(c, A_MOVW, + amem(D_BP, soff + k), + areg(D_AX)); + ins2(c, A_MOVW, + areg(D_AX), + amem(D_BP, base + k)); + k += 2; + } + if (k + 1 <= esz) { + ins2(c, A_MOVB, + amem(D_BP, soff + k), + areg(D_AX)); + ins2(c, A_MOVB, + areg(D_AX), + amem(D_BP, base + k)); + k += 1; + } + } else { + fatal("#270-1c: array-literal " + "aggregate element shape " + "unsupported (rule-7)"); + } + last = e; + idx++; + continue; + } + if (is_tagged_el) { + cg_widen_tagged_store(c, locals, esub, + e, D_BP, base, esz); + last = e; + idx++; + continue; + } + cgexpr(c, e, *locals); + if (is_str_el || is_slice_el) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, base)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, base + 8)); + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, base + 16)); + } else if (is_float_el) { + ins2(c, fmov, areg(D_X0), + amem(D_BP, base)); + } else { + ins2(c, op, areg(D_AX), + amem(D_BP, base)); + } + last = e; + idx++; + } + if (repeat && is_agg) + fatal("#270-1c: `...` repeat of an aggregate " + "array-literal element not wired (rule-7)"); + /* #12: `...` re-stores from AX, but cg_widen_tagged_store + * consumed the node and trashed AX — a repeat-fill would + * write garbage. No consumer needs `[N]tagged=[x,...]`. */ + if (repeat && is_tagged_el) + fatal("#12: `...` repeat of a tagged-union " + "array-literal element not wired (rule-7)"); + if (repeat && last) { + /* fill remaining slots with the value still in + * AX (and BX for str). */ + while (idx < (int)lu->alen) { + int base = off + idx * esz; + if (is_str_el || is_slice_el) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, base)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, base + 8)); + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, base + 16)); + } else if (is_float_el) { + ins2(c, fmov, areg(D_X0), + amem(D_BP, base)); + } else { + ins2(c, op, areg(D_AX), + amem(D_BP, base)); + } + idx++; + } + } +} + static void cgexpr(Cg *c, Node *n, Local *locals) { @@ -6428,7 +6597,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * silently wrong for non-u8). Other non-ident * bases stay esz=1 (unscaled). */ int esz = (base && (base->kind == N_IDENT - || base->kind == N_DOT) + || base->kind == N_DOT + || base->kind == N_ARRLIT) && bu && bu->sub) ? (int)bu->sub->size : 1; /* base addr → push */ @@ -8685,7 +8855,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * non-ident bases stay esz=1 (unscaled) -- #76 residual, * non-ident cluster #74. */ int esz = (base && (base->kind == N_IDENT - || base->kind == N_DOT) && bu && bu->sub) + || base->kind == N_DOT || base->kind == N_ARRLIT) + && bu && bu->sub) ? (int)bu->sub->size : 1; if (base && base->kind == N_IDENT) { int boff = localfind(locals, base->str); @@ -8701,6 +8872,40 @@ cgexpr(Cg *c, Node *n, Local *locals) } else { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); } + } else if (base && base->kind == N_ARRLIT && bu + && bu->kind == TY_ARRAY) { + /* #31: an array LITERAL base — the desugared one-step + * `let xs: []T = [..]` borrow (the ONLY context that + * reaches here; call-arg/return/assign loud-reject at the + * checker, reject_arrlit_borrow, deferred to #33). The + * literal has no storage address — cgexpr would leave + * AX=garbage and the borrow's .ptr would dangle. + * Materialise it into a FRESH per-borrow @slicescr stack + * slot (distinct slot per borrow: a borrow's backing must + * stay live for the slice's lifetime, so it can't share a + * cached SSoT slot the way @aggargscr/@tagscr — drained/ + * consumed in place — do; two live borrows would otherwise + * alias one backing). Reuses local_alloc + the shared + * array-init fill; the checker re-stamped base->type to + * [count]T (#25) so the fill stores at the declared + * element width. + * + * Escape (WHY, rob): a `let xs: []T = [..]; return xs;` + * returns a slice pointing at this frame slot, freed on + * return = dangling. This is IDENTICAL to the pre-existing + * named-array borrow (`let a: [N]T = [..]; return a;`) and + * is Hare-consistent: ww has no escape analysis, no GC, no + * heap promotion — borrowing a local past its frame is a + * programmer footgun, not promoted. Don't "fix" this + * expecting heap promotion; ww deliberately doesn't, same + * as Hare. */ + int cnt = (int)bu->alen; + int bsz = (bu->sub ? (int)bu->sub->size : 1) * cnt; + if (bsz < 1) bsz = 1; + int scr = local_alloc(c, &locals, "@slicescr", bsz, + cg_frame); + cg_arrlit_fill_bp(c, &locals, bu, base, scr); + ins2(c, A_LEAQ, amem(D_BP, scr), areg(D_AX)); } else if (base) { /* #252: N_DOT `[N]T`-field base → field ADDRESS via * cg_dotbase_addr (LEAQ), not the auto-deref VALUE load @@ -9071,165 +9276,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * (is_agg excludes TY_TAGGED) — tracked as task #12. */ if (n->rhs && n->rhs->kind == N_ARRLIT && lu && lu->kind == TY_ARRAY) { - Type *esub = lu->sub; - int esz = esub ? (int)esub->size : 1; - /* #270-1c: an AGGREGATE (struct/array/tuple) element - * of an array literal — the scalar per-element MOVQ - * below stores only the first 8 bytes (unpopulated - * tail). Fill each element slot from its literal - * (cg_structlit_fill_bp) or source ident (word-copy). */ - Type *esubu = type_chase_named(esub); - int is_agg = esubu && (esubu->kind == TY_STRUCT - || esubu->kind == TY_ARRAY - || esubu->kind == TY_TUPLE); - /* #12: a tagged-union element. NOT folded into is_agg — - * is_agg's body does N_STRUCTLIT/N_IDENT word-copy and - * FATALs on the literal/scalar case, never boxing the - * tag+payload. Route each element through the same - * cg_widen_tagged_store choke-point every other tagged - * store uses (let-init, vararg gather, struct-field). */ - int is_tagged_el = esubu && esubu->kind == TY_TAGGED; - int is_str_el = type_isstr(esub); - /* #20/#270 str-slice arm: a slice element is a 24B - * {ptr,len,cap} header just like str; cgexpr lowers it - * into AX/BX/CX. Both must store all three words — the - * scalar 1-word MOVQ below drops .len and .cap. */ - int is_slice_el = type_isslice(esub); - /* float element → store FROM X0; the AX path stores - * raw double low-bits, garbage for f32 (#122, twin of - * the arr[i]= store fix and the cgen.c:6423 read). */ - int is_float_el = type_isfloat(esub); - int fmov = type_isf32(esub) ? A_MOVSS : A_MOVSD; - int op = A_MOVQ; - if (!is_str_el) { - if (esz == 1) op = A_MOVB; - else if (esz == 2) op = A_MOVW; - else if (esz == 4) op = A_MOVL; - /* #128a: esz==2 routes to MOVW (A_MOVW landed in - * both stages' w6a). Pre-fix the 2-byte case fell - * through to MOVQ, over-writing 6B into the next - * element's slot; sequential adjacent writes - * accident-corrected fully-init arrays but - * partial inits clobbered neighbours. */ - } - int idx = 0; - Node *last = NULL; - int repeat = 0; - for (Node *e = n->rhs->list; e; e = e->next) { - if (e->kind == N_FIELD && e->str && - strcmp(e->str, "...") == 0) { - repeat = 1; - break; - } - int base = off + idx * esz; - if (is_agg) { - if (e->kind == N_STRUCTLIT) { - cg_structlit_fill_bp(c, locals, - esubu, e, base); - } else if (e->kind == N_IDENT) { - int soff = localfind(*locals, - e->str); - int k = 0; - for (; k + 8 <= esz; k += 8) { - ins2(c, A_MOVQ, - amem(D_BP, soff + k), - areg(D_AX)); - ins2(c, A_MOVQ, - areg(D_AX), - amem(D_BP, base + k)); - } - if (k + 4 <= esz) { - ins2(c, A_MOVL, - amem(D_BP, soff + k), - areg(D_AX)); - ins2(c, A_MOVL, - areg(D_AX), - amem(D_BP, base + k)); - k += 4; - } - if (k + 2 <= esz) { - ins2(c, A_MOVW, - amem(D_BP, soff + k), - areg(D_AX)); - ins2(c, A_MOVW, - areg(D_AX), - amem(D_BP, base + k)); - k += 2; - } - if (k + 1 <= esz) { - ins2(c, A_MOVB, - amem(D_BP, soff + k), - areg(D_AX)); - ins2(c, A_MOVB, - areg(D_AX), - amem(D_BP, base + k)); - k += 1; - } - } else { - fatal("#270-1c: array-literal " - "aggregate element shape " - "unsupported (rule-7)"); - } - last = e; - idx++; - continue; - } - if (is_tagged_el) { - cg_widen_tagged_store(c, locals, esub, - e, D_BP, base, esz); - last = e; - idx++; - continue; - } - cgexpr(c, e, *locals); - if (is_str_el || is_slice_el) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, base)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, base + 8)); - ins2(c, A_MOVQ, areg(D_CX), - amem(D_BP, base + 16)); - } else if (is_float_el) { - ins2(c, fmov, areg(D_X0), - amem(D_BP, base)); - } else { - ins2(c, op, areg(D_AX), - amem(D_BP, base)); - } - last = e; - idx++; - } - if (repeat && is_agg) - fatal("#270-1c: `...` repeat of an aggregate " - "array-literal element not wired (rule-7)"); - /* #12: `...` re-stores from AX, but cg_widen_tagged_store - * consumed the node and trashed AX — a repeat-fill would - * write garbage. No consumer needs `[N]tagged=[x,...]`. */ - if (repeat && is_tagged_el) - fatal("#12: `...` repeat of a tagged-union " - "array-literal element not wired (rule-7)"); - if (repeat && last) { - /* fill remaining slots with the value still in - * AX (and BX for str). */ - while (idx < (int)lu->alen) { - int base = off + idx * esz; - if (is_str_el || is_slice_el) { - ins2(c, A_MOVQ, areg(D_AX), - amem(D_BP, base)); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, base + 8)); - ins2(c, A_MOVQ, areg(D_CX), - amem(D_BP, base + 16)); - } else if (is_float_el) { - ins2(c, fmov, areg(D_X0), - amem(D_BP, base)); - } else { - ins2(c, op, areg(D_AX), - amem(D_BP, base)); - } - idx++; - } - } + cg_arrlit_fill_bp(c, locals, lu, n->rhs, off); break; } /* Struct ident copy: `let p2: T = p1;` where T is a struct diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index ae27e895..1afd6b73 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -396,13 +396,21 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs) { if (rhs == NULL || rhs->kind != N_ARRLIT) return 0; Type *u = (dt && dt->kind == TY_NAMED) ? dt->under : dt; - if (u == NULL || u->kind != TY_ARRAY) return 0; + /* #25: a SLICE target is admitted via the same per-element coercion + * as the array path — the #258 borrow demands an exact element + * type_eq, which an arrlit's self-stamped [N] can't meet for + * untyped_str / bare-int-width elements. Peel to the slice element T + * and run the array-element coercion against it. */ + if (u == NULL || (u->kind != TY_ARRAY && u->kind != TY_SLICE)) + return 0; Type *et = u->sub; Type *eu = (et && et->kind == TY_NAMED) ? et->under : et; + u64 count = 0; for (Node *e = rhs->list; e; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) continue; + count++; Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; u64 v; @@ -417,6 +425,12 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs) if (!type_assignable(et, e->type) && !assignable_addrfn(c, et, e)) return 0; } + /* #25/#31: re-stamp the literal as [count]T so desugar_arrayslice keys + * on an exact-element-eq array and the cgen N_SLICE-over-N_ARRLIT arm + * (#31) materialises the borrow backing at the DECLARED element width. + * The array path keeps the declared array type, so this is slice-only. */ + if (u->kind == TY_SLICE) + rhs->type = type_array(c->a, et, count); return 1; } @@ -939,6 +953,26 @@ coerce_floatlit(Node *n, Type *target) * the four acceptance sites can call it unconditionally; it no-ops unless * the dst is a slice and the src an array with an exactly-matching * element. */ +/* reject_arrlit_borrow — #31/#33: the array-literal → slice borrow is + * supported only at a `let` init, where clet spills the literal to a + * per-borrow backing slot (#31). In call-arg / return / assign position + * there is no addressable backing — the borrow's .ptr would dangle (the + * original #31 silent segfault). Reject loudly here so the gap is a + * compile error, not a miscompile. rule-10: wwstage rejects the same + * source (its untyped-arrlit element fails the borrow's typeeq); aligning + * cstage DOWN keeps both stages loud-identical. Full non-let support is + * #33. Returns 1 (and emits the error) when it refuses the borrow. */ +static int +reject_arrlit_borrow(Checker *c, Type *dst, Node *expr) +{ + if (expr == NULL || expr->kind != N_ARRLIT) return 0; + Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst; + if (du == NULL || du->kind != TY_SLICE) return 0; + err(c, expr->pos, "array literal cannot borrow as a slice here; " + "bind it to a `let` first"); + return 1; +} + static void desugar_arrayslice(Checker *c, Type *dst, Node *expr) { @@ -1539,8 +1573,11 @@ cexpr(Checker *c, Node *n) && !assignable_addrfn(c, p->type, a)) err(c, a->pos, "argument type %s not assignable to %s", type_name(c->a, at), type_name(c->a, p->type)); - /* #258: `f(arr)` borrows the array as a full slice. */ - desugar_arrayslice(c, p->type, a); + /* #258: `f(arr)` borrows the array as a full slice. + * #31/#33: a bare array LITERAL arg has no backing — + * loud-reject (supported only at a `let`). */ + if (!reject_arrlit_borrow(c, p->type, a)) + desugar_arrayslice(c, p->type, a); p = p->next; } if (p != NULL && !p->variadic) @@ -1568,8 +1605,11 @@ cexpr(Checker *c, Node *n) !assignable_addrfn(c, l, n->rhs)) err(c, n->pos, "cannot assign %s to %s", type_name(c->a, r), type_name(c->a, l)); - /* #258: `s = arr` borrows the array as a full slice. */ - desugar_arrayslice(c, l, n->rhs); + /* #258: `s = arr` borrows the array as a full slice. + * #31/#33: a bare array LITERAL rhs has no backing — + * loud-reject (supported only at a `let`). */ + if (!reject_arrlit_borrow(c, l, n->rhs)) + desugar_arrayslice(c, l, n->rhs); return n->type = l; } case N_STRUCTLIT: { @@ -2032,8 +2072,11 @@ cstmt(Checker *c, Node *n) type_name(c->a, rt), type_name(c->a, c->ret)); /* #104 fold-2: `fn g() f32 = { return 1.0; }` — narrow to f32. */ coerce_floatlit(n->lhs, c->ret); - /* #258: `return arr` borrows the array as a full slice. */ - desugar_arrayslice(c, c->ret, n->lhs); + /* #258: `return arr` borrows the array as a full slice. + * #31/#33: a bare array LITERAL has no backing — loud-reject + * (supported only at a `let`). */ + if (!reject_arrlit_borrow(c, c->ret, n->lhs)) + desugar_arrayslice(c, c->ret, n->lhs); break; } case N_IF: { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index cb680d77..af719fd6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14212,6 +14212,25 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // unchanged when the shape doesn't match, else a fresh N_SLICE whose base // is `val` (which keeps its stamped array type_). The original sibling // link transfers to the N_SLICE so a desugared call-arg keeps its place. +// rejectarrlitborrow — #31/#33 twin of cstage reject_arrlit_borrow. The +// array-literal → slice borrow is supported only at a `let` init (where +// checkletassign re-stamps + the cgslice N_ARRLIT-base arm spills the +// literal to a per-borrow backing slot). In call-arg / return / assign +// position there is no addressable backing — loud-reject so the gap is a +// compile error, not a dangling-ptr miscompile. Both stages reject here +// (rule-10, byte-id-trivial: no asm). Full non-let support is #33. +fn rejectarrlitborrow(c: *checker, dsttn: *node, val: *node) bool = { + if (val == nil) { return false; }; + if (val.kind != nkind.N_ARRLIT) { return false; }; + let du: *node = resolvealias(c, unwrapbang(dsttn)); + if (du == nil) { return false; }; + if (du.kind != nkind.N_TSLICE) { return false; }; + let m: str = "array literal cannot borrow as a slice here; bind it to a `let` first\n"; + cerr(m); + c.errs += 1; + return true; +}; + fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node = { if (dsttn == nil) { return val; }; if (srctn == nil) { return val; }; @@ -14303,10 +14322,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { }; }; }; }; }; - let rep: *node = desugararrayslice(c, param.lhs, atype, a); - if (rep != a) { - if (prev == nil) { n.list = rep; } else { prev.next = rep; }; - a = rep; + // #31/#33: bare array-literal arg has no backing + // — loud-reject (supported only at a `let`). + if (!rejectarrlitborrow(c, param.lhs, a)) { + let rep: *node = desugararrayslice(c, param.lhs, atype, a); + if (rep != a) { + if (prev == nil) { n.list = rep; } else { prev.next = rep; }; + a = rep; + }; }; }; if (param.op != tkind.TK_ELLIPSIS) { param = param.next; }; @@ -14328,7 +14351,11 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); - n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + // #31/#33: bare array-literal rhs has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, ltn, n.rhs)) { + n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + }; }; // inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The @@ -14489,6 +14516,41 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #25/#31: an array literal initialising a SLICE local. Re-stamp the + // literal as [count]T (the slice element) so the #258 borrow's exact- + // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the + // declared element width. Run the same per-element coercion + range- + // check the array path runs (checkarrlitfits against a synthesized + // [count]T), then drive isassignable + the borrow off [count]T. Twin of + // cstage arrlit_init_fits' slice arm. Local-only (c.cur != c.top): the + // borrow runs at runtime; module-level slice-from-arrlit stays #32. + if (c.cur != c.top && n.lhs.kind == nkind.N_TSLICE + && n.rhs.kind == nkind.N_ARRLIT) { + let cnt: u64 = 0u64; + let e0: *node = n.rhs.list; + for (e0 != nil) { + let skip: bool = false; + if (e0.kind == nkind.N_FIELD) { + if (streq(e0.str, "...")) { skip = true; }; + }; + if (!skip) { cnt += 1u64; }; + e0 = e0.next; + }; + let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); + cn.uval = cnt; + let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); + arr.lhs = n.lhs.lhs; // declared slice element type + arr.rhs = cn; + checkarrlitfits(c, arr, n.rhs); + n.rhs.type_ = tinfofornode(c, arr): *void; + // #31: stash the [count]T tnode on the arrlit (arrlit.lhs is free + // — the parser sets only .list) so the cgslice N_ARRLIT-base arm + // can size the backing NODE-wise via elemsizeofc(base.lhs). wwstage + // narrow-primitive tinfos are unsized (i32/u8 .size==0, #8), so the + // element width must come from the type NODE, not the tinfo. + n.rhs.lhs = arr; + src = arr; + }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -14531,7 +14593,11 @@ fn checkretassign(c: *checker, n: *node) void = { if (!conf) { return; }; if (!ok) { errnotassign(c, c.fnret, src, "return"); }; // #258: `return arr` borrows the array as a full slice. - n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + // #31/#33: bare array-literal has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, c.fnret, n.lhs)) { + n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + }; }; // ---- is / as validity ------------------------------------------------ @@ -15956,6 +16022,8 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // the cgindex idiom) or an N_DOT array/slice-field base (#257: // scale by the field's element width, not esz=1 -> silently // wrong for non-u8). Other non-ident bases stay esz=1. + // (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects + // at the checker, supported only at a `let`; #33.) let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); @@ -21425,6 +21493,17 @@ fn cgslice(c: *cgen, n: *node) void = { dotbu = dotbu.under; }; };}; + // #31: an N_ARRLIT base (the desugared one-step `let xs:[]T=[..]` + // borrow — the ONLY context that reaches here; call-arg/return/assign + // loud-reject at the checker, #33) has no storage. Its [count]T type + // NODE is stashed on base.lhs by checkletassign's #25 re-stamp; size / + // count come NODE-wise (elemsizeofc / .rhs intlit), because wwstage + // narrow-primitive tinfos are unsized (#8). Cstage twin reads base->type + // (its Type IS sized). + let arrlittn: *node = nil; + if (base != nil) { if (base.kind == nkind.N_ARRLIT) { + arrlittn = base.lhs; + };}; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom) or an N_DOT array/slice-field base (#252: scale by // the field's element width, not esz=1 — silently wrong for non-u8). @@ -21436,7 +21515,9 @@ fn cgslice(c: *cgen, n: *node) void = { esz = elemsizeofc(c, globaltn); } else { if (dotbu != nil && dotbu.sub != nil) { esz = dotbu.sub.size: i32; - };};}; + } else { if (arrlittn != nil) { + esz = elemsizeofc(c, arrlittn); + };};};}; // base address if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -21466,6 +21547,32 @@ fn cgslice(c: *cgen, n: *node) void = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (base != nil && base.kind == nkind.N_ARRLIT + && arrlittn != nil) { + // #31: materialise the array literal into a FRESH per-borrow + // @slicescr stack slot (distinct slot per borrow — a borrow's + // backing must outlive the lowering, so it can't share a cached + // slot; localalloc is always-fresh, mirror of cstage local_alloc), + // fill it via the shared element-fill, then LEAQ the slot as base. + // Size/count NODE-wise off the stashed [count]T tnode (#8: tinfo + // primitive sizes are 0). Escape (WHY, rob): a `let xs:[]T=[..]; + // return xs;` returns a slice into this frame slot, freed on + // return = dangling — IDENTICAL to the named-array borrow and + // Hare-consistent (no escape analysis / GC / heap promotion; a + // local borrowed past its frame is a footgun, not promoted). + let cnt: i32 = 0; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + cnt = arrlittn.rhs.uval: i32; + }; + }; + let bsz: i32 = elemsizeofc(c, arrlittn) * cnt; + if (bsz < 1) { bsz = 1; }; + let scr: i32 = localalloc(c, "@slicescr", bsz, nil); + cgarrlitfillbp(c, arrlittn, base, scr); + emitline("\tLEAQ\t"); + emitoff(scr: i64); + emitline("(BP), AX\n"); } else { if (base != nil) { // #252: N_DOT `[N]T`-field base → field ADDRESS via // dotbaseaddr (LEAQ), not the auto-deref VALUE load cgexpr @@ -21473,7 +21580,7 @@ fn cgslice(c: *cgen, n: *node) void = { if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; - };};}; + };};};}; emitline("\tPUSHQ\tAX\n"); // lo (default 0) if (lo != nil) { cgexpr(c, lo); } @@ -21538,9 +21645,21 @@ fn cgslice(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$"); emitint(dotbu.alen: i64); emitline(", AX\n"); + } else { if (arrlittn != nil) { + // #31: default-hi for the arrlit base = its element count (the + // stashed [count]T tnode's .rhs intlit). + let hc: i64 = 0i64; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + hc = arrlittn.rhs.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(hc); + emitline(", AX\n"); } else { emitline("\tMOVQ\t$0, AX\n"); - };};};}; + };};};};}; emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); @@ -29389,6 +29508,238 @@ fn cgexprstmt(c: *cgen, n: *node) void = { return; }; +// cgarrlitfillbp — #31: fill the [count]T destination at BP-relative +// `off` from an N_ARRLIT, extracted from the cglet array-init path so +// the slice-borrow base materialisation (cgslice N_ARRLIT-base arm) +// reuses the IDENTICAL element-store sequence — the frame-order / +// store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T +// type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs, +// #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp. +fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { + let elemn: *node = arrtn.lhs; + let esz: i32 = 8; + let isstrel: bool = false; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + if (streq(elemn.str, "str")) { + esz = primtypesize("str"): i32; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + }; + // #270-1c: an AGGREGATE (struct/array/tuple) element of + // an array literal — the scalar per-element store below + // writes only the first 8 bytes (unpopulated tail). Fill + // each element slot from its literal (cgstructlitfillbp) + // or source ident (word-copy). esz is the element's + // natural size (cstage esub->size). + let esubti: *tinfo = nil; + if (elemn != nil) { esubti = elemn.type_: *tinfo; }; + for (esubti != nil && esubti.kind == tykind.TY_NAMED) { + esubti = esubti.under; + }; + let isagg: bool = esubti != nil + && (esubti.kind == tykind.TY_STRUCT + || esubti.kind == tykind.TY_ARRAY + || esubti.kind == tykind.TY_TUPLE); + if (isagg) { esz = esubti.size: i32; }; + // #20/#270 str-slice arm: a slice element (N_TSLICE) is + // a 24B {ptr,len,cap} header — it matches no prim/str/agg + // branch above, so esz stayed the 8 sentinel (wrong stride, + // the -96-vs-80 cs!=ww frame divergence) and the scalar + // store dropped .len/.cap. Size it from the stamped tinfo + // and route it through the 3-word header store below. + let isslicel: bool = esubti != nil + && esubti.kind == tykind.TY_SLICE; + if (isslicel) { esz = esubti.size: i32; }; + // #12: a tagged-union element. NOT folded into isagg — + // isagg's body word-copies/fatals and never boxes the + // tag+payload; route through the cgwidentaggedstore + // choke-point the N_LET tagged path (cgenstmt.ww:1627) + // uses. esz must come from the stamped slot size (#8-class + // trap, rule-13): the narrow override below only rescues + // 1/2/4, so a tagged 16/24B element keeps the wrong 8 + // sentinel stride without this. + let istaggedel: bool = esubti != nil + && esubti.kind == tykind.TY_TAGGED; + if (istaggedel) { esz = esubti.size: i32; }; + // #8: a named-narrow element (`[N]tk`, tk = enum i32) is + // neither a builtin prim (primsize=0 above, so esz stayed + // the 8 sentinel) nor an aggregate, so the scalar store kept + // an 8B stride/MOVQ and overran the stride-4 frame slot — + // smashing the saved BP / return addr (SEGFAULT). Mirror + // cstage's uniform lu->sub->size (cgen.c:6387) and the + // elemsizeofc read-side fix: take the stamped element tinfo's + // size for a narrow scalar (1/2/4). Wider non-prim elements + // (tagged/slice/str two-half) stay the documented follow-up + // at :1742-1744 — the single-MOVx store below is scalar-only. + if (!isstrel && !isagg && esz == 8 && esubti != nil) { + let es: i32 = esubti.size: i32; + if (es == 1 || es == 2 || es == 4) { esz = es; }; + }; + let mop: str = tnodestoreop(c, elemn, esz); + // float element → store FROM X0 (MOVSS/MOVSD): cgexpr + // leaves a float in X0 and for f32 the #104 CVTSD2SS + // narrowing only touches X0; the AX store (mop) would + // write the raw double low-bits, garbage for f32 (#122, + // mirrors cstage cgen.c:6889 arr-lit float store). + let isfloatel: bool = isfloattype(c, elemn); + let fmov: str = "MOVSD"; + if (isf32type(c, elemn)) { fmov = "MOVSS"; }; + let idx: i32 = 0; + let repeat: bool = false; + let e: *node = rhs.list; + for (e != nil) { + let isellip: bool = false; + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { + repeat = true; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + if (isagg) { + if (e.kind == nkind.N_STRUCTLIT) { + let esi: *structinfo = structlookupchain(c, elemn); + cgstructlitfillbp(c, esi, e, off + idx * esz); + } else { if (e.kind == nkind.N_IDENT) { + let sl: *local = localfindnode(c, e.str); + let soff: i32 = 0; + if (sl != nil) { soff = sl.off; }; + let kc: i32 = 0; + for (kc + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 8; + }; + if (kc + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 4; + }; + if (kc + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 2; + }; + if (kc + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 1; + }; + } else { + let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; + os.write(2, m1c.ptr, m1c.len: u64); + os.exit(1); + }; }; + } else { if (istaggedel) { + cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); + } else { + cgexpr(c, e); + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + }; }; + idx += 1; + e = e.next; + }; + }; + if (repeat && isagg) { + let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; + os.write(2, m1cr.ptr, m1cr.len: u64); + os.exit(1); + }; + // #12: `...` re-stores from AX, but cgwidentaggedstore consumed + // the node and trashed AX — the repeat-fill would write garbage. + // No consumer needs `[N]tagged=[x,...]`. + if (repeat && istaggedel) { + let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; + os.write(2, m12r.ptr, m12r.len: u64); + os.exit(1); + }; + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. + if (repeat) { + let total: i32 = idx; + if (arrtn != nil) { + if (arrtn.kind == nkind.N_TARRAY) { + if (arrtn.rhs != nil) { + if (arrtn.rhs.kind == nkind.N_INTLIT) { + total = arrtn.rhs.uval: i32; + }; + }; + }; + }; + for (idx < total) { + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + idx += 1; + }; + }; +}; + fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); @@ -29662,228 +30013,7 @@ fn cglet(c: *cgen, n: *node) void = { // composites generally. The str/slice element now stores all 3 // words; [N]tagged element arrays still hit the gap, task #12.) if (rhs.kind == nkind.N_ARRLIT) { - let elemn: *node = n.lhs.lhs; - let esz: i32 = 8; - let isstrel: bool = false; - if (elemn != nil) { - if (elemn.kind == nkind.N_TNAME) { - if (streq(elemn.str, "str")) { - esz = primtypesize("str"): i32; - isstrel = true; - } else { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; - }; - }; - }; - // #270-1c: an AGGREGATE (struct/array/tuple) element of - // an array literal — the scalar per-element store below - // writes only the first 8 bytes (unpopulated tail). Fill - // each element slot from its literal (cgstructlitfillbp) - // or source ident (word-copy). esz is the element's - // natural size (cstage esub->size). - let esubti: *tinfo = nil; - if (elemn != nil) { esubti = elemn.type_: *tinfo; }; - for (esubti != nil && esubti.kind == tykind.TY_NAMED) { - esubti = esubti.under; - }; - let isagg: bool = esubti != nil - && (esubti.kind == tykind.TY_STRUCT - || esubti.kind == tykind.TY_ARRAY - || esubti.kind == tykind.TY_TUPLE); - if (isagg) { esz = esubti.size: i32; }; - // #20/#270 str-slice arm: a slice element (N_TSLICE) is - // a 24B {ptr,len,cap} header — it matches no prim/str/agg - // branch above, so esz stayed the 8 sentinel (wrong stride, - // the -96-vs-80 cs!=ww frame divergence) and the scalar - // store dropped .len/.cap. Size it from the stamped tinfo - // and route it through the 3-word header store below. - let isslicel: bool = esubti != nil - && esubti.kind == tykind.TY_SLICE; - if (isslicel) { esz = esubti.size: i32; }; - // #12: a tagged-union element. NOT folded into isagg — - // isagg's body word-copies/fatals and never boxes the - // tag+payload; route through the cgwidentaggedstore - // choke-point the N_LET tagged path (cgenstmt.ww:1627) - // uses. esz must come from the stamped slot size (#8-class - // trap, rule-13): the narrow override below only rescues - // 1/2/4, so a tagged 16/24B element keeps the wrong 8 - // sentinel stride without this. - let istaggedel: bool = esubti != nil - && esubti.kind == tykind.TY_TAGGED; - if (istaggedel) { esz = esubti.size: i32; }; - // #8: a named-narrow element (`[N]tk`, tk = enum i32) is - // neither a builtin prim (primsize=0 above, so esz stayed - // the 8 sentinel) nor an aggregate, so the scalar store kept - // an 8B stride/MOVQ and overran the stride-4 frame slot — - // smashing the saved BP / return addr (SEGFAULT). Mirror - // cstage's uniform lu->sub->size (cgen.c:6387) and the - // elemsizeofc read-side fix: take the stamped element tinfo's - // size for a narrow scalar (1/2/4). Wider non-prim elements - // (tagged/slice/str two-half) stay the documented follow-up - // at :1742-1744 — the single-MOVx store below is scalar-only. - if (!isstrel && !isagg && esz == 8 && esubti != nil) { - let es: i32 = esubti.size: i32; - if (es == 1 || es == 2 || es == 4) { esz = es; }; - }; - let mop: str = tnodestoreop(c, elemn, esz); - // float element → store FROM X0 (MOVSS/MOVSD): cgexpr - // leaves a float in X0 and for f32 the #104 CVTSD2SS - // narrowing only touches X0; the AX store (mop) would - // write the raw double low-bits, garbage for f32 (#122, - // mirrors cstage cgen.c:6889 arr-lit float store). - let isfloatel: bool = isfloattype(c, elemn); - let fmov: str = "MOVSD"; - if (isf32type(c, elemn)) { fmov = "MOVSS"; }; - let idx: i32 = 0; - let repeat: bool = false; - let e: *node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == nkind.N_FIELD) { - if (streq(e.str, "...")) { - repeat = true; - isellip = true; - }; - }; - if (isellip) { - e = nil; - } else { - if (isagg) { - if (e.kind == nkind.N_STRUCTLIT) { - let esi: *structinfo = structlookupchain(c, elemn); - cgstructlitfillbp(c, esi, e, off + idx * esz); - } else { if (e.kind == nkind.N_IDENT) { - let sl: *local = localfindnode(c, e.str); - let soff: i32 = 0; - if (sl != nil) { soff = sl.off; }; - let kc: i32 = 0; - for (kc + 8 <= esz) { - emitline("\tMOVQ\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 8; - }; - if (kc + 4 <= esz) { - emitline("\tMOVL\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 4; - }; - if (kc + 2 <= esz) { - emitline("\tMOVW\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 2; - }; - if (kc + 1 <= esz) { - emitline("\tMOVB\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 1; - }; - } else { - let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; - os.write(2, m1c.ptr, m1c.len: u64); - os.exit(1); - }; }; - } else { if (istaggedel) { - cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); - } else { - cgexpr(c, e); - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - }; }; - idx += 1; - e = e.next; - }; - }; - if (repeat && isagg) { - let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; - os.write(2, m1cr.ptr, m1cr.len: u64); - os.exit(1); - }; - // #12: `...` re-stores from AX, but cgwidentaggedstore consumed - // the node and trashed AX — the repeat-fill would write garbage. - // No consumer needs `[N]tagged=[x,...]`. - if (repeat && istaggedel) { - let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; - os.write(2, m12r.ptr, m12r.len: u64); - os.exit(1); - }; - // AX (and BX for str) still holds the last stored value; - // fill remaining slots up to the declared length with it. - if (repeat) { - let total: i32 = idx; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { - if (n.lhs.rhs != nil) { - if (n.lhs.rhs.kind == nkind.N_INTLIT) { - total = n.lhs.rhs.uval: i32; - }; - }; - }; - }; - for (idx < total) { - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - idx += 1; - }; - }; + cgarrlitfillbp(c, n.lhs, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 556adaef..ae9e92ca 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1660,6 +1660,17 @@ fn cgslice(c: *cgen, n: *node) void = { dotbu = dotbu.under; }; };}; + // #31: an N_ARRLIT base (the desugared one-step `let xs:[]T=[..]` + // borrow — the ONLY context that reaches here; call-arg/return/assign + // loud-reject at the checker, #33) has no storage. Its [count]T type + // NODE is stashed on base.lhs by checkletassign's #25 re-stamp; size / + // count come NODE-wise (elemsizeofc / .rhs intlit), because wwstage + // narrow-primitive tinfos are unsized (#8). Cstage twin reads base->type + // (its Type IS sized). + let arrlittn: *node = nil; + if (base != nil) { if (base.kind == nkind.N_ARRLIT) { + arrlittn = base.lhs; + };}; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom) or an N_DOT array/slice-field base (#252: scale by // the field's element width, not esz=1 — silently wrong for non-u8). @@ -1671,7 +1682,9 @@ fn cgslice(c: *cgen, n: *node) void = { esz = elemsizeofc(c, globaltn); } else { if (dotbu != nil && dotbu.sub != nil) { esz = dotbu.sub.size: i32; - };};}; + } else { if (arrlittn != nil) { + esz = elemsizeofc(c, arrlittn); + };};};}; // base address if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -1701,6 +1714,32 @@ fn cgslice(c: *cgen, n: *node) void = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (base != nil && base.kind == nkind.N_ARRLIT + && arrlittn != nil) { + // #31: materialise the array literal into a FRESH per-borrow + // @slicescr stack slot (distinct slot per borrow — a borrow's + // backing must outlive the lowering, so it can't share a cached + // slot; localalloc is always-fresh, mirror of cstage local_alloc), + // fill it via the shared element-fill, then LEAQ the slot as base. + // Size/count NODE-wise off the stashed [count]T tnode (#8: tinfo + // primitive sizes are 0). Escape (WHY, rob): a `let xs:[]T=[..]; + // return xs;` returns a slice into this frame slot, freed on + // return = dangling — IDENTICAL to the named-array borrow and + // Hare-consistent (no escape analysis / GC / heap promotion; a + // local borrowed past its frame is a footgun, not promoted). + let cnt: i32 = 0; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + cnt = arrlittn.rhs.uval: i32; + }; + }; + let bsz: i32 = elemsizeofc(c, arrlittn) * cnt; + if (bsz < 1) { bsz = 1; }; + let scr: i32 = localalloc(c, "@slicescr", bsz, nil); + cgarrlitfillbp(c, arrlittn, base, scr); + emitline("\tLEAQ\t"); + emitoff(scr: i64); + emitline("(BP), AX\n"); } else { if (base != nil) { // #252: N_DOT `[N]T`-field base → field ADDRESS via // dotbaseaddr (LEAQ), not the auto-deref VALUE load cgexpr @@ -1708,7 +1747,7 @@ fn cgslice(c: *cgen, n: *node) void = { if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; - };};}; + };};};}; emitline("\tPUSHQ\tAX\n"); // lo (default 0) if (lo != nil) { cgexpr(c, lo); } @@ -1773,9 +1812,21 @@ fn cgslice(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$"); emitint(dotbu.alen: i64); emitline(", AX\n"); + } else { if (arrlittn != nil) { + // #31: default-hi for the arrlit base = its element count (the + // stashed [count]T tnode's .rhs intlit). + let hc: i64 = 0i64; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + hc = arrlittn.rhs.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(hc); + emitline(", AX\n"); } else { emitline("\tMOVQ\t$0, AX\n"); - };};};}; + };};};};}; emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 732018c4..b756e6dd 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -1471,6 +1471,238 @@ fn cgexprstmt(c: *cgen, n: *node) void = { return; }; +// cgarrlitfillbp — #31: fill the [count]T destination at BP-relative +// `off` from an N_ARRLIT, extracted from the cglet array-init path so +// the slice-borrow base materialisation (cgslice N_ARRLIT-base arm) +// reuses the IDENTICAL element-store sequence — the frame-order / +// store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T +// type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs, +// #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp. +fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { + let elemn: *node = arrtn.lhs; + let esz: i32 = 8; + let isstrel: bool = false; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + if (streq(elemn.str, "str")) { + esz = primtypesize("str"): i32; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + }; + // #270-1c: an AGGREGATE (struct/array/tuple) element of + // an array literal — the scalar per-element store below + // writes only the first 8 bytes (unpopulated tail). Fill + // each element slot from its literal (cgstructlitfillbp) + // or source ident (word-copy). esz is the element's + // natural size (cstage esub->size). + let esubti: *tinfo = nil; + if (elemn != nil) { esubti = elemn.type_: *tinfo; }; + for (esubti != nil && esubti.kind == tykind.TY_NAMED) { + esubti = esubti.under; + }; + let isagg: bool = esubti != nil + && (esubti.kind == tykind.TY_STRUCT + || esubti.kind == tykind.TY_ARRAY + || esubti.kind == tykind.TY_TUPLE); + if (isagg) { esz = esubti.size: i32; }; + // #20/#270 str-slice arm: a slice element (N_TSLICE) is + // a 24B {ptr,len,cap} header — it matches no prim/str/agg + // branch above, so esz stayed the 8 sentinel (wrong stride, + // the -96-vs-80 cs!=ww frame divergence) and the scalar + // store dropped .len/.cap. Size it from the stamped tinfo + // and route it through the 3-word header store below. + let isslicel: bool = esubti != nil + && esubti.kind == tykind.TY_SLICE; + if (isslicel) { esz = esubti.size: i32; }; + // #12: a tagged-union element. NOT folded into isagg — + // isagg's body word-copies/fatals and never boxes the + // tag+payload; route through the cgwidentaggedstore + // choke-point the N_LET tagged path (cgenstmt.ww:1627) + // uses. esz must come from the stamped slot size (#8-class + // trap, rule-13): the narrow override below only rescues + // 1/2/4, so a tagged 16/24B element keeps the wrong 8 + // sentinel stride without this. + let istaggedel: bool = esubti != nil + && esubti.kind == tykind.TY_TAGGED; + if (istaggedel) { esz = esubti.size: i32; }; + // #8: a named-narrow element (`[N]tk`, tk = enum i32) is + // neither a builtin prim (primsize=0 above, so esz stayed + // the 8 sentinel) nor an aggregate, so the scalar store kept + // an 8B stride/MOVQ and overran the stride-4 frame slot — + // smashing the saved BP / return addr (SEGFAULT). Mirror + // cstage's uniform lu->sub->size (cgen.c:6387) and the + // elemsizeofc read-side fix: take the stamped element tinfo's + // size for a narrow scalar (1/2/4). Wider non-prim elements + // (tagged/slice/str two-half) stay the documented follow-up + // at :1742-1744 — the single-MOVx store below is scalar-only. + if (!isstrel && !isagg && esz == 8 && esubti != nil) { + let es: i32 = esubti.size: i32; + if (es == 1 || es == 2 || es == 4) { esz = es; }; + }; + let mop: str = tnodestoreop(c, elemn, esz); + // float element → store FROM X0 (MOVSS/MOVSD): cgexpr + // leaves a float in X0 and for f32 the #104 CVTSD2SS + // narrowing only touches X0; the AX store (mop) would + // write the raw double low-bits, garbage for f32 (#122, + // mirrors cstage cgen.c:6889 arr-lit float store). + let isfloatel: bool = isfloattype(c, elemn); + let fmov: str = "MOVSD"; + if (isf32type(c, elemn)) { fmov = "MOVSS"; }; + let idx: i32 = 0; + let repeat: bool = false; + let e: *node = rhs.list; + for (e != nil) { + let isellip: bool = false; + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { + repeat = true; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + if (isagg) { + if (e.kind == nkind.N_STRUCTLIT) { + let esi: *structinfo = structlookupchain(c, elemn); + cgstructlitfillbp(c, esi, e, off + idx * esz); + } else { if (e.kind == nkind.N_IDENT) { + let sl: *local = localfindnode(c, e.str); + let soff: i32 = 0; + if (sl != nil) { soff = sl.off; }; + let kc: i32 = 0; + for (kc + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 8; + }; + if (kc + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 4; + }; + if (kc + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 2; + }; + if (kc + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 1; + }; + } else { + let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; + os.write(2, m1c.ptr, m1c.len: u64); + os.exit(1); + }; }; + } else { if (istaggedel) { + cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); + } else { + cgexpr(c, e); + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + }; }; + idx += 1; + e = e.next; + }; + }; + if (repeat && isagg) { + let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; + os.write(2, m1cr.ptr, m1cr.len: u64); + os.exit(1); + }; + // #12: `...` re-stores from AX, but cgwidentaggedstore consumed + // the node and trashed AX — the repeat-fill would write garbage. + // No consumer needs `[N]tagged=[x,...]`. + if (repeat && istaggedel) { + let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; + os.write(2, m12r.ptr, m12r.len: u64); + os.exit(1); + }; + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. + if (repeat) { + let total: i32 = idx; + if (arrtn != nil) { + if (arrtn.kind == nkind.N_TARRAY) { + if (arrtn.rhs != nil) { + if (arrtn.rhs.kind == nkind.N_INTLIT) { + total = arrtn.rhs.uval: i32; + }; + }; + }; + }; + for (idx < total) { + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + idx += 1; + }; + }; +}; + fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); @@ -1744,228 +1976,7 @@ fn cglet(c: *cgen, n: *node) void = { // composites generally. The str/slice element now stores all 3 // words; [N]tagged element arrays still hit the gap, task #12.) if (rhs.kind == nkind.N_ARRLIT) { - let elemn: *node = n.lhs.lhs; - let esz: i32 = 8; - let isstrel: bool = false; - if (elemn != nil) { - if (elemn.kind == nkind.N_TNAME) { - if (streq(elemn.str, "str")) { - esz = primtypesize("str"): i32; - isstrel = true; - } else { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; - }; - }; - }; - // #270-1c: an AGGREGATE (struct/array/tuple) element of - // an array literal — the scalar per-element store below - // writes only the first 8 bytes (unpopulated tail). Fill - // each element slot from its literal (cgstructlitfillbp) - // or source ident (word-copy). esz is the element's - // natural size (cstage esub->size). - let esubti: *tinfo = nil; - if (elemn != nil) { esubti = elemn.type_: *tinfo; }; - for (esubti != nil && esubti.kind == tykind.TY_NAMED) { - esubti = esubti.under; - }; - let isagg: bool = esubti != nil - && (esubti.kind == tykind.TY_STRUCT - || esubti.kind == tykind.TY_ARRAY - || esubti.kind == tykind.TY_TUPLE); - if (isagg) { esz = esubti.size: i32; }; - // #20/#270 str-slice arm: a slice element (N_TSLICE) is - // a 24B {ptr,len,cap} header — it matches no prim/str/agg - // branch above, so esz stayed the 8 sentinel (wrong stride, - // the -96-vs-80 cs!=ww frame divergence) and the scalar - // store dropped .len/.cap. Size it from the stamped tinfo - // and route it through the 3-word header store below. - let isslicel: bool = esubti != nil - && esubti.kind == tykind.TY_SLICE; - if (isslicel) { esz = esubti.size: i32; }; - // #12: a tagged-union element. NOT folded into isagg — - // isagg's body word-copies/fatals and never boxes the - // tag+payload; route through the cgwidentaggedstore - // choke-point the N_LET tagged path (cgenstmt.ww:1627) - // uses. esz must come from the stamped slot size (#8-class - // trap, rule-13): the narrow override below only rescues - // 1/2/4, so a tagged 16/24B element keeps the wrong 8 - // sentinel stride without this. - let istaggedel: bool = esubti != nil - && esubti.kind == tykind.TY_TAGGED; - if (istaggedel) { esz = esubti.size: i32; }; - // #8: a named-narrow element (`[N]tk`, tk = enum i32) is - // neither a builtin prim (primsize=0 above, so esz stayed - // the 8 sentinel) nor an aggregate, so the scalar store kept - // an 8B stride/MOVQ and overran the stride-4 frame slot — - // smashing the saved BP / return addr (SEGFAULT). Mirror - // cstage's uniform lu->sub->size (cgen.c:6387) and the - // elemsizeofc read-side fix: take the stamped element tinfo's - // size for a narrow scalar (1/2/4). Wider non-prim elements - // (tagged/slice/str two-half) stay the documented follow-up - // at :1742-1744 — the single-MOVx store below is scalar-only. - if (!isstrel && !isagg && esz == 8 && esubti != nil) { - let es: i32 = esubti.size: i32; - if (es == 1 || es == 2 || es == 4) { esz = es; }; - }; - let mop: str = tnodestoreop(c, elemn, esz); - // float element → store FROM X0 (MOVSS/MOVSD): cgexpr - // leaves a float in X0 and for f32 the #104 CVTSD2SS - // narrowing only touches X0; the AX store (mop) would - // write the raw double low-bits, garbage for f32 (#122, - // mirrors cstage cgen.c:6889 arr-lit float store). - let isfloatel: bool = isfloattype(c, elemn); - let fmov: str = "MOVSD"; - if (isf32type(c, elemn)) { fmov = "MOVSS"; }; - let idx: i32 = 0; - let repeat: bool = false; - let e: *node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == nkind.N_FIELD) { - if (streq(e.str, "...")) { - repeat = true; - isellip = true; - }; - }; - if (isellip) { - e = nil; - } else { - if (isagg) { - if (e.kind == nkind.N_STRUCTLIT) { - let esi: *structinfo = structlookupchain(c, elemn); - cgstructlitfillbp(c, esi, e, off + idx * esz); - } else { if (e.kind == nkind.N_IDENT) { - let sl: *local = localfindnode(c, e.str); - let soff: i32 = 0; - if (sl != nil) { soff = sl.off; }; - let kc: i32 = 0; - for (kc + 8 <= esz) { - emitline("\tMOVQ\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 8; - }; - if (kc + 4 <= esz) { - emitline("\tMOVL\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 4; - }; - if (kc + 2 <= esz) { - emitline("\tMOVW\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 2; - }; - if (kc + 1 <= esz) { - emitline("\tMOVB\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 1; - }; - } else { - let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; - os.write(2, m1c.ptr, m1c.len: u64); - os.exit(1); - }; }; - } else { if (istaggedel) { - cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); - } else { - cgexpr(c, e); - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - }; }; - idx += 1; - e = e.next; - }; - }; - if (repeat && isagg) { - let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; - os.write(2, m1cr.ptr, m1cr.len: u64); - os.exit(1); - }; - // #12: `...` re-stores from AX, but cgwidentaggedstore consumed - // the node and trashed AX — the repeat-fill would write garbage. - // No consumer needs `[N]tagged=[x,...]`. - if (repeat && istaggedel) { - let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; - os.write(2, m12r.ptr, m12r.len: u64); - os.exit(1); - }; - // AX (and BX for str) still holds the last stored value; - // fill remaining slots up to the declared length with it. - if (repeat) { - let total: i32 = idx; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { - if (n.lhs.rhs != nil) { - if (n.lhs.rhs.kind == nkind.N_INTLIT) { - total = n.lhs.rhs.uval: i32; - }; - }; - }; - }; - for (idx < total) { - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - idx += 1; - }; - }; + cgarrlitfillbp(c, n.lhs, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index dd1080e4..11d201ea 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -290,6 +290,8 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // the cgindex idiom) or an N_DOT array/slice-field base (#257: // scale by the field's element width, not esz=1 -> silently // wrong for non-u8). Other non-ident bases stay esz=1. + // (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects + // at the checker, supported only at a `let`; #33.) let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 6c536446..870ac4e1 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3845,6 +3845,25 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // unchanged when the shape doesn't match, else a fresh N_SLICE whose base // is `val` (which keeps its stamped array type_). The original sibling // link transfers to the N_SLICE so a desugared call-arg keeps its place. +// rejectarrlitborrow — #31/#33 twin of cstage reject_arrlit_borrow. The +// array-literal → slice borrow is supported only at a `let` init (where +// checkletassign re-stamps + the cgslice N_ARRLIT-base arm spills the +// literal to a per-borrow backing slot). In call-arg / return / assign +// position there is no addressable backing — loud-reject so the gap is a +// compile error, not a dangling-ptr miscompile. Both stages reject here +// (rule-10, byte-id-trivial: no asm). Full non-let support is #33. +fn rejectarrlitborrow(c: *checker, dsttn: *node, val: *node) bool = { + if (val == nil) { return false; }; + if (val.kind != nkind.N_ARRLIT) { return false; }; + let du: *node = resolvealias(c, unwrapbang(dsttn)); + if (du == nil) { return false; }; + if (du.kind != nkind.N_TSLICE) { return false; }; + let m: str = "array literal cannot borrow as a slice here; bind it to a `let` first\n"; + cerr(m); + c.errs += 1; + return true; +}; + fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node = { if (dsttn == nil) { return val; }; if (srctn == nil) { return val; }; @@ -3936,10 +3955,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { }; }; }; }; }; - let rep: *node = desugararrayslice(c, param.lhs, atype, a); - if (rep != a) { - if (prev == nil) { n.list = rep; } else { prev.next = rep; }; - a = rep; + // #31/#33: bare array-literal arg has no backing + // — loud-reject (supported only at a `let`). + if (!rejectarrlitborrow(c, param.lhs, a)) { + let rep: *node = desugararrayslice(c, param.lhs, atype, a); + if (rep != a) { + if (prev == nil) { n.list = rep; } else { prev.next = rep; }; + a = rep; + }; }; }; if (param.op != tkind.TK_ELLIPSIS) { param = param.next; }; @@ -3961,7 +3984,11 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); - n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + // #31/#33: bare array-literal rhs has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, ltn, n.rhs)) { + n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + }; }; // inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The @@ -4122,6 +4149,41 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #25/#31: an array literal initialising a SLICE local. Re-stamp the + // literal as [count]T (the slice element) so the #258 borrow's exact- + // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the + // declared element width. Run the same per-element coercion + range- + // check the array path runs (checkarrlitfits against a synthesized + // [count]T), then drive isassignable + the borrow off [count]T. Twin of + // cstage arrlit_init_fits' slice arm. Local-only (c.cur != c.top): the + // borrow runs at runtime; module-level slice-from-arrlit stays #32. + if (c.cur != c.top && n.lhs.kind == nkind.N_TSLICE + && n.rhs.kind == nkind.N_ARRLIT) { + let cnt: u64 = 0u64; + let e0: *node = n.rhs.list; + for (e0 != nil) { + let skip: bool = false; + if (e0.kind == nkind.N_FIELD) { + if (streq(e0.str, "...")) { skip = true; }; + }; + if (!skip) { cnt += 1u64; }; + e0 = e0.next; + }; + let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); + cn.uval = cnt; + let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); + arr.lhs = n.lhs.lhs; // declared slice element type + arr.rhs = cn; + checkarrlitfits(c, arr, n.rhs); + n.rhs.type_ = tinfofornode(c, arr): *void; + // #31: stash the [count]T tnode on the arrlit (arrlit.lhs is free + // — the parser sets only .list) so the cgslice N_ARRLIT-base arm + // can size the backing NODE-wise via elemsizeofc(base.lhs). wwstage + // narrow-primitive tinfos are unsized (i32/u8 .size==0, #8), so the + // element width must come from the type NODE, not the tinfo. + n.rhs.lhs = arr; + src = arr; + }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -4164,7 +4226,11 @@ fn checkretassign(c: *checker, n: *node) void = { if (!conf) { return; }; if (!ok) { errnotassign(c, c.fnret, src, "return"); }; // #258: `return arr` borrows the array as a full slice. - n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + // #31/#33: bare array-literal has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, c.fnret, n.lhs)) { + n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + }; }; // ---- is / as validity ------------------------------------------------ diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 908fcce0..d4941372 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14212,6 +14212,25 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // unchanged when the shape doesn't match, else a fresh N_SLICE whose base // is `val` (which keeps its stamped array type_). The original sibling // link transfers to the N_SLICE so a desugared call-arg keeps its place. +// rejectarrlitborrow — #31/#33 twin of cstage reject_arrlit_borrow. The +// array-literal → slice borrow is supported only at a `let` init (where +// checkletassign re-stamps + the cgslice N_ARRLIT-base arm spills the +// literal to a per-borrow backing slot). In call-arg / return / assign +// position there is no addressable backing — loud-reject so the gap is a +// compile error, not a dangling-ptr miscompile. Both stages reject here +// (rule-10, byte-id-trivial: no asm). Full non-let support is #33. +fn rejectarrlitborrow(c: *checker, dsttn: *node, val: *node) bool = { + if (val == nil) { return false; }; + if (val.kind != nkind.N_ARRLIT) { return false; }; + let du: *node = resolvealias(c, unwrapbang(dsttn)); + if (du == nil) { return false; }; + if (du.kind != nkind.N_TSLICE) { return false; }; + let m: str = "array literal cannot borrow as a slice here; bind it to a `let` first\n"; + cerr(m); + c.errs += 1; + return true; +}; + fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node = { if (dsttn == nil) { return val; }; if (srctn == nil) { return val; }; @@ -14303,10 +14322,14 @@ fn desugarcallargs(c: *checker, n: *node) void = { }; }; }; }; }; - let rep: *node = desugararrayslice(c, param.lhs, atype, a); - if (rep != a) { - if (prev == nil) { n.list = rep; } else { prev.next = rep; }; - a = rep; + // #31/#33: bare array-literal arg has no backing + // — loud-reject (supported only at a `let`). + if (!rejectarrlitborrow(c, param.lhs, a)) { + let rep: *node = desugararrayslice(c, param.lhs, atype, a); + if (rep != a) { + if (prev == nil) { n.list = rep; } else { prev.next = rep; }; + a = rep; + }; }; }; if (param.op != tkind.TK_ELLIPSIS) { param = param.next; }; @@ -14328,7 +14351,11 @@ fn checkassign(c: *checker, n: *node) void = { if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); - n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + // #31/#33: bare array-literal rhs has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, ltn, n.rhs)) { + n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); + }; }; // inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The @@ -14489,6 +14516,41 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #25/#31: an array literal initialising a SLICE local. Re-stamp the + // literal as [count]T (the slice element) so the #258 borrow's exact- + // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the + // declared element width. Run the same per-element coercion + range- + // check the array path runs (checkarrlitfits against a synthesized + // [count]T), then drive isassignable + the borrow off [count]T. Twin of + // cstage arrlit_init_fits' slice arm. Local-only (c.cur != c.top): the + // borrow runs at runtime; module-level slice-from-arrlit stays #32. + if (c.cur != c.top && n.lhs.kind == nkind.N_TSLICE + && n.rhs.kind == nkind.N_ARRLIT) { + let cnt: u64 = 0u64; + let e0: *node = n.rhs.list; + for (e0 != nil) { + let skip: bool = false; + if (e0.kind == nkind.N_FIELD) { + if (streq(e0.str, "...")) { skip = true; }; + }; + if (!skip) { cnt += 1u64; }; + e0 = e0.next; + }; + let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); + cn.uval = cnt; + let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); + arr.lhs = n.lhs.lhs; // declared slice element type + arr.rhs = cn; + checkarrlitfits(c, arr, n.rhs); + n.rhs.type_ = tinfofornode(c, arr): *void; + // #31: stash the [count]T tnode on the arrlit (arrlit.lhs is free + // — the parser sets only .list) so the cgslice N_ARRLIT-base arm + // can size the backing NODE-wise via elemsizeofc(base.lhs). wwstage + // narrow-primitive tinfos are unsized (i32/u8 .size==0, #8), so the + // element width must come from the type NODE, not the tinfo. + n.rhs.lhs = arr; + src = arr; + }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. @@ -14531,7 +14593,11 @@ fn checkretassign(c: *checker, n: *node) void = { if (!conf) { return; }; if (!ok) { errnotassign(c, c.fnret, src, "return"); }; // #258: `return arr` borrows the array as a full slice. - n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + // #31/#33: bare array-literal has no backing — loud-reject + // (supported only at a `let`). + if (!rejectarrlitborrow(c, c.fnret, n.lhs)) { + n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); + }; }; // ---- is / as validity ------------------------------------------------ @@ -15956,6 +16022,8 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // the cgindex idiom) or an N_DOT array/slice-field base (#257: // scale by the field's element width, not esz=1 -> silently // wrong for non-u8). Other non-ident bases stay esz=1. + // (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects + // at the checker, supported only at a `let`; #33.) let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); @@ -21425,6 +21493,17 @@ fn cgslice(c: *cgen, n: *node) void = { dotbu = dotbu.under; }; };}; + // #31: an N_ARRLIT base (the desugared one-step `let xs:[]T=[..]` + // borrow — the ONLY context that reaches here; call-arg/return/assign + // loud-reject at the checker, #33) has no storage. Its [count]T type + // NODE is stashed on base.lhs by checkletassign's #25 re-stamp; size / + // count come NODE-wise (elemsizeofc / .rhs intlit), because wwstage + // narrow-primitive tinfos are unsized (#8). Cstage twin reads base->type + // (its Type IS sized). + let arrlittn: *node = nil; + if (base != nil) { if (base.kind == nkind.N_ARRLIT) { + arrlittn = base.lhs; + };}; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom) or an N_DOT array/slice-field base (#252: scale by // the field's element width, not esz=1 — silently wrong for non-u8). @@ -21436,7 +21515,9 @@ fn cgslice(c: *cgen, n: *node) void = { esz = elemsizeofc(c, globaltn); } else { if (dotbu != nil && dotbu.sub != nil) { esz = dotbu.sub.size: i32; - };};}; + } else { if (arrlittn != nil) { + esz = elemsizeofc(c, arrlittn); + };};};}; // base address if (baselocal != nil) { let tn: *node = baselocal.tnode; @@ -21466,6 +21547,32 @@ fn cgslice(c: *cgen, n: *node) void = { emitsymname(c, globalname); emitline("(SB), AX\n"); }; + } else { if (base != nil && base.kind == nkind.N_ARRLIT + && arrlittn != nil) { + // #31: materialise the array literal into a FRESH per-borrow + // @slicescr stack slot (distinct slot per borrow — a borrow's + // backing must outlive the lowering, so it can't share a cached + // slot; localalloc is always-fresh, mirror of cstage local_alloc), + // fill it via the shared element-fill, then LEAQ the slot as base. + // Size/count NODE-wise off the stashed [count]T tnode (#8: tinfo + // primitive sizes are 0). Escape (WHY, rob): a `let xs:[]T=[..]; + // return xs;` returns a slice into this frame slot, freed on + // return = dangling — IDENTICAL to the named-array borrow and + // Hare-consistent (no escape analysis / GC / heap promotion; a + // local borrowed past its frame is a footgun, not promoted). + let cnt: i32 = 0; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + cnt = arrlittn.rhs.uval: i32; + }; + }; + let bsz: i32 = elemsizeofc(c, arrlittn) * cnt; + if (bsz < 1) { bsz = 1; }; + let scr: i32 = localalloc(c, "@slicescr", bsz, nil); + cgarrlitfillbp(c, arrlittn, base, scr); + emitline("\tLEAQ\t"); + emitoff(scr: i64); + emitline("(BP), AX\n"); } else { if (base != nil) { // #252: N_DOT `[N]T`-field base → field ADDRESS via // dotbaseaddr (LEAQ), not the auto-deref VALUE load cgexpr @@ -21473,7 +21580,7 @@ fn cgslice(c: *cgen, n: *node) void = { if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; - };};}; + };};};}; emitline("\tPUSHQ\tAX\n"); // lo (default 0) if (lo != nil) { cgexpr(c, lo); } @@ -21538,9 +21645,21 @@ fn cgslice(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$"); emitint(dotbu.alen: i64); emitline(", AX\n"); + } else { if (arrlittn != nil) { + // #31: default-hi for the arrlit base = its element count (the + // stashed [count]T tnode's .rhs intlit). + let hc: i64 = 0i64; + if (arrlittn.rhs != nil) { + if (arrlittn.rhs.kind == nkind.N_INTLIT) { + hc = arrlittn.rhs.uval: i64; + }; + }; + emitline("\tMOVQ\t$"); + emitint(hc); + emitline(", AX\n"); } else { emitline("\tMOVQ\t$0, AX\n"); - };};};}; + };};};};}; emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); @@ -29389,6 +29508,238 @@ fn cgexprstmt(c: *cgen, n: *node) void = { return; }; +// cgarrlitfillbp — #31: fill the [count]T destination at BP-relative +// `off` from an N_ARRLIT, extracted from the cglet array-init path so +// the slice-borrow base materialisation (cgslice N_ARRLIT-base arm) +// reuses the IDENTICAL element-store sequence — the frame-order / +// store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T +// type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs, +// #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp. +fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { + let elemn: *node = arrtn.lhs; + let esz: i32 = 8; + let isstrel: bool = false; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + if (streq(elemn.str, "str")) { + esz = primtypesize("str"): i32; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + }; + // #270-1c: an AGGREGATE (struct/array/tuple) element of + // an array literal — the scalar per-element store below + // writes only the first 8 bytes (unpopulated tail). Fill + // each element slot from its literal (cgstructlitfillbp) + // or source ident (word-copy). esz is the element's + // natural size (cstage esub->size). + let esubti: *tinfo = nil; + if (elemn != nil) { esubti = elemn.type_: *tinfo; }; + for (esubti != nil && esubti.kind == tykind.TY_NAMED) { + esubti = esubti.under; + }; + let isagg: bool = esubti != nil + && (esubti.kind == tykind.TY_STRUCT + || esubti.kind == tykind.TY_ARRAY + || esubti.kind == tykind.TY_TUPLE); + if (isagg) { esz = esubti.size: i32; }; + // #20/#270 str-slice arm: a slice element (N_TSLICE) is + // a 24B {ptr,len,cap} header — it matches no prim/str/agg + // branch above, so esz stayed the 8 sentinel (wrong stride, + // the -96-vs-80 cs!=ww frame divergence) and the scalar + // store dropped .len/.cap. Size it from the stamped tinfo + // and route it through the 3-word header store below. + let isslicel: bool = esubti != nil + && esubti.kind == tykind.TY_SLICE; + if (isslicel) { esz = esubti.size: i32; }; + // #12: a tagged-union element. NOT folded into isagg — + // isagg's body word-copies/fatals and never boxes the + // tag+payload; route through the cgwidentaggedstore + // choke-point the N_LET tagged path (cgenstmt.ww:1627) + // uses. esz must come from the stamped slot size (#8-class + // trap, rule-13): the narrow override below only rescues + // 1/2/4, so a tagged 16/24B element keeps the wrong 8 + // sentinel stride without this. + let istaggedel: bool = esubti != nil + && esubti.kind == tykind.TY_TAGGED; + if (istaggedel) { esz = esubti.size: i32; }; + // #8: a named-narrow element (`[N]tk`, tk = enum i32) is + // neither a builtin prim (primsize=0 above, so esz stayed + // the 8 sentinel) nor an aggregate, so the scalar store kept + // an 8B stride/MOVQ and overran the stride-4 frame slot — + // smashing the saved BP / return addr (SEGFAULT). Mirror + // cstage's uniform lu->sub->size (cgen.c:6387) and the + // elemsizeofc read-side fix: take the stamped element tinfo's + // size for a narrow scalar (1/2/4). Wider non-prim elements + // (tagged/slice/str two-half) stay the documented follow-up + // at :1742-1744 — the single-MOVx store below is scalar-only. + if (!isstrel && !isagg && esz == 8 && esubti != nil) { + let es: i32 = esubti.size: i32; + if (es == 1 || es == 2 || es == 4) { esz = es; }; + }; + let mop: str = tnodestoreop(c, elemn, esz); + // float element → store FROM X0 (MOVSS/MOVSD): cgexpr + // leaves a float in X0 and for f32 the #104 CVTSD2SS + // narrowing only touches X0; the AX store (mop) would + // write the raw double low-bits, garbage for f32 (#122, + // mirrors cstage cgen.c:6889 arr-lit float store). + let isfloatel: bool = isfloattype(c, elemn); + let fmov: str = "MOVSD"; + if (isf32type(c, elemn)) { fmov = "MOVSS"; }; + let idx: i32 = 0; + let repeat: bool = false; + let e: *node = rhs.list; + for (e != nil) { + let isellip: bool = false; + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { + repeat = true; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + if (isagg) { + if (e.kind == nkind.N_STRUCTLIT) { + let esi: *structinfo = structlookupchain(c, elemn); + cgstructlitfillbp(c, esi, e, off + idx * esz); + } else { if (e.kind == nkind.N_IDENT) { + let sl: *local = localfindnode(c, e.str); + let soff: i32 = 0; + if (sl != nil) { soff = sl.off; }; + let kc: i32 = 0; + for (kc + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 8; + }; + if (kc + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 4; + }; + if (kc + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 2; + }; + if (kc + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((soff + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff((off + idx * esz + kc): i64); + emitline("(BP)\n"); + kc += 1; + }; + } else { + let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; + os.write(2, m1c.ptr, m1c.len: u64); + os.exit(1); + }; }; + } else { if (istaggedel) { + cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); + } else { + cgexpr(c, e); + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + }; }; + idx += 1; + e = e.next; + }; + }; + if (repeat && isagg) { + let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; + os.write(2, m1cr.ptr, m1cr.len: u64); + os.exit(1); + }; + // #12: `...` re-stores from AX, but cgwidentaggedstore consumed + // the node and trashed AX — the repeat-fill would write garbage. + // No consumer needs `[N]tagged=[x,...]`. + if (repeat && istaggedel) { + let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; + os.write(2, m12r.ptr, m12r.len: u64); + os.exit(1); + }; + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. + if (repeat) { + let total: i32 = idx; + if (arrtn != nil) { + if (arrtn.kind == nkind.N_TARRAY) { + if (arrtn.rhs != nil) { + if (arrtn.rhs.kind == nkind.N_INTLIT) { + total = arrtn.rhs.uval: i32; + }; + }; + }; + }; + for (idx < total) { + if (isstrel || isslicel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tCX, "); + emitoff((off + idx * esz + 16): i64); + emitline("(BP)\n"); + } else { if (isfloatel) { + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; }; + idx += 1; + }; + }; +}; + fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); @@ -29662,228 +30013,7 @@ fn cglet(c: *cgen, n: *node) void = { // composites generally. The str/slice element now stores all 3 // words; [N]tagged element arrays still hit the gap, task #12.) if (rhs.kind == nkind.N_ARRLIT) { - let elemn: *node = n.lhs.lhs; - let esz: i32 = 8; - let isstrel: bool = false; - if (elemn != nil) { - if (elemn.kind == nkind.N_TNAME) { - if (streq(elemn.str, "str")) { - esz = primtypesize("str"): i32; - isstrel = true; - } else { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; - }; - }; - }; - // #270-1c: an AGGREGATE (struct/array/tuple) element of - // an array literal — the scalar per-element store below - // writes only the first 8 bytes (unpopulated tail). Fill - // each element slot from its literal (cgstructlitfillbp) - // or source ident (word-copy). esz is the element's - // natural size (cstage esub->size). - let esubti: *tinfo = nil; - if (elemn != nil) { esubti = elemn.type_: *tinfo; }; - for (esubti != nil && esubti.kind == tykind.TY_NAMED) { - esubti = esubti.under; - }; - let isagg: bool = esubti != nil - && (esubti.kind == tykind.TY_STRUCT - || esubti.kind == tykind.TY_ARRAY - || esubti.kind == tykind.TY_TUPLE); - if (isagg) { esz = esubti.size: i32; }; - // #20/#270 str-slice arm: a slice element (N_TSLICE) is - // a 24B {ptr,len,cap} header — it matches no prim/str/agg - // branch above, so esz stayed the 8 sentinel (wrong stride, - // the -96-vs-80 cs!=ww frame divergence) and the scalar - // store dropped .len/.cap. Size it from the stamped tinfo - // and route it through the 3-word header store below. - let isslicel: bool = esubti != nil - && esubti.kind == tykind.TY_SLICE; - if (isslicel) { esz = esubti.size: i32; }; - // #12: a tagged-union element. NOT folded into isagg — - // isagg's body word-copies/fatals and never boxes the - // tag+payload; route through the cgwidentaggedstore - // choke-point the N_LET tagged path (cgenstmt.ww:1627) - // uses. esz must come from the stamped slot size (#8-class - // trap, rule-13): the narrow override below only rescues - // 1/2/4, so a tagged 16/24B element keeps the wrong 8 - // sentinel stride without this. - let istaggedel: bool = esubti != nil - && esubti.kind == tykind.TY_TAGGED; - if (istaggedel) { esz = esubti.size: i32; }; - // #8: a named-narrow element (`[N]tk`, tk = enum i32) is - // neither a builtin prim (primsize=0 above, so esz stayed - // the 8 sentinel) nor an aggregate, so the scalar store kept - // an 8B stride/MOVQ and overran the stride-4 frame slot — - // smashing the saved BP / return addr (SEGFAULT). Mirror - // cstage's uniform lu->sub->size (cgen.c:6387) and the - // elemsizeofc read-side fix: take the stamped element tinfo's - // size for a narrow scalar (1/2/4). Wider non-prim elements - // (tagged/slice/str two-half) stay the documented follow-up - // at :1742-1744 — the single-MOVx store below is scalar-only. - if (!isstrel && !isagg && esz == 8 && esubti != nil) { - let es: i32 = esubti.size: i32; - if (es == 1 || es == 2 || es == 4) { esz = es; }; - }; - let mop: str = tnodestoreop(c, elemn, esz); - // float element → store FROM X0 (MOVSS/MOVSD): cgexpr - // leaves a float in X0 and for f32 the #104 CVTSD2SS - // narrowing only touches X0; the AX store (mop) would - // write the raw double low-bits, garbage for f32 (#122, - // mirrors cstage cgen.c:6889 arr-lit float store). - let isfloatel: bool = isfloattype(c, elemn); - let fmov: str = "MOVSD"; - if (isf32type(c, elemn)) { fmov = "MOVSS"; }; - let idx: i32 = 0; - let repeat: bool = false; - let e: *node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == nkind.N_FIELD) { - if (streq(e.str, "...")) { - repeat = true; - isellip = true; - }; - }; - if (isellip) { - e = nil; - } else { - if (isagg) { - if (e.kind == nkind.N_STRUCTLIT) { - let esi: *structinfo = structlookupchain(c, elemn); - cgstructlitfillbp(c, esi, e, off + idx * esz); - } else { if (e.kind == nkind.N_IDENT) { - let sl: *local = localfindnode(c, e.str); - let soff: i32 = 0; - if (sl != nil) { soff = sl.off; }; - let kc: i32 = 0; - for (kc + 8 <= esz) { - emitline("\tMOVQ\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 8; - }; - if (kc + 4 <= esz) { - emitline("\tMOVL\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVL\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 4; - }; - if (kc + 2 <= esz) { - emitline("\tMOVW\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVW\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 2; - }; - if (kc + 1 <= esz) { - emitline("\tMOVB\t"); - emitoff((soff + kc): i64); - emitline("(BP), AX\n"); - emitline("\tMOVB\tAX, "); - emitoff((off + idx * esz + kc): i64); - emitline("(BP)\n"); - kc += 1; - }; - } else { - let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; - os.write(2, m1c.ptr, m1c.len: u64); - os.exit(1); - }; }; - } else { if (istaggedel) { - cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); - } else { - cgexpr(c, e); - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - }; }; - idx += 1; - e = e.next; - }; - }; - if (repeat && isagg) { - let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; - os.write(2, m1cr.ptr, m1cr.len: u64); - os.exit(1); - }; - // #12: `...` re-stores from AX, but cgwidentaggedstore consumed - // the node and trashed AX — the repeat-fill would write garbage. - // No consumer needs `[N]tagged=[x,...]`. - if (repeat && istaggedel) { - let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; - os.write(2, m12r.ptr, m12r.len: u64); - os.exit(1); - }; - // AX (and BX for str) still holds the last stored value; - // fill remaining slots up to the declared length with it. - if (repeat) { - let total: i32 = idx; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { - if (n.lhs.rhs != nil) { - if (n.lhs.rhs.kind == nkind.N_INTLIT) { - total = n.lhs.rhs.uval: i32; - }; - }; - }; - }; - for (idx < total) { - if (isstrel || isslicel) { - emitline("\tMOVQ\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tBX, "); - emitoff((off + idx * esz + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + idx * esz + 16): i64); - emitline("(BP)\n"); - } else { if (isfloatel) { - emitline("\t"); - emitline(fmov); - emitline("\tX0, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - } else { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); - }; }; - idx += 1; - }; - }; + cgarrlitfillbp(c, n.lhs, rhs, off); c.lastwasreturn = 0; return; }; diff --git a/test/wcc/953_arrlit_slice_run.c b/test/wcc/953_arrlit_slice_run.c new file mode 100644 index 00000000..96f46c2a --- /dev/null +++ b/test/wcc/953_arrlit_slice_run.c @@ -0,0 +1,280 @@ +/* + * 953_arrlit_slice_run — runtime + byte-id + reject net for #25/#31: a + * one-step array-LITERAL initialiser for a SLICE local (`let xs:[]T=[..]`). + * + * #31 (silent miscompile, cs!=ww): the #258 array→slice borrow wrapped the + * un-addressable N_ARRLIT directly as the N_SLICE base; cgen never spilled + * the literal to a stack slot, so .ptr pointed at garbage (`let xs:[]i32= + * [10,20,30]; xs[1]` returned 1; []u8/[]str segfaulted). #25 (over-strict + * reject): a slice target fell through to the exact-element type_eq borrow + * gate, so bare-int-width ([]u8=[1,2,3]) and str elements rejected. + * + * Fix (re-stamp + per-borrow scratch, both stages symmetric, NO new codegen + * shape): the checker re-stamps the slice arrlit as [count]T (per-element + * coercion + range-check, #25); cgen materialises the borrow base into a + * FRESH per-borrow @slicescr stack slot (distinct slot per borrow — a + * borrow's backing outlives the lowering, so it can't share a cached slot; + * two live borrows would otherwise alias one backing), filled via the shared + * array-init element store (#31). Supported ONLY at a `let` init — in + * call-arg / return / assign position there is no addressable backing, so + * both stages LOUD-REJECT ("bind it to a `let` first"), aligning cstage DOWN + * to wwstage per rule-10; full non-let support is deferred (#33). + * + * Accept rows (cstage `ww build` + run for exit code, then w6c vs w6c_ww .s + * cmp for the rule-10 byte-id gate): + * - i32_sum let xs:[]i32=[10,20,30]; xs[0]+xs[1]+xs[2] → 60 + * - i32_idx xs[1] (the #31 pin: was 1, want 20) → 20 + * - u8_coerce let zs:[]u8=[1,2,3]; bare-int→u8 width (#25) → 6 + * - u8_typed let xs:[]u8=[10u8,20u8,30u8]; xs[2] → 30 + * - i64_stride let xs:[]i64=[7i64,42i64]; xs[1] (8B stride) → 42 + * - str_read let ys:[]str=["ab","c"]; ys[0].len*10+ys[1].len → 21 + * - len_read let xs:[]i32=[10,20,30]; xs.len → 3 + * - multi_live let xs=[1,2,3]; let ys=[4,5]; xs[0]+ys[0] → 5 + * (SOUNDNESS PIN: fresh-per-borrow; a shared slot → 4+4=8) + * - borrow_mut xs[1]=99 through the borrow, read back → 99 + * + * Reject rows (cs==ww symmetric loud-reject, both w6c and w6c_ww non-zero): + * - reject_oob let q:[]u8=[256,1] → out-of-range element (#25) + * - reject_call sum([1,2,3]) → non-let borrow (#31/#33) + * - reject_ret return [1,2,3] → non-let borrow + * - reject_assign s = [1,2,3] → non-let borrow + */ +#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"), *fb = fopen(b, "rb"); + if (fa == NULL || fb == NULL) { + if (fa) fclose(fa); + if (fb) fclose(fb); + return -1; + } + int ca, cb, eq = 0; + do { + ca = fgetc(fa); + cb = fgetc(fb); + if (ca != cb) { eq = -1; break; } + } while (ca != EOF); + fclose(fa); + fclose(fb); + return eq; +} + +struct row { const char *label; const char *src; int want_exit; int reject; }; + +static const struct row rows[] = { + { "i32_sum", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i32 = [10, 20, 30];\n" + " return xs[0] + xs[1] + xs[2];\n" + "};\n", 60, 0 }, + { "i32_idx", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i32 = [10, 20, 30];\n" + " return xs[1];\n" + "};\n", 20, 0 }, + { "u8_coerce", + "package main;\n" + "export fn main() i32 = {\n" + " let zs: []u8 = [1, 2, 3];\n" + " return zs[0]: i32 + zs[1]: i32 + zs[2]: i32;\n" + "};\n", 6, 0 }, + { "u8_typed", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []u8 = [10u8, 20u8, 30u8];\n" + " return xs[2]: i32;\n" + "};\n", 30, 0 }, + { "i64_stride", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i64 = [7i64, 42i64];\n" + " return xs[1]: i32;\n" + "};\n", 42, 0 }, + { "str_read", + "package main;\n" + "export fn main() i32 = {\n" + " let ys: []str = [\"ab\", \"c\"];\n" + " return ys[0].len: i32 * 10 + ys[1].len: i32;\n" + "};\n", 21, 0 }, + { "len_read", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i32 = [10, 20, 30];\n" + " return xs.len: i32;\n" + "};\n", 3, 0 }, + { "multi_live", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i32 = [1, 2, 3];\n" + " let ys: []i32 = [4, 5];\n" + " return xs[0] + ys[0];\n" + "};\n", 5, 0 }, + { "borrow_mut", + "package main;\n" + "export fn main() i32 = {\n" + " let xs: []i32 = [1, 2, 3];\n" + " xs[1] = 99;\n" + " return xs[1];\n" + "};\n", 99, 0 }, + /* Reject rows: out-of-range element + the three non-let contexts. */ + { "reject_oob", + "package main;\n" + "export fn main() i32 = {\n" + " let q: []u8 = [256, 1];\n" + " return q[0]: i32;\n" + "};\n", 0, 1 }, + { "reject_call", + "package main;\n" + "fn sum(s: []i32) i32 = { return s[0]; };\n" + "export fn main() i32 = {\n" + " return sum([1, 2, 3]);\n" + "};\n", 0, 1 }, + { "reject_ret", + "package main;\n" + "fn mk() []i32 = { return [1, 2, 3]; };\n" + "export fn main() i32 = {\n" + " return mk()[0];\n" + "};\n", 0, 1 }, + { "reject_assign", + "package main;\n" + "export fn main() i32 = {\n" + " let s: []i32 = [0, 0];\n" + " s = [1, 2, 3];\n" + " return s[0];\n" + "};\n", 0, 1 }, + { NULL, NULL, 0, 0 }, +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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 w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "arrlit_slice: w6c_ww missing — cannot run the " + "cs==ww byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwas_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwas_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwas_%d_%d_ww.s", getpid(), i); + char cmd[2048]; + + if (rows[i].reject) { + /* Both stages must refuse (non-zero). */ + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c, cs_s, src); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c ACCEPTED a row that " + "must reject\n", rows[i].label); + fail++; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c_ww ACCEPTED a row " + "that must reject\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + continue; + } + + /* Positive row: cstage build + run for exit code. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwas_%d_d_%d", + getpid(), i); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + /* rule-10 byte-id: w6c vs w6c_ww .s. */ + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", + rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, + "row[%s]: cstage/wwstage .s DIFFER (rule-10 " + "byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d arrlit-slice tests failed\n", fail, n); + return 1; + } + printf("arrlit_slice: %d/%d ok (cstage run + cs==ww byte-id + " + "reject)\n", n, n); + return 0; +}