Files
ww/selfhost/cmd/w6c/main.combined.ww

21678 lines
684 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// time — clocks, instants, durations. Mirrors Hare's lib/time
// (ref/hare/time/duration.ha, instant.ha, arithm.ha,
// +linux/functions.ha). Calendar / date / strftime / timezone /
// sleep live in separate Hare modules and graduate when callers /
// supporting stdlib arrive.
//
// `duration` is a NAMED alias of i64 (lib/math/random precedent
// at lib/math/random/random.ww:8); ww treats NAMED as a newtype,
// so cross-i64 arithmetic inside this module needs explicit casts.
// Hare's structural alias semantics let those casts vanish, but
// our type checker is strict.
package time;
@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64;
@symbol("rt_abort") fn abort(msg: str) void;
def SYS_CLOCK_GETTIME: i64 = 228;
// ref/hare/time/duration.ha:6. 290y representable range.
export type duration = i64;
// ref/hare/time/duration.ha:9-18. Plan-9 naming (lowercase)
// diverges from Hare's uppercase per project rule 4.
export def nanosecond: duration = 1i64;
export def microsecond: duration = 1000i64;
export def millisecond: duration = 1000000i64;
export def second: duration = 1000000000i64;
// ref/hare/time/instant.ha:9. (sec, nsec) pair — NOT POSIX struct
// timespec (which uses u32 nsec). Layout matches Linux's struct
// timespec on 64-bit (i64+i64) so we can pass &instant directly
// to clock_gettime.
export type instant = struct {
sec: i64,
nsec: i64,
};
// ref/hare/time/+linux/functions.ha:84. First cut exposes only
// realtime and monotonic; Hare's process_cpu / thread_cpu / boot /
// realtime_alarm / boot_alarm / tai graduate when a caller needs
// them (CLAUDE.md rule 9 — Hare-fidelity, no premature surface).
export type clock = enum i32 {
realtime = 0,
monotonic = 1,
};
// ref/hare/time/+linux/functions.ha:138. Hare's now() also aborts
// on impossible errnos. (instant | oserror) is deliberately not
// the return shape — EINVAL / EFAULT are programmer errors (bad
// clock id, bad ptr), and a 1-word-payload sum return walks into
// task #9's cgen-divergence trap.
export fn now(c: clock) instant = {
let i: instant;
let rc = syscall2(SYS_CLOCK_GETTIME, (c as i32): i64, (&i): i64);
if (rc != 0i64) { abort("time.now: clock_gettime failed"); };
return i;
};
// ref/hare/time/arithm.ha:9. Adds duration to instant. The
// negative-duration branch normalises nsec into [0, second).
export fn add(i: instant, x: duration) instant = {
let r: instant;
let xi: i64 = x: i64;
let sec: i64 = second: i64;
let nsec: i64 = nanosecond: i64;
if (xi == 0i64) {
r.sec = i.sec;
r.nsec = i.nsec;
return r;
};
if (xi > 0i64) {
r.sec = i.sec + (i.nsec + xi) / sec;
r.nsec = (i.nsec + xi) % sec;
return r;
};
r.sec = i.sec + (i.nsec + xi - sec + nsec) / sec;
r.nsec = (i.nsec + (xi % sec) + sec) % sec;
return r;
};
// ref/hare/time/arithm.ha:26. Returns duration from a to b.
// Sign convention: b - a.
export fn diff(a: instant, b: instant) duration = {
let sec: i64 = second: i64;
let v: i64 = ((b.sec - a.sec) * sec) + (b.nsec - a.nsec);
return v: duration;
};
// ref/hare/time/arithm.ha:32. -1 if a < b, 0 if equal, +1 if a > b.
export fn compare(a: instant, b: instant) i8 = {
if (a.sec < b.sec) { return -1i8; };
if (a.sec > b.sec) { return 1i8; };
if (a.nsec < b.nsec) { return -1i8; };
if (a.nsec > b.nsec) { return 1i8; };
return 0i8;
};
// 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.
package os;
import time;
@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 <fcntl.h>. Hare names them
// `fs::flag::RDONLY` etc; we use the same leaf names so callers say
// `os.flag.RDONLY` and `os.flag.WRONLY | os.flag.CREATE`.
export type flag = enum i32 {
RDONLY = 0,
WRONLY = 1,
RDWR = 2,
CREATE = 64, // 0x40
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);
};
// PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/
// syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str |
// []u8 | *const u8)`; ww's lib/os public surface narrows to `str`
// (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc).
// Internally, [[kpath]] copies the `str` bytes into a single
// module-level [[pathbuf]] scratch slot and NUL-terminates so the
// raw Linux syscalls (which require C strings) see a valid
// terminator. Same precedent as Hare's static `pathbuf`.
//
// Non-reentrant: one buffer, every [[stat]] / [[open]] / etc.
// rewrites it. Same caveat as strconv's `*tos` family (overwritten
// on next call). Caller must NOT hold a kpath-returned pointer
// across another lib/os path call. Graduates when ww grows a
// thread story.
//
// `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates
// 1-word-payload tagged returns to 24B (cstage emits 16B).
// Task #9; revert at task #10 when fixed. Repro at
// .ai/probe_tagged_return_pointer_payload.ww.
export def PATH_MAX: i32 = 4096;
let pathbuf: [4096]u8;
fn kpath(p: str) *u8 = {
if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG
let i: i32 = 0;
for (i < p.len) { pathbuf[i] = p[i]; i += 1; };
pathbuf[p.len] = 0u8;
return &pathbuf[0];
};
// 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). Returns -errno on failure, fd otherwise.
// Higher-level callers prefer `tryopen`. Mirrors Hare's os::open
// (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf.
// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX.
export fn open(path: str, flags: flag, mode: i32) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG
return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32;
};
export fn tryopen(path: str, 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 <unistd.h> (F_OK=0).
// Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access).
// Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX.
export fn access(path: str, mode: i32) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; };
return syscall2(nr.ACCESS, p: i64, mode: i64): i32;
};
// remove — unlink(2). Mirrors Hare's os::remove
// (ref/hare/os/os.ha:12).
export fn remove(path: str) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; };
return syscall1(nr.UNLINK, p: i64): i32;
};
// mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700).
// Returns 0 on success, negative errno otherwise. Mirrors Hare's
// os::mkdir (ref/hare/os/os.ha:50).
export fn mkdir(path: str, mode: i32) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; };
return syscall2(nr.MKDIR, p: i64, mode: i64): i32;
};
// rmdir — rmdir(2). Mirrors Hare's os::rmdir
// (ref/hare/os/os.ha:58).
export fn rmdir(path: str) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; };
return syscall1(nr.RMDIR, p: 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`.
//
// Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place
// '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly
// instead of recursing through [[mkdir]] — re-entering kpath would
// clobber the buffer mid-walk (single static slot, see kpath's
// non-reentrancy note above).
export fn mkdirs(path: str, mode: i32) (void | oserror) = {
let cp: *u8 = kpath(path);
if (cp == nil: *u8) { return -36i64: oserror; };
let n: i32 = path.len;
if (n == 0) { return; };
// Walk forward; at each '/' boundary, NUL-terminate the prefix,
// raw MKDIR syscall on pathbuf, 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 (pathbuf[i] == 47u8) { // '/'
pathbuf[i] = 0u8;
let r: i32 = syscall2(nr.MKDIR,
(&pathbuf[0]): i64, mode: i64): i32;
pathbuf[i] = 47u8;
if (r < 0) {
if (r != -17) { return r: i64: oserror; };
};
};
i += 1;
};
let r: i32 = syscall2(nr.MKDIR,
(&pathbuf[0]): i64, mode: i64): i32;
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. Mirrors Hare's
// os::exec::exec path arg (str). argv/envp stay `**u8` — the
// kernel takes a NUL-pointer-terminated table of NUL-terminated
// C strings, a different shape from a path.
export fn execve(path: str, argv: **u8, envp: **u8) i32 = {
let p: *u8 = kpath(path);
if (p == nil: *u8) { return -36i32; };
return syscall3(nr.EXECVE, p: 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 <linux/fcntl.h>.
// 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,
};
// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80
// bytes. Times are time.instant (ref/hare/time/instant.ha:9) — the
// canonical Hare shape. 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: time.instant, // 32 (16)
mtime: time.instant, // 48 (16)
ctime: time.instant, // 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]].
type kstat = struct {
dev: u64, // 0
ino: u64, // 8
nlink: u64, // 16
mode: u32, // 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: 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.
// Returns ENAMETOOLONG (-36) as `oserror` if the path overflows
// PATH_MAX.
//
// 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: str) (void | oserror) = {
let cp: *u8 = kpath(path);
if (cp == nil: *u8) { return -36i64: oserror; };
let k: kstat;
let r: i64 = syscall4(nr.NEWFSTATAT,
AT_FDCWD: i64, cp: 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: str) (void | oserror) = {
let cp: *u8 = kpath(path);
if (cp == nil: *u8) { return -36i64: oserror; };
let k: kstat;
let r: i64 = syscall4(nr.NEWFSTATAT,
AT_FDCWD: i64, cp: 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`. ENAMETOOLONG is
// swallowed as `false` — Hare's os::exists doc says "true if a
// node exists at the given path, or false if not."
//
// 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: str) bool = {
let cp: *u8 = kpath(path);
if (cp == nil: *u8) { return false; };
let k: kstat;
let r: i64 = syscall4(nr.NEWFSTATAT,
AT_FDCWD: i64, cp: i64, (&k): i64, 0i64);
return r >= 0i64;
};
// 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.
package wcc;
import 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;
};
};
// types — integer limits. Mirrors Hare's types::limits (I8_MAX, …)
// platform-fixed for amd64. Numeric helpers live in lib/math, matching
// Hare's split between types::limits and math::.
package types;
def I8_MAX: i8 = 127;
def I16_MAX: i16 = 32767;
def I32_MAX: i32 = 2147483647;
def I64_MAX: i64 = 9223372036854775807;
def I8_MIN: i8 = -128;
def I16_MIN: i16 = -32768;
def I32_MIN: i32 = -2147483648;
def I64_MIN: i64 = -9223372036854775808;
def U8_MAX: u8 = 255;
def U16_MAX: u16 = 65535;
def U32_MAX: u32 = 4294967295;
def U64_MAX: u64 = 18446744073709551615;
// bytes — slice operations over []u8. Mirrors Hare's bytes module
// (ref/hare/bytes/) for the in-tree subset: search/equality/prefix
// helpers used by lib/encoding, lib/bufio, lib/memio.
//
// Documented divergences from Hare:
// - index_slice / rindex_slice use naive O(n·m); Hare specialises
// 2/3/4-byte needles and falls back to two_way (Crochemore-Perrin)
// for longer (ref/hare/bytes/index.ha:61, ref/hare/bytes/two_way.ha).
// Correctness equivalent.
// - peek_token dispatches index/rindex by branching on `reverse`
// rather than a function-pointer `ifunc` (ref/hare/bytes/tokenize.ha:97).
// ww has no fn pointers in scope yet — same pattern as lib/strings
// `move`. Outwardly identical.
// - tokenize / rtokenize zero the `delim` field on the constructed
// tokenizer when `in` is empty, rather than mutating the variadic
// param before the struct write (ref/hare/bytes/tokenize.ha:26-28).
// Semantically identical; the variadic param is borrowed and
// captured-by-value into the struct, so mutating either side
// yields the same observable state.
package bytes;
import os;
import types;
// done — iteration sentinel returned by next_token / peek_token at
// end-of-input. ref/hare/bytes/tokenize.ha uses the built-in `done`
// token; ww spells it per-package the same way lib/encoding/utf8 does
// (utf8.ww:36). Plain `void` (not `!void`): continuation signal.
export type done = void;
// tokenizer — cursor over an input slice. Layout mirrors
// ref/hare/bytes/tokenize.ha:6-10. `p` is the cached peek-position;
// I64_MAX (forward) / I64_MIN (reverse) are the unprimed sentinels.
// p < 0 also identifies a reverse-direction iterator.
export type tokenizer = struct {
in: []u8,
delim: []u8,
p: i64,
};
// equal — true iff `a` and `b` have the same length and contents.
// ref/hare/bytes/equal.ha:9.
export fn equal(a: []u8, b: []u8) 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;
};
// index — first offset of `needle` in `s`. u8 needle scans for the
// byte; []u8 needle scans for the substring. void if absent.
// ref/hare/bytes/index.ha:6.
export fn index(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
let i: i32 = 0;
for (i < s.len) {
if (s[i] == c) { return i; };
i += 1;
};
return;
};
case let sub: []u8 => {
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;
};
// rindex — last offset of `needle` in `s`. Empty []u8 needle returns
// s.len (ref/hare/bytes/index.ha:103 — Hare's loop yields r-0 at i=0).
// ref/hare/bytes/index.ha:86.
export fn rindex(s: []u8, needle: (u8 | []u8)) (i32 | void) = {
match (needle) {
case let c: u8 => {
let i: i32 = s.len - 1;
for (i >= 0) {
if (s[i] == c) { return i; };
i -= 1;
};
return;
};
case let sub: []u8 => {
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;
};
// contains — true iff any of `needles` (byte or sub-slice) appears in `s`.
// ref/hare/bytes/contains.ha:6.
export fn contains(s: []u8, needles: (u8 | []u8)...) bool = {
let i: i32 = 0;
for (i < needles.len) {
match (needles[i]) {
case let b: u8 => {
match (index(s, b)) {
case let bo: i32 => return true;
case void => void;
};
};
case let n: []u8 => {
match (index(s, n)) {
case let bo: i32 => return true;
case void => void;
};
};
};
i += 1;
};
return false;
};
// hasprefix — true iff `s` starts with `pre`.
// ref/hare/bytes/contains.ha:21.
export fn hasprefix(s: []u8, pre: []u8) bool = {
if (pre.len > s.len) { return false; };
let i: i32 = 0;
for (i < pre.len) {
if (s[i] != pre[i]) { return false; };
i += 1;
};
return true;
};
// hassuffix — true iff `s` ends with `suf`.
// ref/hare/bytes/contains.ha:35.
export fn hassuffix(s: []u8, suf: []u8) 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;
};
// reverse — in-place reverse of `s`. ref/hare/bytes/reverse.ha:5.
export fn reverse(s: []u8) void = {
let i: i32 = 0;
let j: i32 = s.len - 1;
for (i < j) {
let t: u8 = s[i];
s[i] = s[j];
s[j] = t;
i += 1;
j -= 1;
};
};
// zero — set every byte of `s` to 0. ref/hare/bytes/zero.ha:5.
export fn zero(s: []u8) void = {
let i: i32 = 0;
for (i < s.len) {
s[i] = 0u8;
i += 1;
};
};
// tokenize — iterator yielding tokens from `in` separated by any byte
// in `delim`. Leading / trailing / adjacent delims yield empty tokens.
// `delim` is borrowed; caller keeps it valid for the tokenizer's
// lifetime. ref/hare/bytes/tokenize.ha:22.
export fn tokenize(in: []u8, delim: u8...) tokenizer = {
os.assert(delim.len > 0, "bytes.tokenize called with empty slice");
os.assert((in.len: i64) < types.I64_MAX,
"bytes.tokenize: input length exceeds I64_MAX");
let t: tokenizer;
t.in = in;
t.delim = delim;
if (in.len == 0) {
t.delim.len = 0;
t.delim.cap = 0;
};
t.p = types.I64_MAX;
return t;
};
// rtokenize — reverse-direction tokenize. First next_token yields the
// last token, last next_token yields the first. ref/hare/bytes/tokenize.ha:40.
export fn rtokenize(in: []u8, delim: u8...) tokenizer = {
os.assert(delim.len > 0, "bytes.rtokenize called with empty slice");
os.assert((in.len: i64) < types.I64_MAX,
"bytes.rtokenize: input length exceeds I64_MAX");
let t: tokenizer;
t.in = in;
t.delim = delim;
if (in.len == 0) {
t.delim.len = 0;
t.delim.cap = 0;
};
t.p = types.I64_MIN;
return t;
};
// peek_token — next token without advancing the cursor. Returns done
// once `s.delim` has been zeroed by a prior past-end next_token.
// ref/hare/bytes/tokenize.ha:91.
export fn peek_token(s: *tokenizer) ([]u8 | done) = {
if (s.delim.len == 0) {
let d: done; return d;
};
let reverse: bool = s.p < 0i64;
let known: bool = false;
if (reverse) {
if (s.p != types.I64_MIN) { known = true; };
} else {
if (s.p != types.I64_MAX) { known = true; };
};
if (!known) {
let i: i64 = types.I64_MAX;
if (reverse) { i = types.I64_MIN; };
let dlen: i64 = 0i64;
let slen: i64 = s.in.len: i64;
let k: i32 = 0;
for (k < s.delim.len) {
let d: u8 = s.delim[k];
let ix_found: bool = false;
let ix_val: i32 = 0;
if (reverse) {
match (rindex(s.in, d)) {
case let v: i32 => { ix_found = true; ix_val = v; };
case void => void;
};
} else {
match (index(s.in, d)) {
case let v: i32 => { ix_found = true; ix_val = v; };
case void => void;
};
};
if (ix_found) {
if (!reverse) {
if ((ix_val: i64) < i) { i = ix_val: i64; dlen = 1i64; };
} else {
if ((ix_val: i64) > i) { i = ix_val: i64; dlen = 1i64; };
};
} else {
if (!reverse) {
if (slen < i) { i = slen; };
} else {
if (0i64 > i) { i = 0i64; };
};
};
k += 1;
};
if (reverse) {
if (i == slen) {
s.p = -(slen + 1i64);
} else {
s.p = i + dlen - slen - 1i64;
};
} else {
s.p = i;
};
};
let r: []u8;
if (reverse) {
let start: i32 = (s.in.len: i64 + s.p + 1i64): i32;
r.ptr = s.in.ptr + (start: u64);
r.len = s.in.len - start;
r.cap = r.len;
} else {
let end: i32 = s.p: i32;
r.ptr = s.in.ptr;
r.len = end;
r.cap = end;
};
return r;
};
// next_token — current token, then advance past it and the delim.
// Once the input is exhausted, returns done and zeros `s.delim` so
// subsequent peeks short-circuit. ref/hare/bytes/tokenize.ha:59.
export fn next_token(s: *tokenizer) ([]u8 | done) = {
let b: []u8;
match (peek_token(s)) {
case let v: []u8 => { b = v; };
case done => { let d: done; return d; };
};
let slen: i64 = s.in.len: i64;
let reverse: bool = s.p < 0i64;
if (reverse) {
if (slen + s.p + 1i64 == 0i64) {
s.delim.len = 0;
s.delim.cap = 0;
s.in.len = 0;
s.in.cap = 0;
} else {
let end: i32 = (slen + s.p + 1i64 - 1i64): i32;
s.in.len = end;
s.in.cap = end;
};
s.p = types.I64_MIN;
} else {
if (s.p == slen) {
s.delim.len = 0;
s.delim.cap = 0;
s.in.len = 0;
s.in.cap = 0;
} else {
let adv: u64 = (s.p: u64) + 1u64;
let adv_i32: i32 = (s.p: i32) + 1;
s.in.ptr = s.in.ptr + adv;
s.in.len = s.in.len - adv_i32;
s.in.cap = s.in.cap - adv_i32;
};
s.p = types.I64_MAX;
};
return b;
};
// remaining_tokens — the unconsumed portion of `s.in`. Read-only view.
// ref/hare/bytes/tokenize.ha:145.
export fn remaining_tokens(s: *tokenizer) []u8 = {
return s.in;
};
// rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. We bind it directly because the builtin's
// expansion stores only 8 bytes of the new element (cgen emits a
// single MOVQ), losing the .len/.cap fields of a []u8 element (24B).
// Mirrors the same workaround in lib/shlex.shlex (appendstr, 16B) and
// lib/getopt.getopt (appendoption, 24B); collapses in one go when the
// append builtin learns to store the full element width.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// appendslice — grow `*slice` by one and store `item` (24B). Mirror
// of [[shlex.appendstr]] / [[getopt.appendoption]]. Bypasses the
// `append` builtin's first-8B-only-store gap for a slice-element.
fn appendslice(slice: *[][]u8, item: []u8) void = {
let newlen: i32 = slice.len + 1;
slice.len = newlen;
rtensure(slice: *void, 24u64);
let dst: *[]u8 = &slice.ptr[newlen - 1];
dst.ptr = item.ptr;
dst.len = item.len;
dst.cap = item.cap;
};
// splitn — split `in` on any byte in `delim`, returning up to `n`
// tokens via forward iteration. The trailing slot (when more than
// `n - 1` tokens exist) holds the unconsumed remainder.
//
// The caller frees the returned slice via
// `os.free(r.ptr: *void, (r.cap: u64) * 24u64)`. Element bytes are
// borrowed from `in`.
//
// Hare's `([][]u8 | nomem)` collapses to `[][]u8` here: ww os.alloc
// has no recoverable failure path. Same precedent as
// shlex.split / getopt.tryparse.
//
// ref/hare/bytes/tokenize.ha:156.
export fn splitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
os.assert(delim.len > 0,
"bytes.splitn must not be called with an empty delimiter");
let toks: [][]u8;
toks.ptr = nil: *[]u8;
toks.len = 0;
toks.cap = 0;
let tok: tokenizer = tokenize(in, delim...);
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: []u8 => { appendslice(&toks, s); };
case done => { return toks; };
};
i += 1;
};
match (peek_token(&tok)) {
case done => void;
case let pk: []u8 => {
let r: []u8 = remaining_tokens(&tok);
appendslice(&toks, r);
};
};
return toks;
};
// rsplitn — reverse-direction counterpart to [[splitn]]: tokens are
// collected from the end of `in`. The trailing slot holds the
// unconsumed prefix (everything before the n-th-from-last delim hit).
//
// When the input has fewer than n tokens, the `done` short-circuit
// returns toks UN-reversed (in last-token-first order). Mirrors Hare
// at ref/hare/bytes/tokenize.ha:196-199 where the in-place reverse
// step is gated behind the n-1 loop running to completion. Only the
// "loop ran to completion AND peek saw a remainder" path applies the
// reverse; both early-exit paths skip it.
//
// ref/hare/bytes/tokenize.ha:186.
export fn rsplitn(in: []u8, delim: []u8, n: i32) [][]u8 = {
os.assert(delim.len > 0,
"bytes.rsplitn called with empty delimiter");
let toks: [][]u8;
toks.ptr = nil: *[]u8;
toks.len = 0;
toks.cap = 0;
let tok: tokenizer = rtokenize(in, delim...);
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: []u8 => { appendslice(&toks, s); };
case done => { return toks; };
};
i += 1;
};
match (peek_token(&tok)) {
case done => void;
case let pk: []u8 => {
let r: []u8 = remaining_tokens(&tok);
appendslice(&toks, r);
};
};
// In-place reverse so callers see argv-order, matching Hare
// (ref/hare/bytes/tokenize.ha:207). Element copy is field-wise
// through `*[]u8` because `toks[i] = toks[j]` (full 24B slice
// store) lands in the multi-word-store gap noted at
// cmd/w6c/cgen.c:6515-6523.
let a: i32 = 0;
let b: i32 = toks.len - 1;
for (a < b) {
let pa: *[]u8 = &toks.ptr[a];
let pb: *[]u8 = &toks.ptr[b];
let tp: *u8 = pa.ptr;
let tl: i32 = pa.len;
let tc: i32 = pa.cap;
pa.ptr = pb.ptr;
pa.len = pb.len;
pa.cap = pb.cap;
pb.ptr = tp;
pb.len = tl;
pb.cap = tc;
a += 1;
b -= 1;
};
return toks;
};
// split — full split of `in` on `delim` (no token cap). Mirrors
// `splitn(in, delim, types::SIZE_MAX)`. ww uses `types.I32_MAX`
// because the index type is i32 (lib/CLAUDE.md).
//
// ref/hare/bytes/tokenize.ha:225.
export fn split(in: []u8, delim: []u8) [][]u8 = {
return splitn(in, delim, types.I32_MAX);
};
// encoding/utf8 — UTF-8 encode/decode. Hare port; see
// ref/hare/encoding/utf8/{types,rune,encode,decode,decodetable}.ha.
//
// The decoder is Hoehrmann's branchless DFA, originally published
// at <https://bjoern.hoehrmann.de/utf-8/decoder/dfa/>. Hare's
// ref/hare/encoding/utf8/decodetable.ha:4 restructures Hoehrmann's
// flat table to 2D `[8][256]i8`; we flatten back to 1D `[2048]i8`
// because ww cgen does not yet ship 2D arrays (task #20).
//
// Surface deviation from ref/hare/encoding/utf8:
//
// - `encoderune` takes a caller-supplied `out: []u8` and returns
// the byte count. Hare returns a slice into a `static let buf`;
// the caller-buffer form mirrors lib/encoding/hex.encode and
// skips the static-buffer/slice-return pair.
//
// Deferred (no in-tree caller, follow-up tasks): `appendrune`,
// `strencode`, `strdecode`. Hare's string-iteration surface
// (`strings::iterator`/`strings::next` — ref/hare/strings/iter.ha)
// lives under lib/strings, not here.
// ref/hare/encoding/utf8/types.ha:6 — incomplete trailing sequence.
// Plain `void` (not `!void`): a truncated tail is a control-flow
// signal, not an error caller can ignore.
package utf8;
export type more = void;
// ref/hare/encoding/utf8/types.ha:9 — invalid UTF-8 sequence.
export type invalid = !void;
// `done` is not a built-in singleton in ww (Hare ships it as part of
// the type system). Plain `void` (not `!void`): end-of-input is a
// continuation signal, not an error. lib/io spells its EOF the same
// way (lib/io/io.ww:8-11).
export type done = void;
// ref/hare/encoding/utf8/decodetable.ha:4 — Hoehrmann's UTF-8 DFA,
// flat 1D `[2048]i8`. Layout: dfa[state*256 + byte] gives the next
// state (>0), the accept transition (0 — emit rune), or invalid (-1).
// Values match ref/hare/encoding/utf8/decodetable.ha verbatim.
let dfa: [2048]i8 = [
// state 0 — initial byte: ASCII accepts (0), continuation/illegal
// byte rejects (-1), legal multibyte start emits a state.
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
3i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 4i8, 2i8, 2i8,
5i8, 6i8, 6i8, 6i8, 7i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 1 — expecting one continuation byte (0x80..0xBF).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8, 0i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 2 — expecting one continuation byte (full 0x80..0xBF range).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 3 — first byte was 0xE0; continuation byte must be 0xA0..0xBF
// (rejects overlong 3-byte encodings).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 4 — first byte was 0xED; continuation byte must be 0x80..0x9F
// (rejects UTF-16 surrogate codepoints U+D800..U+DFFF).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8, 1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 5 — first byte was 0xF0; continuation byte must be 0x90..0xBF
// (rejects overlong 4-byte encodings).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 6 — middle continuation byte of a 4-byte sequence (0x80..0xBF).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
// state 7 — first byte was 0xF4; continuation byte must be 0x80..0x8F
// (rejects codepoints above U+10FFFF).
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8, 2i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
-1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8, -1i8,
];
// ref/hare/encoding/utf8/decode.ha:17 — payload-bit masks. Hare's
// [2][8]u8 flattened to 1D [16]u8; row 0 (offsets 0..7) is the
// continuation-byte mask (always 0x3F), row 1 (offsets 8..15) is the
// initial-byte payload mask indexed by the transition class.
let masks: [16]u8 = [
0x3fu8, 0x3fu8, 0x3fu8, 0x3fu8, 0x3fu8, 0x3fu8, 0x3fu8, 0x3fu8,
0x7fu8, 0x1fu8, 0x0fu8, 0x0fu8, 0x0fu8, 0x07u8, 0x07u8, 0x07u8,
];
// ref/hare/encoding/utf8/decode.ha:6 — incremental decoder state.
export type decoder = struct {
offs: i32,
src: []u8,
};
// ref/hare/encoding/utf8/decode.ha:12.
export fn decode(src: []u8) decoder = {
let d: decoder;
d.src = src;
d.offs = 0;
return d;
};
// ref/hare/encoding/utf8/decode.ha:27. Returns the next rune from a
// decoder, `done` at end-of-input, `more` on truncated trailing
// sequence, `invalid` on malformed input (overlong, surrogate,
// out-of-range, bad continuation).
//
// Algorithm is verbatim Hoehrmann (see file header). One structural
// rewrite: Hare encodes the "initial vs continuation byte" decision
// as the branchless `(state - 1): uint >> 31`, which assumes a 32-bit
// uint. ww's uint is 64-bit (cmd/wcc/type.c:58), so the shift answer
// would be 0x1_ffff_ffff rather than 1. We spell the same predicate
// with an explicit conditional.
export fn next(d: *decoder) (rune | done | more | invalid) = {
if (d.offs == d.src.len) {
let dn: done; return dn;
};
let nx: i32 = 0;
let state: i32 = 0;
let r: u32 = 0u32;
for (d.offs < d.src.len) {
let b: u8 = d.src[d.offs];
let bi: i32 = b: i32;
let row: i32 = state * 256 + bi;
let cell: i8 = dfa[row];
nx = cell: i32;
let mi: i32 = 0;
if (state == 0) { mi = 1; };
let m: u8 = masks[mi * 8 + (nx & 7)];
r = (r << 6u32) | ((b & m): u32);
if (nx <= 0) {
d.offs += 1;
if (nx == 0) { return r: rune; };
let e: invalid; return e;
};
state = nx;
d.offs += 1;
};
let mr: more; return mr;
};
// ref/hare/encoding/utf8/decode.ha:207. Strict whole-input check.
// The hot path: tight DFA loop, no rune assembly. Bails the moment
// the table returns -1 so malformed inputs don't pay for the rest
// of the buffer.
export fn validate(src: []u8) (void | invalid) = {
let state: i32 = 0;
let i: i32 = 0;
for (i < src.len) {
if (state < 0) { break; };
let bi: i32 = src[i]: i32;
let cell: i8 = dfa[state * 256 + bi];
state = cell: i32;
i += 1;
};
if (state == 0) { return; };
let e: invalid; return e;
};
// ref/hare/encoding/utf8/rune.ha:5. Encoded byte length of `r` as
// UTF-8. Callers in ww use this to size the buffer they hand to
// [[encoderune]]; values >0x10FFFF or negative are not legal Unicode
// codepoints and Hare aborts on them in `encoderune` itself, so we
// keep `runesz` infallible (matches Hare).
export fn runesz(r: rune) i32 = {
let ch: u32 = r: u32;
if (ch < 128u32) { return 1; };
if (ch < 2048u32) { return 2; };
if (ch < 65536u32) { return 3; };
return 4;
};
// ref/hare/encoding/utf8/rune.ha:15. Expected byte length of the
// codepoint that starts with `c`, or `invalid` if `c` cannot start
// a legal UTF-8 sequence. Constants written in decimal because ww
// doesn't accept Hare's `0b1000_0000` binary syntax: 0x80=128,
// 0xC2=194, 0xE0=224, 0xF0=240, 0xF8=248.
export fn utf8sz(c: u8) (i32 | invalid) = {
if (c < 128u8) { return 1; };
if (c < 194u8) { let e: invalid; return e; };
if (c >= 248u8) { let e: invalid; return e; };
if (c < 224u8) { return 2; };
if (c < 240u8) { return 3; };
return 4;
};
// ref/hare/encoding/utf8/encode.ha:7. Encode `r` into `out` (caller-
// supplied; must hold at least [[runesz]](r) bytes) and return the
// byte count. ABORT if `r` is a UTF-16 surrogate or above U+10FFFF —
// same precondition Hare asserts at ref/hare/encoding/utf8/encode.ha:9.
//
// Surface deviation: Hare returns `[]u8` (slice into a static buf).
// ww uses the caller-buffer form (matches lib/encoding/hex.encode);
// caller can reuse a [4]u8 stack scratch across encodes.
export fn encoderune(out: []u8, r: rune) i32 = {
let ch: u32 = r: u32;
if (ch >= 0xD800u32) {
if (ch <= 0xDFFFu32) {
abort("utf8.encoderune: surrogate codepoint");
};
};
if (ch > 0x10FFFFu32) {
abort("utf8.encoderune: codepoint > U+10FFFF");
};
let n: i32 = 0;
let first: u8 = 0u8;
if (ch < 0x80u32) {
first = 0u8; n = 1;
} else if (ch < 0x800u32) {
first = 0xC0u8; n = 2;
} else if (ch < 0x10000u32) {
first = 0xE0u8; n = 3;
} else {
first = 0xF0u8; n = 4;
};
let v: u32 = ch;
let i: i32 = n - 1;
for (i > 0) {
out[i] = ((v: u8) & 0x3Fu8) | 0x80u8;
v = v >> 6u32;
i -= 1;
};
out[0] = (v: u8) | first;
return n;
};
// ref/hare/encoding/utf8/decode.ha:52. Walks back from `d.offs` to a
// byte that could start a codepoint (state-0 dfa cell != -1), re-decodes
// forward from there, and confirms the forward decode lands back at the
// original offset. Returns `done` at start-of-input; `invalid` if no
// initial byte appears within 4 steps (no legal UTF-8 codepoint exceeds
// 4 bytes), if the forward decode returns `more`/`invalid`, or if it
// lands at a different offset than expected. Returns `more` when the
// walk reaches byte 0 without finding any initial byte.
//
// Hare's `for (d.offs < len(d.src); d.offs -= 1)` relies on size_t
// wrap-around to exit when offs underflows past 0; ww's offs is i32,
// so we spell the same exit as `d.offs >= 0`. Hare's `defer d.offs = t`
// is inlined in each match arm — ww has no defer.
export fn prev(d: *decoder) (rune | done | more | invalid) = {
if (d.offs == 0) {
let dn: done; return dn;
};
let n: i32 = d.offs;
d.offs -= 1;
for (d.offs >= 0) {
let b: u8 = d.src[d.offs];
let bi: i32 = b: i32;
let cell: i8 = dfa[bi];
if (cell: i32 != -1) {
let t: i32 = d.offs;
match (next(d)) {
case let r: rune => {
let landed: i32 = d.offs;
d.offs = t;
if (landed != n) {
let e: invalid; return e;
};
return r;
};
case let dn: done => {
d.offs = t;
let e: invalid; return e;
};
case let m: more => {
d.offs = t;
let e: invalid; return e;
};
case let e: invalid => {
d.offs = t;
let e2: invalid; return e2;
};
};
};
if (n - d.offs == 4) {
let e: invalid; return e;
};
d.offs -= 1;
};
let mr: more; return mr;
};
// ref/hare/encoding/utf8/decode.ha:74. Borrowed view of the bytes from
// the decoder's current position to the end of its source.
export fn remaining(d: *decoder) []u8 = {
let r: []u8;
r.ptr = d.src.ptr + (d.offs: u64);
r.len = d.src.len - d.offs;
r.cap = d.src.len - d.offs;
return r;
};
// ref/hare/encoding/utf8/decode.ha:80. Borrowed view of the bytes
// between two decoders' positions. Precondition (Hare asserts both):
// the decoders share the same source, and `begin.offs <= end.offs`.
export fn slice(begin: *decoder, end: *decoder) []u8 = {
if (begin.src.ptr != end.src.ptr) {
abort("utf8.slice: decoders from different sources");
};
if (begin.offs > end.offs) {
abort("utf8.slice: begin past end");
};
let r: []u8;
r.ptr = begin.src.ptr + (begin.offs: u64);
r.len = end.offs - begin.offs;
r.cap = end.offs - begin.offs;
return r;
};
// ref/hare/encoding/utf8/decode.ha:203. Byte position of the decoder
// in its source.
export fn position(d: *decoder) i32 = {
return d.offs;
};
// strings — operations over str ({ptr,len}). Hare port; see
// ref/hare/strings/.
//
// Documented divergences from Hare:
//
// - `trim` / `ltrim` / `rtrim` 0-arg returns the input unchanged.
// Hare strips ASCII whitespace via `bytes::ltrim(input,
// whitespace...)`; that needs `lib/bytes` variadic graduation
// (future commit).
// - `byteindex` / `rbyteindex` rune arms encode via
// `utf8.encoderune`; the legacy impls scanned for `r: u8` (an
// undocumented ASCII-only restriction that silently dropped
// to the wrong byte for U+80..U+7FF and higher).
// - `dup(s: str) str` — Hare returns `(str | nomem)`. ww's
// `os.alloc` aborts on OOM (no `nomem` type), so we return plain
// `str`. Empty input returns `{nil, 0}`; Hare returns the static
// empty string — same observable result.
// - `iterator` is flattened (`offs`, `src`, `reverse` fields).
// Hare uses anonymous-embedded `utf8::decoder`
// (ref/hare/strings/iter.ha:6-9); ww has no anonymous-embed
// syntax, so `next`/`prev`/`slice` copy `offs`/`src` into a
// local `utf8.decoder` for the call (and `next`/`prev` write
// `offs` back).
// - Hare's private `move()` helper dispatches on a `forward: bool`
// using a function-pointer `let fun = if (forward) &utf8::next
// else &utf8::prev`. ww has no fn-pointers in scope yet, so the
// dispatch is a branch on `forward` selecting the call site.
package strings;
import bytes;
import encoding.utf8;
import os;
import types;
// toutf8 — borrowed []u8 view of `s`. ref/hare/strings/utf8.ha:29.
// `cap` equals `len`; the slice does not own a separate allocation.
export fn toutf8(s: str) []u8 = {
let r: []u8;
r.ptr = s.ptr;
r.len = s.len;
r.cap = s.len;
return r;
};
// fromutf8_unsafe — borrowed str view of `in`. Does not validate.
// ref/hare/strings/utf8.ha:10.
export fn fromutf8_unsafe(in: []u8) str = {
let r: str;
r.ptr = in.ptr;
r.len = in.len;
return r;
};
// compare — three-way bytewise codepoint-order comparison.
// ref/hare/strings/compare.ha:12.
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;
};
// dup — allocate a fresh copy of `s`. Caller releases with
// `os.free(r.ptr, r.len: u64)`. ref/hare/strings/dup.ha:7.
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 each element + the slice header. The natural
// disposer for any `[]str` of dup'd elements (e.g. shlex.split).
// ref/hare/strings/dup.ha:38.
//
// Empty elements (`{nil, 0}` from a zero-length dup) are skipped:
// os.free on a nil pointer at len 0 tickles the rt_free guard. The
// slice header itself is freed at `cap * 16` (one str = 16B); a
// never-grown slice (cap == 0) skips the header free.
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);
};
};
// concat — fresh allocation containing each element of `strs` in
// order. Caller releases with `os.free(r.ptr, r.len: u64)`.
// ref/hare/strings/concat.ha:5. Hare's `nomem` return is dropped:
// `os.alloc` aborts on OOM.
export fn concat(strs: str...) str = {
let total: i32 = 0;
let i: i32 = 0;
for (i < strs.len) { total += strs[i].len; i += 1; };
let r: str;
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = os.alloc(total: u64): *u8;
let off: i32 = 0;
i = 0;
for (i < strs.len) {
let j: i32 = 0;
for (j < strs[i].len) {
buf[off + j] = strs[i][j];
j += 1;
};
off += strs[i].len;
i += 1;
};
r.ptr = buf;
r.len = total;
return r;
};
// join — fresh allocation with `delim` placed between each element of
// `strs`. Caller releases with `os.free(r.ptr, r.len: u64)`.
// ref/hare/strings/concat.ha:46. Hare's `nomem` return is dropped:
// `os.alloc` aborts on OOM.
export fn join(delim: str, strs: str...) str = {
let total: i32 = 0;
let i: i32 = 0;
for (i < strs.len) {
total += strs[i].len;
if (i + 1 < strs.len) { total += delim.len; };
i += 1;
};
let r: str;
r.ptr = nil;
r.len = 0;
if (total == 0) { return r; };
let buf: *u8 = os.alloc(total: u64): *u8;
let off: i32 = 0;
i = 0;
for (i < strs.len) {
let j: i32 = 0;
for (j < strs[i].len) {
buf[off + j] = strs[i][j];
j += 1;
};
off += strs[i].len;
if (i + 1 < strs.len) {
j = 0;
for (j < delim.len) {
buf[off + j] = delim[j];
j += 1;
};
off += delim.len;
};
i += 1;
};
r.ptr = buf;
r.len = total;
return r;
};
// sub — borrowed `s[start..end]`. ref/hare/strings/sub.ha:30 is
// rune-wise; this ww form is byte-wise (no rune iterator yet, planned
// for commit 2). Clamps out-of-range silently where Hare aborts —
// retained for the existing getopt caller; will graduate when the
// rune-wise form lands.
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;
};
// runebytes — encode `r` into caller's `scratch` (must hold 4 bytes)
// and return the borrowed slice trimmed to the encoded length. Hare
// inlines the same shape at ref/hare/strings/index.ha:132.
fn runebytes(scratch: []u8, r: rune) []u8 = {
let n: i32 = utf8.encoderune(scratch, r);
let s: []u8;
s.ptr = scratch.ptr;
s.len = n;
s.cap = n;
return s;
};
// hasprefix — true iff `in` begins with `prefix`.
// ref/hare/strings/suffix.ha:8.
export fn hasprefix(in: str, prefix: (str | rune)) bool = {
let scratch: [4]u8;
let p: []u8 = match (prefix) {
case let s: str => yield toutf8(s);
case let r: rune => yield runebytes(scratch[0:4], r);
};
return bytes.hasprefix(toutf8(in), p);
};
// hassuffix — true iff `in` ends with `suff`.
// ref/hare/strings/suffix.ha:26.
export fn hassuffix(in: str, suff: (str | rune)) bool = {
let scratch: [4]u8;
let s: []u8 = match (suff) {
case let v: str => yield toutf8(v);
case let r: rune => yield runebytes(scratch[0:4], r);
};
return bytes.hassuffix(toutf8(in), s);
};
// byteindex — byte-wise offset of `needle` in `haystack`, or void if
// absent. ref/hare/strings/index.ha:127. Rune arm encodes via
// utf8.encoderune (Hare passes the encoded slice straight to
// bytes::index).
export fn byteindex(haystack: str, needle: (str | rune)) (i32 | void) = {
let scratch: [4]u8;
let n: []u8 = match (needle) {
case let s: str => yield toutf8(s);
case let r: rune => yield runebytes(scratch[0:4], r);
};
return bytes.index(toutf8(haystack), n);
};
// rbyteindex — byte-wise offset of the last `needle` in `haystack`.
// ref/hare/strings/index.ha:138.
export fn rbyteindex(haystack: str, needle: (str | rune)) (i32 | void) = {
let scratch: [4]u8;
let n: []u8 = match (needle) {
case let s: str => yield toutf8(s);
case let r: rune => yield runebytes(scratch[0:4], r);
};
return bytes.rindex(toutf8(haystack), n);
};
// index — rune-wise offset of `needle`'s first occurrence in
// `haystack`, or void if absent. ref/hare/strings/index.ha:10. The
// str-arm reuses `byteindex` for the anchor byte offset and then
// walks `iter` forward to convert byte→rune index; the rune-arm
// mirrors Hare's `index_rune` (ref/hare/strings/index.ha:31).
export fn index(haystack: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let s: str => {
match (byteindex(haystack, s)) {
case void => return;
case let bo: i32 => {
let it: iterator = iter(haystack);
let i: i32 = 0;
for (position(&it) < bo) {
match (next(&it)) {
case let r: rune => i += 1;
case utf8.done => break;
};
};
return i;
};
};
};
case let r: rune => {
let it: iterator = iter(haystack);
let i: i32 = 0;
for (true) {
match (next(&it)) {
case let n: rune => {
if (n == r) { return i; };
i += 1;
};
case utf8.done => return;
};
};
};
};
return;
};
// rindex — rune-wise offset of `needle`'s last occurrence in
// `haystack`, or void if absent. ref/hare/strings/index.ha:22. The
// str-arm reuses `rbyteindex`; the rune-arm walks forward tracking
// the most recent matching rune index (Hare's `rindex_rune` with
// `riter` returns a byte-offset value for multibyte strings, which
// disagrees with the rune-wise docstring; we keep the docstring's
// contract).
export fn rindex(haystack: str, needle: (str | rune)) (i32 | void) = {
match (needle) {
case let s: str => {
match (rbyteindex(haystack, s)) {
case void => return;
case let bo: i32 => {
let it: iterator = iter(haystack);
let i: i32 = 0;
for (position(&it) < bo) {
match (next(&it)) {
case let r: rune => i += 1;
case utf8.done => break;
};
};
return i;
};
};
};
case let r: rune => {
let it: iterator = iter(haystack);
let i: i32 = 0;
let last: i32 = -1;
for (true) {
match (next(&it)) {
case let n: rune => {
if (n == r) { last = i; };
i += 1;
};
case utf8.done => break;
};
};
if (last < 0) { return; };
return last;
};
};
return;
};
// contains — true iff any of `needles` occurs in `haystack`.
// ref/hare/strings/contains.ha:9.
export fn contains(haystack: str, needles: (str | rune)...) bool = {
let i: i32 = 0;
for (i < needles.len) {
match (needles[i]) {
case let s: str => {
match (byteindex(haystack, s)) {
case let bo: i32 => return true;
case void => void;
};
};
case let r: rune => {
match (byteindex(haystack, r)) {
case let bo: i32 => return true;
case void => void;
};
};
};
i += 1;
};
return false;
};
// trimprefix — `s` with `prefix` stripped from the front, or `s`
// unchanged if it doesn't start with `prefix`. Borrowed view.
// ref/hare/strings/trim.ha:60.
export fn trimprefix(input: str, prefix: str) str = {
if (!hasprefix(input, prefix)) { return input; };
let r: str;
r.ptr = input.ptr + (prefix.len: u64);
r.len = input.len - prefix.len;
return r;
};
// trimsuffix — symmetric. ref/hare/strings/trim.ha:69.
export fn trimsuffix(input: str, suffix: str) str = {
if (!hassuffix(input, suffix)) { return input; };
let r: str;
r.ptr = input.ptr;
r.len = input.len - suffix.len;
return r;
};
// ltrim — strip leading runes that occur in `trim`. Borrowed view.
// Empty `trim` returns input unchanged (Hare's no-rune branch strips
// ASCII whitespace via `bytes::ltrim`; needs lib/bytes variadic
// graduation). ref/hare/strings/trim.ha:11.
export fn ltrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
let it: iterator = iter(input);
for (true) {
match (next(&it)) {
case let r: rune => {
let j: i32 = 0;
let found: bool = false;
for (j < trim.len) {
if (r == trim[j]) { found = true; j = trim.len; }
else { j += 1; };
};
if (!found) {
match (prev(&it)) {
case let r2: rune => void;
case utf8.done => void;
};
break;
};
};
case utf8.done => break;
};
};
return iterstr(&it);
};
// rtrim — strip trailing runes that occur in `trim`. Borrowed view.
// ref/hare/strings/trim.ha:32.
export fn rtrim(input: str, trim: rune...) str = {
if (trim.len == 0) { return input; };
let it: iterator = riter(input);
for (true) {
match (next(&it)) {
case let r: rune => {
let j: i32 = 0;
let found: bool = false;
for (j < trim.len) {
if (r == trim[j]) { found = true; j = trim.len; }
else { j += 1; };
};
if (!found) {
match (prev(&it)) {
case let r2: rune => void;
case utf8.done => void;
};
break;
};
};
case utf8.done => break;
};
};
return iterstr(&it);
};
// trim — strip from both ends. ref/hare/strings/trim.ha:54.
export fn trim(input: str, trim: rune...) str = {
return ltrim(rtrim(input, trim...), trim...);
};
// iterator — UTF-8 rune cursor over a `str`. Layout flattens Hare's
// anonymous-embedded `utf8::decoder` (ref/hare/strings/iter.ha:6-9) to
// explicit fields. `reverse` selects walk direction: forward iterators
// (`iter`) advance through utf8.next; reverse iterators (`riter`) advance
// through utf8.prev. May be copied to save state.
export type iterator = struct {
offs: i32,
src: []u8,
reverse: bool,
};
// iter — initialize a forward iterator at the start of `src`.
// ref/hare/strings/iter.ha:24.
export fn iter(src: str) iterator = {
let r: iterator;
r.src = toutf8(src);
r.offs = 0;
r.reverse = false;
return r;
};
// riter — initialize a reverse iterator at the end of `src`. `next`
// on a reverse iterator walks back through the string.
// ref/hare/strings/iter.ha:32.
export fn riter(src: str) iterator = {
let r: iterator;
r.src = toutf8(src);
r.offs = src.len;
r.reverse = true;
return r;
};
// move — private dispatch shared by next/prev. `forward` selects
// utf8.next vs utf8.prev. Aborts on more/invalid per Hare's
// ref/hare/strings/iter.ha:51-58 ("Invalid UTF-8 string (this should
// not happen)"). Hare picks the utf8 function via a fn-pointer; ww
// branches on `forward` at each call site instead.
fn move(forward: bool, it: *iterator) (rune | utf8.done) = {
let d: utf8.decoder;
d.src = it.src;
d.offs = it.offs;
if (forward) {
match (utf8.next(&d)) {
case let r: rune => { it.offs = d.offs; return r; };
case let dn: utf8.done => return dn;
case let m: utf8.more => abort("strings.move: invalid UTF-8");
case let e: utf8.invalid => abort("strings.move: invalid UTF-8");
};
} else {
match (utf8.prev(&d)) {
case let r: rune => { it.offs = d.offs; return r; };
case let dn: utf8.done => return dn;
case let m: utf8.more => abort("strings.move: invalid UTF-8");
case let e: utf8.invalid => abort("strings.move: invalid UTF-8");
};
};
};
// next — advance the iterator one rune. Forward iterators step
// through utf8.next; reverse iterators (riter) step backward through
// utf8.prev. Returns utf8.done at end-of-walk. ref/hare/strings/iter.ha:45.
export fn next(it: *iterator) (rune | utf8.done) = {
return move(!it.reverse, it);
};
// prev — step back one rune. Dual to next: on a forward iterator
// this walks utf8.prev; on a reverse iterator (riter) it walks
// utf8.next. ref/hare/strings/iter.ha:49.
export fn prev(it: *iterator) (rune | utf8.done) = {
return move(it.reverse, it);
};
// iterstr — borrowed view of the bytes remaining in the iterator's
// walk direction. Forward iter: bytes from offs to end; reverse iter:
// bytes from start to offs. ref/hare/strings/iter.ha:63.
export fn iterstr(it: *iterator) str = {
let r: []u8;
if (it.reverse) {
r = it.src[0:it.offs];
} else {
r = it.src[it.offs:it.src.len];
};
return fromutf8_unsafe(r);
};
// slice — borrowed substring between two iterator positions.
// ref/hare/strings/iter.ha:75. Hare passes `*iterator` directly where
// `*utf8::decoder` is expected via anonymous-embed coercion; ww has
// no anonymous embed, so we reconstruct a local utf8.decoder for each
// endpoint and forward — same pattern as `move` above.
export fn slice(begin: *iterator, end: *iterator) str = {
let b: utf8.decoder;
b.src = begin.src;
b.offs = begin.offs;
let e: utf8.decoder;
e.src = end.src;
e.offs = end.offs;
return fromutf8_unsafe(utf8.slice(&b, &e));
};
// position — byte-wise offset of the iterator in its source.
// ref/hare/strings/iter.ha:82.
export fn position(it: *iterator) i32 = {
return it.offs;
};
// tokenizer — re-export of bytes.tokenizer. ref/hare/strings/tokenize.ha:7.
// First cross-module type alias in tree; needs #22's transitive
// alias-chain unwrap (cstage type_chase_named + wwstage
// structlookupchain) to walk struct fields through the chain.
export type tokenizer = bytes.tokenizer;
// tokenize — yield substrings of `s` split on any byte in `delim`.
// Leading / trailing / adjacent delims yield empty tokens. `s` and
// `delim` are borrowed; caller keeps them live for the tokenizer's
// lifetime. ref/hare/strings/tokenize.ha:32. ASCII-only delim
// asserted per Hare lines 35-37: a multibyte rune in delim would
// split on a single continuation byte and yield invalid UTF-8.
export fn tokenize(s: str, delim: str) tokenizer = {
let d: []u8 = toutf8(delim);
let i: i32 = 0;
for (i < d.len) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.tokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.tokenize(toutf8(s), d...);
};
// rtokenize — reverse-direction counterpart to [[tokenize]]. First
// next_token yields the last token, last yields the first.
// ref/hare/strings/tokenize.ha:44.
export fn rtokenize(s: str, delim: str) tokenizer = {
let d: []u8 = toutf8(delim);
let i: i32 = 0;
for (i < d.len) {
os.assert((d[i] & 0x80u8) == 0u8,
"strings.rtokenize cannot tokenize on non-ASCII delimiters");
i += 1;
};
return bytes.rtokenize(toutf8(s), d...);
};
// next_token — current token, advancing the cursor.
// ref/hare/strings/tokenize.ha:62.
export fn next_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.next_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// peek_token — current token without advancing.
// ref/hare/strings/tokenize.ha:71.
export fn peek_token(s: *tokenizer) (str | bytes.done) = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
match (bytes.peek_token(b)) {
case let v: []u8 => return fromutf8_unsafe(v);
case bytes.done => { let d: bytes.done; return d; };
};
};
// remaining_tokens — unconsumed portion of the input ahead of the
// cursor. ref/hare/strings/tokenize.ha:79.
export fn remaining_tokens(s: *tokenizer) str = {
let b: *bytes.tokenizer = s: *bytes.tokenizer;
return fromutf8_unsafe(bytes.remaining_tokens(b));
};
// rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. Direct bind for the same reason as
// lib/shlex.shlex (appendstr, 16B): the builtin's expansion stores
// only 8B of the new element, losing the `.len` half of a `str`.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// appendstr — grow `*slice` by one and store `item` (16B). Mirror of
// lib/shlex.shlex appendstr. Collapses when the append builtin learns
// to store the full element width.
fn appendstr(slice: *[]str, item: str) void = {
let newlen: i32 = slice.len + 1;
slice.len = newlen;
rtensure(slice: *void, 16u64);
let dst: *str = &slice.ptr[newlen - 1];
dst.ptr = item.ptr;
dst.len = item.len;
};
// splitn — split `in` on any byte in `delim`, returning up to `n`
// tokens via forward iteration. The trailing slot (when more than
// `n - 1` tokens exist) holds the unconsumed remainder. Strings
// within the result are borrowed from `in`.
//
// The caller frees the returned slice via
// `os.free(r.ptr: *void, (r.cap: u64) * 16u64)`.
//
// Hare's `([]str | nomem)` collapses to `[]str` here: ww os.alloc
// has no recoverable failure path. Same precedent as
// shlex.split / bytes.splitn.
//
// ref/hare/strings/tokenize.ha:172.
export fn splitn(in: str, delim: str, n: i32) []str = {
let toks: []str;
toks.ptr = nil: *str;
toks.len = 0;
toks.cap = 0;
let tok: tokenizer = tokenize(in, delim);
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: str => { appendstr(&toks, s); };
case bytes.done => { return toks; };
};
i += 1;
};
match (peek_token(&tok)) {
case bytes.done => void;
case let pk: str => {
let r: str = remaining_tokens(&tok);
appendstr(&toks, r);
};
};
return toks;
};
// rsplitn — reverse-direction counterpart to [[splitn]]: tokens are
// collected from the end of `in`. The trailing slot holds the
// unconsumed prefix (everything before the n-th-from-last delim hit).
//
// When the input has fewer than n tokens, the `done` short-circuit
// returns toks UN-reversed (in last-token-first order). Mirrors Hare
// at ref/hare/strings/tokenize.ha:219-224 where the in-place reverse
// step is gated behind the n-1 loop running to completion.
//
// ref/hare/strings/tokenize.ha:200.
export fn rsplitn(in: str, delim: str, n: i32) []str = {
let toks: []str;
toks.ptr = nil: *str;
toks.len = 0;
toks.cap = 0;
let tok: tokenizer = rtokenize(in, delim);
let i: i32 = 0;
for (i < n - 1) {
match (next_token(&tok)) {
case let s: str => { appendstr(&toks, s); };
case bytes.done => { return toks; };
};
i += 1;
};
match (peek_token(&tok)) {
case bytes.done => void;
case let pk: str => {
let r: str = remaining_tokens(&tok);
appendstr(&toks, r);
};
};
// In-place reverse so callers see argv-order, matching Hare
// (ref/hare/strings/tokenize.ha:220). Element copy is field-wise
// through `*str` because `toks[i] = toks[j]` (full 16B str store)
// lands in the multi-word-store gap noted at cmd/w6c/cgen.c:6515.
let a: i32 = 0;
let b: i32 = toks.len - 1;
for (a < b) {
let pa: *str = &toks.ptr[a];
let pb: *str = &toks.ptr[b];
let tp: *u8 = pa.ptr;
let tl: i32 = pa.len;
pa.ptr = pb.ptr;
pa.len = pb.len;
pb.ptr = tp;
pb.len = tl;
a += 1;
b -= 1;
};
return toks;
};
// split — full split of `in` on `delim` (no token cap). Mirrors
// `splitn(in, delim, types::SIZE_MAX)`. ww uses `types.I32_MAX`
// because the index type is i32 (lib/CLAUDE.md).
//
// ref/hare/strings/tokenize.ha:242.
export fn split(in: str, delim: str) []str = {
return splitn(in, delim, types.I32_MAX);
};
// 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.
package strconv;
import os;
import 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("");
};
// 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().
package lex;
import os;
import 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_MODULE = 86, // `module foo;` — directory-as-module decl
TK_LAST = 87,
};
// ---- 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, "import", n)) { return tkind.TK_USE; };
if (streqn(p, "match", n)) { return tkind.TK_MATCH; };
if (streqn(p, "nil", n)) { return tkind.TK_NIL; };
if (streqn(p, "package", n)) { return tkind.TK_MODULE; };
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, "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 "<none>"; };
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 "import"; };
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_MODULE) { return "package"; };
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 "<last>"; };
return "<?>";
};
// ---- writer for tokprint ----------------------------------------------
//
// fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style
// escapes for \, ", \n, \t, \r and \xNN for other non-printables.
fn fputcbyte(fd: i32, b: u8) void = {
let buf: [1]u8;
buf[0] = b;
os.write(fd, buf.ptr, 1u64);
};
fn fputsstr(fd: i32, s: str) void = {
os.write(fd, s.ptr, s.len: u64);
};
fn hexchar(n: u8) u8 = {
if (n < 10u8) { return n + 48u8; }; // '0'..'9'
return (n - 10u8) + 97u8; // 'a'..'f'
};
fn fputhex2(fd: i32, b: u8) void = {
let out: [4]u8;
out[0] = 92u8; // '\\'
out[1] = 120u8; // 'x'
out[2] = hexchar(b >> 4u8);
out[3] = hexchar(b & 15u8);
os.write(fd, out.ptr, 4u64);
};
fn fputq(fd: i32, p: *u8, n: i32) void = {
fputcbyte(fd, 34u8); // '"'
let i: i32 = 0;
for (i < n) {
let c: u8 = p[i];
if (c == 92u8) { // '\\'
fputsstr(fd, "\\\\");
} else {
if (c == 34u8) { // '"'
fputsstr(fd, "\\\"");
} else {
if (c == 10u8) { // '\n'
fputsstr(fd, "\\n");
} else {
if (c == 9u8) { // '\t'
fputsstr(fd, "\\t");
} else {
if (c == 13u8) { // '\r'
fputsstr(fd, "\\r");
} else {
if (c < 32u8) {
fputhex2(fd, c);
} else {
if (c == 127u8) {
fputhex2(fd, c);
} else {
fputcbyte(fd, c);
};
};
};
};
};
};
};
i += 1;
};
fputcbyte(fd, 34u8);
};
// tokprint — write one token line to fd. Format must match
// cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor.
// "<file>:<line>:<col> <kindname>[ <value>]\n"
//
// Takes `t` by pointer because w6c can't yet pass a >16-byte struct
// by value; the C version takes Tok by value.
export fn tokprint(fd: i32, t: *tok) void = {
// Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet
// reduced by w6c — `t.x.y` returns the whole str. Lift the str
// fields into locals so we can use the str pseudo-field path.
let tfile: str = t.file;
let ttext: str = t.text;
if (tfile.len > 0) {
fputsstr(fd, tfile);
} else {
fputsstr(fd, "<none>");
};
fputcbyte(fd, 58u8); // ':'
let 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'
};
// 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.
package ascii;
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;
};
// 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.
package lex;
// Sibling import (tok) auto-resolves via task #22 dir-enum when
// callers `import lex;` (which dir-enums lib/ww/lex/).
import os;
import ascii;
import mem;
// 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,
};
export fn lexinit(l: *lex, a: *arena, file: str, src: *u8, len: u64) void = {
l.file = file;
l.src = src;
l.srclen = len;
l.lpos = 0u64;
l.line = 1;
l.col = 1;
l.a = a;
l.errs = 0;
};
// srcb — byte at offset; helper that lifts the cast out of indexing.
fn srcb(l: *lex, off: u64) i32 = {
let i: i32 = off: i32;
let b: u8 = l.src[i];
return b: i32;
};
fn lpeek(l: *lex, ahead: u64) i32 = {
let p: u64 = l.lpos + ahead;
if (p >= l.srclen) { return -1; };
return srcb(l, p);
};
fn lget(l: *lex) i32 = {
if (l.lpos >= l.srclen) { return -1; };
let c: i32 = srcb(l, l.lpos);
l.lpos += 1u64;
if (c == 10) { // '\n'
l.line += 1;
l.col = 1;
} else {
l.col += 1;
};
return c;
};
fn 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 '//'
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);
};
// 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.
package ww;
import os;
import strconv;
import mem;
import 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", ...)
nmod: 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);
};
// lib/ww/parse/decl.ww — declaration parsing, split out of parse.ww.
package parse;
import os;
import mem;
import tok;
// `import encoding.utf8;` — the driver resolves the dotted path to
// a directory; only the leaf (`utf8`) is needed downstream as the
// module bareword for n_use → decl disambiguation, mirroring Hare's
// `use encoding::utf8;` → `utf8::name` (ref/hare/hare/ast/import.ha:7
// stores `[]str` but identifier-resolution uses the last component).
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);
n.nmod = p.curmod;
let leaf: str;
expectident(p, &leaf);
for (p.curkind == tkind.TK_DOT) {
advance(p); // past `.`
expectident(p, &leaf);
};
n.str = leaf;
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.nmod = p.curmod;
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.nmod = p.curmod;
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.nmod = p.curmod;
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.nmod = p.curmod;
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;
};
// lib/ww/parse/expr.ww — expression parsing, split out of parse.ww.
package parse;
import os;
import mem;
import 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;
};
// 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.
package parse;
// Sibling imports (expr, stmt, decl) auto-resolve via task #22
// dir-enum when callers `import parse;` (which dir-enums
// lib/ww/parse/).
import os;
import mem;
import tok;
type parser = struct {
l: *lex,
a: *arena,
errs: i32,
// nocast: while inside `[...]` we treat ':' as the slice
// separator, not the cast operator. Mirrors parse.c's flag.
nocast: i32,
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,
// curmod: the most-recent `module foo;` declaration. Each
// top-level decl is stamped with this value; on concatenated
// multi-file streams successive `module` decls mark per-file
// section boundaries. Mirrors cstage Parser.curmod.
curmod: 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) {
// `package foo;` — each contributing source's section in a
// concatenated stream begins with one. Single-file inputs
// may omit it (curmod stays empty; decls treated as primary).
//
// Retained divergence from brief: strict missing-`package`
// error softened to silent-default — 63 inline-source test
// wrappers depend on the soft behavior. See task #23 for
// the wrapper migration that unblocks the strict check.
// Rule 7 + rule 8 documentation.
if (p.curkind == tkind.TK_MODULE) {
advance(p);
let name: str;
expectident(p, &name);
expecttok(p, tkind.TK_SEMI, "expected ';' after module name");
p.curmod = name;
continue;
};
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;
};
// lib/ww/parse/stmt.ww — statement parsing, split out of parse.ww.
package parse;
import os;
import mem;
import 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;
};
// 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.
package ww;
import os;
import 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, "<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
};
// 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.
package ww;
// Sibling imports (typ, ast) auto-resolve via task #22 dir-enum
// when callers `import ww;` or pull all three separately.
import mem;
// 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;
};
// 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.
package wcc;
import os;
import mem;
import 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.nmod.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.nmod)) { return d.nmod; };
};
u = u.next;
};
return empty;
};
// srcimports — does the source file that contributed decl-module
// `modtag` carry `use <name>;`? 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.nmod.len > 0) {
if (streq(u.nmod, 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.
// TODO(#11): cstage check.c errors on duplicate top-level type/def/fn
// (see cmd/wcc/check.c L1800/L1839/L1860 "duplicate <kind>") and on
// duplicate top-level let (cmd/wcc/check.c L1880, "duplicate let %s")
// once #32 lands. Wwstage's installdecl just drops the second insert
// silently. Add `if (s == nil) err(...)` here once #11 wires checkfile
// into w6c_ww. Silent-accept matches the deferred-check design — see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
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.
//
// TODO(#11): cstage check.c (post-#32) errors `binding '%s'
// redeclared in same scope` when the tuple-pattern lists the same
// name twice (`for (let (a, a) .. xs)`). Wwstage's resolvewalk has
// no per-block scope (see resolvefnbody's docstring) and is used
// only by wwdump_ww as a diagnostic, so silent-accept here avoids
// false-positives on legal cross-block shadow until #11 adds the
// scoping infrastructure.
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).
//
// TODO(#11): cstage check.c (post-#32) errors `let '%s' redeclared
// in same scope` here. Wwstage resolvewalk has no per-block scope
// (see resolvefnbody's docstring) so a same-fn-body
// `let a=1; { let a=2; };` would falsely trip if we guarded
// scopedefine's nil return today. Silent-accept matches the
// deferred-check design until #11 adds per-block scoping; see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
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.
//
// TODO(#11): cstage check.c (post-#32) errors `param '%s' redeclared`
// when two params share a name. The fn body's scope IS fresh here
// (resolvefnbody opens it before calling us), so guarding scopedefine's
// nil return would be sound — but we defer until #11 wires checkfile
// into w6c_ww so the diagnostic class lands as a single coordinated
// step rather than dribbling in. Matches the cstage-only neg-case
// precedent at test/wcc/708 + test/wcc/696.
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;
};
// 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.
package wcc;
import os;
import mem;
import ast;
import tok;
import typ;
import sym;
import 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.
//
// N_DOT routes through fnparamslookupmod with the module hint
// (callee.lhs.str) — bare fnparamslookup walks same-module-first
// (#4d) which is wrong for a cross-module N_DOT call into a module
// whose same-leaf fn has divergent variadic-vs-non-variadic shape.
// #4d explicitly deferred this re-routing; surfaced by #16 when
// strings.contains gained a variadic shape and a caller's
// bytes.contains call site picked strings.contains' variadic
// params for arg-prep while emitting CALL bytes.contains.
fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
*nfixed_out = 0;
if (callee == nil) { return nil; };
let ps: *node = nil;
if (callee.kind == nkind.N_IDENT) {
if (callee.str.len == 0) { return nil; };
ps = fnparamslookup(c, callee.str);
} else { if (callee.kind == nkind.N_DOT) {
if (callee.str.len == 0) { return nil; };
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
ps = fnparamslookupmod(c, callee.str, cmod);
}; };
return findvariadicparam(ps, nfixed_out);
};
// mkvarargname — fresh local-slot name "<prefix><seq>". Used for
// the per-variadic-call scratch buffers (`@vararg_d_N` for the
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor).
// N is recorded on the N_CALL node at first emit so re-entry into
// cgcall picks the same names regardless of walk order.
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);
};
};
// #21: a CALL returning a tagged-union must
// skip widening — cgexpr leaves AX=tag,
// DX=word0, CX=word1, R8=word2 per the
// tagged-return ABI; the widening branch would
// treat AX as a concrete payload and silently
// drop DX/CX/R8. Restrict to the matching-slot
// case (mirrors cstage type_eq at
// cmd/w6c/cgen.c:4216-4221); tagged-source
// widening into a wider slot is out of scope.
if (taggedcallslot(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
// #12: N_INDEX of a sum-typed slice element —
// cgindex emits the same AX/DX/CX/R8 tagged ABI.
// Without this gate the widening scalar branch
// hardcodes the param's first-variant tag and
// the callee reads a fixed arm on garbage.
if (arg.kind == nkind.N_INDEX) {
let etn: *node = indexvaluetnode(c, arg);
if (etn != nil) {
if (istaggedtype(c, etn)) {
if (slotsize(c, etn) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
};
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", widensz, 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;
};
// By-value struct ident: load qword(s) from the slot
// and push high → low so left-to-right pop on the
// callee side lands word 0 / word 1 into the SysV arg
// register pair. Mirrors cstage cgen.c §4240 (call
// site) so the wwstage prologue's new struct spill arm
// (cgendecl.ww structparamsize branch) sees the same
// reg layout. Pre-#11 the call-site fell through to
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
// 8B word made it across, and the callee's second-arg
// slots picked up the wrong neighbour's value.
let stsz: i32 = structparamsize(c, lc.tnode);
if (stsz > 0) {
if (stsz > 8) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
let nw: i32 = 1;
if (stsz > 8) { nw = 2; };
return rest + nw;
};
};
};
// 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;
};
// #21: CALL returning a tagged-union — the aistagged guard
// above kept us out of the widening path. Push the tagged-
// return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2)
// high → low so the left-to-right POPQ into argregs drains the
// tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387.
let tcs: i32 = taggedcallslot(c, arg);
if (tcs > 0) {
if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); };
if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); };
if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); };
emitline("\tPUSHQ\tAX\n");
return rest + tcs / 8;
};
// #12: N_INDEX of a sum-typed slice element. cgindex above left
// the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below
// would only carry the tag word and drop the payload.
if (arg.kind == nkind.N_INDEX) {
let etn: *node = indexvaluetnode(c, arg);
if (etn != nil) {
if (istaggedtype(c, etn)) {
let isz: i32 = slotsize(c, etn);
if (isz > 24) { emitline("\tPUSHQ\tR8\n"); };
if (isz > 16) { emitline("\tPUSHQ\tCX\n"); };
if (isz > 8) { emitline("\tPUSHQ\tDX\n"); };
emitline("\tPUSHQ\tAX\n");
return rest + isz / 8;
};
};
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged
// type, returns the slot size in bytes; else 0. Used by pushargsrev's
// aistagged guard and natural-push arm, and by cgcall's pop sizer, to
// route a tagged-return call result through the AX/DX/CX/R8 high→low
// push convention rather than the concrete-variant widening path
// (which drops DX/CX/R8). See task #21.
export fn taggedcallslot(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
if (n.kind != nkind.N_CALL) { return 0; };
let callee: *node = n.lhs;
if (callee == nil) { return 0; };
if (callee.kind != nkind.N_IDENT) { return 0; };
let rt: *node = fnretlookup(c, callee.str);
if (!istaggedtype(c, rt)) { return 0; };
return slotsize(c, rt);
};
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); };
// #24: N_CALL returning a slice — cgexpr leaves (AX=ptr,
// BX=len, CX=cap); pushargsrev's slice arm pushes CX/BX/AX
// and cgcall pops 3 words. Without this arm the natural-push
// fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop
// side under-drains by 2 words, leaving R8/R9 unset for the
// receiver. Mirrors nodeisstr's N_CALL arm just below.
// N_DOT (cross-module callee, #34): route through fnretlookupmod
// so a same-leaf caller-module fn with diverging return shape
// doesn't shadow the explicit `mod.f()` qualifier — surfaced by
// strings.slice returning `fromutf8_unsafe(utf8.slice(...))`
// where strings.slice itself returns str.
if (k == nkind.N_CALL) {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rt: *node = fnretlookupmod(c, callee.str, c.curmod);
return isslicetype(c, rt);
};
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rt: *node = fnretlookupmod(c, callee.str, cmod);
return isslicetype(c, rt);
};
};
return false;
};
// 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).
//
// TODO(#11): every consumer of "is-str" here reconstructs the answer
// from raw N_kind because wwstage has no typed AST. Each new expression
// shape needs an explicit arm or it silently falls through to false,
// which downstream drops the second slot (BX/len) at the call site.
// A typed AST check (cstage reads n->type) would replace this whole
// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed),
// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base),
// N_DOT (struct field / chained / pseudo-fields excluded), N_CAST.
// Not covered (separate bugs / out of scope):
// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX`
// and never loads .len into BX; fixing the recognizer alone won't
// help. Tracked alongside the broader cgun-load-shape gap.
// - N_DOT to a tuple positional `t.1` of a str element — wwstage's
// cgdot loads (AX, BX) but tuple-as-arg has independent issues.
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 rt: *node = fnretlookupmod(c, callee.str, c.curmod);
return isstrtype(c, rt);
};
// #34: cross-module N_DOT — route through fnretlookupmod
// so a same-leaf caller-module fn (different return shape)
// doesn't shadow the explicit qualifier.
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rt: *node = fnretlookupmod(c, callee.str, cmod);
return isstrtype(c, rt);
};
};
return false;
};
// N_INDEX: `arr[i]` whose base is an indexable type carrying a
// str element. cgindex correctly loads (AX=ptr, BX=len) for a
// 16B element; without this arm pushargsrev only pushes AX and
// the call-arg pop reads .len from stack residue. Mirror of
// cstage's node_isstr → type_isstr(n->type), where n->type is
// the resolved element type after check.
if (k == nkind.N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bt: *node = nil;
let lc: *local = localfindnode(c, base.str);
if (lc != nil) { bt = lc.tnode; }
else { bt = letvartnode(c, base.str); };
if (bt != nil) {
let elem: *node = nil;
let bk: nkind = bt.kind;
if (bk == nkind.N_TARRAY) { elem = bt.lhs; };
if (bk == nkind.N_TSLICE) { elem = bt.lhs; };
if (bk == nkind.N_TPTR) { elem = bt.lhs; };
if (elem != nil) {
return isstrtype(c, elem);
};
};
};
// N_INDEX through a struct field: e.g. cmd.argsptr[i]
// where argsptr: *str. cgindex correctly loads the
// (ptr, len) pair via indexbaseesz; without this arm
// pushargsrev would only push AX and lose the .len.
if (base.kind == nkind.N_DOT) {
let fld: str = base.str;
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
let inner: *node = base.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
if (tn.kind == nkind.N_TPTR) {
let pinner: *node = tn.lhs;
if (pinner != nil) {
if (pinner.kind == nkind.N_TNAME) {
sname = pinner.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 ft: *node = fi.tnode;
if (ft != nil) {
let elem: *node = nil;
let fk: nkind = ft.kind;
if (fk == nkind.N_TPTR) { elem = ft.lhs; };
if (fk == nkind.N_TSLICE) { elem = ft.lhs; };
if (fk == nkind.N_TARRAY) { elem = ft.lhs; };
if (elem != nil) {
return isstrtype(c, elem);
};
};
};
fi = fi.finext;
};
};
};
};
};
};
};
};
return false;
};
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
// `<expr>.ptr` is *u8 not str; `<expr>.len` is i32 not str.
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == 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; };
// STATUS-3 #22: `!T` carries the error flag on T's underlying
// shape (cmd/wcc/check.c:290 resolve_type N_TBANG copies T's
// kind, just sets iserror). cstage's N_LET sizes off lu->kind,
// so `!void`/`!i32` land in the sz=8 default and `!str`/`!slice`
// keep their composite slot. Defer to the inner type so
// `let e: !void;` mirrors cstage's MOVQ $0 while `!str` falls
// through to the multi-word fill.
if (k == nkind.N_TBANG) { return typeis8byteprimitive(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return false; };
// Plain `void` slot: cstage sz=8 default → MOVQ $0. The let-
// decl is a phantom (a tagged-union variant tag carrier), but
// the slot is still 8B and zero-inits like any other prim.
if (streq(nm, "void")) { return true; };
// 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; };
// STATUS-3 #22: alias to `!T` or to `void` (Hare-style error
// type / phantom variant). cstage resolves the alias and
// lands on sz=8 default. Follow through aliaslookup so
// `type invalid = !void;` and `type done = void;` zero-init.
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
return typeis8byteprimitive(c, aliased);
};
};
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.
// Gated on inner kind, NOT on the field name alone: a struct with
// a literal `ptr: *T` field (lib/memio.state, lib/bufio.state) must
// route through the generic struct-field arm below so the stride
// comes from primsize/structlookup, not the str/slice default. The
// over-broad pre-#21 shortcut hard-coded esz=8 and silently
// miscompiled `m.ptr[i]` for `*u8` callers (also widened the load
// op MOVZBQ → MOVQ in cgindex). Mirrors cstage which routes every
// base through `base->type->sub->size` (cmd/w6c/cgen.c idx_eff).
if (streq(fld, "ptr")) {
let innert: *node = tn;
if (tn.kind == nkind.N_TPTR) { innert = tn.lhs; };
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
};
// Slice element: resolve through elemsizeofc so a
// slice of a named struct (e.g. *[]option) returns
// the struct stride instead of falling through to
// elemsizeof's default 8.
if (innert.kind == nkind.N_TSLICE) {
return elemsizeofc(c, innert);
};
};
// Inner is a struct N_TNAME (or unresolved) — fall through
// to the generic struct-field arm below.
};
// 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; };
// Pointer to named struct: indexing
// stride is the struct slot size.
// Without this, &p.ptr[i] for p.ptr:
// *S falls through to 8 and reads
// the wrong element.
let si: *structinfo = structlookup(c, elem.str);
if (si != nil) { return si.totsize; };
};
};
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; };
};
// `*[]T`: stride is the slice header (24B). Hare-faithful — a
// pointer-to-slice is a 1D array of slices, not of T. Mirrors the
// cstage check.c default `*U → U` path for U=[]T (slice element).
if (elem.kind == nkind.N_TSLICE) { return 24; };
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);
};
// indexvaluetnode — type node of the value produced by an N_INDEX
// expression. Walks base's type and returns its element. Recurses
// through chained N_INDEX so `names[i][k]` (names: **u8) resolves
// the outer base type to *u8 (the post-inner-index value type), so
// cgindex can compute the outer element size honestly. Mirrors
// cstage's `n->lhs->type` via typed-AST (cmd/w6c/cgen.c idx_eff).
// N_DOT base graduated (tasks #28/#30) so `obj.mat[i][k]` reads
// and `obj.arr[i] = v` tagged-element writes route through the
// same helper as the N_IDENT/N_INDEX bases #24/#27 graduated.
fn indexvaluetnode(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_INDEX) { return nil; };
let base: *node = n.lhs;
if (base == nil) { return nil; };
let bt: *node = nil;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) { bt = lc.tnode; }
else { bt = letvartnode(c, base.str); };
};
if (base.kind == nkind.N_INDEX) { bt = indexvaluetnode(c, base); };
if (base.kind == nkind.N_DOT) { bt = dotfieldtnode(c, base); };
if (bt == nil) { return nil; };
let k: nkind = bt.kind;
if (k == nkind.N_TPTR) { return bt.lhs; };
if (k == nkind.N_TSLICE) { return bt.lhs; };
if (k == nkind.N_TARRAY) { return bt.lhs; };
return nil;
};
// 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;
};
// sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B,
// return its natural size; else 0. Tagged unions, tuples, str,
// slices, scalars route through their existing register-return ABIs
// (AX/DX/CX/[R8]) regardless of size. Task #23 mirrors cstage's
// cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup
// and unwraps one leading N_TBANG so `type box = !big;` still
// triggers sret on the underlying big.
//
// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers
// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`.
// When structlookup(c, "b") misses, fall through to aliaslookup and
// recurse on the alias target — mirrors slotsize's N_TNAME arm
// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize.
// structlookupchain — resolve TNAME `tn` to its registered struct,
// chasing alias-of-alias (#22). Returns nil if the chain doesn't
// bottom out at a struct. Mirrors cstage's transitive
// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by
// cgdot / cgassign at every "field-walk on a struct-typed local"
// site so a transitively-aliased struct name resolves to its
// fieldinfo list regardless of chain depth.
export fn structlookupchain(c: *cgen, tn: *node) *structinfo = {
if (tn == nil) { return nil; };
if (tn.kind != nkind.N_TNAME) { return nil; };
let si: *structinfo = structlookup(c, tn.str);
if (si != nil) { return si; };
let cur: *node = tn;
for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) {
let aliased: *node = aliaslookup(c, cur.str);
if (aliased == nil) { cur = nil; }
else {
if (aliased.kind == nkind.N_TNAME) {
si = structlookup(c, aliased.str);
cur = aliased;
} else { cur = nil; };
};
};
return si;
};
export fn sretretsize(c: *cgen, t: *node) i32 = {
if (t == nil) { return 0; };
let r: *node = t;
if (r.kind == nkind.N_TBANG) {
r = r.lhs;
if (r == nil) { return 0; };
};
if (r.kind != nkind.N_TNAME) { return 0; };
// Primitives / aliased-to-primitives are never sret.
if (primsize(r.str) > 0) { return 0; };
if (streq(r.str, "str")) { return 0; };
let si: *structinfo = structlookup(c, r.str);
if (si == nil) {
if (c != nil) {
let aliased: *node = aliaslookup(c, r.str);
if (aliased != nil) {
return sretretsize(c, aliased);
};
};
return 0;
};
let n: i32 = structnaturalsize(si);
if (n <= 24) { return 0; };
return n;
};
// callsretsize — if N_CALL `n`'s callee returns a plain TY_STRUCT
// > 24B, return its natural size; else 0. Wraps sretretsize over the
// callee's resolved return type, used by cglet / cgassign receive
// sites and cgcall to detect sret at the receive / emit boundaries.
export fn callsretsize(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
if (n.kind != nkind.N_CALL) { return 0; };
let callee: *node = n.lhs;
if (callee == nil) { return 0; };
let cn: str;
cn.ptr = nil; cn.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cn = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cn = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cn.len == 0) { return 0; };
let rt: *node = fnretlookupmod(c, cn, cmod);
return sretretsize(c, rt);
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27), fnret/fnparamslookupmod (#28/#31), and
// enumlookup (#4a): without the prefer pass a bare-leaf struct
// name in module M can collapse onto another module's same-leaf
// struct prepended earlier in c.structs, silently picking the
// wrong totsize / field offsets.
let s: *structinfo = c.structs;
for (s != nil) {
if (streq(s.sname, name)) {
if (streq(s.smod, c.curmod)) { return s; };
};
s = s.sinext;
};
s = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (streq(sn, name)) { return s; };
s = s.sinext;
};
// Module-qualified form embedded in name (`pkg.S`): scope the
// leaf to its originating module. The `smod == pkg` guard
// prevents same-leaf structs in two modules from collapsing.
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;
};
// typenodeprimresolved — walk N_TBANG / N_TENUM / N_TNAME alias
// chains to the underlying primitive, returning its byte size and
// signedness. Sets *sz_out = 0 when the type doesn't reduce to a
// width-known primitive (composite, unresolved name, default-storage
// enum, etc.). Mirrors cstage's `type_isint(t) ? t->size : 0` /
// `type_isunsigned` recursion through TY_NAMED and TY_ENUM. Used by
// cgcast's identity-width identity-sign clamp-skip predicate (#33).
export fn typenodeprimresolved(c: *cgen, t: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
let cur: *node = t;
for (cur != nil) {
let k: nkind = cur.kind;
if (k == nkind.N_TBANG) { cur = cur.lhs; }
else { if (k == nkind.N_TENUM) { cur = cur.lhs; }
else { if (k == nkind.N_TNAME) {
let nm: str = cur.str;
// bool is excluded from the int-prim contract: cstage's
// `type_isint(TY_BOOL)` is false, so its identity check
// leaves src_w=0 on a bool source. Match that here so a
// `let y: i8 = b: i8;` (bool b) doesn't fire identity in
// wwstage and skip the MOVSBQ that cstage emits. Other
// call sites (slot sizing, etc.) still want
// primsize("bool")=1, so the exclusion stays local. The
// dedicated `is_bool` path in cgcast owns bool→bool's
// ANDQ $255 on both stages.
if (streq(nm, "bool")) { return; };
let ps: i32 = primsize(nm);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typenameisunsigned(nm);
return;
};
let al: *node = aliaslookup(c, nm);
if (al == nil) { return; };
cur = al;
}
else { return; }; }; };
};
};
// exprprimresolved — best-effort static (primsize, signedness) for an
// expression. Used by cgcast (#33) to derive the source-side primitive
// width and signedness so the identity-width identity-sign clamp-skip
// predicate fires. Sets *sz_out = 0 when the type can't be derived
// (untyped literal, call result with no return-type lookup, etc.);
// caller treats sz=0 as "not identity", which conservatively keeps
// the clamp. Mirror of cstage's `n->lhs->type` lookup with the same
// TY_NAMED / TY_ENUM recursion through type_isint / type_isunsigned.
export fn exprprimresolved(c: *cgen, n: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == nkind.N_INTLIT) {
// Typed-int literal: `7u32` has tsuffix = "u32". Mirrors
// cstage's `cexpr` which assigns `lookup_builtin(tsuffix)`
// as the node's type — without this, wwstage misses the
// suffix and emits a defensive clamp where cstage skips,
// breaking byte-id on rows like `let y: mymode = 7u32:
// mymode;` (mymode = enum u32).
let s: str = n.tsuffix;
if (s.len > 0) {
let ps: i32 = primsize(s);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typenameisunsigned(s);
};
};
return;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
typenodeprimresolved(c, lc.tnode,
sz_out, unsigned_out);
};
return;
};
if (k == nkind.N_CAST) {
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
return;
};
if (k == nkind.N_UN) {
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
return;
};
if (k == nkind.N_DOT) {
typenodeprimresolved(c, dotfieldtnode(c, n),
sz_out, unsigned_out);
return;
};
};
// 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
// <fieldname>`). 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len == 0) { return nil; };
let rt: *node = fnretlookupmod(c, cname, cmod);
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. Called from cglet at emit time so the frame
// grows monotonically per first-use (#15).
//
// `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, srcmod: str, tstruct: *node) void = {
let si: *structinfo = amalloc(c.a, 80u64): *structinfo;
si.sname = name;
si.smod = srcmod;
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.nmod, 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;
};
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
// type (or nil if unresolvable). Used by cgmatch to size the
// @match_spill slot at first use (#15 first-use+fail-loud convergence).
// IDENT scrutinees use a different lookup path (read off the local
// directly, no spill) so this returns nil for them too.
fn matchscrutt(c: *cgen, scrut: *node) *node = {
if (scrut == nil) { return nil; };
let k: nkind = scrut.kind;
if (k == nkind.N_IDENT) { return nil; };
if (k == nkind.N_CALL) {
let callee: *node = scrut.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
// Same-module-first disambiguation: a leaf collision
// on `next` (utf8.next + caller-side next) otherwise
// returns the last-declared (caller) rtype and the
// 4-arm match collapses arms 2+ to tag 0. Task #31.
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len > 0) {
let rt: *node = fnretlookupmod(c, cnm, cmod);
if (rt != nil) { return resolvetagged(c, rt); };
};
};
return nil;
};
if (k == nkind.N_INDEX) {
let ibase: *node = scrut.lhs;
if (ibase == nil) { return nil; };
if (ibase.kind != nkind.N_IDENT) { return nil; };
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) { return 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) { return nil; };
return resolvetagged(c, etn);
};
if (k == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, scrut);
if (ft == nil) { return nil; };
return resolvetagged(c, ft);
};
return nil;
};
// matchspillsz — slot size for the @match_spill scratch a non-ident
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
// when the scrutinee type can't be resolved keeps the historical
// alloc for non-tagged / unresolved cases. Called by cgmatch at first
// use; #15 first-use+fail-loud pins this size per fn.
fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
if (scrutt == nil) { return 16; };
let sz: i32 = slotsize(c, scrutt);
if (sz <= 0) { return 16; };
return sz;
};
// structparamsize — bytes occupied by a user-defined by-value struct
// param if it fits in 1-2 SysV integer eightbytes (cstage cgen.c
// struct_arg_size mirror; gates on size <= 16). Returns 0 for non-
// struct types or oversized structs so callers can fall through to
// other dispatch arms. Pre-#11 the wwstage prologue had no struct
// branch — user-defined struct params dropped through to the 8B
// scalar catch-all, the second-half value registers (DX/CX) were
// never spilled, and field reads from the under-allocated slot
// trailed into the saved-BP word.
fn structparamsize(c: *cgen, t: *node) i32 = {
if (c == nil) { return 0; };
let r: *node = resolvetype(c, t);
if (r == nil) { return 0; };
if (r.kind != nkind.N_TNAME) { return 0; };
let nm: str = r.str;
if (streq(nm, "str")) { return 0; };
if (primsize(nm) > 0) { return 0; };
let si: *structinfo = structlookup(c, nm);
if (si == nil) { return 0; };
if (si.totsize <= 0) { return 0; };
if (si.totsize > 16) { return 0; };
return si.totsize;
};
// 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; };
// Alias-unwrap: wwstage has no typed AST, so an aliased tagged
// return (`type ft = (i64|str|bool); fn f() ft = ...`) reaches
// here as N_TNAME("ft"), not N_TTAGGED. flatvariantidx and the
// fallback both gate on N_TTAGGED → -1 → caller maps to 0,
// silently emitting `MOVQ $0, AX` for every non-leading variant.
// Cstage's check.c canonicalizes N_TNAME → underlying upfront;
// every wwstage cgen consumer of a type-bearing node has to
// remember this step itself. TODO(#11): a wwstage check pass
// between parse and cgen would replace the per-site unwrap with
// a single canonicalization. Same shape of fix as nodeisstr.
let resolved: *node = resolvetagged(c, tagged);
if (resolved != nil) { tagged = resolved; };
let wantname: str = rhstargetname(c, rhs);
if (wantname.len > 0) {
let r: i32 = flatvariantidx(c, tagged, wantname);
if (r >= 0) { return r; };
};
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Cstage's type_eq
// distinguishes a `[]u8` arm from a `u8` arm at type-build; the
// name-only flatvariantidx pass above can't see `[]T`, so without
// the slice axis a (u8 | []u8) widen / match collapses every
// non-str rhs onto the leading scalar variant (task #19). Walks
// the spread-flattened list so a `(...inner | str)` outer agrees
// with the inner's str / slice positions.
let wantstr: bool = nodeisstr(c, rhs);
let wantslice: bool = nodeisslice(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 = isstrtype(c, iv);
let ivisslice: bool = isslicetype(c, iv);
if (ivisstr == wantstr && ivisslice == wantslice) { return idx; };
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
let visstr: bool = isstrtype(c, v);
let visslice: bool = isslicetype(c, v);
if (visstr == wantstr && visslice == wantslice) { 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;
};
// flatslicevariantidx — flat 0-based index of the first slice-shape
// variant in `tagged` (`...inner` spread expanded). When `elem` is an
// N_TNAME, prefer a `[]<elem.str>` variant; falls back to the first
// slice slot if no element match is found. Cstage walks resolved
// Type pointers and dispatches via cg_tag_for_variant / type_eq;
// wwstage's name-keyed flatvariantidx can't see a `[]u8` variant
// (pat.str == ""), collapsing every (scalar | []T) match arm and
// widen-to-tagged call onto tag 0. Task #19. Returns -1 when no
// slice variant exists.
fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
let elemname: str;
elemname.ptr = nil; elemname.len = 0;
if (elem != nil) {
if (elem.kind == nkind.N_TNAME) { elemname = elem.str; };
};
let fallback: i32 = -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 (isslicetype(c, iv)) {
if (fallback < 0) { fallback = idx; };
if (elemname.len > 0) {
if (iv.kind == nkind.N_TSLICE) {
if (iv.lhs != nil) {
if (iv.lhs.kind == nkind.N_TNAME) {
if (variantnamematch(iv.lhs.str, elemname)) { return idx; };
};
};
};
};
};
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
if (isslicetype(c, v)) {
if (fallback < 0) { fallback = idx; };
if (elemname.len > 0) {
if (v.kind == nkind.N_TSLICE) {
if (v.lhs != nil) {
if (v.lhs.kind == nkind.N_TNAME) {
if (variantnamematch(v.lhs.str, elemname)) { return idx; };
};
};
};
};
};
v = v.next;
idx += 1;
};
return fallback;
};
// 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (calleename.len > 0) {
let rt: *node = fnretlookupmod(c, calleename, cmod);
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");
// Shared scratch sized at first use per #15/#26c. A sibling
// site (cgreturn, pushargsrev, cgindex) hitting @tagscr later
// with a larger size fatals (rule 7) — pinned offset can't
// grow in place.
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;
};
// Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
// X0 only — the AX-store fallback below would silently write whatever
// was loaded into AX before the SSE conversion. Literal `1.0` works
// by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
// before MOVSD'ing into X0); every runtime f64 shape (cast, call,
// unary, ident, struct-field load) needs the explicit MOVSD path.
// Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no
// checker so we classify via exprfloatkind (same shape used by cgcast)
// and resolve the variant tag by name directly — rhstargetname has no
// N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the
// str-shape fallback that picks tag 0 for an `(i64 | f64)` union.
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
let fmov: str = "MOVSD";
let fname: str = "f64";
if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; };
cgexpr(c, src);
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
let ftag: i32 = flatvariantidx(c, dt, fname);
if (ftag < 0) { ftag = 0; };
emitline("\tMOVQ\t$");
emitint(ftag: 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);
};
// 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.
package wcc;
import os;
import mem;
import ast;
import tok;
import typ;
import sym;
import 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; };
// `is []T` / `as []T` — slice-shape variant lookup routes through
// the shape-aware helper so non-N_TNAME variant nodes (which
// flatvariantidx's name key can't see) resolve. Task #19.
if (vt.kind == nkind.N_TSLICE) {
return flatslicevariantidx(c, tagged, vt.lhs);
};
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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len > 0) {
let rt: *node = fnretlookupmod(c, cname, cmod);
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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len > 0) {
let rt: *node = fnretlookupmod(c, cname, cmod);
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.
//
// Identity-width identity-sign cast is a no-op at the machine-
// int level: src and dst share both width and signedness, so the
// natural slot/load already carries the right canonical 64-bit
// shape. Skip the clamp in that case. Symmetric with cstage's
// principled gate (#33). Replaces the previous N_TENUM lacuna in
// this walker (the alias-step missed `N_TENUM`, so any cast to
// an enum dst landed on tn==nil and skipped the clamp by
// accident — task #25 mirrored that into cstage as a single-site
// gate, and #33 retires both). The walker now follows N_TENUM
// too so a narrow-to-enum cast (u32→enum-u8, i64→enum-i32)
// resolves to the underlying primitive and the clamp fires —
// fixing a silent miscompile in the process.
if (srcfk == 0 && dstfk == 0) {
let sz: i32 = 0;
let is_unsigned: bool = false;
typenodeprimresolved(c, n.rhs, &sz, &is_unsigned);
let src_sz: i32 = 0;
let src_unsigned: bool = false;
exprprimresolved(c, n.lhs, &src_sz, &src_unsigned);
let identity: bool = false;
if (sz > 0) { if (src_sz == sz) {
if (src_unsigned == is_unsigned) { identity = true; };
}; };
// Detect bool dst by walking n.rhs to the leaf TNAME. bool
// keeps its dedicated ANDQ $255 contract regardless of
// upstream shape; it stays off the identity path.
let leaf_tn: *node = n.rhs;
for (leaf_tn != nil) {
let lk: nkind = leaf_tn.kind;
if (lk == nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; }
else { if (lk == nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; }
else { if (lk == nkind.N_TNAME) {
let lnm: str = leaf_tn.str;
if (primsize(lnm) > 0) { break; };
let lal: *node = aliaslookup(c, lnm);
if (lal == nil) { leaf_tn = nil; }
else { leaf_tn = lal; };
}
else { leaf_tn = nil; }; }; };
};
let is_bool: bool = false;
if (leaf_tn != nil) {
if (leaf_tn.kind == nkind.N_TNAME) {
is_bool = streq(leaf_tn.str, "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 (!identity) {
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");
emitbytes( 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.
// Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the
// MOVQ symname(SB) fallback below would emit a bogus reference
// (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline
// the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage
// Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12.
if (deflookup(c, nm)) {
let drhs: *node = deflookuprhs(c, nm);
if (drhs != nil) {
if (drhs.kind == nkind.N_STRLIT) {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", BX\n");
return;
};
};
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);
} else { if (base.kind == nkind.N_INDEX) {
let bt: *node = indexvaluetnode(c, base);
if (bt != nil) {
esz = elemsizeofc(c, bt);
signed_elem = elemissignedc(c, bt);
};
};};};
};
// 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[:R8] 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.
// Scrutinee type + spill size resolved through matchscrutt
// / matchspillsz at first use (#15) — see cgenutil.ww
// (task #9 align-down to cstage).
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
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");
// CX/R8 writes gated on spill size so 1-word-
// payload variants (slot 16B) don't bump the
// frame past the tag+word0 the receiver reads.
// Mirrors cmd/w6c/cgen.c cgmatch's
// `if (slot_size > 16)` / `> 24` guards.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
if (spillsz > 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 r: i32 = -1;
if (pat.kind == nkind.N_TNAME) {
r = flatvariantidx(c, scrutt, pat.str);
} else { if (pat.kind == nkind.N_TSLICE) {
// `case let s: []T =>` — pat.str is empty
// because the variant is a composite, so
// route through the slice-shape helper.
// Without this every (scalar | []T) match
// arm collapses to tag 0 (task #19).
r = flatslicevariantidx(c, scrutt, pat.lhs);
}; };
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. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.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;
etmod = lhs.lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookupmod(c, etname, etmod);
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) {
// structlookupchain walks the alias chain on
// a miss so `*tokenizer` where tokenizer is
// a transitively-aliased struct still
// resolves to the underlying fieldinfo (#22).
let si: *structinfo = structlookupchain(c, inner);
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) {
// structlookupchain walks the alias chain on
// miss so a transitively-aliased struct (`type
// b = a; a = struct`) still resolves to the
// underlying fieldinfo (#22).
let si: *structinfo = structlookupchain(c, tn);
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 <field>(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");
emitbytes( 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 <field>(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 <fld>(SB), AX` (linker: `undefined reference to <fld>`).
// 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.
// lhs.str is the explicit module hint so a same-leaf
// def in another module (head of c.fnrets) can't shadow
// the explicit qualifier (#17 N_DOT-arm omission audit).
let frt: *node = fnretlookupmod(c, fld, lhs.str);
if (frt != nil) {
emitline("\tLEAQ\t");
emitfnname(c, fld, lhs.str);
emitline("(SB), AX\n");
return;
};
// `mod.MSG` where MSG is `def MSG: str = "..."` —
// strlit-inline matches cstage Sdef walk #2 in
// cmd/w6c/cgen.c N_DOT mod-qualified. Without this
// the MOVQ leaf(SB) fallback emits a bogus ref
// (`alpha.MSG(SB)`, never DATAW-defined). lhs.str is
// the explicit module hint — a 3rd-module qualifier
// `alpha.MSG` from gamma needs alpha (not c.curmod)
// to beat a head-of-c.defs beta.MSG collision (#11).
let drhs: *node = deflookuprhsmod(c, fld, lhs.str);
if (drhs != nil) {
if (drhs.kind == nkind.N_STRLIT) {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", BX\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 <leaf>(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);
};
};
};
} else { if (base.kind == nkind.N_DOT) {
// `&p.ptr[i]` shape: stride is the element
// of the slice/struct-pointer field, not
// the default 8. Mirrors cgindex's N_DOT
// arm so &p.ptr[i] and p.ptr[i] agree.
esz = indexbaseesz(c, base);
};};
};
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (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, restore idx into BX.
// Mirrors cstage's lean three-line shape
// (cmd/w6c/cgen.c TK_AMP N_INDEX complex
// base 2104-2107); the prior MOVQ AX, BX
// + POPQ AX scratch shuffle was rule-10
// verbose-defensive on the wwstage side
// with no semantic asymmetry (task #21).
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tPOPQ\tBX\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) {
// Signed IDIV reads dividend from RDX:RAX; CQO sign-extends
// RAX. Zero-filling DX would treat a negative RAX as a huge
// positive 128-bit value. Unsigned DIV needs RDX zero.
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
return;
};
if (n.op == tkind.TK_PERCENT) {
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
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.
//
// N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage
// only handled N_IDENT, leaving N_DOT calls without widening
// detection — pushargsrev then fell through to the N_IDENT-slice
// fast path and dropped the variant tag word on widened slice args.
// Cstage finds params via the checker-set `n->lhs->type`, sidestepping
// the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
} else { if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
calleeparams = fnparamslookupmod(c, callee.str, cmod);
}; };
};
// Hare-style variadic last param: gather N tail args into a
// frame-resident [N]T (`@vararg_d_<seq>`) plus a 24B slice
// descriptor (`@vararg_sl_<seq>`), 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. Per-call
// seq comes from c.varargseq bumped at gather emit (mirrors
// cstage's mklabel("vararg_d/sl") freshness).
{
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 = c.varargseq;
c.varargseq += 1;
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
// Use raw element size, not stack-padded
// slotsize. cstage cmd/w6c/cgen.c cgcall
// gathers a `T...` slice at velem->size stride
// (MOVL for u32, MOVB for u8); the callee
// `arg[i]` reads at the same raw stride. wwstage
// previously sized through slotsize which pads
// scalars to 8, mismatching the stride at the
// callee read site — runtime miscompile in
// `(rune...)` callees per #36.
let esz: i32 = 8;
if (varp.lhs != nil) {
if (varp.lhs.kind == nkind.N_TNAME) {
let ps: i32 = primsize(varp.lhs.str);
if (ps > 0) { esz = ps; }
else { esz = slotsize(c, varp.lhs); };
} else {
esz = 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);
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
// pops have finished (so they don't clobber RDI). The dest off
// is either the receive site's slot (c.sretdestoff, propagated
// from cglet / cgassign ident) or the per-fn @sretscr discard
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
if (sretcs > 0) {
if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
sretcs, nil);
};
};
// 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;
if (sretcs > 0) { intidx = 1; };
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; };
// #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low
// by pushargsrev; size the per-arg pop to match so the
// next arg's POPQ doesn't land on residual tag/payload
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
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;
};
// `callee` is already in scope from line 2827; reuse it. Pre-#32
// silent-redecl masked the second `let callee` here as a no-op
// (same value, same fn-body scope post-#27).
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;
};
};
};
};
};
};
};
};
};
};
// sret hidden first-arg (#23): load &dest into RDI AFTER all
// user-arg pops have finished — intidx started at 1 so RDI was
// never written. The CALL emit follows immediately.
//
// Forwarding (task #9 follow-up): when outer's `return f();`
// forwards through an sret callee, source RDI from outer's
// saved @sretarg — inner writes directly into outer's caller-
// prealloc dest. No temporary in outer's frame. The @sretscr
// slot stays reserved for byte-id with cstage; it goes unused
// on the forwarding branch.
if (sretcs > 0) {
if (c.sretforward != 0) {
let sretargoff: i32 = localfind(c, "@sretarg");
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};
};
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.
// Route through fnretlookupmod: for N_DOT cross-module callees,
// the bare-leaf fnretlookup's same-module-first walk (#4e) would
// pick the caller-module's same-leaf fn — a str-returning
// caller-side `slice` over a []u8-returning `mod.slice` then
// emits a phantom MOVQ DX, BX after the cross-module CALL (#34).
if (calleename.len > 0) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { cmod = c.curmod; };
if (callee.kind == nkind.N_DOT) {
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
};
let rt: *node = fnretlookupmod(c, calleename, cmod);
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";
// Pointee node for the lhs-sign side of the /=
// and %= dispatch. Mirror of cstage's `vt` at
// cmd/w6c/cgen.c's TK_STAR-compound branch.
let pe: *node = nil;
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) {
pe = 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");
// Post-63332fe: /= and %= via CQO/IDIVQ on the
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, pe);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
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);
// Without this, the tagged-element gate
// below (keyed on elemtn) misses for
// `obj.arr[i] = v` over an [N]Tagged field
// and the store falls through to scalar —
// task #30, sister of the cgindex N_INDEX-
// base fix #24. indexvaluetnode now handles
// N_DOT base, so the element type drops out
// of the same helper.
let bt: *node = indexvaluetnode(c, lhs);
if (bt != nil) { elemtn = bt; };
} else { if (base.kind == nkind.N_INDEX) {
// Chained-write write-side parallel of the
// cgindex N_INDEX-base arm graduated in #24:
// `names[i][k] = v` (names: **u8) — outer
// base is the inner N_INDEX whose value-type
// is *u8, so the outer element is u8 and
// the store is MOVB, not MOVQ.
let bt: *node = indexvaluetnode(c, base);
if (bt != nil) {
esz = elemsizeofc(c, bt);
let bk2: nkind = bt.kind;
if (bk2 == nkind.N_TPTR) { elemtn = bt.lhs; };
if (bk2 == nkind.N_TSLICE) { elemtn = bt.lhs; };
if (bk2 == nkind.N_TARRAY) { elemtn = bt.lhs; };
};
};};};
};
// 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; first-use sizes the slot (#15/#26c).
if (elemtn != nil) {
if (istaggedtype(c, elemtn)) {
let slot_sz: i32 = slotsize(c, elemtn);
let scroff: i32 = localadd(c, "@tagscr",
slot_sz, 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) {
// structlookupchain (#22) handles the
// alias-chain miss; same shape as the
// cgdot pointer-to-struct read site.
let si: *structinfo = structlookupchain(c, inner);
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) {
// structlookupchain (#22) — same shape
// as the cgdot direct-local read site.
let si: *structinfo = structlookupchain(c, tn);
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 `<expr>.field = v` where `<expr>` itself is a chain
// of dots resolving to a *struct. Mirrors the C cgen branch
// added to close trap 1 (cmd/w6c/cgen.c). Without this, only
// `local.field = v` and `local.fieldptr.field = v` get wired
// (the latter through the IDENT-base branch above) — chains
// like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no
// store. Only plain `=` is wired here; chained compound on a
// pointer-field hasn't surfaced.
if (lhs != nil) {
if (lhs.kind == 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");
}
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
// park rhs in CX, slot value (BX) into AX,
// CQO (or zero DX), IDIVQ (or DIVQ) CX,
// ferry AX or DX back to BX for the shared
// store-BX tail below.
else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, lvftn);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
}
else {
// Unsupported compound: store rhs
// directly. Mirrors the local path's
// legacy fallback for unknown ops.
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) {
// sret receive (#23): plain
// TY_STRUCT > 24B from a CALL.
// `s` is the prealloc dest; the
// callee writes through hidden RDI
// directly into off(BP). Mirror of
// cglet's sret branch.
if (lcnsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
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");
};
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the
// freshly loaded slot value) was stored back unchanged
// — a silent no-op rather than the natural rhs-only
// shape the global/deref siblings took. Park rhs in
// CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX
// (quotient) or DX (remainder) back to BX.
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
unsignd = typenodeisunsignedc(c, lcn.tnode);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
};
emitline("\tMOVQ\tBX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
};
return;
};
// 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.
package wcc;
import os;
import mem;
import ast;
import tok;
import typ;
import sym;
import 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (calleename.len > 0) {
let rt: *node = fnretlookupmod(c, calleename, cmod);
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);
// @retscr (not @tagscr) for the return materialise
// path. Cstage cmd/w6c/cgen.c cgreturn uses
// `@retscr` here and reserves the @tagscr SSoT
// for arg-widen / non-BP-base store / N_INDEX
// tagged-element write. Sharing the name in a fn
// that BOTH returns a 32B tagged AND pushes a
// smaller tagged arg fatals localadd's @-prefix
// size-grow guard (rule 7); routing returns
// through their own slot keeps each cache
// monotonic. Hardcoding 24 truncated 32B-slot
// returns and overwrote adjacent locals during
// the pre-zero loop (#38).
let scroff: i32 = localadd(c, "@retscr", rsz, 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;
};
// sret return (#23): plain TY_STRUCT > 24B. Callee writes
// through *(@sretarg) (the caller-prealloc dest saved at
// the prologue), then loads @sretarg into RAX and rets —
// the SysV "return the pointer" discipline. Two rhs shapes
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
let sretargoff: i32 = localfind(c, "@sretarg");
if (sretargoff != 0) {
let scs: i32 = sretretsize(c, c.fnret);
if (scs > 0) {
// sret return-forwarding (task #9 follow-up to
// #23): `return f();` where outer + inner both
// return the same >24B struct shape. Outer's
// @sretarg already holds its caller's prealloc
// dest; pass it to inner in RDI (set by cgcall
// via c.sretforward), inner writes directly
// there, inner's RAX (dest pointer) is already
// outer's return value. The trailing MOVQ
// @sretarg(BP), AX is redundant after inner's
// RET but kept for byte-id symmetry with the
// N_IDENT / N_STRUCTLIT arms below.
if (rhs.kind == nkind.N_CALL) {
c.sretforward = 1;
cgexpr(c, rhs);
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let okrhs: bool = false;
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; };
if (okrhs) {
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 sret_si: *structinfo = structlookup(c, sname);
if (sret_si != nil) {
let emptys: str;
emptys.ptr = nil; emptys.len = 0;
// mode=1 (PTR_LOCAL): base reg = BX,
// reloaded from @sretarg(BP) before
// each field store. disp = 0 because
// the dest pointer IS the struct base.
cgstructlitfill(c, sret_si, rhs,
1, sretargoff, emptys,
0, scs);
};
} else {
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), BX\n");
let k: i32 = 0;
for (k + 8 <= scs) {
emitline("\tMOVQ\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 8;
};
for (k + 4 <= scs) {
emitline("\tMOVL\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 4;
};
for (k < scs) {
emitline("\tMOVB\t");
emitoff((rl.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 1;
};
};
};
// sret return: RAX = dest pointer.
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), 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 route through
// the sret arm above.
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;
};
};
// sret receive (#23): plain TY_STRUCT > 24B from a call.
// The let's own slot IS the caller-prealloc dest; the
// nested cgexpr → cgcall path emits `LEAQ off(BP), DI`
// before the CALL and the callee writes through it. No
// AX/DX/CX shuffle; AX returns the dest pointer per SysV
// sret discipline (irrelevant here).
if (rhs.kind == nkind.N_CALL) {
let scs: i32 = callsretsize(c, rhs);
if (scs > 0) {
c.sretdestoff = off;
cgexpr(c, rhs);
c.sretdestoff = 0;
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;
};
};
};
};
};
// Struct ident copy: `let p2: T = p1;` where T is a struct
// >8B and rhs is a local ident. Per-qword MOVQ from src
// slot to dst slot, with a sized tail (MOVL/MOVB) for
// natural sizes that aren't 8-aligned (e.g. `struct
// { i32, i32, i32 }` is 12B). Pre-fix this path fell
// through to `cgexpr + MOVQ AX, off(BP)` which stored
// only the first qword (and a stale BX for sz==16 lets
// via the str-init tail) — silent partial copy. Mirrors
// cstage cgen.c N_LET struct-ident branch (Task #32).
if (rhs.kind == nkind.N_IDENT) {
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) {
let lsz: i32 = structnaturalsize(lsi);
if (lsz > 8) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let soff: i32 = lc.off;
let ki: i32 = 0;
for (ki + 8 <= lsz) {
emitline("\tMOVQ\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + ki): i64);
emitline("(BP)\n");
ki += 8;
};
if (ki < lsz) {
let tail: i32 = lsz - 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((off + ki): 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: <fn>_loop_N for the top,
// <fn>_endloop_N for the post-body merge. No separate cont
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
emitlabel(topl);
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
c.loopcontbuf[c.looptop] = topl;
c.looptop += 1;
if (n.body != nil) { cgstmt(c, n.body); };
c.looptop -= 1;
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
};
// Tuple-destructure assign: `a, b = call();`. The call's tuple
// return lands in (AX, DX); push DX to free it, store AX into
// the first lvalue, then pop DX into the second. Mirrors
// cmd/w6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
fn cgmassign(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tPUSHQ\tDX\n");
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
if (l0 != nil) {
if (l0.kind == 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cnm = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cnm = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len > 0) {
let rt: *node = fnretlookupmod(c, cnm, cmod);
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;
};
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
//
// Houses the top-level emission glue:
// - cgfnparams: parameter spilling per SysV
// - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue
// deferred via cgen.ww's cgoutbuf so the frame size
// reflects every emit-time localadd (#15/#26c)
// - cgfile: file-level entry (the exported driver)
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgendecl;` directly.
package wcc;
import os;
import mem;
import ast;
import tok;
import typ;
import sym;
import strconv;
// ---- function-level cgen ---------------------------------------------
fn cgfnparams(c: *cgen, params: *node) void = {
let p: *node = params;
// sret (#23): RDI is consumed by the hidden dest pointer
// (already spilled to @sretarg by cgfn); the first user param
// lands in SI.
let idx: i32 = 0;
if (localfind(c, "@sretarg") != 0) { idx = 1; };
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 { let stsz: i32 = structparamsize(c, p.lhs);
if (stsz > 0) {
// User-defined by-value struct ≤ 16B: 1 or 2
// integer eightbytes. Mirrors cstage's
// `struct_eb = (pu->size > 8) ? 2 : 1` and the
// matching reg/stack/stitch arms in cgen.c cgfn.
let nw: i32 = 1;
if (stsz > 8) { nw = 2; };
if (idx + nw <= 6) {
let off: i32 = localadd(c, nm, stsz, 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) {
let off: i32 = localadd(c, nm, stsz, 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 (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_.nmod;
c.fnret = fn_.lhs;
// sret callee (#23): return type is plain TY_STRUCT > 24B.
// Reserve 8B for @sretarg (holds the saved hidden RDI dest
// pointer); cgfnparams skips DI for user args, cgreturn writes
// through *(@sretarg) and returns @sretarg in RAX.
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
// Capture the body into cgoutbuf while c.frame grows under
// emit-time localadd calls (#15/#26c — wwstage dropped its
// scanlocals pre-pass to align DOWN with cstage's first-use
// pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits
// after the body finishes so the frame size reflects every
// localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds
// `subsp`/`text` Progs up front and patches their `from.offset`
// at the end via txt_emit.
cgout_enable(c.a);
if (sret_callee) {
let saoff: i32 = localadd(c, "@sretarg", 8, nil);
emitline("\tMOVQ\tDI, ");
emitoff(saoff: i64);
emitline("(BP)\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 cstage,
// 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");
};
cgout_disable();
let frame: i32 = c.frame;
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// 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.
emitline("TEXT ");
emitfnname(c, fn_.str, fn_.nmod);
emitline(",$");
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\n");
cgout_flush();
};
// ---- 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);
};
// 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 (- ! ~ &amp; *),
// 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.
package wcc;
import os;
import mem;
import ast;
import tok;
import typ;
import sym;
import 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.
import cgenutil;
import cgenexpr;
import cgenstmt;
import 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.nmod;
a.target = body;
a.aanext = c.aliases;
c.aliases = a;
};
};
};
d = d.next;
};
};
fn aliaslookup(c: *cgen, name: str) *node = {
// Same-module first, then any. Mirrors cstage's scope_lookup_prefer
// (cmd/wcc/check.c:65); without the prefer pass a bare `invalid`
// in module M with `type invalid = !void;` can collapse onto a
// strconv-style `type invalid = !i32;` registered earlier in
// c.aliases (head-first walk). The leaf-collision then drives a
// narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide
// (task #27 silent-correct-by-zero-init).
let a: *aliasent = c.aliases;
for (a != nil) {
if (streq(a.aname, name)) {
if (streq(a.amod, c.curmod)) { return a.target; };
};
a = a.aanext;
};
a = c.aliases;
for (a != nil) {
if (streq(a.aname, 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.nmod;
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 = {
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
// the prefer pass a bare-leaf enum ident in module M can collapse
// onto another module's same-leaf enum prepended earlier in
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, c.curmod)) { return e; };
};
e = e.etnext;
};
e = c.enums;
for (e != nil) {
if (streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form embedded in name (`pkg.enum`): scope the
// leaf to its originating module. The `emod == pkg` guard prevents
// same-leaf enums in two modules from collapsing.
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;
};
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
// where the qualifier is an explicit N_IDENT module name. Mirrors
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
// bare enumlookup so a missing or empty mod still finds the leaf.
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
if (mod.len > 0) {
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) {
if (streq(e.emod, mod)) { return e; };
};
e = e.etnext;
};
};
return enumlookup(c, name);
};
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,
sz: i32, // allocated slot size; carried so @-prefix reuse can
// fail-loud (rule 7) if a later site needs a larger
// slot than the first allocation pinned. Per #15/#26c
// size-strategy convergence — wwstage dropped its
// scanlocals pre-pass, so @tagscr/@retscr/@sretscr/
// @tagbase are sized at first-use; subsequent uses
// must fit.
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_<seq>"
bytes: str,
slnext: *strlit,
};
// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr
// gets its CALL target rewritten to `name`.
type ffi = struct {
ident: str,
symbol: str,
fnext: *ffi,
};
// enummember — one (name, value) pair belonging to a registered enum.
// Values are pre-computed at collect time (Hare allows sibling refs
// like `RDWR = READ | WRITE`, so we walk the value expr against the
// already-resolved siblings). Lookup is linear; enum cardinality is
// usually small.
type enummember = struct {
mname: str,
mval: u64,
emnext: *enummember,
};
type enumtype = struct {
ename: str,
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,
// atlocals — persistent registry of `@`-prefix scratch slots
// for the current fn. cgblock save/restores c.locals to scope
// inner shadows (post-#27); a return/cgindex/cgwidentaggedstore
// inside one block must not reallocate @retscr/@tagscr when a
// sibling block uses them again. cgblock leaves atlocals alone
// so the slot offsets survive. localadd checks here first for
// @-prefix names; localfind falls back here when c.locals misses
// an @-name. Pre-#15 this was a handful of named offsets on the
// cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15
// every @-name flows through the same registry.
atlocals: *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. cgcall bumps this on each gather
// emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per
// callsite; mirrors cstage's mklabel("vararg_d/sl") freshness
// so two variadic callsites with different arities in one fn
// get distinct slots (the shared slot fail-louds under #15's
// @-prefix grow-on-pin discipline).
varargseq: i32,
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer
// (RDI) to a caller-prealloc dest; the callee writes through
// that pointer and returns it in RAX.
//
// sretdestoff — caller-side dest BP offset, propagated from a
// receive site (cglet / cgassign ident) to the
// nested cgexpr → cgcall so the call emits
// `LEAQ off(BP), DI` instead of allocating a
// scratch. 0 means no receiver wired.
// sretforward — set by cgreturn `return f();` from an sret callee to
// signal cgcall: source RDI for inner from outer's
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
// dest. Inner writes into outer's caller-prealloc;
// inner's RAX (the dest pointer) is already outer's
// return value. Cleared after cgcall consumes it.
//
// The single-slot caches for @sretarg / @sretscr / @retscr that
// used to live here are gone: localadd's `@`-prefix dedup against
// c.locals (fail-loud on size grow) is the SSoT now. cgenstmt /
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
sretforward: 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.atlocals = nil;
c.frame = 0;
c.lastwasreturn = 0;
c.labelseq = 0;
c.varargseq = 0;
c.sretdestoff = 0;
c.sretforward = 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 cstage allocates via cgexpr's by-value
// `locals` list — so two separate matches each get fresh slots even
// when their bind names collide.
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.sz = asz;
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.sz = 0;
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
// cstage'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.
//
// `@`-prefix scratch slots (`@tagscr`, `@retscr`, `@tagbase`,
// `@sretarg`, `@sretscr`, `@match_spill`, `@vararg_*`) share
// one slot per name per fn. Post #15/#26c the slot is sized
// at first use and reused by every later caller; a later
// caller asking for a larger slot than the first allocation
// pinned fatals (rule 7 — surface, don't silently corrupt
// the frame: the pinned offset already neighbours other
// locals so the slot can't grow in place). Mirrors cstage's
// cg_tagscr / cg_retscr / cg_sretscr same-fn caches in
// cmd/w6c/cgen.c (#26 / #15).
if (name.len > 0) {
if (name[0] == 64u8) { // '@'
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
let cur: *local = c.atlocals;
for (cur != nil) {
let cn: str = cur.name;
if (streq(cn, name)) {
if (asz > cur.sz) {
// rule-7 surface, post-#15: pinned slot
// offset can't grow in place.
let msg: str = "localadd: @-prefix slot grew within fn\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
cur.tnode = tnode;
return cur.off;
};
cur = cur.lnext;
};
// First use: allocate via localalloc (bumps c.frame +
// pushes to c.locals so localfind sees it within this
// block) and pin a parallel entry in c.atlocals so the
// allocation survives cgblock save/restore.
let off: i32 = localalloc(c, name, sz, tnode);
let at: *local = amalloc(c.a, 48u64): *local;
at.name = name;
at.off = off;
at.sz = asz;
at.tnode = tnode;
at.lnext = c.atlocals;
c.atlocals = at;
return off;
};
};
return localalloc(c, name, sz, tnode);
};
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;
};
// @-prefix scratch slots survive cgblock save/restore via
// c.atlocals; a localfindnode from a sibling/outer block must
// still resolve them.
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (streq(a.name, name)) { return a; };
a = a.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;
};
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (streq(a.name, name)) { return a.off; };
a = a.lnext;
};
};
};
return 0;
};
// ---- emit helpers ---------------------------------------------------
// Cgfn defers its prologue (TEXT / SUBQ) until after the body so the
// frame size reflects every emit-time localadd — the scanlocals pre-
// pass that previously pre-computed it was dropped per #15/#26c. The
// body is captured into cgoutbuf while cgoutmode != 0, then flushed
// after the prologue is written to stdout. Module-level state so the
// existing emitline/emitint/emitlabel/emitsymname callers don't have
// to thread a *cgen they don't already hold. Mirrors cstage's deferred
// Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up
// front and patches `from.offset` after the body finishes).
let cgoutbuf: *u8 = nil;
let cgoutbufcap: i32 = 0;
let cgoutbuflen: i32 = 0;
let cgoutmode: i32 = 0;
let cgoutarena: *arena = nil;
def CGOUT_INIT_CAP: i32 = 65536;
fn cgout_grow(need: i32) void = {
if (need <= cgoutbufcap) { return; };
let want: i32 = cgoutbufcap;
if (want == 0) { want = CGOUT_INIT_CAP; };
for (want < need) { want = want * 2; };
let p: *u8 = amalloc(cgoutarena, want: u64): *u8;
let i: i32 = 0;
for (i < cgoutbuflen) {
p[i] = cgoutbuf[i];
i += 1;
};
cgoutbuf = p;
cgoutbufcap = want;
};
fn cgout_enable(a: *arena) void = {
cgoutarena = a;
cgoutbuflen = 0;
cgoutmode = 1;
};
fn cgout_disable() void = { cgoutmode = 0; };
fn cgout_flush() void = {
if (cgoutbuflen > 0) {
os.write(1, cgoutbuf, cgoutbuflen: u64);
cgoutbuflen = 0;
};
};
fn emitbytes(p: *u8, n: u64) void = {
if (cgoutmode != 0) {
let nn: i32 = n: i32;
cgout_grow(cgoutbuflen + nn);
let i: i32 = 0;
for (i < nn) {
cgoutbuf[cgoutbuflen + i] = p[i];
i += 1;
};
cgoutbuflen += nn;
} else {
os.write(1, p, n);
};
};
fn emitline(s: str) void = { emitbytes(s.ptr, s.len: u64); };
fn emitint(v: i64) void = {
let s: str = strconv.i64tos(v, strconv.base.DEC);
emitbytes(s.ptr, s.len: u64);
};
fn emituint(v: u64) void = {
let s: str = strconv.u64tos(v, strconv.base.DEC);
emitbytes(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 "<module>.<fnname>_<prefix>_<seq>" (bare
// "<fnname>_..." when curmod is empty). Returns an arena-owned str.
// Mirrors C cgen's mklabel so diffs match. Module-qualified to
// avoid cross-module same-leaf collisions (task #13); w6a accepts
// '.' in label-cont (lex.c:18).
fn mklabel(c: *cgen, prefix: str) str = {
let buf: [128]u8;
let i: i32 = 0;
let mname: str = c.curmod;
let j: i32 = 0;
for (j < mname.len) {
buf[i] = mname[j];
i += 1; j += 1;
};
if (mname.len > 0) { buf[i] = 46u8; i += 1; }; // '.'
let fname: str = c.fnname;
j = 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 = {
emitbytes(s.ptr, s.len: u64);
emitline(":\n");
};
// mkscratchname — fresh local-slot name ".<prefix>_<labelseq>". 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_<seq>".
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; };
emitbytes( 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; };
emitbytes( bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
emitbytes( 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;
};
// Same helper as emitdefconstants (#24)
// — widens the gate so N_UN over an
// int leaf folds. `let x: i8 = -1i8;`
// arrives as N_UN(TK_MINUS, N_INTLIT)
// after the typed-AST cast peel.
ok = foldintliteral(r, &v);
};
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),");
emitbytes( 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.
// Element fold goes through foldintliteral (same
// helper as emitdefconstants / scalar arm above)
// so `-1i8` and friends emit their two's-complement
// bytes after the leading N_CAST peel — pre-#19
// this arm only matched bare N_INTLIT/N_RUNELIT and
// silently emitted zero for unfoldable elements.
// `...` (N_FIELD with str="...") repeats the last
// folded value across the remaining slots.
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 last: u64 = 0u64;
let inrepeat: bool = false;
for (i < alen) {
let v: u64 = last;
if (!inrepeat && e != nil) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
inrepeat = true;
} else {
e = e.next;
};
} else {
let ev: *node = e;
for (ev != nil) {
if (ev.kind != nkind.N_CAST) { break; };
ev = ev.lhs;
};
if (!foldintliteral(ev, &v)) { v = 0u64; };
last = 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.nmod.len > 0) {
emitbytes( d.nmod.ptr, d.nmod.len: u64);
emitbytes( ".".ptr, 1u64);
};
};
let nm: str = d.str;
emitbytes( 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; };
emitbytes( 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; };
emitbytes( bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
emitbytes( 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;
emitbytes( 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; };
emitbytes( 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; };
emitbytes( bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
emitbytes( 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,
fmod: 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, 64u64): *fnret;
f.fname = d.str;
f.fmod = d.nmod;
f.rtype = d.lhs;
f.params = d.list;
f.frnext = c.fnrets;
c.fnrets = f;
};
d = d.next;
};
};
// fnretlookup — declared return-type node for a fn by leaf name, or nil
// if the name isn't a registered fn. Same-module-first walk before the
// head-walk fallback. Eighth and final leaf of the trio graduation (#4e)
// mirroring aliaslookup (#27), fnret/fnparamslookupmod (#28/#31),
// enum/struct/deflookup (#4a/#4b/#4c), fnparamslookup (#4d): without
// the prefer pass a bare-leaf `foo()` call site in module M (N_IDENT
// callee) silently picks another module's same-leaf `foo` from the
// head of c.fnrets, then every downstream consumer keying on the
// return type (str-pair shuffle, tagged-union ABI, tuple destructure,
// float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires
// against the wrong-module shape.
fn fnretlookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return f.rtype; };
};
f = f.frnext;
};
f = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) { return f.rtype; };
f = f.frnext;
};
return nil;
};
// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)`
// callees route here so a leaf collision (same fn name exported from
// multiple modules) resolves to the explicit module. Falls back to the
// first leaf match if no matching module is registered. Mirror of
// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch
// picks the last-declared `next` regardless of qualifier, so a 4-arm
// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves
// the scrutinee tagged type to `(rune | done)` — flatvariantidx then
// can't see arms 2/3 and collapses them onto tag 0 (task #31).
fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.rtype; };
};
f = f.frnext;
};
};
return fnretlookup(c, name);
};
// fnparamslookup — head of the declared param-list for a fn, or nil
// if the name isn't a registered fn. Same-module-first walk before the
// head-walk fallback. Trio-leaf graduation (#4d) mirroring aliaslookup
// (#27), fnret/fnparamslookupmod (#28/#31), enum/struct/deflookup
// (#4a/#4b/#4c): without the prefer pass a bare-leaf `foo(x)` call in
// module M (callee N_IDENT) silently picks another module's same-leaf
// `foo` from the head of c.fnrets, then pushargsrev's widening
// detection fires (or doesn't) against the wrong param-type — `foo(7)`
// against a same-leaf `(i32 | void)` param re-layouts 7 into a 2-word
// tagged slot vs the same-module `i32` param's single push.
fn fnparamslookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return f.params; };
};
f = f.frnext;
};
f = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) { return f.params; };
f = f.frnext;
};
return nil;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- 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,
dmod: str, // originating module (`// MODULE: foo`), or empty
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, 64u64): *defent;
e.dname = d.str;
e.dmod = d.nmod;
e.drhs = d.rhs;
e.dnext = c.defs;
c.defs = e;
};
d = d.next;
};
};
// Same-module-first walk, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is
// invariant either way, but the structural shape mirrors deflookuprhs
// where the entry's drhs IS module-sensitive.
fn deflookup(c: *cgen, name: str) bool = {
let e: *defent = c.defs;
for (e != nil) {
if (streq(e.dname, name)) {
if (streq(e.dmod, c.curmod)) { return true; };
};
e = e.dnext;
};
e = c.defs;
for (e != nil) {
if (streq(e.dname, 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. Same-module-first walk: without the prefer pass
// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's
// same-leaf `def MSG: str = ...` sitting at the head of c.defs and
// inline the wrong strlit. 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) {
if (streq(e.dname, name)) {
if (streq(e.dmod, c.curmod)) { return e.drhs; };
};
e = e.dnext;
};
e = c.defs;
for (e != nil) {
if (streq(e.dname, name)) { return e.drhs; };
e = e.dnext;
};
return nil;
};
// deflookuprhsmod — same-module-first walk for `mod.NAME` references.
// Trio-leaf *mod variant mirroring fnretlookupmod (#31) / fnparamslookupmod
// (#28) / enumlookupmod (#4a). Module-qualified `alpha.MSG` from a third
// module needs the explicit alpha hint; deflookuprhs prefers c.curmod
// (which doesn't match either source module on a 3rd-module qualifier)
// and falls back to head-pick, possibly inlining beta.MSG's strlit when
// both alpha and beta declare same-leaf str defs. cgdot's mod-qualified
// str-def value-load routes here so a cross-module N_DOT collision
// resolves to the explicit module. Falls back to deflookuprhs's bare-
// leaf two-pass when no module matches.
fn deflookuprhsmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let e: *defent = c.defs;
for (e != nil) {
if (streq(e.dname, name)) {
if (streq(e.dmod, mod)) { return e.drhs; };
};
e = e.dnext;
};
};
return deflookuprhs(c, name);
};
// ---- module-private symbol map --------------------------------------
//
// Every non-FFI top-level fn decl lives in its module's namespace —
// cgen mangles the leaf to `<module>.<name>` 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
nmod: 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.nmod.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.nmod = d.nmod;
m.mnext = c.mods;
c.mods = m;
};
};
};
};
if (d.kind == nkind.N_DEF) {
if (d.exported == 0) {
if (d.nmod.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.nmod = d.nmod;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == nkind.N_TYPEDECL) {
if (d.exported == 0) {
if (d.nmod.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.nmod = d.nmod;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == nkind.N_LET) {
if (d.exported == 0) {
if (d.nmod.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.nmod = d.nmod;
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.nmod; };
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.nmod.len > 0
&& streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
};
};
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.
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookup(c, ident);
if (mod.len > 0) {
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
emitbytes( 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) {
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
emitbytes( 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 "?";
};
// selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c.
//
// w6c = amd64 compiler. Read .ww, parse, codegen, emit Plan 9 amd64
// asm to stdout (or the file given by -o).
//
// w6c_ww -o file.s file.ww
//
// The cgen routines in selfhost/cmd/wcc/cgen.ww write directly to
// fd 1 via os.write(1, ...). For -o, we open the output file and
// dup2 it onto fd 1 before invoking cgfile. This is the same trick
// the bootstrap uses with shell redirection, just in-process.
package main;
import os;
import mem;
import tok;
import lex;
import ast;
import parse;
import typ;
import sym;
import check;
import cgen;
fn cstreq(a: *u8, lit: str) bool = {
let n: u64 = lit.len: u64;
let i: u64 = 0u64;
for (i < n) {
let li: i32 = i: i32;
if (a[i] != lit[li]) { return false; };
i += 1u64;
};
if (a[i] != 0u8) { return false; };
return true;
};
fn cstrlen(p: *u8) u64 = {
let n: u64 = 0u64;
for (p[n] != 0u8) { n += 1u64; };
return n;
};
// pathstr — view a NUL-terminated *u8 as a str. lib/os entrypoints
// take str post-task-#23; this bridges call sites that still hold
// C-string paths (argv entries, arena-allocated buffers).
fn pathstr(p: *u8) str = {
let r: str;
r.ptr = p;
r.len = cstrlen(p): i32;
return r;
};
fn slurp(path: *u8) (*u8, u64) = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return nil, 0u64; };
let szr: (i64 | os.oserror) = os.filesize(fd);
let n: i64 = 0i64;
match (szr) {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let nz: u64 = n: u64;
let buf: *u8 = os.alloc(nz + 1u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, nz);
os.close(fd);
let got: i64 = 0i64;
match (rr) {
case let v: i64 => got = v;
case let e: os.oserror => return nil, 0u64;
};
if (got != n) { return nil, 0u64; };
buf[nz] = 0u8;
return buf, nz;
};
export fn main(argc: i32, argv: **u8) i32 = {
let src: *u8 = nil;
let out: *u8 = nil;
let i: i32 = 1;
for (i < argc) {
let a: *u8 = argv[i];
if (cstreq(a, "-o")) {
i += 1;
if (i >= argc) {
os.write(2, "w6c: -o requires arg\n".ptr, 20u64);
return 2;
};
out = argv[i];
} else { if (a[0u64] == 45u8) {
os.write(2, "w6c: unknown flag\n".ptr, 17u64);
return 2;
} else {
if (src != nil) {
os.write(2, "w6c: only one input\n".ptr, 19u64);
return 2;
};
src = a;
}; };
i += 1;
};
if (src == nil) {
os.write(2, "usage: w6c_ww [-o out.s] file.ww\n".ptr, 32u64);
return 2;
};
let buf: *u8;
let blen: u64;
buf, blen = slurp(src);
if (buf == nil) {
os.write(2, "w6c: cannot read input\n".ptr, 22u64);
return 1;
};
// Redirect fd 1 to the output file before any cgen emit runs.
// cgen.ww writes directly to fd 1; dup2 lets us reuse it without
// threading a file descriptor through the emit helpers.
if (out != nil) {
let ofd: i32 = os.open(pathstr(out),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (ofd < 0) {
os.write(2, "w6c: cannot open output\n".ptr, 23u64);
return 1;
};
if (os.dup2(ofd, 1i32) < 0) {
os.write(2, "w6c: dup2 failed\n".ptr, 16u64);
os.close(ofd);
return 1;
};
os.close(ofd);
};
let ar: *arena = newarena();
let nlen: u64 = cstrlen(src);
let fname: str = astrndup(ar, src, nlen);
let l: lex;
lexinit(&l, ar, fname, buf, blen);
let ps: parser;
parserinit(&ps, ar, &l);
let f: *node = parsefile(&ps);
// Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's
// `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches
// cgen and emits junk asm with a zero exit (silent miscompile).
if (l.errs > 0 || ps.errs > 0) { return 1; };
let cg: cgen;
cgeninit(&cg, ar);
cgfile(&cg, f);
return 0;
};