selfhost: graduate wwstage &N_DOT[N_INDEX] to cstage canonical lean form (#21)

Latent #21 has two surface shapes — register polarity in cgun
TK_AMP N_INDEX's complex-base arm, and indexbaseesz's
over-broad .ptr pseudo-field gate — that share a single semantic
path: &N_DOT[N_INDEX] where the inner N_DOT cannot be peeled
into a plain ident base. Polarity-A (cgenexpr.ww) lifted to
cstage's three-line shape; stride-B (cgenutil.ww) narrowed so
the .ptr arm only fires on actual str/slice inners and falls
through to the generic struct-field arm for struct N_TNAME
bases. The fixes compose at the same call site (esz from
indexbaseesz, then the IMULQ-or-elide gate, then complex-base
emit), so splitting them into two commits would leave a
half-fixed intermediate — neither half stands alone as a
bisect-clean closure. Sentinel 755_amp_dot_idx exercises both
shapes across 4 stride classes (slice-elem 24, struct-elem 16,
u8 stride-1 elide, i64 stride-8); pre-fix 5/12 fail, post-fix
12/12 ok. Latent silent miscompile in lib/memio + lib/bufio's
.ptr[i] shape also unmasked.
This commit is contained in:
2026-05-19 13:43:45 +09:00
parent f0b8c25b29
commit d5e8d699d1
6 changed files with 360 additions and 33 deletions

View File

@@ -1145,18 +1145,31 @@ fn indexbaseesz(c: *cgen, base: *node) i32 = {
if (tn == nil) { return 8; };
// `.ptr` pseudo-field on str/slice → element of the str/slice.
// Gated on inner kind, NOT on the field name alone: a struct with
// a literal `ptr: *T` field (lib/memio.state, lib/bufio.state) must
// route through the generic struct-field arm below so the stride
// comes from primsize/structlookup, not the str/slice default. The
// over-broad pre-#21 shortcut hard-coded esz=8 and silently
// miscompiled `m.ptr[i]` for `*u8` callers (also widened the load
// op MOVZBQ → MOVQ in cgindex). Mirrors cstage which routes every
// base through `base->type->sub->size` (cmd/w6c/cgen.c idx_eff).
if (streq(fld, "ptr")) {
let innert: *node = tn;
if (tn.kind == nkind.N_TPTR) { innert = tn.lhs; };
if (innert == nil) { return 8; };
if (innert.kind == nkind.N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
};
// Slice element: resolve through elemsizeofc so a
// slice of a named struct (e.g. *[]option) returns
// the struct stride instead of falling through to
// elemsizeof's default 8.
if (innert.kind == nkind.N_TSLICE) {
return elemsizeofc(c, innert);
};
};
// Slice element: resolve through elemsizeofc so a slice of a
// named struct (e.g. *[]option) returns the struct stride
// instead of falling through to elemsizeof's default 8.
if (innert.kind == nkind.N_TSLICE) { return elemsizeofc(c, innert); };
return 8;
// Inner is a struct N_TNAME (or unresolved) — fall through
// to the generic struct-field arm below.
};
// Generic struct field: if it's *T, element size is T's size.