ww: hare-feature batch (_, const, [_]T, ..., size/offset, assert, for-else)

This commit is contained in:
2026-05-11 19:38:12 +09:00
parent 579cc39f9b
commit 6219a47c6f
25 changed files with 1496 additions and 1049 deletions

25
selfhost/CLAUDE.md Normal file
View File

@@ -0,0 +1,25 @@
selfhost — ww reimplementation of the toolchain (wcc, w6c, w6a, w6l, ww, wwdump). Compiled by the C bootstrap (`../cmd/`); the goal is to eventually compile itself.
Identifiers: Plan 9 style. lowercase, words run together (`newbuf`, `tcpsock`, `parsefile`). No snake_case.
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:
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;
```
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.
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.
If a port "should work" but the binary is wrong, suspect these first.

View File

@@ -488,62 +488,64 @@ def TK_FALSE: i32 = 28;
def TK_AS: i32 = 29;
def TK_STATIC: i32 = 30;
def TK_MATCH: i32 = 31;
def TK_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 32;
def TK_RPAREN: i32 = 33;
def TK_LBRACE: i32 = 34;
def TK_RBRACE: i32 = 35;
def TK_LBRACK: i32 = 36;
def TK_RBRACK: i32 = 37;
def TK_COMMA: i32 = 38;
def TK_SEMI: i32 = 39;
def TK_COLON: i32 = 40;
def TK_DOT: i32 = 41;
def TK_ELLIPSIS: i32 = 42;
def TK_DOTDOT: i32 = 43;
def TK_AT: i32 = 44;
def TK_QUESTION: i32 = 45;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 46;
def TK_PLUSEQ: i32 = 47;
def TK_MINUSEQ: i32 = 48;
def TK_STAREQ: i32 = 49;
def TK_SLASHEQ: i32 = 50;
def TK_PERCENTEQ: i32 = 51;
def TK_AMPEQ: i32 = 52;
def TK_PIPEEQ: i32 = 53;
def TK_CARETEQ: i32 = 54;
def TK_LSHIFTEQ: i32 = 55;
def TK_RSHIFTEQ: i32 = 56;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 57;
def TK_MINUS: i32 = 58;
def TK_STAR: i32 = 59;
def TK_SLASH: i32 = 60;
def TK_PERCENT: i32 = 61;
def TK_AMP: i32 = 62;
def TK_PIPE: i32 = 63;
def TK_CARET: i32 = 64;
def TK_TILDE: i32 = 65;
def TK_LSHIFT: i32 = 66;
def TK_RSHIFT: i32 = 67;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 68;
def TK_NEQ: i32 = 69;
def TK_LT: i32 = 70;
def TK_LE: i32 = 71;
def TK_GT: i32 = 72;
def TK_GE: i32 = 73;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 74;
def TK_OR: i32 = 75;
def TK_NOT: i32 = 76;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 77;
def TK_ARROW: i32 = 78;
def TK_FATARROW: i32 = 79;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
def TK_LAST: i32 = 80;
def TK_LAST: i32 = 82;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -592,6 +594,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
@@ -654,6 +657,8 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_LPAREN) { return "("; };
if (k == TK_RPAREN) { return ")"; };
@@ -1374,10 +1379,18 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
let k: i32 = kwlookup(p, n: i32);
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {
@@ -1961,6 +1974,18 @@ use os;
use mem;
use tok;
// streq_local — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streq_local(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -1997,6 +2022,43 @@ fn parseprimary(p: *parser) *node = {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
@@ -2036,6 +2098,13 @@ fn parseprimary(p: *parser) *node = {
let tail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
@@ -2121,9 +2190,20 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streq_local(cur.str, "size")) { is_typeop = 1; };
if (streq_local(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;
@@ -2290,10 +2370,13 @@ fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2302,6 +2385,7 @@ fn parseletlocal(p: *parser) *node = {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2380,6 +2464,11 @@ fn parsefor(p: *parser) *node = {
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
@@ -2398,6 +2487,7 @@ fn parsestmt(p: *parser) *node = {
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");
@@ -2522,11 +2612,15 @@ fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2536,6 +2630,7 @@ fn parselet(p: *parser, exported: i32) *node = {
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2571,10 +2666,10 @@ fn parseparams(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: IDENT ':' type. Anonymous-type-only params (used
// in fn type expressions) aren't yet wired here.
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);
@@ -2722,6 +2817,19 @@ fn expectident(p: *parser, into: *str) bool = {
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
@@ -2749,7 +2857,14 @@ fn parsetype(p: *parser) *node = {
return n;
};
let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc);
n.rhs = parseexpr(p);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
@@ -2911,6 +3026,8 @@ export fn parsefile(p: *parser) *node = {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
@@ -2937,7 +3054,7 @@ export fn parsefile(p: *parser) *node = {
advance(p);
};
if (p.curkind == TK_SEMI) { advance(p); };
};};};};};
};};};};};};
if (d != nil) {
if (head == nil) {
@@ -3311,6 +3428,7 @@ type sym = struct {
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,
@@ -4270,6 +4388,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;
@@ -5253,6 +5417,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
@@ -5746,7 +5923,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -5807,10 +5984,84 @@ fn cglet(c: *cgen, n: *node) void = {
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 `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -5821,6 +6072,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -5912,6 +6186,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -5919,7 +6198,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -5932,6 +6211,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
@@ -6029,7 +6312,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;

