selfhost: port forrange — N_FORRANGE parser + cgen + tuple destructure

This commit is contained in:
2026-05-12 16:21:13 +09:00
parent ce5d66e18a
commit e087c843e9
8 changed files with 1280 additions and 66 deletions

View File

@@ -140,32 +140,101 @@ 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):
// Four 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.
// 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 ';')
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);
} else {
// Parse one expr. If next is ';', it's a 3-clause without init.
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)) {
// cond ; post
n.cond = first;
n.rhs = parseexpr(p);
} else {
// just (cond)
n.cond = first;
};
};
expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped

View File

@@ -2715,32 +2715,101 @@ 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):
// Four 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.
// 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 ';')
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);
} else {
// Parse one expr. If next is ';', it's a 3-clause without init.
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)) {
// cond ; post
n.cond = first;
n.rhs = parseexpr(p);
} else {
// just (cond)
n.cond = first;
};
};
expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
@@ -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_<labelseq>" 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_<seq>`
// (counter) and `.rgl_<seq>` (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,

View File

@@ -92,6 +92,33 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// ".sw_<labelseq>" 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_<seq>`
// (counter) and `.rgl_<seq>` (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,

View File

@@ -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.

View File

@@ -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) {

View File

@@ -2715,32 +2715,101 @@ 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):
// Four 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.
// 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 ';')
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);
} else {
// Parse one expr. If next is ';', it's a 3-clause without init.
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)) {
// cond ; post
n.cond = first;
n.rhs = parseexpr(p);
} else {
// just (cond)
n.cond = first;
};
};
expecttok(p, tkind.TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
@@ -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_<labelseq>" 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_<seq>`
// (counter) and `.rgl_<seq>` (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,

View File

@@ -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

View File

@@ -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 },
};