Files
ww/selfhost/cmd/wwdump/main.combined.ww
Hojun-Cho b9443b1f33 os: graduate O_*, SEEK_* defs to flag and whence enums
Mirrors Hare's `fs::flag` and `io::whence`:

    export type flag = enum i32 {
            RDONLY  = 0,
            WRONLY  = 1,
            RDWR    = 2,
            CREATE  = 64,    // 0o100
            TRUNC   = 512,   // 0o1000
    };

    export type whence = enum i32 { SET = 0, CUR = 1, END = 2 };

open/tryopen/lseek signatures take the enum types (`flags: flag`,
`w: whence`) so callers get type-checked: `os.open(p, os.flag.RDONLY,
0)` is the correct shape, and `os.flag.WRONLY | os.flag.CREATE |
os.flag.TRUNC` typechecks as a `flag` via the same-named-type rule.

Callers in selfhost/cmd/{ww,w6c,w6a,w6l,wwdump} updated from
`os.O_RDONLY` etc. to `os.flag.RDONLY`. SYS_* syscall numbers kept
as `def` for now (internal-only, ABI surface, no Hare analogue in
this scope).

selfhost/test/smoke.ww keeps its standalone-compile property by
using a numeric literal (`0`, RDONLY's value) for the open flags
arg — probe 6 in 990_selfhost compiles smoke.ww with no `use`
expansion, so cross-module type refs like `os.flag.RDONLY` can't
resolve there. Untyped 0 → flag via type_isnum.
2026-05-12 04:41:50 +09:00

9401 lines
263 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MODULE: os
// 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;
def SYS_GETCWD: i64 = 79;
def SYS_GETDENTS64: i64 = 217;
// open(2) flags. Linux values, matching <fcntl.h>. Hare names them
// `fs::flag::RDONLY` etc; we use the same leaf names so callers say
// `os.flag.RDONLY` and `os.flag.WRONLY | os.flag.CREATE`.
export type flag = enum i32 {
RDONLY = 0,
WRONLY = 1,
RDWR = 2,
CREATE = 64, // 0x40
TRUNC = 512, // 0x200
};
// lseek(2) whence. Hare names it `io::whence`.
export type whence = enum i32 {
SET = 0,
CUR = 1,
END = 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 w6c_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 `oserror` (an i64 carrying
// -errno). The sum type makes success/failure explicit and lets
// callers `?` the result up the stack.
export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = {
let r: i64 = read(fd, buf, n);
if (r < 0) { return r: oserror; };
return r;
};
export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = {
let r: i64 = write(fd, buf, n);
if (r < 0) { return r: oserror; };
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: flag, mode: i32) i32 = {
return syscall3(SYS_OPEN, path: i64, (flags as i32): i64, mode: i64): i32;
};
export fn tryopen(path: *u8, flags: flag, mode: i32) (i32 | oserror) = {
let fd: i32 = open(path, flags, mode);
if (fd < 0) { return fd: i64: oserror; };
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, w: whence) i64 = {
return syscall3(SYS_LSEEK, fd: i64, off, (w as i32): i64);
};
// oserror — the underlying errno from a failed syscall, as a
// negative i64 (Linux's int convention; e.g. -2 = ENOENT). The
// `!`-flagged alias makes ?-propagation pick this variant as the
// error half of any (T | oserror) shape. Hare's analogue is
// errors::errno carried inside io::error.
export type oserror = !i64;
// filesize — byte length of an open fd via lseek-to-end-and-back.
export fn filesize(fd: i32) (i64 | oserror) = {
let end: i64 = lseek(fd, 0i64, whence.END);
if (end < 0) { return end: oserror; };
let r: i64 = lseek(fd, 0i64, whence.SET);
if (r < 0) { return r: oserror; };
return end;
};
// readall — keep reading until `n` bytes have arrived or the fd
// closes early. Hare name (io::readall); the buffer is caller-
// supplied, matching the Plan 9 subset convention.
export fn readall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = {
let got: u64 = 0u64;
for (got < n) {
let r: i64 = read(fd, buf + got, n - got);
if (r < 0) { return r: oserror; };
if (r == 0) { return got: i64; }; // short read: caller decides
got += r: u64;
};
return got: i64;
};
// writeall — keep writing until `n` bytes have been accepted or the
// fd refuses progress. Hare name (io::writeall).
export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = {
let sent: u64 = 0u64;
for (sent < n) {
let r: i64 = write(fd, buf + sent, n - sent);
if (r < 0) { return r: oserror; };
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 <unistd.h> (F_OK=0).
export fn access(path: *u8, mode: i32) i32 = {
return syscall2(SYS_ACCESS, path: i64, mode: i64): i32;
};
// remove — unlink(2). Hare name; the underlying syscall is unlink(2).
export fn remove(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`, return the pid that ended (or negative errno).
export fn wait4(pid: i32, status: *i32, options: i32, rusage: *void) i32 = {
return syscall4(SYS_WAIT4, pid: i64, status: i64,
options: i64, rusage: i64): i32;
};
// getcwd(2) — Linux flavour. Writes the NUL-terminated cwd into `buf`
// and returns the number of bytes written (including the NUL), or a
// negative errno. The driver uses it to expand `.` to the cwd's
// basename for `ww build` / `ww test`.
export fn getcwd(buf: *u8, n: u64) i64 = {
return syscall2(SYS_GETCWD, buf: i64, n: i64);
};
// getdents64(2) — Linux directory enumeration. The fd must be opened
// with O_RDONLY on a directory. `buf` receives a packed sequence of
// linux_dirent64 records:
//
// struct linux_dirent64 {
// u64 d_ino; // 0..7
// i64 d_off; // 8..15
// u16 d_reclen; // 16..17 — total bytes for this record
// u8 d_type; // 18 — DT_REG/DT_DIR/...
// u8 d_name[]; // 19.. — NUL-terminated name + padding
// };
//
// Returns bytes written into `buf` (advance by d_reclen to walk),
// 0 at end-of-directory, or a negative errno.
export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = {
return syscall3(SYS_GETDENTS64, fd: i64, buf: i64, n: i64);
};
// MODULE: wcc
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/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/wcc/ 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;
};
};
// MODULE: strconv
// strconv — number↔string conversions. Decimal i64 to/from a fixed
// buffer. Error shapes mirror Hare's strconv types: (T | invalid |
// overflow) where each error is a named alias over a payload type
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position. Mirrors
// Hare's strconv::invalid = !size (we use i32 instead of size).
export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count.
// Hare name; the buffer-in shape is the sanctioned Plan 9 subset of
// Hare's `u64tos(u, base) const str`. Unsigned-only so callers don't
// have to think about wraparound when printing a u64 with the high
// bit set.
export fn u64tos(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 i64tos(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;
};
// stoi64 — Hare-style fallible signed decimal parser. No locale, no
// whitespace, no underscores: a leading '-' is the only non-digit
// accepted, and only at position 0.
export fn stoi64(s: str) (i64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let i: i32 = 0;
let neg: bool = false;
if (s[0] == 45u8) { neg = true; i = 1; };
if (i >= s.len) { return i: invalid; };
let v: i64 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c < 48u8) { return i: invalid; };
if (c > 57u8) { return i: invalid; };
v = v * 10 + ((c: i64) - 48);
i += 1;
};
if (neg) { v = -v; };
return v;
};
// stou64 — fallible unsigned decimal parser. No leading sign.
export fn stou64(s: str) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let v: u64 = 0u64;
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c < 48u8) { return i: invalid; };
if (c > 57u8) { return i: invalid; };
v = v * 10u64 + ((c: u64) - 48u64);
i += 1;
};
return v;
};
// MODULE: lex
// lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind /
// Tok / Pos shapes from cmd/wcc/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/wcc/tok.c:tokprint().
use os;
use strconv;
// ---- Tkind ------------------------------------------------------------
// Mirror of the C enum in cmd/wcc/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_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
// Appended at the tail (not grouped with the keyword block) so every
// pre-existing TK_* value stays unchanged — the 990_selfhost test
// diffs wwdump output against the C side, byte for byte.
def TK_IS: i32 = 82;
def TK_VOID: i32 = 83;
def TK_YIELD: i32 = 84;
def TK_ENUM: i32 = 85;
def TK_LAST: i32 = 86;
// ---- Pos / Tok --------------------------------------------------------
//
// `pos` is used at error-reporting boundaries; we always pass it via
// *pos so the value never gets struct-copied (w6c 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 streqn(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/wcc/tok.c.
export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "as", n)) { return TK_AS; };
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
if (streqn(p, "else", n)) { return TK_ELSE; };
if (streqn(p, "enum", n)) { return TK_ENUM; };
if (streqn(p, "export", n)) { return TK_EXPORT; };
if (streqn(p, "false", n)) { return TK_FALSE; };
if (streqn(p, "fn", n)) { return TK_FN; };
if (streqn(p, "for", n)) { return TK_FOR; };
if (streqn(p, "if", n)) { return TK_IF; };
if (streqn(p, "is", n)) { return TK_IS; };
if (streqn(p, "let", n)) { return TK_LET; };
if (streqn(p, "match", n)) { return TK_MATCH; };
if (streqn(p, "nil", n)) { return TK_NIL; };
if (streqn(p, "proc", n)) { return TK_PROC; };
if (streqn(p, "return", n)) { return TK_RETURN; };
if (streqn(p, "static", n)) { return TK_STATIC; };
if (streqn(p, "struct", n)) { return TK_STRUCT; };
if (streqn(p, "switch", n)) { return TK_SWITCH; };
if (streqn(p, "true", n)) { return TK_TRUE; };
if (streqn(p, "type", n)) { return TK_TYPE; };
if (streqn(p, "use", n)) { return TK_USE; };
if (streqn(p, "void", n)) { return TK_VOID; };
if (streqn(p, "yield", n)) { return TK_YIELD; };
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 "<none>"; };
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_IS) { return "is"; };
if (k == TK_VOID) { return "void"; };
if (k == TK_YIELD) { return "yield"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_ENUM) { return "enum"; };
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 "<last>"; };
return "<?>";
};
// ---- writer for tokprint ----------------------------------------------
//
// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style
// escapes for \, ", \n, \t, \r and \xNN for other non-printables.
fn fputcbyte(fd: i32, b: u8) void = {
let buf: [1]u8;
buf[0] = b;
os.write(fd, buf.ptr, 1u64);
};
fn fputsstr(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 = {
fputcbyte(fd, 34u8); // '"'
let i: i32 = 0;
for (i < n) {
let c: u8 = p[i];
if (c == 92u8) { // '\\'
fputsstr(fd, "\\\\");
} else {
if (c == 34u8) { // '"'
fputsstr(fd, "\\\"");
} else {
if (c == 10u8) { // '\n'
fputsstr(fd, "\\n");
} else {
if (c == 9u8) { // '\t'
fputsstr(fd, "\\t");
} else {
if (c == 13u8) { // '\r'
fputsstr(fd, "\\r");
} else {
if (c < 32u8) {
fputhex2(fd, c);
} else {
if (c == 127u8) {
fputhex2(fd, c);
} else {
fputcbyte(fd, c);
};
};
};
};
};
};
};
i += 1;
};
fputcbyte(fd, 34u8);
};
// tokprint — write one token line to fd. Format must match
// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor.
// "<file>:<line>:<col> <kindname>[ <value>]\n"
//
// Takes `t` by pointer because w6c 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 w6c — `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) {
fputsstr(fd, tfile);
} else {
fputsstr(fd, "<none>");
};
fputcbyte(fd, 58u8); // ':'
let buf: [32]u8;
let n: i32 = strconv.i64tos(buf[0:32], t.line: i64);
os.write(fd, buf.ptr, n: u64);
fputcbyte(fd, 58u8);
n = strconv.i64tos(buf[0:32], t.col: i64);
os.write(fd, buf.ptr, n: u64);
fputcbyte(fd, 32u8); // ' '
fputsstr(fd, tokname(t.kind));
if (t.kind == TK_IDENT) {
fputcbyte(fd, 32u8);
fputq(fd, ttext.ptr, ttext.len);
} else { if (t.kind == TK_STR) {
fputcbyte(fd, 32u8);
fputq(fd, ttext.ptr, ttext.len);
} else { if (t.kind == TK_ERR) {
fputcbyte(fd, 32u8);
fputq(fd, ttext.ptr, ttext.len);
} else { if (t.kind == TK_INT) {
fputcbyte(fd, 32u8);
n = strconv.u64tos(buf[0:32], t.uval);
os.write(fd, buf.ptr, n: u64);
} else { if (t.kind == TK_RUNE) {
fputcbyte(fd, 32u8);
n = strconv.u64tos(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.
fputcbyte(fd, 10u8); // '\n'
};
// MODULE: ascii
// ascii — rune-class predicates and case folding for the ASCII range.
// Matches Hare's ascii::isdigit family (rune-taking signature). Runes
// 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: rune) bool = {
if (c < 48) { return false; };
if (c > 57) { return false; };
return true;
};
export fn isupper(c: rune) bool = {
if (c < 65) { return false; };
if (c > 90) { return false; };
return true;
};
export fn islower(c: rune) bool = {
if (c < 97) { return false; };
if (c > 122) { return false; };
return true;
};
export fn isalpha(c: rune) bool = {
if (isupper(c)) { return true; };
return islower(c);
};
export fn isalnum(c: rune) bool = {
if (isalpha(c)) { return true; };
return isdigit(c);
};
// isspace — the C/Hare set: space, tab, NL, VT, FF, CR.
export fn isspace(c: rune) bool = {
if (c == 32) { return true; }; // ' '
if (c == 9) { return true; }; // '\t'
if (c == 10) { return true; }; // '\n'
if (c == 11) { return true; }; // '\v'
if (c == 12) { return true; }; // '\f'
if (c == 13) { return true; }; // '\r'
return false;
};
export fn isxdigit(c: rune) bool = {
if (isdigit(c)) { return true; };
if (c >= 65) {
if (c <= 70) { return true; }; // 'A'..'F'
};
if (c >= 97) {
if (c <= 102) { return true; }; // 'a'..'f'
};
return false;
};
// digitval — value of `c` as a hex/decimal digit. void variant means
// `c` isn't a hex digit. Useful when scanning numeric literals.
export fn digitval(c: rune) (i32 | void) = {
if (isdigit(c)) { return (c - 48): i32; };
if (c >= 65) {
if (c <= 70) { return ((c - 65) + 10): i32; };
};
if (c >= 97) {
if (c <= 102) { return ((c - 97) + 10): i32; };
};
return;
};
// isidstart / isidpart — identifier classes used by the lexer.
// Alpha or '_' starts; alnum or '_' continues.
export fn isidstart(c: rune) bool = {
if (isalpha(c)) { return true; };
if (c == 95) { return true; }; // '_'
return false;
};
export fn isidpart(c: rune) bool = {
if (isalnum(c)) { return true; };
if (c == 95) { return true; };
return false;
};
// tolower / toupper — fold ASCII case. Non-letters pass through.
export fn tolower(c: rune) rune = {
if (isupper(c)) { return c + 32; };
return c;
};
export fn toupper(c: rune) rune = {
if (islower(c)) { return c - 32; };
return c;
};
// MODULE: lex
// lib/ww/lex/lex.ww — port of cmd/wcc/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: w6c 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,
module: str, // current module from `// MODULE: foo` directive; "" if none
};
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;
let empty: str;
empty.ptr = nil;
empty.len = 0;
l.module = empty;
};
// 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 curpos(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 errat 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 errat(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) {
lget(l); lget(l); // consume '//'
// Driver injects `// MODULE: foo` before each
// source file's contents; capture so cgen can
// mangle private symbols by module.
if (lpeek(l, 0u64) == 32) { // ' '
if (lpeek(l, 1u64) == 77) { // 'M'
if (lpeek(l, 2u64) == 79) { // 'O'
if (lpeek(l, 3u64) == 68) { // 'D'
if (lpeek(l, 4u64) == 85) { // 'U'
if (lpeek(l, 5u64) == 76) { // 'L'
if (lpeek(l, 6u64) == 69) { // 'E'
if (lpeek(l, 7u64) == 58) { // ':'
if (lpeek(l, 8u64) == 32) { // ' '
let i: i32 = 0;
for (i < 9) { lget(l); i += 1; };
let start: u64 = l.lpos;
for (true) {
let cx: i32 = lpeek(l, 0u64);
if (cx < 0) { break; };
if (cx == 10) { break; };
if (cx == 13) { break; };
lget(l);
};
let n: u64 = l.lpos - start;
l.module = astrndup(l.a, l.src + start, n);
};};};};};};};};};
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;
curpos(l, &cp);
errat(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.isxdigit(hi: rune)) {
let cp: pos; curpos(l, &cp);
errat(l, &cp, "bad \\x escape");
return false;
};
if (!ascii.isxdigit(lo: rune)) {
let cp: pos; curpos(l, &cp);
errat(l, &cp, "bad \\x escape");
return false;
};
// Hex digits already validated by isxdigit above — `!`
// (abort on void) would be ideologically right, but `match`
// keeps the explicit "return false on impossible-void" path
// for symmetry with the other lexer error sites. Use `!`
// once we have a panic-with-position helper.
let h: i32 = ascii.digitval(hi: rune)!;
let lv: i32 = ascii.digitval(lo: rune)!;
*out = (h << 4) | lv;
return true;
};
let cp: pos; curpos(l, &cp);
errat(l, &cp, "bad escape");
return false;
};
// scandecimalrun — consume a run of decimal digits and underscores.
fn scandecimalrun(l: *lex) void = {
for (true) {
let c: i32 = lpeek(l, 0u64);
if (c < 0) { break; };
if (!ascii.isdigit(c: rune)) {
if (c != 95) { break; };
};
lget(l);
};
};
fn scanhexrun(l: *lex) void = {
for (true) {
let c: i32 = lpeek(l, 0u64);
if (c < 0) { break; };
if (!ascii.isxdigit(c: rune)) {
if (c != 95) { break; };
};
lget(l);
};
};
fn scanbinrun(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 scanoctrun(l: *lex) void = {
for (true) {
let c: i32 = lpeek(l, 0u64);
if (c < 48) { break; };
if (c > 55) {
if (c != 95) { break; };
};
lget(l);
};
};
// scanexp — consume the [eE][+-]?[0-9]+ tail of a float, if present.
fn scanexp(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: rune)) { 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; scanhexrun(l);
} else { if (c1 == 88) { // 'X'
lget(l); lget(l); base = 16; scanhexrun(l);
} else { if (c1 == 98) { // 'b'
lget(l); lget(l); base = 2; scanbinrun(l);
} else { if (c1 == 66) { // 'B'
lget(l); lget(l); base = 2; scanbinrun(l);
} else { if (c1 == 111) { // 'o'
lget(l); lget(l); base = 8; scanoctrun(l);
} else { if (c1 == 79) { // 'O'
lget(l); lget(l); base = 8; scanoctrun(l);
} else {
scandecimalrun(l);
if (lpeek(l, 0u64) == 46) {
let after: i32 = lpeek(l, 1u64);
if (after >= 48) {
if (after <= 57) {
isfloat = true;
lget(l);
scandecimalrun(l);
scanexp(l);
};
};
};
};};};};};};
} else {
scandecimalrun(l);
if (lpeek(l, 0u64) == 46) {
let after: i32 = lpeek(l, 1u64);
if (after >= 48) {
if (after <= 57) {
isfloat = true;
lget(l);
scandecimalrun(l);
scanexp(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) {
errat(l, start, "bad integer literal");
out.kind = TK_ERR;
};
};
let pc: i32 = lpeek(l, 0u64);
if (pc >= 0) {
if (ascii.isidstart(pc: rune)) {
let sb: u64 = l.lpos;
for (true) {
let cc: i32 = lpeek(l, 0u64);
if (cc < 0) { break; };
if (!ascii.isidpart(cc: rune)) { 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: rune)) { break; };
lget(l);
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {
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) {
errat(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) {
errat(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) {
errat(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 emitsimple(start: *pos, k: i32, out: *tok) void = {
out.kind = k;
out.file = start.file;
out.line = start.line;
out.col = start.col;
};
// setposfrom — copy file/line/col from a *pos into a tok. Used by
// the err-token path where we already have a pos.
fn setposfrom(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; curpos(l, &p);
emitsimple(&p, TK_EOF, out);
return;
};
let start: pos; curpos(l, &start);
let c: i32 = lpeek(l, 0u64);
if (c >= 0) {
if (ascii.isidstart(c: rune)) { lexident(l, &start, out); return; };
if (ascii.isdigit(c: rune)) { 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) { emitsimple(&start, TK_LPAREN, out); return; };
if (c == 41) { emitsimple(&start, TK_RPAREN, out); return; };
if (c == 123) { emitsimple(&start, TK_LBRACE, out); return; };
if (c == 125) { emitsimple(&start, TK_RBRACE, out); return; };
if (c == 91) { emitsimple(&start, TK_LBRACK, out); return; };
if (c == 93) { emitsimple(&start, TK_RBRACK, out); return; };
if (c == 44) { emitsimple(&start, TK_COMMA, out); return; };
if (c == 59) { emitsimple(&start, TK_SEMI, out); return; };
if (c == 58) { emitsimple(&start, TK_COLON, out); return; };
if (c == 64) { emitsimple(&start, TK_AT, out); return; };
if (c == 63) { emitsimple(&start, TK_QUESTION, out); return; };
if (c == 126) { emitsimple(&start, TK_TILDE, out); return; };
if (c == 46) { // '.'
if (lpeek(l, 0u64) == 46) {
if (lpeek(l, 1u64) == 46) {
lget(l); lget(l);
emitsimple(&start, TK_ELLIPSIS, out); return;
};
lget(l);
emitsimple(&start, TK_DOTDOT, out); return;
};
emitsimple(&start, TK_DOT, out); return;
};
if (c == 43) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_PLUSEQ, out); return; };
emitsimple(&start, TK_PLUS, out); return;
};
if (c == 45) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_MINUSEQ, out); return; };
if (lpeek(l, 0u64) == 62) { lget(l); emitsimple(&start, TK_ARROW, out); return; };
emitsimple(&start, TK_MINUS, out); return;
};
if (c == 42) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_STAREQ, out); return; };
emitsimple(&start, TK_STAR, out); return;
};
if (c == 47) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_SLASHEQ, out); return; };
emitsimple(&start, TK_SLASH, out); return;
};
if (c == 37) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_PERCENTEQ, out); return; };
emitsimple(&start, TK_PERCENT, out); return;
};
if (c == 38) {
if (lpeek(l, 0u64) == 38) { lget(l); emitsimple(&start, TK_AND, out); return; };
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_AMPEQ, out); return; };
emitsimple(&start, TK_AMP, out); return;
};
if (c == 124) {
if (lpeek(l, 0u64) == 124) { lget(l); emitsimple(&start, TK_OR, out); return; };
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_PIPEEQ, out); return; };
emitsimple(&start, TK_PIPE, out); return;
};
if (c == 94) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_CARETEQ, out); return; };
emitsimple(&start, TK_CARET, out); return;
};
if (c == 61) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_EQ, out); return; };
if (lpeek(l, 0u64) == 62) { lget(l); emitsimple(&start, TK_FATARROW, out); return; };
emitsimple(&start, TK_ASSIGN, out); return;
};
if (c == 33) {
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_NEQ, out); return; };
emitsimple(&start, TK_NOT, out); return;
};
if (c == 60) {
if (lpeek(l, 0u64) == 60) {
lget(l);
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_LSHIFTEQ, out); return; };
emitsimple(&start, TK_LSHIFT, out); return;
};
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_LE, out); return; };
if (lpeek(l, 0u64) == 45) { lget(l); emitsimple(&start, TK_LARROW, out); return; };
emitsimple(&start, TK_LT, out); return;
};
if (c == 62) {
if (lpeek(l, 0u64) == 62) {
lget(l);
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_RSHIFTEQ, out); return; };
emitsimple(&start, TK_RSHIFT, out); return;
};
if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, TK_GE, out); return; };
emitsimple(&start, TK_GT, out); return;
};
errat(l, &start, "unexpected character");
out.kind = TK_ERR;
setposfrom(out, &start);
let one: [1]u8;
one[0] = c: u8;
out.text = astrndup(l.a, one.ptr, 1u64);
};
// MODULE: ww
// lib/ww/ast.ww — port of cmd/wcc/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/wcc/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;
// Appended at the tail to keep all prior N_* values stable. The
// 990_selfhost test diffs astprint against the C side byte-for-byte.
def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_YIELD: i32 = 64;
def N_TENUM: i32 = 65;
def N_TENUMMEMBER: i32 = 66;
def N_LAST: i32 = 67;
// ---- 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", ...)
module: str, // originating module from `// MODULE: foo`; "" if none
};
export fn newnode(a: *arena, k: i32, file: str, line: i32, col: i32) *node = {
let n: *node = amalloc(a, 208u64): *node; // ≥ 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_TYPETEST) { return "typetest"; };
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_YIELD) { return "yield"; };
if (k == N_TENUM) { return "tenum"; };
if (k == N_TENUMMEMBER) { return "tenummember"; };
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.u64tos(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.u64tos(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_TENUMMEMBER ||
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);
};
// MODULE: parse
// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww.
use os;
use mem;
use tok;
// streqlocal — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streqlocal(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
if (p.curkind == TK_INT) {
let n: *node = newnode(p.a, N_INTLIT, pf, pl, pc);
n.uval = p.curuval;
n.str = p.curtext;
advance(p);
return n;
};
if (p.curkind == TK_STR) {
let n: *node = newnode(p.a, N_STRLIT, pf, pl, pc);
n.str = p.curtext;
advance(p);
return n;
};
if (p.curkind == TK_RUNE) {
let n: *node = newnode(p.a, N_RUNELIT, pf, pl, pc);
n.uval = p.curuval;
advance(p);
return n;
};
if (p.curkind == TK_TRUE) {
advance(p);
return newnode(p.a, N_TRUE, pf, pl, pc);
};
if (p.curkind == TK_FALSE) {
advance(p);
return newnode(p.a, N_FALSE, pf, pl, pc);
};
if (p.curkind == TK_NIL) {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_VOID) {
advance(p);
return newnode(p.a, N_VOIDLIT, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
// Tuple literal: (a, b, ...)
if (accepttok(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.curkind == TK_RPAREN) { break; };
let en: *node = parseexpr(p);
tail.next = en;
tail = en;
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RPAREN, "expected ')' in tuple");
return t;
};
expecttok(p, TK_RPAREN, "expected ')'");
return e;
};
if (p.curkind == TK_IDENT) {
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
n.str = p.curtext;
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.curkind == 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.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
let id: str;
expectident(p, &id);
expecttok(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 (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACE, "expected '}' after struct literal");
s.list = head;
return s;
};
return n;
};
if (p.curkind == TK_MATCH) {
// match (e) { case let v: T => stmt; case T => stmt; case => stmt; };
advance(p);
expecttok(p, TK_LPAREN, "expected '(' after match");
let m: *node = newnode(p.a, N_MATCH, pf, pl, pc);
m.lhs = parseexpr(p);
expecttok(p, TK_RPAREN, "expected ')' after match scrutinee");
expecttok(p, TK_LBRACE, "expected '{' to open match body");
let head: *node = nil;
let tail: *node = nil;
for (p.curkind == TK_CASE) {
let cf: str = p.curfile;
let cl: i32 = p.curline;
let cc: i32 = p.curcol;
advance(p); // past `case`
let mc: *node = newnode(p.a, N_MCASE, cf, cl, cc);
if (p.curkind == TK_LET) {
advance(p);
let id: str;
expectident(p, &id);
mc.str = id;
expecttok(p, TK_COLON, "expected ':' after match binding");
mc.lhs = parsetype(p);
} else { if (p.curkind != TK_FATARROW) {
mc.lhs = parsetype(p);
};};
expecttok(p, TK_FATARROW, "expected '=>' in match arm");
mc.body = parsestmt(p);
if (head == nil) { head = mc; tail = mc; }
else { tail.next = mc; tail = mc; };
};
expecttok(p, TK_RBRACE, "expected '}' after match body");
m.list = head;
return m;
};
errmsg(p, "expected expression");
advance(p);
return newnode(p.a, N_NONE, pf, pl, pc);
};
fn parsearglist(p: *parser, closekind: i32, headout: **node) void = {
*headout = nil;
if (p.curkind == closekind) { 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 (!accepttok(p, TK_COMMA)) { break; };
if (p.curkind == closekind) { break; };
};
*headout = head;
};
fn parsepostfix(p: *parser, lhs: *node) *node = {
let cur: *node = lhs;
for (true) {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
if (p.curkind == TK_LPAREN) {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streqlocal(cur.str, "size")) { is_typeop = 1; };
if (streqlocal(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;
};
if (p.curkind == TK_LBRACK) {
advance(p);
// `[ : hi ]` — slice with implicit lo = 0.
if (p.curkind == TK_COLON) {
advance(p);
let n: *node = newnode(p.a, N_SLICE, pf, pl, pc);
n.lhs = cur;
if (p.curkind != TK_RBRACK) {
n.cond = parseexpr(p);
};
expecttok(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.curkind == TK_COLON) {
advance(p);
let n: *node = newnode(p.a, N_SLICE, pf, pl, pc);
n.lhs = cur;
n.rhs = e;
if (p.curkind != TK_RBRACK) {
n.cond = parseexpr(p);
};
expecttok(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;
expecttok(p, TK_RBRACK, "expected ']' after index");
cur = n;
continue;
};
if (p.curkind == TK_DOT) {
advance(p);
let n: *node = newnode(p.a, N_DOT, pf, pl, pc);
n.lhs = cur;
// Hare-style tuple field access: `t.0`, `t.1`. The
// numeric literal becomes the field name string so the
// cgen tuple-positional path matches `cmd/wcc/parse.c`.
if (p.curkind == TK_INT) {
n.str = p.curtext;
advance(p);
} else {
let id: str;
expectident(p, &id);
n.str = id;
};
cur = n;
continue;
};
if (p.curkind == 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;
};
// Hare-style postfix:
// `e as T` — assert lhs is variant T (abort otherwise) → T
// `e is T` — bool: does lhs currently hold variant T?
// Same precedence level as the `:` cast.
if (p.curkind == TK_AS) {
advance(p);
let n: *node = newnode(p.a, N_TYPEASSERT, pf, pl, pc);
n.lhs = cur;
n.rhs = parsetype(p);
cur = n;
continue;
};
if (p.curkind == TK_IS) {
advance(p);
let n: *node = newnode(p.a, N_TYPETEST, pf, pl, pc);
n.lhs = cur;
n.rhs = parsetype(p);
cur = n;
continue;
};
// `e?` — propagate error variant up the stack.
// `e!` — abort on error variant.
if (p.curkind == TK_QUESTION) {
advance(p);
let n: *node = newnode(p.a, N_TRYPROP, pf, pl, pc);
n.lhs = cur;
cur = n;
continue;
};
if (p.curkind == TK_NOT) {
advance(p);
let n: *node = newnode(p.a, N_TRYUNW, pf, pl, pc);
n.lhs = cur;
cur = n;
continue;
};
break;
};
return cur;
};
fn parseunary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let k: i32 = p.curkind;
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.curkind;
let pr: i32 = bprec(op);
if (pr == 0) { return cur; };
if (pr < minp) { return cur; };
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p);
let rhs: *node = parseunary(p);
for (true) {
let np: i32 = bprec(p.curkind);
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.curkind)) {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let op: i32 = p.curkind;
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;
};
// MODULE: parse
// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
use os;
use mem;
use tok;
fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
// Hare-style tuple destructure: `let (a, b) = expr;`.
// Types are optional per binding (matches C parser; Hare itself
// doesn't allow types here, but cmd/wcc/parse.c does).
if (p.curkind == TK_LPAREN) {
advance(p);
let m: *node = newnode(p.a, N_MLET, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (true) {
let lpf: str = p.curfile;
let lpl: i32 = p.curline;
let lpc: i32 = p.curcol;
let l: *node = newnode(p.a, N_LET, lpf, lpl, lpc);
let id: str;
expectbindname(p, &id);
l.str = id;
if (accepttok(p, TK_COLON)) { l.lhs = parsetype(p); };
if (head == nil) { head = l; }
else { tail.next = l; };
tail = l;
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RPAREN, "expected ')' in let destructure");
expecttok(p, TK_ASSIGN, "expected '=' after let destructure");
m.rhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after let");
m.list = head;
if (is_const != 0) {
m.op = TK_CONST;
let lc: *node = head;
for (lc != nil) { lc.op = TK_CONST; lc = lc.next; };
};
return m;
};
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
};
// Comma-multi-let: `let n, s = call();` (ww extension over Hare).
// Collects (name, type) pairs, then '=' rhs. Each binding gets
// its own N_LET; the wrapping N_MLET carries the rhs.
if (p.curkind == TK_COMMA) {
let m: *node = newnode(p.a, N_MLET, pf, pl, pc);
let head: *node = n;
let tail: *node = n;
for (accepttok(p, TK_COMMA)) {
let lpf: str = p.curfile;
let lpl: i32 = p.curline;
let lpc: i32 = p.curcol;
let l: *node = newnode(p.a, N_LET, lpf, lpl, lpc);
let id2: str;
expectbindname(p, &id2);
l.str = id2;
if (accepttok(p, TK_COLON)) { l.lhs = parsetype(p); };
tail.next = l;
tail = l;
};
expecttok(p, TK_ASSIGN, "expected '=' after let names");
m.rhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after let");
m.list = head;
if (is_const != 0) {
m.op = TK_CONST;
let lc: *node = head;
for (lc != nil) { lc.op = TK_CONST; lc = lc.next; };
};
return m;
};
if (accepttok(p, TK_ASSIGN)) {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
fn parseblock(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
expecttok(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.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
let s: *node = parsestmt(p);
if (s != nil) {
if (head == nil) { head = s; tail = s; }
else { tail.next = s; tail = s; };
};
};
expecttok(p, TK_RBRACE, "expected '}' to close block");
blk.list = head;
return blk;
};
fn parseif(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `if`
expecttok(p, TK_LPAREN, "expected '(' after if");
let n: *node = newnode(p.a, N_IF, pf, pl, pc);
n.cond = parseexpr(p);
expecttok(p, TK_RPAREN, "expected ')' after if condition");
n.body = parseblock(p);
if (accepttok(p, TK_ELSE)) {
if (p.curkind == TK_IF) {
n.els = parseif(p);
} else {
n.els = parseblock(p);
};
};
return n;
};
fn parsefor(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `for`
expecttok(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.curkind == TK_LET) {
n.lhs = parseletlocal(p); // init (consumes its own ';')
n.cond = parseexpr(p);
expecttok(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 (accepttok(p, TK_SEMI)) {
// cond ; post
n.cond = first;
n.rhs = parseexpr(p);
} else {
// just (cond)
n.cond = first;
};
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
fn parsestmt(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
// `static` is allowed on local lets per Hare; we accept and skip
// it (it doesn't change the AST shape).
if (p.curkind == TK_STATIC) { advance(p); };
if (p.curkind == TK_LBRACE) {
let b: *node = parseblock(p);
expecttok(p, TK_SEMI, "expected ';' after block");
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");
return n;
};
if (p.curkind == TK_FOR) {
let n: *node = parsefor(p);
expecttok(p, TK_SEMI, "expected ';' after for");
return n;
};
if (p.curkind == TK_RETURN) {
advance(p);
let n: *node = newnode(p.a, N_RETURN, pf, pl, pc);
if (p.curkind != 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.curkind == TK_COMMA) {
let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc);
t.list = first;
let tail: *node = first;
for (accepttok(p, TK_COMMA)) {
let e: *node = parseexpr(p);
tail.next = e;
tail = e;
};
n.lhs = t;
} else {
n.lhs = first;
};
};
expecttok(p, TK_SEMI, "expected ';' after return");
return n;
};
if (p.curkind == TK_DEFER) {
advance(p);
let n: *node = newnode(p.a, N_DEFER, pf, pl, pc);
n.lhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after defer");
return n;
};
if (p.curkind == TK_YIELD) {
advance(p);
let n: *node = newnode(p.a, N_YIELD, pf, pl, pc);
n.lhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after yield");
return n;
};
if (p.curkind == TK_BREAK) {
advance(p);
expecttok(p, TK_SEMI, "expected ';' after break");
return newnode(p.a, N_BREAK, pf, pl, pc);
};
if (p.curkind == TK_CONTINUE) {
advance(p);
expecttok(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/wcc/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.curkind == TK_COMMA) {
let m: *node = newnode(p.a, N_MASSIGN, pf, pl, pc);
let head: *node = e;
let tail: *node = e;
for (p.curkind == TK_COMMA) {
advance(p);
let lv: *node = parsebin(p, parseunary(p), 1);
tail.next = lv;
tail = lv;
};
expecttok(p, TK_ASSIGN, "expected '=' after multi-assign lvalues");
m.rhs = parseexpr(p);
m.list = head;
expecttok(p, TK_SEMI, "expected ';' after multi-assign");
return m;
};
let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc);
n.lhs = e;
expecttok(p, TK_SEMI, "expected ';' after expression statement");
return n;
};
// MODULE: parse
// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
use os;
use mem;
use tok;
fn parseuse(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `use`
let n: *node = newnode(p.a, N_USE, pf, pl, pc);
let id: str;
expectident(p, &id);
n.str = id;
expecttok(p, TK_SEMI, "expected ';' after use");
return n;
};
fn parsedef(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `def`
let n: *node = newnode(p.a, N_DEF, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in def");
n.lhs = parsetype(p);
expecttok(p, TK_ASSIGN, "expected '=' in def");
n.rhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after def");
n.exported = exported;
return n;
};
fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
};
if (accepttok(p, TK_ASSIGN)) {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
fn parseattrs(p: *parser) *node = {
let head: *node = nil;
let tail: *node = nil;
for (p.curkind == TK_AT) {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p);
let a: *node = newnode(p.a, N_ATTR, pf, pl, pc);
let id: str;
expectident(p, &id);
a.str = id;
// `@name(args...)` for FFI-style attrs; `@name` for marker-
// only attrs like @test (no parens).
if (accepttok(p, TK_LPAREN)) {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
a.list = arghead;
expecttok(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.curkind == TK_RPAREN) { return nil; };
let head: *node = nil;
let tail: *node = nil;
for (true) {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);
if (head == nil) { head = n; tail = n; }
else { tail.next = n; tail = n; };
if (!accepttok(p, TK_COMMA)) { break; };
if (p.curkind == TK_RPAREN) { break; };
};
return head;
};
fn parsefn(p: *parser, exported: i32, attrs: *node) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `fn`
let n: *node = newnode(p.a, N_FNDECL, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
n.str = id;
expecttok(p, TK_LPAREN, "expected '(' after fn name");
n.list = parseparams(p);
expecttok(p, TK_RPAREN, "expected ')' after params");
if (p.curkind != TK_ASSIGN) {
if (p.curkind != TK_SEMI) {
n.lhs = parsetype(p);
};
};
if (accepttok(p, TK_ASSIGN)) {
n.body = parseblock(p);
expecttok(p, TK_SEMI, "expected ';' after fn body");
} else {
// Body-less fn: FFI declaration (`fn name(args) ret;`).
expecttok(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.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `type`
let n: *node = newnode(p.a, N_TYPEDECL, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
n.str = id;
expecttok(p, TK_ASSIGN, "expected '=' in type decl");
n.lhs = parsetype(p);
expecttok(p, TK_SEMI, "expected ';' after type decl");
n.exported = exported;
return n;
};
// MODULE: parse
// lib/ww/parse/parse.ww — port of cmd/wcc/parse.c (entry + plumbing).
//
// Split into Hare-style submodule: parse.ww (here) holds the parser
// struct, lexer plumbing, parsetype, parsefile (entry). Expression,
// statement, and declaration parsers live in expr.ww, stmt.ww,
// decl.ww respectively — all in the same `parse` module.
//
// Calling-convention shim: w6c 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;
use expr;
use stmt;
use decl;
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,
curkind: i32,
curfile: str,
curline: i32,
curcol: i32,
curtext: str,
curuval: u64,
};
fn refill(p: *parser) void = {
let t: tok;
lexnext(p.l, &t);
p.curkind = t.kind;
p.curfile = t.file;
p.curline = t.line;
p.curcol = t.col;
p.curtext = t.text;
p.curuval = 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 accepttok(p: *parser, k: i32) bool = {
if (p.curkind == k) { advance(p); return true; };
return false;
};
fn errmsg(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 expecttok(p: *parser, k: i32, what: str) bool = {
if (p.curkind == k) { advance(p); return true; };
errmsg(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.curkind != TK_IDENT) {
errmsg(p, "expected identifier");
advance(p);
return false;
};
*into = p.curtext;
advance(p);
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
// Other forms (slice, array, struct, fn, chan, tuple, tagged) will
// land in subsequent commits.
// joindotted — arena-build "head.tail" for dotted type-name path
// collapse. Mirrors aprintf in C parser; pulled local to avoid a
// cross-module dependency.
fn joindotted(a: *arena, head: str, tail: str) str = {
let n: u64 = head.len: u64 + 1u64 + tail.len: u64;
let p: *u8 = amalloc(a, n + 1u64): *u8;
let i: u64 = 0u64;
let j: i32 = 0;
for (j < head.len) { p[i] = head[j]; i += 1u64; j += 1; };
p[i] = 46u8; // '.'
i += 1u64;
j = 0;
for (j < tail.len) { p[i] = tail[j]; i += 1u64; j += 1; };
p[i] = 0u8;
let r: str;
r.ptr = p;
r.len = n: i32;
return r;
};
fn parsetype(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
if (p.curkind == TK_NOT) {
// `!T` — Hare error-flagged type wrapper.
advance(p);
let n: *node = newnode(p.a, N_TBANG, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == TK_STAR) {
advance(p);
let n: *node = newnode(p.a, N_TPTR, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == TK_LBRACK) {
advance(p);
if (p.curkind == 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);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
};
if (p.curkind == TK_STRUCT) {
advance(p);
expecttok(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.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
let f: *node = newnode(p.a, N_TFIELD, fpf, fpl, fpc);
let fid: str;
expectident(p, &fid);
f.str = fid;
expecttok(p, TK_COLON, "expected ':' in field");
f.lhs = parsetype(p);
if (fhead == nil) { fhead = f; ftail = f; }
else { ftail.next = f; ftail = f; };
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACE, "expected '}' after struct fields");
n.list = fhead;
return n;
};
if (p.curkind == TK_ENUM) {
// `enum [storage] { NAME [= expr], ... }`
// Storage defaults to i32 (lhs == nil). Each member is an
// N_TENUMMEMBER with str=name and lhs = value expr or nil
// (auto-increment when omitted).
advance(p);
let n: *node = newnode(p.a, N_TENUM, pf, pl, pc);
if (p.curkind != TK_LBRACE) {
n.lhs = parsetype(p);
};
expecttok(p, TK_LBRACE, "expected '{' after enum");
let mhead: *node = nil;
let mtail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
let mpf: str = p.curfile;
let mpl: i32 = p.curline;
let mpc: i32 = p.curcol;
let m: *node = newnode(p.a, N_TENUMMEMBER, mpf, mpl, mpc);
let mid: str;
expectident(p, &mid);
m.str = mid;
if (accepttok(p, TK_ASSIGN)) {
m.lhs = parseexpr(p);
};
if (mhead == nil) { mhead = m; mtail = m; }
else { mtail.next = m; mtail = m; };
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACE, "expected '}' after enum members");
n.list = mhead;
return n;
};
if (p.curkind == TK_VOID) {
// `void` keyword in type-expr context — emit as N_TNAME so
// resolution treats it like any other primitive name.
let n: *node = newnode(p.a, N_TNAME, pf, pl, pc);
n.str = "void";
advance(p);
return n;
};
if (p.curkind == TK_IDENT) {
let n: *node = newnode(p.a, N_TNAME, pf, pl, pc);
let acc: str = p.curtext;
advance(p);
// Dotted path collapse: pkg.Type → single TNAME with the
// joined string. Mirrors C parsetype's loop.
for (p.curkind == TK_DOT) {
advance(p);
if (p.curkind != TK_IDENT) { break; };
acc = joindotted(p.a, acc, p.curtext);
advance(p);
};
n.str = acc;
return n;
};
if (p.curkind == TK_LPAREN) {
// (T) or (T, T, ...) or (T | T | ...)
advance(p);
let first: *node = parsetype(p);
if (accepttok(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 (!accepttok(p, TK_PIPE)) { break; };
};
expecttok(p, TK_RPAREN, "expected ')' in tagged-union type");
n.list = head;
return n;
};
if (!accepttok(p, TK_COMMA)) {
expecttok(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 (!accepttok(p, TK_COMMA)) { break; };
if (p.curkind == TK_RPAREN) { break; };
};
expecttok(p, TK_RPAREN, "expected ')' in tuple type");
n.list = head;
return n;
};
if (p.curkind == TK_FN) {
advance(p);
expecttok(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);
expecttok(p, TK_RPAREN, "expected ')' after fn type params");
n.lhs = parsetype(p);
return n;
};
errmsg(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.
export fn parsefile(p: *parser) *node = {
let f: *node = newnode(p.a, N_FILE, p.curfile, p.curline, p.curcol);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_EOF) {
let attrs: *node = parseattrs(p);
let exported: i32 = 0;
if (p.curkind == TK_EXPORT) { exported = 1; advance(p); };
let d: *node = nil;
if (p.curkind == TK_USE) {
d = parseuse(p);
} else { if (p.curkind == TK_DEF) {
d = parsedef(p, exported);
} else { if (p.curkind == TK_TYPE) {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
// Recovery: chew tokens until next ';' or EOF, balancing
// '{' '}' pairs so internal ';'s in unfamiliar forms don't
// derail us.
for (p.curkind != TK_SEMI) {
if (p.curkind == TK_EOF) { break; };
if (p.curkind == TK_LBRACE) {
let depth: i32 = 0;
for (true) {
if (p.curkind == TK_EOF) { break; };
if (p.curkind == TK_LBRACE) { depth += 1; advance(p); continue; };
if (p.curkind == TK_RBRACE) {
depth -= 1;
advance(p);
if (depth == 0) { break; };
continue;
};
advance(p);
};
continue;
};
advance(p);
};
if (p.curkind == 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;
};
// MODULE: ww
// lib/ww/typ.ww — port of cmd/wcc/type.c.
//
// Status: full structural port. The C version uses module-globals for
// the primitive types (tyvoid, tyi32, …); 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/wcc/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_NEVER: i32 = 28;
def TY_UNTYPED_INT: i32 = 29;
def TY_UNTYPED_FLOAT: i32 = 30;
def TY_UNTYPED_STR: i32 = 31;
def TY_UNTYPED_RUNE: i32 = 32;
def TY_UNTYPED_BOOL: i32 = 33;
def TY_UNTYPED_NIL: i32 = 34;
// Appended at the tail to keep prior TY_* values stable — they're
// mirrored in cmd/wcc/ww.h and the selfhost selfcheck depends on
// matching numeric layout.
def TY_ENUM: i32 = 35;
// ---- 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,
tyvoid: *tinfo,
tybool: *tinfo,
tyrune: *tinfo,
tyi8: *tinfo,
tyi16: *tinfo,
tyi32: *tinfo,
tyi64: *tinfo,
tyu8: *tinfo,
tyu16: *tinfo,
tyu32: *tinfo,
tyu64: *tinfo,
tyint: *tinfo,
tyuint: *tinfo,
tyuintptr: *tinfo,
tyf32: *tinfo,
tyf64: *tinfo,
tystr: *tinfo,
tyerr: *tinfo,
tynever: *tinfo,
tyuntypedint: *tinfo,
tyuntypedfloat: *tinfo,
tyuntypedstr: *tinfo,
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *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.tyvoid = prim(a, TY_VOID, "void", 0u64, 1u64);
c.tybool = prim(a, TY_BOOL, "bool", 1u64, 1u64);
c.tyrune = prim(a, TY_RUNE, "rune", 4u64, 4u64);
c.tyi8 = prim(a, TY_I8, "i8", 1u64, 1u64);
c.tyi16 = prim(a, TY_I16, "i16", 2u64, 2u64);
c.tyi32 = prim(a, TY_I32, "i32", 4u64, 4u64);
c.tyi64 = prim(a, TY_I64, "i64", 8u64, 8u64);
c.tyu8 = prim(a, TY_U8, "u8", 1u64, 1u64);
c.tyu16 = prim(a, TY_U16, "u16", 2u64, 2u64);
c.tyu32 = prim(a, TY_U32, "u32", 4u64, 4u64);
c.tyu64 = prim(a, TY_U64, "u64", 8u64, 8u64);
c.tyint = prim(a, TY_INT, "int", 8u64, 8u64);
c.tyuint = prim(a, TY_UINT, "uint", 8u64, 8u64);
c.tyuintptr= prim(a, TY_UINTPTR, "uintptr", 8u64, 8u64);
c.tyf32 = prim(a, TY_F32, "f32", 4u64, 4u64);
c.tyf64 = prim(a, TY_F64, "f64", 8u64, 8u64);
c.tystr = prim(a, TY_STR, "str", 16u64, 8u64);
c.tyerr = prim(a, TY_ERR, "<err>", 0u64, 1u64);
c.tynever = prim(a, TY_NEVER, "never", 0u64, 1u64);
c.tyuntypedint = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64);
c.tyuntypedfloat = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64);
c.tyuntypedstr = prim(a, TY_UNTYPED_STR, "untyped_str", 0u64, 1u64);
c.tyuntypedrune = prim(a, TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64);
c.tyuntypedbool = prim(a, TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64);
c.tyuntypednil = prim(a, TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64);
};
export fn typeptr(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 typeslice(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 typearray(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 typechan(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 typenamed(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 typeisint(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_ENUM) { return typeisint(t.sub); };
if (k == TY_NAMED) { return typeisint(t.under); };
return false;
};
export fn typeisfloat(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 typeisfloat(t.under); };
return false;
};
export fn typeisnum(t: *tinfo) bool = {
if (typeisint(t)) { return true; };
return typeisfloat(t);
};
export fn typeisunsigned(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 typeisunsigned(t.under); };
return false;
};
export fn typeisuntyped(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;
};
// typeeq — structural equality. Named types compare nominally.
export fn typeeq(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 typeeq(a.sub, b.sub); };
if (k == TY_SLICE) { return typeeq(a.sub, b.sub); };
if (k == TY_CHAN) { return typeeq(a.sub, b.sub); };
if (k == TY_ARRAY) {
if (a.alen != b.alen) { return false; };
return typeeq(a.sub, b.sub);
};
if (k == TY_FN) {
if (a.variadic != b.variadic) { return false; };
if (!typeeq(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 (!typeeq(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 (!typeeq(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 (!typeeq(pa.type_, pb.type_)) { return false; };
pa = pa.tnext;
pb = pb.tnext;
};
return true;
};
return true; // primitives match by kind alone
};
// MODULE: ww
// lib/ww/sym.ww — port of cmd/wcc/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/wcc/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,
is_const: i32, // const-bound (assignment rejected)
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 scopelookuplocal(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 scopelookup(s: *scope, name: str) *sym = {
for (s != nil) {
let r: *sym = scopelookuplocal(s, name);
if (r != nil) { return r; };
s = s.parent;
};
return nil;
};
export fn scopedefine(s: *scope, name: str, k: i32, t: *tinfo, decl: *node) *sym = {
if (scopelookuplocal(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;
};
// MODULE: wcc
// selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/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/wcc/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
fnret: *node, // enclosing fn's return type AST (for `?`)
};
// seedprimitives — install the built-in type names so `i32`, `str`,
// etc. can be looked up like ordinary symbols.
fn seedprimitives(c: *checker) void = {
scopedefine(c.top, "void", SK_TYPE, c.tc.tyvoid, nil);
scopedefine(c.top, "bool", SK_TYPE, c.tc.tybool, nil);
scopedefine(c.top, "rune", SK_TYPE, c.tc.tyrune, nil);
scopedefine(c.top, "i8", SK_TYPE, c.tc.tyi8, nil);
scopedefine(c.top, "i16", SK_TYPE, c.tc.tyi16, nil);
scopedefine(c.top, "i32", SK_TYPE, c.tc.tyi32, nil);
scopedefine(c.top, "i64", SK_TYPE, c.tc.tyi64, nil);
scopedefine(c.top, "u8", SK_TYPE, c.tc.tyu8, nil);
scopedefine(c.top, "u16", SK_TYPE, c.tc.tyu16, nil);
scopedefine(c.top, "u32", SK_TYPE, c.tc.tyu32, nil);
scopedefine(c.top, "u64", SK_TYPE, c.tc.tyu64, nil);
scopedefine(c.top, "int", SK_TYPE, c.tc.tyint, nil);
scopedefine(c.top, "uint", SK_TYPE, c.tc.tyuint, nil);
scopedefine(c.top, "uintptr", SK_TYPE, c.tc.tyuintptr, nil);
scopedefine(c.top, "f32", SK_TYPE, c.tc.tyf32, nil);
scopedefine(c.top, "f64", SK_TYPE, c.tc.tyf64, nil);
scopedefine(c.top, "str", SK_TYPE, c.tc.tystr, nil);
scopedefine(c.top, "never", SK_TYPE, c.tc.tynever, nil);
// `nil`, `true`, `false` are keywords — handled at the lex/parser
// level, no symbol needed.
// `len`, `alloc`, `free` are pseudo-builtins; scopedefine them so
// their use sites resolve. The actual semantics live in cgen.
scopedefine(c.top, "len", SK_FN, nil, nil);
scopedefine(c.top, "alloc", SK_FN, nil, nil);
scopedefine(c.top, "free", SK_FN, nil, nil);
};
// installdecl — 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 installdecl(c: *checker, d: *node) void = {
if (d == nil) { return; };
let k: i32 = d.kind;
let nm: str = d.str;
if (k == N_USE) { scopedefine(c.top, nm, SK_USE, nil, d); return; };
if (k == N_DEF) { scopedefine(c.top, nm, SK_DEF, nil, d); return; };
if (k == N_TYPEDECL) { scopedefine(c.top, nm, SK_TYPE, nil, d); return; };
if (k == N_FNDECL) { scopedefine(c.top, nm, SK_FN, nil, d); return; };
if (k == N_LET) { scopedefine(c.top, nm, SK_VAR, nil, d); return; };
};
// resolvewalk — 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).
// Also runs the typed checks (match exhaustiveness, ? subset) in
// the same pass — they need the same scope state.
fn resolvewalk(c: *checker, n: *node) void = {
if (n == nil) { return; };
let k: i32 = n.kind;
// Typed checks fire on the way down so the scrutinee/operand
// is examined before the arm bodies install new bindings.
if (k == N_MATCH) { checkmatchexhaust(c, n); };
if (k == N_TRYPROP) { checktryprop(c, n); };
if (k == N_TYPETEST) { checkisas(c, n); };
if (k == N_TYPEASSERT) { checkisas(c, n); };
if (k == N_LET) { checkletassign(c, n); };
if (k == N_RETURN) { checkretassign(c, n); };
// `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 = scopelookup(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 = scopelookup(c.cur, nm);
// `pkg.Type` — strip the last dot prefix and look up
// the leaf if `pkg` is a use-imported name. Mirrors
// cmd/wcc/check.c resolve_typename.
if (s == nil) {
let dot: i32 = nm.len - 1;
for (dot >= 0) {
if (nm[dot] == 46u8) { break; };
dot -= 1;
};
if (dot > 0) {
let head: str;
head.ptr = nm.ptr;
head.len = dot;
let m: *sym = scopelookup(c.cur, head);
if (m != nil) {
let leaf: str;
leaf.ptr = nm.ptr + (dot + 1): u64;
leaf.len = nm.len - (dot + 1);
s = scopelookup(c.cur, leaf);
};
};
};
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) { resolvewalk(c, n.lhs); };
let nm: str = n.str;
if (nm.len > 0) {
scopedefine(c.cur, nm, SK_VAR, nil, n);
};
if (n.body != nil) { resolvewalk(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) { resolvewalk(c, n.lhs); };
return;
};
if (k == N_FIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
};
if (k == N_TFIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
};
// Walk children (mirroring ast.ww's printer descent order).
if (n.attr != nil) { resolvewalk(c, n.attr); };
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.body != nil) { resolvewalk(c, n.body); };
if (n.els != nil) { resolvewalk(c, n.els); };
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
resolvewalk(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 installdecl, so this duplicate install at
// the file scope just no-ops (scopedefine returns nil on dup).
if (k == N_LET) {
let nm: str = n.str;
if (nm.len > 0) {
scopedefine(c.cur, nm, SK_VAR, nil, n);
};
};
};
// ---- type-level helpers (AST-level, no resolved tinfo) --------------
//
// The selfhost check operates on AST type expressions rather than
// resolved Type structs. These helpers mirror what cmd/wcc/check.c
// does with tinfo, but only on the subset of cases this checker
// needs to enforce: tagged-union exhaustiveness, ? subset
// propagation, and !-flag semantics.
// unwrapbang — strip an N_TBANG wrapper; leaves other nodes alone.
fn unwrapbang(n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind == N_TBANG) { return n.lhs; };
return n;
};
// resolvealias — if n is an N_TNAME pointing at a typedecl, return
// the typedecl's body (possibly recursively). Pass-through for any
// other node. The chain stops once we hit a non-N_TNAME node or a
// name we can't resolve.
fn resolvealias(c: *checker, n: *node) *node = {
let cur: *node = n;
for (cur != nil) {
if (cur.kind != N_TNAME) { return cur; };
let s: *sym = scopelookup(c.cur, cur.str);
if (s == nil) { return cur; };
if (s.skind != SK_TYPE) { return cur; };
let body: *node = nil;
if (s.decl != nil) { body = s.decl.lhs; };
if (body == nil) { return cur; };
cur = unwrapbang(body);
};
return n;
};
// typeeqast — structural equality on AST type expressions, mod
// the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED
// types compare by string (the closest stand-in for pointer
// identity at the AST level); other nodes recurse by kind.
fn typeeqast(a: *node, b: *node) bool = {
let aa: *node = unwrapbang(a);
let bb: *node = unwrapbang(b);
if (aa == nil) { return bb == nil; };
if (bb == nil) { return false; };
if (aa.kind != bb.kind) { return false; };
let k: i32 = aa.kind;
if (k == N_TNAME) { return streq(aa.str, bb.str); };
if (k == N_TPTR) { return typeeqast(aa.lhs, bb.lhs); };
if (k == N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); };
if (k == N_TCHAN) { return typeeqast(aa.lhs, bb.lhs); };
// Conservative: anything else (struct/fn/tagged/tuple/array)
// fails the cheap check. Selfhost code doesn't currently rely
// on equality at these shapes for the targeted checks.
return false;
};
// varianterr — does this variant carry the `!` mark? Either
// the variant itself is N_TBANG or it's an alias whose typedecl
// body is `!T`. Mirrors C check.c's iserror-after-NAMED rule.
fn varianterr(c: *checker, v: *node) bool = {
if (v == nil) { return false; };
if (v.kind == N_TBANG) { return true; };
if (v.kind == N_TNAME) {
let s: *sym = scopelookup(c.cur, v.str);
if (s != nil) {
if (s.skind == SK_TYPE) {
if (s.decl != nil) {
if (s.decl.lhs != nil) {
if (s.decl.lhs.kind == N_TBANG) {
return true;
};
};
};
};
};
};
return false;
};
// taggedhaserr — true iff any variant of `n` (assumed
// N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over
// the legacy "first variant = success" rule.
fn taggedhaserr(c: *checker, n: *node) bool = {
let v: *node = n.list;
for (v != nil) {
if (varianterr(c, v)) { return true; };
v = v.next;
};
return false;
};
// iserrvariant — under flag-aware mode (any !-marked variant),
// returns true iff `v` is `!`-marked. Under legacy mode (no flags),
// returns true iff `v` is not the first variant of `tagged`.
fn iserrvariant(c: *checker, tagged: *node, v: *node) bool = {
if (taggedhaserr(c, tagged)) {
return varianterr(c, v);
};
// Legacy: first variant of the union is success.
if (tagged.list == v) { return false; };
return true;
};
// scruttype — resolve the type expression for a match's
// scrutinee. Handles N_IDENT (look up local/param's declared
// type) and N_DOT (struct-field access). Returns nil if we
// can't statically determine the type. Used by exhaustiveness.
fn scruttype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == N_IDENT) {
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
// For N_LET / N_PARAM: declared type is decl.lhs.
return s.decl.lhs;
};
return nil;
};
// mktname — fabricate an N_TNAME node with str = `nm`. Used by
// exprtype to return primitive type nodes for literal
// expressions. The arena keeps them around as long as the checker.
fn mktname(c: *checker, nm: str) *node = {
let n: *node = newnode(c.a, N_TNAME, "", 0, 0);
n.str = nm;
return n;
};
// exprtype — best-effort type-AST inference for an expression
// node. Handles literals, identifiers, calls, and casts; returns
// nil for shapes we don't statically know (binary ops, struct
// field access into non-primitive types, etc).
fn exprtype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
let k: i32 = e.kind;
if (k == N_INTLIT) { return mktname(c, "untyped_int"); };
if (k == N_FLOATLIT) { return mktname(c, "untyped_float"); };
if (k == N_STRLIT) { return mktname(c, "str"); };
if (k == N_RUNELIT) { return mktname(c, "rune"); };
if (k == N_TRUE) { return mktname(c, "bool"); };
if (k == N_FALSE) { return mktname(c, "bool"); };
if (k == N_VOIDLIT) { return mktname(c, "void"); };
if (k == N_NIL) { return mktname(c, "untyped_nil"); };
if (k == N_IDENT) {
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
if (k == N_CAST) {
// `expr: T` — explicit cast; the type expr is e.rhs.
return e.rhs;
};
if (k == N_CALL) {
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == N_IDENT) { nm = callee.str; };
if (callee.kind == N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs; // fn-decl's lhs is the return type
};
if (k == N_TRYPROP) {
// success unwrap: the success-variant type of operand's
// tagged union.
let opt: *node = exprtype(c, e.lhs);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != N_TTAGGED) { return nil; };
// Hare semantics: success = first non-error variant if
// any !-flag is present; else first variant.
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) { return v; };
v = v.next;
};
return nil;
};
return ou.list;
};
if (k == N_TYPEASSERT) {
// `e as T` → T
return e.rhs;
};
if (k == N_TYPETEST) {
// `e is T` → bool
return mktname(c, "bool");
};
return nil;
};
// isuntypedint / is_str_like / is_bool_like — helpers used
// by the assignability check below to allow common AST shapes
// through without needing real type inference.
fn isuntypedint(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TNAME) { return false; };
return streq(t.str, "untyped_int");
};
fn isuntypedfloat(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TNAME) { return false; };
return streq(t.str, "untyped_float");
};
fn isuntypednil(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TNAME) { return false; };
return streq(t.str, "untyped_nil");
};
fn isnumerictname(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "rune")) { return true; };
if (streq(s, "f32")) { return true; };
if (streq(s, "f64")) { return true; };
return false;
};
fn isstrtname(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TNAME) { return false; };
return streq(t.str, "str");
};
// isassignable — AST-level approximation of C check.c
// type_assignable. Returns true when we know the assignment is
// OK, false only when we're confident it isn't, and "skip" (true)
// when we can't tell — to avoid false positives. The trailing bool
// `confident` lets the caller decide whether to emit an error
// when the result is false: if !confident, the caller should not
// flag it.
fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
*confident = false;
if (dst == nil) { return true; }; // no declared target
if (src == nil) { return true; }; // unknown src type
*confident = true;
let du: *node = resolvealias(c, unwrapbang(dst));
let su: *node = resolvealias(c, unwrapbang(src));
if (du == nil) { *confident = false; return true; };
if (su == nil) { *confident = false; return true; };
if (typeeqast(du, su)) { return true; };
// untyped numeric → any numeric named type.
if (isuntypedint(su)) {
if (isnumerictname(du)) { return true; };
// (T | ...) tagged: only OK if some variant accepts untyped_int.
if (du.kind == N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
let vu: *node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (isnumerictname(vu)) { return true; };
};
v = v.next;
};
*confident = false;
return true;
};
// Known non-numeric primitive: confidently wrong.
if (du.kind == N_TNAME) {
if (streq(du.str, "bool")) { return false; };
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
};
if (isuntypedfloat(su)) {
if (isnumerictname(du)) { return true; };
if (du.kind == N_TNAME) {
if (streq(du.str, "bool")) { return false; };
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
*confident = false;
return true;
};
if (isuntypednil(su)) {
// nil → ptr/slice/chan/fn/nullable
if (du.kind == N_TPTR) { return true; };
if (du.kind == N_TSLICE) { return true; };
if (du.kind == N_TCHAN) { return true; };
if (du.kind == N_TFN) { return true; };
// nullable `(*T | void)` — already accepted by typeeqast
// when matched whole; nil is OK there too.
if (du.kind == N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
if (v.kind == N_TPTR) { return true; };
if (v.kind == N_TSLICE){ return true; };
v = v.next;
};
};
*confident = false;
return true;
};
// Tagged-union variant inclusion: src is one of dst's variants.
if (du.kind == N_TTAGGED && su.kind != N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
let vu: *node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (typeeqast(vu, su)) { return true; };
};
v = v.next;
};
return false;
};
// tagged → tagged: structural variant list compare. Skip
// (don't be confident) — common when forwarding a fallible
// return through another fn with the same shape but possibly
// a different surface spelling.
if (du.kind == N_TTAGGED && su.kind == N_TTAGGED) {
*confident = false;
return true;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == N_TNAME && su.kind == N_TNAME) {
let known_d: bool = isnumerictname(du) || isstrtname(du);
if (!known_d) { if (streq(du.str, "bool")) { known_d = true; }; };
if (!known_d) { if (streq(du.str, "void")) { known_d = true; }; };
let known_s: bool = isnumerictname(su) || isstrtname(su);
if (!known_s) { if (streq(su.str, "bool")) { known_s = true; }; };
if (!known_s) { if (streq(su.str, "void")) { known_s = true; }; };
if (known_d) {
if (known_s) {
// Both primitives, different names → no.
return false;
};
};
};
// Anything else: don't claim confidence.
*confident = false;
return true;
};
// ---- match exhaustiveness --------------------------------------------
//
// For every match arm, verify that every variant of the scrutinee's
// tagged-union type is handled by some case (or a default arm
// exists). Multi-pattern `case A | B =>` covers all alts.
fn casecovers(c: *checker, cs: *node, want: *node) bool = {
if (cs.lhs != nil) {
if (typeeqast(cs.lhs, want)) { return true; };
};
let alt: *node = cs.list;
for (alt != nil) {
if (typeeqast(alt, want)) { return true; };
alt = alt.next;
};
return false;
};
fn errmatchvariant(c: *checker, n: *node, vname: *node) void = {
os.write(2, "match: variant not handled".ptr, 26u64);
if (vname != nil) {
if (vname.kind == N_TNAME) {
os.write(2, " (".ptr, 2u64);
os.write(2, vname.str.ptr, vname.str.len: u64);
os.write(2, ")".ptr, 1u64);
};
};
os.write(2, "\n".ptr, 1u64);
c.errs += 1;
};
// casevariantin — true iff `pat` (a `case T` pattern, including
// each alt of a multi-pattern) names a variant of the tagged
// union `tagged`.
fn casevariantin(tagged: *node, pat: *node) bool = {
let v: *node = tagged.list;
for (v != nil) {
if (typeeqast(v, pat)) { return true; };
v = v.next;
};
return false;
};
fn errbadcase(c: *checker, pat: *node) void = {
os.write(2, "case: not a variant of scrutinee".ptr, 32u64);
if (pat != nil) {
if (pat.kind == N_TNAME) {
os.write(2, " (".ptr, 2u64);
os.write(2, pat.str.ptr, pat.str.len: u64);
os.write(2, ")".ptr, 1u64);
};
};
os.write(2, "\n".ptr, 1u64);
c.errs += 1;
};
fn checkmatchexhaust(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
let st: *node = scruttype(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != N_TTAGGED) { return; };
// Validity: every `case T` pattern (and multi-pattern alts)
// must name a variant of u. Catches typos and dead arms that
// the dispatch would never reach.
let cs0: *node = n.list;
for (cs0 != nil) {
if (cs0.lhs != nil) {
if (!casevariantin(u, cs0.lhs)) {
errbadcase(c, cs0.lhs);
};
let alt: *node = cs0.list;
for (alt != nil) {
if (!casevariantin(u, alt)) {
errbadcase(c, alt);
};
alt = alt.next;
};
};
cs0 = cs0.next;
};
// Default arm absorbs anything; skip exhaustiveness.
let cs: *node = n.list;
for (cs != nil) {
if (cs.lhs == nil) { return; }; // default
cs = cs.next;
};
// For each variant of u, look for a covering case.
let v: *node = u.list;
for (v != nil) {
let covered: bool = false;
let cs2: *node = n.list;
for (cs2 != nil) {
if (casecovers(c, cs2, v)) {
covered = true;
cs2 = nil;
} else {
cs2 = cs2.next;
};
};
if (!covered) { errmatchvariant(c, n, v); };
v = v.next;
};
};
// ---- let init / return assignability --------------------------------
//
// AST-level approximation: when we can infer src's type and dst is
// explicitly declared, verify isassignable. We only emit an error
// when isassignable says "false with confidence." If we can't tell
// (binary ops, complex exprs we don't infer), we stay quiet — full
// type inference lives only on the C side.
fn errnotassign(c: *checker, dst: *node, src: *node, where: str) void = {
os.write(2, where.ptr, where.len: u64);
os.write(2, ": not assignable".ptr, 16u64);
if (src != nil) {
if (src.kind == N_TNAME) {
os.write(2, " (".ptr, 2u64);
os.write(2, src.str.ptr, src.str.len: u64);
os.write(2, " → ".ptr, 5u64);
if (dst != nil) {
if (dst.kind == N_TNAME) {
os.write(2, dst.str.ptr, dst.str.len: u64);
};
};
os.write(2, ")".ptr, 1u64);
};
};
os.write(2, "\n".ptr, 1u64);
c.errs += 1;
};
fn checkletassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; }; // no declared type, nothing to check
if (n.rhs == nil) { return; }; // no init
let src: *node = exprtype(c, n.rhs);
if (src == nil) { return; }; // can't infer
let conf: bool = false;
let ok: bool = isassignable(c, n.lhs, src, &conf);
if (!conf) { return; };
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
};
fn checkretassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) {
// bare `return;` — OK iff fnret is void or a tagged union
// with a void variant. Skip flagging for now; cgen handles
// the void-variant tag synthesis already.
return;
};
if (c.fnret == nil) { return; };
let src: *node = exprtype(c, n.lhs);
if (src == nil) { return; };
let conf: bool = false;
let ok: bool = isassignable(c, c.fnret, src, &conf);
if (!conf) { return; };
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
};
// ---- is / as validity ------------------------------------------------
//
// `e is T` and `e as T` require that e's declared type be a tagged
// union and that T name one of its variants. Operates on AST type
// expressions; falls back silently when we can't determine e's
// type (matches the case-variant rule for match).
fn checkisas(c: *checker, n: *node) void = {
if (n == nil) { return; };
// e is in n.lhs (value), T is in n.rhs (type expr).
let st: *node = scruttype(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != N_TTAGGED) {
os.write(2, "is/as: operand is not a tagged union\n".ptr, 37u64);
c.errs += 1;
return;
};
let want: *node = n.rhs;
if (want == nil) { return; };
if (!casevariantin(u, want)) {
os.write(2, "is/as: not a variant of operand".ptr, 31u64);
if (want.kind == N_TNAME) {
os.write(2, " (".ptr, 2u64);
os.write(2, want.str.ptr, want.str.len: u64);
os.write(2, ")".ptr, 1u64);
};
os.write(2, "\n".ptr, 1u64);
c.errs += 1;
};
};
// ---- ? subset propagation --------------------------------------------
//
// For `expr?`, the operand's error subset must be a subset of the
// enclosing fn's return-type variants. Mirrors C check.c. Operand
// is N_TRYPROP; its lhs is the value-bearing expr; we look at the
// expr's *declared* type for N_IDENT/N_CALL cases.
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == N_IDENT) {
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
if (e.kind == N_CALL) {
// callee return type lookup: callee is e.lhs (N_IDENT or
// N_DOT). We need the fn-decl's lhs (return-type AST).
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == N_IDENT) { nm = callee.str; };
if (callee.kind == N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
return nil;
};
fn checktryprop(c: *checker, n: *node) void = {
if (n == nil) { return; };
let t: *node = exprtypeoftry(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(t));
if (u == nil) { return; };
if (u.kind != N_TTAGGED) { return; };
// Does the operand have any error variants?
let haserr: bool = false;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; };
v = v.next;
};
if (!haserr) { return; };
// Enclosing fn must return a tagged union with each operand
// error variant present.
let r: *node = resolvealias(c, unwrapbang(c.fnret));
if (r == nil) {
os.write(2, "?: enclosing fn has no tagged-union return\n".ptr, 43u64);
c.errs += 1;
return;
};
if (r.kind != N_TTAGGED) {
os.write(2, "?: enclosing fn return is not tagged\n".ptr, 37u64);
c.errs += 1;
return;
};
let ev: *node = u.list;
for (ev != nil) {
if (iserrvariant(c, u, ev)) {
let found: bool = false;
let rv: *node = r.list;
for (rv != nil) {
if (typeeqast(rv, ev)) {
found = true;
rv = nil;
} else { rv = rv.next; };
};
if (!found) {
os.write(2, "?: error variant not in enclosing return\n".ptr, 41u64);
c.errs += 1;
};
};
ev = ev.next;
};
};
// install_param — when entering a fn body, define its params in a
// fresh local scope.
fn installparams(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) {
scopedefine(c.cur, nm, SK_PARAM, nil, p);
};
};
p = p.next;
};
};
// resolvefnbody — 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 resolvefnbody(c: *checker, fnnode: *node) void = {
let outer: *scope = c.cur;
c.cur = newscope(c.a, c.cur);
installparams(c, fnnode.list);
let prevret: *node = c.fnret;
c.fnret = fnnode.lhs; // return type AST, used by `?` check
if (fnnode.body != nil) {
resolvewalk(c, fnnode.body);
};
c.fnret = prevret;
c.cur = outer;
};
export fn checkinit(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;
c.fnret = nil;
seedprimitives(c);
};
export fn checkfile(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) {
installdecl(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) { resolvewalk(c, d.lhs); }; // return type
resolvefnbody(c, d);
} else { if (k == N_DEF) {
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
if (d.rhs != nil) { resolvewalk(c, d.rhs); };
} else { if (k == N_TYPEDECL) {
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
} else { if (k == N_LET) {
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
if (d.rhs != nil) { resolvewalk(c, d.rhs); };
};};};};
d = d.next;
};
};
// MODULE: wcc
// selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww.
//
// General helpers used across cgenexpr / cgenstmt / cgendecl:
// - pushargsrev: per-call arg pushing
// - type predicates: isstr*/isslice*/istagged*/nodeis* families
// - field ops: fieldloadop, fieldstoreop
// - index helpers: indexbaseesz, dotinnerstructptr, elemsizeof
// - slot sizing: structlookup, primsize, slotsize, fieldsize,
// registerstruct, collectstructs
// - rhs helpers: rhstargetname, taggedvariantindex
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgenutil;` directly.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
// ---- expression cgen -------------------------------------------------
// pushargsrev — 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 pushargsrev(c: *cgen, arg: *node) i32 = {
if (arg == nil) { return 0; };
let rest: i32 = pushargsrev(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 baselocal: *local = nil;
if (base != nil) {
if (base.kind == N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
};
};
// base address → push
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == N_TARRAY) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else {
cgexpr(c, base);
};
emitline("\tPUSHQ\tAX\n");
// hi (default base length) → push
if (hi != nil) {
cgexpr(c, hi);
} else { if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == N_TARRAY) {
let lenn: *node = tn.rhs;
if (lenn != nil) {
if (lenn.kind == N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
};
};
} else { if (tn.kind == N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
} else { if (tn.kind == N_TNAME) {
if (streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
};
};};};
};
} else {
emitline("\tMOVQ\t$0, AX\n");
};};
emitline("\tPUSHQ\tAX\n");
// lo (default 0) → AX
if (lo != nil) { cgexpr(c, lo); }
else { emitline("\tMOVQ\t$0, AX\n"); };
emitline("\tPOPQ\tBX\n"); // hi
emitline("\tPOPQ\tCX\n"); // base
emitline("\tMOVQ\tBX, DX\n"); // DX = hi
emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len
emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr
emitline("\tPUSHQ\tDX\n"); // cap
emitline("\tPUSHQ\tDX\n"); // len
emitline("\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 = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
};
};
cgexpr(c, arg);
if (nodeisslice(c, arg)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
if (nodeisstr(c, arg)) {
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 2;
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
fn nodeisslice(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 = localfindnode(c, nm);
if (lc != nil) { return isslicetype(c, lc.tnode); };
return false;
};
if (k == N_SLICE) { return true; };
return false;
};
// nodeisstr — 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 nodeisstr(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 = localfindnode(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 = fnretlookup(c, cnm);
return isstrtype(c, rt);
};
};
return false;
};
if (k == N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
// `<expr>.ptr` is *u8 not str; `<expr>.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 = localfindnode(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 dotinnerstructptr
// to resolve the inner chain to the *struct it lands
// on, then look up `fld` in that struct.
if (base.kind == N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
if (innert.kind == N_TNAME) { sname = innert.str; };
};
};
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return isstrtype(c, fi.tnode);
};
fi = fi.finext;
};
};
};
};
return false;
};
if (k == N_CAST) {
return isstrtype(c, n.rhs);
};
return false;
};
// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr.
fn typenameisunsigned(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;
};
// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc.
fn typenodeisunsigned(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == N_TNAME) { return typenameisunsigned(t.str); };
return false;
};
// typeis8byteprimitive — 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 typeis8byteprimitive(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 (structlookup(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 (primsize(nm) > 0) { return true; };
return false;
};
return false;
};
// typenameissigned — true for i8/i16/i32/i64/int/rune.
fn typenameissigned(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;
};
// fieldloadop — 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 fieldloadop(f: *fieldinfo) 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 (typenameissigned(t.str)) { return "MOVSXD"; };
};
};
return "MOVL";
};
return "MOVQ";
};
// fieldstoreop — pick the store instruction for a non-str struct
// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8.
fn fieldstoreop(f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVB"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// indexbaseesz — 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 indexbaseesz(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 = localfindnode(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 innert: *node = tn;
if (tn.kind == N_TPTR) { innert = tn.lhs; };
if (innert == nil) { return 8; };
if (innert.kind == N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
};
if (innert.kind == N_TSLICE) { return elemsizeof(innert); };
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: *structinfo = structlookup(c, sname);
if (si == nil) { return 8; };
let fi: *fieldinfo = 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 = primsize(elem.str);
if (ps > 0) { return ps; };
};
};
return 8;
};
if (ft.kind == N_TSLICE) { return elemsizeof(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;
};
// dotinnerstructptr — 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 dotinnerstructptr(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 baset: *node = nil;
if (base.kind == N_IDENT) {
let lc: *local = localfindnode(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) { baset = tn; };
if (tn.kind == N_TPTR) { baset = tn.lhs; };
} else { if (base.kind == N_DOT) {
baset = dotinnerstructptr(c, base);
};};
if (baset == nil) { return nil; };
if (baset.kind != N_TNAME) { return nil; };
// Look up the struct, find the field, return the field's *struct.
let si: *structinfo = structlookup(c, baset.str);
if (si == nil) { return nil; };
let fi: *fieldinfo = 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;
};
// elemsizeof — 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 elemsizeof(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 = primsize(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). primsize returns 0 for it.
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
};
return 8;
};
// nodeisunsigned — 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 nodeisunsigned(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 = localfindnode(c, nm);
if (lc != nil) { return typenodeisunsigned(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 = localfindnode(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: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return typenodeisunsigned(fi.tnode);
};
fi = fi.finext;
};
};
};
};
};
};
return false;
};
if (k == N_CAST) { return typenodeisunsigned(n.rhs); };
if (k == N_BIN) {
if (nodeisunsigned(c, n.lhs)) { return true; };
return nodeisunsigned(c, n.rhs);
};
if (k == N_UN) { return nodeisunsigned(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 w6c on byte indexing.
if (k == N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == N_IDENT) {
let lc: *local = localfindnode(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 typenodeisunsigned(elem);
};
};
};
};
};
return false;
};
return false;
};
// ---- type-driven slot sizing ----------------------------------------
fn structlookup(c: *cgen, name: str) *structinfo = {
let s: *structinfo = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (streq(sn, name)) { return s; };
s = s.sinext;
};
return nil;
};
// primsize — size in bytes of a primitive type name (or 0 if not
// recognised as a primitive — the caller falls back to other paths).
// fldnumidx — parse a tuple field name like "0" / "1" / "12" into an
// index, or -1 if not all-digits. Used by cgdot to dispatch
// `t.0` / `t.1` against an N_TTUPLE local without pulling in strconv.
fn fldnumidx(s: str) i32 = {
if (s.len == 0) { return -1; };
let r: i32 = 0;
let i: i32 = 0;
for (i < s.len) {
let b: u8 = s[i];
if (b < 48u8) { return -1; };
if (b > 57u8) { return -1; };
r = r * 10 + ((b - 48u8): i32);
i += 1;
};
return r;
};
fn primsize(name: str) i32 = {
if (streq(name, "u8")) { return 1; };
if (streq(name, "i8")) { return 1; };
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;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;
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) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p);
p = p.next;
};
return total;
};
if (k == N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == N_TNAME) {
let nm: str = typn.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(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: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
return 8;
};
if (k == N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let en: str = elemn.str;
let ps: i32 = primsize(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 = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
};
return 8;
};
// registerstruct — compute field offsets + total size for a struct
// type-decl, store in c.structs. Field type sizes use the same
// slotsize logic (with primitives kept at their natural width — we
// only round to 8 for stack slots, not struct interiors).
fn fieldsize(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 = primsize(nm);
if (ps > 0) { return ps; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
return 8;
};
if (k == N_TPTR) { return 8; };
if (k == N_TSLICE) { return 24; };
if (k == N_TARRAY) {
// Same shape as slotsize's TARRAY branch.
let lenn: *node = tnode.rhs;
let elemn: *node = tnode.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = fieldsize(c, elemn);
return (esz: i64 * elen): i32;
};
return 8;
};
fn registerstruct(c: *cgen, name: str, tstruct: *node) void = {
let si: *structinfo = amalloc(c.a, 64u64): *structinfo;
si.sname = name;
si.fields = nil;
si.totsize = 0;
let head: *fieldinfo = nil;
let tail: *fieldinfo = nil;
let off: i32 = 0;
let f: *node = tstruct.list;
for (f != nil) {
if (f.kind == N_TFIELD) {
let sz: i32 = fieldsize(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: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo;
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.totsize = off;
si.sinext = c.structs;
c.structs = si;
};
fn collectstructs(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) {
registerstruct(c, d.str, body);
};
};
};
d = d.next;
};
};
// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the
// alias chain registered at file load.
fn isstrtyperaw(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 isstrtype(c: *cgen, t: *node) bool = {
if (isstrtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isstrtyperaw(r);
};
fn isslicetyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == N_TSLICE) { return true; };
return false;
};
fn isslicetype(c: *cgen, t: *node) bool = {
if (isslicetyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isslicetyperaw(r);
};
fn istaggedtype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == N_TTAGGED) { return true; };
return false;
};
// isnullabletype — N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == N_TPTR);
let bptr: bool = (b.kind == N_TPTR);
let avoid: bool = (a.kind == N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == N_TPTR) { return 0; }; };
return 1;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// rhstargetname — 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 rhstargetname(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 = localfindnode(c, rhs.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == N_TNAME) { return tn.str; };
};
};
};
return nm;
};
// taggedvariantindex — 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 taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
if (tagged == nil) { return -1; };
if (rhs == nil) { return -1; };
let wantname: str = rhstargetname(c, rhs);
if (wantname.len > 0) {
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, wantname)) { return idx; };
};
v = v.next;
idx += 1;
};
};
// Fallback: by str-shape (resolves aliases).
let wantstr: bool = nodeisstr(c, rhs);
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
let visstr: bool = false;
if (v.kind == N_TNAME) {
if (isstrtype(c, v)) { visstr = true; };
};
if (visstr == wantstr) { return idx; };
v = v.next;
idx += 1;
};
return -1;
};
// MODULE: wcc
// selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww.
//
// cgexpr is a thin dispatcher over n.kind; each non-trivial branch
// lives in a per-kind helper (cgstrlit, cgident, cgindex, cgmatch,
// cgdot, cgun, cgbin, cgcall, cgassign). Trivial literal loads
// (N_INTLIT, N_RUNELIT, N_TRUE/FALSE/NIL, N_CAST) stay inline.
//
// The remainder of cgen lives in cgen.ww (foundation: types, emit
// primitives, the collect* tables, FFI/module maps) and cgenstmt.ww
// (cgstmt).
//
// `use cgenexpr;` is unnecessary at consumer sites — cgen.ww imports
// this file, so any caller of cgen transitively gets cgexpr.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
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.
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
};
if (k == N_RUNELIT) {
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
};
if (k == N_STRLIT) { cgstrlit(c, n); return; };
if (k == N_TRUE) {
emitline("\tMOVQ\t$1, AX\n");
return;
};
if (k == N_FALSE) {
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == N_NIL) {
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == N_VOIDLIT) {
// void value: zero-size, but the consumer's ABI expects a
// deterministic AX. Emit 0 like nil/false do.
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == N_IDENT) { cgident(c, n); return; };
if (k == N_INDEX) { cgindex(c, n); return; };
if (k == N_MATCH) { cgmatch(c, n); 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) { cgdot(c, n); return; };
if (k == N_UN) { cgun(c, n); return; };
if (k == N_BIN) { cgbin(c, n); return; };
if (k == N_CALL) { cgcall(c, n); return; };
if (k == N_ASSIGN) { cgassign(c, n); return; };
if (k == N_TRYPROP) { cgtryprop(c, n); return; };
if (k == N_TRYUNW) { cgtryunw(c, n); return; };
if (k == N_TYPETEST) { cgtypetest(c, n); return; };
if (k == N_TYPEASSERT) { cgtypeassert(c, n); return; };
};
// cgtagvariantidx — find the 0-based variant index of `vt` inside the
// tagged-union type expression `tagged`. -1 if `tagged` isn't an
// N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch
// does inline; pulled out so `is` / `as` can reuse it.
fn cgtagvariantidx(tagged: *node, vt: *node) i32 = {
if (tagged == nil) { return -1; };
if (vt == nil) { return -1; };
if (tagged.kind != N_TTAGGED) { return -1; };
let want: str;
want.ptr = nil; want.len = 0;
if (vt.kind == N_TNAME) { want = vt.str; };
if (want.len == 0) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, want)) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of
// the codegen expects the success value in AX (and BX for str).
// AX=tag, DX=val0, CX=val1 from the call ABI. For str success,
// shuffle (DX,CX) → (AX,BX); else move DX → AX.
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != nil) {
let cname: str;
cname.ptr = nil; cname.len = 0;
if (callee.kind == N_IDENT) { cname = callee.str; };
if (callee.kind == N_DOT) { cname = callee.str; };
if (cname.len > 0) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(cl);
// Unwrap success value. (Same shuffle pattern as cgtryprop.)
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != nil) {
let cname: str;
cname.ptr = nil; cname.len = 0;
if (callee.kind == N_IDENT) { cname = callee.str; };
if (callee.kind == N_DOT) { cname = callee.str; };
if (cname.len > 0) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
fn cgtypetest(c: *cgen, n: *node) void = {
// `e is T` — load the lhs's tag, compare against T's variant
// index, set AX = (tag == idx). Result type is bool.
//
// Slot resolution is inlined (rather than factored into a helper
// with output parameters): wwstage cgen has a trap with i32
// stored via *i32 in this context — direct assignment of the
// local works, indirection through &scrutoff drops sign bits.
let lhs: *node = n.lhs;
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetype(c, lc.tnode);
};
};
};
let want: i32 = cgtagvariantidx(scrutt, n.rhs);
if (want < 0) { want = 0; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
let nel: str = mklabel(c, "is_ne");
let dnl: str = mklabel(c, "is_done");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nel);
emitline("\n\tMOVQ\t$1, AX\n\tJMP\t");
emitline(dnl);
emitline("\n");
emitlabel(nel);
emitline("\tMOVQ\t$0, AX\n");
emitlabel(dnl);
return;
};
// isenumexpr — does this expression's static type resolve to an enum?
// Recognises enum-member access (`Foo.MEMBER`), enum-typed local
// idents, and N_BIN whose either operand is enum (so `R | W` flows
// through the cast pass-through too).
fn isenumexpr(c: *cgen, e: *node) bool = {
if (e == nil) { return false; };
let k: i32 = e.kind;
if (k == N_DOT) {
if (e.lhs != nil) {
if (e.lhs.kind == N_IDENT) {
if (enumlookup(c, e.lhs.str) != nil) { return true; };
};
};
};
if (k == N_IDENT) {
let lc: *local = localfindnode(c, e.str);
if (lc != nil) {
if (lc.tnode != nil) {
if (lc.tnode.kind == N_TNAME) {
if (enumlookup(c, lc.tnode.str) != nil) { return true; };
};
};
};
};
if (k == N_BIN) {
if (isenumexpr(c, e.lhs)) { return true; };
if (isenumexpr(c, e.rhs)) { return true; };
};
if (k == N_UN) {
if (isenumexpr(c, e.lhs)) { return true; };
};
return false;
};
fn isenumtype(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == N_TENUM) { return true; };
if (t.kind == N_TNAME) {
if (enumlookup(c, t.str) != nil) { return true; };
};
return false;
};
fn cgtypeassert(c: *cgen, n: *node) void = {
// Enum ↔ integer: reinterpret-only. The LHS value already
// occupies AX (or AX:BX for str variants, irrelevant here);
// no tag/unwrap. Matches cmd/w6c/cgen.c's same short-circuit.
if (isenumexpr(c, n.lhs) || isenumtype(c, n.rhs)) {
cgexpr(c, n.lhs);
return;
};
// `e as T` — load tag, abort (exit 1) if tag != T's variant
// index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B
// str → (AX, BX). Mirrors cgmatch's slot-based value load.
// Slot resolution inlined; see cgtypetest comment.
let lhs: *node = n.lhs;
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetype(c, lc.tnode);
};
};
};
let want: i32 = cgtagvariantidx(scrutt, n.rhs);
if (want < 0) { want = 0; };
let okl: str = mklabel(c, "asrt_ok");
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJE\t");
emitline(okl);
emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(okl);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
if (isstrtype(c, n.rhs)) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), BX\n");
};
return;
};
fn cgstrlit(c: *cgen, n: *node) void = {
// 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 = internstrlit(c, nstr);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", BX\n");
return;
};
fn cgident(c: *cgen, n: *node) void = {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
// str local: also load the len half into BX.
if (isstrtype(c, lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
};
// slice local: load (ptr, len, cap) into (AX, BX, CX).
if (isslicetype(c, lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), CX\n");
};
return;
};
// Top-level `def` constant — load from its DATA symbol.
if (deflookup(c, nm)) {
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(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. The
// emitsymname helper handles ffiresolve and module-mangling
// in one go, so a body-less FFI binding emits the C symbol
// it was declared with via @symbol(), not the ww-side ident.
let rt: *node = fnretlookup(c, nm);
if (rt != nil) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
return;
};
fn cgindex(c: *cgen, n: *node) void = {
// 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 baselocal: *local = nil;
if (base != nil) {
if (base.kind == N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); };
} else { if (base.kind == N_DOT) {
esz = indexbaseesz(c, base);
};};
};
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == N_TARRAY) { isarray = true; }; };
if (isarray) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
emitline("\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) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
return;
};
if (esz == 1) { emitline("\tMOVZBQ\t(BX), AX\n"); }
else { emitline("\tMOVQ\t(BX), AX\n"); };
return;
};
// Generic fallback when base isn't a plain ident.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
if (esz == 16) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
return;
};
if (esz == 1) { emitline("\tMOVZBQ\t(AX), AX\n"); }
else { emitline("\tMOVQ\t(AX), AX\n"); };
return;
};
fn cgmatch(c: *cgen, n: *node) void = {
// 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 scrutoff: i32 = 0;
let scrutt: *node = nil;
if (scrut != nil) {
if (scrut.kind == N_IDENT) {
let lc: *local = localfindnode(c, scrut.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetype(c, lc.tnode);
};
};
};
let endl: str = mklabel(c, "match_end");
// Push end label as the yield target for this match's arm bodies.
if (c.yieldtop < LOOP_MAX) {
c.yieldbuf[c.yieldtop] = endl;
c.yieldtop += 1;
};
let cs: *node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *node = cs.lhs;
let nullable: bool = isnullabletype(scrutt);
// Compute the variant tag for this arm. Default arm
// (no pattern) skips the tag check.
if (pat != nil) {
if (nullable) {
// Discriminator = pointer-vs-null.
// *T arm: skip if ptr == 0.
// void arm: skip if ptr != 0.
let ptr_tag: i32 = nullableptrtag(scrutt);
let cur_tag: i32 = 0;
if (pat.kind == N_TPTR) { cur_tag = ptr_tag; }
else { if (ptr_tag == 0) { cur_tag = 1; }; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (cur_tag == ptr_tag) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
emitline(nxt);
emitline("\n");
} else {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == N_TNAME) {
if (streq(v.str, patname)) {
want = idx;
found = true;
v = nil;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
};
if (!found) { want = 0; };
};
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
};
// Bind `let v: T` from the slot, if requested.
let bn: str = cs.str;
if (bn.len > 0) {
if (pat != nil) {
if (nullable) {
// Bind the pointer (or skip for the
// void arm, which has zero-size). The
// value IS slot+0.
if (pat.kind == N_TPTR) {
let voff: i32 = localalloc(c, bn, 8, pat);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): 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 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitline("(BP)\n");
};
};
};
};
// Body. Match arms are statements; we cgstmt them.
if (cs.body != nil) { cgstmt(c, cs.body); };
emitline("\tJMP\t");
emitline(endl);
emitline("\n");
emitlabel(nxt);
cs = cs.next;
};
emitlabel(endl);
if (c.yieldtop > 0) { c.yieldtop -= 1; };
return;
};
fn cgdot(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
let fld: str = n.str;
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. With driver-side
// concatenation, both forms key off the leaf type name.
if (lhs != nil) {
let etname: str;
etname.ptr = nil; etname.len = 0;
if (lhs.kind == N_IDENT) {
etname = lhs.str;
};
if (lhs.kind == N_DOT) {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == N_IDENT) {
etname = lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookup(c, etname);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {
emitline("\tMOVQ\t$");
emitint(v: i64);
emitline(", AX\n");
return;
};
};
};
};
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
let nm: str = lhs.str;
let lc: *local = localfindnode(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: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = 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)).
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "BX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tCX, BX\n");
} else {
let op: str = fieldloadop(fi);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", 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: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = 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 (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 8): i64);
emitline("(BP), BX\n");
} else {
let op: str = fieldloadop(fi);
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitline("(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")) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), AX\n");
return;
};
if (streq(fld, "len")) {
let lenn: *node = tn.rhs;
let alen: i64 = 0i64;
if (lenn != nil) {
if (lenn.kind == N_INTLIT) { alen = lenn.uval: i64; };
};
emitline("\tMOVQ\t$");
emitint(alen);
emitline(", AX\n");
return;
};
};
// Hare-style tuple positional access: `t.0`, `t.1`.
// Walk the tuple element type list summing slotsize
// (matches the (scalar, str) init layout which puts
// the scalar in an 8B slot and the str in 16B). For
// a str element, load both halves into (AX, BX) so
// chains like `t.1.len` propagate correctly.
if (lkind == N_TTUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
let tp: *node = tn.list;
let foff: i32 = 0;
let i: i32 = 0;
for (i < idx) {
if (tp == nil) { i = idx; }
else {
foff += slotsize(c, tp);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
if (isstrtyperaw(tp)) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 8): i64);
emitline("(BP), BX\n");
return;
};
let sz: i32 = slotsize(c, tp);
let op: str = "MOVQ";
if (sz == 1) { op = "MOVZBQ"; }
else { if (sz == 4) { op = "MOVL"; }; };
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
};
};
// str/slice pseudo-fields .ptr/.len/.cap on a
// direct local: load at slot+delta.
let delta: i32 = -1;
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 innerkind: i32 = -1;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == N_TNAME) {
if (streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == N_TSLICE) { innerstr = true; };
if (innerstr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "BX");
emitline(", AX\n");
return;
};
};
emitline("\tMOVQ\t");
emitoff((lc.off + delta): i64);
emitline("(BP), AX\n");
return;
};
};
};
};
// `def NAME: str = "..."` field access — inline the literal.
// Sdef-backed strs aren't laid out in memory, so falling
// through to the SB-load fallback below would mis-emit
// `MOVQ <field>(SB), AX` (looking up the field name as a
// symbol). Mirrors cmd/w6c/cgen.c N_DOT off==0 / Sdef branch.
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
let drhs: *node = deflookuprhs(c, lhs.str);
if (drhs != nil) {
if (drhs.kind == N_STRLIT) {
let bytes: str = drhs.str;
if (streq(fld, "ptr")) {
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
return;
};
if (streq(fld, "len")) {
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", 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/tyerr.
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(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);
emitline("\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 w6l/pass.ww.)
if (lhs != nil) {
if (lhs.kind == N_DOT) {
let innert: *node = dotinnerstructptr(c, lhs);
if (innert != nil) {
let sname: str = innert.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
cgexpr(c, lhs); // AX = ptr to inner struct
let lop: str = fieldloadop(fi);
// str field: load both halves.
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
fi = fi.finext;
};
};
};
};
};
return;
};
fn cgun(c: *cgen, n: *node) void = {
// 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) { emitline("\tNEGQ\tAX\n"); return; };
if (n.op == TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; };
if (n.op == TK_STAR) { emitline("\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 = localfind(c, nm);
if (off != 0) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
return;
};
};
};
return;
};
if (n.op == TK_NOT) {
let t: str = mklabel(c, "tt");
let e: str = mklabel(c, "te");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(t); emitline("\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
return;
};
fn cgbin(c: *cgen, n: *node) void = {
let unsignd: bool = nodeisunsigned(c, n.lhs);
if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); };
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, n.lhs);
emitline("\tPOPQ\tBX\n");
if (n.op == TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; };
if (n.op == TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == TK_SLASH) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
return;
};
if (n.op == TK_PERCENT) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
emitline("\tMOVQ\tDX, AX\n");
return;
};
if (n.op == TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; };
if (n.op == TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; };
if (n.op == TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; };
if (n.op == TK_LSHIFT) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHLQ\tCX, AX\n");
return;
};
if (n.op == TK_RSHIFT) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHRQ\tCX, AX\n");
return;
};
if (n.op == TK_AND) { emitline("\tANDQ\tBX, AX\n"); return; };
if (n.op == TK_OR) { emitline("\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 iscmp: bool = false;
let jcc: str = "";
if (n.op == TK_EQ) { iscmp = true; jcc = "JE"; };
if (n.op == TK_NEQ) { iscmp = true; jcc = "JNE"; };
if (n.op == TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; };
if (n.op == TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; };
if (n.op == TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; };
if (n.op == TK_GE) { iscmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; };
if (iscmp) {
let t: str = mklabel(c, "ct");
let e: str = mklabel(c, "ce");
emitline("\tCMPQ\tBX, AX\n");
emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
return;
};
fn cgcall(c: *cgen, n: *node) void = {
let nargs: i32 = pushargsrev(c, n.list);
let i: i32 = 0;
for (i < nargs) {
emitline("\tPOPQ\t");
emitline(argregname(i));
emitline("\n");
i += 1;
};
let callee: *node = n.lhs;
let calleename: str;
calleename.ptr = nil; calleename.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 isfnptrcall: bool = false;
if (callee != nil) {
if (callee.kind == N_IDENT) {
let cn: str = callee.str;
if (localfindnode(c, cn) != nil) {
isfnptrcall = 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 = localfindnode(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: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = 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) {
isfnptrcall = true;
};
};
fi = nil;
} else {
fi = fi.finext;
};
};
};
};
};
};
};
};
};
};
if (isfnptrcall) {
// 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);
emitline("\tCALL\tAX\n");
} else {
emitline("\tCALL\t");
if (callee != nil) {
if (callee.kind == N_IDENT) {
calleename = callee.str;
emitsymname(c, calleename);
} else { if (callee.kind == N_DOT) {
calleename = callee.str;
emitsymname(c, calleename);
};};
};
emitline("(SB)\n");
};
// SysV returns 16-byte aggregates in (AX, DX). Our str
// convention is (AX, BX), so shuffle for str-returning calls.
if (calleename.len > 0) {
let rt: *node = fnretlookup(c, calleename);
if (isstrtype(c, rt)) {
emitline("\tMOVQ\tDX, BX\n");
};
};
return;
};
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
// 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 elemstr: bool = false;
let storeop: str = "MOVQ";
if (inner != nil) {
if (inner.kind == N_IDENT) {
let lc: *local = localfindnode(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")) { elemstr = true; }
else {
let ps: i32 = primsize(pe.str);
if (ps == 1) { storeop = "MOVB"; }
else { if (ps == 4) { storeop = "MOVL"; }; };
};
};
};
};
};
};
};
};
cgexpr(c, n.rhs);
// Push order matches C cgen
// (cmd/w6c/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).
emitline("\tPUSHQ\tAX\n");
if (elemstr) { emitline("\tPUSHQ\tBX\n"); };
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
if (elemstr) {
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
return;
};
emitline("\tPOPQ\tAX\n");
emitline("\t");
emitline(storeop);
emitline("\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 baselocal: *local = nil;
if (base != nil) {
if (base.kind == N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) {
esz = elemsizeof(baselocal.tnode);
};
} else { if (base.kind == N_DOT) {
esz = indexbaseesz(c, base);
};};
};
cgexpr(c, n.rhs); // value → AX
if (esz == 16) { emitline("\tPUSHQ\tBX\n"); };
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx); // idx → AX
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
emitline("\tPUSHQ\tAX\n"); // scaled idx
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == N_TARRAY) { isarray = true; }; };
if (isarray) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
if (esz == 16) {
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
return;
};
if (esz == 1) { emitline("\tMOVB\tAX, (BX)\n"); }
else { emitline("\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 = localfindnode(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: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
if (n.op != TK_ASSIGN) {
// compound: load current value
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let lop: str = fieldloadop(fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
};
cgexpr(c, n.rhs);
if (n.op != TK_ASSIGN) {
emitline("\tPOPQ\tBX\n");
// PLUSEQ is commutative; MINUSEQ
// needs lhs - rhs (BX is old lhs,
// AX is rhs).
if (n.op == TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); };
if (n.op == TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\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 (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
fi = fi.finext;
};
};
};
};
// Direct struct local: store at off+foff.
if (lkind == N_TNAME) {
let sname: str = tn.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
cgexpr(c, n.rhs);
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((lc.off + fi.foff): i64);
emitline("(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 innerkind: i32 = -1;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == N_TNAME) {
if (streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == N_TSLICE) { innerstr = true; };
if (innerstr) {
if (n.op != TK_ASSIGN) {
// Compound on `(*str|*slice).field`: load
// current → push → eval rhs → combine → store.
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tPOPQ\tBX\n");
// PLUSEQ is commutative; MINUSEQ
// needs lhs - rhs.
if (n.op == TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); };
if (n.op == TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, AX\n");
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(delta: i64, "BX");
emitline("\n");
return;
};
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(delta: i64, "BX");
emitline("\n");
return;
};
};
cgexpr(c, n.rhs);
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + delta): i64);
emitline("(BP)\n");
return;
};
};
};
};
};
};
// Chained `<expr>.field = v` where `<expr>` itself is a chain
// of dots resolving to a *struct. Mirrors the C cgen branch
// added to close trap 1 (cmd/w6c/cgen.c). Without this, only
// `local.field = v` and `local.fieldptr.field = v` get wired
// (the latter through the IDENT-base branch above) — chains
// like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no
// store. Only plain `=` is wired here; chained compound on a
// pointer-field hasn't surfaced.
if (lhs != nil) {
if (lhs.kind == N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
let sname: str = innert.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
if (n.op == TK_ASSIGN) {
if (isstrtype(c, fi.tnode)) {
// str rhs: AX=ptr, BX=len.
// Stash both, then load
// the struct ptr into CX
// and write both halves.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, CX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
};
fi = fi.finext;
};
};
};
};
};
};
};
// Local-ident target — plain `=` and the simple compound
// forms (+= -= *= /=); other compounds fall back to
// "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path.
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
let nm: str = lhs.str;
let off: i32 = localfind(c, nm);
if (off == 0) { return; };
// Detect str-typed local — assignment must store both
// halves (AX=ptr at +0, BX=len at +8).
let lcstr: bool = false;
let lcn: *local = localfindnode(c, nm);
if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); };
cgexpr(c, n.rhs);
if (n.op == TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
if (lcstr) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
};
return;
};
if (n.op == TK_PLUSEQ) {
emitline("\tADDQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
if (n.op == TK_MINUSEQ) {
emitline("\tSUBQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
// Generic compound: load → combine in BX → store.
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
if (n.op == TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); };
if (n.op == TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); };
if (n.op == TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); };
if (n.op == TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); };
if (n.op == TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHLQ\tCX, BX\n");
};
if (n.op == TK_RSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n");
};
emitline("\tMOVQ\tBX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
};
return;
};
// MODULE: wcc
// selfhost/cmd/wcc/cgenstmt.ww — split out of cgen.ww.
//
// cgstmt is a thin dispatcher over n.kind; each branch defers to a
// per-kind helper: cgblock, cgreturn, cgexprstmt, cglet, cgif, cgfor,
// cgmassign, cgbreak, cgcontinue.
//
// The expression generator (cgexpr) lives in cgenexpr.ww; the
// foundation (types, emit primitives, collect* tables, FFI/module
// maps) lives in cgen.ww.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
// ---- statement cgen --------------------------------------------------
fn cgstmt(c: *cgen, n: *node) void = {
if (n == nil) { return; };
let k: i32 = n.kind;
if (k == N_BLOCK) { cgblock(c, n); return; };
if (k == N_RETURN) { cgreturn(c, n); return; };
if (k == N_EXPRSTMT) { cgexprstmt(c, n); return; };
if (k == N_LET) { cglet(c, n); return; };
if (k == N_IF) { cgif(c, n); return; };
if (k == N_FOR) { cgfor(c, n); return; };
if (k == N_MASSIGN) { cgmassign(c, n); return; };
if (k == N_MLET) { cgmlet(c, n); return; };
if (k == N_BREAK) { cgbreak(c, n); return; };
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
if (k == N_YIELD) { cgyield(c, n); return; };
if (k == N_DEFER) {
if (c.defertop < DEFER_MAX) {
c.deferbuf[c.defertop] = n.lhs;
c.defertop += 1;
};
return;
};
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
let s: *node = n.list;
for (s != nil) {
cgstmt(c, s);
s = s.next;
};
return;
};
// rundefers — emit cgexpr for every queued defer in LIFO order.
// Called from cgreturn and the cgfn implicit-return path.
fn rundefers(c: *cgen) void = {
let i: i32 = c.defertop - 1;
for (i >= 0) {
cgexpr(c, c.deferbuf[i]);
i -= 1;
};
return;
};
fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`:
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len.
// 24B convention mirrors the tagged-union return below; receive
// sites destructure off the same regs regardless of position.
if (rhs.kind == N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
let v2: *node = v.next;
if (v2 != nil) {
let v0_is_str: bool = nodeisstr(c, v);
let v1_is_str: bool = nodeisstr(c, v2);
if ((v0_is_str || v1_is_str) && !(v0_is_str && v1_is_str)) {
let strn: *node = v;
let scaln: *node = v2;
if (v1_is_str) { strn = v2; scaln = v; };
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
} else {
cgexpr(c, v2);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, v);
emitline("\tPOPQ\tDX\n");
};
} else {
cgexpr(c, v);
};
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 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.
// Nullable folded `(*T | void)`: just one word; AX is
// already the pointer (or 0). No shuffle, no tag.
if (istaggedtype(c.fnret)) {
cgexpr(c, rhs);
if (isnullabletype(c.fnret)) {
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let idx: i32 = taggedvariantindex(c, c.fnret, rhs);
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else {
emitline("\tMOVQ\tAX, DX\n");
};
emitline("\tMOVQ\t$");
if (idx < 0) { idx = 0; };
emitint(idx: i64);
emitline(", AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
cgexpr(c, rhs);
} else {
// Bare `return;` from a tagged-union-returning fn is
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c.fnret)) {
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
} else {
let idx: i32 = voidvariantindex(c.fnret);
if (idx < 0) { idx = 0; };
emitline("\tMOVQ\t$");
emitint(idx: i64);
emitline(", AX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
emitline("\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 (isstrtype(c, c.fnret)) {
emitline("\tMOVQ\tBX, DX\n");
};
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
fn cgexprstmt(c: *cgen, n: *node) void = {
if (n.lhs != nil) { cgexpr(c, n.lhs); };
c.lastwasreturn = 0;
return;
};
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(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 (istaggedtype(n.lhs)) {
let nullable: bool = isnullabletype(n.lhs);
let rhsreturnstagged: bool = false;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
if (callee.kind == N_IDENT) { calleename = callee.str; };
if (callee.kind == N_DOT) { calleename = callee.str; };
if (calleename.len > 0) {
let rt: *node = fnretlookup(c, calleename);
if (istaggedtype(rt)) { rhsreturnstagged = true; };
};
};
};
cgexpr(c, rhs);
if (nullable) {
// Slot is one 8B word; AX is the pointer
// (or 0 for null/void). Same path whether
// the rhs is a call or a bare variant.
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
if (rhsreturnstagged) {
// Spill size/8 registers (tag + value words).
// Slots smaller than 24 don't carry a CX word.
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
if (sz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
let tagidx: i32 = taggedvariantindex(c, n.lhs, rhs);
if (tagidx < 0) { tagidx = 0; };
if (nodeisstr(c, rhs)) {
emitline("\tMOVQ\tAX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
};
emitline("\tMOVQ\t$");
emitint(tagidx: i64);
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
c.lastwasreturn = 0;
return;
};
// 24B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len.
// Layout is positional, so we route each register to the
// slot dictated by element type, not by AX/DX position.
if (n.lhs != nil) {
if (n.lhs.kind == N_TTUPLE) {
let p0: *node = n.lhs.list;
let p1: *node = nil;
if (p0 != nil) { p1 = p0.next; };
let s0_is_str: bool = isstrtyperaw(p0);
let s1_is_str: bool = isstrtyperaw(p1);
if (p0 != nil) {
if (p1 != nil) {
if (s0_is_str != s1_is_str) {
cgexpr(c, rhs);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
};
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
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: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
let fname: str = fieldnode.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fname)) {
cgexpr(c, fieldnode.lhs);
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((off + fi.foff): i64);
emitline("(BP)\n");
fi = nil;
} else {
fi = fi.finext;
};
};
};
fieldnode = fieldnode.next;
};
c.lastwasreturn = 0;
return;
};
};
cgexpr(c, rhs);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
// str init: cgexpr also leaves len in BX; store both.
if (sz == 16) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
};
// slice init: ptr/len/cap in AX/BX/CX.
if (sz == 24) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
} else {
// Bare `let x: T;` with no initializer. C cgen
// (cmd/w6c/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 slotsize 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 (typeis8byteprimitive(c, n.lhs)) {
emitline("\tMOVQ\t$0, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
c.lastwasreturn = 0;
return;
};
fn cgif(c: *cgen, n: *node) void = {
let els: str = mklabel(c, "else");
let endl: str = mklabel(c, "end");
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
if (n.els != nil) { emitline(els); }
else { emitline(endl); };
emitline("\n");
if (n.body != nil) { cgstmt(c, n.body); };
if (n.els != nil) {
emitline("\tJMP\t"); emitline(endl); emitline("\n");
emitlabel(els);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
fn cgfor(c: *cgen, n: *node) void = {
// Match C cgen's label scheme: <fn>_loop_N for the top,
// <fn>_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");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
emitlabel(topl);
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
c.loopcontbuf[c.looptop] = topl;
c.looptop += 1;
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// 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/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\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 = localfind(c, l0.str);
if (off != 0) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
emitline("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == N_IDENT) {
let off: i32 = localfind(c, l1.str);
if (off != 0) {
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
};
};
c.lastwasreturn = 0;
return;
};
// Multi-let from a tuple-returning call: `let n, s = call();` or
// `let (n, s) = call();`. wwstage has no checker, so each binding's
// type is taken from its explicit annotation (l.lhs) when present
// or inferred from the called fn's return-type tuple element.
//
// Per the AX:DX:CX return convention (mirrors C cgen N_MLET):
// (scalar, scalar) — AX → l0, DX → l1.
// (scalar, str) — AX → scalar slot, (DX, CX) → str slot
// as (.ptr, .len). Position-agnostic — the
// regs are routed by element type, not by AX/DX.
fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
let p0t: *node = nil;
let p1t: *node = nil;
if (rhs.kind == N_CALL) {
let callee: *node = rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == N_IDENT) { cnm = callee.str; };
if (callee.kind == N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
let t0: *node = nil;
let t1: *node = nil;
if (l0 != nil) { t0 = l0.lhs; };
if (l1 != nil) { t1 = l1.lhs; };
if (t0 == nil) { t0 = p0t; };
if (t1 == nil) { t1 = p1t; };
let s0_is_str: bool = isstrtyperaw(t0);
let s1_is_str: bool = isstrtyperaw(t1);
cgexpr(c, rhs);
if (l0 != nil) {
if (l1 != nil) {
if (s0_is_str != s1_is_str) {
let sz0: i32 = 8;
let sz1: i32 = 8;
if (s0_is_str) { sz0 = 16; };
if (s1_is_str) { sz1 = 16; };
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tDX, ");
emitoff(off1: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
};
};
if (l0 != nil) {
let off: i32 = localadd(c, l0.str, 8, t0);
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
if (l1 != nil) {
let off: i32 = localadd(c, l1.str, 8, t1);
emitline("\tMOVQ\tDX, ");
emitoff(off: i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
};
fn cgbreak(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopendbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgcontinue(c: *cgen, n: *node) void = {
if (c.looptop > 0) {
let lbl: str = c.loopcontbuf[c.looptop - 1];
emitline("\tJMP\t"); emitline(lbl); emitline("\n");
};
c.lastwasreturn = 0;
return;
};
// MODULE: wcc
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
//
// Houses the top-level emission glue:
// - scanlocals: frame pre-scan that counts each local `let`
// - cgfnparams: parameter spilling per SysV
// - cgfn: fn prologue + body + epilogue
// - cgfile: file-level entry (the exported driver)
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgendecl;` directly.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
//
// Recursively walks the body to count every local `let`. Each gets a
// slot sized by slotsize(typ); 8-byte default. Match-bindings + for-
// init lets count too. Params are added by the cgfn driver.
fn scanlocals(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let total: i32 = 0;
if (n.kind == N_LET) {
// Match localadd's rounding: < 8 bumps to 8, then 8-align.
// scanlocals must agree with localadd 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 scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
};
};
// Multi-let from a tuple-returning call: each binding's size
// comes from its annotated type (l.lhs) when present, else from
// the rhs call's return-tuple element type. Marking via
// scanseenmark also dedupes the recursive descent into n.list
// so each child isn't counted again at the default 8B.
if (n.kind == N_MLET) {
let p0t: *node = nil;
let p1t: *node = nil;
if (n.rhs != nil) {
if (n.rhs.kind == N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == N_IDENT) { cnm = callee.str; };
if (callee.kind == N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
};
let l: *node = n.list;
let pt: *node = p0t;
let bidx: i32 = 0;
for (l != nil) {
if (!scanseenmark(c, l.str)) {
let t: *node = l.lhs;
if (t == nil) {
if (bidx == 0) { t = p0t; };
if (bidx == 1) { t = p1t; };
};
let sz: i32 = 8;
if (t != nil) { sz = slotsize(c, t); };
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
};
l = l.next;
bidx += 1;
};
};
// Match-arm binding (`case let v: T => ...`) gets a slot too.
// Crucially we do NOT dedup these against c.locals: C cgen
// handles a match as an expression with a by-value locals copy,
// 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 localadd 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 (isstrtype(c, pat)) { total += 16; }
else { total += 8; };
};
};
};
if (n.lhs != nil) { total += scanlocals(c, n.lhs); };
if (n.rhs != nil) { total += scanlocals(c, n.rhs); };
if (n.cond != nil) { total += scanlocals(c, n.cond); };
if (n.body != nil) { total += scanlocals(c, n.body); };
if (n.els != nil) { total += scanlocals(c, n.els); };
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
total += scanlocals(c, m);
m = m.next;
};
};
return total;
};
// ---- function-level cgen ---------------------------------------------
fn cgfnparams(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 (istaggedtype(p.lhs)) {
// tagged-union param: spill size/8 registers
// (tag + value words). Slot sized to match.
let slot: i32 = slotsize(c, p.lhs);
let off: i32 = localadd(c, nm, slot, p.lhs);
let nw: i32 = slot / 8;
let w: i32 = 0;
for (w < nw) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
} else { if (isslicetype(c, p.lhs)) {
// slice param: 3 regs (ptr, len, cap), 24-byte slot.
let off: i32 = localadd(c, nm, 24, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
idx += 1;
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + 16): i64);
emitline("(BP)\n");
idx += 1;
} else { if (isstrtype(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 = localadd(c, nm, 16, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + 8): i64);
emitline("(BP)\n");
idx += 1;
} else {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
};};};
};
p = p.next;
};
};
fn cgfn(c: *cgen, fn_: *node) void = {
cgeninit(c, c.a);
c.fnname = fn_.str;
c.fnret = fn_.lhs;
emitline("TEXT ");
if (fn_.exported == 0) {
if (fn_.module.len > 0) {
let isffi: bool = false;
let a: *node = fn_.attr;
for (a != nil) {
if (a.kind == N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
os.write(1, fn_.module.ptr, fn_.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
};
let nm: str = fn_.str;
os.write(1, nm.ptr, nm.len: u64);
emitline(",$");
// Pre-scan total frame: 24 bytes per slice param, 16 per str
// param, 8 per other param, plus per-let from scanlocals.
// Seed c.locals with param-name stubs so scanlocals dedups a
// re-declared `let <name>` in the body against the param's
// slot (matches C cgen). Stubs get cleared before emission.
let scanp: *node = fn_.list;
let frame: i32 = 0;
for (scanp != nil) {
if (scanp.kind == N_PARAM) {
if (istaggedtype(scanp.lhs)) { frame += 24; }
else { if (isslicetype(c, scanp.lhs)) { frame += 24; }
else { if (isstrtype(c, scanp.lhs)) { frame += 16; }
else { frame += 8; }; }; };
scanseenmark(c, scanp.str);
};
scanp = scanp.next;
};
if (fn_.body != nil) { frame += scanlocals(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;
};
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\n");
cgfnparams(c, fn_.list);
c.lastwasreturn = 0;
if (fn_.body != nil) { cgstmt(c, fn_.body); };
if (c.lastwasreturn == 0) {
// Run any registered defers in LIFO order before the
// implicit return.
rundefers(c);
// 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.
emitline("\tMOVQ\t$0, AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
};
};
// ---- file-level entry ------------------------------------------------
export fn cgfile(c: *cgen, file: *node) void = {
if (file == nil) { return; };
c.strlits = nil;
c.strlitseq = 0;
collectaliases(c, file);
collectstructs(c, file);
collectenums(c, file);
collectdefs(c, file);
collectfnrets(c, file);
fficollect(c, file);
collectmods(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;
};
emitdatasection(c);
emitdefconstants(c, file);
};
// MODULE: wcc
// selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c.
//
// Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c`
// producing byte-identical output to C-side `w6c` 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 (- ! ~ &amp; *),
// 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;
// Split files. Bundler pulls these in transitively so consumers only
// need `use cgen;`. Order matters for the flat-bundle concat — utils
// first so cgenexpr/stmt/decl can reference helpers defined here.
use cgenutil;
use cgenexpr;
use cgenstmt;
use cgendecl;
// ---- typedef alias registry -----------------------------------------
//
// `type error = str;` makes `error` a struct-shape alias. We track
// alias→target so isstrtype / isslicetype / structlookup can
// resolve through the chain. Only direct N_TNAME aliases are mapped;
// `type p = struct {...}` is handled by collectstructs.
type aliasent = struct {
aname: str,
target: *node, // the rhs type expr
aanext: *aliasent,
};
fn collectaliases(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: *aliasent = amalloc(c.a, 32u64): *aliasent;
a.aname = d.str;
a.target = body;
a.aanext = c.aliases;
c.aliases = a;
};
};
};
d = d.next;
};
};
fn aliaslookup(c: *cgen, name: str) *node = {
let a: *aliasent = c.aliases;
for (a != nil) {
let an: str = a.aname;
if (streq(an, name)) { return a.target; };
a = a.aanext;
};
return nil;
};
// ---- enum registry --------------------------------------------------
//
// Mirrors cmd/wcc/check.c's enum resolution at collect time: walk
// every `type Foo = enum [storage] { ... }`, pre-compute each
// member's u64 value (supporting auto-increment and sibling refs),
// and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX.
fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: i32 = e.kind;
if (k == N_INTLIT) { *out = e.uval; return true; };
if (k == N_RUNELIT) { *out = e.uval; return true; };
if (k == N_TRUE) { *out = 1u64; return true; };
if (k == N_FALSE) { *out = 0u64; return true; };
if (k == N_IDENT) {
let m: *enummember = prev;
for (m != nil) {
if (streq(m.mname, e.str)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
if (k == N_BIN) {
let a: u64;
let b: u64;
if (!enumevalmember(prev, e.lhs, &a)) { return false; };
if (!enumevalmember(prev, e.rhs, &b)) { return false; };
let op: i32 = e.op;
if (op == TK_PLUS) { *out = a + b; return true; };
if (op == TK_MINUS) { *out = a - b; return true; };
if (op == TK_STAR) { *out = a * b; return true; };
if (op == TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == TK_AMP) { *out = a & b; return true; };
if (op == TK_PIPE) { *out = a | b; return true; };
if (op == TK_CARET) { *out = a ^ b; return true; };
if (op == TK_LSHIFT) { *out = a << b; return true; };
if (op == TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == N_UN) {
let v: u64;
if (!enumevalmember(prev, e.lhs, &v)) { return false; };
let op: i32 = e.op;
if (op == TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == TK_TILDE) { *out = ~v; return true; };
if (op == TK_PLUS) { *out = v; return true; };
return false;
};
return false;
};
fn collectenums(c: *cgen, file: *node) void = {
c.enums = 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_TENUM) {
let et: *enumtype = amalloc(c.a, 48u64): *enumtype;
et.ename = d.str;
et.storage = body.lhs;
et.members = nil;
let prev: u64 = (-1i64): u64;
let mhead: *enummember = nil;
let mtail: *enummember = nil;
let m: *node = body.list;
for (m != nil) {
let val: u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumevalmember(mhead, m.lhs, &val)) {
val = prev + 1u64;
};
};
prev = val;
let em: *enummember = amalloc(c.a, 32u64): *enummember;
em.mname = m.str;
em.mval = val;
em.emnext = nil;
if (mhead == nil) { mhead = em; mtail = em; }
else { mtail.emnext = em; mtail = em; };
m = m.next;
};
et.members = mhead;
et.etnext = c.enums;
c.enums = et;
};
};
};
d = d.next;
};
};
fn enumlookup(c: *cgen, name: str) *enumtype = {
// Strip any `pkg.` prefix and key off the leaf — driver-side
// concatenation flattens the namespace, so `os.whence` and
// `whence` refer to the same registered enum.
let leaf: str = name;
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
leaf.ptr = name.ptr + (i + 1): u64;
leaf.len = name.len - (i + 1);
break;
};
i -= 1;
};
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, leaf)) { return e; };
e = e.etnext;
};
return nil;
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {
if (streq(m.mname, mname)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
// resolvetype — follow typedef alias chains to a "canonical" type
// expr (str/slice/array/struct/...). Stops on cycles via depth limit.
fn resolvetype(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 = aliaslookup(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 cgfile walks N_TYPEDECL with N_TSTRUCT lhs.
// N_DOT and N_ASSIGN consult this to resolve `s.field` for struct or
// *struct bases.
type fieldinfo = struct {
fname: str,
foff: i32,
fsz: i32,
tnode: *node, // the field type expr, for nested struct lookups
finext: *fieldinfo,
};
type structinfo = struct {
sname: str,
fields: *fieldinfo,
totsize: i32,
sinext: *structinfo,
};
// ---- 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_<seq>"
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,
};
// enummember — one (name, value) pair belonging to a registered enum.
// Values are pre-computed at collect time (Hare allows sibling refs
// like `RDWR = READ | WRITE`, so we walk the value expr against the
// already-resolved siblings). Lookup is linear; enum cardinality is
// usually small.
type enummember = struct {
mname: str,
mval: u64,
emnext: *enummember,
};
type enumtype = struct {
ename: str,
storage: *node, // AST type expr for the storage type (i32 by default)
members: *enummember,
etnext: *enumtype,
};
def LOOP_MAX: i32 = 16;
def DEFER_MAX: i32 = 16;
type cgen = struct {
a: *arena,
locals: *local,
frame: i32,
lastwasreturn: i32,
labelseq: i32,
strlitseq: i32,
strlits: *strlit,
ffis: *ffi,
defs: *defent,
fnrets: *fnret,
aliases: *aliasent,
structs: *structinfo,
enums: *enumtype,
mods: *modent, // non-exported decls → originating module
fnname: str,
fnret: *node, // declared return type of current fn (or nil)
looptop: i32,
loopendbuf: *str, // stack of end labels for break
loopcontbuf: *str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: *str, // stack of match end labels for yield
defertop: i32,
deferbuf: **node, // stack of deferred exprs (LIFO at return)
};
fn cgeninit(c: *cgen, a: *arena) void = {
c.a = a;
c.locals = nil;
c.frame = 0;
c.lastwasreturn = 0;
c.labelseq = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them
// at the start of each compilation unit.
c.looptop = 0;
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.yieldtop = 0;
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.defertop = 0;
c.deferbuf = amalloc(a, (DEFER_MAX: u64) * 8u64): **node;
};
// localalloc — 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. scanlocals follows the same rule
// for N_MCASE.
fn localalloc(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 localadd(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/w6c/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. scanlocals 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 localalloc(c, name, sz, tnode);
};
// scanseenmark — called by scanlocals 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 scanseenmark(c: *cgen, name: str) bool = {
if (localfindnode(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 localfindnode(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 localfind(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 emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
fn emitint(v: i64) void = {
let buf: [32]u8;
let n: i32 = strconv.i64tos(buf[0:32], v);
os.write(1, buf.ptr, n: u64);
};
fn emituint(v: u64) void = {
let buf: [32]u8;
let n: i32 = strconv.u64tos(buf[0:32], v);
os.write(1, buf.ptr, n: u64);
};
// emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the
// way Plan 9 6c/6a do.
fn emitdispreg(off: i64, reg: str) void = {
if (off != 0i64) { emitint(off); };
emitline("(");
emitline(reg);
emitline(")");
};
// emitoff — print an integer offset, suppressing it entirely when 0.
// Use before any emitline("(BP)...") or emitline("(SB)...") sequence.
// Plan 9 cc convention: "(BP)" not "0(BP)".
fn emitoff(v: i64) void = {
if (v != 0i64) { emitint(v); };
};
// mklabel — fresh label "<fnname>_<base>_<seq>". 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.fnname;
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.i64tos(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 emitlabel(s: str) void = {
os.write(1, s.ptr, s.len: u64);
emitline(":\n");
};
// ---- string interning ------------------------------------------------
//
// streq is provided by sym.ww and reused here.
// internstrlit — return a stable label for `bytes`. Dedups by content
// so identical literals share storage.
fn internstrlit(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_<seq>".
let buf: [32]u8;
buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_"
let n: i32 = strconv.i64tos(buf[3:32], c.strlitseq: i64);
c.strlitseq += 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;
};
// emitdefconstants — DATA directive per top-level int-literal `def`.
// 8 bytes little-endian to match what the C cgen emits.
fn emitdefconstants(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) {
emitline("DATA ");
if (d.exported == 0) {
if (d.module.len > 0) {
os.write(1, d.module.ptr, d.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
let nm: str = d.str;
os.write(1, nm.ptr, nm.len: u64);
emitline("(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) { emitline("\\\""); }
else { if (b == 92u8) { emitline("\\\\"); }
else {
if (b < 32u8) {
emitline("\\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) {
emitline("\\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;
};
emitline("\"\n");
};
};
d = d.next;
};
};
// emitdatasection — DATA directives for every interned strlit.
// Trailing NUL appended so .ptr can be used as a C string by syscalls.
fn emitdatasection(c: *cgen) void = {
let s: *strlit = c.strlits;
for (s != nil) {
emitline("DATA ");
let lab: str = s.label;
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB),\"");
let bs: str = s.bytes;
let i: i32 = 0;
for (i < bs.len) {
let b: u8 = bs[i];
if (b == 34u8) { emitline("\\\""); } // "
else { if (b == 92u8) { emitline("\\\\"); } // \
else { if (b == 10u8) { emitline("\\n"); }
else { if (b == 9u8) { emitline("\\t"); }
else { if (b == 13u8) { emitline("\\r"); }
else {
if (b < 32u8) {
emitline("\\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) {
emitline("\\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;
};
emitline("\\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 collectfnrets(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 fnretlookup(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 w6c emits; an
// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at
// file load and consult on N_IDENT lookup.
type defent = struct {
dname: str,
drhs: *node,
dnext: *defent,
};
fn collectdefs(c: *cgen, file: *node) void = {
c.defs = nil;
let d: *node = file.list;
for (d != nil) {
if (d.kind == N_DEF) {
let e: *defent = amalloc(c.a, 32u64): *defent;
e.dname = d.str;
e.drhs = d.rhs;
e.dnext = c.defs;
c.defs = e;
};
d = d.next;
};
};
fn deflookup(c: *cgen, name: str) bool = {
let e: *defent = c.defs;
for (e != nil) {
let dn: str = e.dname;
if (streq(dn, name)) { return true; };
e = e.dnext;
};
return false;
};
// Returns the rhs init node for a top-level `def`, or nil if `name`
// doesn't name a def. Used by cgdot to inline `.ptr`/`.len` on
// `def NAME: str = "..."` — those aren't laid out in memory.
fn deflookuprhs(c: *cgen, name: str) *node = {
let e: *defent = c.defs;
for (e != nil) {
let dn: str = e.dname;
if (streq(dn, name)) { return e.drhs; };
e = e.dnext;
};
return nil;
};
// ---- module-private symbol map --------------------------------------
//
// Non-exported top-level decls live in their originating module's
// namespace. cgen mangles those names to `<module>.<name>` at emission
// time, both at the def site (TEXT/DATA) and at every call/load site,
// so two modules can each privately define `cstrlen` without colliding
// at link time. Exported decls and FFI-bound decls keep their bare name.
type modent = struct {
mname: str, // the bare ident as it appears in source
module: str, // the originating module (`// MODULE: foo`)
mnext: *modent,
};
fn collectmods(c: *cgen, file: *node) void = {
c.mods = nil;
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
// Mirror collectfnrets' shape exactly (plain prepend in one
// branch). Earlier nested-if/early-return variants tickled a
// wwstage cgen bug that dropped most prepends.
if (d.kind == N_FNDECL) {
if (d.exported == 0) {
if (d.module.len > 0) {
if (!streq(d.str, "main")) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
};
if (d.kind == N_DEF) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == N_TYPEDECL) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == N_LET) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
d = d.next;
};
};
fn modlookup(c: *cgen, name: str) str = {
let m: *modent = c.mods;
for (m != nil) {
if (streq(m.mname, name)) { return m.module; };
m = m.mnext;
};
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
// emitsymname — write the asm symbol name for `ident`. Honours, in
// order: FFI mapping (@symbol), module mangling (private decls), bare
// name. Use everywhere a top-level name is emitted before `(SB)` or in
// a `TEXT name,$N` header.
fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
// FFI hit — emit the mapped linker symbol verbatim.
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookup(c, ident);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
};
// ---- FFI map ---------------------------------------------------------
fn fficollect(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 symnode: *node = a.list;
if (symnode != nil) {
if (symnode.kind == N_STRLIT) {
let f: *ffi = amalloc(c.a, 48u64): *ffi;
f.ident = d.str;
f.symbol = symnode.str;
f.fnext = c.ffis;
c.ffis = f;
};
};
};
};
a = a.next;
};
};
d = d.next;
};
};
fn ffiresolve(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 argregname(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 "?";
};
// MODULE: wwdump
// selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c.
//
// Reads a .ww file, runs the ww-side lexer, prints tokens through
// the ww-side tokprint. The 990_selfhost test diffs this output
// byte-for-byte against the C-side wwdump on the same file. Any
// divergence is a port bug in lex.ww or tok.ww.
//
// Modes:
// wwdump -t file.ww tokens (default)
// wwdump -a file.ww AST (not yet implemented; reserved)
use os;
use mem;
use tok;
use lex;
use ast;
use parse;
use typ;
use sym;
use check;
use cgen;
use strconv;
// ---- argv helpers -----------------------------------------------------
// argstrlen — strlen on a NUL-terminated *u8. argv strings are always
// NUL-terminated (kernel-supplied) so this is safe.
fn argstrlen(s: *u8) i32 = {
let n: i32 = 0;
for (s[n] != 0u8) { n += 1; };
return n;
};
fn argstr(p: *u8) str = {
let s: str;
s.ptr = p;
s.len = argstrlen(p);
return s;
};
// streqlit — compare a NUL-terminated argv entry to a string literal.
fn streqlit(p: *u8, lit: str) bool = {
let i: i32 = 0;
for (i < lit.len) {
if (p[i] != lit[i]) { return false; };
i += 1;
};
return p[i] == 0u8;
};
// ---- main -------------------------------------------------------------
export fn main(argc: i32, argv: **u8) i32 = {
let mode: i32 = 116; // 't'
let path: *u8 = nil;
let i: i32 = 1;
for (i < argc) {
let a: *u8 = argv[i];
if (streqlit(a, "-t")) {
mode = 116;
} else { if (streqlit(a, "-a")) {
mode = 97; // 'a'
} else { if (streqlit(a, "-r")) {
mode = 114; // 'r' — resolve / name-check
} else { if (streqlit(a, "-c")) {
mode = 99; // 'c' — codegen / emit asm
} else { if (path == nil) {
path = a;
};};};};};
i += 1;
};
if (path == nil) {
os.write(2, "usage: wwdump [-t|-a] file.ww\n".ptr, 30u64);
return 2;
};
let fdorerr: (i32 | os.oserror) = os.tryopen(path, os.flag.RDONLY, 0i32);
let fd: i32 = -1;
match (fdorerr) {
case let v: i32 => fd = v;
case let e: os.oserror => {
os.write(2, "wwdump: cannot open ".ptr, 20u64);
os.write(2, path, argstrlen(path): u64);
os.write(2, "\n".ptr, 1u64);
return 1;
};
};
let szr: (i64 | os.oserror) = os.filesize(fd);
let sz: i64 = 0i64;
match (szr) {
case let v: i64 => sz = v;
case let e: os.oserror => {
os.write(2, "wwdump: filesize failed\n".ptr, 24u64);
os.close(fd);
return 1;
};
};
let a: *arena = newarena();
let buf: *u8 = amalloc(a, sz: u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, sz: u64);
os.close(fd);
let r: i64 = 0i64;
match (rr) {
case let v: i64 => r = v;
case let e: os.oserror => {
os.write(2, "wwdump: read failed\n".ptr, 20u64);
return 1;
};
};
if (r != sz) {
os.write(2, "wwdump: short read\n".ptr, 19u64);
return 1;
};
let l: lex;
lexinit(&l, a, argstr(path), buf, sz: u64);
if (mode == 116) { // '-t'
for (true) {
let t: tok;
lexnext(&l, &t);
tokprint(1i32, &t);
if (t.kind == TK_EOF) { break; };
if (t.kind == TK_ERR) { break; };
};
} else { if (mode == 97) { // '-a'
let ps: parser;
parserinit(&ps, a, &l);
let f: *node = parsefile(&ps);
astprint(1i32, f);
} else { if (mode == 114) { // '-r' — name resolve report
let ps: parser;
parserinit(&ps, a, &l);
let f: *node = parsefile(&ps);
let tc: tctx;
typesinit(&tc, a);
let ck: checker;
checkinit(&ck, a, &tc);
// Quiet by default; flip to 1 when debugging missing names.
ck.verbose = 0;
checkfile(&ck, f);
// (close out the if-else chain — we'll close all braces below)
// "<file>: <resolved>/<resolved+unresolved> resolved"
os.write(1, argstr(path).ptr, argstrlen(path): u64);
os.write(1, ": ".ptr, 2u64);
let buf: [32]u8;
let n: i32 = strconv.i64tos(buf[0:32], ck.nresolved: i64);
os.write(1, buf.ptr, n: u64);
os.write(1, "/".ptr, 1u64);
let total: i32 = ck.nresolved + ck.nunresolved;
n = strconv.i64tos(buf[0:32], total: i64);
os.write(1, buf.ptr, n: u64);
os.write(1, " resolved\n".ptr, 10u64);
if (ck.nunresolved > 0) { return 1; };
} else { if (mode == 99) { // '-c' — codegen / emit asm
let ps: parser;
parserinit(&ps, a, &l);
let f: *node = parsefile(&ps);
let cg: cgen;
cgeninit(&cg, a);
cgfile(&cg, f);
};};};};
if (l.errs > 0) { return 1; };
return 0;
};