View File

@@ -33,7 +33,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;

View File

@@ -736,6 +736,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.

View File

@@ -126,7 +126,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -187,10 +187,84 @@ fn cglet(c: *cgen, n: *node) void = {
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 `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -201,6 +275,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -292,6 +389,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -299,7 +401,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -312,6 +414,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;

View File

@@ -619,6 +619,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;

View File

@@ -488,62 +488,64 @@ def TK_FALSE: i32 = 28;
def TK_AS: i32 = 29;
def TK_STATIC: i32 = 30;
def TK_MATCH: i32 = 31;
def TK_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 32;
def TK_RPAREN: i32 = 33;
def TK_LBRACE: i32 = 34;
def TK_RBRACE: i32 = 35;
def TK_LBRACK: i32 = 36;
def TK_RBRACK: i32 = 37;
def TK_COMMA: i32 = 38;
def TK_SEMI: i32 = 39;
def TK_COLON: i32 = 40;
def TK_DOT: i32 = 41;
def TK_ELLIPSIS: i32 = 42;
def TK_DOTDOT: i32 = 43;
def TK_AT: i32 = 44;
def TK_QUESTION: i32 = 45;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 46;
def TK_PLUSEQ: i32 = 47;
def TK_MINUSEQ: i32 = 48;
def TK_STAREQ: i32 = 49;
def TK_SLASHEQ: i32 = 50;
def TK_PERCENTEQ: i32 = 51;
def TK_AMPEQ: i32 = 52;
def TK_PIPEEQ: i32 = 53;
def TK_CARETEQ: i32 = 54;
def TK_LSHIFTEQ: i32 = 55;
def TK_RSHIFTEQ: i32 = 56;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 57;
def TK_MINUS: i32 = 58;
def TK_STAR: i32 = 59;
def TK_SLASH: i32 = 60;
def TK_PERCENT: i32 = 61;
def TK_AMP: i32 = 62;
def TK_PIPE: i32 = 63;
def TK_CARET: i32 = 64;
def TK_TILDE: i32 = 65;
def TK_LSHIFT: i32 = 66;
def TK_RSHIFT: i32 = 67;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 68;
def TK_NEQ: i32 = 69;
def TK_LT: i32 = 70;
def TK_LE: i32 = 71;
def TK_GT: i32 = 72;
def TK_GE: i32 = 73;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 74;
def TK_OR: i32 = 75;
def TK_NOT: i32 = 76;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 77;
def TK_ARROW: i32 = 78;
def TK_FATARROW: i32 = 79;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
def TK_LAST: i32 = 80;
def TK_LAST: i32 = 82;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -592,6 +594,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
@@ -654,6 +657,8 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_LPAREN) { return "("; };
if (k == TK_RPAREN) { return ")"; };
@@ -1374,10 +1379,18 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
let k: i32 = kwlookup(p, n: i32);
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {
@@ -1961,6 +1974,18 @@ use os;
use mem;
use tok;
// streq_local — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streq_local(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -1997,6 +2022,43 @@ fn parseprimary(p: *parser) *node = {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
@@ -2036,6 +2098,13 @@ fn parseprimary(p: *parser) *node = {
let tail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
@@ -2121,9 +2190,20 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streq_local(cur.str, "size")) { is_typeop = 1; };
if (streq_local(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;
@@ -2290,10 +2370,13 @@ fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2302,6 +2385,7 @@ fn parseletlocal(p: *parser) *node = {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2380,6 +2464,11 @@ fn parsefor(p: *parser) *node = {
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
@@ -2398,6 +2487,7 @@ fn parsestmt(p: *parser) *node = {
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");
@@ -2522,11 +2612,15 @@ fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2536,6 +2630,7 @@ fn parselet(p: *parser, exported: i32) *node = {
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2571,10 +2666,10 @@ fn parseparams(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: IDENT ':' type. Anonymous-type-only params (used
// in fn type expressions) aren't yet wired here.
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);
@@ -2722,6 +2817,19 @@ fn expectident(p: *parser, into: *str) bool = {
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
@@ -2749,7 +2857,14 @@ fn parsetype(p: *parser) *node = {
return n;
};
let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc);
n.rhs = parseexpr(p);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
@@ -2911,6 +3026,8 @@ export fn parsefile(p: *parser) *node = {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
@@ -2937,7 +3054,7 @@ export fn parsefile(p: *parser) *node = {
advance(p);
};
if (p.curkind == TK_SEMI) { advance(p); };
};};};};};
};};};};};};
if (d != nil) {
if (head == nil) {
@@ -3311,6 +3428,7 @@ type sym = struct {
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,
@@ -4270,6 +4388,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;
@@ -5253,6 +5417,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
@@ -5746,7 +5923,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -5807,10 +5984,84 @@ fn cglet(c: *cgen, n: *node) void = {
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 `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -5821,6 +6072,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -5912,6 +6186,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -5919,7 +6198,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -5932,6 +6211,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
@@ -6029,7 +6312,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;