diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f9af05cc..8060de35 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 diff --git a/selfhost/CLAUDE.md b/selfhost/CLAUDE.md index 3847e68e..bcbf4a20 100644 --- a/selfhost/CLAUDE.md +++ b/selfhost/CLAUDE.md @@ -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. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 245e128c..83c55e73 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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"); }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 9b1cf6ac..e0bfb5cc 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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"); }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index e6d67248..fa3c07ec 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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 { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1ae12709..98290cbf 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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"); }; diff --git a/test/wcc/660_field_signed.c b/test/wcc/660_field_signed.c index b7c4004c..807bb98d 100644 --- a/test/wcc/660_field_signed.c +++ b/test/wcc/660_field_signed.c @@ -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