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

@@ -168,6 +168,24 @@ fldstoreop(Type *t, int sz)
return A_MOVQ;
}
/* localloadop — read instruction for a scalar local/let load. Same
* dispatch as fldloadop, but keyed on the value's own type. 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
* already store the value as a sign-extended 8B word so a MOVQ read
* accidentally works; deref-stores are the only path that touches
* fewer bytes than MOVQ reads. Fixing the read makes the slot's
* representation honest regardless of which store path wrote it. */
static int
localloadop(Type *t)
{
int sz = (t && t->size > 0) ? (int)t->size : 8;
if (sz != 1 && sz != 2 && sz != 4) return A_MOVQ;
return fldloadop(t, sz);
}
/* struct ≤16B all-INTEGER: 1 or 2 eightbyte regs.
* Returns 0 if not a struct or too large. */
static int
@@ -1212,7 +1230,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
} else {
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins2(c, localloadop(n->type),
amem(D_BP, off), areg(D_AX));
}
} else {
/* Non-local: function symbols load by address (LEAQ),
@@ -1265,7 +1284,25 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, op, amem(D_CX, 0), areg(D_X0));
goto ident_done;
}
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
/* Top-level lets can be the target of `*p` deref-stores
* (via `&letname: *iN`), so a signed-narrow scalar let
* needs MOVSXD/MOVSWQ/MOVSBQ on the read. Defs are
* read-only constants — their address cannot escape,
* so they keep the simpler MOVQ shape (and the wwstage
* defent registry, which doesn't track the declared
* type, agrees byte-for-byte). */
int gop = let_islet(n->str)
? localloadop(n->type) : A_MOVQ;
if (gop == A_MOVQ) {
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
} else {
/* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN
* source form, so route through a LEAQ scratch
* the same way top-level str/slice/float lets
* do. */
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
ins2(c, gop, amem(D_CX, 0), areg(D_AX));
}
}
ident_done:
break;
@@ -2853,9 +2890,21 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Compound: BX = load; combine with AX; store
* BX. The asm has no RIP-relative ADDQ/SUBQ
* mem-form, so we use the explicit load→
* combine→store sequence uniformly. */
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
* combine→store sequence uniformly. Narrow
* lets go through LEAQ + indirect load with
* localloadop so a prior `*(&letname): *iN`
* deref-store doesn't leave stale upper bytes
* in the read. */
int glop = localloadop(n->lhs->type);
if (glop == A_MOVQ) {
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, glop, amem(D_CX, 0),
areg(D_BX));
}
int did_compound = 1;
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
@@ -2894,17 +2943,24 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Compound: load → combine into BX → store. The two
* direct mem-form combines (ADDQ/SUBQ) are kept for
* the simple cases; the rest go through the generic
* register form. */
if (n->op == TK_PLUSEQ) {
* register form. Signed-narrow slots take the explicit
* load-combine-store path so the load can sign-extend
* through localloadop — ADDQ/SUBQ on amem would read
* the raw 8B, which is wrong when the slot was last
* written by a 4B deref-store. */
int lop = localloadop(n->lhs->type);
if (lop == A_MOVQ && n->op == TK_PLUSEQ) {
ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off));
break;
}
if (n->op == TK_MINUSEQ) {
if (lop == A_MOVQ && n->op == TK_MINUSEQ) {
ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off));
break;
}
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
ins2(c, lop, amem(D_BP, off), areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
@@ -4100,7 +4156,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
areg(D_BX));
goto dot_done;
}
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
/* Same gating as the bare-ident catch-all: lets route
* through localloadop (their slot can be the target of
* a narrow deref-store via `&letname: *iN`); defs and
* unresolved symbols stay on MOVQ so wwstage's defent-
* registry-without-tnode shape agrees byte-for-byte. */
int mqop = let_islet(n->str)
? localloadop(n->type) : A_MOVQ;
if (mqop == A_MOVQ) {
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
} else {
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
ins2(c, mqop, amem(D_CX, 0), areg(D_AX));
}
goto dot_done;
}
/* Chained N_DOT spine through value-struct fields. Handles any

View File

@@ -8,8 +8,6 @@ The C bootstrap's cgen has known silent-miscompilation traps. They produce wrong
1. **`amalloc(n)` with n < struct size silently corrupts neighbours.** No error — the bump arena hands out n bytes and field writes overflow into the next record. When introducing or growing a struct, audit every `amalloc(_, n)` call site and over-size (we routinely pass 48 for a 40-byte struct). Symptom: linked-list prepends lose all but the most recent entry.
2. **Out-param `*p: *i32` deref-stores leave caller slot's upper 4 bytes stale.** `*p = v` lowers to MOVL (4B), but the caller's 8B slot was zero-init via MOVQ; a later i64-widening read emits MOVQ (8B raw) and zero-extends, so a negative i32 round-trips as a 4G-rooted positive i64. Default to `*i64` out-params for offsets / signed indices until task #19 lands. Symptom: garbage frame offsets like `MOVL AX, 4294967280(BP)` in stage-2 asm.
Fixed (no workaround needed):
- `def NAME: str = "..."` field access. `.len`/`.ptr` on an Sdef ident
@@ -74,5 +72,22 @@ Fixed (no workaround needed):
follow the per-index-write contract and stay uninit. See
cmd/w6c/cgen.c N_LET (else branch, sz > 8 && !TY_ARRAY) and
selfhost/cmd/wcc/cgenstmt.ww cglet no-rhs branch.
- Signed-narrow scalar reads sign-extend honestly. `*p: *i32 = v`
stores 4 bytes (MOVL — correct for a 4B pointee), and the read
side now emits MOVSXD/MOVSWQ/MOVSBQ for any signed-narrow local
or let load instead of a raw MOVQ. The natural N_ASSIGN / N_LET
store paths happened to leave the slot sign-extended so MOVQ
reads accidentally worked; deref-stores were the only path that
touched fewer bytes than MOVQ reads, so negative i32 values
round-tripped as 4G-rooted positive i64s. Cgen now routes
scalar reads through `localloadop(t)`; aliases (`type err = !i32`)
resolve the size through TBANG / TNAME / TENUM. See cmd/w6c/cgen.c
N_IDENT/N_DOT mod.name catch-alls, cgassign compound RMW for
locals + top-level lets, and the matching selfhost cgenexpr.ww
cgident / cgassign / module-qualified branches plus
cgenutil.ww `localloadop`. Top-level lets and `mod.name` go
through a LEAQ + indirect load since w6a has no MOVSXD/MOVSWQ/
MOVSBQ D_EXTERN form. dotchainresolve's out-params are back at
their natural i32 width.
If a port "should work" but the binary is wrong, suspect these first.

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"); };

View File

@@ -491,17 +491,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");
@@ -554,7 +562,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)) {
@@ -569,14 +580,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;
@@ -1553,13 +1575,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;
};
};
@@ -1576,26 +1610,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;
@@ -1606,17 +1640,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;
@@ -1631,13 +1665,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;
@@ -1650,13 +1684,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;
@@ -1933,27 +1967,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;
@@ -3885,27 +3919,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;
@@ -3917,17 +3951,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;
@@ -3943,13 +3977,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;
@@ -3963,13 +3997,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;
@@ -4124,10 +4158,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;
@@ -4209,9 +4245,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"); }
@@ -4331,22 +4381,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"); };

View File

@@ -724,6 +724,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
@@ -2325,22 +2358,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;
@@ -2363,7 +2393,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;
};
};
};
@@ -2390,7 +2420,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;
};
@@ -2400,27 +2430,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 {

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"); };

View File

@@ -185,8 +185,7 @@ static const struct row rows[] = {
* widens all numeric out-params to *i64 as a workaround until
* #19 lands. This row pins the WORKAROUND: out-param typed
* *i64, caller reads i64 → MOVQ store/load pair agree, -16
* round-trips intact. When #19 lands, add a sibling row that
* exercises the natural `*p: *i32` shape and expects -16. */
* round-trips intact. */
{ "i64_out_param_neg_widen_deref_workaround",
"fn setneg(p: *i64) void = { *p = -16i64; };\n"
"fn main() i32 = {\n"
@@ -196,6 +195,95 @@ static const struct row rows[] = {
" return 0;\n"
"};\n",
42 },
/* Task #19 fix. Natural `*p: *i32` shape — `*p = -16i32` lowers
* to MOVL (correct 4B store to a 4B pointee), and the caller's
* later i64-widening read must MOVSXD the slot so the sign bit
* propagates. Without the fix, the MOVQ read of the slot returns
* 0x00000000_FFFFFFF0 (4294967280) and the equality fails. */
{ "i32_out_param_neg_widen_deref",
"fn setneg(p: *i32) void = { *p = -16i32; };\n"
"fn main() i32 = {\n"
" let x: i32 = 0i32;\n"
" setneg(&x);\n"
" let z: i64 = x: i64;\n"
" if (z == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Task #19 fix, i16 sibling — `*p: *i16 = -16i16` writes 2B,
* caller widens to i64. Without MOVSWQ the read sees 0xFFF0
* (65520). */
{ "i16_out_param_neg_widen_deref",
"fn setneg(p: *i16) void = { *p = -16i64: i16; };\n"
"fn main() i32 = {\n"
" let x: i16 = 0i16;\n"
" setneg(&x);\n"
" let z: i64 = x: i64;\n"
" if (z == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Task #19 fix, i8 sibling — `*p: *i8 = -16i8` writes 1B, caller
* widens to i64. Without MOVSBQ the read sees 0xF0 (240). */
{ "i8_out_param_neg_widen_deref",
"fn setneg(p: *i8) void = { *p = -16i64: i8; };\n"
"fn main() i32 = {\n"
" let x: i8 = 0i8;\n"
" setneg(&x);\n"
" let z: i64 = x: i64;\n"
" if (z == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Task #19 fix, top-level let target. Same shape but the local
* `x` is replaced by a top-level let so the read goes through
* the RIP-relative path. Without the fix the read is MOVQ from
* &x(SB) and sees zero-extended garbage. */
{ "i32_global_neg_widen_deref",
"let g: i32 = 0i32;\n"
"fn setneg(p: *i32) void = { *p = -16i32; };\n"
"fn main() i32 = {\n"
" setneg(&g);\n"
" let z: i64 = g: i64;\n"
" if (z == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Task #19 fix, compound RMW on a narrow local that was last
* written via a deref-store. Pins the cgassign restructure that
* routes the load half of `x OP= v` through localloadop (so the
* upper bytes feeding the combine come from a sign-extending
* load, not raw 8B off the slot) and gates the direct-mem
* ADDQ/SUBQ shortcut on `load_op == MOVQ`. Reads x as i32 to
* exercise the in-band path without an explicit i64 widen. */
{ "i32_compound_rmw_after_deref_store",
"fn setv(p: *i32, v: i32) void = { *p = v; };\n"
"fn main() i32 = {\n"
" let x: i32 = 0i32;\n"
" setv(&x, 0x7FFFFFFEi32);\n"
" x += 1i32;\n"
" if (x == 0x7FFFFFFFi32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Task #19 fix, aliased narrow read. `type myinv = !i32` wraps
* i32 with the err flag — its tnode is N_TBANG(N_TNAME("i32")).
* Pins the TBANG/TENUM/TNAME → aliaslookup loop inside the
* wwstage `localloadop`: without it `fieldsize` falls back to 8
* on the bare alias name and the read picks MOVQ, so the deref-
* stored -16 round-trips as 4294967280 instead of -16. cstage
* resolves the alias through Type.size and type_isunsigned. */
{ "alias_bang_i32_widen_deref",
"type myinv = !i32;\n"
"fn setneg(p: *myinv) void = { *p = -16i64: myinv; };\n"
"fn main() i32 = {\n"
" let x: myinv = 0i64: myinv;\n"
" setneg(&x);\n"
" let z: i64 = x: i64;\n"
" if (z == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
};
static int