test: migrate 802_lenidx to @test + runww rejects, retire C twin (fold-3)

#19 len/index family: value rows -> test/lang/lenidx_test.ww (primitive-only asserts), 3 reject (callres/sliceexpr/strlit) -> runww //ww:error dual-stage carriers. byteid floor 52->53.
This commit is contained in:
2026-06-22 16:53:49 +09:00
parent 057e805cf0
commit 04f163dde6
6 changed files with 174 additions and 427 deletions

139
test/lang/lenidx_test.ww Normal file
View File

@@ -0,0 +1,139 @@
// lenidx_test — the `len()` builtin over a non-IDENT operand shape (#19, then
// the C5 sweep #10 F2 + #41 FA2/FB1), migrated from test/wcc/802_lenidx_run.c.
// `len(xs[i])` / `len(*p)` / `len(s.field)` etc. once fell to a bare cgexpr
// fallback that left AX = the slice/str DATA POINTER, so len() returned the
// PTR as the length — silent ptr-garbage, byte-identical in BOTH stages (the
// bootstrap corpus never indexes-then-len()s an element, so byte-id was
// blind). The fix routes every place-resolvable operand through one uniform
// header-place arm (.len at place+8) in both stages; non-place operands
// (string literal, slicing expr, call result) now die LOUD (rule 7) — those
// three are the runww reject carriers. The empty/zero rows are the strongest
// pins: a leaked ptr is nonzero, so `len(empty) == 0` catches the regression
// where a bare exit code couldn't. PRIMITIVE-only asserts (no fmt/strconv) so
// a co-miscompile in the assert path cannot mask the bug. The .len/.ptr
// pseudo-field control pins that the fix left the already-correct field paths
// untouched. T2 keeps cs==ww (the #19 lineage: len of an INDEXED element is
// exactly where cs!=ww would surface).
package lenidx_test;
// global [N]str table — the exact #19 repro operand (static-init str array,
// #18 family); referenced from several rows to keep the global codegen path.
let t: [3]str = ["a", "bcd", ""];
type S = struct { pad: i64, name: str };
@test fn lenidx_arrtab_mid() void = {
// len(t[1]) == 3; pre-fix this returned a ptr low-byte, not 3.
assert(len(t[1]) == 3);
};
@test fn lenidx_arrtab_empty() void = {
// empty element: a leaked ptr is nonzero, len must be 0.
assert(len(t[2]) == 0);
};
@test fn lenidx_arrtab_first() void = {
// guards an off-by-one in the index scale.
assert(len(t[0]) == 1);
};
fn slen(xs: []str, i: i64) int = { return len(xs[i]): int; };
@test fn lenidx_slice_param_sum() void = {
// len(xs[i]) over a []str slice ARG (callee-side shape), 1 + 3 + 0 == 4.
let arr: [3]str = ["a", "bcd", ""];
let xs: []str = arr;
assert(slen(xs, 0) + slen(xs, 1) + slen(xs, 2) == 4);
};
@test fn lenidx_neg_control_field_agree() void = {
// the `.len` pseudo-field on the same indexed element must agree with
// len(), AND `.ptr` must still deref to the first byte — the fix must
// leave the already-correct .len/.ptr field paths untouched.
assert(len(t[1]) == t[1].len);
let p: *u8 = t[1].ptr;
assert(*p == 'b');
assert(len(t[1]) == 3);
};
fn derefn(p: *[]i64) int = { return len(*p): int; };
@test fn lenidx_deref_param() void = {
// len(*p) through a *[]T PARAM (FB1) loaded the header's word 0 (the
// data pointer) as the length.
let xs: []i64 = [10, 20, 30];
assert(derefn(&xs) == 3);
};
@test fn lenidx_deref_local() void = {
let xs: []i64 = [10, 20, 30];
let p: *[]i64 = &xs;
assert(len(*p) == 3);
};
@test fn lenidx_deref_empty() void = {
// EMPTY-slice deref: the off-by-header bug returns .ptr (nonzero),
// len() must say 0 — only a non-exit primitive assert pins this.
let xs: []i64 = [1];
let ys: []i64 = xs[0:0];
let p: *[]i64 = &ys;
assert(len(*p) == 0);
};
@test fn lenidx_deref_chain() void = {
// chained deref len(**pp) — the resolver spine must recurse both hops.
let xs: []i64 = [10, 20, 30];
let p: *[]i64 = &xs;
let pp: **[]i64 = &p;
assert(len(**pp) == 3);
};
@test fn lenidx_idx_field() void = {
// len(xs[i].field) (F2) — N_DOT over an N_INDEX base matched no arm.
let xs: [2]S = [S{ pad = 1, name = "a" }, S{ pad = 2, name = "bcde" }];
assert(len(xs[1].name) == 4);
};
fn derefidxn(p: *[]S, i: i64) int = { return len((*p)[i].name): int; };
@test fn lenidx_deref_idx_field() void = {
// full deref spine: len((*p)[i].field).
let arr: [2]S = [S{ pad = 1, name = "a" }, S{ pad = 2, name = "bcde" }];
let xs: []S = arr;
assert(derefidxn(&xs, 1) == 4);
};
fn derefidxcn(p: *[]S, i: i64) int = { return len((*p)[i + 1].name): int; };
@test fn lenidx_deref_idx_field_computed() void = {
// computed index through the spine — i+1 must resolve like a constant.
let arr: [2]S = [S{ pad = 1, name = "a" }, S{ pad = 2, name = "bcde" }];
let xs: []S = arr;
assert(derefidxcn(&xs, 0) == 4);
};
@test fn lenidx_dot_field() void = {
// len(s.field) — the #235 arm's inner non-tuple fallback was ptr-garbage.
let s: S = S{ pad = 7, name = "abc" };
assert(len(s.name) == 3);
};
@test fn lenidx_ptr_field() void = {
// len(p.field) through a *struct.
let s: S = S{ pad = 7, name = "abc" };
let p: *S = &s;
assert(len(p.name) == 3);
};
@test fn lenidx_neutral_array_const() void = {
// neutrality control — the [N]T const fast-path still holds.
let a: [5]u8 = [1, 2, 3, 4, 5];
assert(len(a) == 5);
};
@test fn lenidx_neutral_ident_str() void = {
// neutrality control — the ident-str fast-path still holds.
let s: str = "abcd";
assert(len(s) == 4);
};