// 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: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @symbol("rt_syscall") fn syscall2(num: nr, a: i64, b: i64) i64; @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, a: i64, b: i64, c: i64, d: i64) i64; // alloc / free — runtime mmap-backed page allocator. Untyped: // `alloc(n)` returns a `*void` and `free(p, n)` requires the byte // count back because rt_free is munmap-based and doesn't track // mapping sizes (the kernel needs the length to release the // reservation). // // Diverges from Hare. Hare exposes `alloc` / `free` as typed // language builtins (`alloc(value, cap)?` / `free(ptr)`) that the // compiler lowers to rt::malloc/rt::free; ww has no such builtins, // so the rt-symbol surface is exposed directly. Stdlib callers // that need a typed allocation pattern wrap this with a cast plus // a stored capacity (see [[strings.dup]], [[memio.dynamic]]). // // OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with // no error path. The raw Linux mmap syscall returns a negative // errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); // the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention // that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` // catches it; any deref of such a return faults. Today the stdlib // does not check; OOM faults on first dereference. A typed // fallible variant is a future task. @symbol("rt_alloc") export fn alloc(n: u64) *void; @symbol("rt_free") export 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); }; }; // Linux amd64 syscall numbers. Internal to this module — passed as // the first arg of syscall0..4 via libwwrt's rt_syscall trampoline. // `nr` is the type so the call sites can't accidentally pass an // arbitrary i64 (`syscall1(0i64, ...)` no longer typechecks). type nr = enum i64 { READ = 0, WRITE = 1, OPEN = 2, CLOSE = 3, LSEEK = 8, ACCESS = 21, DUP2 = 33, GETPID = 39, FORK = 57, EXECVE = 59, EXIT = 60, WAIT4 = 61, MKDIR = 83, RMDIR = 84, UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . 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 EXCL = 128, // 0x80 — pair with CREATE to fail on existing path 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(nr.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(nr.WRITE, fd: i64, buf: i64, n: i64); }; export fn read(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(nr.READ, fd: i64, buf: i64, n: i64); }; export fn close(fd: i32) i32 = { return syscall1(nr.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(nr.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(nr.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(nr.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 (F_OK=0). export fn access(path: *u8, mode: i32) i32 = { return syscall2(nr.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(nr.UNLINK, path: i64): i32; }; // mkdir — mkdir(2). Path must be NUL-terminated. Mode is the unix // permission bitset (e.g. 0o700). Returns 0 on success, negative // errno otherwise. Hare name (os::mkdir). export fn mkdir(path: *u8, mode: i32) i32 = { return syscall2(nr.MKDIR, path: i64, mode: i64): i32; }; // rmdir — rmdir(2). Path must be NUL-terminated. Returns 0 on // success, negative errno otherwise. Hare name (os::rmdir). export fn rmdir(path: *u8) i32 = { return syscall1(nr.RMDIR, path: i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent // parent directories with the given mode. EEXIST is silently // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // // `path` must be NUL-terminated AND its bytes must be writable — // mkdirs temporarily replaces '/' separators with NUL while // invoking [[mkdir]] on each prefix, then restores them. Pointing // `path` at a string literal will segfault. Callers hold the bytes // in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same // precedent as [[temp.named]]'s pathbuf. // // Mirrors Hare's os::mkdirs (recursive variant of os::mkdir). export fn mkdirs(path: *u8, mode: i32) (void | oserror) = { // Find the path length (excluding trailing NUL). let n: i32 = 0; for (path[n] != 0u8) { n += 1; }; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, // mkdir it, restore the slash, continue. Skip index 0 so a // leading '/' on absolute paths doesn't trigger an empty mkdir. let i: i32 = 1; for (i < n) { if (path[i] == 47u8) { // '/' path[i] = 0u8; let r: i32 = mkdir(path, mode); path[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; }; i += 1; }; // mkdir the full path. let r: i32 = mkdir(path, mode); if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; return; }; // getpid(2). Used by the driver to mint unique scratch paths. export fn getpid() i32 = { return syscall0(nr.GETPID): i32; }; // fork(2): 0 in the child, child pid in the parent, negative errno // on failure. export fn fork() i32 = { return syscall0(nr.FORK): i32; }; // execve(2): on success, does not return. export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { return syscall3(nr.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(nr.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(nr.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(nr.GETDENTS64, fd: i64, buf: i64, n: i64); }; // ---- environment ------------------------------------------------------ // rt_envp — runtime-side getter. rt/start.s captures envp into a DATAW // slot before calling main; this binding lifts the captured pointer // into ww. Same FFI shape as rt_syscall / rt_alloc / rt_abort: a TEXT // symbol the linker resolves. The returned `**u8` is a NUL-terminated // table of `*u8` entries, each pointing at a NUL-terminated // "NAME=VALUE" byte sequence. // // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes // live in the kernel-supplied envp table at process entry. A future // `setenv` (separate task) that grows the table behind the scenes // would invalidate prior views; v1 has no setenv, so callers can // hold the view indefinitely. // // Mirrors Hare's os::tryenv shape (returns void rather than panicking // on missing). Hare also ships os::getenv (`(str | void)`) and // os::mustenv (panic-on-missing); ww collapses to the single // `(str | void)` form for now — consumers wanting "must" semantics // abort at the call site. // // Algorithm: walk the NUL-pointer-terminated `environ` table doing a // "name=" prefix match against each entry, byte-wise. NUL inside // `name` would never match a real env var (env var names cannot // contain '\0'), so we don't filter — POSIX puts that responsibility // on the caller. export fn getenv(name: str) (str | void) = { let envp: **u8 = rtenvp(); let i: i32 = 0; for (true) { let entry: *u8 = envp[i]; if (entry == nil: *u8) { return; }; let j: i32 = 0; let matched: bool = true; for (j < name.len) { if (entry[j] == 0u8) { matched = false; break; }; if (entry[j] != name[j]) { matched = false; break; }; j += 1; }; if (matched) { if (entry[name.len] == 61u8) { // '=' let val: *u8 = entry + ((name.len + 1): u64); let n: i32 = 0; for (val[n] != 0u8) { n += 1; }; let r: str; r.ptr = val; r.len = n; return r; }; }; i += 1; }; return; }; // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + // ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns // `filestat` by value; ww's cgreturn ABI tops out at 24B today (see // STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] / // [[fstat]] take an out-parameter and return `(void | oserror)`. // Re-evaluate the by-value shape when full sret lands. // // `filestat`, `mode`, and `stat_mask` live in lib/os because ww has // no lib/fs yet; Hare puts them in `fs::`. These types graduate to // lib/fs when that module ships — callers should expect a future // re-export. // // Underlying syscall is SYS_newfstatat (262), which unifies // stat/lstat/fstat through the `dirfd + flags` triple: // stat = newfstatat(AT_FDCWD, path, 0) // lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW) // fstat = newfstatat(fd, "", AT_EMPTY_PATH) // Avoiding SYS_statx — its 256B variable layout would buy btime, // but Hare's filestat doesn't expose btime either, so we stay on // the simpler 144B kernel struct. // fstatat(2) flag values. Linux constants from . // Names mirror Hare's ref/hare/sys/+linux/types.ha:45-51 (capital- // AT_ prefix, top-level `def`s). export def AT_FDCWD: i32 = -100; export def AT_SYMLINK_NOFOLLOW: i32 = 256; // 0x100 export def AT_EMPTY_PATH: i32 = 4096; // 0x1000 // mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/ // types.ha:63). Permission bits are the standard Unix octal subset; // type bits live in the S_IFMT = 0o170000 region. Type-bit test: // // let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask // if (t == os.mode.DIR as u32) { /* directory */ }; // // Numeric values are octal in Hare's source; ww has no octal // literals so they're written as decimal with the octal in a // trailing comment. export type mode = enum u32 { // permission bits USER_RWX = 448u32, // 0o700 USER_RW = 384u32, // 0o600 USER_RX = 320u32, // 0o500 USER_R = 256u32, // 0o400 USER_W = 128u32, // 0o200 USER_X = 64u32, // 0o100 GROUP_RWX = 56u32, // 0o070 GROUP_RW = 48u32, // 0o060 GROUP_RX = 40u32, // 0o050 GROUP_R = 32u32, // 0o040 GROUP_W = 16u32, // 0o020 GROUP_X = 8u32, // 0o010 OTHER_RWX = 7u32, // 0o007 OTHER_RW = 6u32, // 0o006 OTHER_RX = 5u32, // 0o005 OTHER_R = 4u32, // 0o004 OTHER_W = 2u32, // 0o002 OTHER_X = 1u32, // 0o001 SETUID = 2048u32, // 0o4000 SETGID = 1024u32, // 0o2000 STICKY = 512u32, // 0o1000 // file-type bits (S_IFMT mask = 0o170000 = 61440) UNKNOWN = 0u32, FIFO = 4096u32, // 0o010000 CHR = 8192u32, // 0o020000 DIR = 16384u32, // 0o040000 BLK = 24576u32, // 0o060000 REG = 32768u32, // 0o100000 LINK = 40960u32, // 0o120000 SOCK = 49152u32, // 0o140000 }; // stat_mask — which filestat fields the call populated. Mirrors // Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills // every field, so [[stat]] / [[lstat]] / [[fstat]] always set all // seven bits OR-folded (see [[fillfilestat]]); per-bit testing is // the documented sparse-backend pattern (cf. Hare's fs::fs network // backends that only populate mtime+size). export type stat_mask = enum u32 { UID = 1u32, GID = 2u32, SIZE = 4u32, INODE = 8u32, ATIME = 16u32, MTIME = 32u32, CTIME = 64u32, }; // timespec — {sec, nsec} pair, matching Hare's time::instant // (ref/hare/time/types.ha). Local to lib/os; graduates to // lib/time.instant when lib/time and lib/fs ship. Same byte layout // (i64+i64 = 16B) so a future migration is field-rename only. export type timespec = struct { sec: i64, nsec: i64, }; // filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 // bytes. See module-header note re: graduation to lib/fs. export type filestat = struct { mask: stat_mask, // 0 (4) mode: mode, // 4 (4) uid: u32, // 8 (4) gid: u32, // 12 (4) sz: u64, // 16 (8) inode: u64, // 24 (8) atime: timespec, // 32 (16) mtime: timespec, // 48 (16) ctime: timespec, // 64 (16) — ends at 80 }; // kstat — x86_64 kernel `struct stat` layout. Mirrors // arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed // fields). 144 bytes. Module-internal; SYS_newfstatat writes into // this buffer and the public stat fns then copy the bits into the // Hare-shaped [[filestat]]. // // Mode is typed as the public [[mode]] enum (rather than raw u32) // so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage // emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that // wwstage skips (task #25); the in-tree shape sidesteps it. // Identical byte layout (both 4B at offset 24). type kstat = struct { dev: u64, // 0 ino: u64, // 8 nlink: u64, // 16 mode: mode, // 24 uid: u32, // 28 gid: u32, // 32 pad0: u32, // 36 rdev: u64, // 40 sz: i64, // 48 blksize: i64, // 56 blocks: i64, // 64 atime_sec: i64, // 72 atime_nsec: i64, // 80 mtime_sec: i64, // 88 mtime_nsec: i64, // 96 ctime_sec: i64, // 104 ctime_nsec: i64, // 112 unused0: i64, // 120 unused1: i64, // 128 unused2: i64, // 136 — ends at 144 }; // emptypath — single-NUL byte used as the `pathname` arg to // newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL // pointer to a zero-length C string, NOT a null pointer. Bytes are // read-only from the kernel's view; ww has no module-level const so // this is a writable `let`. let emptypath: [1]u8 = [0u8]; // fillfilestat — copy a 144B kstat into the 80B Hare-shaped // filestat. Internal helper used by all three public entry points. // Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259): // newfstatat populates every field, so the mask is the OR-fold of // all seven Hare stat_mask bits. fn fillfilestat(out: *filestat, k: *kstat) void = { out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE | stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME | stat_mask.CTIME; out.mode = k.mode; out.uid = k.uid; out.gid = k.gid; out.sz = k.sz: u64; out.inode = k.ino; out.atime.sec = k.atime_sec; out.atime.nsec = k.atime_nsec; out.mtime.sec = k.mtime_sec; out.mtime.nsec = k.mtime_nsec; out.ctime.sec = k.ctime_sec; out.ctime.nsec = k.ctime_nsec; }; // stat — fill *out with metadata for `path`. Follows symlinks. // `path` must be NUL-terminated (lib/os convention; see task #23 // for a planned `path: str` migration). // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. export fn stat(out: *filestat, path: *u8) (void | oserror) = { let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, path: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). export fn lstat(out: *filestat, path: *u8) (void | oserror) = { let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, path: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // fstat — like [[stat]] but addresses the file by fd. Uses // newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd // directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54). export fn fstat(out: *filestat, fd: i32) (void | oserror) = { let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, fd: i64, (&emptypath[0]): i64, (&k): i64, AT_EMPTY_PATH: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are // followed; a dangling symlink is `false`. // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same // note). The race is unavoidable in this shape. // // Goes through SYS_newfstatat directly rather than match'ing on // [[stat]]'s `(void | oserror)` return. Functionally identical; // the direct shape sidesteps a cstage/wwstage cgen disagreement // on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. export fn exists(path: *u8) bool = { let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, path: i64, (&k): i64, 0i64); return r >= 0i64; }; // 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: strings // strings — operations over the immutable str type ({ *u8, len }). // Mirrors Hare's strings::; `len` and `is-empty` aren't functions // (callers use `s.len` and `s.len == 0` directly). use os; // compare — bytewise three-way comparison: negative if ab. Matches Hare's strings::compare. ASCII-order, not // locale-aware. Callers that just need equality use `compare(a, b) == 0`. export fn compare(a: str, b: str) i32 = { let n: i32 = a.len; if (b.len < n) { n = b.len; }; let i: i32 = 0; for (i < n) { if (a[i] != b[i]) { return (a[i]: i32) - (b[i]: i32); }; i += 1; }; return a.len - b.len; }; export fn hasprefix(s: str, p: str) bool = { if (p.len > s.len) { return false; }; let i: i32 = 0; for (i < p.len) { if (s[i] != p[i]) { return false; }; i += 1; }; return true; }; export fn hassuffix(s: str, suf: str) bool = { if (suf.len > s.len) { return false; }; let off: i32 = s.len - suf.len; let i: i32 = 0; for (i < suf.len) { if (s[off + i] != suf[i]) { return false; }; i += 1; }; return true; }; // byteindex — first byte position of `needle` in `s`. Mirrors Hare's // strings::byteindex: a single-codepoint rune scans for the byte that // encodes it (ASCII only here — multi-byte UTF-8 awaits utf8 encode), // a str needle scans for the substring. Returns void if absent. export fn byteindex(s: str, needle: (str | rune)) (i32 | void) = { match (needle) { case let r: rune => { let c: u8 = r: u8; let i: i32 = 0; for (i < s.len) { if (s[i] == c) { return i; }; i += 1; }; return; }; case let sub: str => { if (sub.len == 0) { return 0; }; if (sub.len > s.len) { return; }; let last: i32 = s.len - sub.len; let i: i32 = 0; for (i <= last) { let j: i32 = 0; let ok: bool = true; for (j < sub.len) { if (s[i + j] != sub[j]) { ok = false; j = sub.len; } else { j += 1; }; }; if (ok) { return i; }; i += 1; }; return; }; }; return; }; // contains — true iff `sub` appears in `s`. Mirrors Hare's // strings::contains shape (byte-wise on the str-needle case). export fn contains(s: str, sub: str) bool = { let r: (i32 | void) = byteindex(s, sub); match (r) { case let i: i32 => return true; case void => return false; }; return false; }; // concat — joins two strings into a fresh str. Caller owns the // returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors // Hare's strings::concat shape. export fn concat(a: str, b: str) str = { let total: i32 = a.len + b.len; let buf: *u8 = os.alloc(total: u64): *u8; let i: i32 = 0; for (i < a.len) { buf[i] = a[i]; i += 1; }; let j: i32 = 0; for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; let r: str; r.ptr = buf; r.len = total; return r; }; // dup — duplicate a string into a fresh allocation. Caller owns the // returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors // Hare's strings::dup shape — Hare returns `(str | nomem)`, ww doesn't // have nomem (os.alloc aborts on OOM), so we return plain `str`. // // Empty input yields a `{nil, 0}` str — Hare returns the static empty // string; same observable result. export fn dup(s: str) str = { let r: str; r.ptr = nil; r.len = 0; if (s.len == 0) { return r; }; let buf: *u8 = os.alloc(s.len: u64): *u8; let i: i32 = 0; for (i < s.len) { buf[i] = s[i]; i += 1; }; r.ptr = buf; r.len = s.len; return r; }; // freeall — release every str element in `s` (those that were // individually allocated) plus the slice's backing storage. Mirrors // Hare's strings::freeall — the natural disposer for any function // returning a fresh `[]str` of dup'd elements (e.g. shlex.split). // // Each element is freed via os.free at its own length; the slice // header storage is freed at `cap * 16` bytes (one str = 16B). Empty // elements (`{nil, 0}` from a zero-length dup) are skipped — calling // os.free on a nil pointer at len 0 would tickle the rt_free guard // that the runtime treats as a logic bug. // // `cap == 0` means the slice was never grown (empty `[]str` with no // backing allocation); skip the header free in that case too. export fn freeall(s: []str) void = { let i: i32 = 0; for (i < s.len) { if (s[i].len > 0) { os.free(s[i].ptr: *void, s[i].len: u64); }; i += 1; }; if (s.cap > 0) { os.free(s.ptr: *void, (s.cap: u64) * 16u64); }; }; // rbyteindex — last byte position of `needle` in `s`. Mirrors Hare's // strings::rbyteindex. Rune needle scans for the byte that encodes it // (ASCII only); str needle scans for the substring. Empty str needle // matches at s.len. export fn rbyteindex(s: str, needle: (str | rune)) (i32 | void) = { match (needle) { case let r: rune => { let c: u8 = r: u8; let i: i32 = s.len - 1; for (i >= 0) { if (s[i] == c) { return i; }; i -= 1; }; return; }; case let sub: str => { if (sub.len == 0) { return s.len; }; if (sub.len > s.len) { return; }; let i: i32 = s.len - sub.len; for (i >= 0) { let j: i32 = 0; let ok: bool = true; for (j < sub.len) { if (s[i + j] != sub[j]) { ok = false; j = sub.len; } else { j += 1; }; }; if (ok) { return i; }; i -= 1; }; return; }; }; return; }; // sub — borrowed substring `s[start..end]`. Mirrors Hare's // strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of- // range indices are clamped silently here, where Hare aborts. export fn sub(s: str, start: i32, end: i32) str = { let lo: i32 = start; let hi: i32 = end; if (lo < 0) { lo = 0; }; if (hi > s.len) { hi = s.len; }; if (hi < lo) { hi = lo; }; let r: str; r.ptr = s.ptr + (lo: u64); r.len = hi - lo; return r; }; // trimprefix — `s` with `pre` stripped from the front, or `s` // unchanged if it doesn't start with `pre`. Returns a borrowed view. // Mirrors Hare's strings::trimprefix. export fn trimprefix(s: str, pre: str) str = { if (!hasprefix(s, pre)) { return s; }; let r: str; r.ptr = s.ptr + (pre.len: u64); r.len = s.len - pre.len; return r; }; // trimsuffix — `s` with `suf` stripped from the end, or `s` unchanged // if it doesn't end with `suf`. Returns a borrowed view. Mirrors // Hare's strings::trimsuffix. export fn trimsuffix(s: str, suf: str) str = { if (!hassuffix(s, suf)) { return s; }; let r: str; r.ptr = s.ptr; r.len = s.len - suf.len; return r; }; // ltrimbyte / rtrimbyte / trimbyte — strip occurrences of a single // byte from the left, right, or both ends. Returns a borrowed view. // Hare's strings::ltrim / rtrim / trim take a rune varargs set; ww's // subset takes a single byte (the common ASCII case). export fn ltrimbyte(s: str, c: u8) str = { let i: i32 = 0; for (i < s.len) { if (s[i] != c) { break; }; i += 1; }; let r: str; r.ptr = s.ptr + (i: u64); r.len = s.len - i; return r; }; export fn rtrimbyte(s: str, c: u8) str = { let n: i32 = s.len; for (n > 0) { if (s[n - 1] != c) { break; }; n -= 1; }; let r: str; r.ptr = s.ptr; r.len = n; return r; }; export fn trimbyte(s: str, c: u8) str = { return rtrimbyte(ltrimbyte(s, c), c); }; // MODULE: strconv // strconv — number↔string conversions. // // Mirrors Hare's strconv:: surface. The *tos functions return a // `const str` view into a module-level buffer that is overwritten on // the next call to the same function; callers must copy the bytes if // they need to outlive the next invocation. See [[strings.dup]] to // duplicate. Matches Hare's strconv::*tos semantics. use os; use strings; // 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. export type invalid = !i32; // overflow — input was valid but doesn't fit the target type. // Mirrors Hare's strconv::overflow = !void. export type overflow = !void; // error — any error from a strconv call. Mirrors Hare's strconv::error. export type error = !(invalid | overflow); // base — numeric base for parsing/formatting. Mirrors Hare's // `strconv::base` (Hare uses `enum uint`; we pick `enum i32` since // the underlying parse/format loops index with i32). // // HEX is an alias for HEX_UPPER; HEX_LOWER is a pseudo-base that // produces lowercase a-f digits. export type base = enum i32 { DEFAULT = 0, BIN = 2, OCT = 8, DEC = 10, HEX_UPPER = 16, HEX = 16, HEX_LOWER = 17, }; fn basenum(b: base) i64 = { if (b == base.BIN) { return 2; }; if (b == base.OCT) { return 8; }; if (b == base.HEX) { return 16; }; if (b == base.HEX_UPPER) { return 16; }; if (b == base.HEX_LOWER) { return 16; }; return 10; // DEC and DEFAULT }; fn basedigit(d: i64, b: base) u8 = { if (d < 10) { return (d + 48): u8; }; let off: i64 = d - 10; if (b == base.HEX_LOWER) { return (off + 97): u8; }; return (off + 65): u8; }; // u64tos — convert v to a base-b numeric string. Returns a view into // `u64tos_buf` which is overwritten on the next call. Matches Hare's // strconv::u64tos. let u64tos_buf: [65]u8; export fn u64tos(v: u64, b: base) str = { let nb: u64 = basenum(b): u64; let tmp: [65]u8; let i: i32 = 0; let n: u64 = v; if (n == 0u64) { tmp[0] = 48u8; i = 1; }; for (n > 0u64) { let d: i64 = (n % nb): i64; tmp[i] = basedigit(d, b); n = n / nb; i += 1; }; let out: i32 = 0; for (i > 0) { i -= 1; u64tos_buf[out] = tmp[i]; out += 1; }; let r: str; r.ptr = &u64tos_buf[0]; r.len = out; return r; }; // i64tos — convert v to a base-b numeric string. Returns a view into // `i64tos_buf` which is overwritten on the next call. Independent // buffer from u64tos so i64tos's own call to u64tos doesn't clobber // the in-flight result. Matches Hare's strconv::i64tos. let i64tos_buf: [66]u8; export fn i64tos(v: i64, b: base) str = { let neg: bool = false; let n: i64 = v; if (n < 0) { neg = true; n = -n; }; let nb: i64 = basenum(b); let tmp: [65]u8; let i: i32 = 0; if (n == 0) { tmp[0] = 48u8; i = 1; }; for (n > 0) { let d: i64 = n % nb; tmp[i] = basedigit(d, b); n = n / nb; i += 1; }; let out: i32 = 0; if (neg) { i64tos_buf[out] = 45u8; out += 1; }; // '-' for (i > 0) { i -= 1; i64tos_buf[out] = tmp[i]; out += 1; }; let r: str; r.ptr = &i64tos_buf[0]; r.len = out; return r; }; export fn i32tos(v: i32, b: base) str = { return i64tos(v: i64, b); }; export fn i16tos(v: i16, b: base) str = { return i64tos(v: i64, b); }; export fn i8tos(v: i8, b: base) str = { return i64tos(v: i64, b); }; export fn u32tos(v: u32, b: base) str = { return u64tos(v: u64, b); }; export fn u16tos(v: u16, b: base) str = { return u64tos(v: u64, b); }; export fn u8tos(v: u8, b: base) str = { return u64tos(v: u64, b); }; // digval — value of digit byte `c` under base `b`, or -1 if not a // valid digit. Letters are accepted case-insensitively under HEX / // HEX_UPPER; only lowercase under HEX_LOWER. fn digval(c: u8, b: base) i32 = { if (c >= 48u8) { if (c <= 57u8) { return (c - 48u8): i32; }; }; if (b == base.HEX_LOWER) { if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; }; return -1; }; if (c >= 65u8) { if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; }; if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; }; return -1; }; // stoi64 — parse signed base-b number. Mirrors Hare's strconv::stoi64. // No locale, no whitespace, no underscores: optional leading '-' then // digits. Returns invalid with the offending index or overflow on // out-of-range. export fn stoi64(s: str, b: base) (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 nb: i32 = basenum(b): i32; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; let d: i32 = digval(c, b); if (d < 0) { return i: invalid; }; if (d >= nb) { return i: invalid; }; v = v * (nb: i64) + (d: i64); i += 1; }; if (neg) { v = -v; }; return v; }; // stou64 — parse unsigned base-b number. Mirrors Hare's strconv::stou64. export fn stou64(s: str, b: base) (u64 | invalid | overflow) = { if (s.len == 0) { return 0: invalid; }; let nb: u64 = basenum(b): u64; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; let d: i32 = digval(c, b); if (d < 0) { return i: invalid; }; if ((d: u64) >= nb) { return i: invalid; }; v = v * nb + (d: u64); i += 1; }; return v; }; export fn stoi32(s: str, b: base) (i32 | invalid | overflow) = { let r = stoi64(s, b); match (r) { case let v: i64 => { if (v > 2147483647i64) { return overflow{}; }; if (v < -2147483648i64) { return overflow{}; }; return v: i32; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; // unreachable; appeases the path-cov checker }; export fn stoi16(s: str, b: base) (i16 | invalid | overflow) = { let r = stoi64(s, b); match (r) { case let v: i64 => { if (v > 32767i64) { return overflow{}; }; if (v < -32768i64) { return overflow{}; }; return v: i16; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; export fn stoi8(s: str, b: base) (i8 | invalid | overflow) = { let r = stoi64(s, b); match (r) { case let v: i64 => { if (v > 127i64) { return overflow{}; }; if (v < -128i64) { return overflow{}; }; return v: i8; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; export fn stou32(s: str, b: base) (u32 | invalid | overflow) = { let r = stou64(s, b); match (r) { case let v: u64 => { if (v > 4294967295u64) { return overflow{}; }; return v: u32; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; export fn stou16(s: str, b: base) (u16 | invalid | overflow) = { let r = stou64(s, b); match (r) { case let v: u64 => { if (v > 65535u64) { return overflow{}; }; return v: u16; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; export fn stou8(s: str, b: base) (u8 | invalid | overflow) = { let r = stou64(s, b); match (r) { case let v: u64 => { if (v > 255u64) { return overflow{}; }; return v: u8; }; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; // f64tos — convert v to a decimal string. Returns owned str; release // via os.free. Mirrors Hare's strconv::f64tos (current ww impl is // fixed-point only, max 6 fractional digits, no NaN/Inf support — // see graduate-to-Ryū note below). // // Surface: // // - finite values only. NaN/±Inf detection needs an f64→u64 bit // reinterpret cast that the cgen doesn't expose yet. // - fixed-point only, up to 6 fractional digits. Trailing zeros // after the decimal point are trimmed. Trailing '.' is dropped. // - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) // fall back to the literal token "huge". Hare would print these // in scientific notation via Ryū; we will graduate when the // compiler grows the bit-reinterpret cast. // // Round-trip is therefore lossy past 6 fractional digits. // // No float literals in the body — 990's wwdump diff requires this // file's TK_FLOAT count to match between C and ww front-ends, and // the ww-side wwdump currently skips TK_FLOAT.fval while the C side // %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: // build f64 constants via int-to-f64 casts. let f64tos_buf: [64]u8; export fn f64tos(v: f64) str = { let out: i32 = 0; let f: f64 = v; let zero: f64 = 0: f64; if (f < zero) { f64tos_buf[out] = 45u8; // '-' out += 1; f = -f; }; // 9e18 is comfortably under I64_MAX (9.22e18). Past this the // `f: i64` cast wraps and the integer part comes back as garbage. let cap: f64 = 9000000000000000000i64: f64; if (f >= cap) { let s: str = "huge"; let k: i32 = 0; for (k < s.len) { f64tos_buf[out] = s[k]; out += 1; k += 1; }; let r: str; r.ptr = &f64tos_buf[0]; r.len = out; return r; }; let ip: i64 = f: i64; // Fractional part scaled to 6 decimal digits, with round-to- // nearest via +0.5. (f64 compound assigns mis-lower in cgen — // use the explicit form, as the rest of lib does.) let frac: f64 = f - (ip: f64); let scale: f64 = 1000000: f64; frac = frac * scale; let half: f64 = (1: f64) / (2: f64); let fp: i64 = (frac + half): i64; // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer // part needs to advance. if (fp >= 1000000) { ip += 1; fp = 0; }; let intstr: str = i64tos(ip, base.DEC); let k: i32 = 0; for (k < intstr.len) { f64tos_buf[out] = intstr.ptr[k]; out += 1; k += 1; }; if (fp != 0) { f64tos_buf[out] = 46u8; // '.' out += 1; let fracstr: str = u64tos(fp: u64, base.DEC); // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → // fp=50000, fracstr="50000", pad one '0' before). let z: i32 = 6 - fracstr.len; for (z > 0) { f64tos_buf[out] = 48u8; out += 1; z -= 1; }; k = 0; for (k < fracstr.len) { f64tos_buf[out] = fracstr.ptr[k]; out += 1; k += 1; }; // Trim trailing zeros in the fractional part. for (out > 0) { if (f64tos_buf[out - 1] != 48u8) { break; }; out -= 1; }; }; let r: str; r.ptr = &f64tos_buf[0]; r.len = out; return r; }; // strerror — convert an strconv error to a user-readable string. // Returns owned str; release via os.free. Mirrors Hare's // strconv::strerror. export fn strerror(e: error) str = { match (e) { case let v: invalid => return strings.dup("input is not a valid number"); case let v: overflow => return strings.dup("input number doesn't fit target type"); }; return strings.dup(""); }; // 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 `Tkind` enum in cmd/wcc/ww.h. Numeric values are // explicit and must stay in sync — the 990_selfhost test diffs wwdump // output against the C side, byte for byte. type tkind = enum i32 { TK_NONE = 0, TK_EOF = 1, TK_ERR = 2, TK_IDENT = 3, TK_INT = 4, TK_FLOAT = 5, TK_RUNE = 6, TK_STR = 7, TK_FN = 8, TK_LET = 9, TK_DEF = 10, TK_IF = 11, TK_ELSE = 12, TK_FOR = 13, TK_SWITCH = 14, TK_CASE = 15, TK_RETURN = 16, TK_USE = 17, TK_TYPE = 18, TK_STRUCT = 19, TK_DEFER = 20, TK_BREAK = 21, TK_CONTINUE = 22, TK_EXPORT = 23, TK_PROC = 24, TK_CHAN = 25, TK_NIL = 26, TK_TRUE = 27, TK_FALSE = 28, TK_AS = 29, TK_STATIC = 30, TK_MATCH = 31, TK_CONST = 32, TK_UNDER = 33, TK_LPAREN = 34, TK_RPAREN = 35, TK_LBRACE = 36, TK_RBRACE = 37, TK_LBRACK = 38, TK_RBRACK = 39, TK_COMMA = 40, TK_SEMI = 41, TK_COLON = 42, TK_DOT = 43, TK_ELLIPSIS = 44, TK_DOTDOT = 45, TK_AT = 46, TK_QUESTION = 47, TK_ASSIGN = 48, TK_PLUSEQ = 49, TK_MINUSEQ = 50, TK_STAREQ = 51, TK_SLASHEQ = 52, TK_PERCENTEQ = 53, TK_AMPEQ = 54, TK_PIPEEQ = 55, TK_CARETEQ = 56, TK_LSHIFTEQ = 57, TK_RSHIFTEQ = 58, TK_PLUS = 59, TK_MINUS = 60, TK_STAR = 61, TK_SLASH = 62, TK_PERCENT = 63, TK_AMP = 64, TK_PIPE = 65, TK_CARET = 66, TK_TILDE = 67, TK_LSHIFT = 68, TK_RSHIFT = 69, TK_EQ = 70, TK_NEQ = 71, TK_LT = 72, TK_LE = 73, TK_GT = 74, TK_GE = 75, TK_AND = 76, TK_OR = 77, TK_NOT = 78, TK_LARROW = 79, TK_ARROW = 80, TK_FATARROW = 81, // Tail-appended values — keeps every prior TK_* numeric value // stable for the 990_selfhost byte-diff against the C side. TK_IS = 82, TK_VOID = 83, TK_YIELD = 84, TK_ENUM = 85, TK_LAST = 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: tkind, file: str, // path of the source the token came from line: i32, col: i32, text: str, // arena-owned token text (tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR) uval: u64, // tkind.TK_INT, tkind.TK_RUNE fval: f64, // tkind.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 tkind.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) tkind = { if (streqn(p, "as", n)) { return tkind.TK_AS; }; if (streqn(p, "break", n)) { return tkind.TK_BREAK; }; if (streqn(p, "case", n)) { return tkind.TK_CASE; }; if (streqn(p, "chan", n)) { return tkind.TK_CHAN; }; if (streqn(p, "const", n)) { return tkind.TK_CONST; }; if (streqn(p, "continue", n)) { return tkind.TK_CONTINUE; }; if (streqn(p, "def", n)) { return tkind.TK_DEF; }; if (streqn(p, "defer", n)) { return tkind.TK_DEFER; }; if (streqn(p, "else", n)) { return tkind.TK_ELSE; }; if (streqn(p, "enum", n)) { return tkind.TK_ENUM; }; if (streqn(p, "export", n)) { return tkind.TK_EXPORT; }; if (streqn(p, "false", n)) { return tkind.TK_FALSE; }; if (streqn(p, "fn", n)) { return tkind.TK_FN; }; if (streqn(p, "for", n)) { return tkind.TK_FOR; }; if (streqn(p, "if", n)) { return tkind.TK_IF; }; if (streqn(p, "is", n)) { return tkind.TK_IS; }; if (streqn(p, "let", n)) { return tkind.TK_LET; }; if (streqn(p, "match", n)) { return tkind.TK_MATCH; }; if (streqn(p, "nil", n)) { return tkind.TK_NIL; }; if (streqn(p, "proc", n)) { return tkind.TK_PROC; }; if (streqn(p, "return", n)) { return tkind.TK_RETURN; }; if (streqn(p, "static", n)) { return tkind.TK_STATIC; }; if (streqn(p, "struct", n)) { return tkind.TK_STRUCT; }; if (streqn(p, "switch", n)) { return tkind.TK_SWITCH; }; if (streqn(p, "true", n)) { return tkind.TK_TRUE; }; if (streqn(p, "type", n)) { return tkind.TK_TYPE; }; if (streqn(p, "use", n)) { return tkind.TK_USE; }; if (streqn(p, "void", n)) { return tkind.TK_VOID; }; if (streqn(p, "yield", n)) { return tkind.TK_YIELD; }; return tkind.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: tkind) str = { if (k == tkind.TK_NONE) { return ""; }; if (k == tkind.TK_EOF) { return "EOF"; }; if (k == tkind.TK_ERR) { return "ERR"; }; if (k == tkind.TK_IDENT) { return "IDENT"; }; if (k == tkind.TK_INT) { return "INT"; }; if (k == tkind.TK_FLOAT) { return "FLOAT"; }; if (k == tkind.TK_RUNE) { return "RUNE"; }; if (k == tkind.TK_STR) { return "STR"; }; if (k == tkind.TK_FN) { return "fn"; }; if (k == tkind.TK_LET) { return "let"; }; if (k == tkind.TK_DEF) { return "def"; }; if (k == tkind.TK_IF) { return "if"; }; if (k == tkind.TK_ELSE) { return "else"; }; if (k == tkind.TK_FOR) { return "for"; }; if (k == tkind.TK_SWITCH) { return "switch"; }; if (k == tkind.TK_CASE) { return "case"; }; if (k == tkind.TK_RETURN) { return "return"; }; if (k == tkind.TK_USE) { return "use"; }; if (k == tkind.TK_TYPE) { return "type"; }; if (k == tkind.TK_STRUCT) { return "struct"; }; if (k == tkind.TK_DEFER) { return "defer"; }; if (k == tkind.TK_BREAK) { return "break"; }; if (k == tkind.TK_CONTINUE) { return "continue"; }; if (k == tkind.TK_EXPORT) { return "export"; }; if (k == tkind.TK_PROC) { return "proc"; }; if (k == tkind.TK_CHAN) { return "chan"; }; if (k == tkind.TK_NIL) { return "nil"; }; if (k == tkind.TK_TRUE) { return "true"; }; if (k == tkind.TK_FALSE) { return "false"; }; if (k == tkind.TK_AS) { return "as"; }; if (k == tkind.TK_IS) { return "is"; }; if (k == tkind.TK_VOID) { return "void"; }; if (k == tkind.TK_YIELD) { return "yield"; }; if (k == tkind.TK_STATIC) { return "static"; }; if (k == tkind.TK_MATCH) { return "match"; }; if (k == tkind.TK_CONST) { return "const"; }; if (k == tkind.TK_UNDER) { return "_"; }; if (k == tkind.TK_ENUM) { return "enum"; }; if (k == tkind.TK_LPAREN) { return "("; }; if (k == tkind.TK_RPAREN) { return ")"; }; if (k == tkind.TK_LBRACE) { return "{"; }; if (k == tkind.TK_RBRACE) { return "}"; }; if (k == tkind.TK_LBRACK) { return "["; }; if (k == tkind.TK_RBRACK) { return "]"; }; if (k == tkind.TK_COMMA) { return ","; }; if (k == tkind.TK_SEMI) { return ";"; }; if (k == tkind.TK_COLON) { return ":"; }; if (k == tkind.TK_DOT) { return "."; }; if (k == tkind.TK_ELLIPSIS) { return "..."; }; if (k == tkind.TK_DOTDOT) { return ".."; }; if (k == tkind.TK_AT) { return "@"; }; if (k == tkind.TK_QUESTION) { return "?"; }; if (k == tkind.TK_ASSIGN) { return "="; }; if (k == tkind.TK_PLUSEQ) { return "+="; }; if (k == tkind.TK_MINUSEQ) { return "-="; }; if (k == tkind.TK_STAREQ) { return "*="; }; if (k == tkind.TK_SLASHEQ) { return "/="; }; if (k == tkind.TK_PERCENTEQ) { return "%="; }; if (k == tkind.TK_AMPEQ) { return "&="; }; if (k == tkind.TK_PIPEEQ) { return "|="; }; if (k == tkind.TK_CARETEQ) { return "^="; }; if (k == tkind.TK_LSHIFTEQ) { return "<<="; }; if (k == tkind.TK_RSHIFTEQ) { return ">>="; }; if (k == tkind.TK_PLUS) { return "+"; }; if (k == tkind.TK_MINUS) { return "-"; }; if (k == tkind.TK_STAR) { return "*"; }; if (k == tkind.TK_SLASH) { return "/"; }; if (k == tkind.TK_PERCENT) { return "%"; }; if (k == tkind.TK_AMP) { return "&"; }; if (k == tkind.TK_PIPE) { return "|"; }; if (k == tkind.TK_CARET) { return "^"; }; if (k == tkind.TK_TILDE) { return "~"; }; if (k == tkind.TK_LSHIFT) { return "<<"; }; if (k == tkind.TK_RSHIFT) { return ">>"; }; if (k == tkind.TK_EQ) { return "=="; }; if (k == tkind.TK_NEQ) { return "!="; }; if (k == tkind.TK_LT) { return "<"; }; if (k == tkind.TK_LE) { return "<="; }; if (k == tkind.TK_GT) { return ">"; }; if (k == tkind.TK_GE) { return ">="; }; if (k == tkind.TK_AND) { return "&&"; }; if (k == tkind.TK_OR) { return "||"; }; if (k == tkind.TK_NOT) { return "!"; }; if (k == tkind.TK_LARROW) { return "<-"; }; if (k == tkind.TK_ARROW) { return "->"; }; if (k == tkind.TK_FATARROW) { return "=>"; }; if (k == tkind.TK_LAST) { return ""; }; 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. // ":: [ ]\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, ""); }; fputcbyte(fd, 58u8); // ':' let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); os.write(fd, ls.ptr, ls.len: u64); fputcbyte(fd, 58u8); let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); os.write(fd, cs.ptr, cs.len: u64); fputcbyte(fd, 32u8); // ' ' fputsstr(fd, tokname(t.kind)); if (t.kind == tkind.TK_IDENT) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_STR) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_ERR) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_INT) { fputcbyte(fd, 32u8); let us: str = strconv.u64tos(t.uval, strconv.base.DEC); os.write(fd, us.ptr, us.len: u64); } else { if (t.kind == tkind.TK_RUNE) { fputcbyte(fd, 32u8); let us: str = strconv.u64tos(t.uval, strconv.base.DEC); os.write(fd, us.ptr, us.len: u64); };};};};}; // tkind.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; }; // valid — `c` is in the 0..127 ASCII range. export fn valid(c: rune) bool = { if (c < 0) { return false; }; if (c > 127) { return false; }; return true; }; // validstr — every byte in `s` is ASCII (0..127). export fn validstr(s: str) bool = { let i: i32 = 0; for (i < s.len) { // High-bit test rather than `> 127u8`; both cgens lower // the bitwise form identically. The `> u8` form picks // JA vs JG depending on signed/unsigned dispatch. if ((s[i] & 128u8) != 0u8) { return false; }; i += 1; }; return true; }; // iscntrl — control chars: 0..31 and 127. export fn iscntrl(c: rune) bool = { if (c >= 0) { if (c <= 31) { return true; }; }; if (c == 127) { return true; }; return false; }; // isblank — space and tab. export fn isblank(c: rune) bool = { if (c == 32) { return true; }; // ' ' if (c == 9) { return true; }; // '\t' return false; }; // isprint — printable: space through '~'. export fn isprint(c: rune) bool = { if (c < 32) { return false; }; if (c > 126) { return false; }; return true; }; // isgraph — printable, non-space. export fn isgraph(c: rune) bool = { if (c < 33) { return false; }; if (c > 126) { return false; }; return true; }; // ispunct — printable, non-alnum, non-space. export fn ispunct(c: rune) bool = { if (!isgraph(c)) { return false; }; if (isalnum(c)) { return false; }; return true; }; // 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; }; // strcasecmp — three-way ASCII case-insensitive compare. export fn strcasecmp(a: str, b: str) i32 = { let n: i32 = a.len; if (b.len < n) { n = b.len; }; let i: i32 = 0; for (i < n) { let ca: rune = tolower(a[i]: rune); let cb: rune = tolower(b[i]: rune); if (ca != cb) { return (ca - cb): i32; }; i += 1; }; return a.len - b.len; }; // 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; // isidstart / isidpart — identifier classification. Lexer-local // because the "alpha or '_' / alnum or '_'" set isn't part of Hare's // ascii::; ascii::isalpha + the '_' check live here instead. fn isidstart(c: rune) bool = { if (ascii.isalpha(c)) { return true; }; if (c == 95) { return true; }; // '_' return false; }; fn isidpart(c: rune) bool = { if (ascii.isalnum(c)) { return true; }; if (c == 95) { return true; }; return false; }; // hexval — value of `c` as a hex digit (0..15) or void if not a hex // digit. Used by string-literal `\xHH` escapes. fn hexval(c: rune) (i32 | void) = { if (ascii.isdigit(c)) { return (c - 48): i32; }; if (c >= 65) { if (c <= 70) { return ((c - 65) + 10): i32; }; // 'A'..'F' }; if (c >= 97) { if (c <= 102) { return ((c - 97) + 10): i32; }; // 'a'..'f' }; return; }; 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 = hexval(hi: rune)!; let lv: i32 = hexval(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); }; }; // parsef64 — minimal decimal-float parser. Reads digits[.digits][eE[+-]digits] // from the first `n` bytes of `s` (no leading sign — the lexer emits // the unary minus as a separate token). The result rounds to the // nearest f64 only via the trailing pow-10 multiply; this matches // `strtod` to 1 ULP on typical literals and is good enough for the // wwstage's own use (no float literals appear in the bootstrap // source). Anything past `n` or non-digit is silently ignored. fn parsef64(s: *u8, n: u64) f64 = { let i: u64 = 0u64; let intp: i64 = 0i64; for (i < n) { let b: u8 = s[i]; if (b < 48u8) { break; }; if (b > 57u8) { break; }; intp = intp * 10i64 + (b - 48u8): i64; i += 1u64; }; let frac: i64 = 0i64; let fscale: i64 = 1i64; if (i < n) { if (s[i] == 46u8) { // '.' i += 1u64; for (i < n) { let b: u8 = s[i]; if (b < 48u8) { break; }; if (b > 57u8) { break; }; frac = frac * 10i64 + (b - 48u8): i64; fscale = fscale * 10i64; i += 1u64; }; }; }; let exp: i32 = 0; let expneg: bool = false; if (i < n) { let e: u8 = s[i]; if (e == 101u8 || e == 69u8) { // 'e' / 'E' i += 1u64; if (i < n) { if (s[i] == 45u8) { // '-' expneg = true; i += 1u64; } else { if (s[i] == 43u8) { // '+' i += 1u64; };}; }; for (i < n) { let b: u8 = s[i]; if (b < 48u8) { break; }; if (b > 57u8) { break; }; exp = exp * 10 + (b - 48u8): i32; i += 1u64; }; }; }; let result: f64 = intp: f64; if (frac != 0i64) { result = result + (frac: f64) / (fscale: f64); }; if (exp != 0) { // Use int-to-float casts so this file stays free of float // literals — 990's wwdump diff relies on lib/ww/lex/lex.ww // tokenising identically through C and ww, and the C dumper // %g-formats TK_FLOAT.fval while the ww dumper currently // skips it. Hiding the constants behind casts keeps both // sides emitting `FLOAT` with no payload. let factor: f64 = 1: f64; let ten: f64 = 10: f64; let k: i32 = 0; for (k < exp) { factor = factor * ten; k += 1; }; if (expneg) { result = result / factor; } else { result = result * factor; }; }; return result; }; fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.kind = tkind.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.kind = tkind.TK_FLOAT; // Strip underscores from the digits (Hare allows 1_000.5) // before parsing — match what cmd/wcc/lex.c does with // strtod over a cleaned buffer. let clean: *u8 = amalloc(l.a, n + 1u64): *u8; let i: u64 = 0u64; let j: u64 = 0u64; for (i < n) { let b: u8 = l.src[begin + i]; if (b != 95u8) { // '_' clean[j] = b; j += 1u64; }; i += 1u64; }; clean[j] = 0u8; let fv: f64 = parsef64(clean, j); out.fval = fv; // Stash the IEEE bits in uval — cgen consumers read floats // as integers (n.uval) to avoid an SSE round-trip when // materialising the constant. let pu: *u64 = (&fv): *u64; out.uval = *pu; } 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 = tkind.TK_ERR; }; }; let pc: i32 = lpeek(l, 0u64); if (pc >= 0) { if (isidstart(pc: rune)) { let sb: u64 = l.lpos; for (true) { let cc: i32 = lpeek(l, 0u64); if (cc < 0) { break; }; if (!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 (!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 = tkind.TK_UNDER; out.text = astrndup(l.a, p, n); return; }; }; let k: tkind = kwlookup(p, n: i32); if (k != tkind.TK_NONE) { out.kind = k; } else { out.kind = tkind.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 = tkind.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 = tkind.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 = tkind.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 = tkind.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 = tkind.TK_RUNE; out.file = start.file; out.line = start.line; out.col = start.col; out.uval = ch: u64; }; fn emitsimple(start: *pos, k: tkind, 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 = tkind.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, tkind.TK_EOF, out); return; }; let start: pos; curpos(l, &start); let c: i32 = lpeek(l, 0u64); if (c >= 0) { if (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, tkind.TK_LPAREN, out); return; }; if (c == 41) { emitsimple(&start, tkind.TK_RPAREN, out); return; }; if (c == 123) { emitsimple(&start, tkind.TK_LBRACE, out); return; }; if (c == 125) { emitsimple(&start, tkind.TK_RBRACE, out); return; }; if (c == 91) { emitsimple(&start, tkind.TK_LBRACK, out); return; }; if (c == 93) { emitsimple(&start, tkind.TK_RBRACK, out); return; }; if (c == 44) { emitsimple(&start, tkind.TK_COMMA, out); return; }; if (c == 59) { emitsimple(&start, tkind.TK_SEMI, out); return; }; if (c == 58) { emitsimple(&start, tkind.TK_COLON, out); return; }; if (c == 64) { emitsimple(&start, tkind.TK_AT, out); return; }; if (c == 63) { emitsimple(&start, tkind.TK_QUESTION, out); return; }; if (c == 126) { emitsimple(&start, tkind.TK_TILDE, out); return; }; if (c == 46) { // '.' if (lpeek(l, 0u64) == 46) { if (lpeek(l, 1u64) == 46) { lget(l); lget(l); emitsimple(&start, tkind.TK_ELLIPSIS, out); return; }; lget(l); emitsimple(&start, tkind.TK_DOTDOT, out); return; }; emitsimple(&start, tkind.TK_DOT, out); return; }; if (c == 43) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_PLUSEQ, out); return; }; emitsimple(&start, tkind.TK_PLUS, out); return; }; if (c == 45) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_MINUSEQ, out); return; }; if (lpeek(l, 0u64) == 62) { lget(l); emitsimple(&start, tkind.TK_ARROW, out); return; }; emitsimple(&start, tkind.TK_MINUS, out); return; }; if (c == 42) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_STAREQ, out); return; }; emitsimple(&start, tkind.TK_STAR, out); return; }; if (c == 47) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_SLASHEQ, out); return; }; emitsimple(&start, tkind.TK_SLASH, out); return; }; if (c == 37) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_PERCENTEQ, out); return; }; emitsimple(&start, tkind.TK_PERCENT, out); return; }; if (c == 38) { if (lpeek(l, 0u64) == 38) { lget(l); emitsimple(&start, tkind.TK_AND, out); return; }; if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_AMPEQ, out); return; }; emitsimple(&start, tkind.TK_AMP, out); return; }; if (c == 124) { if (lpeek(l, 0u64) == 124) { lget(l); emitsimple(&start, tkind.TK_OR, out); return; }; if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_PIPEEQ, out); return; }; emitsimple(&start, tkind.TK_PIPE, out); return; }; if (c == 94) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_CARETEQ, out); return; }; emitsimple(&start, tkind.TK_CARET, out); return; }; if (c == 61) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_EQ, out); return; }; if (lpeek(l, 0u64) == 62) { lget(l); emitsimple(&start, tkind.TK_FATARROW, out); return; }; emitsimple(&start, tkind.TK_ASSIGN, out); return; }; if (c == 33) { if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_NEQ, out); return; }; emitsimple(&start, tkind.TK_NOT, out); return; }; if (c == 60) { if (lpeek(l, 0u64) == 60) { lget(l); if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_LSHIFTEQ, out); return; }; emitsimple(&start, tkind.TK_LSHIFT, out); return; }; if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_LE, out); return; }; if (lpeek(l, 0u64) == 45) { lget(l); emitsimple(&start, tkind.TK_LARROW, out); return; }; emitsimple(&start, tkind.TK_LT, out); return; }; if (c == 62) { if (lpeek(l, 0u64) == 62) { lget(l); if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_RSHIFTEQ, out); return; }; emitsimple(&start, tkind.TK_RSHIFT, out); return; }; if (lpeek(l, 0u64) == 61) { lget(l); emitsimple(&start, tkind.TK_GE, out); return; }; emitsimple(&start, tkind.TK_GT, out); return; }; errat(l, &start, "unexpected character"); out.kind = tkind.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. // Mirror of the C `Nkind` enum in cmd/wcc/ww.h. Numeric values are // explicit and must stay in sync — the 990_selfhost test diffs // astprint against the C side byte-for-byte. Tail-appended entries // (TYPETEST onward) preserve every prior N_* value. type nkind = enum i32 { N_NONE = 0, N_INTLIT = 1, N_FLOATLIT = 2, N_STRLIT = 3, N_RUNELIT = 4, N_TRUE = 5, N_FALSE = 6, N_NIL = 7, N_IDENT = 8, N_BIN = 9, N_UN = 10, N_CALL = 11, N_INDEX = 12, N_DOT = 13, N_CAST = 14, N_STRUCTLIT = 15, N_ARRLIT = 16, N_FIELD = 17, N_ASSIGN = 18, N_ALLOC = 19, N_FREE = 20, N_RECV = 21, N_SLICE = 22, N_SPREAD = 23, N_BLOCK = 24, N_EXPRSTMT = 25, N_LET = 26, N_RETURN = 27, N_IF = 28, N_FOR = 29, N_FORRANGE = 30, N_DEFER = 31, N_BREAK = 32, N_CONTINUE = 33, N_SWITCH = 34, N_CASE = 35, N_FILE = 36, N_USE = 37, N_DEF = 38, N_TYPEDECL = 39, N_FNDECL = 40, N_PARAM = 41, N_TNAME = 42, N_TPTR = 43, N_TSLICE = 44, N_TARRAY = 45, N_TFN = 46, N_TSTRUCT = 47, N_TFIELD = 48, N_TCHAN = 49, N_ATTR = 50, N_TTUPLE = 51, N_TTAGGED = 52, N_TUPLE = 53, N_MATCH = 54, N_MCASE = 55, N_TRYPROP = 56, N_TRYUNW = 57, N_MLET = 58, N_MASSIGN = 59, N_TYPETEST = 60, N_TYPEASSERT = 61, N_VOIDLIT = 62, N_TBANG = 63, N_YIELD = 64, N_TENUM = 65, N_TENUMMEMBER = 66, N_LAST = 67, }; // ---- Node ------------------------------------------------------------- type node = struct { kind: nkind, file: str, line: i32, col: i32, op: tkind, // for nkind.N_BIN / nkind.N_UN / nkind.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: nkind, 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: nkind) str = { if (k == nkind.N_NONE) { return "none"; }; if (k == nkind.N_INTLIT) { return "int"; }; if (k == nkind.N_FLOATLIT) { return "float"; }; if (k == nkind.N_STRLIT) { return "str"; }; if (k == nkind.N_RUNELIT) { return "rune"; }; if (k == nkind.N_TRUE) { return "true"; }; if (k == nkind.N_FALSE) { return "false"; }; if (k == nkind.N_NIL) { return "nil"; }; if (k == nkind.N_IDENT) { return "id"; }; if (k == nkind.N_BIN) { return "bin"; }; if (k == nkind.N_UN) { return "un"; }; if (k == nkind.N_CALL) { return "call"; }; if (k == nkind.N_INDEX) { return "index"; }; if (k == nkind.N_DOT) { return "dot"; }; if (k == nkind.N_CAST) { return "cast"; }; if (k == nkind.N_STRUCTLIT) { return "structlit"; }; if (k == nkind.N_ARRLIT) { return "arrlit"; }; if (k == nkind.N_FIELD) { return "field"; }; if (k == nkind.N_ASSIGN) { return "assign"; }; if (k == nkind.N_ALLOC) { return "alloc"; }; if (k == nkind.N_FREE) { return "free"; }; if (k == nkind.N_RECV) { return "recv"; }; if (k == nkind.N_SLICE) { return "slice"; }; if (k == nkind.N_SPREAD) { return "spread"; }; if (k == nkind.N_BLOCK) { return "block"; }; if (k == nkind.N_EXPRSTMT) { return "exprstmt"; }; if (k == nkind.N_LET) { return "let"; }; if (k == nkind.N_RETURN) { return "return"; }; if (k == nkind.N_IF) { return "if"; }; if (k == nkind.N_FOR) { return "for"; }; if (k == nkind.N_FORRANGE) { return "forrange"; }; if (k == nkind.N_DEFER) { return "defer"; }; if (k == nkind.N_BREAK) { return "break"; }; if (k == nkind.N_CONTINUE) { return "continue"; }; if (k == nkind.N_SWITCH) { return "switch"; }; if (k == nkind.N_CASE) { return "case"; }; if (k == nkind.N_FILE) { return "file"; }; if (k == nkind.N_USE) { return "use"; }; if (k == nkind.N_DEF) { return "def"; }; if (k == nkind.N_TYPEDECL) { return "typedecl"; }; if (k == nkind.N_FNDECL) { return "fn"; }; if (k == nkind.N_PARAM) { return "param"; }; if (k == nkind.N_TNAME) { return "tname"; }; if (k == nkind.N_TPTR) { return "tptr"; }; if (k == nkind.N_TSLICE) { return "tslice"; }; if (k == nkind.N_TARRAY) { return "tarray"; }; if (k == nkind.N_TFN) { return "tfn"; }; if (k == nkind.N_TSTRUCT) { return "tstruct"; }; if (k == nkind.N_TFIELD) { return "tfield"; }; if (k == nkind.N_TCHAN) { return "tchan"; }; if (k == nkind.N_ATTR) { return "attr"; }; if (k == nkind.N_TTUPLE) { return "ttuple"; }; if (k == nkind.N_TTAGGED) { return "ttagged"; }; if (k == nkind.N_TUPLE) { return "tuple"; }; if (k == nkind.N_MATCH) { return "match"; }; if (k == nkind.N_MCASE) { return "mcase"; }; if (k == nkind.N_TRYPROP) { return "tryprop"; }; if (k == nkind.N_TRYUNW) { return "tryunw"; }; if (k == nkind.N_MLET) { return "mlet"; }; if (k == nkind.N_MASSIGN) { return "massign"; }; if (k == nkind.N_TYPETEST) { return "typetest"; }; if (k == nkind.N_TYPEASSERT) { return "typeassert"; }; if (k == nkind.N_VOIDLIT) { return "voidlit"; }; if (k == nkind.N_TBANG) { return "tbang"; }; if (k == nkind.N_YIELD) { return "yield"; }; if (k == nkind.N_TENUM) { return "tenum"; }; if (k == nkind.N_TENUMMEMBER) { return "tenummember"; }; if (k == nkind.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 == nkind.N_INTLIT) { putc1(fd, 32u8); let s: str = strconv.u64tos(n.uval, strconv.base.DEC); os.write(fd, s.ptr, s.len: u64); } else { if (n.kind == nkind.N_RUNELIT) { putc1(fd, 32u8); let s: str = strconv.u64tos(n.uval, strconv.base.DEC); os.write(fd, s.ptr, s.len: u64); } else { if ( n.kind == nkind.N_STRLIT || n.kind == nkind.N_IDENT || n.kind == nkind.N_USE || n.kind == nkind.N_DOT || n.kind == nkind.N_DEF || n.kind == nkind.N_TYPEDECL || n.kind == nkind.N_FNDECL || n.kind == nkind.N_PARAM || n.kind == nkind.N_LET || n.kind == nkind.N_TNAME || n.kind == nkind.N_TFIELD || n.kind == nkind.N_TENUMMEMBER || n.kind == nkind.N_FIELD || n.kind == nkind.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 == nkind.N_BIN || n.kind == nkind.N_UN || n.kind == nkind.N_ASSIGN ) { putc1(fd, 32u8); let on: str = tokname(n.op); os.write(fd, on.ptr, on.len: u64); };};};}; if (n.kind == nkind.N_FNDECL) { if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; }; if (n.kind == nkind.N_DEF) { if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; }; if (n.kind == nkind.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 == tkind.TK_INT) { let n: *node = newnode(p.a, nkind.N_INTLIT, pf, pl, pc); n.uval = p.curuval; n.str = p.curtext; // Plumb the typed-int suffix (`42i64`, `3u8`) through to // the node. Cgen's rhstargetname reads tsuffix to pick the // matching tagged-union variant; without this, typed-int // rhs of `h.e = 42i64;` falls through to the "first non-str // variant" fallback and writes tag 0. Mirror of cmd/wcc/ // parse.c parseprimary TK_INT. n.tsuffix = p.curtsuffix; advance(p); return n; }; if (p.curkind == tkind.TK_FLOAT) { let n: *node = newnode(p.a, nkind.N_FLOATLIT, pf, pl, pc); n.fval = p.curfval; // uval carries the IEEE 754 bit pattern — the lexer sets // both, and cgen consumers prefer the integer view so they // don't need a float ABI to materialise the constant. n.uval = p.curuval; n.str = p.curtext; n.tsuffix = p.curtsuffix; advance(p); return n; }; if (p.curkind == tkind.TK_STR) { let n: *node = newnode(p.a, nkind.N_STRLIT, pf, pl, pc); n.str = p.curtext; advance(p); return n; }; if (p.curkind == tkind.TK_RUNE) { let n: *node = newnode(p.a, nkind.N_RUNELIT, pf, pl, pc); n.uval = p.curuval; advance(p); return n; }; if (p.curkind == tkind.TK_TRUE) { advance(p); return newnode(p.a, nkind.N_TRUE, pf, pl, pc); }; if (p.curkind == tkind.TK_FALSE) { advance(p); return newnode(p.a, nkind.N_FALSE, pf, pl, pc); }; if (p.curkind == tkind.TK_NIL) { advance(p); return newnode(p.a, nkind.N_NIL, pf, pl, pc); }; if (p.curkind == tkind.TK_VOID) { advance(p); return newnode(p.a, nkind.N_VOIDLIT, pf, pl, pc); }; if (p.curkind == tkind.TK_UNDER) { // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT // with empty str (newnode zeroes the node, so str.len is // already 0); the checker rejects it outside lvalue // positions. advance(p); return newnode(p.a, nkind.N_IDENT, pf, pl, pc); }; if (p.curkind == tkind.TK_LBRACK) { // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). // The repeat marker is an nkind.N_FIELD node with str = "..." // appended to the element list so cgen can detect it. advance(p); let n: *node = newnode(p.a, nkind.N_ARRLIT, pf, pl, pc); let head: *node = nil; let tail: *node = nil; for (p.curkind != tkind.TK_RBRACK) { if (p.curkind == tkind.TK_EOF) { break; }; let e: *node = parseexpr(p); if (head == nil) { head = e; tail = e; } else { tail.next = e; tail = e; }; if (accepttok(p, tkind.TK_ELLIPSIS)) { let rep: *node = newnode(p.a, nkind.N_FIELD, p.curfile, p.curline, p.curcol); rep.str = "..."; tail.next = rep; tail = rep; break; }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RBRACK, "expected ']' after array literal"); n.list = head; return n; }; if (p.curkind == tkind.TK_LPAREN) { advance(p); let e: *node = parseexpr(p); // Tuple literal: (a, b, ...) if (accepttok(p, tkind.TK_COMMA)) { let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc); t.list = e; let tail: *node = e; for (true) { if (p.curkind == tkind.TK_RPAREN) { break; }; let en: *node = parseexpr(p); tail.next = en; tail = en; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); return t; }; expecttok(p, tkind.TK_RPAREN, "expected ')'"); return e; }; if (p.curkind == tkind.TK_IDENT) { let n: *node = newnode(p.a, nkind.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 == tkind.TK_LBRACE) { advance(p); let s: *node = newnode(p.a, nkind.N_STRUCTLIT, pf, pl, pc); s.lhs = n; let head: *node = nil; let tail: *node = nil; for (p.curkind != tkind.TK_RBRACE) { if (p.curkind == tkind.TK_EOF) { break; }; // Trailing `...` autofill marker. Stash on s.op so // cgen can zero-fill the slot before per-field stores. if (p.curkind == tkind.TK_ELLIPSIS) { advance(p); s.op = tkind.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, tkind.TK_ASSIGN, "expected '=' in struct lit field"); let v: *node = parseexpr(p); let f: *node = newnode(p.a, nkind.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, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); s.list = head; return s; }; return n; }; if (p.curkind == tkind.TK_MATCH) { // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; advance(p); expecttok(p, tkind.TK_LPAREN, "expected '(' after match"); let m: *node = newnode(p.a, nkind.N_MATCH, pf, pl, pc); m.lhs = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after match scrutinee"); expecttok(p, tkind.TK_LBRACE, "expected '{' to open match body"); let head: *node = nil; let tail: *node = nil; for (p.curkind == tkind.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, nkind.N_MCASE, cf, cl, cc); if (p.curkind == tkind.TK_LET) { advance(p); let id: str; expectident(p, &id); mc.str = id; expecttok(p, tkind.TK_COLON, "expected ':' after match binding"); mc.lhs = parsetype(p); } else { if (p.curkind != tkind.TK_FATARROW) { mc.lhs = parsetype(p); };}; expecttok(p, tkind.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, tkind.TK_RBRACE, "expected '}' after match body"); m.list = head; return m; }; errmsg(p, "expected expression"); advance(p); return newnode(p.a, nkind.N_NONE, pf, pl, pc); }; fn parsearglist(p: *parser, closekind: tkind, 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); // Hare-style spread: `expr...` in an arg slot becomes a // marker the callee/builtin can iterate over. Mirrors // cmd/wcc/parse.c. The only consumer today is `append`. if (accepttok(p, tkind.TK_ELLIPSIS)) { let sp: *node = newnode(p.a, nkind.N_SPREAD, e.file, e.line, e.col); sp.lhs = e; e = sp; }; if (head == nil) { head = e; tail = e; } else { tail.next = e; tail = e; }; if (!accepttok(p, tkind.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 == tkind.TK_LPAREN) { advance(p); let n: *node = newnode(p.a, nkind.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 == nkind.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, tkind.TK_RPAREN, &arghead); n.list = arghead; }; expecttok(p, tkind.TK_RPAREN, "expected ')' after args"); cur = n; continue; }; if (p.curkind == tkind.TK_LBRACK) { advance(p); // `[ : hi ]` — slice with implicit lo = 0. if (p.curkind == tkind.TK_COLON) { advance(p); let n: *node = newnode(p.a, nkind.N_SLICE, pf, pl, pc); n.lhs = cur; if (p.curkind != tkind.TK_RBRACK) { n.cond = parseexpr(p); }; expecttok(p, tkind.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 == tkind.TK_COLON) { advance(p); let n: *node = newnode(p.a, nkind.N_SLICE, pf, pl, pc); n.lhs = cur; n.rhs = e; if (p.curkind != tkind.TK_RBRACK) { n.cond = parseexpr(p); }; expecttok(p, tkind.TK_RBRACK, "expected ']' in slice"); cur = n; continue; }; let n: *node = newnode(p.a, nkind.N_INDEX, pf, pl, pc); n.lhs = cur; n.rhs = e; expecttok(p, tkind.TK_RBRACK, "expected ']' after index"); cur = n; continue; }; if (p.curkind == tkind.TK_DOT) { advance(p); let n: *node = newnode(p.a, nkind.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 == tkind.TK_INT) { n.str = p.curtext; advance(p); } else { let id: str; expectident(p, &id); n.str = id; }; cur = n; continue; }; if (p.curkind == tkind.TK_COLON) { if (p.nocast != 0) { return cur; }; advance(p); let n: *node = newnode(p.a, nkind.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 == tkind.TK_AS) { advance(p); let n: *node = newnode(p.a, nkind.N_TYPEASSERT, pf, pl, pc); n.lhs = cur; n.rhs = parsetype(p); cur = n; continue; }; if (p.curkind == tkind.TK_IS) { advance(p); let n: *node = newnode(p.a, nkind.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 == tkind.TK_QUESTION) { advance(p); let n: *node = newnode(p.a, nkind.N_TRYPROP, pf, pl, pc); n.lhs = cur; cur = n; continue; }; if (p.curkind == tkind.TK_NOT) { advance(p); let n: *node = newnode(p.a, nkind.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: tkind = p.curkind; if (k == tkind.TK_MINUS) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.TK_MINUS; n.lhs = parseunary(p); return n; }; if (k == tkind.TK_PLUS) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.TK_PLUS; n.lhs = parseunary(p); return n; }; if (k == tkind.TK_NOT) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.TK_NOT; n.lhs = parseunary(p); return n; }; if (k == tkind.TK_TILDE) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.TK_TILDE; n.lhs = parseunary(p); return n; }; if (k == tkind.TK_STAR) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.TK_STAR; n.lhs = parseunary(p); return n; }; if (k == tkind.TK_AMP) { advance(p); let n: *node = newnode(p.a, nkind.N_UN, pf, pl, pc); n.op = tkind.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: tkind = 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, nkind.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: tkind = p.curkind; advance(p); let n: *node = newnode(p.a, nkind.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 = tkind.TK_CONST. let is_const: i32 = 0; if (p.curkind == tkind.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 == tkind.TK_LPAREN) { advance(p); let m: *node = newnode(p.a, nkind.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, nkind.N_LET, lpf, lpl, lpc); let id: str; expectbindname(p, &id); l.str = id; if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); }; if (head == nil) { head = l; } else { tail.next = l; }; tail = l; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in let destructure"); expecttok(p, tkind.TK_ASSIGN, "expected '=' after let destructure"); m.rhs = parseexpr(p); expecttok(p, tkind.TK_SEMI, "expected ';' after let"); m.list = head; if (is_const != 0) { m.op = tkind.TK_CONST; let lc: *node = head; for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; }; }; return m; }; let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc); let id: str; expectbindname(p, &id); n.str = id; if (accepttok(p, tkind.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 nkind.N_LET; the wrapping nkind.N_MLET carries the rhs. if (p.curkind == tkind.TK_COMMA) { let m: *node = newnode(p.a, nkind.N_MLET, pf, pl, pc); let head: *node = n; let tail: *node = n; for (accepttok(p, tkind.TK_COMMA)) { let lpf: str = p.curfile; let lpl: i32 = p.curline; let lpc: i32 = p.curcol; let l: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc); let id2: str; expectbindname(p, &id2); l.str = id2; if (accepttok(p, tkind.TK_COLON)) { l.lhs = parsetype(p); }; tail.next = l; tail = l; }; expecttok(p, tkind.TK_ASSIGN, "expected '=' after let names"); m.rhs = parseexpr(p); expecttok(p, tkind.TK_SEMI, "expected ';' after let"); m.list = head; if (is_const != 0) { m.op = tkind.TK_CONST; let lc: *node = head; for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; }; }; return m; }; if (accepttok(p, tkind.TK_ASSIGN)) { n.rhs = parseexpr(p); }; expecttok(p, tkind.TK_SEMI, "expected ';' after let"); if (is_const != 0) { n.op = tkind.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, tkind.TK_LBRACE, "expected '{' to open block"); let blk: *node = newnode(p.a, nkind.N_BLOCK, pf, pl, pc); let head: *node = nil; let tail: *node = nil; for (p.curkind != tkind.TK_RBRACE) { if (p.curkind == tkind.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, tkind.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, tkind.TK_LPAREN, "expected '(' after if"); let n: *node = newnode(p.a, nkind.N_IF, pf, pl, pc); n.cond = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after if condition"); n.body = parseblock(p); if (accepttok(p, tkind.TK_ELSE)) { if (p.curkind == tkind.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, tkind.TK_LPAREN, "expected '(' after for"); // Four forms (matching C parser): // for (cond) — only cond // for (init; cond; post) — C-style 3-clause // for (let x .. expr) — Hare-style range, single binding // for (let (a, b) .. expr) — range with tuple destructure // Range and 3-clause both lead with `let`, so we commit to consuming // `let` then disambiguate by looking at what follows. if (p.curkind == tkind.TK_LET) { advance(p); // past `let` // Tuple destructure: `for (let (a, b) .. expr)`. if (p.curkind == tkind.TK_LPAREN) { advance(p); let names: *node = nil; let ntail: *node = nil; for (true) { let npf: str = p.curfile; let npl: i32 = p.curline; let npc: i32 = p.curcol; let e: *node = newnode(p.a, nkind.N_IDENT, npf, npl, npc); let nm: str; expectbindname(p, &nm); e.str = nm; if (names == nil) { names = e; } else { ntail.next = e; }; ntail = e; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in for-range names"); expecttok(p, tkind.TK_DOTDOT, "expected '..' after for-range names"); let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); rng.list = names; rng.lhs = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); rng.body = parseblock(p); if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; return rng; }; // Single binding range or C-style let-init. We need to consume // the IDENT/UNDER to know which: if followed by '..' it's a // range; otherwise build a synthetic LET for the C-style for-init // with the consumed name baked in. if (p.curkind == tkind.TK_IDENT || p.curkind == tkind.TK_UNDER) { let isunder: bool = (p.curkind == tkind.TK_UNDER); let nm: str; nm.ptr = nil; nm.len = 0; if (!isunder) { nm = p.curtext; }; let lpf: str = p.curfile; let lpl: i32 = p.curline; let lpc: i32 = p.curcol; advance(p); // consume IDENT/UNDER if (p.curkind == tkind.TK_DOTDOT) { advance(p); let rng: *node = newnode(p.a, nkind.N_FORRANGE, pf, pl, pc); rng.str = nm; // "" for `_` rng.lhs = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); rng.body = parseblock(p); if (accepttok(p, tkind.TK_ELSE)) { rng.els = parseblock(p); }; return rng; }; // Not a range — finish the let manually and continue as // a 3-clause for-init. let first: *node = newnode(p.a, nkind.N_LET, lpf, lpl, lpc); first.str = nm; if (accepttok(p, tkind.TK_COLON)) { first.lhs = parsetype(p); }; if (accepttok(p, tkind.TK_ASSIGN)) { first.rhs = parseexpr(p); }; expecttok(p, tkind.TK_SEMI, "expected ';' after for-init let"); let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); n.lhs = first; n.cond = parseexpr(p); expecttok(p, tkind.TK_SEMI, "expected ';' after for cond"); n.rhs = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after for"); n.body = parseblock(p); if (accepttok(p, tkind.TK_ELSE)) { n.els = parseblock(p); }; return n; }; errmsg(p, "expected name after 'let' in for"); }; // for (cond) or for (cond; post) let n: *node = newnode(p.a, nkind.N_FOR, pf, pl, pc); let first: *node = parseexpr(p); if (accepttok(p, tkind.TK_SEMI)) { n.cond = first; n.rhs = parseexpr(p); } else { n.cond = first; }; expecttok(p, tkind.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, tkind.TK_ELSE)) { n.els = parseblock(p); }; return n; }; fn parseswitch(p: *parser) *node = { let pf: str = p.curfile; let pl: i32 = p.curline; let pc: i32 = p.curcol; advance(p); // past `switch` expecttok(p, tkind.TK_LPAREN, "expected '(' after switch"); let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc); n.lhs = parseexpr(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression"); expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body"); let head: *node = nil; let tail: *node = nil; for (p.curkind == tkind.TK_CASE) { let cpf: str = p.curfile; let cpl: i32 = p.curline; let cpc: i32 = p.curcol; advance(p); // past `case` let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc); let eh: *node = nil; let et: *node = nil; if (p.curkind != tkind.TK_COLON) { p.nocast = 1; for (true) { let e: *node = parseexpr(p); if (eh == nil) { eh = e; } else { et.next = e; }; et = e; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; p.nocast = 0; }; cs.list = eh; expecttok(p, tkind.TK_COLON, "expected ':' after case label"); let bh: *node = nil; let bt: *node = nil; for (p.curkind != tkind.TK_CASE) { if (p.curkind == tkind.TK_RBRACE) { break; }; if (p.curkind == tkind.TK_EOF) { break; }; let s: *node = parsestmt(p); if (s != nil) { if (bh == nil) { bh = s; } else { bt.next = s; }; bt = s; }; }; let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc); blk.list = bh; cs.body = blk; if (head == nil) { head = cs; } else { tail.next = cs; }; tail = cs; }; expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch"); n.list = head; 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 == tkind.TK_STATIC) { advance(p); }; if (p.curkind == tkind.TK_LBRACE) { let b: *node = parseblock(p); expecttok(p, tkind.TK_SEMI, "expected ';' after block"); return b; }; if (p.curkind == tkind.TK_LET) { return parseletlocal(p); }; if (p.curkind == tkind.TK_CONST) { return parseletlocal(p); }; if (p.curkind == tkind.TK_IF) { let n: *node = parseif(p); expecttok(p, tkind.TK_SEMI, "expected ';' after if"); return n; }; if (p.curkind == tkind.TK_FOR) { let n: *node = parsefor(p); expecttok(p, tkind.TK_SEMI, "expected ';' after for"); return n; }; if (p.curkind == tkind.TK_SWITCH) { let n: *node = parseswitch(p); expecttok(p, tkind.TK_SEMI, "expected ';' after switch"); return n; }; if (p.curkind == tkind.TK_RETURN) { advance(p); let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc); if (p.curkind != tkind.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 == tkind.TK_COMMA) { let t: *node = newnode(p.a, nkind.N_TUPLE, pf, pl, pc); t.list = first; let tail: *node = first; for (accepttok(p, tkind.TK_COMMA)) { let e: *node = parseexpr(p); tail.next = e; tail = e; }; n.lhs = t; } else { n.lhs = first; }; }; expecttok(p, tkind.TK_SEMI, "expected ';' after return"); return n; }; if (p.curkind == tkind.TK_DEFER) { advance(p); let n: *node = newnode(p.a, nkind.N_DEFER, pf, pl, pc); n.lhs = parseexpr(p); expecttok(p, tkind.TK_SEMI, "expected ';' after defer"); return n; }; if (p.curkind == tkind.TK_YIELD) { advance(p); let n: *node = newnode(p.a, nkind.N_YIELD, pf, pl, pc); n.lhs = parseexpr(p); expecttok(p, tkind.TK_SEMI, "expected ';' after yield"); return n; }; if (p.curkind == tkind.TK_BREAK) { advance(p); expecttok(p, tkind.TK_SEMI, "expected ';' after break"); return newnode(p.a, nkind.N_BREAK, pf, pl, pc); }; if (p.curkind == tkind.TK_CONTINUE) { advance(p); expecttok(p, tkind.TK_SEMI, "expected ';' after continue"); return newnode(p.a, nkind.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 == tkind.TK_COMMA) { let m: *node = newnode(p.a, nkind.N_MASSIGN, pf, pl, pc); let head: *node = e; let tail: *node = e; for (p.curkind == tkind.TK_COMMA) { advance(p); let lv: *node = parsebin(p, parseunary(p), 1); tail.next = lv; tail = lv; }; expecttok(p, tkind.TK_ASSIGN, "expected '=' after multi-assign lvalues"); m.rhs = parseexpr(p); m.list = head; expecttok(p, tkind.TK_SEMI, "expected ';' after multi-assign"); return m; }; let n: *node = newnode(p.a, nkind.N_EXPRSTMT, pf, pl, pc); n.lhs = e; expecttok(p, tkind.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, nkind.N_USE, pf, pl, pc); let id: str; expectident(p, &id); n.str = id; expecttok(p, tkind.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, nkind.N_DEF, pf, pl, pc); n.module = p.l.module; let id: str; expectident(p, &id); n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in def"); n.lhs = parsetype(p); expecttok(p, tkind.TK_ASSIGN, "expected '=' in def"); n.rhs = parseexpr(p); expecttok(p, tkind.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 = tkind.TK_CONST so the checker can reject reassignment. let is_const: i32 = 0; if (p.curkind == tkind.TK_CONST) { is_const = 1; }; advance(p); let n: *node = newnode(p.a, nkind.N_LET, pf, pl, pc); n.module = p.l.module; let id: str; expectbindname(p, &id); n.str = id; if (accepttok(p, tkind.TK_COLON)) { n.lhs = parsetype(p); }; if (accepttok(p, tkind.TK_ASSIGN)) { n.rhs = parseexpr(p); }; expecttok(p, tkind.TK_SEMI, "expected ';' after let"); n.exported = exported; if (is_const != 0) { n.op = tkind.TK_CONST; }; return n; }; fn parseattrs(p: *parser) *node = { let head: *node = nil; let tail: *node = nil; for (p.curkind == tkind.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, nkind.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, tkind.TK_LPAREN)) { let arghead: *node = nil; parsearglist(p, tkind.TK_RPAREN, &arghead); a.list = arghead; expecttok(p, tkind.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 == tkind.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, nkind.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, tkind.TK_COLON, "expected ':' in parameter"); n.lhs = parsetype(p); // Hare-style variadic: `name: T...`. Marker on n.op so check // promotes the param's type to []T and call sites gather / // forward. Mirrors cmd/wcc/parse.c parseparams. if (accepttok(p, tkind.TK_ELLIPSIS)) { n.op = tkind.TK_ELLIPSIS; }; if (head == nil) { head = n; tail = n; } else { tail.next = n; tail = n; }; if (n.op == tkind.TK_ELLIPSIS) { break; // variadic must be the last param }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; if (p.curkind == tkind.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, nkind.N_FNDECL, pf, pl, pc); n.module = p.l.module; let id: str; expectident(p, &id); n.str = id; expecttok(p, tkind.TK_LPAREN, "expected '(' after fn name"); n.list = parseparams(p); expecttok(p, tkind.TK_RPAREN, "expected ')' after params"); if (p.curkind != tkind.TK_ASSIGN) { if (p.curkind != tkind.TK_SEMI) { n.lhs = parsetype(p); }; }; if (accepttok(p, tkind.TK_ASSIGN)) { n.body = parseblock(p); expecttok(p, tkind.TK_SEMI, "expected ';' after fn body"); } else { // Body-less fn: FFI declaration (`fn name(args) ret;`). expecttok(p, tkind.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, nkind.N_TYPEDECL, pf, pl, pc); n.module = p.l.module; let id: str; expectident(p, &id); n.str = id; expecttok(p, tkind.TK_ASSIGN, "expected '=' in type decl"); n.lhs = parsetype(p); expecttok(p, tkind.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: tkind, curfile: str, curline: i32, curcol: i32, curtext: str, curuval: u64, curfval: f64, // curtsuffix: typed numeric literal suffix ("i32", "u64", ...) on // the current TK_INT / TK_FLOAT token, or empty. Parseprimary // copies this onto the N_INTLIT / N_FLOATLIT node so cgen's // rhstargetname can map `42i64` to the i64 variant of a tagged // union without falling back to "first non-str variant" (which // silently picked tag 0 for typed-int literals; see #10). curtsuffix: str, }; 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; p.curfval = t.fval; p.curtsuffix = t.tsuffix; }; 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: tkind) 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: tkind, what: str) bool = { if (p.curkind == k) { advance(p); return true; }; errmsg(p, what); return false; }; // expectident — consume the current tkind.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 != tkind.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 == tkind.TK_UNDER) { *into = ""; 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 == tkind.TK_NOT) { // `!T` — Hare error-flagged type wrapper. advance(p); let n: *node = newnode(p.a, nkind.N_TBANG, pf, pl, pc); n.lhs = parsetype(p); return n; }; if (p.curkind == tkind.TK_STAR) { advance(p); let n: *node = newnode(p.a, nkind.N_TPTR, pf, pl, pc); n.lhs = parsetype(p); return n; }; if (p.curkind == tkind.TK_LBRACK) { advance(p); if (p.curkind == tkind.TK_RBRACK) { advance(p); let n: *node = newnode(p.a, nkind.N_TSLICE, pf, pl, pc); n.lhs = parsetype(p); return n; }; let n: *node = newnode(p.a, nkind.N_TARRAY, pf, pl, pc); // `[_]T` — length inferred from initialiser. n.rhs stays nil // as the sentinel; the cgen path for nkind.N_LET fills it from the // array literal's element count. if (p.curkind == tkind.TK_UNDER) { advance(p); } else { n.rhs = parseexpr(p); }; expecttok(p, tkind.TK_RBRACK, "expected ']' in array type"); n.lhs = parsetype(p); return n; }; if (p.curkind == tkind.TK_STRUCT) { advance(p); expecttok(p, tkind.TK_LBRACE, "expected '{' after struct"); let n: *node = newnode(p.a, nkind.N_TSTRUCT, pf, pl, pc); let fhead: *node = nil; let ftail: *node = nil; for (p.curkind != tkind.TK_RBRACE) { if (p.curkind == tkind.TK_EOF) { break; }; let fpf: str = p.curfile; let fpl: i32 = p.curline; let fpc: i32 = p.curcol; let f: *node = newnode(p.a, nkind.N_TFIELD, fpf, fpl, fpc); let fid: str; expectident(p, &fid); f.str = fid; expecttok(p, tkind.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, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RBRACE, "expected '}' after struct fields"); n.list = fhead; return n; }; if (p.curkind == tkind.TK_ENUM) { // `enum [storage] { NAME [= expr], ... }` // Storage defaults to i32 (lhs == nil). Each member is an // nkind.N_TENUMMEMBER with str=name and lhs = value expr or nil // (auto-increment when omitted). advance(p); let n: *node = newnode(p.a, nkind.N_TENUM, pf, pl, pc); if (p.curkind != tkind.TK_LBRACE) { n.lhs = parsetype(p); }; expecttok(p, tkind.TK_LBRACE, "expected '{' after enum"); let mhead: *node = nil; let mtail: *node = nil; for (p.curkind != tkind.TK_RBRACE) { if (p.curkind == tkind.TK_EOF) { break; }; let mpf: str = p.curfile; let mpl: i32 = p.curline; let mpc: i32 = p.curcol; let m: *node = newnode(p.a, nkind.N_TENUMMEMBER, mpf, mpl, mpc); let mid: str; expectident(p, &mid); m.str = mid; if (accepttok(p, tkind.TK_ASSIGN)) { m.lhs = parseexpr(p); }; if (mhead == nil) { mhead = m; mtail = m; } else { mtail.next = m; mtail = m; }; if (!accepttok(p, tkind.TK_COMMA)) { break; }; }; expecttok(p, tkind.TK_RBRACE, "expected '}' after enum members"); n.list = mhead; return n; }; if (p.curkind == tkind.TK_VOID) { // `void` keyword in type-expr context — emit as nkind.N_TNAME so // resolution treats it like any other primitive name. let n: *node = newnode(p.a, nkind.N_TNAME, pf, pl, pc); n.str = "void"; advance(p); return n; }; if (p.curkind == tkind.TK_IDENT) { let n: *node = newnode(p.a, nkind.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 == tkind.TK_DOT) { advance(p); if (p.curkind != tkind.TK_IDENT) { break; }; acc = joindotted(p.a, acc, p.curtext); advance(p); }; n.str = acc; return n; }; if (p.curkind == tkind.TK_LPAREN) { // (T) or (T, T, ...) or (T | T | ...) // // Each tagged variant may be prefixed with `...` to mark a // spread — when the variant resolves to another tagged union // its variants are flattened into the enclosing union. We // tag the spread on node.op = TK_ELLIPSIS so resolve_type // can distinguish intent. Mirrors C parsetype. advance(p); let firstspread: bool = accepttok(p, tkind.TK_ELLIPSIS); let first: *node = parsetype(p); if (firstspread) { first.op = tkind.TK_ELLIPSIS; }; if (accepttok(p, tkind.TK_PIPE)) { let n: *node = newnode(p.a, nkind.N_TTAGGED, pf, pl, pc); let head: *node = first; let tail: *node = first; for (true) { let spread: bool = accepttok(p, tkind.TK_ELLIPSIS); let e: *node = parsetype(p); if (spread) { e.op = tkind.TK_ELLIPSIS; }; tail.next = e; tail = e; if (!accepttok(p, tkind.TK_PIPE)) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tagged-union type"); n.list = head; return n; }; if (firstspread) { errmsg(p, "spread '...' only valid before tagged-union variants"); }; if (!accepttok(p, tkind.TK_COMMA)) { expecttok(p, tkind.TK_RPAREN, "expected ')' after parenthesised type"); return first; }; let n: *node = newnode(p.a, nkind.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, tkind.TK_COMMA)) { break; }; if (p.curkind == tkind.TK_RPAREN) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple type"); n.list = head; return n; }; if (p.curkind == tkind.TK_FN) { advance(p); expecttok(p, tkind.TK_LPAREN, "expected '(' after fn in type"); let n: *node = newnode(p.a, nkind.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, tkind.TK_RPAREN, "expected ')' after fn type params"); n.lhs = parsetype(p); return n; }; errmsg(p, "expected type"); advance(p); return newnode(p.a, nkind.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: tkind) i32 = { if (k == tkind.TK_OR) { return 1; }; if (k == tkind.TK_AND) { return 2; }; if (k == tkind.TK_EQ) { return 3; }; if (k == tkind.TK_NEQ) { return 3; }; if (k == tkind.TK_LT) { return 4; }; if (k == tkind.TK_LE) { return 4; }; if (k == tkind.TK_GT) { return 4; }; if (k == tkind.TK_GE) { return 4; }; if (k == tkind.TK_PIPE) { return 5; }; if (k == tkind.TK_CARET) { return 6; }; if (k == tkind.TK_AMP) { return 7; }; if (k == tkind.TK_LSHIFT) { return 8; }; if (k == tkind.TK_RSHIFT) { return 8; }; if (k == tkind.TK_PLUS) { return 9; }; if (k == tkind.TK_MINUS) { return 9; }; if (k == tkind.TK_STAR) { return 10; }; if (k == tkind.TK_SLASH) { return 10; }; if (k == tkind.TK_PERCENT) { return 10; }; return 0; }; fn isassignop(k: tkind) bool = { if (k == tkind.TK_ASSIGN) { return true; }; if (k == tkind.TK_PLUSEQ) { return true; }; if (k == tkind.TK_MINUSEQ) { return true; }; if (k == tkind.TK_STAREQ) { return true; }; if (k == tkind.TK_SLASHEQ) { return true; }; if (k == tkind.TK_PERCENTEQ) { return true; }; if (k == tkind.TK_AMPEQ) { return true; }; if (k == tkind.TK_PIPEEQ) { return true; }; if (k == tkind.TK_CARETEQ) { return true; }; if (k == tkind.TK_LSHIFTEQ) { return true; }; if (k == tkind.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, nkind.N_FILE, p.curfile, p.curline, p.curcol); let head: *node = nil; let tail: *node = nil; for (p.curkind != tkind.TK_EOF) { let attrs: *node = parseattrs(p); let exported: i32 = 0; if (p.curkind == tkind.TK_EXPORT) { exported = 1; advance(p); }; let d: *node = nil; if (p.curkind == tkind.TK_USE) { d = parseuse(p); } else { if (p.curkind == tkind.TK_DEF) { d = parsedef(p, exported); } else { if (p.curkind == tkind.TK_TYPE) { d = parsetypedecl(p, exported); } else { if (p.curkind == tkind.TK_LET) { d = parselet(p, exported); } else { if (p.curkind == tkind.TK_CONST) { d = parselet(p, exported); } else { if (p.curkind == tkind.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 != tkind.TK_SEMI) { if (p.curkind == tkind.TK_EOF) { break; }; if (p.curkind == tkind.TK_LBRACE) { let depth: i32 = 0; for (true) { if (p.curkind == tkind.TK_EOF) { break; }; if (p.curkind == tkind.TK_LBRACE) { depth += 1; advance(p); continue; }; if (p.curkind == tkind.TK_RBRACE) { depth -= 1; advance(p); if (depth == 0) { break; }; continue; }; advance(p); }; continue; }; advance(p); }; if (p.curkind == tkind.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. // Mirror of the C `TypeKind` enum in cmd/wcc/ww.h. Numeric values // are explicit and must stay in sync — the selfhost selfcheck and // typed-AST printers depend on matching numeric layout. type tykind = enum i32 { TY_NONE = 0, TY_VOID = 1, TY_BOOL = 2, TY_RUNE = 3, TY_I8 = 4, TY_I16 = 5, TY_I32 = 6, TY_I64 = 7, TY_U8 = 8, TY_U16 = 9, TY_U32 = 10, TY_U64 = 11, TY_UINT = 12, TY_INT = 13, TY_UINTPTR = 14, TY_F32 = 15, TY_F64 = 16, TY_STR = 17, TY_PTR = 18, TY_SLICE = 19, TY_ARRAY = 20, TY_STRUCT = 21, TY_FN = 22, TY_CHAN = 23, TY_NAMED = 24, TY_TUPLE = 25, TY_TAGGED = 26, TY_ERR = 27, TY_NEVER = 28, TY_UNTYPED_INT = 29, TY_UNTYPED_FLOAT = 30, TY_UNTYPED_STR = 31, TY_UNTYPED_RUNE = 32, TY_UNTYPED_BOOL = 33, TY_UNTYPED_NIL = 34, // Tail-appended values keep prior TY_* stable for the byte-diff // against cmd/wcc/ww.h. TY_ENUM = 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: tykind, 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: tykind) *tinfo = { let t: *tinfo = amalloc(a, 96u64): *tinfo; t.kind = k; return t; }; fn prim(a: *arena, k: tykind, 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, tykind.TY_VOID, "void", 0u64, 1u64); c.tybool = prim(a, tykind.TY_BOOL, "bool", 1u64, 1u64); c.tyrune = prim(a, tykind.TY_RUNE, "rune", 4u64, 4u64); c.tyi8 = prim(a, tykind.TY_I8, "i8", 1u64, 1u64); c.tyi16 = prim(a, tykind.TY_I16, "i16", 2u64, 2u64); c.tyi32 = prim(a, tykind.TY_I32, "i32", 4u64, 4u64); c.tyi64 = prim(a, tykind.TY_I64, "i64", 8u64, 8u64); c.tyu8 = prim(a, tykind.TY_U8, "u8", 1u64, 1u64); c.tyu16 = prim(a, tykind.TY_U16, "u16", 2u64, 2u64); c.tyu32 = prim(a, tykind.TY_U32, "u32", 4u64, 4u64); c.tyu64 = prim(a, tykind.TY_U64, "u64", 8u64, 8u64); c.tyint = prim(a, tykind.TY_INT, "int", 8u64, 8u64); c.tyuint = prim(a, tykind.TY_UINT, "uint", 8u64, 8u64); c.tyuintptr= prim(a, tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tyf32 = prim(a, tykind.TY_F32, "f32", 4u64, 4u64); c.tyf64 = prim(a, tykind.TY_F64, "f64", 8u64, 8u64); c.tystr = prim(a, tykind.TY_STR, "str", 16u64, 8u64); c.tyerr = prim(a, tykind.TY_ERR, "", 0u64, 1u64); c.tynever = prim(a, tykind.TY_NEVER, "never", 0u64, 1u64); c.tyuntypedint = prim(a, tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); c.tyuntypedfloat = prim(a, tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); c.tyuntypedstr = prim(a, tykind.TY_UNTYPED_STR, "untyped_str", 0u64, 1u64); c.tyuntypedrune = prim(a, tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); c.tyuntypedbool = prim(a, tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); c.tyuntypednil = prim(a, tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); }; export fn typeptr(a: *arena, sub: *tinfo) *tinfo = { let t: *tinfo = newtype(a, tykind.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, tykind.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, tykind.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, tykind.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, tykind.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: tykind = t.kind; if (k == tykind.TY_I8) { return true; }; if (k == tykind.TY_I16) { return true; }; if (k == tykind.TY_I32) { return true; }; if (k == tykind.TY_I64) { return true; }; if (k == tykind.TY_U8) { return true; }; if (k == tykind.TY_U16) { return true; }; if (k == tykind.TY_U32) { return true; }; if (k == tykind.TY_U64) { return true; }; if (k == tykind.TY_INT) { return true; }; if (k == tykind.TY_UINT){ return true; }; if (k == tykind.TY_UINTPTR) { return true; }; if (k == tykind.TY_RUNE){ return true; }; if (k == tykind.TY_UNTYPED_INT) { return true; }; if (k == tykind.TY_UNTYPED_RUNE) { return true; }; if (k == tykind.TY_ENUM) { return typeisint(t.sub); }; if (k == tykind.TY_NAMED) { return typeisint(t.under); }; return false; }; export fn typeisfloat(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_F32) { return true; }; if (k == tykind.TY_F64) { return true; }; if (k == tykind.TY_UNTYPED_FLOAT) { return true; }; if (k == tykind.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: tykind = t.kind; if (k == tykind.TY_U8) { return true; }; if (k == tykind.TY_U16) { return true; }; if (k == tykind.TY_U32) { return true; }; if (k == tykind.TY_U64) { return true; }; if (k == tykind.TY_UINT){ return true; }; if (k == tykind.TY_UINTPTR) { return true; }; if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); }; return false; }; export fn typeisuntyped(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_UNTYPED_INT) { return true; }; if (k == tykind.TY_UNTYPED_FLOAT) { return true; }; if (k == tykind.TY_UNTYPED_STR) { return true; }; if (k == tykind.TY_UNTYPED_RUNE) { return true; }; if (k == tykind.TY_UNTYPED_BOOL) { return true; }; if (k == tykind.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: tykind = a.kind; if (k == tykind.TY_PTR) { return typeeq(a.sub, b.sub); }; if (k == tykind.TY_SLICE) { return typeeq(a.sub, b.sub); }; if (k == tykind.TY_CHAN) { return typeeq(a.sub, b.sub); }; if (k == tykind.TY_ARRAY) { if (a.alen != b.alen) { return false; }; return typeeq(a.sub, b.sub); }; if (k == tykind.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 == tykind.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 == tykind.TY_NAMED) { return false; }; // nominal: only same ptr if (k == tykind.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. type skind = enum i32 { SK_NONE = 0, SK_VAR = 1, SK_PARAM = 2, SK_DEF = 3, SK_TYPE = 4, SK_FN = 5, SK_USE = 6, SK_FIELD = 7, }; type sym = struct { name: str, skind: skind, type_: *tinfo, decl: *node, exported: i32, is_const: i32, // const-bound (assignment rejected) mod: str, // importing module's bareword for symbols // from a `use`-imported module; "" for primary // (root) compilation unit symbols. Used by // scopelookupinmodule to disambiguate same-leaf- // name types coming from different imports. 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; }; // scopelookupinmodule — module-filtered chain walk. // // Same FNV bucket + hashnext chain + parent walk as scopelookup, plus // a `b.mod.len > 0 && streq(b.mod, mod)` filter. When `mod` is empty // we fall back to unfiltered scopelookup semantics, so callers that // don't care about disambiguation get the default. // // Used by the dot-prefixed type-name lookup in selfhost/cmd/wcc/ // check.ww to pick the right same-leaf-name type when two imports // each export it (`bufio.stream` vs `io.stream`). export fn scopelookupinmodule(s: *scope, mod: str, name: str) *sym = { if (mod.len == 0) { return scopelookup(s, name); }; for (s != nil) { let h: u64 = hashstr(name); let bi: i32 = (h % (s.nbuckets: u64)): i32; let b: *sym = s.buckets[bi]; for (b != nil) { if (streq(b.name, name)) { if (b.mod.len > 0) { if (streq(b.mod, mod)) { return b; }; }; }; b = b.hashnext; }; s = s.parent; }; return nil; }; // scopelookupprefer — bare-leaf lookup with same-module preference. // // Walks the same FNV bucket + hashnext chain + parent walk scopelookup // uses. Within each scope's bucket: Pass 1 prefers entries whose // `sym.mod` matches `mod`; Pass 2 falls back to the first match // regardless of mod (same semantics as scopelookup). We only descend // to the parent scope when the current scope has no matching entry at // all — so a local binding in a closer scope still shadows a same-name // fn from a parent scope, even when the parent entry mod-matches. // // When `mod` is empty we just call scopelookup — there's no module // identity to prefer. // // Used at bare-leaf lookup sites inside a known current module so that // a bare `read` inside lib/os resolves to os.read rather than the // io.read that happens to hash earlier into the flat scope. Mirrors // cmd/wcc/sym.c scope_lookup_prefer. export fn scopelookupprefer(s: *scope, mod: str, name: str) *sym = { if (mod.len == 0) { return scopelookup(s, name); }; let p: *scope = s; for (p != nil) { let h: u64 = hashstr(name); let bi: i32 = (h % (p.nbuckets: u64)): i32; let b: *sym = p.buckets[bi]; let fallback: *sym = nil; for (b != nil) { if (streq(b.name, name)) { if (b.mod.len > 0) { if (streq(b.mod, mod)) { return b; }; }; if (fallback == nil) { fallback = b; }; }; b = b.hashnext; }; if (fallback != nil) { return fallback; }; p = p.parent; }; return nil; }; export fn scopedefine(s: *scope, name: str, k: skind, t: *tinfo, decl: *node) *sym = { let empty: str; return scopedefineinmodule(s, name, empty, k, t, decl); }; // scopedefineinmodule — bucket insert with per-mod dedup. // // Same insertion as scopedefine, but the duplicate-rejection key is // (name, mod) rather than name alone. This lets two imports each // register their own `stream` SK_TYPE in the flat scope, and lets the // primary register `stream` (mod="") alongside imported `stream`s. // // Within a single (name, mod) pair the first registration wins; later // attempts return nil and the caller can flag the error. export fn scopedefineinmodule(s: *scope, name: str, mod: str, k: skind, t: *tinfo, decl: *node) *sym = { let h: u64 = hashstr(name); let bi: i32 = (h % (s.nbuckets: u64)): i32; let b: *sym = s.buckets[bi]; for (b != nil) { if (streq(b.name, name)) { if (b.mod.len == 0) { if (mod.len == 0) { return nil; }; } else { if (mod.len > 0) { if (streq(b.mod, mod)) { return nil; }; }; }; }; b = b.hashnext; }; let sy: *sym = amalloc(s.a, 112u64): *sym; sy.name = name; sy.skind = k; sy.type_ = t; sy.decl = decl; sy.mod = mod; sy.scope = s; 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 nkind.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 `?`) curmod: str, // importing-module bareword for the decl // currently being walked; "" for primary // compilation unit. Drives same-module // preference in bare-leaf lookups. file: *node, // N_FILE root; used by checkmoduleshadow // to consult the declaring source's own // `use` directives. }; // 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", skind.SK_TYPE, c.tc.tyvoid, nil); scopedefine(c.top, "bool", skind.SK_TYPE, c.tc.tybool, nil); scopedefine(c.top, "rune", skind.SK_TYPE, c.tc.tyrune, nil); scopedefine(c.top, "i8", skind.SK_TYPE, c.tc.tyi8, nil); scopedefine(c.top, "i16", skind.SK_TYPE, c.tc.tyi16, nil); scopedefine(c.top, "i32", skind.SK_TYPE, c.tc.tyi32, nil); scopedefine(c.top, "i64", skind.SK_TYPE, c.tc.tyi64, nil); scopedefine(c.top, "u8", skind.SK_TYPE, c.tc.tyu8, nil); scopedefine(c.top, "u16", skind.SK_TYPE, c.tc.tyu16, nil); scopedefine(c.top, "u32", skind.SK_TYPE, c.tc.tyu32, nil); scopedefine(c.top, "u64", skind.SK_TYPE, c.tc.tyu64, nil); scopedefine(c.top, "int", skind.SK_TYPE, c.tc.tyint, nil); scopedefine(c.top, "uint", skind.SK_TYPE, c.tc.tyuint, nil); scopedefine(c.top, "uintptr", skind.SK_TYPE, c.tc.tyuintptr, nil); scopedefine(c.top, "f32", skind.SK_TYPE, c.tc.tyf32, nil); scopedefine(c.top, "f64", skind.SK_TYPE, c.tc.tyf64, nil); scopedefine(c.top, "str", skind.SK_TYPE, c.tc.tystr, nil); scopedefine(c.top, "never", skind.SK_TYPE, c.tc.tynever, nil); // `nil`, `true`, `false` are keywords — handled at the lex/parser // level, no symbol needed. // `len`, `alloc`, `free`, `append` are pseudo-builtins; scopedefine // them so their use sites resolve. The actual semantics live in cgen. scopedefine(c.top, "len", skind.SK_FN, nil, nil); scopedefine(c.top, "alloc", skind.SK_FN, nil, nil); scopedefine(c.top, "free", skind.SK_FN, nil, nil); scopedefine(c.top, "append", skind.SK_FN, nil, nil); }; // declmod — module-tag stamp for a top-level decl. // // The driver concatenates imported sources before the primary file and // emits `// MODULE: foo` directives the lexer pins onto each decl's // `module` field. We treat a decl as "imported" iff its module // directive matches some `use IDENT;` bareword in this compilation // unit. Primary-file decls return "" so they coexist (mod="") with // imported decls of the same leaf name in scopelookupinmodule. fn declmod(file: *node, d: *node) str = { let empty: str; if (d == nil) { return empty; }; if (d.module.len == 0) { return empty; }; if (file == nil) { return empty; }; let u: *node = file.list; for (u != nil) { if (u.kind == nkind.N_USE) { if (streq(u.str, d.module)) { return d.module; }; }; u = u.next; }; return empty; }; // srcimports — does the source file that contributed decl-module // `modtag` carry `use ;`? Mirrors cstage's src_imports — // `modtag.len == 0` means primary, matching declmod's empty-str // return for primary-source decls. fn srcimports(file: *node, modtag: str, name: str) bool = { if (file == nil) { return false; }; if (name.len == 0) { return false; }; let u: *node = file.list; for (u != nil) { if (u.kind == nkind.N_USE) { // Skip self-imports: lib/fmt/fmttest.ww carries // `use fmt;` while its module tag is also "fmt". // That directive doesn't introduce a foreign // module bareword and lib/fmt's own // `fn bsprintf(fmt: str, ...)` is not a shadow. if (u.module.len > 0) { if (streq(u.module, u.str)) { u = u.next; continue; }; }; let um: str = declmod(file, u); let m: bool = false; if (modtag.len == 0) { if (um.len == 0) { m = true; }; } else { if (streq(um, modtag)) { m = true; }; }; if (m) { if (streq(u.str, name)) { return true; }; }; }; u = u.next; }; return false; }; // checkmoduleshadow — enforce "value names and module names are // disjoint" at nested-scope binds. Mirrors cstage check_module_shadow // (cmd/wcc/check.c). Fires for fn params / lets / forrange iters / // mcase bindings whose name matches an in-scope `use foo;` import // declared in the same source file. Top-level decls are exempt // (their same-leaf-as-module pattern is the intentional coexistence // shape — `use fnmatch; fn fnmatch(...)` etc.). fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = { if (name.len == 0) { return; }; if (c.cur == c.top) { return; }; let seen: bool = false; let s: *scope = c.cur; for (s != nil) { let r: *sym = scopelookuplocal(s, name); if (r != nil) { if (r.skind == skind.SK_USE) { seen = true; s = nil; }; }; if (s != nil) { s = s.parent; }; }; if (!seen) { return; }; if (!srcimports(c.file, c.curmod, name)) { return; }; os.write(2, kindstr.ptr, kindstr.len: u64); os.write(2, " '".ptr, 2u64); os.write(2, name.ptr, name.len: u64); os.write(2, "' shadows imported module '".ptr, 27u64); os.write(2, name.ptr, name.len: u64); os.write(2, "'\n".ptr, 2u64); c.errs += 1; }; // 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. // // Architectural note: wwstage uses COEXISTENCE rather than the cstage // promote-SK_USE-in-place approach in cmd/wcc/check.c. SK_USE and any // same-leaf SK_TYPE/SK_FN/SK_DEF/SK_VAR live as separate entries in // the same scope-bucket, distinguished by `sym.mod`. The dot-prefix // lookup in resolvewalk + scopelookupinmodule's mod-filter already // disambiguate `fnmatch.flag` against an `fn fnmatch(...)` of the same // leaf — no `use_alias` flag needed. So the cstage L1722-class bug // (promotion missing use_alias) is structurally non-reachable here. // Don't port the use_alias flag from cstage without first re-reading // the architecture: adding a field to `sym` changes its size and risks // the wwstage cgen amalloc-undersize trap (rob-pike). #11 (wwstage // checkfile pass) will reconsider this when wwstage grows a real check // pass on the cgen path. fn installdecl(c: *checker, file: *node, d: *node) void = { if (d == nil) { return; }; let k: nkind = d.kind; let nm: str = d.str; let mod: str = declmod(file, d); if (k == nkind.N_USE) { scopedefine(c.top, nm, skind.SK_USE, nil, d); return; }; if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; }; if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; }; if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; }; if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; }; }; // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and // nkind.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: nkind = n.kind; // Typed checks fire on the way down so the scrutinee/operand // is examined before the arm bodies install new bindings. if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); }; if (k == nkind.N_TRYPROP) { checktryprop(c, n); }; if (k == nkind.N_TYPETEST) { checkisas(c, n); }; if (k == nkind.N_TYPEASSERT) { checkisas(c, n); }; if (k == nkind.N_LET) { checkletassign(c, n); }; if (k == nkind.N_RETURN) { checkretassign(c, n); }; // `use IDENT;` — name is a module label, not a free ident. if (k == nkind.N_USE) { return; }; if (k == nkind.N_IDENT) { let nm: str = n.str; if (nm.len > 0) { let s: *sym = scopelookupprefer(c.cur, c.curmod, 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 == nkind.N_TNAME) { let nm: str = n.str; if (nm.len > 0) { let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); // `pkg.Type` — strip the last dot prefix and look up // the leaf with a mod filter so same-leaf-name types // from different imports (`bufio.stream` vs // `io.stream`) disambiguate to the right one. // 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 = scopelookupinmodule(c.cur, head, 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; }; }; }; // `for (let x .. slice) body` / `for (let (a, b) .. slice) body` — // each binding name becomes a fresh local. Walk the slice expr first // so its idents resolve before the bindings shadow anything, then // install bindings and walk the body/else. if (k == nkind.N_FORRANGE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; if (n.list != nil) { let m: *node = n.list; for (m != nil) { let bnm: str = m.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, "binding"); scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); }; m = m.next; }; } else { let bnm: str = n.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, "binding"); scopedefine(c.cur, bnm, skind.SK_VAR, nil, n); }; }; if (n.body != nil) { resolvewalk(c, n.body); }; if (n.els != nil) { resolvewalk(c, n.els); }; return; }; // `match (e) { case let v: T => stmt; ... }` — the binding `v` // is declared by the case arm and visible inside its body. Push a // fresh scope so `case let e: str` doesn't collide with an outer // `let e: *T` (scopedefine drops same-scope dupes silently and // would leave references to `e` resolving to the outer type). // Mirrors cmd/wcc/check.c's newscope/saved-restore around cstmt. if (k == nkind.N_MCASE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; let outer: *scope = c.cur; c.cur = newscope(c.a, outer); let nm: str = n.str; if (nm.len > 0) { checkmoduleshadow(c, nm, "binding"); scopedefine(c.cur, nm, skind.SK_VAR, nil, n); }; if (n.body != nil) { resolvewalk(c, n.body); }; c.cur = outer; return; }; if (k == nkind.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 == nkind.N_FIELD) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; return; }; if (k == nkind.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 == nkind.N_LET) { let nm: str = n.str; if (nm.len > 0) { checkmoduleshadow(c, nm, "let"); scopedefine(c.cur, nm, skind.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 nkind.N_TBANG wrapper; leaves other nodes alone. fn unwrapbang(n: *node) *node = { if (n == nil) { return nil; }; if (n.kind == nkind.N_TBANG) { return n.lhs; }; return n; }; // resolvealias — if n is an nkind.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-nkind.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 != nkind.N_TNAME) { return cur; }; let s: *sym = scopelookup(c.cur, cur.str); if (s == nil) { return cur; }; if (s.skind != 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: nkind = aa.kind; if (k == nkind.N_TNAME) { return streq(aa.str, bb.str); }; if (k == nkind.N_TPTR) { return typeeqast(aa.lhs, bb.lhs); }; if (k == nkind.N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); }; if (k == nkind.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 nkind.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 == nkind.N_TBANG) { return true; }; if (v.kind == nkind.N_TNAME) { let s: *sym = scopelookup(c.cur, v.str); if (s != nil) { if (s.skind == skind.SK_TYPE) { if (s.decl != nil) { if (s.decl.lhs != nil) { if (s.decl.lhs.kind == nkind.N_TBANG) { return true; }; }; }; }; }; }; return false; }; // taggedhaserr — true iff any variant of `n` (assumed // nkind.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 nkind.N_IDENT (look up local/param's declared // type) and nkind.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 == nkind.N_IDENT) { let s: *sym = scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; // For nkind.N_LET / nkind.N_PARAM: declared type is decl.lhs. return s.decl.lhs; }; return nil; }; // mktname — fabricate an nkind.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, nkind.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: nkind = e.kind; if (k == nkind.N_INTLIT) { return mktname(c, "untyped_int"); }; if (k == nkind.N_FLOATLIT) { return mktname(c, "untyped_float"); }; if (k == nkind.N_STRLIT) { return mktname(c, "str"); }; if (k == nkind.N_RUNELIT) { return mktname(c, "rune"); }; if (k == nkind.N_TRUE) { return mktname(c, "bool"); }; if (k == nkind.N_FALSE) { return mktname(c, "bool"); }; if (k == nkind.N_VOIDLIT) { return mktname(c, "void"); }; if (k == nkind.N_NIL) { return mktname(c, "untyped_nil"); }; if (k == nkind.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 == nkind.N_CAST) { // `expr: T` — explicit cast; the type expr is e.rhs. return e.rhs; }; if (k == nkind.N_CALL) { let callee: *node = e.lhs; if (callee == nil) { return nil; }; let nm: str; nm.ptr = nil; nm.len = 0; if (callee.kind == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.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 != 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 == nkind.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 != nkind.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 == nkind.N_TYPEASSERT) { // `e as T` → T return e.rhs; }; if (k == nkind.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 != nkind.N_TNAME) { return false; }; return streq(t.str, "untyped_int"); }; fn isuntypedfloat(t: *node) bool = { if (t == nil) { return false; }; if (t.kind != nkind.N_TNAME) { return false; }; return streq(t.str, "untyped_float"); }; fn isuntypednil(t: *node) bool = { if (t == nil) { return false; }; if (t.kind != nkind.N_TNAME) { return false; }; return streq(t.str, "untyped_nil"); }; fn isnumerictname(t: *node) bool = { if (t == nil) { return false; }; if (t.kind != nkind.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 != nkind.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 == nkind.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 == nkind.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 == nkind.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 == nkind.N_TPTR) { return true; }; if (du.kind == nkind.N_TSLICE) { return true; }; if (du.kind == nkind.N_TCHAN) { return true; }; if (du.kind == nkind.N_TFN) { return true; }; // nullable `(*T | void)` — already accepted by typeeqast // when matched whole; nil is OK there too. if (du.kind == nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { if (v.kind == nkind.N_TPTR) { return true; }; if (v.kind == nkind.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 == nkind.N_TTAGGED && su.kind != nkind.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 == nkind.N_TTAGGED && su.kind == nkind.N_TTAGGED) { *confident = false; return true; }; // Two known primitives with different names are confidently // incompatible. `i32 ↔ bool`, `str ↔ i32`, etc. if (du.kind == nkind.N_TNAME && su.kind == nkind.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 == nkind.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 == nkind.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 != nkind.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 == nkind.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 == nkind.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 != nkind.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 == nkind.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 nkind.N_TRYPROP; its lhs is the value-bearing expr; we look at the // expr's *declared* type for nkind.N_IDENT/nkind.N_CALL cases. fn exprtypeoftry(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; if (e.kind == nkind.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 == nkind.N_CALL) { // callee return type lookup: callee is e.lhs (nkind.N_IDENT or // nkind.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 == nkind.N_IDENT) { nm = callee.str; }; if (callee.kind == nkind.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 != 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 != nkind.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 != nkind.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 == nkind.N_PARAM) { let nm: str = p.str; if (nm.len > 0) { checkmoduleshadow(c, nm, "param"); scopedefine(c.cur, nm, skind.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; let empty: str; c.curmod = empty; c.file = nil; seedprimitives(c); }; export fn checkfile(c: *checker, file: *node) void = { if (file == nil) { return; }; if (file.kind != nkind.N_FILE) { return; }; c.file = file; // Pass 1: install all top-level names. let d: *node = file.list; for (d != nil) { installdecl(c, file, d); d = d.next; }; // Pass 2: walk decl bodies/types and resolve identifiers. // Track the per-decl module bareword so bare-leaf lookups inside // the body prefer same-module entries over alphabetically-earlier // same-leaf imports. d = file.list; for (d != nil) { c.curmod = declmod(file, d); let k: nkind = d.kind; if (k == nkind.N_FNDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); } else { if (k == nkind.N_DEF) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; } else { if (k == nkind.N_TYPEDECL) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; } else { if (k == nkind.N_LET) { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; };};};}; d = d.next; }; let empty: str; c.curmod = empty; }; // 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; // ---- variadic-call helpers (Hare-style `T...` param) ----------------- // slicewrap — synthesise an N_TSLICE node wrapping the given element // type AST. Used by the Hare-style variadic path so the local entry // for the param (callee side) and the call-site slice descriptor // (caller side) both advertise their effective type as []ELEM — // every isslicetype / nodeisslice check then succeeds naturally. fn slicewrap(c: *cgen, elem: *node) *node = { let s: *node = newnode(c.a, nkind.N_TSLICE, "", 0, 0); s.lhs = elem; return s; }; // findvariadicparam — walk a param-list head and return the variadic // param node (the one with op == TK_ELLIPSIS) plus the count of // non-variadic params before it. Returns nil/0 when no variadic. // nfixed_out cannot be nil. fn findvariadicparam(ps: *node, nfixed_out: *i32) *node = { *nfixed_out = 0; let p: *node = ps; for (p != nil) { if (p.kind == nkind.N_PARAM) { if (p.op == tkind.TK_ELLIPSIS) { return p; }; *nfixed_out += 1; }; p = p.next; }; return nil; }; // callee_variadic_param — convenience wrapper: looks up the callee // by name and finds its variadic param + nfixed. Returns nil if the // callee isn't registered or has no variadic param. fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { *nfixed_out = 0; if (callee == nil) { return nil; }; let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len == 0) { return nil; }; let ps: *node = fnparamslookup(c, cnm); return findvariadicparam(ps, nfixed_out); }; // mkvarargname — fresh local-slot name "". Used for // the per-variadic-call scratch buffers (`@vararg_d_N` for the // element-data buffer, `@vararg_sl_N` for the 24B slice descriptor) // where N is recorded on the N_CALL node at scanlocals time so both // the prologue reservation and the call-site emission agree. fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = { let buf: [128]u8; let i: i32 = 0; let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; let ns: str = strconv.i64tos(seq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 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; }; // ---- 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. // // `param` is the corresponding declared parameter for `arg` (N_PARAM // node from the callee's signature) or nil. When param's type is a // tagged union and `arg`'s surface type is a concrete variant of it, // we materialise (tag, value-words, pad) for the parameter slot before // pushing — mirrors cmd/w6c/cgen.c's call-arg widening. fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (arg == nil) { return 0; }; let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam); // Implicit widening from a concrete variant to a tagged-union // parameter slot. Skips when the arg is already a tagged local // (line 121's slice-or-tagged shortcut handles that). let widensz: i32 = 0; let widentag: i32 = 0; if (param != nil) { if (param.kind == nkind.N_PARAM) { // Hare-style variadic `T...`: effective param type is // []T (slice). The arg here is the synthesised slice // descriptor (or a forwarded `xs...` slice), not a // value of T being widened into a tagged slot — skip // the widening detection so the slice-ident fast path // at the bottom of pushargsrev gets the push. if (param.op == tkind.TK_ELLIPSIS) { widensz = 0; } else { let ptype: *node = param.lhs; if (istaggedtype(c, ptype)) { let aistagged: bool = false; if (arg.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, arg.str); if (lc != nil) { aistagged = istaggedtype(c, lc.tnode); }; }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); let t: i32 = taggedvariantindex(c, tagged, arg); if (t < 0) { t = 0; }; widentag = t; }; }; }; }; }; if (widensz == 8) { // Nullable fold: pointer value IS the discriminator. No // separate tag word. cgexpr(c, arg); emitline("\tPUSHQ\tAX\n"); return rest + 1; }; if (widensz > 0) { // Struct-payload widening into a tagged-union param uses // @tagscr (zero + cgwidentaggedstore writes fields + tag, // then push slot words high → low). Scalar / str go via // the direct push fast path below — keeps wwstage's asm // byte-identical to cstage for selfhost source. let pname: str = rhsstructpayload(c, arg); if (pname.len > 0) { let ptype: *node = param.lhs; let scroff: i32 = localadd(c, "@tagscr", 24, nil); emitline("\tXORQ\tAX, AX\n"); let zz: i32 = 0; for (zz < widensz) { emitline("\tMOVQ\tAX, "); emitoff((scroff + zz): i64); emitline("(BP)\n"); zz += 8; }; cgwidentaggedstore(c, ptype, arg, "BP", scroff, widensz); let pp: i32 = widensz - 8; for (pp >= 0) { emitline("\tMOVQ\t"); emitoff((scroff + pp): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); pp -= 8; }; return rest + widensz / 8; }; cgexpr(c, arg); if (nodeisslice(c, arg)) { // Slice payload (24B): cgexpr leaves (AX=ptr, BX=len, // CX=cap). Slot layout: [+0]=tag, [+8]=ptr, [+16]=len, // [+24]=cap. Push high→low so pop drains tag first. // Requires widensz >= 32; a smaller slot would mean the // destination union doesn't list slice as a variant // (caller should have flagged a type error). emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); } else { if (nodeisstr(c, arg)) { // slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low // so pop drains tag first into arg-reg[0]. emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); } else { // Scalar variant: single value word at +8. Pad a zero // high word when slot is 24B (some other variant of // the union is 16B-shaped). let pp: i32 = widensz - 8; for (pp > 8) { emitline("\tXORQ\tDX, DX\n"); emitline("\tPUSHQ\tDX\n"); pp -= 8; }; emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t$"); emitint(widentag: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); };}; return rest + widensz / 8; }; // nkind.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 == nkind.N_SLICE) { let base: *node = arg.lhs; let lo: *node = arg.rhs; let hi: *node = arg.cond; let baselocal: *local = nil; let globaltn: *node = nil; let globalname: str; globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal == nil) { let gt: *node = letvartnode(c, bn); if (gt != nil) { globaltn = gt; globalname = bn; }; }; }; }; // base address → push if (baselocal != nil) { let tn: *node = baselocal.tnode; if (tn != nil) { if (tn.kind == nkind.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 { if (globaltn != nil) { if (globaltn.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), 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 == nkind.N_TARRAY) { let lenn: *node = tn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); }; }; } else { if (tn.kind == nkind.N_TSLICE) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); } else { if (tn.kind == nkind.N_TNAME) { if (streq(tn.str, "str")) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); }; };};}; }; } else { if (globaltn != nil) { if (globaltn.kind == nkind.N_TARRAY) { let lenn: *node = globaltn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); }; }; } else { if (globaltn.kind == nkind.N_TSLICE) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), CX\n"); emitline("\tMOVQ\t8(CX), 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). // For tagged ident with a >24B slot (slice-payload variant), // push a fourth word from off+24. if (arg.kind == nkind.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(c, lc.tnode)) { let nwords: i32 = 3; if (istaggedtype(c, lc.tnode)) { let ssz: i32 = slotsize(c, lc.tnode); nwords = ssz / 8; }; let w: i32 = nwords - 1; for (w >= 0) { emitline("\tMOVQ\t"); emitoff((off + w*8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); w -= 1; }; return rest + nwords; }; }; }; // Float arg: cgexpr leaves the value in X0. Push 8 bytes from // X0 via SUBQ+MOVSD so cgcall's pop side can drain into the // XMM stream (X0..X7). f32 still occupies 8B on the stack — // the MOVSS load on the pop side touches only the low 4. let fk: i32 = exprfloatkind(c, arg); if (fk != 0) { cgexpr(c, arg); let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); return rest + 1; }; 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: nkind = n.kind; if (k == nkind.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 == nkind.N_SLICE) { return true; }; if (k == nkind.N_CAST) { return isslicetype(c, n.rhs); }; // N_DOT to a slice field: resolve the field through the struct // (or *struct) the base ident / inner chain lands on, then check // the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg // push/pop counts 3 words for `p.sl` and `p.inner.sl` shapes. // `.ptr` / `.len` / `.cap` are pseudo-fields — they yield ptr // (*u8) and i32, not a slice — so we exclude them up front. if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.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 == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; if (lkind == nkind.N_TNAME) { sname = tn.str; }; if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; }; }; }; if (base.kind == nkind.N_DOT) { let innert: *node = dotinnerstructptr(c, base); if (innert != nil) { if (innert.kind == nkind.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) { if (streq(fi.fname, fld)) { return isslicetype(c, fi.tnode); }; fi = fi.finext; }; }; }; // Chained dot through value-struct hops (`o.inner.sl`, // `p.inner.sl`): dotinnerstructptr above only walks // *struct fields, so a value-struct chain falls through. // dotchainresolve handles arbitrary depth through value // struct AND `*T` root, returning the leaf fieldinfo. let rootnm: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let lfi: *fieldinfo = nil; let sdelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let ok: bool = dotchainresolve(c, n, &rootnm, &rootoff, &totaloff, &lfi, &sdelta, &isglobal, &ptrroot); if (ok && sdelta < 0 && lfi != nil) { return isslicetype(c, lfi.tnode); }; }; return false; }; 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: nkind = n.kind; if (k == nkind.N_STRLIT) { return true; }; if (k == nkind.N_IDENT) { let nm: str = n.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { // Use isstrtype so `!str` aliases (parserr = !str) and // `type foo = str;` chains resolve through. The bare // `streq("str", ...)` test missed them and dropped the // MOVQ BX,CX shuffle on returns of str-aliased locals. if (isstrtype(c, lc.tnode)) { return true; }; }; return false; }; if (k == nkind.N_CALL) { let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { let cnm: str = callee.str; let rt: *node = fnretlookup(c, cnm); return isstrtype(c, rt); }; }; return false; }; if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.str; // `.ptr` is *u8 not str; `.len` is i32 not str. if (streq(fld, "ptr")) { return false; }; if (streq(fld, "len")) { return false; }; if (streq(fld, "cap")) { return false; }; if (base != nil) { let sname: str; sname.ptr = nil; sname.len = 0; if (base.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; if (lkind == nkind.N_TNAME) { sname = tn.str; }; if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; if (inner != nil) { if (inner.kind == nkind.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 == nkind.N_DOT) { let innert: *node = dotinnerstructptr(c, base); if (innert != nil) { if (innert.kind == nkind.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; }; }; }; // Chained dot through value-struct hops (`p.inner.s`): // dotinnerstructptr above only walks *struct fields; // dotchainresolve handles arbitrary depth through // value struct AND `*T` root. Mirror of the nodeisslice // fallback so chained str-field args also push 2 words. let rootnm: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let lfi: *fieldinfo = nil; let sdelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let ok: bool = dotchainresolve(c, n, &rootnm, &rootoff, &totaloff, &lfi, &sdelta, &isglobal, &ptrroot); if (ok && sdelta < 0 && lfi != nil) { return isstrtype(c, lfi.tnode); }; }; return false; }; if (k == nkind.N_CAST) { return isstrtype(c, n.rhs); }; return false; }; // typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr/rune. // rune is a Unicode codepoint (0..0x10FFFF); cgen treats it as // unsigned so narrow-cast / sub-word load paths zero-extend (MOVL, // not MOVSXD). Mirrors cstage's type_isunsigned post task #5. 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; }; if (streq(nm, "rune")) { return true; }; return false; }; // typenodeisunsigned — recurse through TNAME aliases / TBANG / TENUM // to the resolved primitive. Mirrors cstage's type_isunsigned which // recurses into TY_NAMED.under and TY_ENUM.sub. fn typenodeisunsignedc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let k: nkind = t.kind; if (k == nkind.N_TBANG) { return typenodeisunsignedc(c, t.lhs); }; if (k == nkind.N_TENUM) { return typenodeisunsignedc(c, t.lhs); }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (typenameisunsigned(nm)) { return true; }; if (typenameissigned(nm)) { return false; }; // Follow aliases / enum storage. let al: *node = aliaslookup(c, nm); if (al != nil) { return typenodeisunsignedc(c, al); }; let en: *enumtype = enumlookup(c, nm); if (en != nil) { if (en.storage != nil) { return typenodeisunsignedc(c, en.storage); }; return false; // default storage i32 is signed }; }; return false; }; // typenodeisunsigned — legacy callers without *cgen context. Only // resolves primitive TNAMEs (no alias/enum recursion); use the // _c variant where the cgen registry is in scope. fn typenodeisunsigned(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.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 nkind.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: nkind = t.kind; if (k == nkind.N_TPTR) { return true; }; if (k == nkind.N_TFN) { return true; }; if (k == nkind.N_TCHAN) { return true; }; if (k == nkind.N_TSLICE) { return false; }; if (k == nkind.N_TARRAY) { // C cgen (cmd/w6c/cgen.c:3317) zero-inits TY_ARRAY whenever // its raw byte size is 8 — e.g. `[8]bool`, `[2]i32`, `[4]i16`, // `[1]i64`. Mirror that here so the wwstage matches. let lenn: *node = t.rhs; let elemn: *node = t.lhs; if (lenn == nil) { return false; }; if (lenn.kind != nkind.N_INTLIT) { return false; }; let elen: i64 = lenn.uval: i64; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; return (esz: i64 * elen) == 8i64; }; if (k == nkind.N_TTUPLE) { return false; }; if (k == nkind.N_TTAGGED){ return false; }; if (k == nkind.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; }; // elemissigned — given an indexable type (`*T`, `[]T`, `[N]T`), is // its element a signed narrow primitive (i8/i16/i32)? Used by // cgindex to pick MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at // esz=1/2). Mirrors cstage's `signed_elem`. Follows alias/enum // chains so `[]Alias` arrays resolve to the underlying signedness. fn elemissignedc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let elem: *node = nil; let k: nkind = t.kind; if (k == nkind.N_TPTR) { elem = t.lhs; }; if (k == nkind.N_TSLICE) { elem = t.lhs; }; if (k == nkind.N_TARRAY) { elem = t.lhs; }; if (elem == nil) { return false; }; return fieldissignedc(c, elem); }; fn elemissigned(t: *node) bool = { if (t == nil) { return false; }; let elem: *node = nil; let k: nkind = t.kind; if (k == nkind.N_TPTR) { elem = t.lhs; }; if (k == nkind.N_TSLICE) { elem = t.lhs; }; if (k == nkind.N_TARRAY) { elem = t.lhs; }; if (elem == nil) { return false; }; if (elem.kind != nkind.N_TNAME) { return false; }; return typenameissigned(elem.str); }; // typenameissigned — true for i8/i16/i32/i64/int. rune is excluded // (it's a non-negative Unicode codepoint, treated as unsigned). 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; }; return false; }; // fieldissignedc — does this field/element type need sign-extension // on a sub-word load? Walks TBANG / TENUM / TNAME-aliases to the // resolved primitive. Mirrors cstage's fld_issigned: bool is treated // as unsigned (0/1 ⇒ MOVZBQ); rune is unsigned (codepoint ⇒ MOVL). fn fieldissignedc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let k: nkind = t.kind; if (k == nkind.N_TBANG) { return fieldissignedc(c, t.lhs); }; if (k == nkind.N_TENUM) { return fieldissignedc(c, t.lhs); }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "bool")) { return false; }; if (typenameisunsigned(nm)) { return false; }; if (typenameissigned(nm)) { return true; }; let al: *node = aliaslookup(c, nm); if (al != nil) { return fieldissignedc(c, al); }; let en: *enumtype = enumlookup(c, nm); if (en != nil) { if (en.storage != nil) { return fieldissignedc(c, en.storage); }; return true; // default i32 storage is signed }; }; return false; }; // fieldloadop — pick the load instruction for a non-str struct // field by its declared size + signedness. Mirrors cstage's // fldloadop: MOVZBQ/MOVSBQ for 1B, MOVZWQ/MOVSWQ for 2B, // MOVL/MOVSXD for 4B, MOVQ for 8B. f might be nil for fields // outside our struct registry. fn fieldloadop(c: *cgen, f: *fieldinfo) str = { if (f == nil) { return "MOVQ"; }; let sz: i32 = f.fsz; let sigd: bool = fieldissignedc(c, f.tnode); if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; }; if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; }; if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; }; return "MOVQ"; }; // fieldstoreop — pick the store instruction for a non-str struct // field by its declared size. MOVB for 1, MOVW for 2, MOVL for 4, // MOVQ for 8. c kept in the signature for symmetry with fieldloadop. fn fieldstoreop(c: *cgen, f: *fieldinfo) str = { if (f == nil) { return "MOVQ"; }; let sz: i32 = f.fsz; if (sz == 1) { return "MOVB"; }; if (sz == 2) { return "MOVW"; }; if (sz == 4) { return "MOVL"; }; return "MOVQ"; }; // tnodeloadop / tnodestoreop — same dispatch as fieldloadop / // fieldstoreop but keyed on a raw type-AST node (tuple element type, // pointer-target, slice-element, etc.) rather than a struct fieldinfo. // Used at the index / tuple / pointer-deref sites where there's no // fieldinfo entry but the type-node + size are both known. fn tnodeloadop(c: *cgen, t: *node, sz: i32) str = { let sigd: bool = fieldissignedc(c, t); if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; }; if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; }; if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; }; return "MOVQ"; }; fn tnodestoreop(c: *cgen, t: *node, sz: i32) str = { if (sz == 1) { return "MOVB"; }; if (sz == 2) { return "MOVW"; }; if (sz == 4) { return "MOVL"; }; return "MOVQ"; }; // loadopsz — load op when the (size, signedness) pair has already // been resolved upstream and the type-node isn't carried through. // cgindex precomputes `signed_elem` via elemissignedc; cgforrange // precomputes `bind_signed[b]` via paramissigned. Same dispatch as // tnodeloadop's tail; only the keying differs. fn loadopsz(sigd: bool, sz: i32) str = { if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; }; if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; }; if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; }; return "MOVQ"; }; // localloadop — read instruction for a scalar local/let load. Same // dispatch as fieldloadop, but keyed on the value's own tnode. Lets // the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot instead // of a raw MOVQ, so a slot that was last written by a narrow deref- // store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as a // properly-sign-extended i64. The natural N_ASSIGN / N_LET paths // store the rhs as a sign-extended 8B word, so MOVQ accidentally // works; deref-stores are the only path that touches fewer bytes // than MOVQ reads. Mirror of cstage's localloadop in cmd/w6c/cgen.c. // Resolves TBANG / TENUM / TNAME-alias chains so `type err = !i32` // picks up size 4 the same way the cstage checker pre-computes // t->size — without this, aliased narrows fall through to MOVQ. export fn localloadop(c: *cgen, tnode: *node) str = { let t: *node = tnode; for (t != nil) { let k: nkind = t.kind; if (k == nkind.N_TBANG) { t = t.lhs; } else { if (k == nkind.N_TENUM) { t = t.lhs; } else { if (k == nkind.N_TNAME) { let nm: str = t.str; if (primsize(nm) > 0) { break; }; let al: *node = aliaslookup(c, nm); if (al == nil) { break; }; t = al; } else { break; }; }; }; }; let sz: i32 = fieldsize(c, t); if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; }; let sigd: bool = fieldissignedc(c, tnode); return loadopsz(sigd, sz); }; // 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 != nkind.N_DOT) { return 8; }; let fld: str = base.str; let inner: *node = base.lhs; if (inner == nil) { return 8; }; if (inner.kind != nkind.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 == nkind.N_TPTR) { innert = tn.lhs; }; if (innert == nil) { return 8; }; if (innert.kind == nkind.N_TNAME) { if (streq(innert.str, "str")) { return 1; }; }; if (innert.kind == nkind.N_TSLICE) { return elemsizeof(innert); }; return 8; }; // Generic struct field: if it's *T, element size is T's size. let lkind: nkind = tn.kind; let sname: str; sname.ptr = nil; sname.len = 0; if (lkind == nkind.N_TNAME) { sname = tn.str; }; if (lkind == nkind.N_TPTR) { let pinner: *node = tn.lhs; if (pinner != nil) { if (pinner.kind == nkind.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 == nkind.N_TPTR) { let elem: *node = ft.lhs; if (elem != nil) { if (elem.kind == nkind.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 == nkind.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 == nkind.N_TNAME) { if (streq(ft.str, "str")) { return 1; }; }; return 8; }; fi = fi.finext; }; return 8; }; // dotinnerstructptr — for an nkind.N_DOT whose lhs is a chain of dots // or an nkind.N_IDENT, walk the chain and return the nkind.N_TNAME tnode of the // struct that the chain dereferences to (i.e., for `r.sym` where // .sym is *lsym, return nkind.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 != nkind.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 == nkind.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 (nkind.N_TNAME) or *struct (nkind.N_TPTR). if (tn.kind == nkind.N_TNAME) { baset = tn; }; if (tn.kind == nkind.N_TPTR) { baset = tn.lhs; }; } else { if (base.kind == nkind.N_DOT) { baset = dotinnerstructptr(c, base); };}; if (baset == nil) { return nil; }; if (baset.kind != nkind.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 != nkind.N_TPTR) { return nil; }; let inner: *node = ft.lhs; if (inner == nil) { return nil; }; if (inner.kind != nkind.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). // For aliased element types (e.g. `[N]formattable`), callers that // need the resolved slot size should use elemsizeofc(c, t) which // follows aliases via slotsize. fn elemsizeof(t: *node) i32 = { if (t == nil) { return 1; }; let k: nkind = t.kind; let elem: *node = nil; if (k == nkind.N_TPTR) { elem = t.lhs; }; if (k == nkind.N_TSLICE) { elem = t.lhs; }; if (k == nkind.N_TARRAY) { elem = t.lhs; }; if (k == nkind.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; }; // `*[N]T`: drill through the pointer into the array's element so // indexing scales by T's width, not the whole-array byte size. if (elem.kind == nkind.N_TARRAY) { if (elem.lhs != nil) { elem = elem.lhs; }; }; if (elem.kind == nkind.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; }; // elemsizeofc — like elemsizeof but resolves aliased element types // (struct / tagged / `type foo = bar;`) via slotsize. Used where // cgindex / cgassign need a correct stride for `[N]Alias` arrays // whose Alias resolves to a tagged union (e.g. `[N]formattable`). fn elemsizeofc(c: *cgen, t: *node) i32 = { if (t == nil) { return 1; }; let direct: i32 = elemsizeof(t); if (direct != 8) { return direct; }; let k: nkind = t.kind; let elem: *node = nil; if (k == nkind.N_TPTR) { elem = t.lhs; }; if (k == nkind.N_TSLICE) { elem = t.lhs; }; if (k == nkind.N_TARRAY) { elem = t.lhs; }; if (elem == nil) { return direct; }; if (elem.kind == nkind.N_TNAME) { let ps: i32 = primsize(elem.str); if (ps > 0) { return ps; }; }; return slotsize(c, elem); }; // nodeisunsigned — best-effort cgen-time inference from the AST. We // don't have a typed AST yet, so we walk surface nodes: // nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet) // nkind.N_IDENT — look up the local's declared type // nkind.N_DOT — look up the field's declared type via struct reg // nkind.N_BIN / nkind.N_UN — recurse: unsigned if either operand is unsigned // nkind.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: nkind = n.kind; if (k == nkind.N_IDENT) { let nm: str = n.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { return typenodeisunsigned(lc.tnode); }; return false; }; if (k == nkind.N_DOT) { let base: *node = n.lhs; let fld: str = n.str; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; let sname: str; sname.ptr = nil; sname.len = 0; if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; }; if (lkind == nkind.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 == nkind.N_CAST) { return typenodeisunsigned(n.rhs); }; if (k == nkind.N_BIN) { if (nodeisunsigned(c, n.lhs)) { return true; }; return nodeisunsigned(c, n.rhs); }; if (k == nkind.N_UN) { return nodeisunsigned(c, n.lhs); }; // nkind.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 == nkind.N_INDEX) { let base: *node = n.lhs; if (base != nil) { if (base.kind == nkind.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 == nkind.N_TPTR) { elem = tn.lhs; }; if (tn.kind == nkind.N_TARRAY) { elem = tn.lhs; }; if (tn.kind == nkind.N_TSLICE) { elem = tn.lhs; }; if (elem != nil) { return typenodeisunsigned(elem); }; }; }; }; }; return false; }; return false; }; // nodeprimwidth — primitive byte width of an expression, or 0 if not // statically determinable. Mirrors nodeisunsigned's structural walk. // Used by cgun TK_TILDE to clamp narrow unsigned ~ results to type // width (NOTQ inverts the full 64-bit register). fn nodeprimwidth(c: *cgen, n: *node) i32 = { if (n == nil) { return 0; }; let k: nkind = n.kind; if (k == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); }; }; }; return 0; }; if (k == nkind.N_CAST) { let tn: *node = n.rhs; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); }; }; return 0; }; if (k == nkind.N_UN) { return nodeprimwidth(c, n.lhs); }; return 0; }; // ---- type-driven slot sizing ---------------------------------------- // structnaturalsize — type-natural size of `si`, i.e. max(foff + // fsz) across declared fields. Mirrors cstage's `lu->size` for a // TY_STRUCT (rounded only to the struct's maxalign). // // NOTE: si.totsize is mis-named — it's actually the *slot-padded* // size (rounded up to 8 for stack-slot use; see registerstruct's // tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride, // and similar consumers want that slot-padded number. The // receive-side ABI (#5) and any future "TYPE size, not slot size" // query wants the natural size. Until si.totsize is split into // si.naturalsize + si.slotsize (tracked as the wwstage-sizing // follow-up task), recover the type-natural size from the field // chain here. fn structnaturalsize(si: *structinfo) i32 = { if (si == nil) { return 0; }; let n: i32 = 0; let fi: *fieldinfo = si.fields; for (fi != nil) { let end: i32 = fi.foff + fi.fsz; if (end > n) { n = end; }; fi = fi.finext; }; return n; }; fn structlookup(c: *cgen, name: str) *structinfo = { // Exact match first: bare-from-source struct names and already- // leafed lookups hit here directly. let s: *structinfo = c.structs; for (s != nil) { let sn: str = s.sname; if (streq(sn, name)) { return s; }; s = s.sinext; }; // Module-qualified form: `pkg.S` → match the leaf scoped to its // originating module. Mirrors aliaslookup's mod-filter; the // `smod == pkg` guard is what prevents two modules with same- // leaf-name structs from collapsing into whichever entry appears // first in the chain. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' let pkg: str; pkg.ptr = name.ptr; pkg.len = i; let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); let b: *structinfo = c.structs; for (b != nil) { if (streq(b.sname, leaf)) { if (streq(b.smod, pkg)) { return b; }; }; b = b.sinext; }; return nil; }; i -= 1; }; 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 nkind.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; }; // variantnamematch — tagged-union variant names are compared as if // they'd been alias-resolved. Pattern names can be module-qualified // (`strconv.invalid` from a `case let e: strconv.invalid =>`), // while the variant's declared name inside its own module is bare // (`invalid`). With no checker the cgen can't follow imports, so we // accept exact match plus suffix-after-`.` on either side. Mirrors // the C cgen's type_eq, which goes through resolved Type pointers. fn variantnamematch(vname: str, pname: str) bool = { if (streq(vname, pname)) { return true; }; // `pname` is qualified, `vname` is bare: drop module prefix. let i: i32 = 0; for (i < pname.len) { if (pname[i] == '.': u8) { let tail: str; tail.ptr = pname.ptr + i + 1; tail.len = pname.len - i - 1; if (streq(tail, vname)) { return true; }; }; i += 1; }; // `vname` is qualified, `pname` is bare: same trick in reverse. let j: i32 = 0; for (j < vname.len) { if (vname[j] == '.': u8) { let tail: str; tail.ptr = vname.ptr + j + 1; tail.len = vname.len - j - 1; if (streq(tail, pname)) { return true; }; }; j += 1; }; return false; }; // inferletcalltype — for an annotation-less `let x = expr;`, return // a usable tnode for cgen's struct-aware paths. Today: `let x = // f()?` infers x's type from the success variant of f's tagged // return; without this, x has tnode = nil and `x.field` falls into // the SB-symbol fallback (linker reports `undefined reference to // `). We don't infer for plain `let x = f()` yet — // non-tagged returns don't carry their type back the same way. fn inferletcalltype(c: *cgen, rhs: *node) *node = { if (rhs == nil) { return nil; }; // `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged // return to its success variant; the rhs we want the type of // is the inner call expression. let unwrap: bool = false; let call: *node = rhs; if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; }; if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; }; if (call == nil) { return nil; }; if (call.kind != nkind.N_CALL) { return nil; }; let callee: *node = call.lhs; if (callee == nil) { return nil; }; let cname: str; cname.ptr = nil; cname.len = 0; if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; if (callee.kind == nkind.N_DOT) { cname = callee.str; }; if (cname.len == 0) { return nil; }; let rt: *node = fnretlookup(c, cname); if (rt == nil) { return nil; }; if (unwrap) { // Strip error variants — success type is the first // variant of the tagged return. if (rt.kind != nkind.N_TTAGGED) { return nil; }; return rt.list; }; // Plain call: declared return type is the local's type. return rt; }; // 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. // // `let x = f();` (no annotation): infer from `f`'s declared return // type so a 24B tagged-union return reserves all three spill slots, // not the default 8B. Without this, the AX:DX:CX spill in cglet's // tagged-init branch writes past the local and tramples the next // slot. 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 == nkind.N_TARRAY) { if (n.lhs.rhs == nil) { if (n.rhs != nil) { if (n.rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { // Composite primitive: `str` is 16B // (ptr+len) — primsize returns 0 for // it, so it'd slot 8B without this. if (streq(elemn.str, "str")) { esz = 16; } else { 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 == nkind.N_FIELD) { if (streq(e.str, "...")) { e = nil; adv = false; }; }; if (adv) { cnt += 1; e = e.next; }; }; return esz * cnt; }; }; }; }; }; if (n.lhs != nil) { return slotsize(c, n.lhs); }; // Annotation-less init: defer to the call's return type if we // can infer it. Tagged-union returns need 24B; everything else // matches slotsize on the inferred type. let inferred: *node = inferletcalltype(c, n.rhs); if (inferred != nil) { return slotsize(c, inferred); }; return 8; }; fn slotsize(c: *cgen, typn: *node) i32 = { if (typn == nil) { return 8; }; let k: nkind = typn.kind; if (k == nkind.N_TPTR) { return 8; }; if (k == nkind.N_TFN) { return 8; }; if (k == nkind.N_TCHAN) { return 8; }; if (k == nkind.N_TSLICE) { return 24; }; if (k == nkind.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 == nkind.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 nkind.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 == nkind.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; }; // Type alias (`type foo = !str;` / `type foo = bar;`): // follow it so a tagged-union variant of a !str-aliased // error type contributes 16 bytes to the max payload // rather than 8 (the default). if (c != nil) { let aliased: *node = aliaslookup(c, nm); if (aliased != nil) { if (aliased.kind == nkind.N_TBANG) { return slotsize(c, aliased.lhs); }; return slotsize(c, aliased); }; }; return 8; }; if (k == nkind.N_TARRAY) { let lenn: *node = typn.rhs; let elemn: *node = typn.lhs; let elen: i64 = 1i64; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; }; }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; // `str` is a composite primitive (ptr+len, 16B); // primsize returns 0 for it, so without this // explicit case a `[N]str` would slot 8B/elem, // collapsing the per-element stride and losing // every .len half. if (streq(en, "str")) { esz = 16; }; let ps: i32 = primsize(en); if (esz == 8) { if (ps > 0) { esz = ps; } else { // Named struct / aliased type: size off // the structinfo if present, else follow // the alias via aliaslookup so // `[N]formattable` reads the resolved // tagged slot (e.g. 24B for // `(i64|str|bool)`), not the fall- // through 8B. let si: *structinfo = structlookup(c, en); if (si != nil) { esz = si.totsize; } else { if (c != nil) { let al: *node = aliaslookup(c, en); if (al != nil) { esz = slotsize(c, al); }; }; }; }; }; } else { if (elemn.kind == nkind.N_TTAGGED) { // Tagged-union element: full slot (8 tag + // padded max payload). Matches C cgen's // resolve_type for `[N]TAGGED`. esz = slotsize(c, elemn); } else { if (elemn.kind == nkind.N_TPTR) { esz = 8; } else { if (elemn.kind == nkind.N_TSTRUCT) { esz = slotsize(c, elemn); }; }; }; }; }; return (esz: i64 * elen): i32; }; if (k == nkind.N_TSTRUCT) { // Inline anonymous struct — sum of field sizes. let f: *node = typn.list; let total: i32 = 0; for (f != nil) { if (f.kind == nkind.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: nkind = tnode.kind; if (k == nkind.N_TTAGGED){ return slotsize(c, tnode); }; if (k == nkind.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; }; // Enum: size of its storage type. Mirrors the C cgen, which // reads Type.size off the TY_ENUM (which inherits from .sub). let en: *enumtype = enumlookup(c, nm); if (en != nil) { if (en.storage != nil) { if (en.storage.kind == nkind.N_TNAME) { let sps: i32 = primsize(en.storage.str); if (sps > 0) { return sps; }; }; }; return 4; // default storage is i32 }; // Type alias to a tagged-union — recurse through aliaslookup // so `e: ev` (where `ev = (i64 | i32)`) takes 16B in the // containing struct rather than the 8B default. if (c != nil) { let aliased: *node = aliaslookup(c, nm); if (aliased != nil) { return fieldsize(c, aliased); }; }; return 8; }; if (k == nkind.N_TPTR) { return 8; }; if (k == nkind.N_TSLICE) { return 24; }; if (k == nkind.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 == nkind.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, module: str, tstruct: *node) void = { let si: *structinfo = amalloc(c.a, 80u64): *structinfo; si.sname = name; si.smod = module; 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 == nkind.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 == nkind.N_TYPEDECL) { let body: *node = d.lhs; if (body != nil) { if (body.kind == nkind.N_TSTRUCT) { registerstruct(c, d.str, d.module, 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 == nkind.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); if (isstrtyperaw(r)) { return true; }; // `parserr = !str` — `!T` aliases shouldn't hide their // underlying type from str-routing. Unwrap and re-check. if (r != nil) { if (r.kind == nkind.N_TBANG) { let inner: *node = r.lhs; if (isstrtyperaw(inner)) { return true; }; if (inner != nil) { let r2: *node = resolvetype(c, inner); if (isstrtyperaw(r2)) { return true; }; }; }; }; return false; }; fn isslicetyperaw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.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 istaggedtyperaw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TTAGGED) { return true; }; return false; }; // resolvetagged — return the underlying N_TTAGGED node for `t`, or nil // if `t` doesn't ultimately denote a tagged union. Follows N_TNAME // aliases (via resolvetype) and unwraps one leading N_TBANG so // `type error = !(invalid | overflow);` resolves to its inner // `(invalid | overflow)` node. Use at sites that read variant lists // or detect nullable folding off a scrutinee — cgmatch, cgtypetest, // cgtypeassert — so aliased `!(A|B)` shapes still dispatch. export fn resolvetagged(c: *cgen, t: *node) *node = { let r: *node = resolvetype(c, t); if (r == nil) { return nil; }; if (r.kind == nkind.N_TBANG) { let inner: *node = r.lhs; if (inner == nil) { return nil; }; r = resolvetype(c, inner); if (r == nil) { return nil; }; }; if (r.kind == nkind.N_TTAGGED) { return r; }; return nil; }; // istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its // underlying decl, then unwrap a leading N_TBANG so `type error = // !(invalid | overflow);` is still recognised as tagged. Without the // bang unwrap the prologue treats the param as scalar (8B), spilling // only DI and losing the value-word SI; the match read of slot+8 then // trails into saved BP. fn istaggedtype(c: *cgen, t: *node) bool = { if (istaggedtyperaw(t)) { return true; }; if (c == nil) { return false; }; let r: *node = resolvetype(c, t); if (istaggedtyperaw(r)) { return true; }; if (r != nil) { if (r.kind == nkind.N_TBANG) { let inner: *node = r.lhs; if (istaggedtyperaw(inner)) { return true; }; if (inner != nil) { let r2: *node = resolvetype(c, inner); if (istaggedtyperaw(r2)) { return true; }; }; }; }; return false; }; // isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. fn isf32typeraw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind != nkind.N_TNAME) { return false; }; return streq(t.str, "f32"); }; fn isf64typeraw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind != nkind.N_TNAME) { return false; }; return streq(t.str, "f64"); }; // isfloattype — f32 / f64 (and aliases of those). Used by cglet, // cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to // dispatch the MOVSS/MOVSD-shaped paths. export fn isfloattype(c: *cgen, t: *node) bool = { if (isf32typeraw(t)) { return true; }; if (isf64typeraw(t)) { return true; }; if (c == nil) { return false; }; let r: *node = resolvetype(c, t); if (isf32typeraw(r)) { return true; }; if (isf64typeraw(r)) { return true; }; return false; }; // isf32type — narrower predicate: true only for f32 (after alias // resolution). f64 returns false. Used to pick MOVSS vs MOVSD and // the SS-variant arithmetic / cast opcodes. export fn isf32type(c: *cgen, t: *node) bool = { if (isf32typeraw(t)) { return true; }; if (c == nil) { return false; }; let r: *node = resolvetype(c, t); return isf32typeraw(r); }; // exprfloatkind — classify an expression's value-class so callers can // pick float vs integer codegen without a full type system. Returns: // 0 — integer-like (or unknown — same fallback the existing cgen // takes today) // 1 — f32 // 2 — f64 // Recognises: float literals, idents bound to float lets/locals, // chained casts whose target is float, and (recursively) the inner // expr of a non-narrowing wrapping construct. Anything we can't // pin down conservatively reports integer — the worst case is that // CVT* is skipped for an exotic case the user can still spell with // an explicit local. export fn exprfloatkind(c: *cgen, n: *node) i32 = { if (n == nil) { return 0; }; let k: nkind = n.kind; if (k == nkind.N_FLOATLIT) { return 2; }; if (k == nkind.N_CAST) { if (isf32type(c, n.rhs)) { return 1; }; if (isfloattype(c, n.rhs)) { return 2; }; return 0; }; if (k == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.str); if (lc != nil) { if (isf32type(c, lc.tnode)) { return 1; }; if (isfloattype(c, lc.tnode)) { return 2; }; return 0; }; let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, n.str)) { if (isf32type(c, lv.tnode)) { return 1; }; if (isfloattype(c, lv.tnode)) { return 2; }; return 0; }; lv = lv.lvnext; }; return 0; }; if (k == nkind.N_UN) { // Unary on a float (TK_MINUS) returns float; everything // else is integer-coded. if (n.op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); }; return 0; }; if (k == nkind.N_BIN) { // Arithmetic binops inherit the operands' kind. Comparison // (eq/ne/lt/...) returns bool — integer. let op: tkind = n.op; if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); }; if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); }; if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); }; if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); }; return 0; }; if (k == nkind.N_CALL) { // Look up the callee's declared return type — fnretlookup // returns the type-AST. Routes float-returning fns through // the X0 ABI so cglet / cgassign know to spill from X0. let nm: str; nm.ptr = nil; nm.len = 0; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) { nm = n.lhs.str; }; }; if (nm.len > 0) { let rt: *node = fnretlookup(c, nm); if (isf32type(c, rt)) { return 1; }; if (isfloattype(c, rt)) { return 2; }; }; return 0; }; if (k == nkind.N_DOT) { // `p.field` where the struct field is f64/f32. Without this, // `v.fval: i64` lowers to CVTSI on an integer-load value // instead of CVTTSD2SI on the X0 the cgdot path actually // emits for an f64 field. let base: *node = n.lhs; let fld: str = n.str; if (base != nil) { let sname: str; sname.ptr = nil; sname.len = 0; if (base.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; if (tn.kind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { sname = pe.str; }; }; }; }; }; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { if (isf32type(c, fi.tnode)) { return 1; }; if (isfloattype(c, fi.tnode)) { return 2; }; return 0; }; fi = fi.finext; }; }; }; }; return 0; }; return 0; }; // isnullabletype — nkind.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 != nkind.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 == nkind.N_TPTR); let bptr: bool = (b.kind == nkind.N_TPTR); let avoid: bool = (a.kind == nkind.N_TNAME); if (avoid) { avoid = streq(a.str, "void"); }; let bvoid: bool = (b.kind == nkind.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 != nkind.N_TTAGGED) { return 0; }; let a: *node = t.list; if (a != nil) { if (a.kind == nkind.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 != nkind.N_TTAGGED) { return -1; }; let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { if (v.kind == nkind.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; }; // Unary `-` / `+` / `~` inherit the inner expression's type: // cstage's checker stamps N_UN's type from cunop's inner walk, // so `-42i64` is ty_i64 there. Wwstage has no checker stage — // peel the operator here so a typed-int literal under a sign // reaches its tsuffix branch below instead of falling into // taggedvariantindex's "first non-str variant" fallback. Mirror // of cmd/wcc/check.c cunop TK_MINUS/PLUS/TILDE returning t. if (rhs.kind == nkind.N_UN) { let op: tkind = rhs.op; if (op == tkind.TK_MINUS || op == tkind.TK_PLUS || op == tkind.TK_TILDE) { if (rhs.lhs != nil) { return rhstargetname(c, rhs.lhs); }; }; }; if (rhs.kind == nkind.N_CAST) { let t: *node = rhs.rhs; if (t != nil) { if (t.kind == nkind.N_TNAME) { return t.str; }; }; return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; if (rhs.kind == nkind.N_TRUE) { return "bool"; }; if (rhs.kind == nkind.N_FALSE) { return "bool"; }; if (rhs.kind == nkind.N_RUNELIT) { return "rune"; }; if (rhs.kind == nkind.N_INTLIT) { // Typed int literal (`42i64`, `3u8`): suffix names the // concrete variant so flatvariantidx finds it. Untyped // literals (tsuffix=="") fall through to the isstr scan. let s: str = rhs.tsuffix; if (s.len > 0) { return s; }; }; // `T{}` carries its type name on the lhs N_IDENT — the parser // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. // Needed so `return eof{};` (variant of a tagged union) resolves // to the `eof` variant index rather than falling through to the // "first non-str variant" fallback in taggedvariantindex. if (rhs.kind == nkind.N_STRUCTLIT) { let tref: *node = rhs.lhs; if (tref != nil) { if (tref.kind == nkind.N_IDENT) { return tref.str; }; if (tref.kind == nkind.N_TNAME) { return tref.str; }; }; return nm; }; if (rhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.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 r: i32 = flatvariantidx(c, tagged, wantname); if (r >= 0) { return r; }; }; // Fallback: by str-shape (resolves aliases). Walks the // spread-flattened variant list so a `(...inner | str)` outer // agrees with the (i32 | str) inner's str position. let wantstr: bool = nodeisstr(c, rhs); let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { let isspread: bool = (v.op == tkind.TK_ELLIPSIS); if (isspread) { let inner: *node = v; if (inner.kind == nkind.N_TNAME) { let a: *node = aliaslookup(c, inner.str); if (a != nil) { inner = a; }; }; if (inner != nil) { if (inner.kind == nkind.N_TTAGGED) { let iv: *node = inner.list; for (iv != nil) { let ivisstr: bool = false; if (iv.kind == nkind.N_TNAME) { if (isstrtype(c, iv)) { ivisstr = true; }; }; if (ivisstr == wantstr) { return idx; }; iv = iv.next; idx += 1; }; v = v.next; continue; }; }; }; let visstr: bool = false; if (v.kind == nkind.N_TNAME) { if (isstrtype(c, v)) { visstr = true; }; }; if (visstr == wantstr) { return idx; }; v = v.next; idx += 1; }; return -1; }; // flatvariantidx — walk `tagged`'s variant list (with spread `...inner` // expansion) and return the flat 0-based index where `want` matches. // Mirrors check.c's spread flatten at type resolution: an outer // `(...inner | T)` has the inner's variants inlined in declaration // order, so the tag indices stay in sync between cstage (which // resolves types upfront) and wwstage (which doesn't). Returns -1 if // no variant matches. fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = { if (tagged == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; if (want.len == 0) { return -1; }; let v: *node = tagged.list; let idx: i32 = 0; for (v != nil) { let isspread: bool = (v.op == tkind.TK_ELLIPSIS); if (isspread) { let inner: *node = v; if (inner.kind == nkind.N_TNAME) { let a: *node = aliaslookup(c, inner.str); if (a != nil) { inner = a; }; }; if (inner != nil) { if (inner.kind == nkind.N_TTAGGED) { let iv: *node = inner.list; for (iv != nil) { if (iv.kind == nkind.N_TNAME) { if (variantnamematch(iv.str, want)) { return idx; }; }; iv = iv.next; idx += 1; }; v = v.next; continue; }; }; }; if (v.kind == nkind.N_TNAME) { if (variantnamematch(v.str, want)) { return idx; }; }; v = v.next; idx += 1; }; return -1; }; // cgwidentagremap — when widening from one tagged union to a wider one, // rewrite the source's variant tag at slot_off+0 to use the destination's // variant indices. No-op when src and dst index orders coincide. // Mirrors cg_widen_tag_remap in cmd/w6c/cgen.c. fn cgwidentagremap(c: *cgen, dst: *node, src: *node, slot_off: i32) void = { if (dst == nil) { return; }; if (src == nil) { return; }; if (dst.kind != nkind.N_TTAGGED) { return; }; if (src.kind != nkind.N_TTAGGED) { return; }; let identity: bool = true; let v: *node = src.list; let idx: i32 = 0; for (v != nil) { let di: i32 = cgtagvariantidx(c, dst, v); if (di < 0) { di = 0; }; if (di != idx) { identity = false; v = nil; } else { v = v.next; idx += 1; }; }; if (identity) { return; }; let done: str = mklabel(c, "remap_done"); emitline("\tMOVQ\t"); emitoff(slot_off: i64); emitline("(BP), AX\n"); v = src.list; idx = 0; for (v != nil) { let next: str = mklabel(c, "remap_next"); let di: i32 = cgtagvariantidx(c, dst, v); if (di < 0) { di = 0; }; emitline("\tCMPQ\t$"); emitint(idx: i64); emitline(", AX\n"); emitline("\tJNE\t"); emitline(next); emitline("\n"); emitline("\tMOVQ\t$"); emitint(di: i64); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); emitline("(BP)\n"); emitline("\tJMP\t"); emitline(done); emitline("\n"); emitlabel(next); v = v.next; idx += 1; }; emitlabel(done); return; }; // rhsisstructpayload — is `src` a struct value (literal or local ident // of a struct type)? Returns the struct name, or empty str. Only true // when the name is registered in c.structs — `!void` / `!i32` aliases // share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must // fall through to the scalar/str/tagged-source paths instead. fn rhsstructpayload(c: *cgen, src: *node) str = { let empty: str; empty.ptr = nil; empty.len = 0; if (src == nil) { return empty; }; if (src.kind == nkind.N_STRUCTLIT) { let trefn: *node = src.lhs; if (trefn != nil) { let nm: str; nm.ptr = nil; nm.len = 0; if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; }; if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; }; if (nm.len > 0) { if (structlookup(c, nm) != nil) { return nm; }; }; }; return empty; }; if (src.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, src.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { if (structlookup(c, tn.str) != nil) { return tn.str; }; }; }; }; }; return empty; }; // rhstaggedsource — return the tagged-type node for `src` when src is a // tagged-typed local ident; nil otherwise. The slot-copy path uses this // to walk variants for tag remap. fn rhstaggedident(c: *cgen, src: *node) *node = { if (src == nil) { return nil; }; if (src.kind != nkind.N_IDENT) { return nil; }; let lc: *local = localfindnode(c, src.str); if (lc == nil) { return nil; }; let tn: *node = lc.tnode; if (!istaggedtype(c, tn)) { return nil; }; return resolvetagged(c, tn); }; // dotfieldtnode — for an N_DOT src whose base is a local ident or // *struct, return the declared type node of the named field, or nil // if the shape doesn't resolve (e.g. enum-member access, pseudo- // field `.len`, top-level global). Used by rhstaggedabicall and // related predicates to walk into the field's tagged type. fn dotfieldtnode(c: *cgen, n: *node) *node = { if (n == nil) { return nil; }; if (n.kind != nkind.N_DOT) { return nil; }; let base: *node = n.lhs; let fld: str = n.str; if (base == nil) { return nil; }; if (base.kind != nkind.N_IDENT) { return nil; }; let lc: *local = localfindnode(c, base.str); let btn: *node = nil; if (lc != nil) { btn = lc.tnode; } else { btn = letvartnode(c, base.str); }; if (btn == nil) { return nil; }; let bk: nkind = btn.kind; let sname: str; sname.ptr = nil; sname.len = 0; if (bk == nkind.N_TPTR) { let inner: *node = btn.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; }; if (bk == nkind.N_TNAME) { sname = btn.str; }; if (sname.len == 0) { return nil; }; let si: *structinfo = structlookup(c, sname); if (si == nil) { return nil; }; let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { return fi.tnode; }; fi = fi.finext; }; return nil; }; // rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX // return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a // tagged-element base, and N_DOT of a tagged-typed struct field (after // #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to // decide whether cgexpr/spill works for the tagged-source branch of // cgwidentaggedstore. fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (src == nil) { return false; }; if (src.kind == nkind.N_CALL) { let callee: *node = src.lhs; if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); if (rt != nil) { if (istaggedtype(c, rt)) { return true; }; }; }; }; return false; }; if (src.kind == nkind.N_INDEX) { let base: *node = src.lhs; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bl: *local = localfindnode(c, base.str); if (bl != nil) { let btn: *node = bl.tnode; if (btn != nil) { let bk: nkind = btn.kind; let elemt: *node = nil; if (bk == nkind.N_TARRAY) { elemt = btn.lhs; }; if (bk == nkind.N_TSLICE) { elemt = btn.lhs; }; if (bk == nkind.N_TPTR) { elemt = btn.lhs; }; if (elemt != nil) { if (istaggedtype(c, elemt)) { return true; }; }; }; }; }; }; }; // N_DOT of a tagged-typed struct field — cgdot loads // AX=tag, DX=word0, CX=word1[, R8=word2], so downstream // spill matches the call/index shapes. if (src.kind == nkind.N_DOT) { let ft: *node = dotfieldtnode(c, src); if (ft != nil) { if (istaggedtype(c, ft)) { return true; }; }; }; return false; }; // cgloadtaggedfield — load a tagged-union slot at `basereg`+foff // into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1, // R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B // = + word2 (slice variant). Mirrors the cstage tagged-field load // in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches). // // Load order is fixed regardless of basereg: tag, word0, word2, // word1. CX (word1 target) goes LAST because basereg may itself // be CX — top-level globals address via LEAQ name(SB), CX — and // overwriting it earlier would trash the base address for the // remaining loads. For BP / BX bases the order is harmless. // Callers must guarantee basereg is one of "BP", "BX", "CX"; the // only register loaded into that is NOT a target is BX, so AX- // or DX-rooted callers must spill first. fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = { // tag → AX emitline("\tMOVQ\t"); emitdispreg(foff: i64, basereg); emitline(", AX\n"); // word0 → DX emitline("\tMOVQ\t"); emitdispreg((foff + 8): i64, basereg); emitline(", DX\n"); // word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32). if (slot_sz > 24) { emitline("\tMOVQ\t"); emitdispreg((foff + 24): i64, basereg); emitline(", R8\n"); }; // word1 → CX (load LAST; conflicts with CX-base globals). if (slot_sz > 16) { emitline("\tMOVQ\t"); emitdispreg((foff + 16): i64, basereg); emitline(", CX\n"); }; }; // cgwidentaggedstore — write tagged-union slot bytes for `src` into // the slot at `basereg`+slot_off, sized to slot_sz. Mirrors // cg_widen_tagged_store in cmd/w6c/cgen.c. // // `basereg` selects the addressing root: // - "BP": function-frame slot (let / assign / return / structlit / // array-elem scratch). Body writes straight to slot_off(BP). // - else (e.g. "BX" for *struct field, top-level struct LEAQ // base): pointer-rooted dst. cgexpr inside trashes every GPR, // so we route through a fresh BP-rooted scratch slot, spill // basereg before the body, reload after, then word-copy // scratch → (basereg, slot_off). // // Branches by source shape: // - nullable dst (8B slot): cgexpr → AX → slot+0. // - tagged src ident: copy slot words, zero-pad, tag-remap. // - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr, // spill words; no remap (callee already speaks dst tag order — or // it doesn't, in which case the source is the wider one and remap // would need a reversed direction we don't currently emit). // - struct src (literal or ident): zero slot, write fields at +8+foff, // tag last. // - str src: tag@+0, ptr@+8, len@+16. // - scalar src: tag@+0, value@+8. fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node, basereg: str, slot_off: i32, slot_sz: i32) void = { if (streq(basereg, "BP")) { cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz); return; }; // Pointer-rooted dst: spill basereg (cgexpr will trash it), // materialise into a BP-rooted scratch via the BP path, then // reload basereg and word-copy scratch → caller's slot. let bspill: i32 = localadd(c, "@tagbase", 8, nil); emitline("\tMOVQ\t"); emitline(basereg); emitline(", "); emitoff(bspill: i64); emitline("(BP)\n"); let scr: i32 = localadd(c, "@tagscr", slot_sz, nil); emitline("\tXORQ\tAX, AX\n"); let z: i32 = 0; for (z < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((scr + z): i64); emitline("(BP)\n"); z += 8; }; cgwidentaggedstorebp(c, dst, src, scr, slot_sz); emitline("\tMOVQ\t"); emitoff(bspill: i64); emitline("(BP), "); emitline(basereg); emitline("\n"); let k: i32 = 0; for (k < slot_sz) { emitline("\tMOVQ\t"); emitoff((scr + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((slot_off + k): i64, basereg); emitline("\n"); k += 8; }; }; // cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore // for the natural "BP" case and via the wrapper's scratch path for // pointer-rooted dst. Direct callers exist only in case of future // inlined uses inside this file; new code should call the wrapper. fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz: i32) void = { let dt: *node = resolvetagged(c, dst); if (dt == nil) { return; }; // Nullable fold: one 8B word holding the pointer (or 0 for void). if (isnullabletype(dst)) { cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; // `expr: TaggedAlias` where the cast's destination IS the union // itself is a widening, not a re-interpret. cgexpr on a CAST // produces the inner's register shape (str: AX=ptr, BX=len), not // the tagged AX/DX/CX triple — so peel to the inner and route // through the matching concrete-variant branch below. A cast to // a concrete variant (`7: i32`) is left intact so the existing // scalar / str / slice branches pick the right variant tag. if (src != nil) { if (src.kind == nkind.N_CAST) { if (src.lhs != nil) { let inner: *node = src.lhs; let inneristagged: bool = false; if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { inneristagged = istaggedtype(c, lc.tnode); }; }; if (rhstaggedabicall(c, inner)) { inneristagged = true; }; // Cast's destination = the dst tagged union // itself? The rhs of N_CAST holds the target // type. Compare nominally via str match on // the tagged-alias name. let castisdst: bool = false; let castrhs: *node = src.rhs; if (castrhs != nil) { if (castrhs.kind == nkind.N_TTAGGED) { castisdst = true; }; if (castrhs.kind == nkind.N_TNAME) { if (dst != nil) { if (dst.kind == nkind.N_TNAME) { if (streq(castrhs.str, dst.str)) { castisdst = true; }; }; }; }; }; if (castisdst && !inneristagged) { src = inner; }; }; }; }; // Tagged source ident: byte-copy slot words then tag-remap. let st: *node = rhstaggedident(c, src); if (st != nil) { let lc: *local = localfindnode(c, src.str); let ssz: i32 = slotsize(c, lc.tnode); let soff: i32 = lc.off; let k: i32 = 0; for (k < ssz) { emitline("\tMOVQ\t"); emitoff((soff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + k): i64); emitline("(BP)\n"); k += 8; }; if (ssz < slot_sz) { emitline("\tXORQ\tAX, AX\n"); let p: i32 = ssz; for (p < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + p): i64); emitline("(BP)\n"); p += 8; }; }; cgwidentagremap(c, dt, st, slot_off); return; }; // Tagged source via AX/DX/CX/R8 register ABI (N_CALL, N_INDEX // of tagged element). R8 carries the 4th word for slice-payload // variants (slot 32B). if (rhstaggedabicall(c, src)) { cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); emitline("(BP)\n"); if (slot_sz > 8) { emitline("\tMOVQ\tDX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); }; if (slot_sz > 16) { emitline("\tMOVQ\tCX, "); emitoff((slot_off + 16): i64); emitline("(BP)\n"); }; if (slot_sz > 24) { emitline("\tMOVQ\tR8, "); emitoff((slot_off + 24): i64); emitline("(BP)\n"); }; return; }; // Struct payload (literal or ident). let sname: str = rhsstructpayload(c, src); if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { emitline("\tXORQ\tAX, AX\n"); let zoff: i32 = 0; for (zoff < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + zoff): i64); emitline("(BP)\n"); zoff += 8; }; let tag: i32 = taggedvariantindex(c, dt, src); if (tag < 0) { tag = 0; }; if (src.kind == nkind.N_STRUCTLIT) { let fnode: *node = src.list; for (fnode != nil) { if (fnode.kind == nkind.N_FIELD) { let fname: str = fnode.str; let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fname)) { cgexpr(c, fnode.lhs); if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((slot_off + 8 + fi.foff): i64); emitline("(BP)\n"); } else { if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + fi.foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot_off + 8 + fi.foff + 8): i64); emitline("(BP)\n"); } else { let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((slot_off + 8 + fi.foff): i64); emitline("(BP)\n"); }; }; fi = nil; } else { fi = fi.finext; }; }; }; fnode = fnode.next; }; } else { // Struct ident source: byte-copy struct words to slot+8+k. let lc: *local = localfindnode(c, src.str); let soff: i32 = 0; if (lc != nil) { soff = lc.off; }; let stotal: i32 = si.totsize; let ki: i32 = 0; for (ki + 8 <= stotal) { emitline("\tMOVQ\t"); emitoff((soff + ki): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + ki): i64); emitline("(BP)\n"); ki += 8; }; if (ki < stotal) { let tail: i32 = stotal - ki; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((soff + ki): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitoff((slot_off + 8 + ki): i64); emitline("(BP)\n"); }; }; emitline("\tMOVQ\t$"); emitint(tag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; }; // Str payload. if (nodeisstr(c, src)) { cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot_off + 16): i64); emitline("(BP)\n"); let tag: i32 = taggedvariantindex(c, dt, src); if (tag < 0) { tag = 0; }; emitline("\tMOVQ\t$"); emitint(tag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; // Slice payload (24B): cgexpr leaves (AX=ptr, BX=len, CX=cap). // Slot layout: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. if (nodeisslice(c, src)) { cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot_off + 16): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((slot_off + 24): i64); emitline("(BP)\n"); let tag: i32 = taggedvariantindex(c, dt, src); if (tag < 0) { tag = 0; }; emitline("\tMOVQ\t$"); emitint(tag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; // Scalar payload. cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); let tag: i32 = taggedvariantindex(c, dt, src); if (tag < 0) { tag = 0; }; emitline("\tMOVQ\t$"); emitint(tag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; // Spine-walk a chained N_DOT (n) inward to a root ident, summing field // offsets through value-struct intermediates. Optional slice/str leaf // pseudo-field (.ptr / .len / .cap) on the last segment is folded into // *outslicedelta (0/8/16); otherwise *outleaffi is the leaf fieldinfo // and *outslicedelta stays -1. Returns true on success; on false the // caller falls through to other branches. // // Mirrors cmd/w6c/cgen.c's N_DOT chained walker; both stages must agree // on the same shapes so the bootstrap fixed-point holds. The chain // depth is capped at 16 — deeper chains are vanishingly rare and fall // through. // // On success the caller emits one load/store at root_base + *outtotaloff // (+ slicedelta for pseudo leaf). Root resolves as: local frame slot // (*outisglobal false, base = *outrootoff(BP)) or top-level let // (*outisglobal true, base reached via LEAQ *outrootname(SB), CX). // // Numeric out-params are i32 — offsets fit naturally and the post-#19 // localloadop sign-extends i32 deref-stored slots on read, so negative // frame offsets round-trip intact. export fn dotchainresolve(c: *cgen, n: *node, outrootname: *str, outrootoff: *i32, outtotaloff: *i32, outleaffi: **fieldinfo, outslicedelta: *i32, outisglobal: *bool, outptrroot: *bool) bool = { *outrootname = ""; *outrootoff = 0; *outisglobal = false; *outptrroot = false; *outtotaloff = 0; *outleaffi = nil; *outslicedelta = -1; if (n == nil) { return false; }; if (n.kind != nkind.N_DOT) { return false; }; let stk: [16]*node; let nsteps: i32 = 0; let cur: *node = n; for (cur != nil) { if (cur.kind != nkind.N_DOT) { break; }; if (nsteps >= 16) { return false; }; stk[nsteps] = cur; nsteps += 1; cur = cur.lhs; }; if (nsteps < 2) { return false; }; if (cur == nil) { return false; }; if (cur.kind != nkind.N_IDENT) { return false; }; *outrootname = cur.str; let rootstruct: str = ""; let lc: *local = localfindnode(c, cur.str); if (lc != nil) { if (lc.tnode != nil) { if (lc.tnode.kind == nkind.N_TNAME) { rootstruct = lc.tnode.str; *outrootoff = lc.off; }; // `*T` root (param/local): dereference at emit time; // pointee struct supplies the field layout. Callers // that opt in via *outptrroot emit a MOVQ load of the // slot before indexing. if (lc.tnode.kind == nkind.N_TPTR) { let pe: *node = lc.tnode.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { rootstruct = pe.str; *outrootoff = lc.off; *outptrroot = true; }; }; }; }; }; if (rootstruct.len == 0) { let gsi: *structinfo = letvarstructinfo(c, cur.str); if (gsi != nil) { rootstruct = gsi.sname; *outisglobal = true; }; }; if (rootstruct.len == 0) { return false; }; let curstruct: str = rootstruct; let i: i32 = nsteps - 1; for (i >= 0) { let csi: *structinfo = structlookup(c, curstruct); if (csi == nil) { return false; }; if (stk[i] == nil) { return false; }; let stepnm: str = stk[i].str; let fi: *fieldinfo = csi.fields; let found: *fieldinfo = nil; for (fi != nil) { if (streq(fi.fname, stepnm)) { found = fi; break; }; fi = fi.finext; }; if (found == nil) { return false; }; if (i == 0) { *outtotaloff = *outtotaloff + found.foff; *outleaffi = found; return true; }; let ft: *node = found.tnode; if (ft == nil) { return false; }; if (ft.kind == nkind.N_TNAME) { if (streq(ft.str, "str")) { if (i != 1) { return false; }; let pseudo: str = stk[0].str; let delta: i32 = -1; if (streq(pseudo, "ptr")) { delta = 0; } else { if (streq(pseudo, "len")) { delta = 8; }; }; if (delta < 0) { return false; }; *outtotaloff = *outtotaloff + found.foff; *outslicedelta = delta; return true; }; if (primsize(ft.str) != 0) { return false; }; *outtotaloff = *outtotaloff + found.foff; curstruct = ft.str; i -= 1; } else { if (ft.kind == nkind.N_TSLICE) { if (i != 1) { return false; }; let pseudo: str = stk[0].str; let delta: i32 = -1; if (streq(pseudo, "ptr")) { delta = 0; } else { if (streq(pseudo, "len")) { delta = 8; } else { if (streq(pseudo, "cap")) { delta = 16; }; }; }; if (delta < 0) { return false; }; *outtotaloff = *outtotaloff + found.foff; *outslicedelta = delta; return true; } else { return false; }; }; }; return false; }; // cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT // value into one of three destination flavors. Mirror of cstage // cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT, // cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs // N_STRUCTLIT (BP-rel / via *struct local / via struct global) at // single-dot and chained-dot sites. // // Destination modes: // 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP). // srcoff/srcname unused. // 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before // the ELLIPSIS zero-fill loop and before EVERY // field store (cgexpr clobbers BX between // fields). Stores at disp+i(BX). srcname // unused. // 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB), // BX` with the same cadence as DST_PTR_LOCAL. // srcoff unused. // // Param semantics (locked in here so the recursion contract is // clear): // - `disp` is the per-recursion accumulator — grows by `fi.foff` // as we descend into a nested struct-typed structlit field. // - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are // *constant* across the whole call tree — they identify the // root dst, which doesn't change with depth. // - `totsize` is also constant; pass the natural size for dot // sites (structnaturalsize) and si.totsize for BP-rel sites, // matching each site's pre-#18 zero-fill bound. // // Why a helper? The inline field-walk previously did // `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose // value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct- // in-register convention — it lands AX = first qword and the // trailing bytes silently stay zero. #17 fixed the BP-rel sites; // #18 extends the same recursion to the four cgassign N_DOT-lhs // structlit walks (single-dot via_ptr/global/local + chained // depth>=2). // // The non-BP modes emit a redundant BX reload at the start of each // recursive nested zero-fill / each recursive scalar store — this is // correctness-by-construction (BX is always freshly loaded right // before use), and the redundancy only fires on the nested-STRUCTLIT // shapes that didn't compile before. Byte-identity for the no- // nested case (the only shape selfhost source uses today) is // preserved because the existing inline code's reload-before-each- // store pattern matches the helper's per-store reload exactly. // // Graduation note (task #13): the scalar store currently uses the // explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage // byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once // #13 aligns both stages, the dispatch can switch to fieldstoreop // which already returns MOVW where appropriate. fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node, mode: i32, srcoff: i32, srcname: str, disp: i32, totsize: i32) void = { if (si == nil) { return; }; let basereg: str = "BP"; if (mode != 0) { basereg = "BX"; }; if (lit.op == tkind.TK_ELLIPSIS) { // `..., ...` autofill — zero the entire slot first so // unmentioned fields read as 0. Sized stores: 8/4/1. For // non-BP modes, reload BX once before the loop (cgexpr-free // region between iterations, so one reload is enough). emitline("\tXORQ\tAX, AX\n"); if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), BX\n"); }; if (mode == 2) { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), BX\n"); }; let zi: i32 = 0; for (zi + 8 <= totsize) { emitline("\tMOVQ\tAX, "); if (mode == 0) { emitoff((disp + zi): i64); emitline("(BP)\n"); } else { emitdispreg((disp + zi): i64, basereg); emitline("\n"); }; zi += 8; }; for (zi + 4 <= totsize) { emitline("\tMOVL\tAX, "); if (mode == 0) { emitoff((disp + zi): i64); emitline("(BP)\n"); } else { emitdispreg((disp + zi): i64, basereg); emitline("\n"); }; zi += 4; }; for (zi < totsize) { emitline("\tMOVB\tAX, "); if (mode == 0) { emitoff((disp + zi): i64); emitline("(BP)\n"); } else { emitdispreg((disp + zi): i64, basereg); emitline("\n"); }; zi += 1; }; }; let fieldnode: *node = lit.list; for (fieldnode != nil) { if (fieldnode.kind == nkind.N_FIELD) { let fname: str = fieldnode.str; let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fname)) { // Tagged-union field: delegate to the shared // widening writer (handles str/scalar/struct // literal/ident payload + tagged-subset tag // remap). For non-BP modes, reload BX first so // the widener sees a valid base reg. if (istaggedtype(c, fi.tnode)) { if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), BX\n"); }; if (mode == 2) { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), BX\n"); }; cgwidentaggedstore(c, fi.tnode, fieldnode.lhs, basereg, disp + fi.foff, fi.fsz); fi = nil; } else { // Nested struct-typed structlit value: look up // the inner struct's metadata and recurse at the // field's offset. Pre-#17/#18 the cgexpr-then- // store below would land AX = first qword and // the rest silently stayed zero. let nested: bool = false; if (fieldnode.lhs != nil) { if (fieldnode.lhs.kind == nkind.N_STRUCTLIT) { if (fi.tnode != nil) { if (fi.tnode.kind == nkind.N_TNAME) { if (primsize(fi.tnode.str) == 0) { let isi: *structinfo = structlookup(c, fi.tnode.str); if (isi != nil) { // Nested fill: pick the size // discipline matching the outer // site — dot sites pass natural // size, BP-rel sites pass // totsize. Mirror it. let inner_tot: i32 = isi.totsize; if (mode != 0) { inner_tot = structnaturalsize(isi); }; cgstructlitfill(c, isi, fieldnode.lhs, mode, srcoff, srcname, disp + fi.foff, inner_tot); nested = true; }; }; }; }; }; }; // Nested struct-typed CALL value (#20). cgexpr // leaves AX=bytes[0..7], DX=bytes[8..15], CX= // bytes[16..23] per #4's cgreturn ABI. Pre-#20 // the cgexpr-then-AX-store fallthrough below // silently dropped past the first qword for any // fsz > 8 (only AX got stored). // // Sized stores: MOVQ for full 8B chunks plus a // sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`. // Mirror of cstage cg_structlit_fill's #20 branch. // MOVW-for-tail==2 only fires on shapes that // didn't compile before, so no #13 byte-identity // concern. // // Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches // #4's cgreturn ABI: >24B falls through (sret // deferred); fsz%8 ∈ {3,5,6,7} would need shift- // store and is also unsupported by #4 — falls // through to the existing AX-only wrongness // (consistent, tracked as follow-up). // // INVARIANT: between cgexpr(N_CALL) and the // AX/DX/CX stores below, NO instruction may touch // AX/DX/CX. The BX reload is safe; any other // emission added here will silently corrupt the // return value. let callwhole: bool = false; if (!nested) { if (fieldnode.lhs != nil) { if (fieldnode.lhs.kind == nkind.N_CALL) { if (fi.tnode != nil) { if (fi.tnode.kind == nkind.N_TNAME) { if (primsize(fi.tnode.str) == 0) { let csi: *structinfo = structlookup(c, fi.tnode.str); if (csi != nil) { // Use the inner struct's // NATURAL size (no 8B slot // rounding) so MOVL/MOVW/ // MOVB tail dispatch matches // cstage's fl->type->size // (which is natural per // check.c). fi.fsz here is // wwstage's slot-padded // totsize — using it would // emit 2× MOVQ where cstage // emits MOVQ+MOVL for a // 12B inner, etc. (task #15 // territory; sidestepped // locally.) let cfsz: i32 = structnaturalsize(csi); let crem: i32 = cfsz - (cfsz / 8) * 8; if (cfsz <= 24) { if (crem == 0 || crem == 1 || crem == 2 || crem == 4) { cgexpr(c, fieldnode.lhs); if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), BX\n"); }; if (mode == 2) { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), BX\n"); }; let full: i32 = cfsz / 8; let ci: i32 = 0; for (ci < full) { let r: str = "AX"; if (ci == 1) { r = "DX"; }; if (ci == 2) { r = "CX"; }; emitline("\tMOVQ\t"); emitline(r); emitline(", "); if (mode == 0) { emitoff((disp + fi.foff + ci * 8): i64); emitline("(BP)\n"); } else { emitdispreg((disp + fi.foff + ci * 8): i64, basereg); emitline("\n"); }; ci += 1; }; if (crem > 0) { let top: str = "MOVB"; if (crem == 4) { top = "MOVL"; }; if (crem == 2) { top = "MOVW"; }; let tr: str = "AX"; if (full == 1) { tr = "DX"; }; if (full == 2) { tr = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(tr); emitline(", "); if (mode == 0) { emitoff((disp + fi.foff + full * 8): i64); emitline("(BP)\n"); } else { emitdispreg((disp + fi.foff + full * 8): i64, basereg); emitline("\n"); }; }; callwhole = true; }; }; }; }; }; }; }; }; }; if (nested) { fi = nil; } else if (callwhole) { fi = nil; } else { cgexpr(c, fieldnode.lhs); // For non-BP modes, cgexpr just clobbered // BX; reload it before the store. if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), BX\n"); }; if (mode == 2) { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), BX\n"); }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); if (mode == 0) { emitoff((disp + fi.foff): i64); emitline("(BP)\n"); } else { emitdispreg((disp + fi.foff): i64, basereg); emitline("\n"); }; fi = nil; } else { // Explicit {1→MOVB, 4→MOVL, else MOVQ} // dispatch (not fieldstoreop) to match // cstage byte-identically. wwstage's // fieldstoreop would return MOVW for // fsz==2 which cstage doesn't emit — // tracked as task #13. let fsz: i32 = fi.fsz; let op: str = "MOVQ"; if (fsz == 1) { op = "MOVB"; }; if (fsz == 4) { op = "MOVL"; }; emitline("\t"); emitline(op); emitline("\tAX, "); if (mode == 0) { emitoff((disp + fi.foff): i64); emitline("(BP)\n"); } else { emitdispreg((disp + fi.foff): i64, basereg); emitline("\n"); }; fi = nil; }; }; }; } else { fi = fi.finext; }; }; }; fieldnode = fieldnode.next; }; }; // Thin wrapper preserving the BP-rel call shape used by cglet, // cgreturn, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to // the pre-#18 cgstructlitfillbp. fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (si == nil) { return; }; cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize); }; // 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 // (nkind.N_INTLIT, nkind.N_RUNELIT, nkind.N_TRUE/FALSE/NIL, nkind.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: nkind = n.kind; if (k == nkind.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 == nkind.N_FLOATLIT) { // Materialise the f64 bit pattern in AX, push, then MOVSD it // into X0. The bits come from n.uval — the parser populates // it from the lexer's bitcast of t.fval, so this path stays // integer-only (no SSE in the cgen source). The f32 // narrowing is handled at the consumer site, not here — the // literal always carries the full double precision until // typed by context. emitline("\tMOVQ\t$"); emitint(n.uval: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVSD\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); return; }; if (k == nkind.N_RUNELIT) { emitline("\tMOVQ\t$"); emitint(n.uval: i64); emitline(", AX\n"); return; }; if (k == nkind.N_STRLIT) { cgstrlit(c, n); return; }; if (k == nkind.N_TRUE) { emitline("\tMOVQ\t$1, AX\n"); return; }; if (k == nkind.N_FALSE) { emitline("\tMOVQ\t$0, AX\n"); return; }; if (k == nkind.N_NIL) { emitline("\tMOVQ\t$0, AX\n"); return; }; if (k == nkind.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 == nkind.N_IDENT) { cgident(c, n); return; }; if (k == nkind.N_INDEX) { cgindex(c, n); return; }; if (k == nkind.N_SLICE) { cgslice(c, n); return; }; if (k == nkind.N_MATCH) { cgmatch(c, n); return; }; if (k == nkind.N_CAST) { cgcast(c, n); return; }; if (k == nkind.N_DOT) { cgdot(c, n); return; }; if (k == nkind.N_UN) { cgun(c, n); return; }; if (k == nkind.N_BIN) { cgbin(c, n); return; }; if (k == nkind.N_CALL) { cgcall(c, n); return; }; if (k == nkind.N_ASSIGN) { cgassign(c, n); return; }; if (k == nkind.N_TRYPROP) { cgtryprop(c, n); return; }; if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; // Default fallback: produce a deterministic AX = 0. Mirrors // the C cgen's `default: cgexpr_int(c, 0)` branch, which is // what `return eof{};` (N_STRUCTLIT with an empty !void // variant) silently relies on — without this AX carries a // stale value into the tagged-union return shuffle. emitline("\tMOVQ\t$0, AX\n"); }; // cgtagvariantidx — find the 0-based variant index of `vt` inside the // tagged-union type expression `tagged`. -1 if `tagged` isn't an // nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch // does inline; pulled out so `is` / `as` can reuse it. fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; let want: str; want.ptr = nil; want.len = 0; if (vt.kind == nkind.N_TNAME) { want = vt.str; }; if (want.len == 0) { return -1; }; return flatvariantidx(c, tagged, want); }; // 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 == nkind.N_CALL) { let callee: *node = n.lhs.lhs; if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; if (callee.kind == nkind.N_DOT) { cname = callee.str; }; if (cname.len > 0) { let rt: *node = fnretlookup(c, cname); if (rt != nil) { if (rt.kind == nkind.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 == nkind.N_CALL) { let callee: *node = n.lhs.lhs; if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; if (callee.kind == nkind.N_DOT) { cname = callee.str; }; if (cname.len > 0) { let rt: *node = fnretlookup(c, cname); if (rt != nil) { if (rt.kind == nkind.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 == nkind.N_IDENT) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; }; }; let want: i32 = cgtagvariantidx(c, 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 nkind.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: nkind = e.kind; if (k == nkind.N_DOT) { if (e.lhs != nil) { if (e.lhs.kind == nkind.N_IDENT) { if (enumlookup(c, e.lhs.str) != nil) { return true; }; }; }; }; if (k == nkind.N_IDENT) { let lc: *local = localfindnode(c, e.str); if (lc != nil) { if (lc.tnode != nil) { if (lc.tnode.kind == nkind.N_TNAME) { if (enumlookup(c, lc.tnode.str) != nil) { return true; }; }; }; }; }; if (k == nkind.N_BIN) { if (isenumexpr(c, e.lhs)) { return true; }; if (isenumexpr(c, e.rhs)) { return true; }; }; if (k == nkind.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 == nkind.N_TENUM) { return true; }; if (t.kind == nkind.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 == nkind.N_IDENT) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; }; }; let want: i32 = cgtagvariantidx(c, 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 cgcast(c: *cgen, n: *node) void = { let srcfk: i32 = exprfloatkind(c, n.lhs); let dstf64: bool = isfloattype(c, n.rhs); let dstf32: bool = isf32type(c, n.rhs); let dstfk: i32 = 0; if (dstf32) { dstfk = 1; } else { if (dstf64) { dstfk = 2; }; }; cgexpr(c, n.lhs); // str → []T: cgexpr left (AX=ptr, BX=len). Slice register // convention is (AX=ptr, BX=len, CX=cap); synthesise cap = len // so downstream arg-push / let-init paths see the canonical // triple. Detect via dst-is-slice + src-ident's local-tnode // being str (the common shape; non-ident sources rare). if (isslicetype(c, n.rhs)) { let srcstr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.lhs.str); if (lc != nil) { if (isstrtype(c, lc.tnode)) { srcstr = true; }; }; }; }; if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); }; }; // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; // int↔int casts narrow via an explicit clamp before the early // return so `(big_u64): u32` doesn't leak the upper 32 bits. // Hare semantics: `expr: T` truncates to T's bit width (mod 2^n). // Mirrors cmd/w6c/cgen.c's N_CAST clamp. Unsigned narrow clears // the upper bits via MOVL/ANDQ; signed narrow sign-extends via // MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates. if (srcfk == 0 && dstfk == 0) { let tn: *node = n.rhs; // Walk through alias chains (`type random = u64`) and the // `!T` error-flag wrapper (`type invalid = !i32`) — the // bang is a tagged-union marker, not a representational // change, so it must not block the narrow-cast clamp. for (tn != nil) { if (tn.kind == nkind.N_TBANG) { tn = tn.lhs; } else { if (tn.kind != nkind.N_TNAME) { tn = nil; } else { let nm: str = tn.str; if (primsize(nm) > 0) { break; }; let alias: *node = aliaslookup(c, nm); if (alias == nil) { tn = nil; } else { tn = alias; }; }; }; }; if (tn != nil) { let nm: str = tn.str; let sz: i32 = primsize(nm); let is_unsigned: bool = typenameisunsigned(nm); let is_bool: bool = streq(nm, "bool"); // Symmetric narrow on signed vs unsigned (task #5): // unsigned (incl. rune) clears upper bits; signed // sign-extends. bool is size 1 but neither — falls // through to its dedicated ANDQ $255 below. if (sz > 0) { if (sz < 8) { if (!is_bool) { if (is_unsigned) { if (sz == 4) { emitline("\tMOVL\tAX, AX\n"); } else { let mask: i64 = 0xFFi64; if (sz == 2) { mask = 0xFFFFi64; }; emitline("\tANDQ\t$"); emitint(mask); emitline(", AX\n"); }; } else { if (sz == 1) { emitline("\tMOVSBQ\tAX, AX\n"); } else { if (sz == 2) { emitline("\tMOVSWQ\tAX, AX\n"); } else { if (sz == 4) { emitline("\tMOVSXD\tAX, AX\n"); }; }; }; }; }; }; }; if (is_bool) { emitline("\tANDQ\t$255, AX\n"); }; }; return; }; if (srcfk == 0 && dstfk == 2) { emitline("\tCVTSI2SD\tAX, X0\n"); return; }; if (srcfk == 0 && dstfk == 1) { emitline("\tCVTSI2SS\tAX, X0\n"); return; }; if (srcfk == 2 && dstfk == 0) { emitline("\tCVTTSD2SI\tX0, AX\n"); return; }; if (srcfk == 1 && dstfk == 0) { emitline("\tCVTTSS2SI\tX0, AX\n"); return; }; if (srcfk == 2 && dstfk == 1) { emitline("\tCVTSD2SS\tX0, X0\n"); return; }; if (srcfk == 1 && dstfk == 2) { emitline("\tCVTSS2SD\tX0, X0\n"); return; }; // Same-kind float→float: nothing to emit. }; 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; // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle // so consumers (cgbin, cgcast, return) pick up the SSE value // directly. if (isfloattype(c, lc.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, lc.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff(off: i64); emitline("(BP), X0\n"); return; }; // str / slice locals load (ptr[, len[, cap]]) through MOVQ // since the header is always 8B-clean. Scalar locals route // through localloadop so signed-narrow slots sign-extend // after a narrow deref-store. let isstr: bool = isstrtype(c, lc.tnode); let issl: bool = isslicetype(c, lc.tnode); let lop: str = "MOVQ"; if (!isstr) { if (!issl) { lop = localloadop(c, lc.tnode); }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff(off: i64); emitline("(BP), AX\n"); if (isstr) { emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), BX\n"); }; if (issl) { 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 // emitfnname 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. // Bare ident → same-module by ww's resolver, hint with c.curmod. let rt: *node = fnretlookup(c, nm); if (rt != nil) { emitline("\tLEAQ\t"); emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; // Top-level mutable `let` — RIP-relative load from its DATAW // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for // scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence // for str / slice globals so the ABI pair / triple lands in // (AX, BX[, CX]). Names that aren't lets either (typos, // never-defined) drop through to the silent return. if (isletvar(c, nm)) { let isstr: bool = letvarisstr(c, nm); let issl: bool = letvarisslice(c, nm); if (isstr || issl) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); if (issl) { // Overwrites the address holder with the // cap as the last step — CX is no longer // needed once both ptr/len are loaded. emitline("\tMOVQ\t16(CX), CX\n"); }; return; }; // Float global: same LEAQ-indirect shape, since MOVSS/ // MOVSD have no D_EXTERN operand form in w6a. Signed-narrow // scalar globals route through the same LEAQ scratch since // MOVSXD/MOVSWQ/MOVSBQ also have no D_EXTERN form. let lvtnode: *node = nil; let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, nm)) { if (isfloattype(c, lv.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); emitline("\t(CX), X0\n"); return; }; lvtnode = lv.tnode; lv = nil; } else { lv = lv.lvnext; }; }; let glop: str = localloadop(c, lvtnode); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); emitline("\t(CX), AX\n"); }; return; }; return; }; fn cgindex(c: *cgen, n: *node) void = { // Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL, // str → (ptr, len) into (AX, BX), 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 signed_elem: bool = false; let baselocal: *local = nil; // Global `[N]T` array or `*T` pointer used as an index base. // The local-ident lookup above misses it; we need LEAQ name(SB) // (array, the symbol IS the storage) or MOVQ name(SB) (pointer, // the symbol holds the address) to feed the addend. let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); signed_elem = elemissignedc(c, baselocal.tnode); } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); signed_elem = elemissignedc(c, tn); }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeofc(c, tn); signed_elem = elemissignedc(c, tn); }; }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); };}; }; // Tagged-union element: load slot words into (AX=tag, DX=val0, // CX=val1) matching the tagged-return ABI so call-arg / let / // match consumers see the same shape as a tagged-returning fn. // Slot size = esz (8/16/24); nullable folded element is one // word, which the fallthrough below handles via MOVQ AX. let elem_tagged: bool = false; let elem_slot_sz: i32 = esz; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bl: *local = baselocal; let etn: *node = nil; if (bl != nil) { let btn: *node = bl.tnode; if (btn != nil) { let bk: nkind = btn.kind; if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; if (bk == nkind.N_TPTR) { etn = btn.lhs; }; }; } else { let tn: *node = letvartnode(c, base.str); if (tn != nil) { let bk: nkind = tn.kind; if (bk == nkind.N_TARRAY) { etn = tn.lhs; }; if (bk == nkind.N_TSLICE) { etn = tn.lhs; }; if (bk == nkind.N_TPTR) { etn = tn.lhs; }; }; }; if (istaggedtype(c, etn)) { if (!isnullabletype(etn)) { elem_tagged = true; elem_slot_sz = slotsize(c, etn); esz = elem_slot_sz; }; }; }; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr || isglobalptr) { if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (elem_tagged) { if (elem_slot_sz > 24) { emitline("\tMOVQ\t24(BX), R8\n"); }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; if (esz == 16) { emitline("\tMOVQ\t8(BX), CX\n"); emitline("\tMOVQ\t(BX), AX\n"); emitline("\tMOVQ\tCX, BX\n"); return; }; let lop1: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop1); emitline("\t(BX), AX\n"); return; }; if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.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"); if (elem_tagged) { if (elem_slot_sz > 24) { emitline("\tMOVQ\t24(BX), R8\n"); }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; // 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; }; let lop2: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop2); emitline("\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 (elem_tagged) { // AX holds the element address. Copy to BX (loading slot+0 // into AX clobbers it), then read slot words. emitline("\tMOVQ\tAX, BX\n"); if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; if (esz == 16) { emitline("\tMOVQ\t8(AX), BX\n"); emitline("\tMOVQ\t(AX), AX\n"); return; }; let lop3: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop3); emitline("\t(AX), AX\n"); return; }; // cgslice — `base[lo:hi]` as a slice value. Leaves (AX=base+lo, // BX=hi-lo, CX=hi-lo) so callers can route to a slice slot, // return, or arg with the same triple ABI. Cap defaults to the // new length; no syntax for a wider cap yet. Element scaling // on the ptr isn't wired — non-u8 slices need a follow-up audit. fn cgslice(c: *cgen, n: *node) void = { let base: *node = n.lhs; let lo: *node = n.rhs; let hi: *node = n.cond; let baselocal: *local = nil; let globaltn: *node = nil; let globalname: str; globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { baselocal = localfindnode(c, base.str); if (baselocal == nil) { let gt: *node = letvartnode(c, base.str); if (gt != nil) { globaltn = gt; globalname = base.str; }; }; }; }; // base address if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; if (isarray) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), AX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), AX\n"); }; } else { if (globaltn != nil) { // Top-level let: [N]T → LEAQ name(SB); pointer/slice/str // → MOVQ name(SB) (the symbol holds the {ptr,len,cap} or // {ptr,len} or pointer value). if (globaltn.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); }; } else { if (base != nil) { cgexpr(c, base); };};}; emitline("\tPUSHQ\tAX\n"); // lo (default 0) if (lo != nil) { cgexpr(c, lo); } else { emitline("\tMOVQ\t$0, AX\n"); }; emitline("\tPUSHQ\tAX\n"); // hi (default base length) if (hi != nil) { cgexpr(c, hi); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let handled: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { let lenn: *node = tn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; }; }; } else { if (tn.kind == nkind.N_TSLICE) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); handled = true; } else { if (tn.kind == nkind.N_TNAME) { if (streq(tn.str, "str")) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); handled = true; }; };};}; }; if (!handled) { emitline("\tMOVQ\t$0, AX\n"); }; } else { if (globaltn != nil) { let handled: bool = false; if (globaltn.kind == nkind.N_TARRAY) { let lenn: *node = globaltn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; }; }; } else { if (globaltn.kind == nkind.N_TSLICE) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), CX\n"); emitline("\tMOVQ\t8(CX), AX\n"); handled = true; };}; if (!handled) { emitline("\tMOVQ\t$0, AX\n"); }; } else { emitline("\tMOVQ\t$0, AX\n"); };};}; emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); emitline("\tADDQ\tCX, AX\n"); emitline("\tSUBQ\tCX, BX\n"); emitline("\tMOVQ\tBX, CX\n"); }; 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 == nkind.N_IDENT) { let lc: *local = localfindnode(c, scrut.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow // the AX:DX:CX convention; tagged-element loads // (N_INDEX) and tagged-field loads (N_DOT, fixed by // #28) produce the same triple. Nullable returns are // single-word (AX = ptr); only +0 is read, so the // extra stores are harmless. We recover the scrutinee // type from fnretlookup (N_CALL), the base local's // array element type (N_INDEX), or the struct field // type (N_DOT) so dispatch can compute variant // indices. Slot size is then derived from the // scrutinee type so slice-variant tagged-unions // (32B slot) don't overflow a hardcoded 24B scratch. if (scrut.kind == nkind.N_CALL) { let callee: *node = scrut.lhs; if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); if (rt != nil) { scrutt = resolvetagged(c, rt); }; }; }; }; if (scrut.kind == nkind.N_INDEX) { let ibase: *node = scrut.lhs; if (ibase != nil) { if (ibase.kind == nkind.N_IDENT) { let bl: *local = localfindnode(c, ibase.str); let btn: *node = nil; if (bl != nil) { btn = bl.tnode; } else { btn = letvartnode(c, ibase.str); }; if (btn != nil) { let bk: nkind = btn.kind; let etn: *node = nil; if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; if (bk == nkind.N_TPTR) { etn = btn.lhs; }; if (etn != nil) { scrutt = resolvetagged(c, etn); }; }; }; }; }; if (scrut.kind == nkind.N_DOT) { let ft: *node = dotfieldtnode(c, scrut); if (ft != nil) { scrutt = resolvetagged(c, ft); }; }; // Size the spill to the scrutinee slot. Default 24B // preserves the historical alloc for non-tagged or // unresolved cases (nullable, str-returning, etc.). let spillsz: i32 = 24; if (scrutt != nil) { let resolved: i32 = slotsize(c, scrutt); if (resolved > spillsz) { spillsz = resolved; }; }; scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); emitline("(BP)\n"); if (!isnullabletype(scrutt)) { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((scrutoff + 16): i64); emitline("(BP)\n"); // R8 carries the 4th return word when the // scrutinee's tagged union has a slice-payload // variant (slot 32B). Harmless for narrower // returns — R8 is callee-clobbered either way. let ssz: i32 = slotsize(c, scrutt); if (ssz > 24) { emitline("\tMOVQ\tR8, "); emitoff((scrutoff + 24): i64); emitline("(BP)\n"); }; }; }; }; 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); // Per-arm scope: save c.locals before allocating the bind // and restore after the body runs, so the arm's bind (and // any nested lets) don't leak past the arm. Matches the // checker's newscope/restore around N_MCASE. Without this, // `let e: *T = ...; match (r) { case let e: str => ... }; // use e` would resolve `e` after the match to the inner // str slot instead of the outer ptr. let arm_locals_saved: *local = c.locals; // 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 == nkind.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 == nkind.N_TTAGGED) { let patname: str; patname.ptr = nil; patname.len = 0; if (pat.kind == nkind.N_TNAME) { patname = pat.str; }; let r: i32 = flatvariantidx(c, scrutt, patname); if (r >= 0) { want = r; }; }; }; 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 == nkind.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 { // Size the bind from the variant's declared // layout. slotsize covers str (16), []T (24), // N_TNAME named struct (si.totsize), aliases, // tuples, primitives (8). Hardcoding str/slice // + fall-through-8 dropped the high words of a // TY_STRUCT variant (e.g. only v.x reached the // bind for `case let v: pair`, project #31); // mirrors cstage's `bu->size` fallback in // cgen.c cgmatch. let bsz: i32 = slotsize(c, pat); if (bsz <= 0) { bsz = 8; }; // 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); // Word-by-word copy. Round bsz up to 8 in case // a non-multiple-of-8 struct size leaked through // (registerstruct already pads totsize, but be // defensive — same shape as cstage's nwords = // (bsz + 7) / 8). let nwords: i32 = (bsz + 7) / 8; let bw: i32 = 0; for (bw < nwords) { emitline("\tMOVQ\t"); emitoff((scrutoff + 8 + 8 * bw): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((voff + 8 * bw): i64); emitline("(BP)\n"); bw += 1; }; }; }; }; // Body. Match arms are statements; we cgstmt them. if (cs.body != nil) { cgstmt(c, cs.body); }; // Restore the locals head — pop everything the arm pushed // so post-match code resolves names to their original (outer) // bindings. c.locals = arm_locals_saved; 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; // `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR, // IDENT(p)). Substitute the inner IDENT as dotlhs so the // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT // lhs retarget in cgassign. v1 scope: N_IDENT inner only; // (*expr).f follow-up task pending. Enum-leaf lookup above and // chained-N_DOT branches below keep checking raw lhs since // (*p) is neither shape. let dotlhs: *node = lhs; if (dotlhs != nil) { if (dotlhs.kind == nkind.N_UN) { if (dotlhs.op == tkind.TK_STAR) { if (dotlhs.lhs != nil) { if (dotlhs.lhs.kind == nkind.N_IDENT) { dotlhs = dotlhs.lhs; }; }; }; }; }; // 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 == nkind.N_IDENT) { etname = lhs.str; }; if (lhs.kind == nkind.N_DOT) { if (lhs.lhs != nil) { if (lhs.lhs.kind == nkind.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 (dotlhs != nil) { if (dotlhs.kind == nkind.N_IDENT) { let nm: str = dotlhs.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-to-struct: deref then field load. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.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)) { // tagged-union field via *struct: stage // the *struct in BX, then load the four // payload regs via cgloadtaggedfield. // BX isn't a target (AX/DX/CX/R8), so // load order doesn't matter. Mirrors // the direct-local branch above so the // match / let-init / call-arg consumer // shape is identical regardless of // pointer rooting. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); cgloadtaggedfield(c, "BX", fi.foff, tsz); return; }; // 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 { if (isslicetype(c, fi.tnode)) { // slice field via *struct: load // (ptr, len, cap) into (AX, BX, CX). // BX holds the *struct pointer, so // load .len LAST so the earlier // reads still index off the base. emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "BX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "BX"); emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 via *struct: route through X0. // MOVQ into AX leaves the SSE reg stale // and any downstream consumer (arg // pass, return, arithmetic) reads // garbage. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", X0\n"); } else { let op: str = fieldloadop(c, 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 == nkind.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)) { // tagged-union field: emit the AX=tag, // DX=word0, CX=word1[, R8=word2] load // sequence so the match / let-init / // call-arg consumers see the same shape // as a tagged-returning fn. Pre-#28 fell // through to the scalar fieldloadop and // only AX (tag) was loaded — payload // words came from whatever the caller // left in DX/CX/R8. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); cgloadtaggedfield(c, "BP", lc.off + fi.foff, tsz); return; }; // 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 { if (isslicetype(c, fi.tnode)) { // slice field: load (ptr, len, cap) // into (AX, BX, CX). Base is BP so // no aliasing — order doesn't matter. 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"); emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 16): i64); emitline("(BP), CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), X0\n"); } else { let op: str = fieldloadop(c, 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 == nkind.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 == nkind.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 == nkind.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 = tnodeloadop(c, tp, sz); 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 == nkind.N_TPTR) { let inner: *node = tn.lhs; let innerkind: nkind = nkind.N_NONE; if (inner != nil) { innerkind = inner.kind; }; let innerstr: bool = false; if (innerkind == nkind.N_TNAME) { if (streq(inner.str, "str")) { innerstr = true; }; }; if (innerkind == nkind.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 (SB), AX` (looking up the field name as a // symbol). Mirrors cmd/w6c/cgen.c nkind.N_DOT off==0 / Sdef branch. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let drhs: *node = deflookuprhs(c, lhs.str); if (drhs != nil) { if (drhs.kind == nkind.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; }; }; }; }; }; // Top-level str/slice global field access — load .ptr / .len // (and .cap for slices) via &name(SB) into CX, then MOVQ // delta(CX), AX. Without this the module-qualified fallback // below would mis-emit `MOVQ (SB), AX`. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (isletvar(c, lhs.str)) { let isstr: bool = letvarisstr(c, lhs.str); let issl: bool = letvarisslice(c, lhs.str); if (isstr || issl) { let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; if (issl) { if (streq(fld, "cap")) { delta = 16; }; }; if (delta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); emitline("\tMOVQ\t"); emitdispreg(delta: i64, "CX"); emitline(", AX\n"); return; }; }; }; }; }; // Top-level struct global field read — LEAQ name(SB), CX then // load at fi.foff(CX). Mirrors the local "Direct struct local" // branch above, swapping the BP frame slot for the global VA. // Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let si: *structinfo = letvarstructinfo(c, lhs.str); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); // tagged-union field: load via the tagged- // return ABI off CX. cgloadtaggedfield orders // the loads so CX (word1 target) is written // LAST — otherwise the base address would be // trashed before the +24/R8 (slice variant) // read could index off it. Pre-#28 fell // through to fieldloadop and dropped payload. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); cgloadtaggedfield(c, "CX", fi.foff, tsz); return; }; if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "CX"); emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 global field: route through X0. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", X0\n"); } else { let op: str = fieldloadop(c, fi); emitline("\t"); emitline(op); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); }; }; return; }; fi = fi.finext; }; }; }; }; // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` // (and slice/`*[N]S`) base. Without this the cgen falls through // to the module-qualified SB fallback below and emits // `MOVQ (SB), AX` (linker: `undefined reference to `). // One branch covers both shapes: compute `&arr[i]` into BX, then // either deref (`*Struct` element) or move-to-AX (value `Struct` // element), so the leaf load is `(field.offset)(AX)` either way. // Bypasses cgindex deliberately — cgindex's final MOVQ would // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; let sname: str; sname.ptr = nil; sname.len = 0; let viaptr: bool = false; if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { let inner: *node = elemt.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { sname = elemt.str; };}; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { let esz: i32 = elemsizeofc(c, tn); cgexpr(c, lhs.rhs); // idx → AX if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), AX\n"); } else { emitline("\tMOVQ\tBX, AX\n"); }; 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; }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", X0\n"); return; }; let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; }; };}; };}; }; }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case; // signed-narrow leaves route through LEAQ + localloadop so a // prior narrow deref-store doesn't leave stale upper bytes. Same // fallback the C cgen takes when bt is NULL/tyerr. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { // `let p = mod.fn` — fn rvalue via N_DOT. Mirror of // cstage cgdot's TY_FN branch (mafn with module hint). // Without this the MOVQ leaf(SB) fallback below would // load 8 bytes of fn-prologue code into AX instead of // the fn address. let frt: *node = fnretlookup(c, fld); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, lhs.str); emitline("(SB), AX\n"); return; }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, fld); emitline("(SB), CX\n"); emitline("\t"); emitline(mqop); emitline("\t(CX), AX\n"); }; return; }; }; // Chained N_DOT spine through value-struct fields (any depth). // Walks the spine to a root ident, summing field offsets, then // emits ONE load at base + total_off. Also handles a slice/str // pseudo-field leaf (`b.buf.len`): the walk lands on the slice/ // str header and slicedelta picks ptr/len/cap. Mirror of cstage // cgen.c's chained-DOT read branch. Without this, depth ≥ 3 // shapes (`v.a.a.a`) and `b.buf.len` fall through to the non- // ident-base pseudo branch below — which would cgexpr the inner // (loading only .ptr into AX) and shuffle stale BX into AX. // Placed BEFORE the .ptr/.len fast paths so the chain wins. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaffi: *fieldinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, n, &rootname, &rootoff, &totaloff, &leaffi, &slicedelta, &isglobal, &ptrroot); if (pok) { // `*T` root: load the pointer slot once into CX, // then index every leaf at total_off off CX. Same // emit shape as the global path (LEAQ → CX) — only // the loader instruction differs. let viacx: bool = isglobal || ptrroot; if (slicedelta >= 0) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg((totaloff + slicedelta): i64, "CX"); emitline(", AX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + slicedelta): i64); emitline("(BP), AX\n"); }; return; }; if (isstrtype(c, leaffi.tnode)) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 8): i64, "CX"); emitline(", BX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 8): i64); emitline("(BP), BX\n"); }; return; }; if (isslicetype(c, leaffi.tnode)) { // Slice leaf: load all three header words into // (AX=ptr, BX=len, CX=cap). For the viacx path // (global or `*T` root) CX is the base; load // .cap LAST so the base survives the earlier // reads. For BP-rooted locals the registers // don't alias so order is free. if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 16): i64, "CX"); emitline(", CX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 16): i64); emitline("(BP), CX\n"); }; return; }; if (isfloattype(c, leaffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, leaffi.tnode)) { mov = "MOVSS"; }; if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(totaloff: i64, "CX"); emitline(", X0\n"); } else { emitline("\t"); emitline(mov); emitline("\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), X0\n"); }; return; }; let lop: str = fieldloadop(c, leaffi); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); } else { emitline("\t"); emitline(lop); emitline("\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), 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 == nkind.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 // 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; }; // slice field: load (ptr, len, cap) // into (AX, BX, CX). AX is the *struct // base, so load .ptr (which targets // AX) LAST. if (isslicetype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; // f64/f32 chained field: route through X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", X0\n"); return; }; let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; }; }; }; // Chained `(ident).f1.f2` read where f1 is a struct-by-value // field. Mirror of the cgassign branch added for the same shape. // Without this, `L.cur.kind` (cur a by-value struct of *L) // falls into the SB-fallback and emits `MOVQ kind(SB), AX`. // Kept as a fallback below the generalized walker above (placed // earlier in cgdot) to preserve byte-identical output on shapes // it already handles. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let inner: *node = lhs.lhs; let innerfld: str = lhs.str; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let lkind: nkind = tn.kind; let outname: str; outname.ptr = nil; outname.len = 0; let isptr: bool = false; if (lkind == nkind.N_TNAME) { outname = tn.str; }; if (lkind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { outname = pe.str; isptr = true; };}; }; if (outname.len > 0) { let osi: *structinfo = structlookup(c, outname); if (osi != nil) { let ofi: *fieldinfo = osi.fields; for (ofi != nil) { if (streq(ofi.fname, innerfld)) { let oft: *node = ofi.tnode; if (oft != nil) { if (oft.kind == nkind.N_TNAME) { if (primsize(oft.str) == 0) { let isi: *structinfo = structlookup(c, oft.str); if (isi != nil) { let ffi: *fieldinfo = isi.fields; for (ffi != nil) { if (streq(ffi.fname, fld)) { let totoff: i32 = ofi.foff + ffi.foff; if (isstrtype(c, ffi.tnode)) { if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); emitline("\tMOVQ\t"); emitdispreg((totoff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg(totoff: i64, "CX"); emitline(", AX\n"); } else { emitline("\tMOVQ\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + totoff + 8): i64); emitline("(BP), BX\n"); }; return; }; if (isfloattype(c, ffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", X0\n"); } else { emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), X0\n"); }; return; }; let lop: str = fieldloadop(c, ffi); if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", AX\n"); } else { emitline("\t"); emitline(lop); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); }; return; }; ffi = ffi.finext; }; }; }; };}; }; ofi = ofi.finext; }; }; }; };}; };}; }; }; // Nested module-qualified field where the chain didn't fold to a // known shape (raw w6c on a single file with `use mod;` but no // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of // cmd/w6c/cgen.c N_DOT nested fallback. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); return; }; }; 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. let fk: i32 = exprfloatkind(c, n.lhs); if (n.op == tkind.TK_MINUS && fk != 0) { // Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract. // Zero bit pattern equals 0.0 for both f32 and f64 so we // reuse the integer-zero materialisation. let mov: str = "MOVSD"; let sub: str = "SUBSD"; if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; }; cgexpr(c, n.lhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(sub); emitline("\tX1, X0\n"); return; }; // Address-of has its own evaluation strategy — we want the address // of the operand, not its value. Special-case here so `&arr[i]` // doesn't compile the value load and then discard it. if (n.op == tkind.TK_AMP) { let opnd: *node = n.lhs; if (opnd != nil) { if (opnd.kind == nkind.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; }; if (isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); return; }; return; }; // Address-of through a DOT chain. Mirror of cstage // cgen.c TK_AMP N_DOT branch. Three shapes converge // here, all returning an 8B address (no fldloadop — // just LEAQ / MOVQ+LEAQ). // // 1. Value-struct fields, any depth (`&o.f`, // `&o.i.a`, `&o.a.b.c`) and slice/str pseudo-field // tail (`&s.len`, `&b.buf.len`): the chained // (depth ≥ 2) case reuses dotchainresolve; the // single-DOT case is handled below by inspecting // the IDENT base's tnode. Byte-identical to the // cstage spine walker for both depths. // 2. Pointer-field (`&p.f` where p:*T): single-DOT // only; spine walker aborts on the *T base. Load // p into AX, then LEAQ field_off(AX), AX. Mirror // of the read at cgdot 1144. if (opnd.kind == nkind.N_DOT) { // Shape 1 chained: depth-≥2 via dotchainresolve. // `opnd.lhs.kind == N_DOT` gates the helper at // nsteps ≥ 2 (matches the read path's gate). if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_DOT) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaffi: *fieldinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, opnd, &rootname, &rootoff, &totaloff, &leaffi, &slicedelta, &isglobal, &ptrroot); // `&` through a `*T`-rooted chain is a // separate shape (would need MOVQ + LEAQ // disp(CX), AX). Not exercised by current // callers — skip and fall through. if (ptrroot) { pok = false; }; if (pok) { let extra: i32 = 0; if (slicedelta >= 0) { extra = slicedelta; }; if (isglobal) { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg((totaloff + extra): i64, "CX"); emitline(", AX\n"); } else { emitline("\tLEAQ\t"); emitoff((rootoff + totaloff + extra): i64); emitline("(BP), AX\n"); }; return; }; }; }; // Shape 1/2 single-DOT on an IDENT base. Inspect // the base's tnode to pick value-struct vs slice/ // str pseudo vs pointer-field. if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_IDENT) { let basenm: str = opnd.lhs.str; let fld: str = opnd.str; let lc: *local = localfindnode(c, basenm); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-field: &p.f where p:*T. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.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) { if (streq(fi.fname, fld)) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), AX\n"); emitline("\tLEAQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; }; }; // Value-struct local: &o.f. if (lkind == nkind.N_TNAME) { let sname: str = tn.str; let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); return; }; fi = fi.finext; }; }; }; // Slice/str pseudo-field on a local: // &s.ptr / &s.len / &s.cap. Delta is // 0/8/16 — matches the spine walker. 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) { let isslor: bool = false; if (lkind == nkind.N_TSLICE) { isslor = true; }; if (lkind == nkind.N_TNAME) { if (streq(tn.str, "str")) { isslor = true; }; }; if (isslor) { emitline("\tLEAQ\t"); emitoff((lc.off + delta): i64); emitline("(BP), AX\n"); return; }; }; }; // Global root: top-level let, either a // struct or a slice/str. if (isletvar(c, basenm)) { let gsi: *structinfo = letvarstructinfo(c, basenm); if (gsi != nil) { let fi: *fieldinfo = gsi.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitsymname(c, basenm); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; let isstr: bool = letvarisstr(c, basenm); let issl: bool = letvarisslice(c, basenm); if (isstr || issl) { let gdelta: i32 = -1; if (streq(fld, "ptr")) { gdelta = 0; }; if (streq(fld, "len")) { gdelta = 8; }; if (issl) { if (streq(fld, "cap")) { gdelta = 16; }; }; if (gdelta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, basenm); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg(gdelta: i64, "CX"); emitline(", AX\n"); return; }; }; }; }; }; // Fall through silently (mirrors cstage silent- // drop fallback at the end of the TK_AMP block). return; }; if (opnd.kind == nkind.N_INDEX) { // &base[i] = base + i*esz, no dereference. let base: *node = opnd.lhs; let idx: *node = opnd.rhs; let esz: i32 = 8; let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; let baselocal: *local = nil; let isarr: bool = false; if (base != nil) { if (base.kind == nkind.N_IDENT) { baselocal = localfindnode(c, base.str); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); let tn: *node = baselocal.tnode; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; } else { let tn: *node = letvartnode(c, base.str); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = base.str; esz = elemsizeofc(c, tn); }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = base.str; esz = elemsizeofc(c, tn); }; }; }; }; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { if (isarr) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { // Complex base: spill scaled idx, eval // base to AX, move to BX, restore idx. emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); };};}; emitline("\tADDQ\tBX, AX\n"); return; }; }; return; }; cgexpr(c, n.lhs); if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); // NOTQ inverts the whole 64-bit register; clamp narrow // unsigned results to type width so subsequent 64-bit // compares against typed literals agree. u32 uses MOVL r,r // (zero-extends upper 32) because ANDQ $0xFFFFFFFF would // sign-extend imm32 to all-ones and act as a no-op. if (nodeisunsigned(c, n.lhs)) { let w: i32 = nodeprimwidth(c, n.lhs); if (w == 1) { emitline("\tANDQ\t$255, AX\n"); }; if (w == 2) { emitline("\tANDQ\t$65535, AX\n"); }; if (w == 4) { emitline("\tMOVL\tAX, AX\n"); }; }; return; }; if (n.op == tkind.TK_STAR) { emitline("\tMOVQ\t(AX), AX\n"); return; }; if (n.op == tkind.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 = { // Short-circuit `&&` / `||`. Operands are bool (0/1); the type // checker enforces it. Eval LHS into AX, branch over RHS on the // short-circuit polarity, otherwise eval RHS into AX. The // surviving AX is the result. Must precede any eager-eval path // below — `if (p != nil && p.x > 0)` would segfault on a nil // deref otherwise. Byte-identical to cmd/w6c/cgen.c N_BIN. if (n.op == tkind.TK_AND || n.op == tkind.TK_OR) { let prefix: str = "andend"; let jshrt: str = "JE"; if (n.op == tkind.TK_OR) { prefix = "orend"; jshrt = "JNE"; }; let end: str = mklabel(c, prefix); cgexpr(c, n.lhs); emitline("\tCMPQ\t$0, AX\n"); emitline("\t"); emitline(jshrt); emitline("\t"); emitline(end); emitline("\n"); cgexpr(c, n.rhs); emitlabel(end); return; }; let unsignd: bool = nodeisunsigned(c, n.lhs); if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; // Float arithmetic: both operands flow through X0. Spill rhs // across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no // general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS // variants for f32. Comparison uses UCOMISD + JCC and falls // out to the existing CMPQ-based path below. let lfk: i32 = exprfloatkind(c, n.lhs); let rfk: i32 = exprfloatkind(c, n.rhs); let fk: i32 = lfk; if (fk == 0) { fk = rfk; }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; if (n.op == tkind.TK_PLUS || n.op == tkind.TK_MINUS || n.op == tkind.TK_STAR || n.op == tkind.TK_SLASH) { cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, n.lhs); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); let op: str = "ADDSD"; if (n.op == tkind.TK_MINUS) { op = "SUBSD"; }; if (n.op == tkind.TK_STAR) { op = "MULSD"; }; if (n.op == tkind.TK_SLASH) { op = "DIVSD"; }; if (fk == 1) { if (n.op == tkind.TK_PLUS) { op = "ADDSS"; }; if (n.op == tkind.TK_MINUS) { op = "SUBSS"; }; if (n.op == tkind.TK_STAR) { op = "MULSS"; }; if (n.op == tkind.TK_SLASH) { op = "DIVSS"; }; }; emitline("\t"); emitline(op); emitline("\tX1, X0\n"); return; }; let isfcmp: bool = false; let jcc: str = ""; // UCOMISD/SS sets ZF/PF/CF; unordered (NaN) propagates as // "not equal / not less". JA/JAE/JB/JBE keys off CF which // matches the ordered comparisons we need. if (n.op == tkind.TK_EQ) { isfcmp = true; jcc = "JE"; }; if (n.op == tkind.TK_NEQ) { isfcmp = true; jcc = "JNE"; }; if (n.op == tkind.TK_LT) { isfcmp = true; jcc = "JB"; }; if (n.op == tkind.TK_LE) { isfcmp = true; jcc = "JBE"; }; if (n.op == tkind.TK_GT) { isfcmp = true; jcc = "JA"; }; if (n.op == tkind.TK_GE) { isfcmp = true; jcc = "JAE"; }; if (isfcmp) { cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, n.lhs); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); let ucomi: str = "UCOMISD"; if (fk == 1) { ucomi = "UCOMISS"; }; emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n"); let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); 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; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, n.lhs); emitline("\tPOPQ\tBX\n"); if (n.op == tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_SLASH) { emitline("\tMOVQ\t$0, DX\n"); if (unsignd) { emitline("\tDIVQ\tBX\n"); } else { emitline("\tIDIVQ\tBX\n"); }; return; }; if (n.op == tkind.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 == tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_LSHIFT) { emitline("\tMOVQ\tBX, CX\n"); emitline("\tSHLQ\tCX, AX\n"); return; }; if (n.op == tkind.TK_RSHIFT) { emitline("\tMOVQ\tBX, CX\n"); emitline("\tSHRQ\tCX, AX\n"); return; }; // TK_AND / TK_OR handled with short-circuit codegen at the top of // cgbin — they never reach this eager-eval tail. // 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 == tkind.TK_EQ) { iscmp = true; jcc = "JE"; }; if (n.op == tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; }; if (n.op == tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; if (n.op == tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; if (n.op == tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; if (n.op == tkind.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; }; // cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value) // bytes via rt_alloc, then write the value's bytes into the new // region. For an N_STRUCTLIT arg, allocate the struct's totsize and // emit per-field stores at each field's offset. For a scalar/ptr, // allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's // alloc-special branch in N_CALL. Returns the heap ptr in AX. fn cgalloc(c: *cgen, n: *node) void = { let v: *node = n.list; let sz: i32 = 8; let si: *structinfo = nil; if (v.kind == nkind.N_STRUCTLIT) { let trefn: *node = v.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (trefn != nil) { if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; }; si = structlookup(c, sname); if (si != nil) { sz = si.totsize; }; }; emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\trt_alloc(SB)\n"); emitline("\tPUSHQ\tAX\n"); if (v.kind == nkind.N_STRUCTLIT) { if (si != nil) { let f: *node = v.list; for (f != nil) { if (f.kind == nkind.N_FIELD) { let fname: str = f.str; let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, f.lhs); // alloc(T{ fval = v }) for f64/f32 field: cgexpr left // the value in X0, not AX — route the store via MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tMOVQ\t(SP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitint(fi.foff: i64); emitline("(BX)\n"); fi = nil; } else { emitline("\tMOVQ\t(SP), BX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitint(fi.foff: i64); emitline("(BX)\n"); fi = nil; }; } else { fi = fi.finext; }; }; }; f = f.next; }; }; } else { cgexpr(c, v); emitline("\tMOVQ\t(SP), BX\n"); let sop: str = "MOVQ"; if (sz == 1) { sop = "MOVB"; } else { if (sz == 4) { sop = "MOVL"; }; }; emitline("\t"); emitline(sop); emitline("\tAX, (BX)\n"); }; emitline("\tPOPQ\tAX\n"); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: // ; cgexpr → AX // ; PUSHQ AX // ; ADDQ $1, s.len(BP) // ; LEAQ s(BP), DI ; arg1 = &s // ; MOVQ esz, SI ; arg2 = membsz // ; CALL rt_ensure(SB) // ; MOVQ s.len(BP), CX ; CX = new len // ; SUBQ $1, CX ; CX = slot index // ; [IMULQ esz, CX] ; byte offset (esz>1) // ; MOVQ s.ptr(BP), BX // ; ADDQ CX, BX // ; POPQ AX // ; MOV* AX, (BX) ; store (MOVB / MOVQ) // nkind.N_SPREAD wraps the same body in a counted loop over items.len. fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; if (sn.kind != nkind.N_IDENT) { return; }; let snlocal: *local = localfindnode(c, sn.str); if (snlocal == nil) { return; }; let sn_off: i32 = snlocal.off; let esz: i32 = elemsizeof(snlocal.tnode); let etnode: *node = nil; if (snlocal.tnode != nil) { let stk: nkind = snlocal.tnode.kind; if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; }; let store_op: str = tnodestoreop(c, etnode, esz); let vn: *node = sn.next; for (vn != nil) { if (vn.kind == nkind.N_SPREAD) { let it: *node = vn.lhs; if (it == nil) { vn = vn.next; continue; }; if (it.kind != nkind.N_IDENT) { vn = vn.next; continue; }; let itlocal: *local = localfindnode(c, it.str); if (itlocal == nil) { vn = vn.next; continue; }; let it_off: i32 = itlocal.off; let load_op: str = tnodeloadop(c, etnode, esz); emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let ll: str = mklabel(c, "spr_l"); let le: str = mklabel(c, "spr_e"); emitlabel(ll); emitline("\tMOVQ\t(SP), CX\n"); emitline("\tMOVQ\t"); emitoff((it_off + 8): i64); emitline("(BP), DX\n"); emitline("\tCMPQ\tDX, CX\n"); emitline("\tJGE\t"); emitline(le); emitline("\n"); emitline("\tMOVQ\t"); emitoff(it_off: i64); emitline("(BP), BX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tADDQ\tCX, BX\n"); emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tADDQ\t$1, "); emitoff((sn_off + 8): i64); emitline("(BP)\n"); emitline("\tLEAQ\t"); emitoff(sn_off: i64); emitline("(BP), DI\n"); emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", SI\n"); emitline("\tCALL\trt_ensure(SB)\n"); emitline("\tMOVQ\t"); emitoff((sn_off + 8): i64); emitline("(BP), CX\n"); emitline("\tSUBQ\t$1, CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t"); emitoff(sn_off: i64); emitline("(BP), BX\n"); emitline("\tADDQ\tCX, BX\n"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); emitline("\tADDQ\t$1, (SP)\n"); emitline("\tJMP\t"); emitline(ll); emitline("\n"); emitlabel(le); emitline("\tADDQ\t$8, SP\n"); vn = vn.next; continue; }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\n"); emitline("\tADDQ\t$1, "); emitoff((sn_off + 8): i64); emitline("(BP)\n"); emitline("\tLEAQ\t"); emitoff(sn_off: i64); emitline("(BP), DI\n"); emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", SI\n"); emitline("\tCALL\trt_ensure(SB)\n"); emitline("\tMOVQ\t"); emitoff((sn_off + 8): i64); emitline("(BP), CX\n"); emitline("\tSUBQ\t$1, CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t"); emitoff(sn_off: i64); emitline("(BP), BX\n"); emitline("\tADDQ\tCX, BX\n"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; }; return; }; fn cgcall(c: *cgen, n: *node) void = { // Hare-style `append(s, v)` / `append(s, items...)` builtin — // special-cased before pushargsrev so the spread variant can run // a counted loop over the items slice instead of a normal call. let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { if (streq(callee.str, "append")) { if (n.list != nil) { if (n.list.next != nil) { cgappend(c, n); return; }; }; }; // `alloc(value)` builtin: heap-init a fresh *T with the // value's bytes. For struct literals, lower to rt_alloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // alloc path. if (streq(callee.str, "alloc")) { if (n.list != nil) { cgalloc(c, n); return; }; }; }; }; // Look up the callee's declared params for tagged-union widening. // fn-pointer calls (callee is a local) don't get widening — the // user must build the tagged value explicitly. Matches the most // common case (direct named calls). let calleeparams: *node = nil; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { calleeparams = fnparamslookup(c, callee.str); }; }; // Hare-style variadic last param: gather N tail args into a // frame-resident [N]T (`@vararg_d_`) plus a 24B slice // descriptor (`@vararg_sl_`), then splice a synthesised // N_IDENT pointing at the descriptor into n.list so the rest // of the call machinery sees one slice slot for the variadic. // Forwarding shape (`xs...`) skips the gather: the spread's // inner slice expression replaces the wrapper in place. Empty // (no trailing args) writes a {nil, 0, 0} descriptor. The seq // matches the one scanlocals stamped on n.uval. { let nfixed_v: i32 = 0; let varp: *node = callee_variadic_param(c, callee, &nfixed_v); if (varp != nil) { let nargs0: i32 = 0; let aw: *node = n.list; for (aw != nil) { nargs0 += 1; aw = aw.next; }; let nvar: i32 = nargs0 - nfixed_v; if (nvar < 0) { nvar = 0; }; let forwarding: bool = false; if (nvar == 1) { let aaf: *node = n.list; let kk: i32 = 0; for (kk < nfixed_v) { aaf = aaf.next; kk += 1; }; if (aaf != nil) { if (aaf.kind == nkind.N_SPREAD) { forwarding = true; }; }; }; if (forwarding) { let prev: *node = nil; let cur2: *node = n.list; let kk2: i32 = 0; for (kk2 < nfixed_v) { prev = cur2; cur2 = cur2.next; kk2 += 1; }; let inner: *node = cur2.lhs; if (inner != nil) { inner.next = nil; }; if (prev == nil) { n.list = inner; } else { prev.next = inner; }; } else { let seq: i32 = n.uval: i32; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); let esz: i32 = slotsize(c, varp.lhs); if (esz < 1) { esz = 1; }; let velemtagged: bool = istaggedtype(c, varp.lhs); let velemstr: bool = isstrtype(c, varp.lhs); let velemslice: bool = isslicetype(c, varp.lhs); let doff: i32 = 0; if (nvar > 0) { doff = localadd(c, dname, nvar * esz, nil); }; let soff: i32 = localadd(c, sname, 24, slicewrap(c, varp.lhs)); let aa2: *node = n.list; let kk3: i32 = 0; for (kk3 < nfixed_v) { aa2 = aa2.next; kk3 += 1; }; let j: i32 = 0; let prevarg: *node = n.list; if (nfixed_v == 0) { prevarg = nil; } else { let kk4: i32 = 0; for (kk4 < nfixed_v - 1) { prevarg = prevarg.next; kk4 += 1; }; }; for (aa2 != nil) { let slot: i32 = doff + j * esz; if (velemtagged) { cgwidentaggedstore(c, varp.lhs, aa2, "BP", slot, esz); } else { if (velemstr) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot + 8): i64); emitline("(BP)\n"); } else { if (velemslice) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((slot + 16): i64); emitline("(BP)\n"); } else { cgexpr(c, aa2); let op: str = tnodestoreop(c, varp.lhs, esz); emitline("\t"); emitline(op); emitline("\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); }; }; }; j += 1; aa2 = aa2.next; }; if (nvar > 0) { emitline("\tLEAQ\t"); emitoff(doff: i64); emitline("(BP), AX\n"); } else { emitline("\tXORQ\tAX, AX\n"); }; emitline("\tMOVQ\tAX, "); emitoff(soff: i64); emitline("(BP)\n"); emitline("\tMOVQ\t$"); emitint(nvar: i64); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitoff((soff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((soff + 16): i64); emitline("(BP)\n"); let sn: *node = newnode(c.a, nkind.N_IDENT, "", 0, 0); sn.str = sname; if (prevarg == nil) { n.list = sn; } else { prevarg.next = sn; }; }; }; }; let nargs: i32 = pushargsrev(c, n.list, calleeparams); // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else // pops into the int stream (DI..R9) per the SysV ABI. Walk the // args list alongside the pop counter so we know each arg's // register class. SysV has only 6 int arg regs (DI/SI/DX/CX/R8/R9); // the remaining slots stay on the stack and the callee reads them // via 16+8*k(BP). Caller-cleanup is emitted after the CALL. let intidx: i32 = 0; let fpidx: i32 = 0; let a: *node = n.list; let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { let fk: i32 = exprfloatkind(c, a); if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; if (fpidx < 8) { emitline("\t"); emitline(mov); emitline("\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; } else { stackslots += 1; }; popped += 1; } else { let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; let words: i32 = 1 + extra; let w: i32 = 0; for (w < words) { if (intidx < 6) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; } else { stackslots += 1; }; popped += 1; w += 1; }; }; a = a.next; }; // Drain any remaining slots that the arg-walker didn't account // for (tagged-union arg sizes > 8B, struct-by-value, etc.). The // existing C cgen pops these into the int stream, so the worst // case here is identical pre-port behaviour. let i: i32 = popped; for (i < nargs) { if (intidx < 6) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; } else { stackslots += 1; }; 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 nkind.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 == nkind.N_IDENT) { let cn: str = callee.str; if (localfindnode(c, cn) != nil) { isfnptrcall = true; }; }; if (callee.kind == nkind.N_DOT) { let base: *node = callee.lhs; let fld: str = callee.str; if (base != nil) { if (base.kind == nkind.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: nkind = tn.kind; let sname: str; sname.ptr = nil; sname.len = 0; if (lkind == nkind.N_TNAME) { sname = tn.str; }; if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; if (inner != nil) { if (inner.kind == nkind.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 == nkind.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 == nkind.N_IDENT) { // Bare `f()` — same-module by ww's resolver, // so c.curmod is the disambiguation hint. calleename = callee.str; emitfnname(c, calleename, c.curmod); } else { if (callee.kind == nkind.N_DOT) { // `m.f()` — pass the explicit module bareword // so cross-module same-leaf exports resolve. calleename = callee.str; let hint: str; hint.ptr = nil; hint.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { hint = callee.lhs.str; }; }; emitfnname(c, calleename, hint); };}; }; emitline("(SB)\n"); }; // Caller cleanup for stack-passed args (args 7+, or any // overflow past the int/float reg windows). Mirrors C cgen: // pushed 8 bytes each, ADDQ them off after the CALL. if (stackslots > 0) { emitline("\tADDQ\t$"); emitint((stackslots * 8): i64); emitline(", SP\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 nkind.N_IDENT with empty str // (planted by parseprimary on the tkind.TK_UNDER token). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (lhs.str.len == 0) { if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); return; }; }; }; }; // Tagged-union local reassignment: `r = expr;` where r has a // tagged-union type. Delegate to cgwidentaggedstore (same path // as cglet's tagged-init). Covers nullable fold, tagged source, // struct payload, str payload, scalar payload, with tag remap. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (n.op == tkind.TK_ASSIGN) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode, n.rhs, "BP", lc.off, lsz); 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 == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { if (n.op == tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; let elemf32: bool = false; let storeop: str = "MOVQ"; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { if (streq(pe.str, "str")) { elemstr = true; } else { if (streq(pe.str, "f64")) { elemfloat = true; } else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; }; }; }; }; }; }; }; cgexpr(c, n.rhs); // `*p = v` for *f64 / *f32: value sits in X0. Spill // to the stack, evaluate the pointer (clobbers AX), // then reload X0 and MOVSD/MOVSS through the pointer. if (elemfloat) { let mov: str = "MOVSD"; if (elemf32) { mov = "MOVSS"; }; emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (BX)\n"); return; }; // 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; }; }; }; }; // `*p OP= v` — compound assign through a pointer deref. The // plain-assign branch above only fires for TK_ASSIGN; without // this, compound ops fall through and emit nothing (silent // no-op — exactly the trap that broke fmt.println). Mirror of // cmd/w6c/cgen.c's N_UN/TK_STAR compound branch. if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { if (n.op != tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let loadop: str = "MOVQ"; let storeop: str = "MOVQ"; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { let ps: i32 = fieldsize(c, pe); if (ps == 1 || ps == 2 || ps == 4) { loadop = tnodeloadop(c, pe, ps); storeop = tnodestoreop(c, pe, ps); }; }; }; }; }; }; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(loadop); emitline("\t(BX), AX\n"); emitline("\tPOPQ\tCX\n"); let combineop: str = "MOVQ"; if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; } else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; } else { if (n.op == tkind.TK_STAREQ) { combineop = "IMULQ"; } else { if (n.op == tkind.TK_AMPEQ) { combineop = "ANDQ"; } else { if (n.op == tkind.TK_PIPEEQ) { combineop = "ORQ"; } else { if (n.op == tkind.TK_CARETEQ) { combineop = "XORQ"; } else { if (n.op == tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; } else { if (n.op == tkind.TK_RSHIFTEQ) { combineop = "SHRQ"; }; }; }; }; }; }; }; }; emitline("\t"); emitline(combineop); emitline("\tCX, AX\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 == nkind.N_INDEX) { if (n.op == tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; let esz: i32 = 8; let baselocal: *local = nil; let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); let btn: *node = baselocal.tnode; if (btn != nil) { let bk: nkind = btn.kind; if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); };}; }; // Tagged-union element: materialize source in a shared // scratch slot via cgwidentaggedstore (handles struct / // str / scalar / subset / nullable variants uniformly), // then compute &arr[i] and byte-copy. The scratch // (@tagscr) is reused across all tagged-arr stores in // the function and counted once in scanlocals. if (elemtn != nil) { if (istaggedtype(c, elemtn)) { let slot_sz: i32 = slotsize(c, elemtn); let scroff: i32 = localadd(c, "@tagscr", 24, nil); // Pre-zero scratch (matches push helper). emitline("\tXORQ\tAX, AX\n"); let zz: i32 = 0; for (zz < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((scroff + zz): i64); emitline("(BP)\n"); zz += 8; }; cgwidentaggedstore(c, elemtn, n.rhs, "BP", scroff, slot_sz); cgexpr(c, idx); if (slot_sz > 1) { emitline("\tMOVQ\t$"); emitint(slot_sz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarr: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; if (isarr) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); };};}; emitline("\tADDQ\tAX, BX\n"); let cc: i32 = 0; for (cc < slot_sz) { emitline("\tMOVQ\t"); emitoff((scroff + cc): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(cc: i64); emitline("(BX)\n"); cc += 8; }; return; }; }; 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 (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.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; }; let isop: str = tnodestoreop(c, elemtn, esz); emitline("\t"); emitline(isop); emitline("\tAX, (BX)\n"); return; }; }; }; // `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric // write-side of the cgdot N_INDEX-lhs branch added for task #8. // Compute &arr[i] inline (LEAQ for `[N]Struct`, MOVQ for // `[N]*Struct` / `[]Struct` / `*Struct`), deref once when the // element is `*Struct`, then store rhs at field.offset(addr). // Without this both shapes silently drop the store — there is no // existing wwstage branch for N_DOT(N_INDEX,...) lhs at all (the // N_INDEX-lhs branch above handles bare `arr[i] = v`, not the // field write). if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil && lhs.lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs.lhs; let idx: *node = lhs.lhs.rhs; let fld2: str = lhs.str; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { if (idx != nil) { let lc: *local = localfindnode(c, idxbase.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; let sname: str; sname.ptr = nil; sname.len = 0; let viaptr: bool = false; if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { let inner: *node = elemt.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { sname = elemt.str; };}; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld2)) { let esz: i32 = elemsizeofc(c, tn); // f64/f32: rhs in X0. Spill to stack, // compute &arr[i] in BX (deref if *T), // then reload X0 and MOVSD/MOVSS. if (n.op == tkind.TK_ASSIGN) { if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // str rhs: AX=ptr, BX=len. Stash both, // compute addr in CX so the pop pair // restores AX/BX intact. if (isstrtype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); }; emitline("\tADDQ\tAX, CX\n"); if (viaptr) { emitline("\tMOVQ\t(CX), 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; }; // scalar plain `=` cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\tPOPQ\tAX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // compound: rhs→push; compute struct // addr→BX (deref if *T); push addr; // load old field→AX; pop addr→BX, // rhs→CX; combine; store. Float/str // compound not wired. cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\tPUSHQ\tBX\n"); let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); }; let sop2: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop2); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; };}; }; };}; }; }; // 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). Base accepts the // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) // by retargeting to the inner IDENT so the via_ptr branch fires // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_UN) { if (base.op == tkind.TK_STAR) { if (base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { base = base.lhs; }; }; }; }; if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-to-struct: deref then store. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.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)) { // Tagged-union field via *struct base — full slot // rewrite via cgwidentaggedstore basereg="BX". Pre-#26 // fell through to the scalar store and dropped tag // + payload. if (n.op == tkind.TK_ASSIGN && istaggedtype(c, fi.tnode)) { let fsz: i32 = slotsize(c, fi.tnode); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); cgwidentaggedstore(c, fi.tnode, n.rhs, "BX", fi.foff, fsz); return; }; // struct-typed field via *struct base — three // rhs shapes (call/structlit added with #5; // closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX per #4's cgreturn // ABI; load *struct ptr into BX after the // call, sized stores per natural struct size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. // si.totsize is slot-padded; use // structnaturalsize for the type-size query. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((fi.foff + i * 8): i64, "BX"); emitline("\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((fi.foff + full * 8): i64, "BX"); emitline("\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX // from lc.off(BP) before zero-fill and before every // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", fi.foff, ssz); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); }; return; };}; }; if (n.op != tkind.TK_ASSIGN) { // compound: load current value emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let lop: str = fieldloadop(c, 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 != tkind.TK_ASSIGN) { emitline("\tPOPQ\tBX\n"); // PLUSEQ is commutative; MINUSEQ // needs lhs - rhs (BX is old lhs, // AX is rhs). if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.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 == tkind.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; }; // slice field via *struct: rhs left // (AX=ptr, BX=len, CX=cap). CX is // taken, so stage the struct addr // in DX. Store all three words at // foff/+8/+16. if (isslicetype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((fi.foff + 16): i64, "DX"); emitline("\n"); return; }; // f64/f32 plain `=` via *struct: cgexpr left the // value in X0. Reload struct ptr and MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let sop: str = fieldstoreop(c, 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 == nkind.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)) { // Tagged-union field in a direct struct local — // full slot rewrite at (lc.off + fi.foff)(BP) // via cgwidentaggedstore basereg="BP". Pre-#26 // fell through and dropped tag + payload. if (n.op == tkind.TK_ASSIGN && istaggedtype(c, fi.tnode)) { let fsz: i32 = slotsize(c, fi.tnode); cgwidentaggedstore(c, fi.tnode, n.rhs, "BP", lc.off + fi.foff, fsz); return; }; // struct-typed field on a direct struct // local — three rhs shapes (call/structlit // added with #5; closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX; sized stores // directly at (lc.off+fi.foff)(BP). // N_STRUCTLIT: field-walk; each inner // field stored at +fi.foff+inner_foff(BP). // BP-rel direct, no addr scratch needed. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((lc.off + fi.foff + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((lc.off + fi.foff + full * 8): i64); emitline("(BP)\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=0 (DST_BP) — direct BP-rel, // no BX reload. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 0, 0, "", lc.off + fi.foff, ssz); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); }; return; };}; }; cgexpr(c, n.rhs); // str field: cgexpr left (AX=ptr, BX=len); // store both halves at +0/+8. Without this, // `L.src = s` would only write the ptr and // `L.src.len` would carry whatever was on the // stack. if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\tAX, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP)\n"); return; }; // slice field direct: cgexpr left // (AX=ptr, BX=len, CX=cap); store all // three at +0/+8/+16. The generic // fldstoreop below would only write AX, // dropping .len/.cap. if (isslicetype(c, fi.tnode)) { emitline("\tMOVQ\tAX, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((lc.off + fi.foff + 16): i64); emitline("(BP)\n"); return; }; // f64/f32 direct struct local store: route via X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); return; }; let sop: str = fieldstoreop(c, 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 == nkind.N_TPTR) { let inner: *node = tn.lhs; let innerkind: nkind = nkind.N_NONE; if (inner != nil) { innerkind = inner.kind; }; let innerstr: bool = false; if (innerkind == nkind.N_TNAME) { if (streq(inner.str, "str")) { innerstr = true; }; }; if (innerkind == nkind.N_TSLICE) { innerstr = true; }; if (innerstr) { if (n.op != tkind.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 == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.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; }; }; }; }; }; }; // Top-level struct global field assignment: `g.f = expr;` and // `g.f += expr;` for a scalar/str field. Reached when the local // lookup miss but the IDENT base is a registered struct `let`. // LEAQ name(SB) into BX/CX takes the place of the frame slot // addressing the local branches use. Compound (PLUSEQ/MINUSEQ) // follows the same load → push → eval → combine → store shape // as the via-ptr local path. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; if (localfindnode(c, bn) == nil) { let si: *structinfo = letvarstructinfo(c, bn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { // struct-typed field on a global struct base — // three rhs shapes (call/structlit added with // #5; closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX // after call, sized stores per natural size. // N_STRUCTLIT: field-walk; reload BX per store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((fi.foff + i * 8): i64, "BX"); emitline("\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((fi.foff + full * 8): i64, "BX"); emitline("\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=2 (DST_GLOBAL) reloads BX // via LEAQ bn(SB) before zero-fill and before every // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structnaturalsize(ssi); cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, fi.foff, ssz); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); }; return; };}; }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode)) { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), CX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "CX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "CX"); emitline("\n"); return; }; // f64/f32 plain `=` on global struct field: value is // in X0; LEAQ the base into BX and MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // Compound on scalar field: load // → push → eval rhs → combine → // store. cgexpr clobbers BX, so // re-LEAQ for the store. let lop: str = fieldloadop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; }; }; }; }; // Chained `.field = v` where `` 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 == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.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 == tkind.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; }; // f64/f32 chained plain `=`: cgexpr rhs left value in // X0. Spill to stack so cgexpr(base) can use AX, then // reload and MOVSD/MOVSS into the slot. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); 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(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; }; fi = fi.finext; }; }; }; }; }; }; }; // Chained N_DOT spine write through value-struct fields (any // depth) — `o.i.a = 10`, `v.a.b.c = …`. Also handles a slice/str // pseudo-field leaf (`b.buf.len = 5`). Mirror of cstage cgen.c's // chained-DOT write branch. Without this, depth ≥ 3 writes and // the slice/str pseudo-field write through a value-struct chain // silently emit no store. Only plain `=` is wired. if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil && lhs.lhs.kind == nkind.N_DOT && n.op == tkind.TK_ASSIGN) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaffi: *fieldinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let yok: bool = dotchainresolve(c, lhs, &rootname, &rootoff, &totaloff, &leaffi, &slicedelta, &isglobal, &ptrroot); if (yok) { // `*T` root and global share the CX-based emit: // loader runs AFTER cgexpr(rhs) so AX/BX/X0 stay // intact, then stores at total_off off CX. let viacx: bool = isglobal || ptrroot; if (slicedelta >= 0) { cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg((totaloff + slicedelta): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff + slicedelta): i64); emitline("(BP)\n"); }; return; }; if (isstrtype(c, leaffi.tnode)) { cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg(totaloff: i64, "CX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((totaloff + 8): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((rootoff + totaloff + 8): i64); emitline("(BP)\n"); }; return; }; // TY_STRUCT terminal: three rhs shapes: // - N_IDENT: word-copy from the rhs local slot // (cgexpr is skipped — no whole-struct register // convention for an arbitrary local). // - N_CALL (added with #5): cgexpr leaves the // value in AX/DX/CX per #4's cgreturn ABI; sized // stores write only the declared field size. // cgreturn touches only AX/DX/CX so for // ptrroot/global we load the dst addr into BX // (not CX) after the call to keep CX as the // third value word. // - N_STRUCTLIT (added with #5): field-by-field // store; for ptrroot/global the dst addr is // reloaded into BX before each store so cgexpr // can clobber AX/BX between fields. if (n.rhs != nil && n.rhs.kind == nkind.N_CALL && leaffi.tnode != nil && leaffi.tnode.kind == nkind.N_TNAME && primsize(leaffi.tnode.str) == 0) { let lsi: *structinfo = structlookup(c, leaffi.tnode.str); if (lsi != nil) { // si.totsize is slot-padded (rounded to 8); // receive ABI needs the TYPE's natural size. let lsz: i32 = structnaturalsize(lsi); if (lsz <= 24) { let tlm: i32 = lsz - (lsz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), BX\n"); }; }; let full: i32 = lsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; if (viacx) { emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((totaloff + i * 8): i64, "BX"); emitline("\n"); } else { emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((rootoff + totaloff + i * 8): i64); emitline("(BP)\n"); }; i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; if (viacx) { emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((totaloff + full * 8): i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((rootoff + totaloff + full * 8): i64); emitline("(BP)\n"); }; }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode picks the dst flavor: // ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from // rootoff(BP). // isglobal → mode=2 (DST_GLOBAL), reload BX via // LEAQ rootname(SB). // else → mode=0 (DST_BP), direct BP-rel, no reload. if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && leaffi.tnode != nil && leaffi.tnode.kind == nkind.N_TNAME && primsize(leaffi.tnode.str) == 0) { let lsi: *structinfo = structlookup(c, leaffi.tnode.str); if (lsi != nil) { // si.totsize is slot-padded (rounded to 8); // receive ABI needs the TYPE's natural size. let lsz: i32 = structnaturalsize(lsi); let dmode: i32 = 0; let ddisp: i32 = rootoff + totaloff; if (ptrroot) { dmode = 1; ddisp = totaloff; }; if (isglobal) { dmode = 2; ddisp = totaloff; }; cgstructlitfill(c, lsi, n.rhs, dmode, rootoff, rootname, ddisp, lsz); return; }; }; if (n.rhs != nil && n.rhs.kind == nkind.N_IDENT && leaffi.tnode != nil && leaffi.tnode.kind == nkind.N_TNAME && primsize(leaffi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, leaffi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; }; let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\tMOVQ\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(lop); emitline("\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; }; return; };}; }; if (isfloattype(c, leaffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, leaffi.tnode)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(totaloff: i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); }; return; }; let sop: str = fieldstoreop(c, leaffi); cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(totaloff: i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); }; return; }; }; }; // Chained `(ident).f1.f2 = v` where f1 is a struct-by-value // field. The earlier chained-DOT branch handles f1: *T (deref // then store). This handles f1: T (in-place sub-struct), which // would otherwise silently emit no store — lispcore's lexer had // to flatten `cur.kind`/`cur.ival`/... into top-level fields to // work around it. Only plain `=` is wired; compound on a by- // value sub-field hasn't surfaced. // Kept as fallback below the generalized walker for any shape // the walker doesn't recognize. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_DOT) { let inner: *node = base.lhs; let innerfld: str = base.str; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let lkind: nkind = tn.kind; let outname: str; outname.ptr = nil; outname.len = 0; let isptr: bool = false; if (lkind == nkind.N_TNAME) { outname = tn.str; }; if (lkind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { outname = pe.str; isptr = true; };}; }; if (outname.len > 0) { let osi: *structinfo = structlookup(c, outname); if (osi != nil) { let ofi: *fieldinfo = osi.fields; for (ofi != nil) { if (streq(ofi.fname, innerfld)) { let oft: *node = ofi.tnode; if (oft != nil) { if (oft.kind == nkind.N_TNAME) { if (primsize(oft.str) == 0) { let isi: *structinfo = structlookup(c, oft.str); if (isi != nil) { let ffi: *fieldinfo = isi.fields; for (ffi != nil) { if (streq(ffi.fname, fld)) { if (n.op == tkind.TK_ASSIGN) { let totoff: i32 = ofi.foff + ffi.foff; cgexpr(c, n.rhs); if (isstrtype(c, ffi.tnode)) { if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(totoff: i64, "CX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((totoff + 8): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((lc.off + totoff + 8): i64); emitline("(BP)\n"); }; return; }; if (isfloattype(c, ffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(totoff: i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); }; return; }; let sop: str = fieldstoreop(c, ffi); if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(totoff: i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); }; return; }; }; ffi = ffi.finext; }; }; }; };}; }; ofi = ofi.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 == nkind.N_IDENT) { let nm: str = lhs.str; let off: i32 = localfind(c, nm); if (off == 0) { // Top-level let target: RIP-relative store // for `=`, or load→combine→store for the // compound forms. For a str/slice global, // take its address into CX and store both // halves (plus cap for slice — stashed via // DI since LEAQ overwrites CX); the asm has // no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; // Float global: rhs lands in X0; store via // LEAQ+indirect since MOVSS/MOVSD have no // D_EXTERN operand form. let lvf: *letvar = c.lets; let isfg: bool = false; let isf32g: bool = false; let lvftn: *node = nil; for (lvf != nil) { if (streq(lvf.name, nm)) { isfg = isfloattype(c, lvf.tnode); isf32g = isf32type(c, lvf.tnode); lvftn = lvf.tnode; lvf = nil; } else { lvf = lvf.lvnext; }; }; if (isfg) { cgexpr(c, n.rhs); let mov: str = "MOVSD"; let addf: str = "ADDSD"; let subf: str = "SUBSD"; let mulf: str = "MULSD"; let divf: str = "DIVSD"; if (isf32g) { mov = "MOVSS"; addf = "ADDSS"; subf = "SUBSS"; mulf = "MULSS"; divf = "DIVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); if (n.op == tkind.TK_ASSIGN) { emitline("\t"); emitline(mov); emitline("\tX0, (CX)\n"); return; }; // Compound: X1 = load; X1 OP= X0; store X1. // ADDSD/SUBSD/MULSD/DIVSD are register-register // only, so we can't combine direct to memory. let fop: str; fop.ptr = nil; fop.len = 0; if (n.op == tkind.TK_PLUSEQ) { fop = addf; }; if (n.op == tkind.TK_MINUSEQ) { fop = subf; }; if (n.op == tkind.TK_STAREQ) { fop = mulf; }; if (n.op == tkind.TK_SLASHEQ) { fop = divf; }; if (fop.len == 0) { // Unsupported (e.g., %= on float): // fall back to plain store of rhs. emitline("\t"); emitline(mov); emitline("\tX0, (CX)\n"); return; }; emitline("\t"); emitline(mov); emitline("\t(CX), X1\n"); emitline("\t"); emitline(fop); emitline("\tX0, X1\n"); emitline("\t"); emitline(mov); emitline("\tX1, (CX)\n"); return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { if (letvarisstr(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\tAX, (CX)\n"); emitline("\tMOVQ\tBX, 8(CX)\n"); return; }; if (letvarisslice(c, nm)) { emitline("\tMOVQ\tCX, DI\n"); emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\tAX, (CX)\n"); emitline("\tMOVQ\tBX, 8(CX)\n"); emitline("\tMOVQ\tDI, 16(CX)\n"); return; }; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); return; }; // Compound RMW for a top-level let: load through // LEAQ + localloadop when the slot is narrow so // a prior `*(&letname): *iN` deref-store doesn't // leave stale upper bytes feeding the combine. let glop: str = localloadop(c, lvftn); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); emitline("\t(CX), BX\n"); }; let didcompound: bool = true; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } else { if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tSHLQ\tCX, BX\n"); } else { if (n.op == tkind.TK_RSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tSHRQ\tCX, BX\n"); } else { // Unsupported compound: store rhs // directly. Mirrors the local path's // fallback for TK_SLASHEQ etc. didcompound = false; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); };};};};};};};}; if (didcompound) { emitline("\tMOVQ\tBX, "); emitsymname(c, nm); emitline("(SB)\n"); }; return; }; // Detect str/slice-typed local — assignment must store // both halves (AX=ptr at +0, BX=len at +8) for str, // plus the cap (CX at +16) for slice. let lcstr: bool = false; let lcsl: bool = false; let lcn: *local = localfindnode(c, nm); if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); lcsl = isslicetype(c, lcn.tnode); }; let lcf: bool = false; let lcf32: bool = false; if (lcn != nil) { lcf = isfloattype(c, lcn.tnode); lcf32 = isf32type(c, lcn.tnode); }; // Struct-typed local reassignment: `s = expr;` where s // is a TY_STRUCT local of size <=24B. Two rhs shapes // (mirrors cglet's N_STRUCTLIT and the call-result // receive branch): // - N_STRUCTLIT: walk fields, store at off+foff // directly. ASYMMETRY-safe (no register copy from // the caller; values come from cgexpr). // - N_CALL: cgexpr → AX/DX/CX, sized stores per the // declared struct size — MOVQ for full 8B chunks // plus MOVL/MOVW/MOVB tail. See cglet receive // site for the ASYMMETRY rationale. // Struct-IDENT word-copy rhs (s = p) is left unwired; // #5 is scoped to receive-side of #4 (calls + literals). // fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else // MOVQ} pattern (not fieldstoreop) to match cstage // cgen.c N_ASSIGN byte-identically — wwstage's // fieldstoreop returns MOVW for fsz==2 which cstage // doesn't emit (tracked separately as the cstage/ // wwstage MOVW divergence task). if (lcn != nil) { let lctn: *node = lcn.tnode; let lcsname: str; lcsname.ptr = nil; lcsname.len = 0; if (lctn != nil) { if (lctn.kind == nkind.N_TNAME) { lcsname = lctn.str; }; }; if (lcsname.len > 0) { let lcsi: *structinfo = structlookup(c, lcsname); if (lcsi != nil) { // si.totsize is slot-padded (rounded to 8); // receive ABI needs the TYPE's natural size. let lcnsz: i32 = structnaturalsize(lcsi); if (n.op == tkind.TK_ASSIGN) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { // Delegate to the shared BP-relative // structlit fill helper. Handles // TK_ELLIPSIS autofill + per-field // walk; nested struct-typed values // recurse via the helper (#17 fix). // Helper uses the explicit {1→MOVB, // 4→MOVL, else MOVQ} sized-store // dispatch (NOT fieldstoreop) to stay // byte-identical with cstage pending // #13 (fsz==2 MOVW divergence). See // cgstructlitfillbp docstring. cgstructlitfillbp(c, lcsi, n.rhs, off); return; }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { let lcsz: i32 = lcnsz; if (lcsz <= 24) { let tlm: i32 = lcsz - (lcsz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); let full: i32 = lcsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((off + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((off + full * 8): i64); emitline("(BP)\n"); }; return; }; }; }; }; }; }; }; // Float-typed local: rhs lands in X0; store via MOVSD/ // MOVSS, no AX shuffle. Compound (+= -= *= /=) loads // slot into X1, combines into X1, stores X1 back — // ADDSD/SUBSD/MULSD/DIVSD are register-register only. if (lcf) { cgexpr(c, n.rhs); let mov: str = "MOVSD"; let addf: str = "ADDSD"; let subf: str = "SUBSD"; let mulf: str = "MULSD"; let divf: str = "DIVSD"; if (lcf32) { mov = "MOVSS"; addf = "ADDSS"; subf = "SUBSS"; mulf = "MULSS"; divf = "DIVSS"; }; if (n.op == tkind.TK_ASSIGN) { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); return; }; let fop: str; fop.ptr = nil; fop.len = 0; if (n.op == tkind.TK_PLUSEQ) { fop = addf; }; if (n.op == tkind.TK_MINUSEQ) { fop = subf; }; if (n.op == tkind.TK_STAREQ) { fop = mulf; }; if (n.op == tkind.TK_SLASHEQ) { fop = divf; }; if (fop.len == 0) { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff(off: i64); emitline("(BP), X1\n"); emitline("\t"); emitline(fop); emitline("\tX0, X1\n"); emitline("\t"); emitline(mov); emitline("\tX1, "); emitoff(off: i64); emitline("(BP)\n"); return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); if (lcstr || lcsl) { emitline("\tMOVQ\tBX, "); emitoff((off + 8): i64); emitline("(BP)\n"); }; if (lcsl) { emitline("\tMOVQ\tCX, "); emitoff((off + 16): i64); emitline("(BP)\n"); }; return; }; // Pick the load width for compound RMW. Signed-narrow // locals must sign-extend the slot before the combine // — ADDQ/SUBQ on amem reads 8B raw, which is wrong // after a 4B deref-store leaves the upper bytes stale. let llop: str = "MOVQ"; if (lcn != nil) { llop = localloadop(c, lcn.tnode); }; if (streq(llop, "MOVQ")) { if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); return; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); return; }; }; // Generic compound: load → combine in BX → store. emitline("\t"); emitline(llop); emitline("\t"); emitoff(off: i64); emitline("(BP), BX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tSHLQ\tCX, BX\n"); }; if (n.op == tkind.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: nkind = n.kind; if (k == nkind.N_BLOCK) { cgblock(c, n); return; }; if (k == nkind.N_RETURN) { cgreturn(c, n); return; }; if (k == nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; }; if (k == nkind.N_LET) { cglet(c, n); return; }; if (k == nkind.N_IF) { cgif(c, n); return; }; if (k == nkind.N_FOR) { cgfor(c, n); return; }; if (k == nkind.N_FORRANGE) { cgforrange(c, n); return; }; if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; if (k == nkind.N_MLET) { cgmlet(c, n); return; }; if (k == nkind.N_BREAK) { cgbreak(c, n); return; }; if (k == nkind.N_CONTINUE) { cgcontinue(c, n); return; }; if (k == nkind.N_YIELD) { cgyield(c, n); return; }; if (k == nkind.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 = { // Save/restore the locals head across the block (post-#27). // Inner-scope `let` bindings prepend to c.locals via localadd; // without this restore, the prepended stubs leak into sibling // and ancestor scopes, and localfind (head-first) returns the // inner binding's offset for an identifier that semantically // belongs to the outer scope. The frame is left grown — we // don't reclaim popped slots, matching cstage's lowering. // // cgfn iterates fn_.body.list directly to bypass this save/ // restore at the function's outermost block — defers (and the // implicit-return epilogue) need locals intact. let saved: *local = c.locals; let s: *node = n.list; for (s != nil) { cgstmt(c, s); s = s.next; }; c.locals = saved; 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 == nkind.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, c.fnret)) { // Forwarding a fallible call: `return f();` where f // also returns a tagged union. The result is already // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. // Mirrors the rhsreturnstagged path in cglet and the // !type_istagged guard in C cgen's N_RETURN. let forwardtagged: bool = false; if (rhs.kind == nkind.N_CALL) { let callee: *node = rhs.lhs; if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; }; // Struct payload or tagged-subset return — materialise // the widened value in scratch via cgwidentaggedstore // (handles tag remap and zero pad), then load AX/DX/CX // from the slot. let needswiden: bool = false; if (!isnullabletype(c.fnret)) { if (!forwardtagged) { let sname: str = rhsstructpayload(c, rhs); if (sname.len > 0) { needswiden = true; }; if (rhstaggedident(c, rhs) != nil) { needswiden = true; }; }; }; if (needswiden) { let rsz: i32 = slotsize(c, c.fnret); let scroff: i32 = localadd(c, "@tagscr", 24, nil); emitline("\tXORQ\tAX, AX\n"); let zz: i32 = 0; for (zz < rsz) { emitline("\tMOVQ\tAX, "); emitoff((scroff + zz): i64); emitline("(BP)\n"); zz += 8; }; cgwidentaggedstore(c, c.fnret, rhs, "BP", scroff, rsz); emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); if (rsz > 8) { emitline("\tMOVQ\t"); emitoff((scroff + 8): i64); emitline("(BP), DX\n"); }; if (rsz > 16) { emitline("\tMOVQ\t"); emitoff((scroff + 16): i64); emitline("(BP), CX\n"); }; if (rsz > 24) { emitline("\tMOVQ\t"); emitoff((scroff + 24): i64); emitline("(BP), R8\n"); }; emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; if (forwardtagged) { emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; let idx: i32 = taggedvariantindex(c, c.fnret, rhs); // Tagged-return ABI: AX=tag, DX=word0, CX=word1, // R8=word2. Receiver (cgwidentaggedstore call-source // arm) writes AX/DX/CX/R8 unconditionally sized by the // dst slot; unused ABI words must be zeroed here so a // stale CX/R8 from the caller (e.g. a slice-stride // IMULQ before the call) does not land in slot+16 / // slot+24. (Task #18.) let rsz: i32 = slotsize(c, c.fnret); if (nodeisslice(c, rhs)) { // cgexpr leaves (AX=ptr, BX=len, CX=cap). // Shuffle into return ABI: DX=ptr, CX=len, // R8=cap. emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); } else { if (nodeisstr(c, rhs)) { emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); // str fills DX,CX. Zero R8 if dst covers slot+24. if (rsz > 24) { emitline("\tMOVQ\t$0, R8\n"); }; } else { emitline("\tMOVQ\tAX, DX\n"); // scalar fills DX only. Zero CX / R8 if dst // covers slot+16 / slot+24. if (rsz > 16) { emitline("\tMOVQ\t$0, CX\n"); }; if (rsz > 24) { emitline("\tMOVQ\t$0, R8\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; }; // Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7], // DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c // N_RETURN TY_STRUCT branch. Two rhs shapes are wired: // N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT // (field-by-field store at scratch+foff, with tagged fields // delegated to cgwidentaggedstore). Call-result chain return // is deferred to #5's receive side. Sizes > 24B fall through // to the scalar path below (only AX gets the first qword), // pending sret. let rname: str; rname.ptr = nil; rname.len = 0; if (c.fnret != nil) { if (c.fnret.kind == nkind.N_TNAME) { rname = c.fnret.str; }; }; if (rname.len > 0) { let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { let rsz: i32 = rsi.totsize; if (rsz <= 24) { let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; if (okrhs) { let scroff: i32 = localadd(c, "@retscr", 24, nil); emitline("\tXORQ\tAX, AX\n"); emitline("\tMOVQ\tAX, "); emitoff(scroff: i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + 16): i64); emitline("(BP)\n"); if (rhs.kind == nkind.N_STRUCTLIT) { // Delegate to the shared BP-relative // structlit fill helper. Same store // sequence the inline pre-#17 walk // emitted (tagged + float + scalar), // plus nested struct-typed structlit // values recurse instead of dropping // trailing bytes. cgstructlitfillbp(c, rsi, rhs, scroff); } else { // N_IDENT: word-copy from rhs slot // to scratch. Whole 8B words via // MOVQ; tail via MOVL/MOVB so we // read no further than the source // slot's declared size. let rl: *local = localfindnode(c, rhs.str); if (rl != nil) { let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; for (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; for (k < rsz) { emitline("\tMOVB\t"); emitoff((rl.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; }; }; emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((scroff + 8): i64); emitline("(BP), DX\n"); emitline("\tMOVQ\t"); emitoff((scroff + 16): i64); emitline("(BP), CX\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, 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 x = f()?` has no annotation but the cgen's struct-field // paths need a tnode to dispatch off. Infer from f's tagged // success variant — see inferletcalltype. let tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // Tagged-union init: delegate to cgwidentaggedstore, which // handles nullable fold, tagged source (ident or AX/DX/CX // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. if (istaggedtype(c, tn)) { cgwidentaggedstore(c, tn, rhs, "BP", off, sz); 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 == nkind.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 nkind.N_FIELD with str=="...") fills the // remaining slots up to the declared length with that value. // // str element (16B = ptr+len) needs both halves stored. cgstrlit // / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ // from AX would leave .len as whatever the stack held — silent // miscompile. Worse, primsize("str") returns 0 so esz would fall // back to 8, also collapsing the per-element stride (element i+1 // would overwrite element i's would-be .len half). Detect the // str-element case up front so both esz and the store path are // right. (primsize's default-to-8-on-zero pattern is brittle for // composites generally; same gap blocks slice / struct / tuple / // tagged element arrays — tracked as a follow-up.) if (rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { if (streq(elemn.str, "str")) { esz = 16; isstrel = true; } else { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; }; let mop: str = tnodestoreop(c, elemn, esz); let idx: i32 = 0; let repeat: bool = false; let e: *node = rhs.list; for (e != nil) { let isellip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; isellip = true; }; }; if (isellip) { e = nil; } else { cgexpr(c, e); if (isstrel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(mop); emitline("\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); }; idx += 1; e = e.next; }; }; // AX (and BX for str) 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 == nkind.N_TARRAY) { if (n.lhs.rhs != nil) { if (n.lhs.rhs.kind == nkind.N_INTLIT) { total = n.lhs.rhs.uval: i32; }; }; }; }; for (idx < total) { if (isstrel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); } else { 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=...};`. // Delegates to the shared cgstructlitfillbp helper: TK_ELLIPSIS // autofill + per-field walk, with nested struct-typed structlit // values recursing into the helper instead of landing only AX // (the #17 silent-zero fix). Mirror of cstage cgen.c N_LET // structlit branch. if (rhs.kind == nkind.N_STRUCTLIT) { let trefn: *node = rhs.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (trefn != nil) { if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; }; let si: *structinfo = structlookup(c, sname); if (si != nil) { cgstructlitfillbp(c, si, rhs, off); c.lastwasreturn = 0; return; }; }; // Whole-struct receive for sizes <=24B (call-result rhs). // Counterpart of #4's cgreturn ABI: cgexpr leaves // AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], // zero-padded to 24B by the producer. // // ASYMMETRY (do NOT mirror the sender): producer emits three // uniform MOVQs into a zero-padded 24B scratch slot; the // receiver writes only `sz` bytes — MOVQ for full 8B chunks // plus a sized tail (MOVL/MOVW/MOVB) by the *declared* // struct size. Otherwise a trailing 1..7-byte chunk would // overrun into the next local slot. // // Tail chunks in {3,5,6,7} (unreachable under WW struct // alignment rules — field aligns force size%align==0) fall // through to the generic scalar store rather than emit a // stomping MOVQ tail. Sizes >24B also fall through (sret // deferred, same constraint as #4). Mirrors the cstage // cgen.c N_LET receive branch. if (rhs.kind == nkind.N_CALL) { let sname: str; sname.ptr = nil; sname.len = 0; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; }; if (sname.len > 0) { let lsi: *structinfo = structlookup(c, sname); if (lsi != nil) { // si.totsize is slot-padded (rounded to 8) for // stack-slot use; the receive ABI needs the // TYPE's natural size — see structnaturalsize. let lsz: i32 = structnaturalsize(lsi); let tlm: i32 = lsz - (lsz / 8) * 8; if (lsz <= 24) { if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, rhs); let full: i32 = lsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((off + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((off + full * 8): i64); emitline("(BP)\n"); }; c.lastwasreturn = 0; return; }; }; }; }; }; cgexpr(c, rhs); // Float local: cgexpr leaves the value in X0. Spill via // MOVSS (f32, 4B) or MOVSD (f64, 8B). if (isfloattype(c, n.lhs)) { let mov: str = "MOVSD"; if (isf32type(c, n.lhs)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); c.lastwasreturn = 0; return; }; 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 N_LET no-rhs branch) zero-inits in two // shapes: // - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.): // single `MOVQ $0, off(BP)`. // - multi-word composites (str/slice/tuple/struct/tagged): // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot // so reads after the bare let see {0...} rather than // stack garbage. // `[N]T` arrays of size != 8 keep the per-index-write // contract — they're left uninit. let isarr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (!isarr) { if (sz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= sz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; for (zi + 4 <= sz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; for (zi < sz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 1; }; }; }; }; }; 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: _loop_N for the top, // _endloop_N for the post-body merge. No separate cont // label when there's no post-expression. let topl: str = mklabel(c, "loop"); let endl: str = mklabel(c, "endloop"); // `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 == nkind.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 == nkind.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 nkind.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 == nkind.N_CALL) { let callee: *node = rhs.lhs; if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); if (rt != nil) { if (rt.kind == nkind.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; }; // paramfieldsize — raw byte size of a tuple-field type. Mirrors the // `tp->type->size` read in C cgen N_FORRANGE: 1 for i8/u8/bool, 4 for // i32/u32, 8 for i64/u64/*T/fn/slice-elt, 16 for str, default 8. fn paramfieldsize(t: *node) i32 = { if (t == nil) { return 8; }; let k: nkind = t.kind; if (k == nkind.N_TPTR) { return 8; }; if (k == nkind.N_TFN) { return 8; }; if (k == nkind.N_TCHAN) { return 8; }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "str")) { return 16; }; let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; }; return 8; }; // paramissigned — does this type need sign-extending on a sub-word // (1/2/4B) load? Mirrors cstage's signed_field check via // fieldissignedc (resolves TBANG / TENUM / alias chains). fn paramissigned(c: *cgen, t: *node) bool = { return fieldissignedc(c, t); }; // cgforrange — lower `for (let x .. slice) body` (and the tuple- // destructure cousin `for (let (a, b) .. slice) body`). The body is // wrapped in a counted loop driven by stack-spilled `.rgi`/`.rgl`. // Each iteration computes the element address `s.ptr + i*esz` and // either loads the whole element into the named local or pulls each // tuple field into its own local. Mirrors cmd/w6c/cgen.c N_FORRANGE // byte-for-byte (label names + labelseq consumption order). fn cgforrange(c: *cgen, n: *node) void = { let slc: *node = n.lhs; let slclocal: *local = nil; let slctn: *node = nil; if (slc != nil) { if (slc.kind == nkind.N_IDENT) { slclocal = localfindnode(c, slc.str); if (slclocal != nil) { slctn = slclocal.tnode; }; }; }; // Element type — peek through TSLICE/TARRAY for the tuple param walk. let elemt: *node = nil; if (slctn != nil) { let sk: nkind = slctn.kind; if (sk == nkind.N_TSLICE) { elemt = slctn.lhs; }; if (sk == nkind.N_TARRAY) { elemt = slctn.lhs; }; }; // esz: raw elem byte size. For tuple-element slices `[](T0, T1)`, // C cgen reads the resolved tuple's size (sum of raw param sizes, // no slot-padding) so e.g. `(i64, i64)` is 16, `(i32, i32)` is 8. // elemsizeof returns 8 for non-primitive elem, which would be // wrong here — compute from the tuple param walk instead. let esz: i32 = elemsizeof(slctn); if (elemt != nil) { if (elemt.kind == nkind.N_TTUPLE) { let total: i32 = 0; let p: *node = elemt.list; for (p != nil) { total += paramfieldsize(p); p = p.next; }; esz = total; }; }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. let iname: str = mkscratchname(c, "rgi"); let lname: str = mkscratchname(c, "rgl"); let ioff: i32 = localalloc(c, iname, 8, nil); let loff: i32 = localalloc(c, lname, 8, nil); // Per-binding (up to 8 — matches the C array). Parallel arrays so // we don't depend on local-struct cgen. let bind_off: [8]i32; let bind_sz: [8]i32; let bind_foff: [8]i32; let bind_signed: [8]bool; let nbinds: i32 = 0; if (destruct) { let tp: *node = nil; if (elemt != nil) { if (elemt.kind == nkind.N_TTUPLE) { tp = elemt.list; }; }; let field_off: i32 = 0; let m: *node = n.list; for (m != nil) { if (nbinds >= 8) { m = nil; } else { let fsz: i32 = 8; let signf: bool = false; if (tp != nil) { fsz = paramfieldsize(tp); signf = paramissigned(c, tp); }; let slot_sz: i32 = fsz; if (slot_sz < 8) { slot_sz = 8; }; bind_sz[nbinds] = fsz; bind_foff[nbinds] = field_off; bind_signed[nbinds] = signf; let bnm: str = m.str; if (bnm.len > 0) { bind_off[nbinds] = localadd(c, bnm, slot_sz, tp); } else { bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tp); }; field_off += fsz; nbinds += 1; if (tp != nil) { tp = tp.next; }; m = m.next; }; }; } else { let slot_sz: i32 = esz; if (slot_sz < 8) { slot_sz = 8; }; bind_sz[0] = esz; bind_foff[0] = 0; // Single-binding signed-narrow detection: mirror C which // reads `u->sub->kind` for the elem type. bind_signed[0] = false; if (elemt != nil) { bind_signed[0] = paramissigned(c, elemt); }; if (n.str.len > 0) { // Register with elem tnode so x.field on a loop // var resolves through the standard local-typed // path instead of falling into the SB fallback. bind_off[0] = localadd(c, n.str, slot_sz, elemt); } else { bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt); }; nbinds = 1; }; // init: ioff(BP) = 0 emitline("\tMOVQ\t$0, "); emitoff(ioff: i64); emitline("(BP)\n"); // loff(BP) = len let isarr: bool = false; let isslicestr: bool = false; if (slctn != nil) { let tk: nkind = slctn.kind; if (tk == nkind.N_TSLICE) { isslicestr = true; }; if (tk == nkind.N_TARRAY) { isarr = true; }; if (tk == nkind.N_TNAME) { if (streq(slctn.str, "str")) { isslicestr = true; }; }; }; if (isslicestr) { if (slc.kind == nkind.N_IDENT) { if (slclocal != nil) { emitline("\tMOVQ\t"); emitoff((slclocal.off + 8): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(loff: i64); emitline("(BP)\n"); }; }; } else { if (isarr) { let alen: i64 = 0i64; if (slctn.rhs != nil) { if (slctn.rhs.kind == nkind.N_INTLIT) { alen = slctn.rhs.uval: i64; }; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", "); emitoff(loff: i64); emitline("(BP)\n"); } else { cgexpr(c, slc); emitline("\tMOVQ\tAX, "); emitoff(loff: i64); emitline("(BP)\n"); };}; let loopl: str = mklabel(c, "rloop"); let endl: str = mklabel(c, "rend"); let naturall: str = endl; if (n.els != nil) { naturall = mklabel(c, "relseloop"); }; c.loopcontbuf[c.looptop] = loopl; c.loopendbuf[c.looptop] = endl; c.looptop += 1; emitlabel(loopl); emitline("\tMOVQ\t"); emitoff(ioff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff(loff: i64); emitline("(BP), BX\n"); emitline("\tCMPQ\tBX, AX\n"); emitline("\tJGE\t"); emitline(naturall); emitline("\n"); // BX = base + i*esz if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (slc.kind == nkind.N_IDENT) { if (slclocal != nil) { if (isarr) { emitline("\tLEAQ\t"); emitoff(slclocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(slclocal.off: i64); emitline("(BP), BX\n"); }; }; }; emitline("\tADDQ\tAX, BX\n"); // Per-binding load from BX+foff. Signedness comes from bind_signed // (set via paramissigned → fieldissignedc), so enum-aliased narrows // pick the right MOVS*Q without a literal-name gate. let b: i32 = 0; for (b < nbinds) { let op: str = loadopsz(bind_signed[b], bind_sz[b]); emitline("\t"); emitline(op); emitline("\t"); emitoff(bind_foff[b]: i64); emitline("(BX), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(bind_off[b]: i64); emitline("(BP)\n"); b += 1; }; if (n.body != nil) { cgstmt(c, n.body); }; c.looptop -= 1; emitline("\tADDQ\t$1, "); emitoff(ioff: i64); emitline("(BP)\n"); emitline("\tJMP\t"); emitline(loopl); emitline("\n"); if (n.els != nil) { emitlabel(naturall); cgstmt(c, n.els); }; emitlabel(endl); c.lastwasreturn = 0; return; }; // cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to // a chain of compares against the scrutinee. Scrutinee lands in a // fresh 8B local slot so case bodies can spill SP without losing it. // Cases are tried top-to-bottom; the `case:` arm with no exprs is the // default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c // N_SWITCH: same labelseq consumption order so labels match byte-for- // byte. fn cgswitch(c: *cgen, n: *node) void = { let swname: str = mkscratchname(c, "sw"); let sloff: i32 = localalloc(c, swname, 8, nil); if (n.lhs != nil) { cgexpr(c, n.lhs); }; emitline("\tMOVQ\tAX, "); emitoff(sloff: i64); emitline("(BP)\n"); let endl: str = mklabel(c, "swend"); let defcase: *node = nil; let cs: *node = n.list; for (cs != nil) { if (cs.list == nil) { defcase = cs; cs = cs.next; continue; }; let body: str = mklabel(c, "swcase"); let nxt: str = mklabel(c, "swnext"); let e: *node = cs.list; for (e != nil) { cgexpr(c, e); emitline("\tMOVQ\t"); emitoff(sloff: i64); emitline("(BP), BX\n"); emitline("\tCMPQ\tBX, AX\n"); emitline("\tJE\t"); emitline(body); emitline("\n"); e = e.next; }; emitline("\tJMP\t"); emitline(nxt); emitline("\n"); emitlabel(body); if (cs.body != nil) { cgstmt(c, cs.body); }; emitline("\tJMP\t"); emitline(endl); emitline("\n"); emitlabel(nxt); cs = cs.next; }; if (defcase != nil) { if (defcase.body != nil) { cgstmt(c, defcase.body); }; }; emitlabel(endl); 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 == nkind.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. Post-#27 every let allocates fresh (no // name dedup), so we always count + always append a stub. // The stub carries n.lhs as tnode so later scanlocals // nodes can dispatch on type — e.g. detecting `arr[i] = ...` // where arr is a tagged-element array (needs @tagscr). // localfindnode walks head-first, so the freshest stub // (innermost binding) wins lookup. let sz: i32 = letslotsize(c, n); if (sz < 8) { sz = 8; }; if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; total += sz; let stub: *local = amalloc(c.a, 48u64): *local; stub.name = n.str; stub.off = 0; stub.tnode = n.lhs; stub.lnext = c.locals; c.locals = stub; }; // 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 == nkind.N_MLET) { let p0t: *node = nil; let p1t: *node = nil; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { let callee: *node = n.rhs.lhs; if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); if (rt != nil) { if (rt.kind == nkind.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) { 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; // Always-fresh stub (post-#27); tnode carries the // binding's type so later array-index dispatch can // resolve the let through localfindnode. let stub: *local = amalloc(c.a, 48u64): *local; stub.name = l.str; stub.off = 0; stub.tnode = t; stub.lnext = c.locals; c.locals = stub; l = l.next; bidx += 1; }; }; // `switch` allocates an 8B scratch slot for the scrutinee so case // bodies can spill through SP without losing it. The slot is named // ".sw_" at cgen time — unique per switch — so it must // not dedup. Count it here so the frame SUBQ matches. if (n.kind == nkind.N_SWITCH) { total += 8; }; // `for (let x .. s)` allocates two 8B scratch slots — `.rgi_` // (counter) and `.rgl_` (length) — plus one slot per binding. // Per-binding sz defaults to 8 (covers scalar primitives + ptrs). // `str` tuple-fields would need 16 — selfhost doesn't yet emit // those, so the simple count tracks C cgen for current fixtures. if (n.kind == nkind.N_FORRANGE) { total += 16; // .rgi + .rgl scratch // Each forrange binding gets a fresh 8B slot (post-#27). // Stub is also appended so the body's references resolve // to this binding via head-first localfindnode lookup. if (n.list != nil) { let m: *node = n.list; for (m != nil) { let bnm: str = m.str; total += 8; if (bnm.len > 0) { let stub: *local = amalloc(c.a, 48u64): *local; stub.name = bnm; stub.off = 0; stub.tnode = m.lhs; stub.lnext = c.locals; c.locals = stub; }; m = m.next; }; } else { let bnm: str = n.str; total += 8; if (bnm.len > 0) { let stub: *local = amalloc(c.a, 48u64): *local; stub.name = bnm; stub.off = 0; stub.tnode = nil; stub.lnext = c.locals; c.locals = stub; }; }; }; // `match (non-ident)` needs a 24B `@match_spill` scratch slot for // cgmatch to land the AX:DX:CX return triple. Mirrors C cgen's // localoff("@match_spill", ...). N_IDENT scrutinees read the slot // directly off the local — no spill needed. if (n.kind == nkind.N_MATCH) { let sc: *node = n.lhs; if (sc != nil) { if (sc.kind != nkind.N_IDENT) { total += 24; }; }; }; // 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 == nkind.N_MCASE) { let bn: str = n.str; if (bn.len > 0) { let pat: *node = n.lhs; if (pat != nil) { // Must mirror cgmatch's bind-slot sizing in // cgenexpr.ww (`bsz = slotsize(c, pat)`): // hardcoding str/slice/8 here underbooked the // frame for TY_STRUCT variants — the emit-time // localalloc(bsz=24) then wrote past the SUBQ'd // SP, smashing whatever the OS put under it // (project #31). let psz: i32 = slotsize(c, pat); if (psz <= 0) { psz = 8; }; if ((psz & 7) != 0) { psz = (psz + 7) & ~7; }; total += psz; }; }; // Match arms get a fresh local scope at emission time // (cgmatch saves c.locals before each arm and restores // after). scanlocals must mirror that: walk the arm // body with a saved/restored seenmark set so two arms // declaring the same name each get their own slot, // matching the per-arm frame growth the emit phase // produces. if (n.body != nil) { let saved: *local = c.locals; total += scanlocals(c, n.body); c.locals = saved; }; return total; }; // Tagged-arr/slice index store needs a 24B scratch slot // (`@tagscr`) for cgwidentaggedstore to materialise the source // in before copying to the element address. Reserved once per // function (dedup'd via scanseenmark) regardless of how many // tagged-arr stores the body contains. if (n.kind == nkind.N_ASSIGN) { let alhs: *node = n.lhs; if (alhs != nil) { if (alhs.kind == nkind.N_INDEX) { let abase: *node = alhs.lhs; if (abase != nil) { if (abase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, abase.str); let btn: *node = nil; if (lc != nil) { btn = lc.tnode; } else { btn = letvartnode(c, abase.str); }; if (btn != nil) { let bk: nkind = btn.kind; let etn: *node = nil; if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; if (bk == nkind.N_TPTR) { etn = btn.lhs; }; if (etn != nil) { if (istaggedtype(c, etn)) { if (!scanseenmark(c, "@tagscr")) { total += 24; }; }; }; }; }; }; }; }; }; // Tagged-union struct-field write: `s.f = v` or `(*p).f = v` // where f is a tagged-union field. cgassign delegates to // cgwidentaggedstore; for pointer-rooted dst the wrapper // allocates @tagbase (8B) and @tagscr (slot_sz). Both names // dedup with other tagged scratch users in the same function. if (n.kind == nkind.N_ASSIGN) { let alhs: *node = n.lhs; if (alhs != nil) { if (alhs.kind == nkind.N_DOT) { let abase: *node = alhs.lhs; if (abase != nil) { if (abase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, abase.str); let btn: *node = nil; if (lc != nil) { btn = lc.tnode; } else { btn = letvartnode(c, abase.str); }; let isptr: bool = false; let stype: *node = nil; if (btn != nil) { if (btn.kind == nkind.N_TPTR) { isptr = true; stype = btn.lhs; }; if (btn.kind == nkind.N_TNAME) { stype = btn; }; }; if (stype != nil) { if (stype.kind == nkind.N_TNAME) { let si: *structinfo = structlookup(c, stype.str); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, alhs.str)) { if (istaggedtype(c, fi.tnode)) { if (isptr) { if (!scanseenmark(c, "@tagbase")) { total += 8; }; if (!scanseenmark(c, "@tagscr")) { total += 24; }; }; }; fi = nil; } else { fi = fi.finext; }; }; }; }; }; }; }; }; }; }; // Tagged-union return with struct payload or tagged-subset // source — cgreturn materialises in @tagscr then loads // AX/DX/CX. Detect via the same rhsstructpayload predicate // the cgen uses, so we only reserve when the cgen will // actually emit a scratch-using path. `!void` / `!i32` // aliases share N_STRUCTLIT shape but resolve to // non-struct types — they fall through to scalar/str and // don't need scratch. if (n.kind == nkind.N_RETURN) { if (c.fnret != nil) { if (istaggedtype(c, c.fnret)) { if (!isnullabletype(c.fnret)) { let rhs: *node = n.lhs; let needs: bool = false; if (rhs != nil) { let sn: str = rhsstructpayload(c, rhs); if (sn.len > 0) { needs = true; }; if (rhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { needs = true; }; }; }; }; if (needs) { if (!scanseenmark(c, "@tagscr")) { total += 24; }; }; }; }; }; }; // Whole-struct return for sizes <= 24B uses @retscr — when // the function's return type is a registered TY_STRUCT of // size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT, // cgreturn materialises in @retscr then loads AX/DX/CX. // Mirrors cstage cgen.c which allocates the scratch slot // inline; here we must pre-reserve so the prologue SUBQ // reserves enough frame. if (n.kind == nkind.N_RETURN) { if (c.fnret != nil) { if (c.fnret.kind == nkind.N_TNAME) { let rname: str = c.fnret.str; let rsi: *structinfo = structlookup(c, rname); if (rsi != nil) { if (rsi.totsize <= 24) { let rhs: *node = n.lhs; let okrhs: bool = false; if (rhs != nil) { if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; }; if (okrhs) { if (!scanseenmark(c, "@retscr")) { total += 24; }; }; }; }; }; }; }; // Call-site struct-payload widening uses @tagscr — when the // arg is a struct literal/ident and the callee's param is // tagged, pushargsrev materialises in scratch and pushes. // Scalar / str args take the direct-push fast path (no // scratch). Tagged-typed ident args also skip widening (the // slot is already laid out, so pushargsrev pushes slot words // directly). Both fast paths agree with C cgen bytewise, so // only struct-payload sites get a scratch reservation. if (n.kind == nkind.N_CALL) { let callee: *node = n.lhs; let cnm: str; cnm.ptr = nil; cnm.len = 0; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; }; if (cnm.len > 0) { let ps: *node = fnparamslookup(c, cnm); let a: *node = n.list; for (a != nil) { if (ps == nil) { a = nil; } else { if (ps.kind == nkind.N_PARAM) { let pt: *node = ps.lhs; if (istaggedtype(c, pt)) { if (!isnullabletype(pt)) { let sn: str = rhsstructpayload(c, a); if (sn.len > 0) { let isidentstruct: bool = false; if (a.kind == nkind.N_IDENT) { // Struct ident as // tagged arg — pushargsrev // still routes through the // scratch path. isidentstruct = true; }; let _u: bool = isidentstruct; if (!scanseenmark(c, "@tagscr")) { total += 24; }; }; }; }; }; if (a != nil) { a = a.next; ps = ps.next; }; }; }; }; // Hare-style variadic call: reserve @vararg_d_ for the // element data and @vararg_sl_ for the 24B slice // descriptor. The seq is recorded on the N_CALL node so // cgcall picks the same names regardless of walk order // (scanlocals descends LTR; pushargsrev evaluates RTL). let nfixed: i32 = 0; let varp: *node = callee_variadic_param(c, n.lhs, &nfixed); if (varp != nil) { let nargs: i32 = 0; let aw: *node = n.list; for (aw != nil) { nargs += 1; aw = aw.next; }; let nvar: i32 = nargs - nfixed; if (nvar < 0) { nvar = 0; }; let forwarding: bool = false; if (nvar == 1) { let aa: *node = n.list; let k0: i32 = 0; for (k0 < nfixed) { aa = aa.next; k0 += 1; }; if (aa != nil) { if (aa.kind == nkind.N_SPREAD) { forwarding = true; }; }; }; if (!forwarding) { let seq: i32 = c.varargseq; n.uval = seq: u64; c.varargseq += 1; let esz: i32 = slotsize(c, varp.lhs); if (esz < 1) { esz = 1; }; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); if (nvar > 0) { if (!scanseenmark(c, dname)) { let dsz: i32 = nvar * esz; if ((dsz & 7) != 0) { dsz = (dsz + 7) & ~7; }; total += dsz; }; }; if (!scanseenmark(c, sname)) { total += 24; }; }; }; }; 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; let fidx: i32 = 0; // Cursor for args that overflow the SysV reg windows. Each // stack-passed arg lives at 16+8*k(BP) — no spill, the local // is registered with a *positive* offset pointing into the // caller's frame. Mirrors C cgen's cg_stack_arg_cursor. let stkcursor: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; // Hare-style variadic `T...`: callee receives a []T // slice (3 register words / 24B). Mirror the slice- // param spill below but use a synthesised TSLICE // tnode so body references see the slot as a slice. if (p.op == tkind.TK_ELLIPSIS) { let tn: *node = slicewrap(c, p.lhs); if (idx + 3 <= 6) { let off: i32 = localadd(c, nm, 24, tn); 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 (idx < 6) { // Partial-fit stitch — variadic `T...` is a slice // at the ABI boundary (the call site synthesises a // 24B descriptor and pushes ptr/len/cap), so this // mirrors the slice branch at cgendecl.ww:518. let off: i32 = localadd(c, nm, 24, tn); let regs_left: i32 = 6 - idx; let w: i32 = 0; for (w < regs_left) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + w*8): i64); emitline("(BP)\n"); idx += 1; w += 1; }; for (w < 3) { emitline("\tMOVQ\t"); emitoff((16 + stkcursor*8): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + w*8): i64); emitline("(BP)\n"); stkcursor += 1; w += 1; }; } else { localaddstack(c, nm, tn, 16 + stkcursor*8); stkcursor += 3; };}; p = p.next; continue; }; if (isfloattype(c, p.lhs)) { // Float param: SysV uses the XMM stream // (X0..X7). 8B (f64) or 4B (f32) slot. let fsz: i32 = 8; if (isf32type(c, p.lhs)) { fsz = 4; }; if (fidx < 8) { let off: i32 = localadd(c, nm, fsz, p.lhs); let mov: str = "MOVSD"; if (fsz == 4) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitline(fargregname(fidx)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); fidx += 1; } else { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += 1; }; p = p.next; continue; }; if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; if (idx + nw <= 6) { let off: i32 = localadd(c, nm, slot, p.lhs); 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 (idx < 6 && nw > 1) { // Partial fit: fill remaining regs, then read // the tail from positive BP offsets. Mirrors // the caller's greedy reg fill in pushargsrev. let off: i32 = localadd(c, nm, slot, p.lhs); let regs_left: i32 = 6 - idx; let w: i32 = 0; for (w < regs_left) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + w*8): i64); emitline("(BP)\n"); idx += 1; w += 1; }; for (w < nw) { emitline("\tMOVQ\t"); emitoff((16 + stkcursor*8): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + w*8): i64); emitline("(BP)\n"); stkcursor += 1; w += 1; }; } else { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += nw; };}; } else { if (isslicetype(c, p.lhs)) { if (idx + 3 <= 6) { 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 (idx < 6) { // Partial-fit stitch — mirrors tagged at lines // 440-469. Caller's pushargsrev greedy-fills the // remaining argregs (ptr,len,cap order), the tail // spills to +16+stkcursor*8(BP). let off: i32 = localadd(c, nm, 24, p.lhs); let regs_left: i32 = 6 - idx; let w: i32 = 0; for (w < regs_left) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + w*8): i64); emitline("(BP)\n"); idx += 1; w += 1; }; for (w < 3) { emitline("\tMOVQ\t"); emitoff((16 + stkcursor*8): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + w*8): i64); emitline("(BP)\n"); stkcursor += 1; w += 1; }; } else { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += 3; };}; } else { if (isstrtype(c, p.lhs)) { if (idx + 2 <= 6) { 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 { if (idx < 6) { // Partial-fit stitch — mirrors tagged at lines // 440-469. Only idx=5 hits this (nw=2, // regs_left=1): ptr lands in R9, len at // +16+stkcursor*8(BP). let off: i32 = localadd(c, nm, 16, p.lhs); let regs_left: i32 = 6 - idx; let w: i32 = 0; for (w < regs_left) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + w*8): i64); emitline("(BP)\n"); idx += 1; w += 1; }; for (w < 2) { emitline("\tMOVQ\t"); emitoff((16 + stkcursor*8): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + w*8): i64); emitline("(BP)\n"); stkcursor += 1; w += 1; }; } else { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += 2; };}; } else { if (idx < 6) { let off: i32 = localadd(c, nm, 8, p.lhs); emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); idx += 1; } else { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += 1; }; };};}; }; p = p.next; }; }; fn cgfn(c: *cgen, fn_: *node) void = { cgeninit(c, c.a); c.fnname = fn_.str; c.curmod = fn_.module; c.fnret = fn_.lhs; // Emit the TEXT label via emitfnname so the def site picks up the // same skip rule (FFI / `main` / empty-module) and the same module // hint (this fn's own module) that the call sites use. Drops the // `exported == 0` skip in the legacy inline form — exported fns // now mangle too, so cross-module same-leaf exports coexist. emitline("TEXT "); emitfnname(c, fn_.str, fn_.module); emitline(",$"); // Pre-scan total frame: only count params that land in a local // slot. SysV-class accounting; mirrors runtime walk in cstage // cgen.c §5130-5223 and cgfnparams below. A stack-spilled param // is addressed at a positive BP offset by cgfnparams (via // localaddstack) and consumes no frame, so adding its size here // would over-allocate. Seed c.locals with param-name stubs so // scanlocals dedups a re-declared `let ` 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; let argi: i32 = 0; let fargi: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { let isvar: bool = scanp.op == tkind.TK_ELLIPSIS; let isf: bool = false; let istg: bool = false; let issl: bool = false; let isst: bool = false; if (!isvar) { isf = isfloattype(c, scanp.lhs); istg = istaggedtype(c, scanp.lhs); if (!isf && !istg) { issl = isslicetype(c, scanp.lhs); if (!issl) { isst = isstrtype(c, scanp.lhs); }; }; }; let eb: i32 = 1; let sz: i32 = 8; if (isvar) { eb = 3; sz = 24; } else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; } else { if (issl) { eb = 3; sz = 24; } else { if (isst) { eb = 2; sz = 16; } else { if (isf) { eb = 1; sz = 8; if (isf32type(c, scanp.lhs)) { sz = 4; }; }; }; }; }; }; let regs_left: i32 = 6 - argi; if (isf) { regs_left = 8 - fargi; }; if (regs_left >= eb) { frame += sz; if (isf) { fargi += 1; } else { argi += eb; }; } else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isvar)) { // Multi-word param straddles the reg/stack boundary; // cgfnparams stitches the tail from positive BP // offsets into a single local slot, so we still // reserve the full size. Symmetric across tagged, // slice, str and variadic `T...` // (cgendecl.ww:467/518/564/394). frame += sz; argi = 6; } else { // Pure stack: lives at +BP(16+stkcursor*8); no // local slot consumed. The reg cursor stays put. }; }; scanseenmark(c, scanp.str); }; scanp = scanp.next; }; c.varargseq = 0; if (fn_.body != nil) { frame += scanlocals(c, fn_.body); }; c.varargseq = 0; // 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; // Iterate the fn body's statements directly rather than dispatching // the outermost N_BLOCK through cgstmt — cgblock now save/restores // c.locals to scope inner shadows (post-#27), but the function body // is not "an inner block": defers (queued during the body) and the // implicit-return epilogue both call cgexpr after this loop and // resolve identifiers via localfind, so the body's locals must // still be in c.locals when we get there. if (fn_.body != nil) { if (fn_.body.kind == nkind.N_BLOCK) { let s: *node = fn_.body.list; for (s != nil) { cgstmt(c, s); s = s.next; }; } else { 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); // Enums must register before structs — fieldsize on a tkind-typed // field needs the enum's storage size, otherwise it falls back to // 8 (wrong load width). collectenums(c, file); collectstructs(c, file); collectdefs(c, file); collectfnrets(c, file); fficollect(c, file); collectmods(c, file); collectlets(c, file); let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { if (d.body != nil) { cgfn(c, d); }; }; d = d.next; }; letpreintern(c, file); emitdatasection(c); emitdefconstants(c, file); emitletdataw(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: nkind.N_FILE, nkind.N_FNDECL (params, frame for locals, prologue // + dual-epilogue suppression; FFI body-less fn skipped) // - stmts: nkind.N_BLOCK, nkind.N_RETURN, nkind.N_EXPRSTMT, nkind.N_LET (no init), // nkind.N_LET (int-literal / ident / call / nkind.N_BIN init), // nkind.N_IF (with optional else), nkind.N_FOR (cond-only and full // init/cond/post), nkind.N_BREAK, nkind.N_CONTINUE // - exprs: nkind.N_INTLIT, nkind.N_IDENT (local/param), nkind.N_BIN with full op // coverage (+/-/*/// %, &/|/^, <>, comparisons with // signed-vs-unsigned dispatch, &&/||), nkind.N_UN (- ! ~ & *), // nkind.N_CALL (recursive R-to-L push, pop into argregs L-to-R), // nkind.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 nkind.N_TNAME aliases are mapped; // `type p = struct {...}` is handled by collectstructs. type aliasent = struct { aname: str, amod: str, // originating module (`// MODULE: foo`), or empty 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 == nkind.N_TYPEDECL) { let body: *node = d.lhs; if (body != nil) { if (body.kind != nkind.N_TSTRUCT) { let a: *aliasent = amalloc(c.a, 64u64): *aliasent; a.aname = d.str; a.amod = d.module; 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; }; // Module-qualified form: `pkg.alias` → match the leaf name // scoped to its originating module. Mirrors check.c's module- // qualified type resolution; requiring `amod == pkg` is what // prevents two modules with same-leaf-name aliases from // collapsing into whichever entry appears first in the chain. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' let pkg: str; pkg.ptr = name.ptr; pkg.len = i; let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); let b: *aliasent = c.aliases; for (b != nil) { if (streq(b.aname, leaf)) { if (streq(b.amod, pkg)) { return b.target; }; }; b = b.aanext; }; i = -1; } else { i -= 1; }; }; 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. // foldintliteral — fold the literal subset usable for top-level // constant slots: int/rune literal, true/false/nil, and a unary // +/-/~ over the same (any depth). No sibling-ident, no binary op. // Shared between enumevalmember (literal leaves) and // emitdefconstants (top-level def rhs). // // Whitelist kept tight on purpose: anything richer (sibling refs, // arithmetic) belongs in enumevalmember, which calls this for its // literal leaves and handles the rest itself. fn foldintliteral(e: *node, out: *u64) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == nkind.N_TRUE) { *out = 1u64; return true; }; if (k == nkind.N_FALSE) { *out = 0u64; return true; }; if (k == nkind.N_NIL) { *out = 0u64; return true; }; if (k == nkind.N_UN) { let v: u64; if (!foldintliteral(e.lhs, &v)) { return false; }; let op: tkind = e.op; if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == tkind.TK_TILDE) { *out = ~v; return true; }; if (op == tkind.TK_PLUS) { *out = v; return true; }; return false; }; return false; }; fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { if (e == nil) { return false; }; if (foldintliteral(e, out)) { return true; }; let k: nkind = e.kind; if (k == nkind.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 == nkind.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: tkind = e.op; if (op == tkind.TK_PLUS) { *out = a + b; return true; }; if (op == tkind.TK_MINUS) { *out = a - b; return true; }; if (op == tkind.TK_STAR) { *out = a * b; return true; }; if (op == tkind.TK_SLASH) { if (b == 0u64) { return false; }; *out = a / b; return true; }; if (op == tkind.TK_PERCENT) { if (b == 0u64) { return false; }; *out = a % b; return true; }; if (op == tkind.TK_AMP) { *out = a & b; return true; }; if (op == tkind.TK_PIPE) { *out = a | b; return true; }; if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; return false; }; if (k == nkind.N_UN) { let v: u64; if (!enumevalmember(prev, e.lhs, &v)) { return false; }; let op: tkind = e.op; if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == tkind.TK_TILDE) { *out = ~v; return true; }; if (op == tkind.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 == nkind.N_TYPEDECL) { let body: *node = d.lhs; if (body != nil) { if (body.kind == nkind.N_TENUM) { let et: *enumtype = amalloc(c.a, 64u64): *enumtype; et.ename = d.str; et.emod = d.module; 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 = { // Exact match first: bare-from-source idents and already-leafed // names hit here directly. let e: *enumtype = c.enums; for (e != nil) { if (streq(e.ename, name)) { return e; }; e = e.etnext; }; // Module-qualified form: `pkg.enum` → match the leaf scoped to // its originating module. Mirrors aliaslookup's mod-filter; the // `emod == pkg` guard is what prevents two modules with same- // leaf-name enums from collapsing into whichever entry appears // first in the chain. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' let pkg: str; pkg.ptr = name.ptr; pkg.len = i; let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); let b: *enumtype = c.enums; for (b != nil) { if (streq(b.ename, leaf)) { if (streq(b.emod, pkg)) { return b; }; }; b = b.etnext; }; return nil; }; i -= 1; }; 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 != nkind.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 nkind.N_TYPEDECL with nkind.N_TSTRUCT lhs. // nkind.N_DOT and nkind.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, smod: str, // originating module (`// MODULE: foo`), or empty fields: *fieldinfo, totsize: i32, sinext: *structinfo, }; // ---- locals / frame -------------------------------------------------- type local = struct { name: str, off: i32, tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil lnext: *local, }; // strlit — interned string literal record. Emitted as a DATA directive // after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len). type strlit = struct { label: str, // "_S_" bytes: str, slnext: *strlit, }; // ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr // gets its CALL target rewritten to `name`. type ffi = struct { ident: str, symbol: str, fnext: *ffi, }; // 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, emod: str, // originating module (`// MODULE: foo`), or empty 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, // fn (any export status) + non-exported // let/def/type decls → originating module lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, curmod: str, // current fn's `// MODULE: foo` directive (len=0 // when the fn is in the primary file). Drives // bare-IDENT call mangling — `frob()` from // inside lib/foo binds to `foo.frob` even when // other modules also export `frob`. Set in cgfn // before walking the body. 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) // Variadic-call gather state. scanlocals walks the body in pre- // order DFS and assigns per-call scratch names `@vararg_d_N` / // `@vararg_sl_N` using this counter; cgcall resets and walks in // the same order so the names line up at emission time. varargseq: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. // Populated alongside modents; consulted by cgassign, cgdot, cgident // and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot // instead of being silently dropped. tnode is the declared type AST // node — needed to distinguish scalar (8B) from str (16B) globals // when picking the load/store sequence. type letvar = struct { name: str, tnode: *node, lvnext: *letvar, }; fn cgeninit(c: *cgen, a: *arena) void = { c.a = a; c.locals = nil; c.frame = 0; c.lastwasreturn = 0; c.labelseq = 0; c.varargseq = 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 nkind.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; }; // localaddstack — register a param at a positive BP offset. Used for // args that overflow the 6 SysV int / 8 float reg windows; the caller // pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), // etc. (after the saved RIP+BP). No spill instruction is emitted; the // slot IS the caller's stack slot. fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = { let l: *local = amalloc(c.a, 48u64): *local; l.name = name; l.off = off; l.tnode = tnode; l.lnext = c.locals; c.locals = l; }; fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // User-let path (post-#27): always allocate a fresh slot per // binding. Pre-fix this deduped by name to share one slot // across same-name lets in disjoint scopes — inherited from // C cgen's localoff. Both stages had the same silent-stack- // corruption bug: an inner 8B `let a: i64` allocated first // would force a later outer `let a: [128]u8` onto the 8B slot, // and `a[127]` would write at +119(BP), past the saved RIP. // Localfind walks head-first, so the most-recent binding still // wins lookups inside its scope. Tnode is carried on the // freshly-pushed entry, so type dispatch in cgenutil never // sees a stale predecessor. // // Synthetic scratch slots (`@tagscr`, `@retscr`, `@tagbase`) // keep the per-fn dedup. Each scratch is sized identically // across its call sites and intended to be shared — the // scanlocals pre-pass also dedups via scanseenmark, so frame // reservation and emit-time allocation stay in sync. The // `@`-prefix carve-out preserves that contract; user names // can never start with `@` (lexer-rejected). if (name.len > 0) { if (name[0] == 64u8) { // '@' 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 s: str = strconv.i64tos(v, strconv.base.DEC); os.write(1, s.ptr, s.len: u64); }; fn emituint(v: u64) void = { let s: str = strconv.u64tos(v, strconv.base.DEC); os.write(1, s.ptr, s.len: 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 "__". Returns an // arena-owned str. Mirrors C cgen's mklabel so diffs match. fn mklabel(c: *cgen, prefix: 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 < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; 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"); }; // mkscratchname — fresh local-slot name "._". Used for // compiler-synthesised slots (switch scrutinee, forrange index/len) // that need to be unique per use site but are never referenced by user // code. Increments labelseq so the same source position lines up with // C cgen's labelseq stream. fn mkscratchname(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; buf[i] = 46u8; i += 1; // '.' let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; 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; }; // ---- 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_". let buf: [32]u8; buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; }; 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; }; // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float // types are handled separately by letfloatprim — they need MOVSS/MOVSD // and use 4-byte (f32) or 8-byte (f64) slots. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; 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, "u8")) { return true; }; if (streq(nm, "u16")) { return true; }; if (streq(nm, "u32")) { return true; }; if (streq(nm, "u64")) { return true; }; if (streq(nm, "int")) { return true; }; if (streq(nm, "uint")) { return true; }; if (streq(nm, "uintptr")) { return true; }; return false; }; // letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B. // Returns the slot size or 0 if not a float type. fn letfloatprim(nm: str) i32 = { if (streq(nm, "f32")) { return 4; }; if (streq(nm, "f64")) { return 8; }; return 0; }; // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. // 4 → f32 (literal init supported) // 8 → scalar or f64 (literal init supported) // 16 → str (only zero-init / nil / "" supported) // 24 → slice (only zero-init supported) // varies → struct (zero-init only; field reads/scalar-field writes) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; if (t.kind == nkind.N_TSLICE) { return 24; }; if (t.kind == nkind.N_TARRAY) { let lenn: *node = t.rhs; let elemn: *node = t.lhs; let alen: i32 = 1; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; }; }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; return alen * esz; }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; let fsz: i32 = letfloatprim(nm); if (fsz > 0) { return fsz; }; if (streq(nm, "str")) { return 16; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; let next: *node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; }; return 0; }; fn collectlets(c: *cgen, file: *node) void = { c.lets = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { if (letemitsize(c, d) > 0) { let lv: *letvar = amalloc(c.a, 48u64): *letvar; lv.name = nm; lv.tnode = d.lhs; lv.lvnext = c.lets; c.lets = lv; }; }; }; d = d.next; }; }; fn isletvar(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { return true; }; lv = lv.lvnext; }; return false; }; // letvarisstr — is the named top-level let a str global? Resolves // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare // MOVQ scalar load. // letvartnode — direct lookup of a top-level let's tnode. Used by // cgindex / cgassign to detect global `[N]T` arrays and `*T` // pointers, where the addressing path needs LEAQ name(SB) (array) // or MOVQ name(SB) (pointer) and the element size from T. fn letvartnode(c: *cgen, name: str) *node = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { return lv.tnode; }; lv = lv.lvnext; }; return nil; }; fn letvarisstr(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return false; }; let nm: str = t.str; if (streq(nm, "str")) { return true; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return false; }; t = nx; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarisslice — is the named top-level let a slice global? // Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the // load sequence ends with `MOVQ 16(CX), CX` (overwrites the // address holder with the cap). Mirrors C cgen's `let_isslice`. fn letvarisslice(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; if (t == nil) { return false; }; if (t.kind == nkind.N_TSLICE) { return true; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarisfloat — slot size for a named float global, or 0 if not // a float-typed let. Walks aliases so the byte-identity contract // matches C cgen's `let_isfloat` (which resolves Type kinds). fn letvarisfloat(c: *cgen, name: str) i32 = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return 0; }; let fsz: i32 = letfloatprim(t.str); if (fsz > 0) { return fsz; }; let nx: *node = aliaslookup(c, t.str); if (nx == nil) { return 0; }; t = nx; }; return 0; }; lv = lv.lvnext; }; return 0; }; // letvarisstruct — is the named top-level let a struct global? // Struct globals use LEAQ name(SB), CX as the field-access base; the // cgdot read and cgassign write paths branch on this to skip the // frame-relative addressing they use for locals. fn letvarisstruct(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return false; }; let nm: str = t.str; if (structlookup(c, nm) != nil) { return true; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return false; }; t = nx; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarstructinfo — for a struct global, return its structinfo // so the cgdot/cgassign paths can look up fields. nil if the let // isn't a struct (or wasn't found). fn letvarstructinfo(c: *cgen, name: str) *structinfo = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return nil; }; let nm: str = t.str; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return nil; }; t = nx; }; return nil; }; lv = lv.lvnext; }; return nil; }; // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { if (b == 34u8) { emitline("\\\""); return; }; if (b == 92u8) { emitline("\\\\"); return; }; 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); return; }; 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); return; }; let bb: [1]u8; bb[0] = b; os.write(1, bb.ptr, 1u64); }; // letpreintern — intern strlits referenced from top-level str-let // initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c // let_pre_intern: emitletdataw later looks up the same label, and // emitdatasection emits the DATA row in the same .s file. Running // emitletdataw after emitdatasection would flip the (DATA strlits, // DATAW lets) section order and break byte-identity. export fn letpreintern(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let sz: i32 = letemitsize(c, d); if (sz == 16) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_STRLIT) { if (r.str.len > 0) { internstrlit(c, r.str); }; }; }; }; }; d = d.next; }; }; // emitletdataw — DATAW directive per top-level `let` global. // 8B scalar with int/rune/bool/nil literal init (or no init). // 16B str — no init / `nil` / `""` → 16 zero bytes; or non-empty // strlit init → 8 zero placeholder + 8 LE len bytes plus a // DATAR slot+0,strlit reloc that the linker patches at load. // sz struct — zero only. // Non-literal scalar inits and unsupported shapes are skipped so the // link surfaces an undefined-symbol error if the binding is used. fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { // Float global: 4B (f32) or 8B (f64). // Two init shapes: // - no rhs: emit fsz zero bytes // - N_FLOATLIT: bake the IEEE bits the // parser stashed in r.uval (lexer // bit-casts t.fval into t.uval). f32 // emits the low 4 bytes; f64 emits 8. let bits: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; ok = false; if (r != nil) { if (r.kind == nkind.N_FLOATLIT) { bits = r.uval; ok = true; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let nb: u64 = bits; for (i < fsz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; i += 1; }; emitline("\"\n"); }; }; // Skip the scalar 8B path when the global is a // fixed-size array that just happens to sum to 8 // bytes (e.g. [4]u16, [8]u8) — the array path // below handles it and the duplicate DATAW would // otherwise differ across stages on user code. let isarr8: bool = false; if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; }; }; if (sz == 8 && !issg && fsz == 0 && !isarr8) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; ok = false; if (r != nil) { if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; }; if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; }; if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; }; if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; }; if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; emitdatawbyte(b); i += 1; }; emitline("\"\n"); }; }; if (sz == 16 && !issg) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; // str-literal init (non-empty): emit // the 16B payload as 8 placeholder zero // bytes + 8 LE bytes of length, then a // DATAR reloc to patch the ptr half with // the strlit's runtime VA. let strlitinit: bool = false; if (r != nil) { if (r.kind == nkind.N_STRLIT) { if (r.str.len > 0) { strlitinit = true; }; }; }; if (strlitinit) { let lab: str = internstrlit(c, r.str); let v: u64 = r.str.len: u64; emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; i = 0; let nv: u64 = v; for (i < 8) { emitdatawbyte((nv & 255u64): u8); nv = nv >> 8u64; i += 1; }; emitline("\"\n"); emitline("DATAR "); emitsymname(c, nm); emitline("+0(SB),"); os.write(1, lab.ptr, lab.len: u64); emitline("(SB)\n"); } else { // zero-init: accept no rhs, nil, // or empty strlit. let ok: bool = true; if (d.rhs != nil) { ok = false; if (r != nil) { if (r.kind == nkind.N_NIL) { ok = true; }; if (r.kind == nkind.N_STRLIT) { if (r.str.len == 0) { ok = true; }; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < 16) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; }; if (sz == 24 && !issg) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at // link. let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; ok = false; if (r != nil) { if (r.kind == nkind.N_NIL) { ok = true; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < 24) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; // Struct globals — any size, zero-init only. // A struct literal init isn't compile-time // evaluated yet; skip and the link will surface // an undefined-symbol error if referenced. if (issg) { if (d.rhs == nil) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < sz) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; // Top-level `[N]T = [a, b, ...]` array global. // Emits N*esz bytes with each element's bytes // little-endian for the declared primitive width. // Without this, `let arr: [N]T = ...` references // from function bodies link-fail with `undefined // reference to arr`, and bare-name addressing // (LEAQ arr(SB)) inside cgindex / cgassign has no // symbol to bind to. if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { let elemn: *node = d.lhs.lhs; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; let total: i32 = sz; let alen: i32 = total / esz; let elems: *node = nil; if (d.rhs != nil) { if (d.rhs.kind == nkind.N_ARRLIT) { elems = d.rhs.list; }; }; emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let e: *node = elems; let fillv: u64 = 0u64; let inrepeat: bool = false; for (i < alen) { let v: u64 = fillv; if (!inrepeat && e != nil) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { // `..., ...` repeat marker: prior v stays. inrepeat = true; } else { if (e.lhs != nil) { if (e.lhs.kind == nkind.N_INTLIT) { v = e.lhs.uval; }; if (e.lhs.kind == nkind.N_RUNELIT) { v = e.lhs.uval; }; }; fillv = v; e = e.next; }; } else { if (e.kind == nkind.N_INTLIT) { v = e.uval; }; if (e.kind == nkind.N_RUNELIT) { v = e.uval; }; fillv = v; e = e.next; }; }; let nb: u64 = v; let b: i32 = 0; for (b < esz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; b += 1; }; i += 1; }; emitline("\"\n"); }; }; }; }; d = d.next; }; }; // emitdefconstants — DATA directive per top-level fold-to-literal // `def`. 8 bytes little-endian to match what the C cgen emits. // foldintliteral gates: int/rune literal, true/false/nil, and a // unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as // N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the // gate is for. fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { let r: *node = d.rhs; let v: u64 = 0u64; let ok: bool = false; if (r != nil) { ok = foldintliteral(r, &v); }; 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, params: *node, frnext: *fnret, }; fn collectfnrets(c: *cgen, file: *node) void = { c.fnrets = nil; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { let f: *fnret = amalloc(c.a, 48u64): *fnret; f.fname = d.str; f.rtype = d.lhs; f.params = d.list; 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; }; // fnparamslookup — head of the declared param-list for a fn, or nil // if the name isn't a registered fn. Used by cgcall / pushargsrev to // detect implicit widening from a concrete variant into a tagged-union // parameter slot. fn fnparamslookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { return f.params; }; 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 nkind.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 == nkind.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 -------------------------------------- // // Every non-FFI top-level fn decl lives in its module's namespace — // cgen mangles the leaf to `.` at the def site (TEXT) // and at every call/load site, so cross-module same-leaf fns (lib/os // `read` vs lib/io `read`, both exported) coexist at link time. // Non-fn decls (let/def/type) stick to the older "non-exported only" // rule: their export-side namespace is the user-facing data ABI and // mangling them changes the surface. FFI-bound decls (@symbol) keep // their explicit C symbol regardless of kind. // // Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` // for fns. Both stages must match exactly — ww2/ww3/ww4 byte-identity // depends on it. 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 == nkind.N_FNDECL) { // Fns mangle regardless of export status — covers // lib/os.read vs lib/io.read collision. if (d.module.len > 0) { let isffi: bool = false; let a: *node = d.attr; for (a != nil) { if (a.kind == nkind.N_ATTR) { let an: str = a.str; if (streq(an, "symbol")) { isffi = true; }; }; a = a.next; }; if (!isffi) { 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 == nkind.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 == nkind.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 == nkind.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; }; // modlookupforfn — hint-aware lookup for fn names. Walks c.mods // preferring entries where module matches `hint`; falls back to the // first leaf-name match when nothing matches the hint (legacy single- // owner shape, also covers lookups with hint.len==0). Needed because // multiple modules can now register the same fn leaf — bare `lookup` // would otherwise grab whichever module was prepended last. fn modlookupforfn(c: *cgen, name: str, hint: str) str = { let m: *modent = c.mods; let first: str; first.ptr = nil; first.len = 0; for (m != nil) { if (streq(m.mname, name)) { if (hint.len > 0 && m.module.len > 0 && streq(m.module, hint)) { return m.module; }; if (first.len == 0 && first.ptr == nil) { first = m.module; }; }; m = m.mnext; }; return first; }; // 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 non-fn name is emitted before `(SB)` // — DATA labels for top-level lets/defs, address-of-let, etc. Fn names // (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates // cross-module same-leaf fn exports. 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); }; // emitfnname — write the asm symbol name for a fn `ident`, threading // `hint` (the explicit module from a `mod.fn` use site, or c.curmod // for bare-IDENT calls) through modlookupforfn. Same FFI override // semantics as emitsymname; same dot-separator format. Use at every // CALL / LEAQ-of-fn / TEXT-def site. fn emitfnname(c: *cgen, ident: str, hint: str) void = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { os.write(1, resolved.ptr, resolved.len: u64); return; }; let mod: str = modlookupforfn(c, ident, hint); 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 == nkind.N_FNDECL) { let a: *node = d.attr; for (a != nil) { if (a.kind == nkind.N_ATTR) { let aname: str = a.str; if (streq(aname, "symbol")) { let symnode: *node = a.list; if (symnode != nil) { if (symnode.kind == nkind.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 "?"; }; // fargregname — XMM scalar-float arg registers (SysV: X0..X7). // Parallel to argregname / sysv_argregs; float args advance their // own counter so int and float arg slots don't conflict. export fn fargregname(i: i32) str = { if (i == 0) { return "X0"; }; if (i == 1) { return "X1"; }; if (i == 2) { return "X2"; }; if (i == 3) { return "X3"; }; if (i == 4) { return "X4"; }; if (i == 5) { return "X5"; }; if (i == 6) { return "X6"; }; if (i == 7) { return "X7"; }; 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 == tkind.TK_EOF) { break; }; if (t.kind == tkind.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) // ": / resolved" os.write(1, argstr(path).ptr, argstrlen(path): u64); os.write(1, ": ".ptr, 2u64); let rs: str = strconv.i64tos(ck.nresolved: i64, strconv.base.DEC); os.write(1, rs.ptr, rs.len: u64); os.write(1, "/".ptr, 1u64); let total: i32 = ck.nresolved + ck.nunresolved; let ts: str = strconv.i64tos(total: i64, strconv.base.DEC); os.write(1, ts.ptr, ts.len: 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; };