From c5f30f2fce6c8a215d3f05aa955c7fe76b7fe5cf Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 20:49:21 +0900 Subject: [PATCH] ww: cgen trap batch (def-str field, chained-ptr write, scalar+str tuple ABI) --- cmd/w6c/cgen.c | 234 +++++++++++++- lib/ww/CLAUDE.md | 2 +- lib/ww/parse/expr.ww | 14 +- lib/ww/parse/stmt.ww | 66 ++++ selfhost/CLAUDE.md | 34 +- selfhost/cmd/w6c/main.combined.ww | 447 ++++++++++++++++++++++++++- selfhost/cmd/w6l/main.combined.ww | 11 +- selfhost/cmd/w6l/pass.ww | 11 +- selfhost/cmd/wcc/cgendecl.ww | 48 +++ selfhost/cmd/wcc/cgenexpr.ww | 108 +++++++ selfhost/cmd/wcc/cgenstmt.ww | 180 ++++++++++- selfhost/cmd/wcc/cgenutil.ww | 31 +- selfhost/cmd/wwdump/main.combined.ww | 447 ++++++++++++++++++++++++++- test/wcc/700_e2e.c | 63 ++++ 14 files changed, 1629 insertions(+), 67 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 9151e048..a74a638b 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1038,6 +1038,111 @@ cgexpr(Cg *c, Node *n, Local *locals) break; } } + /* Chained `.field = v` where evaluates to a *struct. + * cgexpr on the inner expression already returns the pointer; + * we then store at (ptr + field.offset). Without this, only the + * single-level N_IDENT base above is wired and shapes like + * `r.sym.flag = 1` (where r.sym: *T) silently emit no store — + * the read still works because the chained-N_DOT read path is + * wired below. (This was trap 1 of the cgen miscompilations.) */ + if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs + && n->lhs->lhs->kind != N_IDENT) { + Type *bt = n->lhs->lhs->type; + Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + if (bu && bu->kind == TY_PTR && bu->sub) { + Type *inner = bu->sub; + if (inner->kind == TY_NAMED) inner = inner->under; + if (inner && inner->kind == TY_STRUCT) { + Tfield *f = NULL; + for (Tfield *fl = inner->fields; fl; fl = fl->next) + if (strcmp(fl->name, n->lhs->str) == 0) + { f = fl; break; } + if (f != NULL) { + Type *ft = f->type; + Type *fu = (ft && ft->kind == TY_NAMED) + ? ft->under : ft; + int fsz = (int)(ft ? ft->size : 8); + int signed_field = ft && ( + ft->kind == TY_I8 || + ft->kind == TY_I16 || + ft->kind == TY_I32); + int store_op = A_MOVQ; + if (fsz == 1) store_op = A_MOVB; + else if (fsz == 4) store_op = A_MOVL; + int foff = (int)f->offset; + if (n->op == TK_ASSIGN) { + if (fu && fu->kind == TY_STR) { + /* str rhs: (AX=ptr, BX=len). Stash + * both, then load the struct ptr + * into CX and write both halves. */ + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs->lhs, locals); + ins2(c, A_MOVQ, areg(D_AX), + areg(D_CX)); + ins1(c, A_POPQ, areg(D_AX)); + ins1(c, A_POPQ, areg(D_BX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_CX, foff + 0)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_CX, foff + 8)); + } else { + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs->lhs, locals); + ins2(c, A_MOVQ, areg(D_AX), + areg(D_BX)); + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, store_op, areg(D_AX), + amem(D_BX, foff)); + } + break; + } + /* compound op: AX=rhs → push; eval ptr → push; + * load old field → AX; pop ptr→BX, rhs→CX; + * combine; store. Float/str compound on a + * chained pointer-field is not wired. */ + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs->lhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + int load_op = A_MOVQ; + if (fsz == 1) load_op = A_MOVZBQ; + else if (fsz == 4) + load_op = signed_field ? A_MOVSXD : A_MOVL; + ins2(c, load_op, amem(D_AX, foff), + areg(D_AX)); + ins1(c, A_POPQ, areg(D_BX)); + ins1(c, A_POPQ, areg(D_CX)); + switch (n->op) { + case TK_PLUSEQ: + ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); + break; + case TK_MINUSEQ: + ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); + break; + case TK_STAREQ: + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + break; + case TK_AMPEQ: + ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); + break; + case TK_PIPEEQ: + ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); + break; + case TK_CARETEQ: + ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); + break; + default: break; + } + ins2(c, store_op, areg(D_AX), + amem(D_BX, foff)); + break; + } + } + } + } /* float assignment to a local */ if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) { cgexpr(c, n->rhs, locals); /* X0 */ @@ -1887,6 +1992,29 @@ cgexpr(Cg *c, Node *n, Local *locals) && (lenfld || capfld || ptrfld)) { if (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); + if (off == 0) { + /* Not a local — could be `def NAME: str + * = "lit"`. Sdef-backed strs aren't laid + * out in memory; emit .ptr/.len from the + * literal directly, mirroring the bare + * N_IDENT branch above. Without this we'd + * load BP+8 (return-address slot) as the + * "len". */ + for (Sdef *s = sdefs; s; s = s->next) { + if (strcmp(s->name, n->lhs->str) != 0) + continue; + if (ptrfld) { + const char *lab = intern_strlit(c, + s->bytes, s->len); + ins2(c, A_LEAQ, asym(lab), areg(D_AX)); + } else { + ins2(c, A_MOVQ, + aimm((long long)s->len), + areg(D_AX)); + } + goto dot_done; + } + } int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_MOVQ, amem(D_BP, off + delta), areg(D_AX)); @@ -1925,6 +2053,8 @@ cgexpr(Cg *c, Node *n, Local *locals) } if (tp != NULL) { int fsz = (int)(tp->type ? tp->type->size : 8); + Type *fu = (tp->type && tp->type->kind == TY_NAMED) + ? tp->type->under : tp->type; int signed_field = tp->type && ( tp->type->kind == TY_I8 || tp->type->kind == TY_I16 || @@ -1933,6 +2063,15 @@ cgexpr(Cg *c, Node *n, Local *locals) if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; int off = localfind(locals, n->lhs->str); + /* str element: load (ptr, len) into (AX, BX) so chains + * like `t.1.len` propagate through the str-rhs + * convention. Without this we'd MOVQ 8B and the .len + * shuffle (BX→AX) would surface garbage. */ + if (fu && fu->kind == TY_STR) { + ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX)); + break; + } ins2(c, op, amem(D_BP, off + foff), areg(D_AX)); } break; @@ -2225,6 +2364,37 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); break; } + /* 24B tuple initialiser for `(scalar, str)` / `(str, scalar)`. + * Per the AX:DX:CX return convention: AX = scalar elem, + * DX = str.ptr, CX = str.len. The slot is laid out positionally + * (e0 at +0, e1 at +8 for scalars; str takes 16B starting at + * its position), so we route each register to the slot dictated + * by the element's type, not by AX/DX position. */ + if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 24) { + Tparam *p0 = lu->params; + Tparam *p1 = p0 ? p0->next : NULL; + Type *t0 = p0 ? p0->type : NULL; + Type *t1 = p1 ? p1->type : NULL; + Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0; + Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1; + int e0_str = u0 && u0->kind == TY_STR; + int e1_str = u1 && u1->kind == TY_STR; + if (e0_str ^ e1_str) { + cgexpr(c, n->rhs, *locals); + if (e0_str) { + /* layout: str@+0 (16B), scalar@+16. */ + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 16)); + } else { + /* layout: scalar@+0 (8B), str@+8 (16B). */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); + } + break; + } + } /* Tagged-union initialiser. Two shapes: * 1) rhs already produces a tagged-union value (e.g. a fn * call returning (T | E)). cgexpr leaves AX=tag, @@ -2496,14 +2666,34 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } if (n->lhs && n->lhs->kind == N_TUPLE) { - /* small tuples (≤2 elems, ≤8B each) return in AX:DX */ + /* 2-tuple ABI: + * (scalar, scalar) — AX = e0, DX = e1. (16B, fits SysV.) + * (scalar, str) — AX = scalar elem, + * DX = str.ptr, CX = str.len. (24B custom.) + * (str, scalar) — same regs, type-keyed not position-keyed. + * + * The 24B convention mirrors the existing tagged-union return + * (AX:DX:CX); receive sites destructure off the same regs. */ Node *e0 = n->lhs->list; Node *e1 = e0 ? e0->next : NULL; if (e1 && e1->next == NULL) { - cgexpr(c, e1, *locals); - ins1(c, A_PUSHQ, areg(D_AX)); - cgexpr(c, e0, *locals); - ins1(c, A_POPQ, areg(D_DX)); + int e0_is_str = node_isstr(e0); + int e1_is_str = node_isstr(e1); + if (e0_is_str ^ e1_is_str) { + Node *strn = e0_is_str ? e0 : e1; + Node *scaln = e0_is_str ? e1 : e0; + cgexpr(c, scaln, *locals); /* AX = scalar */ + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, strn, *locals); /* AX=ptr, BX=len */ + ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); + ins2(c, A_MOVQ, areg(D_AX), areg(D_DX)); + ins1(c, A_POPQ, areg(D_AX)); + } else { + cgexpr(c, e1, *locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, e0, *locals); + ins1(c, A_POPQ, areg(D_DX)); + } } else { /* >2-tuple not yet implemented; fall back to first elem */ if (e0) cgexpr(c, e0, *locals); @@ -2681,11 +2871,41 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } case N_MLET: { - /* eval rhs; first result in AX, second in DX. Bind both. */ + /* eval rhs; consume the per-type return-ABI registers. + * (scalar, scalar) — AX → l0, DX → l1. + * (scalar, str) — AX → scalar slot, (DX, CX) → str slot + * as (.ptr, .len). Position-agnostic. + * Local sizing comes from each l->type so the str slot gets + * the full 16B; without this, only DX would land and the + * len half (CX) would have nowhere to go. */ cgexpr(c, n->rhs, *locals); - ins1(c, A_PUSHQ, areg(D_DX)); /* save 2nd while we store 1st */ Node *l0 = n->list; Node *l1 = l0 ? l0->next : NULL; + Type *t0 = l0 ? l0->type : NULL; + Type *t1 = l1 ? l1->type : NULL; + Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0; + Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1; + int s0_is_str = u0 && u0->kind == TY_STR; + int s1_is_str = u1 && u1->kind == TY_STR; + if (l0 && l1 && (s0_is_str ^ s1_is_str)) { + int sz0 = s0_is_str ? 16 : 8; + int sz1 = s1_is_str ? 16 : 8; + int off0 = localoff(c, locals, l0->str, sz0, frame); + int off1 = localoff(c, locals, l1->str, sz1, frame); + if (s0_is_str) { + /* l0 is str: ptr=DX, len=CX. l1 is scalar: l1 = AX. */ + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off0 + 0)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off0 + 8)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off1)); + } else { + /* l0 is scalar; l1 is str. */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off1 + 0)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off1 + 8)); + } + break; + } + ins1(c, A_PUSHQ, areg(D_DX)); /* save 2nd while we store 1st */ if (l0) { int off = localoff(c, locals, l0->str, 8, frame); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); diff --git a/lib/ww/CLAUDE.md b/lib/ww/CLAUDE.md index a1fba70b..9955e864 100644 --- a/lib/ww/CLAUDE.md +++ b/lib/ww/CLAUDE.md @@ -10,4 +10,4 @@ This directory is **not** a Hare-style stdlib module — it's a compiler-fronten When porting from `cmd/wcc/*.c`: - Keep the same data layout. The selfhost goal is byte-compatible structures so dump/inspect tools work either way. - Read `cmd/wcc/ww.h` first for canonical field names and ordering. -- See `selfhost/CLAUDE.md` for the wwstage cgen traps to avoid (two-level field write, `(scalar,str)` tuple return, `def : str`, undersized `amalloc`). +- See `selfhost/CLAUDE.md` for the wwstage cgen traps to avoid (undersized `amalloc`). diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index 513379d6..2e40837d 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -281,9 +281,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; }; diff --git a/lib/ww/parse/stmt.ww b/lib/ww/parse/stmt.ww index f560fa4a..2b8e7386 100644 --- a/lib/ww/parse/stmt.ww +++ b/lib/ww/parse/stmt.ww @@ -12,6 +12,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); @@ -19,6 +55,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); }; diff --git a/selfhost/CLAUDE.md b/selfhost/CLAUDE.md index 518449ca..144e1637 100644 --- a/selfhost/CLAUDE.md +++ b/selfhost/CLAUDE.md @@ -4,22 +4,28 @@ Identifiers: Plan 9 style. lowercase, words run together (`newbuf`, `tcpsock`, ` Syntax and idioms: Hare-shaped. Trailing `;`, `=` after fn/type signatures, `export` for visibility, `match`/`?`/`!` for tagged-union errors. Consult `ref/hare/` for canonical signatures and error-handling patterns before inventing your own. -The C bootstrap's cgen ("wwstage") has four known silent-miscompilation traps. They produce wrong runtime behavior, not compile errors. When porting C → ww here, default to the workarounds: +The C bootstrap's cgen has known silent-miscompilation traps. They produce wrong runtime behavior, not compile errors. When porting C → ww here, default to the workarounds: -1. **Two-level field write through pointer field doesn't stick.** - `r.sym.isdyn = 1` where `r.sym: *T` drops the write. Bind the inner pointer to a local first: - ``` - let sym: *lsym = r.sym; - sym.isdyn = 1; - ``` +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. **Tuple return `(scalar, str)` corrupts the str half.** Both ptr and len come back garbage. Split into two functions — one returns the scalar, another returns the str. Plain `str` returns are fine. +Fixed (no workaround needed): -3. **`def NAME: str = "...";` is broken.** The `.len` picks up an unrelated accumulator. Wrap the literal in a nullary fn instead: - ``` - fn namestr() str = { return "..."; }; - ``` - -4. **`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. +- `def NAME: str = "..."` field access. `.len`/`.ptr` on an Sdef ident + now inline the literal length / strlit address rather than reading + BP+8. See cmd/w6c/cgen.c N_DOT. +- Two-level field write through pointer field: `r.sym.isdyn = 1` where + `r.sym: *T` now stores. The chained-N_DOT N_ASSIGN branch evaluates + the inner pointer and stores at *(ptr + field.offset). See + cmd/w6c/cgen.c N_ASSIGN and selfhost/cmd/wcc/cgenexpr.ww cgassign. +- Tuple return `(scalar, str)` (24B). Returns now follow an AX:DX:CX + convention: AX = scalar elem, DX = str.ptr, CX = str.len. Receive + sites all destructure off the same regs regardless of positional + order: + let n, s = call(); // ww comma form + let (n, s) = call(); // Hare-style paren form + let t: (i64, str) = call(); // positional t.0 / t.1.len + Wwstage parser + cgen are byte-identical to C cgen on these shapes. + See cmd/w6c/cgen.c N_RETURN/N_LET/N_DOT/N_MLET, lib/ww/parse/{stmt, + expr}.ww and selfhost/cmd/wcc/{cgenstmt,cgenexpr,cgenutil,cgendecl}.ww. 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 82628b5e..d7d6e484 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 `.field = v` where `` 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, diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index ce415750..b1d1d88d 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -1345,14 +1345,13 @@ export fn resolve(l: *lnk) i32 = { for (r != nil) { if (r.sym != nil) { if (r.sym.defined == 0) { - let sym: *lsym = r.sym; - if (sym.isdyn == 0) { + if (r.sym.isdyn == 0) { let so: *lso = l.sos; for (so != nil) { - if (soprovides(so, sym.name) != 0) { - sym.isdyn = 1; - sym.dynlib = so; - sym.pltidx = l.dynn; + if (soprovides(so, r.sym.name) != 0) { + r.sym.isdyn = 1; + r.sym.dynlib = so; + r.sym.pltidx = l.dynn; l.dynn += 1; so = nil; // break } else { diff --git a/selfhost/cmd/w6l/pass.ww b/selfhost/cmd/w6l/pass.ww index 6126de3d..edee3ed9 100644 --- a/selfhost/cmd/w6l/pass.ww +++ b/selfhost/cmd/w6l/pass.ww @@ -35,14 +35,13 @@ export fn resolve(l: *lnk) i32 = { for (r != nil) { if (r.sym != nil) { if (r.sym.defined == 0) { - let sym: *lsym = r.sym; - if (sym.isdyn == 0) { + if (r.sym.isdyn == 0) { let so: *lso = l.sos; for (so != nil) { - if (soprovides(so, sym.name) != 0) { - sym.isdyn = 1; - sym.dynlib = so; - sym.pltidx = l.dynn; + if (soprovides(so, r.sym.name) != 0) { + r.sym.isdyn = 1; + r.sym.dynlib = so; + r.sym.pltidx = l.dynn; l.dynn += 1; so = nil; // break } else { diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 05b25f9c..39b7ca61 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -39,6 +39,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, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 836bb118..bfaf6286 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -419,6 +419,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; @@ -1049,6 +1092,71 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // Chained `.field = v` where `` 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. diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 4160d7cb..1ed6ac25 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -36,6 +36,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; }; @@ -54,18 +56,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); }; @@ -187,6 +206,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 `...` @@ -459,6 +522,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]; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index cb6c8130..742f2648 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -599,6 +599,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; }; @@ -672,7 +689,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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9d976adb..1bf42340 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 `.field = v` where `` 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, diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 7ddf84d1..e4194d79 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -806,6 +806,69 @@ static const struct row rows[] = { " };\n" " return acc;\n" "};", 16 }, /* 3 + 3 + len(\"no newline\")=10 */ + /* `(scalar, str)` tuple return: AX:DX:CX convention extends the + * tagged-union ABI. AX = scalar, DX = str.ptr, CX = str.len. + * Receive sites destructure off the same regs regardless of + * positional order. Without the fix, len was lost (only AX:DX + * returned), every receive shape gave garbage. */ + { "fn split() (i64, str) = { return 42, \"hello\"; };\n" + "fn main() i32 = {\n" + " let n, s = split();\n" + " return (n: i32) + (s.len: i32);\n" + "};", 47 }, + { "fn split() (str, i64) = { return \"hello\", 42; };\n" + "fn main() i32 = {\n" + " let s, n = split();\n" + " return (n: i32) + (s.len: i32);\n" + "};", 47 }, + { "fn split() (i64, str) = { return 42, \"hello\"; };\n" + "fn main() i32 = {\n" + " let t: (i64, str) = split();\n" + " return (t.0: i32) + (t.1.len: i32);\n" + "};", 47 }, + /* Hare-style paren tuple-destructure with str element */ + { "fn split() (i64, str) = { return 42, \"hello\"; };\n" + "fn main() i32 = {\n" + " let (n, s) = split();\n" + " return (n: i32) + (s.len: i32);\n" + "};", 47 }, + /* Chained field write through a pointer field: `r.sym.flag = v` + * where `r.sym: *T`. The cgen must evaluate the inner pointer, + * then store at *(ptr + field.offset). Without the fix, the + * single-level N_IDENT-base path doesn't fire (base is itself + * an N_DOT) and the assignment silently emits no instructions. + * Compound op + 1-byte field + 3-level chain all exercised. */ + { "type inner = struct { tag: u8, pad: u8, flag: i32 };\n" + "type outer = struct { sym: *inner };\n" + "fn main() i32 = {\n" + " let i: inner = inner { tag = 0u8, pad = 0u8, flag = 10 };\n" + " let r: outer = outer { sym = &i };\n" + " r.sym.flag += 32;\n" + " r.sym.tag = 5u8;\n" + " return r.sym.flag + (r.sym.tag: i32);\n" + "};", 47 }, + { "type leaf = struct { v: i32 };\n" + "type mid = struct { l: *leaf };\n" + "type top = struct { m: *mid };\n" + "fn main() i32 = {\n" + " let lf: leaf = leaf { v = 0 };\n" + " let md: mid = mid { l = &lf };\n" + " let tp: top = top { m = &md };\n" + " tp.m.l.v = 99;\n" + " return tp.m.l.v;\n" + "};", 99 }, + /* `def NAME: str = \"lit\"` field access. The Sdef has no stack + * slot, so .len/.ptr must inline the literal length / strlit + * address; without the fix, .len reads BP+8 (return-address slot) + * as garbage. */ + { "def MSG: str = \"hello world\";\n" + "fn main() i32 = { return MSG.len: i32; };", 11 }, + { "use os;\n" + "def GREETING: str = \"hi\\n\";\n" + "fn main() i32 = {\n" + " os.write(1, GREETING.ptr, GREETING.len: u64);\n" + " return GREETING.len: i32;\n" + "};", 3 }, { NULL, 0 } };