w6c+selfhost: localloadop helper for sign-aware ident loads (closes #19)

Read-side fix dual to fldloadop: signed-narrow local/global ident loads
now MOVSXD/MOVSWQ/MOVSBQ from the slot instead of raw MOVQ. Deref-stores
(MOVL/MOVW/MOVB) no longer corrupt downstream i64 widens. Compound RMW
restructured to gate direct-mem ADDQ/SUBQ on load_op == MOVQ. Top-level
lets use LEAQ+indirect (w6a doesn't expose MOVSXD/MOVSWQ/MOVSBQ for
D_EXTERN).

dotchainresolve out-params restored to natural *i32 (workaround retired).
selfhost/CLAUDE.md graduated.
This commit is contained in:
2026-05-14 01:58:52 +09:00
parent 5f87c60e6c
commit d2f4659305
7 changed files with 728 additions and 281 deletions

View File

@@ -6294,6 +6294,39 @@ fn loadopsz(sigd: bool, sz: i32) str = {
return "MOVQ";
};
// localloadop — read instruction for a scalar local/let load. Same
// dispatch as fieldloadop, but keyed on the value's own tnode. Lets
// the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot instead
// of a raw MOVQ, so a slot that was last written by a narrow deref-
// store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as a
// properly-sign-extended i64. The natural N_ASSIGN / N_LET paths
// store the rhs as a sign-extended 8B word, so MOVQ accidentally
// works; deref-stores are the only path that touches fewer bytes
// than MOVQ reads. Mirror of cstage's localloadop in cmd/w6c/cgen.c.
// Resolves TBANG / TENUM / TNAME-alias chains so `type err = !i32`
// picks up size 4 the same way the cstage checker pre-computes
// t->size — without this, aliased narrows fall through to MOVQ.
export fn localloadop(c: *cgen, tnode: *node) str = {
let t: *node = tnode;
for (t != nil) {
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { t = t.lhs; }
else { if (k == nkind.N_TENUM) { t = t.lhs; }
else { if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (primsize(nm) > 0) { break; };
let al: *node = aliaslookup(c, nm);
if (al == nil) { break; };
t = al;
}
else { break; }; }; };
};
let sz: i32 = fieldsize(c, t);
if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; };
let sigd: bool = fieldissignedc(c, tnode);
return loadopsz(sigd, sz);
};
// indexbaseesz — element size for `arr[i]` where the base is a
// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice).
// For str the element is one byte; for `[]T` / `*[]T` we drill into
@@ -7895,22 +7928,19 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz:
// (*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.
// Numeric out-params are i32 — offsets fit naturally and the post-#19
// localloadop sign-extends i32 deref-stored slots on read, so negative
// frame offsets round-trip intact.
export fn dotchainresolve(c: *cgen, n: *node,
outrootname: *str, outrootoff: *i64, outtotaloff: *i64,
outleaffi: **fieldinfo, outslicedelta: *i64,
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
outleaffi: **fieldinfo, outslicedelta: *i32,
outisglobal: *bool) bool = {
*outrootname = "";
*outrootoff = 0i64;
*outrootoff = 0;
*outisglobal = false;
*outtotaloff = 0i64;
*outtotaloff = 0;
*outleaffi = nil;
*outslicedelta = -1i64;
*outslicedelta = -1;
if (n == nil) { return false; };
if (n.kind != nkind.N_DOT) { return false; };
let stk: [16]*node;
@@ -7933,7 +7963,7 @@ export fn dotchainresolve(c: *cgen, n: *node,
if (lc.tnode != nil) {
if (lc.tnode.kind == nkind.N_TNAME) {
rootstruct = lc.tnode.str;
*outrootoff = lc.off: i64;
*outrootoff = lc.off;
};
};
};
@@ -7960,7 +7990,7 @@ export fn dotchainresolve(c: *cgen, n: *node,
};
if (found == nil) { return false; };
if (i == 0) {
*outtotaloff = *outtotaloff + (found.foff: i64);
*outtotaloff = *outtotaloff + found.foff;
*outleaffi = found;
return true;
};
@@ -7970,27 +8000,27 @@ export fn dotchainresolve(c: *cgen, n: *node,
if (streq(ft.str, "str")) {
if (i != 1) { return false; };
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; };
*outtotaloff = *outtotaloff + (found.foff: i64);
let delta: i32 = -1;
if (streq(pseudo, "ptr")) { delta = 0; }
else { if (streq(pseudo, "len")) { delta = 8; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + found.foff;
*outslicedelta = delta;
return true;
};
if (primsize(ft.str) != 0) { return false; };
*outtotaloff = *outtotaloff + (found.foff: i64);
*outtotaloff = *outtotaloff + found.foff;
curstruct = ft.str;
i -= 1;
} else { if (ft.kind == nkind.N_TSLICE) {
if (i != 1) { return false; };
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; };
*outtotaloff = *outtotaloff + (found.foff: i64);
let delta: i32 = -1;
if (streq(pseudo, "ptr")) { delta = 0; }
else { if (streq(pseudo, "len")) { delta = 8; }
else { if (streq(pseudo, "cap")) { delta = 16; }; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + found.foff;
*outslicedelta = delta;
return true;
} else {
@@ -8494,17 +8524,25 @@ fn cgident(c: *cgen, n: *node) void = {
emitline("(BP), X0\n");
return;
};
emitline("\tMOVQ\t");
// str / slice locals load (ptr[, len[, cap]]) through MOVQ
// since the header is always 8B-clean. Scalar locals route
// through localloadop so signed-narrow slots sign-extend
// after a narrow deref-store.
let isstr: bool = isstrtype(c, lc.tnode);
let issl: bool = isslicetype(c, lc.tnode);
let lop: str = "MOVQ";
if (!isstr) { if (!issl) { lop = localloadop(c, lc.tnode); }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff(off: i64);
emitline("(BP), AX\n");
// str local: also load the len half into BX.
if (isstrtype(c, lc.tnode)) {
if (isstr) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
};
// slice local: load (ptr, len, cap) into (AX, BX, CX).
if (isslicetype(c, lc.tnode)) {
if (issl) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
@@ -8557,7 +8595,10 @@ fn cgident(c: *cgen, n: *node) void = {
return;
};
// Float global: same LEAQ-indirect shape, since MOVSS/
// MOVSD have no D_EXTERN operand form in w6a.
// MOVSD have no D_EXTERN operand form in w6a. Signed-narrow
// scalar globals route through the same LEAQ scratch since
// MOVSXD/MOVSWQ/MOVSBQ also have no D_EXTERN form.
let lvtnode: *node = nil;
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, nm)) {
@@ -8572,14 +8613,25 @@ fn cgident(c: *cgen, n: *node) void = {
emitline("\t(CX), X0\n");
return;
};
lvtnode = lv.tnode;
lv = nil;
} else {
lv = lv.lvnext;
};
};
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
let glop: str = localloadop(c, lvtnode);
if (streq(glop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\t");
emitline(glop);
emitline("\t(CX), AX\n");
};
return;
};
return;
@@ -9556,13 +9608,25 @@ fn cgdot(c: *cgen, n: *node) void = {
};
// Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local.
// Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback
// the C cgen takes when bt is NULL/tyerr.
// Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case;
// signed-narrow leaves route through LEAQ + localloadop so a
// prior narrow deref-store doesn't leave stale upper bytes. Same
// fallback the C cgen takes when bt is NULL/tyerr.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
let mqop: str = localloadop(c, letvartnode(c, fld));
if (streq(mqop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, fld);
emitline("(SB), CX\n");
emitline("\t");
emitline(mqop);
emitline("\t(CX), AX\n");
};
return;
};
};
@@ -9579,26 +9643,26 @@ fn cgdot(c: *cgen, n: *node) void = {
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let rootname: str = "";
let rootoff: i64 = 0i64;
let totaloff: i64 = 0i64;
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaffi: *fieldinfo = nil;
let slicedelta: i64 = -1i64;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let pok: bool = dotchainresolve(c, n,
&rootname, &rootoff, &totaloff,
&leaffi, &slicedelta, &isglobal);
if (pok) {
if (slicedelta >= 0i64) {
if (slicedelta >= 0) {
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t");
emitdispreg(totaloff + slicedelta, "CX");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline(", AX\n");
} else {
emitline("\tMOVQ\t");
emitoff(rootoff + totaloff + slicedelta);
emitoff((rootoff + totaloff + slicedelta): i64);
emitline("(BP), AX\n");
};
return;
@@ -9609,17 +9673,17 @@ fn cgdot(c: *cgen, n: *node) void = {
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg(totaloff + 8i64, "CX");
emitdispreg((totaloff + 8): i64, "CX");
emitline(", BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff(rootoff + totaloff + 8i64);
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP), BX\n");
};
return;
@@ -9634,13 +9698,13 @@ fn cgdot(c: *cgen, n: *node) void = {
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline(", X0\n");
} else {
emitline("\t");
emitline(mov);
emitline("\t");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP), X0\n");
};
return;
@@ -9653,13 +9717,13 @@ fn cgdot(c: *cgen, n: *node) void = {
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
} else {
emitline("\t");
emitline(lop);
emitline("\t");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP), AX\n");
};
return;
@@ -9936,27 +10000,27 @@ fn cgun(c: *cgen, n: *node) void = {
if (opnd.lhs != nil) {
if (opnd.lhs.kind == nkind.N_DOT) {
let rootname: str = "";
let rootoff: i64 = 0i64;
let totaloff: i64 = 0i64;
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaffi: *fieldinfo = nil;
let slicedelta: i64 = -1i64;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let pok: bool = dotchainresolve(c, opnd,
&rootname, &rootoff, &totaloff,
&leaffi, &slicedelta, &isglobal);
if (pok) {
let extra: i64 = 0i64;
if (slicedelta >= 0i64) { extra = slicedelta; };
let extra: i32 = 0;
if (slicedelta >= 0) { extra = slicedelta; };
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tLEAQ\t");
emitdispreg(totaloff + extra, "CX");
emitdispreg((totaloff + extra): i64, "CX");
emitline(", AX\n");
} else {
emitline("\tLEAQ\t");
emitoff(rootoff + totaloff + extra);
emitoff((rootoff + totaloff + extra): i64);
emitline("(BP), AX\n");
};
return;
@@ -11888,27 +11952,27 @@ fn cgassign(c: *cgen, n: *node) void = {
&& lhs.lhs.kind == nkind.N_DOT
&& n.op == tkind.TK_ASSIGN) {
let rootname: str = "";
let rootoff: i64 = 0i64;
let totaloff: i64 = 0i64;
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaffi: *fieldinfo = nil;
let slicedelta: i64 = -1i64;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let yok: bool = dotchainresolve(c, lhs,
&rootname, &rootoff, &totaloff,
&leaffi, &slicedelta, &isglobal);
if (yok) {
if (slicedelta >= 0i64) {
if (slicedelta >= 0) {
cgexpr(c, n.rhs);
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(totaloff + slicedelta, "CX");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(rootoff + totaloff + slicedelta);
emitoff((rootoff + totaloff + slicedelta): i64);
emitline("(BP)\n");
};
return;
@@ -11920,17 +11984,17 @@ fn cgassign(c: *cgen, n: *node) void = {
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg(totaloff + 8i64, "CX");
emitdispreg((totaloff + 8): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff(rootoff + totaloff + 8i64);
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP)\n");
};
return;
@@ -11946,13 +12010,13 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
};
return;
@@ -11966,13 +12030,13 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(totaloff, "CX");
emitdispreg(totaloff: i64, "CX");
emitline("\n");
} else {
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff(rootoff + totaloff);
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
};
return;
@@ -12127,10 +12191,12 @@ fn cgassign(c: *cgen, n: *node) void = {
let lvf: *letvar = c.lets;
let isfg: bool = false;
let isf32g: bool = false;
let lvftn: *node = nil;
for (lvf != nil) {
if (streq(lvf.name, nm)) {
isfg = isfloattype(c, lvf.tnode);
isf32g = isf32type(c, lvf.tnode);
lvftn = lvf.tnode;
lvf = nil;
} else {
lvf = lvf.lvnext;
@@ -12212,9 +12278,23 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("(SB)\n");
return;
};
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
// Compound RMW for a top-level let: load through
// LEAQ + localloadop when the slot is narrow so
// a prior `*(&letname): *iN` deref-store doesn't
// leave stale upper bytes feeding the combine.
let glop: str = localloadop(c, lvftn);
if (streq(glop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\t");
emitline(glop);
emitline("\t(CX), BX\n");
};
let didcompound: bool = true;
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }
@@ -12334,22 +12414,34 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
if (n.op == tkind.TK_PLUSEQ) {
emitline("\tADDQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
// Pick the load width for compound RMW. Signed-narrow
// locals must sign-extend the slot before the combine
// — ADDQ/SUBQ on amem reads 8B raw, which is wrong
// after a 4B deref-store leaves the upper bytes stale.
let llop: str = "MOVQ";
if (lcn != nil) { llop = localloadop(c, lcn.tnode); };
if (streq(llop, "MOVQ")) {
if (n.op == tkind.TK_PLUSEQ) {
emitline("\tADDQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
};
// Generic compound: load → combine in BX → store.
emitline("\tMOVQ\t");
emitline("\t");
emitline(llop);
emitline("\t");
emitoff(off: i64);
emitline("(BP), BX\n");
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); };
if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); };
if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); };
if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); };
if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); };