// 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; @symbol("rt_free") export fn free(p: *void, n: u64) void; @symbol("rt_abort") fn abort(msg: str) void; // Hare-style runtime check. Caller passes a message that's printed // to stderr before exit(1). export fn assert(cond: bool, msg: str) void = { if (!cond) { abort(msg); }; }; // Linux amd64 syscall numbers. Internal to this module — passed as // the first arg of syscall0..4 via libwwrt's rt_syscall trampoline. // `nr` is the type so the call sites can't accidentally pass an // arbitrary i64 (`syscall1(0i64, ...)` no longer typechecks). type nr = enum i64 { READ = 0, WRITE = 1, OPEN = 2, CLOSE = 3, LSEEK = 8, ACCESS = 21, DUP2 = 33, GETPID = 39, FORK = 57, EXECVE = 59, EXIT = 60, WAIT4 = 61, MKDIR = 83, RMDIR = 84, UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them // `fs::flag::RDONLY` etc; we use the same leaf names so callers say // `os.flag.RDONLY` and `os.flag.WRONLY | os.flag.CREATE`. export type flag = enum i32 { RDONLY = 0, WRONLY = 1, RDWR = 2, CREATE = 64, // 0x40 EXCL = 128, // 0x80 — pair with CREATE to fail on existing path TRUNC = 512, // 0x200 }; // lseek(2) whence. Hare names it `io::whence`. export type whence = enum i32 { SET = 0, CUR = 1, END = 2, }; export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; // 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 (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_malloc / rt_abort: a TEXT // symbol the linker resolves. The returned `**u8` is a NUL-terminated // table of `*u8` entries, each pointing at a NUL-terminated // "NAME=VALUE" byte sequence. // // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes // live in the kernel-supplied envp table at process entry. A future // `setenv` (separate task) that grows the table behind the scenes // would invalidate prior views; v1 has no setenv, so callers can // hold the view indefinitely. // // Mirrors Hare's os::tryenv shape (returns void rather than panicking // on missing). Hare also ships os::getenv (`(str | void)`) and // os::mustenv (panic-on-missing); ww collapses to the single // `(str | void)` form for now — consumers wanting "must" semantics // abort at the call site. // // Algorithm: walk the NUL-pointer-terminated `environ` table doing a // "name=" prefix match against each entry, byte-wise. NUL inside // `name` would never match a real env var (env var names cannot // contain '\0'), so we don't filter — POSIX puts that responsibility // on the caller. export fn getenv(name: str) (str | void) = { let envp: **u8 = rtenvp(); let i: i32 = 0; for (true) { let entry: *u8 = envp[i]; if (entry == nil: *u8) { return; }; let j: i32 = 0; let matched: bool = true; for (j < name.len) { if (entry[j] == 0u8) { matched = false; break; }; if (entry[j] != name[j]) { matched = false; break; }; j += 1; }; if (matched) { if (entry[name.len] == 61u8) { // '=' let val: *u8 = entry + ((name.len + 1): u64); let n: i32 = 0; for (val[n] != 0u8) { n += 1; }; let r: str; r.ptr = val; r.len = n; return r; }; }; i += 1; }; return; }; // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + // ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns // `filestat` by value; ww's cgreturn ABI tops out at 24B today (see // STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] / // [[fstat]] take an out-parameter and return `(void | oserror)`. // Re-evaluate the by-value shape when full sret lands. // // `filestat`, `mode`, and `stat_mask` live in lib/os because ww has // no lib/fs yet; Hare puts them in `fs::`. These types graduate to // lib/fs when that module ships — callers should expect a future // re-export. // // Underlying syscall is SYS_newfstatat (262), which unifies // stat/lstat/fstat through the `dirfd + flags` triple: // stat = newfstatat(AT_FDCWD, path, 0) // lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW) // fstat = newfstatat(fd, "", AT_EMPTY_PATH) // Avoiding SYS_statx — its 256B variable layout would buy btime, // but Hare's filestat doesn't expose btime either, so we stay on // the simpler 144B kernel struct. // fstatat(2) flag values. Linux constants from . // Names mirror Hare's ref/hare/sys/+linux/types.ha:45-51 (capital- // AT_ prefix, top-level `def`s). export def AT_FDCWD: i32 = -100; export def AT_SYMLINK_NOFOLLOW: i32 = 256; // 0x100 export def AT_EMPTY_PATH: i32 = 4096; // 0x1000 // mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/ // types.ha:63). Permission bits are the standard Unix octal subset; // type bits live in the S_IFMT = 0o170000 region. Type-bit test: // // let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask // if (t == os.mode.DIR as u32) { /* directory */ }; // // Numeric values are octal in Hare's source; ww has no octal // literals so they're written as decimal with the octal in a // trailing comment. export type mode = enum u32 { // permission bits USER_RWX = 448u32, // 0o700 USER_RW = 384u32, // 0o600 USER_RX = 320u32, // 0o500 USER_R = 256u32, // 0o400 USER_W = 128u32, // 0o200 USER_X = 64u32, // 0o100 GROUP_RWX = 56u32, // 0o070 GROUP_RW = 48u32, // 0o060 GROUP_RX = 40u32, // 0o050 GROUP_R = 32u32, // 0o040 GROUP_W = 16u32, // 0o020 GROUP_X = 8u32, // 0o010 OTHER_RWX = 7u32, // 0o007 OTHER_RW = 6u32, // 0o006 OTHER_RX = 5u32, // 0o005 OTHER_R = 4u32, // 0o004 OTHER_W = 2u32, // 0o002 OTHER_X = 1u32, // 0o001 SETUID = 2048u32, // 0o4000 SETGID = 1024u32, // 0o2000 STICKY = 512u32, // 0o1000 // file-type bits (S_IFMT mask = 0o170000 = 61440) UNKNOWN = 0u32, FIFO = 4096u32, // 0o010000 CHR = 8192u32, // 0o020000 DIR = 16384u32, // 0o040000 BLK = 24576u32, // 0o060000 REG = 32768u32, // 0o100000 LINK = 40960u32, // 0o120000 SOCK = 49152u32, // 0o140000 }; // stat_mask — which filestat fields the call populated. Mirrors // Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills // every field, so [[stat]] / [[lstat]] / [[fstat]] always set all // seven bits OR-folded (see [[fillfilestat]]); per-bit testing is // the documented sparse-backend pattern (cf. Hare's fs::fs network // backends that only populate mtime+size). export type stat_mask = enum u32 { UID = 1u32, GID = 2u32, SIZE = 4u32, INODE = 8u32, ATIME = 16u32, MTIME = 32u32, CTIME = 64u32, }; // 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; }; // rt — runtime primitives exposed to ww programs. // Mirrors Hare's rt:: module placement (ref/hare/rt/). package rt; // malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a // `*void`; callers cast to the target type. Diverges from Hare: Hare // exposes `alloc` / `free` as typed language builtins 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_malloc 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_malloc 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 (task #39). ref/hare/rt/malloc.ha:27. @symbol("rt_malloc") export fn malloc(n: u64) *void; // 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; def U8_MIN: u8 = 0; def U16_MIN: u16 = 0; def U32_MIN: u32 = 0; def U64_MIN: u64 = 0; // int/uint are machine-word (Go-style, type.c:58); limits derived from // size(int) per #114 + user ruling; cf Go math.MaxInt; diverges from // Hare's per-arch literal (arch+x86_64.ha) because ww's int is 64-bit. def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; def INT_MIN: int = -1 << (size(int)*8 - 1); def UINT_MIN: uint = 0; def UINT_MAX: uint = ~(0: uint); // size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). def SIZE_MIN: size = U64_MIN; def SIZE_MAX: size = U64_MAX; // uintptr not in the unsigned class, so the cast is required (Hare's form). def UINTPTR_MIN: uintptr = U64_MIN: uintptr; def UINTPTR_MAX: uintptr = U64_MAX: uintptr; def RUNE_MIN: rune = '\0'; // 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; }; // ltrim — borrowed view of `in` with leading bytes in `trim` stripped. // `trim` must be non-empty. ref/hare/bytes/trim.ha:7. export fn ltrim(in: []u8, trim: u8...) []u8 = { os.assert(trim.len > 0, "bytes.ltrim called with empty trim set"); let i: i32 = 0; for (i < in.len && contains(trim, in[i])) { i += 1; }; let r: []u8; r.ptr = in.ptr + (i: u64); r.len = in.len - i; r.cap = r.len; return r; }; // rtrim — borrowed view of `in` with trailing bytes in `trim` stripped. // `trim` must be non-empty. ref/hare/bytes/trim.ha:17. Hare's loop uses // `size` underflow at i==0 to terminate; ww indices are signed i32, so // the equivalent termination is spelled `i >= 0` explicitly. export fn rtrim(in: []u8, trim: u8...) []u8 = { os.assert(trim.len > 0, "bytes.rtrim called with empty trim set"); let i: i32 = in.len - 1; for (i >= 0 && contains(trim, in[i])) { i -= 1; }; let r: []u8; r.ptr = in.ptr; r.len = i + 1; r.cap = r.len; return r; }; // trim — borrowed view of `in` with both ends in `trim` stripped. // ref/hare/bytes/trim.ha:27. export fn trim(in: []u8, trim: u8...) []u8 = { return ltrim(rtrim(in, trim...), trim...); }; // 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 . 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; // ref/hare/encoding/utf8/types.ha:12 — fixed message; `invalid` carries // no payload, so the rendering is constant. export fn strerror(err: invalid) str = { return "Invalid UTF-8"; }; // `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: // // - `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 rt; 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; }; // frombytes — borrowed str view of `in`. Pure reinterpret per // CLAUDE.md rule 9 carve-out; ref/hare/strings/utf8.ha:10. export fn frombytes(in: []u8) str = { let r: str; r.ptr = in.ptr; r.len = in.len; return r; }; // compare — three-way bytewise codepoint-order comparison. Return is // a sign (neg/zero/pos), not an index, so it tracks Hare's `int` // rather than the str-index i32 (#8). ref/hare/strings/compare.ha:12. export fn compare(a: str, b: str) int = { 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]: int) - (b[i]: int); }; i += 1; }; return (a.len: int) - (b.len: int); }; // 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 = alloc([], s.len: u64)!; let i: i32 = 0; for (i < s.len) { buf[i] = s[i]; i += 1; }; buf.len = s.len; return frombytes(buf); }; // dupall — fresh `[]str` whose elements are independent copies of // `s`'s elements. Caller releases via [[freeall]]. // ref/hare/strings/dup.ha:26 (#6). // // Hare gates the per-element dup behind `?` and rolls back via // `defer if (!ok) freeall(newsl)`. ww has no `defer if`; more // importantly, ww's [[dup]] is still unchecked (returns plain `str`, // aborts via os.alloc on OOM — see top-of-file divergence note), // so the only nomem propagation point is the initial slice alloc. // With no inner failure path, the rollback is structurally a no-op // and is omitted; it returns once dup graduates to `(str | nomem)` // (#46). The pre-allocated slice has `cap == s.len`, so appendstr's // rt_ensure call never reaches the grow branch. // // Empty input bypasses the alloc: rt_malloc(0) is an mmap of 0 bytes // which returns -EINVAL, and the alloc-slice `?` shortcut routes // that through nomem — Hare's heap allocator hands back a sentinel // instead (#47). Return `{nil, 0, 0}` directly so callers get the // Hare-observable shape (len==0, freeall is a no-op via cap==0). export fn dupall(s: []str) ([]str | nomem) = { if (s.len == 0) { let r: []str; r.ptr = nil: *str; r.len = 0; r.cap = 0; return r; }; let newsl: []str = alloc([], s.len)?; let i: i32 = 0; for (i < s.len) { appendstr(&newsl, dup(s[i])); i += 1; }; return newsl; }; // 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 * size(str)` — the literal // would drift under #1's str-layout bump, so route through the // typ.ww SSoT. 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) * size(str): u64); }; }; // 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 = alloc([], total: u64)!; 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; }; buf.len = total; return frombytes(buf); }; // 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 = alloc([], total: u64)!; 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; }; buf.len = total; return frombytes(buf); }; // utf8bytelenbounded — walk `it` forward `end` runes and return the // resulting byte offset. ref/hare/strings/sub.ha:10. Aborts on // short input per Hare's contract for the rune-wise [[sub]]. fn utf8bytelenbounded(it: *iterator, end: i32) i32 = { let i: i32 = 0; for (i < end) { match (next(it)) { case let r: rune => void; case utf8.done => abort("strings.sub: index exceeds string length"); }; i += 1; }; return it.offs; }; // sub — borrowed substring [start, end) where start/end are rune // indices. ref/hare/strings/sub.ha:30. Hare's 2-arg `sub(s, start)` // defaulting end=END is omitted: ww has no default-parameter syntax // (filed as #37). Byte-indexed counterpart: [[bytesub]]. export fn sub(s: str, start: i32, end: i32) str = { os.assert(start <= end, "strings.sub: start is higher than end"); let it: iterator = iter(s); let starti: i32 = utf8bytelenbounded(&it, start); let endi: i32 = utf8bytelenbounded(&it, end - start); let r: str; r.ptr = s.ptr + (starti: u64); r.len = endi - starti; return r; }; // bytesub — borrowed substring [start, end) where start/end are byte // offsets. ref/hare/strings/sub.ha:59 (#7). Returns `utf8.invalid` if // either endpoint lands on a continuation byte (would split a // codepoint); the equivalent Hare predicate is `s[i] & 0xc0 == 0x80` // at ref/hare/strings/sub.ha:72-73. export fn bytesub(s: str, start: i32, end: i32) (str | utf8.invalid) = { os.assert(start <= end, "strings.bytesub: start is higher than end"); os.assert(end <= s.len, "strings.bytesub: end exceeds string length"); if (start < s.len) { if ((s[start] & 0xC0u8) == 0x80u8) { let e: utf8.invalid; return e; }; }; if (end < s.len) { if ((s[end] & 0xC0u8) == 0x80u8) { let e: utf8.invalid; return e; }; }; let r: str; r.ptr = s.ptr + (start: u64); r.len = end - start; 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); }; // indexstring — str-arm of [[index]]. Dual-rune-iterator walk: at each // candidate rune index `i`, compare `haystack` from that position // against `needle` rune-by-rune until needle is exhausted (match) or // a mismatch / haystack-exhaustion breaks the inner loop. Mirrors // ref/hare/strings/index.ha:59 (#10). Hare copies `rest_iter = s_iter` // directly via struct assignment; ww re-seats `rest_iter` field-wise // because the let-init struct-copy form diverges between cstage and // wwstage on this iterator type (993_ww_ww + 995_self_rebuild fail, // filed as #41) and rule #10 (CLAUDE.md) forbids stage asymmetry. fn indexstring(haystack: str, needle: str) (i32 | void) = { let s_iter: iterator = iter(haystack); let i: i32 = 0; for (true) { let rest_iter: iterator; rest_iter.src = s_iter.src; rest_iter.offs = s_iter.offs; rest_iter.reverse = s_iter.reverse; let needle_iter: iterator = iter(needle); let matched: bool = false; for (true) { let rest_done: bool = false; let rest_r: rune; match (next(&rest_iter)) { case let r: rune => rest_r = r; case utf8.done => rest_done = true; }; let needle_done: bool = false; let needle_r: rune; match (next(&needle_iter)) { case let r: rune => needle_r = r; case utf8.done => needle_done = true; }; if (rest_done && !needle_done) { break; }; if (needle_done) { matched = true; break; }; if (rest_r != needle_r) { break; }; }; if (matched) { return i; }; match (next(&s_iter)) { case let r: rune => i += 1; case utf8.done => return; }; }; return; }; // index — rune-wise offset of `needle`'s first occurrence in // `haystack`, or void if absent. ref/hare/strings/index.ha:10. The // str-arm delegates to [[indexstring]] (dual-iterator rune-by-rune // walk per Hare's `index_string`, #10); 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 => return indexstring(haystack, s); 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; }; // whitespace — ASCII whitespace set used by the 0-arg ltrim/rtrim/trim // branches (#9). ref/hare/strings/trim.ha:6. let whitespace: [4]u8 = [0x20u8, 0x0Au8, 0x09u8, 0x0Du8]; // ltrim — strip leading runes that occur in `trim`. Borrowed view. // 0-arg strips ASCII whitespace via [[bytes.ltrim]] (#9). // ref/hare/strings/trim.ha:11. The spread expression is inlined // because `let ws: []u8 = whitespace[0:4]` produces a slice whose // ptr doesn't track the module-level array storage (filed as #40); // `b.flush = flushdefault[0:1]` in lib/bufio is the same shape via // the working field-assign path. export fn ltrim(input: str, trim: rune...) str = { if (trim.len == 0) { return frombytes(bytes.ltrim(toutf8(input), whitespace[0:4]...)); }; 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. // 0-arg strips ASCII whitespace via [[bytes.rtrim]] (#9). Spread is // inlined to dodge #40 — see [[ltrim]]. // ref/hare/strings/trim.ha:32. export fn rtrim(input: str, trim: rune...) str = { if (trim.len == 0) { return frombytes(bytes.rtrim(toutf8(input), whitespace[0:4]...)); }; 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 frombytes(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 frombytes(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 frombytes(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 frombytes(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 frombytes(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, size(str): u64); 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) * size(str): u64)`. // // 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); }; // lpad — left-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Length comparison is BYTES, mirroring Hare's `len(s) >= maxlen` // at ref/hare/strings/pad.ha:9. A multibyte `p` whose encoded width // doesn't divide `maxlen - s.len` evenly leaves a trailing pad byte // pair sliced mid-codepoint at byte `maxlen-1`, exactly as Hare's // `res[..maxlen]` does (ref/hare/strings/pad.ha:20). When // `(maxlen - s.len) * pad.len >= maxlen` (multibyte pad overflows the // budget), `s` is entirely sliced off — same as Hare. Caller releases // with `os.free(r.ptr, r.len: u64)`. Hare's `nomem` return is dropped: // `os.alloc` aborts on OOM. Buf size == r.len keeps the free-contract // shape of [[dup]] / [[concat]] / [[join]]; Hare's `alloc([], maxlen)!` // over-allocs via append then slices, but Hare's slice-free recovers // the true capacity from the heap allocator (rt/ensure.ha:24), which // ww's munmap-based `os.free` cannot do. export fn lpad(s: str, p: rune, maxlen: i32) str = { if (s.len >= maxlen) { return dup(s); }; let scratch: [4]u8; let pad: []u8 = runebytes(scratch[0:4], p); let buf: []u8 = alloc([], maxlen: u64)!; let padwrite: i32 = (maxlen - s.len) * pad.len; if (padwrite > maxlen) { padwrite = maxlen; }; let off: i32 = 0; for (off < padwrite) { buf[off] = pad.ptr[off % pad.len]; off += 1; }; let k: i32 = 0; let srem: i32 = maxlen - off; if (srem > s.len) { srem = s.len; }; for (k < srem) { buf[off + k] = s[k]; k += 1; }; buf.len = maxlen; return frombytes(buf); }; // replace — fresh allocation of `s` with every non-overlapping // occurrence of `needle` replaced by `target`. Caller releases with // `os.free(r.ptr, r.len: u64)`. ref/hare/strings/replace.ha:8 (#4). // // Hare delegates to [[multireplace]] with a single pair; ww has no // `(str, str)` variadic shape today (#39), so this is a standalone // two-pass implementation: pass 1 counts matches to size the result, // pass 2 copies chunks and `target` into a single fresh buffer. // Single nomem path (the `alloc([], total)?`) preserves Hare's // signature without a per-write `append(...)?` (ww's append builtin // aborts on OOM, #11). Empty `needle` would hasprefix-match every // position with a zero stride — same infinite loop Hare exhibits at // ref/hare/strings/replace.ha:31; not gated. export fn replace(s: str, needle: str, target: str) (str | nomem) = { let sb: []u8 = toutf8(s); let nb: []u8 = toutf8(needle); let tb: []u8 = toutf8(target); let count: i32 = 0; let i: i32 = 0; for (i < sb.len) { if (bytes.hasprefix(sb[i:sb.len], nb)) { count += 1; i += nb.len; } else { i += 1; }; }; let total: i32 = sb.len + count * (tb.len - nb.len); if (total == 0) { let r: str; r.ptr = nil; r.len = 0; return r; }; let res: []u8 = alloc([], total)?; let off: i32 = 0; i = 0; for (i < sb.len) { if (bytes.hasprefix(sb[i:sb.len], nb)) { let j: i32 = 0; for (j < tb.len) { res.ptr[off + j] = tb.ptr[j]; j += 1; }; off += tb.len; i += nb.len; } else { res.ptr[off] = sb.ptr[i]; off += 1; i += 1; }; }; res.len = total; return frombytes(res); }; // rpad — right-pad `s` with `p` rune until the result reaches `maxlen` // bytes. Symmetric with [[lpad]]. ref/hare/strings/pad.ha:39. export fn rpad(s: str, p: rune, maxlen: i32) str = { if (s.len >= maxlen) { return dup(s); }; let scratch: [4]u8; let pad: []u8 = runebytes(scratch[0:4], p); let buf: []u8 = alloc([], maxlen: u64)!; let k: i32 = 0; for (k < s.len) { buf[k] = s[k]; k += 1; }; let padwrite: i32 = maxlen - s.len; let i: i32 = 0; for (i < padwrite) { buf[s.len + i] = pad.ptr[i % pad.len]; i += 1; }; buf.len = maxlen; return frombytes(buf); }; // strconv — arbitrary-precision decimal engine for float↔string // conversion. Mirrors ref/hare/strconv/decimal.ha (Hare in turn ports // Go's lib/strconv/decimal.go). Pure integer arithmetic; no f32/f64 // references (#121 residual-guard SAFE). // // Spelling divergences from Hare (mechanical, ww-side parser shape): // - Hare `let a = X, b = Y;` → two single `let` statements // (ww parser doesn't accept comma-separated bindings). // - Hare `tbl[lo..]` open-ended slice → direct indexing // `tbl[lo + i]` at point-of-use (equivalent algorithm; no // allocation, no aliasing). ww `[lo:hi]` uses `:`; `..` form // is not parsed. // - Hare `0z`/`1z` size literals → ww has no `z` suffix; pre-bind // `let SZ_ZERO: size = (0u64: size);` etc. at function entry // ("hoisted size casts as local consts" — ww `T: type` casts // embedded inside expressions confuse the parser). // - Hare `~0u64` typed-suffix literal → ww parser rejects `~` on // typed-suffix; route via a named zero local + `~zero`. // - Hare `for (cond; afterthought)` 2-clause → ww 3-clause // `for (init; cond; post)` (when continue is used; the post // must run each iteration) or inline-the-afterthought in body // (when no continue exists in the loop). // - Hare `fn foo() T = if (cond) {...} else expr;` expression body // → ww requires a `{}` block body throughout. // - Hare bare `assert(cond)` builtin → `os.assert(cond, msg)`; // wwstage cgen has no `assert` intercept (deferred fold). // - In-file instances of the above hoist pattern: `i_sz` (line 93) // hoists a per-iteration size cast out of a for-loop comparison // (bullet 3 sub-case — the size-cast hoist applied inside a loop // body, not just at function entry); `lowbit_lit` (line 242) // decomposes Hare's `(nd > 0 && d.digits[nd - 1] & 1 != 0)` into // a stepwise boolean local to dodge ww parser precedence on mixed // `&` / `&&` / `!=` within a single expression. // // CGEN class closures consumed (post-prereqs): // - #131 (4acab6e) — `len(d.digits)` compile-time-folds cs==ww // - #134 (36bf603) — `d.digits[nd] >= 5u8` picks JAE (unsigned) // - #133 (3986818) — `d.digits[i] += 1u8` load-op-store BOTH // stages // - #135 (ade6840) — `(*d).digits[i]` read+write N_DOT-base addr // // Drew CGEN-SAFE invariants: // - #129: module-level decls here are integer-literal defs only. // - #128: digits is fundamental [800]u8, zero-init only. // - #121: zero float ops. // - Drew watch-item `*d = decimal{...};` reset (line 110 in Hare): // pointer-deref reset to composite-literal probed cs==ww // byte-id safe. package strconv; import os; // ref/hare/strconv/decimal.ha:5. def maxshift: u8 = 60u8; // ref/hare/strconv/decimal.ha:6. def decimal_point_range: u16 = 2047u16; // ref/hare/strconv/decimal.ha:8-26. Field layout 1:1. The 800-digit // bound covers subnormal doubles (min exp -1074, max mantissa 4e16 // → at most 767 digits; 800 leaves headroom). export type decimal = struct { digits: [800]u8, nd: size, dp: i32, negative: bool, truncated: bool, }; // ref/hare/strconv/decimal.ha:29-33. Strip trailing zeros. fn trim(d: *decimal) void = { let SZ_ZERO: size = (0u64: size); let SZ_ONE: size = (1u64: size); for (d.nd > SZ_ZERO && d.digits[d.nd - SZ_ONE] == 0u8) { d.nd -= SZ_ONE; }; }; // ref/hare/strconv/decimal.ha:35-55. Compute the digit-count // increase for a left-shift `shift` (consults left_shift_table + // pow5_table from stof_data.ww, bb6f840). Uses `continue` so the // loop stays in 3-clause form for byte-id-correct post-increment. fn leftshift_newdigits(d: *decimal, shift: u32) u32 = { shift &= 63u32; let x_a: u32 = (left_shift_table[shift]: u32); let x_b: u32 = (left_shift_table[shift + 1u32]: u32); let nn: u32 = x_a >> 11u32; let pow5_a: u32 = 0x7FFu32 & x_a; let pow5_b: u32 = 0x7FFu32 & x_b; let n: u32 = pow5_b - pow5_a; for (let i: u32 = 0u32; i < n; i += 1u32) { let i_sz: size = (i: size); if (i_sz >= d.nd) { return nn - 1u32; } else if (d.digits[i] == pow5_table[pow5_a + i]) { continue; } else if (d.digits[i] < pow5_table[pow5_a + i]) { return nn - 1u32; } else { return nn; }; }; return nn; }; // ref/hare/strconv/decimal.ha:57-91. Shift `d` left by k bits. fn leftshift(d: *decimal, k: u32) void = { let SZ_ONE: size = (1u64: size); let SZ_BOUND: size = (len(d.digits): size); let kU64: u64 = (k: u64); let MAXSHIFT_U32: u32 = (maxshift: u32); os.assert(k <= MAXSHIFT_U32, "strconv.leftshift: k > maxshift"); if (d.nd == (0u64: size)) { return; }; let nn: u32 = leftshift_newdigits(d, k); let r: int = (d.nd: int) - 1; let w: size = (r: size) + (nn: size); let n: u64 = 0u64; for (r >= 0) { n += (d.digits[r]: u64) << kU64; let quo: u64 = n / 10u64; let rem: u64 = n - 10u64 * quo; if (w < SZ_BOUND) { d.digits[w] = (rem: u8); } else if (rem != 0u64) { d.truncated = true; }; n = quo; r -= 1; w -= SZ_ONE; }; for (n > 0u64) { let quo: u64 = n / 10u64; let rem: u64 = n - 10u64 * quo; if (w < SZ_BOUND) { d.digits[w] = (rem: u8); } else if (rem != 0u64) { d.truncated = true; }; n = quo; w -= SZ_ONE; }; d.nd += (nn: size); if (d.nd > SZ_BOUND) { d.nd = SZ_BOUND; }; d.dp += (nn: i32); trim(d); }; // ref/hare/strconv/decimal.ha:93-134. Shift `d` right by k bits. // Two outer Hare 2-clause loops (`for (cond; r += 1)`) are inlined // as `for (cond) { ... r += SZ_ONE; }` since neither uses continue. fn rightshift(d: *decimal, k: u32) void = { let SZ_ZERO: size = (0u64: size); let SZ_ONE: size = (1u64: size); let SZ_BOUND: size = (len(d.digits): size); let kU64: u64 = (k: u64); let r: size = SZ_ZERO; let w: size = SZ_ZERO; let n: u64 = 0u64; for ((n >> kU64) == 0u64) { if (r >= d.nd) { if (n == 0u64) { d.nd = SZ_ZERO; return; }; for ((n >> kU64) == 0u64) { n *= 10u64; r += SZ_ONE; }; break; }; n = n * 10u64 + (d.digits[r]: u64); r += SZ_ONE; }; d.dp -= (r: i32) - 1; if (d.dp < -(decimal_point_range: i32)) { // Drew-watch-item: pointer-deref reset to composite // literal — probed cs==ww byte-id safe in pre-flight. *d = decimal { ... }; return; }; let mask: u64 = (1u64 << kU64) - 1u64; for (r < d.nd) { let dig: u64 = n >> kU64; n &= mask; d.digits[w] = (dig: u8); w += SZ_ONE; n = n * 10u64 + (d.digits[r]: u64); r += SZ_ONE; }; for (n > 0u64) { let dig: u64 = n >> kU64; n &= mask; if (w < SZ_BOUND) { d.digits[w] = (dig: u8); w += SZ_ONE; } else if (dig > 0u64) { d.truncated = true; }; n *= 10u64; }; d.nd = w; trim(d); }; // ref/hare/strconv/decimal.ha:138-153. Shift right (k < 0) or left // (k > 0). Hardware shifts cap at 60 bits without losing top // digits, so break large shifts into maxshift-sized chunks. fn decimal_shift(d: *decimal, k: int) void = { let MAXSHIFT_INT: int = (maxshift: int); let MAXSHIFT_U32: u32 = (maxshift: u32); if (d.nd == (0u64: size)) { return; }; if (k > 0) { for (k > MAXSHIFT_INT) { leftshift(d, MAXSHIFT_U32); k -= MAXSHIFT_INT; }; leftshift(d, (k: u32)); } else if (k < 0) { for (k < -MAXSHIFT_INT) { rightshift(d, MAXSHIFT_U32); k += MAXSHIFT_INT; }; rightshift(d, ((-k): u32)); }; }; // ref/hare/strconv/decimal.ha:155-160. Banker's rounding decision: // at the exact half (digit==5, no more digits) round to even (the // preceding digit's low bit decides); past-half rounds up; below- // half rounds down. Hare's expression-bodied `if` re-shaped as a // block per ww parser. fn should_round_up(d: *decimal, nd: uint) bool = { let nd_sz: size = (nd: size); let SZ_ONE: size = (1u64: size); let U_ONE: uint = (1u32: uint); let U_ZERO: uint = (0u32: uint); if (nd_sz < d.nd) { if (d.digits[nd] == 5u8 && (nd_sz + SZ_ONE) == d.nd) { let lowbit_lit: bool = false; if (nd > U_ZERO) { if ((d.digits[nd - U_ONE] & 1u8) != 0u8) { lowbit_lit = true; }; }; return d.truncated || lowbit_lit; } else { return d.digits[nd] >= 5u8; }; }; return false; }; // ref/hare/strconv/decimal.ha:162-166. Round to `nd` digits. fn round(d: *decimal, nd: uint) void = { if ((nd: size) >= d.nd) { return; }; if (should_round_up(d, nd)) { roundup(d, nd); } else { rounddown(d, nd); }; }; // ref/hare/strconv/decimal.ha:168-172. Truncate to `nd` digits. fn rounddown(d: *decimal, nd: uint) void = { if ((nd: size) >= d.nd) { return; }; d.nd = (nd: size); trim(d); }; // ref/hare/strconv/decimal.ha:174-186. Round up to `nd` digits; // propagate carry. If all 9s, the result is a single 1 with the // decimal point advanced. fn roundup(d: *decimal, nd: uint) void = { let SZ_ONE: size = (1u64: size); if ((nd: size) >= d.nd) { return; }; for (let i: int = (nd: int) - 1; i >= 0; i -= 1) { if (d.digits[i] < 9u8) { d.digits[i] += 1u8; d.nd = (i: size) + SZ_ONE; return; }; }; d.digits[0] = 1u8; d.nd = SZ_ONE; d.dp += 1; }; // ref/hare/strconv/decimal.ha:188-202. Read `d` as the integer // rounded to `d.dp` digits. Returns 0 if `d.dp <= 0`; returns // ~0u64 if `d.dp > 18` (exceeds u64 range). Hare's two 2-clause // loops (`for (cond; i += 1)`) are inlined per the spelling // divergence at file top. fn decimal_round(d: *decimal) u64 = { let SZ_ZERO: size = (0u64: size); let SZ_ONE: size = (1u64: size); if (d.nd == SZ_ZERO || d.dp < 0) { return 0u64; }; if (d.dp > 18) { // Hare's `~0u64` doesn't parse on a typed-suffix literal // in ww; route via a named zero. let zero: u64 = 0u64; return ~zero; }; let dp_sz: size = ((d.dp: uint): size); let i: size = SZ_ZERO; let n: u64 = 0u64; for (i < dp_sz && i < d.nd) { n = n * 10u64 + (d.digits[i]: u64); i += SZ_ONE; }; for (i < dp_sz) { n *= 10u64; i += SZ_ONE; }; if (should_round_up(d, (d.dp: uint))) { n += 1u64; }; return n; }; // floats — f64 classification, sign, bit-reinterpret core, and the f64 // decompose half (subnormal-normalize + frexp). Ported from // ref/hare/math/floats.ha (fold-1: classify/sign/bits; fold-2a: // issubnormalf64/normalizef64/frexpf64; strconv-foundation fold-1a: // F32 bit-layout + f32bits/f32frombits + floatinfo struct type; // fold-1b: NAN_BITS/INF_BITS sentinels; γ-cleanup: f64info/f32info // instances re-folded once #149 lowered &math.f64info). frexpf64's // zero guard `n == 0f64` rides the #103 // fix (no-decimal f64 literal now materialized into XMM) and its // (f64, i64) tuple return rides the #105 fix (tuple f64-word read). // The ldexp/modfrac/nextafter family stays deferred (need f64 DIVIDE). package math; // Returns the binary representation of the given f64. // ref/hare/math/floats.ha:5. Parens around &n are load-bearing: ww's `:` // cast binds tighter than unary `&`, so Hare's `*(&n: *u64)` would parse // as `*(&(n: *u64))`; `(&n): *u64` reinterprets the address as intended. export fn f64bits(n: f64) u64 = { return *((&n): *u64); }; // Returns the binary representation of the given f32. // ref/hare/math/floats.ha:8 export fn f32bits(n: f32) u32 = { return *((&n): *u32); }; // Returns f64 with the given binary representation. // ref/hare/math/floats.ha:11 export fn f64frombits(n: u64) f64 = { return *((&n): *f64); }; // Returns f32 with the given binary representation. // ref/hare/math/floats.ha:14 export fn f32frombits(n: u32) f32 = { return *((&n): *f32); }; // ref/hare/math/floats.ha:17,20,23 declare these as untyped int. ww has // no untyped def (every def carries a type) and routes shift/bitwise // through unify_arith, which rejects mixed operand types (cmd/wcc/ // check.c:769). The bit-structure consts are used only as u64 shift // amounts and mask widths, so they are typed u64 here — the closest // stand-in for Hare's untyped-int adapt at those use sites. // The number of bits in the significand of the binary representation of f64. export def F64_MANTISSA_BITS: u64 = 52; // The number of bits in the exponent of the binary representation of f64. export def F64_EXPONENT_BITS: u64 = 11; // The bias of the exponent of the binary representation of f64. Subtract this // from the exponent in the binary representation to get the actual exponent. export def F64_EXPONENT_BIAS: u64 = 1023; // Mask with each bit of an f64's mantissa set. // ref/hare/math/floats.ha:37 export def F64_MANTISSA_MASK: u64 = (1 << F64_MANTISSA_BITS) - 1; // Mask with each bit of an f64's exponent set. // ref/hare/math/floats.ha:40 export def F64_EXPONENT_MASK: u64 = (1 << F64_EXPONENT_BITS) - 1; // The mask that gets an f64's sign. // ref/hare/math/floats.ha:75 def F64_SIGN_MASK: u64 = 1u64 << 63; // Mask that clears an f64's exponent field, keeping sign + mantissa. // ref/hare/math/floats.ha:77. Hare hardcodes the 0x800FFFFFFFFFFFFF binary // literal because its lexer can't const-fold the expression; ww's #88 // def-const-fold can, so the readable form is kept. floats.ha:79's NOTE // expression has an `0u64 &` upstream typo (it would yield 0); the value it // documents is exactly ~(F64_EXPONENT_MASK << F64_MANTISSA_BITS). def F64_EXP_REMOVAL_MASK: u64 = ~(F64_EXPONENT_MASK << F64_MANTISSA_BITS); // The f64 bit pattern whose exponent field evaluates to zero (0.5 scale). // ref/hare/math/floats.ha:84 def F64_EXP_ZERO: u64 = (F64_EXPONENT_BIAS - 1) << F64_MANTISSA_BITS; // F32 bit-structure constants. ref/hare/math/floats.ha:27,30,33 declare // these as untyped int; ww has no untyped def, so they ride u32 (matching // the u32 bit container, the same way the F64 family rides u64 — see // the note above F64_MANTISSA_BITS). // The number of bits in the significand of the binary representation of f32. // ref/hare/math/floats.ha:27 export def F32_MANTISSA_BITS: u32 = 23u32; // The number of bits in the exponent of the binary representation of f32. // ref/hare/math/floats.ha:30 export def F32_EXPONENT_BITS: u32 = 8u32; // The bias of the exponent of the binary representation of f32. Subtract this // from the exponent in the binary representation to get the actual exponent. // ref/hare/math/floats.ha:33 export def F32_EXPONENT_BIAS: u32 = 127u32; // Mask with each bit of an f32's mantissa set. // ref/hare/math/floats.ha:43 export def F32_MANTISSA_MASK: u32 = (1u32 << F32_MANTISSA_BITS) - 1u32; // Mask with each bit of an f32's exponent set. // ref/hare/math/floats.ha:46 export def F32_EXPONENT_MASK: u32 = (1u32 << F32_EXPONENT_BITS) - 1u32; // The mask that gets an f32's sign. // ref/hare/math/floats.ha:87 def F32_SIGN_MASK: u32 = 1u32 << 31; // Mask that clears an f32's exponent field, keeping sign + mantissa. // ref/hare/math/floats.ha:92. Hare hardcodes the binary literal (its // lexer can't const-fold the expression); ww's #88 def-const-fold can, // so the readable form is kept (same call as F64_EXP_REMOVAL_MASK). def F32_EXP_REMOVAL_MASK: u32 = ~(F32_EXPONENT_MASK << F32_MANTISSA_BITS); // The f32 bit pattern whose exponent field evaluates to zero (0.5 scale). // ref/hare/math/floats.ha:95 def F32_EXP_ZERO: u32 = (F32_EXPONENT_BIAS - 1u32) << F32_MANTISSA_BITS; // floatinfo — IEEE-754 shape parameters for a binary float type, passed // to width-generic helpers in strconv (eisel_lemire, floatbits, hex_to_bits, // mkfloat). ref/hare/math/floats.ha:101. Hare's `int` maps to ww's `int` // (machine word, 8B; project_int_machine_word_derived_limits), so the // expbias field stays `int` — that keeps the fold-4 stof port byte-for-byte // against ref/hare/strconv/stof.ha:248,288 (`let e: int = 0` arithmetic // against `f.expbias` of the same type, no cast at use site). export type floatinfo = struct { // Bits in significand. mantbits: u64, // Bits in exponent. expbits: u64, // Bias of exponent. expbias: int, // Mask for mantissa. mantmask: u64, // Mask for exponent. expmask: u64, }; // floatinfo instances for the f64 / f32 types, consumed by the // width-generic strconv helpers via &math.f64info (cross-module // address-of, lowered since #149). ref/hare/math/floats.ha:117,126. // Hare spells the masks (1 << 52) - 1 / (1 << 23) - 1; the #129 A.2 // struct-composite static-init path folds only bare-literal field // initializers, not const-fold expressions, so the value-identical hex // literals are used here (0xFFFFFFFFFFFFF == (1<<52)-1, 0x7FFFFF == // (1<<23)-1 — same hex-literal style as the NAN_BITS/INF_BITS sentinels // below). expbias rides `int` (the field type) with no suffix. export def f64info: floatinfo = floatinfo { mantbits = 52u64, expbits = 11u64, expbias = 1023, mantmask = 0xFFFFFFFFFFFFFu64, expmask = 0x7FFu64, }; export def f32info: floatinfo = floatinfo { mantbits = 23u64, expbits = 8u64, expbias = 127, mantmask = 0x7FFFFFu64, expmask = 0xFFu64, }; // IEEE-754 quiet-NaN and positive-Infinity f64 bit sentinels. // ref/hare/math/floats.ha:137,141. Hare exports `def NAN = 0.0/0.0;` and // `def INF = 1.0/0.0;` (untyped float def-fold); ww's cgen doesn't lower // `def: f64 = expr;` (the symbol comes out undefined at link time — see // #129). Callers materialize the f64 sentinel via f64frombits(NAN_BITS) // / f64frombits(INF_BITS); same bit-exact value, one extra reinterpret. // 0x7FF8000000000000 is the IEEE-754 binary64 quiet-NaN (sign=0, exp= // all-ones, mantissa MSB=1, rest=0); 0x7FF0000000000000 is +Infinity // (sign=0, exp=all-ones, mantissa=0). Re-fold to `def NAN: f64 = ...` // when #129 closes (γ-cleanup pattern per amalloc-drop precedent). export def NAN_BITS: u64 = 0x7FF8000000000000u64; export def INF_BITS: u64 = 0x7FF0000000000000u64; // Returns true if the given floating-point number is NaN. // ref/hare/math/floats.ha:144 (Hare's expression body inlined into a // block: ww has no expression-bodied fn form, only brace blocks). export fn isnan(n: f64) bool = { return n != n; }; // Returns true if the given floating-point number is infinite. // ref/hare/math/floats.ha:147 export fn isinf(n: f64) bool = { const bits = f64bits(n); const mant = bits & F64_MANTISSA_MASK; const exp = bits >> F64_MANTISSA_BITS & F64_EXPONENT_MASK; return exp == F64_EXPONENT_MASK && mant == 0; }; // Returns true if the given f64 is subnormal. // ref/hare/math/floats.ha:179 export fn issubnormalf64(n: f64) bool = { const bits = f64bits(n); const mant = bits & F64_MANTISSA_MASK; const exp = bits >> F64_MANTISSA_BITS & F64_EXPONENT_MASK; return exp == 0 && mant != 0; }; // Returns the absolute value of f64 n. // ref/hare/math/floats.ha:195 export fn absf64(n: f64) f64 = { if (isnan(n)) { return n; }; return f64frombits(f64bits(n) & ~F64_SIGN_MASK); }; // Returns 1 if x is positive and -1 if x is negative. Note that zero is also // signed. // ref/hare/math/floats.ha:212 export fn signf64(x: f64) i64 = { if (f64bits(x) & F64_SIGN_MASK == 0) { return 1i64; } else { return -1i64; }; }; // Returns whether or not x is positive. // ref/hare/math/floats.ha:231 export fn ispositivef64(x: f64) bool = { return signf64(x) == 1i64; }; // Returns whether or not x is negative. // ref/hare/math/floats.ha:237 export fn isnegativef64(x: f64) bool = { return signf64(x) == -1i64; }; // Returns x, but with the sign of y. // ref/hare/math/floats.ha:243 export fn copysignf64(x: f64, y: f64) f64 = { return f64frombits((f64bits(x) & ~F64_SIGN_MASK) | (f64bits(y) & F64_SIGN_MASK)); }; // Takes a potentially subnormal f64 n and returns a normal f64 normal_float // and an exponent exp such that n == normal_float * 2^{exp}. // ref/hare/math/floats.ha:256 export fn normalizef64(n: f64) (f64, i64) = { if (issubnormalf64(n)) { const factor = 1i64 << (F64_MANTISSA_BITS: i64); const normal_float = (n * (factor: f64)); return (normal_float, -(F64_MANTISSA_BITS: i64)); }; return (n, 0); }; // Breaks a f64 down into its mantissa and exponent. The mantissa will be // between 0.5 and 1. // ref/hare/math/floats.ha:278 export fn frexpf64(n: f64) (f64, i64) = { if (isnan(n) || isinf(n) || n == 0f64) { return (n, 0); }; const normalized = normalizef64(n); const normal_float = normalized.0; const normalization_exp = normalized.1; const bits = f64bits(normal_float); const raw_exp: u64 = (bits >> F64_MANTISSA_BITS) & F64_EXPONENT_MASK; const exp: i64 = normalization_exp + (raw_exp: i64) - (F64_EXPONENT_BIAS: i64) + 1; const mantissa: f64 = f64frombits((bits & F64_EXP_REMOVAL_MASK) | F64_EXP_ZERO); return (mantissa, exp); }; // math — numeric helpers. Subset of Hare's math::; only the absolute- // value pair for the signed integer types we currently care about. The // return type is unsigned so that abs(I32_MIN) doesn't overflow. package math; export fn absi32(n: i32) u32 = { if (n < 0) { return (-n): u32; }; return n: u32; }; export fn absi64(n: i64) u64 = { if (n < 0) { return (-n): u64; }; return n: u64; }; // strconv — float→string via Ryū (shortest round-trippable decimal). // Mirrors ref/hare/strconv/ftos_ryu.ha (the algorithm core) + // ref/hare/strconv/ftos.ha:432 (the f64tos driver). Ryū: Ulf Adams, // https://doi.org/10.1145/3192366.3192369 — Hare translated it from the // reference C (https://github.com/ulfjack/ryu); ww follows Hare. // // SCOPE — the f64tos + f32tos shortest-representation subset (Hare's // ffmt::G, prec=void, fflags::NONE). f32tos (ftos.ha:448) + its f32 Ryū // sub-path (f32todecf32 + mulpow5inv/pow5_divpow2 + mulshift32 + the *32 // helpers, reusing the shared u64-core + the f64 SPLIT2 tables — the f32 // path has no separate tables, matching ftos_ryu.ha) ship here in fold-5b // (task #67): the gating #143 f32-arg-push cgen fix landed (aff7725, MOVSS // both stages), so f32tos's math.f32bits(n) call — passing an f32 arg — is // now byte-id-clean. One deferral remains: // - the parametric fftosf/ffmt/fflags/ftosf surface → task #64 (needs // io::handle/memio + a `(size|io::error)?` per appendrune (#158); // for G/void/NONE the ffmt/fflags/precision/multiprecision-fallback // machinery is provably dead code — `ok` is always true → init_dec/ // compute_round/round unreachable — which bootstrap-coverage rejects). // The lib note blesses "a documented subset". This file ships ZERO float // literals — Ryū is all bit/integer arithmetic on f64bits(n) — so the // wwdump TK_FLOAT embedding concern is moot. // // Decomposition divergences (the #163-166 tuple/struct-ABI cluster — // ww's partial tuple support miscompiles the shapes Hare uses; the // WORKING shapes, struct-RETURN + scalar-PARAMS, are this algorithm's // own idiom: ftos_ryu.ha:12 already uses `struct r128` not a tuple for // u128mul, and fold-4/stof.ww decomposed likewise): // - `mulshiftall64`'s tuple param `mul:(u64,u64)` → two scalar params // `mul0,mul1` (#163: tuple-as-param reads garbage); its 3-tuple // return `(u64,u64,u64)` → 24B struct `ryuv` (#164: 3-tuple return // reads 0; struct-RETURN is byte-id-clean — r128 precedent). NO // struct-as-PARAM anywhere (#165: 16B struct-param diverges cs≠ww). // - `f64computeinvpow5`/`f64computepow5` keep their 2-tuple `(u64,u64)` // return (call-return 2-tuple + `.0`/`.1` is byte-id-clean — the // math/floats.ww frexpf64 precedent). // - dead `mulshift64` (tuple-param, never called) + dead // `F32/F64_DECIMAL_DIGITS` dropped. // // Spelling divergences (mechanical, ww parser/cgen; cite ftos_ryu.ha): // - scalar-PARAM mutation (`m<<=1`, `value*=…`) → copy-to-local // (stof.ww hex_to_bits precedent). // - `&&=` → `x = x && y`. `ibool=if(b)1 else 0` expr-body → block. // comma `let a=…, b=…` → split. `if/else` expr-yield → pre-bound // local + block. `assert()` → `os.assert(cond,msg)`. // - 2D row-bind `mul=TBL[base]` → direct double-index `TBL[base][0/1]` // (#155 / #156, stof.ww eisel_lemire precedent). // - ibool's u8 result + the u8 BITCOUNT defs cast explicitly to u32/u64 // at each use (Hare promotes; ww is strict — int-machine-word note). // - a `(N: uint)` cast embedded inside an array subscript `[ ]` is // rejected by the ww parser → hoist to a named local before the // index (decimal.ww "hoist size casts" note); see init_dec_mant_exp // + encode_e_dec. package strconv; import math; import os; // ref/hare/strconv/ftos_ryu.ha:33. (hi:lo) >> s, low 64 bits. Hare's // "TODO: use 128-bit integers" — ww has no u128; pure-u64 decomposition. // (u128mul + the r128 struct live in stof.ww, fold-4's first consumer; // reused in-package here.) fn u128rshift(lo: u64, hi: u64, s: u32) u64 = { os.assert(s <= 64u32, "strconv.u128rshift: s > 64"); return (hi << (64u64 - (s: u64))) | (lo >> (s: u64)); }; // ref/hare/strconv/ftos_ryu.ha:39. Largest p with 5^p | value. fn pow5fac(v: u64) u32 = { let value: u64 = v; let m_inv_5: u64 = 14757395258967641293u64; // 5 * m_inv_5 == 1 (mod 2^64) let n_div_5: u64 = 3689348814741910323u64; let count: u32 = 0u32; for (true) { os.assert(value != 0u64, "strconv.pow5fac: value == 0"); value *= m_inv_5; if (value > n_div_5) { break; }; count += 1u32; }; return count; }; // ref/hare/strconv/ftos_ryu.ha:64. fn ibool(b: bool) u8 = { if (b) { return 1u8; }; return 0u8; }; // ref/hare/strconv/ftos_ryu.ha:66-67. fn pow5multiple(v: u64, p: u32) bool = { return pow5fac(v) >= p; }; // ref/hare/strconv/ftos_ryu.ha:69. fn pow2multiple(v: u64, p: u32) bool = { os.assert(v > 0u64, "strconv.pow2multiple: v == 0"); os.assert(p < 64u32, "strconv.pow2multiple: p >= 64"); return (v & ((1u64 << (p: u64)) - 1u64)) == 0u64; }; // ref/hare/strconv/ftos_ryu.ha:89. The (v+, v-rounded, v-) triple. // Decomposed: tuple param → mul0/mul1 scalars (#163); 3-tuple return → // this struct (#164). The `mm_shift==1` `if/else`-yield → pre-bound // `v_minus` + block. type ryuv = struct { vp: u64, vr: u64, vm: u64 }; fn mulshiftall64(m: u64, mul0: u64, mul1: u64, j: i32, mm_shift: u32) ryuv = { let mm: u64 = m << 1u64; let r0: r128 = u128mul(mm, mul0); let r1: r128 = u128mul(mm, mul1); let lo: u64 = r0.lo; let tmp: u64 = r0.hi; let mid: u64 = tmp + r1.lo; let hi: u64 = r1.hi + (ibool(mid < tmp): u64); let lo2: u64 = lo + mul0; let mid2: u64 = mid + mul1 + (ibool(lo2 < lo): u64); let hi2: u64 = hi + (ibool(mid2 < mid): u64); let v_plus: u64 = u128rshift(mid2, hi2, ((j - 64 - 1): u32)); let v_minus: u64 = 0u64; if (mm_shift == 1u32) { let lo3: u64 = lo - mul0; let mid3: u64 = mid - mul1 - (ibool(lo3 > lo): u64); let hi3: u64 = hi - (ibool(mid3 > mid): u64); v_minus = u128rshift(mid3, hi3, ((j - 64 - 1): u32)); } else { let lo3: u64 = lo + lo; let mid3: u64 = mid + mid + (ibool(lo3 < lo): u64); let hi3: u64 = hi + hi + (ibool(mid3 < mid): u64); let lo4: u64 = lo3 - mul0; let mid4: u64 = mid3 - mul1 - (ibool(lo4 > lo3): u64); let hi4: u64 = hi3 - (ibool(mid4 > mid3): u64); v_minus = u128rshift(mid4, hi4, ((j - 64): u32)); }; let v_rounded: u64 = u128rshift(mid, hi, ((j - 64 - 1): u32)); return ryuv { vp = v_plus, vr = v_rounded, vm = v_minus }; }; // ref/hare/strconv/ftos_ryu.ha:140. fn log2pow5(e: u32) u32 = { os.assert(e <= 3528u32, "strconv.log2pow5: e > 3528"); return (e * 1217359u32) >> 19u32; }; // ref/hare/strconv/ftos_ryu.ha:145-147. fn ceil_log2pow5(e: u32) u32 = { return log2pow5(e) + 1u32; }; fn pow5bits(e: u32) u32 = { return ceil_log2pow5(e); }; // ref/hare/strconv/ftos_ryu.ha:149. fn log10pow2(e: u32) u32 = { os.assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); return (e * 78913u32) >> 18u32; }; // ref/hare/strconv/ftos_ryu.ha:154. fn log10pow5(e: u32) u32 = { os.assert(e <= 2620u32, "strconv.log10pow5: e > 2620"); return (e * 732923u32) >> 20u32; }; // ref/hare/strconv/ftos_ryu.ha:224. Returns the (low, high) split of the // inverse power of five. 2-tuple kept (works); row-bind → double-index. fn f64computeinvpow5(i: u32) (u64, u64) = { let base: u32 = (i + (POW5_TABLE_SZ: u32) - 1u32) / (POW5_TABLE_SZ: u32); let base2: u32 = base * (POW5_TABLE_SZ: u32); let off: u32 = base2 - i; if (off == 0u32) { return (F64_POW5_INV_SPLIT2[base][0], F64_POW5_INV_SPLIT2[base][1]); }; let m: u64 = POW5_TABLE[off]; let r1: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][1]); let r0: r128 = u128mul(m, F64_POW5_INV_SPLIT2[base][0] - 1u64); let high1: u64 = r1.hi; let low1: u64 = r1.lo; let high0: u64 = r0.hi; let low0: u64 = r0.lo; let sum: u64 = high0 + low1; if (sum < high0) { high1 += 1u64; }; let delta: u32 = pow5bits(base2) - pow5bits(i); let res0: u64 = u128rshift(low0, sum, delta) + 1u64 + (((POW5_INV_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); let res1: u64 = u128rshift(sum, high1, delta); return (res0, res1); }; // ref/hare/strconv/ftos_ryu.ha:246. fn f64computepow5(i: u32) (u64, u64) = { let base: u32 = i / (POW5_TABLE_SZ: u32); let base2: u32 = base * (POW5_TABLE_SZ: u32); let off: u32 = i - base2; if (off == 0u32) { return (F64_POW5_SPLIT2[base][0], F64_POW5_SPLIT2[base][1]); }; let m: u64 = POW5_TABLE[off]; let r1: r128 = u128mul(m, F64_POW5_SPLIT2[base][1]); let r0: r128 = u128mul(m, F64_POW5_SPLIT2[base][0]); let high1: u64 = r1.hi; let low1: u64 = r1.lo; let high0: u64 = r0.hi; let low0: u64 = r0.lo; let sum: u64 = high0 + low1; if (sum < high0) { high1 += 1u64; }; let delta: u32 = pow5bits(i) - pow5bits(base2); let res0: u64 = u128rshift(low0, sum, delta) + (((POW5_OFFSETS[i / 16u32] >> ((i % 16u32) << 1u32)) & 3u32): u64); let res1: u64 = u128rshift(sum, high1, delta); return (res0, res1); }; // ref/hare/strconv/ftos_ryu.ha:267. Shortest decimal of an f64: // value == mantissa * 10^exponent. `exponent` rides i64 not Hare's i32 // (ftos_ryu.ha:269): a 16B struct-return with a NARROW (i32) second // field unpacks MOVL in wwstage vs MOVQ in cstage (store-width cs≠ww // byte-id split, #169); an 8B i64 field unpacks MOVQ in both. The value // always fits i32 (cast at the init_dec_mant_exp call site). type decf64 = struct { mantissa: u64, exponent: i64 }; // ref/hare/strconv/ftos_ryu.ha:272. `mantissa`/`exponent` are the raw // IEEE-754 fields of an f64. fn f64todecf64(mantissa: u64, exponent: u32) decf64 = { let e2: i32 = (math.F64_EXPONENT_BIAS + math.F64_MANTISSA_BITS + 2u64): i32; let m2: u64 = 0u64; if (exponent == 0u32) { e2 = 1i32 - e2; m2 = mantissa; } else { e2 = (exponent: i32) - e2; m2 = (1u64 << math.F64_MANTISSA_BITS) | mantissa; }; let accept_bounds: bool = (m2 & 1u64) == 0u64; let mv: u64 = 4u64 * m2; let mm_shift: u32 = ibool(mantissa != 0u64 || exponent <= 1u32): u32; let vp: u64 = 0u64; let vr: u64 = 0u64; let vm: u64 = 0u64; let e10: i32 = 0i32; let vm_trailing_zeros: bool = false; let vr_trailing_zeros: bool = false; if (e2 >= 0i32) { let q: u32 = log10pow2(e2: u32) - (ibool(e2 > 3i32): u32); e10 = q: i32; let k: u32 = (F64_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; let i: i32 = -e2 + ((q + k): i32); let pow5 = f64computeinvpow5(q); let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, i, mm_shift); vp = res.vp; vr = res.vr; vm = res.vm; if (q <= 21u32) { if ((mv - 5u64 * (mv / 5u64)) == 0u64) { vr_trailing_zeros = pow5multiple(mv, q); } else if (accept_bounds) { vm_trailing_zeros = pow5multiple(mv - 1u64 - (mm_shift: u64), q); } else { vp -= (ibool(pow5multiple(mv + 2u64, q)): u64); }; }; } else { let q: u32 = log10pow5((-e2): u32) - (ibool(-e2 > 1i32): u32); e10 = e2 + (q: i32); let i: i32 = -e2 - (q: i32); let k: i32 = (pow5bits(i: u32): i32) - (F64_POW5_BITCOUNT: i32); let j: i32 = (q: i32) - k; let pow5 = f64computepow5(i: u32); let res: ryuv = mulshiftall64(m2, pow5.0, pow5.1, j, mm_shift); vp = res.vp; vr = res.vr; vm = res.vm; if (q <= 1u32) { vr_trailing_zeros = true; if (accept_bounds) { vm_trailing_zeros = mm_shift == 1u32; } else { vp -= 1u64; }; } else if (q < 63u32) { vr_trailing_zeros = pow2multiple(mv, q); }; }; let removed: i32 = 0i32; let last_removed_digit: u8 = 0u8; let output: u64 = 0u64; if (vm_trailing_zeros || vr_trailing_zeros) { for (true) { let vpby10: u64 = vp / 10u64; let vmby10: u64 = vm / 10u64; if (vpby10 <= vmby10) { break; }; let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); let vrby10: u64 = vr / 10u64; let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); vm_trailing_zeros = vm_trailing_zeros && (vmmod10 == 0u32); vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); last_removed_digit = (vrmod10: u8); vr = vrby10; vp = vpby10; vm = vmby10; removed += 1i32; }; if (vm_trailing_zeros) { for (true) { let vmby10: u64 = vm / 10u64; let vmmod10: u32 = (vm: u32) - 10u32 * (vmby10: u32); if (vmmod10 != 0u32) { break; }; let vpby10: u64 = vp / 10u64; let vrby10: u64 = vr / 10u64; let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); vr_trailing_zeros = vr_trailing_zeros && (last_removed_digit == 0u8); last_removed_digit = (vrmod10: u8); vr = vrby10; vp = vpby10; vm = vmby10; removed += 1i32; }; }; if (vr_trailing_zeros && last_removed_digit == 5u8 && (vr & 1u64) == 0u64) { last_removed_digit = 4u8; // round to even }; let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeros)); let cond2: bool = last_removed_digit >= 5u8; output = vr + (ibool(cond1 || cond2): u64); } else { let round_up: bool = false; let vpby100: u64 = vp / 100u64; let vmby100: u64 = vm / 100u64; if (vpby100 > vmby100) { let vrby100: u64 = vr / 100u64; let vrmod100: u32 = (vr: u32) - 100u32 * (vrby100: u32); round_up = vrmod100 >= 50u32; vr = vrby100; vp = vpby100; vm = vmby100; removed += 2i32; }; for (true) { let vmby10: u64 = vm / 10u64; let vpby10: u64 = vp / 10u64; if (vpby10 <= vmby10) { break; }; let vrby10: u64 = vr / 10u64; let vrmod10: u32 = (vr: u32) - 10u32 * (vrby10: u32); round_up = vrmod10 >= 5u32; vr = vrby10; vp = vpby10; vm = vmby10; removed += 1i32; }; output = vr + (ibool(vr == vm || round_up): u64); }; let exp: i32 = e10 + removed; return decf64 { exponent = (exp: i64), mantissa = output }; }; // ==== f32 Ryū sub-path (ftos_ryu.ha). The *32 helpers below mirror their // u64 siblings at 32-bit width; they reuse the SHARED f64computeinvpow5/ // f64computepow5 (and thus the f64 SPLIT2 tables) per ftos_ryu.ha — there // is no separate f32 table. Same scalar-PARAM-mutation → copy-to-local, // comma-split, assert → os.assert, expr-yield → block divergences as the // f64 path above. ==== // ref/hare/strconv/ftos_ryu.ha:52. Largest p with 5^p | value (32-bit). fn pow5fac32(v: u32) u32 = { let value: u32 = v; let count: u32 = 0u32; for (true) { os.assert(value != 0u32, "strconv.pow5fac32: value == 0"); let q: u32 = value / 5u32; let r: u32 = value % 5u32; if (r != 0u32) { break; }; value = q; count += 1u32; }; return count; }; // ref/hare/strconv/ftos_ryu.ha:67. fn pow5multiple32(v: u32, p: u32) bool = { return pow5fac32(v) >= p; }; // ref/hare/strconv/ftos_ryu.ha:75. fn pow2multiple32(v: u32, p: u32) bool = { os.assert(v > 0u32, "strconv.pow2multiple32: v == 0"); os.assert(p < 32u32, "strconv.pow2multiple32: p >= 32"); return (v & ((1u32 << p) - 1u32)) == 0u32; }; // ref/hare/strconv/ftos_ryu.ha:121. `m * a_lo` etc. carry an explicit // (m: u64) cast (Hare promotes the u32 operand; ww is strict). The bound // assert inlines U32_MAX's value: ww's types.U32_MAX is package-private // (lib/types/types.ww — no `export`), so Hare's `types::U32_MAX` can't be // referenced cross-package. fn mulshift32(m: u32, a: u64, s: u32) u32 = { os.assert(s > 32u32, "strconv.mulshift32: s <= 32"); let a_lo: u64 = (a: u32): u64; let a_hi: u64 = a >> 32u64; let b0: u64 = (m: u64) * a_lo; let b1: u64 = (m: u64) * a_hi; let sum: u64 = (b0 >> 32u64) + b1; let ss: u64 = sum >> ((s: u64) - 32u64); os.assert(ss <= 4294967295u64, "strconv.mulshift32: ss > U32_MAX"); return ss: u32; }; // ref/hare/strconv/ftos_ryu.ha:130. fn mulpow5inv_divpow2(m: u32, q: u32, j: i32) u32 = { let pow5 = f64computeinvpow5(q); return mulshift32(m, pow5.1 + 1u64, (j: u32)); }; // ref/hare/strconv/ftos_ryu.ha:135. fn mulpow5_divpow2(m: u32, i: u32, j: i32) u32 = { let pow5 = f64computepow5(i); return mulshift32(m, pow5.1, (j: u32)); }; // ref/hare/strconv/ftos_ryu.ha:387. `exponent` rides i64 not Hare's i32, // for the same reason decf64 does: widening the field to a full second // eightbyte SIDESTEPS the #169 narrow-i32-field struct-return unpack (a // narrow i32 there unpacks MOVL wwstage vs MOVQ cstage). The value always // fits i32 (cast at the init_dec_mant_exp call site). `mantissa` stays u32 // (Hare's width); the {u32, pad, i64} layout's first eightbyte holds // mantissa@0 + 4B pad and reads cleanly — byte-id CONFIRMED by the 990-997 // gate (0-diff cs vs ww), not relied on as an ABI guarantee. type decf32 = struct { mantissa: u32, exponent: i64 }; // ref/hare/strconv/ftos_ryu.ha:392. Shortest decimal of an f32: // value == mantissa * 10^exponent. `mantissa`/`exponent` are the raw // IEEE-754 fields of an f32. fn f32todecf32(mantissa: u32, exponent: u32) decf32 = { let e2: i32 = (math.F32_EXPONENT_BIAS + math.F32_MANTISSA_BITS + 2u32): i32; let m2: u32 = 0u32; if (exponent == 0u32) { e2 = 1i32 - e2; m2 = mantissa; } else { e2 = (exponent: i32) - e2; m2 = (1u32 << math.F32_MANTISSA_BITS) | mantissa; }; let accept_bounds: bool = (m2 & 1u32) == 0u32; let mv: u32 = 4u32 * m2; let mp: u32 = mv + 2u32; let mm_shift: u32 = ibool(mantissa != 0u32 || exponent <= 1u32): u32; let mm: u32 = mv - 1u32 - mm_shift; let vr: u32 = 0u32; let vp: u32 = 0u32; let vm: u32 = 0u32; let e10: i32 = 0i32; let vm_trailing_zeroes: bool = false; let vr_trailing_zeroes: bool = false; let last_removed_digit: u8 = 0u8; if (e2 >= 0i32) { let q: u32 = log10pow2(e2: u32); e10 = q: i32; let k: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q) - 1u32; let i: i32 = -e2 + ((q + k): i32); vr = mulpow5inv_divpow2(mv, q, i); vp = mulpow5inv_divpow2(mp, q, i); vm = mulpow5inv_divpow2(mm, q, i); if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { let l: u32 = (F32_POW5_INV_BITCOUNT: u32) + pow5bits(q - 1u32) - 1u32; last_removed_digit = (mulpow5inv_divpow2(mv, q - 1u32, -e2 + ((q + l): i32) - 1i32) % 10u32): u8; }; if (q <= 9u32) { if (mv % 5u32 == 0u32) { vr_trailing_zeroes = pow5multiple32(mv, q); } else if (accept_bounds) { vm_trailing_zeroes = pow5multiple32(mm, q); } else { vp -= (ibool(pow5multiple32(mp, q)): u32); }; }; } else { let q: u32 = log10pow5((-e2): u32); e10 = (q: i32) + e2; let i: u32 = (-e2 - (q: i32)): u32; let k: u32 = pow5bits(i) - (F32_POW5_BITCOUNT: u32); let j: i32 = (q: i32) - (k: i32); vr = mulpow5_divpow2(mv, i, j); vp = mulpow5_divpow2(mp, i, j); vm = mulpow5_divpow2(mm, i, j); if (q != 0u32 && (vp - 1u32) / 10u32 <= vm / 10u32) { j = (q: i32) - 1i32 - ((pow5bits(i + 1u32): i32) - (F32_POW5_BITCOUNT: i32)); last_removed_digit = (mulpow5_divpow2(mv, (i + 1u32), j) % 10u32): u8; }; if (q <= 1u32) { vr_trailing_zeroes = true; if (accept_bounds) { vm_trailing_zeroes = mm_shift == 1u32; } else { vp -= 1u32; }; } else if (q < 31u32) { vr_trailing_zeroes = pow2multiple32(mv, q - 1u32); }; }; let removed: i32 = 0i32; let output: u32 = 0u32; if (vm_trailing_zeroes || vr_trailing_zeroes) { for ((vp / 10u32) > (vm / 10u32)) { vm_trailing_zeroes = vm_trailing_zeroes && ((vm - (vm / 10u32) * 10u32) == 0u32); vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); last_removed_digit = (vr % 10u32): u8; vr /= 10u32; vp /= 10u32; vm /= 10u32; removed += 1i32; }; if (vm_trailing_zeroes) { for ((vm % 10u32) == 0u32) { vr_trailing_zeroes = vr_trailing_zeroes && (last_removed_digit == 0u8); last_removed_digit = (vr % 10u32): u8; vr /= 10u32; vp /= 10u32; vm /= 10u32; removed += 1i32; }; }; if (vr_trailing_zeroes && last_removed_digit == 5u8 && vr % 2u32 == 0u32) { last_removed_digit = 4u8; // round to even }; let cond1: bool = (vr == vm) && ((!accept_bounds) || (!vm_trailing_zeroes)); let cond2: bool = last_removed_digit >= 5u8; output = vr + (ibool(cond1 || cond2): u32); } else { for ((vp / 10u32) > (vm / 10u32)) { last_removed_digit = (vr % 10u32): u8; vr /= 10u32; vp /= 10u32; vm /= 10u32; removed += 1i32; }; output = vr + (ibool(vr == vm || last_removed_digit >= 5u8): u32); }; let exp: i32 = e10 + removed; return decf32 { mantissa = output, exponent = (exp: i64) }; }; // ==== G-format encode layer (ftos.ha) — only the ffmt::G / prec=void / // fflags::NONE-REACHABLE logic. The SHOW_POINT/precision/E-vs-uppercase // arms (ftos.ha:88-105, 127-145, 170-213's zeros/caps) are UNREACHABLE // for G/void/NONE (ffpoint(NONE)=false, prec is never uint, f is always // G) and are NOT ported — porting them stubbed would be untested dead // code. The parametric ftosf/ffmt/fflags surface is deferred (task #64; // needs a parametric consumer + io::handle + #158). ==== // ref/hare/strconv/ftos.ha:49. Decimal digit-count of n (n <= 1e17). fn declen(n: u64) uint = { os.assert(n <= 100000000000000000u64, "strconv.declen: n > 1e17"); if (n >= 100000000000000000u64) { return (18u32: uint); }; if (n >= 10000000000000000u64) { return (17u32: uint); }; if (n >= 1000000000000000u64) { return (16u32: uint); }; if (n >= 100000000000000u64) { return (15u32: uint); }; if (n >= 10000000000000u64) { return (14u32: uint); }; if (n >= 1000000000000u64) { return (13u32: uint); }; if (n >= 100000000000u64) { return (12u32: uint); }; if (n >= 10000000000u64) { return (11u32: uint); }; if (n >= 1000000000u64) { return (10u32: uint); }; if (n >= 100000000u64) { return (9u32: uint); }; if (n >= 10000000u64) { return (8u32: uint); }; if (n >= 1000000u64) { return (7u32: uint); }; if (n >= 100000u64) { return (6u32: uint); }; if (n >= 10000u64) { return (5u32: uint); }; if (n >= 1000u64) { return (4u32: uint); }; if (n >= 100u64) { return (3u32: uint); }; if (n >= 10u64) { return (2u32: uint); }; return (1u32: uint); }; // ref/hare/strconv/ftos.ha:217. Lay the Ryū shortest (mantissa,exponent) // into the decimal `d`. `mantissa` is mutated in Hare → local `mant`. fn init_dec_mant_exp(d: *decimal, mantissa: u64, exponent: i32) void = { // Hoisted uint casts: ww parser rejects a `(N: uint)` cast embedded // inside an array subscript (decimal.ww "hoist size casts" note). let U_ZERO: uint = (0u32: uint); let U_ONE: uint = (1u32: uint); let mant: u64 = mantissa; let dl: uint = declen(mant); let i: uint = U_ZERO; for (i < dl) { d.digits[dl - i - U_ONE] = (mant % 10u64): u8; mant /= 10u64; i += U_ONE; }; d.nd = (dl: size); d.dp = (dl: i32) + exponent; }; // ref/hare/strconv/ftos.ha:71. writestr → buffer-cursor adaptation (the // *tos static-buffer convention replaces Hare's io::handle sink). fn putstr(buf: []u8, out: i32, s: str) i32 = { let o: i32 = out; let k: i32 = 0i32; for (k < s.len) { buf[o] = s[k]; o += 1i32; k += 1i32; }; return o; }; // ref/hare/strconv/ftos.ha:109. Fixed-point render (G/void/NONE-reachable // logic only). Writes into `buf` at cursor `out`, returns the new cursor. fn encode_f_dec(d: *decimal, buf: []u8, out: i32) i32 = { let o: i32 = out; let lo: i32 = 0i32; if (d.dp <= 0i32) { lo = d.dp - 1i32; }; let hi: i32 = d.dp; if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; if (hi > (d.nd: i32) && d.dp <= 0i32) { hi = (d.nd: i32); } else if (hi > d.dp && d.dp > 0i32) { hi = d.dp; if ((d.nd: i32) > d.dp) { hi = (d.nd: i32); }; }; let i: i32 = lo; for (i < hi) { if (i == d.dp) { buf[o] = 46u8; // '.' o += 1i32; }; if (0i32 <= i && i < (d.nd: i32)) { buf[o] = (d.digits[i] + 48u8): u8; } else { buf[o] = 48u8; // '0' }; o += 1i32; i += 1i32; }; return o; }; // ref/hare/strconv/ftos.ha:160. Scientific render (G/void/NONE-reachable // logic only): no precision zeros, lowercase 'e', no '+'/two-digit pad. fn encode_e_dec(d: *decimal, buf: []u8, out: i32) i32 = { let o: i32 = out; os.assert(d.nd > (0u64: size), "strconv.encode_e_dec: nd == 0"); buf[o] = (d.digits[0] + 48u8): u8; o += 1i32; if ((d.nd: i32) > 1i32) { buf[o] = 46u8; // '.' o += 1i32; }; let i: size = (1u64: size); for (i < d.nd) { buf[o] = (d.digits[i] + 48u8): u8; o += 1i32; i += (1u64: size); }; buf[o] = 101u8; // 'e' o += 1i32; let e: i32 = d.dp - 1i32; if (e < 0i32) { e = -e; buf[o] = 45u8; // '-' o += 1i32; }; // Hoisted uint casts (ww parser rejects `(N: uint)` inside `[ ]`). let U_ONE: uint = (1u32: uint); let U_TWO: uint = (2u32: uint); let U_THREE: uint = (3u32: uint); let ebuf: [3]u8 = [0u8, 0u8, 0u8]; // exponents are at most 3 digits let l: uint = declen(e: u64); let k: uint = (0u32: uint); for (k < l) { ebuf[U_TWO - k] = (e % 10i32): u8; e /= 10i32; k += U_ONE; }; let m: uint = U_THREE - l; for (m < U_THREE) { buf[o] = (ebuf[m] + 48u8): u8; o += 1i32; m += U_ONE; }; return o; }; // ref/hare/strconv/ftos.ha:432. f64 → shortest base-10 str. Returns a // view into a static buffer overwritten on the next call (the *tos // convention; see strings.dup to retain). Equivalent to Hare's ftosf // with format G + precision void. The fftosf G/void/NONE path is inlined // (the parametric surface is deferred — task #64). // // Max output is 24 (ftos.ha:434): sign + digit + '.' + 16 digits + 'e' + // exp-sign + 3 exp-digits. Sized 32 not 24: a no-rhs [24]u8 module buffer // emits 4 DATAW in wwstage vs 2 in cstage (#43, the size-16/24 emitletdataw // split); 32 emits 2 in both (byte-id). The extra 8 bytes are unused. let f64tos_buf: [32]u8; export fn f64tos(n: f64) str = { let bits: u64 = math.f64bits(n); let mantissa: u64 = bits & math.F64_MANTISSA_MASK; let exponent: u32 = ((bits >> math.F64_MANTISSA_BITS) & math.F64_EXPONENT_MASK): u32; let sign: bool = (bits >> (math.F64_EXPONENT_BITS + math.F64_MANTISSA_BITS)) > 0u64; let special: bool = exponent == (math.F64_EXPONENT_MASK: u32); let o: i32 = 0i32; let r: str; r.ptr = &f64tos_buf[0]; // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). if (special && mantissa != 0u64) { o = putstr(f64tos_buf[0:32], o, "nan"); r.len = o; return r; }; if (sign) { f64tos_buf[o] = 45u8; // '-' o += 1i32; }; if (special) { o = putstr(f64tos_buf[0:32], o, "infinity"); r.len = o; return r; }; if (exponent == 0u32 && mantissa == 0u64) { f64tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) o += 1i32; r.len = o; return r; }; let d = decimal { ... }; // Reads of d.nd / d.dp ride a *decimal pointer: wwstage resolves a // scalar-field read of a LOCAL struct (`d.nd`) to a bogus global // symbol (`nd(SB)`), but a pointer-deref field read (`pd.nd`) lowers // correctly in both stages (the stof.ww/decimal.ww *decimal precedent) // — #170. The init/trim/encode calls already took &d; route via pd. let pd: *decimal = &d; let dd: decf64 = f64todecf64(mantissa, exponent); init_dec_mant_exp(pd, dd.mantissa, (dd.exponent: i32)); // ok = !ffpoint(NONE) || ... is always true → no multiprecision // fallback (ftos.ha:365). f == G → trim (ftos.ha:386). trim(pd); if (pd.nd == (0u64: size)) { f64tos_buf[o] = 48u8; // rounded to zero o += 1i32; } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { o = encode_e_dec(pd, f64tos_buf[0:32], o); } else { o = encode_f_dec(pd, f64tos_buf[0:32], o); }; r.len = o; return r; }; // ref/hare/strconv/ftos.ha:448. f32 → shortest base-10 str. Same static- // buffer convention + G/void/NONE-inlined path as f64tos. f32bits(n) // passes an f32 arg → MOVSS both stages post-#143 (aff7725); this is the // piece fold-5b was gated on. // // Hare sizes this [14]u8 (ftos.ha:451: 1 + 1 + 1 + 7 + 1 + 1 + 2). Sized // 32 to reuse f64tos's proven byte-id-clean band: a no-rhs [N]u8 module // buffer at the size-16/24 band emits divergent DATAW counts cs≠ww (#43); // 32 emits 2 DATAW in both. The unused tail bytes are harmless. let f32tos_buf: [32]u8; export fn f32tos(n: f32) str = { let bits: u32 = math.f32bits(n); let mantissa: u32 = bits & math.F32_MANTISSA_MASK; let exponent: u32 = (bits >> math.F32_MANTISSA_BITS) & math.F32_EXPONENT_MASK; let sign: bool = (bits >> (math.F32_EXPONENT_BITS + math.F32_MANTISSA_BITS)) > 0u32; let special: bool = exponent == math.F32_EXPONENT_MASK; let o: i32 = 0i32; let r: str; r.ptr = &f32tos_buf[0]; // NaN carries no sign prefix (ftos.ha:331-333, before sign handling). if (special && mantissa != 0u32) { o = putstr(f32tos_buf[0:32], o, "nan"); r.len = o; return r; }; if (sign) { f32tos_buf[o] = 45u8; // '-' o += 1i32; }; if (special) { o = putstr(f32tos_buf[0:32], o, "infinity"); r.len = o; return r; }; if (exponent == 0u32 && mantissa == 0u32) { f32tos_buf[o] = 48u8; // '0' (encode_zero, G/void/NONE) o += 1i32; r.len = o; return r; }; let d = decimal { ... }; // *decimal pointer for the field reads (the #170 dodge; see f64tos). let pd: *decimal = &d; let dd: decf32 = f32todecf32(mantissa, exponent); init_dec_mant_exp(pd, (dd.mantissa: u64), (dd.exponent: i32)); trim(pd); if (pd.nd == (0u64: size)) { f32tos_buf[o] = 48u8; // rounded to zero o += 1i32; } else if (pd.dp < -1i32 || (pd.dp - (pd.nd: i32)) > 2i32) { o = encode_e_dec(pd, f32tos_buf[0:32], o); } else { o = encode_f_dec(pd, f32tos_buf[0:32], o); }; r.len = o; return r; }; // strconv — Ryū float→string lookup tables + bit-count constants. // Mirrors ref/hare/strconv/ftos_ryu.ha:159-222 byte-exact. Pure data // fold (strconv #106 fold-5): no logic, consumed by ftos.ww's // f64computeinvpow5 / f64computepow5 (the Ryū power-of-five cores). // // File-organisation divergence: Hare keeps these tables INLINE in // ftos_ryu.ha. ww splits data from logic into ftos_data.ww (mirroring // the stof.ww / stof_data.ww split) — same `package strconv`, so the // tables stay visible to ftos.ww with no qualification. // // Spelling divergences (same as stof_data.ww, candidate #130 + rule-12): // - Hare `const TBL = [...]` → ww module-level `let` (ww has no // module-`const` keyword; the values are never written). // - every literal carries its element-width suffix (`u64`/`u32`): // cstage rejects bare integer literals in `[N]uXX` init while // wwstage accepts them; the suffixed form is the only shape both // stages agree on. // - the [N][2]u64 tables stay faithful 2D (rule-12, not flattened); // the 2D module-level static-init + double-index read landed in // #156 (cbeffea), proven by stof_data.ww's powers_of_ten[596][2]u64. package strconv; // ref/hare/strconv/ftos_ryu.ha:159-160. Bit-counts of the split // power-of-five tables. Defined u8 (faithful); ftos.ww casts to u32/i32 // at each use site (Hare promotes a u8 def inside mixed-width arithmetic; // ww is strict — explicit cast, project_int_machine_word_derived_limits). def F64_POW5_INV_BITCOUNT: u8 = 125u8; def F64_POW5_BITCOUNT: u8 = 125u8; // ref/hare/strconv/ftos_ryu.ha:162-163. The f32 split-table bit-counts, // derived from the f64 siblings (Hare: F64_..._BITCOUNT - 64). Consumed by // f32todecf32 (ftos.ww), landed in fold-5b (task #67) — the f32 path reuses // the f64 SPLIT2 tables (via f64computeinvpow5/f64computepow5), so no // separate F32 tables exist (matches ftos_ryu.ha). u8 like the f64 defs; // ftos.ww casts to u32/i32 at each use. def F32_POW5_INV_BITCOUNT: u8 = F64_POW5_INV_BITCOUNT - 64u8; def F32_POW5_BITCOUNT: u8 = F64_POW5_BITCOUNT - 64u8; // ref/hare/strconv/ftos_ryu.ha:165-181. let F64_POW5_INV_SPLIT2: [15][2]u64 = [ [1u64, 2305843009213693952u64], [5955668970331000884u64, 1784059615882449851u64], [8982663654677661702u64, 1380349269358112757u64], [7286864317269821294u64, 2135987035920910082u64], [7005857020398200553u64, 1652639921975621497u64], [17965325103354776697u64, 1278668206209430417u64], [8928596168509315048u64, 1978643211784836272u64], [10075671573058298858u64, 1530901034580419511u64], [597001226353042382u64, 1184477304306571148u64], [1527430471115325346u64, 1832889850782397517u64], [12533209867169019542u64, 1418129833677084982u64], [5577825024675947042u64, 2194449627517475473u64], [11006974540203867551u64, 1697873161311732311u64], [10313493231639821582u64, 1313665730009899186u64], [12701016819766672773u64, 2032799256770390445u64], ]; // ref/hare/strconv/ftos_ryu.ha:183-188. let POW5_INV_OFFSETS: [19]u32 = [ 0x54544554u32, 0x04055545u32, 0x10041000u32, 0x00400414u32, 0x40010000u32, 0x41155555u32, 0x00000454u32, 0x00010044u32, 0x40000000u32, 0x44000041u32, 0x50454450u32, 0x55550054u32, 0x51655554u32, 0x40004000u32, 0x01000001u32, 0x00010500u32, 0x51515411u32, 0x05555554u32, 0x00000000u32, ]; // ref/hare/strconv/ftos_ryu.ha:190-204. let F64_POW5_SPLIT2: [13][2]u64 = [ [0u64, 1152921504606846976u64], [0u64, 1490116119384765625u64], [1032610780636961552u64, 1925929944387235853u64], [7910200175544436838u64, 1244603055572228341u64], [16941905809032713930u64, 1608611746708759036u64], [13024893955298202172u64, 2079081953128979843u64], [6607496772837067824u64, 1343575221513417750u64], [17332926989895652603u64, 1736530273035216783u64], [13037379183483547984u64, 2244412773384604712u64], [1605989338741628675u64, 1450417759929778918u64], [9630225068416591280u64, 1874621017369538693u64], [665883850346957067u64, 1211445438634777304u64], [14931890668723713708u64, 1565756531257009982u64], ]; // ref/hare/strconv/ftos_ryu.ha:206-211. let POW5_OFFSETS: [21]u32 = [ 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x00000000u32, 0x40000000u32, 0x59695995u32, 0x55545555u32, 0x56555515u32, 0x41150504u32, 0x40555410u32, 0x44555145u32, 0x44504540u32, 0x45555550u32, 0x40004000u32, 0x96440440u32, 0x55565565u32, 0x54454045u32, 0x40154151u32, 0x55559155u32, 0x51405555u32, 0x00000105u32, ]; // ref/hare/strconv/ftos_ryu.ha:213. Divisor/index stride in // f64computeinvpow5 / f64computepow5 (ftos.ww). Kept as a def for those // arithmetic uses; POW5_TABLE's dimension below must be a literal (cstage // rejects a def-named array length — "array length must be an integer // literal"; wwstage accepts it but emits an empty DATAW — divergence // #167, so the literal `26` is the only shape both stages agree on; // matches decimal.ww's `[800]u8` array-dimension-literal precedent). def POW5_TABLE_SZ: u8 = 26u8; // ref/hare/strconv/ftos_ryu.ha:215-222. 5^0 .. 5^25 (the 5^26 entry is // commented out in Hare too — it lives implicitly in the SPLIT2 tables). let POW5_TABLE: [26]u64 = [ 1u64, 5u64, 25u64, 125u64, 625u64, 3125u64, 15625u64, 78125u64, 390625u64, 1953125u64, 9765625u64, 48828125u64, 244140625u64, 1220703125u64, 6103515625u64, 30517578125u64, 152587890625u64, 762939453125u64, 3814697265625u64, 19073486328125u64, 95367431640625u64, 476837158203125u64, 2384185791015625u64, 11920928955078125u64, 59604644775390625u64, 298023223876953125u64, ]; // 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; }; // strconv — string-to-float. Mirrors ref/hare/strconv/stof.ha // (Hare in turn adapts Go): Eisel-Lemire fast path [1] with the // Simple-Decimal-Conversion slow path [2] (decimal.ww) as fallback. // [1]: https://nigeltao.github.io/blog/2020/eisel-lemire.html // [2]: https://nigeltao.github.io/blog/2020/parse-number-f64-simple.html // // The Eisel-Lemire fast path (`eisel_lemire` + the `powers_of_ten` // table in stof_data.ww + the three call sites: floatbits's d.nd<=19 // block, stof64/stof32's !truncated block) is a pure speed // optimisation — it returns the same correctly-rounded value the // decimal slow path (decimal_parse → floatbits) computes, or void to // defer. Its prereqs landed: the 2D `[596][2]u64` static-init + // double-index read (#156) and the tagged float-variant return-pack // (#157, which the public `(f64|invalid|overflow)` return needs). // // Spelling divergences from Hare (mechanical, ww parser/cgen shape): // - str scan index rides `i32` (ww `str.len: i32` + `invalid = !i32` // payload), not Hare's `size`/`len(s)`. lib CLAUDE.md str-index note. // - char literals kept faithful (`buf[i] == '.'`, `c - '0'`); probed // byte-id + value-correct both stages. // - Hare `?` error-propagation → nested statement-`match` with all- // return arms + a `case void => void` continuation. ww's `?` // lowering and a bound `match`-expression with mixed yield/return // arms both diverge cs≠ww (the latter wwstage-checker-rejected); // strconv.ww's stoi32 set the explicit-match precedent. // - Hare `for (cond; afterthought)` 2-clause + `continue` → ww // 2-clause `for (cond)` with the afterthought inlined at body end // AND before each `continue` (ww has no empty-init 3-clause // `for (; c; p)`; #138 post-skip is dodged since 2-clause has no // post). decimal.ww set the inline-afterthought precedent. // - Hare `if`/`switch`-expression yield → explicit if-statements + // pre-bound scalar locals (ww has no expression-bodied if). // - Hare fn-pointer-in-tuple + `switch yield` selecting the digit // predicate in fast_parse → a `base==HEX` bool + an `isdigitbase` // helper that branches to ascii.isdigit/isxdigit (no fn-ptr, no // tuple, no switch). // - struct-param field MUTATION (hex_to_bits mutates its by-value // `p`) → copy p's fields to scalar locals at entry; ww miscompiles // + diverges on writing a by-value struct param's fields (filed). // - default arg dropped: Hare `b: base = base::DEC` → callers pass // base explicitly (no lib fn ships a default arg; strconv.ww // stoi64 precedent). The base param is normalised through a local // `bb` (param reassignment avoided). // - `math::NAN`/`math::INF` (f32) absent in ww math → materialised // via f32frombits of the IEEE-754 f32 bit patterns (same honest // construction as math/floats.ww's NAN_BITS/INF_BITS). // - narrowing int→i32 assignments carry explicit casts (ww `int` is // an 8B machine word; project_int_machine_word_derived_limits). // - `r128`/`u128mul` live here (fold-4 is first consumer); fold-5 // ftos (Ryū) shares them in-package. package strconv; import ascii; import math; import os; import strings; // ref/hare/strconv/ftos_ryu.ha:12. 64×64→128 result halves. type r128 = struct { hi: u64, lo: u64, }; // ref/hare/strconv/ftos_ryu.ha:18. 64×64→128 via 32-bit decomposition // (Hare's own "TODO: use 128-bit integers when implemented" — ww has // no u128; the decomposition is the portable shape both stages agree // on). Comma let-bindings split per decimal.ww divergence. fn u128mul(a: u64, b: u64) r128 = { let a0: u64 = (a: u32): u64; let a1: u64 = a >> 32u64; let b0: u64 = (b: u32): u64; let b1: u64 = b >> 32u64; let p00: u64 = a0 * b0; let p01: u64 = a0 * b1; let p10: u64 = a1 * b0; let p11: u64 = a1 * b1; let p00_lo: u64 = (p00: u32): u64; let p00_hi: u64 = p00 >> 32u64; let mid1: u64 = p10 + p00_hi; let mid1_lo: u64 = (mid1: u32): u64; let mid1_hi: u64 = mid1 >> 32u64; let mid2: u64 = p01 + mid1_lo; let mid2_lo: u64 = (mid2: u32): u64; let mid2_hi: u64 = mid2 >> 32u64; let r_hi: u64 = p11 + mid1_hi + mid2_hi; let r_lo: u64 = (mid2_lo << 32u64) | p00_lo; return r128 { hi = r_hi, lo = r_lo }; }; // ref/hare/strconv/stof.ha:14. fn todig(c: u8) u8 = { if ('0' <= c && c <= '9') { return c - '0'; }; if ('a' <= c && c <= 'f') { return c - 'a' + 10u8; }; if ('A' <= c && c <= 'F') { return c - 'A' + 10u8; }; abort("strconv.todig: unreachable"); return 0u8; // unreachable; rt_abort is void-typed (path-cov) }; @symbol("rt_abort") fn abort(msg: str) void; // ref/hare/strconv/stof.ha:25. type fast_parsed_float = struct { mantissa: u64, exponent: i32, negative: bool, truncated: bool, }; // Digit-class predicate selector for fast_parse — replaces Hare's // fn-pointer-in-tuple (`&ascii::isdigit` / `&ascii::isxdigit`). fn isdigitbase(c: rune, ishex: bool) bool = { if (ishex) { return ascii.isxdigit(c); }; return ascii.isdigit(c); }; // ref/hare/strconv/stof.ha:32. fn fast_parse(s: str, b: base) (fast_parsed_float | invalid) = { let buf: []u8 = strings.toutf8(s); let i: i32 = 0; let neg: bool = false; let trunc: bool = false; if (buf[i] == '-') { neg = true; i += 1; } else if (buf[i] == '+') { i += 1; }; let ishex: bool = (b == base.HEX); let expchr: rune = 'e'; let max_ndmant: int = 19; if (ishex) { expchr = 'p'; max_ndmant = 16; }; let bnum: u64 = (b: i32): u64; let sawdot: bool = false; let sawdigits: bool = false; let nd: int = 0; let ndmant: int = 0; let dp: int = 0; let mant: u64 = 0u64; let exp: i32 = 0i32; for (i < s.len) { if (buf[i] == '.') { if (sawdot) { return i: invalid; }; sawdot = true; dp = nd; } else if (isdigitbase(buf[i]: rune, ishex)) { sawdigits = true; if (buf[i] == '0' && nd == 0) { dp -= 1; i += 1; continue; }; nd += 1; if (ndmant < max_ndmant) { mant = mant * bnum + (todig(buf[i]): u64); ndmant += 1; } else if (buf[i] != '0') { trunc = true; }; } else { break; }; i += 1; }; if (!sawdigits) { return i: invalid; }; if (!sawdot) { dp = nd; }; if (b == base.HEX) { dp *= 4; ndmant *= 4; }; if (i < s.len && ascii.tolower(buf[i]: rune) == expchr) { i += 1; if (i >= s.len) { return i: invalid; }; let expsign: int = 1; if (buf[i] == '+') { i += 1; } else if (buf[i] == '-') { expsign = -1; i += 1; }; if (i >= s.len || !ascii.isdigit(buf[i]: rune)) { return i: invalid; }; let e: int = 0; for (i < s.len && ascii.isdigit(buf[i]: rune)) { if (e < 10000) { e = e * 10 + ((buf[i] - '0'): int); }; i += 1; }; dp += e * expsign; } else if (b == base.HEX) { return i: invalid; // hex floats must have an exponent }; if (i != s.len) { return i: invalid; }; if (mant != 0u64) { exp = (dp - ndmant): i32; }; return fast_parsed_float { mantissa = mant, exponent = exp, negative = neg, truncated = trunc, }; }; // ref/hare/strconv/stof.ha:115. Fills the slow-path decimal `d`. fn decimal_parse(d: *decimal, s: str) (void | invalid) = { let i: i32 = 0; let buf: []u8 = strings.toutf8(s); d.negative = false; d.truncated = false; if (buf[0] == '+') { i += 1; } else if (buf[0] == '-') { d.negative = true; i += 1; }; let sawdot: bool = false; let sawdigits: bool = false; for (i < s.len) { if (buf[i] == '.') { if (sawdot) { return i: invalid; }; sawdot = true; d.dp = (d.nd: i32); } else if (ascii.isdigit(buf[i]: rune)) { sawdigits = true; if (buf[i] == '0' && d.nd == (0u64: size)) { d.dp -= 1; i += 1; continue; }; if (d.nd < (len(d.digits): size)) { d.digits[d.nd] = buf[i] - '0'; d.nd += (1u64: size); } else if (buf[i] != '0') { d.truncated = true; }; } else { break; }; i += 1; }; if (!sawdigits) { return i: invalid; }; if (!sawdot) { d.dp = (d.nd: i32); }; if (i < s.len && (buf[i] == 'e' || buf[i] == 'E')) { i += 1; if (i >= s.len) { return i: invalid; }; let expsign: int = 1; if (buf[i] == '+') { i += 1; } else if (buf[i] == '-') { expsign = -1; i += 1; }; if (i >= s.len || !ascii.isdigit(buf[i]: rune)) { return i: invalid; }; let e: int = 0; for (i < s.len && ascii.isdigit(buf[i]: rune)) { if (e < 10000) { e = e * 10 + ((buf[i] - '0'): int); }; i += 1; }; d.dp += (e * expsign): i32; }; if (i != s.len) { return i: invalid; }; return; }; // ref/hare/strconv/stof.ha:173. Count of leading zero bits in n>0. fn leading_zeroes(n: u64) uint = { os.assert(n > 0u64, "strconv.leading_zeroes: n == 0"); let b: u64 = 0u64; if ((n & 0xFFFFFFFF00000000u64) > 0u64) { n >>= 32u64; b |= 32u64; }; if ((n & 0xFFFF0000u64) > 0u64) { n >>= 16u64; b |= 16u64; }; if ((n & 0xFF00u64) > 0u64) { n >>= 8u64; b |= 8u64; }; if ((n & 0xF0u64) > 0u64) { n >>= 4u64; b |= 4u64; }; if ((n & 0xCu64) > 0u64) { n >>= 2u64; b |= 2u64; }; if ((n & 0x2u64) > 0u64) { n >>= 1u64; b |= 1u64; }; return ((63u64 - b): uint); }; // ref/hare/strconv/stof.ha:203. Eisel-Lemire fast path: a correctly- // rounded f64/f32 from (mantissa, exp10) when the 128-bit product is // unambiguous, else void → caller falls to the decimal slow path. // Divergences at-site: `mantissa <<= clz` (scalar-param mutate) → local // `mnt`; whole-struct local reassign `x = merged` copies only the first // word in cgen → per-field `x.hi = …; x.lo = …` (#155); `po10 = // powers_of_ten[i]` row-bind → direct double-index (#155, A2); bitwise- // vs-compare fully parenthesised; comma let-bindings split. fn eisel_lemire( mantissa: u64, exp10: i32, neg: bool, f: *math.floatinfo, ) (u64 | void) = { if (mantissa == 0u64 || exp10 > 288 || exp10 < -307) { return; }; let idx: i32 = exp10 + 307; let clz: uint = leading_zeroes(mantissa); let mnt: u64 = mantissa << (clz: u64); let shift: u64 = 64u64 - f.mantbits - 3u64; let mask: u64 = (1u64 << shift) - 1u64; // log(10)/log(2) ≈ 217706 / 65536; x / 65536 = x >> 16. let exp: int = (217706 * (exp10: int)) >> 16; let e2: u64 = ((exp + f.expbias + 64): u64) - (clz: u64); let x: r128 = u128mul(mnt, powers_of_ten[idx][1]); if ((x.hi & mask) == mask && (x.lo + mnt) < mnt) { let y: r128 = u128mul(mnt, powers_of_ten[idx][0]); let merged: r128 = r128 { hi = x.hi, lo = x.lo + y.hi }; if (merged.lo < x.lo) { // local-struct-field compound-assign drops the load in // wwstage (sets =1, not +=1) — explicit form, byte-id. merged.hi = merged.hi + 1u64; }; if ((merged.hi & mask) == mask && (merged.lo + 1u64) == 0u64 && (y.lo + mnt) < mnt) { return; }; x.hi = merged.hi; x.lo = merged.lo; }; let msb: u64 = x.hi >> 63u64; let mant: u64 = x.hi >> (msb + shift); e2 -= 1u64 ^ msb; if (x.lo == 0u64 && (x.hi & mask) == 0u64 && (mant & 3u64) == 1u64) { return; }; mant += mant & 1u64; mant >>= 1u64; if ((mant >> (f.mantbits + 1u64)) > 0u64) { mant >>= 1u64; e2 += 1u64; }; if (e2 <= 0u64 || e2 >= (1u64 << f.expbits) - 1u64) { return; }; return mkfloat(mant, (e2: uint), neg, f); }; // ref/hare/strconv/stof.ha:247. Slow-path: decimal `d` → IEEE bits. fn floatbits(d: *decimal, f: *math.floatinfo) (u64 | overflow) = { let e: int = 0; let m: u64 = 0u64; let powtab: [19]i8 = [ 0i8, 3i8, 6i8, 9i8, 13i8, 16i8, 19i8, 23i8, 26i8, 29i8, 33i8, 36i8, 39i8, 43i8, 46i8, 49i8, 53i8, 56i8, 59i8, ]; if (d.nd == (0u64: size) || d.dp < -326) { if (d.negative) { return mkfloat(0u64, (0u32: uint), d.negative, f); }; return 0u64; } else if (d.dp > 310) { return overflow{}; }; if (d.nd <= (19u64: size)) { let dmant: u64 = 0u64; let i: size = (0u64: size); for (i < d.nd) { dmant = 10u64 * dmant + (d.digits[i]: u64); i += (1u64: size); }; let exp10: i32 = d.dp - (d.nd: i32); match (eisel_lemire(dmant, exp10, d.negative, f)) { case let r: u64 => { return r; }; case void => void; }; }; for (d.dp > 0) { let n: int = 0; if ((d.dp: uint) >= (len(powtab): uint)) { n = (maxshift: int); } else { n = (powtab[d.dp]: int); }; decimal_shift(d, -n); e += n; }; for (d.dp <= 0) { let n: int = 0; if (d.dp == 0) { if (d.digits[0] >= 5u8) { break; }; if (d.digits[0] < 2u8) { n = 2; } else { n = 1; }; } else if ((-d.dp) >= (len(powtab): i32)) { n = (maxshift: int); } else { n = (powtab[-d.dp]: int); }; decimal_shift(d, n); e -= n; }; e -= 1; if (e <= -f.expbias + 1) { let nn: int = -f.expbias - e + 1; decimal_shift(d, -nn); e += nn; }; if (e + f.expbias >= ((1u64 << f.expbits): int) - 1) { return overflow{}; }; decimal_shift(d, (f.mantbits: int) + 1); m = decimal_round(d); if (m == (2u64 << f.mantbits)) { m >>= 1u64; e += 1; if (e + f.expbias >= ((1u64 << f.expbits): int) - 1) { return overflow{}; }; }; if ((m & (1u64 << f.mantbits)) == 0u64) { e = -f.expbias; }; return mkfloat(m, ((e + f.expbias): uint), d.negative, f); }; // ref/hare/strconv/stof.ha:311. Assemble sign|exp|mantissa. fn mkfloat(m: u64, e: uint, negative: bool, f: *math.floatinfo) u64 = { let n: u64 = m & ((1u64 << f.mantbits) - 1u64); n |= ((e: u64) & ((1u64 << f.expbits) - 1u64)) << f.mantbits; if (negative) { n |= 1u64 << (f.mantbits + f.expbits); }; return n; }; // ref/hare/strconv/stof.ha:320. Exact f64 powers of ten 1e0..1e22 (all // exactly representable; see stof64exact). let f64pow10: [23]f64 = [ 1.0e0, 1.0e1, 1.0e2, 1.0e3, 1.0e4, 1.0e5, 1.0e6, 1.0e7, 1.0e8, 1.0e9, 1.0e10, 1.0e11, 1.0e12, 1.0e13, 1.0e14, 1.0e15, 1.0e16, 1.0e17, 1.0e18, 1.0e19, 1.0e20, 1.0e21, 1.0e22, ]; // ref/hare/strconv/stof.ha:326. fn stof64exact(mant: u64, exp: i32, neg: bool) (f64 | void) = { if (mant >> math.F64_MANTISSA_BITS != 0u64) { return; }; let n: f64 = (mant: i64): f64; if (neg) { n = -n; }; if (exp == 0i32) { return n; }; if (-22i32 <= exp && exp <= 22i32) { if (exp >= 0i32) { // f64 compound-assign mis-lowers in cgen — explicit // form (strconv.ww f64tos precedent). n = n * f64pow10[exp]; } else { n = n / f64pow10[-exp]; }; } else { return; }; return n; }; // ref/hare/strconv/stof.ha:345. Exact f32 powers of ten 1e0..1e10. let f32pow10: [11]f32 = [ 1.0e0f32, 1.0e1f32, 1.0e2f32, 1.0e3f32, 1.0e4f32, 1.0e5f32, 1.0e6f32, 1.0e7f32, 1.0e8f32, 1.0e9f32, 1.0e10f32, ]; // ref/hare/strconv/stof.ha:349. fn stof32exact(mant: u64, exp: i32, neg: bool) (f32 | void) = { if (mant >> (math.F32_MANTISSA_BITS: u64) != 0u64) { return; }; let n: f32 = (mant: i32): f32; if (neg) { n = -n; }; if (exp == 0i32) { return n; }; if (-10i32 <= exp && exp <= 10i32) { if (exp >= 0i32) { // f32 compound-assign mis-lowers in cgen — explicit form. n = n * f32pow10[exp]; } else { n = n / (f64pow10[-exp]: f32); }; } else { return; }; return n; }; // ref/hare/strconv/stof.ha:369. Adapted from Go's atofHex. The by-value // `p` is mutated in Hare; ww copies its fields to scalar locals (struct // param field-write miscompiles + diverges — filed). fn hex_to_bits(p: fast_parsed_float, info: *math.floatinfo) (u64 | overflow) = { let pmant: u64 = p.mantissa; let pexp: i32 = p.exponent; let pneg: bool = p.negative; let ptrunc: bool = p.truncated; let max_exp: int = ((1u64 << info.expbits): int) - info.expbias - 2; let min_exp: int = -info.expbias + 1; pexp += (info.mantbits: i32); // Shift left until a leading 1 bit followed by mantbits + 2 rounding. for (pmant != 0u64 && pmant >> (info.mantbits + 2u64) == 0u64) { pmant <<= 1u64; pexp -= 1; }; if (ptrunc) { pmant |= 1u64; }; // Too many bits: shift right (sticky-or the dropped bit). for (pmant >> (3u64 + info.mantbits) != 0u64) { pmant = (pmant >> 1u64) | (pmant & 1u64); pexp += 1; }; // Denormalise if the exponent is small. for (pmant > 1u64 && pexp < (min_exp: i32) - 2) { pmant = (pmant >> 1u64) | (pmant & 1u64); pexp += 1; }; // Round to even. let round: u64 = pmant & 3u64; pmant >>= 2u64; round |= pmant & 1u64; pexp += 2; if (round == 3u64) { pmant += 1u64; if (pmant == 1u64 << (1u64 + info.mantbits)) { pmant >>= 1u64; pexp += 1; }; }; // Denormal or zero. if (pmant >> info.mantbits == 0u64) { pexp = (-info.expbias): i32; }; if (pexp > (max_exp: i32)) { return overflow{}; }; let bits: u64 = pmant & info.mantmask; bits |= (((pexp + (info.expbias: i32)): u64) & info.expmask) << info.mantbits; if (pneg) { bits |= 1u64 << (info.mantbits + info.expbits); }; return bits; }; // ref/hare/strconv/stof.ha:425. "nan"/"infinity"/±"infinity", // case-insensitive. ww math has no f32 NAN/INF consts → f32frombits of // the IEEE-754 f32 bit patterns (qNaN 0x7FC00000, ±Inf 0x7F800000 / // 0xFF800000). fn special(s: str) (f32 | void) = { if (ascii.strcasecmp(s, "nan") == 0) { return math.f32frombits(0x7FC00000u32); } else if (ascii.strcasecmp(s, "infinity") == 0) { return math.f32frombits(0x7F800000u32); } else if (ascii.strcasecmp(s, "+infinity") == 0) { return math.f32frombits(0x7F800000u32); } else if (ascii.strcasecmp(s, "-infinity") == 0) { return math.f32frombits(0xFF800000u32); }; return; }; // ref/hare/strconv/stof.ha:445. Parse `s` as f64 (base DEC or HEX). See // the module note: the EL fast path is HELD; the decimal fallback gives // correct results meanwhile. export fn stof64(s: str, b: base) (f64 | invalid | overflow) = { let bb: base = b; if (bb == base.DEFAULT) { bb = base.DEC; } else if (bb == base.HEX_LOWER) { bb = base.HEX; }; os.assert(bb == base.DEC || bb == base.HEX, "strconv.stof64: base must be DEC or HEX"); if (s.len == 0) { return 0: invalid; }; match (special(s)) { case let f: f32 => { return (f: f64); }; case void => void; }; match (fast_parse(s, bb)) { case let p: fast_parsed_float => { if (bb == base.HEX) { match (hex_to_bits(p, &math.f64info)) { case let bits: u64 => { return math.f64frombits(bits); }; case let eo: overflow => { return eo; }; }; } else if (!p.truncated) { match (stof64exact(p.mantissa, p.exponent, p.negative)) { case let n: f64 => { return n; }; case void => void; }; match (eisel_lemire(p.mantissa, p.exponent, p.negative, &math.f64info)) { case let n: u64 => { return math.f64frombits(n); }; case void => void; }; }; let d = decimal { ... }; match (decimal_parse(&d, s)) { case let ei: invalid => { return ei; }; case void => void; }; match (floatbits(&d, &math.f64info)) { case let n: u64 => { return math.f64frombits(n); }; case let eo: overflow => { return eo; }; }; }; case let ei: invalid => { return ei; }; }; return 0: invalid; // unreachable (path-cov) }; // ref/hare/strconv/stof.ha:491. Parse `s` as f32 (base DEC or HEX). export fn stof32(s: str, b: base) (f32 | invalid | overflow) = { let bb: base = b; if (bb == base.DEFAULT) { bb = base.DEC; } else if (bb == base.HEX_LOWER) { bb = base.HEX; }; os.assert(bb == base.DEC || bb == base.HEX, "strconv.stof32: base must be DEC or HEX"); if (s.len == 0) { return 0: invalid; }; match (special(s)) { case let f: f32 => { return f; }; case void => void; }; match (fast_parse(s, bb)) { case let p: fast_parsed_float => { if (bb == base.HEX) { match (hex_to_bits(p, &math.f32info)) { case let bits: u64 => { return math.f32frombits(bits: u32); }; case let eo: overflow => { return eo; }; }; } else if (!p.truncated) { match (stof32exact(p.mantissa, p.exponent, p.negative)) { case let n: f32 => { return n; }; case void => void; }; match (eisel_lemire(p.mantissa, p.exponent, p.negative, &math.f32info)) { case let n: u64 => { return math.f32frombits(n: u32); }; case void => void; }; }; let d = decimal { ... }; match (decimal_parse(&d, s)) { case let ei: invalid => { return ei; }; case void => void; }; match (floatbits(&d, &math.f32info)) { case let n: u64 => { return math.f32frombits(n: u32); }; case let eo: overflow => { return eo; }; }; }; case let ei: invalid => { return ei; }; }; return 0: invalid; // unreachable (path-cov) }; // strconv — stof/ftos lookup tables. Mirrors ref/hare/strconv/stof_data.ha // byte-exact. Pure-data fold (strconv #106 fold-2, was fold-3 before drew // re-sequenced 2026-05-26): no logic, exercised transitively when fold-3's // `leftshift_newdigits` lands (ref/hare/strconv/decimal.ha:35). // // ww uses module-level `let` for compile-time array data (ref/hare/strconv // `const` has no ww keyword equivalent; lib/encoding/utf8/utf8.ww:48 sets // the precedent with [2048]i8 dfa). Literal suffixes (`u16`, `u8`) are // required because cstage rejects bare integer literals in `[N]u8`/`[N]u16` // init while wwstage accepts them; the suffixed form is the only shape // both stages agree on (candidate #130). // // `powers_of_ten: [596][2]u64` (ref/hare/strconv/stof_data.ha:73) is the // Eisel-Lemire fast-path table (consumed by stof.ww's eisel_lemire); it // lands here in fold-4 alongside its consumer, indexed `[exp10 + 307]` // for exp10 in [-307, 288]. Faithful 2D `[596][2]u64` (the {hi,lo} pair // IS the 128-bit truncated power-of-ten; rule-12, not flattened) — the // 2D module-level static-init + double-index read it needs landed in // #156 (cbeffea). See the table at the foot of this file. package strconv; // ref/hare/strconv/stof_data.ha:4. Powers-of-five decimal-expansion // metadata for `leftshift_newdigits` (decimal.ha:35). The top 5 bits // of each entry are the new-digit-count `nn`; the low 11 bits index // `pow5_table` for the digits themselves. let left_shift_table: [65]u16 = [ 0x0000u16, 0x0800u16, 0x0801u16, 0x0803u16, 0x1006u16, 0x1009u16, 0x100Du16, 0x1812u16, 0x1817u16, 0x181Du16, 0x2024u16, 0x202Bu16, 0x2033u16, 0x203Cu16, 0x2846u16, 0x2850u16, 0x285Bu16, 0x3067u16, 0x3073u16, 0x3080u16, 0x388Eu16, 0x389Cu16, 0x38ABu16, 0x38BBu16, 0x40CCu16, 0x40DDu16, 0x40EFu16, 0x4902u16, 0x4915u16, 0x4929u16, 0x513Eu16, 0x5153u16, 0x5169u16, 0x5180u16, 0x5998u16, 0x59B0u16, 0x59C9u16, 0x61E3u16, 0x61FDu16, 0x6218u16, 0x6A34u16, 0x6A50u16, 0x6A6Du16, 0x6A8Bu16, 0x72AAu16, 0x72C9u16, 0x72E9u16, 0x7B0Au16, 0x7B2Bu16, 0x7B4Du16, 0x8370u16, 0x8393u16, 0x83B7u16, 0x83DCu16, 0x8C02u16, 0x8C28u16, 0x8C4Fu16, 0x9477u16, 0x949Fu16, 0x94C8u16, 0x9CF2u16, 0x051Cu16, 0x051Cu16, 0x051Cu16, 0x051Cu16, ]; // ref/hare/strconv/stof_data.ha:15. Decimal digits of 5^k for k=1..60, // concatenated. Indexed via `left_shift_table` (above); each shift k // reads its `pow5_b - pow5_a` digits starting at `pow5_a`. let pow5_table: [0x051C]u8 = [ 5u8, 2u8, 5u8, 1u8, 2u8, 5u8, 6u8, 2u8, 5u8, 3u8, 1u8, 2u8, 5u8, 1u8, 5u8, 6u8, 2u8, 5u8, 7u8, 8u8, 1u8, 2u8, 5u8, 3u8, 9u8, 0u8, 6u8, 2u8, 5u8, 1u8, 9u8, 5u8, 3u8, 1u8, 2u8, 5u8, 9u8, 7u8, 6u8, 5u8, 6u8, 2u8, 5u8, 4u8, 8u8, 8u8, 2u8, 8u8, 1u8, 2u8, 5u8, 2u8, 4u8, 4u8, 1u8, 4u8, 0u8, 6u8, 2u8, 5u8, 1u8, 2u8, 2u8, 0u8, 7u8, 0u8, 3u8, 1u8, 2u8, 5u8, 6u8, 1u8, 0u8, 3u8, 5u8, 1u8, 5u8, 6u8, 2u8, 5u8, 3u8, 0u8, 5u8, 1u8, 7u8, 5u8, 7u8, 8u8, 1u8, 2u8, 5u8, 1u8, 5u8, 2u8, 5u8, 8u8, 7u8, 8u8, 9u8, 0u8, 6u8, 2u8, 5u8, 7u8, 6u8, 2u8, 9u8, 3u8, 9u8, 4u8, 5u8, 3u8, 1u8, 2u8, 5u8, 3u8, 8u8, 1u8, 4u8, 6u8, 9u8, 7u8, 2u8, 6u8, 5u8, 6u8, 2u8, 5u8, 1u8, 9u8, 0u8, 7u8, 3u8, 4u8, 8u8, 6u8, 3u8, 2u8, 8u8, 1u8, 2u8, 5u8, 9u8, 5u8, 3u8, 6u8, 7u8, 4u8, 3u8, 1u8, 6u8, 4u8, 0u8, 6u8, 2u8, 5u8, 4u8, 7u8, 6u8, 8u8, 3u8, 7u8, 1u8, 5u8, 8u8, 2u8, 0u8, 3u8, 1u8, 2u8, 5u8, 2u8, 3u8, 8u8, 4u8, 1u8, 8u8, 5u8, 7u8, 9u8, 1u8, 0u8, 1u8, 5u8, 6u8, 2u8, 5u8, 1u8, 1u8, 9u8, 2u8, 0u8, 9u8, 2u8, 8u8, 9u8, 5u8, 5u8, 0u8, 7u8, 8u8, 1u8, 2u8, 5u8, 5u8, 9u8, 6u8, 0u8, 4u8, 6u8, 4u8, 4u8, 7u8, 7u8, 5u8, 3u8, 9u8, 0u8, 6u8, 2u8, 5u8, 2u8, 9u8, 8u8, 0u8, 2u8, 3u8, 2u8, 2u8, 3u8, 8u8, 7u8, 6u8, 9u8, 5u8, 3u8, 1u8, 2u8, 5u8, 1u8, 4u8, 9u8, 0u8, 1u8, 1u8, 6u8, 1u8, 1u8, 9u8, 3u8, 8u8, 4u8, 7u8, 6u8, 5u8, 6u8, 2u8, 5u8, 7u8, 4u8, 5u8, 0u8, 5u8, 8u8, 0u8, 5u8, 9u8, 6u8, 9u8, 2u8, 3u8, 8u8, 2u8, 8u8, 1u8, 2u8, 5u8, 3u8, 7u8, 2u8, 5u8, 2u8, 9u8, 0u8, 2u8, 9u8, 8u8, 4u8, 6u8, 1u8, 9u8, 1u8, 4u8, 0u8, 6u8, 2u8, 5u8, 1u8, 8u8, 6u8, 2u8, 6u8, 4u8, 5u8, 1u8, 4u8, 9u8, 2u8, 3u8, 0u8, 9u8, 5u8, 7u8, 0u8, 3u8, 1u8, 2u8, 5u8, 9u8, 3u8, 1u8, 3u8, 2u8, 2u8, 5u8, 7u8, 4u8, 6u8, 1u8, 5u8, 4u8, 7u8, 8u8, 5u8, 1u8, 5u8, 6u8, 2u8, 5u8, 4u8, 6u8, 5u8, 6u8, 6u8, 1u8, 2u8, 8u8, 7u8, 3u8, 0u8, 7u8, 7u8, 3u8, 9u8, 2u8, 5u8, 7u8, 8u8, 1u8, 2u8, 5u8, 2u8, 3u8, 2u8, 8u8, 3u8, 0u8, 6u8, 4u8, 3u8, 6u8, 5u8, 3u8, 8u8, 6u8, 9u8, 6u8, 2u8, 8u8, 9u8, 0u8, 6u8, 2u8, 5u8, 1u8, 1u8, 6u8, 4u8, 1u8, 5u8, 3u8, 2u8, 1u8, 8u8, 2u8, 6u8, 9u8, 3u8, 4u8, 8u8, 1u8, 4u8, 4u8, 5u8, 3u8, 1u8, 2u8, 5u8, 5u8, 8u8, 2u8, 0u8, 7u8, 6u8, 6u8, 0u8, 9u8, 1u8, 3u8, 4u8, 6u8, 7u8, 4u8, 0u8, 7u8, 2u8, 2u8, 6u8, 5u8, 6u8, 2u8, 5u8, 2u8, 9u8, 1u8, 0u8, 3u8, 8u8, 3u8, 0u8, 4u8, 5u8, 6u8, 7u8, 3u8, 3u8, 7u8, 0u8, 3u8, 6u8, 1u8, 3u8, 2u8, 8u8, 1u8, 2u8, 5u8, 1u8, 4u8, 5u8, 5u8, 1u8, 9u8, 1u8, 5u8, 2u8, 2u8, 8u8, 3u8, 6u8, 6u8, 8u8, 5u8, 1u8, 8u8, 0u8, 6u8, 6u8, 4u8, 0u8, 6u8, 2u8, 5u8, 7u8, 2u8, 7u8, 5u8, 9u8, 5u8, 7u8, 6u8, 1u8, 4u8, 1u8, 8u8, 3u8, 4u8, 2u8, 5u8, 9u8, 0u8, 3u8, 3u8, 2u8, 0u8, 3u8, 1u8, 2u8, 5u8, 3u8, 6u8, 3u8, 7u8, 9u8, 7u8, 8u8, 8u8, 0u8, 7u8, 0u8, 9u8, 1u8, 7u8, 1u8, 2u8, 9u8, 5u8, 1u8, 6u8, 6u8, 0u8, 1u8, 5u8, 6u8, 2u8, 5u8, 1u8, 8u8, 1u8, 8u8, 9u8, 8u8, 9u8, 4u8, 0u8, 3u8, 5u8, 4u8, 5u8, 8u8, 5u8, 6u8, 4u8, 7u8, 5u8, 8u8, 3u8, 0u8, 0u8, 7u8, 8u8, 1u8, 2u8, 5u8, 9u8, 0u8, 9u8, 4u8, 9u8, 4u8, 7u8, 0u8, 1u8, 7u8, 7u8, 2u8, 9u8, 2u8, 8u8, 2u8, 3u8, 7u8, 9u8, 1u8, 5u8, 0u8, 3u8, 9u8, 0u8, 6u8, 2u8, 5u8, 4u8, 5u8, 4u8, 7u8, 4u8, 7u8, 3u8, 5u8, 0u8, 8u8, 8u8, 6u8, 4u8, 6u8, 4u8, 1u8, 1u8, 8u8, 9u8, 5u8, 7u8, 5u8, 1u8, 9u8, 5u8, 3u8, 1u8, 2u8, 5u8, 2u8, 2u8, 7u8, 3u8, 7u8, 3u8, 6u8, 7u8, 5u8, 4u8, 4u8, 3u8, 2u8, 3u8, 2u8, 0u8, 5u8, 9u8, 4u8, 7u8, 8u8, 7u8, 5u8, 9u8, 7u8, 6u8, 5u8, 6u8, 2u8, 5u8, 1u8, 1u8, 3u8, 6u8, 8u8, 6u8, 8u8, 3u8, 7u8, 7u8, 2u8, 1u8, 6u8, 1u8, 6u8, 0u8, 2u8, 9u8, 7u8, 3u8, 9u8, 3u8, 7u8, 9u8, 8u8, 8u8, 2u8, 8u8, 1u8, 2u8, 5u8, 5u8, 6u8, 8u8, 4u8, 3u8, 4u8, 1u8, 8u8, 8u8, 6u8, 0u8, 8u8, 0u8, 8u8, 0u8, 1u8, 4u8, 8u8, 6u8, 9u8, 6u8, 8u8, 9u8, 9u8, 4u8, 1u8, 4u8, 0u8, 6u8, 2u8, 5u8, 2u8, 8u8, 4u8, 2u8, 1u8, 7u8, 0u8, 9u8, 4u8, 3u8, 0u8, 4u8, 0u8, 4u8, 0u8, 0u8, 7u8, 4u8, 3u8, 4u8, 8u8, 4u8, 4u8, 9u8, 7u8, 0u8, 7u8, 0u8, 3u8, 1u8, 2u8, 5u8, 1u8, 4u8, 2u8, 1u8, 0u8, 8u8, 5u8, 4u8, 7u8, 1u8, 5u8, 2u8, 0u8, 2u8, 0u8, 0u8, 3u8, 7u8, 1u8, 7u8, 4u8, 2u8, 2u8, 4u8, 8u8, 5u8, 3u8, 5u8, 1u8, 5u8, 6u8, 2u8, 5u8, 7u8, 1u8, 0u8, 5u8, 4u8, 2u8, 7u8, 3u8, 5u8, 7u8, 6u8, 0u8, 1u8, 0u8, 0u8, 1u8, 8u8, 5u8, 8u8, 7u8, 1u8, 1u8, 2u8, 4u8, 2u8, 6u8, 7u8, 5u8, 7u8, 8u8, 1u8, 2u8, 5u8, 3u8, 5u8, 5u8, 2u8, 7u8, 1u8, 3u8, 6u8, 7u8, 8u8, 8u8, 0u8, 0u8, 5u8, 0u8, 0u8, 9u8, 2u8, 9u8, 3u8, 5u8, 5u8, 6u8, 2u8, 1u8, 3u8, 3u8, 7u8, 8u8, 9u8, 0u8, 6u8, 2u8, 5u8, 1u8, 7u8, 7u8, 6u8, 3u8, 5u8, 6u8, 8u8, 3u8, 9u8, 4u8, 0u8, 0u8, 2u8, 5u8, 0u8, 4u8, 6u8, 4u8, 6u8, 7u8, 7u8, 8u8, 1u8, 0u8, 6u8, 6u8, 8u8, 9u8, 4u8, 5u8, 3u8, 1u8, 2u8, 5u8, 8u8, 8u8, 8u8, 1u8, 7u8, 8u8, 4u8, 1u8, 9u8, 7u8, 0u8, 0u8, 1u8, 2u8, 5u8, 2u8, 3u8, 2u8, 3u8, 3u8, 8u8, 9u8, 0u8, 5u8, 3u8, 3u8, 4u8, 4u8, 7u8, 2u8, 6u8, 5u8, 6u8, 2u8, 5u8, 4u8, 4u8, 4u8, 0u8, 8u8, 9u8, 2u8, 0u8, 9u8, 8u8, 5u8, 0u8, 0u8, 6u8, 2u8, 6u8, 1u8, 6u8, 1u8, 6u8, 9u8, 4u8, 5u8, 2u8, 6u8, 6u8, 7u8, 2u8, 3u8, 6u8, 3u8, 2u8, 8u8, 1u8, 2u8, 5u8, 2u8, 2u8, 2u8, 0u8, 4u8, 4u8, 6u8, 0u8, 4u8, 9u8, 2u8, 5u8, 0u8, 3u8, 1u8, 3u8, 0u8, 8u8, 0u8, 8u8, 4u8, 7u8, 2u8, 6u8, 3u8, 3u8, 3u8, 6u8, 1u8, 8u8, 1u8, 6u8, 4u8, 0u8, 6u8, 2u8, 5u8, 1u8, 1u8, 1u8, 0u8, 2u8, 2u8, 3u8, 0u8, 2u8, 4u8, 6u8, 2u8, 5u8, 1u8, 5u8, 6u8, 5u8, 4u8, 0u8, 4u8, 2u8, 3u8, 6u8, 3u8, 1u8, 6u8, 6u8, 8u8, 0u8, 9u8, 0u8, 8u8, 2u8, 0u8, 3u8, 1u8, 2u8, 5u8, 5u8, 5u8, 5u8, 1u8, 1u8, 1u8, 5u8, 1u8, 2u8, 3u8, 1u8, 2u8, 5u8, 7u8, 8u8, 2u8, 7u8, 0u8, 2u8, 1u8, 1u8, 8u8, 1u8, 5u8, 8u8, 3u8, 4u8, 0u8, 4u8, 5u8, 4u8, 1u8, 0u8, 1u8, 5u8, 6u8, 2u8, 5u8, 2u8, 7u8, 7u8, 5u8, 5u8, 5u8, 7u8, 5u8, 6u8, 1u8, 5u8, 6u8, 2u8, 8u8, 9u8, 1u8, 3u8, 5u8, 1u8, 0u8, 5u8, 9u8, 0u8, 7u8, 9u8, 1u8, 7u8, 0u8, 2u8, 2u8, 7u8, 0u8, 5u8, 0u8, 7u8, 8u8, 1u8, 2u8, 5u8, 1u8, 3u8, 8u8, 7u8, 7u8, 7u8, 8u8, 7u8, 8u8, 0u8, 7u8, 8u8, 1u8, 4u8, 4u8, 5u8, 6u8, 7u8, 5u8, 5u8, 2u8, 9u8, 5u8, 3u8, 9u8, 5u8, 8u8, 5u8, 1u8, 1u8, 3u8, 5u8, 2u8, 5u8, 3u8, 9u8, 0u8, 6u8, 2u8, 5u8, 6u8, 9u8, 3u8, 8u8, 8u8, 9u8, 3u8, 9u8, 0u8, 3u8, 9u8, 0u8, 7u8, 2u8, 2u8, 8u8, 3u8, 7u8, 7u8, 6u8, 4u8, 7u8, 6u8, 9u8, 7u8, 9u8, 2u8, 5u8, 5u8, 6u8, 7u8, 6u8, 2u8, 6u8, 9u8, 5u8, 3u8, 1u8, 2u8, 5u8, 3u8, 4u8, 6u8, 9u8, 4u8, 4u8, 6u8, 9u8, 5u8, 1u8, 9u8, 5u8, 3u8, 6u8, 1u8, 4u8, 1u8, 8u8, 8u8, 8u8, 2u8, 3u8, 8u8, 4u8, 8u8, 9u8, 6u8, 2u8, 7u8, 8u8, 3u8, 8u8, 1u8, 3u8, 4u8, 7u8, 6u8, 5u8, 6u8, 2u8, 5u8, 1u8, 7u8, 3u8, 4u8, 7u8, 2u8, 3u8, 4u8, 7u8, 5u8, 9u8, 7u8, 6u8, 8u8, 0u8, 7u8, 0u8, 9u8, 4u8, 4u8, 1u8, 1u8, 9u8, 2u8, 4u8, 4u8, 8u8, 1u8, 3u8, 9u8, 1u8, 9u8, 0u8, 6u8, 7u8, 3u8, 8u8, 2u8, 8u8, 1u8, 2u8, 5u8, 8u8, 6u8, 7u8, 3u8, 6u8, 1u8, 7u8, 3u8, 7u8, 9u8, 8u8, 8u8, 4u8, 0u8, 3u8, 5u8, 4u8, 7u8, 2u8, 0u8, 5u8, 9u8, 6u8, 2u8, 2u8, 4u8, 0u8, 6u8, 9u8, 5u8, 9u8, 5u8, 3u8, 3u8, 6u8, 9u8, 1u8, 4u8, 0u8, 6u8, 2u8, 5u8, ]; // ref/hare/strconv/stof_data.ha:73. Eisel-Lemire 128-bit power-of-ten // table (see header note). 596 rows, {hi, lo} u64 pair per row. let powers_of_ten: [596][2]u64 = [ [0xA5D3B6D479F8E056u64, 0x8FD0C16206306BABu64], [0x8F48A4899877186Cu64, 0xB3C4F1BA87BC8696u64], [0x331ACDABFE94DE87u64, 0xE0B62E2929ABA83Cu64], [0x9FF0C08B7F1D0B14u64, 0x8C71DCD9BA0B4925u64], [0x07ECF0AE5EE44DD9u64, 0xAF8E5410288E1B6Fu64], [0xC9E82CD9F69D6150u64, 0xDB71E91432B1A24Au64], [0xBE311C083A225CD2u64, 0x892731AC9FAF056Eu64], [0x6DBD630A48AAF406u64, 0xAB70FE17C79AC6CAu64], [0x092CBBCCDAD5B108u64, 0xD64D3D9DB981787Du64], [0x25BBF56008C58EA5u64, 0x85F0468293F0EB4Eu64], [0xAF2AF2B80AF6F24Eu64, 0xA76C582338ED2621u64], [0x1AF5AF660DB4AEE1u64, 0xD1476E2C07286FAAu64], [0x50D98D9FC890ED4Du64, 0x82CCA4DB847945CAu64], [0xE50FF107BAB528A0u64, 0xA37FCE126597973Cu64], [0x1E53ED49A96272C8u64, 0xCC5FC196FEFD7D0Cu64], [0x25E8E89C13BB0F7Au64, 0xFF77B1FCBEBCDC4Fu64], [0x77B191618C54E9ACu64, 0x9FAACF3DF73609B1u64], [0xD59DF5B9EF6A2417u64, 0xC795830D75038C1Du64], [0x4B0573286B44AD1Du64, 0xF97AE3D0D2446F25u64], [0x4EE367F9430AEC32u64, 0x9BECCE62836AC577u64], [0x229C41F793CDA73Fu64, 0xC2E801FB244576D5u64], [0x6B43527578C1110Fu64, 0xF3A20279ED56D48Au64], [0x830A13896B78AAA9u64, 0x9845418C345644D6u64], [0x23CC986BC656D553u64, 0xBE5691EF416BD60Cu64], [0x2CBFBE86B7EC8AA8u64, 0xEDEC366B11C6CB8Fu64], [0x7BF7D71432F3D6A9u64, 0x94B3A202EB1C3F39u64], [0xDAF5CCD93FB0CC53u64, 0xB9E08A83A5E34F07u64], [0xD1B3400F8F9CFF68u64, 0xE858AD248F5C22C9u64], [0x23100809B9C21FA1u64, 0x91376C36D99995BEu64], [0xABD40A0C2832A78Au64, 0xB58547448FFFFB2Du64], [0x16C90C8F323F516Cu64, 0xE2E69915B3FFF9F9u64], [0xAE3DA7D97F6792E3u64, 0x8DD01FAD907FFC3Bu64], [0x99CD11CFDF41779Cu64, 0xB1442798F49FFB4Au64], [0x40405643D711D583u64, 0xDD95317F31C7FA1Du64], [0x482835EA666B2572u64, 0x8A7D3EEF7F1CFC52u64], [0xDA3243650005EECFu64, 0xAD1C8EAB5EE43B66u64], [0x90BED43E40076A82u64, 0xD863B256369D4A40u64], [0x5A7744A6E804A291u64, 0x873E4F75E2224E68u64], [0x711515D0A205CB36u64, 0xA90DE3535AAAE202u64], [0x0D5A5B44CA873E03u64, 0xD3515C2831559A83u64], [0xE858790AFE9486C2u64, 0x8412D9991ED58091u64], [0x626E974DBE39A872u64, 0xA5178FFF668AE0B6u64], [0xFB0A3D212DC8128Fu64, 0xCE5D73FF402D98E3u64], [0x7CE66634BC9D0B99u64, 0x80FA687F881C7F8Eu64], [0x1C1FFFC1EBC44E80u64, 0xA139029F6A239F72u64], [0xA327FFB266B56220u64, 0xC987434744AC874Eu64], [0x4BF1FF9F0062BAA8u64, 0xFBE9141915D7A922u64], [0x6F773FC3603DB4A9u64, 0x9D71AC8FADA6C9B5u64], [0xCB550FB4384D21D3u64, 0xC4CE17B399107C22u64], [0x7E2A53A146606A48u64, 0xF6019DA07F549B2Bu64], [0x2EDA7444CBFC426Du64, 0x99C102844F94E0FBu64], [0xFA911155FEFB5308u64, 0xC0314325637A1939u64], [0x793555AB7EBA27CAu64, 0xF03D93EEBC589F88u64], [0x4BC1558B2F3458DEu64, 0x96267C7535B763B5u64], [0x9EB1AAEDFB016F16u64, 0xBBB01B9283253CA2u64], [0x465E15A979C1CADCu64, 0xEA9C227723EE8BCBu64], [0x0BFACD89EC191EC9u64, 0x92A1958A7675175Fu64], [0xCEF980EC671F667Bu64, 0xB749FAED14125D36u64], [0x82B7E12780E7401Au64, 0xE51C79A85916F484u64], [0xD1B2ECB8B0908810u64, 0x8F31CC0937AE58D2u64], [0x861FA7E6DCB4AA15u64, 0xB2FE3F0B8599EF07u64], [0x67A791E093E1D49Au64, 0xDFBDCECE67006AC9u64], [0xE0C8BB2C5C6D24E0u64, 0x8BD6A141006042BDu64], [0x58FAE9F773886E18u64, 0xAECC49914078536Du64], [0xAF39A475506A899Eu64, 0xDA7F5BF590966848u64], [0x6D8406C952429603u64, 0x888F99797A5E012Du64], [0xC8E5087BA6D33B83u64, 0xAAB37FD7D8F58178u64], [0xFB1E4A9A90880A64u64, 0xD5605FCDCF32E1D6u64], [0x5CF2EEA09A55067Fu64, 0x855C3BE0A17FCD26u64], [0xF42FAA48C0EA481Eu64, 0xA6B34AD8C9DFC06Fu64], [0xF13B94DAF124DA26u64, 0xD0601D8EFC57B08Bu64], [0x76C53D08D6B70858u64, 0x823C12795DB6CE57u64], [0x54768C4B0C64CA6Eu64, 0xA2CB1717B52481EDu64], [0xA9942F5DCF7DFD09u64, 0xCB7DDCDDA26DA268u64], [0xD3F93B35435D7C4Cu64, 0xFE5D54150B090B02u64], [0xC47BC5014A1A6DAFu64, 0x9EFA548D26E5A6E1u64], [0x359AB6419CA1091Bu64, 0xC6B8E9B0709F109Au64], [0xC30163D203C94B62u64, 0xF867241C8CC6D4C0u64], [0x79E0DE63425DCF1Du64, 0x9B407691D7FC44F8u64], [0x985915FC12F542E4u64, 0xC21094364DFB5636u64], [0x3E6F5B7B17B2939Du64, 0xF294B943E17A2BC4u64], [0xA705992CEECF9C42u64, 0x979CF3CA6CEC5B5Au64], [0x50C6FF782A838353u64, 0xBD8430BD08277231u64], [0xA4F8BF5635246428u64, 0xECE53CEC4A314EBDu64], [0x871B7795E136BE99u64, 0x940F4613AE5ED136u64], [0x28E2557B59846E3Fu64, 0xB913179899F68584u64], [0x331AEADA2FE589CFu64, 0xE757DD7EC07426E5u64], [0x3FF0D2C85DEF7621u64, 0x9096EA6F3848984Fu64], [0x0FED077A756B53A9u64, 0xB4BCA50B065ABE63u64], [0xD3E8495912C62894u64, 0xE1EBCE4DC7F16DFBu64], [0x64712DD7ABBBD95Cu64, 0x8D3360F09CF6E4BDu64], [0xBD8D794D96AACFB3u64, 0xB080392CC4349DECu64], [0xECF0D7A0FC5583A0u64, 0xDCA04777F541C567u64], [0xF41686C49DB57244u64, 0x89E42CAAF9491B60u64], [0x311C2875C522CED5u64, 0xAC5D37D5B79B6239u64], [0x7D633293366B828Bu64, 0xD77485CB25823AC7u64], [0xAE5DFF9C02033197u64, 0x86A8D39EF77164BCu64], [0xD9F57F830283FDFCu64, 0xA8530886B54DBDEBu64], [0xD072DF63C324FD7Bu64, 0xD267CAA862A12D66u64], [0x4247CB9E59F71E6Du64, 0x8380DEA93DA4BC60u64], [0x52D9BE85F074E608u64, 0xA46116538D0DEB78u64], [0x67902E276C921F8Bu64, 0xCD795BE870516656u64], [0x00BA1CD8A3DB53B6u64, 0x806BD9714632DFF6u64], [0x80E8A40ECCD228A4u64, 0xA086CFCD97BF97F3u64], [0x6122CD128006B2CDu64, 0xC8A883C0FDAF7DF0u64], [0x796B805720085F81u64, 0xFAD2A4B13D1B5D6Cu64], [0xCBE3303674053BB0u64, 0x9CC3A6EEC6311A63u64], [0xBEDBFC4411068A9Cu64, 0xC3F490AA77BD60FCu64], [0xEE92FB5515482D44u64, 0xF4F1B4D515ACB93Bu64], [0x751BDD152D4D1C4Au64, 0x991711052D8BF3C5u64], [0xD262D45A78A0635Du64, 0xBF5CD54678EEF0B6u64], [0x86FB897116C87C34u64, 0xEF340A98172AACE4u64], [0xD45D35E6AE3D4DA0u64, 0x9580869F0E7AAC0Eu64], [0x8974836059CCA109u64, 0xBAE0A846D2195712u64], [0x2BD1A438703FC94Bu64, 0xE998D258869FACD7u64], [0x7B6306A34627DDCFu64, 0x91FF83775423CC06u64], [0x1A3BC84C17B1D542u64, 0xB67F6455292CBF08u64], [0x20CABA5F1D9E4A93u64, 0xE41F3D6A7377EECAu64], [0x547EB47B7282EE9Cu64, 0x8E938662882AF53Eu64], [0xE99E619A4F23AA43u64, 0xB23867FB2A35B28Du64], [0x6405FA00E2EC94D4u64, 0xDEC681F9F4C31F31u64], [0xDE83BC408DD3DD04u64, 0x8B3C113C38F9F37Eu64], [0x9624AB50B148D445u64, 0xAE0B158B4738705Eu64], [0x3BADD624DD9B0957u64, 0xD98DDAEE19068C76u64], [0xE54CA5D70A80E5D6u64, 0x87F8A8D4CFA417C9u64], [0x5E9FCF4CCD211F4Cu64, 0xA9F6D30A038D1DBCu64], [0x7647C3200069671Fu64, 0xD47487CC8470652Bu64], [0x29ECD9F40041E073u64, 0x84C8D4DFD2C63F3Bu64], [0xF468107100525890u64, 0xA5FB0A17C777CF09u64], [0x7182148D4066EEB4u64, 0xCF79CC9DB955C2CCu64], [0xC6F14CD848405530u64, 0x81AC1FE293D599BFu64], [0xB8ADA00E5A506A7Cu64, 0xA21727DB38CB002Fu64], [0xA6D90811F0E4851Cu64, 0xCA9CF1D206FDC03Bu64], [0x908F4A166D1DA663u64, 0xFD442E4688BD304Au64], [0x9A598E4E043287FEu64, 0x9E4A9CEC15763E2Eu64], [0x40EFF1E1853F29FDu64, 0xC5DD44271AD3CDBAu64], [0xD12BEE59E68EF47Cu64, 0xF7549530E188C128u64], [0x82BB74F8301958CEu64, 0x9A94DD3E8CF578B9u64], [0xE36A52363C1FAF01u64, 0xC13A148E3032D6E7u64], [0xDC44E6C3CB279AC1u64, 0xF18899B1BC3F8CA1u64], [0x29AB103A5EF8C0B9u64, 0x96F5600F15A7B7E5u64], [0x7415D448F6B6F0E7u64, 0xBCB2B812DB11A5DEu64], [0x111B495B3464AD21u64, 0xEBDF661791D60F56u64], [0xCAB10DD900BEEC34u64, 0x936B9FCEBB25C995u64], [0x3D5D514F40EEA742u64, 0xB84687C269EF3BFBu64], [0x0CB4A5A3112A5112u64, 0xE65829B3046B0AFAu64], [0x47F0E785EABA72ABu64, 0x8FF71A0FE2C2E6DCu64], [0x59ED216765690F56u64, 0xB3F4E093DB73A093u64], [0x306869C13EC3532Cu64, 0xE0F218B8D25088B8u64], [0x1E414218C73A13FBu64, 0x8C974F7383725573u64], [0xE5D1929EF90898FAu64, 0xAFBD2350644EEACFu64], [0xDF45F746B74ABF39u64, 0xDBAC6C247D62A583u64], [0x6B8BBA8C328EB783u64, 0x894BC396CE5DA772u64], [0x066EA92F3F326564u64, 0xAB9EB47C81F5114Fu64], [0xC80A537B0EFEFEBDu64, 0xD686619BA27255A2u64], [0xBD06742CE95F5F36u64, 0x8613FD0145877585u64], [0x2C48113823B73704u64, 0xA798FC4196E952E7u64], [0xF75A15862CA504C5u64, 0xD17F3B51FCA3A7A0u64], [0x9A984D73DBE722FBu64, 0x82EF85133DE648C4u64], [0xC13E60D0D2E0EBBAu64, 0xA3AB66580D5FDAF5u64], [0x318DF905079926A8u64, 0xCC963FEE10B7D1B3u64], [0xFDF17746497F7052u64, 0xFFBBCFE994E5C61Fu64], [0xFEB6EA8BEDEFA633u64, 0x9FD561F1FD0F9BD3u64], [0xFE64A52EE96B8FC0u64, 0xC7CABA6E7C5382C8u64], [0x3DFDCE7AA3C673B0u64, 0xF9BD690A1B68637Bu64], [0x06BEA10CA65C084Eu64, 0x9C1661A651213E2Du64], [0x486E494FCFF30A62u64, 0xC31BFA0FE5698DB8u64], [0x5A89DBA3C3EFCCFAu64, 0xF3E2F893DEC3F126u64], [0xF89629465A75E01Cu64, 0x986DDB5C6B3A76B7u64], [0xF6BBB397F1135823u64, 0xBE89523386091465u64], [0x746AA07DED582E2Cu64, 0xEE2BA6C0678B597Fu64], [0xA8C2A44EB4571CDCu64, 0x94DB483840B717EFu64], [0x92F34D62616CE413u64, 0xBA121A4650E4DDEBu64], [0x77B020BAF9C81D17u64, 0xE896A0D7E51E1566u64], [0x0ACE1474DC1D122Eu64, 0x915E2486EF32CD60u64], [0x0D819992132456BAu64, 0xB5B5ADA8AAFF80B8u64], [0x10E1FFF697ED6C69u64, 0xE3231912D5BF60E6u64], [0xCA8D3FFA1EF463C1u64, 0x8DF5EFABC5979C8Fu64], [0xBD308FF8A6B17CB2u64, 0xB1736B96B6FD83B3u64], [0xAC7CB3F6D05DDBDEu64, 0xDDD0467C64BCE4A0u64], [0x6BCDF07A423AA96Bu64, 0x8AA22C0DBEF60EE4u64], [0x86C16C98D2C953C6u64, 0xAD4AB7112EB3929Du64], [0xE871C7BF077BA8B7u64, 0xD89D64D57A607744u64], [0x11471CD764AD4972u64, 0x87625F056C7C4A8Bu64], [0xD598E40D3DD89BCFu64, 0xA93AF6C6C79B5D2Du64], [0x4AFF1D108D4EC2C3u64, 0xD389B47879823479u64], [0xCEDF722A585139BAu64, 0x843610CB4BF160CBu64], [0xC2974EB4EE658828u64, 0xA54394FE1EEDB8FEu64], [0x733D226229FEEA32u64, 0xCE947A3DA6A9273Eu64], [0x0806357D5A3F525Fu64, 0x811CCC668829B887u64], [0xCA07C2DCB0CF26F7u64, 0xA163FF802A3426A8u64], [0xFC89B393DD02F0B5u64, 0xC9BCFF6034C13052u64], [0xBBAC2078D443ACE2u64, 0xFC2C3F3841F17C67u64], [0xD54B944B84AA4C0Du64, 0x9D9BA7832936EDC0u64], [0x0A9E795E65D4DF11u64, 0xC5029163F384A931u64], [0x4D4617B5FF4A16D5u64, 0xF64335BCF065D37Du64], [0x504BCED1BF8E4E45u64, 0x99EA0196163FA42Eu64], [0xE45EC2862F71E1D6u64, 0xC06481FB9BCF8D39u64], [0x5D767327BB4E5A4Cu64, 0xF07DA27A82C37088u64], [0x3A6A07F8D510F86Fu64, 0x964E858C91BA2655u64], [0x890489F70A55368Bu64, 0xBBE226EFB628AFEAu64], [0x2B45AC74CCEA842Eu64, 0xEADAB0ABA3B2DBE5u64], [0x3B0B8BC90012929Du64, 0x92C8AE6B464FC96Fu64], [0x09CE6EBB40173744u64, 0xB77ADA0617E3BBCBu64], [0xCC420A6A101D0515u64, 0xE55990879DDCAABDu64], [0x9FA946824A12232Du64, 0x8F57FA54C2A9EAB6u64], [0x47939822DC96ABF9u64, 0xB32DF8E9F3546564u64], [0x59787E2B93BC56F7u64, 0xDFF9772470297EBDu64], [0x57EB4EDB3C55B65Au64, 0x8BFBEA76C619EF36u64], [0xEDE622920B6B23F1u64, 0xAEFAE51477A06B03u64], [0xE95FAB368E45ECEDu64, 0xDAB99E59958885C4u64], [0x11DBCB0218EBB414u64, 0x88B402F7FD75539Bu64], [0xD652BDC29F26A119u64, 0xAAE103B5FCD2A881u64], [0x4BE76D3346F0495Fu64, 0xD59944A37C0752A2u64], [0x6F70A4400C562DDBu64, 0x857FCAE62D8493A5u64], [0xCB4CCD500F6BB952u64, 0xA6DFBD9FB8E5B88Eu64], [0x7E2000A41346A7A7u64, 0xD097AD07A71F26B2u64], [0x8ED400668C0C28C8u64, 0x825ECC24C873782Fu64], [0x728900802F0F32FAu64, 0xA2F67F2DFA90563Bu64], [0x4F2B40A03AD2FFB9u64, 0xCBB41EF979346BCAu64], [0xE2F610C84987BFA8u64, 0xFEA126B7D78186BCu64], [0x0DD9CA7D2DF4D7C9u64, 0x9F24B832E6B0F436u64], [0x91503D1C79720DBBu64, 0xC6EDE63FA05D3143u64], [0x75A44C6397CE912Au64, 0xF8A95FCF88747D94u64], [0xC986AFBE3EE11ABAu64, 0x9B69DBE1B548CE7Cu64], [0xFBE85BADCE996168u64, 0xC24452DA229B021Bu64], [0xFAE27299423FB9C3u64, 0xF2D56790AB41C2A2u64], [0xDCCD879FC967D41Au64, 0x97C560BA6B0919A5u64], [0x5400E987BBC1C920u64, 0xBDB6B8E905CB600Fu64], [0x290123E9AAB23B68u64, 0xED246723473E3813u64], [0xF9A0B6720AAF6521u64, 0x9436C0760C86E30Bu64], [0xF808E40E8D5B3E69u64, 0xB94470938FA89BCEu64], [0xB60B1D1230B20E04u64, 0xE7958CB87392C2C2u64], [0xB1C6F22B5E6F48C2u64, 0x90BD77F3483BB9B9u64], [0x1E38AEB6360B1AF3u64, 0xB4ECD5F01A4AA828u64], [0x25C6DA63C38DE1B0u64, 0xE2280B6C20DD5232u64], [0x579C487E5A38AD0Eu64, 0x8D590723948A535Fu64], [0x2D835A9DF0C6D851u64, 0xB0AF48EC79ACE837u64], [0xF8E431456CF88E65u64, 0xDCDB1B2798182244u64], [0x1B8E9ECB641B58FFu64, 0x8A08F0F8BF0F156Bu64], [0xE272467E3D222F3Fu64, 0xAC8B2D36EED2DAC5u64], [0x5B0ED81DCC6ABB0Fu64, 0xD7ADF884AA879177u64], [0x98E947129FC2B4E9u64, 0x86CCBB52EA94BAEAu64], [0x3F2398D747B36224u64, 0xA87FEA27A539E9A5u64], [0x8EEC7F0D19A03AADu64, 0xD29FE4B18E88640Eu64], [0x1953CF68300424ACu64, 0x83A3EEEEF9153E89u64], [0x5FA8C3423C052DD7u64, 0xA48CEAAAB75A8E2Bu64], [0x3792F412CB06794Du64, 0xCDB02555653131B6u64], [0xE2BBD88BBEE40BD0u64, 0x808E17555F3EBF11u64], [0x5B6ACEAEAE9D0EC4u64, 0xA0B19D2AB70E6ED6u64], [0xF245825A5A445275u64, 0xC8DE047564D20A8Bu64], [0xEED6E2F0F0D56712u64, 0xFB158592BE068D2Eu64], [0x55464DD69685606Bu64, 0x9CED737BB6C4183Du64], [0xAA97E14C3C26B886u64, 0xC428D05AA4751E4Cu64], [0xD53DD99F4B3066A8u64, 0xF53304714D9265DFu64], [0xE546A8038EFE4029u64, 0x993FE2C6D07B7FABu64], [0xDE98520472BDD033u64, 0xBF8FDB78849A5F96u64], [0x963E66858F6D4440u64, 0xEF73D256A5C0F77Cu64], [0xDDE7001379A44AA8u64, 0x95A8637627989AADu64], [0x5560C018580D5D52u64, 0xBB127C53B17EC159u64], [0xAAB8F01E6E10B4A6u64, 0xE9D71B689DDE71AFu64], [0xCAB3961304CA70E8u64, 0x9226712162AB070Du64], [0x3D607B97C5FD0D22u64, 0xB6B00D69BB55C8D1u64], [0x8CB89A7DB77C506Au64, 0xE45C10C42A2B3B05u64], [0x77F3608E92ADB242u64, 0x8EB98A7A9A5B04E3u64], [0x55F038B237591ED3u64, 0xB267ED1940F1C61Cu64], [0x6B6C46DEC52F6688u64, 0xDF01E85F912E37A3u64], [0x2323AC4B3B3DA015u64, 0x8B61313BBABCE2C6u64], [0xABEC975E0A0D081Au64, 0xAE397D8AA96C1B77u64], [0x96E7BD358C904A21u64, 0xD9C7DCED53C72255u64], [0x7E50D64177DA2E54u64, 0x881CEA14545C7575u64], [0xDDE50BD1D5D0B9E9u64, 0xAA242499697392D2u64], [0x955E4EC64B44E864u64, 0xD4AD2DBFC3D07787u64], [0xBD5AF13BEF0B113Eu64, 0x84EC3C97DA624AB4u64], [0xECB1AD8AEACDD58Eu64, 0xA6274BBDD0FADD61u64], [0x67DE18EDA5814AF2u64, 0xCFB11EAD453994BAu64], [0x80EACF948770CED7u64, 0x81CEB32C4B43FCF4u64], [0xA1258379A94D028Du64, 0xA2425FF75E14FC31u64], [0x096EE45813A04330u64, 0xCAD2F7F5359A3B3Eu64], [0x8BCA9D6E188853FCu64, 0xFD87B5F28300CA0Du64], [0x775EA264CF55347Du64, 0x9E74D1B791E07E48u64], [0x95364AFE032A819Du64, 0xC612062576589DDAu64], [0x3A83DDBD83F52204u64, 0xF79687AED3EEC551u64], [0xC4926A9672793542u64, 0x9ABE14CD44753B52u64], [0x75B7053C0F178293u64, 0xC16D9A0095928A27u64], [0x5324C68B12DD6338u64, 0xF1C90080BAF72CB1u64], [0xD3F6FC16EBCA5E03u64, 0x971DA05074DA7BEEu64], [0x88F4BB1CA6BCF584u64, 0xBCE5086492111AEAu64], [0x2B31E9E3D06C32E5u64, 0xEC1E4A7DB69561A5u64], [0x3AFF322E62439FCFu64, 0x9392EE8E921D5D07u64], [0x09BEFEB9FAD487C2u64, 0xB877AA3236A4B449u64], [0x4C2EBE687989A9B3u64, 0xE69594BEC44DE15Bu64], [0x0F9D37014BF60A10u64, 0x901D7CF73AB0ACD9u64], [0x538484C19EF38C94u64, 0xB424DC35095CD80Fu64], [0x2865A5F206B06FB9u64, 0xE12E13424BB40E13u64], [0xF93F87B7442E45D3u64, 0x8CBCCC096F5088CBu64], [0xF78F69A51539D748u64, 0xAFEBFF0BCB24AAFEu64], [0xB573440E5A884D1Bu64, 0xDBE6FECEBDEDD5BEu64], [0x31680A88F8953030u64, 0x89705F4136B4A597u64], [0xFDC20D2B36BA7C3Du64, 0xABCC77118461CEFCu64], [0x3D32907604691B4Cu64, 0xD6BF94D5E57A42BCu64], [0xA63F9A49C2C1B10Fu64, 0x8637BD05AF6C69B5u64], [0x0FCF80DC33721D53u64, 0xA7C5AC471B478423u64], [0xD3C36113404EA4A8u64, 0xD1B71758E219652Bu64], [0x645A1CAC083126E9u64, 0x83126E978D4FDF3Bu64], [0x3D70A3D70A3D70A3u64, 0xA3D70A3D70A3D70Au64], [0xCCCCCCCCCCCCCCCCu64, 0xCCCCCCCCCCCCCCCCu64], [0x0000000000000000u64, 0x8000000000000000u64], [0x0000000000000000u64, 0xA000000000000000u64], [0x0000000000000000u64, 0xC800000000000000u64], [0x0000000000000000u64, 0xFA00000000000000u64], [0x0000000000000000u64, 0x9C40000000000000u64], [0x0000000000000000u64, 0xC350000000000000u64], [0x0000000000000000u64, 0xF424000000000000u64], [0x0000000000000000u64, 0x9896800000000000u64], [0x0000000000000000u64, 0xBEBC200000000000u64], [0x0000000000000000u64, 0xEE6B280000000000u64], [0x0000000000000000u64, 0x9502F90000000000u64], [0x0000000000000000u64, 0xBA43B74000000000u64], [0x0000000000000000u64, 0xE8D4A51000000000u64], [0x0000000000000000u64, 0x9184E72A00000000u64], [0x0000000000000000u64, 0xB5E620F480000000u64], [0x0000000000000000u64, 0xE35FA931A0000000u64], [0x0000000000000000u64, 0x8E1BC9BF04000000u64], [0x0000000000000000u64, 0xB1A2BC2EC5000000u64], [0x0000000000000000u64, 0xDE0B6B3A76400000u64], [0x0000000000000000u64, 0x8AC7230489E80000u64], [0x0000000000000000u64, 0xAD78EBC5AC620000u64], [0x0000000000000000u64, 0xD8D726B7177A8000u64], [0x0000000000000000u64, 0x878678326EAC9000u64], [0x0000000000000000u64, 0xA968163F0A57B400u64], [0x0000000000000000u64, 0xD3C21BCECCEDA100u64], [0x0000000000000000u64, 0x84595161401484A0u64], [0x0000000000000000u64, 0xA56FA5B99019A5C8u64], [0x0000000000000000u64, 0xCECB8F27F4200F3Au64], [0x4000000000000000u64, 0x813F3978F8940984u64], [0x5000000000000000u64, 0xA18F07D736B90BE5u64], [0xA400000000000000u64, 0xC9F2C9CD04674EDEu64], [0x4D00000000000000u64, 0xFC6F7C4045812296u64], [0xF020000000000000u64, 0x9DC5ADA82B70B59Du64], [0x6C28000000000000u64, 0xC5371912364CE305u64], [0xC732000000000000u64, 0xF684DF56C3E01BC6u64], [0x3C7F400000000000u64, 0x9A130B963A6C115Cu64], [0x4B9F100000000000u64, 0xC097CE7BC90715B3u64], [0x1E86D40000000000u64, 0xF0BDC21ABB48DB20u64], [0x1314448000000000u64, 0x96769950B50D88F4u64], [0x17D955A000000000u64, 0xBC143FA4E250EB31u64], [0x5DCFAB0800000000u64, 0xEB194F8E1AE525FDu64], [0x5AA1CAE500000000u64, 0x92EFD1B8D0CF37BEu64], [0xF14A3D9E40000000u64, 0xB7ABC627050305ADu64], [0x6D9CCD05D0000000u64, 0xE596B7B0C643C719u64], [0xE4820023A2000000u64, 0x8F7E32CE7BEA5C6Fu64], [0xDDA2802C8A800000u64, 0xB35DBF821AE4F38Bu64], [0xD50B2037AD200000u64, 0xE0352F62A19E306Eu64], [0x4526F422CC340000u64, 0x8C213D9DA502DE45u64], [0x9670B12B7F410000u64, 0xAF298D050E4395D6u64], [0x3C0CDD765F114000u64, 0xDAF3F04651D47B4Cu64], [0xA5880A69FB6AC800u64, 0x88D8762BF324CD0Fu64], [0x8EEA0D047A457A00u64, 0xAB0E93B6EFEE0053u64], [0x72A4904598D6D880u64, 0xD5D238A4ABE98068u64], [0x47A6DA2B7F864750u64, 0x85A36366EB71F041u64], [0x999090B65F67D924u64, 0xA70C3C40A64E6C51u64], [0xFFF4B4E3F741CF6Du64, 0xD0CF4B50CFE20765u64], [0xBFF8F10E7A8921A4u64, 0x82818F1281ED449Fu64], [0xAFF72D52192B6A0Du64, 0xA321F2D7226895C7u64], [0x9BF4F8A69F764490u64, 0xCBEA6F8CEB02BB39u64], [0x02F236D04753D5B4u64, 0xFEE50B7025C36A08u64], [0x01D762422C946590u64, 0x9F4F2726179A2245u64], [0x424D3AD2B7B97EF5u64, 0xC722F0EF9D80AAD6u64], [0xD2E0898765A7DEB2u64, 0xF8EBAD2B84E0D58Bu64], [0x63CC55F49F88EB2Fu64, 0x9B934C3B330C8577u64], [0x3CBF6B71C76B25FBu64, 0xC2781F49FFCFA6D5u64], [0x8BEF464E3945EF7Au64, 0xF316271C7FC3908Au64], [0x97758BF0E3CBB5ACu64, 0x97EDD871CFDA3A56u64], [0x3D52EEED1CBEA317u64, 0xBDE94E8E43D0C8ECu64], [0x4CA7AAA863EE4BDDu64, 0xED63A231D4C4FB27u64], [0x8FE8CAA93E74EF6Au64, 0x945E455F24FB1CF8u64], [0xB3E2FD538E122B44u64, 0xB975D6B6EE39E436u64], [0x60DBBCA87196B616u64, 0xE7D34C64A9C85D44u64], [0xBC8955E946FE31CDu64, 0x90E40FBEEA1D3A4Au64], [0x6BABAB6398BDBE41u64, 0xB51D13AEA4A488DDu64], [0xC696963C7EED2DD1u64, 0xE264589A4DCDAB14u64], [0xFC1E1DE5CF543CA2u64, 0x8D7EB76070A08AECu64], [0x3B25A55F43294BCBu64, 0xB0DE65388CC8ADA8u64], [0x49EF0EB713F39EBEu64, 0xDD15FE86AFFAD912u64], [0x6E3569326C784337u64, 0x8A2DBF142DFCC7ABu64], [0x49C2C37F07965404u64, 0xACB92ED9397BF996u64], [0xDC33745EC97BE906u64, 0xD7E77A8F87DAF7FBu64], [0x69A028BB3DED71A3u64, 0x86F0AC99B4E8DAFDu64], [0xC40832EA0D68CE0Cu64, 0xA8ACD7C0222311BCu64], [0xF50A3FA490C30190u64, 0xD2D80DB02AABD62Bu64], [0x792667C6DA79E0FAu64, 0x83C7088E1AAB65DBu64], [0x577001B891185938u64, 0xA4B8CAB1A1563F52u64], [0xED4C0226B55E6F86u64, 0xCDE6FD5E09ABCF26u64], [0x544F8158315B05B4u64, 0x80B05E5AC60B6178u64], [0x696361AE3DB1C721u64, 0xA0DC75F1778E39D6u64], [0x03BC3A19CD1E38E9u64, 0xC913936DD571C84Cu64], [0x04AB48A04065C723u64, 0xFB5878494ACE3A5Fu64], [0x62EB0D64283F9C76u64, 0x9D174B2DCEC0E47Bu64], [0x3BA5D0BD324F8394u64, 0xC45D1DF942711D9Au64], [0xCA8F44EC7EE36479u64, 0xF5746577930D6500u64], [0x7E998B13CF4E1ECBu64, 0x9968BF6ABBE85F20u64], [0x9E3FEDD8C321A67Eu64, 0xBFC2EF456AE276E8u64], [0xC5CFE94EF3EA101Eu64, 0xEFB3AB16C59B14A2u64], [0xBBA1F1D158724A12u64, 0x95D04AEE3B80ECE5u64], [0x2A8A6E45AE8EDC97u64, 0xBB445DA9CA61281Fu64], [0xF52D09D71A3293BDu64, 0xEA1575143CF97226u64], [0x593C2626705F9C56u64, 0x924D692CA61BE758u64], [0x6F8B2FB00C77836Cu64, 0xB6E0C377CFA2E12Eu64], [0x0B6DFB9C0F956447u64, 0xE498F455C38B997Au64], [0x4724BD4189BD5EACu64, 0x8EDF98B59A373FECu64], [0x58EDEC91EC2CB657u64, 0xB2977EE300C50FE7u64], [0x2F2967B66737E3EDu64, 0xDF3D5E9BC0F653E1u64], [0xBD79E0D20082EE74u64, 0x8B865B215899F46Cu64], [0xECD8590680A3AA11u64, 0xAE67F1E9AEC07187u64], [0xE80E6F4820CC9495u64, 0xDA01EE641A708DE9u64], [0x3109058D147FDCDDu64, 0x884134FE908658B2u64], [0xBD4B46F0599FD415u64, 0xAA51823E34A7EEDEu64], [0x6C9E18AC7007C91Au64, 0xD4E5E2CDC1D1EA96u64], [0x03E2CF6BC604DDB0u64, 0x850FADC09923329Eu64], [0x84DB8346B786151Cu64, 0xA6539930BF6BFF45u64], [0xE612641865679A63u64, 0xCFE87F7CEF46FF16u64], [0x4FCB7E8F3F60C07Eu64, 0x81F14FAE158C5F6Eu64], [0xE3BE5E330F38F09Du64, 0xA26DA3999AEF7749u64], [0x5CADF5BFD3072CC5u64, 0xCB090C8001AB551Cu64], [0x73D9732FC7C8F7F6u64, 0xFDCB4FA002162A63u64], [0x2867E7FDDCDD9AFAu64, 0x9E9F11C4014DDA7Eu64], [0xB281E1FD541501B8u64, 0xC646D63501A1511Du64], [0x1F225A7CA91A4226u64, 0xF7D88BC24209A565u64], [0x3375788DE9B06958u64, 0x9AE757596946075Fu64], [0x0052D6B1641C83AEu64, 0xC1A12D2FC3978937u64], [0xC0678C5DBD23A49Au64, 0xF209787BB47D6B84u64], [0xF840B7BA963646E0u64, 0x9745EB4D50CE6332u64], [0xB650E5A93BC3D898u64, 0xBD176620A501FBFFu64], [0xA3E51F138AB4CEBEu64, 0xEC5D3FA8CE427AFFu64], [0xC66F336C36B10137u64, 0x93BA47C980E98CDFu64], [0xB80B0047445D4184u64, 0xB8A8D9BBE123F017u64], [0xA60DC059157491E5u64, 0xE6D3102AD96CEC1Du64], [0x87C89837AD68DB2Fu64, 0x9043EA1AC7E41392u64], [0x29BABE4598C311FBu64, 0xB454E4A179DD1877u64], [0xF4296DD6FEF3D67Au64, 0xE16A1DC9D8545E94u64], [0x1899E4A65F58660Cu64, 0x8CE2529E2734BB1Du64], [0x5EC05DCFF72E7F8Fu64, 0xB01AE745B101E9E4u64], [0x76707543F4FA1F73u64, 0xDC21A1171D42645Du64], [0x6A06494A791C53A8u64, 0x899504AE72497EBAu64], [0x0487DB9D17636892u64, 0xABFA45DA0EDBDE69u64], [0x45A9D2845D3C42B6u64, 0xD6F8D7509292D603u64], [0x0B8A2392BA45A9B2u64, 0x865B86925B9BC5C2u64], [0x8E6CAC7768D7141Eu64, 0xA7F26836F282B732u64], [0x3207D795430CD926u64, 0xD1EF0244AF2364FFu64], [0x7F44E6BD49E807B8u64, 0x8335616AED761F1Fu64], [0x5F16206C9C6209A6u64, 0xA402B9C5A8D3A6E7u64], [0x36DBA887C37A8C0Fu64, 0xCD036837130890A1u64], [0xC2494954DA2C9789u64, 0x802221226BE55A64u64], [0xF2DB9BAA10B7BD6Cu64, 0xA02AA96B06DEB0FDu64], [0x6F92829494E5ACC7u64, 0xC83553C5C8965D3Du64], [0xCB772339BA1F17F9u64, 0xFA42A8B73ABBF48Cu64], [0xFF2A760414536EFBu64, 0x9C69A97284B578D7u64], [0xFEF5138519684ABAu64, 0xC38413CF25E2D70Du64], [0x7EB258665FC25D69u64, 0xF46518C2EF5B8CD1u64], [0xEF2F773FFBD97A61u64, 0x98BF2F79D5993802u64], [0xAAFB550FFACFD8FAu64, 0xBEEEFB584AFF8603u64], [0x95BA2A53F983CF38u64, 0xEEAABA2E5DBF6784u64], [0xDD945A747BF26183u64, 0x952AB45CFA97A0B2u64], [0x94F971119AEEF9E4u64, 0xBA756174393D88DFu64], [0x7A37CD5601AAB85Du64, 0xE912B9D1478CEB17u64], [0xAC62E055C10AB33Au64, 0x91ABB422CCB812EEu64], [0x577B986B314D6009u64, 0xB616A12B7FE617AAu64], [0xED5A7E85FDA0B80Bu64, 0xE39C49765FDF9D94u64], [0x14588F13BE847307u64, 0x8E41ADE9FBEBC27Du64], [0x596EB2D8AE258FC8u64, 0xB1D219647AE6B31Cu64], [0x6FCA5F8ED9AEF3BBu64, 0xDE469FBD99A05FE3u64], [0x25DE7BB9480D5854u64, 0x8AEC23D680043BEEu64], [0xAF561AA79A10AE6Au64, 0xADA72CCC20054AE9u64], [0x1B2BA1518094DA04u64, 0xD910F7FF28069DA4u64], [0x90FB44D2F05D0842u64, 0x87AA9AFF79042286u64], [0x353A1607AC744A53u64, 0xA99541BF57452B28u64], [0x42889B8997915CE8u64, 0xD3FA922F2D1675F2u64], [0x69956135FEBADA11u64, 0x847C9B5D7C2E09B7u64], [0x43FAB9837E699095u64, 0xA59BC234DB398C25u64], [0x94F967E45E03F4BBu64, 0xCF02B2C21207EF2Eu64], [0x1D1BE0EEBAC278F5u64, 0x8161AFB94B44F57Du64], [0x6462D92A69731732u64, 0xA1BA1BA79E1632DCu64], [0x7D7B8F7503CFDCFEu64, 0xCA28A291859BBF93u64], [0x5CDA735244C3D43Eu64, 0xFCB2CB35E702AF78u64], [0x3A0888136AFA64A7u64, 0x9DEFBF01B061ADABu64], [0x088AAA1845B8FDD0u64, 0xC56BAEC21C7A1916u64], [0x8AAD549E57273D45u64, 0xF6C69A72A3989F5Bu64], [0x36AC54E2F678864Bu64, 0x9A3C2087A63F6399u64], [0x84576A1BB416A7DDu64, 0xC0CB28A98FCF3C7Fu64], [0x656D44A2A11C51D5u64, 0xF0FDF2D3F3C30B9Fu64], [0x9F644AE5A4B1B325u64, 0x969EB7C47859E743u64], [0x873D5D9F0DDE1FEEu64, 0xBC4665B596706114u64], [0xA90CB506D155A7EAu64, 0xEB57FF22FC0C7959u64], [0x09A7F12442D588F2u64, 0x9316FF75DD87CBD8u64], [0x0C11ED6D538AEB2Fu64, 0xB7DCBF5354E9BECEu64], [0x8F1668C8A86DA5FAu64, 0xE5D3EF282A242E81u64], [0xF96E017D694487BCu64, 0x8FA475791A569D10u64], [0x37C981DCC395A9ACu64, 0xB38D92D760EC4455u64], [0x85BBE253F47B1417u64, 0xE070F78D3927556Au64], [0x93956D7478CCEC8Eu64, 0x8C469AB843B89562u64], [0x387AC8D1970027B2u64, 0xAF58416654A6BABBu64], [0x06997B05FCC0319Eu64, 0xDB2E51BFE9D0696Au64], [0x441FECE3BDF81F03u64, 0x88FCF317F22241E2u64], [0xD527E81CAD7626C3u64, 0xAB3C2FDDEEAAD25Au64], [0x8A71E223D8D3B074u64, 0xD60B3BD56A5586F1u64], [0xF6872D5667844E49u64, 0x85C7056562757456u64], [0xB428F8AC016561DBu64, 0xA738C6BEBB12D16Cu64], [0xE13336D701BEBA52u64, 0xD106F86E69D785C7u64], [0xECC0024661173473u64, 0x82A45B450226B39Cu64], [0x27F002D7F95D0190u64, 0xA34D721642B06084u64], [0x31EC038DF7B441F4u64, 0xCC20CE9BD35C78A5u64], [0x7E67047175A15271u64, 0xFF290242C83396CEu64], [0x0F0062C6E984D386u64, 0x9F79A169BD203E41u64], [0x52C07B78A3E60868u64, 0xC75809C42C684DD1u64], [0xA7709A56CCDF8A82u64, 0xF92E0C3537826145u64], [0x88A66076400BB691u64, 0x9BBCC7A142B17CCBu64], [0x6ACFF893D00EA435u64, 0xC2ABF989935DDBFEu64], [0x0583F6B8C4124D43u64, 0xF356F7EBF83552FEu64], [0xC3727A337A8B704Au64, 0x98165AF37B2153DEu64], [0x744F18C0592E4C5Cu64, 0xBE1BF1B059E9A8D6u64], [0x1162DEF06F79DF73u64, 0xEDA2EE1C7064130Cu64], [0x8ADDCB5645AC2BA8u64, 0x9485D4D1C63E8BE7u64], [0x6D953E2BD7173692u64, 0xB9A74A0637CE2EE1u64], [0xC8FA8DB6CCDD0437u64, 0xE8111C87C5C1BA99u64], [0x1D9C9892400A22A2u64, 0x910AB1D4DB9914A0u64], [0x2503BEB6D00CAB4Bu64, 0xB54D5E4A127F59C8u64], [0x2E44AE64840FD61Du64, 0xE2A0B5DC971F303Au64], [0x5CEAECFED289E5D2u64, 0x8DA471A9DE737E24u64], [0x7425A83E872C5F47u64, 0xB10D8E1456105DADu64], [0xD12F124E28F77719u64, 0xDD50F1996B947518u64], [0x82BD6B70D99AAA6Fu64, 0x8A5296FFE33CC92Fu64], [0x636CC64D1001550Bu64, 0xACE73CBFDC0BFB7Bu64], [0x3C47F7E05401AA4Eu64, 0xD8210BEFD30EFA5Au64], [0x65ACFAEC34810A71u64, 0x8714A775E3E95C78u64], [0x7F1839A741A14D0Du64, 0xA8D9D1535CE3B396u64], [0x1EDE48111209A050u64, 0xD31045A8341CA07Cu64], [0x934AED0AAB460432u64, 0x83EA2B892091E44Du64], [0xF81DA84D5617853Fu64, 0xA4E4B66B68B65D60u64], [0x36251260AB9D668Eu64, 0xCE1DE40642E3F4B9u64], [0xC1D72B7C6B426019u64, 0x80D2AE83E9CE78F3u64], [0xB24CF65B8612F81Fu64, 0xA1075A24E4421730u64], [0xDEE033F26797B627u64, 0xC94930AE1D529CFCu64], [0x169840EF017DA3B1u64, 0xFB9B7CD9A4A7443Cu64], [0x8E1F289560EE864Eu64, 0x9D412E0806E88AA5u64], [0xF1A6F2BAB92A27E2u64, 0xC491798A08A2AD4Eu64], [0xAE10AF696774B1DBu64, 0xF5B5D7EC8ACB58A2u64], [0xACCA6DA1E0A8EF29u64, 0x9991A6F3D6BF1765u64], [0x17FD090A58D32AF3u64, 0xBFF610B0CC6EDD3Fu64], [0xDDFC4B4CEF07F5B0u64, 0xEFF394DCFF8A948Eu64], [0x4ABDAF101564F98Eu64, 0x95F83D0A1FB69CD9u64], [0x9D6D1AD41ABE37F1u64, 0xBB764C4CA7A4440Fu64], [0x84C86189216DC5EDu64, 0xEA53DF5FD18D5513u64], [0x32FD3CF5B4E49BB4u64, 0x92746B9BE2F8552Cu64], [0x3FBC8C33221DC2A1u64, 0xB7118682DBB66A77u64], [0x0FABAF3FEAA5334Au64, 0xE4D5E82392A40515u64], [0x29CB4D87F2A7400Eu64, 0x8F05B1163BA6832Du64], [0x743E20E9EF511012u64, 0xB2C71D5BCA9023F8u64], [0x914DA9246B255416u64, 0xDF78E4B2BD342CF6u64], [0x1AD089B6C2F7548Eu64, 0x8BAB8EEFB6409C1Au64], [0xA184AC2473B529B1u64, 0xAE9672ABA3D0C320u64], [0xC9E5D72D90A2741Eu64, 0xDA3C0F568CC4F3E8u64], [0x7E2FA67C7A658892u64, 0x8865899617FB1871u64], [0xDDBB901B98FEEAB7u64, 0xAA7EEBFB9DF9DE8Du64], [0x552A74227F3EA565u64, 0xD51EA6FA85785631u64], [0xD53A88958F87275Fu64, 0x8533285C936B35DEu64], [0x8A892ABAF368F137u64, 0xA67FF273B8460356u64], [0x2D2B7569B0432D85u64, 0xD01FEF10A657842Cu64], [0x9C3B29620E29FC73u64, 0x8213F56A67F6B29Bu64], [0x8349F3BA91B47B8Fu64, 0xA298F2C501F45F42u64], [0x241C70A936219A73u64, 0xCB3F2F7642717713u64], [0xED238CD383AA0110u64, 0xFE0EFB53D30DD4D7u64], [0xF4363804324A40AAu64, 0x9EC95D1463E8A506u64], [0xB143C6053EDCD0D5u64, 0xC67BB4597CE2CE48u64], [0xDD94B7868E94050Au64, 0xF81AA16FDC1B81DAu64], [0xCA7CF2B4191C8326u64, 0x9B10A4E5E9913128u64], [0xFD1C2F611F63A3F0u64, 0xC1D4CE1F63F57D72u64], [0xBC633B39673C8CECu64, 0xF24A01A73CF2DCCFu64], [0xD5BE0503E085D813u64, 0x976E41088617CA01u64], [0x4B2D8644D8A74E18u64, 0xBD49D14AA79DBC82u64], [0xDDF8E7D60ED1219Eu64, 0xEC9C459D51852BA2u64], [0xCABB90E5C942B503u64, 0x93E1AB8252F33B45u64], [0x3D6A751F3B936243u64, 0xB8DA1662E7B00A17u64], [0x0CC512670A783AD4u64, 0xE7109BFBA19C0C9Du64], [0x27FB2B80668B24C5u64, 0x906A617D450187E2u64], [0xB1F9F660802DEDF6u64, 0xB484F9DC9641E9DAu64], [0x5E7873F8A0396973u64, 0xE1A63853BBD26451u64], [0xDB0B487B6423E1E8u64, 0x8D07E33455637EB2u64], [0x91CE1A9A3D2CDA62u64, 0xB049DC016ABC5E5Fu64], [0x7641A140CC7810FBu64, 0xDC5C5301C56B75F7u64], [0xA9E904C87FCB0A9Du64, 0x89B9B3E11B6329BAu64], [0x546345FA9FBDCD44u64, 0xAC2820D9623BF429u64], [0xA97C177947AD4095u64, 0xD732290FBACAF133u64], [0x49ED8EABCCCC485Du64, 0x867F59A9D4BED6C0u64], [0x5C68F256BFFF5A74u64, 0xA81F301449EE8C70u64], [0x73832EEC6FFF3111u64, 0xD226FC195C6A2F8Cu64], ]; // 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 — graduated to the Ryū shortest-round-trippable implementation // in ftos.ww (strconv #106 fold-5). The old lossy fixed-point version // (6 fractional digits, "huge" fallback ≥9e18, no NaN/Inf) was deleted // here per the lib-note graduation rule ("replace in one go, don't keep // both"); ftos.ww's f64tos is the live one. f32tos follows in fold-5b // (task #67, gated on the #143 f32-arg-push cgen fix). // 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 ""; }; 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 ""; }; return ""; }; // ---- writer for tokprint ---------------------------------------------- // // fputq mirrors cmd/wcc/tok.c:fputq — quote the string with C-style // escapes for \, ", \n, \t, \r and \xNN for other non-printables. fn fputcbyte(fd: i32, b: u8) void = { let buf: [1]u8; buf[0] = b; os.write(fd, buf.ptr, 1u64); }; fn fputsstr(fd: i32, s: str) void = { os.write(fd, s.ptr, s.len: u64); }; fn hexchar(n: u8) u8 = { if (n < 10u8) { return n + 48u8; }; // '0'..'9' return (n - 10u8) + 97u8; // 'a'..'f' }; fn fputhex2(fd: i32, b: u8) void = { let out: [4]u8; out[0] = 92u8; // '\\' out[1] = 120u8; // 'x' out[2] = hexchar(b >> 4u8); out[3] = hexchar(b & 15u8); os.write(fd, out.ptr, 4u64); }; fn fputq(fd: i32, p: *u8, n: i32) void = { fputcbyte(fd, 34u8); // '"' let i: i32 = 0; for (i < n) { let c: u8 = p[i]; if (c == 92u8) { // '\\' fputsstr(fd, "\\\\"); } else { if (c == 34u8) { // '"' fputsstr(fd, "\\\""); } else { if (c == 10u8) { // '\n' fputsstr(fd, "\\n"); } else { if (c == 9u8) { // '\t' fputsstr(fd, "\\t"); } else { if (c == 13u8) { // '\r' fputsstr(fd, "\\r"); } else { if (c < 32u8) { fputhex2(fd, c); } else { if (c == 127u8) { fputhex2(fd, c); } else { fputcbyte(fd, c); }; }; }; }; }; }; }; i += 1; }; fputcbyte(fd, 34u8); }; // tokprint — write one token line to fd. Format must match // cmd/wcc/tok.c:tokprint() byte-for-byte: that's the diff anchor. // ":: [ ]\n" // // Takes `t` by pointer because w6c can't yet pass a >16-byte struct // by value; the C version takes Tok by value. export fn tokprint(fd: i32, t: *tok) void = { // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet // reduced by w6c — `t.x.y` returns the whole str. Lift the str // fields into locals so we can use the str pseudo-field path. let tfile: str = t.file; let ttext: str = t.text; if (tfile.len > 0) { fputsstr(fd, tfile); } else { fputsstr(fd, ""); }; fputcbyte(fd, 58u8); // ':' let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); os.write(fd, ls.ptr, ls.len: u64); fputcbyte(fd, 58u8); let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); os.write(fd, cs.ptr, cs.len: u64); fputcbyte(fd, 32u8); // ' ' fputsstr(fd, tokname(t.kind)); if (t.kind == tkind.TK_IDENT) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_STR) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_ERR) { fputcbyte(fd, 32u8); fputq(fd, ttext.ptr, ttext.len); } else { if (t.kind == tkind.TK_INT) { fputcbyte(fd, 32u8); let us: str = strconv.u64tos(t.uval, strconv.base.DEC); os.write(fd, us.ptr, us.len: u64); } else { if (t.kind == tkind.TK_RUNE) { fputcbyte(fd, 32u8); let us: str = strconv.u64tos(t.uval, strconv.base.DEC); os.write(fd, us.ptr, us.len: u64); };};};};}; // tkind.TK_FLOAT is intentionally not handled here — %g formatting // won't byte-match across implementations. Diff fixtures must // be float-free until we implement a stable float formatter. fputcbyte(fd, 10u8); // '\n' }; // 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 strings; // 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, errs: i32, }; export fn lexinit(l: *lex, 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.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; let view: str; view.ptr = l.src + begin; view.len = n: i32; out.text = strings.dup(view); 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 = alloc([], n + 1u64)!; 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.ptr, 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) { let view: str; view.ptr = p; view.len = sl: i32; out.tsuffix = strings.dup(view); } 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; let view: str; view.ptr = p; view.len = n: i32; out.text = strings.dup(view); return; }; }; let k: tkind = kwlookup(p, n: i32); if (k != tkind.TK_NONE) { out.kind = k; } else { out.kind = tkind.TK_IDENT; }; let view: str; view.ptr = p; view.len = n: i32; out.text = strings.dup(view); }; fn lexstr(l: *lex, start: *pos, out: *tok) void = { let cap: u64 = 32u64; let nb: u64 = 0u64; let buf: []u8 = alloc([], cap)!; 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; let view: str; view.ptr = "".ptr; view.len = 0; out.text = strings.dup(view); 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 = alloc([], ncap)!; 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.ptr; 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; let view: str; view.ptr = "".ptr; view.len = 0; out.text = strings.dup(view); 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; let view: str; view.ptr = "".ptr; view.len = 0; out.text = strings.dup(view); 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; let view: str; view.ptr = one.ptr; view.len = 1; out.text = strings.dup(view); }; // 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 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_TPARAM — chain wrapper for N_TTUPLE.list elements. Mirror of // cstage's Tparam (cmd/wcc/check.c:1437-1451) lifted to the AST so // `exprtype` can return shared element-type nodes (sym.decl.lhs, // struct field's .lhs, another N_TTUPLE's .list element) without // corrupting source ASTs by reusing their .next. .lhs holds the // element type AST (possibly shared); .next chains within the // parent N_TTUPLE.list. Cstage keeps Tparam at the Type-layer; ww // has no separate type layer for tuple chains, so the wrapper sits // at the AST layer. Other node fields are unused. Never appears // outside an N_TTUPLE.list; astprint unwraps transparently to keep // the 990 -a byte-diff against cstage. N_TPARAM = 67, N_LAST = 68, }; // ---- 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(k: nkind, file: str, line: i32, col: i32) *node = { // fval cast-init: 990's wwdump TK_FLOAT diff requires this file // to tokenise identically through C and ww (lex.ww:382 has the // same workaround for the cstage %g-formats vs ww-skips divergence). let n: *node = alloc(node{kind=k, file=file, line=line, col=col, op=tkind.TK_NONE, str="", uval=0u64, fval=0: f64, lhs=nil, rhs=nil, cond=nil, body=nil, els=nil, list=nil, next=nil, attr=nil, exported=0, type_=nil, tsuffix="", nmod=""})!; return n; }; // ---- printer ---------------------------------------------------------- export 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_TPARAM) { return "tparam"; }; 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; }; // N_TPARAM wraps an N_TTUPLE.list element so exprtype can return // shared element-type nodes without corrupting their .next chain. // Cstage has no AST-level wrapper, so unwrap here to keep the 990 // -a byte-diff with cstage's astprint. if (n.kind == nkind.N_TPARAM) { pr(fd, n.lhs, d); 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 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(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(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(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(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(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(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(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 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(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(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(nkind.N_STRLIT, pf, pl, pc); n.str = p.curtext; advance(p); return n; }; if (p.curkind == tkind.TK_RUNE) { let n: *node = newnode(nkind.N_RUNELIT, pf, pl, pc); n.uval = p.curuval; advance(p); return n; }; if (p.curkind == tkind.TK_TRUE) { advance(p); return newnode(nkind.N_TRUE, pf, pl, pc); }; if (p.curkind == tkind.TK_FALSE) { advance(p); return newnode(nkind.N_FALSE, pf, pl, pc); }; if (p.curkind == tkind.TK_NIL) { advance(p); return newnode(nkind.N_NIL, pf, pl, pc); }; if (p.curkind == tkind.TK_VOID) { advance(p); return newnode(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(nkind.N_TRYPROP, pf, pl, pc); n.lhs = cur; cur = n; continue; }; if (p.curkind == tkind.TK_NOT) { advance(p); let n: *node = newnode(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(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(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(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(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(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(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(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(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 tok; type parser = struct { l: *lex, 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, l: *lex) void = { p.l = l; 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 — build "head.tail" for dotted type-name path // collapse. Mirrors aprintf in C parser; pulled local to avoid a // cross-module dependency. fn joindotted(head: str, tail: str) str = { let n: u64 = head.len: u64 + 1u64 + tail.len: u64; let buf: []u8 = alloc([], n + 1u64)!; let i: u64 = 0u64; let j: i32 = 0; for (j < head.len) { buf[i] = head[j]; i += 1u64; j += 1; }; buf[i] = 46u8; // '.' i += 1u64; j = 0; for (j < tail.len) { buf[i] = tail[j]; i += 1u64; j += 1; }; buf[i] = 0u8; let r: str; r.ptr = buf.ptr; 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(nkind.N_TBANG, pf, pl, pc); n.lhs = parsetype(p); return n; }; if (p.curkind == tkind.TK_STAR) { advance(p); let n: *node = newnode(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(nkind.N_TSLICE, pf, pl, pc); n.lhs = parsetype(p); return n; }; let n: *node = newnode(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(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(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(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(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(nkind.N_TNAME, pf, pl, pc); n.str = "void"; advance(p); return n; }; if (p.curkind == tkind.TK_IDENT) { let n: *node = newnode(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(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(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; }; // Wrap each element in N_TPARAM so the chain owns its .next. // Mirrors cstage's Tparam (cmd/wcc/check.c:1437-1451); lifted // to the AST layer here because wwstage has no separate type // layer, and exprtype must return shared element-type nodes // (sym.decl.lhs, struct field's .lhs, another N_TTUPLE element) // without corrupting source ASTs. let n: *node = newnode(nkind.N_TTUPLE, pf, pl, pc); let firstwrap: *node = newnode(nkind.N_TPARAM, pf, pl, pc); firstwrap.lhs = first; let head: *node = firstwrap; let tail: *node = firstwrap; for (true) { let e: *node = parsetype(p); let w: *node = newnode(nkind.N_TPARAM, e.file, e.line, e.col); w.lhs = e; tail.next = w; tail = w; 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(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(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(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 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(nkind.N_BREAK, pf, pl, pc); }; if (p.curkind == tkind.TK_CONTINUE) { advance(p); expecttok(p, tkind.TK_SEMI, "expected ';' after continue"); return newnode(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(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(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 program. package ww; import os; // ---- 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, TY_SIZE = 36, // #85 fold-1; mirrors TY_UINTPTR (8/8 amd64) TY_OPAQUE = 37, // #108(a); abstract + unsized, behind indirection only }; // #108(a): unsized sentinel for abstract types (tinfo.size / .align). // Mirrors harec SIZE_UNDEFINED = (size_t)-1 (ref/harec/include/types.h // :58) and cstage cmd/wcc/ww.h; not 0, so a bare opaque local can't // fabricate a 0-byte slot. Value == U64_MAX. def SIZE_UNDEFINED: u64 = 18446744073709551615; // ---- tinfo / tfield / tparam ----------------------------------------- type tfield = struct { name: str, type_: *tinfo, offset: u64, tnext: *tfield, }; type tparam = struct { name: str, type_: *tinfo, // #61a: per-variant `!T` error mark for TY_TAGGED variants. // cstage-MIRROR divergence: harec carries no per-variant flag — // it models `!T` as a distinct STORAGE_ERROR type node // (ref/harec/include/types.h:144, src/types.c:151-159 // type_is_error). wwstage tinfo has no iserror field // (check.ww TTAGGED arm), so the bit rides the shared param // struct instead, matching cstage Type.iserror semantics. // Faithful STORAGE_ERROR-node port filed as #62. iserror: bool, tnext: *tparam, }; // #57 A.6.3i-phase-1: tuple positional element. Distinct from tfield // (named, struct member) per harec's split at ref/harec/include/types.h // :109-115 (struct_field) vs :122-126 (type_tuple) — tuple positionals // carry no name (positional only) and a separate next-link. Rule-12 // sea-of-stars mirrors Hare's structural choice; the empty-name idiom // from #50's TTAGGED-on-tparam would conflate two semantic axes // (variants can be named; positionals never can). type ttupleelem = struct { type_: *tinfo, offset: u64, tnext: *ttupleelem, }; type tinfo = struct { kind: tykind, size: u64, align: u64, sub: *tinfo, // ptr/slice/array/chan element alen: u64, fields: *tfield, params: *tparam, tupleelems: *ttupleelem, // #57 A.6.3i-phase-1: TY_TUPLE // positional chain (harec types.h:122-126 // `struct type_tuple`). Distinct slot from // .fields so struct-member vs tuple- // positional stay axis-separated. ret: *tinfo, variadic: i32, nullable: i32, // #61 A.3: TY_TAGGED `(*T | void)` fold collapses to // 8B ptr slot (null is the void variant). Mirrors // cstage Type.nullable (cmd/wcc/ww.h:430-433). name: str, under: *tinfo, slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`. // `size` stays natural (Hare-faithful); // `slotsize` carries the slot-padded width // cgen's let/struct-field layout demands. // For primitives/ptr/slice/chan/fn/str/tagged // `slotsize == size`; struct + tuple + array // of struct diverge — see check.ww tinfo- // fornode + cgenutil.ww registerstruct. // Pad-to-8 of narrow primitives in let slots // still lives at slotsize()'s read site; // graduating it here would break `[N]i32` // stride (4*N stays natural). }; // #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes // tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors // other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars // over hash-table cleverness, and Sym/Scope already pay the FNV cost // for the resolver pass. type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, }; // ---- tctx — the box of primitive types ------------------------------- type tctx = struct { 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, tysize: *tinfo, tyopaque: *tinfo, tyf32: *tinfo, tyf64: *tinfo, tystr: *tinfo, tyerr: *tinfo, tynever: *tinfo, tyuntypedint: *tinfo, tyuntypedfloat: *tinfo, tyuntypedstr: *tinfo, tyuntypedrune: *tinfo, tyuntypedbool: *tinfo, tyuntypednil: *tinfo, tinfocache: *tinfocacheent, }; // ---- constructors ----------------------------------------------------- export fn newtype(k: tykind) *tinfo = { let t: *tinfo = alloc(tinfo{kind=k, size=0u64, align=0u64, sub=nil, alen=0u64, fields=nil, params=nil, tupleelems=nil, ret=nil, variadic=0, nullable=0, name="", under=nil, slotsize=0u64})!; return t; }; fn prim(k: tykind, nm: str, sz: u64, al: u64) *tinfo = { let t: *tinfo = newtype(k); t.name = nm; t.size = sz; if (al > 0u64) { t.align = al; } else { t.align = sz; }; t.slotsize = sz; return t; }; export fn typesinit(c: *tctx) void = { c.tyvoid = prim(tykind.TY_VOID, "void", 0u64, 1u64); c.tybool = prim(tykind.TY_BOOL, "bool", 1u64, 1u64); c.tyrune = prim(tykind.TY_RUNE, "rune", 4u64, 4u64); c.tyi8 = prim(tykind.TY_I8, "i8", 1u64, 1u64); c.tyi16 = prim(tykind.TY_I16, "i16", 2u64, 2u64); c.tyi32 = prim(tykind.TY_I32, "i32", 4u64, 4u64); c.tyi64 = prim(tykind.TY_I64, "i64", 8u64, 8u64); c.tyu8 = prim(tykind.TY_U8, "u8", 1u64, 1u64); c.tyu16 = prim(tykind.TY_U16, "u16", 2u64, 2u64); c.tyu32 = prim(tykind.TY_U32, "u32", 4u64, 4u64); c.tyu64 = prim(tykind.TY_U64, "u64", 8u64, 8u64); c.tyint = prim(tykind.TY_INT, "int", 8u64, 8u64); c.tyuint = prim(tykind.TY_UINT, "uint", 8u64, 8u64); c.tyuintptr= prim(tykind.TY_UINTPTR, "uintptr", 8u64, 8u64); c.tysize = prim(tykind.TY_SIZE, "size", 8u64, 8u64); // #85 c.tyf32 = prim(tykind.TY_F32, "f32", 4u64, 4u64); c.tyf64 = prim(tykind.TY_F64, "f64", 8u64, 8u64); // str IS []u8: { *u8, len, cap } — 24B, 3-reg ABI (#1/Phase 3). // Size sourced from a u8-slice's size (typeslice SSoT) so str and // []u8 can never drift; no second hardcoded 24. Mirrors cstage // type.c `type_slice(a, ty_u8)->size`. // // The slice tinfo MUST land in a local first: the inline form // `typeslice(c.tyu8).size` triggers a cgen bug — `call().field` // where the call returns a *pointer* emits no deref (it uses the // returned pointer AS the field value), so tystr.size would become // a heap address → runaway slot-size loops. Filed as task #6 // (cstage cgen.c N_DOT base=N_CALL-returning-pointer + wwstage // cgdot mirror); retained here as a local until that lands. let u8slice: *tinfo = typeslice(c.tyu8); c.tystr = prim(tykind.TY_STR, "str", u8slice.size, 8u64); c.tystr.sub = c.tyu8; // str IS []u8: element is u8 (Phase 2 F1) c.tyerr = prim(tykind.TY_ERR, "", 0u64, 1u64); c.tynever = prim(tykind.TY_NEVER, "never", 0u64, 1u64); // #108(a): abstract + unsized. SIZE_UNDEFINED (not 0) blocks a bare // `let x: opaque` 0-byte slot; legal only behind indirection. // Mirrors cstage type.c ty_opaque (harec types.c:1446). c.tyopaque = prim(tykind.TY_OPAQUE, "opaque", SIZE_UNDEFINED, SIZE_UNDEFINED); c.tyuntypedint = prim(tykind.TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); c.tyuntypedfloat = prim(tykind.TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); c.tyuntypedstr = prim(tykind.TY_UNTYPED_STR, "untyped_str", 0u64, 1u64); c.tyuntypedrune = prim(tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); c.tyuntypedbool = prim(tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); c.tyuntypednil = prim(tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); }; export fn typeptr(sub: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_PTR); t.sub = sub; t.size = 8u64; t.align = 8u64; t.slotsize = 8u64; return t; }; export fn typeslice(sub: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_SLICE); t.sub = sub; t.size = 24u64; // sizelint-ok: SSoT for slice header (#64) t.align = 8u64; t.slotsize = 24u64; // sizelint-ok: SSoT for slice slotsize (#64) return t; }; export fn typearray(sub: *tinfo, n: u64) *tinfo = { let t: *tinfo = newtype(tykind.TY_ARRAY); t.sub = sub; t.alen = n; if (sub != nil) { t.size = sub.size * n; t.align = sub.align; // #61 A.5: ti.slotsize = stride * elen using the element's // slot-padded width. Primitives have slotsize == size so // `[N]i32` stride stays 4 (natural); structs have padded // slotsize so `[N]Triplet` stride lifts to 16. t.slotsize = sub.slotsize * n; } else { t.align = 1u64; }; return t; }; export fn typechan(sub: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_CHAN); t.sub = sub; t.size = 8u64; t.align = 8u64; t.slotsize = 8u64; return t; }; export fn typenamed(name: str, under: *tinfo) *tinfo = { let t: *tinfo = newtype(tykind.TY_NAMED); t.name = name; t.under = under; if (under != nil) { t.size = under.size; t.align = under.align; t.slotsize = under.slotsize; }; return t; }; // #61 audit §1.8 — A.1 infrastructure: tinfocache lookup/bind. Keyed // by AST node-pointer so two different N_TNAME("i32") nodes get // independent entries that both resolve to c.tyi32. Used by // tinfofornode in check.ww; cgen still reads sizes via primtypesize // until A.2+ graduates each walker family. export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { let e: *tinfocacheent = c.tinfocache; for (e != nil) { if (e.key == key) { return e.val; }; e = e.cnext; }; return nil; }; export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfocache})!; c.tinfocache = e; }; // ---- 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_SIZE) { 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); }; // TY_RUNE is unsigned: Unicode codepoint (0..0x10FFFF) zero-extends on // sub-word load (MOVL, not MOVSXD). TY_ENUM recurses on .sub so a // `type k = enum u32 {…}` reads as unsigned. Cite cstage type.c:178 // `type_isunsigned`; rule 10 keeps wwstage aligned down to cstage. 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_SIZE) { return true; }; if (k == tykind.TY_RUNE){ return true; }; if (k == tykind.TY_NAMED) { return typeisunsigned(t.under); }; if (k == tykind.TY_ENUM) { return typeisunsigned(t.sub); }; return false; }; // typeissigned — does this type need sign-extension on a sub-word // (1/2/4B) load? Mirrors cstage cgen.c:240 `fld_issigned`. Cgen-facing // predicate (TY_BOOL is unsigned for storage purposes — 0/1 → MOVZBQ), // so it doesn't simply mirror `!typeisunsigned`. Pair-of-`is*` // convention follows ref/hare/types/ helpers. export fn typeissigned(t: *tinfo) bool = { if (t == nil) { return false; }; if (t.kind == tykind.TY_BOOL) { return false; }; if (typeisunsigned(t)) { return false; }; return typeisint(t); }; // typeisstr — TY_STR (and TY_UNTYPED_STR for literals pre-default). // Cite cstage cgen.c:159 `type_isstr` — single TY_NAMED peel, accepts // the same untyped form. ww walks the .under chain so alias-of-alias // (`type s2 = s1; type s1 = str;`) lands the same way. export fn typeisstr(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_STR) { return true; }; if (k == tykind.TY_UNTYPED_STR) { return true; }; if (k == tykind.TY_NAMED) { return typeisstr(t.under); }; return false; }; // typeisslice — TY_SLICE. Cite cstage cgen.c:174 `type_isslice`. export fn typeisslice(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_SLICE) { return true; }; if (k == tykind.TY_NAMED) { return typeisslice(t.under); }; return false; }; // typeistagged — TY_TAGGED (alias-aware). Cite cstage cgen.c:516 // `type_istagged` — same single-peel shape. The node-keyed wwstage // helper this replaces also unwrapped a leading N_TBANG so // `type error = !(invalid | overflow);` registered as tagged. Post- // A.6.2 the TBANG unwrap is handled by tinfofornode (check.ww:1145- // 1152 returns the inner tinfo unchanged) so we recover the cstage // semantics with the bare kind check + NAMED chase. export fn typeistagged(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return true; }; if (k == tykind.TY_NAMED) { return typeistagged(t.under); }; return false; }; // typeisf32 — narrower-than-typeisfloat: only TY_F32 (after alias // chase). Cite cstage cgen.c:188 `type_isf32`. Used to pick MOVSS vs // MOVSD and the SS-variant arithmetic / cast opcodes. export fn typeisf32(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_F32) { return true; }; if (k == tykind.TY_NAMED) { return typeisf32(t.under); }; return false; }; // typeisnullable — TY_TAGGED with the `(*T | void)` one-word fold. // Cite cstage cgen.c:396 `type_isnullable`. The .nullable flag is // stamped by tinfofornode (check.ww:1309-1318) when the two-variant // shape matches. export fn typeisnullable(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_TAGGED) { return t.nullable != 0; }; if (k == tykind.TY_NAMED) { return typeisnullable(t.under); }; return false; }; // typeis8byteprim — does this type take exactly one 8-byte stack // slot (ptr / fn / chan / 64-bit int / scalar primitive padded up to // 8 / `[N]T` whose natural width is 8) rather than a wider aggregate? // Mirrors the ladder cstage's cgen.c N_LET zero-init takes on `sz==8` // (cmd/wcc/check.c sizing + cgen.c N_LET). The node-keyed wwstage // helper this replaces predates tinfo and AST-walked TBANG / TNAME // alias chains; tinfofornode now collapses TBANG (check.ww:1145) and // TY_NAMED.under carries the chain, so the tinfo walk handles every // shape the AST walker did. export fn typeis8byteprim(t: *tinfo) bool = { if (t == nil) { return false; }; let k: tykind = t.kind; if (k == tykind.TY_PTR) { return true; }; if (k == tykind.TY_FN) { return true; }; if (k == tykind.TY_CHAN) { return true; }; if (k == tykind.TY_SLICE) { return false; }; if (k == tykind.TY_TUPLE) { return false; }; if (k == tykind.TY_TAGGED) { return false; }; if (k == tykind.TY_STR) { return false; }; if (k == tykind.TY_STRUCT) { return false; }; if (k == tykind.TY_ARRAY) { return t.size == 8u64; }; if (k == tykind.TY_NAMED) { return typeis8byteprim(t.under); }; // Remaining: primitives (i8/u8/.../i64/u64/bool/rune/f32/f64/ // int/uint/uintptr) and TY_VOID. All slot-pad to 8 and zero-init // in cstage's `sz==8` branch. return true; }; 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; // 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, }; // 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(parent: *scope) *scope = { let buckets_sl: []*sym = alloc([], NBUCKETS: u64)!; let s: *scope = alloc(scope{parent=parent, first=nil, last=nil, buckets=buckets_sl.ptr, nbuckets=NBUCKETS})!; 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; }; // scopelookuptype — find an SK_TYPE entry by name regardless of mod. // // Same FNV bucket + hashnext chain + parent walk as scopelookup, with // an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME // vs imported-module-bareword collision: when scopelookup returns the // SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct // declared in lib/ww/lex/tok.ww with `package lex;` while // `import tok;` registers a same-name SK_USE), the resolver needs // the type entry regardless of its declared package — the struct's // mod may differ from the leaf (lex/tok pair) so // scopelookupinmodule(c, leaf, leaf) won't find it. // // Mirrors the bare-vs-qualified disambiguation pattern from task #57. export fn scopelookuptype(s: *scope, name: str) *sym = { 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.skind == skind.SK_TYPE) { return b; }; }; b = b.hashnext; }; s = s.parent; }; return nil; }; // scopelookupuselocal — find a same-leaf SK_USE entry within ONE scope. // // Same FNV bucket + hashnext chain as scopelookuplocal, with a // `skind == SK_USE` filter and NO parent walk. The dot-lhs twin of // scopelookuptype: when a `use mod;` and a colliding top-level // `fn mod` / `type mod` of the same leaf coexist (random.random, // fnmatch.fnmatch), the mod-preferring scopelookupprefer returns the // SK_FN/SK_TYPE whose mod matches the importing unit's package, masking // the SK_USE. A dot-lhs `mod.x` must resolve `mod` to the SK_USE for the // module-qualified arm to fire, so the resolver re-resolves through this // filter — keyed on the scope where scopelookupprefer LANDED — when it // lands on a non-USE same-leaf entry. // // Single-scope (not a parent walk) so a local binding that shares a leaf // with a top-level `use` keeps value semantics: scopelookupprefer // resolves the local in its inner scope, whose bucket holds no SK_USE, // so this returns nil and the dot stays field access. Only a genuine // same-scope coexistence (top-level use + top-level type/fn) re-resolves. // // This is the coexistence-equivalent of cstage's Sym.use_alias bit // (cmd/wcc/check.c: set at the SK_USE→SK_X promotion sites, consulted by // the `kind == SK_USE || use_alias` dot guards in resolve_typename and // cexpr's N_DOT arm). Wwstage installs the SK_USE and the same-leaf // type/fn as SEPARATE coexisting entries (see selfhost/cmd/wcc/check.ww // installdecl), so no flag is needed — the SK_USE is never overwritten, // only out-preferred. Cite: project memory module_type_name_collision // (cstage fix 2026-05-13). export fn scopelookupuselocal(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) { if (streq(b.name, name)) { if (b.skind == skind.SK_USE) { return b; }; }; b = b.hashnext; }; 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 = alloc(sym{name=name, skind=k, type_=t, decl=decl, exported=0, is_const=0, mod=mod, snext=nil, hashnext=s.buckets[bi], scope=s})!; 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 tok; import strconv; type checker = struct { 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); // #29: predeclare `type nomem = !void;` so user code needn't // declare it locally. Synthesize an nkind.N_TYPEDECL whose lhs is // nkind.N_TBANG{nkind.N_TNAME("void")} so varianterr and other // iserror-aware paths treat `nomem` identically to a user-written // alias. Mirrors cmd/wcc/check.c lookup_builtin returning // ty_nomem (NAMED, under=ty_void, iserror=1). Note: cgen owns a // separate alias chain — see collectaliases in cgen.ww for the // companion seed. let empty: str; let tnvoid: *node = newnode(nkind.N_TNAME, empty, 0, 0); tnvoid.str = "void"; let bang: *node = newnode(nkind.N_TBANG, empty, 0, 0); bang.lhs = tnvoid; let nomemdecl: *node = newnode(nkind.N_TYPEDECL, empty, 0, 0); nomemdecl.str = "nomem"; nomemdecl.lhs = bang; scopedefine(c.top, "nomem", skind.SK_TYPE, nil, nomemdecl); // `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); // #42: typed builtins folded to integer literals at check time — // `size(T)` / `align(T)` (arg is a type-expression planted by the // parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is // an N_DOT). exprtype intercepts these and rewrites the N_CALL to // N_INTLIT so cgen never sees an unresolved size/align/offset symbol. // Mirrors cmd/wcc/check.c:907-955. scopedefine(c.top, "size", skind.SK_FN, nil, nil); scopedefine(c.top, "align", skind.SK_FN, nil, nil); scopedefine(c.top, "offset", 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 ;`? 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`. This avoids the // cstage use_alias FLAG (a field on the sym, which would grow its size // and risk the wwstage cgen amalloc-undersize trap, rob-pike) — but the // flag's RESOLUTION job still has to be done. When the colliding decl's // package equals the importing unit's curmod (the `package fnmatch;` / // `package random;` self-import: random.random, fnmatch.fnmatch), the // mod-preferring scopelookupprefer returns the same-leaf SK_FN/SK_TYPE, // not the coexisting SK_USE, so a dot-lhs `mod.x` would miss the // module-qualified arm and the call nil-stamps (#6a-D). The dot-lhs // resolvers in exprtype's N_CALL and N_DOT arms re-resolve through // scopelookupuselocal (lib/ww/sym.ww) to the SK_USE that coexists in the // landed scope — the coexistence-equivalent of cstage's use_alias bit. // Cite: project memory module_type_name_collision (cstage fix // 2026-05-13). #11 (wwstage checkfile pass) revisits the dup-decl errors // below 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 ") 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; }; }; // stamptuplebinds — distribute a tuple's per-element types onto a // destructure binding chain, walked in lockstep with the resolved // N_TTUPLE element chain (each `elems` link carries its element type on // .lhs). Mirror of harec's create_unpack_bindings // (ref/harec/src/check.c:1354-1419), which harec shares between // let-unpack (check_expr_binding) and the for-each loop header // (ref/harec/src/check.c:2308-2317) — the one shape behind ww's // `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension // multi-assign `a, _ = f()`. // // `define` (the binding contexts: let-unpack + for-range) installs each // named binder as a fresh SK_VAR and back-fills its declared type onto // .lhs so use sites resolve through the N_IDENT exprtype path. Multi- // assign targets are pre-declared lvalues, so it passes false: .lhs is // left untouched (an N_INDEX/N_DOT target carries a live operand there) // and only the type_ stamp fires on the still-untyped slots. // // Each binder/target node's own type_ is stamped from its element type so // the asserttyped gate sees a typed node. This covers the discard `_` (an // empty-str N_IDENT with no decl to read a type back from): harec drops // `_` yet still advances the tuple slot, so that slot's element type is // the honest type to stamp — `_` is UNBOUND, not UNTYPED. fn stamptuplebinds(c: *checker, binds: *node, elems: *node, define: bool, what: str) void = { let b: *node = binds; let pt: *node = elems; for (b != nil) { let et: *node = nil; if (pt != nil) { et = pt.lhs; }; if (define) { if (b.lhs == nil) { b.lhs = et; }; let bnm: str = b.str; if (bnm.len > 0) { checkmoduleshadow(c, bnm, what); scopedefine(c.cur, bnm, skind.SK_VAR, nil, b); }; }; if (b.type_ == nil) { let src: *node = b.lhs; if (src == nil) { src = et; }; if (src != nil) { let ti: *tinfo = tinfofornode(c, src); if (ti != nil) { b.type_ = ti: *void; }; }; }; b = b.next; if (pt != nil) { pt = pt.next; }; }; }; // 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) { // Tuple destructure `for (let (a,b) .. xs)`: peel the // iterable's element type and distribute its tuple // element types onto the binders, the same lockstep walk // harec runs for the for-each header // (ref/harec/src/check.c:2308-2317 → create_unpack_bindings). let elems: *node = nil; let it: *node = exprtype(c, n.lhs, nil); if (it != nil) { let et: *node = nil; if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; if (et != nil) { if (et.kind == nkind.N_TTUPLE) { elems = et.list; }; }; }; stamptuplebinds(c, n.list, elems, true, "binding"); } 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(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; }; // #53: lexical block. Push a child scope so locals introduced by // inner-block lets (and the `let` install at the tail of this fn) go // out of scope at block exit. Without this, a deeply nested // `let i: u64 = 0u64;` survived to shadow a same-named outer // `let i: i32 = 1;` for the whole fn body, and exprtype handed // stale primitive types to checkletassign — silent miscompile // becomes a false-positive on the next driver (`wwdump_ww -r` // flagged the u64→i32 pair in selfhost/cmd/ww/enumeratedir). // Mirrors cstage cstmt N_BLOCK at cmd/wcc/check.c:1559-1566. if (k == nkind.N_BLOCK) { let outer: *scope = c.cur; c.cur = newscope(outer); let m: *node = n.list; for (m != nil) { resolvewalk(c, m); m = m.next; }; c.cur = outer; return; }; // `let (a, b) = call();` / `let a, b = call();` — destructure // bindings. #121 (Package B, A-narrow): distribute the callee's // tuple return-type element types onto the un-annotated bindings so // later references stamp n.type_, matching cgen's structural binding- // type classifier (cgmlet's rettupleof→localadd path). This closes // the unstamped-float-destructure gap that the exprfloatkind collapse // (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding // reads stamp nil → would disagree with the structural f64. // // #6a-A: backfill off ANY call rhs, not just a bare N_IDENT callee, so // a module-qualified `let (res, ov) = checked.addi64(a, b)` (N_DOT // callee) stamps its bindings too. This is now a SINGLE path: just call // exprtype(rhs) and consume the resolved N_TTUPLE — no callee resolution // here. Harec's create_unpack_bindings does the same: ZERO callee // resolution, it walks an already-typed tuple result (ref/harec/src/ // check.c:1354-1419). The module-qualified resolution that makes this // correct for an N_DOT callee lives at the ROOT, in exprtype's N_CALL // arm (the SK_USE-gated scopelookupinmodule there), so the binding just // consumes. A D-class module whose leaf collides with a type/fn name // resolves to nil/wrong-kind at the exprtype root (the SK_USE gate // fails) → no N_TTUPLE → those destructures stay unstamped, a separate // nominal-collision fold (#6a-D), not this one. Annotated bindings keep // their own type. Mirrors the N_FORRANGE binding-install shape above; // the bindings would otherwise install (unstamped) via the generic // N_LET walk, so this early return must register them itself. if (k == nkind.N_MLET) { if (n.rhs != nil) { resolvewalk(c, n.rhs); }; let pt: *node = nil; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { // `rt` would shadow the imported lib/rt module // (checkmoduleshadow errors); `rty` avoids it. let rty: *node = exprtype(c, n.rhs, nil); if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) { pt = rty.list; }; }; }; }; stamptuplebinds(c, n.list, pt, true, "let"); return; }; // `a, _ = call();` — tuple multi-assign (a retained ww extension over // Hare; harec has no statement-position unpack-assign). Targets are // pre-declared lvalues, resolved by the per-target resolvewalk below // before the distribution; stamptuplebinds(define=false) only stamps // still-nil slots, which is exactly the discard `_` (no decl, so the // N_IDENT exprtype path leaves it untyped). Distribution mirrors the // N_MLET/N_FORRANGE binders; see stamptuplebinds. if (k == nkind.N_MASSIGN) { if (n.rhs != nil) { resolvewalk(c, n.rhs); }; let pt: *node = nil; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { let rty: *node = exprtype(c, n.rhs, nil); if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) { pt = rty.list; }; }; }; }; let l: *node = n.list; for (l != nil) { resolvewalk(c, l); l = l.next; }; stamptuplebinds(c, n.list, pt, false, ""); 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); }; // A.6.0: branch returns early; stamp here so the post-walk // dispatch below sees N_DOT covered. exprtype N_DOT arm is // added in A.6.1; for now this is a no-op nil return. let _t: *node = exprtype(c, n, nil); 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; }; }; // #61 audit §1.8 — A.2 population: stamp tinfo onto type-expression // nodes once their children have been walked (sub-element TNAMEs // are now in scope so resolvealias inside tinfofornode can follow // user-defined aliases). Cgen's slotsize fast-path reads off // n.type_; uncovered shapes fall through to the cstage-mirror // walker until the next sub-commit graduates them. if (k == nkind.N_TNAME || k == nkind.N_TPTR || k == nkind.N_TSLICE || k == nkind.N_TCHAN || k == nkind.N_TBANG || k == nkind.N_TARRAY || k == nkind.N_TFN || k == nkind.N_TSTRUCT || k == nkind.N_TTUPLE || k == nkind.N_TTAGGED || k == nkind.N_TENUM) { if (n.type_ == nil) { let ti: *tinfo = tinfofornode(c, n); if (ti != nil) { n.type_ = ti: *void; }; }; }; if (k == nkind.N_TENUM) { stampenumvals(c, n); }; // #42's size/align/offset fold trigger lived here pre-A.6.0; the // A.6.0 end-of-fn general dispatch (below) now fires exprtype on // every N_CALL — same context-free coverage, one dispatch site. // 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). // // Cross-block `let a; { let a; };` no longer trips dup-silence // since #53 added N_BLOCK push/pop above — the inner `a` lands in // the inner block's scope. Same-scope dup `let a=1; let a=2;` // still silent-accepts here; promoting that to an error stays // queued behind #11 (test/wcc/708 + test/wcc/696 are the 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); }; }; // #104 fold-2: narrow a bare f32-context float literal AFTER the // child walk above — the post-order exprtype dispatch (below) re- // stamps a bare N_FLOATLIT back to untyped_float, so coercing earlier // (e.g. in checkletassign) would be undone. Placed here, the f32 // stamp on n.rhs / n.lhs sticks; cgen's fold-1 narrow then fires. let // / return only — see coercefloatlit's docstring for the rule-10 scope // (the cstage twin coerces in clet / cstmt N_RETURN). c.fnret is set // by resolvefnbody for the enclosing fn, mirroring checkretassign. if (k == nkind.N_LET) { coercefloatlit(c, n.rhs, n.lhs); }; if (k == nkind.N_RETURN) { coercefloatlit(c, n.lhs, c.fnret); }; // A.6.0: post-order dispatch of exprtype on every expression-yielding // node kind so n.type_ stamps fire universally — not only when reached // through checkletassign / checkretassign / checktryprop / the size- // align-offset fold. Mirrors cstage cmd/wcc/check.c cstmt's recursive // cexpr (cmd/wcc/check.c:1567 N_EXPRSTMT, :1570 N_RETURN, :1584 N_IF // cond, etc.). Plumbing-only: stamps fire from existing exprtype kind // arms (literals + idents); per-kind stamp coverage lands in A.6.1. // Stamps are tinfocache-backed idempotent so multi-walk via let / // return / try entry points is safe. N_DOT is dispatched in its own // early-return branch above; not listed here. N_LET / N_RETURN / // N_EXPRSTMT / N_IF / N_FOR / N_FORRANGE / N_BLOCK / N_MATCH-as-stmt // are not value-typed nodes; their expression children get stamped on // the recursive descent into them. Type-expression kinds (N_T*) are // covered separately by the tinfofornode block above. if (k == nkind.N_INTLIT || k == nkind.N_FLOATLIT || k == nkind.N_STRLIT || k == nkind.N_RUNELIT || k == nkind.N_TRUE || k == nkind.N_FALSE || k == nkind.N_NIL || k == nkind.N_VOIDLIT || k == nkind.N_IDENT || k == nkind.N_BIN || k == nkind.N_UN || k == nkind.N_CALL || k == nkind.N_INDEX || k == nkind.N_CAST || k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT || k == nkind.N_RECV || k == nkind.N_SLICE || k == nkind.N_SPREAD || k == nkind.N_TUPLE || k == nkind.N_TRYPROP || k == nkind.N_TRYUNW || k == nkind.N_TYPETEST || k == nkind.N_TYPEASSERT || k == nkind.N_YIELD || k == nkind.N_MATCH) { let _t: *node = exprtype(c, n, nil); }; }; // ---- 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; }; // aliassym — resolve a single nkind.N_TNAME to its IMMEDIATE type // symbol (one level, no chain walk). Returns nil for non-TNAME nodes, // unresolvable names, or non-SK_TYPE bindings. #64 factors the lookup // out of resolvealias so tinfofornode's TY_NAMED build can reach the // decl sym (nominal identity = sym.type_ ptr-identity) instead of // flattening to the underlying. Mirrors cstage resolve_typename // (cmd/wcc/check.c:60-88), which returns the sym's NAMED, not the base. fn aliassym(c: *checker, n: *node) *sym = { if (n == nil) { return nil; }; if (n.kind != nkind.N_TNAME) { return nil; }; let nm: str = n.str; // #51: pkg.alias type refs land here as a single TNAME whose // str is the joined form (lib/ww/parse/parse.ww:258-265 in // parsetype). Split on the rightmost '.' and bind the leaf in // the head module's scope. Mirrors cstage resolve_typename // cmd/wcc/check.c:74-83 strrchr branch — without this the // raw `os.oserror` lookup misses and checkisas false-positives // every cross-module tagged scrutinee. let dotidx: i32 = -1; let i: i32 = 0; for (i < nm.len) { if (nm[i] == 46u8) { dotidx = i; }; i += 1; }; let s: *sym = nil; if (dotidx >= 0) { let head: str; head.ptr = nm.ptr; head.len = dotidx; let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; s = scopelookupinmodule(c.cur, head, leaf); } else { // #53: same-module preference. Mirrors cstage // cmd/wcc/check.c:66 scope_lookup_prefer. Without this, // two modules each declaring `type invalid = ...` collide // on the head-first bucket walk: e.g. utf8.invalid `!void` // vs strconv.invalid `!i32` resolves to whichever // registered first, driving localloadop MOVSXD/MOVQ // divergence at 994/995. Other bare-leaf callers in this // file (L1597 exprtype N_IDENT, L1795/L2720 N_DOT-callee // leaf, L600 varianterr, L647 scruttype) tracked as #55. s = scopelookupprefer(c.cur, c.curmod, nm); // #61 A.5: bare TNAME that collides with an imported // module bareword. Two shapes hit this: // - `let l: lex;` where `lex` struct lives in // `package lex;` (mod matches leaf). // - `let t: tok;` where `tok` struct lives in // `package lex;` (mod differs from leaf — tok.ww // declares `package lex;`). // scopelookup bucket-walks the flat scope and can land // on the SK_USE entry first; without the fallback we'd // return the unresolved TNAME and tinfofornode aborts on // body == n. scopelookuptype walks the same bucket but // filters on SK_TYPE so the struct entry surfaces // regardless of its declaring package. Mirrors the // bare-vs-qualified pattern from task #57. if (s != nil) { if (s.skind != skind.SK_TYPE) { let sm: *sym = scopelookuptype(c.cur, nm); if (sm != nil) { s = sm; }; }; }; }; if (s == nil) { return nil; }; if (s.skind != skind.SK_TYPE) { return nil; }; return s; }; // 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 = aliassym(c, cur); if (s == nil) { 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 (module-qualified ref). 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; }; // #51: `match (pkg.var)` / `pkg.var is T` — module-qualified ref. // lhs is N_IDENT (module bareword), str is the leaf. Bind via // scopelookupinmodule so the declared type carries the same // shape resolvealias' dotted-name branch now consumes. Falls // silently to nil when lhs is a value (struct-field access) — // the rest of the lenient-check contract. if (e.kind == nkind.N_DOT) { if (e.lhs == nil) { return nil; }; if (e.lhs.kind != nkind.N_IDENT) { return nil; }; let s: *sym = scopelookupinmodule(c.cur, e.lhs.str, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; 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(nkind.N_TNAME, "", 0, 0); n.str = nm; return n; }; // #43: SSoT for primitive type byte sizes. astsize's N_TNAME-primitive // arm and every wwstage cgen size walker (slotsize/fieldsize/letemit- // size/elemsizeof/paramfieldsize) consult this table so a future // ty_str.size bump (#1) lands in one place. Returns -1 for non-prim // names; callers fall back to alias/struct/enum lookup. Cstage's // equivalent SSoT is cmd/wcc/type.c:46-79 (ty_void/ty_bool/.../ty_str). fn primtypesize(nm: str) i64 = { if (streq(nm, "void")) { return 0i64; }; if (streq(nm, "bool")) { return 1i64; }; if (streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; }; if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "size")) { return 8i64; }; // str IS []u8: 24B, sourced from the slice header SSoT so str and // []u8 can never drift; no second hardcoded 24 (#1/Phase 3). if (streq(nm, "str")) { return tyslicesize(); }; return -1i64; }; // #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors // cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's // header layout in #34 touches only this constant. fn tyslicesize() i64 = { return 24i64; }; // sizelint-ok: SSoT for ty_slice header (#64) // #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f) // fold. Mirror cstage resolve_type's size/align computation // (cmd/wcc/check.c:286-528) on AST nodes — wwstage check.ww never // materialises tinfo for user types so the fold has to walk the AST // directly. Struct layout follows cstage check.c:471-526 (align each // field, max align for the whole record, round size up to alignment). fn astalign(c: *checker, t: *node) i64 = { if (t == nil) { return 1i64; }; let k: nkind = t.kind; if (k == nkind.N_TBANG) { return astalign(c, t.lhs); }; if (k == nkind.N_TPTR) { return 8i64; }; if (k == nkind.N_TSLICE) { return 8i64; }; if (k == nkind.N_TCHAN) { return 8i64; }; if (k == nkind.N_TFN) { return 8i64; }; if (k == nkind.N_TARRAY) { return astalign(c, t.lhs); }; if (k == nkind.N_TTAGGED) { return 8i64; }; if (k == nkind.N_TTUPLE) { let m: i64 = 1i64; let p: *node = t.list; for (p != nil) { let pa: i64 = astalign(c, p.lhs); if (pa > m) { m = pa; }; p = p.next; }; return m; }; if (k == nkind.N_TSTRUCT) { let m: i64 = 1i64; let f: *node = t.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { let fa: i64 = astalign(c, f.lhs); if (fa > m) { m = fa; }; }; f = f.next; }; return m; }; if (k == nkind.N_TENUM) { if (t.lhs != nil) { return astalign(c, t.lhs); }; return 4i64; }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "void") || streq(nm, "bool") || streq(nm, "i8") || streq(nm, "u8")) { return 1i64; }; if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; }; if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; }; if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "size") || streq(nm, "str")) { return 8i64; }; let resolved: *node = resolvealias(c, t); if (resolved != nil && resolved != t) { return astalign(c, resolved); }; }; return 1i64; }; fn astsize(c: *checker, t: *node) i64 = { if (t == nil) { return 0i64; }; let k: nkind = t.kind; if (k == nkind.N_TBANG) { return astsize(c, t.lhs); }; if (k == nkind.N_TPTR) { return 8i64; }; if (k == nkind.N_TSLICE) { return tyslicesize(); }; if (k == nkind.N_TCHAN) { return 8i64; }; if (k == nkind.N_TFN) { return 8i64; }; if (k == nkind.N_TARRAY) { let elen: i64 = 0i64; if (t.rhs != nil) { if (t.rhs.kind == nkind.N_INTLIT) { elen = t.rhs.uval: i64; }; }; return astsize(c, t.lhs) * elen; }; if (k == nkind.N_TTUPLE) { let total: i64 = 0i64; let p: *node = t.list; for (p != nil) { total += astsize(c, p.lhs); p = p.next; }; return total; }; if (k == nkind.N_TSTRUCT) { let off: i64 = 0i64; let maxal: i64 = 1i64; let f: *node = t.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { let fa: i64 = astalign(c, f.lhs); if (fa > maxal) { maxal = fa; }; off = (off + fa - 1i64) & ~(fa - 1i64); off += astsize(c, f.lhs); }; f = f.next; }; return (off + maxal - 1i64) & ~(maxal - 1i64); }; if (k == nkind.N_TTAGGED) { // 8 (tag) + max variant payload, rounded up to 8. let maxsz: i64 = 0i64; let v: *node = t.list; for (v != nil) { let sz: i64 = astsize(c, v); if (sz > maxsz) { maxsz = sz; }; v = v.next; }; let pad: i64 = (maxsz + 7i64) & ~7i64; return 8i64 + pad; }; if (k == nkind.N_TENUM) { if (t.lhs != nil) { return astsize(c, t.lhs); }; return 4i64; }; if (k == nkind.N_TNAME) { let nm: str = t.str; let ps: i64 = primtypesize(nm); if (ps >= 0i64) { return ps; }; let resolved: *node = resolvealias(c, t); if (resolved != nil && resolved != t) { return astsize(c, resolved); }; }; return 0i64; }; // astunsized — #108(b): true iff `t` contains an unsized component. A // type is unsized iff it is the abstract `opaque` (size/align == // SIZE_UNDEFINED) OR an aggregate (array / struct / tuple / tagged) // with a recursively-unsized member. The wwstage has NO type-decl // construction guards (those are cstage-only, rule-10), so its size()/ // align() FOLD must detect every opaque-containing type itself — a // leaf-only check would silently fold size([4]opaque) / size(struct{x: // opaque}) / size((opaque, i32)) to garbage (rule 7). Does NOT peel // TPTR/TSLICE/TCHAN/TFN — `*opaque` (8B) and `[]opaque` (24B header) // are sized and legal behind indirection. Cstage twin: the leaf // `m == SIZE_UNDEFINED` size/align guard PLUS the per-construction // require_sized guards that reject unsized aggregates at the type decl // (so the cstage size/align fold only ever sees a leaf opaque); harec // ref/harec/src/check.c:2720, type_store.c:1147 (tuple) / :449 (tagged). fn astunsized(c: *checker, t: *node) bool = { if (t == nil) { return false; }; let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return false; }; let k: nkind = u.kind; if (k == nkind.N_TNAME) { if (streq(u.str, "opaque")) { return true; }; return false; }; if (k == nkind.N_TARRAY) { return astunsized(c, u.lhs); }; if (k == nkind.N_TTUPLE) { let p: *node = u.list; for (p != nil) { if (astunsized(c, p.lhs)) { return true; }; p = p.next; }; return false; }; if (k == nkind.N_TSTRUCT) { let f: *node = u.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { if (astunsized(c, f.lhs)) { return true; }; }; f = f.next; }; return false; }; if (k == nkind.N_TTAGGED) { let v: *node = u.list; for (v != nil) { if (astunsized(c, v)) { return true; }; v = v.next; }; return false; }; return false; }; // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand // type. Returns nil if no yield is reachable from `body`. Doesn't // descend into a nested N_MATCH — each match opens its own yield // scope. exprtype is idempotent on already-stamped nodes (tinfocache // path at L467) so re-entering it on the yield operand here is safe. fn matchyieldtype(c: *checker, body: *node) *node = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; return exprtype(c, body.lhs, nil); }; if (k == nkind.N_MATCH) { return nil; }; if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { let t: *node = matchyieldtype(c, s); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { let t: *node = matchyieldtype(c, body.body); if (t != nil) { return t; }; return matchyieldtype(c, body.els); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { return matchyieldtype(c, body.body); }; return nil; }; // astoffset — byte offset of `dot.str` inside the struct type of // `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR // (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields // honouring per-field alignment, return -1 if the field name is // absent so the caller can flag the error and fold to 0. fn astoffset(c: *checker, dot: *node) i64 = { if (dot == nil) { return -1i64; }; if (dot.kind != nkind.N_DOT) { return -1i64; }; let recv: *node = scruttype(c, dot.lhs); if (recv == nil) { return -1i64; }; let rtyp: *node = resolvealias(c, unwrapbang(recv)); if (rtyp == nil) { return -1i64; }; if (rtyp.kind == nkind.N_TPTR) { rtyp = resolvealias(c, unwrapbang(rtyp.lhs)); }; if (rtyp == nil) { return -1i64; }; if (rtyp.kind != nkind.N_TSTRUCT) { return -1i64; }; let off: i64 = 0i64; let f: *node = rtyp.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { let fa: i64 = astalign(c, f.lhs); off = (off + fa - 1i64) & ~(fa - 1i64); if (streq(f.str, dot.str)) { return off; }; off += astsize(c, f.lhs); }; f = f.next; }; return -1i64; }; // arenau64tos — decimal string for the folded INTLIT's `str` field. // Cstage uses aprintf("%llu") at the same site (cmd/wcc/check.c:921); // wwstage cgen only reads `uval` for N_INTLIT codegen so `str` is // just for the AST printer, but set it for parity with the parser's // own literal-emit shape. fn arenau64tos(v: u64) str = { let buf: []u8 = alloc([], 24u64)!; let i: i32 = 23; buf[i] = 0u8; if (v == 0u64) { i -= 1; buf[i] = 48u8; }; let n: u64 = v; for (n > 0u64) { i -= 1; buf[i] = (48u64 + (n % 10u64)): u8; n /= 10u64; }; let r: str; r.ptr = buf.ptr + (i: u64); r.len = 23 - i; return r; }; // foldtointlit — mutate `n` in place to an N_INTLIT with value `v`. // Used by the #42 size/align/offset intercepts so cgen sees the // folded literal rather than an unresolved call. Mirrors cstage // cmd/wcc/check.c:919-927 / :951-958. fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.kind = nkind.N_INTLIT; n.uval = v: u64; n.str = arenau64tos(v: u64); n.lhs = nil; n.list = nil; let empty: str; n.tsuffix = empty; }; // foldbinop — shared constant binary-op core for the two compile-time // integer evaluators in this file: enumvalfold (enum member exprs) and // evaldefconst (top-level def rhs, #88). One op table so the cstage // (cmd/wcc/check.c fold_binop) and wwstage stamp the bit-identical // literal — rule 10 lives at the check pass for #88. Returns false on // division by zero or an op outside the constant subset; the caller // maps that to its own diagnostic. fn foldbinop(op: tkind, a: u64, b: u64, out: *u64) bool = { 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; }; // deffolderr — loud diagnostic + checker error count bump for an // unfoldable def rhs (cycle / narrowing-cast / bad op). c.errs > 0 // gates cgen off in main.ww:165, so this fails the build rather than // emitting a missing DATA row silently (rule 7). cstage twin: err() // in cmd/wcc/check.c. fn deffolderr(c: *checker, n: *node, msg: str) void = { os.write(2, n.file.ptr, n.file.len: u64); os.write(2, ": error: ".ptr, 9u64); os.write(2, msg.ptr, msg.len: u64); os.write(2, "\n".ptr, 1u64); c.errs += 1; }; // defcastfits — wwstage twin of cstage def_cast_fits (cmd/wcc/check.c): // does the folded u64 `v` survive narrowing to integer target `t`? // Identity / widening / same-width casts always fit; a genuine // narrowing cast whose value falls outside the target range must NOT // be silently truncated (rule 7 / drew). Width via the type table // (t.size, rule 13); the 8s are CHAR_BIT and the u64 byte-width, not // type-layout sizes, so they sit outside rule 13's scope. Pure-u64 so // the range check is bit-identical to cstage (rule 10). fn defcastfits(t: *tinfo, v: u64) bool = { if (!typeisint(t)) { return true; }; // non-int target: keep value let w: u64 = t.size; if (w >= 8u64) { return true; }; // 64-bit target: no narrowing let bits: u64 = w * 8u64; if (typeisunsigned(t)) { return (v >> bits) == 0u64; }; // signed: truncate to `bits` then sign-extend; fits iff unchanged let mask: u64 = (1u64 << bits) - 1u64; let sign: u64 = 1u64 << (bits - 1u64); let ext: u64 = ((v & mask) ^ sign) - sign; return ext == v; }; // evaldefconst — fold a top-level def's rhs to a u64 constant, // resolving sibling and imported def references, casts, and // arithmetic (#88). Reuses foldintliteral (leaf/unary) + foldbinop // (arith); the ONLY thing it does that enumvalfold doesn't is resolve // an identifier through the checker's flat scope (scopelookupprefer // for a bare sibling ref, scopelookupinmodule for `mod.NAME`) to the // referent def's own rhs, then recurse. // // Why this stays distinct from enumvalfold rather than a full merge // (rule 8 WHY): enum-member eval carries implicit prev+1 auto-increment // and forward-only sibling lookup over the member chain; def eval has // neither — it resolves through the scope/decl graph, which references // forward and across modules. The two lookup models don't reconcile // cleanly, so they share the arith core (foldbinop) + leaf fold // (foldintliteral) and keep separate top-level shapes. // // `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A, // incl. cross-module) hits the cap and fails loud rather than hanging // (rule 7), mirroring the cgen nsteps>=16 abort precedent. fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = { if (n == nil) { return false; }; if (depth >= 16) { deffolderr(c, n, "def value: reference chain too deep (cycle?)"); return false; }; if (foldintliteral(n, out)) { return true; }; let k: nkind = n.kind; if (k == nkind.N_BIN) { let a: u64 = 0u64; let b: u64 = 0u64; if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; }; if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; }; if (foldbinop(n.op, a, b, out)) { return true; }; if ((n.op == tkind.TK_SLASH || n.op == tkind.TK_PERCENT) && b == 0u64) { deffolderr(c, n, "def value: division by zero"); } else { deffolderr(c, n, "def value: unsupported binary op"); }; return false; }; if (k == nkind.N_UN) { // foldintliteral already covers unary-over-leaf; this arm // catches unary over a resolved ref, e.g. `-A`. let v: u64 = 0u64; if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; if (n.op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (n.op == tkind.TK_TILDE) { *out = ~v; return true; }; if (n.op == tkind.TK_PLUS) { *out = v; return true; }; deffolderr(c, n, "def value: unsupported unary op"); return false; }; if (k == nkind.N_CAST) { // n.lhs = value; n.type_ = resolved target (stamped by // exprtype's N_CAST arm during resolvewalk). Strip the cast // keeping the value; a narrowing cast that loses it fails loud. let v: u64 = 0u64; if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; }; let t: *tinfo = (n.type_): *tinfo; if (!defcastfits(t, v)) { deffolderr(c, n, "def value: narrowing cast loses value"); return false; }; *out = v; return true; }; if (k == nkind.N_IDENT) { let s: *sym = scopelookupprefer(c.cur, c.curmod, n.str); if (s == nil) { return false; }; if (s.skind != skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; if (s.decl.rhs == nil) { return false; }; return evaldefconst(c, s.decl.rhs, out, depth + 1); }; if (k == nkind.N_DOT) { if (n.lhs == nil) { return false; }; if (n.lhs.kind != nkind.N_IDENT) { return false; }; let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str); if (s == nil) { return false; }; if (s.skind != skind.SK_DEF) { return false; }; if (s.decl == nil) { return false; }; if (s.decl.rhs == nil) { return false; }; return evaldefconst(c, s.decl.rhs, out, depth + 1); }; return false; }; // stampintlit — rewrite a const-folded def rhs in place to its literal // value, preserving the node's resolved type_ so the DATA-row emit // width and pass-3 asserttyped see a properly-typed literal leaf. Lets // cgen's existing emitdefconstants lay down the row with no codegen // change (#88). Shape mirrors foldtointlit (the #42 stamp). fn stampintlit(n: *node, v: u64) void = { n.kind = nkind.N_INTLIT; n.uval = v; n.op = tkind.TK_NONE; n.str = arenau64tos(v); n.lhs = nil; n.rhs = nil; n.cond = nil; n.body = nil; n.els = nil; n.list = nil; let empty: str; n.tsuffix = empty; // n.type_ left intact (the type exprtype inferred for the rhs). }; // enumvalfold — fold an enum member's value expression to a u64 // constant. The Hare-fidelity set: literal leaves, unary +/-/~, // binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>), // and sibling backref. Mirrors cstage cmd/wcc/check.c:185-208 // (fold_int_literal) + :210-284 (eval_enum_value); the wider // constexpr evaluator is at ref/harec/src/eval.c (harec resolves // each enum member via eval_expr per ref/harec/src/check.c:4419- // 4434). Wwstage cgen.ww:158-227 (foldintliteral + enumevalmember) // already ships this set for codegen — check now matches. // // `body` is the N_TENUM whose .list is the member chain. `until` // is the member currently being resolved; sibling lookup walks // forward from body.list and stops at `until` to enforce harec's // lnext forward-only-ref discipline (ref/harec/src/check.c:4436- // 4438). `e` starts as that member's lhs and recurses into its // children. Returns false on unfoldable shape, unknown sibling, // or division by zero — callers bail the wrapping N_DOT fold. // // Recursion bound: O(N²) worst case on chained sibling backrefs // (each ident lookup re-walks 0..until). Enum bodies are tiny in // practice — harec accepts the same shape without memoisation per // resolve_enum_field's wrap_resolver chain // (ref/harec/src/check.c:4438) — so the quadratic is harmless. fn enumvalfold(body: *node, until: *node, 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 = 0u64; if (!enumvalfold(body, until, 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; }; if (k == nkind.N_BIN) { let a: u64 = 0u64; let b: u64 = 0u64; if (!enumvalfold(body, until, e.lhs, &a)) { return false; }; if (!enumvalfold(body, until, e.rhs, &b)) { return false; }; return foldbinop(e.op, a, b, out); }; if (k == nkind.N_IDENT) { let prev: u64 = (-1i64): u64; let m: *node = body.list; for (m != nil && m != until) { let val: u64 = 0u64; if (m.lhs == nil) { val = prev + 1u64; } else { if (!enumvalfold(body, m, m.lhs, &val)) { return false; }; }; prev = val; if (streq(m.str, e.str)) { *out = val; return true; }; m = m.next; }; return false; }; return false; }; // stampenumvals — give every node in each enum-member value-expr a // non-nil type_. resolvewalk's post-order exprtype (L543) stamps the // literal leaves, but a sibling backref (`B = A + 4`) resolves to // nothing — enum members aren't installed as scope idents — so the // backref N_IDENT and the N_BIN/N_UN wrapping it stay nil. asserttyped // walks the enum DEFINITION (whether or not a member is `.`-accessed) // and its value-node invariant then fires on those. harec checks each // member's value-expr at the enum's underlying type // (ref/harec/src/check.c:4419 — check_expression with type->alias.type), // so the whole constant subtree carries the underlying integer type; // mirror that. The value itself is folded to a constant at every use // site (enumvalfold) and at codegen (cgen.ww enumevalmember), so cgen // never reads these node types — this stamp is checker metadata only. fn stampenumvals(c: *checker, n: *node) void = { let under: *tinfo = c.tc.tyi32; if (n.lhs != nil) { let s: *tinfo = tinfofornode(c, n.lhs); if (s != nil) { under = s; }; }; let m: *node = n.list; for (m != nil) { stampnilexpr(m.lhs, under); m = m.next; }; }; // stampnilexpr — stamp nil-typed nodes in a constant expr subtree to // `ti`. lhs/rhs cover the enum constexpr grammar enumvalfold accepts // (literals, unary, binary, sibling backref); non-nil nodes keep the // type exprtype already derived. fn stampnilexpr(n: *node, ti: *tinfo) void = { if (n == nil) { return; }; if (n.type_ == nil) { n.type_ = ti: *void; }; stampnilexpr(n.lhs, ti); stampnilexpr(n.rhs, ti); }; // #61 A.5 helper: per-element slot size when `pt` appears inside a // tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI // spills each element into its own register / 8B eightbyte, so narrow // scalars pad to 8 (cgen's let_emit_size + AX:DX:CX positional layout). // str/slice and composites consult `pt.size` so a future #1 bump on // any primitive layout propagates through the typ.ww SSoT seed // instead of getting baked into this detour. pointer/fn/chan stay // 8; void contributes 0 (never appears in tuples emitted by user // code, but kept for SSoT symmetry with cgen's N_TNAME-"void" // fallback arm). fn tupleelemslot(pt: *tinfo) u64 = { if (pt == nil) { return 8u64; }; // #63 Phase-N step 1: peel TY_NAMED before this structural query. // #64 builds per-decl NAMED wrappers (tinfofornode), so the peel // now fires on aliased operands; byte-id holds because it collapses // NAMED to the alias-invariant underlying this read consumes. let t: *tinfo = pt; for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; if (t == nil) { return 8u64; }; let pk: tykind = t.kind; if (pk == tykind.TY_VOID) { return 0u64; }; if (pk == tykind.TY_STR) { return t.size; }; if (pk == tykind.TY_SLICE) { return t.size; }; if (pk == tykind.TY_PTR || pk == tykind.TY_FN || pk == tykind.TY_CHAN || pk == tykind.TY_I64 || pk == tykind.TY_U64 || pk == tykind.TY_INT || pk == tykind.TY_UINT || pk == tykind.TY_UINTPTR || pk == tykind.TY_SIZE || pk == tykind.TY_F64) { return 8u64; }; if (pk == tykind.TY_BOOL || pk == tykind.TY_RUNE || pk == tykind.TY_I8 || pk == tykind.TY_I16 || pk == tykind.TY_I32 || pk == tykind.TY_U8 || pk == tykind.TY_U16 || pk == tykind.TY_U32 || pk == tykind.TY_F32 || pk == tykind.TY_ENUM) { return 8u64; }; // Composite — struct/tuple/array/tagged carry their own slot total. return t.slotsize; }; // #61 A.5 helper: per-field slot size mirroring cgenutil.ww // registerstruct/fieldsize. Nested struct fields contribute their // slot-padded total (si.totsize equivalent); primitives keep their // natural width (struct interior packing is unaffected by stack-slot // pad-to-8); arrays use their slot-padded element-stride * elen. fn fieldslotsize(ft: *tinfo) u64 = { if (ft == nil) { return 8u64; }; // #63 Phase-N step 1: peel TY_NAMED before this structural query. // #64 builds per-decl NAMED wrappers (tinfofornode), so the peel // now fires on aliased operands; byte-id holds because it collapses // NAMED to the alias-invariant underlying this read consumes. let t: *tinfo = ft; for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; if (t == nil) { return 8u64; }; let fk: tykind = t.kind; if (fk == tykind.TY_STRUCT) { return t.slotsize; }; if (fk == tykind.TY_ARRAY) { return t.slotsize; }; if (fk == tykind.TY_TAGGED) { return t.size; }; // str / slice read t.size so the typ.ww SSoT seed is the single // source for #1 (str→24) / #34 (slice graduation) — no hardcoded // literal here to drift. if (fk == tykind.TY_SLICE) { return t.size; }; if (fk == tykind.TY_PTR || fk == tykind.TY_FN || fk == tykind.TY_CHAN) { return 8u64; }; if (fk == tykind.TY_STR) { return t.size; }; // Primitives keep natural width inside structs (matches // cgenutil fieldsize: primsize, not pad-to-8). TY_TUPLE inside a // struct currently defaults to 8 in cgenutil — preserve that // shape until a future graduation aligns the two. if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE || fk == tykind.TY_I8 || fk == tykind.TY_I16 || fk == tykind.TY_I32 || fk == tykind.TY_I64 || fk == tykind.TY_U8 || fk == tykind.TY_U16 || fk == tykind.TY_U32 || fk == tykind.TY_U64 || fk == tykind.TY_INT || fk == tykind.TY_UINT || fk == tykind.TY_UINTPTR || fk == tykind.TY_SIZE || fk == tykind.TY_F32 || fk == tykind.TY_F64 || fk == tykind.TY_ENUM) { return t.size; }; return 8u64; }; // #61 audit §1.8 — resolve a type-expression AST node to its *tinfo. // Mirrors cstage's resolve_type (cmd/wcc/check.c:286-565) which // produces ty_* singletons / arena-allocated composites from a Node*. // Cache lives in c.tc (typ.ww) so the same shape can be reused across // modules within one check pass. Rob+Drew convergence 2026-05-20: cgen // reads sizes from here starting with slotsize in A.2; subsequent // sub-commits graduate elemsize/fieldsize/letemitsize/etc. onto the // same pivot. // // A.2 coverage: primitive TNAME singletons, TNAME aliases (via // resolvealias), TBANG (inner unchanged — see iserror note), TPTR, // TSLICE, TCHAN, TARRAY, TFN, TENUM, TTUPLE, TSTRUCT, TTAGGED. Size // computation tracks cstage natural sizes; cgen's slot-padding // contract (cmd/w6c/cgen.c let_emit_size:691-720 pads narrow scalars // to 8B) stays in slotsize's fallback walker. fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n == nil) { return nil; }; let cached: *tinfo = tinfocachelookup(c.tc, n); if (cached != nil) { return cached; }; let r: *tinfo = nil; let k: nkind = n.kind; if (k == nkind.N_TNAME) { let nm: str = n.str; if (streq(nm, "void")) { r = c.tc.tyvoid; }; if (streq(nm, "bool")) { r = c.tc.tybool; }; if (streq(nm, "rune")) { r = c.tc.tyrune; }; if (streq(nm, "i8")) { r = c.tc.tyi8; }; if (streq(nm, "i16")) { r = c.tc.tyi16; }; if (streq(nm, "i32")) { r = c.tc.tyi32; }; if (streq(nm, "i64")) { r = c.tc.tyi64; }; if (streq(nm, "u8")) { r = c.tc.tyu8; }; if (streq(nm, "u16")) { r = c.tc.tyu16; }; if (streq(nm, "u32")) { r = c.tc.tyu32; }; if (streq(nm, "u64")) { r = c.tc.tyu64; }; if (streq(nm, "int")) { r = c.tc.tyint; }; if (streq(nm, "uint")) { r = c.tc.tyuint; }; if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; if (streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 if (streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) if (streq(nm, "f32")) { r = c.tc.tyf32; }; if (streq(nm, "f64")) { r = c.tc.tyf64; }; if (streq(nm, "str")) { r = c.tc.tystr; }; if (streq(nm, "never")) { r = c.tc.tynever; }; if (streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; }; if (streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; }; if (streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; }; if (streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; }; if (streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; }; if (streq(nm, "untyped_nil")) { r = c.tc.tyuntypednil; }; if (r == nil) { // #64 Phase-N step 2 (THE FLIP): build a per-decl // TY_NAMED wrapper instead of collapsing the alias to // its underlying. sym.type_ caches the wrapper so every // TNAME resolving to the same decl yields the SAME tinfo // pointer — ptr-identity IS nominal identity (the whole // point; typeeq is the only consumer, wired in step 3). // CHAINS, not flatten: under is the IMMEDIATE body's // tinfo, so `type a = b` gives NAMED(a).under = NAMED(b) // — mirrors cstage's two-phase type_named // (cmd/wcc/check.c:1900-1929): pass1 creates the NAMED, // pass2 patches under/size off resolve_type(d->lhs), // where resolve_typename returns the inner NAMED. let s: *sym = aliassym(c, n); if (s != nil) { if (s.type_ != nil) { r = s.type_; } else { let body: *node = nil; if (s.decl != nil) { body = unwrapbang(s.decl.lhs); }; if (body != nil) { // PRE-BIND before resolving under: a // self-referential field (`type node = // struct {next: *node}`) re-finds this // NAMED via sym.type_ instead of re- // entering the chain. Mirrors cstage // pass1's sym->type install // (check.c:1909/1917) ahead of pass2's // under patch, and A.2's TSTRUCT/TFN/ // TTAGGED tinfocachebind cycle-break. let named: *tinfo = typenamed(s.name, nil); s.type_ = named; let under: *tinfo = tinfofornode(c, body); named.under = under; if (under != nil) { named.size = under.size; named.align = under.align; named.slotsize = under.slotsize; }; r = named; }; }; }; }; } else { if (k == nkind.N_TBANG) { // #61 audit §1.8: `!T` propagates the inner shape; cstage's // resolve_type sets ty->iserror on the wrapper but no wwstage // cgen reader consumes it yet, so A.1 drops the flag and // returns the inner tinfo unchanged. Mirrors typeeqast's // unwrapbang pre-walk; graduate alongside the first cgen // site that needs iserror discrimination. r = tinfofornode(c, n.lhs); } else { if (k == nkind.N_TPTR) { r = typeptr(tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TSLICE) { r = typeslice(tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TCHAN) { r = typechan(tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TARRAY) { // Cstage cmd/wcc/check.c:314-326: length must be an integer // literal (`[_]T` keeps alen=0 as the inferred-length sentinel // patched at letslotsize-time). // // #61 A.5: ti.size = natural (sub.size * elen), ti.slotsize = // slot-padded (sub.slotsize * elen) — typearray handles both. // Reverts A.4's r.size override (which conflated stride with // natural size); the slot-padded stride now lives in slotsize // where cgenutil's fast-path reads it. let elen: u64 = 0u64; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; let sub: *tinfo = tinfofornode(c, n.lhs); r = typearray(sub, elen); } else { if (k == nkind.N_TFN) { // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B // (call-target pointer shape). Pre-bind before recursing into // the return type so a recursive `type F = fn() F` self-ref // doesn't spin (cycle-break mirror of the TSTRUCT/TTAGGED // pattern below). r = newtype(tykind.TY_FN); r.size = 8u64; r.align = 8u64; r.slotsize = 8u64; tinfocachebind(c.tc, n, r); r.ret = tinfofornode(c, n.lhs); } else { if (k == nkind.N_TENUM) { // Cstage cmd/wcc/check.c:529-542: storage type's size/align // (default i32 = 4B/4B). Cgen's slotsize-TENUM fallback pads // to 8B per its stack-slot contract; tinfo.size carries the // raw storage width so size(EnumT) folds to the correct value. r = newtype(tykind.TY_ENUM); let storage: *tinfo = nil; if (n.lhs != nil) { storage = tinfofornode(c, n.lhs); }; if (storage == nil) { storage = c.tc.tyi32; }; r.sub = storage; r.size = storage.size; r.align = storage.align; r.slotsize = storage.size; } else { if (k == nkind.N_TTUPLE) { // Cstage cmd/wcc/check.c:329-345: sum of element sizes with // per-element alignment NOT padded — cstage uses raw sums for // tuples and 8B-rounding lives at the call/return ABI layer. // Pre-bind for cycle protection (recursive tuple shapes). // // #61 A.5: ti.size = natural sum (cstage parity); ti.slotsize // = per-element slot sum mirroring cgenutil.ww:2018-2029 // slotsize TTUPLE — narrow scalars pad to 8 (cgen spills each // tuple element into its own register / stack-slot eightbyte), // composites contribute their own ti.slotsize. r = newtype(tykind.TY_TUPLE); tinfocachebind(c.tc, n, r); // #57 A.6.3i-phase-1: populate r.tupleelems as a ttupleelem // linked list (head=positional 0) in lock-step with the // size/align accumulator. Harec analog ref/harec/src/type_ // store.c:532-589 tuple_init_from_atype — {type, offset, next} // per member onto type->tuple.next chain. Diverges from cstage // cmd/wcc/check.c:329-345 which stores tuple positionals on // t->params (Tparam, no offset, consumer recomputes by walking // at cgen.c:5723-5750); the offset-stored shape lets Phase // 2/J consumers (dotchainresolve) read offsets directly per // the A.6 stamp-once-read-many arc. Direct analog // 26724fe (#50 phase 1, A.6.3f-a) for the head/tail append // pattern. Offset matches cstage's raw-sum layout (no per- // element padding) — rule 10 aligns wwstage tuple layout down // to cstage, distinct from harec's add_padding(&offset, // memb.align) at type_store.c:561. let teh: *ttupleelem = nil; let tet: *ttupleelem = nil; let total: u64 = 0u64; let slottotal: u64 = 0u64; let maxal: u64 = 1u64; let p: *node = n.list; for (p != nil) { let pt: *tinfo = tinfofornode(c, p.lhs); let elemoff: u64 = total; let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=elemoff, tnext=nil})!; if (teh == nil) { teh = te; } else { tet.tnext = te; }; tet = te; if (pt != nil) { if (pt.align > maxal) { maxal = pt.align; }; total += pt.size; slottotal += tupleelemslot(pt); }; p = p.next; }; r.tupleelems = teh; r.size = total; r.align = maxal; r.slotsize = slottotal; } else { if (k == nkind.N_TSTRUCT) { // Cstage cmd/wcc/check.c:468-527: per-field alignment, max // align for the whole record, total rounded up to alignment. // Anonymous-embed promotion is deferred (#13). // // Pre-bind into the cache BEFORE walking fields so a // self-referential pointer field (e.g., `next: *node` inside // `type node = struct {..., next: *node, ...}`) terminates: // the inner tinfofornode(TNAME(node)) resolvealias-recurses // back to this same body node, hits the cache, and returns // the in-progress stub. r.size is filled in below; the stub's // only consumer during the recursion is typeptr (8B/8B // regardless of pointee size), so partial-fill is safe. // // #61 A.5: alongside the natural layout (cstage parity), walk // the same fields with the slot-padded sizing cgenutil.ww // registerstruct uses (fieldsize → si.totsize for nested // struct; size-derived alignment; final round to 8). That // slot total lands in ti.slotsize so the cgen fast-path can // graduate TY_STRUCT off the AST walker. r = newtype(tykind.TY_STRUCT); tinfocachebind(c.tc, n, r); // #57 A.6.3i-phase-1: populate r.fields as a tfield linked // list (head=first declared field) in lock-step with the // natural-layout offset accumulator. Mirrors cstage cmd/wcc/ // check.c:468-527 (Tfield {name, type, offset, next} per // member onto t->fields). Direct analog 26724fe (#50 phase 1, // A.6.3f-a) for the head/tail append pattern. Harec cite: // ref/harec/include/types.h:109-115 struct_field and // ref/harec/src/type_store.c:314-347 struct_init_from_atype. // Anonymous-embed promotion not populated here (#13 per // the cstage cite at check.ww:1263). let fh: *tfield = nil; let ft_: *tfield = nil; let off: u64 = 0u64; let maxalign: u64 = 1u64; let soff: u64 = 0u64; let f: *node = n.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { let ft: *tinfo = tinfofornode(c, f.lhs); if (ft != nil) { if (ft.align > maxalign) { maxalign = ft.align; }; if (ft.align > 0u64) { off = (off + ft.align - 1u64) & ~(ft.align - 1u64); }; let fldoff: u64 = off; let tf: *tfield = alloc(tfield{name=f.str, type_=ft, offset=fldoff, tnext=nil})!; if (fh == nil) { fh = tf; } else { ft_.tnext = tf; }; ft_ = tf; off += ft.size; // Slot-padded layout (mirror of cgenutil // fieldsize + registerstruct align rules). let fsz: u64 = fieldslotsize(ft); let faln: u64 = 1u64; if (fsz >= 8u64) { faln = 8u64; } else { if (fsz >= 4u64) { faln = 4u64; } else { if (fsz >= 2u64) { faln = 2u64; }; }; }; if ((soff & (faln - 1u64)) != 0u64) { soff = (soff + faln - 1u64) & ~(faln - 1u64); }; soff += fsz; }; }; f = f.next; }; r.fields = fh; if (maxalign > 0u64) { r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64); }; r.align = maxalign; if ((soff & 7u64) != 0u64) { soff = (soff + 7u64) & ~7u64; }; r.slotsize = soff; } else { if (k == nkind.N_TTAGGED) { // Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant) // rounded up to 8. Pre-bind for cycle protection (recursive // sum-type shapes through NAMED variants). r = newtype(tykind.TY_TAGGED); tinfocachebind(c.tc, n, r); // #50 / A.6.3f phase 1: populate ti.params as a tparam linked // list (head=first source variant). #61a: flatten `...inner` // tagged spreads into the chain and stamp each variant's // iserror. Mirrors cstage check.c:366-389 — dealias one NAMED // level, require TY_TAGGED, splice its (already-flattened) // variants in declaration order; otherwise append the single // variant. size/align stays accounted off the surface member // (vt), so r.size is byte-identical to pre-#61a: the flatten + // iserror are additive, with no #61a-stage readers (the variant // machinery + cgwidentaggedstore/matchscrutt migrate onto the // chain in #61b/c). Same shared Tparam shape ww reuses across // struct-fields / tuple-fields / fn-params / tagged-variants // (sea-of-stars per rule 12). Phase 2 (#50b) retired cgenutil's // AST-keyed nullableptrtag onto the chain. let head: *tparam = nil; let tail: *tparam = nil; let maxsz: u64 = 0u64; let al: u64 = 8u64; let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); if (vt != nil) { if (vt.size > maxsz) { maxsz = vt.size; }; if (vt.align > al) { al = vt.align; }; }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { if (vu != nil) { if (vu.kind == tykind.TY_NAMED) { vu = vu.under; }; }; }; if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) { // Spliced variants carry the inner union's // already-stamped iserror; no re-derivation. let src: *tparam = vu.params; for (src != nil) { let tp: *tparam = alloc(tparam{name="", type_=src.type_, iserror=src.iserror, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; tail = tp; src = src.tnext; }; } else { let ve: bool = varianterr(c, v); let tp: *tparam = alloc(tparam{name="", type_=vt, iserror=ve, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; tail = tp; }; v = v.next; }; r.params = head; // #61 A.3 nullable fold: `(*T | void)` collapses to a single // 8B pointer slot, null is the void variant. Mirrors // cmd/wcc/check.c:412-426 — bare TNAME("void"), not `!void`, // and not NAMED. AST-kind discrimination retained: wwstage // tinfo carries no `iserror` field, so cstage's tinfo-level // (kind==TY_VOID && !iserror) check doesn't port symmetrically. let a: *node = n.list; if (a != nil) { let b: *node = a.next; if (b != nil && b.next == nil) { 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"); }; let isnull: bool = false; if (aptr) { if (bvoid) { isnull = true; }; }; if (avoid) { if (bptr) { isnull = true; }; }; if (isnull) { r.size = 8u64; r.align = 8u64; r.nullable = 1; r.slotsize = 8u64; return r; }; }; }; let pad: u64 = (maxsz + 7u64) & ~7u64; r.size = 8u64 + pad; r.align = al; r.slotsize = 8u64 + pad; };};};};};};};};};};}; if (r != nil) { // #61 A.5: any arm that didn't set slotsize gets ti.size as // the default (covers primitives via prim() + the ptr/slice/ // chan paths which already populate slotsize, plus TBANG which // inherits the inner's tinfo unchanged). if (r.slotsize == 0u64) { r.slotsize = r.size; }; tinfocachebind(c.tc, n, r); }; return r; }; // unifyarith — usual-arithmetic-conversion analogue at the AST-tnode // layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec // ref/harec/src/types.c type_promote. Trailing `return ltn` covers // mismatched typed pairs; cstage flags the same shape — wwstage's // checker stays silent here per existing discipline. // // Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn // and rtn can be nil when an operand was an inherent-IDENT bail // (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo- // builtin callee with sym.decl == nil; #19 retires these as // dedicated AST kinds). Propagation, not silent gap — asserttyped // gates those idents at the consumer layer. fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn); let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn); if (lu && ru) { if (isuntypedfloat(ltn) || isuntypedfloat(rtn)) { return mktname(c, "untyped_float"); }; return mktname(c, "untyped_int"); }; let conf: bool = false; if (lu) { if (isassignable(c, rtn, ltn, &conf)) { return rtn; }; }; if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; if (typeeqast(ltn, rtn)) { return ltn; }; // Mismatched typed pair — return ltn so the binop stamps something; // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when // the helper was split out of binoptype. return ltn; }; // coercefloatlit — twin of cstage cmd/wcc/check.c coerce_floatlit (see there // for the full rationale + the rule-10 scope note). Stamp an un-suffixed // float literal (whose type_ is the untyped_float singleton) as f32 when the // target type resolves to f32, so fold-1's cgen narrow (isf32type, cgenexpr. // ww) fires off the now-f32 node.type_. SCOPED to a DIRECT untyped_float // N_FLOATLIT at let-init / return only: the wwstage cgen's exprfloatkind // (cgenutil.ww) hardcodes N_FLOATLIT -> f64 and cgbin / the unary negate pick // f32 off the operands' float-kind, not the node stamp, so a stamped literal // inside an arith-binop / behind a unary minus does NOT narrow there — // binop / unary-minus / assign / call-arg / struct-field wait on #120. fn coercefloatlit(c: *checker, e: *node, target: *node) void = { if (e == nil) { return; }; if (target == nil) { return; }; let tu: *node = resolvealias(c, unwrapbang(target)); if (tu == nil) { return; }; if (tu.kind != nkind.N_TNAME) { return; }; if (!streq(tu.str, "f32")) { return; }; if (e.kind == nkind.N_FLOATLIT) { if ((e.type_: *tinfo) == c.tc.tyuntypedfloat) { let f32t: *node = mktname(c, "f32"); e.type_ = tinfofornode(c, f32t): *void; }; }; }; // binoptype — derive the result tnode of an N_BIN operator expression. // Mirrors cstage cmd/wcc/check.c:598-640 `cbinop`. Operates on tnodes // returned by exprtype; ptr arithmetic / bitwise / shifts / comparisons / // logicals all reflect cstage's rules. Type-mismatch diagnostics are // elided here (cstage gates the same shape). fn binoptype(c: *checker, e: *node) *node = { let op: tkind = e.op; let ltn: *node = exprtype(c, e.lhs, nil); let rtn: *node = exprtype(c, e.rhs, nil); if (op == tkind.TK_PLUS || op == tkind.TK_MINUS) { if (ltn != nil && ltn.kind == nkind.N_TPTR && isinttypeast(rtn)) { return ltn; }; }; if (op == tkind.TK_PLUS) { if (isinttypeast(ltn) && rtn != nil && rtn.kind == nkind.N_TPTR) { return rtn; }; }; if (op == tkind.TK_MINUS) { if (ltn != nil && rtn != nil && ltn.kind == nkind.N_TPTR && rtn.kind == nkind.N_TPTR) { return mktname(c, "i64"); }; }; if (op == tkind.TK_PLUS || op == tkind.TK_MINUS || op == tkind.TK_STAR || op == tkind.TK_SLASH || op == tkind.TK_PERCENT || op == tkind.TK_AMP || op == tkind.TK_PIPE || op == tkind.TK_CARET || op == tkind.TK_LSHIFT || op == tkind.TK_RSHIFT) { return unifyarith(c, ltn, rtn); }; if (op == tkind.TK_EQ || op == tkind.TK_NEQ || op == tkind.TK_LT || op == tkind.TK_LE || op == tkind.TK_GT || op == tkind.TK_GE || op == tkind.TK_AND || op == tkind.TK_OR) { return mktname(c, "bool"); }; // Unreachable for valid input: op is one of TK_PLUS/MINUS/STAR/ // SLASH/PERCENT/AMP/PIPE/CARET/LSHIFT/RSHIFT/EQ/NEQ/LT/LE/GT/GE/ // AND/OR per parser invariant (lib/ww/parse/expr.ww binary-op // table); all are handled above. 5-lite-b #34. return nil; }; // unoptype — derive the result tnode of an N_UN unary expression. Mirrors // cstage cmd/wcc/check.c:642-687 `cunop` and harec ref/harec/src/types.c // type_promote. The slice/str .len/.cap pseudo-field address-of widening // to *i64 mirrors check.c:672-682 directly — its purpose is documented // at the cstage site. fn unoptype(c: *checker, e: *node) *node = { let op: tkind = e.op; let opt: *node = exprtype(c, e.lhs, nil); if (op == tkind.TK_MINUS || op == tkind.TK_PLUS) { return opt; }; if (op == tkind.TK_NOT) { return mktname(c, "bool"); }; if (op == tkind.TK_TILDE) { return opt; }; if (op == tkind.TK_STAR) { // opt nil here means the operand was an inherent-IDENT bail // (exprtype N_IDENT arm L1596-1599 — SK_USE / pseudo-builtin // sym.decl == nil — or another helper's nil propagation); // 5-lite-b #34, A.6.2.1c #24. The `u == nil` post-resolvealias // check was eliminated here — unwrapbang(non-nil) returns // non-nil (parser invariant N_TBANG.lhs always set) and // resolvealias passes through non-nil unchanged (L513 // `for (cur != nil)` only exits via `return cur` or `return n`). if (opt == nil) { return nil; }; let u: *node = resolvealias(c, unwrapbang(opt)); // Invalid input (non-pointer dereference); cstage errors at // cmd/wcc/check.c:660. 5-lite-b #34. if (u.kind != nkind.N_TPTR) { return nil; }; return u.lhs; }; if (op == tkind.TK_AMP) { if (e.lhs != nil && e.lhs.kind == nkind.N_DOT) { let fld: str = e.lhs.str; if (streq(fld, "len") || streq(fld, "cap")) { let base: *node = e.lhs.lhs; if (base != nil && base.type_ != nil) { let bu: *tinfo = base.type_: *tinfo; if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; }; if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu != nil) { if (bu.kind == tykind.TY_SLICE || bu.kind == tykind.TY_STR) { let pp: *node = newnode(nkind.N_TPTR, "", 0, 0); pp.lhs = mktname(c, "i64"); return pp; }; }; }; }; }; // opt nil → propagation from inherent-IDENT bail (5-lite-b // #34). Generic &expr widens to *opt; without opt we can't // synthesize the pointer node. if (opt == nil) { return nil; }; let pp: *node = newnode(nkind.N_TPTR, "", 0, 0); pp.lhs = opt; return pp; }; // Unreachable for valid input: op is one of TK_MINUS/PLUS/NOT/ // TILDE/STAR/AMP per parser invariant (lib/ww/parse/expr.ww unary // op set); all are handled above. 5-lite-b #34. return nil; }; // indexresult — derive the result tnode of an N_INDEX expression. // Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c // type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays // to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the // generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D // array of slices, not of T); generic *T → T. fn indexresult(c: *checker, e: *node) *node = { let basetn: *node = exprtype(c, e.lhs, nil); let _idx: *node = exprtype(c, e.rhs, nil); let u: *node = resolvealias(c, unwrapbang(basetn)); // basetn nil → propagation from inherent-IDENT bail at exprtype // N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24). // unwrapbang(nil)=nil and resolvealias(nil)=nil pass through. if (u == nil) { return nil; }; if (u.kind == nkind.N_TSLICE) { return u.lhs; }; if (u.kind == nkind.N_TARRAY) { return u.lhs; }; if (u.kind == nkind.N_TNAME) { if (streq(u.str, "str")) { return mktname(c, "u8"); }; }; if (u.kind == nkind.N_TPTR) { let inner: *node = u.lhs; let iu: *node = resolvealias(c, unwrapbang(inner)); if (iu != nil && iu.kind == nkind.N_TARRAY) { return iu.lhs; }; return inner; }; // Invalid input (non-indexable base — cstage errors at // cmd/wcc/check.c:893). 5-lite-b #34. return nil; }; // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, casts, binary/unary // ops, indexing, module-qualified refs + enum-member folds; returns // nil for shapes we don't statically know (struct field access into // non-primitive types, etc). // `hint`: optional declared-type AST passed by the caller (let // target, assign target). nil = "no hint, derive from self". Threaded // for use by A.6.1's STRUCTLIT/ARRLIT arms which can't self-type and // need the enclosing declared type to resolve. Ignored by every arm // in A.6.0; the param is plumbed here so the per-kind work that // follows doesn't ripple a fresh signature change. Mirrors harec's // `check_expression(..., result_type, ...)` per // `feedback_hare_frontend_reference.md`. // // Dispatcher invariant (5-lite-a #33): every value-producing nkind // listed in resolvewalk's post-order dispatch (L474-489) reaches a // stamping arm here that sets e.type_ before returning. No // fall-through. Arms that return nil (binoptype trailing, unoptype // TK_STAR opt-nil, indexresult u-nil, N_DOT outer fold-miss, N_SLICE // non-sliceable base, etc.) are propagation from a callee's nil — // not silent gaps. Mirror of harec's // `assert(expr->result)` at ref/harec/src/check.c:3810. The // asserttyped pass at L2871 enforces the invariant on every // dispatched node post-checker, with gates for the residual // inherent-IDENT bails (SK_USE, pseudo-builtin sym.decl==nil, // N_DOT-LHS syntactic position, EXPR_ASSERT-family abort/assert) until // #19 retires the bail shape. fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (e == nil) { return nil; }; let k: nkind = e.kind; // #61 audit §1.8 — A.2 widens A.1's single N_INTLIT population to // every primitive literal arm + N_IDENT. Cgen size walkers // (slotsize first; elemsize/fieldsize/letemitsize follow) consult // node.type_ as the SSoT; populating literals + idents closes the // loop from the read side. if (k == nkind.N_INTLIT) { // Typed-int literal (`7u32`, `0i8`): tsuffix names a builtin // primitive. Mirrors cstage cmd/wcc/check.c:694-701 cexpr's // `lookup_builtin(n->tsuffix)`; falls through to untyped_int // when the suffix doesn't resolve. if (e.tsuffix.len > 0) { let suf: *node = mktname(c, e.tsuffix); let ti: *tinfo = tinfofornode(c, suf); if (ti != nil) { e.type_ = ti: *void; return suf; }; }; let tn: *node = mktname(c, "untyped_int"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_FLOATLIT) { // Typed-float literal (`1.5f32`, `0.0f64`): tsuffix names a // builtin primitive. Mirrors cstage cmd/wcc/check.c:702-709 // cexpr's `lookup_builtin(n->tsuffix)`; falls through to // untyped_float when the suffix doesn't resolve. if (e.tsuffix.len > 0) { let suf: *node = mktname(c, e.tsuffix); let ti: *tinfo = tinfofornode(c, suf); if (ti != nil) { e.type_ = ti: *void; return suf; }; }; let tn: *node = mktname(c, "untyped_float"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_STRLIT) { let tn: *node = mktname(c, "str"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_RUNELIT) { let tn: *node = mktname(c, "rune"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_TRUE) { let tn: *node = mktname(c, "bool"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_FALSE) { let tn: *node = mktname(c, "bool"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_VOIDLIT) { let tn: *node = mktname(c, "void"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_NIL) { let tn: *node = mktname(c, "untyped_nil"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_IDENT) { let s: *sym = scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; let t: *node = s.decl.lhs; // Propagate the declared type's tinfo onto the use site so // downstream cgen walkers can read n.type_ off an ident. if (t != nil) { if (t.type_ != nil) { e.type_ = t.type_; } else { let ti: *tinfo = tinfofornode(c, t); if (ti != nil) { e.type_ = ti: *void; t.type_ = ti: *void; }; }; }; return t; }; if (k == nkind.N_BIN) { let tn: *node = binoptype(c, e); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_UN) { let tn: *node = unoptype(c, e); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_INDEX) { let tn: *node = indexresult(c, e); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (k == nkind.N_CAST) { // `expr: T` — explicit cast; the type expr is e.rhs. Mirrors // cstage cmd/wcc/check.c:737 `n->type = resolve_type(c, n->rhs)`. e.type_ = tinfofornode(c, e.rhs): *void; return e.rhs; }; if (k == nkind.N_CALL) { let callee: *node = e.lhs; if (callee == nil) { return nil; }; // #31: synthesize the `alloc(value)` / `alloc([], n)` builtin // return shape so checkletassign sees the same `(*T | nomem)` / // `([]T | nomem)` cstage's check.c stamps at L981-1006. Without // this, exprtype returns the seeded decl's nil lhs and the let // silently accepts `let p: *T = alloc(v);` — rule 10 trap. // Same-module gate mirrors cstage's `c->cur_mod && // scope_lookup_in_module(...)` check from task #23. if (callee.kind == nkind.N_IDENT) { if (streq(callee.str, "alloc")) { let shadowed: bool = false; if (c.curmod.len > 0) { if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { shadowed = true; }; }; if (!shadowed) { if (e.list != nil) { // Slice form: `alloc([], n)`. if (e.list.kind == nkind.N_ARRLIT) { if (e.list.list == nil) { if (e.list.next != nil) { if (e.list.next.next == nil) { let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = mktname(c, "u8"); let nome: *node = mktname(c, "nomem"); sl.next = nome; let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0); tt.list = sl; e.type_ = tinfofornode(c, tt): *void; return tt; }; }; }; }; // Value form: `alloc(value)`. if (e.list.next == nil) { let argt: *node = exprtype(c, e.list, nil); let ptr: *node = newnode(nkind.N_TPTR, "", 0, 0); ptr.lhs = argt; let nome: *node = mktname(c, "nomem"); ptr.next = nome; let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0); tt.list = ptr; e.type_ = tinfofornode(c, tt): *void; return tt; }; }; }; }; }; // #42: size(T) / align(T) / offset(e.f) typed-builtin intercepts. // Fold the N_CALL in place to an N_INTLIT so cgen never sees an // unresolved size/align/offset symbol. Same-module shadow gate // mirrors the alloc precedent (#23) so a user `fn size(...)` // inside this module suppresses the builtin. Mirrors cstage // cmd/wcc/check.c:907-960. if (callee.kind == nkind.N_IDENT) { let bname: str = callee.str; let issize: bool = streq(bname, "size"); let isalign: bool = streq(bname, "align"); let isoffset: bool = streq(bname, "offset"); if (issize || isalign || isoffset) { let shadowed: bool = false; if (c.curmod.len > 0) { if (scopelookupinmodule(c.cur, c.curmod, bname) != nil) { shadowed = true; }; }; if (!shadowed) { if (e.list != nil) { // Post-fold the node IS an N_INTLIT-shaped // untyped_int constant. Mirrors cstage // cmd/wcc/check.c:926/958 which stamps // ty_untyped_int after the fold. The return // tnode mktname("i32") is the assignability // target for callers, not the constant's // own type. let utn: *node = mktname(c, "untyped_int"); if (issize) { // #108(b): rule-10 twin of the cstage // size/align unsized guard. if (astunsized(c, e.list)) { deffolderr(c, e, "cannot take size of unsized type 'opaque'"); }; let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; if (isalign) { if (astunsized(c, e.list)) { deffolderr(c, e, "cannot take align of unsized type 'opaque'"); }; let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; // offset(e.f): the arg is a value expression // (N_DOT), parsed via parsearglist — not a // type expression. if (isoffset) { if (e.list.next == nil && e.list.kind == nkind.N_DOT) { let off: i64 = astoffset(c, e.list); if (off < 0i64) { os.write(2, "offset: no field '".ptr, 18u64); os.write(2, e.list.str.ptr, e.list.str.len: u64); os.write(2, "'\n".ptr, 2u64); c.errs += 1; off = 0i64; }; foldtointlit(c, e, off); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; }; }; }; }; }; // len(x) / append(s,...) / free(p) — Hare pseudo-builtins. // Mirror cstage cmd/wcc/check.c:896-1011 (rule 10 requires // stage byte-id; both stages stamp the same shape). No shadow // guard: cstage's len/append/free intercepts have none either // (check.c:901/962/1005), and the names are seeded into c.top // at L85-88 so a user same-module decl dup-silences. Harec // models these as dedicated AST kinds — EXPR_LEN at // ref/harec/src/check.c:2630 (result `&builtin_type_size`), // EXPR_APPEND at :745 (result `(nomem | void)`), EXPR_FREE at // :2443 (result `&builtin_type_void`). The cstage divergence // (len → i32 not size, append → void not tagged) pre-dates // this task; #19 (Drew's δ — dedicated AST kinds) is the // Hare-faithful path. This is the intercept-shim minimum to // unblock #15 (A.6.2.1e assertion enable). if (callee.kind == nkind.N_IDENT) { if (streq(callee.str, "len")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (streq(callee.str, "append") || streq(callee.str, "free")) { let tn: *node = mktname(c, "void"); e.type_ = tinfofornode(c, tn): *void; return tn; }; }; 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; }; // #56: bare-leaf N_IDENT calls go through scopelookupprefer so // `foo()` inside module M binds to M.foo rather than another // module's same-leaf foo at the head of the flat scope bucket. // Mirrors cstage cexpr N_IDENT routing through // scope_lookup_prefer with c->cur_mod. // // #6a-A: a module-qualified `mod.fn()` callee resolves via the // callee.lhs module hint when `mod` is an import (SK_USE) — // scopelookupinmodule(mod, leaf) — mirroring cstage cexpr N_DOT // (cmd/wcc/check.c:1035 scope_lookup_in_module) and cgen's // rettupleof (cgen.ww:2263 fnretlookupmod). Closing this at the // N_CALL root (vs the N_MLET backfill) makes every consumer of a // module-qual call result — destructure binding AND a bare // `mod.fn().0` rvalue — read the right return type via the one // expr path, harec-faithful (binding-unpack does zero callee // resolution, ref/harec/src/check.c:1354-1419). Without it the // bare-leaf scopelookup grabbed whichever same-leaf fn heads the // flat scope — wrong on a cross-module shadow (753_convwrap_audit: // alpha.foo (i64,str) vs beta.foo (i64,i64)). // // #6a-D: a D-class callee whose `mod` leaf is itself a type/fn // (SK_TYPE/SK_FN — random.random / fnmatch.fnmatch collision) // resolves through scopelookupprefer to that same-leaf entry, not // the coexisting SK_USE, when curmod matches the colliding decl's // package — so the SK_USE gate below misses and the call stays // nil-stamped. scopelookupuselocal re-resolves `mod` to the SK_USE // that coexists in the same scope (coexistence-equivalent of // cstage's use_alias; see lib/ww/sym.ww + memory // module_type_name_collision). let s: *sym = nil; if (callee.kind == nkind.N_IDENT) { s = scopelookupprefer(c.cur, c.curmod, nm); } else { let ms: *sym = nil; if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) { ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str); if (ms != nil && ms.skind != skind.SK_USE) { let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str); if (mu != nil) { ms = mu; }; }; }; if (ms != nil && ms.skind == skind.SK_USE) { s = scopelookupinmodule(c.cur, callee.lhs.str, nm); } else { s = scopelookup(c.cur, nm); }; }; if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) { // fn-decl's lhs is the return-type AST node. Mirrors cstage // cmd/wcc/check.c:984+ regular-CALL `n->type = // build_fn_type(c, s->decl)->ret` shape. e.type_ = tinfofornode(c, s.decl.lhs): *void; return s.decl.lhs; }; }; }; // A callee that is a fn-VALUE — a fn-pointer struct field // (`w.emit(...)`), local, or param — has no free SK_FN entry, so // the name lookup above misses. Read the result off the checked // callee node's own type instead: autodereference + dealias to // the TY_FN, then take its result. Mirrors harec's check_expr_call // `expr->result = type_dealias(check_autodereference(lvalue-> // result))->func.result` (ref/harec/src/check.c:1566-1581). The // name path stays primary because a fn-NAME callee node in wwstage // already carries its RETURN type (fn-decl.lhs), not its fn-type — // so a `fn make() fn() void` callee would otherwise mis-yield void. let ct: *node = resolvealias(c, unwrapbang(exprtype(c, callee, nil))); for (ct != nil && ct.kind == nkind.N_TPTR) { ct = resolvealias(c, unwrapbang(ct.lhs)); }; if (ct != nil) { if (ct.kind == nkind.N_TFN) { let res: *node = ct.lhs; e.type_ = tinfofornode(c, res): *void; return res; }; }; return nil; }; if (k == nkind.N_DOT) { // A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c // :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands // in A.6.1.5b. SK_USE gates case 1; a #6a-D dot-lhs collision // (random.random / fnmatch.fnmatch — the module leaf is also a // same-scope type/fn) re-resolves through scopelookupuselocal so // the SK_USE coexisting alongside the type/fn wins (the // coexistence-equivalent of cstage's use_alias; see installdecl // docstring + lib/ww/sym.ww). Enum-member fold delegates // non-literal lhs shapes (sibling backref, unary, binary, shift) // to enumvalfold, matching cstage cmd/wcc/check.c:210-284 and // harec's enum-resolve constexpr set at // ref/harec/src/check.c:4419-4434. let lhsn: *node = e.lhs; if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) { let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str); if (ms != nil && ms.skind != skind.SK_USE) { let mu: *sym = scopelookupuselocal(ms.scope, lhsn.str); if (mu != nil) { ms = mu; }; }; if (ms != nil) { // Fold case 1: module-qualified ref. Mirror cstage // check.c:749-775. cstage returns ty_err on SK_USE // with missing leaf (extern decl); wwstage falls // through to outer case — cgen has its own module- // qualified resolution and the lenient checker policy // keeps the silent miss documented at scruttype L656. if (ms.skind == skind.SK_USE) { let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); if (fs != nil) { if (fs.decl != nil) { let tn: *node = fs.decl.lhs; if (tn != nil) { e.type_ = tinfofornode(c, tn): *void; return tn; }; }; }; }; // Fold case 2 inner: bare `EnumT.MEMBER` where EnumT // is an SK_TYPE in the flat scope. Mirror cstage // check.c:780-803. if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { let body: *node = ms.decl.lhs; let ub: *node = resolvealias(c, unwrapbang(body)); if (ub != nil) { if (ub.kind == nkind.N_TENUM) { let prev: u64 = (-1i64): u64; let m: *node = ub.list; for (m != nil) { let val: u64 = 0u64; if (m.lhs == nil) { val = prev + 1u64; } else { if (!enumvalfold(ub, m, m.lhs, &val)) { return nil; }; }; prev = val; if (streq(m.str, e.str)) { foldtointlit(c, e, val: i64); e.type_ = tinfofornode(c, body): *void; return body; }; m = m.next; }; }; }; }; }; }; }; }; // Fold case 2 outer: base resolves to enum, e.g. // `pkg.EnumT.MEMBER` where the inner N_DOT (pkg.EnumT) folded // via case 1 above to the enum body. Mirror cstage // check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare // but cstage handles it at L808). let basetn: *node = exprtype(c, lhsn, nil); if (basetn != nil) { let bu: *node = resolvealias(c, unwrapbang(basetn)); if (bu != nil) { if (bu.kind == nkind.N_TPTR) { bu = resolvealias(c, unwrapbang(bu.lhs)); }; }; if (bu != nil) { if (bu.kind == nkind.N_TENUM) { let prev: u64 = (-1i64): u64; let m: *node = bu.list; for (m != nil) { let val: u64 = 0u64; if (m.lhs == nil) { val = prev + 1u64; } else { if (!enumvalfold(bu, m, m.lhs, &val)) { return nil; }; }; prev = val; if (streq(m.str, e.str)) { foldtointlit(c, e, val: i64); e.type_ = tinfofornode(c, basetn): *void; return basetn; }; m = m.next; }; }; }; // A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure // type-AST stamps; never rewrite e.kind. Lenient on misses // (cstage errors); falls through to nil under scruttype L656. // // Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage // L833-842. `str` lives as N_TNAME("str") in wwstage — no // dedicated N_TSTR kind — so test the trio shape here. if (bu != nil) { let isstr: bool = (bu.kind == nkind.N_TNAME) && streq(bu.str, "str"); if (bu.kind == nkind.N_TSLICE || bu.kind == nkind.N_TARRAY || isstr) { if (streq(e.str, "len")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (streq(e.str, "cap")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; return tn; }; if (streq(e.str, "ptr")) { let elem: *node = bu.lhs; if (isstr) { elem = mktname(c, "u8"); }; let pp: *node = newnode(nkind.N_TPTR, "", 0, 0); pp.lhs = elem; e.type_ = tinfofornode(c, pp): *void; return pp; }; }; }; // Struct field walk. Cstage L843-849 errors on missing field. if (bu != nil) { if (bu.kind == nkind.N_TSTRUCT) { let f: *node = bu.list; for (f != nil) { if (f.kind == nkind.N_TFIELD) { if (streq(f.str, e.str)) { e.type_ = tinfofornode(c, f.lhs): *void; return f.lhs; }; }; f = f.next; }; }; }; // Tuple positional access `t.0`, `t.1`, …. Cstage L850-866 // errors on non-numeric / out-of-range; wwstage falls // through. fldnumidx (cgenutil) returns -1 on non-digit. if (bu != nil) { if (bu.kind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(e.str); if (idx >= 0) { let p: *node = bu.list; for (idx > 0 && p != nil) { p = p.next; idx -= 1; }; if (p != nil) { let pt: *node = p.lhs; e.type_ = tinfofornode(c, pt): *void; return pt; }; }; }; }; }; return nil; }; if (k == nkind.N_STRUCTLIT) { // A.6.1.6 — head-only stamp of the struct-lit's overall type. // Mirror cstage cmd/wcc/check.c:1161-1197; field-level walk // (cstage L1178-1194) parked behind #23 / Phase 2 — field // values are walked by the post-order exprtype dispatch at // L460-489, so each field expr still gets its own n.type_. // // Parser at lib/ww/parse/expr.ww:147-148 always plants the // TYPE_IDENT in e.lhs; e.lhs == nil would be a future Hare- // style anonymous struct lit we don't yet parse — bail. if (e.lhs == nil) { return nil; }; if (e.lhs.kind == nkind.N_IDENT) { let ms: *sym = scopelookupprefer(c.cur, c.curmod, e.lhs.str); if (ms != nil) { if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { let tn: *node = ms.decl.lhs; if (tn != nil) { // #66 Phase-N step 3: stamp e.type_ to the NOMINAL // per-decl NAMED, not the flattened body. `overflow{}` // where `type overflow = !void` must carry // NAMED(overflow) so the typeeq variant match // (cgenutil flatvariantidx) selects the overflow arm // instead of falling to the scalar shape fallback; // stamping tinfofornode(tn) gave the body (TY_VOID) // and lost nominal identity. Resolve through a // synthesized TNAME to reuse tinfofornode's TY_NAMED // build/cache (check.ww:1157) — the SAME NAMED ptr // the union variant resolved to. Mirrors cstage // resolving overflow{} to the overflow Type, and ww's // own N_CAST / N_IDENT arms which already stamp NAMED. // Return the body node tn unchanged: byte-id rides // e.type_ (cgen), while the checker's AST-level // assign/return checks keep their prior input. let tnm: *node = mktname(c, e.lhs.str); let nti: *tinfo = tinfofornode(c, tnm); if (nti != nil) { e.type_ = nti: *void; } else { e.type_ = tinfofornode(c, tn): *void; }; return tn; }; }; }; }; // Lenient on miss: cstage L1170 errors, wwstage falls // through (scruttype L656 / A.6.1.5b N_DOT struct-miss). return nil; }; // Synthetic type-expr (`(*T){...}` etc). Mirror cstage L1175 // resolve_type(c, n->lhs). e.type_ = tinfofornode(c, e.lhs): *void; return e.lhs; }; if (k == nkind.N_ARRLIT) { // A.6.1.7 — head-only stamp of the array-lit's overall type. // Mirror cstage cmd/wcc/check.c:1198-1211: walk elements, // skip the `...` repeat sentinel (parse/expr.ww:101-105), // first-element-wins for the elem type, count non-skipped // elements, synthesize an N_TARRAY{elt, INTLIT count}. Empty // list defaults to `[0]i32` per cstage L1209. Per-element // stamps still fire via the post-order dispatch at L460-489 // (N_ARRLIT is in the kind list since A.6.0); the re-walk in // the loop below is tinfocache-idempotent (L467). // // Documented cstage divergence: cstage L1206 applies // type_default to lift untyped_int → i32 etc; wwstage stamps // the raw exprtype result, matching the alloc-value-form // precedent at L1509. Consumers default-type via the declared // `let` slot until A.6.3 lands. Mixed-type elements follow // cstage first-element-wins; no unify check (future scope). let elt: *node = nil; let count: u64 = 0u64; let it: *node = e.list; for (it != nil) { let skip: bool = false; if (it.kind == nkind.N_FIELD) { if (streq(it.str, "...")) { skip = true; }; }; if (!skip) { let t: *node = exprtype(c, it, nil); if (elt == nil) { elt = t; }; count += 1u64; }; it = it.next; }; if (elt == nil) { elt = mktname(c, "i32"); }; let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); arr.lhs = elt; let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); cn.uval = count; arr.rhs = cn; e.type_ = tinfofornode(c, arr): *void; return arr; }; if (k == nkind.N_SLICE) { // A.6.2.0a — head-only stamp of the slice expression's overall // type. Mirrors cstage cmd/wcc/check.c:1214-1228 N_SLICE: peel // alias on the base; [N]T → []T, []T → []T (return base), str // → str, *T (non-nil sub) → []T. Slice bounds (e.rhs start, // e.cond end) are already covered by the post-order dispatch // at L460-489 (typically N_INTLIT/N_IDENT/N_BIN, all in the // dispatch list), so we do not double-walk them here. // Documented cstage divergence: cstage L1227 errors on a // non-sliceable base; wwstage returns nil (lenient on miss), // matching scruttype L656 / A.6.1.5b N_DOT precedent. let basetn: *node = exprtype(c, e.lhs, nil); let bu: *node = resolvealias(c, unwrapbang(basetn)); if (bu == nil) { return nil; }; if (bu.kind == nkind.N_TARRAY) { let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = bu.lhs; e.type_ = tinfofornode(c, sl): *void; return sl; }; if (bu.kind == nkind.N_TSLICE) { e.type_ = tinfofornode(c, basetn): *void; return basetn; }; if (bu.kind == nkind.N_TNAME) { if (streq(bu.str, "str")) { let tn: *node = mktname(c, "str"); e.type_ = tinfofornode(c, tn): *void; return tn; }; }; if (bu.kind == nkind.N_TPTR && bu.lhs != nil) { let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = bu.lhs; e.type_ = tinfofornode(c, sl): *void; return sl; }; return nil; }; if (k == nkind.N_TUPLE) { // A.6.2.0b — head-only stamp of the tuple expression's // overall type. Mirrors cstage cmd/wcc/check.c:1437-1451 // N_TUPLE: walk e.list, type each element via exprtype, and // assemble an N_TTUPLE whose .list chains N_TPARAM wrappers // (one per element) so shared element-type ASTs (sym.decl.lhs, // another tuple's element, struct field's .lhs) keep their // own .next untouched — see lib/ww/ast.ww:101 and the // A.6.2.0b-pre parser precedent at lib/ww/parse/parse.ww:302. // Per-element exprtype recursion is tinfocache-idempotent // (resolvewalk L460-489 already dispatches into N_TUPLE // children). Lenient on empty list: grammar requires >= 2 // elements (lib/ww/parse/expr.ww:117 single-elem returns the // expression), so empty is unreachable and yields nil here // (matches scruttype L656 / A.6.1.5b N_DOT lenient-on-miss). if (e.list == nil) { return nil; }; let head: *node = nil; let tail: *node = nil; let it: *node = e.list; for (it != nil) { let elemt: *node = exprtype(c, it, nil); let w: *node = newnode(nkind.N_TPARAM, "", 0, 0); w.lhs = elemt; if (head == nil) { head = w; } else { tail.next = w; }; tail = w; it = it.next; }; let tt: *node = newnode(nkind.N_TTUPLE, "", 0, 0); tt.list = head; e.type_ = tinfofornode(c, tt): *void; return tt; }; if (k == nkind.N_RECV) { // A.6.2.0d — head-only stamp of the receive expression's // overall type. Mirrors cstage cmd/wcc/check.c:1230-1236 // N_RECV: peel alias on the channel base; chan T → T. // Documented cstage divergence: cstage L1234 errors on a // non-chan base; wwstage returns nil (lenient on miss), // matching scruttype L656 / A.6.1.5b N_DOT precedent. let basetn: *node = exprtype(c, e.lhs, nil); let bu: *node = resolvealias(c, unwrapbang(basetn)); if (bu == nil) { return nil; }; if (bu.kind == nkind.N_TCHAN) { e.type_ = tinfofornode(c, bu.lhs): *void; return bu.lhs; }; return nil; }; if (k == nkind.N_SPREAD) { // A.6.2.0e — pass-through stamp. Mirrors cstage check.c:1212-1213. // The spread expression `xs...` carries the operand's type. let t: *node = exprtype(c, e.lhs, nil); if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; return t; }; if (k == nkind.N_MATCH) { // A.6.2.0g — port of cstage cmd/wcc/check.c:1316-1330 match-as- // expression stamp. The match's type is the first arm's yield // operand type; void if no arm yields. Wwstage skips cstage's // arm-yield-unification check (L1322-1327) — that's a checker // concern, this arm only stamps. Closes the consumer half of // the match-as-expression contract that A.6.2.0f opened on the // producer side (N_YIELD). let yt: *node = nil; let cs: *node = e.list; for (cs != nil) { let t: *node = matchyieldtype(c, cs.body); if (t != nil) { yt = t; break; }; cs = cs.next; }; if (yt == nil) { yt = mktname(c, "void"); }; e.type_ = tinfofornode(c, yt): *void; return yt; }; if (k == nkind.N_YIELD) { // A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT // stamp N_YIELD (statement-shaped). Wwstage's A.6.2 invariant // requires every post-dispatch kind have type_ set. Yield's // value type is the operand's type per Hare's unified stmt/expr // AST (ref/hare/hare/ast/expr.ha:449-461 — yield_expr is an // expression with a type). Bare `yield;` (no operand) stamps // void. if (e.lhs == nil) { let v: *node = mktname(c, "void"); e.type_ = tinfofornode(c, v): *void; return v; }; let t: *node = exprtype(c, e.lhs, nil); if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; return t; }; if (k == nkind.N_TRYPROP) { // success unwrap: the success-variant type of operand's // tagged union. let opt: *node = exprtype(c, e.lhs, nil); 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)) { e.type_ = tinfofornode(c, v): *void; return v; }; v = v.next; }; return nil; }; e.type_ = tinfofornode(c, ou.list): *void; return ou.list; }; if (k == nkind.N_TRYUNW) { // `e!` abort-on-error unwrap; success variant is what the // receiver gets, identical to `?` shape modulo control flow. // #31: required so `let p: *T = alloc(v)!;` resolves to *T. let opt: *node = exprtype(c, e.lhs, nil); let ou: *node = resolvealias(c, unwrapbang(opt)); if (ou == nil) { return nil; }; if (ou.kind != nkind.N_TTAGGED) { return nil; }; if (taggedhaserr(c, ou)) { let v: *node = ou.list; for (v != nil) { if (!iserrvariant(c, ou, v)) { e.type_ = tinfofornode(c, v): *void; return v; }; v = v.next; }; return nil; }; e.type_ = tinfofornode(c, ou.list): *void; return ou.list; }; if (k == nkind.N_TYPEASSERT) { // `e as T` → T. Mirrors cstage cmd/wcc/check.c TYPEASSERT // `n->type = resolve_type(c, n->rhs)`. e.type_ = tinfofornode(c, e.rhs): *void; return e.rhs; }; if (k == nkind.N_TYPETEST) { // `e is T` → bool let tn: *node = mktname(c, "bool"); e.type_ = tinfofornode(c, tn): *void; return tn; }; 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"); }; // isinttypeast — int-typed AST node. Either a primitive int name // (i8..i64/u8..u64/int/uint/uintptr/rune) or an N_TENUM. Floats are // excluded so the enum↔int reinterpret in checkisas (#52) refuses a // surprise `enum as f64` shape. Mirrors cstage's type_isint // (cmd/wcc/type.c) restricted to the kinds reachable from AST. fn isinttypeast(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TENUM) { return true; }; 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, "size")) { return true; }; if (streq(s, "rune")) { return true; }; return false; }; 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, "size")) { 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. // Recursive isassignable mirrors cstage type_assignable // (cmd/wcc/type.c:298-299) and harec tagged_select_subtype's // recursive type_is_assignable call (ref/harec/src/types.c:702-739, // :718; invoked from the TAGGED arm at :1110-1112). #39 cascade: // the prior typeeqast-only walk rejected widenings that aren't // strict surface-eq (NAMED-aliased variants, nested tagged inside // a variant, concrete → variant after the wrap-induced exprtype // reshape). #55 surface-nominal fast path is preserved by the // recursive call's leading typeeqast (line 2257). #57 bare-vs- // qualified TNAME residual unchanged. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { 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; }; // tagged → non-tagged: requires `?` / `!` / match to project a // variant. #31: this is what traps `let p: *T = alloc(v);` // where the builtin returns `(*T | nomem)` and the LHS is bare. if (su.kind == nkind.N_TTAGGED && du.kind != nkind.N_TTAGGED) { return false; }; // 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.rhs == nil) { return; }; // no init // hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT // arms consume it. Plumbing-only at this point. let src: *node = exprtype(c, n.rhs, nil); // Inferred binding (`let r = expr;`, no type annotation). Mirror // cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt) t = // type_default(initt);` and ref/harec/src/check.c:1422 // check_expr_binding. wwstage carries the let's type on decl.lhs // (exprtype N_IDENT at L1546 reads s.decl.lhs); cstage carries it // on Sym.type — same observable result, rule-10 byte-id holds. // Defaulting (untyped_int → i32, etc.) is exprtype's job at use // sites, not the binding site. if (n.lhs == nil) { if (src != nil) { n.lhs = src; }; return; }; if (src == nil) { return; }; // can't infer // #45: alloc([], n) defers element type to the let-init context // (Hare-style). exprtype's alloc-slice branch synthesizes // ([]u8 | nomem) / []u8 (for the ?/! wrap) with no LHS context; // when the let declares []T, retype src to []T / ([]T | nomem) // so isassignable sees exact equality. cgenstmt cglet drives the // element size from n.lhs already (cmd/wcc/cgenstmt.ww), so this // stays symmetric with cstage check.c clet's parallel retype. if (n.lhs.kind == nkind.N_TSLICE) { let wrapped: bool = false; let inner: *node = n.rhs; if (inner.kind == nkind.N_TRYPROP) { wrapped = true; inner = inner.lhs; } else { if (inner.kind == nkind.N_TRYUNW) { wrapped = true; inner = inner.lhs; }; }; if (inner != nil && inner.kind == nkind.N_CALL) { let callee: *node = inner.lhs; let a0: *node = inner.list; let a1: *node = nil; let a2: *node = nil; if (a0 != nil) { a1 = a0.next; }; if (a1 != nil) { a2 = a1.next; }; if (callee != nil && callee.kind == nkind.N_IDENT && streq(callee.str, "alloc") && a0 != nil && a0.kind == nkind.N_ARRLIT && a0.list == nil && a1 != nil && a2 == nil) { let shadowed: bool = false; if (c.curmod.len > 0) { if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { shadowed = true; }; }; if (!shadowed) { let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = n.lhs.lhs; if (wrapped) { src = sl; } else { let nome: *node = mktname(c, "nomem"); sl.next = nome; let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0); tt.list = sl; src = tt; }; }; }; }; }; // #130: array-init accept-if-fits. When lhs is [N]T and rhs is an // array literal, per-element check: foldable int literal → // defcastfits range-check (reject out-of-range loud, rule-7/Drew — // Hare range-checks at literal-value level); non-foldable → // isassignable to the element type. This BOTH accepts in-range // bare-int (the #130 headline, matching cstage) AND closes the // wwstage over-accept where str→u8 / out-of-range silently passed // (#146 merged). Mirrors cstage check.c arrlit_init_fits. Scoped // to the array path; scalar-init range-check is a separate // language-wide gap (#148). if (n.lhs.kind == nkind.N_TARRAY) { if (n.rhs.kind == nkind.N_ARRLIT) { let elemtn: *node = n.lhs.lhs; let at: *tinfo = tinfofornode(c, n.lhs); let et: *tinfo = nil; if (at != nil) { et = at.sub; }; for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; let e: *node = n.rhs.list; for (e != nil) { let skip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { skip = true; }; }; if (!skip) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; let v: u64 = 0u64; let folded: bool = false; if (et != nil) { if (typeisint(et)) { if (ev != nil) { folded = foldintliteral(ev, &v); }; }; }; if (folded) { if (!defcastfits(et, v)) { let m: str = "let: array element out of range\n"; os.write(2, m.ptr, m.len: u64); c.errs += 1; return; }; } else { let est: *node = exprtype(c, ev, elemtn); let conf2: bool = false; if (est != nil) { if (!isassignable(c, elemtn, est, &conf2)) { if (conf2) { errnotassign(c, elemtn, est, "let"); return; }; }; }; }; }; e = e.next; }; return; }; }; 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, nil); 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; }; // #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`). // Mirrors cstage cmd/wcc/check.c:1346-1357 — N_TYPEASSERT with an // enum on either side and integer types on both reinterprets in // the same register, no tag check involved. Returns early before // the tagged-union gate so lib/time/instant.ww `(c as i32)` and // the lib/os syscall casts stop false-positiving. `is` (TYPETEST) // stays rejected on non-tagged operands — cstage cmd/wcc/check.c // gates the bypass on N_TYPEASSERT only. if (n.kind == nkind.N_TYPEASSERT) { let v: *node = resolvealias(c, unwrapbang(n.rhs)); let lhsenum: bool = false; let rhsenum: bool = false; if (u != nil) { if (u.kind == nkind.N_TENUM) { lhsenum = true; }; }; if (v != nil) { if (v.kind == nkind.N_TENUM) { rhsenum = true; }; }; if (lhsenum || rhsenum) { if (isinttypeast(u)) { if (isinttypeast(v)) { 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) { // Hare-style variadic `T...`: normalize p.lhs to []T so // downstream consumers (N_IDENT exprtype lookups via // s.decl.lhs, cgen's variadic-slot synthesis) see the // effective slice type. Mirrors cstage check.c:455 // `tp->type = type_slice(c->a, pt)` and harec // check_func_type. Surface-fidelity preserved: wwdump // -a runs parser only and never reaches this mutation. if (p.op == tkind.TK_ELLIPSIS) { if (p.lhs != nil && p.lhs.kind != nkind.N_TSLICE) { let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0); sl.lhs = p.lhs; p.lhs = sl; }; }; 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.cur); installparams(c, fnnode.list); // #61 audit §1.8 — A.2: walk each param's declared type-expr so // tinfofornode stamps n.type_ on it. installparams binds the name // but never recurses into the type; without this, cgen's slotsize // fast-path hits the fallback for every param load/store. let p: *node = fnnode.list; for (p != nil) { if (p.kind == nkind.N_PARAM) { if (p.lhs != nil) { resolvewalk(c, p.lhs); }; }; p = p.next; }; 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; }; // isassertfam — `abort`/`assert` are language builtins, not value // calls. harec models each as a dedicated EXPR_ASSERT whose result is // builtin void (assert) or never (bare abort) at // ref/harec/src/check.c:877,893; there is no callee ident, so nothing // is left untyped. wwstage parses them as an N_CALL over a bare // N_IDENT callee that binds to no decl, so both the call and its // callee carry no type by design. Recognized exactly as the cstage // builtin intercept (cmd/wcc/check.c:1314,1328): the reserved name // with no shadowing user symbol. fn isassertfam(c: *checker, id: *node) bool = { if (id == nil) { return false; }; if (id.kind != nkind.N_IDENT) { return false; }; if (!streq(id.str, "abort") && !streq(id.str, "assert")) { return false; }; return scopelookupprefer(c.cur, c.curmod, id.str) == nil; }; // asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the // file tree and fires for any node in resolvewalk's value-producing // dispatch set (L474-489) whose n.type_ remained nil. Mirror of harec's // `assert(expr->result)` at ref/harec/src/check.c:3810. The invariant // is ARMED: a non-exempt nil-typed value node writes its one-line // diagnostic to stderr and bails (os.exit 1), so a stamping regression // fails loud rather than shipping a partially-typed tree. The // 990_selfhost / 901 probes drive the wwstage checker over the resolved // units that exercise this gate. // // Gates (per Drew 2026-05-22 — "guards value-producing expression // nodes; SK_USE refs and bare builtin callees are syntactic positions, // gate them out with WHY pointing at #19"): // // 1. N_IDENT whose resolved sym kind is SK_USE — module references // (`os` in `os.write`). Harec models these via EXPR_ACCESS whose // lookup-target is an OBJ_USE directly; there is no intermediate // "ident-as-value" expr. Until #19 ports that AST shape, skip. // 2. N_IDENT whose resolved sym has decl == nil — pseudo-builtin // callees (len/append/free/alloc/size/align/offset, seeded in // checkinit L85-97 with decl=nil). Harec spells these as // dedicated EXPR_* kinds (EXPR_LEN, EXPR_APPEND, EXPR_FREE, // EXPR_ALLOC at ref/harec/src/check.c:2630/745/2443/...). // Drew's δ (#19) retires the seeded-SK_FN-with-nil-decl hack. // 3. N_IDENT at the LHS-of-N_DOT syntactic position — the bare // name half of a member-access expr is a lookup target, not a // value-producing sub-expression. Harec's EXPR_ACCESS stores the // member as a string, not a node. // 4. The EXPR_ASSERT family — an N_CALL whose callee is `abort` or // `assert`, and the bare N_IDENT callee itself (see isassertfam). // harec's EXPR_ASSERT carries a void/never result with no callee // ident (ref/harec/src/check.c:877,893); wwstage's // N_CALL-over-bare-ident shape leaves both nodes nil by design. // // `indot` tracks gate 3: true only when the immediate caller is an // N_DOT recursing into its .lhs. fn asserttyped(c: *checker, n: *node, indot: bool) void = { if (n == nil) { return; }; let k: nkind = n.kind; let isexpr: bool = k == nkind.N_INTLIT || k == nkind.N_FLOATLIT || k == nkind.N_STRLIT || k == nkind.N_RUNELIT || k == nkind.N_TRUE || k == nkind.N_FALSE || k == nkind.N_NIL || k == nkind.N_VOIDLIT || k == nkind.N_IDENT || k == nkind.N_BIN || k == nkind.N_UN || k == nkind.N_CALL || k == nkind.N_INDEX || k == nkind.N_CAST || k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT || k == nkind.N_RECV || k == nkind.N_DOT || k == nkind.N_SLICE || k == nkind.N_SPREAD || k == nkind.N_TUPLE || k == nkind.N_TRYPROP || k == nkind.N_TRYUNW || k == nkind.N_TYPETEST || k == nkind.N_TYPEASSERT || k == nkind.N_YIELD || k == nkind.N_MATCH; let skip: bool = false; if (isexpr && k == nkind.N_IDENT) { if (indot) { skip = true; }; if (!skip) { let s: *sym = scopelookup(c.cur, n.str); if (s != nil) { if (s.skind == skind.SK_USE) { skip = true; }; if (s.decl == nil) { skip = true; }; }; }; if (!skip) { if (isassertfam(c, n)) { skip = true; }; }; }; if (isexpr && k == nkind.N_CALL) { if (isassertfam(c, n.lhs)) { skip = true; }; }; if (isexpr && !skip) { if (n.type_ == nil) { os.write(2, "asserttyped: ".ptr, 13u64); let kn: str = nkname(k); os.write(2, kn.ptr, kn.len: u64); os.write(2, " ".ptr, 1u64); if (n.file.len > 0) { os.write(2, n.file.ptr, n.file.len: u64); os.write(2, ":".ptr, 1u64); let ls: str = strconv.i32tos(n.line, strconv.base.DEC); os.write(2, ls.ptr, ls.len: u64); }; if (n.str.len > 0) { os.write(2, " '".ptr, 2u64); os.write(2, n.str.ptr, n.str.len: u64); os.write(2, "'".ptr, 1u64); }; os.write(2, "\n".ptr, 1u64); os.exit(1); }; }; if (k == nkind.N_DOT) { if (n.lhs != nil) { asserttyped(c, n.lhs, true); }; return; }; if (n.attr != nil) { asserttyped(c, n.attr, false); }; if (n.lhs != nil) { asserttyped(c, n.lhs, false); }; if (n.rhs != nil) { asserttyped(c, n.rhs, false); }; if (n.cond != nil) { asserttyped(c, n.cond, false); }; if (n.body != nil) { asserttyped(c, n.body, false); }; if (n.els != nil) { asserttyped(c, n.els, false); }; let m: *node = n.list; for (m != nil) { asserttyped(c, m, false); m = m.next; }; }; export fn checkinit(c: *checker, tc: *tctx) void = { c.tc = tc; c.top = newscope(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); // A.6.2.1-pre — attr-subtree gap: top-level dispatch below walks // d.lhs / d.body per kind but never d.attr, leaving `@symbol("…")` // arg literals (N_STRLIT) outside the post-order exprtype // dispatch. Mirror resolvewalk L405 which descends n.attr on // inner nodes. if (d.attr != nil) { resolvewalk(c, d.attr); }; 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); }; // #88: const-fold sibling/imported def refs, casts, and // arithmetic so cgen's literal-only emitdefconstants can // lay down the DATA row. GATED on the plain literal fold // missing first, so existing literal/unary defs keep // their rhs node and the emitted bytes stay byte-identical. if (d.rhs != nil) { let dv: u64 = 0u64; if (!foldintliteral(d.rhs, &dv)) { if (evaldefconst(c, d.rhs, &dv, 0)) { stampintlit(d.rhs, dv); }; }; }; } 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); }; // #130: top-level let assignability — the subtree // resolvewalk above stamps types but never runs the // init-assignability check (function-body lets get it // via resolvewalk's post-order L247; top-level lets // were missed). Needed for the array accept-if-fits // range-check to fire on module-level `let A:[N]u8=[..]`. checkletassign(c, d); };};};}; d = d.next; }; // Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each // decl with its curmod set so asserttyped's gate lookups resolve // against the same module context exprtype saw during pass 2. d = file.list; for (d != nil) { c.curmod = declmod(file, d); asserttyped(c, d, false); d = d.next; }; let empty: str; c.curmod = empty; }; // io — stream interface (Plan 9 Bio / Hare io::stream shape). // // No closures, no methods. A `stream` is a struct of function // pointers plus a `ctx: *void`. The error channel is the return // value of read/write/close — Hare-shaped tagged unions instead of // errno-style integer sentinels. // eof — read past the end of the stream. Hare uses the `done` // singleton for EOF; ww doesn't have `done` yet so we ship a // named-void variant tag. package io; export type eof = void; // closed — operation attempted on a stream that has already been // closed. ww-specific: Hare's io collapses this into the wider // errors union, but our stream vtable has no handle-ownership // semantics, so a distinct tag is honest. Named void. export type closed = void; // underread — an I/O handle hit eof partway through a fixed-size // read. Payload is the byte count actually delivered. Mirrors // Hare's `io::underread = !size`; ww uses i32 because the // underlying buffer-length type is i32 today. export type underread = !i32; export type stream = struct { ctx: *void, read: fn(s: *stream, buf: []u8) (i32 | eof | closed), write: fn(s: *stream, buf: []u8) (i32 | closed), close: fn(s: *stream) (void | closed), }; export fn read(s: *stream, buf: []u8) (i32 | eof | closed) = { return s.read(s, buf); }; export fn write(s: *stream, buf: []u8) (i32 | closed) = { return s.write(s, buf); }; export fn close(s: *stream) (void | closed) = { return s.close(s); }; // memio — in-memory io stream. // // Hare's memio:: surface, drop underscores. Two flavours behind a // single [[io.stream]]: // // fixed caller owns the buffer, writes stop when full. // dynamic memio owns the buffer, writes grow it; close frees. // // Call shape divergence from Hare: the caller supplies both the // memio `state` and the `io.stream` slot, by pointer. ww cgen does // not yet implement &x.field or 32B-struct return-by-value, so the // Hare `let s = memio::fixed(buf)` shape isn't reachable; collapse // to a single returned struct when those land (lib/CLAUDE.md // "graduate in one go"). // // let mem: memio.state; // let s: io.stream; // memio.fixed(&mem, &s, buf); // io.write(&s, bytes); // // Subset of Hare's surface: io.stream's variants are {eof, closed}, // so memio drops Hare's NONBLOCK flag (would need an `again` variant // in lib/io). string()'s utf8-validating constructor is omitted per // CLAUDE.md rule 9 carve-out. Hare's seek / copy callbacks are // likewise absent: lib/io's stream vtable has only read/write/close // slots, so memio can't wire a seeker or copier even if we wanted // to. All three come back when their dependencies do. package memio; import io; import os; import rt; // state — memio's per-stream bookkeeping. The caller owns the slot // and passes its address into a constructor. `ptr/len/cap` are the // slice fields kept flat to dodge a chained-dot write through the // state pointer (cgen doesn't store into `m.buf.ptr` reliably). export type state = struct { ptr: *u8, len: i32, cap: i32, pos: i32, }; // fixed — wire `s` over a caller-supplied buffer. Writes never grow; // they return 0 once `pos` reaches the end of the buffer. export fn fixed(m: *state, s: *io.stream, buf: []u8) void = { m.ptr = buf.ptr; m.len = buf.len; m.cap = buf.len; m.pos = 0; s.ctx = m: *void; s.read = readfn; s.write = fixedwrite; s.close = closenoop; }; // dynamic — wire `s` with no initial buffer. Writes grow the backing // allocation; [[io.close]] frees it. export fn dynamic(m: *state, s: *io.stream) void = { m.ptr = nil; m.len = 0; m.cap = 0; m.pos = 0; s.ctx = m: *void; s.read = readfn; s.write = dynamicwrite; s.close = dynamicclose; }; // dynamicfrom — like [[dynamic]] but seeded with an existing slice. // Ownership of the slice transfers to the stream; [[io.close]] frees // it. The slice must come from the runtime allocator: close calls // [[os.free]] with `m.cap` bytes, which is taken from `buf.cap` (the // slice's allocated capacity), not its logical length. Passing a // half-filled append slice (len < cap) and using only `buf.len` here // would under-free on close. export fn dynamicfrom(m: *state, s: *io.stream, buf: []u8) void = { m.ptr = buf.ptr; m.len = buf.len; m.cap = buf.cap; m.pos = 0; s.ctx = m: *void; s.read = readfn; s.write = dynamicwrite; s.close = dynamicclose; }; // buffer — borrowed view of bytes written so far (buf[..pos]). // Seek to the end before calling if the full buffer is wanted. export fn buffer(m: *state) []u8 = { let r: []u8; r.ptr = m.ptr; r.len = m.pos; return r; }; // string — bytes written so far, as a str view. Hare returns // (str | utf8::invalid); ww doesn't ship utf8 validation yet, so // this returns the unchecked view. export fn string(m: *state) str = { let r: str; r.ptr = m.ptr; r.len = m.pos; return r; }; // reset — rewind the cursor and truncate the logical content to 0. // Backing storage is preserved; subsequent writes (dynamic) re-fill // from the start without reallocation. export fn reset(m: *state) void = { m.pos = 0; m.len = 0; }; // borrowedread — return an `amt`-byte view starting at `pos` without // copying, advancing the cursor. eof if fewer bytes are available. export fn borrowedread(m: *state, amt: i32) ([]u8 | io.eof) = { if (m.len - m.pos < amt) { let e: io.eof; return e; }; let r: []u8; r.ptr = m.ptr + (m.pos: u64); r.len = amt; m.pos += amt; return r; }; // ---- vtable callbacks ------------------------------------------------ fn readfn(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = { let m: *state = s.ctx: *state; if (m.pos >= m.len) { let e: io.eof; return e; }; let avail: i32 = m.len - m.pos; let n: i32 = buf.len; if (avail < n) { n = avail; }; let i: i32 = 0; for (i < n) { buf[i] = m.ptr[m.pos + i]; i += 1; }; m.pos += n; return n; }; fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { let m: *state = s.ctx: *state; if (m.pos >= m.len) { return 0; }; let space: i32 = m.len - m.pos; let n: i32 = buf.len; if (space < n) { n = space; }; let i: i32 = 0; for (i < n) { m.ptr[m.pos + i] = buf[i]; i += 1; }; m.pos += n; return n; }; fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { let m: *state = s.ctx: *state; let need: i32 = m.pos + buf.len; if (need > m.cap) { dynamicgrow(m, need); }; let i: i32 = 0; for (i < buf.len) { m.ptr[m.pos + i] = buf[i]; i += 1; }; m.pos += buf.len; if (m.pos > m.len) { m.len = m.pos; }; return buf.len; }; fn dynamicclose(s: *io.stream) (void | io.closed) = { let m: *state = s.ctx: *state; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nil; m.len = 0; m.cap = 0; m.pos = 0; return; }; fn closenoop(s: *io.stream) (void | io.closed) = { return; }; // Double-and-copy growth. Initial bump from 0 lands at 8 to amortise // small write bursts without a tail of reallocs. // // `dynamicgrow`, not Hare's bare `grow`: cstage bundles all imported // modules into a flat TU and resolves private fns by unqualified name, // so historically two `fn grow` decls (here + the now-deleted bump // arena's `grow`) would have collided. Module-prefixed name kept the // symmetry with `dynamicwrite`/`dynamicclose`; retained pending task // #9 (module-aware private-fn scoping in cstage). fn dynamicgrow(m: *state, need: i32) void = { let newcap: i32 = m.cap; if (newcap < 8) { newcap = 8; }; for (newcap < need) { newcap *= 2; }; let nbuf: *u8 = rt.malloc(newcap: u64): *u8; let i: i32 = 0; for (i < m.len) { nbuf[i] = m.ptr[i]; i += 1; }; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; m.ptr = nbuf; m.cap = newcap; }; // 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: elemsizeof, elemsizeofc // - slot sizing: structlookup, primsize, slotsize, fieldsize, // registerstruct, collectstructs // - rhs helpers: taggedvariantindex // // Bundler pulls this in transitively via cgen.ww; consumers don't // need to `use cgenutil;` directly. package wcc; import os; 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(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 "". 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 = alloc([], (total: u64) + 1u64)!; let k: i32 = 0; for (k < total) { p[k] = buf[k]; k += 1; }; p[total] = 0u8; let r: str; r.ptr = p.ptr; 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. // #60: arg.type_ is the checker-stamped element // tinfo (check.ww indexresult); istaggedtype/ // slotsize read .type_, so feed the N_INDEX node // directly. cstage reads the element via // base->type->sub (cmd/w6c/cgen.c:3518). if (arg.kind == nkind.N_INDEX) { if (istaggedtype(c, arg)) { if (slotsize(c, arg) == 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.type_: *tinfo, 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)) { // str IS []u8: slot 32 [+0]=tag,[+8]=ptr,[+16]=len, // [+24]=cap — same shape as the slice arm above. Push // cap, len, ptr, tag high→low so pop drains tag first // into arg-reg[0] (#1/Phase 3). 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 { // 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; }; }; }; }; // esz from the type table for an N_IDENT base (#76; mirrors // the cgindex idiom). Non-ident base stays esz=1 -> ptr // unscaled, matching cstage's base->kind==N_IDENT gate. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); };}; // 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 // ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit). // BX=lo*esz; AX=lo PRESERVED for cap. BX (dead hi) reloaded // by cgbasecap below. if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", BX\n"); emitline("\tIMULQ\tAX, BX\n"); emitline("\tADDQ\tBX, CX\n"); } else { emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr }; // cap = base_cap - lo (#20); AX=lo, BX free. if (cgbasecap(c, base, "BX")) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tPUSHQ\tBX\n"); // cap } else { emitline("\tPUSHQ\tDX\n"); // cap = len }; 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 = 0; if (arg != nil) { let at: *tinfo = arg.type_: *tinfo; if (typeisf32(at)) { fk = 1; } else { if (typeisfloat(at)) { fk = 2; }; }; }; 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); // #163: tuple ARG (param twin of #164's return). cgexpr left the // tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1); restage it // into @tupargscr by SysV class (tupstore, the #164 helper) and push // the slot words high->low so the pop drains slot+0 first into the // SysV ARG cursor. The frame slot decouples the return-class regs // from the overlapping arg-class regs. rettupleof scopes to an // N_CALL producer (tuple idents/literals as values are a separate // unimplemented gap; the SEND never pushes stale regs, rule 7). let tuparg: *node = rettupleof(c, arg); if (tuparg != nil) { let gptot: i32 = 0; let sstot: i32 = 0; let tsz: i32 = 0; let p: *node = tuparg.list; for (p != nil) { let et: *node = p.lhs; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (isfloattype(c, et)) { sstot += 1; } else { gptot += tupebytes(wide); }; tsz += slotsize(c, et); p = p.next; }; // The producing call already satisfied #164's return caps; // guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]). if (gptot > 4 || sstot > 2) { let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let scr: i32 = localadd(c, "@tupargscr", tsz, nil); let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; p = tuparg.list; for (p != nil) { let et: *node = p.lhs; let wide: bool = isstrtype(c, et) || isslicetype(c, et); tupstore(c, gpcur, ssecur, scr + eoff, wide, et); if (isfloattype(c, et)) { ssecur += 1; } else { gpcur += tupebytes(wide); }; eoff += slotsize(c, et); p = p.next; }; let w: i32 = tsz - 8; for (w >= 0) { emitline("\tMOVQ\t"); emitoff((scr + w): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); w -= 8; }; return rest + tsz / 8; }; if (nodeisslice(c, arg)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); return rest + 3; }; if (nodeisstr(c, arg)) { // str IS []u8: cgexpr left (AX=ptr, BX=len, CX=cap). Push // the triple, same as the slice arm above (#1/Phase 3). emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); return rest + 3; }; // #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. #60: // arg.type_ is the element tinfo (istaggedtype/slotsize read // .type_) — feed the N_INDEX node directly, dropping the // indexvaluetnode walk. if (arg.kind == nkind.N_INDEX) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); 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 rtyp: *node = fnretlookup(c, callee.str); if (!istaggedtype(c, rtyp)) { return 0; }; return slotsize(c, rtyp); }; 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 `frombytes(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 rtyp: *node = fnretlookupmod(c, callee.str, c.curmod); return isslicetype(c, rtyp); }; 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 rtyp: *node = fnretlookupmod(c, callee.str, cmod); return isslicetype(c, rtyp); }; }; return false; }; // N_DOT: read the checker-stamped n.type_. Struct field, nested // dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all // resolve to the right tinfo via check.ww:1947-1978 (pseudo-field // + struct-field stamps). Cstage cgen.c:182-184 node_isslice = // type_isslice(n->type) — same shape. Collapsed per A.6.3h (#56). if (k == nkind.N_DOT) { return typeisslice(n.type_: *tinfo); }; 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 (reads checker-stamped n.type_ — #56 A.6.3h), 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. 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 rtyp: *node = fnretlookupmod(c, callee.str, c.curmod); return isstrtype(c, rtyp); }; // #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 rtyp: *node = fnretlookupmod(c, callee.str, cmod); return isstrtype(c, rtyp); }; }; 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 off the stamped element size; without // this arm pushargsrev would only push AX and lose .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; }; // N_DOT: read the checker-stamped n.type_. Struct field, nested // dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all // resolve to the right tinfo via check.ww:1947-1978. Cstage // cgen.c:168-170 node_isstr = type_isstr(n->type) — same shape. // Collapsed per A.6.3h (#56). if (k == nkind.N_DOT) { return typeisstr(n.type_: *tinfo); }; if (k == nkind.N_CAST) { return isstrtype(c, n.rhs); }; return false; }; // typeis8byteprimitive — does this type take exactly one 8-byte // slot rather than a wider aggregate? One-liner via typeis8byteprim // (cstage N_LET sz==8 ladder SSoT). t.type_ is stamped at check.ww // L426-436 for every type-AST kind callers reach. Collapsed onto the // tinfo helper per A.6.3b (#46). fn typeis8byteprimitive(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeis8byteprim(t.type_: *tinfo); }; // elemissignedc — given an indexable type-AST (`*T`, `[]T`, `[N]T`), // is its element a signed narrow primitive? Used by cgindex to pick // MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at esz=1/2). Mirrors // cstage's `signed_elem` (cmd/w6c/cgen.c idx_eff path). Reads through // the stamped tinfo so alias/enum recursion lives in lib/ww/typ.ww. fn elemissignedc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let ti: *tinfo = t.type_: *tinfo; if (ti == nil) { return false; }; // #65 Phase-N step-2 cleanup: peel TY_NAMED before the .sub read. // #64 now flows per-decl NAMED wrappers, so a NAMED-of-(`*T`/`[]T`/ // `[N]T`) reaching here would read NAMED.sub (nil) instead of the // element. Mirrors cstage idx_eff's type_unwrap (cmd/w6c/cgen.c:790) // before the eff->sub read (:3518-3520). Byte-id-neutral: every // aliased indexable in-tree has a u8 element (typeissigned=false // either way). Transitive peel matches the #63 idiom. for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return false; }; return typeissigned(ti.sub); }; // elemisfloatc — given an indexable type-AST (`*T`, `[]T`, `[N]T`), is // its element an f32/f64? Used by cgindex to route the element load to // MOVSS/MOVSD into X0 instead of the integer loadopsz into AX (#119 — // the array-element twin of the scalar-float global load at cgen.c: // 2014). Reads through the stamped tinfo, peeling TY_NAMED before the // .sub read exactly as elemissignedc does (#64/#65). Float-ness comes // from the SAME tinfo the esz already reads — never a fresh node-stamp // (the unstamped-base trap that broke the exprfloatkind collapse, #121). fn elemisfloatc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let ti: *tinfo = t.type_: *tinfo; if (ti == nil) { return false; }; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return false; }; return typeisfloat(ti.sub); }; // elemisf32c — narrower elemisfloatc: true only when the element is f32, // so cgindex picks MOVSS over MOVSD at the #119 element load. fn elemisf32c(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; let ti: *tinfo = t.type_: *tinfo; if (ti == nil) { return false; }; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return false; }; return typeisf32(ti.sub); }; // elemisarrayc — given an indexable type-AST (`*T` / `[]T` / `[N]T`), is // its element itself an array (`[N][M]T` → element `[M]T`)? cgindex then // leaves the sub-array's ADDRESS in the result reg rather than // dereferencing — a nested index adds its offset and only the final // scalar element dereferences (#156, sister of #135 N_DOT-base-on- // array-field). Node-based with the `*[N]T` drill-through, mirroring // elemsizeof (:920-948) so elem-is-array aligns with the esz this same // tnode feeds. cstage twin: idx_eff(bt)->sub unwrapped == TY_ARRAY // (cmd/w6c/cgen.c). `c` kept for signature symmetry with elemisfloatc. fn elemisarrayc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; 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 false; }; if (k == nkind.N_TPTR) { if (elem.kind == nkind.N_TARRAY) { if (elem.lhs != nil) { elem = elem.lhs; }; }; }; return elem.kind == nkind.N_TARRAY; }; // tinfoisarray — TY_ARRAY (NAMED-aware), the tinfo-keyed companion to // elemisarrayc for cgindex's N_DOT/N_INDEX base branches, where the // element type comes from n.type_ (stamped tinfo) not a tnode. Same // role as typeisslice/typeisstr in lib/ww/typ.ww; kept cgen-local to // avoid widening the frontend surface for one #156 read-half check. fn tinfoisarray(t: *tinfo) bool = { let u: *tinfo = t; for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; if (u == nil) { return false; }; return u.kind == tykind.TY_ARRAY; }; // fieldissignedc — does this field/element type-AST need sign- // extension on a sub-word load? One-liner via typeissigned (cstage // cgen.c:240 `fld_issigned` SSoT). t.type_ is stamped at check.ww // L426-436 for every type-AST kind we see here (TNAME / TPTR / // TBANG / TENUM / TARRAY / TSLICE — see resolvewalk). fn fieldissignedc(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeissigned(t.type_: *tinfo); }; // 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.type_. // 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 // — tinfo.size carries the same numeric width cstage's `t->size` // reports, with TBANG / TENUM / TNAME-alias chains pre-folded by // tinfofornode (check.ww:1102-1153 TNAME, 1154-1161 TBANG, // 1196-1208 TENUM). export fn localloadop(c: *cgen, tnode: *node) str = { if (tnode == nil) { return "MOVQ"; }; let ti: *tinfo = tnode.type_: *tinfo; if (ti == nil) { return "MOVQ"; }; let sz: i32 = ti.size: i32; if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; }; let sigd: bool = typeissigned(ti); return loadopsz(sigd, sz); }; // 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; // str's element is u8 (F1: tystr.sub = tyu8), named directly // rather than read off str.sub because elemsizeof gets a raw // N_TNAME at the ident-base index path with no checker-stamped // tinfo — str.sub lives on .type_.sub, unstamped at this site // (cf. cgforrange's `if (sti != nil)` guard). This IS the // str.sub-equivalent; the structural collapse onto str.sub is // blocked on tinfo-stamping here, not intent (#24). cstage twin // reads eff->sub->size (cmd/w6c/cgen.c N_INDEX). if (streq(nm, "str")) { return primtypesize("u8"): i32; }; // 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. // FOOTGUN (#156): this drill ALSO fires for a bare 2D `[N][M]T` // (elem = the inner `[M]T`), so elemsizeof of a 2D array bottoms // out at the SCALAR T size, NOT the `[M]T` sub-array stride. 2D // double-index (cgindex) needs the sub-array stride — call // elemsizeofc, the 2D-correct entry, which recovers slotsize([M]T) // when elemsizeof returns 8. Never call elemsizeof for a 2D stride. 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 tyslicesize(): i32; }; 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 primtypesize("str"): i32; }; let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; }; return 8; }; // elemsizeofc — like elemsizeof but resolves aliased element types // (struct / tagged / `type foo = bar;`) via slotsize. Used where // cgindex / cgassign need a correct stride for `[N]Alias` arrays // whose Alias resolves to a tagged union (e.g. `[N]formattable`). fn elemsizeofc(c: *cgen, t: *node) i32 = { if (t == nil) { return 1; }; let direct: i32 = elemsizeof(t); if (direct != 8) { return direct; }; let k: nkind = t.kind; let elem: *node = nil; if (k == nkind.N_TPTR) { elem = t.lhs; }; if (k == nkind.N_TSLICE) { elem = t.lhs; }; if (k == nkind.N_TARRAY) { elem = t.lhs; }; if (elem == nil) { return direct; }; if (elem.kind == nkind.N_TNAME) { let ps: i32 = primsize(elem.str); if (ps > 0) { return ps; }; }; return slotsize(c, elem); }; // nodeisunsigned — best-effort cgen-time inference from the AST. We // walk surface nodes (N_DOT now reads n.type_ — #55 A.6.3g): // nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet) // nkind.N_IDENT — look up the local's declared type // nkind.N_DOT — read the checker-stamped n.type_ (#55 A.6.3g) // 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) { if (lc.tnode == nil) { return false; }; return typeisunsigned(lc.tnode.type_: *tinfo); }; return false; }; if (k == nkind.N_DOT) { return typeisunsigned(n.type_: *tinfo); }; if (k == nkind.N_CAST) { if (n.rhs == nil) { return false; }; return typeisunsigned(n.rhs.type_: *tinfo); }; 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 its element type is // unsigned. Read the checker-stamped result type directly, // mirroring the N_DOT arm above and cstage cgen.c:2541 // (`type_isunsigned(n->lhs->type)` on operand's stamped tinfo). // Replaces the prior structural base-walk that only fired for // N_IDENT base — fell through to `return false` for N_DOT base // (e.g. `d.digits[nd]` where d is a *struct), making // `d.digits[nd] >= 5u8` pick signed JGE instead of unsigned JAE. // Embodies the #121 principle (collapse structural onto stamp). // #134. if (k == nkind.N_INDEX) { return typeisunsigned(n.type_: *tinfo); }; // nkind.N_CALL: a call returning an unsigned type (e.g. `fn f() u64`) // is unsigned. Read the checker-stamped result type directly — the // N_CALL twin of the #134 N_INDEX arm above. cstage reads the same // stamp via `type_isunsigned(n->lhs->type)`, stamped at check.c N_CALL // `n->type = u->ret`; without this arm the wwstage fell through to // `return false`, picking signed IDIV/SAR over unsigned DIV/SHR on a // call-result div/mod/shift operand. #168. if (k == nkind.N_CALL) { return typeisunsigned(n.type_: *tinfo); }; 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, UNROUNDED. This is the memory-copy // extent: cstage copies exactly these bytes for the >24B sret // write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and // struct-to-struct moves, so a trailing narrow field (bool@32 in a // 33B struct padded to 40) keeps its MOVB tail rather than widening // to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww // mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT // number — see structabisize. // // NOTE: si.totsize is yet a THIRD metric — the slot-padded size // (rounded up to 8 for stack-slot use; see registerstruct's tail // `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride // want that slot number. 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; }; // structabisize — the ≤24B register-return ABI size of `si`: the // natural extent rounded up to the struct's maxalign. SSoT-equal to // cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`). // Distinct from structnaturalsize because the register RECV/RETURN // ABI packs the value into AX/DX/CX at 8-byte granularity: cstage // writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`, // :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail // (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not // MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory // copies (sret >24B, struct ident-copy) and field-offset math stay // on structnaturalsize. #169. // // maxalign comes from each field's TRUE alignment (tinfo.align), not // the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but // align 1, so an fsz ladder would over-round. Mirrors cstage's // maxalign = max(ft->align) (check.c:708). fn structabisize(si: *structinfo) i32 = { if (si == nil) { return 0; }; let n: i32 = 0; let maxaln: i32 = 1; let fi: *fieldinfo = si.fields; for (fi != nil) { let end: i32 = fi.foff + fi.fsz; if (end > n) { n = end; }; if (fi.tnode != nil) { let ti: *tinfo = fi.tnode.type_: *tinfo; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti != nil) { let aln: i32 = ti.align: i32; if (aln > maxaln) { maxaln = aln; }; }; }; fi = fi.finext; }; return (n + maxaln - 1) & ~(maxaln - 1); }; // 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 rtyp: *node = fnretlookupmod(c, cn, cmod); return sretretsize(c, rtyp); }; 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, "size")) { 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 = typeisunsigned(cur.type_: *tinfo); 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 = typeisunsigned(n.type_: *tinfo); }; }; 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) { // #59: read the checker-stamped tinfo instead of re-deriving the // field type via dotfieldtnode's structinfo walk. Mirrors cstage // castsrcprim N_DOT (cmd/w6c/cgen.c:323-344): the base must // resolve to a struct (or ptr-to-struct) before the field type // counts. That guard excludes pseudo-fields .len/.cap/.ptr (the // checker stamps them i32/*T at check.ww:1987-2004) and tuple // positionals, keeping them at sz=0 — asymmetry there breaks 995 // byte-id (cgen.c:286-290). The field's own width/sign is the // N_DOT's stamped type_ (check.ww:2012). One TY_NAMED peel, then // typeisint ? size : 0; bool falls out because typeisint(bool) is // false — the same exclusion the old streq("bool") arm encoded. let bu: *tinfo = nil; if (n.lhs != nil) { bu = n.lhs.type_: *tinfo; }; if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; }; if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu != nil && bu.kind == tykind.TY_STRUCT) { let u: *tinfo = n.type_: *tinfo; if (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; if (typeisint(u)) { *sz_out = u.size: i32; *unsigned_out = typeisunsigned(u); }; }; 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 // `). 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 rtyp: *node = fnretlookupmod(c, cname, cmod); if (rtyp == nil) { return nil; }; if (unwrap) { // Strip error variants — success type is the first // variant of the tagged return. if (rtyp.kind != nkind.N_TTAGGED) { return nil; }; return rtyp.list; }; // Plain call: declared return type is the local's type. return rtyp; }; // 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 = primtypesize("str"): i32; } 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; }; // #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps // `n.type_` on every N_T* kind via tinfofornode, which folds TBANG // (inner unchanged, check.ww:1154-1161), TNAME alias chains // (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/ // TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize. // // cstage SSoT is distributed — there is no single slot_size(Type*). // Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) + // max variant payload rounded to 8); the wwstage TTAGGED arm at // check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use // the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the // match-spill slot. Pointer-and-narrow → 8 is the local-frame // convention encoded at every cstage `localoff(…, 8, …)` callsite // (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at // this read site so `[N]i32` stride stays 4 (natural) — moving it // into ti.slotsize would lift array stride to 8/elem. // // `c: *cgen` retained unused for callsite stability (localloadop // precedent, 68219a1). fn slotsize(c: *cgen, typn: *node) i32 = { if (typn == nil) { return 8; }; let ti: *tinfo = typn.type_: *tinfo; if (ti == nil) { return 8; }; // #63 Phase-N step 1: peel TY_NAMED before this structural query. // #64 builds per-decl NAMED wrappers (tinfofornode), so the peel // now fires on aliased operands; byte-id holds because it collapses // NAMED to the alias-invariant underlying this read consumes. for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return 8; }; let kk: tykind = ti.kind; if (kk == tykind.TY_VOID) { return 0; }; if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || kk == tykind.TY_CHAN || kk == tykind.TY_FN || kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { return ti.size: i32; }; if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE || kk == tykind.TY_ARRAY) { return ti.slotsize: i32; }; return 8; }; // fieldsize — slot-padded byte width of a struct field's type-AST, // consumed by registerstruct's alignment + offset math (≥8→8 / // ≥4→4 / ≥2→2 ladder at L1875-1877) and by the `*p OP=` deref- // compound at cgenexpr.ww:3594. Mirror of check.ww:1053 // `fieldslotsize` (the same dispatch on the populated tinfo); // slotsize-template precedent at a828c03. cstage SSoT is // `f->type->size` (cmd/w6c/cgen.c:1386, :1656, :2515, :2535); // wwstage routes through ti.slotsize for composites since the // stack-slot pad rules live on tinfo (#48 verdict), and through // ti.size for the kinds whose natural size already equals their // in-struct width. // // `c: *cgen` retained unused for callsite stability (slotsize / // localloadop precedent, a828c03 / 68219a1). fn fieldsize(c: *cgen, tnode: *node) i32 = { if (tnode == nil) { return 8; }; let ti: *tinfo = tnode.type_: *tinfo; if (ti == nil) { return 8; }; // #63 Phase-N step 1: peel TY_NAMED before this structural query. // #64 builds per-decl NAMED wrappers (tinfofornode), so the peel // now fires on aliased operands; byte-id holds because it collapses // NAMED to the alias-invariant underlying this read consumes. for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return 8; }; let k: tykind = ti.kind; if (k == tykind.TY_STRUCT) { return ti.slotsize: i32; }; if (k == tykind.TY_ARRAY) { return ti.slotsize: i32; }; if (k == tykind.TY_TAGGED) { return ti.size: i32; }; if (k == tykind.TY_SLICE) { return ti.size: i32; }; if (k == tykind.TY_PTR || k == tykind.TY_FN || k == tykind.TY_CHAN) { return 8; }; if (k == tykind.TY_STR) { return ti.size: i32; }; // Primitives + TY_ENUM keep natural width inside structs // (cstage parity: cgen.c reads f->type->size directly). TY_TUPLE // flows here too — pre-collapse fallback was 8, the populated // ti.size carries the natural sum; ken-thompson 2026-05-23 review: // keep the corrected behavior, no fixtures in selfhost exercise // a tuple-typed struct field today (995 byte-id is the gate). if (ti.size > 0u64) { return ti.size: i32; }; return 8; }; fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *node) void = { let si: *structinfo = alloc(structinfo{ sname = name, smod = srcmod, })!; 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 = alloc(fieldinfo{ fname = f.str, foff = off, fsz = sz, 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; }; }; // isstrtype — alias-aware. Reads the stamped tinfo so `str`, // `type alias = str`, `!str`, and chained aliases all route to the // str-shaped slot. Cite cstage cgen.c:159 `type_isstr` SSoT. // Collapsed onto typeisstr per A.6.3b (#46); the prior AST walker is // reconstituted by typeisstr's TY_NAMED chase + tinfofornode's // TBANG-unwrap (check.ww:1145). fn isstrtype(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeisstr(t.type_: *tinfo); }; fn isslicetype(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeisslice(t.type_: *tinfo); }; // 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 rtyp: *node = fnretlookupmod(c, cnm, cmod); if (rtyp != nil) { return resolvetagged(c, rtyp); }; }; }; 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) { // #67: read the field's stamped tinfo off the N_DOT node // (post-#66 N_DOT carries the field type) instead of the // dotfieldtnode AST walk. cgmatch's gate reads scrutt.type_ // via istaggedtype, so the resolved N_TTAGGED node the walk // produced is no longer the carrier; the istaggedtype guard // preserves the nil-for-non-tagged contract the sibling // N_CALL/N_INDEX branches get from resolvetagged. if (!istaggedtype(c, scrut)) { return nil; }; return scrut; }; 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; }; // structfloatclass — SysV per-eightbyte classification for the #165 // float-bearing-struct param case (param twin of #171's struct return; // classifies per-eightbyte, not #163's per-element). Returns 0 when the // struct does NOT qualify — the caller keeps the all-GP transport, which // is correct + byte-identical there — for: not a <=16B struct; an all- // integer layout (no float to route); an f32 field; >1 float packed in // one eightbyte; a float straddling the 8-byte SysV eightbyte boundary; // or an aggregate field (SysV would recurse, out of scope). Otherwise a // packed result whose low bits hold the eightbyte count nb (1|2) and bit // (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every // eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 / // sub-eightbyte packing deferred (#165b). Mirrors cstage // struct_float_class (cmd/w6c/cgen.c). fn structfloatclass(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; }; // SysV classifies aggregates in 8-byte eightbytes; 8 is the // eightbyte stride, not a type footprint. let nb: i32 = 1; if (si.totsize > 8) { nb = 2; }; let nflt0: i32 = 0; let nflt1: i32 = 0; let nint0: i32 = 0; let nint1: i32 = 0; let fi: *fieldinfo = si.fields; for (fi != nil) { let foff: i32 = fi.foff; let fsz: i32 = fi.fsz; let e: i32 = foff / 8; if (e < 0) { return 0; }; if (e >= nb) { return 0; }; if (isfloattype(c, fi.tnode)) { if (isf32type(c, fi.tnode)) { return 0; }; if ((foff & 7) != 0) { return 0; }; if (fsz != 8) { return 0; }; if (e == 0) { nflt0 += 1; } else { nflt1 += 1; }; } else { if (isslicetype(c, fi.tnode)) { return 0; }; if (isstrtype(c, fi.tnode)) { return 0; }; if (istaggedtype(c, fi.tnode)) { return 0; }; if (structparamsize(c, fi.tnode) > 0) { return 0; }; // Alias-aware array/tuple reject, mirroring cstage's // NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class). // A direct-AST-kind N_TARRAY test misses an aliased array // and every tuple field; the fsz>8 guard below also lets a // <=8B one slip, so such a struct would wrongly SSE-route on // this stage but stay GP on cstage (a #165b leak). let rf: *node = resolvetype(c, fi.tnode); if (rf != nil) { if (rf.kind == nkind.N_TARRAY) { return 0; }; if (rf.kind == nkind.N_TTUPLE) { return 0; }; }; if (fsz > 8) { return 0; }; if ((foff + fsz - 1) / 8 != e) { return 0; }; if (e == 0) { nint0 += 1; } else { nint1 += 1; }; }; fi = fi.finext; }; let enc: i32 = nb; let hasfloat: bool = false; if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; } else { if (nflt0 != 0) { return 0; }; }; if (nb == 2) { if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; } else { if (nflt1 != 0) { return 0; }; }; }; if (!hasfloat) { return 0; }; return enc; }; // istaggedtype — alias-aware. Reads stamped tinfo so `T`, // `type alias = (A|B)`, `type error = !(invalid|overflow)` all // resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap // (check.ww:1145) so we don't re-walk it here. Cite cstage cgen.c // (`type_chase_named` + TY_TAGGED). Collapsed per A.6.3b (#46). fn istaggedtype(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeistagged(t.type_: *tinfo); }; // isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage // cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across // cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn- // prologue. Collapsed per A.6.3b (#46). export fn isfloattype(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeisfloat(t.type_: *tinfo); }; // isf32type — narrower: true only for f32 (after alias chase). Cite // cstage cgen.c:188 `type_isf32`. Picks MOVSS vs MOVSD and the SS- // variant arithmetic / cast opcodes. Collapsed per A.6.3b (#46). export fn isf32type(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; return typeisf32(t.type_: *tinfo); }; // isnullabletype — `(*T | void)` one-word fold per Hare's // `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`; // the .nullable flag lands on tinfo at check.ww:1309-1318 when the // two-variant shape matches. Collapsed per A.6.3b (#46). export fn isnullabletype(t: *node) bool = { if (t == nil) { return false; }; return typeisnullable(t.type_: *tinfo); }; // nullableptrtag — 0-based index of the *T variant in a nullable // union. Mirror of cstage cgen.c:404-416 `nullable_ptr_tag`: linear // scan ti.params, strip TY_NAMED on each variant, return idx of first // TY_PTR. Phase 1 (26724fe) populated the chain in tinfofornode's // TTAGGED arm so this walk could retire the AST-keyed predecessor. export fn nullableptrtag(t: *node) i32 = { if (t == nil) { return 0; }; let ti: *tinfo = t.type_: *tinfo; if (ti == nil) { return 0; }; // #63 Phase-N step 1: peel TY_NAMED before this structural query. // #64 builds per-decl NAMED wrappers (tinfofornode), so the peel // now fires on aliased operands; byte-id holds because it collapses // NAMED to the alias-invariant underlying this read consumes. for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return 0; }; if (ti.kind != tykind.TY_TAGGED) { return 0; }; let p: *tparam = ti.params; let i: i32 = 0; for (p != nil) { let vt: *tinfo = p.type_; if (vt != nil) { if (vt.kind == tykind.TY_NAMED) { vt = vt.under; }; if (vt != nil) { if (vt.kind == tykind.TY_PTR) { return i; }; }; }; p = p.tnext; i += 1; }; return 0; }; // 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; }; // 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; }; return taggedvariantindext(c, tagged.type_: *tinfo, rhs); }; // taggedvariantindext — tinfo-keyed core of taggedvariantindex. Given // the dst tagged tinfo `du` (NAMED-peeled internally, gated TY_TAGGED) // and the source value node, returns the 0-based variant index. #68: the // tagged-store machinery reads tinfo directly (no type node), so the // node-keyed taggedvariantindex delegates here off `tagged.type_`. // // #66 Phase-N step 3: match the value's stamped type against the variant // types by typeeq (flatvariantidxt), replacing the rhstargetname surface- // name compare. Untyped/loose values (whose .type_ is untyped_* and can't // typeeq a concrete variant) return -1 there and drop to the str/slice // shape scan below — ww has no type_assignable to mirror cg_variant_match's // untyped-src arm. fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { if (du == nil) { return -1; }; if (rhs == nil) { return -1; }; let ti: *tinfo = du; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo); if (r >= 0) { return r; }; // Shape fallback: classify rhs as (str, slice, scalar/other) and // pick the first variant of matching shape. Stands in for cstage's // type_assignable on an untyped src — the typeeq pass above can't // match untyped_* against a concrete variant, and the slice axis // keeps a (u8 | []u8) widen off the leading scalar variant (task // #19). tinfo.params is already spread-flattened (#61a — `...inner` // inlined in declaration order), so the old N_TTAGGED.list spread- // walk collapses to a flat scan over p.type_. let wantstr: bool = nodeisstr(c, rhs); let wantslice: bool = nodeisslice(c, rhs); let p: *tparam = ti.params; let idx: i32 = 0; for (p != nil) { let vt: *tinfo = p.type_; let visstr: bool = typeisstr(vt); let visslice: bool = typeisslice(vt); if (visstr == wantstr && visslice == wantslice) { return idx; }; p = p.tnext; idx += 1; }; return -1; }; // flatvariantidx — flat 0-based index of the variant whose type matches // the pattern node `pat`, by typeeq on the stamped tinfos. Reads the // pre-flattened variant chain off tinfo.params (#61a — `...inner` // spreads already inlined in declaration order); peels TY_NAMED then // gates TY_TAGGED. // // #66 Phase-N step 3 (THE FLIP, user-ruled B-full): match by // typeeq(p.type_, pat.type_) — nominal identity carried by the per-decl // TY_NAMED ptr — instead of the surface-name compare. So `type linerr = // !str` ≠ str and a cross-module `a.T` ≠ `b.T` are now distinguished. // Mirrors cstage cg_variant_match (cmd/w6c/cgen.c:451): both-NAMED → // ptr-id (typeeq, typ.ww:514), one-NAMED → kind mismatch → false. ww has // no type_assignable, so the untyped/loose arm (cg_variant_match's first // branch) lives in the caller's str/slice shape fallback, not here. fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { if (tagged == nil) { return -1; }; if (pat == nil) { return -1; }; return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo); }; // flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based // index of the variant whose type typeeq's `want`, over the pre- // flattened tinfo.params chain (#61a). Peels TY_NAMED then gates // TY_TAGGED. #68: the tagged-store machinery reads tinfo directly and // has no type node to hand the node-keyed flatvariantidx, so the typeeq // core lives here; flatvariantidx + taggedvariantindext + cgwidentagremap // all funnel through it. Mirrors cstage cg_tag_for_variant // (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq // arm (:451). fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { if (want == nil) { return -1; }; let ti: *tinfo = tagged; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; let p: *tparam = ti.params; let idx: i32 = 0; for (p != nil) { if (typeeq(p.type_, want)) { return idx; }; p = p.tnext; idx += 1; }; return -1; }; // flatslicevariantidx — flat 0-based index of a slice-shape variant in // `tagged`. Prefers the variant whose element typeeq's the pattern // element `elem`; falls back to the first slice-shape slot when no exact // element match is found (the untyped/loose arm — ww has no // type_assignable). Reads the flattened tinfo.params chain (#61a); peels // TY_NAMED then gates TY_TAGGED. The slice axis exists because a scalar- // vs-`[]T` distinction has no surface name to key on (task #19). // // #66 Phase-N step 3: element compare flips from surface-name to // typeeq(p.type_.sub, elem.type_). Mirrors cstage cg_tag_for_variant // over Type->params. Returns -1 when no slice variant exists. fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = { if (tagged == nil) { return -1; }; let ti: *tinfo = tagged.type_: *tinfo; for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; let want: *tinfo = nil; if (elem != nil) { want = elem.type_: *tinfo; }; let fallback: i32 = -1; let p: *tparam = ti.params; let idx: i32 = 0; for (p != nil) { let vt: *tinfo = p.type_; if (vt != nil) { if (typeisslice(vt)) { if (fallback < 0) { fallback = idx; }; if (want != nil) { let su: *tinfo = vt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su != nil) { if (typeeq(su.sub, want)) { return idx; }; }; }; }; }; p = p.tnext; 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. // // #68: both `du` (dst) and `su` (src) are now the tagged tinfos — peel // TY_NAMED then walk su.params, mapping each source variant to its dst // index by typeeq (flatvariantidxt). Mirrors cg_widen_tag_remap // (cmd/w6c/cgen.c:1177) over su->params + cg_tag_for_variant (:503). fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { let dt: *tinfo = du; for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; }; if (dt == nil) { return; }; if (dt.kind != tykind.TY_TAGGED) { return; }; let st: *tinfo = su; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st == nil) { return; }; if (st.kind != tykind.TY_TAGGED) { return; }; let identity: bool = true; let p: *tparam = st.params; let idx: i32 = 0; for (p != nil) { let di: i32 = flatvariantidxt(dt, p.type_); if (di < 0) { di = 0; }; if (di != idx) { identity = false; p = nil; } else { p = p.tnext; idx += 1; }; }; if (identity) { return; }; let done: str = mklabel(c, "remap_done"); emitline("\tMOVQ\t"); emitoff(slot_off: i64); emitline("(BP), AX\n"); p = st.params; idx = 0; for (p != nil) { let next: str = mklabel(c, "remap_next"); let di: i32 = flatvariantidxt(dt, p.type_); 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); p = p.tnext; 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); }; // 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 rtyp: *node = fnretlookupmod(c, calleename, cmod); if (rtyp != nil) { if (istaggedtype(c, rtyp)) { 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. #58 A.6.3i-phase-2: // read the checker-stamped n.type_ (check.ww N_DOT struct-field // stamp) instead of re-deriving via dotfieldtnode — matches // cstage cg_widen_tagged_store reading src->type directly // (cmd/w6c/cgen.c:1302-1305). typeistagged(nil) is false. if (src.kind == nkind.N_DOT) { if (typeistagged(src.type_: *tinfo)) { 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, cap@+24 (str IS []u8, #1/Phase 3). // - scalar src: tag@+0, value@+8. fn cgwidentaggedstore(c: *cgen, dst: *tinfo, 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: *tinfo, src: *node, slot_off: i32, slot_sz: i32) void = { // #68: dst is the tagged tinfo. Peel TY_NAMED → du and gate // TY_TAGGED, mirroring cstage cg_widen_tagged_store's // `du = (dst->kind==TY_NAMED)?dst->under:dst` + TY_TAGGED guard // (cmd/w6c/cgen.c:1273). resolvetagged's N_TBANG unwrap is already // handled upstream by tinfofornode (check.ww:1203-1210). let dt: *tinfo = dst; for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; }; if (dt == nil) { return; }; if (dt.kind != tykind.TY_TAGGED) { return; }; // Nullable fold: one 8B word holding the pointer (or 0 for void). if (dt.nullable != 0) { 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. #68: compare on the stamped tinfos — // `castu == dt` (peeled-underlying ptr-id) || // (castu tagged && typeeq(castt, dst)) — mirroring // cstage cg_widen_tagged_store's // `cast_is_widen = (castu==du) || (castu->kind== // TY_TAGGED && type_eq(castt, dst))` // (cmd/w6c/cgen.c:1295-1296). Replaces the prior // surface-name streq; same-alias TNAMEs share one // NAMED tinfo (check.ww:1160-1173) so typeeq hits // the a==b fast path. let castisdst: bool = false; let castrhs: *node = src.rhs; if (castrhs != nil) { let castt: *tinfo = castrhs.type_: *tinfo; let castu: *tinfo = castt; for (castu != nil && castu.kind == tykind.TY_NAMED) { castu = castu.under; }; if (castu != nil) { if (castu == dt) { castisdst = true; } else { if (castu.kind == tykind.TY_TAGGED) { if (typeeq(castt, dst)) { castisdst = true; }; }; }; }; }; if (castisdst && !inneristagged) { src = inner; }; }; }; }; // Tagged source ident: byte-copy slot words then tag-remap. // rhstaggedident gates "src is a tagged-typed local ident"; the // remap reads the source tagged tinfo off the local's tnode (#68). 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, lc.tnode.type_: *tinfo, 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 = taggedvariantindext(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)) { // str IS []u8: 3-word field // (ptr,len,cap) from cgexpr's // AX/BX/CX (#1/Phase 3). 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"); emitline("\tMOVQ\tCX, "); emitoff((slot_off + 8 + fi.foff + 16): 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 IS []u8 — same 32B payload as a slice: cgexpr leaves // (AX=ptr, BX=len, CX=cap); slot layout [+0]=tag, [+8]=ptr, // [+16]=len, [+24]=cap. str folds onto the slice arm (#1/Phase 3 // collapse; cite cstage cg_widen_tagged_store). if (nodeisslice(c, src) || 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"); emitline("\tMOVQ\tCX, "); emitoff((slot_off + 24): i64); emitline("(BP)\n"); let tag: i32 = taggedvariantindext(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. Classify off // the checker stamp (src.type_) — the SSoT cstage reads via // node_isfloat / type_isf32 — 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. The armed asserttyped bail (check.ww) // guarantees src carries a non-nil stamp, so the sibling-evidence // loud-abort that used to pin "the float arm requires a stamped // value" is dead and removed. let fkind: i32 = 0; if (src != nil) { let srct: *tinfo = src.type_: *tinfo; if (typeisf32(srct)) { fkind = 1; } else { if (typeisfloat(srct)) { fkind = 2; }; }; }; if (fkind != 0) { let fmov: str = "MOVSD"; if (fkind == 1) { fmov = "MOVSS"; }; cgexpr(c, src); emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); // #66 Phase-N step 3: the float arm has no pattern node to ride // the typeeq flatvariantidx path, so pick the variant by float // kind (f32 vs f64) over tinfo.params — a shape classification // like the slice axis, not nominal identity. let wantf32: bool = (fkind == 1); let ftag: i32 = -1; let fti: *tinfo = dt; for (fti != nil && fti.kind == tykind.TY_NAMED) { fti = fti.under; }; if (fti != nil) { if (fti.kind == tykind.TY_TAGGED) { let fp: *tparam = fti.params; let fidx: i32 = 0; for (fp != nil) { let fvt: *tinfo = fp.type_; for (fvt != nil && fvt.kind == tykind.TY_NAMED) { fvt = fvt.under; }; if (fvt != nil) { if (typeisfloat(fvt)) { if (typeisf32(fvt) == wantf32) { ftag = fidx; break; }; }; }; fp = fp.tnext; fidx += 1; }; }; }; 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 = taggedvariantindext(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 *outleaftype is the leaf *tinfo // 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. // #71 (A.6.3j): the spine offset-sum + leaf-type now read off // tinfo.fields (natural layout) instead of the structinfo/fieldinfo // walk. Mirror of cstage cmd/w6c/cgen.c:3156-3216 which walks // `cur->lhs->type` fields. Root resolution (the local/ptr/global split // and the rootoff/ptrroot/isglobal flags) stays on localfindnode/ // letvarstructinfo unchanged, so the firing set + addressing mode are // byte-identical to the structinfo era; only the layout SOURCE moves. // The slot-padded foff and the natural tfield.offset coincide for every // shape the byte-id gate exercises (cstage already reads the natural // offset), so this is offset-preserving. Leaf out-param is the field's // stamped *tinfo (was *fieldinfo); the str/slice pseudo-leaf leaves it // nil and the callers gate on slicedelta>=0 first. export fn dotchainresolve(c: *cgen, n: *node, outrootname: *str, outrootoff: *i32, outtotaloff: *i32, outleaftype: **tinfo, outslicedelta: *i32, outisglobal: *bool, outptrroot: *bool) bool = { *outrootname = ""; *outrootoff = 0; *outisglobal = false; *outptrroot = false; *outtotaloff = 0; *outleaftype = 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 resolved: bool = false; let lc: *local = localfindnode(c, cur.str); if (lc != nil) { if (lc.tnode != nil) { if (lc.tnode.kind == nkind.N_TNAME) { *outrootoff = lc.off; resolved = true; }; // `*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) { *outrootoff = lc.off; *outptrroot = true; resolved = true; }; }; }; }; }; if (!resolved) { let gsi: *structinfo = letvarstructinfo(c, cur.str); if (gsi != nil) { *outisglobal = true; resolved = true; }; }; if (!resolved) { return false; }; // Root struct layout = the stamped root-ident type_, peeled NAMED // (plus one TY_PTR hop for a `*struct` root). tfield.type_ then // supplies each nested struct directly, so no name re-lookup. let curstruct: *tinfo = cur.type_: *tinfo; for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) { curstruct = curstruct.under; }; if (*outptrroot) { if (curstruct == nil) { return false; }; if (curstruct.kind != tykind.TY_PTR) { return false; }; curstruct = curstruct.sub; for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) { curstruct = curstruct.under; }; }; let i: i32 = nsteps - 1; for (i >= 0) { if (curstruct == nil) { return false; }; if (curstruct.kind != tykind.TY_STRUCT) { return false; }; if (stk[i] == nil) { return false; }; let stepnm: str = stk[i].str; let tf: *tfield = curstruct.fields; let found: *tfield = nil; for (tf != nil) { if (streq(tf.name, stepnm)) { found = tf; break; }; tf = tf.tnext; }; if (found == nil) { return false; }; let foff: i32 = found.offset: i32; if (i == 0) { *outtotaloff = *outtotaloff + foff; *outleaftype = found.type_; return true; }; let ft: *tinfo = found.type_; for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; }; if (ft == nil) { return false; }; if (ft.kind == tykind.TY_STR) { // str IS []u8: .cap is the third header word, same as // the TY_SLICE leaf below — cstage treats str≡slice for // .ptr/.len/.cap (cmd/w6c/cgen.c:2478) (#1/Phase 3, #11). 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 + foff; *outslicedelta = delta; return true; }; if (ft.kind == tykind.TY_SLICE) { 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 + foff; *outslicedelta = delta; return true; }; if (ft.kind != tykind.TY_STRUCT) { return false; }; *outtotaloff = *outtotaloff + foff; curstruct = ft; i -= 1; }; 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. // - the ELLIPSIS zero-fill extent is read internally as // structabisize(si) — cstage's cg_structlit_fill computes // `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size // (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The // pre-#169 callers passed two different sizes (natural at DOT // sites, slot-padded at BP-rel sites); neither matched cstage // for maxalign<8 structs (the zero-fill ran MOVQ where cstage // ran MOVL — value-correct, asm-divergent). // // 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) void = { if (si == nil) { return; }; let basereg: str = "BP"; if (mode != 0) { basereg = "BX"; }; let totsize: i32 = structabisize(si); 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.type_: *tinfo, 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) { cgstructlitfill(c, isi, fieldnode.lhs, mode, srcoff, srcname, disp + fi.foff); 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 if (isstrtype(c, fi.tnode)) { // str IS []u8: 3-word field (ptr,len,cap). // cgexpr leaves AX/BX/CX; for non-BP modes // the dst base goes in DX to dodge BX=len / // CX=cap (the generic store reloads BX, which // would clobber len) (#1/Phase 3). cgexpr(c, fieldnode.lhs); if (mode == 0) { emitline("\tMOVQ\tAX, "); emitoff((disp + fi.foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((disp + fi.foff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((disp + fi.foff + 16): i64); emitline("(BP)\n"); } else { if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), DX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), DX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg((disp + fi.foff): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((disp + fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((disp + fi.foff + 16): i64, "DX"); emitline("\n"); }; 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. fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = { if (si == nil) { return; }; cgstructlitfill(c, si, lit, 0, 0, "", bpoff); }; // 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 ast; import tok; import typ; import sym; import strconv; // cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits // into AX, PUSH, MOVSD off the stack into X0. Shared by N_FLOATLIT (bits // already in n.uval from the lexer's bitcast) and the f64/f32-typed // N_INTLIT arm (#103 FACE X). fn cgfloatbits(c: *cgen, bits: u64) void = { emitline("\tMOVQ\t$"); emitint(bits: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVSD\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); }; fn cgexpr(c: *cgen, n: *node) void = { if (n == nil) { return; }; let k: nkind = n.kind; if (k == nkind.N_INTLIT) { // A no-decimal `0f64`/`8f64` is an N_INTLIT carrying float // TYPE; it must reach X0 like a true float literal, not the // integer-immediate path (which strands it in AX and an SSE // compare/mul reads a stale X0 — #103 FACE X). The bits are // the IEEE pattern of the integer value, mirroring cstage's // `(double)(long long)n->uval`; the (&fv):*u64 bitcast is the // lex.ww idiom (lib/ww/lex/lex.ww). if (isfloattype(c, n)) { let fv: f64 = (n.uval: i64): f64; let pu: *u64 = (&fv): *u64; cgfloatbits(c, *pu); // #104: cgfloatbits materialises a DOUBLE in X0; an // f32-typed literal must narrow with hardware single- // rounding so the downstream MOVSS reads a true single. if (isf32type(c, n)) { emitline("\tCVTSD2SS\tX0, X0\n"); }; return; }; // 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) { // The bits come from n.uval — the parser populates it from // the lexer's bitcast of t.fval. cgfloatbits(c, n.uval); // #104: narrow the double in X0 to single for an f32 literal. if (isf32type(c, n)) { emitline("\tCVTSD2SS\tX0, X0\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 lookup routes through the // element-aware helper, which carries the loose first-slice-shape // fallback (cstage type_assignable stand-in) that flatvariantidx's // strict typeeq below doesn't. Task #19; #66 refresh. if (vt.kind == nkind.N_TSLICE) { return flatslicevariantidx(c, tagged, vt.lhs); }; // #66 Phase-N step 3: match by typeeq on vt's stamped tinfo // (flatvariantidx), not vt's surface name. return flatvariantidx(c, tagged, vt); }; // 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 rtyp: *node = fnretlookupmod(c, cname, cmod); if (rtyp != nil) { if (rtyp.kind == nkind.N_TTAGGED) { let first: *node = rtyp.list; if (first != nil) { if (isstrtype(c, first)) { succisstr = true; }; }; }; }; }; }; }; }; if (succisstr) { // str IS []u8: success arrives DX=ptr, CX=len, R8=cap // (slot 32B). Move len out before cap overwrites CX // (#1/Phase 3). emitline("\tMOVQ\tCX, BX\n"); emitline("\tMOVQ\tR8, CX\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 rtyp: *node = fnretlookupmod(c, cname, cmod); if (rtyp != nil) { if (rtyp.kind == nkind.N_TTAGGED) { let first: *node = rtyp.list; if (first != nil) { if (isstrtype(c, first)) { succisstr = true; }; }; }; }; }; }; }; }; if (succisstr) { // str IS []u8: success arrives DX=ptr, CX=len, R8=cap // (slot 32B). Move len out before cap overwrites CX // (#1/Phase 3). emitline("\tMOVQ\tCX, BX\n"); emitline("\tMOVQ\tR8, CX\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 = 0; if (n.lhs != nil) { let st: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(st)) { srcfk = 1; } else { if (typeisfloat(st)) { srcfk = 2; }; }; }; 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 = { // str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in BX, // cap in CX. A static literal has no spare storage, so cap = len // (#1/Phase 3). Call sites that expect a str arg pick these up. 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"); emitline("\tMOVQ\t$"); emitint(nstr.len: i64); emitline(", CX\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) { // str IS []u8: load (ptr,len,cap) into AX/BX/CX, // identical to the slice arm below (#1/Phase 3). emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), CX\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"); // str IS []u8: cap = len for a static def literal // (#1/Phase 3). emitline("\tMOVQ\t$"); emitint(bytes.len: i64); emitline(", CX\n"); return; }; }; // Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape // as the let-float arm below — MOVSS/MOVSD have no D_EXTERN // operand form. Pre-#129 fell through to the MOVQ-AX // integer-convention fallback, leaving X0 untouched (#129 // LOAD-side twin of the emitfloatlitdata DATA-side SSoT). if (isfloattype(c, n)) { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); emitline("\t(CX), X0\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 rtyp: *node = fnretlookup(c, nm); if (rtyp != 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) { // str IS []u8: both str and slice carry a third 8B // (cap); load it unconditionally. The address holder CX // is overwritten by the cap as the last step, after // ptr/len are already loaded (#1/Phase 3). emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); 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; }; // cgslicehdr — load the 24B slice/str header at base+0 into the // (AX=ptr, BX=len, CX=cap) triple. base holds the element address; // the load that targets base destroys it, so that word is emitted // LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr). // Shared by the cgindex str-element arms (caller does the kind-gate) // and, later, the typeassert str-variant leaf (#9). c retained unused // for callsite symmetry with cstage cgslicehdr. fn cgslicehdr(c: *cgen, base: str) void = { if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; }; // dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an // N_DOT with N_IDENT inner. Returns true if emitted; callers fall back // to `cgexpr(c, base); MOVQ AX, dstreg` on false. Cstage twin: // cmd/w6c/cgen.c `cg_dotbase_addr`. // // #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto- // derefs + loads the field's 8-byte VALUE as if it were a pointer. For // an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read / `d.fld // [i] OP= v`), the caller wants the field's ADDRESS — this helper // supplies it inline. Reusable primitive of the inverse template // `arr[i].field = v` (cstage cgen.c arr[i].field address-eval). Chained // N_DOT (`a.b.c.field[i]`) deferred — not in #135 scope. fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = { if (base == nil) { return false; }; if (base.kind != nkind.N_DOT) { return false; }; let inner: *node = base.lhs; if (inner == nil) { return false; }; if (inner.kind != nkind.N_IDENT) { return false; }; // #128b: module-qualified `mod.arr` where arr is an imported // top-level `let X: [N]T`. The checker leaves SK_USE module- // idents without a localfindnode entry; detect via letvartnode // resolving to N_TARRAY and emit LEAQ X(SB). Without this, the // cgindex fallback's cgexpr(base) auto-MOVQs the symbol's first // 8 bytes as if it were a pointer-var — wrong shape (cstage // sister fix in cg_dotbase_addr). let lc: *local = localfindnode(c, inner.str); if (lc == nil) { let gt: *node = letvartnode(c, base.str); if (gt != nil && gt.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), "); emitline(dstreg); emitline("\n"); return true; }; return false; }; let bu: *tinfo = inner.type_: *tinfo; for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu == nil) { return false; }; let viaptr: bool = false; let structt: *tinfo = nil; if (bu.kind == tykind.TY_PTR) { let st: *tinfo = bu.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { structt = st; viaptr = true; }; }; } else { if (bu.kind == tykind.TY_STRUCT) { structt = bu; }; }; if (structt == nil) { return false; }; let f: *tfield = structt.fields; let foff: i64 = -1; let ft: *tinfo = nil; for (f != nil) { if (streq(f.name, base.str)) { foff = f.offset: i64; ft = f.type_; break; }; f = f.tnext; }; if (foff < 0) { return false; }; // Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields // the existing cgexpr(base) path correctly loads the pointer/ // header value; over-firing here would skip the deref. Cstage // twin gate at cg_dotbase_addr. for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; }; if (ft == nil) { return false; }; if (ft.kind != tykind.TY_ARRAY) { return false; }; let innoff: i64 = lc.off: i64; if (viaptr) { emitline("\tMOVQ\t"); emitoff(innoff); emitline("(BP), "); emitline(dstreg); emitline("\n"); if (foff != 0) { emitline("\tADDQ\t$"); emitint(foff); emitline(", "); emitline(dstreg); emitline("\n"); }; } else { emitline("\tLEAQ\t"); emitoff(innoff + foff); emitline("(BP), "); emitline(dstreg); emitline("\n"); }; return true; }; 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; // #119: float element loads route to MOVSS/MOVSD into X0, not the // integer loadopsz into AX. float_elem/f32_elem are set per-branch // from the SAME tinfo esz reads — never a fresh node-stamp (#121). let float_elem: bool = false; let f32_elem: bool = false; // #156 (PREREQ-1 read-half): element is itself an array ([N][M]T → // element [M]T) → leave the sub-array's ADDRESS in the result reg // instead of dereferencing; the outer index adds its offset and the // final scalar element dereferences. Sister of #135. Mirrors cstage // esubu->kind == TY_ARRAY. Node-based (elemisarrayc) for ident bases, // n.type_ tinfo-based for N_DOT/N_INDEX bases — same source split as // esz above. let elem_isarray: bool = false; // #1/Phase 3: str and slice are both 24B (and a >16B struct is // 24B+ too), so the header branches below MUST gate on KIND // (elemisstr/elemisslice, mirroring cstage's elem_is_str|| // elem_is_slice), not a bare `esz == primtypesize("str")` size // check — a size gate would route a plain >16B struct into the // 3-word {ptr,len,cap} load and diverge from cstage (#60 collision // class; sentinel 754). let elemisstr: bool = false; let elemisslice: 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); float_elem = elemisfloatc(c, baselocal.tnode); f32_elem = elemisf32c(c, baselocal.tnode); elem_isarray = elemisarrayc(c, baselocal.tnode); } else { let tn: *node = letvartnode(c, bn); // #129 A.3: array-typed defs now have DATA storage; // resolve their base via the same N_TARRAY path as // lets. defvartnode is the def-side sister of // letvartnode (parallel to defvarstructinfo at the // A.2 cgdot widening site). if (tn == nil) { tn = defvartnode(c, bn); }; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); signed_elem = elemissignedc(c, tn); float_elem = elemisfloatc(c, tn); f32_elem = elemisf32c(c, tn); elem_isarray = elemisarrayc(c, tn); }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeofc(c, tn); signed_elem = elemissignedc(c, tn); float_elem = elemisfloatc(c, tn); f32_elem = elemisf32c(c, tn); elem_isarray = elemisarrayc(c, tn); }; }; }; } else { if (base.kind == nkind.N_DOT) { // `s.ptr[i]` / struct-field index: stride is the // checker-stamped element tinfo's natural size, the // same idiom as the N_INDEX-base arm below (#60/#72). // cstage idx_eff(base->type)->sub->size (cmd/w6c/ // cgen.c:3517-18). esz-only — N_DOT-base signedness // stays unset, as before. let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner // index's value type). cstage reads base->type->sub->size // for esz (cmd/w6c/cgen.c:2070-2071). Drops the // indexvaluetnode walk. let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; signed_elem = typeissigned(et); float_elem = typeisfloat(et); f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; };};}; }; // 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; }; }; elemisstr = isstrtype(c, etn); elemisslice = isslicetype(c, etn); }; }; 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"); // #156: array element ([N][M]T) → leave the sub-array ADDRESS // in AX (BX holds base+idx*esz); nested index dereferences. if (elem_isarray) { emitline("\tMOVQ\tBX, AX\n"); return; }; 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/slice element: load the full (ptr, len, cap) header into // (AX, BX, CX) — both are 24B since #1, so cap must survive. // Kind-gate on isstrtype||isslicetype, never size==24 (a >16B // struct is 24B+ too but takes the struct-copy path). Base is BX. if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; // #119: float element → MOVSS/MOVSD into X0 (the consumer's // ADDSD/MOVSD spill machinery already expects X0); the integer // loadopsz below would leave it in AX and the SSE side reads // stale. Twin of cgen.c:2014's scalar-float global load. if (float_elem) { let fop1: str = "MOVSD"; if (f32_elem) { fop1 = "MOVSS"; }; emitline("\t"); emitline(fop1); emitline("\t(BX), X0\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"); // #156: array element ([N][M]T) → leave the sub-array ADDRESS // in AX (BX holds base+idx*esz); nested index dereferences. if (elem_isarray) { emitline("\tMOVQ\tBX, AX\n"); return; }; 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/slice element: full (ptr, len, cap) header into (AX, BX, CX); // cap must survive (#1). Kind-gate, never size==24. Base BX. if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; // #119: float element → X0 (see the global arm above). if (float_elem) { let fop2: str = "MOVSD"; if (f32_elem) { fop2 = "MOVSS"; }; emitline("\t"); emitline(fop2); emitline("\t(BX), X0\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. // #135: N_DOT base on `[N]T` field needs the field's ADDRESS, // not its value. cgexpr would auto-deref + load the 8-byte value // as if it were a pointer. dotbaseaddr emits the address inline. emitline("\tPUSHQ\tAX\n"); if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; emitline("\tPOPQ\tBX\n"); emitline("\tADDQ\tBX, AX\n"); // #156: array element ([N][M]T) → AX already holds &elem // (base+idx*esz); a nested index dereferences. See ident arms. if (elem_isarray) { return; }; 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; }; // str/slice element via fallback base: full (ptr, len, cap) header // into (AX, BX, CX); cap must survive (#1). Kind-gate, never // size==24. Base AX. if (elemisstr || elemisslice) { cgslicehdr(c, "AX"); return; }; // #119: float element → X0 (see the global arm above). The base // address is in AX; MOVSS/MOVSD reads the element into X0. if (float_elem) { let fop3: str = "MOVSD"; if (f32_elem) { fop3 = "MOVSS"; }; emitline("\t"); emitline(fop3); emitline("\t(AX), X0\n"); return; }; let lop3: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop3); emitline("\t(AX), AX\n"); return; }; // cgbasecap — load the capacity of a sub-slice's UNDERLYING storage // into `dst` for the #20 cap = base_cap - lo formula (drew: harec // eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start; // ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal); // slice/str -> the .capacity word in the header at +16 (mirrors the // hi-default +8 length dispatch, emitted unconditionally). Returns // false when base_cap isn't cleanly available so the caller keeps the // prior cap=len: a non-ident base (its header cap was discarded; // recomputing would re-evaluate a possibly side-effecting base -- #74, // which also owns the pre-existing defaulted-hi len gap there), or // a GLOBAL str base (no +16 load here, #73 -- matching the cstage // carve-out keeps both stages byte-identical). cstage twin: // cmd/w6c/cgen.c cg_base_cap. fn cgbasecap(c: *cgen, base: *node, dst: str) bool = { if (base == nil) { return false; }; if (base.kind != nkind.N_IDENT) { return false; }; let baselocal: *local = localfindnode(c, base.str); if (baselocal != nil) { let tn: *node = baselocal.tnode; if (tn == nil) { return false; }; if (tn.kind == nkind.N_TARRAY) { let lenn: *node = tn.rhs; if (lenn == nil) { return false; }; if (lenn.kind != nkind.N_INTLIT) { return false; }; emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (tn.kind == nkind.N_TSLICE) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 16): i64); emitline("(BP), "); emitline(dst); emitline("\n"); return true; }; if (tn.kind == nkind.N_TNAME) { if (streq(tn.str, "str")) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 16): i64); emitline("(BP), "); emitline(dst); emitline("\n"); return true; }; }; return false; }; let gt: *node = letvartnode(c, base.str); if (gt == nil) { return false; }; if (gt.kind == nkind.N_TARRAY) { let lenn: *node = gt.rhs; if (lenn == nil) { return false; }; if (lenn.kind != nkind.N_INTLIT) { return false; }; emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (gt.kind == nkind.N_TSLICE) { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), "); emitline(dst); emitline("\n"); emitline("\tMOVQ\t16("); emitline(dst); emitline("), "); emitline(dst); emitline("\n"); return true; }; return false; }; // cgslice — `base[lo:hi]` as a slice value. Leaves (AX=base+lo*esz, // BX=hi-lo, CX=base_cap-lo) so callers can route to a slice slot, // return, or arg with the same triple ABI. cap is the storage // remaining to the base's end (#20, Go/Hare-identical) via cgbasecap. // ptr advances by BYTES (lo*esz, #76; ref/hare/rt/ensure.ha:30 // membsz-unit); esz from the type table, mirroring the cgindex idiom. 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; }; }; }; }; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom). Non-ident base stays esz=1 -> ptr unscaled, // matching cstage's base->kind==N_IDENT gate. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); };}; // 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"); // ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit). // DX=lo*esz; CX=lo PRESERVED for len + cap (#20). if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", DX\n"); emitline("\tIMULQ\tCX, DX\n"); emitline("\tADDQ\tDX, AX\n"); } else { emitline("\tADDQ\tCX, AX\n"); }; emitline("\tSUBQ\tCX, BX\n"); // cap = base_cap - lo (#20); CX=lo, BX=len here. if (cgbasecap(c, base, "DX")) { emitline("\tSUBQ\tCX, DX\n"); emitline("\tMOVQ\tDX, CX\n"); } else { 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) { // #67: gate on the stamped tinfo, not the node kind // — matchscrutt now returns the scrutinee node itself // for an N_DOT field (its .type_ is the tagged tinfo) // rather than the resolved N_TTAGGED node. if (istaggedtype(c, scrutt)) { let r: i32 = -1; if (pat.kind == nkind.N_TNAME) { r = flatvariantidx(c, scrutt, pat); } 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 IS []u8 — same 3-word {ptr,len,cap} // as a 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. str folds onto the slice // arm (#1/Phase 3 collapse; cite cstage // cgen.c N_DOT *struct S2). emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { 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 IS []u8 — same 3-word {ptr,len,cap} // as a slice field: load (ptr, len, cap) // into (AX, BX, CX). Base is BP so no // aliasing — order doesn't matter. str // folds onto the slice arm (#1/Phase 3 // collapse; cite cstage cgen.c N_DOT S1). if (isstrtype(c, fi.tnode) || isslicetype(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"); 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: scalar in an // 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a // str element, load (ptr, len, cap) into (AX, BX, CX), // the canonical slice-header ABI. No slice-element // sibling here, so the triple is hand-authored; base is // BP (frame, not a target reg) so ptr/len/cap order has // no clobber risk. 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.lhs); tp = tp.next; i += 1; }; }; if (tp != nil) { let tpt: *node = tp.lhs; if (isstrtype(c, tpt)) { 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"); emitline("\tMOVQ\t"); emitoff((lc.off + foff + 16): i64); emitline("(BP), CX\n"); return; }; // f64/f32 tuple field must ride X0 via // MOVSD/MOVSS; the integer load op left it // in AX (#103 FACE Z). Mirrors the float // local load above and cstage cgen.c:1462, // 1838 (the #96 pattern). if (isfloattype(c, tpt)) { let mov: str = "MOVSD"; if (isf32type(c, tpt)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + foff): i64); emitline("(BP), X0\n"); return; }; let sz: i32 = slotsize(c, tpt); let op: str = tnodeloadop(c, tpt, sz); emitline("\t"); emitline(op); emitline("\t"); emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); return; }; }; }; // str/slice pseudo-fields .ptr/.len/.cap on a // direct local: load at slot+delta. let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; if (streq(fld, "cap")) { delta = 16; }; if (delta >= 0) { // Pointer to str/slice (`*[]u8`, `*str`): // deref, then load at delta within the // pointed-to header. C cgen does the same. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let innerkind: nkind = nkind.N_NONE; if (inner != nil) { innerkind = inner.kind; }; let innerstr: bool = false; if (innerkind == nkind.N_TNAME) { if (streq(inner.str, "str")) { innerstr = true; }; }; if (innerkind == nkind.N_TSLICE) { innerstr = true; }; if (innerstr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitdispreg(delta: i64, "BX"); emitline(", AX\n"); return; }; }; emitline("\tMOVQ\t"); emitoff((lc.off + delta): i64); emitline("(BP), AX\n"); return; }; }; }; }; // `def NAME: str = "..."` field access — inline the literal. // Sdef-backed strs aren't laid out in memory, so falling // through to the SB-load fallback below would mis-emit // `MOVQ (SB), AX` (looking up the field name as a // symbol). Mirrors cmd/w6c/cgen.c nkind.N_DOT off==0 / Sdef branch. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let drhs: *node = deflookuprhs(c, lhs.str); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; if (streq(fld, "ptr")) { let lab: str = internstrlit(c, bytes); emitline("\tLEAQ\t"); 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 (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; }; // str IS []u8: .cap is valid on a str global too, // not slice-only — mirrors cstage (#1/Phase 3, #11). 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. // #129 A.2: also handles struct-typed `def`s via defvarstructinfo; // emitstructdata gives them DATA storage at name(SB), and this // LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def // fell through to the integer-let MOVQ catch-all (reading garbage // from the wrong offset). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let si: *structinfo = letvarstructinfo(c, lhs.str); if (si == nil) { si = defvarstructinfo(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; }; // str IS []u8 — 3-word {ptr,len,cap}, the local // slice-field arm (BP) retargeted to the CX global // base. cap→CX LAST: CX is the base, so .ptr/.len // must read first. cstage folds local+global in one // base_reg arm; ww splits them, so this global arm // carries its own lift (filed divergence task). 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"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "CX"); emitline(", CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 global field: route through X0. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", X0\n"); } else { let op: str = fieldloadop(c, fi); emitline("\t"); emitline(op); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); }; }; return; }; fi = fi.finext; }; }; }; }; // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` // (and slice/`*[N]S`) base. Without this the cgen falls through // to the module-qualified SB fallback below and emits // `MOVQ (SB), AX` (linker: `undefined reference to `). // One branch covers both shapes: compute `&arr[i]` into BX, then // either deref (`*Struct` element) or move-to-AX (value `Struct` // element), so the leaf load is `(field.offset)(AX)` either way. // Bypasses cgindex deliberately — cgindex's final MOVQ would // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; let sname: str; sname.ptr = nil; sname.len = 0; let viaptr: bool = false; if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { let inner: *node = elemt.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { sname = elemt.str; };}; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { let esz: i32 = elemsizeofc(c, tn); cgexpr(c, lhs.rhs); // idx → AX if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), AX\n"); } else { emitline("\tMOVQ\tBX, AX\n"); }; if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { // str/slice: the 3-word {ptr,len,cap} // slice header (#1). AX holds the // element base, so load .ptr (which // targets AX) LAST. Matches the // caseB *struct slice arm and // cgslicehdr(D_AX). 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; }; 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 leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, n, &rootname, &rootoff, &totaloff, &leaftype, &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 (typeisstr(leaftype)) { 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 (typeisslice(leaftype)) { // 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 (typeisfloat(leaftype)) { let mov: str = "MOVSD"; if (typeisf32(leaftype)) { 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 = loadopsz(typeissigned(leaftype), leaftype.slotsize: i32); 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) { // #70 (#12): inner-struct layout via the stamped lhs.type_ // (peel *→struct) + tinfo.fields, replacing dotinnerstructptr's // structinfo walk. Gate is strict-equal to the deleted helper: // fire only when the chain root is a LOCAL ident AND every dot // in the chain resolves through a *struct (dotinnerstructptr // recursed per level on a *struct field and bailed on a by- // value-struct intermediate). Reproducing that exactly avoids // an untested widening past cstage; a deliberate widen, if ever // wanted, is a future task with its own probe. Global-root // chains stay in their pre-existing shared base-eval breakage // (filed #27), untouched here. let croot: *node = lhs; let allptr: bool = true; for (croot != nil && croot.kind == nkind.N_DOT) { let ct: *tinfo = croot.type_: *tinfo; for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; }; let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; }; if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr) { if (croot != nil) { if (croot.kind == nkind.N_IDENT) { if (localfindnode(c, croot.str) != nil) { it = lhs.type_: *tinfo; }; }; }; }; for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; }; if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { let tf: *tfield = st.fields; for (tf != nil) { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; cgexpr(c, lhs); // AX = ptr to inner struct // str IS []u8 — same 3-word {ptr,len,cap} // as a slice field: load (ptr, len, cap) // into (AX, BX, CX). AX is the *struct // base, so load .ptr (which targets // AX) LAST. str folds onto the slice // arm (#1/Phase 3 collapse; cite cstage // cgen.c N_DOT chained *struct caseB). if (typeisstr(ft) || typeisslice(ft)) { emitline("\tMOVQ\t"); emitdispreg((tf.offset + 8u64): i64, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((tf.offset + 16u64): i64, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); return; }; // f64/f32 chained field: route through X0. if (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", X0\n"); return; }; let lop: str = loadopsz(typeissigned(ft), ft.slotsize: i32); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); return; }; tf = tf.tnext; }; }; }; }; }; }; }; // Chained `(ident).f1.f2` read where f1 is a struct-by-value // field. Mirror of the cgassign branch added for the same shape. // Without this, `L.cur.kind` (cur a by-value struct of *L) // falls into the SB-fallback and emits `MOVQ kind(SB), AX`. // Kept as a fallback below the generalized walker above (placed // earlier in cgdot) to preserve byte-identical output on shapes // it already handles. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let inner: *node = lhs.lhs; let innerfld: str = lhs.str; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let lkind: nkind = tn.kind; let outname: str; outname.ptr = nil; outname.len = 0; let isptr: bool = false; if (lkind == nkind.N_TNAME) { outname = tn.str; }; if (lkind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { outname = pe.str; isptr = true; };}; }; if (outname.len > 0) { let osi: *structinfo = structlookup(c, outname); if (osi != nil) { let ofi: *fieldinfo = osi.fields; for (ofi != nil) { if (streq(ofi.fname, innerfld)) { let oft: *node = ofi.tnode; if (oft != nil) { if (oft.kind == nkind.N_TNAME) { if (primsize(oft.str) == 0) { let isi: *structinfo = structlookup(c, oft.str); if (isi != nil) { let ffi: *fieldinfo = isi.fields; for (ffi != nil) { if (streq(ffi.fname, fld)) { let totoff: i32 = ofi.foff + ffi.foff; if (isstrtype(c, ffi.tnode)) { if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); emitline("\tMOVQ\t"); emitdispreg((totoff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg(totoff: i64, "CX"); emitline(", AX\n"); } else { emitline("\tMOVQ\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + totoff + 8): i64); emitline("(BP), BX\n"); }; return; }; if (isfloattype(c, ffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", X0\n"); } else { emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), X0\n"); }; return; }; let lop: str = fieldloadop(c, ffi); if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", AX\n"); } else { emitline("\t"); emitline(lop); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); }; return; }; ffi = ffi.finext; }; }; }; };}; }; ofi = ofi.finext; }; }; }; };}; };}; }; }; // Nested module-qualified field where the chain didn't fold to a // known shape (raw w6c on a single file with `use mod;` but no // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of // cmd/w6c/cgen.c N_DOT nested fallback. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); return; }; }; return; }; fn cgun(c: *cgen, n: *node) void = { // Match C cgen ordering: evaluate operand first (load into AX), // then apply the unary op. AMP / STAR override AX with the // address / deref. The wasted load before AMP keeps our asm // byte-identical to the C version. let fk: i32 = 0; if (n.lhs != nil) { let lt: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(lt)) { fk = 1; } else { if (typeisfloat(lt)) { fk = 2; }; }; }; 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; }; // #149/#147: address-of a top-level def with DATA // storage. emitdefs / emitstructdata / emitarraydata // all emit to emitsymname(name), so the address is // the same LEAQ name(SB) as a let. Address-of twin of // A.2/A.3's LOAD-side widening. if (defisaddressable(c, opnd)) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); return; }; // rule-7: the name IS a def but has no DATA symbol // (str def inlined, or computed-rhs float like // `def NAN = 0.0/0.0`). Loud, not a wild deref. if (deflookup(c, nm)) { let m1: str = "ww: cannot take address of non-addressable def '"; os.write(2, m1.ptr, m1.len: u64); os.write(2, nm.ptr, nm.len: u64); let m2: str = "': no DATA symbol (str/computed-rhs def; #149/#147)\n"; os.write(2, m2.ptr, m2.len: u64); os.exit(1); }; 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 leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, opnd, &rootname, &rootoff, &totaloff, &leaftype, &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; }; // str IS []u8: &str.cap is valid too, not slice-only // — mirrors cstage (#1/Phase 3, #11). 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; }; }; }; }; }; // #149 Shape 2: `&mod.G` module-qualified address-of // of an exported global (let or def). The base is an // N_IDENT that's neither a local nor a global let, so // it's an SK_USE module qualifier; LEAQ the leaf // symbol. Kind-agnostic (covers cross-module &let / // &def / &scalar) — the address-of twin of the value- // read mod-qual path (cgenexpr.ww). A fn leaf resolves // via emitfnname (fn address), mirroring that read // path's TY_FN branch. if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_IDENT) { let basenm: str = opnd.lhs.str; if (localfindnode(c, basenm) == nil) { // A def base (`&Pdef.field`) is NOT a module // qualifier: cstage's Shape-2 gate (base // type_ == NULL/ty_err) excludes it because // the checker types a def-struct/def-array // base, but the ww gate (not-local && not-let) // does not. Without this guard a def base would // mis-LEAQ the field leaf (e.g. `y(SB)`) while // cstage silent-drops, breaking cs==ww (rule // 10). Excluding defs restores byte-id; the // `&def.field` silent-drop itself is a separate // pre-#149 gap (file as #150-family). if (!isletvar(c, basenm) && !deflookup(c, basenm)) { let fld: str = opnd.str; let frt: *node = fnretlookupmod(c, fld, basenm); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, basenm); emitline("(SB), AX\n"); return; }; emitline("\tLEAQ\t"); emitsymname(c, fld); emitline("(SB), 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]`: stride is the checker-stamped // element tinfo's natural size, mirroring // cgindex's N_DOT arm so &p.ptr[i] and // p.ptr[i] agree. cstage idx_eff(base->type) // ->sub->size (cmd/w6c/cgen.c:3517-18). #72. let dt: *tinfo = opnd.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; }; };}; }; 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) { // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float // field/ident load idiom. if (isfloattype(c, n)) { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t(AX), X0\n"); } else { // Load-twin of the landed signed-narrow-scalar-reads // sweep (selfhost/CLAUDE.md "Signed-narrow scalar // reads sign-extend honestly"); TK_STAR was the // omitted site, refiled as #116. A raw MOVQ pulls 8B // through a narrow `*iN` and overlaps the next element // — the `*p` value reads honest only when the caller's // sink truncates (i32 store, i32 return). Width- // preserving sinks (CMPQ, 64-bit arith) saw garbage in // the high bytes. localloadop keys MOVSXD/MOVSWQ/ // MOVSBQ + MOVL/MOVZWQ/MOVZBQ off n.type_; n is the // deref expression, n.type_ is the pointee tinfo // (check.ww unoptype TK_STAR L1871-1886 with // TY_NAMED/TY_ENUM peel pre-folded by // tinfofornode/typeissigned), the same shape the // float arm above feeds isfloattype. let lop: str = localloadop(c, n); emitline("\t"); emitline(lop); emitline("\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. // Value-class read off the checker stamp (n.type_) — the SSoT // shared with cstage cgen.c node_isfloat / type_isf32. The armed // asserttyped bail (check.ww) guarantees every checked value-node // is stamped, so the read can't see a nil-typed float operand; // the sibling-evidence loud-aborts that used to pin that contract // are therefore dead and removed. let lfk: i32 = 0; if (n.lhs != nil) { let llt: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(llt)) { lfk = 1; } else { if (typeisfloat(llt)) { lfk = 2; }; }; }; let rfk: i32 = 0; if (n.rhs != nil) { let rrt: *tinfo = n.rhs.type_: *tinfo; if (typeisf32(rrt)) { rfk = 1; } else { if (typeisfloat(rrt)) { rfk = 2; }; }; }; 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; if (n.op == tkind.TK_EQ) { isfcmp = true; }; if (n.op == tkind.TK_NEQ) { isfcmp = true; }; if (n.op == tkind.TK_LT) { isfcmp = true; }; if (n.op == tkind.TK_LE) { isfcmp = true; }; if (n.op == tkind.TK_GT) { isfcmp = true; }; if (n.op == tkind.TK_GE) { isfcmp = true; }; 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"); // IEEE-754: UCOMISD/SS sets PF=ZF=CF=1 on unordered (a // NaN operand). Any relop with a NaN operand is // unordered -> `!=` true, the other five false. PF must // steer `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so // `nan != nan` came out false; JE/JB/JBE fire on the // unordered ZF/CF. `>`/`>=` (JA/JAE) need CF=0, which // unordered never gives, so they are ALREADY NaN-correct // and stay byte-identical to the pre-#97 single-template // arm — no redundant PF guard. if (n.op == tkind.TK_NEQ) { // not-equal OR unordered -> true let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\tJNE\t"); emitline(t); emitline("\n"); emitline("\tJP\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; }; if (n.op == tkind.TK_EQ || n.op == tkind.TK_LT || n.op == tkind.TK_LE) { // unordered -> false; otherwise the ordered Jcc decides. let jcc: str = "JE"; if (n.op == tkind.TK_LT) { jcc = "JB"; }; if (n.op == tkind.TK_LE) { jcc = "JBE"; }; let fl: str = mklabel(c, "cf"); let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\tJP\t"); emitline(fl); emitline("\n"); emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); emitlabel(fl); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; // `>`/`>=`: JA/JAE already reject unordered (CF=1), so // keep the pre-#97 single-template shape verbatim. let jcc: str = "JA"; if (n.op == tkind.TK_GE) { jcc = "JAE"; }; 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) { // #136: signed RSHIFT → SAR (arithmetic, sign-extends MSB); // unsigned → SHR (logical, zero-fill). `unsignd` derived above // at cgbin head from nodeisunsigned(lhs) || nodeisunsigned(rhs). emitline("\tMOVQ\tBX, CX\n"); if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\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_malloc, 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. // // Task #30: result is the graduated `(*T | nomem)` tagged-pointer // pair (AX=tag, DX=ptr). rt_malloc now returns 0 on OOM // (rt/alloc.s); branch on AX to emit the nomem variant (tag=1, // DX=0) or the success variant (tag=0, DX=ptr) after the // value-init stores complete. Callers wrap with `!` / `?` to // consume the union. 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; }; }; let okl: str = mklabel(c, "alloc_ok"); let donel: str = mklabel(c, "alloc_done"); emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\t"); emitline(ffiresolve(c, "malloc")); emitline("(SB)\n"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); emitline("\tMOVQ\t$1, AX\n"); emitline("\tMOVQ\t$0, DX\n"); emitline("\tJMP\t"); emitline(donel); emitline("\n"); emitlabel(okl); 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, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); fi = nil; } else { if (isstrtype(c, fi.tnode)) { // str IS []u8: cgexpr leaves (AX=ptr, // BX=len, CX=cap). Route the heap base // through DX so all three survive — CX // holds cap, BX holds len (#1/Phase 3). emitline("\tMOVQ\t(SP), 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"); fi = nil; } else { emitline("\tMOVQ\t(SP), BX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\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\tDX\n"); emitline("\tMOVQ\t$0, AX\n"); emitlabel(donel); }; // 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_malloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // alloc path. // // Same-module-scope guard: skip the builtin when a fn // `alloc` is declared in the current module (lib/os and // rt/ensure both shadow it). Mirrors cstage check.c's // scope_lookup_prefer gating on the `abort` precedent; // without it, the bare same-module call lands in the // typed-builtin path and shadows the user decl. Task #23. if (streq(callee.str, "alloc")) { if (n.list != nil) { if (!samemodfn(c, "alloc")) { cgalloc(c, n); return; }; }; }; // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297. // Required for byte-id when compiler-imported lib code uses // len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)` // over the [800]u8 field). Without this intercept wwstage falls // through to a regular CALL len(SB) while cstage folds to // `MOVQ $alen, AX` — rule-10 byte-id break (#131). // // Argument-type-driven branches: // TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8. // TY_ARRAY → fold `MOVQ $alen, AX`. // else → evaluate operand (cstage's pseudo-.len fallback — // unlikely to fire on Hare-shaped sources). if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; let at: *tinfo = a.type_: *tinfo; let u: *tinfo = at; for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; if (u != nil) { if ((u.kind == tykind.TY_SLICE || u.kind == tykind.TY_STR) && a.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc != nil) { emitline("\tMOVQ\t"); emitoff((lc.off + 8): i64); emitline("(BP), AX\n"); return; }; }; if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); emitline(", AX\n"); return; }; }; // Fallback: evaluate the argument and let AX carry // whatever the value-load shape yields. Mirrors // cstage's `cgexpr(c, a, locals)` fallthrough. cgexpr(c, a); 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_`) plus a 24B slice // descriptor (`@vararg_sl_`), then splice a synthesised // N_IDENT pointing at the descriptor into n.list so the rest // of the call machinery sees one slice slot for the variadic. // Forwarding shape (`xs...`) skips the gather: the spread's // inner slice expression replaces the wrapper in place. Empty // (no trailing args) writes a {nil, 0, 0} descriptor. 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. // check.ww installparams promotes varp.lhs to // []T (mirrors cstage check.c:455 tp->type // wrap). Element predicates / esz read varp.lhs // .lhs; Ken's gate: only deref when the wrap // shape is confirmed N_TSLICE (mirrors cstage // cgen.c:4352 `vsu->kind == TY_SLICE` guard). let velem: *node = varp.lhs; if (varp.lhs != nil && varp.lhs.kind == nkind.N_TSLICE) { velem = varp.lhs.lhs; }; let esz: i32 = 8; if (velem != nil) { if (velem.kind == nkind.N_TNAME) { let ps: i32 = primsize(velem.str); if (ps > 0) { esz = ps; } else { esz = slotsize(c, velem); }; } else { esz = slotsize(c, velem); }; }; if (esz < 1) { esz = 1; }; let velemtagged: bool = istaggedtype(c, velem); let velemstr: bool = isstrtype(c, velem); let velemslice: bool = isslicetype(c, velem); let doff: i32 = 0; if (nvar > 0) { doff = localadd(c, dname, nvar * esz, nil); }; // #60: vararg gather builds a {ptr,len,cap} slice // descriptor — route through tyslicesize so #34's // slice-header bump propagates here. varp.lhs is // already the []T wrap from installparams, so we // consume it directly (re-slicewrap → [][]T). let soff: i32 = localadd(c, sname, tyslicesize(): i32, 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) { // dst is the per-element tagged type; // pass velem (cstage cgen.c:4382 passes // velem, not the slice wrap vsu). cgwidentaggedstore(c, velem.type_: *tinfo, 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(nkind.N_IDENT, "", 0, 0); sn.str = sname; // Synthesised after the checker has run, so the // asserttyped bail (check.ww) never stamps it. // Stamp the variadic param's []T slice tinfo // (resolvefnbody resolve-walks varp.lhs) so the // downstream value-class reads see a non-nil // stamp — the one cgen node the bail can't cover. sn.type_ = varp.lhs.type_; 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 = 0; if (a != nil) { let at: *tinfo = a.type_: *tinfo; if (typeisf32(at)) { fk = 1; } else { if (typeisfloat(at)) { fk = 2; }; }; }; 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 tuparg: *node = rettupleof(c, a); if (tuparg != nil) { // #163: drain the tuple's staged words (slot+0 pushed // first) into the SysV arg cursor by SysV class — a // float MOVSD/MOVSS off (SP) into the next XMM, else // POPQ into the next INTEGER arg reg; a slice/str its // 3 words. Reg overflow loud-stops (rule 7); the // partial-spill stitch is out of scope (twin of #164). let p: *node = tuparg.list; for (p != nil) { let et: *node = p.lhs; if (isfloattype(c, et)) { if (fpidx >= 8) { let msg: str = "tuple arg float element overflows SSE arg regs (X0..X7); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let mov: str = "MOVSD"; if (isf32type(c, et)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; popped += 1; } else { let wide: bool = isstrtype(c, et) || isslicetype(c, et); let eb: i32 = tupebytes(wide); if (intidx + eb > 6) { let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let k: i32 = 0; for (k < eb) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; popped += 1; k += 1; }; }; p = p.next; }; } else { let stfc: i32 = 0; if (a.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }; }; if (stfc != 0) { // #165: float-bearing struct arg — drain by SysV // eightbyte class: a lone-f64 eightbyte MOVSD off // (SP) into the next XMM (X0..X7), a pure-INT // eightbyte POPQ into the next INTEGER arg reg // (DI/SI/..). The struct-ident push staged raw slot // words (class-independent); only the drain differs. // Gated to qualifying floats; all-int + f32-packed // keep the generic pop below. Reg overflow loud- // stops (rule 7), the partial-spill stitch out of // scope (#163 twin). let nb: i32 = stfc & 15; let e: i32 = 0; for (e < nb) { let issse: bool = (stfc & (16 << e)) != 0; if (issse) { if (fpidx >= 8) { let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tMOVSD\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; } else { if (intidx >= 6) { let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; }; popped += 1; e += 1; }; } else { let extra: i32 = 0; // str IS []u8: 3-word arg, same as slice (#1/Phase 3). if (nodeisstr(c, a)) { extra = 2; }; 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"); }; // str IS []u8: a str-returning callee leaves AX=ptr, BX=len, // CX=cap — same as a slice, so there is no receive-side shuffle // (#1/Phase 3). 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.type_: *tinfo, 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"; }; }; }; }; }; }; // A slice IS the same 3-word {ptr,len,cap} // header as str (ref/hare/rt/ensure.ha:4-8), // so `*p = sliceval` takes str's stash+store // path (#79; precedent cgenstmt.ww:1631, // cgenexpr.ww:1684). LIKE str this is // alias-BLIND: a slice-alias `*Foo` / non-ident // deref-store stays 1-word, the SAME divergence // str carries; resolved-vs-syntactic detection // is unified UP in #80, not patched here. if (pe.kind == nkind.N_TSLICE) { elemstr = true; }; }; }; }; }; }; }; 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; }; // str IS []u8: PUSHQ AX (ptr) first, then // PUSHQ BX (len) + PUSHQ CX (cap) across the // pointer eval which clobbers BX/CX. Pop drains // cap (top) → 16(BX), then len, then ptr → 0(BX) // with len → 8(BX) (#1/Phase 3). emitline("\tPUSHQ\tAX\n"); if (elemstr) { emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tCX\n"); }; cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); if (elemstr) { emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 16(BX)\n"); 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). // #136: lift unsignd above the SLASHEQ block so // RSHIFTEQ can route SHRQ vs SARQ on the same key. let unsignd: bool = false; if (pe != nil) { unsignd = typeisunsigned(pe.type_: *tinfo); }; if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) { 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) { // #136: signed RSHIFTEQ → SARQ. if (unsignd) { combineop = "SHRQ"; } else { combineop = "SARQ"; }; }; }; }; }; }; }; }; }; 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) { // lhs.type_ is the checker-stamped element tinfo // of the N_INDEX: esz is its natural size and the // tagged-element gate (below) reads the same // .type_ — same idiom as cgindex's n.type_ read // (#60/#72). cstage idx_eff(base->type)->sub->size // (cmd/w6c/cgen.c:3517-18). let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { // Chained-write write-side parallel of the // cgindex N_INDEX-base arm (#24): `names[i][k] // = v` (names: **u8) — outer element is u8 so // the store is MOVB, not MOVQ. lhs.type_ is the // checker-stamped outer element tinfo; esz is // its natural size and the gate reads it via // .type_. Drops the indexvaluetnode walk // (#69/#61d, mirror #60). cstage: esz = // idx_eff(base->type)->sub->size // (cmd/w6c/cgen.c:3517-3518). let et: *tinfo = lhs.type_: *tinfo; if (et != nil) { esz = et.size: i32; elemtn = 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.type_: *tinfo, 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 // str/slice: spill cap (CX) + len (BX) before // computing the index so the post-index store can // pop all three. str=24B (#1/Phase 3) collides with // slice=24B, so this MUST gate on kind (cstage's // elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581), // never a bare esz==24: a >16B struct is also >=24B // but takes the struct-copy path, not this 3-word // {ptr,len,cap} store. Write-side mirror of the // cgindex read-path gate (#7/754). if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); }; // Float element: spill X0 (not AX — AX is junk for // floats) across the idx/base eval. A call-index // (a[geti()]=v) clobbers X0 and would otherwise lose // the value. Mirrors the *p=v float deref store // twin in cgassign (#125). let spisfloat: bool = isfloattype(c, elemtn); let spmov: str = "MOVSD"; if (isf32type(c, elemtn)) { spmov = "MOVSS"; }; if (spisfloat) { emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(spmov); emitline("\tX0, (SP)\n"); } else { 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 { if (dotbaseaddr(c, base, "BX")) { // #135: N_DOT base address-of-field inline. } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); // Reload value: float reloads X0 from the spill slot; // non-float pops AX. Twin of the value-spill site // above (#125). if (spisfloat) { emitline("\t"); emitline(spmov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); } else { emitline("\tPOPQ\tAX\n"); // value }; // str/slice: pop the saved len + cap and store // all three words. Kind-gate, not size — see the // spill site above (#1/Phase 3, #7/754). if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 16(BX)\n"); return; }; // float element → store FROM X0 (MOVSS/MOVSD): cgexpr // left the value in X0, and the value-spill pair // above keeps X0 live across the idx/base eval so // a call-index (a[geti()]=v) doesn't lose it (#125). // For f32 the #104 CVTSD2SS narrowing only touches X0, // so the AX store below would write raw double low- // bits, garbage for f32 (#122, mirrors cstage cgen.c // arr[i]= float store). if (isfloattype(c, elemtn)) { let fmov: str = "MOVSD"; if (isf32type(c, elemtn)) { fmov = "MOVSS"; }; emitline("\t"); emitline(fmov); emitline("\tX0, (BX)\n"); return; }; let isop: str = tnodestoreop(c, elemtn, esz); emitline("\t"); emitline(isop); emitline("\tAX, (BX)\n"); return; }; // Compound assign on an indexed scalar element // (`arr[i] OP= v`). Pre-#133 the outer `if (n.op == // TK_ASSIGN)` had no else and non-ASSIGN ops fell off // the cgassign function emitting NOTHING — silent // no-op. Mirror the chained-pointer-field compound // template at cmd/w6c/cgen.c:3281-3317: same address // computation as the ASSIGN arm above, then // tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop. // Float / str / slice / tagged element compound stays // unwired — cstage's compound template never carried // those payload kinds. Same shape gate as the cstage // branch (cgen.c #133). 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) { let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { let et: *tinfo = lhs.type_: *tinfo; if (et != nil) { esz = et.size: i32; elemtn = lhs; }; };};}; }; // #133-expanded: hard-error unwired payload kinds // LOUD (rule-7) — replaces prior silent skip. if (elemtn != nil) { if (istaggedtype(c, elemtn)) { let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isstrtype(c, elemtn)) { let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isslicetype(c, elemtn)) { let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isfloattype(c, elemtn)) { let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; }; 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"); }; emitline("\tPUSHQ\tAX\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 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 { if (dotbaseaddr(c, base, "BX")) { // #135: N_DOT base address-of-field inline. } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); emitline("\tADDQ\tAX, BX\n"); let lop: str = tnodeloadop(c, elemtn, esz); emitline("\t"); emitline(lop); emitline("\t(BX), AX\n"); emitline("\tPOPQ\tCX\n"); // #133-expanded: all 10 integer compound ops wired. // SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+ // DIVQ (unsigned). LSHIFTEQ via SHLQ; RSHIFTEQ via // SARQ (signed) or SHRQ (unsigned) per #136. // Signedness from elemtn.type_. let unsignd_c: bool = false; if (elemtn != nil) { if (elemtn.type_ != nil) { unsignd_c = typeisunsigned(elemtn.type_: *tinfo); }; }; let wired: bool = false; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_SLASHEQ) { if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; wired = true; }; if (n.op == tkind.TK_PERCENTEQ) { if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; emitline("\tMOVQ\tDX, AX\n"); wired = true; }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_RSHIFTEQ) { if (unsignd_c) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired = true; }; if (!wired) { let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let sop: str = tnodestoreop(c, elemtn, esz); emitline("\t"); emitline(sop); 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/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the index/address // computation (IMULQ's CX scratch // clobbers cap), stage &arr[i] in DX // off the str AX/BX/CX convention // (mirrors s.f=v), then store the full // triple at foff+0/+8/+16. if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); 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), DX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); }; emitline("\tADDQ\tAX, DX\n"); if (viaptr) { emitline("\tMOVQ\t(DX), DX\n"); }; emitline("\tPOPQ\tAX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\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; }; // 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.type_: *tinfo, 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 the ABI size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize (cstage // SSoT lu->size, check.c:760; cgen.c:7720 // sz=lu->size at the receive twin). 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 = structabisize(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) { cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", fi.foff); 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"); }; }; if (n.op == tkind.TK_ASSIGN) { // str/slice field via *struct: str IS []u8, so both // store the full 3-word {ptr,len,cap} from (AX,BX,CX). // CX holds cap, so stage the struct addr in DX and // store at foff/+8/+16 (#1/Phase 3). if (isstrtype(c, fi.tnode) || 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.type_: *tinfo, 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 = structabisize(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) { cgstructlitfill(c, ssi, n.rhs, 0, 0, "", lc.off + fi.foff); 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/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop // below would write only AX, dropping .len/.cap (#1/Phase 3). if (isstrtype(c, fi.tnode) || 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 = structabisize(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) { cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, fi.foff); 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)) { // str IS []u8: cgexpr left // (AX=ptr, BX=len, CX=cap). CX // holds cap, so stage the base // addr in DX and store all three // words (#1/Phase 3). emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), 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 `=` on global struct field: value is // in X0; LEAQ the base into BX and MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // Compound on scalar field: load // → push → eval rhs → combine → // store. cgexpr clobbers BX, so // re-LEAQ for the store. let lop: str = fieldloadop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; }; }; }; }; // Chained `.field = v` where `` itself is a chain // of dots resolving to a *struct. Mirrors the C cgen branch // added to close trap 1 (cmd/w6c/cgen.c). Without this, only // `local.field = v` and `local.fieldptr.field = v` get wired // (the latter through the IDENT-base branch above) — chains // like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no // store. Only plain `=` is wired here; chained compound on a // pointer-field hasn't surfaced. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_DOT) { // #70 (#12): inner-struct layout via the stamped // base.type_ (peel *→struct) + tinfo.fields, // replacing dotinnerstructptr's structinfo walk. // Gate is strict-equal to the deleted helper: fire // only when the chain root is a LOCAL ident AND every // dot resolves through a *struct (dotinnerstructptr // recursed per level on a *struct field, bailing on a // by-value-struct intermediate). Reproducing that // exactly avoids an untested widening past cstage. // Global-root chains stay in their pre-existing shared // base-eval breakage (filed #27). let croot: *node = base; let allptr: bool = true; for (croot != nil && croot.kind == nkind.N_DOT) { let ct: *tinfo = croot.type_: *tinfo; for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; }; let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; }; if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr) { if (croot != nil) { if (croot.kind == nkind.N_IDENT) { if (localfindnode(c, croot.str) != nil) { it = base.type_: *tinfo; }; }; }; }; for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; }; if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { let tf: *tfield = st.fields; for (tf != nil) { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; if (n.op == tkind.TK_ASSIGN) { if (typeisstr(ft) || typeisslice(ft)) { // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the base-expr eval // (it may clobber any reg), stage the // *struct ptr in DX off the str // AX/BX/CX convention (mirrors s.f=v), // then store the full triple at // foff+0/+8/+16. cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, DX\n"); emitline("\tPOPQ\tAX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(tf.offset: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((tf.offset + 8u64): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((tf.offset + 16u64): i64, "DX"); 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 (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { 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(tf.offset: 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 = tnodestoreop(c, n.rhs, ft.slotsize: i32); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(tf.offset: i64, "BX"); emitline("\n"); return; }; // #133-expanded site 3: chained-pointer- // field compound. Pre-#133-expanded the // wwstage chained-DOT-spine branch only // handled TK_ASSIGN; compound ops on a // chained-*struct.field shape (e.g. // `d.i.v += 7`) silently emitted nothing. // cstage cgen.c:3281-3317 handles this // (now-expanded for the same 10 ops + // hard-errors); this is its rule-10 twin. // All 10 integer compound ops wired; // float/str/slice/tagged field-type // hard-errors LOUD. Signed RSHIFTEQ uses // SARQ (signed) or SHRQ (unsigned) per #136. if (n.op != tkind.TK_ASSIGN) { if (typeisstr(ft)) { let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeisslice(ft)) { let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeisfloat(ft)) { let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeistagged(ft)) { let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tPUSHQ\tAX\n"); let fsz: i32 = ft.slotsize: i32; let unsignd_f: bool = typeisunsigned(ft); let lopf: str = loadopsz(!unsignd_f, fsz); emitline("\t"); emitline(lopf); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); let wired_f: bool = false; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_SLASHEQ) { if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; wired_f = true; }; if (n.op == tkind.TK_PERCENTEQ) { if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; emitline("\tMOVQ\tDX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_RSHIFTEQ) { if (unsignd_f) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired_f = true; }; if (!wired_f) { let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let sopf: str = tnodestoreop(c, n.rhs, fsz); emitline("\t"); emitline(sopf); emitline("\tAX, "); emitdispreg(tf.offset: i64, "BX"); emitline("\n"); return; }; }; tf = tf.tnext; }; }; }; }; }; }; }; }; }; // 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 leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let yok: bool = dotchainresolve(c, lhs, &rootname, &rootoff, &totaloff, &leaftype, &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 (typeisstr(leaftype) || typeisslice(leaftype)) { // str/slice: store ptr/len/cap. cgexpr leaves // CX=cap, so the viacx base goes in DX (not CX) to // avoid clobbering it — same as the single-dot str // field store (#1/Phase 3). cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), DX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), DX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg(totaloff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((totaloff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((totaloff + 16): i64, "DX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((rootoff + totaloff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((rootoff + totaloff + 16): 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. // #71: the struct-terminal cases below still drive the // structinfo machinery (structnaturalsize / // cgstructlitfill), so recover the struct NAME from the // leaf tinfo's TY_NAMED wrapper. Peeled-TY_STRUCT + // structlookup!=nil is byte-equal to the old `N_TNAME && // primsize==0 && structlookup` guard: a named non-struct // (tagged/alias) peels to a non-STRUCT kind, and // structlookup decides struct-ness off the same declared // name either way. let leafstruct: bool = false; let leafname: str = ""; if (leaftype != nil) { let lp: *tinfo = leaftype; for (lp != nil && lp.kind == tykind.TY_NAMED) { lp = lp.under; }; if (lp != nil) { if (lp.kind == tykind.TY_STRUCT) { leafstruct = true; }; }; if (leaftype.kind == tykind.TY_NAMED) { leafname = leaftype.name; }; }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize // (cstage SSoT lu->size, check.c:760). let lsz: i32 = structabisize(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 && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { 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); return; }; }; if (n.rhs != nil && n.rhs.kind == nkind.N_IDENT && leafstruct) { let ssi: *structinfo = structlookup(c, leafname); 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 (typeisfloat(leaftype)) { let mov: str = "MOVSD"; if (typeisf32(leaftype)) { 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; }; // Scalar leaf store-op by size — the same size→op // dispatch fieldstoreop used on the leaf fieldinfo, now // keyed on the leaf tinfo's slot width (#71). let sop: str = "MOVQ"; if (leaftype != nil) { let ssz: i32 = leaftype.slotsize: i32; if (ssz == 1) { sop = "MOVB"; } else { if (ssz == 2) { sop = "MOVW"; } else { if (ssz == 4) { sop = "MOVL"; }; }; }; }; 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) { // str/slice top-level let: str IS []u8, so both store the // full 3-word {ptr,len,cap}. Stash cap in DI before LEAQ // overwrites CX, then store ptr/len/cap via &name(SB) // (#1/Phase 3). if (letvarisstr(c, nm) || 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) { // #136: signed RSHIFTEQ → SARQ. let unsignd_r: bool = false; if (lvftn != nil) { if (lvftn.type_ != nil) { unsignd_r = typeisunsigned(lvftn.type_: *tinfo); }; }; if (!unsignd_r) { unsignd_r = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); } else { emitline("\tSARQ\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 = false; if (lvftn != nil) { unsignd = typeisunsigned(lvftn.type_: *tinfo); }; 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"); }; // str IS []u8: store the cap word too, identical to // the slice store (#1/Phase 3). if (lcstr || 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) { // #136: signed RSHIFTEQ → SARQ. let unsignd_r: bool = false; if (lcn != nil) { if (lcn.tnode != nil) { if (lcn.tnode.type_ != nil) { unsignd_r = typeisunsigned(lcn.tnode.type_: *tinfo); }; }; }; if (!unsignd_r) { unsignd_r = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); } else { emitline("\tSARQ\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) { if (lcn.tnode != nil) { unsignd = typeisunsigned(lcn.tnode.type_: *tinfo); }; }; 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 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; }; // #83: positional tuple register-return ABI. Tuple elements ride // consecutive eightbytes over [AX,DX,CX,R8] (tupreg by index); a // slice/str rides its 3-word {ptr,len,cap} header (tyslicesize SSoT, // ref/hare/rt/ensure.ha:4-8), a scalar rides 1. SEND (cgreturn) and // RECEIVE (cgmlet/cgmassign) walk the SAME widths so element->register // agrees — mirrors harec create_unpack_bindings // (ref/harec/src/check.c:1354-1416). Capacity is 4 (AX,DX,CX,R8). fn tupreg(i: i32) str = { if (i == 0) { return "AX"; }; if (i == 1) { return "DX"; }; if (i == 2) { return "CX"; }; return "R8"; }; // #164 (#107): SSE half of the SysV dual register-class return. A float // element rides the SSE row [X0,X1] on a counter INDEPENDENT of the // INTEGER row tupreg — a float lands in the next XMM regardless of its // positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX}, // {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage // tuple_sse_seq (cmd/w6c/cgen.c). fn tupsse(i: i32) str = { if (i == 0) { return "X0"; }; return "X1"; }; fn tupebytes(wide: bool) i32 = { if (wide) { return (tyslicesize() / 8i64): i32; }; return 1; }; // rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil). // wwstage has no checker, so the receive sites read each tuple element's // width from the called fn's declared return type. Mirrors the callee // resolution shared by cgmlet/cgmassign. fn rettupleof(c: *cgen, rhs: *node) *node = { if (rhs == nil) { return nil; }; if (rhs.kind != nkind.N_CALL) { return nil; }; let callee: *node = rhs.lhs; if (callee == nil) { return 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) { return nil; }; let rtyp: *node = fnretlookupmod(c, cnm, cmod); if (rtyp == nil) { return nil; }; if (rtyp.kind != nkind.N_TTUPLE) { return nil; }; return rtyp; }; // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive // INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a // scalar stores 1 INTEGER word. The caller owns the dual cursor // (validated + advanced). Byte-identical to the cstage tuple_store // (cmd/w6c/cgen.c). fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 0)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 1)); emitline(", "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); emitline(tupreg(gpcur + 2)); emitline(", "); emitoff((off + 16): i64); emitline("(BP)\n"); return; }; // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg // (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else // the slot gets garbage and the FACE-Z field read sees it. The SSE // regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105 // single-float byte-id; idx1=X1 is the #107 multi-float extension. if (isfloattype(c, tn)) { // #121 (Package B) RESIDUAL sibling-evidence guard, pin form. // In destructure mode tn IS the tuple-element-type-AST node // (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value // stored" rides X0 with no separate AST. isfloattype(c, tn) at // the branch head already implies tn.type_!=nil (typeisfloat is // false on nil), so this assertion is structurally unreachable // today — RETAINED to PIN the contract: "the float-store branch // requires a stamped slot." Catches a future change that opens // this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple // element binding where the destructure stamp didn't land — // #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/ // 1475 + asserttyped file:line at check.ww:3340-3344. if (tn != nil) { if (tn.type_ == nil) { let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at "; os.write(2, msg.ptr, msg.len: u64); if (tn.file.len > 0) { os.write(2, tn.file.ptr, tn.file.len: u64); os.write(2, ":".ptr, 1u64); let ls: str = strconv.i32tos(tn.line, strconv.base.DEC); os.write(2, ls.ptr, ls.len: u64); os.write(2, " ".ptr, 1u64); }; let kn: str = nkname(tn.kind); os.write(2, kn.ptr, kn.len: u64); os.write(2, "\n".ptr, 1u64); os.exit(1); }; }; let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitline(tupsse(ssecur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\tMOVQ\t"); emitline(tupreg(gpcur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); }; fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; if (rhs != nil) { // #83 / #164 (#107): positional register-return over a SysV // dual class cursor (harec create_unpack_bindings, ref/harec/src/ // check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 = // tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8] // (tupreg) — a slice/str its 3-word {ptr,len,cap} header // (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a // scalar 1 word in AX. Integer words spill L->R to the stack and // pop into the INTEGER cursor in reverse so positional slot i // lands in tupreg(i) (byte-id with #83 when no float is present). // Each float must spill X0 to @tupfscr as we walk, since a later // element's cgexpr clobbers X0; after the integer pops the saved // floats reload into X0/X1 by SSE index — INDEPENDENT of the // INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows // loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME // class split drives the receive sites. if (rhs.kind == nkind.N_TUPLE) { let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; let e: *node = rhs.list; for (e != nil) { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); gptotal = gptotal + tupebytes(wide); }; e = e.next; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage // uses fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssecount > ssecap) { let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let fscr: i32 = 0; if (ssecount > 0) { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; e = rhs.list; for (e != nil) { let isflt: bool = isfloattype(c, e); cgexpr(c, e); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((fscr + sseidx * 8): i64); emitline("(BP)\n"); sseidx = sseidx + 1; } else { emitline("\tPUSHQ\tAX\n"); // scalar / .ptr if (nodeisstr(c, e) || nodeisslice(c, e)) { emitline("\tPUSHQ\tBX\n"); // .len emitline("\tPUSHQ\tCX\n"); // .cap }; }; e = e.next; }; let i: i32 = gptotal - 1; for (i >= 0) { emitline("\tPOPQ\t"); emitline(tupreg(i)); emitline("\n"); i = i - 1; }; let j: i32 = 0; e = rhs.list; for (e != nil) { if (isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((fscr + j * 8): i64); emitline("(BP), "); emitline(tupsse(j)); emitline("\n"); j = j + 1; }; e = e.next; }; 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 rtyp: *node = fnretlookupmod(c, calleename, cmod); if (istaggedtype(c, rtyp)) { 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.type_: *tinfo, 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); // Value-class read off the checker stamp (rhs.type_) — // the SSoT cstage reads via node_isfloat / type_isf32. let rfk: i32 = 0; if (rhs != nil) { let rety: *tinfo = rhs.type_: *tinfo; if (typeisf32(rety)) { rfk = 1; } else { if (typeisfloat(rety)) { rfk = 2; }; }; }; 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)) { // str IS []u8: cgexpr leaves (AX=ptr, BX=len, // CX=cap). Same shuffle as the slice arm above — // DX=ptr, CX=len, R8=cap (#1/Phase 3). emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); } else { if (rfk != 0) { // #157: float variant — cgexpr left the value // in X0, not AX. No MOVQ-xmm->gp encoding, so // bridge X0->DX through a stack slot (same arg- // push idiom). Zero the slot first so the f32 // case (MOVSS writes only the low 4 bytes) // leaves a deterministic high-4 — cs==ww byte- // id, matching f64's MOVSD which fills all 8. // The AX-independent spill also removes the // stale-AX cs!=ww on multi-variant returns. emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let mov: str = "MOVSD"; if (rfk == 1) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); emitline("\tMOVQ\t(SP), DX\n"); emitline("\tADDQ\t$8, SP\n"); if (rsz > 16) { emitline("\tMOVQ\t$0, CX\n"); }; 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); }; } 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) { // ≤24B register RETURN: cstage sizes by rt->size // (maxalign-rounded), not the slot-padded totsize // (round-to-8) — see structabisize (#169). let rsz: i32 = structabisize(rsi); 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; }; }; }; // #171a: float-bearing struct RETURN (return // twin of #165's param recv). A qualifying // struct's float eightbytes ride the SSE return // row (X0,X1 = tupsse), its INT eightbytes the // INTEGER return row (AX,DX = tupreg), on // INDEPENDENT cursors per SysV (ref/qbe/amd64/ // sysv.c retr) — so a float lands in the next // XMM regardless of its positional eightbyte // (struct{f64,i32}: e0→X0, e1→AX, NOT DX). The // scratch is zero-padded to 24B so a full MOVQ // on a trailing INT eightbyte reads no garbage // (the #169 sized tail is a RECV concern). // structfloatclass gates to qualifying structs; // all-int + f32 keep the AX/DX/CX transport // (byte-id / #171b). let sfc: i32 = structfloatclass(c, c.fnret); if (sfc != 0) { let nb: i32 = sfc & 15; let gpcur: i32 = 0; let ssecur: i32 = 0; let e: i32 = 0; for (e < nb) { let issse: bool = (sfc & (16 << e)) != 0; if (issse) { emitline("\tMOVSD\t"); emitoff((scroff + e*8): i64); emitline("(BP), "); emitline(tupsse(ssecur)); emitline("\n"); ssecur += 1; } else { emitline("\tMOVQ\t"); emitoff((scroff + e*8): i64); emitline("(BP), "); emitline(tupreg(gpcur)); emitline("\n"); gpcur += 1; }; e += 1; }; } else { 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"); }; // str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now // returns exactly like a slice, no AX:DX shuffle (#1/Phase 3). 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; // `let s: []T = alloc([], n)!;` / `?` shortcut (#32, #45). // Mirror of cstage cgen.c N_LET arrlit-empty branch: allocate // n*esz bytes via rt_malloc, then build the {ptr, 0, n} slice // header in the let slot. The `!`/`?` wraps the builtin's // `([]T | nomem)` return; walk into the N_TRYUNW / N_TRYPROP // to keep the direct-store fast path rather than falling // through to cgalloc (which models scalar alloc and would // land an 8B region and a junk slice header). `?` propagates // nomem via AX = tag of nomem in c.fnret, then epilogue RET. { let scall: *node = nil; let viatryunw: bool = false; let viatryprop: bool = false; if (rhs.kind == nkind.N_TRYUNW) { if (rhs.lhs != nil) { if (rhs.lhs.kind == nkind.N_CALL) { scall = rhs.lhs; viatryunw = true; }; }; } else { if (rhs.kind == nkind.N_TRYPROP) { if (rhs.lhs != nil) { if (rhs.lhs.kind == nkind.N_CALL) { scall = rhs.lhs; viatryprop = true; }; }; }; }; let shapeok: bool = false; // #43: route the slice-shape size guard through SSoT. // The N_TSLICE kind gate already discriminates here, so // this is belt-and-suspenders, but the literal would // silently miss after #1 if check.ww's astsize ever // drifted from this dispatch. if (scall != nil && tn != nil && tn.kind == nkind.N_TSLICE && sz == tyslicesize(): i32) { let callee: *node = scall.lhs; let a0: *node = scall.list; let a1: *node = nil; let a2: *node = nil; if (a0 != nil) { a1 = a0.next; }; if (a1 != nil) { a2 = a1.next; }; if (callee != nil && a0 != nil && a1 != nil && a2 == nil) { if (callee.kind == nkind.N_IDENT && streq(callee.str, "alloc") && a0.kind == nkind.N_ARRLIT && a0.list == nil) { shapeok = true; }; }; }; if (shapeok) { // #32: cstage uses `lu->sub->size` (cgen.c:6387), so // the element width must resolve struct/tagged/alias // names too — not just primitives. elemsizeofc follows // TNAME through structlookup/aliaslookup, matching the // cstage path byte-identically. A bare primsize/slotsize // fork would silently land esz=1 on `[]point`. let esz: i32 = elemsizeofc(c, tn); let count: *node = scall.list.next; cgexpr(c, count); emitline("\tPUSHQ\tAX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", BX\n"); emitline("\tIMULQ\tBX, AX\n"); }; emitline("\tMOVQ\tAX, DI\n"); emitline("\tCALL\t"); emitline(ffiresolve(c, "malloc")); emitline("(SB)\n"); if (viatryunw) { let okl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); emitline("\tMOVQ\t$1, DI\n"); emitline("\tMOVQ\t$60, AX\n"); emitline("\tSYSCALL\n"); emitlabel(okl); }; if (viatryprop) { // #45: null = nomem; propagate to the // enclosing fn's tagged return. AX = tag // of nomem variant in c.fnret, epilogue // RETs to caller. let okl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); // #66 Phase-N step 3: nomem propagation has no // pattern node, so it can't ride the typeeq // flatvariantidx path. cstage passes the ty_nomem // singleton to cg_tag_for_variant; the wwstage cgen // holds no tinfo singleton, so find the nomem // variant by its NAMED name over tinfo.params. let nidx: i32 = -1; let nti: *tinfo = nil; if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; }; for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; }; if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) { let np: *tparam = nti.params; let nidx2: i32 = 0; for (np != nil) { let nvt: *tinfo = np.type_; if (nvt != nil) { if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; }; np = np.tnext; nidx2 += 1; }; }; }; if (nidx < 0) { nidx = 1; }; emitline("\tMOVQ\t$"); emitint(nidx: i64); emitline(", AX\n"); emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(okl); }; emitline("\tPOPQ\tBX\n"); emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t$0, "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + 16): i64); emitline("(BP)\n"); c.lastwasreturn = 0; return; }; }; // 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.type_: *tinfo, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; // 32B tuple init for `let t: (scalar, str) = call()` / // `let t: (str, scalar) = call()` (#105 / #164/#107). Each // element rides its SysV class — a float its SSE cursor reg // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg // (tupreg), a slice/str its 3-word {ptr,len,cap} header over // consecutive INTEGER cursor regs — on INDEPENDENT counters. // tupstore routes each element from its real class into its // positional slot (eoff steps by the element's slot size: a // slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple // (#1/Phase 3, task #5). Mirror of the cstage unified branch. 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 p0t: *node = nil; let p1t: *node = nil; if (p0 != nil) { p0t = p0.lhs; }; if (p1 != nil) { p1t = p1.lhs; }; let s0_is_str: bool = isstrtype(c, p0t) || isslicetype(c, p0t); let s1_is_str: bool = isstrtype(c, p1t) || isslicetype(c, p1t); if (p0 != nil) { if (p1 != nil) { if (s0_is_str != s1_is_str) { cgexpr(c, rhs); let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *node = n.lhs.list; for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); let wide: bool = isstrtype(c, qt) || isslicetype(c, qt); tupstore(c, gpcur, ssecur, off + eoff, wide, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; if (wide) { eoff = eoff + (tyslicesize(): i32); } else { eoff = eoff + 8; }; q = q.next; }; c.lastwasreturn = 0; return; }; }; }; }; }; // 16B tuple init from a function call (#105 / #164/#107). Each // eightbyte rides its SysV class: a float its SSE cursor reg // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg // (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves // floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ // spill would store garbage where a float rode and the #103- // FACE-Z field read (MOVSD-from-slot) would see it. tupstore // routes each word from its real class; the same split drives // the destructure / reassign sites. Without this branch a 16B // tuple receive fell to the generic single-word store below and // dropped word1 — silent loss of t.1 (#102). let rt16: *node = rettupleof(c, rhs); if (rt16 != nil && sz == 16) { cgexpr(c, rhs); let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *node = rt16.list; for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); tupstore(c, gpcur, ssecur, off + eoff, false, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + 1; }; eoff = eoff + 8; q = q.next; }; 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 = primtypesize("str"): i32; isstrel = true; } else { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; }; let mop: str = tnodestoreop(c, elemn, esz); // float element → store FROM X0 (MOVSS/MOVSD): cgexpr // leaves a float in X0 and for f32 the #104 CVTSD2SS // narrowing only touches X0; the AX store (mop) would // write the raw double low-bits, garbage for f32 (#122, // mirrors cstage cgen.c:6889 arr-lit float store). let isfloatel: bool = isfloattype(c, elemn); let fmov: str = "MOVSD"; if (isf32type(c, elemn)) { fmov = "MOVSS"; }; 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 { if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((off + idx * esz): 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 { if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((off + idx * esz): 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. // #171a: float-bearing struct RECEIVE (return twin of #165's // param recv). cgexpr leaves each float eightbyte in its SSE // return reg (X0,X1 = tupsse) and each INT eightbyte in its // INTEGER return reg (AX,DX = tupreg), on INDEPENDENT cursors // per SysV (ref/qbe/amd64/sysv.c retr) — so a float is read // from the next XMM regardless of its positional eightbyte // (struct{f64,i32}: e0←X0, e1←AX). A qualifying struct's // abisize is maxalign-rounded to a multiple of 8 (an f64 // forces align 8), so every eightbyte is a full word — the // #169 sized tail is unreachable here. structfloatclass gates // to qualifying structs; all-int + f32 fall to the GP recv // below (byte-id / #171b). if (rhs.kind == nkind.N_CALL && tn != nil) { let sfc: i32 = structfloatclass(c, tn); if (sfc != 0) { cgexpr(c, rhs); let nb: i32 = sfc & 15; let gpcur: i32 = 0; let ssecur: i32 = 0; let e: i32 = 0; for (e < nb) { let issse: bool = (sfc & (16 << e)) != 0; if (issse) { emitline("\tMOVSD\t"); emitline(tupsse(ssecur)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); ssecur += 1; } else { emitline("\tMOVQ\t"); emitline(tupreg(gpcur)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); gpcur += 1; }; e += 1; }; c.lastwasreturn = 0; return; }; }; 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) { // ≤24B register RECV: the value arrives packed // in AX/DX/CX, so size by the maxalign-rounded // ABI size (cstage lu->size), not the natural // extent — see structabisize (#169). let lsz: i32 = structabisize(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 IS []u8: cgexpr leaves (ptr,len,cap) in AX/BX/CX; store // all three, same as the slice arm below (#1/Phase 3). // #60: gate by kind too — under #1's str=24 bump, sizeof(str) // and sizeof(slice) collide, so a bare `sz ==` check fires // both branches for one let. Mirrors cstage cgen.c's // `type_isstr(lt) && sz == ty_str->size` shape. if (isstrtype(c, tn) && sz == primtypesize("str"): i32) { emitline("\tMOVQ\tBX, "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + 16): i64); emitline("(BP)\n"); }; // slice init: ptr/len/cap in AX/BX/CX. Same kind+size gate as // the str arm — without the kind check this fires on a str let // once sz==24 (#60). if (isslicetype(c, tn) && sz == tyslicesize(): i32) { 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; }; }; // Zero-fill extent. cstage sizes the run on `lu->size` // (the maxalign-rounded ABI size, check.c:760); wwstage's // `sz` from letslotsize is slot-padded (round-to-8), so a // struct with maxalign<8 and a sub-8 tail would over-zero // MOVQ where cstage emits MOVL/MOVB. Read structabisize // for a struct-typed let to converge; slot allocation // stays on `sz` (frame uses slot-padded slots). let zsz: i32 = sz; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { zsz = structabisize(szi); }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (!isarr) { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= zsz) { emitline("\tMOVQ\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 8; }; for (zi + 4 <= zsz) { emitline("\tMOVL\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 4; }; for (zi < zsz) { emitline("\tMOVB\tAX, "); emitoff((off + zi): i64); emitline("(BP)\n"); zi += 1; }; }; }; }; }; c.lastwasreturn = 0; return; }; fn cgif(c: *cgen, n: *node) void = { let els: str = mklabel(c, "else"); let endl: str = mklabel(c, "end"); cgexpr(c, n.cond); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); if (n.els != nil) { emitline(els); } else { emitline(endl); }; emitline("\n"); if (n.body != nil) { cgstmt(c, n.body); }; if (n.els != nil) { emitline("\tJMP\t"); emitline(endl); emitline("\n"); emitlabel(els); cgstmt(c, n.els); }; emitlabel(endl); c.lastwasreturn = 0; return; }; fn cgfor(c: *cgen, n: *node) void = { // Match C cgen's label scheme: _loop_N for the top, // _endloop_N for the post-body merge. No separate cont // label when there's no post-expression. let topl: str = mklabel(c, "loop"); let endl: str = mklabel(c, "endloop"); // `else` runs at natural cond-false exit; break skips it. When // present, branch the cond-fail edge to a separate natural_exit // label so the else body sits between it and the break target. let naturall: str = endl; if (n.els != nil) { naturall = mklabel(c, "elseloop"); }; // #138: `continue` in a 3-clause `for (init; cond; post)` must // run the post-step before re-testing cond. Pre-fix the continue- // target was `topl`, which SKIPPED the post-step → state never // advanced → infinite loop. Allocate a dedicated `post` label // only when there IS a post-step (`n.rhs != nil`); else keep // continue → loop-top, byte-id with 1-clause for. let conttgt: str = topl; if (n.rhs != nil) { conttgt = mklabel(c, "post"); }; 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] = conttgt; c.looptop += 1; if (n.body != nil) { cgstmt(c, n.body); }; c.looptop -= 1; if (n.rhs != nil) { emitlabel(conttgt); 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 = { // #83: positional per-element destructure REASSIGN. Same cursor as // cgmlet (and cgreturn; harec create_unpack_bindings, // ref/harec/src/check.c:1354-1416), but the slots already exist // (reassignment) so localfind them. wwstage has no checker, so each // element's width comes from the called fn's return-type tuple // element (N_TTUPLE param) walked in lockstep with the bindings; a // slice/str rides its 3-word {ptr,len,cap} header // (ref/hare/rt/ensure.ha:4-8). A missing/non-ident binding consumes // its register slot without storing (mirrors harec `_`). This bare- // comma `a, s = f()` multi-assign is a retained ww-EXTENSION beyond // Hare (Hare tuple-unpack is binding-only); ww keeps the Go/rob-pike // multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops. let rettuple: *node = rettupleof(c, n.rhs); if (n.rhs != nil) { cgexpr(c, n.rhs); }; let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); gptotal = gptotal + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssetotal > ssecap) { let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gpcur: i32 = 0; let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; // harec `_` (off==0): skip the store but CONSUME the cursor // slot so the next element stays aligned. if (off != 0) { tupstore(c, gpcur, ssecur, off, wide, tn); }; if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; 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:R8 return convention (mirrors C cgen nkind.N_MLET): // (scalar, scalar) — AX → l0, DX → l1. // (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot // as (.ptr, .len, .cap). Position-agnostic — the // regs are routed by element type, not by AX/DX. // str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5). fn cgmlet(c: *cgen, n: *node) void = { let rhs: *node = n.rhs; if (rhs == nil) { return; }; // #83: positional per-element destructure let-binding. Same cursor // as cgmassign (and cgreturn; harec create_unpack_bindings, // ref/harec/src/check.c:1354-1416). wwstage has no checker, so each // binding's type is its explicit annotation (l.lhs) when present, // else the called fn's return-type tuple element (N_TTUPLE param) // walked in lockstep. A slice/str rides its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) into a header-sized slot; a // scalar rides 1 word into an 8B slot. Over-capacity loud-stops. let rettuple: *node = rettupleof(c, rhs); cgexpr(c, rhs); let ssecap: i32 = 2; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); gptotal = gptotal + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > 4) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (ssetotal > ssecap) { let msg: str = "tuple destructure exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gpcur: i32 = 0; let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = l.lhs; if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); tupstore(c, gpcur, ssecur, off, wide, tn); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + tupebytes(wide); }; l = l.next; if (pt != nil) { pt = pt.next; }; }; 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 primtypesize("str"): i32; }; 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; }; // str IS []u8 (F1: tystr.sub = tyu8). []u8 hands cgen a real // u8 element node (slctn.lhs); a str scrutinee has none, so the // loop var would register tnode=nil and read back as a wide // MOVQ. Synthesise the u8 element off str.sub so the loop-var // registration carries a u8 tnode and localloadop narrows the // read-back to MOVZBQ on its own — aligning wwstage up to // cstage, whose checker stamps the binding u8. Kind-gated so // str's own type stays nominal. if (sk == nkind.N_TNAME) { if (streq(slctn.str, "str")) { let sti: *tinfo = slctn.type_: *tinfo; if (sti != nil) { if (sti.sub != nil) { let u8n: *node = newnode(nkind.N_TNAME, slctn.file, slctn.line, slctn.col); u8n.str = "u8"; u8n.type_ = sti.sub: *void; elemt = u8n; }; }; }; }; }; // 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.lhs); 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; // tp walks the N_TPARAM wrapper chain; tpt is the // actual element type AST. let tpt: *node = nil; if (tp != nil) { tpt = tp.lhs; }; if (tpt != nil) { fsz = paramfieldsize(tpt); signf = paramissigned(c, tpt); }; 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, tpt); } else { bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tpt); }; 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"); }; // #138 (range form): `continue` must run the implicit `i+=1` // post-step before re-testing the bound. Pre-fix cont = loopl // (top), skipping the ADDQ $1, ioff below — infinite loop on // the value that triggered continue. Dedicated `rpost` label. let rpost: str = mklabel(c, "rpost"); c.loopcontbuf[c.looptop] = rpost; 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; emitlabel(rpost); 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 cgoutstate 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 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). p.lhs is already // the []T wrap installed by check.ww installparams // (mirrors cstage check.c:455 tp->type promotion), so // we consume it directly — re-wrapping via slicewrap // would yield [][]T. if (p.op == tkind.TK_ELLIPSIS) { let tn: *node = p.lhs; if (idx + 3 <= 6) { let off: i32 = localadd(c, nm, tyslicesize(): i32, 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, tyslicesize(): i32, 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 (p.lhs != nil) { if (p.lhs.kind == nkind.N_TTUPLE) { // #163: tuple PARAM receive (param twin of #164's // return). Walk the tuple's elements over the SysV // arg cursor — a float reads its XMM (X0..X7), // everything else an INTEGER arg reg (DI/SI/..); a // slice/str its 3-word {ptr,len,cap} — storing each // into the param slot positionally (eoff steps by // slotsize, matching the t.0/t.1 field-access walk + // the SEND). Reg overflow loud-stops (rule 7); the // partial-spill stitch is out of scope (twin of #164). let off: i32 = localadd(c, nm, slotsize(c, p.lhs), p.lhs); let eoff: i32 = 0; let te: *node = p.lhs.list; for (te != nil) { let et: *node = te.lhs; if (isfloattype(c, et)) { if (fidx >= 8) { let msg: str = "tuple param float element overflows SSE arg regs (X0..X7); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let mov: str = "MOVSD"; if (isf32type(c, et)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitline(fargregname(fidx)); emitline(", "); emitoff((off + eoff): i64); emitline("(BP)\n"); fidx += 1; } else { let wide: bool = isstrtype(c, et) || isslicetype(c, et); let eb: i32 = tupebytes(wide); if (idx + eb > 6) { let msg: str = "tuple param element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let k: i32 = 0; for (k < eb) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + eoff + k*8): i64); emitline("(BP)\n"); idx += 1; k += 1; }; }; eoff += slotsize(c, et); te = te.next; }; 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; }; let sfc: i32 = structfloatclass(c, p.lhs); if (sfc != 0) { // #165: float-bearing struct PARAM receive (param // twin of #163's tuple). Classify each SysV // eightbyte; a lone-f64 eightbyte reads its XMM // (X0..X7), a pure-INT eightbyte its INTEGER arg reg // (DI/SI/..), stored into the param slot at the // 8-byte eightbyte stride. Gated to qualifying // structs by structfloatclass — all-int + f32-packed // fall through to the GP struct arm below (byte-id / // #165b). Reg overflow loud-stops (rule 7). let off: i32 = localadd(c, nm, structparamsize(c, p.lhs), p.lhs); let nb: i32 = sfc & 15; let e: i32 = 0; for (e < nb) { let issse: bool = (sfc & (16 << e)) != 0; if (issse) { if (fidx >= 8) { let msg: str = "float struct param eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tMOVSD\t"); emitline(fargregname(fidx)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); fidx += 1; } else { if (idx >= 6) { let msg: str = "float struct param eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + e*8): i64); emitline("(BP)\n"); idx += 1; }; e += 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, tyslicesize(): i32, 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, tyslicesize(): i32, 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 + 3 <= 6) { // str IS []u8: 3-word param (ptr,len,cap), same as // the slice arm above (#1/Phase 3). #60: route slot // width through the primtypesize SSoT so #1's ty_str // bump propagates here. let off: i32 = localadd(c, nm, primtypesize("str"): i32, 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 the slice arm above. // #60: same SSoT routing as the regs-fit arm above. let off: i32 = localadd(c, nm, primtypesize("str"): i32, 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 { 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.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 cgoutstate 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(); 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 (- ! ~ & *), // 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 ast; import tok; import typ; import sym; import strconv; import io; import memio; // 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; // #29: seed `type nomem = !void;` here AS WELL AS in check.ww's // seedprimitives. The two seeds aren't redundant: wwstage's check // owns c.top (used by name resolution); cgen owns its own // c.aliases chain (used by resolvetype / slotsize / TBANG checks). // Without this seed, resolvetype("nomem") returns the raw N_TNAME // — slotsize falls through to 8B without zero-init, diverging from // cstage's `let e: nomem;` MOVQ $0 emit on the slot (rule 10). // Inserted at the head so the user-decl loop below prepends; the // same-module / any-match passes in aliaslookup then let a local // `type nomem = !void;` shadow this fallback within its module. let empty: str; let tnvoid: *node = newnode(nkind.N_TNAME, empty, 0, 0); tnvoid.str = "void"; let bang: *node = newnode(nkind.N_TBANG, empty, 0, 0); bang.lhs = tnvoid; let nomemal: *aliasent = alloc(aliasent{aname="nomem", amod=empty, target=bang, aanext=nil})!; c.aliases = nomemal; 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 = alloc(aliasent{aname=d.str, amod=d.nmod, target=body, 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 = alloc(enumtype{ename=d.str, emod=d.nmod, storage=body.lhs, members=nil, etnext=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 = alloc(enummember{mname=m.str, mval=val, 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_" 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 { 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) void = { 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; let loopendbuf: []str = alloc([], LOOP_MAX: u64)!; c.loopendbuf = loopendbuf; let loopcontbuf: []str = alloc([], LOOP_MAX: u64)!; c.loopcontbuf = loopcontbuf; c.yieldtop = 0; let yieldbuf: []str = alloc([], LOOP_MAX: u64)!; c.yieldbuf = yieldbuf; c.defertop = 0; let deferbuf: []*node = alloc([], DEFER_MAX: u64)!; c.deferbuf = deferbuf; }; // 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 = alloc(local{name=name, off=off, sz=asz, tnode=tnode, 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 = alloc(local{name=name, off=off, sz=0, tnode=tnode, 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 = alloc(local{name=name, off=off, sz=asz, tnode=tnode, 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 cgoutstate 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). // // `cgoutinit` guards a one-shot [[memio.dynamic]] wiring so the // backing buffer is sticky across fns: [[cgout_flush]]'s // [[memio.reset]] rewinds `pos`/`len` without touching `cap`, so the // allocation amortises the same way the previous arena buffer did. // Re-init per fn would abandon the buffer (no [[io.close]] path → no // [[os.free]]) and re-grow from 0 via the 8→…→65536 ladder for every // function. Same idiom as lib/log/log.ww:124 `ensureinit`. let cgoutstate: memio.state; let cgoutstream: io.stream; let cgoutmode: i32 = 0; let cgoutinit: i32 = 0; fn cgout_enable() void = { if (cgoutinit == 0) { memio.dynamic(&cgoutstate, &cgoutstream); cgoutinit = 1; }; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { if (cgoutstate.pos > 0) { os.write(1, cgoutstate.ptr, cgoutstate.pos: u64); memio.reset(&cgoutstate); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { let buf: []u8; buf.ptr = p; buf.len = n: i32; // memio.dynamicwrite never returns io.closed (memio.ww:166); // bare-discard mirrors lib/log/log.ww:169 fmt.fprintln. io.write(&cgoutstream, buf); } 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(")"); }; // emitmovqload — `MOVQ off(base), dst`, the per-word unit of a // 3-word slice/str header load (cgslicehdr). fn emitmovqload(off: i64, base: str, dst: str) void = { emitline("\tMOVQ\t"); emitdispreg(off, base); emitline(", "); emitline(dst); emitline("\n"); }; // 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 ".__" (bare // "_..." 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 = alloc([], (total: u64) + 1u64)!; let k: i32 = 0; for (k < total) { p[k] = buf[k]; k += 1; }; p[total] = 0u8; let r: str; r.ptr = p.ptr; r.len = total; return r; }; fn emitlabel(s: str) void = { emitbytes(s.ptr, s.len: u64); emitline(":\n"); }; // mkscratchname — fresh local-slot name "._". Used for // compiler-synthesised slots (switch scrutinee, forrange index/len) // that need to be unique per use site but are never referenced by user // code. Increments labelseq so the same source position lines up with // C cgen's labelseq stream. fn mkscratchname(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; buf[i] = 46u8; i += 1; // '.' let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; c.labelseq += 1; let total: i32 = i + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let k: i32 = 0; for (k < total) { p[k] = buf[k]; k += 1; }; p[total] = 0u8; let r: str; r.ptr = p.ptr; r.len = total; return r; }; // ---- string interning ------------------------------------------------ // // streq is provided by sym.ww and reused here. // internstrlit — return a stable label for `bytes`. Dedups by content // so identical literals share storage. fn internstrlit(c: *cgen, bytes: str) str = { let s: *strlit = c.strlits; for (s != nil) { let bs: str = s.bytes; if (streq(bs, bytes)) { return s.label; }; s = s.slnext; }; // New label "_S_". let buf: [32]u8; buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; }; c.strlitseq += 1; let total: i32 = 3 + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let i: i32 = 0; for (i < total) { p[i] = buf[i]; i += 1; }; p[total] = 0u8; let lab: str; lab.ptr = p.ptr; lab.len = total; let nw: *strlit = alloc(strlit{label=lab, bytes=bytes, 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; }; if (streq(nm, "size")) { 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 tyslicesize(): i32; }; 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 primtypesize("str"): i32; }; 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 = alloc(letvar{name=nm, tnode=d.lhs, 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; }; // defvarstructinfo — sister of letvarstructinfo for top-level struct // `def`s. #129 A.2 adds DATA storage for struct-typed defs; the // LOAD-side cgdot direct-struct-global branch needs to resolve the // def's structinfo the same way it resolves a let's, so the field- // offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and // the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED // through to the underlying struct name. Returns nil for non-struct // defs (int/float/str — those use the existing emitsymname-based // paths). fn defvarstructinfo(c: *cgen, name: str) *structinfo = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { let t: *node = e.dtnode; 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; }; e = e.dnext; }; return nil; }; // defvartnode — sister of letvartnode for top-level `def`s. Returns // the type-spec node (defent.dtnode) for the named def, or nil. #129 // A.3 uses it in cgindex's array-base resolution so a `def: [N]T` // resolves through the same N_TARRAY-detect → LEAQ name(SB) shape as // a let array. Parallel to defvarstructinfo (#129 A.2) at the LOAD // side widening. fn defvartnode(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { return e.dtnode; }; e = e.dnext; }; 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); // #43: route the str-let gate through primtypesize so // #1 doesn't desync this with emitletdataw's matching // `sz == primtypesize("str"): i32` strlit-init branch. if (sz == primtypesize("str"): i32) { 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. // Emit a (DATA|DATAW) row for a float-typed top-level let/def with a // FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared // SSoT for emitletdataw float arm + emitdefconstants float arm (#129 // Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors // foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had // never been given the same treatment so `let g: f64 = -1.5;` // silently fell through to no-emit + undef-ref at link. Negation is // an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling // f64/f32 bitcast helpers into cgen. Returns true on emit, false if // rhs doesn't reduce to a foldable float literal. fn emitfloatlitdata(c: *cgen, directive: str, name: str, sz: i32, rhs: *node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; let neg: bool = false; if (rhs != nil) { let r: *node = rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_UN) { if (r.op == tkind.TK_MINUS) { neg = true; r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; };}; }; }; if (r == nil) { return false; }; if (r.kind != nkind.N_FLOATLIT) { return false; }; // r.uval holds f64 bits regardless of literal suffix (lexer // stores the pre-narrow bits). f32 needs an explicit // (double→float) narrowing at emit time — mirrors cstage's // `union { float f; u32 u; } x; x.f = (float)r->fval` // (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes // of the f64 bits, which silently emitted 0 for f32 lits; // the bug never bit because no current consumer has a f32 // let-init (surfaced by the consolidation gate). bits = r.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); // IEEE-754 sign-bit XOR for negation happens INSIDE the emit // loop on the top byte only — equivalent to a whole-u64 XOR with // 2^63 but never materialises that constant. Avoids strconv's // i64tos-on-i64-MIN bug (#144) and any future cstage const-fold // of `1 << 63` back to the i64-MIN immediate, either of which // would break cs==ww byte-id on the cgen.ww self-rebuild (995). let i: i32 = 0; let nb: u64 = bits; for (i < sz) { let b: u8 = (nb & 255u64): u8; if (neg) { if (i == sz - 1) { b = b ^ 128u8; }; }; emitdatawbyte(b); nb = nb >> 8u64; i += 1; }; emitline("\"\n"); return true; }; // emitstructlitbytes — payload of a struct-typed top-level let/def // with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via // the per-field offset (rule 13), dispatches per field type: // foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float, // recursive call for nested struct. Other field kinds (str / slice / // ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts // loud rather than silently emitting wrong bytes. Mirror of cstage // emit_struct_lit_bytes. `base` offsets the field-start computation // so the recursive call walks an inner struct's fields within its // outer parent's byte stream. fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, base: u64) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su == nil) { return false; }; if (su.kind != tykind.TY_STRUCT) { return false; }; let pos: u64 = base; let f: *tfield = su.fields; for (f != nil) { let fstart: u64 = base + f.offset; for (pos < fstart) { emitdatawbyte(0u8); pos = pos + 1u64; }; let v: *node = nil; if (rhs != nil) { let fnod: *node = rhs.list; for (fnod != nil) { if (streq(fnod.str, f.name)) { v = fnod.lhs; break; }; fnod = fnod.next; }; }; let fsz: i32 = f.type_.size: i32; if (v == nil) { let i: i32 = 0; for (i < fsz) { emitdatawbyte(0u8); i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; continue; }; let vr: *node = v; for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; }; let fu: *tinfo = f.type_; for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; }; if (fu != nil && fu.kind == tykind.TY_STRUCT) { if (vr == nil) { let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (vr.kind != nkind.N_STRUCTLIT) { let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; emitstructlitbytes(c, f.type_, vr, fstart); pos = fstart + fsz: u64; f = f.tnext; continue; }; // #129 A.3: array-typed field with N_ARRLIT rhs (the shape // parked in A.2). Recurses through emitarraylitbytes for // element-kind dispatch. Rule-7 stops loudly if rhs shape // doesn't match. if (fu != nil && fu.kind == tykind.TY_ARRAY) { if (vr == nil) { let m: str = "emitstructlitbytes: array field rhs nil (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (vr.kind != nkind.N_ARRLIT) { let m: str = "emitstructlitbytes: array field rhs not N_ARRLIT (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (!emitarraylitbytes(c, f.type_, vr, 1)) { let m: str = "emitstructlitbytes: array field rhs has non-reducible elements (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; pos = fstart + fsz: u64; f = f.tnext; continue; }; if (typeisfloat(f.type_)) { let isf32: bool = (fsz == 4); let neg: bool = false; let fr: *node = vr; if (fr != nil) { if (fr.kind == nkind.N_UN) { if (fr.op == tkind.TK_MINUS) { neg = true; fr = fr.lhs; for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; }; } else { if (fr.op == tkind.TK_PLUS) { fr = fr.lhs; for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; }; };}; };}; if (fr == nil) { let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (fr.kind != nkind.N_FLOATLIT) { let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let bits: u64 = fr.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; let i: i32 = 0; let nb: u64 = bits; for (i < fsz) { let b: u8 = (nb & 255u64): u8; if (neg) { if (i == fsz - 1) { b = b ^ 128u8; }; }; emitdatawbyte(b); nb = nb >> 8u64; i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; continue; }; let iv: u64 = 0u64; if (!foldintliteral(vr, &iv)) { let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let i: i32 = 0; let nb: u64 = iv; for (i < fsz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; }; let endpos: u64 = base + structt.size; for (pos < endpos) { emitdatawbyte(0u8); pos = pos + 1u64; }; return true; }; // emitstructdata — top-level wrapper. Opens the DATA/DATAW directive // then delegates to emitstructlitbytes. Shared between emitletdataw // struct arm and emitdefconstants struct arm (#129 A.2). fn emitstructdata(c: *cgen, directive: str, name: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su == nil) { return false; }; if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); emitstructlitbytes(c, structt, rhs, 0u64); emitline("\"\n"); return true; }; // emitarraylitbytes — emit alen * esz bytes for an [N]T top-level let/ // def with N_ARRLIT rhs. Mirrors cstage emit_array_lit_bytes. Per- // element dispatch: // - int (covers bool/rune/typed-int/N_UN-int): foldintliteral per // element. Existing pre-#129-A.3 emitletdataw array arm logic // preserved byte-for-byte so bootstrap consumers (lib/os, lib/ // bufio, lib/strings, lib/encoding/utf8, lib/strconv/stof_data) // don't shift. // - float (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude via // pointer-cast round-trip (mirror emitfloatlitdata), sign-XOR // top byte of each element inline. No 2^63 immediate. // - struct: per element call emitstructlitbytes (#129 A.2 helper). // - other element kinds (ptr/nested-array): returns false — caller // falls through to zero-init. // // Two-pass validate-then-emit (`emit_phase=0` validate-only, `=1` // actually emit) keeps emit-on-failure from emitting partial bytes // into an open DATA literal. fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, emit_phase: i32) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; let esz: i32 = au.sub.size: i32; let alen: i32 = au.alen: i32; let eu: *tinfo = au.sub; for (eu != nil && eu.kind == tykind.TY_NAMED) { eu = eu.under; }; if (eu != nil && eu.kind == tykind.TY_STRUCT) { // Validate: every element must be N_STRUCTLIT (after N_CAST). let idx: i32 = 0; let last_ev: *node = nil; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != nkind.N_STRUCTLIT) { return false; }; last_ev = ev; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; let repeat: bool = false; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; emitstructlitbytes(c, au.sub, ev, 0u64); idx += 1; e = e.next; }; for (idx < alen) { if (repeat && last_ev != nil) { emitstructlitbytes(c, au.sub, last_ev, 0u64); } else { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; }; idx += 1; }; return true; }; // #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T // inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of // the TY_ARRAY-field-in-struct arm in emitstructlitbytes — recurse // into emitarraylitbytes per element; recursion bottoms out at // scalar (int/float) elements. esz = au.sub.size gives the per- // element stride (rule 13). The `...` repeat marker with nested- // array elements is rejected loud (rule 7): no consumer needs it // (powers_of_ten is fully enumerated). if (eu != nil && eu.kind == tykind.TY_ARRAY) { let idx: i32 = 0; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { let m: str = "emitarraylitbytes: '...' repeat with nested-array elements unsupported (#129 A.3, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != nkind.N_ARRLIT) { return false; }; if (!emitarraylitbytes(c, au.sub, ev, 0)) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; e = rhs.list; for (e != nil && idx < alen) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; emitarraylitbytes(c, au.sub, ev, 1); idx += 1; e = e.next; }; for (idx < alen) { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; idx += 1; }; return true; }; if (typeisfloat(au.sub)) { let isf32: bool = typeisf32(au.sub); // Validate. let idx: i32 = 0; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == nkind.N_UN) { if (ev.op == tkind.TK_MINUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; };}; };}; if (ev == nil) { return false; }; if (ev.kind != nkind.N_FLOATLIT) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; let last_bits: u64 = 0u64; let last_neg: bool = false; let repeat: bool = false; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; let neg: bool = false; if (ev != nil) { if (ev.kind == nkind.N_UN) { if (ev.op == tkind.TK_MINUS) { neg = true; ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; };}; };}; let bits: u64 = ev.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; let bb: i32 = 0; let nb: u64 = bits; for (bb < esz) { let byt: u8 = (nb & 255u64): u8; if (neg) { if (bb == esz - 1) { byt = byt ^ 128u8; }; }; emitdatawbyte(byt); nb = nb >> 8u64; bb += 1; }; last_bits = bits; last_neg = neg; idx += 1; e = e.next; }; for (idx < alen) { if (repeat) { let bb: i32 = 0; let nb: u64 = last_bits; for (bb < esz) { let byt: u8 = (nb & 255u64): u8; if (last_neg) { if (bb == esz - 1) { byt = byt ^ 128u8; }; }; emitdatawbyte(byt); nb = nb >> 8u64; bb += 1; }; } else { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; }; idx += 1; }; return true; }; // Int-element path — preserved byte-for-byte from the pre-A.3 // emitletdataw in-place arm so bootstrap consumers (u8/i8/u16 // arrays) don't shift. let idx: i32 = 0; let e: *node = rhs.list; let last: u64 = 0u64; let repeat: bool = false; // Validate first. for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (!foldintliteral(ev, &last)) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; last = 0u64; repeat = false; e = rhs.list; let inrepeat: bool = false; for (idx < 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 && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (!foldintliteral(ev, &v)) { v = 0u64; }; last = v; e = e.next; }; }; let nb: u64 = v; let bb: i32 = 0; for (bb < esz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; bb += 1; }; idx += 1; }; return true; }; // emitarraydata — top-level wrapper. Two-pass validate-then-emit // avoids partial-byte corruption if the rhs shape can't reduce. // nil rhs is the "no-rhs zero-init" shape (e.g. `let buf: [N]u8;` // in lib/strconv/strconv.ww:287, lib/os/os.ww:92, etc.) — emit // alen*esz zero bytes. This was the implicit pre-A.3 emitletdataw // behavior (the old loop emitted zeros when `elems` was nil); the // refactor would have skipped emit entirely without this branch, // causing `undefined reference to strconv.f64tos_buf` at link. fn emitarraydata(c: *cgen, directive: str, name: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; if (rhs == nil) { let total: u64 = arrt.size; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; emitline("\"\n"); return true; }; if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); return true; }; 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: routes through the // emitfloatlitdata SSoT helper, shared // with emitdefconstants's float arm // (#129 Phase A.1, rule-12). Bare-call // discards the bool return (mirrors // cgen.ww:723 fmt.fprintln pattern). emitfloatlitdata(c, "DATAW", nm, fsz, d.rhs); }; // #129 A.2: struct-typed let with N_STRUCTLIT rhs // routes through the emitstructdata SSoT helper. // Pre-A.2 emitletdataw had no struct arm, so the // declaration fell out of the .data section and // the link surfaced an undefined-symbol error. if (issg) { let r: *node = d.rhs; if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; emitstructdata(c, "DATAW", nm, st, r); }; }; }; // 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 == primtypesize("str"): i32 && !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; let szstr: i32 = primtypesize("str"): i32; for (i < szstr) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; }; if (sz == tyslicesize(): i32 && !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; let szsl: i32 = tyslicesize(): i32; for (i < szsl) { 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"); }; }; // #129 A.3: array global routes through the // emitarraydata SSoT helper. Int-elem path is // byte-for-byte preserved (bootstrap consumers in // lib/os, lib/bufio, lib/strings, lib/encoding/ // utf8, lib/strconv/stof_data don't shift). Float/ // struct elements gain emit via element-kind // dispatch. Helper validates pre-emit so partial // fold-failures don't corrupt the DATA literal. // No-rhs arrays (e.g. `let buf: [N]u8;`) go through // the same helper with rhs=nil → zero-fill branch. if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { let rh: *node = d.rhs; let route: bool = false; if (rh == nil) { route = true; }; if (rh != nil) { if (rh.kind == nkind.N_ARRLIT) { route = true; }; }; if (route) { let at: *tinfo = d.lhs.type_: *tinfo; emitarraydata(c, "DATAW", nm, at, rh); }; }; }; }; }; 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) { // Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) // rhs: route through the same SSoT helper as // emitletdataw's float arm. Pre-#129 this fell // through to no-emit + undef-ref at link. Type-size // walk mirrors letvarisfloat (#129 Phase A.1). let dfsz: i32 = 0; let dt: *node = d.lhs; for (dt != nil) { if (dt.kind != nkind.N_TNAME) { dfsz = 0; break; }; let fsz: i32 = letfloatprim(dt.str); if (fsz > 0) { dfsz = fsz; break; }; let nx: *node = aliaslookup(c, dt.str); if (nx == nil) { dfsz = 0; break; }; dt = nx; }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, dfsz, d.rhs); } else { // #129 A.2: struct-typed def with N_STRUCTLIT // rhs. The checker stamps d.lhs.type_ with the // struct's tinfo; helper peels TY_NAMED. Parallel // to emitletdataw struct arm; uses DATA (read- // only) directive. if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; let su: *tinfo = st; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", d.str, st, r); }; }; };}; // #129 A.3: array-typed def with N_ARRLIT rhs. // Parallel to emitletdataw array arm; uses DATA. if (r != nil) { if (r.kind == nkind.N_ARRLIT) { let at: *tinfo = d.lhs.type_: *tinfo; let au: *tinfo = at; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", d.str, at, r); }; }; };}; }; }; if (ok) { // #127: route DATA-emit through the SAME emitsymname // SSoT that LOAD/CALL sites use. Replaces the prior // 8-line d.exported/d.nmod prefix logic with a single // modlookup-based mangle, removing duplicate logic // (rule-12 sea-of-stars). Mirrors cstage emit_defs at // cmd/w6c/cgen.c:8494 (mod_mangle). Bootstrap-neutral // post-90d31c5 (the PATH_MAX duplicate-def consumer // that motivated the divergence is gone), so the asm // surface is unchanged on the corpus. emitline("DATA "); emitsymname(c, d.str); 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 = alloc(fnret{fname=d.str, fmod=d.nmod, rtype=d.lhs, params=d.list, 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; }; // samemodfn — true iff `name` is registered as a fn in c.curmod. Used // by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`, // future free/append/len audits) when the current module declares its // own decl by that name. Mirrors cstage's same-module check at // cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over // the flat scope would also match `use os;`-imported decls in a primary, // suppressing the builtin spuriously; the same-module-tag filter here // (and `c.curmod && ...` on the cstage side) keeps the gate strict. fn samemodfn(c: *cgen, name: str) bool = { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, c.curmod)) { return true; }; }; f = f.frnext; }; return false; }; // 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, dtnode: *node, // #129 A.2: type-spec node (d.lhs); needed for // struct-def structinfo lookup at the cgdot // LOAD-side widening site. 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 = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, 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); }; // #149: rhs peels (N_CAST / unary ±) to a float literal — the exact // shape emitfloatlitdata (cgen.ww) emits a DATA symbol for. The scalar- // float address-of gate must equal that emission set, or `&def` LEAQs a // symbol the data pass never wrote. Keep in sync with emitfloatlitdata's // peel. fn floatlitleaf(rhs: *node) bool = { let r: *node = rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_UN) { if (r.op == tkind.TK_MINUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; }; }; }; }; if (r == nil) { return false; }; return r.kind == nkind.N_FLOATLIT; }; // #149/#147: a top-level def is addressable for `&def` iff emitdefs emits // a DATA symbol for it — struct, array, scalar int (foldintliteral), or // scalar float whose rhs peels to a FLOATLIT. Gate held identical to // cstage def_is{struct,array,scalar}def so the addressable set matches // byte-for-byte (rule 10). str defs and computed-rhs floats (#147 // `def NAN = 0.0/0.0`) have no symbol and are excluded → routed to the // loud error, never a LEAQ of a missing symbol. `opnd` is the `&`-operand // N_IDENT; its checker-stamped type_ carries the def's type (same as the // cgident float-def read at cgenexpr.ww). fn defisaddressable(c: *cgen, opnd: *node) bool = { let nm: str = opnd.str; if (defvarstructinfo(c, nm) != nil) { return true; }; let dtn: *node = defvartnode(c, nm); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { return true; }; }; let drhs: *node = deflookuprhs(c, nm); if (drhs == nil) { return false; }; let v: u64 = 0u64; if (foldintliteral(drhs, &v)) { return true; }; if (isfloattype(c, opnd)) { if (floatlitleaf(drhs)) { return true; }; }; return false; }; // ---- module-private symbol map -------------------------------------- // // Every non-FFI top-level fn decl lives in its module's namespace — // cgen mangles the leaf to `.` at the def site (TEXT) // and at every call/load site, so cross-module same-leaf fns (lib/os // `read` vs lib/io `read`, both exported) coexist at link time. // Non-fn decls (let/def/type) stick to the older "non-exported only" // rule: their export-side namespace is the user-facing data ABI and // mangling them changes the surface. FFI-bound decls (@symbol) keep // their explicit C symbol regardless of kind. // // Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` // for fns. Both stages must match exactly — ww2/ww3/ww4 byte-identity // depends on it. type modent = struct { mname: str, // the bare ident as it appears in source 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 = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; }; if (d.kind == nkind.N_DEF) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; if (d.kind == nkind.N_TYPEDECL) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; if (d.kind == nkind.N_LET) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, 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 = alloc(ffi{ident=d.str, symbol=symnode.str, 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 rt; import strings; 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 = alloc([], nz + 1u64)!; buf.len = (nz + 1u64): i32; let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, 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.ptr, 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 nlen: u64 = cstrlen(src); let view: str; view.ptr = src; view.len = nlen: i32; let fname: str = strings.dup(view); let l: lex; lexinit(&l, fname, buf, blen); let ps: parser; parserinit(&ps, &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; }; // #50: run check before cgen so AST mutations from #42 (size/align/ // offset fold) and the audit §1.8 node.type_ population land before // cgen walks the file. Mirrors cmd/w6c/main.c:73-75. Five precondition // fixes for fixture cleanliness: #51 cross-module type refs, #52 // enum↔int reinterpret, #53 N_BLOCK scoping, #55 nominal-first variant // compare, #56 bare-leaf same-module preference. let tc: tctx; typesinit(&tc); let ck: checker; checkinit(&ck, &tc); checkfile(&ck, f); if (ck.errs > 0) { return 1; }; let cg: cgen; cgeninit(&cg); cgfile(&cg, f); return 0; };