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

@@ -381,6 +381,20 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
return off;
};
// localaddstack — register a param at a positive BP offset. Used for
// args that overflow the 6 SysV int / 8 float reg windows; the caller
// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP),
// etc. (after the saved RIP+BP). No spill instruction is emitted; the
// slot IS the caller's stack slot.
fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = off;
l.tnode = tnode;
l.lnext = c.locals;
c.locals = l;
};
fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// Name-based slot reuse for N_LETs and params: if `name` is
// already declared in this function, return its existing
@@ -634,6 +648,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
for (t != nil) {
if (t.kind == nkind.N_TPTR) { return 8; };
if (t.kind == nkind.N_TSLICE) { return 24; };
if (t.kind == nkind.N_TARRAY) {
let lenn: *node = t.rhs;
let elemn: *node = t.lhs;
let alen: i32 = 1;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
return alen * esz;
};
if (t.kind != nkind.N_TNAME) { return 0; };
let nm: str = t.str;
if (letscalarprim(nm)) { return 8; };
@@ -683,6 +713,19 @@ fn isletvar(c: *cgen, name: str) bool = {
// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// MOVQ scalar load.
// letvartnode — direct lookup of a top-level let's tnode. Used by
// cgindex / cgassign to detect global `[N]T` arrays and `*T`
// pointers, where the addressing path needs LEAQ name(SB) (array)
// or MOVQ name(SB) (pointer) and the element size from T.
fn letvartnode(c: *cgen, name: str) *node = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) { return lv.tnode; };
lv = lv.lvnext;
};
return nil;
};
fn letvarisstr(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
@@ -1055,6 +1098,73 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitline("\"\n");
};
};
// Top-level `[N]T = [a, b, ...]` array global.
// Emits N*esz bytes with each element's bytes
// little-endian for the declared primitive width.
// Without this, `let arr: [N]T = ...` references
// from function bodies link-fail with `undefined
// reference to arr`, and bare-name addressing
// (LEAQ arr(SB)) inside cgindex / cgassign has no
// symbol to bind to.
if (d.lhs != nil) {
if (d.lhs.kind == nkind.N_TARRAY) {
let elemn: *node = d.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let total: i32 = sz;
let alen: i32 = total / esz;
let elems: *node = nil;
if (d.rhs != nil) {
if (d.rhs.kind == nkind.N_ARRLIT) {
elems = d.rhs.list;
};
};
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
let e: *node = elems;
let fillv: u64 = 0u64;
let inrepeat: bool = false;
for (i < alen) {
let v: u64 = fillv;
if (!inrepeat && e != nil) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
// `..., ...` repeat marker: prior v stays.
inrepeat = true;
} else {
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_INTLIT) { v = e.lhs.uval; };
if (e.lhs.kind == nkind.N_RUNELIT) { v = e.lhs.uval; };
};
fillv = v;
e = e.next;
};
} else {
if (e.kind == nkind.N_INTLIT) { v = e.uval; };
if (e.kind == nkind.N_RUNELIT) { v = e.uval; };
fillv = v;
e = e.next;
};
};
let nb: u64 = v;
let b: i32 = 0;
for (b < esz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
b += 1;
};
i += 1;
};
emitline("\"\n");
};
};
};
};
d = d.next;