ww: cgen trap batch (def-str field, chained-ptr write, scalar+str tuple ABI)

This commit is contained in:
2026-05-11 20:49:21 +09:00
parent 2ac15e4e99
commit c5f30f2fce
14 changed files with 1629 additions and 67 deletions

View File

@@ -2251,9 +2251,17 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_DOT, pf, pl, pc);
n.lhs = cur;
let id: str;
expectident(p, &id);
n.str = id;
// Hare-style tuple field access: `t.0`, `t.1`. The
// numeric literal becomes the field name string so the
// cgen tuple-positional path matches `cmd/wcc/parse.c`.
if (p.curkind == TK_INT) {
n.str = p.curtext;
advance(p);
} else {
let id: str;
expectident(p, &id);
n.str = id;
};
cur = n;
continue;
};
@@ -2374,6 +2382,42 @@ fn parseletlocal(p: *parser) *node = {
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
// Hare-style tuple destructure: `let (a, b) = expr;`.
// Types are optional per binding (matches C parser; Hare itself
// doesn't allow types here, but cmd/wcc/parse.c does).
if (p.curkind == TK_LPAREN) {
advance(p);
let m: *node = newnode(p.a, N_MLET, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (true) {
let lpf: str = p.curfile;
let lpl: i32 = p.curline;
let lpc: i32 = p.curcol;
let l: *node = newnode(p.a, N_LET, lpf, lpl, lpc);
let id: str;
expectbindname(p, &id);
l.str = id;
if (accepttok(p, TK_COLON)) { l.lhs = parsetype(p); };
if (head == nil) { head = l; }
else { tail.next = l; };
tail = l;
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RPAREN, "expected ')' in let destructure");
expecttok(p, TK_ASSIGN, "expected '=' after let destructure");
m.rhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after let");
m.list = head;
if (is_const != 0) {
m.op = TK_CONST;
let lc: *node = head;
for (lc != nil) { lc.op = TK_CONST; lc = lc.next; };
};
return m;
};
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectbindname(p, &id);
@@ -2381,6 +2425,36 @@ fn parseletlocal(p: *parser) *node = {
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
};
// Comma-multi-let: `let n, s = call();` (ww extension over Hare).
// Collects (name, type) pairs, then '=' rhs. Each binding gets
// its own N_LET; the wrapping N_MLET carries the rhs.
if (p.curkind == TK_COMMA) {
let m: *node = newnode(p.a, N_MLET, pf, pl, pc);
let head: *node = n;
let tail: *node = n;
for (accepttok(p, TK_COMMA)) {
let lpf: str = p.curfile;
let lpl: i32 = p.curline;
let lpc: i32 = p.curcol;
let l: *node = newnode(p.a, N_LET, lpf, lpl, lpc);
let id2: str;
expectbindname(p, &id2);
l.str = id2;
if (accepttok(p, TK_COLON)) { l.lhs = parsetype(p); };
tail.next = l;
tail = l;
};
expecttok(p, TK_ASSIGN, "expected '=' after let names");
m.rhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after let");
m.list = head;
if (is_const != 0) {
m.op = TK_CONST;
let lc: *node = head;
for (lc != nil) { lc.op = TK_CONST; lc = lc.next; };
};
return m;
};
if (accepttok(p, TK_ASSIGN)) {
n.rhs = parseexpr(p);
};
@@ -4368,6 +4442,23 @@ fn structlookup(c: *cgen, name: str) *structinfo = {
// primsize — size in bytes of a primitive type name (or 0 if not
// recognised as a primitive — the caller falls back to other paths).
// fldnumidx — parse a tuple field name like "0" / "1" / "12" into an
// index, or -1 if not all-digits. Used by cgdot to dispatch
// `t.0` / `t.1` against an N_TTUPLE local without pulling in strconv.
fn fldnumidx(s: str) i32 = {
if (s.len == 0) { return -1; };
let r: i32 = 0;
let i: i32 = 0;
for (i < s.len) {
let b: u8 = s[i];
if (b < 48u8) { return -1; };
if (b > 57u8) { return -1; };
r = r * 10 + ((b - 48u8): i32);
i += 1;
};
return r;
};
fn primsize(name: str) i32 = {
if (streq(name, "u8")) { return 1; };
if (streq(name, "i8")) { return 1; };
@@ -4441,7 +4532,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
if (k == N_TFN) { return 8; };
if (k == N_TCHAN) { return 8; };
if (k == N_TSLICE) { return 24; };
if (k == N_TTUPLE) { return 16; };
if (k == N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p);
p = p.next;
};
return total;
};
if (k == N_TTAGGED){ return 24; };
if (k == N_TNAME) {
let nm: str = typn.str;
@@ -5100,6 +5203,49 @@ fn cgdot(c: *cgen, n: *node) void = {
return;
};
};
// Hare-style tuple positional access: `t.0`, `t.1`.
// Walk the tuple element type list summing slotsize
// (matches the (scalar, str) init layout which puts
// the scalar in an 8B slot and the str in 16B). For
// a str element, load both halves into (AX, BX) so
// chains like `t.1.len` propagate correctly.
if (lkind == N_TTUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
let tp: *node = tn.list;
let foff: i32 = 0;
let i: i32 = 0;
for (i < idx) {
if (tp == nil) { i = idx; }
else {
foff += slotsize(c, tp);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
if (isstrtyperaw(tp)) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 8): i64);
emitline("(BP), BX\n");
return;
};
let sz: i32 = slotsize(c, tp);
let op: str = "MOVQ";
if (sz == 1) { op = "MOVZBQ"; }
else { if (sz == 4) { op = "MOVL"; }; };
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
};
};
// str/slice pseudo-fields .ptr/.len/.cap on a
// direct local: load at slot+delta.
let delta: i32 = -1;
@@ -5730,6 +5876,71 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// Chained `<expr>.field = v` where `<expr>` itself is a chain
// of dots resolving to a *struct. Mirrors the C cgen branch
// added to close trap 1 (cmd/w6c/cgen.c). Without this, only
// `local.field = v` and `local.fieldptr.field = v` get wired
// (the latter through the IDENT-base branch above) — chains
// like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no
// store. Only plain `=` is wired here; chained compound on a
// pointer-field hasn't surfaced.
if (lhs != nil) {
if (lhs.kind == N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
let sname: str = innert.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
if (n.op == TK_ASSIGN) {
if (isstrtype(c, fi.tnode)) {
// str rhs: AX=ptr, BX=len.
// Stash both, then load
// the struct ptr into CX
// and write both halves.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, CX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
};
fi = fi.finext;
};
};
};
};
};
};
};
// Local-ident target — plain `=` and the simple compound
// forms (+= -= *= /=); other compounds fall back to
// "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path.
@@ -5833,6 +6044,8 @@ fn cgstmt(c: *cgen, n: *node) void = {
if (k == N_MASSIGN) { cgmassign(c, n); return; };
if (k == N_MLET) { cgmlet(c, n); return; };
if (k == N_BREAK) { cgbreak(c, n); return; };
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
@@ -5851,18 +6064,35 @@ fn cgblock(c: *cgen, n: *node) void = {
fn cgreturn(c: *cgen, n: *node) void = {
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;` — pack as (AX=v0, DX=v1).
// Matches C cgen: evaluate v1 first (PUSHQ), then v0
// into AX, then POPQ DX. End state: AX = v0, DX = v1.
// Tuple return `return a, b;`:
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len.
// 24B convention mirrors the tagged-union return below; receive
// sites destructure off the same regs regardless of position.
if (rhs.kind == N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
};
@@ -5984,6 +6214,50 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
// 24B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len.
// Layout is positional, so we route each register to the
// slot dictated by element type, not by AX/DX position.
if (n.lhs != nil) {
if (n.lhs.kind == N_TTUPLE) {
let p0: *node = n.lhs.list;
let p1: *node = nil;
if (p0 != nil) { p1 = p0.next; };
let s0_is_str: bool = isstrtyperaw(p0);
let s1_is_str: bool = isstrtyperaw(p1);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
@@ -6256,6 +6530,109 @@ fn cgmassign(c: *cgen, n: *node) void = {
return;
};
// Multi-let from a tuple-returning call: `let n, s = call();` or
// `let (n, s) = call();`. wwstage has no checker, so each binding's
// type is taken from its explicit annotation (l.lhs) when present
// or inferred from the called fn's return-type tuple element.
//
// Per the AX:DX:CX return convention (mirrors C cgen N_MLET):
// (scalar, scalar) — AX → l0, DX → l1.
// (scalar, str) — AX → scalar slot, (DX, CX) → str slot
// as (.ptr, .len). Position-agnostic — the
// regs are routed by element type, not by AX/DX.
fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == N_IDENT) { cnm = callee.str; };
if (callee.kind == N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtyperaw(t0);
let s1_is_str: bool = isstrtyperaw(t1);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = 16; };
if (s1_is_str) { sz1 = 16; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
fn cgbreak(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopendbuf[c.looptop - 1];
@@ -6318,6 +6695,54 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
total += sz;
};
};
// Multi-let from a tuple-returning call: each binding's size
// comes from its annotated type (l.lhs) when present, else from
// the rhs call's return-tuple element type. Marking via
// scanseenmark also dedupes the recursive descent into n.list
// so each child isn't counted again at the default 8B.
if (n.kind == N_MLET) {
let p0t: *node = nil;
let p1t: *node = nil;
if (n.rhs != nil) {
if (n.rhs.kind == N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == N_IDENT) { cnm = callee.str; };
if (callee.kind == N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
};
let l: *node = n.list;
let pt: *node = p0t;
let bidx: i32 = 0;
for (l != nil) {
if (!scanseenmark(c, l.str)) {
let t: *node = l.lhs;
if (t == nil) {
if (bidx == 0) { t = p0t; };
if (bidx == 1) { t = p1t; };
};
let sz: i32 = 8;
if (t != nil) { sz = slotsize(c, t); };
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
};
l = l.next;
bidx += 1;
};
};
// Match-arm binding (`case let v: T => ...`) gets a slot too.
// Crucially we do NOT dedup these against c.locals: C cgen
// handles a match as an expression with a by-value locals copy,