From 37bffa52842ab1f5846a71cdeb757d8306104930 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 11:41:12 +0900 Subject: [PATCH] =?UTF-8?q?test:=20995=5Fself=5Frebuild=20=E2=80=94=20wwst?= =?UTF-8?q?age=20rebuilds=20itself=20byte-identical?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives ww_ww (which already shells to 6c_ww/6a_ww/6l_ww) over each wwstage tool's source and diffs the resulting binary against the cstage-built canonical in $BIN. A green run means the toolchain can recompile itself end-to-end without invoking cc, modulo the cold-start binary that brings the wwstage into existence. Stricter than `make bootstrap`: that loop pins wwdump's cgen self-stabilising; this pins all five wwstage tools (6c, 6a, 6l, ww, wwdump) round-tripping through the wwstage pipeline. The .combined.ww refreshes are the expander picking up the parser/cgen changes from the prior commit. selfhost/cmd/6c/ gains its main.combined.ww for the first time — 995 builds it, 994 reads it. --- Makefile | 7 +- selfhost/cmd/6a/main.combined.ww | 9 + selfhost/cmd/6c/main.combined.ww | 6779 ++++++++++++++++++++++++++ selfhost/cmd/6l/main.combined.ww | 9 + selfhost/cmd/wwdump/main.combined.ww | 176 +- test/wwc/995_self_rebuild.c | 146 + 6 files changed, 7108 insertions(+), 18 deletions(-) create mode 100644 selfhost/cmd/6c/main.combined.ww create mode 100644 test/wwc/995_self_rebuild.c diff --git a/Makefile b/Makefile index ebc63bed..8af30ee9 100644 --- a/Makefile +++ b/Makefile @@ -188,7 +188,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_6c $(BIN)/test_6a $(BIN)/test_6l $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ $(BIN)/test_selfhost $(BIN)/test_6a_ww $(BIN)/test_6l_ww \ - $(BIN)/test_6c_ww $(BIN)/test_ww_ww + $(BIN)/test_6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild $(BIN)/test_smoke: test/wwc/000_smoke.c $(LIB)/libwwc.a | $(BIN) $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc @@ -247,6 +247,11 @@ $(BIN)/test_ww_ww: test/wwc/993_ww_ww.c $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/6c $(BIN)/6a $(BIN)/6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_self_rebuild: test/wwc/995_self_rebuild.c $(BIN)/ww_ww \ + $(BIN)/6c_ww $(BIN)/6a_ww $(BIN)/6l_ww $(BIN)/wwdump_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + test: all $(TESTS) @WW=$(BIN)/ww BIN=$(BIN) sh test/run diff --git a/selfhost/cmd/6a/main.combined.ww b/selfhost/cmd/6a/main.combined.ww index c0e78b4d..a300d860 100644 --- a/selfhost/cmd/6a/main.combined.ww +++ b/selfhost/cmd/6a/main.combined.ww @@ -24,6 +24,7 @@ def SYS_OPEN: i64 = 2; def SYS_CLOSE: i64 = 3; def SYS_LSEEK: i64 = 8; def SYS_ACCESS: i64 = 21; +def SYS_DUP2: i64 = 33; def SYS_GETPID: i64 = 39; def SYS_FORK: i64 = 57; def SYS_EXECVE: i64 = 59; @@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = { return syscall1(SYS_CLOSE, fd: i64): i32; }; +// dup2(2): make `newfd` refer to the same description as `oldfd`, +// closing `newfd` first if open. Returns `newfd` on success or a +// negative errno. Used by 6c_ww to redirect stdout into an output +// file without changing the cgen emit path. +export fn dup2(oldfd: i32, newfd: i32) i32 = { + return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32; +}; + // Fallible wrappers. The error variant is a plain str (Plan 9 errstr // model, see lib/errors); the sum type makes success/failure explicit // without overloading length-zero. diff --git a/selfhost/cmd/6c/main.combined.ww b/selfhost/cmd/6c/main.combined.ww new file mode 100644 index 00000000..c15942f5 --- /dev/null +++ b/selfhost/cmd/6c/main.combined.ww @@ -0,0 +1,6779 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_ACCESS: i64 = 21; +def SYS_DUP2: i64 = 33; +def SYS_GETPID: i64 = 39; +def SYS_FORK: i64 = 57; +def SYS_EXECVE: i64 = 59; +def SYS_EXIT: i64 = 60; +def SYS_WAIT4: i64 = 61; +def SYS_UNLINK: i64 = 87; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// dup2(2): make `newfd` refer to the same description as `oldfd`, +// closing `newfd` first if open. Returns `newfd` on success or a +// negative errno. Used by 6c_ww to redirect stdout into an output +// file without changing the cgen emit path. +export fn dup2(oldfd: i32, newfd: i32) i32 = { + return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// ---- process and filesystem helpers used by the `ww` driver ---------- + +// access(2): returns 0 if the file is reachable, negative errno +// otherwise. mode is the bitset described in (F_OK=0). +export fn access(path: *u8, mode: i32) i32 = { + return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; +}; + +// unlink(2). +export fn unlink(path: *u8) i32 = { + return syscall1(SYS_UNLINK, path: i64): i32; +}; + +// getpid(2). Used by the driver to mint unique scratch paths. +export fn getpid() i32 = { + return syscall0(SYS_GETPID): i32; +}; + +// fork(2): 0 in the child, child pid in the parent, negative errno +// on failure. +export fn fork() i32 = { + return syscall0(SYS_FORK): i32; +}; + +// execve(2): on success, does not return. +export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { + return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; +}; + +// wait4(2): wait for `pid` (or any child if -1), store status in +// `*status_out`, return the pid that ended (or negative errno). +export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { + return syscall4(SYS_WAIT4, pid: i64, status_out: i64, + options: i64, rusage: i64): i32; +}; + +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; + +// strconv — number↔string conversions. Decimal i64 to/from a fixed +// buffer. Two error idioms ship side by side: +// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the +// tagged-union work; kept for callers that already use it. +// - Hare style (parse64/parseu64): `(value | str)`. The error +// variant carries a short, allocation-free message describing +// why the parse failed. Prefer this for new code. + +// u64toa — write `v` in decimal into `buf` and return the byte count. +// Unsigned-only so callers don't have to think about wraparound when +// printing a u64 that happens to have the high bit set. +export fn u64toa(buf: []u8, v: u64) i32 = { + let tmp: [32]u8; + let i: i32 = 0; + let n: u64 = v; + for (n > 0u64) { + tmp[i] = ((n % 10u64) + 48u64): u8; + n = n / 10u64; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn i64toa(buf: []u8, v: i64) i32 = { + let neg: bool = false; + let n: i64 = v; + if (n < 0) { + neg = true; + n = -n; + }; + let tmp: [32]u8; + let i: i32 = 0; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + if (neg) { + buf[out] = 45u8; // '-' + out += 1; + }; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn atoi64(s: str) (i64, bool) = { + let v: i64 = 0; + let i: i32 = 0; + let neg: bool = false; + if (s.len > 0) { + if (s[0] == 45u8) { neg = true; i = 1; }; + }; + if (i >= s.len) { return 0, false; }; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return 0, false; }; + if (c > 57u8) { return 0, false; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v, true; +}; + +// parse64 — Hare-style fallible signed decimal parser. The value +// variant is i64; the error variant is a short str describing the +// reason. No locale, no whitespace, no underscores: a leading '-' is +// the only non-digit accepted, and only at position 0. +export fn parse64(s: str) (i64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let i: i32 = 0; + let neg: bool = false; + if (s[0] == 45u8) { neg = true; i = 1; }; + if (i >= s.len) { return "parse: lone sign"; }; + let v: i64 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v; +}; + +// parseu64 — fallible unsigned decimal parser. No leading sign. +export fn parseu64(s: str) (u64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let v: u64 = 0u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10u64 + ((c: u64) - 48u64); + i += 1; + }; + return v; +}; + +// selfhost/cmd/wwc/tok.ww — port of cmd/wwc/tok.c plus the Tkind / +// Tok / Pos shapes from cmd/wwc/ww.h. +// +// Token kind values must stay numerically equal to the C side: the +// 990_selfhost test diffs ww-side wwdump output against C-side +// wwdump output, byte-for-byte. Reordering this list shifts the +// integers and breaks the diff. +// +// Bottom of file: tokprint, which emits one token per line in a +// format identical to cmd/wwc/tok.c:tokprint(). + +use os; +use strconv; + +// ---- Tkind ------------------------------------------------------------ +// Mirror of the C enum in cmd/wwc/ww.h. Don't reorder. + +def TK_NONE: i32 = 0; +def TK_EOF: i32 = 1; +def TK_ERR: i32 = 2; +def TK_IDENT: i32 = 3; +def TK_INT: i32 = 4; +def TK_FLOAT: i32 = 5; +def TK_RUNE: i32 = 6; +def TK_STR: i32 = 7; + +def TK_FN: i32 = 8; +def TK_LET: i32 = 9; +def TK_DEF: i32 = 10; +def TK_IF: i32 = 11; +def TK_ELSE: i32 = 12; +def TK_FOR: i32 = 13; +def TK_SWITCH: i32 = 14; +def TK_CASE: i32 = 15; +def TK_RETURN: i32 = 16; +def TK_USE: i32 = 17; +def TK_TYPE: i32 = 18; +def TK_STRUCT: i32 = 19; +def TK_DEFER: i32 = 20; +def TK_BREAK: i32 = 21; +def TK_CONTINUE: i32 = 22; +def TK_EXPORT: i32 = 23; +def TK_PROC: i32 = 24; +def TK_CHAN: i32 = 25; +def TK_NIL: i32 = 26; +def TK_TRUE: i32 = 27; +def TK_FALSE: i32 = 28; +def TK_AS: i32 = 29; +def TK_STATIC: i32 = 30; +def TK_MATCH: i32 = 31; + +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_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_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_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_AND: i32 = 74; +def TK_OR: i32 = 75; +def TK_NOT: i32 = 76; + +def TK_LARROW: i32 = 77; +def TK_ARROW: i32 = 78; +def TK_FATARROW: i32 = 79; + +def TK_LAST: i32 = 80; + +// ---- Pos / Tok -------------------------------------------------------- +// +// `pos` is used at error-reporting boundaries; we always pass it via +// *pos so the value never gets struct-copied (6c can't yet copy a +// 24-byte struct). +// +// `tok` is flat — file/line/col live directly on the token rather than +// nested inside a `pos` field. Same reason: nested struct field +// assignment isn't supported, and flat primitives are. + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +type tok = struct { + kind: i32, + file: str, // path of the source the token came from + line: i32, + col: i32, + text: str, // arena-owned token text (TK_IDENT, TK_STR, TK_ERR) + uval: u64, // TK_INT, TK_RUNE + fval: f64, // TK_FLOAT + tsuffix: str, // typed numeric literal suffix or empty +}; + +// ---- keyword lookup --------------------------------------------------- + +fn streq_n(a: *u8, b: str, n: i32) bool = { + if (b.len != n) { return false; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// kwlookup — returns the matching TK_* keyword kind for a byte run, +// or TK_NONE if it's an ordinary identifier. Linear search over a +// small alphabetised list, matching cmd/wwc/tok.c. +export fn kwlookup(p: *u8, n: i32) i32 = { + if (streq_n(p, "as", n)) { return TK_AS; }; + if (streq_n(p, "break", n)) { return TK_BREAK; }; + if (streq_n(p, "case", n)) { return TK_CASE; }; + if (streq_n(p, "chan", n)) { return TK_CHAN; }; + if (streq_n(p, "continue", n)) { return TK_CONTINUE; }; + if (streq_n(p, "def", n)) { return TK_DEF; }; + if (streq_n(p, "defer", n)) { return TK_DEFER; }; + if (streq_n(p, "else", n)) { return TK_ELSE; }; + if (streq_n(p, "export", n)) { return TK_EXPORT; }; + if (streq_n(p, "false", n)) { return TK_FALSE; }; + if (streq_n(p, "fn", n)) { return TK_FN; }; + if (streq_n(p, "for", n)) { return TK_FOR; }; + if (streq_n(p, "if", n)) { return TK_IF; }; + if (streq_n(p, "let", n)) { return TK_LET; }; + if (streq_n(p, "match", n)) { return TK_MATCH; }; + if (streq_n(p, "nil", n)) { return TK_NIL; }; + if (streq_n(p, "proc", n)) { return TK_PROC; }; + if (streq_n(p, "return", n)) { return TK_RETURN; }; + if (streq_n(p, "static", n)) { return TK_STATIC; }; + if (streq_n(p, "struct", n)) { return TK_STRUCT; }; + if (streq_n(p, "switch", n)) { return TK_SWITCH; }; + if (streq_n(p, "true", n)) { return TK_TRUE; }; + if (streq_n(p, "type", n)) { return TK_TYPE; }; + if (streq_n(p, "use", n)) { return TK_USE; }; + return TK_NONE; +}; + +// ---- tokname ---------------------------------------------------------- +// +// Returns the canonical printable spelling for a token kind. Matches +// the C tokname()'s output exactly so wwdump output diffs cleanly. + +export fn tokname(k: i32) str = { + if (k == TK_NONE) { return ""; }; + if (k == TK_EOF) { return "EOF"; }; + if (k == TK_ERR) { return "ERR"; }; + if (k == TK_IDENT) { return "IDENT"; }; + if (k == TK_INT) { return "INT"; }; + if (k == TK_FLOAT) { return "FLOAT"; }; + if (k == TK_RUNE) { return "RUNE"; }; + if (k == TK_STR) { return "STR"; }; + + if (k == TK_FN) { return "fn"; }; + if (k == TK_LET) { return "let"; }; + if (k == TK_DEF) { return "def"; }; + if (k == TK_IF) { return "if"; }; + if (k == TK_ELSE) { return "else"; }; + if (k == TK_FOR) { return "for"; }; + if (k == TK_SWITCH) { return "switch"; }; + if (k == TK_CASE) { return "case"; }; + if (k == TK_RETURN) { return "return"; }; + if (k == TK_USE) { return "use"; }; + if (k == TK_TYPE) { return "type"; }; + if (k == TK_STRUCT) { return "struct"; }; + if (k == TK_DEFER) { return "defer"; }; + if (k == TK_BREAK) { return "break"; }; + if (k == TK_CONTINUE) { return "continue"; }; + if (k == TK_EXPORT) { return "export"; }; + if (k == TK_PROC) { return "proc"; }; + if (k == TK_CHAN) { return "chan"; }; + if (k == TK_NIL) { return "nil"; }; + if (k == TK_TRUE) { return "true"; }; + if (k == TK_FALSE) { return "false"; }; + if (k == TK_AS) { return "as"; }; + if (k == TK_STATIC) { return "static"; }; + if (k == TK_MATCH) { return "match"; }; + + if (k == TK_LPAREN) { return "("; }; + if (k == TK_RPAREN) { return ")"; }; + if (k == TK_LBRACE) { return "{"; }; + if (k == TK_RBRACE) { return "}"; }; + if (k == TK_LBRACK) { return "["; }; + if (k == TK_RBRACK) { return "]"; }; + if (k == TK_COMMA) { return ","; }; + if (k == TK_SEMI) { return ";"; }; + if (k == TK_COLON) { return ":"; }; + if (k == TK_DOT) { return "."; }; + if (k == TK_ELLIPSIS) { return "..."; }; + if (k == TK_DOTDOT) { return ".."; }; + if (k == TK_AT) { return "@"; }; + if (k == TK_QUESTION) { return "?"; }; + + if (k == TK_ASSIGN) { return "="; }; + if (k == TK_PLUSEQ) { return "+="; }; + if (k == TK_MINUSEQ) { return "-="; }; + if (k == TK_STAREQ) { return "*="; }; + if (k == TK_SLASHEQ) { return "/="; }; + if (k == TK_PERCENTEQ) { return "%="; }; + if (k == TK_AMPEQ) { return "&="; }; + if (k == TK_PIPEEQ) { return "|="; }; + if (k == TK_CARETEQ) { return "^="; }; + if (k == TK_LSHIFTEQ) { return "<<="; }; + if (k == TK_RSHIFTEQ) { return ">>="; }; + + if (k == TK_PLUS) { return "+"; }; + if (k == TK_MINUS) { return "-"; }; + if (k == TK_STAR) { return "*"; }; + if (k == TK_SLASH) { return "/"; }; + if (k == TK_PERCENT) { return "%"; }; + if (k == TK_AMP) { return "&"; }; + if (k == TK_PIPE) { return "|"; }; + if (k == TK_CARET) { return "^"; }; + if (k == TK_TILDE) { return "~"; }; + if (k == TK_LSHIFT) { return "<<"; }; + if (k == TK_RSHIFT) { return ">>"; }; + + if (k == TK_EQ) { return "=="; }; + if (k == TK_NEQ) { return "!="; }; + if (k == TK_LT) { return "<"; }; + if (k == TK_LE) { return "<="; }; + if (k == TK_GT) { return ">"; }; + if (k == TK_GE) { return ">="; }; + + if (k == TK_AND) { return "&&"; }; + if (k == TK_OR) { return "||"; }; + if (k == TK_NOT) { return "!"; }; + + if (k == TK_LARROW) { return "<-"; }; + if (k == TK_ARROW) { return "->"; }; + if (k == TK_FATARROW) { return "=>"; }; + + if (k == TK_LAST) { return ""; }; + return ""; +}; + +// ---- writer for tokprint ---------------------------------------------- +// +// fputq mirrors cmd/wwc/tok.c:fputq — quote the string with C-style +// escapes for \, ", \n, \t, \r and \xNN for other non-printables. + +fn fputc_byte(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn fputs_str(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn hexchar(n: u8) u8 = { + if (n < 10u8) { return n + 48u8; }; // '0'..'9' + return (n - 10u8) + 97u8; // 'a'..'f' +}; + +fn fputhex2(fd: i32, b: u8) void = { + let out: [4]u8; + out[0] = 92u8; // '\\' + out[1] = 120u8; // 'x' + out[2] = hexchar(b >> 4u8); + out[3] = hexchar(b & 15u8); + os.write(fd, out.ptr, 4u64); +}; + +fn fputq(fd: i32, p: *u8, n: i32) void = { + fputc_byte(fd, 34u8); // '"' + let i: i32 = 0; + for (i < n) { + let c: u8 = p[i]; + if (c == 92u8) { // '\\' + fputs_str(fd, "\\\\"); + } else { + if (c == 34u8) { // '"' + fputs_str(fd, "\\\""); + } else { + if (c == 10u8) { // '\n' + fputs_str(fd, "\\n"); + } else { + if (c == 9u8) { // '\t' + fputs_str(fd, "\\t"); + } else { + if (c == 13u8) { // '\r' + fputs_str(fd, "\\r"); + } else { + if (c < 32u8) { + fputhex2(fd, c); + } else { + if (c == 127u8) { + fputhex2(fd, c); + } else { + fputc_byte(fd, c); + }; + }; + }; + }; + }; + }; + }; + i += 1; + }; + fputc_byte(fd, 34u8); +}; + +// tokprint — write one token line to fd. Format must match +// cmd/wwc/tok.c:tokprint() byte-for-byte: that's the diff anchor. +// ":: [ ]\n" +// +// Takes `t` by pointer because 6c can't yet pass a >16-byte struct +// by value; the C version takes Tok by value. +export fn tokprint(fd: i32, t: *tok) void = { + // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet + // reduced by 6c — `t.x.y` returns the whole str. Lift the str + // fields into locals so we can use the str pseudo-field path. + let tfile: str = t.file; + let ttext: str = t.text; + if (tfile.len > 0) { + fputs_str(fd, tfile); + } else { + fputs_str(fd, ""); + }; + fputc_byte(fd, 58u8); // ':' + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], t.line: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 58u8); + n = strconv.i64toa(buf[0:32], t.col: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 32u8); // ' ' + fputs_str(fd, tokname(t.kind)); + + if (t.kind == TK_IDENT) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_STR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_ERR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_INT) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + } else { if (t.kind == TK_RUNE) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + };};};};}; + // TK_FLOAT is intentionally not handled here — %g formatting + // won't byte-match across implementations. Diff fixtures must + // be float-free until we implement a stable float formatter. + + fputc_byte(fd, 10u8); // '\n' +}; + +// ascii — byte-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family. Bytes outside 0..127 always +// answer `false`. The lexer hot path uses these inline; they are +// expected to inline to a couple of compares. + +export fn isdigit(c: u8) bool = { + if (c < 48u8) { return false; }; + if (c > 57u8) { return false; }; + return true; +}; + +export fn isupper(c: u8) bool = { + if (c < 65u8) { return false; }; + if (c > 90u8) { return false; }; + return true; +}; + +export fn islower(c: u8) bool = { + if (c < 97u8) { return false; }; + if (c > 122u8) { return false; }; + return true; +}; + +export fn isalpha(c: u8) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: u8) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: u8) bool = { + if (c == 32u8) { return true; }; // ' ' + if (c == 9u8) { return true; }; // '\t' + if (c == 10u8) { return true; }; // '\n' + if (c == 11u8) { return true; }; // '\v' + if (c == 12u8) { return true; }; // '\f' + if (c == 13u8) { return true; }; // '\r' + return false; +}; + +export fn ishex(c: u8) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65u8) { + if (c <= 70u8) { return true; }; // 'A'..'F' + }; + if (c >= 97u8) { + if (c <= 102u8) { return true; }; // 'a'..'f' + }; + return false; +}; + +// digitval — value of `c` as a hex/decimal digit, or -1 if not one. +// Useful when scanning numeric literals. +export fn digitval(c: u8) i32 = { + if (isdigit(c)) { return (c - 48u8): i32; }; + if (c >= 65u8) { + if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; + }; + if (c >= 97u8) { + if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; + }; + return -1; +}; + +// isidstart / isidpart — identifier classes used by the lexer. +// Alpha or '_' starts; alnum or '_' continues. +export fn isidstart(c: u8) bool = { + if (isalpha(c)) { return true; }; + if (c == 95u8) { return true; }; // '_' + return false; +}; + +export fn isidpart(c: u8) bool = { + if (isalnum(c)) { return true; }; + if (c == 95u8) { return true; }; + return false; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: u8) u8 = { + if (isupper(c)) { return c + 32u8; }; + return c; +}; + +export fn toupper(c: u8) u8 = { + if (islower(c)) { return c - 32u8; }; + return c; +}; + +// selfhost/cmd/wwc/lex.ww — port of cmd/wwc/lex.c. +// +// The DFA, the helpers, and the order of decisions all mirror the C +// version exactly. The 990_selfhost test diffs the resulting token +// stream against the C-side wwdump byte-for-byte; any divergence is +// a port bug. +// +// Calling-convention note: 6c can't yet pass or return structs >16 +// bytes by value, so `tok` and `pos` are passed by pointer (out +// params). The C version passes `Tok` by value; we differ here only +// in shape, not in observable behaviour. Token kind values stay +// numerically identical. + +use os; +use ascii; +use mem; +use tok; + +type lex = struct { + file: str, + src: *u8, // raw bytes; not necessarily NUL-terminated + srclen: u64, + lpos: u64, + line: i32, + col: i32, + a: *arena, + errs: i32, +}; + +export fn lexinit(l: *lex, a: *arena, file: str, src: *u8, len: u64) void = { + l.file = file; + l.src = src; + l.srclen = len; + l.lpos = 0u64; + l.line = 1; + l.col = 1; + l.a = a; + l.errs = 0; +}; + +// srcb — byte at offset; helper that lifts the cast out of indexing. +fn srcb(l: *lex, off: u64) i32 = { + let i: i32 = off: i32; + let b: u8 = l.src[i]; + return b: i32; +}; + +fn lpeek(l: *lex, ahead: u64) i32 = { + let p: u64 = l.lpos + ahead; + if (p >= l.srclen) { return -1; }; + return srcb(l, p); +}; + +fn lget(l: *lex) i32 = { + if (l.lpos >= l.srclen) { return -1; }; + let c: i32 = srcb(l, l.lpos); + l.lpos += 1u64; + if (c == 10) { // '\n' + l.line += 1; + l.col = 1; + } else { + l.col += 1; + }; + return c; +}; + +fn cur_pos(l: *lex, out: *pos) void = { + out.file = l.file; + out.line = l.line; + out.col = l.col; +}; + +// putuint — write `v` (signed, but always non-negative here) to fd 2 +// in decimal. Standalone so err_at doesn't drag in fmt and create a +// dependency cycle with strconv. +fn putuint(fd: i32, v: i32) void = { + let tmp: [16]u8; + let i: i32 = 0; + let n: i32 = v; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { tmp[0] = 48u8; i = 1; }; + let buf: [16]u8; + let m: i32 = 0; + for (i > 0) { i -= 1; buf[m] = tmp[i]; m += 1; }; + os.write(fd, buf.ptr, m: u64); +}; + +fn err_at(l: *lex, p: *pos, msg: str) void = { + let pf: str = p.file; + os.write(2, pf.ptr, pf.len: u64); + os.write(2, ":".ptr, 1u64); + putuint(2, p.line); + os.write(2, ":".ptr, 1u64); + putuint(2, p.col); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + l.errs += 1; +}; + +fn skipws(l: *lex) bool = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { return false; }; + if (c == 32) { lget(l); continue; }; + if (c == 9) { lget(l); continue; }; + if (c == 13) { lget(l); continue; }; + if (c == 10) { lget(l); continue; }; + if (c == 47) { // '/' + let c2: i32 = lpeek(l, 1u64); + if (c2 == 47) { + for (true) { + let cx: i32 = lpeek(l, 0u64); + if (cx < 0) { return false; }; + if (cx == 10) { break; }; + lget(l); + }; + continue; + }; + if (c2 == 42) { // '*' + lget(l); lget(l); + let prev: i32 = -1; + for (true) { + let x: i32 = lget(l); + if (x < 0) { + let cp: pos; + cur_pos(l, &cp); + err_at(l, &cp, "unterminated /* comment"); + return false; + }; + if (prev == 42) { + if (x == 47) { break; }; + }; + prev = x; + }; + continue; + }; + }; + return true; + }; + return false; +}; + +fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = { + let v: u64 = 0u64; + let got: bool = false; + let i: u64 = 0u64; + for (i < n) { + let ix: i32 = i: i32; + let c: u8 = p[ix]; + if (c == 95u8) { // '_' + i += 1u64; + continue; + }; + let d: i32 = -1; + if (c >= 48u8) { + if (c <= 57u8) { d = (c - 48u8): i32; }; + }; + if (d < 0) { + if (c >= 97u8) { + if (c <= 102u8) { d = ((c - 97u8) + 10u8): i32; }; + }; + }; + if (d < 0) { + if (c >= 65u8) { + if (c <= 70u8) { d = ((c - 65u8) + 10u8): i32; }; + }; + }; + if (d < 0) { *ok = false; return 0u64; }; + if (d >= base) { *ok = false; return 0u64; }; + v = v * (base: u64) + (d: u64); + got = true; + i += 1u64; + }; + *ok = got; + return v; +}; + +fn escape(l: *lex, out: *i32) bool = { + let c: i32 = lget(l); + if (c < 0) { return false; }; + if (c == 110) { *out = 10; return true; }; + if (c == 116) { *out = 9; return true; }; + if (c == 114) { *out = 13; return true; }; + if (c == 92) { *out = 92; return true; }; + if (c == 39) { *out = 39; return true; }; + if (c == 34) { *out = 34; return true; }; + if (c == 48) { *out = 0; return true; }; + if (c == 97) { *out = 7; return true; }; + if (c == 98) { *out = 8; return true; }; + if (c == 102) { *out = 12; return true; }; + if (c == 118) { *out = 11; return true; }; + if (c == 120) { + let hi: i32 = lget(l); + let lo: i32 = lget(l); + if (hi < 0) { return false; }; + if (lo < 0) { return false; }; + if (!ascii.ishex(hi: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + if (!ascii.ishex(lo: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + let h: i32 = ascii.digitval(hi: u8); + let lv: i32 = ascii.digitval(lo: u8); + *out = (h << 4) | lv; + return true; + }; + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad escape"); + return false; +}; + +// scan_decimal_run — consume a run of decimal digits and underscores. +fn scan_decimal_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_hex_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.ishex(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_bin_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c == 48) { lget(l); continue; }; + if (c == 49) { lget(l); continue; }; + if (c == 95) { lget(l); continue; }; + break; + }; +}; + +fn scan_oct_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 48) { break; }; + if (c > 55) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +// scan_exp — consume the [eE][+-]?[0-9]+ tail of a float, if present. +fn scan_exp(l: *lex) void = { + let e: i32 = lpeek(l, 0u64); + if (e != 101) { if (e != 69) { return; }; }; // 'e' or 'E' + lget(l); + let s: i32 = lpeek(l, 0u64); + if (s == 43) { lget(l); } + else { if (s == 45) { lget(l); }; }; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { break; }; + lget(l); + }; +}; + +fn lexnum(l: *lex, start: *pos, out: *tok) void = { + out.kind = TK_INT; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let begin: u64 = l.lpos; + let base: i32 = 10; + let isfloat: bool = false; + + let c0: i32 = lpeek(l, 0u64); + let c1: i32 = lpeek(l, 1u64); + + if (c0 == 48) { // '0' + if (c1 == 120) { // 'x' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 88) { // 'X' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 98) { // 'b' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 66) { // 'B' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 111) { // 'o' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { if (c1 == 79) { // 'O' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + };};};};};}; + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + }; + + let n: u64 = l.lpos - begin; + out.text = astrndup(l.a, l.src + begin, n); + + if (isfloat) { + // out.fval is already 0 from the top-of-lexnext clear. + // We don't strtod the literal yet — the diff fixtures we + // care about are float-free; any TK_FLOAT seen in source + // gets a placeholder value until we wire a real parser. + out.kind = TK_FLOAT; + } else { + let digs: *u8 = l.src + begin; + let dn: u64 = n; + if (base != 10) { + digs = digs + 2u64; + dn -= 2u64; + }; + let ok: bool = false; + out.uval = parseint(digs, dn, base, &ok); + if (!ok) { + err_at(l, start, "bad integer literal"); + out.kind = TK_ERR; + }; + }; + + let pc: i32 = lpeek(l, 0u64); + if (pc >= 0) { + if (ascii.isidstart(pc: u8)) { + let sb: u64 = l.lpos; + for (true) { + let cc: i32 = lpeek(l, 0u64); + if (cc < 0) { break; }; + if (!ascii.isidpart(cc: u8)) { break; }; + lget(l); + }; + let sl: u64 = l.lpos - sb; + let p: *u8 = l.src + sb; + let isok: bool = false; + if (sl == 2u64) { + if (p[0] == 105u8) { + if (p[1] == 56u8) { isok = true; }; // i8 + }; + if (p[0] == 117u8) { + if (p[1] == 56u8) { isok = true; }; // u8 + }; + }; + if (sl == 3u64) { + if (p[0] == 105u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; // i16 + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // i32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // i64 + }; + if (p[0] == 117u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; + }; + if (p[0] == 102u8) { + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // f32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // f64 + }; + }; + if (isok) { + out.tsuffix = astrndup(l.a, p, sl); + } else { + l.lpos = sb; + }; + }; + }; +}; + +fn lexident(l: *lex, start: *pos, out: *tok) void = { + let begin: u64 = l.lpos; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isidpart(c: u8)) { break; }; + lget(l); + }; + 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; + if (k != TK_NONE) { + out.kind = k; + } else { + out.kind = TK_IDENT; + }; + out.text = astrndup(l.a, p, n); +}; + +fn lexstr(l: *lex, start: *pos, out: *tok) void = { + let cap: u64 = 32u64; + let nb: u64 = 0u64; + let buf: *u8 = amalloc(l.a, cap): *u8; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated string"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + if (c == 34) { lget(l); break; }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (nb + 1u64 >= cap) { + let ncap: u64 = cap * 2u64; + let nb2: *u8 = amalloc(l.a, ncap): *u8; + let i: u64 = 0u64; + for (i < nb) { + let ix: i32 = i: i32; + nb2[ix] = buf[ix]; + i += 1u64; + }; + buf = nb2; + cap = ncap; + }; + let nbi: i32 = nb: i32; + buf[nbi] = ch: u8; + nb += 1u64; + }; + out.kind = TK_STR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let s: str; + s.ptr = buf; + s.len = nb: i32; + out.text = s; +}; + +fn lexrune(l: *lex, start: *pos, out: *tok) void = { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated rune"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (lpeek(l, 0u64) != 39) { + err_at(l, start, "rune literal missing closing '"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + lget(l); + out.kind = TK_RUNE; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.uval = ch: u64; +}; + +fn emit_simple(start: *pos, k: i32, out: *tok) void = { + out.kind = k; + out.file = start.file; + out.line = start.line; + out.col = start.col; +}; + +// set_pos_from — copy file/line/col from a *pos into a tok. Used by +// the err-token path where we already have a pos. +fn set_pos_from(out: *tok, p: *pos) void = { + out.file = p.file; + out.line = p.line; + out.col = p.col; +}; + +export fn lexnext(l: *lex, out: *tok) void = { + // Reset the out token so callers can rely on stale fields being + // cleared (they only inspect kind, pos, text, uval, fval, tsuffix + // per kind). + out.kind = TK_NONE; + out.uval = 0u64; + // out.fval starts cleared by the caller's stack-local init (lex.ww + // allocates the tok with `let t: tok;` which zeroes). We avoid + // writing a 0.0 literal here so this file itself stays float-free + // and the C/ww wwdump diff over it is byte-identical. + let empty: str; + empty.ptr = nil; + empty.len = 0; + out.text = empty; + out.tsuffix = empty; + + if (!skipws(l)) { + let p: pos; cur_pos(l, &p); + emit_simple(&p, TK_EOF, out); + return; + }; + let start: pos; cur_pos(l, &start); + let c: i32 = lpeek(l, 0u64); + + if (c >= 0) { + if (ascii.isidstart(c: u8)) { lexident(l, &start, out); return; }; + if (ascii.isdigit(c: u8)) { lexnum(l, &start, out); return; }; + }; + + if (c == 34) { lget(l); lexstr(l, &start, out); return; }; + if (c == 39) { lget(l); lexrune(l, &start, out); return; }; + + lget(l); + + if (c == 40) { emit_simple(&start, TK_LPAREN, out); return; }; + if (c == 41) { emit_simple(&start, TK_RPAREN, out); return; }; + if (c == 123) { emit_simple(&start, TK_LBRACE, out); return; }; + if (c == 125) { emit_simple(&start, TK_RBRACE, out); return; }; + if (c == 91) { emit_simple(&start, TK_LBRACK, out); return; }; + if (c == 93) { emit_simple(&start, TK_RBRACK, out); return; }; + if (c == 44) { emit_simple(&start, TK_COMMA, out); return; }; + if (c == 59) { emit_simple(&start, TK_SEMI, out); return; }; + if (c == 58) { emit_simple(&start, TK_COLON, out); return; }; + if (c == 64) { emit_simple(&start, TK_AT, out); return; }; + if (c == 63) { emit_simple(&start, TK_QUESTION, out); return; }; + if (c == 126) { emit_simple(&start, TK_TILDE, out); return; }; + + if (c == 46) { // '.' + if (lpeek(l, 0u64) == 46) { + if (lpeek(l, 1u64) == 46) { + lget(l); lget(l); + emit_simple(&start, TK_ELLIPSIS, out); return; + }; + lget(l); + emit_simple(&start, TK_DOTDOT, out); return; + }; + emit_simple(&start, TK_DOT, out); return; + }; + + if (c == 43) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PLUSEQ, out); return; }; + emit_simple(&start, TK_PLUS, out); return; + }; + if (c == 45) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_MINUSEQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_ARROW, out); return; }; + emit_simple(&start, TK_MINUS, out); return; + }; + if (c == 42) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_STAREQ, out); return; }; + emit_simple(&start, TK_STAR, out); return; + }; + if (c == 47) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_SLASHEQ, out); return; }; + emit_simple(&start, TK_SLASH, out); return; + }; + if (c == 37) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PERCENTEQ, out); return; }; + emit_simple(&start, TK_PERCENT, out); return; + }; + if (c == 38) { + if (lpeek(l, 0u64) == 38) { lget(l); emit_simple(&start, TK_AND, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_AMPEQ, out); return; }; + emit_simple(&start, TK_AMP, out); return; + }; + if (c == 124) { + if (lpeek(l, 0u64) == 124) { lget(l); emit_simple(&start, TK_OR, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PIPEEQ, out); return; }; + emit_simple(&start, TK_PIPE, out); return; + }; + if (c == 94) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_CARETEQ, out); return; }; + emit_simple(&start, TK_CARET, out); return; + }; + if (c == 61) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_EQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_FATARROW, out); return; }; + emit_simple(&start, TK_ASSIGN, out); return; + }; + if (c == 33) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_NEQ, out); return; }; + emit_simple(&start, TK_NOT, out); return; + }; + if (c == 60) { + if (lpeek(l, 0u64) == 60) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LSHIFTEQ, out); return; }; + emit_simple(&start, TK_LSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LE, out); return; }; + if (lpeek(l, 0u64) == 45) { lget(l); emit_simple(&start, TK_LARROW, out); return; }; + emit_simple(&start, TK_LT, out); return; + }; + if (c == 62) { + if (lpeek(l, 0u64) == 62) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_RSHIFTEQ, out); return; }; + emit_simple(&start, TK_RSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_GE, out); return; }; + emit_simple(&start, TK_GT, out); return; + }; + + err_at(l, &start, "unexpected character"); + out.kind = TK_ERR; + set_pos_from(out, &start); + let one: [1]u8; + one[0] = c: u8; + out.text = astrndup(l.a, one.ptr, 1u64); +}; + +// selfhost/cmd/wwc/ast.ww — port of cmd/wwc/ast.c (Node defs + printer). +// +// Status: AST printer is fully ported. Constructor `newnode` is here. +// The parser (parse.ww) is currently minimal — see its file header. +// +// Calling-convention shim: same as tok/lex — `node` is too big to pass +// by value (8 *node pointers + 2 strs + a few ints), so callers always +// hand around `*node`. Only `newnode` allocates and returns a *node. + +use os; +use strconv; +use mem; +use tok; + +// ---- Nkind ------------------------------------------------------------ +// +// Mirror of cmd/wwc/ww.h Nkind. Values must stay numerically equal so +// the AST diff probe in 990_selfhost works. + +def N_NONE: i32 = 0; + +def N_INTLIT: i32 = 1; +def N_FLOATLIT: i32 = 2; +def N_STRLIT: i32 = 3; +def N_RUNELIT: i32 = 4; +def N_TRUE: i32 = 5; +def N_FALSE: i32 = 6; +def N_NIL: i32 = 7; +def N_IDENT: i32 = 8; + +def N_BIN: i32 = 9; +def N_UN: i32 = 10; +def N_CALL: i32 = 11; +def N_INDEX: i32 = 12; +def N_DOT: i32 = 13; +def N_CAST: i32 = 14; +def N_STRUCTLIT:i32 = 15; +def N_ARRLIT: i32 = 16; +def N_FIELD: i32 = 17; +def N_ASSIGN: i32 = 18; +def N_ALLOC: i32 = 19; +def N_FREE: i32 = 20; +def N_RECV: i32 = 21; +def N_SLICE: i32 = 22; +def N_SPREAD: i32 = 23; + +def N_BLOCK: i32 = 24; +def N_EXPRSTMT: i32 = 25; +def N_LET: i32 = 26; +def N_RETURN: i32 = 27; +def N_IF: i32 = 28; +def N_FOR: i32 = 29; +def N_FORRANGE: i32 = 30; +def N_DEFER: i32 = 31; +def N_BREAK: i32 = 32; +def N_CONTINUE: i32 = 33; +def N_SWITCH: i32 = 34; +def N_CASE: i32 = 35; + +def N_FILE: i32 = 36; +def N_USE: i32 = 37; +def N_DEF: i32 = 38; +def N_TYPEDECL: i32 = 39; +def N_FNDECL: i32 = 40; +def N_PARAM: i32 = 41; + +def N_TNAME: i32 = 42; +def N_TPTR: i32 = 43; +def N_TSLICE: i32 = 44; +def N_TARRAY: i32 = 45; +def N_TFN: i32 = 46; +def N_TSTRUCT: i32 = 47; +def N_TFIELD: i32 = 48; +def N_TCHAN: i32 = 49; + +def N_ATTR: i32 = 50; +def N_TTUPLE: i32 = 51; +def N_TTAGGED: i32 = 52; +def N_TUPLE: i32 = 53; +def N_MATCH: i32 = 54; +def N_MCASE: i32 = 55; +def N_TRYPROP: i32 = 56; +def N_TRYUNW: i32 = 57; +def N_MLET: i32 = 58; +def N_MASSIGN: i32 = 59; + +def N_LAST: i32 = 60; + +// ---- Node ------------------------------------------------------------- + +type node = struct { + kind: i32, + file: str, + line: i32, + col: i32, + op: i32, // for N_BIN / N_UN / N_ASSIGN + str: str, + uval: u64, + fval: f64, + lhs: *node, + rhs: *node, + cond: *node, + body: *node, + els: *node, + list: *node, + next: *node, + attr: *node, + exported: i32, // bool — `export` keyword present + type_: *void, // filled in by checker; type.ww treats it as *tinfo + tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) +}; + +export fn newnode(a: *arena, k: i32, file: str, line: i32, col: i32) *node = { + let n: *node = amalloc(a, 192u64): *node; // 192 ≥ struct size + n.kind = k; + n.file = file; + n.line = line; + n.col = col; + return n; +}; + +// ---- printer ---------------------------------------------------------- + +fn nkname(k: i32) str = { + if (k == N_NONE) { return "none"; }; + if (k == N_INTLIT) { return "int"; }; + if (k == N_FLOATLIT) { return "float"; }; + if (k == N_STRLIT) { return "str"; }; + if (k == N_RUNELIT) { return "rune"; }; + if (k == N_TRUE) { return "true"; }; + if (k == N_FALSE) { return "false"; }; + if (k == N_NIL) { return "nil"; }; + if (k == N_IDENT) { return "id"; }; + if (k == N_BIN) { return "bin"; }; + if (k == N_UN) { return "un"; }; + if (k == N_CALL) { return "call"; }; + if (k == N_INDEX) { return "index"; }; + if (k == N_DOT) { return "dot"; }; + if (k == N_CAST) { return "cast"; }; + if (k == N_STRUCTLIT) { return "structlit"; }; + if (k == N_ARRLIT) { return "arrlit"; }; + if (k == N_FIELD) { return "field"; }; + if (k == N_ASSIGN) { return "assign"; }; + if (k == N_ALLOC) { return "alloc"; }; + if (k == N_FREE) { return "free"; }; + if (k == N_RECV) { return "recv"; }; + if (k == N_SLICE) { return "slice"; }; + if (k == N_SPREAD) { return "spread"; }; + if (k == N_BLOCK) { return "block"; }; + if (k == N_EXPRSTMT) { return "exprstmt"; }; + if (k == N_LET) { return "let"; }; + if (k == N_RETURN) { return "return"; }; + if (k == N_IF) { return "if"; }; + if (k == N_FOR) { return "for"; }; + if (k == N_FORRANGE) { return "forrange"; }; + if (k == N_DEFER) { return "defer"; }; + if (k == N_BREAK) { return "break"; }; + if (k == N_CONTINUE) { return "continue"; }; + if (k == N_SWITCH) { return "switch"; }; + if (k == N_CASE) { return "case"; }; + if (k == N_FILE) { return "file"; }; + if (k == N_USE) { return "use"; }; + if (k == N_DEF) { return "def"; }; + if (k == N_TYPEDECL) { return "typedecl"; }; + if (k == N_FNDECL) { return "fn"; }; + if (k == N_PARAM) { return "param"; }; + if (k == N_TNAME) { return "tname"; }; + if (k == N_TPTR) { return "tptr"; }; + if (k == N_TSLICE) { return "tslice"; }; + if (k == N_TARRAY) { return "tarray"; }; + if (k == N_TFN) { return "tfn"; }; + if (k == N_TSTRUCT) { return "tstruct"; }; + if (k == N_TFIELD) { return "tfield"; }; + if (k == N_TCHAN) { return "tchan"; }; + if (k == N_ATTR) { return "attr"; }; + if (k == N_TTUPLE) { return "ttuple"; }; + if (k == N_TTAGGED) { return "ttagged"; }; + if (k == N_TUPLE) { return "tuple"; }; + if (k == N_MATCH) { return "match"; }; + if (k == N_MCASE) { return "mcase"; }; + if (k == N_TRYPROP) { return "tryprop"; }; + if (k == N_TRYUNW) { return "tryunw"; }; + if (k == N_MLET) { return "mlet"; }; + if (k == N_MASSIGN) { return "massign"; }; + if (k == N_LAST) { return "last"; }; + return "?"; +}; + +fn ind(fd: i32, d: i32) void = { + let i: i32 = 0; + for (i < d) { + os.write(fd, " ".ptr, 2u64); + i += 1; + }; +}; + +fn putc1(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn putq(fd: i32, s: str) void = { + putc1(fd, 34u8); // '"' + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c == 34u8) { // '"' + os.write(fd, "\\\"".ptr, 2u64); + } else { if (c == 92u8) { // '\\' + os.write(fd, "\\\\".ptr, 2u64); + } else { if (c == 10u8) { // '\n' + os.write(fd, "\\n".ptr, 2u64); + } else { if (c == 9u8) { // '\t' + os.write(fd, "\\t".ptr, 2u64); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + putc1(fd, c); + };};};};}; + i += 1; + }; + putc1(fd, 34u8); +}; + +fn pr(fd: i32, n: *node, d: i32) void = { + if (n == nil) { + ind(fd, d); + os.write(fd, "()\n".ptr, 3u64); + return; + }; + ind(fd, d); + putc1(fd, 40u8); // '(' + let nm: str = nkname(n.kind); + os.write(fd, nm.ptr, nm.len: u64); + + if (n.kind == N_INTLIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if (n.kind == N_RUNELIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if ( + n.kind == N_STRLIT || + n.kind == N_IDENT || + n.kind == N_USE || + n.kind == N_DOT || + n.kind == N_DEF || + n.kind == N_TYPEDECL || + n.kind == N_FNDECL || + n.kind == N_PARAM || + n.kind == N_LET || + n.kind == N_TNAME || + n.kind == N_TFIELD || + n.kind == N_FIELD || + n.kind == N_ATTR + ) { + // Match C ast.c: print the str field whenever it's non-nil, + // even if its length is zero (e.g. an empty STRLIT prints + // `(str ""`). + let s: str = n.str; + if (s.ptr != nil) { + putc1(fd, 32u8); + putq(fd, s); + }; + } else { if ( + n.kind == N_BIN || + n.kind == N_UN || + n.kind == N_ASSIGN + ) { + putc1(fd, 32u8); + let on: str = tokname(n.op); + os.write(fd, on.ptr, on.len: u64); + };};};}; + + if (n.kind == N_FNDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_DEF) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_TYPEDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + putc1(fd, 10u8); // '\n' + + if (n.attr != nil) { + ind(fd, d + 1); + os.write(fd, "(@\n".ptr, 3u64); + let m: *node = n.attr; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; + if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; + if (n.cond != nil) { pr(fd, n.cond, d + 1); }; + if (n.body != nil) { pr(fd, n.body, d + 1); }; + if (n.els != nil) { pr(fd, n.els, d + 1); }; + if (n.list != nil) { + ind(fd, d + 1); + os.write(fd, "(list\n".ptr, 6u64); + let m: *node = n.list; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + ind(fd, d); + os.write(fd, ")\n".ptr, 2u64); +}; + +export fn astprint(fd: i32, n: *node) void = { + pr(fd, n, 0); +}; + +// selfhost/cmd/wwc/parse.ww — port of cmd/wwc/parse.c. +// +// Status: GROWING stub. Currently handles top-level `use IDENT;`, +// `def NAME: TYPE = LIT;`, `type NAME = TYPE;`, and `fn NAME(params) +// RET;` (header-only — bodies are recovered past). Unknown decls are +// chewed token-by-token until the next ';' so the diff probe can +// still anchor on partial fixtures. +// +// The full port is multi-session work — parse.c is 1,183 lines of +// hand-rolled recursive descent + Pratt expression parser. Each +// surface form lands here gradually so the AST diff in 990_selfhost +// grows toward whole-language coverage one increment at a time. +// +// Calling-convention shim: 6c can't yet pass a sub-struct field +// (e.g. p.cur.line where p.cur is a `tok` of size 76). The parser +// stores the current token as flat primitive fields rather than a +// nested `tok` struct; `refill` copies a freshly lexed token in. + +use os; +use mem; +use tok; + +type parser = struct { + l: *lex, + a: *arena, + errs: i32, + // nocast: while inside `[...]` we treat ':' as the slice + // separator, not the cast operator. Mirrors parse.c's flag. + nocast: i32, + cur_kind: i32, + cur_file: str, + cur_line: i32, + cur_col: i32, + cur_text: str, + cur_uval: u64, +}; + +fn refill(p: *parser) void = { + let t: tok; + lexnext(p.l, &t); + p.cur_kind = t.kind; + p.cur_file = t.file; + p.cur_line = t.line; + p.cur_col = t.col; + p.cur_text = t.text; + p.cur_uval = t.uval; +}; + +export fn parserinit(p: *parser, a: *arena, l: *lex) void = { + p.l = l; + p.a = a; + p.errs = 0; + p.nocast = 0; + refill(p); +}; + +fn advance(p: *parser) void = { refill(p); }; + +fn accept_tok(p: *parser, k: i32) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + return false; +}; + +fn err_msg(p: *parser, msg: str) void = { + let pre: str = "parse: "; + os.write(2, pre.ptr, pre.len: u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + p.errs += 1; +}; + +fn expect_tok(p: *parser, k: i32, what: str) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + err_msg(p, what); + return false; +}; + +// expectident — consume the current TK_IDENT and return its text. +// Returns the empty str on error (and advances to make progress). +fn expectident(p: *parser, into: *str) bool = { + if (p.cur_kind != TK_IDENT) { + err_msg(p, "expected identifier"); + advance(p); + return false; + }; + *into = p.cur_text; + advance(p); + return true; +}; + +// ---- type expressions ------------------------------------------------ +// +// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`). +// Other forms (slice, array, struct, fn, chan, tuple, tagged) will +// land in subsequent commits. + +fn parsetype(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_LBRACK) { + advance(p); + if (p.cur_kind == TK_RBRACK) { + advance(p); + let n: *node = newnode(p.a, N_TSLICE, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc); + n.rhs = parseexpr(p); + expect_tok(p, TK_RBRACK, "expected ']' in array type"); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_STRUCT) { + advance(p); + expect_tok(p, TK_LBRACE, "expected '{' after struct"); + let n: *node = newnode(p.a, N_TSTRUCT, pf, pl, pc); + let fhead: *node = nil; + let ftail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let f: *node = newnode(p.a, N_TFIELD, fpf, fpl, fpc); + let fid: str; + expectident(p, &fid); + f.str = fid; + expect_tok(p, TK_COLON, "expected ':' in field"); + f.lhs = parsetype(p); + if (fhead == nil) { fhead = f; ftail = f; } + else { ftail.next = f; ftail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct fields"); + n.list = fhead; + return n; + }; + + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // Dotted path collapse (pkg.Type) deferred — fixtures don't + // need it yet. + return n; + }; + + if (p.cur_kind == TK_LPAREN) { + // (T) or (T, T, ...) or (T | T | ...) + advance(p); + let first: *node = parsetype(p); + if (accept_tok(p, TK_PIPE)) { + let n: *node = newnode(p.a, N_TTAGGED, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_PIPE)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tagged-union type"); + n.list = head; + return n; + }; + if (!accept_tok(p, TK_COMMA)) { + expect_tok(p, TK_RPAREN, "expected ')' after parenthesised type"); + return first; + }; + let n: *node = newnode(p.a, N_TTUPLE, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple type"); + n.list = head; + return n; + }; + + if (p.cur_kind == TK_FN) { + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after fn in type"); + let n: *node = newnode(p.a, N_TFN, pf, pl, pc); + // Anonymous-or-named params: parseparams handles named only; + // for fn-type expressions the C parser allows IDENT-less + // (anonymous) params. Stub: only named params for now. + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after fn type params"); + n.lhs = parsetype(p); + return n; + }; + + err_msg(p, "expected type"); + advance(p); + return newnode(p.a, N_TNAME, pf, pl, pc); +}; + +// ---- expressions (Pratt) --------------------------------------------- +// +// Forwards: parseexpr → parsebin → parseunary → parsepostfix(parseprimary). +// Tuple literals, match expressions, struct literals, slice [lo:hi], +// and the ?/! try operators are not yet wired — they'll arrive as the +// AST diff fixture grows to need them. + +fn bprec(k: i32) i32 = { + if (k == TK_OR) { return 1; }; + if (k == TK_AND) { return 2; }; + if (k == TK_EQ) { return 3; }; + if (k == TK_NEQ) { return 3; }; + if (k == TK_LT) { return 4; }; + if (k == TK_LE) { return 4; }; + if (k == TK_GT) { return 4; }; + if (k == TK_GE) { return 4; }; + if (k == TK_PIPE) { return 5; }; + if (k == TK_CARET) { return 6; }; + if (k == TK_AMP) { return 7; }; + if (k == TK_LSHIFT) { return 8; }; + if (k == TK_RSHIFT) { return 8; }; + if (k == TK_PLUS) { return 9; }; + if (k == TK_MINUS) { return 9; }; + if (k == TK_STAR) { return 10; }; + if (k == TK_SLASH) { return 10; }; + if (k == TK_PERCENT) { return 10; }; + return 0; +}; + +fn isassignop(k: i32) bool = { + if (k == TK_ASSIGN) { return true; }; + if (k == TK_PLUSEQ) { return true; }; + if (k == TK_MINUSEQ) { return true; }; + if (k == TK_STAREQ) { return true; }; + if (k == TK_SLASHEQ) { return true; }; + if (k == TK_PERCENTEQ) { return true; }; + if (k == TK_AMPEQ) { return true; }; + if (k == TK_PIPEEQ) { return true; }; + if (k == TK_CARETEQ) { return true; }; + if (k == TK_LSHIFTEQ) { return true; }; + if (k == TK_RSHIFTEQ) { return true; }; + return false; +}; + +// Forward references between parseunary/parseexpr/parsebin/parsepostfix +// are resolved by the two-pass checker — no body-less prototypes needed. + +fn parseprimary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_INT) { + let n: *node = newnode(p.a, N_INTLIT, pf, pl, pc); + n.uval = p.cur_uval; + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_STR) { + let n: *node = newnode(p.a, N_STRLIT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_RUNE) { + let n: *node = newnode(p.a, N_RUNELIT, pf, pl, pc); + n.uval = p.cur_uval; + advance(p); + return n; + }; + if (p.cur_kind == TK_TRUE) { + advance(p); + return newnode(p.a, N_TRUE, pf, pl, pc); + }; + if (p.cur_kind == TK_FALSE) { + advance(p); + return newnode(p.a, N_FALSE, pf, pl, pc); + }; + if (p.cur_kind == TK_NIL) { + advance(p); + return newnode(p.a, N_NIL, pf, pl, pc); + }; + if (p.cur_kind == TK_LPAREN) { + advance(p); + let e: *node = parseexpr(p); + // Tuple literal: (a, b, ...) + if (accept_tok(p, TK_COMMA)) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = e; + let tail: *node = e; + for (true) { + if (p.cur_kind == TK_RPAREN) { break; }; + let en: *node = parseexpr(p); + tail.next = en; + tail = en; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple"); + return t; + }; + expect_tok(p, TK_RPAREN, "expected ')'"); + return e; + }; + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_IDENT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // `IDENT {` — struct literal. Disambiguate: only consume as a + // struct lit when we're not in a context where '{' starts a + // block (e.g. `if (cond) {`). The parser is called from + // expressions, never directly from cond contexts that need a + // block; in stmt parsing, the for/if drivers consume their + // own paren/cond, so this is safe. + if (p.cur_kind == TK_LBRACE) { + advance(p); + let s: *node = newnode(p.a, N_STRUCTLIT, pf, pl, pc); + s.lhs = n; + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let id: str; + expectident(p, &id); + expect_tok(p, TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(p.a, N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + return s; + }; + return n; + }; + if (p.cur_kind == TK_MATCH) { + // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after match"); + let m: *node = newnode(p.a, N_MATCH, pf, pl, pc); + m.lhs = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after match scrutinee"); + expect_tok(p, TK_LBRACE, "expected '{' to open match body"); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_CASE) { + let cf: str = p.cur_file; + let cl: i32 = p.cur_line; + let cc: i32 = p.cur_col; + advance(p); // past `case` + let mc: *node = newnode(p.a, N_MCASE, cf, cl, cc); + if (p.cur_kind == TK_LET) { + advance(p); + let id: str; + expectident(p, &id); + mc.str = id; + expect_tok(p, TK_COLON, "expected ':' after match binding"); + mc.lhs = parsetype(p); + } else { if (p.cur_kind != TK_FATARROW) { + mc.lhs = parsetype(p); + };}; + expect_tok(p, TK_FATARROW, "expected '=>' in match arm"); + mc.body = parsestmt(p); + if (head == nil) { head = mc; tail = mc; } + else { tail.next = mc; tail = mc; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after match body"); + m.list = head; + return m; + }; + err_msg(p, "expected expression"); + advance(p); + return newnode(p.a, N_NONE, pf, pl, pc); +}; + +fn parsearglist(p: *parser, close_kind: i32, head_out: **node) void = { + *head_out = nil; + if (p.cur_kind == close_kind) { return; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let e: *node = parseexpr(p); + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == close_kind) { break; }; + }; + *head_out = head; +}; + +fn parsepostfix(p: *parser, lhs: *node) *node = { + let cur: *node = lhs; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + if (p.cur_kind == TK_LPAREN) { + 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; + expect_tok(p, TK_RPAREN, "expected ')' after args"); + cur = n; + continue; + }; + if (p.cur_kind == TK_LBRACK) { + advance(p); + // `[ : hi ]` — slice with implicit lo = 0. + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + // Suppress cast inside `[...]` so ':' parses as slice + // separator rather than the postfix cast operator. + let prev: i32 = p.nocast; + p.nocast = 1; + let e: *node = parseexpr(p); + p.nocast = prev; + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + let n: *node = newnode(p.a, N_INDEX, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + expect_tok(p, TK_RBRACK, "expected ']' after index"); + cur = n; + continue; + }; + if (p.cur_kind == TK_DOT) { + advance(p); + let n: *node = newnode(p.a, N_DOT, pf, pl, pc); + n.lhs = cur; + let id: str; + expectident(p, &id); + n.str = id; + cur = n; + continue; + }; + if (p.cur_kind == TK_COLON) { + if (p.nocast != 0) { + return cur; + }; + advance(p); + let n: *node = newnode(p.a, N_CAST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + break; + }; + return cur; +}; + +fn parseunary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let k: i32 = p.cur_kind; + if (k == TK_MINUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_MINUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_PLUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_PLUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_NOT) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_NOT; n.lhs = parseunary(p); + return n; + }; + if (k == TK_TILDE) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_TILDE; n.lhs = parseunary(p); + return n; + }; + if (k == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_STAR; n.lhs = parseunary(p); + return n; + }; + if (k == TK_AMP) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_AMP; n.lhs = parseunary(p); + return n; + }; + return parsepostfix(p, parseprimary(p)); +}; + +fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { + let cur: *node = lhs; + for (true) { + let op: i32 = p.cur_kind; + let pr: i32 = bprec(op); + if (pr == 0) { return cur; }; + if (pr < minp) { return cur; }; + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let rhs: *node = parseunary(p); + for (true) { + let np: i32 = bprec(p.cur_kind); + if (np <= pr) { break; }; + rhs = parsebin(p, rhs, np); + }; + let n: *node = newnode(p.a, N_BIN, pf, pl, pc); + n.op = op; n.lhs = cur; n.rhs = rhs; + cur = n; + }; + return cur; +}; + +fn parseexpr(p: *parser) *node = { + let e: *node = parsebin(p, parseunary(p), 1); + if (isassignop(p.cur_kind)) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let op: i32 = p.cur_kind; + advance(p); + let n: *node = newnode(p.a, N_ASSIGN, pf, pl, pc); + n.op = op; + n.lhs = e; + n.rhs = parseexpr(p); // right-associative + return n; + }; + return e; +}; + +// ---- statements ------------------------------------------------------ +// +// Subset wired today: block, let, return, if (no else-if chain), for +// (single-cond C-style), expr-stmt, defer, break, continue. Switch +// and match arms are not yet wired; tuple-let / multi-let neither. + +fn parselet_local(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + return n; +}; + +fn parseblock(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + expect_tok(p, TK_LBRACE, "expected '{' to open block"); + let blk: *node = newnode(p.a, N_BLOCK, pf, pl, pc); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (head == nil) { head = s; tail = s; } + else { tail.next = s; tail = s; }; + }; + }; + expect_tok(p, TK_RBRACE, "expected '}' to close block"); + blk.list = head; + return blk; +}; + +fn parseif(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `if` + expect_tok(p, TK_LPAREN, "expected '(' after if"); + let n: *node = newnode(p.a, N_IF, pf, pl, pc); + n.cond = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after if condition"); + n.body = parseblock(p); + if (accept_tok(p, TK_ELSE)) { + if (p.cur_kind == TK_IF) { + n.els = parseif(p); + } else { + n.els = parseblock(p); + }; + }; + return n; +}; + +fn parsefor(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `for` + expect_tok(p, TK_LPAREN, "expected '(' after for"); + let n: *node = newnode(p.a, N_FOR, pf, pl, pc); + // Three forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — full + // for (true) — infinite (cond is 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. + if (p.cur_kind == TK_LET) { + n.lhs = parselet_local(p); // init (consumes its own ';') + n.cond = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + } else { + // Parse one expr. If next is ';', it's a 3-clause without init. + let first: *node = parseexpr(p); + if (accept_tok(p, TK_SEMI)) { + // cond ; post + n.cond = first; + n.rhs = parseexpr(p); + } else { + // just (cond) + n.cond = first; + }; + }; + expect_tok(p, TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + return n; +}; + +fn parsestmt(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + // `static` is allowed on local lets per Hare; we accept and skip + // it (it doesn't change the AST shape). + if (p.cur_kind == TK_STATIC) { advance(p); }; + + if (p.cur_kind == TK_LBRACE) { + let b: *node = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after block"); + return b; + }; + if (p.cur_kind == TK_LET) { return parselet_local(p); }; + if (p.cur_kind == TK_IF) { + let n: *node = parseif(p); + expect_tok(p, TK_SEMI, "expected ';' after if"); + return n; + }; + if (p.cur_kind == TK_FOR) { + let n: *node = parsefor(p); + expect_tok(p, TK_SEMI, "expected ';' after for"); + return n; + }; + if (p.cur_kind == TK_RETURN) { + advance(p); + let n: *node = newnode(p.a, N_RETURN, pf, pl, pc); + if (p.cur_kind != TK_SEMI) { + let first: *node = parseexpr(p); + // Hare-style multi-value: `return a, b;` becomes a + // tuple expression so codegen sees one rvalue. + if (p.cur_kind == TK_COMMA) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = first; + let tail: *node = first; + for (accept_tok(p, TK_COMMA)) { + let e: *node = parseexpr(p); + tail.next = e; + tail = e; + }; + n.lhs = t; + } else { + n.lhs = first; + }; + }; + expect_tok(p, TK_SEMI, "expected ';' after return"); + return n; + }; + if (p.cur_kind == TK_DEFER) { + advance(p); + let n: *node = newnode(p.a, N_DEFER, pf, pl, pc); + n.lhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after defer"); + return n; + }; + if (p.cur_kind == TK_BREAK) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after break"); + return newnode(p.a, N_BREAK, pf, pl, pc); + }; + if (p.cur_kind == TK_CONTINUE) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after continue"); + return newnode(p.a, N_CONTINUE, pf, pl, pc); + }; + // expression statement, or tuple-destructure multi-assign: + // a, b = expr; + // Mirrors cmd/wwc/parse.c:1015-1031. We parse the first lvalue + // with parseexpr (matches the C side); subsequent lvalues go + // through parsebin(parseunary, 1) so the `=` stays for us to + // consume — parseexpr would absorb it. + let e: *node = parseexpr(p); + if (p.cur_kind == TK_COMMA) { + let m: *node = newnode(p.a, N_MASSIGN, pf, pl, pc); + let head: *node = e; + let tail: *node = e; + for (p.cur_kind == TK_COMMA) { + advance(p); + let lv: *node = parsebin(p, parseunary(p), 1); + tail.next = lv; + tail = lv; + }; + expect_tok(p, TK_ASSIGN, "expected '=' after multi-assign lvalues"); + m.rhs = parseexpr(p); + m.list = head; + expect_tok(p, TK_SEMI, "expected ';' after multi-assign"); + return m; + }; + let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc); + n.lhs = e; + expect_tok(p, TK_SEMI, "expected ';' after expression statement"); + return n; +}; + +// ---- top-level decl parsers ------------------------------------------ + +fn parseuse(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `use` + let n: *node = newnode(p.a, N_USE, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_SEMI, "expected ';' after use"); + return n; +}; + +fn parsedef(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `def` + let n: *node = newnode(p.a, N_DEF, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in def"); + n.lhs = parsetype(p); + expect_tok(p, TK_ASSIGN, "expected '=' in def"); + n.rhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after def"); + n.exported = exported; + return n; +}; + +fn parselet(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + n.exported = exported; + return n; +}; + +fn parseattrs(p: *parser) *node = { + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_AT) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let a: *node = newnode(p.a, N_ATTR, pf, pl, pc); + let id: str; + expectident(p, &id); + a.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after attribute name"); + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expect_tok(p, TK_RPAREN, "expected ')' after attribute args"); + if (head == nil) { head = a; tail = a; } + else { tail.next = a; tail = a; }; + }; + return head; +}; + +fn parseparams(p: *parser) *node = { + if (p.cur_kind == TK_RPAREN) { return nil; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + 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. + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in parameter"); + n.lhs = parsetype(p); + if (head == nil) { head = n; tail = n; } + else { tail.next = n; tail = n; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + return head; +}; + +fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `fn` + let n: *node = newnode(p.a, N_FNDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after fn name"); + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after params"); + if (p.cur_kind != TK_ASSIGN) { + if (p.cur_kind != TK_SEMI) { + n.lhs = parsetype(p); + }; + }; + if (accept_tok(p, TK_ASSIGN)) { + n.body = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after fn body"); + } else { + // Body-less fn: FFI declaration (`fn name(args) ret;`). + expect_tok(p, TK_SEMI, "expected ';' after fn header"); + }; + n.exported = exported; + n.attr = attrs; + return n; +}; + +fn parsetypedecl(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `type` + let n: *node = newnode(p.a, N_TYPEDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_ASSIGN, "expected '=' in type decl"); + n.lhs = parsetype(p); + expect_tok(p, TK_SEMI, "expected ';' after type decl"); + n.exported = exported; + return n; +}; + +// ---- file-level loop ------------------------------------------------- + +export fn parsefile(p: *parser) *node = { + let f: *node = newnode(p.a, N_FILE, p.cur_file, p.cur_line, p.cur_col); + let head: *node = nil; + let tail: *node = nil; + + for (p.cur_kind != TK_EOF) { + let attrs: *node = parseattrs(p); + let exported: i32 = 0; + if (p.cur_kind == TK_EXPORT) { exported = 1; advance(p); }; + + let d: *node = nil; + if (p.cur_kind == TK_USE) { + d = parseuse(p); + } else { if (p.cur_kind == TK_DEF) { + d = parsedef(p, exported); + } else { if (p.cur_kind == TK_TYPE) { + d = parsetypedecl(p, exported); + } else { if (p.cur_kind == TK_LET) { + d = parselet(p, exported); + } else { if (p.cur_kind == TK_FN) { + d = parsefn(p, exported, attrs); + } else { + // Recovery: chew tokens until next ';' or EOF, balancing + // '{' '}' pairs so internal ';'s in unfamiliar forms don't + // derail us. + for (p.cur_kind != TK_SEMI) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { + let depth: i32 = 0; + for (true) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { depth += 1; advance(p); continue; }; + if (p.cur_kind == TK_RBRACE) { + depth -= 1; + advance(p); + if (depth == 0) { break; }; + continue; + }; + advance(p); + }; + continue; + }; + advance(p); + }; + if (p.cur_kind == TK_SEMI) { advance(p); }; + };};};};}; + + if (d != nil) { + if (head == nil) { + head = d; + tail = d; + } else { + tail.next = d; + tail = d; + }; + }; + }; + f.list = head; + return f; +}; + +// selfhost/cmd/wwc/type.ww — port of cmd/wwc/type.c. +// +// Status: full structural port. The C version uses module-globals for +// the primitive types (ty_void, ty_i32, …); ww doesn't have writable +// global storage yet, so we bundle the primitives into a `tctx` that +// the checker passes around explicitly. typesinit fills the tctx +// once per arena. + +use os; +use mem; + +// ---- TypeKind --------------------------------------------------------- +// Numeric values must stay aligned with cmd/wwc/ww.h TypeKind so the +// next diff signal (typed-AST printer / cgen) can compare across the +// two implementations. + +def TY_NONE: i32 = 0; +def TY_VOID: i32 = 1; +def TY_BOOL: i32 = 2; +def TY_RUNE: i32 = 3; +def TY_I8: i32 = 4; +def TY_I16: i32 = 5; +def TY_I32: i32 = 6; +def TY_I64: i32 = 7; +def TY_U8: i32 = 8; +def TY_U16: i32 = 9; +def TY_U32: i32 = 10; +def TY_U64: i32 = 11; +def TY_UINT: i32 = 12; +def TY_INT: i32 = 13; +def TY_UINTPTR: i32 = 14; +def TY_F32: i32 = 15; +def TY_F64: i32 = 16; +def TY_STR: i32 = 17; +def TY_PTR: i32 = 18; +def TY_SLICE: i32 = 19; +def TY_ARRAY: i32 = 20; +def TY_STRUCT: i32 = 21; +def TY_FN: i32 = 22; +def TY_CHAN: i32 = 23; +def TY_NAMED: i32 = 24; +def TY_TUPLE: i32 = 25; +def TY_TAGGED: i32 = 26; +def TY_ERR: i32 = 27; +def TY_UNTYPED_INT: i32 = 28; +def TY_UNTYPED_FLOAT: i32 = 29; +def TY_UNTYPED_STR: i32 = 30; +def TY_UNTYPED_RUNE: i32 = 31; +def TY_UNTYPED_BOOL: i32 = 32; +def TY_UNTYPED_NIL: i32 = 33; + +// ---- tinfo / tfield / tparam ----------------------------------------- + +type tfield = struct { + name: str, + type_: *tinfo, + offset: u64, + tnext: *tfield, +}; + +type tparam = struct { + name: str, + type_: *tinfo, + tnext: *tparam, +}; + +type tinfo = struct { + kind: i32, + size: u64, + align: u64, + sub: *tinfo, // ptr/slice/array/chan element + alen: u64, + fields: *tfield, + params: *tparam, + ret: *tinfo, + variadic: i32, + name: str, + under: *tinfo, +}; + +// ---- tctx — the box of primitive types ------------------------------- + +type tctx = struct { + a: *arena, + ty_void: *tinfo, + ty_bool: *tinfo, + ty_rune: *tinfo, + ty_i8: *tinfo, + ty_i16: *tinfo, + ty_i32: *tinfo, + ty_i64: *tinfo, + ty_u8: *tinfo, + ty_u16: *tinfo, + ty_u32: *tinfo, + ty_u64: *tinfo, + ty_int: *tinfo, + ty_uint: *tinfo, + ty_uintptr: *tinfo, + ty_f32: *tinfo, + ty_f64: *tinfo, + ty_str: *tinfo, + ty_err: *tinfo, + ty_untyped_int: *tinfo, + ty_untyped_float: *tinfo, + ty_untyped_str: *tinfo, + ty_untyped_rune: *tinfo, + ty_untyped_bool: *tinfo, + ty_untyped_nil: *tinfo, +}; + +// ---- constructors ----------------------------------------------------- + +export fn newtype(a: *arena, k: i32) *tinfo = { + let t: *tinfo = amalloc(a, 96u64): *tinfo; + t.kind = k; + return t; +}; + +fn prim(a: *arena, k: i32, nm: str, sz: u64, al: u64) *tinfo = { + let t: *tinfo = newtype(a, k); + t.name = nm; + t.size = sz; + if (al > 0u64) { t.align = al; } else { t.align = sz; }; + return t; +}; + +export fn typesinit(c: *tctx, a: *arena) void = { + c.a = a; + c.ty_void = prim(a, TY_VOID, "void", 0u64, 1u64); + c.ty_bool = prim(a, TY_BOOL, "bool", 1u64, 1u64); + c.ty_rune = prim(a, TY_RUNE, "rune", 4u64, 4u64); + c.ty_i8 = prim(a, TY_I8, "i8", 1u64, 1u64); + c.ty_i16 = prim(a, TY_I16, "i16", 2u64, 2u64); + c.ty_i32 = prim(a, TY_I32, "i32", 4u64, 4u64); + c.ty_i64 = prim(a, TY_I64, "i64", 8u64, 8u64); + c.ty_u8 = prim(a, TY_U8, "u8", 1u64, 1u64); + c.ty_u16 = prim(a, TY_U16, "u16", 2u64, 2u64); + c.ty_u32 = prim(a, TY_U32, "u32", 4u64, 4u64); + c.ty_u64 = prim(a, TY_U64, "u64", 8u64, 8u64); + c.ty_int = prim(a, TY_INT, "int", 8u64, 8u64); + c.ty_uint = prim(a, TY_UINT, "uint", 8u64, 8u64); + c.ty_uintptr= prim(a, TY_UINTPTR, "uintptr", 8u64, 8u64); + c.ty_f32 = prim(a, TY_F32, "f32", 4u64, 4u64); + c.ty_f64 = prim(a, TY_F64, "f64", 8u64, 8u64); + c.ty_str = prim(a, TY_STR, "str", 16u64, 8u64); + c.ty_err = prim(a, TY_ERR, "", 0u64, 1u64); + + c.ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); + c.ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); + c.ty_untyped_str = prim(a, TY_UNTYPED_STR, "untyped_str", 0u64, 1u64); + c.ty_untyped_rune = prim(a, TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); + c.ty_untyped_bool = prim(a, TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); + c.ty_untyped_nil = prim(a, TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); +}; + +export fn type_ptr(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_PTR); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_slice(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_SLICE); + t.sub = sub; + t.size = 24u64; + t.align = 8u64; + return t; +}; + +export fn type_array(a: *arena, sub: *tinfo, n: u64) *tinfo = { + let t: *tinfo = newtype(a, TY_ARRAY); + t.sub = sub; + t.alen = n; + if (sub != nil) { + t.size = sub.size * n; + t.align = sub.align; + } else { + t.align = 1u64; + }; + return t; +}; + +export fn type_chan(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_CHAN); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_named(a: *arena, name: str, under: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_NAMED); + t.name = name; + t.under = under; + if (under != nil) { + t.size = under.size; + t.align = under.align; + }; + return t; +}; + +// ---- predicates ------------------------------------------------------- + +export fn type_isint(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_I8) { return true; }; + if (k == TY_I16) { return true; }; + if (k == TY_I32) { return true; }; + if (k == TY_I64) { return true; }; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_INT) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_RUNE){ return true; }; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_NAMED) { return type_isint(t.under); }; + return false; +}; + +export fn type_isfloat(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_F32) { return true; }; + if (k == TY_F64) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_NAMED) { return type_isfloat(t.under); }; + return false; +}; + +export fn type_isnum(t: *tinfo) bool = { + if (type_isint(t)) { return true; }; + return type_isfloat(t); +}; + +export fn type_isunsigned(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_NAMED) { return type_isunsigned(t.under); }; + return false; +}; + +export fn type_isuntyped(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_UNTYPED_STR) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_UNTYPED_BOOL) { return true; }; + if (k == TY_UNTYPED_NIL) { return true; }; + return false; +}; + +// type_eq — structural equality. Named types compare nominally. +export fn type_eq(a: *tinfo, b: *tinfo) bool = { + if (a == b) { return true; }; + if (a == nil) { return false; }; + if (b == nil) { return false; }; + if (a.kind != b.kind) { return false; }; + let k: i32 = a.kind; + if (k == TY_PTR) { return type_eq(a.sub, b.sub); }; + if (k == TY_SLICE) { return type_eq(a.sub, b.sub); }; + if (k == TY_CHAN) { return type_eq(a.sub, b.sub); }; + if (k == TY_ARRAY) { + if (a.alen != b.alen) { return false; }; + return type_eq(a.sub, b.sub); + }; + if (k == TY_FN) { + if (a.variadic != b.variadic) { return false; }; + if (!type_eq(a.ret, b.ret)) { return false; }; + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + if (k == TY_STRUCT) { + let fa: *tfield = a.fields; + let fb: *tfield = b.fields; + for (true) { + if (fa == nil) { if (fb == nil) { return true; }; return false; }; + if (fb == nil) { return false; }; + let na: str = fa.name; + let nb: str = fb.name; + if (na.len != nb.len) { return false; }; + let i: i32 = 0; + for (i < na.len) { + if (na[i] != nb[i]) { return false; }; + i += 1; + }; + if (!type_eq(fa.type_, fb.type_)) { return false; }; + fa = fa.tnext; + fb = fb.tnext; + }; + return true; + }; + if (k == TY_NAMED) { return false; }; // nominal: only same ptr + if (k == TY_TUPLE) { + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + return true; // primitives match by kind alone +}; + +// selfhost/cmd/wwc/sym.ww — port of cmd/wwc/sym.c. +// +// Per-scope hashtable, chained to the parent. Lookup walks up. +// Plan 9 / Hare flavoured. Duplicate definitions in the same scope +// return nil; the caller flags the error. + +use mem; +use typ; +use ast; + +// Symbol kinds — must stay numerically aligned with cmd/wwc/ww.h Skind. +def SK_NONE: i32 = 0; +def SK_VAR: i32 = 1; +def SK_PARAM: i32 = 2; +def SK_DEF: i32 = 3; +def SK_TYPE: i32 = 4; +def SK_FN: i32 = 5; +def SK_USE: i32 = 6; +def SK_FIELD: i32 = 7; + +type sym = struct { + name: str, + skind: i32, + type_: *tinfo, + decl: *node, + exported: i32, + snext: *sym, // iteration order + hashnext: *sym, // hash bucket chain + scope: *scope, +}; + +def NBUCKETS: i32 = 16; + +type scope = struct { + parent: *scope, + first: *sym, + last: *sym, + buckets: **sym, // length = NBUCKETS + nbuckets: i32, + a: *arena, +}; + +// FNV-1a 64 — same hash the C side uses, so bucket distribution is +// identical when both walk a scope in declaration order. +fn hashstr(s: str) u64 = { + let h: u64 = 14695981039346656037u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + h = h ^ (c: u64); + h = h * 1099511628211u64; + i += 1; + }; + return h; +}; + +export fn newscope(a: *arena, parent: *scope) *scope = { + let s: *scope = amalloc(a, 64u64): *scope; + s.parent = parent; + s.a = a; + s.nbuckets = NBUCKETS; + s.buckets = amalloc(a, (NBUCKETS: u64) * 8u64): **sym; + return s; +}; + +export fn streq(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; +}; + +export fn scope_lookup_local(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + let bn: str = b.name; + if (streq(bn, name)) { return b; }; + b = b.hashnext; + }; + return nil; +}; + +export fn scope_lookup(s: *scope, name: str) *sym = { + for (s != nil) { + let r: *sym = scope_lookup_local(s, name); + if (r != nil) { return r; }; + s = s.parent; + }; + return nil; +}; + +export fn scope_define(s: *scope, name: str, k: i32, t: *tinfo, decl: *node) *sym = { + if (scope_lookup_local(s, name) != nil) { return nil; }; + let sy: *sym = amalloc(s.a, 80u64): *sym; + sy.name = name; + sy.skind = k; + sy.type_ = t; + sy.decl = decl; + sy.scope = s; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + sy.hashnext = s.buckets[bi]; + s.buckets[bi] = sy; + if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; + s.last = sy; + return sy; +}; + +// selfhost/cmd/wwc/check.ww — minimal port of cmd/wwc/check.c. +// +// Status: name-resolution + primitive-type seeding only. Full type +// inference, conversion rules, tagged-union dispatch typing, return- +// type checking, etc. all live in cmd/wwc/check.c (937 lines) and +// will land here in subsequent commits. +// +// What this version does: +// 1. Creates a top scope and seeds it with primitive type names so +// `i32`, `str`, `*u8` etc. resolve. +// 2. Walks the file's top-level decls (use/def/type/fn/let) and +// installs Sym entries for each. +// 3. Recursively walks fn bodies; for every N_IDENT used as an +// expression or as a type name, looks it up and counts the +// resolved vs. unresolved. +// 4. Returns a summary the caller (wwdump -r) prints; the test +// asserts unresolved == 0 on every selfhost fixture, which is +// the floor signal that the frontend can name-resolve real ww. + +use os; +use mem; +use tok; + +type checker = struct { + a: *arena, + tc: *tctx, + top: *scope, + cur: *scope, + nresolved: i32, + nunresolved: i32, + errs: i32, + verbose: i32, // when non-zero, log each unresolved name +}; + +// seed_primitives — install the built-in type names so `i32`, `str`, +// etc. can be looked up like ordinary symbols. +fn seed_primitives(c: *checker) void = { + scope_define(c.top, "void", SK_TYPE, c.tc.ty_void, nil); + scope_define(c.top, "bool", SK_TYPE, c.tc.ty_bool, nil); + scope_define(c.top, "rune", SK_TYPE, c.tc.ty_rune, nil); + scope_define(c.top, "i8", SK_TYPE, c.tc.ty_i8, nil); + scope_define(c.top, "i16", SK_TYPE, c.tc.ty_i16, nil); + scope_define(c.top, "i32", SK_TYPE, c.tc.ty_i32, nil); + scope_define(c.top, "i64", SK_TYPE, c.tc.ty_i64, nil); + scope_define(c.top, "u8", SK_TYPE, c.tc.ty_u8, nil); + scope_define(c.top, "u16", SK_TYPE, c.tc.ty_u16, nil); + scope_define(c.top, "u32", SK_TYPE, c.tc.ty_u32, nil); + scope_define(c.top, "u64", SK_TYPE, c.tc.ty_u64, nil); + scope_define(c.top, "int", SK_TYPE, c.tc.ty_int, nil); + scope_define(c.top, "uint", SK_TYPE, c.tc.ty_uint, nil); + scope_define(c.top, "uintptr", SK_TYPE, c.tc.ty_uintptr, nil); + scope_define(c.top, "f32", SK_TYPE, c.tc.ty_f32, nil); + scope_define(c.top, "f64", SK_TYPE, c.tc.ty_f64, nil); + scope_define(c.top, "str", SK_TYPE, c.tc.ty_str, nil); + // `nil`, `true`, `false` are keywords — handled at the lex/parser + // level, no symbol needed. + // `len`, `alloc`, `free` are pseudo-builtins; scope_define them so + // their use sites resolve. The actual semantics live in cgen. + scope_define(c.top, "len", SK_FN, nil, nil); + scope_define(c.top, "alloc", SK_FN, nil, nil); + scope_define(c.top, "free", SK_FN, nil, nil); +}; + +// install_decl — install the top-level decl's name into the top scope. +// We don't compute its type yet (that's the resolve pass) — just bind +// the name so forward references resolve. +fn install_decl(c: *checker, d: *node) void = { + if (d == nil) { return; }; + let k: i32 = d.kind; + let nm: str = d.str; + if (k == N_USE) { scope_define(c.top, nm, SK_USE, nil, d); return; }; + if (k == N_DEF) { scope_define(c.top, nm, SK_DEF, nil, d); return; }; + if (k == N_TYPEDECL) { scope_define(c.top, nm, SK_TYPE, nil, d); return; }; + if (k == N_FNDECL) { scope_define(c.top, nm, SK_FN, nil, d); return; }; + if (k == N_LET) { scope_define(c.top, nm, SK_VAR, nil, d); return; }; +}; + +// resolve_walk — recursive AST walk that, for every N_IDENT and +// N_TNAME seen, looks up the name and bumps the resolved/unresolved +// counters. Local lets are installed in the current scope as soon as +// their init/type expressions have been walked (forward use of a let +// before its declaration would resolve to nothing — same semantics as +// the C checker's collect-then-resolve flow within a function). +fn resolve_walk(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + // `use IDENT;` — name is a module label, not a free ident. + if (k == N_USE) { return; }; + + if (k == N_IDENT) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved id: ".ptr, 17u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + if (k == N_TNAME) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved tname: ".ptr, 20u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + // `match (e) { case let v: T => stmt; ... }` — the binding `v` + // is declared by the case arm and visible inside its body. + if (k == N_MCASE) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + if (n.body != nil) { resolve_walk(c, n.body); }; + return; + }; + + if (k == N_DOT) { + // Walk only the base; the .field name is a member, not a + // free identifier. + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_FIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_TFIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + // Walk children (mirroring ast.ww's printer descent order). + if (n.attr != nil) { resolve_walk(c, n.attr); }; + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + if (n.rhs != nil) { resolve_walk(c, n.rhs); }; + if (n.cond != nil) { resolve_walk(c, n.cond); }; + if (n.body != nil) { resolve_walk(c, n.body); }; + if (n.els != nil) { resolve_walk(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + resolve_walk(c, m); + m = m.next; + }; + }; + + // After walking children: a local `let X: T = init;` registers + // `X` so subsequent statements can resolve it. Top-level lets + // are installed in install_decl, so this duplicate install at + // the file scope just no-ops (scope_define returns nil on dup). + if (k == N_LET) { + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + }; +}; + +// install_param — when entering a fn body, define its params in a +// fresh local scope. +fn install_params(c: *checker, params: *node) void = { + let p: *node = params; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_PARAM, nil, p); + }; + }; + p = p.next; + }; +}; + +// resolve_fnbody — open a child scope for the fn, install its params, +// then walk the body. Local lets installed by walk_stmt (a future +// extension); for the current pass we just resolve-walk without +// per-statement scopes. +fn resolve_fnbody(c: *checker, fnnode: *node) void = { + let outer: *scope = c.cur; + c.cur = newscope(c.a, c.cur); + install_params(c, fnnode.list); + if (fnnode.body != nil) { + resolve_walk(c, fnnode.body); + }; + c.cur = outer; +}; + +export fn check_init(c: *checker, a: *arena, tc: *tctx) void = { + c.a = a; + c.tc = tc; + c.top = newscope(a, nil); + c.cur = c.top; + c.nresolved = 0; + c.nunresolved = 0; + c.errs = 0; + c.verbose = 0; + seed_primitives(c); +}; + +export fn check_file(c: *checker, file: *node) void = { + if (file == nil) { return; }; + if (file.kind != N_FILE) { return; }; + + // Pass 1: install all top-level names. + let d: *node = file.list; + for (d != nil) { + install_decl(c, d); + d = d.next; + }; + + // Pass 2: walk decl bodies/types and resolve identifiers. + d = file.list; + for (d != nil) { + let k: i32 = d.kind; + if (k == N_FNDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; // return type + resolve_fnbody(c, d); + } else { if (k == N_DEF) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + } else { if (k == N_TYPEDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + } else { if (k == N_LET) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + };};};}; + d = d.next; + }; +}; + +// selfhost/cmd/wwc/cgen.ww — port of cmd/6c/cgen.c. +// +// Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` +// producing byte-identical output to C-side `6c` for the same source, +// then by assembling + linking + running the result. +// +// Current coverage: +// - decls: N_FILE, N_FNDECL (params, frame for locals, prologue +// + dual-epilogue suppression; FFI body-less fn skipped) +// - stmts: N_BLOCK, N_RETURN, N_EXPRSTMT, N_LET (no init), +// N_LET (int-literal / ident / call / N_BIN init), +// N_IF (with optional else), N_FOR (cond-only and full +// init/cond/post), N_BREAK, N_CONTINUE +// - exprs: N_INTLIT, N_IDENT (local/param), N_BIN with full op +// coverage (+/-/*/// %, &/|/^, <>, comparisons with +// signed-vs-unsigned dispatch, &&/||), N_UN (- ! ~ & *), +// N_CALL (recursive R-to-L push, pop into argregs L-to-R), +// N_ASSIGN to local idents (plain and compound +=/-=) +// +// Type info is shallow — frame slots are 8 bytes per local, all loads +// /stores are MOVQ. Programs that mix i8/i32/i64 locals work but spill +// 8 bytes per local. Float, str, slice, struct, match, defer, alloc, +// tagged-union return — none of those are wired yet. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- typedef alias registry ----------------------------------------- +// +// `type error = str;` makes `error` a struct-shape alias. We track +// alias→target so is_str_type / is_slice_type / struct_lookup can +// resolve through the chain. Only direct N_TNAME aliases are mapped; +// `type p = struct {...}` is handled by collect_structs. + +type alias_ent = struct { + aname: str, + target: *node, // the rhs type expr + aanext: *alias_ent, +}; + +fn collect_aliases(c: *cgen, file: *node) void = { + c.aliases = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind != N_TSTRUCT) { + let a: *alias_ent = amalloc(c.a, 32u64): *alias_ent; + a.aname = d.str; + a.target = body; + a.aanext = c.aliases; + c.aliases = a; + }; + }; + }; + d = d.next; + }; +}; + +fn alias_lookup(c: *cgen, name: str) *node = { + let a: *alias_ent = c.aliases; + for (a != nil) { + let an: str = a.aname; + if (streq(an, name)) { return a.target; }; + a = a.aanext; + }; + return nil; +}; + +// resolve_type — follow typedef alias chains to a "canonical" type +// expr (str/slice/array/struct/...). Stops on cycles via depth limit. +fn resolve_type(c: *cgen, t: *node) *node = { + let cur: *node = t; + let depth: i32 = 0; + for (depth < 16) { + if (cur == nil) { return nil; }; + if (cur.kind != N_TNAME) { return cur; }; + let nm: str = cur.str; + let next: *node = alias_lookup(c, nm); + if (next == nil) { return cur; }; + cur = next; + depth += 1; + }; + return cur; +}; + +// ---- struct registry ------------------------------------------------ +// +// Per-file map from struct name → list of fields with computed offsets +// and sizes. Built when cg_file walks N_TYPEDECL with N_TSTRUCT lhs. +// N_DOT and N_ASSIGN consult this to resolve `s.field` for struct or +// *struct bases. + +type field_info = struct { + fname: str, + foff: i32, + fsz: i32, + tnode: *node, // the field type expr, for nested struct lookups + finext: *field_info, +}; + +type struct_info = struct { + sname: str, + fields: *field_info, + tot_size: i32, + sinext: *struct_info, +}; + +// ---- locals / frame -------------------------------------------------- + +type local = struct { + name: str, + off: i32, + tnode: *node, // declared type expr (N_TNAME / N_TPTR / ...) or nil + lnext: *local, +}; + +// strlit — interned string literal record. Emitted as a DATA directive +// after all functions; cgexpr N_STRLIT loads (LEAQ ptr, MOVQ len). +type strlit = struct { + label: str, // "_S_" + bytes: str, + slnext: *strlit, +}; + +// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr +// gets its CALL target rewritten to `name`. +type ffi = struct { + ident: str, + symbol: str, + fnext: *ffi, +}; + +def LOOP_MAX: i32 = 16; + +type cgen = struct { + a: *arena, + locals: *local, + frame: i32, + last_was_return: i32, + labelseq: i32, + strlit_seq: i32, + strlits: *strlit, + ffis: *ffi, + defs: *def_ent, + fnrets: *fnret, + aliases: *alias_ent, + structs: *struct_info, + fn_name: str, + fn_ret: *node, // declared return type of current fn (or nil) + loop_top: i32, + loop_end_buf: *str, // stack of end labels for break + loop_cont_buf: *str, // stack of cont labels for continue +}; + +fn cgen_init(c: *cgen, a: *arena) void = { + c.a = a; + c.locals = nil; + c.frame = 0; + c.last_was_return = 0; + c.labelseq = 0; + // Note: strlit_seq, strlits, ffis are *not* reset here; they + // persist across cgfn calls within one file. cg_file resets them + // at the start of each compilation unit. + c.loop_top = 0; + c.loop_end_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; + c.loop_cont_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; +}; + +// local_alloc — append a slot for `name` without dedup. Used for +// match-arm bindings, which C cgen allocates via cgexpr's by-value +// `locals` list — so two separate matches each get fresh slots even +// when their bind names collide. scan_locals follows the same rule +// for N_MCASE. +fn local_alloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = off; + l.tnode = tnode; + l.lnext = c.locals; + c.locals = l; + return off; +}; + +fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { + // Name-based slot reuse for N_LETs and params: if `name` is + // already declared in this function, return its existing + // offset. Mirrors C cgen (cmd/6c/cgen.c:localoff). Two + // disjoint scopes that declare the same name share one slot — + // so `escape` in wwdump (three `let cp: pos;` across separate + // branches) reserves one slot, not three. scan_locals does + // the matching dedup at prologue time so the SUBQ stays in + // sync. + // + // On a dedup hit we also overwrite the stored tnode to match + // the new declaration's type. C reads `n->lhs->type` (filled + // by the checker) at every N_DOT/N_CAST site; we read + // `lc.tnode`, so it must follow source order. Without this, + // a later `let m: *node` inside a branch keeps an earlier + // `let m: i32`'s tnode and `m.next` falls into the SB fallback. + let cur: *local = c.locals; + for (cur != nil) { + let cn: str = cur.name; + if (streq(cn, name)) { + cur.tnode = tnode; + return cur.off; + }; + cur = cur.lnext; + }; + return local_alloc(c, name, sz, tnode); +}; + +// scan_seen_mark — called by scan_locals on every let / match-bind +// site. Returns true if `name` is already tracked in c.locals (so +// the slot will be shared at emission time — no new frame bump). +// Otherwise appends a name-only stub and returns false. Stubs are +// thrown away when cgfn resets c.locals before emission. +fn scan_seen_mark(c: *cgen, name: str) bool = { + if (local_find_node(c, name) != nil) { return true; }; + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = 0; + l.tnode = nil; + l.lnext = c.locals; + c.locals = l; + return false; +}; + +fn local_find_node(c: *cgen, name: str) *local = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (streq(ln, name)) { return l; }; + l = l.lnext; + }; + return nil; +}; + +fn local_find(c: *cgen, name: str) i32 = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (ln.len == name.len) { + let i: i32 = 0; + let eq: bool = true; + for (i < name.len) { + if (ln[i] != name[i]) { eq = false; i = name.len; } + else { i += 1; }; + }; + if (eq) { return l.off; }; + }; + l = l.lnext; + }; + return 0; +}; + +// ---- emit helpers --------------------------------------------------- + +fn emit_line(s: str) void = { os.write(1, s.ptr, s.len: u64); }; + +fn emit_int(v: i64) void = { + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +fn emit_uint(v: u64) void = { + let buf: [32]u8; + let n: i32 = strconv.u64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +// emit_disp_reg — print "disp(reg)" or "(reg)" when disp == 0, the +// way Plan 9 6c/6a do. +fn emit_disp_reg(off: i64, reg: str) void = { + if (off != 0i64) { emit_int(off); }; + emit_line("("); + emit_line(reg); + emit_line(")"); +}; + +// emit_off — print an integer offset, suppressing it entirely when 0. +// Use before any emit_line("(BP)...") or emit_line("(SB)...") sequence. +// Plan 9 cc convention: "(BP)" not "0(BP)". +fn emit_off(v: i64) void = { + if (v != 0i64) { emit_int(v); }; +}; + +// mklabel — fresh label "__". Returns an +// arena-owned str. Mirrors C cgen's mklabel so diffs match. +fn mklabel(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + let fname: str = c.fn_name; + let j: i32 = 0; + for (j < fname.len) { + buf[i] = fname[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + j = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64toa(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + +fn emit_label(s: str) void = { + os.write(1, s.ptr, s.len: u64); + emit_line(":\n"); +}; + +// ---- string interning ------------------------------------------------ +// +// streq is provided by sym.ww and reused here. + +// intern_strlit — return a stable label for `bytes`. Dedups by content +// so identical literals share storage. +fn intern_strlit(c: *cgen, bytes: str) str = { + let s: *strlit = c.strlits; + for (s != nil) { + let bs: str = s.bytes; + if (streq(bs, bytes)) { + return s.label; + }; + s = s.slnext; + }; + // New label "_S_". + let buf: [32]u8; + buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" + let n: i32 = strconv.i64toa(buf[3:32], c.strlit_seq: i64); + c.strlit_seq += 1; + let total: i32 = 3 + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let i: i32 = 0; + for (i < total) { p[i] = buf[i]; i += 1; }; + p[total] = 0u8; + let lab: str; + lab.ptr = p; + lab.len = total; + let nw: *strlit = amalloc(c.a, 48u64): *strlit; + nw.label = lab; + nw.bytes = bytes; + nw.slnext = c.strlits; + c.strlits = nw; + return lab; +}; + +// emit_def_constants — DATA directive per top-level int-literal `def`. +// 8 bytes little-endian to match what the C cgen emits. +fn emit_def_constants(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let r: *node = d.rhs; + let v: u64 = 0u64; + let ok: bool = false; + if (r != nil) { + if (r.kind == N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == N_NIL) { v = 0u64; ok = true; }; + }; + if (ok) { + emit_line("DATA "); + let nm: str = d.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + // C emit_defs only special-cases " and \; + // every other non-printable goes as \xHH. + if (b == 34u8) { emit_line("\\\""); } + else { if (b == 92u8) { emit_line("\\\\"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emit_line("\"\n"); + }; + }; + d = d.next; + }; +}; + +// emit_data_section — DATA directives for every interned strlit. +// Trailing NUL appended so .ptr can be used as a C string by syscalls. +fn emit_data_section(c: *cgen) void = { + let s: *strlit = c.strlits; + for (s != nil) { + emit_line("DATA "); + let lab: str = s.label; + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB),\""); + let bs: str = s.bytes; + let i: i32 = 0; + for (i < bs.len) { + let b: u8 = bs[i]; + if (b == 34u8) { emit_line("\\\""); } // " + else { if (b == 92u8) { emit_line("\\\\"); } // \ + else { if (b == 10u8) { emit_line("\\n"); } + else { if (b == 9u8) { emit_line("\\t"); } + else { if (b == 13u8) { emit_line("\\r"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };};};};}; + i += 1; + }; + emit_line("\\x00\"\n"); + s = s.slnext; + }; +}; + +// ---- fn return-type map --------------------------------------------- +// +// Per-file: ident → ret-type-node. Used to decide whether to shuffle +// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so +// the value flows through cgen as the canonical (AX, BX) str pair. + +type fnret = struct { + fname: str, + rtype: *node, + frnext: *fnret, +}; + +fn collect_fnrets(c: *cgen, file: *node) void = { + c.fnrets = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let f: *fnret = amalloc(c.a, 32u64): *fnret; + f.fname = d.str; + f.rtype = d.lhs; + f.frnext = c.fnrets; + c.fnrets = f; + }; + d = d.next; + }; +}; + +fn fnret_lookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + let fn_: str = f.fname; + if (streq(fn_, name)) { return f.rtype; }; + f = f.frnext; + }; + return nil; +}; + +// ---- def-constant registry ------------------------------------------ +// +// `def NAME: T = LIT;` becomes a DATA symbol the C-side 6c emits; an +// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at +// file load and consult on N_IDENT lookup. + +type def_ent = struct { + dname: str, + dnext: *def_ent, +}; + +fn collect_defs(c: *cgen, file: *node) void = { + c.defs = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let e: *def_ent = amalloc(c.a, 32u64): *def_ent; + e.dname = d.str; + e.dnext = c.defs; + c.defs = e; + }; + d = d.next; + }; +}; + +fn def_lookup(c: *cgen, name: str) bool = { + let e: *def_ent = c.defs; + for (e != nil) { + let dn: str = e.dname; + if (streq(dn, name)) { return true; }; + e = e.dnext; + }; + return false; +}; + +// ---- FFI map --------------------------------------------------------- + +fn ffi_collect(c: *cgen, file: *node) void = { + c.ffis = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let a: *node = d.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let aname: str = a.str; + if (streq(aname, "symbol")) { + let sym_node: *node = a.list; + if (sym_node != nil) { + if (sym_node.kind == N_STRLIT) { + let f: *ffi = amalloc(c.a, 48u64): *ffi; + f.ident = d.str; + f.symbol = sym_node.str; + f.fnext = c.ffis; + c.ffis = f; + }; + }; + }; + }; + a = a.next; + }; + }; + d = d.next; + }; +}; + +fn ffi_resolve(c: *cgen, ident: str) str = { + let f: *ffi = c.ffis; + for (f != nil) { + let id: str = f.ident; + if (streq(id, ident)) { return f.symbol; }; + f = f.fnext; + }; + return ident; +}; + +// ---- ABI argreg helpers --------------------------------------------- + +fn argreg_name(i: i32) str = { + if (i == 0) { return "DI"; }; + if (i == 1) { return "SI"; }; + if (i == 2) { return "DX"; }; + if (i == 3) { return "CX"; }; + if (i == 4) { return "R8"; }; + if (i == 5) { return "R9"; }; + return "?"; +}; + +// ---- expression cgen ------------------------------------------------- + +// push_args_rev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn push_args_rev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = push_args_rev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + }; + }; + // base address → push + if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emit_line("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let len_n: *node = tn.rhs; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { + emit_line("\tMOVQ\t$"); + emit_uint(len_n.uval); + emit_line(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + }; + };};}; + }; + } else { + emit_line("\tMOVQ\t$0, AX\n"); + };}; + emit_line("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emit_line("\tMOVQ\t$0, AX\n"); }; + emit_line("\tPOPQ\tBX\n"); // hi + emit_line("\tPOPQ\tCX\n"); // base + emit_line("\tMOVQ\tBX, DX\n"); // DX = hi + emit_line("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emit_line("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emit_line("\tPUSHQ\tDX\n"); // cap + emit_line("\tPUSHQ\tDX\n"); // len + emit_line("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (is_slice_type(c, lc.tnode) || is_tagged_type(lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (node_isslice(c, arg)) { + emit_line("\tPUSHQ\tCX\n"); + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (node_isstr(c, arg)) { + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emit_line("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn node_isslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return is_slice_type(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// node_isstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn node_isstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnret_lookup(c, cnm); + return is_str_type(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + }; + }; + // Chained dot (`p.foo.bar`): use dot_inner_struct_ptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, base); + if (inner_t != nil) { + if (inner_t.kind == N_TNAME) { sname = inner_t.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return is_str_type(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return is_str_type(c, n.rhs); + }; + return false; +}; + +// type_name_isunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn type_name_isunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// type_node_isunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn type_node_isunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return type_name_isunsigned(t.str); }; + return false; +}; + +// type_is_8byte_primitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn type_is_8byte_primitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (struct_lookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (prim_size(nm) > 0) { return true; }; + return false; + }; + return false; +}; + +// type_name_issigned — true for i8/i16/i32/i64/int/rune. +fn type_name_issigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// field_load_op — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn field_load_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (type_name_issigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// field_store_op — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn field_store_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// index_base_esz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn index_base_esz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = local_find_node(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let inner_t: *node = tn; + if (tn.kind == N_TPTR) { inner_t = tn.lhs; }; + if (inner_t == nil) { return 8; }; + if (inner_t.kind == N_TNAME) { + if (streq(inner_t.str, "str")) { return 1; }; + }; + if (inner_t.kind == N_TSLICE) { return elem_size_of(inner_t); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *struct_info = struct_lookup(c, sname); + if (si == nil) { return 8; }; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = prim_size(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elem_size_of(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dot_inner_struct_ptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dot_inner_struct_ptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let base_t: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { base_t = tn; }; + if (tn.kind == N_TPTR) { base_t = tn.lhs; }; + } else { if (base.kind == N_DOT) { + base_t = dot_inner_struct_ptr(c, base); + };}; + if (base_t == nil) { return nil; }; + if (base_t.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *struct_info = struct_lookup(c, base_t.str); + if (si == nil) { return nil; }; + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elem_size_of — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elem_size_of(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). prim_size returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// node_isunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn node_isunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return type_node_isunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return type_node_isunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return type_node_isunsigned(n.rhs); }; + if (k == N_BIN) { + if (node_isunsigned(c, n.lhs)) { return true; }; + return node_isunsigned(c, n.rhs); + }; + if (k == N_UN) { return node_isunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C 6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return type_node_isunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; + return false; +}; + +fn cgexpr(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_INTLIT) { + // Print signed (i64), not unsigned (u64). C cgen uses + // `$%lld` so 64-bit constants with bit 63 set show up as + // negative — e.g. FNV-1a's offset basis prints as + // $-3750763034362895579, not $14695981039346656037. + emit_line("\tMOVQ\t$"); + emit_int(n.uval: i64); + emit_line(", AX\n"); + return; + }; + if (k == N_RUNELIT) { + emit_line("\tMOVQ\t$"); + emit_int(n.uval: i64); + emit_line(", AX\n"); + return; + }; + if (k == N_STRLIT) { + // Result is the (ptr, len) pair: ptr in AX, len in BX. Call + // sites that expect a str arg pick these up directly. + let nstr: str = n.str; + let lab: str = intern_strlit(c, nstr); + emit_line("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB), AX\n"); + emit_line("\tMOVQ\t$"); + emit_int(nstr.len: i64); + emit_line(", BX\n"); + return; + }; + if (k == N_TRUE) { + emit_line("\tMOVQ\t$1, AX\n"); + return; + }; + if (k == N_FALSE) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + if (k == N_NIL) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + // str local: also load the len half into BX. + if (is_str_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + }; + // slice local: load (ptr, len, cap) into (AX, BX, CX). + if (is_slice_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), CX\n"); + }; + return; + }; + // Top-level `def` constant — load from its DATA symbol. + if (def_lookup(c, nm)) { + emit_line("\tMOVQ\t"); + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB), AX\n"); + return; + }; + // Fn-name used as a value (e.g. `let f = some_fn;` or + // `... = some_fn;`). LEAQ the symbol address into AX — + // resolved through ffi_resolve so a body-less FFI binding + // emits the C symbol it was declared with via @symbol(), + // not the ww-side ident. + let rt: *node = fnret_lookup(c, nm); + if (rt != nil) { + let resolved: str = ffi_resolve(c, nm); + emit_line("\tLEAQ\t"); + os.write(1, resolved.ptr, resolved.len: u64); + emit_line("(SB), AX\n"); + return; + }; + return; + }; + + if (k == N_INDEX) { + // Element-size-aware load: u8-element bases use MOVZBQ, + // everything else MOVQ. Fast path when the base is a bare + // ident (mem.ww shape). + let base: *node = n.lhs; + let idx: *node = n.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { esz = elem_size_of(base_local.tnode); }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, idx); + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + emit_line("\tADDQ\tAX, BX\n"); + // str element (16B): load (ptr, len) into (AX, BX) so + // the value flows through the str-rhs convention. + if (esz == 16) { + emit_line("\tMOVQ\t8(BX), CX\n"); + emit_line("\tMOVQ\t(BX), AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(BX), AX\n"); } + else { emit_line("\tMOVQ\t(BX), AX\n"); }; + return; + }; + // Generic fallback when base isn't a plain ident. + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emit_line("\tPOPQ\tBX\n"); + emit_line("\tADDQ\tBX, AX\n"); + if (esz == 16) { + emit_line("\tMOVQ\t8(AX), BX\n"); + emit_line("\tMOVQ\t(AX), AX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(AX), AX\n"); } + else { emit_line("\tMOVQ\t(AX), AX\n"); }; + return; + }; + + if (k == N_MATCH) { + // match (e) { case let v: T => stmt; ... } + // + // Read the tagged-union slot and dispatch by tag. Slot + // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings + // (`case let v: T =>`) get a fresh local slot loaded from + // slot+8 (and slot+16 for str-typed payload). + let scrut: *node = n.lhs; + let scrut_off: i32 = 0; + let scrut_t: *node = nil; + if (scrut != nil) { + if (scrut.kind == N_IDENT) { + let lc: *local = local_find_node(c, scrut.str); + if (lc != nil) { + scrut_off = lc.off; + scrut_t = resolve_type(c, lc.tnode); + }; + }; + }; + let endl: str = mklabel(c, "match_end"); + let cs: *node = n.list; + for (cs != nil) { + let nxt: str = mklabel(c, "match_next"); + let pat: *node = cs.lhs; + // Compute the variant tag for this arm. Default arm + // (no pattern) skips the tag check. + if (pat != nil) { + let want: i32 = 0; + if (scrut_t != nil) { + if (scrut_t.kind == N_TTAGGED) { + let pat_name: str; + pat_name.ptr = nil; pat_name.len = 0; + if (pat.kind == N_TNAME) { pat_name = pat.str; }; + let v: *node = scrut_t.list; + let idx: i32 = 0; + let found: bool = false; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, pat_name)) { + want = idx; + found = true; + v = nil; + }; + }; + if (v != nil) { + v = v.next; + idx += 1; + }; + }; + if (!found) { want = 0; }; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(scrut_off: i64); + emit_line("(BP), AX\n"); + emit_line("\tCMPQ\t$"); + emit_int(want: i64); + emit_line(", AX\n"); + emit_line("\tJNE\t"); + emit_line(nxt); + emit_line("\n"); + }; + // Bind `let v: T` from the slot, if requested. + let bn: str = cs.str; + if (bn.len > 0) { + if (pat != nil) { + let bsz: i32 = 8; + if (is_str_type(c, pat)) { bsz = 16; }; + // local_alloc (not local_add): match-arm + // binds don't dedup with same-named binds + // in *other* matches, since C's cgexpr + // allocates a fresh slot per match expr. + let voff: i32 = local_alloc(c, bn, bsz, pat); + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off(voff: i64); + emit_line("(BP)\n"); + if (bsz == 16) { + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off((voff + 8): i64); + emit_line("(BP)\n"); + }; + }; + }; + // Body. Match arms are statements; we cgstmt them. + if (cs.body != nil) { cgstmt(c, cs.body); }; + emit_line("\tJMP\t"); + emit_line(endl); + emit_line("\n"); + emit_label(nxt); + cs = cs.next; + }; + emit_label(endl); + return; + }; + + if (k == N_CAST) { + // Type casts are mostly no-ops at the asm level for our + // integer-shaped operands. Evaluate the source; AX holds + // the bits unchanged. (Sign- or zero-extending narrow loads + // to wider types is the loader's job, not cast's, in this + // minimal cgen.) + cgexpr(c, n.lhs); + return; + }; + + if (k == N_DOT) { + let lhs: *node = n.lhs; + let fld: str = n.str; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then field load. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { + sname = inner.str; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field via *struct: load len into a + // scratch first (so loading ptr into AX + // last leaves (AX=ptr, BX=len)). + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "BX"); + emit_line(", CX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: field load at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field: load both halves so chained + // `.ptr` / `.len` see (AX=ptr, BX=len). + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff + 8): i64); + emit_line("(BP), BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + // Array pseudo-fields: `.ptr` is the array's + // address (LEAQ); `.len` is the static element + // count (immediate). + if (lkind == N_TARRAY) { + if (streq(fld, "ptr")) { + emit_line("\tLEAQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), AX\n"); + return; + }; + if (streq(fld, "len")) { + let len_n: *node = tn.rhs; + let alen: i64 = 0i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { alen = len_n.uval: i64; }; + }; + emit_line("\tMOVQ\t$"); + emit_int(alen); + emit_line(", AX\n"); + return; + }; + }; + // str/slice pseudo-fields .ptr/.len/.cap on a + // direct local: load at slot+delta. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + // Pointer to str/slice (`*[]u8`, `*str`): + // deref, then load at delta within the + // pointed-to header. C cgen does the same. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", AX\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off((lc.off + delta): i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + }; + // Module-qualified value reference: `mod.name` where `mod` + // is N_IDENT bound as SK_USE and the leaf isn't a local. + // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback + // the C cgen takes when bt is NULL/ty_err. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + emit_line("\tMOVQ\t"); + emit_line(fld); + emit_line("(SB), AX\n"); + return; + }; + }; + // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. + // Evaluate the str-producing expression — that leaves + // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` + // shuffles BX→AX. Mirrors what C cgen does (it just evaluates + // the literal and picks the half it wants). + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emit_line("\tMOVQ\tBX, AX\n"); + return; + }; + // Chained struct-field-via-ptr-via-ptr access: + // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 + // Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct + // field). Outer DOT dereferences and reads `val`. Without this + // path the cgen falls through and AX retains whatever the + // inner expression left there — typically the *struct pointer + // itself, so reads silently get the pointer value instead of + // the field. (Showed up porting 6l/pass.ww.) + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, lhs); + if (inner_t != nil) { + let sname: str = inner_t.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + cgexpr(c, lhs); // AX = ptr to inner struct + let lop: str = field_load_op(fi); + // str field: load both halves. + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "AX"); + emit_line(", BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + return; + }; + + if (k == N_UN) { + // Match C cgen ordering: evaluate operand first (load into AX), + // then apply the unary op. AMP / STAR override AX with the + // address / deref. The wasted load before AMP keeps our asm + // byte-identical to the C version. + cgexpr(c, n.lhs); + if (n.op == TK_MINUS) { emit_line("\tNEGQ\tAX\n"); return; }; + if (n.op == TK_TILDE) { emit_line("\tNOTQ\tAX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tMOVQ\t(AX), AX\n"); return; }; + if (n.op == TK_AMP) { + let opnd: *node = n.lhs; + if (opnd != nil) { + if (opnd.kind == N_IDENT) { + let nm: str = opnd.str; + let off: i32 = local_find(c, nm); + if (off != 0) { + emit_line("\tLEAQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + return; + }; + if (n.op == TK_NOT) { + let t: str = mklabel(c, "tt"); + let e: str = mklabel(c, "te"); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_BIN) { + let unsignd: bool = node_isunsigned(c, n.lhs); + if (!unsignd) { unsignd = node_isunsigned(c, n.rhs); }; + + cgexpr(c, n.rhs); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, n.lhs); + emit_line("\tPOPQ\tBX\n"); + if (n.op == TK_PLUS) { emit_line("\tADDQ\tBX, AX\n"); return; }; + if (n.op == TK_MINUS) { emit_line("\tSUBQ\tBX, AX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tIMULQ\tBX, AX\n"); return; }; + if (n.op == TK_SLASH) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + return; + }; + if (n.op == TK_PERCENT) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + emit_line("\tMOVQ\tDX, AX\n"); + return; + }; + if (n.op == TK_AMP) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_PIPE) { emit_line("\tORQ\tBX, AX\n"); return; }; + if (n.op == TK_CARET) { emit_line("\tXORQ\tBX, AX\n"); return; }; + if (n.op == TK_LSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHLQ\tCX, AX\n"); + return; + }; + if (n.op == TK_RSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHRQ\tCX, AX\n"); + return; + }; + if (n.op == TK_AND) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_OR) { emit_line("\tORQ\tBX, AX\n"); return; }; + + // Comparison: emit CMPQ, jump on signed/unsigned variant, + // materialise 0/1 in AX. Same shape as the C cgen. + let is_cmp: bool = false; + let jcc: str = ""; + if (n.op == TK_EQ) { is_cmp = true; jcc = "JE"; }; + if (n.op == TK_NEQ) { is_cmp = true; jcc = "JNE"; }; + if (n.op == TK_LT) { is_cmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; + if (n.op == TK_LE) { is_cmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; + if (n.op == TK_GT) { is_cmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; + if (n.op == TK_GE) { is_cmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; }; + if (is_cmp) { + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emit_line("\tCMPQ\tBX, AX\n"); + emit_line("\t"); emit_line(jcc); emit_line("\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_CALL) { + let nargs: i32 = push_args_rev(c, n.list); + let i: i32 = 0; + for (i < nargs) { + emit_line("\tPOPQ\t"); + emit_line(argreg_name(i)); + emit_line("\n"); + i += 1; + }; + let callee: *node = n.lhs; + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + // Detect fn-pointer field call: `w.emit(args)` where `w` is + // a struct local and `emit` is an N_TFN field. Load the + // field value into AX and CALL through it. Also detect a + // bare `fp(args)` where `fp` is a local holding a function + // pointer — mirror C cgen's localfind dispatch (commit + // 635818e). Without this the call emits `CALL fp(SB)` and + // the linker rightly fails. + let is_fnptr_call: bool = false; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cn: str = callee.str; + if (local_find_node(c, cn) != nil) { + is_fnptr_call = true; + }; + }; + if (callee.kind == N_DOT) { + let base: *node = callee.lhs; + let fld: str = callee.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + if (ft.kind == N_TFN) { + is_fnptr_call = true; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + if (is_fnptr_call) { + // Load fn-ptr field value into AX; CALL AX. We emit the + // load AFTER the args have been popped (so AX/BX/etc + // don't get clobbered by the field load before the pops). + // `popped args` left DI/SI/etc set; AX is free. + cgexpr(c, callee); + emit_line("\tCALL\tAX\n"); + } else { + emit_line("\tCALL\t"); + if (callee != nil) { + if (callee.kind == N_IDENT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + } else { if (callee.kind == N_DOT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + };}; + }; + emit_line("(SB)\n"); + }; + // SysV returns 16-byte aggregates in (AX, DX). Our str + // convention is (AX, BX), so shuffle for str-returning calls. + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_str_type(c, rt)) { + emit_line("\tMOVQ\tDX, BX\n"); + }; + }; + return; + }; + + if (k == N_ASSIGN) { + let lhs: *node = n.lhs; + // `*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. + // We default to MOVQ (8B) since most fixtures use it; for + // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + if (lhs != nil) { + if (lhs.kind == N_UN) { + if (lhs.op == TK_STAR) { + if (n.op == TK_ASSIGN) { + let inner: *node = lhs.lhs; + let elem_str: bool = false; + let store_op: str = "MOVQ"; + if (inner != nil) { + if (inner.kind == N_IDENT) { + let lc: *local = local_find_node(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { + if (pe.kind == N_TNAME) { + if (streq(pe.str, "str")) { elem_str = true; } + else { + let ps: i32 = prim_size(pe.str); + if (ps == 1) { store_op = "MOVB"; } + else { if (ps == 4) { store_op = "MOVL"; }; }; + }; + }; + }; + }; + }; + }; + }; + }; + cgexpr(c, n.rhs); + // Push order matches C cgen + // (cmd/6c/cgen.c:1033-1041): PUSHQ AX + // (ptr) first, then PUSHQ BX (len) if + // str, so the pop sequence is POP CX + // (len) → POP AX (ptr) → MOVQ AX, + // (BX) → MOVQ CX, 8(BX). + emit_line("\tPUSHQ\tAX\n"); + if (elem_str) { emit_line("\tPUSHQ\tBX\n"); }; + cgexpr(c, inner); + emit_line("\tMOVQ\tAX, BX\n"); + if (elem_str) { + emit_line("\tPOPQ\tCX\n"); + emit_line("\tPOPQ\tAX\n"); + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + emit_line("\tPOPQ\tAX\n"); + emit_line("\t"); + emit_line(store_op); + emit_line("\tAX, (BX)\n"); + return; + }; + }; + }; + }; + // Array/slice/ptr index store: `arr[i] = v;`. Element size + // from base.tnode picks MOVB vs MOVQ. + if (lhs != nil) { + if (lhs.kind == N_INDEX) { + if (n.op == TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { + esz = elem_size_of(base_local.tnode); + }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, n.rhs); // value → AX + if (esz == 16) { emit_line("\tPUSHQ\tBX\n"); }; + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, idx); // idx → AX + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + emit_line("\tPUSHQ\tAX\n"); // scaled idx + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emit_line("\tMOVQ\tAX, BX\n"); + }; + emit_line("\tPOPQ\tAX\n"); // scaled idx + emit_line("\tADDQ\tAX, BX\n"); + emit_line("\tPOPQ\tAX\n"); // value + if (esz == 16) { + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tPOPQ\tCX\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVB\tAX, (BX)\n"); } + else { emit_line("\tMOVQ\tAX, (BX)\n"); }; + return; + }; + }; + }; + // Struct/ptr-to-struct field assignment: `s.f = expr;` or + // `p.f = expr;`. Only plain `=` is wired (compound on field + // is rare and not yet needed by our fixtures). + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then store. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + if (n.op != TK_ASSIGN) { + // compound: load current value + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let lop: str = field_load_op(fi); + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + }; + cgexpr(c, n.rhs); + if (n.op != TK_ASSIGN) { + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs (BX is old lhs, + // AX is rhs). + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + }; + // str field via *struct: rhs left + // (AX=ptr, BX=len). Use CX as the + // address scratch so we don't clobber + // the len half before storing it. + if (n.op == TK_ASSIGN) { + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), CX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(fi.foff: i64, "CX"); + emit_line("\n"); + emit_line("\tMOVQ\tBX, "); + emit_disp_reg((fi.foff + 8): i64, "CX"); + emit_line("\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: store at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + cgexpr(c, n.rhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP)\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + // str/slice pseudo-field assignment. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + if (n.op != TK_ASSIGN) { + // Compound on `(*str|*slice).field`: load + // current → push → eval rhs → combine → store. + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs. + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\tAX, "); + emit_off((lc.off + delta): i64); + emit_line("(BP)\n"); + return; + }; + }; + }; + }; + }; + }; + // Local-ident target — plain `=` and the simple compound + // forms (+= -= *= /=); other compounds fall back to + // "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let off: i32 = local_find(c, nm); + if (off == 0) { return; }; + // Detect str-typed local — assignment must store both + // halves (AX=ptr at +0, BX=len at +8). + let lc_str: bool = false; + let lcn: *local = local_find_node(c, nm); + if (lcn != nil) { lc_str = is_str_type(c, lcn.tnode); }; + cgexpr(c, n.rhs); + if (n.op == TK_ASSIGN) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + if (lc_str) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + return; + }; + if (n.op == TK_PLUSEQ) { + emit_line("\tADDQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + // Generic compound: load → combine in BX → store. + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), BX\n"); + if (n.op == TK_STAREQ) { emit_line("\tIMULQ\tAX, BX\n"); }; + if (n.op == TK_AMPEQ) { emit_line("\tANDQ\tAX, BX\n"); }; + if (n.op == TK_PIPEEQ) { emit_line("\tORQ\tAX, BX\n"); }; + if (n.op == TK_CARETEQ) { emit_line("\tXORQ\tAX, BX\n"); }; + if (n.op == TK_LSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHLQ\tCX, BX\n"); + }; + if (n.op == TK_RSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHRQ\tCX, BX\n"); + }; + emit_line("\tMOVQ\tBX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + }; + return; + }; +}; + +// ---- type-driven slot sizing ---------------------------------------- + +fn struct_lookup(c: *cgen, name: str) *struct_info = { + let s: *struct_info = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// prim_size — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn prim_size(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slot_size(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let len_n: *node = typ_n.rhs; + let elem_n: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = 8; + if (elem_n != nil) { + if (elem_n.kind == N_TNAME) { + let en: str = elem_n.str; + let ps: i32 = prim_size(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slot_size(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// register_struct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slot_size logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn field_size(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slot_size's TARRAY branch. + let len_n: *node = tnode.rhs; + let elem_n: *node = tnode.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = field_size(c, elem_n); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn register_struct(c: *cgen, name: str, tstruct: *node) void = { + let si: *struct_info = amalloc(c.a, 64u64): *struct_info; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *field_info = nil; + let tail: *field_info = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = field_size(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *field_info = amalloc(c.a, 48u64): *field_info; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collect_structs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + register_struct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// ---- frame pre-scan -------------------------------------------------- +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slot_size(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scan_locals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match local_add's rounding: < 8 bumps to 8, then 8-align. + // scan_locals must agree with local_add or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. Same-name re-declarations share the first + // slot (see scan_seen_mark / local_add). + if (!scan_seen_mark(c, n.str)) { + let sz: i32 = slot_size(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + }; + // 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, + // so two separate matches in the same function each allocate + // their `v`/`e` slots fresh. Treating these as deduped would + // shrink the frame below what local_add then bumps it to. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (is_str_type(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scan_locals(c, n.lhs); }; + if (n.rhs != nil) { total += scan_locals(c, n.rhs); }; + if (n.cond != nil) { total += scan_locals(c, n.cond); }; + if (n.body != nil) { total += scan_locals(c, n.body); }; + if (n.els != nil) { total += scan_locals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scan_locals(c, m); + m = m.next; + }; + }; + return total; +}; + +// ---- statement cgen -------------------------------------------------- + +fn cgstmt(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_BLOCK) { + let s: *node = n.list; + for (s != nil) { + cgstmt(c, s); + s = s.next; + }; + return; + }; + + if (k == N_RETURN) { + let rhs: *node = n.lhs; + if (rhs != nil) { + // Tuple return `return a, b;` — pack as (AX=v0, DX=v1). + // Matches C cgen: evaluate v1 first (PUSHQ), then v0 + // into AX, then POPQ DX. End state: AX = v0, DX = v1. + if (rhs.kind == N_TUPLE) { + let v: *node = rhs.list; + if (v != nil) { + let v2: *node = v.next; + if (v2 != nil) { + cgexpr(c, v2); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, v); + emit_line("\tPOPQ\tDX\n"); + } else { + cgexpr(c, v); + }; + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + // Tagged-union return: pack as (AX=tag, DX=value0, CX=value1). + // For str variant, cgexpr leaves (AX=ptr, BX=len), so we + // shuffle DX←AX (ptr) and CX←BX (len), then load tag. + // For other variants, cgexpr leaves AX, shuffle DX←AX. + if (is_tagged_type(c.fn_ret)) { + cgexpr(c, rhs); + let idx: i32 = tagged_variant_index(c, c.fn_ret, rhs); + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tMOVQ\tAX, DX\n"); + } else { + emit_line("\tMOVQ\tAX, DX\n"); + }; + emit_line("\tMOVQ\t$"); + if (idx < 0) { idx = 0; }; + emit_int(idx: i64); + emit_line(", AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + cgexpr(c, rhs); + } else { + // Bare `return;` in a void fn — zero AX so the caller + // sees a deterministic value (matches C cgen, which + // always falls through to `cgexpr_int(c, 0)`). + emit_line("\tMOVQ\t$0, AX\n"); + }; + // SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX). + // cgexpr leaves str in (AX, BX); shuffle BX→DX. + if (is_str_type(c, c.fn_ret)) { + emit_line("\tMOVQ\tBX, DX\n"); + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + + if (k == N_EXPRSTMT) { + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + c.last_was_return = 0; + return; + }; + + if (k == N_LET) { + let nm: str = n.str; + let sz: i32 = slot_size(c, n.lhs); + let off: i32 = local_add(c, nm, sz, n.lhs); + if (n.rhs != nil) { + let rhs: *node = n.rhs; + // Tagged-union init: `let r: (T | E) = expr;`. + // - If rhs is a CALL to a fn returning tagged-union, + // the result is already in (AX=tag, DX=v0, CX=v1); + // just spill all three. + // - Otherwise rhs is a bare variant value: pack tag + + // value(s). + if (is_tagged_type(n.lhs)) { + let rhs_returns_tagged: bool = false; + if (rhs.kind == N_CALL) { + let callee: *node = rhs.lhs; + if (callee != nil) { + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + if (callee.kind == N_IDENT) { callee_name = callee.str; }; + if (callee.kind == N_DOT) { callee_name = callee.str; }; + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_tagged_type(rt)) { rhs_returns_tagged = true; }; + }; + }; + }; + cgexpr(c, rhs); + if (rhs_returns_tagged) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tDX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + c.last_was_return = 0; + return; + }; + let tag_idx: i32 = tagged_variant_index(c, n.lhs, rhs); + if (tag_idx < 0) { tag_idx = 0; }; + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tBX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + } else { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + emit_line("\tMOVQ\t$"); + emit_int(tag_idx: i64); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + c.last_was_return = 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. + if (rhs.kind == N_STRUCTLIT) { + let trefn: *node = rhs.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == N_TNAME) { sname = trefn.str; }; }; + }; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let field_node: *node = rhs.list; + for (field_node != nil) { + if (field_node.kind == N_FIELD) { + let fname: str = field_node.str; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, field_node.lhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((off + fi.foff): i64); + emit_line("(BP)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + field_node = field_node.next; + }; + c.last_was_return = 0; + return; + }; + }; + cgexpr(c, rhs); + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + // str init: cgexpr also leaves len in BX; store both. + if (sz == 16) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + // slice init: ptr/len/cap in AX/BX/CX. + if (sz == 24) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + }; + } else { + // Bare `let x: T;` with no initializer. C cgen + // (cmd/6c/cgen.c:2181-2183) zero-inits only when + // the underlying type's natural size is 8 — pointers, + // i64/u64, function pointers, ints. Structs/arrays/ + // slices/strings/tagged/tuples are left for per-field + // writes. ww's slot_size pads struct slots up to 8, + // so we can't just check sz == 8: walk the type AST + // directly to make the same call. + if (type_is_8byte_primitive(c, n.lhs)) { + emit_line("\tMOVQ\t$0, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + c.last_was_return = 0; + return; + }; + + if (k == N_IF) { + let els: str = mklabel(c, "else"); + let endl: str = mklabel(c, "end"); + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); + if (n.els != nil) { emit_line(els); } + else { emit_line(endl); }; + emit_line("\n"); + if (n.body != nil) { cgstmt(c, n.body); }; + if (n.els != nil) { + emit_line("\tJMP\t"); emit_line(endl); emit_line("\n"); + emit_label(els); + cgstmt(c, n.els); + }; + emit_label(endl); + c.last_was_return = 0; + return; + }; + + if (k == N_FOR) { + // Match C cgen's label scheme: _loop_N for the top, + // _endloop_N for the post-body merge. No separate cont + // label when there's no post-expression. + let topl: str = mklabel(c, "loop"); + let endl: str = mklabel(c, "endloop"); + + if (n.lhs != nil) { cgstmt(c, n.lhs); }; + + emit_label(topl); + if (n.cond != nil) { + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(endl); emit_line("\n"); + }; + + c.loop_end_buf[c.loop_top] = endl; + c.loop_cont_buf[c.loop_top] = topl; + c.loop_top += 1; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.loop_top -= 1; + + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tJMP\t"); emit_line(topl); emit_line("\n"); + emit_label(endl); + c.last_was_return = 0; + return; + }; + + // Tuple-destructure assign: `a, b = call();`. The call's tuple + // return lands in (AX, DX); push DX to free it, store AX into + // the first lvalue, then pop DX into the second. Mirrors + // cmd/6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same + // as C — no fixture uses >2 today). + if (k == N_MASSIGN) { + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tPUSHQ\tDX\n"); + let l0: *node = n.list; + let l1: *node = nil; + if (l0 != nil) { l1 = l0.next; }; + if (l0 != nil) { + if (l0.kind == N_IDENT) { + let off: i32 = local_find(c, l0.str); + if (off != 0) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + emit_line("\tPOPQ\tDX\n"); + if (l1 != nil) { + if (l1.kind == N_IDENT) { + let off: i32 = local_find(c, l1.str); + if (off != 0) { + emit_line("\tMOVQ\tDX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + c.last_was_return = 0; + return; + }; + + if (k == N_BREAK) { + if (c.loop_top > 0) { + let lbl: str = c.loop_end_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + if (k == N_CONTINUE) { + if (c.loop_top > 0) { + let lbl: str = c.loop_cont_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + + c.last_was_return = 0; +}; + +// ---- function-level cgen --------------------------------------------- + +// is_str_type — true when the type expr resolves (through any +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn is_str_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn is_str_type(c: *cgen, t: *node) bool = { + if (is_str_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_str_type_raw(r); +}; + +fn is_slice_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn is_slice_type(c: *cgen, t: *node) bool = { + if (is_slice_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_slice_type_raw(r); +}; + +fn is_tagged_type(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhs_target_name — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhs_target_name(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = local_find_node(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// tagged_variant_index — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn tagged_variant_index(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let want_name: str = rhs_target_name(c, rhs); + if (want_name.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want_name)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let want_str: bool = node_isstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let v_is_str: bool = false; + if (v.kind == N_TNAME) { + if (is_str_type(c, v)) { v_is_str = true; }; + }; + if (v_is_str == want_str) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgfn_params(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (is_tagged_type(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_slice_type(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_str_type(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = local_add(c, nm, 16, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + } else { + let off: i32 = local_add(c, nm, 8, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgen_init(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emit_line("TEXT "); + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scan_locals. + // Seed c.locals with param-name stubs so scan_locals dedups a + // re-declared `let ` in the body against the param's + // slot (matches C cgen). Stubs get cleared before emission. + let scan_p: *node = fn_.list; + let frame: i32 = 0; + for (scan_p != nil) { + if (scan_p.kind == N_PARAM) { + if (is_tagged_type(scan_p.lhs)) { frame += 24; } + else { if (is_slice_type(c, scan_p.lhs)) { frame += 24; } + else { if (is_str_type(c, scan_p.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + scan_seen_mark(c, scan_p.str); + }; + scan_p = scan_p.next; + }; + if (fn_.body != nil) { frame += scan_locals(c, fn_.body); }; + // Drop the stubs so emission rebuilds c.locals with real offsets. + c.locals = nil; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emit_int(frame: i64); + emit_line("\n"); + + emit_line("\tPUSHQ\tBP\n"); + emit_line("\tMOVQ\tSP, BP\n"); + emit_line("\tSUBQ\t$"); + emit_int(frame: i64); + emit_line(", SP\n"); + + cgfn_params(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cg_file(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collect_aliases(c, file); + collect_structs(c, file); + collect_defs(c, file); + collect_fnrets(c, file); + ffi_collect(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emit_data_section(c); + emit_def_constants(c, file); +}; + +// selfhost/cmd/6c/main.ww — port of cmd/6c/main.c. +// +// 6c = amd64 compiler. Read .ww, parse, codegen, emit Plan 9 amd64 +// asm to stdout (or the file given by -o). +// +// 6c_ww -o file.s file.ww +// +// The cgen routines in selfhost/cmd/wwc/cgen.ww write directly to +// fd 1 via os.write(1, ...). For -o, we open the output file and +// dup2 it onto fd 1 before invoking cg_file. This is the same trick +// the bootstrap uses with shell redirection, just in-process. + +use os; +use mem; +use tok; +use lex; +use ast; +use parse; +use typ; +use sym; +use check; +use cgen; + +fn streq_cs(a: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (a[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (a[i] != 0u8) { return false; }; + return true; +}; + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +fn slurp(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let nz: u64 = n: u64; + let buf: *u8 = os.alloc(nz + 1u64): *u8; + let got: i64 = os.readfull(fd, buf, nz); + os.close(fd); + if (got != n) { return nil, 0u64; }; + buf[nz] = 0u8; + return buf, nz; +}; + +export fn main(argc: i32, argv: **u8) i32 = { + let src_cs: *u8 = nil; + let out_cs: *u8 = nil; + + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_cs(a, "-o")) { + i += 1; + if (i >= argc) { + os.write(2, "6c: -o requires arg\n".ptr, 20u64); + return 2; + }; + out_cs = argv[i]; + } else { if (a[0u64] == 45u8) { + os.write(2, "6c: unknown flag\n".ptr, 17u64); + return 2; + } else { + if (src_cs != nil) { + os.write(2, "6c: only one input\n".ptr, 19u64); + return 2; + }; + src_cs = a; + }; }; + i += 1; + }; + + if (src_cs == nil) { + os.write(2, "usage: 6c_ww [-o out.s] file.ww\n".ptr, 32u64); + return 2; + }; + + let buf: *u8; + let blen: u64; + buf, blen = slurp(src_cs); + if (buf == nil) { + os.write(2, "6c: cannot read input\n".ptr, 22u64); + return 1; + }; + + // Redirect fd 1 to the output file before any cgen emit runs. + // cgen.ww writes directly to fd 1; dup2 lets us reuse it without + // threading a file descriptor through the emit helpers. + if (out_cs != nil) { + let ofd: i32 = os.open(out_cs, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 420i32); // 0o644 + if (ofd < 0) { + os.write(2, "6c: cannot open output\n".ptr, 23u64); + return 1; + }; + if (os.dup2(ofd, 1i32) < 0) { + os.write(2, "6c: dup2 failed\n".ptr, 16u64); + os.close(ofd); + return 1; + }; + os.close(ofd); + }; + + let ar: *arena = newarena(); + let nlen: u64 = cstrlen(src_cs); + let fname: str = astrndup(ar, src_cs, nlen); + + let l: lex; + lexinit(&l, ar, fname, buf, blen); + + let ps: parser; + parserinit(&ps, ar, &l); + let f: *node = parsefile(&ps); + + let cg: cgen; + cgen_init(&cg, ar); + cg_file(&cg, f); + + if (l.errs > 0) { return 1; }; + return 0; +}; + diff --git a/selfhost/cmd/6l/main.combined.ww b/selfhost/cmd/6l/main.combined.ww index 49100009..59134e5d 100644 --- a/selfhost/cmd/6l/main.combined.ww +++ b/selfhost/cmd/6l/main.combined.ww @@ -24,6 +24,7 @@ def SYS_OPEN: i64 = 2; def SYS_CLOSE: i64 = 3; def SYS_LSEEK: i64 = 8; def SYS_ACCESS: i64 = 21; +def SYS_DUP2: i64 = 33; def SYS_GETPID: i64 = 39; def SYS_FORK: i64 = 57; def SYS_EXECVE: i64 = 59; @@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = { return syscall1(SYS_CLOSE, fd: i64): i32; }; +// dup2(2): make `newfd` refer to the same description as `oldfd`, +// closing `newfd` first if open. Returns `newfd` on success or a +// negative errno. Used by 6c_ww to redirect stdout into an output +// file without changing the cgen emit path. +export fn dup2(oldfd: i32, newfd: i32) i32 = { + return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32; +}; + // Fallible wrappers. The error variant is a plain str (Plan 9 errstr // model, see lib/errors); the sum type makes success/failure explicit // without overloading length-zero. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 86775984..92ab289f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2633,9 +2633,31 @@ fn parsestmt(p: *parser) *node = { expect_tok(p, TK_SEMI, "expected ';' after continue"); return newnode(p.a, N_CONTINUE, pf, pl, pc); }; - // expression statement + // expression statement, or tuple-destructure multi-assign: + // a, b = expr; + // Mirrors cmd/wwc/parse.c:1015-1031. We parse the first lvalue + // with parseexpr (matches the C side); subsequent lvalues go + // through parsebin(parseunary, 1) so the `=` stays for us to + // consume — parseexpr would absorb it. + let e: *node = parseexpr(p); + if (p.cur_kind == TK_COMMA) { + let m: *node = newnode(p.a, N_MASSIGN, pf, pl, pc); + let head: *node = e; + let tail: *node = e; + for (p.cur_kind == TK_COMMA) { + advance(p); + let lv: *node = parsebin(p, parseunary(p), 1); + tail.next = lv; + tail = lv; + }; + expect_tok(p, TK_ASSIGN, "expected '=' after multi-assign lvalues"); + m.rhs = parseexpr(p); + m.list = head; + expect_tok(p, TK_SEMI, "expected ';' after multi-assign"); + return m; + }; let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc); - n.lhs = parseexpr(p); + n.lhs = e; expect_tok(p, TK_SEMI, "expected ';' after expression statement"); return n; }; @@ -4346,15 +4368,14 @@ fn node_isstr(c: *cgen, n: *node) bool = { if (streq(fld, "len")) { return false; }; if (streq(fld, "cap")) { return false; }; if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; if (base.kind == N_IDENT) { - let bn: str = base.str; - let lc: *local = local_find_node(c, bn); + let lc: *local = local_find_node(c, base.str); if (lc != nil) { let tn: *node = lc.tnode; let lkind: i32 = -1; if (tn != nil) { lkind = tn.kind; }; - let sname: str; - sname.ptr = nil; sname.len = 0; if (lkind == N_TNAME) { sname = tn.str; }; if (lkind == N_TPTR) { let inner: *node = tn.lhs; @@ -4362,18 +4383,27 @@ fn node_isstr(c: *cgen, n: *node) bool = { if (inner.kind == N_TNAME) { sname = inner.str; }; }; }; - if (sname.len > 0) { - let si: *struct_info = struct_lookup(c, sname); - if (si != nil) { - let fi: *field_info = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (streq(fn_, fld)) { - return is_str_type(c, fi.tnode); - }; - fi = fi.finext; - }; + }; + }; + // Chained dot (`p.foo.bar`): use dot_inner_struct_ptr + // to resolve the inner chain to the *struct it lands + // on, then look up `fld` in that struct. + if (base.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, base); + if (inner_t != nil) { + if (inner_t.kind == N_TNAME) { sname = inner_t.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return is_str_type(c, fi.tnode); }; + fi = fi.finext; }; }; }; @@ -4404,6 +4434,36 @@ fn type_node_isunsigned(t: *node) bool = { return false; }; +// type_is_8byte_primitive — does this type take exactly one 8-byte +// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive +// padded up to 8) rather than a wider aggregate? Used by N_LET +// zero-init to mirror C cgen's "only zero if sz == 8 at the type +// level" rule. Strings (16), slices (24), tagged unions (>=16), +// tuples (16), structs (varies), arrays — all fall through to +// false here even when their *slot* rounds up to 8. +fn type_is_8byte_primitive(c: *cgen, t: *node) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == N_TPTR) { return true; }; + if (k == N_TFN) { return true; }; + if (k == N_TCHAN) { return true; }; + if (k == N_TSLICE) { return false; }; + if (k == N_TARRAY) { return false; }; + if (k == N_TTUPLE) { return false; }; + if (k == N_TTAGGED){ return false; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return false; }; + // Struct alias: not a primitive even if the slot is 8B. + if (struct_lookup(c, nm) != nil) { return false; }; + // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). + // All of these get slot-padded to 8 and zero-init in C. + if (prim_size(nm) > 0) { return true; }; + return false; + }; + return false; +}; + // type_name_issigned — true for i8/i16/i32/i64/int/rune. fn type_name_issigned(nm: str) bool = { if (streq(nm, "i8")) { return true; }; @@ -4506,6 +4566,12 @@ fn index_base_esz(c: *cgen, base: *node) i32 = { return 8; }; if (ft.kind == N_TSLICE) { return elem_size_of(ft); }; + // str-typed field: indexing yields one byte + // (`n.s[i]` where .s is str — matches C cgen's + // MOVZBQ for byte indexing). + if (ft.kind == N_TNAME) { + if (streq(ft.str, "str")) { return 1; }; + }; return 8; }; fi = fi.finext; @@ -4655,6 +4721,32 @@ fn node_isunsigned(c: *cgen, n: *node) bool = { return node_isunsigned(c, n.rhs); }; if (k == N_UN) { return node_isunsigned(c, n.lhs); }; + // N_INDEX: `p[i]` is unsigned iff p's element type is unsigned. + // Walks the base local's declared type and pulls the element + // out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the + // compare-codegen for `p[i] >= 48u8` falls back to signed JGE + // instead of JAE, diverging from C 6c on byte indexing. + if (k == N_INDEX) { + let base: *node = n.lhs; + if (base != nil) { + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let elem: *node = nil; + if (tn.kind == N_TPTR) { elem = tn.lhs; }; + if (tn.kind == N_TARRAY) { elem = tn.lhs; }; + if (tn.kind == N_TSLICE) { elem = tn.lhs; }; + if (elem != nil) { + return type_node_isunsigned(elem); + }; + }; + }; + }; + }; + return false; + }; return false; }; @@ -6165,6 +6257,20 @@ fn cgstmt(c: *cgen, n: *node) void = { emit_off((off + 16): i64); emit_line("(BP)\n"); }; + } else { + // Bare `let x: T;` with no initializer. C cgen + // (cmd/6c/cgen.c:2181-2183) zero-inits only when + // the underlying type's natural size is 8 — pointers, + // i64/u64, function pointers, ints. Structs/arrays/ + // slices/strings/tagged/tuples are left for per-field + // writes. ww's slot_size pads struct slots up to 8, + // so we can't just check sz == 8: walk the type AST + // directly to make the same call. + if (type_is_8byte_primitive(c, n.lhs)) { + emit_line("\tMOVQ\t$0, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; }; c.last_was_return = 0; return; @@ -6221,6 +6327,42 @@ fn cgstmt(c: *cgen, n: *node) void = { return; }; + // Tuple-destructure assign: `a, b = call();`. The call's tuple + // return lands in (AX, DX); push DX to free it, store AX into + // the first lvalue, then pop DX into the second. Mirrors + // cmd/6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same + // as C — no fixture uses >2 today). + if (k == N_MASSIGN) { + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tPUSHQ\tDX\n"); + let l0: *node = n.list; + let l1: *node = nil; + if (l0 != nil) { l1 = l0.next; }; + if (l0 != nil) { + if (l0.kind == N_IDENT) { + let off: i32 = local_find(c, l0.str); + if (off != 0) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + emit_line("\tPOPQ\tDX\n"); + if (l1 != nil) { + if (l1.kind == N_IDENT) { + let off: i32 = local_find(c, l1.str); + if (off != 0) { + emit_line("\tMOVQ\tDX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + }; + }; + }; + c.last_was_return = 0; + return; + }; + if (k == N_BREAK) { if (c.loop_top > 0) { let lbl: str = c.loop_end_buf[c.loop_top - 1]; diff --git a/test/wwc/995_self_rebuild.c b/test/wwc/995_self_rebuild.c new file mode 100644 index 00000000..d3e6983b --- /dev/null +++ b/test/wwc/995_self_rebuild.c @@ -0,0 +1,146 @@ +/* + * 995_self_rebuild — the wwstage rebuilds itself. + * + * Drives ww_ww (the ww-side driver, which shells to 6c_ww/6a_ww/6l_ww) + * over each wwstage tool's source and diffs the result byte-for-byte + * against the cstage-built binary in $BIN. A green run means the + * toolchain can recompile itself without touching cc, modulo the + * cold-start binary — which is the v1.0 lock from PLAN.md. + * + * This is stricter than `make bootstrap`: that loop only proves the + * wwdump cgen self-stabilises; this proves every wwstage tool round- + * trips through the wwstage pipeline. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { + if (fa) fclose(fa); + if (fb) fclose(fb); + return -1; + } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +/* Each tool builds via `ww_ww build -I -I selfhost/cmd/wwc src`. + * Some tools have a local module dir (6a, 6l with sibling .ww files); + * wwc-only tools (6c, ww, wwdump) just need the wwc -I. inc_local is + * "" for those. + */ +static int +rebuild_one(const char *bin, const char *cwd, const char *tool, + const char *src_rel, const char *inc_local) +{ + char workdir[64]; + snprintf(workdir, sizeof workdir, "/tmp/wwsr_%d_%s", getpid(), tool); + char cmd[4096]; + snprintf(cmd, sizeof cmd, "rm -rf %s && mkdir -p %s", workdir, workdir); + if (runwait(cmd) != 0) return -1; + + if (inc_local && inc_local[0]) { + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww_ww build -I %s/%s -I %s/selfhost/cmd/wwc " + "%s/%s >/dev/null 2>&1", + workdir, bin, cwd, inc_local, cwd, cwd, src_rel); + } else { + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww_ww build -I %s/selfhost/cmd/wwc " + "%s/%s >/dev/null 2>&1", + workdir, bin, cwd, cwd, src_rel); + } + if (runwait(cmd) != 0) { + fprintf(stderr, "self-rebuild FAIL: ww_ww build errored on %s\n", tool); + return -1; + } + + char rebuilt[256], canonical[256]; + snprintf(rebuilt, sizeof rebuilt, "%s/main", workdir); + snprintf(canonical, sizeof canonical, "%s/%s_ww", bin, tool); + + int rc = slurp_eq(rebuilt, canonical); + if (rc != 0) { + fprintf(stderr, "self-rebuild FAIL: %s rebuilt != cstage %s\n", + tool, canonical); + } + + /* Leave the driver's intermediates (.s/.o/.combined.ww) next to the + * source — 991/992/994 read those fixtures, and `make wwstage` had + * already produced byte-identical copies anyway. */ + snprintf(cmd, sizeof cmd, "rm -rf %s", workdir); + runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + struct { + const char *tool; + const char *src; + const char *inc_local; + } tools[] = { + { "6c", "selfhost/cmd/6c/main.ww", "" }, + { "6a", "selfhost/cmd/6a/main.ww", "selfhost/cmd/6a" }, + { "6l", "selfhost/cmd/6l/main.ww", "selfhost/cmd/6l" }, + { "ww", "selfhost/cmd/ww/main.ww", "" }, + { "wwdump", "selfhost/cmd/wwdump/main.ww", "" }, + { NULL, NULL, NULL }, + }; + + int fail = 0, n = 0; + for (int i = 0; tools[i].tool; i++) { + if (rebuild_one(bin, cwd, tools[i].tool, tools[i].src, + tools[i].inc_local) != 0) + fail++; + n++; + } + if (fail) { + fprintf(stderr, "self-rebuild: %d/%d tool(s) diverged\n", fail, n); + return 1; + } + printf("self-rebuild: %d wwstage tool(s) round-trip byte-identical " + "through ww_ww + 6c_ww + 6a_ww + 6l_ww\n", n); + return 0; +}