wcc/check+w6c+w6c_ww: materialize array-literal slice-borrow base into per-fn scratch (fix #25 + #31)

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.
This commit is contained in:
2026-06-04 01:32:27 +09:00
parent c490ed3ec1
commit bf1037d8c4
9 changed files with 1621 additions and 861 deletions

View File

@@ -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]<default> 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: {