selfhost: fix several wwstage cgen miscompilations

Surfaced via examples/lisp, which had to work around the following in
source. Each lowering now matches cstage on the same shape.

- cgassign / cgdot: two-level field through a non-pointer sub-struct.
  `(*L).cur.kind = k` (cur a struct-by-value field of L) silently
  dropped the store; the corresponding read fell into the SB-symbol
  fallback and the linker reported `undefined reference to kind`. The
  two new branches resolve outer-field offset + inner-field offset
  and emit a single direct store/load at the combined slot, both for
  T-by-value and *T-base shapes.

- cgdot: `xs[i].field` chains the trailing field load through the
  N_INDEX result for [N]T / []T / *T element-of-struct-ptr. The
  cgforrange loop variable now carries the elem tnode so the same
  fast path covers `for (let x .. xs) { x.field }`.

- cgindex / cgassign: top-level `[N]T` array and `*T` pointer used
  as an index base. cgindex now emits LEAQ name(SB) (array) or
  MOVQ name(SB) (pointer) with the correct element scaling; without
  this the fallback emitted neither base and walked off the saved
  BP slot. Adds letvartnode() helper, an N_TARRAY branch to
  letemitsize so the array shows up in c.lets, and an N_TARRAY
  initialiser path in emitletdataw that lays the literal bytes into
  DATAW.

- cglet / scanlocals: infer the local's tnode for an unannotated
  `let x = f()` / `let x = f()?`. inferletcalltype() reads the
  callee's declared return; `?` and `!` strip to the success variant
  so a tagged-union let allocates the full 24B slot and the
  struct-field dispatch in cgdot/cgassign sees the right type.
  letslotsize now defers to slotsize on the inferred type.

- slotsize: follow type aliases for tagged-union variants. With
  `type parserr = !str;`, the variant slot was 8B instead of the
  required 16B; the tagged let stomped on the next slot at the
  AX/DX/CX spill.

- cgreturn: tagged-union return forwarding. `return f();` where f
  also returns a tagged union now passes the (tag, payload1,
  payload2) triple through unchanged instead of re-wrapping it.

- cgreturn / cglet / taggedvariantindex: dispatch by variant name
  with module-qualified-vs-bare matching, and recognise N_STRUCTLIT
  as the variant tag for `return eof{};`. cgexpr default emits
  `MOVQ $0, AX` so the surrounding return shuffle isn't left with
  a stale AX.

- isstrtype / nodeisstr: resolve through `!T` aliases. `parserr =
  !str` was not propagating the str-shape to the rhs check and the
  MOVQ BX,CX shuffle was being dropped from str-typed local
  returns.

- exprfloatkind: recognise `p.field` as f64/f32 when the struct
  field is so declared, so `v.fval: i64` lowers to CVTTSD2SI on X0.

- cgassign: str field on a direct struct local writes both halves.
  `L.src = s;` previously dropped s.len.

- cgcall: pop into the int reg window only up to 6 (DI..R9); rest
  stays on the stack and the caller emits ADDQ to clean up.
  cgfnparams accepts >6-arg signatures by registering the overflow
  params at positive BP offsets (16+8*k(BP)), no spill instruction
  emitted.

All 26 harness tests pass; bootstrap reaches a byte-stable fixed
point at ww3 == ww4.
This commit is contained in:
2026-05-13 03:06:46 +09:00
parent ebfd8c3652
commit 7c75dd218a
5 changed files with 854 additions and 108 deletions

View File

@@ -143,6 +143,25 @@ fn cgreturn(c: *cgen, n: *node) void = {
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c.fnret)) {
// Forwarding a fallible call: `return f();` where f
// also returns a tagged union. The result is already
// in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag.
// Mirrors the rhsreturnstagged path in cglet and the
// !type_istagged guard in C cgen's N_RETURN.
let forwardtagged: bool = false;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
if (callee.kind == nkind.N_IDENT) { calleename = callee.str; };
if (callee.kind == nkind.N_DOT) { calleename = callee.str; };
if (calleename.len > 0) {
let rt: *node = fnretlookup(c, calleename);
if (istaggedtype(rt)) { forwardtagged = true; };
};
};
};
cgexpr(c, rhs);
if (isnullabletype(c.fnret)) {
emitline("\tMOVQ\tBP, SP\n");
@@ -151,6 +170,13 @@ fn cgreturn(c: *cgen, n: *node) void = {
c.lastwasreturn = 1;
return;
};
if (forwardtagged) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
@@ -213,7 +239,12 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
// `let x = f()?` has no annotation but the cgen's struct-field
// paths need a tnode to dispatch off. Infer from f's tagged
// success variant — see inferletcalltype.
let tn: *node = n.lhs;
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
let off: i32 = localadd(c, nm, sz, tn);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
// Tagged-union init: `let r: (T | E) = expr;`.
@@ -222,8 +253,8 @@ fn cglet(c: *cgen, n: *node) void = {
// just spill all three.
// - Otherwise rhs is a bare variant value: pack tag +
// value(s).
if (istaggedtype(n.lhs)) {
let nullable: bool = isnullabletype(n.lhs);
if (istaggedtype(tn)) {
let nullable: bool = isnullabletype(tn);
let rhsreturnstagged: bool = false;
if (rhs.kind == nkind.N_CALL) {
let callee: *node = rhs.lhs;
@@ -266,7 +297,7 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
let tagidx: i32 = taggedvariantindex(c, n.lhs, rhs);
let tagidx: i32 = taggedvariantindex(c, tn, rhs);
if (tagidx < 0) { tagidx = 0; };
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tAX, ");
@@ -841,9 +872,9 @@ fn cgforrange(c: *cgen, n: *node) void = {
bind_signed[nbinds] = signf;
let bnm: str = m.str;
if (bnm.len > 0) {
bind_off[nbinds] = localadd(c, bnm, slot_sz, nil);
bind_off[nbinds] = localadd(c, bnm, slot_sz, tp);
} else {
bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil);
bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tp);
};
field_off += fsz;
nbinds += 1;
@@ -863,9 +894,12 @@ fn cgforrange(c: *cgen, n: *node) void = {
bind_signed[0] = paramissigned(elemt);
};
if (n.str.len > 0) {
bind_off[0] = localadd(c, n.str, slot_sz, nil);
// Register with elem tnode so x.field on a loop
// var resolves through the standard local-typed
// path instead of falling into the SB fallback.
bind_off[0] = localadd(c, n.str, slot_sz, elemt);
} else {
bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil);
bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt);
};
nbinds = 1;
};