cgen: sub-slice cap = base_cap - lo

A sub-slice `base[lo:hi]` now sets cap to base_cap - lo (the storage
remaining to the underlying end; Go/Hare-identical) instead of hi - lo
(== len). base_cap is the array length N for [N]T, or the .capacity
word carried in a slice/str header at +16. Authored once per stage in
the cg_base_cap / cgbasecap helper, applied at both cap sites: the
N_SLICE value path (which serves let-init since the prior commit) and
the call-arg push. Both stages stay byte-identical (find-4 closed).

cap arithmetic per ref/harec/src/eval.c:1017 (slice: slice.cap -=
start) and eval.c:1024 (array: cap = array.length - start); capacity
is a distinct field per ref/hare/rt/ensure.ha:4-8 and cap >= len per
ref/harec/src/check.c:596. Only the cap arithmetic transfers: the ptr
stays unscaled (lo*esz is #76) and eval.c's stricter start>=end bound
is not ported (ww's runtime bound is start>end).

str[lo:hi] yields str with a real .capacity (D1), so the str base uses
the same +16 load -- no downgrade to []u8. base_cap falls back to len
(prior behavior) where it isn't cleanly available: a non-ident base
(its header cap was discarded by cgexpr; len is likewise wrong for a
defaulted hi there, pre-existing) and a global str base (wwstage
cgslice has no global-str load, #73 -- the carve-out keeps both
stages byte-identical).

Test: 942_subslice_cap_run, table-driven over both drivers, array /
slice / str base + an append-no-realloc row, each shape chosen so
base_cap-lo != hi-lo.

Fold in three pre-existing fixtures that asserted the old cap == len
and so failed under the corrected semantics (project #20):
681_arr_elem_field_write (slice_field_value_write,
slice_field_ptr_write, slice_field_distinct_bytes),
693_dot_tagged_source (local_struct_slice_variant,
via_ptr_slice_variant, letinit_slice_roundtrip, top_level_global_slice),
and 695_match_bind_struct (slice_neg_control). Each cap word updated to
base_cap - lo: a [8]u8 base sliced at lo=0 yields cap 8 (5->8, 3->8);
distinct_bytes slices a [16]u8 at lo=0, yielding cap 16 (6->16). len /
mark / ptr assertions are unchanged -- only the cap word moved.
This commit is contained in:
2026-05-25 01:12:38 +09:00
parent 324df92e1d
commit 8b23ff3517
10 changed files with 595 additions and 39 deletions

View File

@@ -1155,6 +1155,44 @@ localfind(Local *head, const char *name)
return 0; /* 0 = not found (caller must verify) */
}
/* cg_base_cap — load the capacity of a sub-slice's UNDERLYING storage
* into `dst` for the #20 cap = base_cap - lo formula (drew: harec
* eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start;
* ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal);
* slice/str -> the .capacity word carried in the header at +16 (the
* +16 load mirrors the hi-default +8 length dispatch, but emitted
* unconditionally). Returns 0 when base_cap isn't cleanly available so
* the caller keeps the prior cap=len: a non-ident base (cgexpr already
* discarded its header cap; recomputing would re-evaluate a possibly
* side-effecting base -- #74, which also owns the pre-existing
* defaulted-hi len gap there), or a GLOBAL str base (wwstage cgslice
* has no global-str load, #73 -- matching it keeps the stages
* byte-identical rather than introducing a fresh divergence). */
static int
cg_base_cap(Cg *c, Node *base, Type *bu, Local *locals, int dst)
{
if (!base || base->kind != N_IDENT)
return 0;
if (bu && bu->kind == TY_ARRAY) {
ins2(c, A_MOVQ, aimm((long long)bu->alen), areg(dst));
return 1;
}
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) && let_islet(base->str);
if (isglobal && bu->kind == TY_STR)
return 0;
if (isglobal) {
ins2(c, A_LEAQ, masym(c, base->str), areg(dst));
ins2(c, A_MOVQ, amem(dst, 16), areg(dst));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff + 16), areg(dst));
}
return 1;
}
return 0;
}
/* ------------------------------------------------------------------ */
/* expressions: result lands in AX. Returns 1 on success. */
@@ -4606,8 +4644,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_SUBQ, areg(D_AX), areg(D_DX));
/* ptr = base + lo */
ins2(c, A_ADDQ, areg(D_AX), areg(D_CX));
/* push cap, len, ptr (top) */
ins1(c, A_PUSHQ, areg(D_DX)); /* cap */
/* push cap, len, ptr (top). cap = base_cap - lo
* (#20); AX=lo, BX free. */
if (cg_base_cap(c, base, bu, locals, D_BX)) {
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins1(c, A_PUSHQ, areg(D_BX)); /* cap */
} else {
ins1(c, A_PUSHQ, areg(D_DX)); /* cap = len */
}
ins1(c, A_PUSHQ, areg(D_DX)); /* len */
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
continue;
@@ -6317,12 +6361,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
case N_SLICE: {
/* base[lo:hi] as a slice value. Leaves the triple in
* (AX=base+lo, BX=hi-lo, CX=hi-lo) so callers can route
* to a slice slot, return, or arg with the same ABI. Cap
* defaults to the new length — there's no syntax for a
* larger cap yet. Element scaling on the ptr isn't wired
* (matches the let-init path), so non-u8 slices need a
* follow-up audit when fixtures exercise them. */
* (AX=base+lo, BX=hi-lo, CX=base_cap-lo) so callers can
* route to a slice slot, return, or arg with the same ABI.
* cap is the storage remaining to the base's end (#20,
* Go/Hare-identical), via cg_base_cap. Element scaling on
* the ptr isn't wired (lo*esz is #76), so non-u8 slices
* need that follow-up before their ptr is correct. */
Node *base = n->lhs;
Node *lo = n->rhs;
Node *hi = n->cond;
@@ -6373,7 +6417,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
/* cap = base_cap - lo (#20); CX=lo, BX=len here. */
if (cg_base_cap(c, base, bu, locals, D_DX)) {
ins2(c, A_SUBQ, areg(D_CX), areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX), areg(D_CX));
} else {
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
}
break;
}
default: