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