selfhost: retire dotchain bundle workaround (closes #17)

dotchain struct + bundle pre-declares retired. dotchainresolve now
takes 6 direct out-params; per-iter locals declared inline. Bootstrap
ww2==ww3==ww4 byte-identical — end-to-end validation of session's
N_DOT / N_INDEX / TK_AMP / fldloadop fixes.

i64 widths retained on out-params via task #19 workaround (i32 deref-
stores leave caller slot upper 4B stale, MOVQ reads zero-extend to
garbage). Documented in selfhost/CLAUDE.md active-workarounds.
This commit is contained in:
2026-05-14 01:11:07 +09:00
parent f26c86f5b7
commit 5f87c60e6c
6 changed files with 383 additions and 417 deletions

View File

@@ -2308,58 +2308,41 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
return;
};
// dotchain — packed result struct for dotchainresolve. Out-params are
// bundled to keep the helper at <= 6 register-passed args; wwstage's
// per-fn arg-frame computation over-allocates by 16 bytes for any
// function with > 6 args (task #7, a pre-existing quirk independent
// of this fix), which would silently break the bootstrap fixed-point
// gate (993 / 994 / 995).
//
// Numeric fields are all i64, not i32. wwstage zero-inits an i32
// local with MOVQ (8-byte store) but subsequent `out.totaloff = …`
// updates would emit MOVL (4-byte store), leaving the upper 4 bytes
// stale from the wider init. Keeping the out-params at i64 makes the
// init width and the update width agree, so the field reads back
// what was written across both stages.
type dotchain = struct {
rootname: str,
rootoff: i64,
totaloff: i64,
leaffi: *fieldinfo,
slicedelta: i64,
isglobal: bool,
};
// Spine-walk a chained N_DOT (n) inward to a root ident, summing field
// offsets through value-struct intermediates. Optional slice/str leaf
// pseudo-field (.ptr / .len / .cap) on the last segment is folded into
// `out.slicedelta` (0/8/16); otherwise out.leaffi is the leaf fieldinfo
// and slicedelta stays -1. Returns true on success; on false the caller
// falls through to other branches.
// *outslicedelta (0/8/16); otherwise *outleaffi is the leaf fieldinfo
// and *outslicedelta stays -1. Returns true on success; on false the
// caller falls through to other branches.
//
// Mirrors cmd/w6c/cgen.c's N_DOT chained walker; both stages must agree
// on the same shapes so the bootstrap fixed-point holds. The chain
// depth is capped at 16 — deeper chains are vanishingly rare and fall
// through.
//
// On success the caller emits one load/store at root_base + out.totaloff
// On success the caller emits one load/store at root_base + *outtotaloff
// (+ slicedelta for pseudo leaf). Root resolves as: local frame slot
// (out.rootoff != 0, isglobal false) or top-level let (isglobal true,
// root accessed via LEAQ name(SB), CX).
export fn dotchainresolve(c: *cgen, n: *node, out: *dotchain) bool = {
out.rootname = "";
out.rootoff = 0i64;
out.isglobal = false;
out.totaloff = 0i64;
out.leaffi = nil;
out.slicedelta = -1i64;
// (*outisglobal false, base = *outrootoff(BP)) or top-level let
// (*outisglobal true, base reached via LEAQ *outrootname(SB), CX).
//
// Numeric out-params are i64 so the deref-stores stay 8-byte (MOVQ).
// `*p: *i32 = v` writes only 4 bytes via MOVL, leaving the caller's
// 8-byte slot's upper half stale from its zero-init — and a later MOVQ
// read sees the zero-extended low half, so a negative i32 root offset
// would come back as a huge positive i64. Tracked as task #19; until
// it lands, callers cast to i32 at the assign sites.
export fn dotchainresolve(c: *cgen, n: *node,
outrootname: *str, outrootoff: *i64, outtotaloff: *i64,
outleaffi: **fieldinfo, outslicedelta: *i64,
outisglobal: *bool) bool = {
*outrootname = "";
*outrootoff = 0i64;
*outisglobal = false;
*outtotaloff = 0i64;
*outleaffi = nil;
*outslicedelta = -1i64;
if (n == nil) { return false; };
if (n.kind != nkind.N_DOT) { return false; };
// Walk inward, recording the N_DOT node at each step (leaf first).
// We hold *node pointers (8B each, slotsize-stable across stages)
// and read .str on demand — a [16]str array would mis-slot at
// wwstage where slotsize("str") returns 8, breaking the bootstrap
// fixed-point.
let stk: [16]*node;
let nsteps: i32 = 0;
let cur: *node = n;
@@ -2373,95 +2356,72 @@ export fn dotchainresolve(c: *cgen, n: *node, out: *dotchain) bool = {
if (nsteps < 2) { return false; };
if (cur == nil) { return false; };
if (cur.kind != nkind.N_IDENT) { return false; };
out.rootname = cur.str;
// Resolve the root's struct type and base.
*outrootname = cur.str;
let rootstruct: str = "";
let lc: *local = localfindnode(c, cur.str);
let gsi: *structinfo = nil;
if (lc != nil) {
if (lc.tnode != nil) {
if (lc.tnode.kind == nkind.N_TNAME) {
rootstruct = lc.tnode.str;
out.rootoff = lc.off: i64;
*outrootoff = lc.off: i64;
};
};
};
if (rootstruct.len == 0) {
gsi = letvarstructinfo(c, cur.str);
let gsi: *structinfo = letvarstructinfo(c, cur.str);
if (gsi != nil) {
rootstruct = gsi.sname;
out.isglobal = true;
*outisglobal = true;
};
};
if (rootstruct.len == 0) { return false; };
// Walk outward, resolving each field. stk is leaf-first; iterate
// from i = nsteps - 1 (the root-most field) down to i = 0 (leaf).
let curstruct: str = rootstruct;
// Pre-declare per-iteration spills here so cstage / wwstage agree
// on the frame layout. Both must emit byte-identical asm for the
// bootstrap fixed-point (tests 993/995) — letting these locals get
// declared inside the branch bodies trips a per-stage divergence in
// slot counting.
let stepnm: str = "";
let fi: *fieldinfo = nil;
let found: *fieldinfo = nil;
let ft: *node = nil;
let s0nd: *node = nil;
let pseudo: str = "";
let delta: i64 = 0i64;
let i: i32 = nsteps - 1;
for (i >= 0) {
let csi: *structinfo = structlookup(c, curstruct);
if (csi == nil) { return false; };
if (stk[i] == nil) { return false; };
stepnm = stk[i].str;
fi = csi.fields;
found = nil;
let stepnm: str = stk[i].str;
let fi: *fieldinfo = csi.fields;
let found: *fieldinfo = nil;
for (fi != nil) {
if (streq(fi.fname, stepnm)) { found = fi; break; };
fi = fi.finext;
};
if (found == nil) { return false; };
if (i == 0) {
out.totaloff = out.totaloff + (found.foff: i64);
out.leaffi = found;
*outtotaloff = *outtotaloff + (found.foff: i64);
*outleaffi = found;
return true;
};
// Intermediate step. Must be a nested value-struct, OR a slice/
// str field with the leaf (i == 1, stk[0]) as a pseudo-field.
ft = found.tnode;
let ft: *node = found.tnode;
if (ft == nil) { return false; };
if (ft.kind == nkind.N_TNAME) {
if (streq(ft.str, "str")) {
if (i != 1) { return false; };
s0nd = stk[0];
if (s0nd == nil) { return false; };
pseudo = s0nd.str;
delta = -1i64;
let pseudo: str = stk[0].str;
let delta: i64 = -1i64;
if (streq(pseudo, "ptr")) { delta = 0i64; }
else { if (streq(pseudo, "len")) { delta = 8i64; }; };
if (delta < 0i64) { return false; };
out.totaloff = out.totaloff + (found.foff: i64);
out.slicedelta = delta;
*outtotaloff = *outtotaloff + (found.foff: i64);
*outslicedelta = delta;
return true;
};
if (primsize(ft.str) != 0) { return false; };
// Nested value-struct (named).
out.totaloff = out.totaloff + (found.foff: i64);
*outtotaloff = *outtotaloff + (found.foff: i64);
curstruct = ft.str;
i -= 1;
} else { if (ft.kind == nkind.N_TSLICE) {
if (i != 1) { return false; };
s0nd = stk[0];
if (s0nd == nil) { return false; };
pseudo = s0nd.str;
delta = -1i64;
let pseudo: str = stk[0].str;
let delta: i64 = -1i64;
if (streq(pseudo, "ptr")) { delta = 0i64; }
else { if (streq(pseudo, "len")) { delta = 8i64; }
else { if (streq(pseudo, "cap")) { delta = 16i64; }; }; };
if (delta < 0i64) { return false; };
out.totaloff = out.totaloff + (found.foff: i64);
out.slicedelta = delta;
*outtotaloff = *outtotaloff + (found.foff: i64);
*outslicedelta = delta;
return true;
} else {
return false;