diff --git a/lib/ww/parse/stmt.ww b/lib/ww/parse/stmt.ww index fd854fcc..dc619d99 100644 --- a/lib/ww/parse/stmt.ww +++ b/lib/ww/parse/stmt.ww @@ -140,31 +140,100 @@ fn parsefor(p: *parser) *node = { let pc: i32 = p.curcol; advance(p); // past `for` expecttok(p, tkind.TK_LPAREN, "expected '(' after for"); - let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); - // Three forms (matching C parser): - // for (cond) — only cond - // for (init; cond; post) — full - // for (true) — infinite (cond is nkind.N_TRUE) - // Distinguish by counting ';'. Look at first chunk: if it's a - // `let` stmt that's the init. Otherwise, parse expr; if next is - // ';' it was cond. If we see two ';' total after init, post is - // next. Simpler: peek for `let` to decide init form. + + // Four forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — C-style 3-clause + // for (let x .. expr) — Hare-style range, single binding + // for (let (a, b) .. expr) — range with tuple destructure + // Range and 3-clause both lead with `let`, so we commit to consuming + // `let` then disambiguate by looking at what follows. if (p.curkind == tkind.TK_LET) { - n.lhs = parseletlocal(p); // init (consumes its own ';') - n.cond = parseexpr(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + advance(p); // past `let` + + // Tuple destructure: `for (let (a, b) .. expr)`. + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let names: *node = nil; + let ntail: *node = nil; + for (true) { + let npf: str = p.curfile; + let npl: i32 = p.curline; + let npc: i32 = p.curcol; + let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc); + let nm: str; + expectbindname(p, &nm); + e.str = nm; + if (names == nil) { names = e; } + else { ntail.next = e; }; + ntail = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names"); + expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names"); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.list = names; + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Single binding range or C-style let-init. We need to consume + // the IDENT/UNDER to know which: if followed by '..' it's a + // range; otherwise build a synthetic LET for the C-style for-init + // with the consumed name baked in. + if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) { + let isunder: bool = (p.curkind == tkind.TK_UNDER); + let nm: str; + nm.ptr = nil; nm.len = 0; + if (!isunder) { nm = p.curtext; }; + let lpf: str = p.curfile; + let lpl: i32 = p.curline; + let lpc: i32 = p.curcol; + advance(p); // consume IDENT/UNDER + + if (p.curkind == tkind.TK_DOTDOT) { + advance(p); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.str = nm; // "" for `_` + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Not a range — finish the let manually and continue as + // a 3-clause for-init. + let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc); + first.str = nm; + if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); }; + if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); }; + expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let"); + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + n.lhs = first; + n.cond = parseexpr(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); }; + return n; + }; + + errmsg(p, "expected name after 'let' in for"); + }; + + // for (cond) or for (cond; post) + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + let first: *node = parseexpr(p); + if (accepttok(p, tkind.TK_SEMI)) { + n.cond = first; n.rhs = parseexpr(p); } else { - // Parse one expr. If next is ';', it's a 3-clause without init. - let first: *node = parseexpr(p); - if (accepttok(p, tkind.TK_SEMI)) { - // cond ; post - n.cond = first; - n.rhs = parseexpr(p); - } else { - // just (cond) - n.cond = first; - }; + n.cond = first; }; expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); n.body = parseblock(p); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 113d1f56..cc7db2d9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2715,31 +2715,100 @@ fn parsefor(p: *parser) *node = { let pc: i32 = p.curcol; advance(p); // past `for` expecttok(p, tkind.TK_LPAREN, "expected '(' after for"); - let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); - // Three forms (matching C parser): - // for (cond) — only cond - // for (init; cond; post) — full - // for (true) — infinite (cond is nkind.N_TRUE) - // Distinguish by counting ';'. Look at first chunk: if it's a - // `let` stmt that's the init. Otherwise, parse expr; if next is - // ';' it was cond. If we see two ';' total after init, post is - // next. Simpler: peek for `let` to decide init form. + + // Four forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — C-style 3-clause + // for (let x .. expr) — Hare-style range, single binding + // for (let (a, b) .. expr) — range with tuple destructure + // Range and 3-clause both lead with `let`, so we commit to consuming + // `let` then disambiguate by looking at what follows. if (p.curkind == tkind.TK_LET) { - n.lhs = parseletlocal(p); // init (consumes its own ';') - n.cond = parseexpr(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + advance(p); // past `let` + + // Tuple destructure: `for (let (a, b) .. expr)`. + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let names: *node = nil; + let ntail: *node = nil; + for (true) { + let npf: str = p.curfile; + let npl: i32 = p.curline; + let npc: i32 = p.curcol; + let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc); + let nm: str; + expectbindname(p, &nm); + e.str = nm; + if (names == nil) { names = e; } + else { ntail.next = e; }; + ntail = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names"); + expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names"); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.list = names; + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Single binding range or C-style let-init. We need to consume + // the IDENT/UNDER to know which: if followed by '..' it's a + // range; otherwise build a synthetic LET for the C-style for-init + // with the consumed name baked in. + if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) { + let isunder: bool = (p.curkind == tkind.TK_UNDER); + let nm: str; + nm.ptr = nil; nm.len = 0; + if (!isunder) { nm = p.curtext; }; + let lpf: str = p.curfile; + let lpl: i32 = p.curline; + let lpc: i32 = p.curcol; + advance(p); // consume IDENT/UNDER + + if (p.curkind == tkind.TK_DOTDOT) { + advance(p); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.str = nm; // "" for `_` + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Not a range — finish the let manually and continue as + // a 3-clause for-init. + let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc); + first.str = nm; + if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); }; + if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); }; + expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let"); + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + n.lhs = first; + n.cond = parseexpr(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); }; + return n; + }; + + errmsg(p, "expected name after 'let' in for"); + }; + + // for (cond) or for (cond; post) + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + let first: *node = parseexpr(p); + if (accepttok(p, tkind.TK_SEMI)) { + n.cond = first; n.rhs = parseexpr(p); } else { - // Parse one expr. If next is ';', it's a 3-clause without init. - let first: *node = parseexpr(p); - if (accepttok(p, tkind.TK_SEMI)) { - // cond ; post - n.cond = first; - n.rhs = parseexpr(p); - } else { - // just (cond) - n.cond = first; - }; + n.cond = first; }; expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); n.body = parseblock(p); @@ -4119,6 +4188,32 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // `for (let x .. slice) body` / `for (let (a, b) .. slice) body` — + // each binding name becomes a fresh local. Walk the slice expr first + // so its idents resolve before the bindings shadow anything, then + // install bindings and walk the body/else. + if (k == nkind.N_FORRANGE) { + if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); + }; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; + }; + if (n.body != nil) { resolvewalk(c, n.body); }; + if (n.els != nil) { resolvewalk(c, n.els); }; + return; + }; + // `match (e) { case let v: T => stmt; ... }` — the binding `v` // is declared by the case arm and visible inside its body. if (k == nkind.N_MCASE) { @@ -8411,6 +8506,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; @@ -9095,6 +9192,271 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// paramfieldsize — raw byte size of a tuple-field type. Mirrors the +// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for +// i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8. +fn paramfieldsize(t: *node) i32 = { + if (t == nil) { return 8; }; + let k: nkind = t.kind; + if (k == nkind.N_TPTR) { return 8; }; + if (k == nkind.N_TFN) { return 8; }; + if (k == nkind.N_TCHAN) { return 8; }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// paramissigned — does this primitive type need sign-extending on a +// narrow (4B) load? Mirrors C cgen's `binds[b].signed_field` flag. +fn paramissigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + return false; +}; + +// cgforrange — lower `for (let x .. slice) body` (and the tuple- +// destructure cousin `for (let (a, b) .. slice) body`). The body is +// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`. +// Each iteration computes the element address `s.ptr + i*esz` and +// either loads the whole element into the named local or pulls each +// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE +// byte-for-byte (label names + labelseq consumption order). +fn cgforrange(c: *cgen, n: *node) void = { + let slc: *node = n.lhs; + let slclocal: *local = nil; + let slctn: *node = nil; + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + slclocal = localfindnode(c, slc.str); + if (slclocal != nil) { slctn = slclocal.tnode; }; + }; + }; + // Element type — peek through TSLICE/TARRAY for the tuple param walk. + let elemt: *node = nil; + if (slctn != nil) { + let sk: nkind = slctn.kind; + if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; }; + if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; }; + }; + // esz: raw elem byte size. For tuple-element slices `[](T0, T1)`, + // C cgen reads the resolved tuple's size (sum of raw param sizes, + // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. + // elemsizeof returns 8 for non-primitive elem, which would be + // wrong here — compute from the tuple param walk instead. + let esz: i32 = elemsizeof(slctn); + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { + let total: i32 = 0; + let p: *node = elemt.list; + for (p != nil) { + total += paramfieldsize(p); + p = p.next; + }; + esz = total; + }; + }; + let destruct: bool = (n.list != nil); + + // .rgi (counter) + .rgl (length) scratch slots. + let iname: str = mkscratchname(c, "rgi"); + let lname: str = mkscratchname(c, "rgl"); + let ioff: i32 = localalloc(c, iname, 8, nil); + let loff: i32 = localalloc(c, lname, 8, nil); + + // Per-binding (up to 8 — matches the C array). Parallel i64 arrays + // keep every elem at 8B so the indexed-store hits the working MOVQ + // path (selfhost cgen doesn't yet emit MOVL for i32-array writes, + // and doesn't zero-init `[8]bool` uninit slots — both byte-diverge + // from C w6c on the wwstage rebuild). + let bind_off: [8]i64; + let bind_sz: [8]i64; + let bind_foff: [8]i64; + let bind_signed: [8]i64; // 0 / 1 + let nbinds: i32 = 0; + + if (destruct) { + let tp: *node = nil; + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; }; + }; + let field_off: i32 = 0; + let m: *node = n.list; + for (m != nil) { + if (nbinds >= 8) { m = nil; } + else { + let fsz: i32 = 8; + let signf: bool = false; + if (tp != nil) { + fsz = paramfieldsize(tp); + signf = paramissigned(tp); + }; + let slot_sz: i32 = fsz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[nbinds] = fsz: i64; + bind_foff[nbinds] = field_off: i64; + if (signf) { bind_signed[nbinds] = 1i64; } + else { bind_signed[nbinds] = 0i64; }; + let bnm: str = m.str; + if (bnm.len > 0) { + bind_off[nbinds] = localadd(c, bnm, slot_sz, nil): i64; + } else { + bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + field_off += fsz; + nbinds += 1; + if (tp != nil) { tp = tp.next; }; + m = m.next; + }; + }; + } else { + let slot_sz: i32 = esz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[0] = esz: i64; + bind_foff[0] = 0i64; + // Single-binding signed-narrow detection: mirror C which + // reads `u->sub->kind` for the elem type. + let signf0: bool = false; + if (elemt != nil) { signf0 = paramissigned(elemt); }; + if (signf0) { bind_signed[0] = 1i64; } + else { bind_signed[0] = 0i64; }; + if (n.str.len > 0) { + bind_off[0] = localadd(c, n.str, slot_sz, nil): i64; + } else { + bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + nbinds = 1; + }; + + // init: ioff(BP) = 0 + emitline("\tMOVQ\t$0, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + + // loff(BP) = len + let isarr: bool = false; + let isslicestr: bool = false; + if (slctn != nil) { + let tk: nkind = slctn.kind; + if (tk == nkind.N_TSLICE) { isslicestr = true; }; + if (tk == nkind.N_TARRAY) { isarr = true; }; + if (tk == nkind.N_TNAME) { + if (streq(slctn.str, "str")) { isslicestr = true; }; + }; + }; + if (isslicestr) { + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + emitline("\tMOVQ\t"); + emitoff((slclocal.off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + }; + }; + } else { if (isarr) { + let alen: i64 = 0i64; + if (slctn.rhs != nil) { + if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", "); + emitoff(loff: i64); + emitline("(BP)\n"); + } else { + cgexpr(c, slc); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + };}; + + let loopl: str = mklabel(c, "rloop"); + let endl: str = mklabel(c, "rend"); + let naturall: str = endl; + if (n.els != nil) { naturall = mklabel(c, "relseloop"); }; + + c.loopcontbuf[c.looptop] = loopl; + c.loopendbuf[c.looptop] = endl; + c.looptop += 1; + + emitlabel(loopl); + emitline("\tMOVQ\t"); + emitoff(ioff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff(loff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(naturall); emitline("\n"); + + // BX = base + i*esz + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + }; + }; + }; + emitline("\tADDQ\tAX, BX\n"); + + // Per-binding load from BX+foff. + let b: i32 = 0; + for (b < nbinds) { + let op: str = "MOVQ"; + if (bind_sz[b] == 1i64) { op = "MOVZBQ"; } + else { if (bind_sz[b] == 4i64) { + if (bind_signed[b] != 0i64) { op = "MOVSXD"; } + else { op = "MOVL"; }; + };}; + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]); + emitline("(BP)\n"); + b += 1; + }; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.looptop -= 1; + + emitline("\tADDQ\t$1, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); emitline(loopl); emitline("\n"); + if (n.els != nil) { + emitlabel(naturall); + cgstmt(c, n.els); + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + // cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to // a chain of compares against the scrutinee. Scrutinee lands in a // fresh 8B local slot so case bodies can spill SP without losing it. @@ -9269,6 +9631,33 @@ fn scanlocals(c: *cgen, n: *node) i32 = { // ".sw_" at cgen time — unique per switch — so it must // not dedup. Count it here so the frame SUBQ matches. if (n.kind == nkind.N_SWITCH) { total += 8; }; + // `for (let x .. s)` allocates two 8B scratch slots — `.rgi_` + // (counter) and `.rgl_` (length) — plus one slot per binding. + // Per-binding sz defaults to 8 (covers scalar primitives + ptrs). + // `str` tuple-fields would need 16 — selfhost doesn't yet emit + // those, so the simple count tracks C cgen for current fixtures. + if (n.kind == nkind.N_FORRANGE) { + total += 16; // .rgi + .rgl scratch + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len == 0) { + total += 8; // discard binding still gets a slot + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len == 0) { + total += 8; + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + }; + }; // 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/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index f1931f95..7860fb7b 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -92,6 +92,33 @@ fn scanlocals(c: *cgen, n: *node) i32 = { // ".sw_" at cgen time — unique per switch — so it must // not dedup. Count it here so the frame SUBQ matches. if (n.kind == nkind.N_SWITCH) { total += 8; }; + // `for (let x .. s)` allocates two 8B scratch slots — `.rgi_` + // (counter) and `.rgl_` (length) — plus one slot per binding. + // Per-binding sz defaults to 8 (covers scalar primitives + ptrs). + // `str` tuple-fields would need 16 — selfhost doesn't yet emit + // those, so the simple count tracks C cgen for current fixtures. + if (n.kind == nkind.N_FORRANGE) { + total += 16; // .rgi + .rgl scratch + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len == 0) { + total += 8; // discard binding still gets a slot + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len == 0) { + total += 8; + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + }; + }; // 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/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index cbe19755..9e0c0ec3 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -34,6 +34,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; @@ -718,6 +720,271 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// paramfieldsize — raw byte size of a tuple-field type. Mirrors the +// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for +// i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8. +fn paramfieldsize(t: *node) i32 = { + if (t == nil) { return 8; }; + let k: nkind = t.kind; + if (k == nkind.N_TPTR) { return 8; }; + if (k == nkind.N_TFN) { return 8; }; + if (k == nkind.N_TCHAN) { return 8; }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// paramissigned — does this primitive type need sign-extending on a +// narrow (4B) load? Mirrors C cgen's `binds[b].signed_field` flag. +fn paramissigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + return false; +}; + +// cgforrange — lower `for (let x .. slice) body` (and the tuple- +// destructure cousin `for (let (a, b) .. slice) body`). The body is +// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`. +// Each iteration computes the element address `s.ptr + i*esz` and +// either loads the whole element into the named local or pulls each +// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE +// byte-for-byte (label names + labelseq consumption order). +fn cgforrange(c: *cgen, n: *node) void = { + let slc: *node = n.lhs; + let slclocal: *local = nil; + let slctn: *node = nil; + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + slclocal = localfindnode(c, slc.str); + if (slclocal != nil) { slctn = slclocal.tnode; }; + }; + }; + // Element type — peek through TSLICE/TARRAY for the tuple param walk. + let elemt: *node = nil; + if (slctn != nil) { + let sk: nkind = slctn.kind; + if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; }; + if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; }; + }; + // esz: raw elem byte size. For tuple-element slices `[](T0, T1)`, + // C cgen reads the resolved tuple's size (sum of raw param sizes, + // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. + // elemsizeof returns 8 for non-primitive elem, which would be + // wrong here — compute from the tuple param walk instead. + let esz: i32 = elemsizeof(slctn); + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { + let total: i32 = 0; + let p: *node = elemt.list; + for (p != nil) { + total += paramfieldsize(p); + p = p.next; + }; + esz = total; + }; + }; + let destruct: bool = (n.list != nil); + + // .rgi (counter) + .rgl (length) scratch slots. + let iname: str = mkscratchname(c, "rgi"); + let lname: str = mkscratchname(c, "rgl"); + let ioff: i32 = localalloc(c, iname, 8, nil); + let loff: i32 = localalloc(c, lname, 8, nil); + + // Per-binding (up to 8 — matches the C array). Parallel i64 arrays + // keep every elem at 8B so the indexed-store hits the working MOVQ + // path (selfhost cgen doesn't yet emit MOVL for i32-array writes, + // and doesn't zero-init `[8]bool` uninit slots — both byte-diverge + // from C w6c on the wwstage rebuild). + let bind_off: [8]i64; + let bind_sz: [8]i64; + let bind_foff: [8]i64; + let bind_signed: [8]i64; // 0 / 1 + let nbinds: i32 = 0; + + if (destruct) { + let tp: *node = nil; + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; }; + }; + let field_off: i32 = 0; + let m: *node = n.list; + for (m != nil) { + if (nbinds >= 8) { m = nil; } + else { + let fsz: i32 = 8; + let signf: bool = false; + if (tp != nil) { + fsz = paramfieldsize(tp); + signf = paramissigned(tp); + }; + let slot_sz: i32 = fsz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[nbinds] = fsz: i64; + bind_foff[nbinds] = field_off: i64; + if (signf) { bind_signed[nbinds] = 1i64; } + else { bind_signed[nbinds] = 0i64; }; + let bnm: str = m.str; + if (bnm.len > 0) { + bind_off[nbinds] = localadd(c, bnm, slot_sz, nil): i64; + } else { + bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + field_off += fsz; + nbinds += 1; + if (tp != nil) { tp = tp.next; }; + m = m.next; + }; + }; + } else { + let slot_sz: i32 = esz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[0] = esz: i64; + bind_foff[0] = 0i64; + // Single-binding signed-narrow detection: mirror C which + // reads `u->sub->kind` for the elem type. + let signf0: bool = false; + if (elemt != nil) { signf0 = paramissigned(elemt); }; + if (signf0) { bind_signed[0] = 1i64; } + else { bind_signed[0] = 0i64; }; + if (n.str.len > 0) { + bind_off[0] = localadd(c, n.str, slot_sz, nil): i64; + } else { + bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + nbinds = 1; + }; + + // init: ioff(BP) = 0 + emitline("\tMOVQ\t$0, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + + // loff(BP) = len + let isarr: bool = false; + let isslicestr: bool = false; + if (slctn != nil) { + let tk: nkind = slctn.kind; + if (tk == nkind.N_TSLICE) { isslicestr = true; }; + if (tk == nkind.N_TARRAY) { isarr = true; }; + if (tk == nkind.N_TNAME) { + if (streq(slctn.str, "str")) { isslicestr = true; }; + }; + }; + if (isslicestr) { + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + emitline("\tMOVQ\t"); + emitoff((slclocal.off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + }; + }; + } else { if (isarr) { + let alen: i64 = 0i64; + if (slctn.rhs != nil) { + if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", "); + emitoff(loff: i64); + emitline("(BP)\n"); + } else { + cgexpr(c, slc); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + };}; + + let loopl: str = mklabel(c, "rloop"); + let endl: str = mklabel(c, "rend"); + let naturall: str = endl; + if (n.els != nil) { naturall = mklabel(c, "relseloop"); }; + + c.loopcontbuf[c.looptop] = loopl; + c.loopendbuf[c.looptop] = endl; + c.looptop += 1; + + emitlabel(loopl); + emitline("\tMOVQ\t"); + emitoff(ioff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff(loff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(naturall); emitline("\n"); + + // BX = base + i*esz + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + }; + }; + }; + emitline("\tADDQ\tAX, BX\n"); + + // Per-binding load from BX+foff. + let b: i32 = 0; + for (b < nbinds) { + let op: str = "MOVQ"; + if (bind_sz[b] == 1i64) { op = "MOVZBQ"; } + else { if (bind_sz[b] == 4i64) { + if (bind_signed[b] != 0i64) { op = "MOVSXD"; } + else { op = "MOVL"; }; + };}; + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]); + emitline("(BP)\n"); + b += 1; + }; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.looptop -= 1; + + emitline("\tADDQ\t$1, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); emitline(loopl); emitline("\n"); + if (n.els != nil) { + emitlabel(naturall); + cgstmt(c, n.els); + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + // cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to // a chain of compares against the scrutinee. Scrutinee lands in a // fresh 8B local slot so case bodies can spill SP without losing it. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 4a57a99b..370865a1 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -154,6 +154,32 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // `for (let x .. slice) body` / `for (let (a, b) .. slice) body` — + // each binding name becomes a fresh local. Walk the slice expr first + // so its idents resolve before the bindings shadow anything, then + // install bindings and walk the body/else. + if (k == nkind.N_FORRANGE) { + if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); + }; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; + }; + if (n.body != nil) { resolvewalk(c, n.body); }; + if (n.els != nil) { resolvewalk(c, n.els); }; + return; + }; + // `match (e) { case let v: T => stmt; ... }` — the binding `v` // is declared by the case arm and visible inside its body. if (k == nkind.N_MCASE) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5844aa16..87c8706e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2715,31 +2715,100 @@ fn parsefor(p: *parser) *node = { let pc: i32 = p.curcol; advance(p); // past `for` expecttok(p, tkind.TK_LPAREN, "expected '(' after for"); - let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); - // Three forms (matching C parser): - // for (cond) — only cond - // for (init; cond; post) — full - // for (true) — infinite (cond is nkind.N_TRUE) - // Distinguish by counting ';'. Look at first chunk: if it's a - // `let` stmt that's the init. Otherwise, parse expr; if next is - // ';' it was cond. If we see two ';' total after init, post is - // next. Simpler: peek for `let` to decide init form. + + // Four forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — C-style 3-clause + // for (let x .. expr) — Hare-style range, single binding + // for (let (a, b) .. expr) — range with tuple destructure + // Range and 3-clause both lead with `let`, so we commit to consuming + // `let` then disambiguate by looking at what follows. if (p.curkind == tkind.TK_LET) { - n.lhs = parseletlocal(p); // init (consumes its own ';') - n.cond = parseexpr(p); - expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + advance(p); // past `let` + + // Tuple destructure: `for (let (a, b) .. expr)`. + if (p.curkind == tkind.TK_LPAREN) { + advance(p); + let names: *node = nil; + let ntail: *node = nil; + for (true) { + let npf: str = p.curfile; + let npl: i32 = p.curline; + let npc: i32 = p.curcol; + let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc); + let nm: str; + expectbindname(p, &nm); + e.str = nm; + if (names == nil) { names = e; } + else { ntail.next = e; }; + ntail = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names"); + expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names"); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.list = names; + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Single binding range or C-style let-init. We need to consume + // the IDENT/UNDER to know which: if followed by '..' it's a + // range; otherwise build a synthetic LET for the C-style for-init + // with the consumed name baked in. + if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) { + let isunder: bool = (p.curkind == tkind.TK_UNDER); + let nm: str; + nm.ptr = nil; nm.len = 0; + if (!isunder) { nm = p.curtext; }; + let lpf: str = p.curfile; + let lpl: i32 = p.curline; + let lpc: i32 = p.curcol; + advance(p); // consume IDENT/UNDER + + if (p.curkind == tkind.TK_DOTDOT) { + advance(p); + let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); + rng.str = nm; // "" for `_` + rng.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + rng.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; + return rng; + }; + + // Not a range — finish the let manually and continue as + // a 3-clause for-init. + let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc); + first.str = nm; + if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); }; + if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); }; + expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let"); + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + n.lhs = first; + n.cond = parseexpr(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); }; + return n; + }; + + errmsg(p, "expected name after 'let' in for"); + }; + + // for (cond) or for (cond; post) + let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); + let first: *node = parseexpr(p); + if (accepttok(p, tkind.TK_SEMI)) { + n.cond = first; n.rhs = parseexpr(p); } else { - // Parse one expr. If next is ';', it's a 3-clause without init. - let first: *node = parseexpr(p); - if (accepttok(p, tkind.TK_SEMI)) { - // cond ; post - n.cond = first; - n.rhs = parseexpr(p); - } else { - // just (cond) - n.cond = first; - }; + n.cond = first; }; expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); n.body = parseblock(p); @@ -4119,6 +4188,32 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // `for (let x .. slice) body` / `for (let (a, b) .. slice) body` — + // each binding name becomes a fresh local. Walk the slice expr first + // so its idents resolve before the bindings shadow anything, then + // install bindings and walk the body/else. + if (k == nkind.N_FORRANGE) { + if (n.lhs != nil) { resolvewalk(c, n.lhs); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); + }; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len > 0) { + scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); + }; + }; + if (n.body != nil) { resolvewalk(c, n.body); }; + if (n.els != nil) { resolvewalk(c, n.els); }; + return; + }; + // `match (e) { case let v: T => stmt; ... }` — the binding `v` // is declared by the case arm and visible inside its body. if (k == nkind.N_MCASE) { @@ -8411,6 +8506,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; @@ -9095,6 +9192,271 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// paramfieldsize — raw byte size of a tuple-field type. Mirrors the +// `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for +// i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8. +fn paramfieldsize(t: *node) i32 = { + if (t == nil) { return 8; }; + let k: nkind = t.kind; + if (k == nkind.N_TPTR) { return 8; }; + if (k == nkind.N_TFN) { return 8; }; + if (k == nkind.N_TCHAN) { return 8; }; + if (k == nkind.N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = primsize(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// paramissigned — does this primitive type need sign-extending on a +// narrow (4B) load? Mirrors C cgen's `binds[b].signed_field` flag. +fn paramissigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + let nm: str = t.str; + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + return false; +}; + +// cgforrange — lower `for (let x .. slice) body` (and the tuple- +// destructure cousin `for (let (a, b) .. slice) body`). The body is +// wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`. +// Each iteration computes the element address `s.ptr + i*esz` and +// either loads the whole element into the named local or pulls each +// tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE +// byte-for-byte (label names + labelseq consumption order). +fn cgforrange(c: *cgen, n: *node) void = { + let slc: *node = n.lhs; + let slclocal: *local = nil; + let slctn: *node = nil; + if (slc != nil) { + if (slc.kind == nkind.N_IDENT) { + slclocal = localfindnode(c, slc.str); + if (slclocal != nil) { slctn = slclocal.tnode; }; + }; + }; + // Element type — peek through TSLICE/TARRAY for the tuple param walk. + let elemt: *node = nil; + if (slctn != nil) { + let sk: nkind = slctn.kind; + if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; }; + if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; }; + }; + // esz: raw elem byte size. For tuple-element slices `[](T0, T1)`, + // C cgen reads the resolved tuple's size (sum of raw param sizes, + // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. + // elemsizeof returns 8 for non-primitive elem, which would be + // wrong here — compute from the tuple param walk instead. + let esz: i32 = elemsizeof(slctn); + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { + let total: i32 = 0; + let p: *node = elemt.list; + for (p != nil) { + total += paramfieldsize(p); + p = p.next; + }; + esz = total; + }; + }; + let destruct: bool = (n.list != nil); + + // .rgi (counter) + .rgl (length) scratch slots. + let iname: str = mkscratchname(c, "rgi"); + let lname: str = mkscratchname(c, "rgl"); + let ioff: i32 = localalloc(c, iname, 8, nil); + let loff: i32 = localalloc(c, lname, 8, nil); + + // Per-binding (up to 8 — matches the C array). Parallel i64 arrays + // keep every elem at 8B so the indexed-store hits the working MOVQ + // path (selfhost cgen doesn't yet emit MOVL for i32-array writes, + // and doesn't zero-init `[8]bool` uninit slots — both byte-diverge + // from C w6c on the wwstage rebuild). + let bind_off: [8]i64; + let bind_sz: [8]i64; + let bind_foff: [8]i64; + let bind_signed: [8]i64; // 0 / 1 + let nbinds: i32 = 0; + + if (destruct) { + let tp: *node = nil; + if (elemt != nil) { + if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; }; + }; + let field_off: i32 = 0; + let m: *node = n.list; + for (m != nil) { + if (nbinds >= 8) { m = nil; } + else { + let fsz: i32 = 8; + let signf: bool = false; + if (tp != nil) { + fsz = paramfieldsize(tp); + signf = paramissigned(tp); + }; + let slot_sz: i32 = fsz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[nbinds] = fsz: i64; + bind_foff[nbinds] = field_off: i64; + if (signf) { bind_signed[nbinds] = 1i64; } + else { bind_signed[nbinds] = 0i64; }; + let bnm: str = m.str; + if (bnm.len > 0) { + bind_off[nbinds] = localadd(c, bnm, slot_sz, nil): i64; + } else { + bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + field_off += fsz; + nbinds += 1; + if (tp != nil) { tp = tp.next; }; + m = m.next; + }; + }; + } else { + let slot_sz: i32 = esz; + if (slot_sz < 8) { slot_sz = 8; }; + bind_sz[0] = esz: i64; + bind_foff[0] = 0i64; + // Single-binding signed-narrow detection: mirror C which + // reads `u->sub->kind` for the elem type. + let signf0: bool = false; + if (elemt != nil) { signf0 = paramissigned(elemt); }; + if (signf0) { bind_signed[0] = 1i64; } + else { bind_signed[0] = 0i64; }; + if (n.str.len > 0) { + bind_off[0] = localadd(c, n.str, slot_sz, nil): i64; + } else { + bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil): i64; + }; + nbinds = 1; + }; + + // init: ioff(BP) = 0 + emitline("\tMOVQ\t$0, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + + // loff(BP) = len + let isarr: bool = false; + let isslicestr: bool = false; + if (slctn != nil) { + let tk: nkind = slctn.kind; + if (tk == nkind.N_TSLICE) { isslicestr = true; }; + if (tk == nkind.N_TARRAY) { isarr = true; }; + if (tk == nkind.N_TNAME) { + if (streq(slctn.str, "str")) { isslicestr = true; }; + }; + }; + if (isslicestr) { + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + emitline("\tMOVQ\t"); + emitoff((slclocal.off + 8): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + }; + }; + } else { if (isarr) { + let alen: i64 = 0i64; + if (slctn.rhs != nil) { + if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; }; + }; + emitline("\tMOVQ\t$"); + emitint(alen); + emitline(", "); + emitoff(loff: i64); + emitline("(BP)\n"); + } else { + cgexpr(c, slc); + emitline("\tMOVQ\tAX, "); + emitoff(loff: i64); + emitline("(BP)\n"); + };}; + + let loopl: str = mklabel(c, "rloop"); + let endl: str = mklabel(c, "rend"); + let naturall: str = endl; + if (n.els != nil) { naturall = mklabel(c, "relseloop"); }; + + c.loopcontbuf[c.looptop] = loopl; + c.loopendbuf[c.looptop] = endl; + c.looptop += 1; + + emitlabel(loopl); + emitline("\tMOVQ\t"); + emitoff(ioff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff(loff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJGE\t"); emitline(naturall); emitline("\n"); + + // BX = base + i*esz + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (slc.kind == nkind.N_IDENT) { + if (slclocal != nil) { + if (isarr) { + emitline("\tLEAQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(slclocal.off: i64); + emitline("(BP), BX\n"); + }; + }; + }; + emitline("\tADDQ\tAX, BX\n"); + + // Per-binding load from BX+foff. + let b: i32 = 0; + for (b < nbinds) { + let op: str = "MOVQ"; + if (bind_sz[b] == 1i64) { op = "MOVZBQ"; } + else { if (bind_sz[b] == 4i64) { + if (bind_signed[b] != 0i64) { op = "MOVSXD"; } + else { op = "MOVL"; }; + };}; + emitline("\t"); + emitline(op); + emitline("\t"); + emitoff(bind_foff[b]); + emitline("(BX), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(bind_off[b]); + emitline("(BP)\n"); + b += 1; + }; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.looptop -= 1; + + emitline("\tADDQ\t$1, "); + emitoff(ioff: i64); + emitline("(BP)\n"); + emitline("\tJMP\t"); emitline(loopl); emitline("\n"); + if (n.els != nil) { + emitlabel(naturall); + cgstmt(c, n.els); + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + // cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to // a chain of compares against the scrutinee. Scrutinee lands in a // fresh 8B local slot so case bodies can spill SP without losing it. @@ -9269,6 +9631,33 @@ fn scanlocals(c: *cgen, n: *node) i32 = { // ".sw_" at cgen time — unique per switch — so it must // not dedup. Count it here so the frame SUBQ matches. if (n.kind == nkind.N_SWITCH) { total += 8; }; + // `for (let x .. s)` allocates two 8B scratch slots — `.rgi_` + // (counter) and `.rgl_` (length) — plus one slot per binding. + // Per-binding sz defaults to 8 (covers scalar primitives + ptrs). + // `str` tuple-fields would need 16 — selfhost doesn't yet emit + // those, so the simple count tracks C cgen for current fixtures. + if (n.kind == nkind.N_FORRANGE) { + total += 16; // .rgi + .rgl scratch + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + let bnm: str = m.str; + if (bnm.len == 0) { + total += 8; // discard binding still gets a slot + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + m = m.next; + }; + } else { + let bnm: str = n.str; + if (bnm.len == 0) { + total += 8; + } else { if (!scanseenmark(c, bnm)) { + total += 8; + };}; + }; + }; // 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/990_selfhost.c b/test/wcc/990_selfhost.c index 8035ea26..c65b39a1 100644 --- a/test/wcc/990_selfhost.c +++ b/test/wcc/990_selfhost.c @@ -345,6 +345,32 @@ probe_ww_compile(const char *bin) " dec(&c); dec(&c); dec(&c);\n" " return c.x;\n" "};", 2 }, + /* Hare-style for-range over a slice: `for (let b .. s)`. + * Allocates `.rgi`/`.rgl` scratch slots, walks i=0..s.len + * loading s.ptr[i] into the binding. esz=1 here so the + * load is MOVZBQ. Sum 10+20+30+40 = 100. */ + { "use os;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " append(s, 10u8, 20u8, 30u8, 40u8);\n" + " let total: i32 = 0;\n" + " for (let b .. s) { total += b: i32; };\n" + " return total;\n" + "};", 100 }, + /* for-range with tuple destructure on `(i64, i64)`. Element + * size is 16 (sum of raw param sizes); each binding loads + * via BX+foff. Buf has (1,10) (2,20); sum = 33. */ + { "fn main() i32 = {\n" + " let buf: [4]i64;\n" + " buf[0] = 1i64; buf[1] = 10i64; buf[2] = 2i64; buf[3] = 20i64;\n" + " let s: [](i64, i64);\n" + " s.ptr = buf.ptr: *(i64, i64);\n" + " s.len = 2; s.cap = 2;\n" + " let total: i64 = 0i64;\n" + " for (let (k, v) .. s) { total += k + v; };\n" + " return total: i32;\n" + "};", 33 }, /* append(s, v, ...) builtin — Hare's rt::ensure model. * Each value: PUSHQ AX, ADDQ $1 to s.len, LEAQ s/MOVQ esz * args for rt_ensure, then write into the freshly-grown diff --git a/test/wcc/994_w6c_ww.c b/test/wcc/994_w6c_ww.c index 5057840a..7e8824a3 100644 --- a/test/wcc/994_w6c_ww.c +++ b/test/wcc/994_w6c_ww.c @@ -178,6 +178,27 @@ main(void) " append(dst, src...);\n" " return dst.len: i32;\n" "};" }, + { "forrange", + "use os;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " append(s, 10u8, 20u8, 30u8);\n" + " let total: i32 = 0;\n" + " for (let b .. s) { total += b: i32; };\n" + " return total;\n" + "};" }, + { "forrange_tuple", + "fn main() i32 = {\n" + " let buf: [4]i64;\n" + " buf[0] = 1i64; buf[1] = 10i64; buf[2] = 2i64; buf[3] = 20i64;\n" + " let s: [](i64, i64);\n" + " s.ptr = buf.ptr: *(i64, i64);\n" + " s.len = 2; s.cap = 2;\n" + " let total: i64 = 0i64;\n" + " for (let (k, v) .. s) { total += k + v; };\n" + " return total: i32;\n" + "};" }, { NULL, NULL }, };