// 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; 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; }; // 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; // 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; // [[args]] allocates the []str view via the `alloc` builtin, whose malloc // lowers to rt_malloc only when the rt binding is in the bundle (mirror // lib/strings/strings.ww:30 — every alloc-using module imports rt). os is // bundled by ~every program, so without this a plain `ww build` of any // os-importing program links bare libc `malloc` (undefined). Task #17. import rt; @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; // 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; // ref/hare/sys/+linux/types.ha:886-888. ww folds `sys` into `os`, so the // std fd NUMBERS live here (the sys role). Typed i32, NOT io.file as in // Hare's os::stdout_file (ref/hare/os/+linux/stdfd.ha:28): Hare's `os` // imports `io`, but ww's `os` is the import floor and must never import // io (lib/CLAUDE.md) — so the io.file/io.handle binding can't live here. // Consumers (lib/fmt's stdio wrappers) cast i32→io.file at the use site, // where the handle layer is already in scope. export def STDIN_FILENO: i32 = 0; export def STDOUT_FILENO: i32 = 1; export def STDERR_FILENO: i32 = 2; 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; // errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ // errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os // (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 // rather than int: Linux errnos are kernel ints (32-bit), keeping os's // kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, // the syscall's *negative* raw return) — the two model different // things, so they are not unified; the negative→positive normalization // lives at the oserror→errors.error boundary in those callers. export type errno = !i32; // Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, // matching Hare's defs (the kernel returns -N; the wrap-to-positive is // the caller's concern). Subset: exactly the errnos [[errors.errno]] // maps to a named condition; grow as callers surface more. export def ENOENT: errno = 2; export def EINTR: errno = 4; export def EAGAIN: errno = 11; export def EACCES: errno = 13; export def EBUSY: errno = 16; export def EEXIST: errno = 17; export def EINVAL: errno = 22; export def EOVERFLOW: errno = 75; export def ENETUNREACH: errno = 101; export def ETIMEDOUT: errno = 110; export def ECONNREFUSED: errno = 111; export def ECANCELED: errno = 125; // strerror — human-readable text for an [[errno]] (Hare's // sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL // SUBSET: the mapped errnos above plus a generic fallback; grow the // switch as callers surface more (lib/CLAUDE.md documented-subset, not // a workaround). Messages verbatim from the reference. Hare's // unknown_errno formats the numeric value; that is deferred. export fn strerror(err: errno) str = { switch (err) { case ENOENT: return "No such file or directory"; case EINTR: return "Interrupted system call"; case EAGAIN: return "Resource temporarily unavailable"; case EACCES: return "Permission denied"; case EBUSY: return "Device or resource busy"; case EEXIST: return "File exists"; case EINVAL: return "Invalid argument"; case EOVERFLOW: return "Value too large for defined data type"; case ENETUNREACH: return "Network is unreachable"; case ETIMEDOUT: return "Connection timed out"; case ECONNREFUSED: return "Connection refused"; case ECANCELED: return "Operation canceled"; }; return "Unknown error"; }; // 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] == '/') { 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; // rt_argc / rt_argv — runtime-side getters for the argc/argv captured by // rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as // rt_envp). [[args]] is the only consumer. @symbol("rt_argc") fn rtargc() i64; @symbol("rt_argv") fn rtargv() **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] == '=') { 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; }; // argsbuilt / argscache — build-once cache for [[args]]. drew ruling // (task #17): an explicit `built` sentinel, NOT len==0 overloading (a // real argv always has argv[0], but the sentinel keeps the contract // honest and decoupled from content). args() is loop-callable; under // ww's no-free model a per-call rebuild would leak the slice each call, // so the slice is materialised once and reused. let argsbuilt: bool = false; let argscache: []str; // args — the process arguments as a borrowed []str. args[0] is the // program name; args[1..] are the invocation arguments. Each str views // the NUL-terminated argv bytes in place (valid for the process // lifetime), so the slice must not be mutated or freed by the caller. // // DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` // GLOBAL populated by an @init that walks rt's argv // (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the // faithful global is not expressible; this is the fn-shaped equivalent // (Hare NAME kept, shape diverged). Revisit if @init lands (#26). export fn args() []str = { if (argsbuilt) { return argscache; }; let argc: i32 = rtargc(): i32; let argv: **u8 = rtargv(); let r: []str = alloc([], argc: u64)!; let i: i32 = 0; for (i < argc) { let c: *u8 = argv[i]; let n: i32 = 0; for (c[n] != 0u8) { n += 1; }; let s: str; s.ptr = c; s.len = n; append(r, s); i += 1; }; argscache = r; argsbuilt = true; return r; }; // ---- 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; }; // 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 → `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); 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()` → `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 = { 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) { 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 = { assert(v > 0u64, "strconv.pow2multiple: v == 0"); 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 = { 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 = { assert(e <= 1650u32, "strconv.log10pow2: e > 1650"); return (e * 78913u32) >> 18u32; }; // ref/hare/strconv/ftos_ryu.ha:154. fn log10pow5(e: u32) u32 = { 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, 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) { 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 = { assert(v > 0u32, "strconv.pow2multiple32: v == 0"); 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 = { 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); 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 = { 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] = '.'; o += 1i32; }; if (0i32 <= i && i < (d.nd: i32)) { buf[o] = (d.digits[i] + 48u8): u8; } else { buf[o] = '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; 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] = '.'; 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] = 'e'; o += 1i32; let e: i32 = d.dp - 1i32; if (e < 0i32) { e = -e; buf[o] = '-'; 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] = '-'; 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] = '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] = '0'; // 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] = '-'; 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] = '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] = '0'; // 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, ]; // 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 = { 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 = { 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 = { assert(delim.len > 0, "bytes.tokenize called with empty slice"); 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 = { assert(delim.len > 0, "bytes.rtokenize called with empty slice"); 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; }; // 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 = { 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 => { append(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); append(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 = { 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 => { append(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); append(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); }; // cut — split `in` along the first instance of `delim`, returning the // portion before and the portion after the delimiter as a borrowed // tuple. When `delim` is absent, the whole input is the first half and // the second is empty. ref/hare/bytes/tokenize.ha:392. // // Delim is spelled (u8 | []u8) to match index/rindex (bytes.ww:57/91); // the tagged union is an unordered set, so this is the same type as // Hare's ([]u8 | u8), not a divergence. export fn cut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { let ln: i32 = match (delim) { case let c: u8 => yield 1i32; case let sub: []u8 => { assert(sub.len > 0, "bytes.cut called with empty delimiter"); yield sub.len; }; }; match (index(in, delim)) { case let i: i32 => { let lo: i32 = i + ln; return (in[0:i], in[lo:in.len]); }; case void => { let empty: []u8; empty.ptr = nil; empty.len = 0; empty.cap = 0; return (in, empty); }; }; }; // rcut — like [[cut]] but splits along the last instance of `delim`. // ref/hare/bytes/tokenize.ha:413. export fn rcut(in: []u8, delim: (u8 | []u8)) ([]u8, []u8) = { let ln: i32 = match (delim) { case let c: u8 => yield 1i32; case let sub: []u8 => { assert(sub.len > 0, "bytes.rcut called with empty delimiter"); yield sub.len; }; }; match (rindex(in, delim)) { case let i: i32 => { let lo: i32 = i + ln; return (in[0:i], in[lo:in.len]); }; case void => { let empty: []u8; empty.ptr = nil; empty.len = 0; empty.cap = 0; return (in, empty); }; }; }; // 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 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. // offs is `size` (unsigned), matching the Hare field: prev()'s walk-back // relies on the underflow past 0 wrapping to SIZE_MAX so the `offs < len` // guards in next()/prev() exit safely. An i32 offs went to -1 and the // signed `-1 < len` guard then read src[-1] (#70). Index sites cast to // i32 (ww's slice index is i32 and `[...]` reads ':' as the slice // separator, so the cast can't be inline) and are only reached when // offs is in [0, len). export type decoder = struct { offs: size, 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: size) { let dn: done; return dn; }; let nx: i32 = 0; let state: i32 = 0; let r: u32 = 0u32; for (d.offs < d.src.len: size) { let oi: i32 = d.offs: i32; let b: u8 = d.src[oi]; 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; 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; offs is `size` here // too, so the same `d.offs < d.src.len` guard exits and leaves offs at // SIZE_MAX on the more-path (a subsequent next() then returns more, not // an OOB read — #70). 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: size = d.offs; d.offs -= 1; for (d.offs < d.src.len: size) { let oi: i32 = d.offs: i32; let b: u8 = d.src[oi]; let bi: i32 = b: i32; let cell: i8 = dfa[bi]; if (cell: i32 != -1) { let t: size = d.offs; match (next(d)) { case let r: rune => { let landed: size = 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 = { // No-runtime-net residual (#70): Hare's d.src[d.offs..] bounds-asserts // loudly when offs is out of range (e.g. SIZE_MAX after prev() returned // `more`). ww has no such net, so a stale offs would silently build a // ptr-1/len+1 OOB view. Guard it loudly instead: callers must not call // remaining() after next()/prev() returned `more`. if (d.offs > d.src.len: size) { abort("utf8.remaining: decoder offset past end of source"); }; let oi: i32 = d.offs: i32; let r: []u8; r.ptr = d.src.ptr + (d.offs: u64); r.len = d.src.len - oi; r.cap = d.src.len - oi; 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 span: i32 = (end.offs - begin.offs): i32; let r: []u8; r.ptr = begin.src.ptr + (begin.offs: u64); r.len = span; r.cap = span; 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: i32; }; // 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 append'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) { append(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 = { 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) = { assert(start <= end, "strings.bytesub: start is higher than end"); assert(end <= s.len, "strings.bytesub: end exceeds string length"); if (start < s.len && (s[start] & 0xC0u8) == 0x80u8) { let e: utf8.invalid; return e; }; if (end < s.len && (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; // utf8.decoder.offs is `size` (#70); the iterator carries i32. The // rune-return path keeps offs in [0, len), so the narrowing cast back // is safe. d.offs = it.offs: size; if (forward) { match (utf8.next(&d)) { case let r: rune => { it.offs = d.offs: i32; 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: i32; 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: size; // decoder.offs is size (#70) let e: utf8.decoder; e.src = end.src; e.offs = end.offs: size; 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) { 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) { 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)); }; // cut — split `in` along the first instance of `delim`, returning the // portions before and after it. When `delim` is absent the whole input // is the first half and the second is empty. Both halves are borrowed // from `in`; caller ensures `delim` is non-empty. // ref/hare/strings/tokenize.ha:288. export fn cut(in: str, delim: str) (str, str) = { let (a, b) = bytes.cut(toutf8(in), toutf8(delim)); return (frombytes(a), frombytes(b)); }; // rcut — like [[cut]] but split along the LAST instance of `delim`. // ref/hare/strings/tokenize.ha:302. export fn rcut(in: str, delim: str) (str, str) = { let (a, b) = bytes.rcut(toutf8(in), toutf8(delim)); return (frombytes(a), frombytes(b)); }; // 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 => { append(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); append(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 => { append(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); append(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); }; // 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; import strings; export fn isdigit(c: rune) bool = { if (c < '0') { return false; }; if (c > '9') { return false; }; return true; }; export fn isupper(c: rune) bool = { if (c < 'A') { return false; }; if (c > 'Z') { return false; }; return true; }; export fn islower(c: rune) bool = { if (c < 'a') { return false; }; if (c > 'z') { 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 == ' ') { return true; }; if (c == '\t') { return true; }; if (c == '\n') { return true; }; if (c == '\v') { return true; }; if (c == '\f') { return true; }; if (c == '\r') { return true; }; return false; }; export fn isxdigit(c: rune) bool = { if (isdigit(c)) { return true; }; if (c >= 'A') { if (c <= 'F') { return true; }; }; if (c >= 'a') { if (c <= 'f') { return true; }; }; 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 == ' ') { return true; }; if (c == '\t') { return true; }; return false; }; // isprint — printable: space through '~'. export fn isprint(c: rune) bool = { if (c < ' ') { return false; }; if (c > '~') { return false; }; return true; }; // isgraph — printable, non-space. export fn isgraph(c: rune) bool = { if (c < '!') { return false; }; if (c > '~') { 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; }; // strlower — ASCII-lowercased copy of s, newly allocated. // ref/hare/ascii/string.ha:11. export fn strlower(s: str) (str | nomem) = { // empty bypass: ww alloc([],0) routes through nomem; Hare allocs 0 // and zero-loops (ref/hare/ascii/string.ha:12). if (s.len == 0) { let r: str; r.ptr = nil; r.len = 0; return r; }; let buf: []u8 = alloc([], s.len: u64)?; return strlower_buf(s, buf); }; // strlower_buf — ASCII-lowercase s into buf (overwrites). nomem if buf // too small. ref/hare/ascii/string.ha:21. // Byte-wise fold: ASCII case-fold only touches bytes <0x80; UTF-8 // multibyte bytes are >=0x80 and pass through unchanged, so byte-wise // equals Hare's rune fold and is length-preserving. // ww uses an explicit `buf.cap < s.len` check + `let nm: nomem` value // because it has no static-append builtin; Hare reaches the same // nomem-on-too-small via `static append(buf, ...)?` (string.ha:25). export fn strlower_buf(s: str, buf: []u8) (str | nomem) = { if (buf.cap < s.len) { let nm: nomem; return nm; }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = tolower(s[i]: rune): u8; i += 1; }; buf.len = s.len; return strings.frombytes(buf); }; // strupper — ASCII-uppercased copy of s, newly allocated. // ref/hare/ascii/string.ha:33. export fn strupper(s: str) (str | nomem) = { if (s.len == 0) { let r: str; r.ptr = nil; r.len = 0; return r; }; let buf: []u8 = alloc([], s.len: u64)?; return strupper_buf(s, buf); }; // strupper_buf — see strlower_buf. ref/hare/ascii/string.ha:43. export fn strupper_buf(s: str, buf: []u8) (str | nomem) = { if (buf.cap < s.len) { let nm: nomem; return nm; }; let i: i32 = 0; for (i < s.len) { buf.ptr[i] = toupper(s[i]: rune): u8; i += 1; }; buf.len = s.len; return strings.frombytes(buf); }; // 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) }; // 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 = { 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; }; 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; }; 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 ascii; import bytes; 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 }; // lut_upper / lut_lower — digit→glyph tables. Verbatim port of the // `static const lut_upper`/`lut_lower` rune arrays in // ref/hare/strconv/utos.ha:14-20. Module-level `let` (ww has no module // `const`; never written) following the ftos_data.ww table convention. // Declared [16]rune (faithful to Hare's inferred rune element type); // u64tos casts the indexed glyph to u8 at the store, as Hare does // (utos.ha:35). let lut_upper: [16]rune = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', ]; let lut_lower: [16]rune = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', ]; // u64tos_buf — overwritten on each u64tos call (Hare's `static let buf`, // utos.ha:12). 64 = the widest u64 rendering (binary). `[0...]` kept for // fidelity; the initial value is irrelevant (only the freshly-written // prefix is ever read) but the fill form is exercised (probed: emits // byte-identically cross-stage). let u64tos_buf: [64]u8 = [0...]; // 64 binary digits // u64tos — convert u to a base-b numeric string. Returns a view into // `u64tos_buf`, overwritten on the next call; copy via strings.dup to // outlive it. Verbatim port of ref/hare/strconv/utos.ha:10-42. // Divergences: // - Hare's `static assert(types::U64_MAX == ...)` dropped (ww has no // static assert; the bound lives in lib/types/types.ww:20). // - Hare selects the LUT via an if-EXPRESSION and reassigns `b` to // HEX_UPPER / DEC inline (utos.ha:21-26). ww has no if-expression // (standing divergence, stof.ww:31), so the glyph case is a `lower` // bool branch and the divisor is basenum(b) — the file's existing // normalize helper, which maps DEFAULT→10 and HEX_LOWER→16 exactly // as Hare's reassignment does. // - Hare's `types::string { data = &buf, ... }` + `*(&s: *str)` // reinterpret (utos.ha:28,41) → strings.frombytes (CLAUDE.md rule 9 // carve-out: ww's lib/types has no `string` struct; frombytes is the // honest ww idiom, cf. ascii/strings). export fn u64tos(u: u64, b: base) str = { let nb: u64 = basenum(b): u64; let lower: bool = (b == base.HEX_LOWER); let length: i32 = 0; let n: u64 = u; if (n == 0u64) { u64tos_buf[length] = lut_upper[0]: u8; length += 1; }; for (n > 0u64) { let d: i64 = (n % nb): i64; if (lower) { u64tos_buf[length] = lut_lower[d]: u8; } else { u64tos_buf[length] = lut_upper[d]: u8; }; length += 1; n = n / nb; }; bytes.reverse(u64tos_buf[0:length]); return strings.frombytes(u64tos_buf[0:length]); }; // i64tos_buf — independent from u64tos_buf so i64tos's own u64tos call // (the magnitude) doesn't clobber the in-flight result. 65 = 64 digits // plus the leading '-'. Hare's `static let buf: [65]u8` (itos.ha:18). let i64tos_buf: [65]u8 = [0...]; // 64 binary digits plus '-' // i64tos — convert i to a base-b numeric string. Returns a view into // `i64tos_buf`. Verbatim port of ref/hare/strconv/itos.ha:10-32. // Divergences: // - `static assert` dropped (see u64tos); the DEFAULT→DEC normalize // rides basenum(b) inside the u64tos call (itos.ha:12-14). // - Hare's slice-assign `buf[1..len(u)+1] = u[..]` + the bounds assert // (itos.ha:26-28) → explicit copy loop (existing-file convention; // the [65] buffer holds the 64-digit max + sign exactly, so the // bound is structural). // - `*(&s: *str)` → strings.frombytes (see u64tos). export fn i64tos(i: i64, b: base) str = { if (i >= 0) { return u64tos(i: u64, b); }; i64tos_buf[0] = '-'; // `(-i): u64`: for I64_MIN, -i wraps (two's complement) back to the // I64_MIN bit pattern; reinterpreting to u64 yields the true // magnitude 9223372036854775808. ref/hare/strconv/itos.ha:25. Probed // on both stages (NEG then i64→u64 reinterpret byte-identical); // closes the i64tos-on-I64_MIN bug noted at cgen.ww #144. let u: str = u64tos((-i): u64, b); let k: i32 = 0; for (k < u.len) { i64tos_buf[k + 1] = u[k]; k += 1; }; return strings.frombytes(i64tos_buf[0 : u.len + 1]); }; 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); }; // itos — int (ww machine-word, 8B → i64-width) → string. // ref/hare/strconv/itos.ha:52. export fn itos(i: int, b: base) str = { return i64tos(i: 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); }; // utos — uint (8B → u64-width) → string. ref/hare/strconv/utos.ha:62. export fn utos(u: uint, b: base) str = { return u64tos(u: u64, b); }; // ztos — size (8B → u64-width) → string. ref/hare/strconv/utos.ha:67. export fn ztos(u: size, b: base) str = { return u64tos(u: u64, b); }; // uptrtos — uintptr → string. ref/hare/strconv/utos.ha:72 (param `uptr` // cast `uptr: u64`). export fn uptrtos(uptr: uintptr, b: base) str = { return u64tos(uptr: u64, b); }; // rune_to_integer — digit value of r (0-9 → 0-9; a-z/A-Z → 10-35), // or void if r is not alphanumeric. Verbatim port of // ref/hare/strconv/stou.ha:8-15 (ww yields the void variant with a // bare `return;`, per lib/bytes/bytes.ww:65). fn rune_to_integer(r: rune) (u64 | void) = { if (ascii.isdigit(r)) { return (r: u32 - '0'): u64; } else if (ascii.isalpha(r) && ascii.islower(r)) { return (r: u32 - 'a'): u64 + 10; } else if (ascii.isalpha(r) && ascii.isupper(r)) { return (r: u32 - 'A'): u64 + 10; }; return; }; // parseint — shared sign+digit+overflow core for stoi64/stou64. // Verbatim port of ref/hare/strconv/stou.ha:17-65. Divergences: // - param `base` → `b` (ww: avoid the type/value name collision; the // file already names the enum arg `b`). // - Hare's DEFAULT→DEC / HEX_LOWER→HEX base reassignment + the // base-validity assert collapse into basenum(b), which already maps // every base to its numeric value {2,8,10,16} (default 10). HEX_LOWER // thus parses case-insensitively, matching Hare's normalize-then-parse. // - str is byte-indexable, so Hare's `buf = strings::toutf8(s)` is // elided (existing file convention, cf. the old stoi64/stou64). // - n *= base / n += digit spelled as plain assignment (sibling-fn // convention). fn parseint(s: str, b: base) ((bool, u64) | invalid | overflow) = { let nb: u64 = basenum(b): u64; if (s.len == 0) { return 0: invalid; }; let i: i32 = 0; let sign: bool = s[i] == '-'; if (sign || s[i] == '+') { i += 1; }; // Require at least one digit. if (i == s.len) { return i: invalid; }; let n: u64 = 0u64; // Hare's `for (i < len(buf); i += 1)` (stou.ha:43) → condition-only // for + tail increment (ww has no 2-clause for; sort.ww:25). Early // returns exit before the increment, so it's never skipped. for (i < s.len) { let digit: u64 = match (rune_to_integer(s[i]: rune)) { case void => return i: invalid; case let d: u64 => yield d; }; if (digit >= nb) { return i: invalid; }; let old: u64 = n; n = n * nb; n = n + digit; if (n < old) { return overflow{}; }; i += 1; }; return (sign, n); }; // stoi64 — parse signed base-b number. Verbatim port of // ref/hare/strconv/stoi.ha:9-17. types.I64_MAX is inlined (the const is // package-private — see the mulshift32 note in ftos.ww). export fn stoi64(s: str, b: base) (i64 | invalid | overflow) = { let (sign, u) = parseint(s, b)?; // Two's complement: I64_MIN = -I64_MAX - 1. Hare's two if-expressions // (stoi.ha:12,16) are lowered to statement-if — ww has no // if-expression (standing divergence, see the note in stof.ww:31). let max: u64 = 9223372036854775807u64; if (sign) { max = max + 1u64; }; if (u > max) { return overflow{}; }; let r: i64 = u: i64; if (sign) { r = -r; }; return r; }; // stou64 — parse unsigned base-b number. Verbatim port of // ref/hare/strconv/stou.ha:70-76. export fn stou64(s: str, b: base) (u64 | invalid | overflow) = { let (sign, u) = parseint(s, b)?; if (sign) { return overflow{}; }; return u; }; 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; }; // stoi — parse signed base-b number into an int. Mirrors Hare's // strconv::stoi (ref/hare/strconv/stoi.ha:53), which clamps to // types::INT_MIN/INT_MAX via stoiminmax. ww's int is a machine word // (8B → i64-width, so INT_MIN/INT_MAX == I64_MIN/I64_MAX per // lib/types/types.ww:30-31), so stoi64's result always fits and the // clamp is a no-op — omitted, not inlined (the bound consts are // package-private; see the inline note at mulshift32 in ftos.ww). export fn stoi(s: str, b: base) (int | invalid | overflow) = { let r = stoi64(s, b); match (r) { case let v: i64 => return v: int; 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; }; // stou — parse unsigned base-b number into a uint. Mirrors Hare's // strconv::stou (ref/hare/strconv/stou.ha:107), which clamps to // types::UINT_MAX via stoumax. ww's uint is a machine word (8B → // u64-width, so UINT_MAX == U64_MAX per lib/types/types.ww:33), so // stou64's result always fits and the clamp is a no-op. export fn stou(s: str, b: base) (uint | invalid | overflow) = { let r = stou64(s, b); match (r) { case let v: u64 => return v: uint; case let e: invalid => return e; case let e: overflow => return e; }; return 0: invalid; }; // stoz — parse unsigned base-b number into a size. Mirrors Hare's // strconv::stoz (ref/hare/strconv/stou.ha:113). ww's size is u64-width // (SIZE_MAX == U64_MAX per lib/types/types.ww:37), so the clamp is a no-op. export fn stoz(s: str, b: base) (size | invalid | overflow) = { let r = stou64(s, b); match (r) { case let v: u64 => return v: size; 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; import strings; // ---- 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_MODRESET = 87, // `//ww:module-reset` driver bundle boundary: // reset curmod to "" before a package-less file // (#16 option-B; cstage TK_MODRESET twin) TK_LAST = 88, }; // ---- 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 --------------------------------------------------- // keep alphabetised, so kwlookup is easy to read — mirrors the C twin // cmd/wcc/tok.c:18-47. Two parallel arrays, not a [N]kwent array-of- // struct: a str *inside* an aggregate element is the filed #18 follow-up // (str-in-aggregate). `let`, not `def`: #18's module-level [N]str static // init is scoped to the DATAW (`let`) directive (A_DATAR needs a DATAW // holder); `def [N]str` is the same filed follow-up. kwkinds is a plain // [N]tkind enum byte-array (pre-#18 path). Explicit [30], NOT [_]: // [_] static-init silently miscompiles to a zero-length array in-tree // (probe at cc69daf — len() returns 0, no diagnostic); filed bug. let kwnames: [30]str = [ "as", "break", "case", "chan", "const", "continue", "def", "defer", "else", "enum", "export", "false", "fn", "for", "if", "is", "import", "let", "match", "nil", "package", "proc", "return", "static", "struct", "switch", "true", "type", "void", "yield", ]; let kwkinds: [30]tkind = [ tkind.TK_AS, tkind.TK_BREAK, tkind.TK_CASE, tkind.TK_CHAN, tkind.TK_CONST, tkind.TK_CONTINUE, tkind.TK_DEF, tkind.TK_DEFER, tkind.TK_ELSE, tkind.TK_ENUM, tkind.TK_EXPORT, tkind.TK_FALSE, tkind.TK_FN, tkind.TK_FOR, tkind.TK_IF, tkind.TK_IS, tkind.TK_USE, tkind.TK_LET, tkind.TK_MATCH, tkind.TK_NIL, tkind.TK_MODULE, tkind.TK_PROC, tkind.TK_RETURN, tkind.TK_STATIC, tkind.TK_STRUCT, tkind.TK_SWITCH, tkind.TK_TRUE, tkind.TK_TYPE, tkind.TK_VOID, tkind.TK_YIELD, ]; // kwlookup — returns the matching TK_* keyword kind for a byte run, // or tkind.TK_NONE if it's an ordinary identifier. Linear scan over the // table, matching cmd/wcc/tok.c:kwlookup (N=30, no hash). export fn kwlookup(p: *u8, n: i32) tkind = { let cand: str; cand.ptr = p; cand.len = n; let i: i32 = 0; for (i < len(kwnames)) { if (strings.compare(cand, kwnames[i]) == 0) { return kwkinds[i]; }; i += 1; }; 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 = { switch (k) { case tkind.TK_NONE: return ""; case tkind.TK_EOF: return "EOF"; case tkind.TK_ERR: return "ERR"; case tkind.TK_IDENT: return "IDENT"; case tkind.TK_INT: return "INT"; case tkind.TK_FLOAT: return "FLOAT"; case tkind.TK_RUNE: return "RUNE"; case tkind.TK_STR: return "STR"; case tkind.TK_FN: return "fn"; case tkind.TK_LET: return "let"; case tkind.TK_DEF: return "def"; case tkind.TK_IF: return "if"; case tkind.TK_ELSE: return "else"; case tkind.TK_FOR: return "for"; case tkind.TK_SWITCH: return "switch"; case tkind.TK_CASE: return "case"; case tkind.TK_RETURN: return "return"; case tkind.TK_USE: return "import"; case tkind.TK_TYPE: return "type"; case tkind.TK_STRUCT: return "struct"; case tkind.TK_DEFER: return "defer"; case tkind.TK_BREAK: return "break"; case tkind.TK_CONTINUE: return "continue"; case tkind.TK_EXPORT: return "export"; case tkind.TK_PROC: return "proc"; case tkind.TK_CHAN: return "chan"; case tkind.TK_NIL: return "nil"; case tkind.TK_TRUE: return "true"; case tkind.TK_FALSE: return "false"; case tkind.TK_AS: return "as"; case tkind.TK_IS: return "is"; case tkind.TK_VOID: return "void"; case tkind.TK_YIELD: return "yield"; case tkind.TK_STATIC: return "static"; case tkind.TK_MATCH: return "match"; case tkind.TK_CONST: return "const"; case tkind.TK_UNDER: return "_"; case tkind.TK_ENUM: return "enum"; case tkind.TK_MODULE: return "package"; case tkind.TK_MODRESET: return "//ww:module-reset"; case tkind.TK_LPAREN: return "("; case tkind.TK_RPAREN: return ")"; case tkind.TK_LBRACE: return "{"; case tkind.TK_RBRACE: return "}"; case tkind.TK_LBRACK: return "["; case tkind.TK_RBRACK: return "]"; case tkind.TK_COMMA: return ","; case tkind.TK_SEMI: return ";"; case tkind.TK_COLON: return ":"; case tkind.TK_DOT: return "."; case tkind.TK_ELLIPSIS: return "..."; case tkind.TK_DOTDOT: return ".."; case tkind.TK_AT: return "@"; case tkind.TK_QUESTION: return "?"; case tkind.TK_ASSIGN: return "="; case tkind.TK_PLUSEQ: return "+="; case tkind.TK_MINUSEQ: return "-="; case tkind.TK_STAREQ: return "*="; case tkind.TK_SLASHEQ: return "/="; case tkind.TK_PERCENTEQ: return "%="; case tkind.TK_AMPEQ: return "&="; case tkind.TK_PIPEEQ: return "|="; case tkind.TK_CARETEQ: return "^="; case tkind.TK_LSHIFTEQ: return "<<="; case tkind.TK_RSHIFTEQ: return ">>="; case tkind.TK_PLUS: return "+"; case tkind.TK_MINUS: return "-"; case tkind.TK_STAR: return "*"; case tkind.TK_SLASH: return "/"; case tkind.TK_PERCENT: return "%"; case tkind.TK_AMP: return "&"; case tkind.TK_PIPE: return "|"; case tkind.TK_CARET: return "^"; case tkind.TK_TILDE: return "~"; case tkind.TK_LSHIFT: return "<<"; case tkind.TK_RSHIFT: return ">>"; case tkind.TK_EQ: return "=="; case tkind.TK_NEQ: return "!="; case tkind.TK_LT: return "<"; case tkind.TK_LE: return "<="; case tkind.TK_GT: return ">"; case tkind.TK_GE: return ">="; case tkind.TK_AND: return "&&"; case tkind.TK_OR: return "||"; case tkind.TK_NOT: return "!"; case tkind.TK_LARROW: return "<-"; case tkind.TK_ARROW: return "->"; case tkind.TK_FATARROW: return "=>"; case 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] = '\\'; out[1] = '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, '"'); let i: i32 = 0; for (i < n) { let c: u8 = p[i]; switch (c) { case '\\': fputsstr(fd, "\\\\"); case '"': fputsstr(fd, "\\\""); case '\n': fputsstr(fd, "\\n"); case '\t': fputsstr(fd, "\\t"); case '\r': fputsstr(fd, "\\r"); case: if (c < ' ' || c == 127u8) { fputhex2(fd, c); } else { fputcbyte(fd, c); }; }; i += 1; }; fputcbyte(fd, '"'); }; // 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, ':'); let ls: str = strconv.i64tos(t.line: i64, strconv.base.DEC); os.write(fd, ls.ptr, ls.len: u64); fputcbyte(fd, ':'); let cs: str = strconv.i64tos(t.col: i64, strconv.base.DEC); os.write(fd, cs.ptr, cs.len: u64); fputcbyte(fd, ' '); fputsstr(fd, tokname(t.kind)); switch (t.kind) { case tkind.TK_IDENT, tkind.TK_STR, tkind.TK_ERR: fputcbyte(fd, ' '); fputq(fd, ttext.ptr, ttext.len); case tkind.TK_INT, tkind.TK_RUNE: fputcbyte(fd, ' '); 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, '\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; import strconv; // 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 == '_') { return true; }; return false; }; fn isidpart(c: rune) bool = { if (ascii.isalnum(c)) { return true; }; if (c == '_') { 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 - '0'): i32; }; if (c >= 'A') { if (c <= 'F') { return ((c - 'A') + 10): i32; }; }; if (c >= 'a') { if (c <= 'f') { return ((c - 'a') + 10): i32; }; }; return; }; type lex = struct { file: str, src: *u8, // raw bytes; not necessarily NUL-terminated srclen: u64, lpos: u64, line: i32, col: i32, errs: i32, // a `//ww:module-reset` directive was seen in the last skipped run; // lexnext emits TK_MODRESET before the next real token (#16 opt-B). modreset: 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; l.modreset = 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 == '\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; }; 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); let ls: str = strconv.u64tos(p.line: u64, strconv.base.DEC); os.write(2, ls.ptr, ls.len: u64); os.write(2, ":".ptr, 1u64); let cs: str = strconv.u64tos(p.col: u64, strconv.base.DEC); os.write(2, cs.ptr, cs.len: u64); 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 == ' ') { lget(l); continue; }; if (c == '\t') { lget(l); continue; }; if (c == '\r') { lget(l); continue; }; if (c == '\n') { lget(l); continue; }; if (c == '/') { let c2: i32 = lpeek(l, 1u64); if (c2 == '/') { lget(l); lget(l); // consume '//' // #16 opt-B: recognize the driver's curmod-reset // boundary directive `//ww:module-reset` (whole // line) and flag it; lexnext emits TK_MODRESET. // The body is then skipped like any comment. // Mirrors cstage lex.c skipws. Compare via lpeek // (no consume) so the skip loop below is unchanged. let dir: str = "ww:module-reset"; let di: i32 = 0; let matched: bool = true; for (di < dir.len) { if (lpeek(l, di: u64) != dir[di]: i32) { matched = false; break; }; di += 1; }; if (matched) { let nx: i32 = lpeek(l, dir.len: u64); if (nx == '\n') { l.modreset = 1; } else { if (nx < 0) { l.modreset = 1; }; }; }; for (true) { let cx: i32 = lpeek(l, 0u64); if (cx < 0) { return false; }; if (cx == '\n') { break; }; lget(l); }; continue; }; if (c2 == '*') { 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 == '*') { if (x == '/') { 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 == '_') { 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; }; if (v > ~0u64 / (base: u64)) { *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 == 'n') { *out = '\n'; return true; }; if (c == 't') { *out = '\t'; return true; }; if (c == 'r') { *out = '\r'; return true; }; if (c == '\\') { *out = '\\'; return true; }; if (c == '\'') { *out = '\''; return true; }; if (c == '"') { *out = '"'; return true; }; if (c == '0') { *out = '\0'; return true; }; if (c == 'a') { *out = '\a'; return true; }; if (c == 'b') { *out = '\b'; return true; }; if (c == 'f') { *out = '\f'; return true; }; if (c == 'v') { *out = '\v'; return true; }; if (c == 'x') { 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 != '_') { 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 != '_') { break; }; }; lget(l); }; }; fn scanbinrun(l: *lex) void = { for (true) { let c: i32 = lpeek(l, 0u64); if (c == '0') { lget(l); continue; }; if (c == '1') { lget(l); continue; }; if (c == '_') { lget(l); continue; }; break; }; }; fn scanoctrun(l: *lex) void = { for (true) { let c: i32 = lpeek(l, 0u64); if (c < '0') { break; }; if (c > '7') { if (c != '_') { 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 != 'e') { if (e != 'E') { return; }; }; lget(l); let s: i32 = lpeek(l, 0u64); if (s == '+') { lget(l); } else { if (s == '-') { lget(l); }; }; for (true) { let c: i32 = lpeek(l, 0u64); if (c < 0) { break; }; if (!ascii.isdigit(c: rune)) { break; }; lget(l); }; }; fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.kind = 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 == '0') { if (c1 == 'x') { lget(l); lget(l); base = 16; scanhexrun(l); } else { if (c1 == 'X') { lget(l); lget(l); base = 16; scanhexrun(l); } else { if (c1 == 'b') { lget(l); lget(l); base = 2; scanbinrun(l); } else { if (c1 == 'B') { lget(l); lget(l); base = 2; scanbinrun(l); } else { if (c1 == 'o') { lget(l); lget(l); base = 8; scanoctrun(l); } else { if (c1 == 'O') { lget(l); lget(l); base = 8; scanoctrun(l); } else { scandecimalrun(l); if (lpeek(l, 0u64) == '.') { let after: i32 = lpeek(l, 1u64); if (after >= '0') { if (after <= '9') { isfloat = true; lget(l); scandecimalrun(l); scanexp(l); }; }; }; };};};};};}; } else { scandecimalrun(l); if (lpeek(l, 0u64) == '.') { let after: i32 = lpeek(l, 1u64); if (after >= '0') { if (after <= '9') { 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 != '_') { clean[j] = b; j += 1u64; }; i += 1u64; }; clean[j] = 0u8; let cleanv: str; cleanv.ptr = clean.ptr; cleanv.len = j: i32; // strconv's correctly-rounded decimal engine — cstage folds // via strtod, and a leaner pow-10 fold here was 1-2 ULP off // on long-mantissa/extreme literals (cs≠ww DATA bits, #62). // `0: f64` cast, not a 0.0 literal: 990's wwdump diff relies // on this file tokenising identically through C and ww, and // the C dumper %g-formats TK_FLOAT.fval while the ww dumper // skips it. // Retained divergence (task #21): SUBNORMAL literals are // accepted here correctly-rounded (Hare stof semantics) // but rejected by cstage (glibc strtod flags partial // underflow with ERANGE). let fv: f64 = 0: f64; match (strconv.stof64(cleanv, strconv.base.DEC)) { case let v: f64 => { fv = v; }; case let e: strconv.invalid => { errat(l, start, "bad float literal"); }; case let e: strconv.overflow => { errat(l, start, "bad float literal"); }; }; 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] == 'i') { if (p[1] == '8') { isok = true; }; // i8 }; if (p[0] == 'u') { if (p[1] == '8') { isok = true; }; // u8 }; }; if (sl == 3u64) { if (p[0] == 'i') { if (p[1] == '1') { if (p[2] == '6') { isok = true; }; }; // i16 if (p[1] == '3') { if (p[2] == '2') { isok = true; }; }; // i32 if (p[1] == '6') { if (p[2] == '4') { isok = true; }; }; // i64 }; if (p[0] == 'u') { if (p[1] == '1') { if (p[2] == '6') { isok = true; }; }; if (p[1] == '3') { if (p[2] == '2') { isok = true; }; }; if (p[1] == '6') { if (p[2] == '4') { isok = true; }; }; }; if (p[0] == 'f') { if (p[1] == '3') { if (p[2] == '2') { isok = true; }; }; // f32 if (p[1] == '6') { if (p[2] == '4') { 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] == '_') { 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 == '"') { lget(l); break; }; let ch: i32 = 0; if (c == '\\') { 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 == '\\') { lget(l); if (!escape(l, &ch)) { ch = 0; }; } else { ch = lget(l); }; if (lpeek(l, 0u64) != '\'') { 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; let more: bool = skipws(l); let start: pos; curpos(l, &start); // A `//ww:module-reset` seen in the skipped run surfaces as its own // token before the next real one (#16 opt-B boundary reset). if (l.modreset != 0) { l.modreset = 0; emitsimple(&start, tkind.TK_MODRESET, out); return; }; if (!more) { emitsimple(&start, tkind.TK_EOF, out); return; }; 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 == '"') { lget(l); lexstr(l, &start, out); return; }; if (c == '\'') { lget(l); lexrune(l, &start, out); return; }; lget(l); if (c == '(') { emitsimple(&start, tkind.TK_LPAREN, out); return; }; if (c == ')') { emitsimple(&start, tkind.TK_RPAREN, out); return; }; if (c == '{') { emitsimple(&start, tkind.TK_LBRACE, out); return; }; if (c == '}') { emitsimple(&start, tkind.TK_RBRACE, out); return; }; if (c == '[') { emitsimple(&start, tkind.TK_LBRACK, out); return; }; if (c == ']') { emitsimple(&start, tkind.TK_RBRACK, out); return; }; if (c == ',') { emitsimple(&start, tkind.TK_COMMA, out); return; }; if (c == ';') { emitsimple(&start, tkind.TK_SEMI, out); return; }; if (c == ':') { emitsimple(&start, tkind.TK_COLON, out); return; }; if (c == '@') { emitsimple(&start, tkind.TK_AT, out); return; }; if (c == '?') { emitsimple(&start, tkind.TK_QUESTION, out); return; }; if (c == '~') { emitsimple(&start, tkind.TK_TILDE, out); return; }; if (c == '.') { if (lpeek(l, 0u64) == '.') { if (lpeek(l, 1u64) == '.') { 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 == '+') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_PLUSEQ, out); return; }; emitsimple(&start, tkind.TK_PLUS, out); return; }; if (c == '-') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_MINUSEQ, out); return; }; if (lpeek(l, 0u64) == '>') { lget(l); emitsimple(&start, tkind.TK_ARROW, out); return; }; emitsimple(&start, tkind.TK_MINUS, out); return; }; if (c == '*') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_STAREQ, out); return; }; emitsimple(&start, tkind.TK_STAR, out); return; }; if (c == '/') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_SLASHEQ, out); return; }; emitsimple(&start, tkind.TK_SLASH, out); return; }; if (c == '%') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_PERCENTEQ, out); return; }; emitsimple(&start, tkind.TK_PERCENT, out); return; }; if (c == '&') { if (lpeek(l, 0u64) == '&') { lget(l); emitsimple(&start, tkind.TK_AND, out); return; }; if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_AMPEQ, out); return; }; emitsimple(&start, tkind.TK_AMP, out); return; }; if (c == '|') { if (lpeek(l, 0u64) == '|') { lget(l); emitsimple(&start, tkind.TK_OR, out); return; }; if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_PIPEEQ, out); return; }; emitsimple(&start, tkind.TK_PIPE, out); return; }; if (c == '^') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_CARETEQ, out); return; }; emitsimple(&start, tkind.TK_CARET, out); return; }; if (c == '=') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_EQ, out); return; }; if (lpeek(l, 0u64) == '>') { lget(l); emitsimple(&start, tkind.TK_FATARROW, out); return; }; emitsimple(&start, tkind.TK_ASSIGN, out); return; }; if (c == '!') { if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_NEQ, out); return; }; emitsimple(&start, tkind.TK_NOT, out); return; }; if (c == '<') { if (lpeek(l, 0u64) == '<') { lget(l); if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_LSHIFTEQ, out); return; }; emitsimple(&start, tkind.TK_LSHIFT, out); return; }; if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_LE, out); return; }; if (lpeek(l, 0u64) == '-') { lget(l); emitsimple(&start, tkind.TK_LARROW, out); return; }; emitsimple(&start, tkind.TK_LT, out); return; }; if (c == '>') { if (lpeek(l, 0u64) == '>') { lget(l); if (lpeek(l, 0u64) == '=') { lget(l); emitsimple(&start, tkind.TK_RSHIFTEQ, out); return; }; emitsimple(&start, tkind.TK_RSHIFT, out); return; }; if (lpeek(l, 0u64) == '=') { 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 = { switch (k) { case nkind.N_NONE: return "none"; case nkind.N_INTLIT: return "int"; case nkind.N_FLOATLIT: return "float"; case nkind.N_STRLIT: return "str"; case nkind.N_RUNELIT: return "rune"; case nkind.N_TRUE: return "true"; case nkind.N_FALSE: return "false"; case nkind.N_NIL: return "nil"; case nkind.N_IDENT: return "id"; case nkind.N_BIN: return "bin"; case nkind.N_UN: return "un"; case nkind.N_CALL: return "call"; case nkind.N_INDEX: return "index"; case nkind.N_DOT: return "dot"; case nkind.N_CAST: return "cast"; case nkind.N_STRUCTLIT: return "structlit"; case nkind.N_ARRLIT: return "arrlit"; case nkind.N_FIELD: return "field"; case nkind.N_ASSIGN: return "assign"; case nkind.N_ALLOC: return "alloc"; case nkind.N_FREE: return "free"; case nkind.N_RECV: return "recv"; case nkind.N_SLICE: return "slice"; case nkind.N_SPREAD: return "spread"; case nkind.N_BLOCK: return "block"; case nkind.N_EXPRSTMT: return "exprstmt"; case nkind.N_LET: return "let"; case nkind.N_RETURN: return "return"; case nkind.N_IF: return "if"; case nkind.N_FOR: return "for"; case nkind.N_FORRANGE: return "forrange"; case nkind.N_DEFER: return "defer"; case nkind.N_BREAK: return "break"; case nkind.N_CONTINUE: return "continue"; case nkind.N_SWITCH: return "switch"; case nkind.N_CASE: return "case"; case nkind.N_FILE: return "file"; case nkind.N_USE: return "use"; case nkind.N_DEF: return "def"; case nkind.N_TYPEDECL: return "typedecl"; case nkind.N_FNDECL: return "fn"; case nkind.N_PARAM: return "param"; case nkind.N_TNAME: return "tname"; case nkind.N_TPTR: return "tptr"; case nkind.N_TSLICE: return "tslice"; case nkind.N_TARRAY: return "tarray"; case nkind.N_TFN: return "tfn"; case nkind.N_TSTRUCT: return "tstruct"; case nkind.N_TFIELD: return "tfield"; case nkind.N_TCHAN: return "tchan"; case nkind.N_ATTR: return "attr"; case nkind.N_TTUPLE: return "ttuple"; case nkind.N_TTAGGED: return "ttagged"; case nkind.N_TUPLE: return "tuple"; case nkind.N_MATCH: return "match"; case nkind.N_MCASE: return "mcase"; case nkind.N_TRYPROP: return "tryprop"; case nkind.N_TRYUNW: return "tryunw"; case nkind.N_MLET: return "mlet"; case nkind.N_MASSIGN: return "massign"; case nkind.N_TYPETEST: return "typetest"; case nkind.N_TYPEASSERT: return "typeassert"; case nkind.N_VOIDLIT: return "voidlit"; case nkind.N_TBANG: return "tbang"; case nkind.N_YIELD: return "yield"; case nkind.N_TENUM: return "tenum"; case nkind.N_TENUMMEMBER: return "tenummember"; case nkind.N_TPARAM: return "tparam"; case 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, '"'); let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; if (c == '"') { os.write(fd, "\\\"".ptr, 2u64); } else { if (c == '\\') { os.write(fd, "\\\\".ptr, 2u64); } else { if (c == '\n') { os.write(fd, "\\n".ptr, 2u64); } else { if (c == '\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, '('); 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, '\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 = "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] = '.'; 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 = p.curfile; let pl = p.curline; let pc = p.curcol; if (p.curkind == tkind.TK_NOT) { // `!T` — Hare error-flagged type wrapper. advance(p); let n = newnode(nkind.N_TBANG, pf, pl, pc); n.lhs = parsetype(p); return n; }; if (p.curkind == tkind.TK_STAR) { advance(p); let n = 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 = newnode(nkind.N_TSLICE, pf, pl, pc); n.lhs = parsetype(p); return n; }; let n = 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 = 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 = p.curfile; let fpl = p.curline; let fpc = p.curcol; let f = 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 = 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 = p.curfile; let mpl = p.curline; let mpc = p.curcol; let m = 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 = newnode(nkind.N_TNAME, pf, pl, pc); n.str = "void"; advance(p); return n; }; if (p.curkind == tkind.TK_IDENT) { let n = newnode(nkind.N_TNAME, pf, pl, pc); let acc = 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 = accepttok(p, tkind.TK_ELLIPSIS); let first = parsetype(p); if (firstspread) { first.op = tkind.TK_ELLIPSIS; }; if (accepttok(p, tkind.TK_PIPE)) { let n = newnode(nkind.N_TTAGGED, pf, pl, pc); let head = first; let tail = first; for (true) { let spread = accepttok(p, tkind.TK_ELLIPSIS); let e = 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 = newnode(nkind.N_TTUPLE, pf, pl, pc); let firstwrap = newnode(nkind.N_TPARAM, pf, pl, pc); firstwrap.lhs = first; let head = firstwrap; let tail = firstwrap; for (true) { let e = parsetype(p); let w = 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 = 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 = 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; }; // `//ww:module-reset` — bundle boundary before a package-less // file. Reset curmod to "" so the file's decls (and its own // `import os;`) are attributed to the primary module, not the // preceding bundled package. Codegen-neutral: "" curmod keeps // bare symbols. (#16 option-B; closes task #11.) // Driver-emitted ONLY before package-less files; a hand-placed // directive after a mid-file `package` would strip subsequent // decls to bare — that usage is deliberate-only. if (p.curkind == tkind.TK_MODRESET) { advance(p); let empty: str; empty.ptr = nil; empty.len = 0; p.curmod = empty; continue; }; let attrs = 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 { // cstage parse.c:1395-1400 errorf+p->errs++ on the // default arm: an unknown top-level construct is a loud // reject, not a silent skip. ww chews the whole decl at // once (below), so one error per fallback entry. errmsg(p, "expected top-level decl"); // 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 = p.curfile; let pl = p.curline; let pc = 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 = newnode(nkind.N_MLET, pf, pl, pc); let head: *node = nil; let tail: *node = nil; for (true) { let lpf = p.curfile; let lpl = p.curline; let lpc = p.curcol; let l = 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 = head; for (lc != nil) { lc.op = tkind.TK_CONST; lc = lc.next; }; }; return m; }; let n = 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 = newnode(nkind.N_MLET, pf, pl, pc); let head = n; let tail = n; for (accepttok(p, tkind.TK_COMMA)) { let lpf = p.curfile; let lpl = p.curline; let lpc = p.curcol; let l = 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 = 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 = p.curfile; let pl = p.curline; let pc = p.curcol; expecttok(p, tkind.TK_LBRACE, "expected '{' to open block"); let blk = 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 = 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 = p.curfile; let pl = p.curline; let pc = p.curcol; advance(p); // past `if` expecttok(p, tkind.TK_LPAREN, "expected '(' after if"); let n = 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 = p.curfile; let pl = p.curline; let pc = 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 = p.curfile; let npl = p.curline; let npc = p.curcol; let e = 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 = 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 = (p.curkind == tkind.TK_UNDER); let nm: str; nm.ptr = nil; nm.len = 0; if (!isunder) { nm = p.curtext; }; let lpf = p.curfile; let lpl = p.curline; let lpc = p.curcol; advance(p); // consume IDENT/UNDER if (p.curkind == tkind.TK_DOTDOT) { advance(p); let rng = 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 = 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 = 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 = newnode(nkind.N_FOR, pf, pl, pc); let first = 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 = p.curfile; let pl = p.curline; let pc = p.curcol; advance(p); // past `switch` expecttok(p, tkind.TK_LPAREN, "expected '(' after switch"); let n = 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 = p.curfile; let cpl = p.curline; let cpc = p.curcol; advance(p); // past `case` let cs = 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 = 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 = parsestmt(p); if (s != nil) { if (bh == nil) { bh = s; } else { bt.next = s; }; bt = s; }; }; let blk = 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 = p.curfile; let pl = p.curline; let pc = 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 = 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 = parseif(p); expecttok(p, tkind.TK_SEMI, "expected ';' after if"); return n; }; if (p.curkind == tkind.TK_FOR) { let n = parsefor(p); expecttok(p, tkind.TK_SEMI, "expected ';' after for"); return n; }; if (p.curkind == tkind.TK_SWITCH) { let n = parseswitch(p); expecttok(p, tkind.TK_SEMI, "expected ';' after switch"); return n; }; if (p.curkind == tkind.TK_RETURN) { advance(p); let n = newnode(nkind.N_RETURN, pf, pl, pc); if (p.curkind != tkind.TK_SEMI) { let first = 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 = newnode(nkind.N_TUPLE, pf, pl, pc); t.list = first; let tail = first; for (accepttok(p, tkind.TK_COMMA)) { let e = 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 = 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 = 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 = parseexpr(p); if (p.curkind == tkind.TK_COMMA) { let m = newnode(nkind.N_MASSIGN, pf, pl, pc); let head = e; let tail = e; for (p.curkind == tkind.TK_COMMA) { advance(p); let lv = 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 = 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, resolving: i32, // #62/#69: TY_NAMED demand-resolution cycle guard. // Mirrors cstage Type.resolving (cmd/wcc/ww.h) // and harec idecl->in_progress (ref/harec/src/ // check.c:4767): set while the alias body // resolves; a VALUE-position read of an // in-progress named is a true type cycle and // loud-rejects. Pointer positions never read // size, so legal self-refs stay accepted. 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. perf #18: the original // flat prepend-only list made the cache-MISS scan O(N) per call -> // O(N2) over a compile (91% of all wwstage instructions on a 5k-line // input). Now a node-ptr hash index, mirroring sym.ww scope.buckets // (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch // only one bucket -> O(1) amortized. Identical lookup results (same // *tinfo for the same node), so emitted asm is byte-identical. type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, }; // Tuning knob, NOT a type size (rule-13 N/A): power-of-two so the // bucket index is a MASK, not a mod. ~5400 nodes on a big input -> // well under one entry/bucket. Mirror of sym.ww:38 NBUCKETS (16), // scaled up — sym's 16 would give ~340-deep chains here. def NBUCKETS_TINFO: u64 = 8192u64; // ---- 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, tinfobuckets: **tinfocacheent, // length NBUCKETS_TINFO; node-ptr hash index }; // ---- 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); let tib: []*tinfocacheent = alloc([], NBUCKETS_TINFO)!; // mirror sym.ww:63 c.tinfobuckets = tib.ptr; }; 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; // peel-ok: construction 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. // Node ptrs are 8+-aligned, so the low 3-4 bits are always zero — // shift right 4 before masking or every 16th bucket would cluster. fn tinfobucket(key: *node) u64 = { return ((key: u64) >> 4u64) & (NBUCKETS_TINFO - 1u64); }; export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { let bi: u64 = tinfobucket(key); let e: *tinfocacheent = c.tinfobuckets[bi]; 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 bi: u64 = tinfobucket(key); let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfobuckets[bi]})!; c.tinfobuckets[bi] = 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); }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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; }; // peel-ok: recursive chase 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_TAGGED) { // Structural: variant lists match position-by-position, and // the nullable `(*T|void)` fold is part of identity. Mirrors // cstage type_eq's TY_TAGGED arm (cmd/wcc/type.c:271-284); // the missing branch let any two tagged unions compare equal // (fell through to the primitive `return true`), which // #218's cgvariantmatch structural fallback was the first // caller to exercise. if (a.nullable != b.nullable) { 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_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) use_alias: i32, // #30: this value/type decl ALSO names an imported // module (the fnmatch.fnmatch / random.random shape). // Set when installtop promotes a same-leaf SK_USE in // place; the N_DOT guards treat such a sym as a module // for `name.member`. Mirror cstage Sym.use_alias // (cmd/wcc/check.c:2831-2951 promote + 87/1337 guards). 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, same-module preferred. // // 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 — the struct's mod may differ from the leaf (lex/tok // pair) so scopelookupinmodule(c, leaf, leaf) won't find it. // // #58/#50: within each scope, Pass-1 prefers an SK_TYPE whose `sym.mod` // matches `mod`; Pass-2 falls back to the first SK_TYPE regardless of // mod (chain-first, the prior behavior). scopedefineinmodule PREPENDS, // so chain-first = last-registered — when two modules export the same // type leaf the bare walk silently picked the newest-installed one, // install-order-dependent, while cstage is deterministic on cur_mod. // Mirrors cstage cmd/wcc/sym.c scope_lookup_type(s, mod, name) (the // kind-filtered + mod-preferring single walk); sole caller passes // c.curmod. i32-correct: streq throughout, only `.len > 0` guards (the // reverted attempt compared str.len as u64 — str.len is i32). export fn scopelookuptype(s: *scope, mod: str, name: str) *sym = { 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.skind == skind.SK_TYPE) { if (mod.len > 0) { 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; }; // 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. // // #30 (design reversal): this serves the DISTINCT-mod two-sym case only — // a `fn fnmatch` (mod="fnmatch") coexisting with `import fnmatch`'s SK_USE // (mod=""), where scopelookupprefer lands on the value and the dot re- // resolves to the SK_USE here. The SAME-mod collision (a primary-package // decl whose leaf also names a bundled module — `type sym` vs `import sym`, // `@test fn ascii` vs the fnmatch->ascii bundle floor) is NO LONGER left to // two coexisting syms: that ripples into every bare-ref resolver (a missed // site is a byte-id-consistent-but-wrong cat-A risk the gate can't prove // away). Instead installtop now PROMOTES the SK_USE in place to the value // kind with use_alias=1 (selfhost/cmd/wcc/check.ww installtop), mirroring // cstage's Sym.use_alias promote exactly (cmd/wcc/check.c:2831-2951); the // N_DOT guards honor `skind == SK_USE || use_alias` directly. ONE // correctly-kinded sym → all resolvers correct by construction. Cite: // task #30; project memory module_type_name_collision (cstage 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, use_alias=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; }; // scopesamekeysym — the entry scopedefineinmodule(name, mod) treats as a // duplicate (same name, same mod-key), or nil if the key is free. Lets a // caller that got a nil from scopedefineinmodule learn WHAT it collided // with (e.g. a pre-seeded builtin vs a genuine user redeclaration). The // match logic mirrors scopedefineinmodule's reject branch exactly. export fn scopesamekeysym(s: *scope, name: str, mod: str) *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 b; }; } else { if (mod.len > 0) { if (streq(b.mod, mod)) { return b; }; }; }; }; b = b.hashnext; }; return nil; }; // 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, istest: i32, // #15: `w6c_ww -T` — collect @test fns + // synth the entry; loud-reject a user main. 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. allococtx: *node, // #3/B': the one empty alloc([], n) call node // with let-declared slice context this walk; // any other empty alloc has no element hint // and must fail to infer (harec // check.c:1801). Set by checkletassign // around its exprtype, nil elsewhere. }; // cerr — bare stderr fragment writer for the checker's piecewise // diagnostics. Tool-local (NOT a lib wrapper): messages are built from // many fragments and we route through os.write to avoid libc stdio. // .len replaces the error-prone hand-counted byte literals these sites // carried. Lives here (not err.ww) because the selfhost build pulls // check.ww but not err.ww (which ports err.c for the C-driven path). fn cerr(m: str) void = { os.write(2, m.ptr, m.len: u64); }; // circularnamed — #62/#69 cycle guard, cstage circular_named twin // (cmd/wcc/check.c): a VALUE-position read of a TY_NAMED whose body is // still resolving is a true type cycle (infinite size) — loud, per // harec's in_progress check (ref/harec/src/check.c:4767 "Circular // dependency for '%s'"). Pointer/slice/chan/fn positions never read // the target's size and legitimately receive the in-progress // placeholder, so `type node = struct { next: *node }` stays legal. // Pre-#62 a pure alias cycle left a CYCLIC under-chain in the table // and every NAMED-chain chase loop downstream spun forever (the #69 // compiler hang); a struct-value cycle recursed the slot walkers to // stack overflow. fn circularnamed(c: *checker, t: *tinfo, n: *node) bool = { if (t == nil) { return false; }; if (t.kind != tykind.TY_NAMED) { return false; }; if (t.resolving == 0) { return false; }; if (n != nil) { cerr(n.file); cerr(": "); }; cerr("error: circular type dependency: '"); cerr(t.name); cerr("'\n"); c.errs += 1; // Loud-STOP, not accumulate: wwstage's AST-level alias walkers // (resolvealias, cgenutil aliaslookup chains) follow TNAME->TNAME // by NAME, blind to the tinfo table — on a cyclic alias graph they // spin forever even after the table edge is cut to tyerr (measured: // error printed once, then hang). cstage accumulates instead — its // single-peel ternaries can't loop. Asymmetry is deliberate; both // stages reject with the same message + non-zero exit. os.exit(1); }; // 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`, `delete`, `insert` 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); scopedefine(c.top, "delete", skind.SK_FN, nil, nil); scopedefine(c.top, "insert", 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; }; cerr(kindstr); cerr(" '"); cerr(name); cerr("' shadows imported module '"); cerr(name); cerr("'\n"); 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). // #23: top-level duplicate type/def/fn/let now reject loud here, keyed // on (name, mod) exactly as cstage's install pass does // (cmd/wcc/check.c:2852/2911/2932/2955) — see dupdecl below. (Same-scope // LOCAL dup `let a=1; let a=2;` is a different path and stays deferred to // #11; test/wcc/708 + test/wcc/696 are that cstage-only neg-case.) 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); // check-(c) self-import: a package may not import itself. Pure // owner==leaf string compare, package-model-independent. check-(a) // unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed // pulls lack import->file->symbol provenance). Message byte-identical // to cstage check.c. if (k == nkind.N_USE) { if (mod.len != 0 && streq(nm, mod)) { cerr("self-import: package '"); cerr(mod); cerr("' cannot import itself\n"); c.errs += 1i32; }; // #30 value-before-use: a same-leaf VALUE/type decl is already // installed (source order placed `fn aa` before `import aa`). // Promote it in place with use_alias instead of installing a // coexisting SK_USE, so `aa.member` resolves through the N_DOT // use_alias guard. This is the order-mirror of installtop's // value-arm promote and reaches the IDENTICAL single-sym end // state (skind=value, use_alias=1, decl=value). cstage is order- // independent — it installs all N_USE in a dedicated first pass // (cmd/wcc/check.c:2811+) so its value-arm promote always finds // the SK_USE; wwstage installs in source order, so this direction // is closed here, mirroring cstage's self-import N_USE arm // (check.c:2823-2834 `if (prev) prev->use_alias = 1`). A pure // duplicate import (prev already SK_USE) keeps the coexisting- // entry path (orthogonal to #30, pre-existing wwstage behavior). let prev: *sym = scopelookuplocal(c.top, nm); if (prev != nil) { if (prev.skind != skind.SK_USE) { prev.use_alias = 1i32; return; }; }; scopedefine(c.top, nm, skind.SK_USE, nil, d); return; }; if (k == nkind.N_DEF) { installtop(c, d, nm, mod, skind.SK_DEF, "def"); return; }; if (k == nkind.N_TYPEDECL) { installtop(c, d, nm, mod, skind.SK_TYPE, "type"); return; }; if (k == nkind.N_FNDECL) { installtop(c, d, nm, mod, skind.SK_FN, "fn"); return; }; if (k == nkind.N_LET) { installtop(c, d, nm, mod, skind.SK_VAR, "let"); return; }; }; // installtop — install a top-level decl name into c.top, rejecting a // genuine within-module duplicate loud (#23) with cstage's exact // "duplicate %s" wording (cmd/wcc/check.c:2852/2911/2932/2955). // // scopedefineinmodule returns nil only on a same-(name, mod) re-install. // Same-leaf cross-package decls carry distinct mods (the flat-bundle // model) and an imported-module bareword's SK_USE keys on mod="", so a // coexisting same-leaf type/fn in its own package never collides. // // The one nil that is NOT a user duplicate: a redeclaration of a // pre-seeded builtin (the predeclared `nomem`, or a primtype name). cstage // keeps no builtins in the scope at all — resolve_typename consults // lookup_builtin FIRST (cmd/wcc/check.c:69) and the builtin always wins, // so a user `type nomem = !void` / `type int = ...` installs dead and is // silently ignored, never a duplicate error. wwstage seeds builtins INTO // c.top, so the same redeclaration surfaces here as a collision; we mirror // cstage by dropping it (the seeded builtin stays, and wins resolution) // rather than erroring. A pre-seeded builtin is identified by its sym // carrying no real source decl (primtypes: decl=nil; `nomem`: a // checkinit-synthesized N_TYPEDECL with an empty .file) — user decls // always carry their parsed source file. fn installtop(c: *checker, d: *node, nm: str, mod: str, k: skind, kind: str) void = { // #30: a top-level value/type decl whose leaf ALSO names an imported // module PROMOTES that same-leaf SK_USE in place — one correctly-kinded // sym carrying use_alias=1, so bare refs (call/structlit/var) resolve to // the value/type while `name.member` still resolves the module via the // N_DOT use_alias guard. Mirrors cstage check.c:2831/2848/2871/2907/ // 2928/2951 exactly (its USE-first install pass guarantees the SK_USE is // present when the value decl lands). A bundled `ascii` module + a // primary-package `@test fn ascii` (989_lib_byteid) is the live case. // wwstage installs in source order, so this arm handles the use-before- // value direction; the value-before-use direction (`fn aa` then `import // aa`) is closed by the symmetric promote in installdecl's N_USE arm // (set use_alias on the pre-installed value sym). Both directions reach // the identical single-sym end state, so cstage and wwstage agree on // every source order (#30). let puse: *sym = scopelookuplocal(c.top, nm); if (puse != nil) { if (puse.skind == skind.SK_USE) { puse.skind = k; puse.decl = d; puse.use_alias = 1i32; if (mod.len > 0) { if (puse.mod.len == 0) { puse.mod = mod; }; }; return; }; }; if (scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; }; let prev: *sym = scopesamekeysym(c.top, nm, mod); if (prev != nil) { if (prev.decl == nil) { return; }; if (prev.decl.file.len == 0) { return; }; }; cerr(d.file); cerr(": error: duplicate "); cerr(kind); cerr(" "); cerr(nm); cerr("\n"); c.errs += 1; }; // 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_TRYUNW) { 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) { // Unshadowed abort/assert binds no sym BY // DESIGN (the EXPR_ASSERT family has no callee // object — see isassertfam); count it resolved // so wwdump -r's zero-unresolved gate holds // over builtin-using lib code (#58 respell). if (isassertfam(c, n)) { c.nresolved += 1; } else { c.nunresolved += 1; if (c.verbose != 0) { cerr(" unresolved id: "); cerr(nm); cerr("\n"); }; }; } 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) { cerr(" unresolved tname: "); cerr(nm); cerr("\n"); }; } 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"); // C4 (task #7): bind the ELEMENT type so field // reads off a by-value aggregate binding // (`for (let t .. threads) { t.pc }`) resolve — // pre-C4 the binding's decl was the N_FORRANGE // node itself, whose .lhs is the SCRUTINEE expr, // so exprtype's decl.lhs read handed the dot a // non-type node and asserttyped bailed (cstage // types it: check.c N_FORRANGE scope_define(..., // elem, ...)). Synthetic N_LET binder whose .lhs // is the element tnode — the stamptuplebinds // `b.lhs = et` idiom. A str scrutinee keeps the // old decl: cgen synthesises the u8 elem there // and no dot applies to a u8 binding. let et: *node = nil; let it: *node = exprtype(c, n.lhs, nil); // #80 (F2a batch-4 c4): an alias-typed iterable // arrives as N_TNAME — without the AST-level // dealias the binder fell to the N_FORRANGE // fallback decl, stayed untyped, and any binop // over the rangevar asserttyped-bailed (cs // accepts: its scope_define types the elem). if (it != nil) { it = resolvealias(c, unwrapbang(it)); }; if (it != nil) { if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; }; if (et != nil) { let bn: *node = newnode(nkind.N_LET, n.file, n.line, n.col); bn.str = bnm; bn.lhs = et; scopedefine(c.cur, bnm, skind.SK_VAR, nil, bn); } else { 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; // #242: consume the rhs's tuple type for ANY rhs, not just an // N_CALL — `let (a,b) = t` over a plain tuple ident (e.g. a // match-bound union payload) must stamp its bindings too, or // the un-annotated binder stays untyped and asserttyped aborts. // Mirrors cstage check.c:2017 (cexpr(rhs), unconditional). if (n.rhs != nil) { // `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; // #242: consume the rhs tuple type for ANY rhs (see N_MLET). if (n.rhs != nil) { let rty: *node = exprtype(c, n.rhs, nil); if (rty != nil && rty.kind == nkind.N_TTUPLE) { pt = rty.list; } else { // #38/F2 (review item 39): the multi-assign rhs must be a // tuple. cstage check.c:2562-2568 errors "multi-assign rhs // is not a tuple (got %s)" when cexpr(rhs)->kind != TY_TUPLE // — and, like ww's exprtype (N_CALL returns the decl's bare // return tnode, :3319), it does NOT chase the NAMED wrapper, // so a tuple-ALIAS return (`fn f() pair`) is rejected too. // Without this ww distributed nil element widths and // cgmassign silently dropped the str len/cap stores. ww's // piecewise cerr can't splice the type spelling, so the // "(got %s)" tail is omitted. deffolderr(c, n, "multi-assign rhs is not a tuple"); }; }; 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. // #258: desugar an array arg/rhs into an implicit full slice at the // call-arg and assignment contexts (let / return drive their own // desugar in checkletassign / checkretassign). Placed post-child-walk // so arg/operand types are stamped, and before the end-dispatch // exprtype below so a regular N_CALL is still an N_CALL (not folded to // an N_INTLIT by the size/align intercept). Mirrors cstage's post- // order cexpr desugar at the call-arg / N_ASSIGN sites. if (k == nkind.N_CALL) { desugarcallargs(c, n); }; if (k == nkind.N_ASSIGN) { checkassign(c, n); }; 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. The exprtype N_IDENT / N_DOT-callee // + exprtypeoftry / &fn-synth bare-leaf callers now all use // scopelookupprefer (the #56/#4/#11a wave). The only bare-leaf // type lookups left — varianterr (:1051) + scruttype (:1098) — // stay mod-blind but have no reproducible divergence; #58. 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) { // #58/#50: prefer the curmod-matching SK_TYPE. // Two modules exporting the same type leaf (e.g. // utf8.invalid !void vs strconv.invalid !i32) // otherwise resolve install-order-dependent here // when a value binding shadows the leaf; cstage // passes c->cur_mod to scope_lookup_type (sym.c). let sm: *sym = scopelookuptype(c.cur, c.curmod, 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 fast-path, else by resolved-decl identity // (the AST analog of cstage type.c:278 `TY_NAMED: a == b` and harec // types.c:579 `STORAGE_ALIAS: ident_equal`). #14 B-full Layer 1: // bare `oserror` vs qualified `os.oserror` resolve to the SAME // SK_TYPE sym (aliassym maps both via #51/#53), so a cross-module // nominal forward compares equal where the surface streq said false. fn typeeqast(c: *checker, 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) { if (streq(aa.str, bb.str)) { return true; }; let sa: *sym = aliassym(c, aa); let sb: *sym = aliassym(c, bb); if (sa != nil && sa == sb) { return true; }; return false; }; if (k == nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TFN) { // Divergence: cstage type.c:239 compares resolved Type; we // compare AST. See #178. if (!typeeqast(c, aa.lhs, bb.lhs)) { return false; }; let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; let ac: bool = streq(pa.str, "..."); let bc: bool = streq(pb.str, "..."); if (ac != bc) { return false; }; if (!ac) { let va: bool = pa.op == tkind.TK_ELLIPSIS; let vb: bool = pb.op == tkind.TK_ELLIPSIS; if (va != vb) { return false; }; if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; }; pa = pa.next; pb = pb.next; }; return pb == nil; }; // #206: tuple structural equality — mirror of cstage type.c:261-268 // (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its // N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never // proves structurally equal to itself, so the #206 punt-tightening // would confidently reject a bare-&fn into a structural `*fn(...)` // slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped // (parse.ww:302-318), so compare each link's .lhs. if (k == nkind.N_TTUPLE) { let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; pa = pa.next; pb = pb.next; }; return pb == nil; }; // #47 gap-B: tagged structural equality — mirror of cstage // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element // (e.g. ((void|size),(void|size),size)) recurses here from the // N_TTUPLE arm; without it the per-element compare falls to the // catch-all and the whole case-against-tagged-scrutinee is rejected. // Variants are DIRECT .list nodes (casevariantin walks tagged.list // + typeeqast(v,..) directly), NOT N_TPARAM-wrapped like tuple elems. // Divergence: cstage's nullable-flag check (type.c:293) is a resolved- // Type property with no AST analogue; for case-match both sides share // a spelling so it's moot — no phantom AST nullable check. if (k == nkind.N_TTAGGED) { let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; // #115: a `...inner` spread variant stays unflattened at // this AST layer (casevariantin flattens only at the OUTER // level; cstage flattens in resolve_type before type_eq ever // runs). A position-by-position compare cannot honour it — // the N_TNAME leg is pure streq, so `...ab` would silently // match a plain `ab`, accepting a case cstage rejects. // Conservatively loud-reject any spread until the flatten // lands; b1c's (void|size) has none, so byte-id is untouched. if (pa.op == tkind.TK_ELLIPSIS) { return false; }; if (pb.op == tkind.TK_ELLIPSIS) { return false; }; if (!typeeqast(c, pa, pb)) { return false; }; pa = pa.next; pb = pb.next; }; return pb == nil; }; // Conservative: anything else (struct/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) { // #58: mod-blind by leaf — latent. A same-leaf error-vs-plain // type pair across two modules could in principle flip iserror, // but the union's variants resolve at its declaration before // varianterr runs, so no repro exists. Tracked: #58. 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) { // #58: mod-blind by leaf — lenient-only. Feeds match // exhaustiveness; codegen reads the stamped n.type_, so a // mis-resolution cannot drive a wrong binary (no repro). // Tracked: #58. 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) { // Route through the normalization SSoT: a lone survivor // collapses (align((i32|never))==align(i32)==4, not the tag // word's 8), matching cstage align() reading resolve_type(...) // ->align (cmd/wcc/check.c:1550). Same #1 family as astsize. let ti: *tinfo = tinfofornode(c, t); if (ti != nil) { return ti.align: i64; }; 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) { // #141: a def-dimensioned field array sized to 0 here, so the // struct loop (off += astsize(field)) overlapped the next // field; arrayelen folds the def. let elen: i64 = arrayelen(c, t.rhs): i64; return astsize(c, t.lhs) * elen; }; if (k == nkind.N_TTUPLE) { // Route through the type table, NOT a packed element-sum: // slot layout is the tuple SSoT (C-t0, user-ratified) and // tupleelemslot is its one wwstage answer. The packed walk // this replaces was a C-t0 escape — size((u32,u32)) folded // to 8 here while cstage (check.c N_TTUPLE) and the wwstage // type table both said 16 (task #22 commit 0). let ti: *tinfo = tinfofornode(c, t); if (ti != nil) { return ti.size: i64; }; return 0i64; }; 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) { // Route through tinfofornode (the tagged-normalization SSoT) // so the size() fold matches cgen's layout AND cstage: the raw // 8+roundup8(max) here ignored never-drop / dedup / single- // collapse / nullable, so size((*u8|void)) folded 16 not 8 and // size((i32|never)) 16 not 4 (#1). Mirrors the N_TTUPLE arm // above and cstage size() reading resolve_type(...)->size // (cmd/wcc/check.c:1547-1550). let ti: *tinfo = tinfofornode(c, t); if (ti != nil) { return ti.size: i64; }; return 0i64; }; 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 as a resolved *tinfo (the match-as-expression's 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. bname/btype // carry the enclosing arm's case-binding name + declared type node. // // #264: returns a *tinfo (not a type *node) because the post-walk call // READS the operand's cached node.type_ (a tinfo), mirroring cstage's // match_yield_type which reads body->lhs->type (cmd/wcc/check.c:121-122). // The two callsites differ on whether the operand is stamped yet: // - POST-walk (exprtype N_MATCH post-order, resolvewalk L631): the // in-scope N_MCASE arm walk (L411) has already stamped the operand, // so we read body.lhs.type_ directly — NO exprtype re-run. Re-running // exprtype out of the (popped) arm scope returned nil and the // N_UN/N_BIN/N_INDEX restamp arms overwrote the good deref/element // stamp with nil -> asserttyped:un/bin/index. Reading cached avoids // the re-derive entirely, so the clobber cannot occur by construction. // - PRE-walk (checkletassign L302 / checkretassign L303 run exprtype // on the match rhs BEFORE the L533 in-scope descent): the operand is // nil here, so we re-derive via exprtype — benign (nil->nil no-op on // the unstamped operand) AND load-bearing: it types the void-arm // literal (`yield -1`) so the let/return-assign has a usable type // node for isassignable. cstage is single-pass (no pre-walk call), so // its match_yield_type has no such branch; eliminating this pre-walk // call is #279. `nodeout` carries that re-derived type NODE back to // the consumer for the pre-walk assignability check (isassignable is // node-based); it stays nil on the post-walk cached read, where the // consumer's *node return is discarded (no nested match-as-subexpr // consumes it — only checkletassign/checkretassign at the pre-walk // call use it). The #241 `yield ` fallback (the dominant // match-bind-then-yield idiom, Hare's parseint `case let t => yield t`) // stays, now resolving btype to a tinfo. fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node, nodeout: **node) *tinfo = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; if (body.lhs.type_ != nil) { // For the bare-binder idiom `yield `, ALSO surface // btype as the *node: the tuple-destructure consumers // (N_MLET/N_MASSIGN at resolvewalk L482/L503) read // exprtype(N_MATCH)'s *node return as an N_TTUPLE to // distribute onto `let (a,b) = match(x){ case let t => yield // t }` (test 945 match_yield — the only tuple-destructure-of- // match idiom in tree, grep-confirmed). btype is the binder's // declared type node, which IS the match's type here; the // cached tinfo returned below equals tinfofornode(btype). if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 && streq(body.lhs.str, bname)) { *nodeout = btype; }; return body.lhs.type_: *tinfo; }; let t: *node = exprtype(c, body.lhs, nil); if (t != nil) { *nodeout = t; return tinfofornode(c, t); }; if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 && streq(body.lhs.str, bname)) { *nodeout = btype; return tinfofornode(c, btype); }; return nil; }; if (k == nkind.N_MATCH) { return nil; }; if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { let t: *tinfo = matchyieldtype(c, s, bname, btype, nodeout); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { let t: *tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); if (t != nil) { return t; }; return matchyieldtype(c, body.els, bname, btype, nodeout); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { return matchyieldtype(c, body.body, bname, btype, nodeout); }; return nil; }; // yieldclass — #38/F2 (review item 6): the coarse assignability family of a // match-arm yield type, used to approximate cstage's type_assignable in the // cross-arm unification (ww has tinfo typeeq but no tinfo type_assignable). // 1=numeric (int/float/enum/rune + untyped_int/float/rune, NAMED-chased), // 2=str (str + untyped_str), 3=bool, 0=unknown/other (ptr/struct/tuple/slice/ // tagged/...). Class 0 stays LENIENT so the unification rejects only a DEFINITE // family mismatch (the catA repro: an int arm vs a str arm) and never an // untyped->concrete promotion (untyped_int vs i32/f64 both land in class 1) // that cstage accepts. The families mirror typeisnum/typeisstr (lib/ww/typ.ww). fn yieldclass(t: *tinfo) i32 = { let u: *tinfo = tichase(t); if (u == nil) { return 0i32; }; if (typeisnum(u)) { return 1i32; }; if (typeisstr(u)) { return 2i32; }; if (u.kind == tykind.TY_BOOL) { return 3i32; }; if (u.kind == tykind.TY_UNTYPED_BOOL) { return 3i32; }; return 0i32; }; // 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 = { cerr(n.file); cerr(": error: "); cerr(msg); cerr("\n"); 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). }; // arrayelen — an array type's dimension as an element count. An // N_INTLIT yields its uval; a def-ref or const-expr dim (`[MAX]u8`, // MAX a def) folds through evaldefconst (#141, ken oracle); a nil // child (the `[_]T` inferred-length sentinel) or non-const rhs gives // 0. cstage carries the folded length in the resolved Type, but // wwstage computes size lazily on independent paths (no AST-stamp // SSoT), so every N_TARRAY length reader routes here to fold the dim // identically — astsize (struct layout), tinfofornode (canonical // tinfo), checkarrlitfits (count gate). fn arrayelen(c: *checker, rhs: *node) u64 = { if (rhs == nil) { return 0u64; }; if (rhs.kind == nkind.N_INTLIT) { return rhs.uval; }; let v: u64 = 0u64; if (evaldefconst(c, rhs, &v, 0)) { return v; }; return 0u64; }; // 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; t = tichase(t); 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; }; // #22 (user-ratified 2026-06-04): slot = roundup8(size(elem)) — 8B // is a FLOOR, not a ceiling. (str,str)=48B predates this; tagged // was the one truncated >8B kind (the #237 fieldslotsize-missing- // TY_TUPLE precedent: fieldslotsize below already carried this // arm). Cstage twin: check.c N_TTUPLE; cgen accessor: tuple_eslot // / tupeslot. if (pk == tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; 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 — one 8B eightbyte, NOT t.slotsize: cgen's cursor // transport strides `wide ? size : 8` at every tuple site (both // stages), so a composite element rides one register word today. // The checker mirrors what cgen emits (tuple arc C-t0; cstage // check.c N_TTUPLE twin) — a slotsize answer here would re-open the // checker-vs-cgen layout split the slot-SSoT ruling closed. No // composite-element tuple exists in the corpus; transport for >8B // composites is its own unwired gap. return 8u64; }; // #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; t = tichase(t); 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; }; // #237: a tuple-typed struct field carries its own slot total (the // per-element slot sum stamped at the N_TTUPLE arm above — slices at // 24 each). Without this it fell to the 8B default below, undersizing // the enclosing struct's slotsize (size stayed correct), so a `let s:S` // slot was too small — a silent stack-corrupting miscompile. Aligns // with cgenutil.ww fieldsize, which already returns the tuple's size. if (fk == tykind.TY_TUPLE) { return t.slotsize; }; // Primitives keep natural width inside structs (matches // cgenutil fieldsize: primsize, not pad-to-8). 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. // variantpresent — tagged-union dedup predicate. Mirrors cstage // variant_present/variant_match (cmd/wcc/check.c:111-126): NAMED types // are nominal (pointer-identical), everything else structural — exactly // typeeq's contract (TY_NAMED → only same ptr, else structural; lib/ww/ // typ.ww:581). nil guards match variant_match's `a==NULL||b==NULL → 0` // so two unresolved variants never collapse. fn variantpresent(head: *tparam, vt: *tinfo) bool = { if (vt == nil) { return false; }; let p: *tparam = head; for (p != nil) { if (p.type_ != nil) { if (typeeq(p.type_, vt)) { return true; }; }; p = p.tnext; }; return false; }; 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; switch (k) { case 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; named.resolving = 1; let under: *tinfo = tinfofornode(c, body); // #62/#69: alias-root cycle (`type a = b; // type b = a` / `type a = a`) — checked // BEFORE clearing the flag so self-aliases // trip on their own mark. tyerr instead of // the cyclic under keeps the table ACYCLIC // by construction: every NAMED-chain chase // loop stays terminating. Mirrors cstage // resolve_typedecl. if (circularnamed(c, under, n)) { under = c.tc.tyerr; }; named.resolving = 0; // peellint-ok: construction — the one // WRITE that builds the NAMED link; // not a peel, can't route via tichase. named.under = under; if (under != nil) { named.size = under.size; named.align = under.align; named.slotsize = under.slotsize; }; r = named; }; }; }; }; case 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); case nkind.N_TPTR: r = typeptr(tinfofornode(c, n.lhs)); case nkind.N_TSLICE: r = typeslice(tinfofornode(c, n.lhs)); case nkind.N_TCHAN: r = typechan(tinfofornode(c, n.lhs)); case 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. // #38/F2 (review item 2): a non-const, non-`[_]T` dimension is // LOUD here, mirroring cstage resolve_type N_TARRAY // (cmd/wcc/check.c:715 "array length must be an integer literal"). // arrayelen folds 0 for both the `[_]T` sentinel AND a runtime // dim, so the size-walker reader (astsize, via arrayelen) can't // tell them apart; resolve the type HERE — the one resolve seam // cstage gates at — so c.errs trips before any 0-sized slot ships. // The fold is inlined (not via arrayelen) to error exactly once: // evaldefconst itself reports div-by-zero / unsupported-op, so a // guard that re-folds would double-report. arrayelen stays the // fold SSoT for astsize's later size() read. let elen: u64 = 0u64; // #141: fold def-dim if (n.rhs != nil) { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; } else { let v: u64 = 0u64; if (evaldefconst(c, n.rhs, &v, 0)) { elen = v; } else { cerr(n.file); cerr(": "); cerr("error: array length must be an integer literal\n"); c.errs += 1; }; }; }; let sub: *tinfo = tinfofornode(c, n.lhs); // #62/#69: `type a = [2]a` value cycle — loud, cstage twin. if (circularnamed(c, sub, n)) { sub = c.tc.tyerr; }; r = typearray(sub, elen); case 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); case 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; case nkind.N_TTUPLE: // Slot layout is the tuple SSoT (tuple arc C-t0, user-ratified): // ti.size = ti.slotsize = per-element slot sum (tupleelemslot — // a str/slice its header, everything else one 8B eightbyte), // the stride cgen's cursor transport actually writes. ww- // internal ABI only (tuples never cross extern); // size((u32,u32))=16 is observable via size() and diverges from // Hare (harec type_store.c:533-580 anonymous-struct rule) AND // from ww's own structs (which pack narrow fields post- // fldloadop) — that internal inconsistency is what task #60 // eventually fixes; re-open before any serialization/FFI/ // density use. Pre-C-t0 ti.size was the packed raw sum while // cgen strode 8B slots — the checker-says-8/cgen-does-16 split // behind the packed-tuple miscompile family (#32/#33/#48). // Pre-bind for cycle protection (recursive tuple shapes). 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 N_TTUPLE which stores tuple positionals on // t->params (Tparam, no offset, consumer recomputes by // walking); the offset-stored shape lets 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. Offsets are // slot-cumulative (C-t0), distinct from harec's // add_padding(&offset, memb.align) at type_store.c:561. let teh: *ttupleelem = nil; let tet: *ttupleelem = nil; let slottotal: u64 = 0u64; let maxal: u64 = 1u64; let p: *node = n.list; for (p != nil) { let pt: *tinfo = tinfofornode(c, p.lhs); // #62/#69: tuple-member value cycle — loud, cstage twin. if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; }; // #24: a composite element (array/struct/nested tuple // >8B) cannot ride the 8B cursor slot — the #60 layout // drops it on construction and segvs on t.N[i] read. // Reject until #60/DISP-A inlines it; cstage twin. let cu: *tinfo = tichase(pt); if (cu != nil && (cu.kind == tykind.TY_ARRAY || cu.kind == tykind.TY_STRUCT || cu.kind == tykind.TY_TUPLE)) { cerr("error: tuple element must be a scalar, str, slice, or tagged-union (composite element deferred to task #60)\n"); c.errs += 1; pt = c.tc.tyerr; }; let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!; if (teh == nil) { teh = te; } else { tet.tnext = te; }; tet = te; if (pt != nil) { if (pt.align > maxal) { maxal = pt.align; }; slottotal += tupleelemslot(pt); }; p = p.next; }; r.tupleelems = teh; r.size = slottotal; r.align = maxal; r.slotsize = slottotal; case 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); // #62/#69: struct-field value cycle (`type s1 = // struct { x: s2 }; type s2 = struct { x: s1 }`) // — loud; pre-#62 this stack-overflowed the slot // walkers. cstage twin. if (circularnamed(c, ft, f)) { ft = c.tc.tyerr; }; 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; case nkind.N_TTAGGED: // THE tagged-union normalization SSoT (astsize/astalign delegate // here). Mirrors cstage resolve_type N_TTAGGED (cmd/wcc/check.c: // 801-882): 8B tag + max(variant) rounded up to 8, AFTER type-set // normalization — drop `never` (807/824), dedup variants by // variantpresent==variant_match (837/825), collapse a lone // survivor to that variant (847-848), fold a two-variant // `(*T|void)` to an 8B nullable pointer (859-874). #1/#3: astsize // (check.ww) AND this arm formerly sized the RAW declaration list // (no drop/dedup/collapse), so size((i32|never)) folded 16 not 4 // and (i32|i32|str) numbered str's tag 2 not 1 (cgen reads the // deduped ti.params). 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 surviving variant). #61a: flatten `...inner` // tagged spreads into the chain and stamp each variant's iserror. // Same shared Tparam shape ww reuses across struct-fields / // tuple-fields / fn-params / tagged-variants (sea-of-stars per // rule 12). let head: *tparam = nil; let tail: *tparam = nil; let maxsz: u64 = 0u64; let al: u64 = 8u64; let nv: i32 = 0; let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); // #62/#69: union-member value cycle — loud, cstage twin. if (circularnamed(c, vt, v)) { vt = c.tc.tyerr; }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; // Full chase (F2a batch-4 c3-B1): the single peel left a // 2-level-alias inner union a SURFACE member — the box // sized off the inner union's own header, not its // spliced variants (cs twin check.c:755 chases). if (isspread) { vu = tichase(vu); }; if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) { // #209: a `...inner` spread's PAYLOAD is its // members, not the whole inner union — size/align // off each surviving spliced member (cstage check.c: // 822-834 st->size), NOT the surface member vt (which // would over-size by the inner union's own 8B tag word // and desync the `field` slot from cstage's). Spliced // variants carry the inner union's already-stamped // iserror; no re-derivation. let src: *tparam = vu.params; for (src != nil) { let st: *tinfo = src.type_; if (st != nil && st.kind == tykind.TY_NEVER) { src = src.tnext; continue; }; if (variantpresent(head, st)) { src = src.tnext; continue; }; if (st != nil) { if (st.size > maxsz) { maxsz = st.size; }; if (st.align > al) { al = st.align; }; }; 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; nv += 1; src = src.tnext; }; } else { if (vt != nil && vt.kind == tykind.TY_NEVER) { v = v.next; continue; }; if (variantpresent(head, vt)) { v = v.next; continue; }; if (vt != nil) { if (vt.size > maxsz) { maxsz = vt.size; }; if (vt.align > al) { al = vt.align; }; }; 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; nv += 1; }; v = v.next; }; // Collapse on the NORMALIZED survivor count (cstage check.c: // 847-882). The trailing tinfocachebind below re-binds n→r, so // a collapsed `r` shadows the pre-bound tagged placeholder. if (nv == 0) { r = c.tc.tynever; } else if (nv == 1) { r = head.type_; } else { r.params = head; // #61 A.3 nullable fold on the normalized pair so // `(*T|void|never)` and dup'd shapes fold too. Mirrors // cmd/wcc/check.c:859-874 — bare TY_VOID (NOT `!void`, // carried by the tparam iserror flag), not NAMED. The // per-variant iserror (varianterr) now lets this port at // the tinfo layer; pre-#1 it AST-keyed n.list instead. let isnull: bool = false; if (nv == 2) { let pa: *tparam = head; let pb: *tparam = head.tnext; let aptr: bool = (pa.type_ != nil && pa.type_.kind == tykind.TY_PTR); let bptr: bool = (pb.type_ != nil && pb.type_.kind == tykind.TY_PTR); let avoid: bool = (pa.type_ != nil && pa.type_.kind == tykind.TY_VOID && !pa.iserror); let bvoid: bool = (pb.type_ != nil && pb.type_.kind == tykind.TY_VOID && !pb.iserror); 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; } else { 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(c, 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; }; }; }; // coercerunelit — #29: an un-suffixed rune literal narrowing into an // integer slot. Same intent as coercefloatlit (:2324) but CHECKER-only: // wwstage stamps N_RUNELIT concrete `rune` (:2652), so isassignable's // untyped arms never fire and the operand falls to the "two known // primitives, different names → confident reject" arm (:4139) — the #29 // over-reject. cstage stamps N_RUNELIT untyped_rune (cmd/wcc/check.c:1296) // which type_assignable admits into ANY integer UNCONDITIONALLY // (cmd/wcc/type.c:379 — coarse, NO range check; harec's range-precise // promote_flexible at types.c:928 is the reference cstage already // flattened, so wwstage mirrors cstage's coarse rule, not harec's gate). // Return the integer target tnode so the caller overrides its `src`/ // `atype` and isassignable sees su==target → accept. Does NOT touch // e.type_: cgen narrows a rune immediate by the DESTINATION width (proven // byte-id at let/call-arg/index-store; #251 array-elem twin) and for a // tagged-union target the variant boxing falls to taggedvariantindext's // scalar-shape fallback (cgenutil.ww:3054) which picks the first scalar // variant — the same u8 variant cstage's untyped_rune boxes into — so the // unchanged node stamp keeps cgen provably identical to the pre-fix // bare-literal lowering. Array-LITERAL elements are NOT routed here: they // keep harec's per-element range gate via checkarrlitfits's foldint / // defcastfits path (:4634). For a tagged-union target (fnmatch pat_next's // (u8 | star | ...)) return the first integer variant. fn coercerunelit(c: *checker, e: *node, target: *node) *node = { if (e == nil) { return nil; }; if (e.kind != nkind.N_RUNELIT) { return nil; }; if (target == nil) { return nil; }; let tu: *node = resolvealias(c, unwrapbang(target)); if (tu == nil) { return nil; }; if (isinttypeast(tu)) { return tu; }; if (tu.kind == nkind.N_TTAGGED) { let v: *node = tu.list; for (v != nil) { let vu: *node = resolvealias(c, unwrapbang(v)); if (vu != nil) { if (isinttypeast(vu)) { return vu; }; }; v = v.next; }; }; return nil; }; // 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); // #38/F2: ptr ± int / int + ptr use intkindast (the type_isint mirror, // incl untyped_int / untyped_rune / alias-chased) — NOT isinttypeast, // which misses untyped_int. cstage's ptr-arith arm gates on type_isint // (check.c:1179/1181), so `p + 1` (untyped_int) returns the ptr there // and never reaches the isnum gate; aligning these arms keeps the new // arithmetic gate below from rejecting valid pointer arithmetic. if (op == tkind.TK_PLUS || op == tkind.TK_MINUS) { if (ltn != nil && ltn.kind == nkind.N_TPTR && intkindast(c, rtn)) { return ltn; }; }; if (op == tkind.TK_PLUS) { if (intkindast(c, 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"); }; }; // #38/F2 (review item 5): operand-kind gates the wwstage checker // elided. Mirror cstage cbinop (cmd/wcc/check.c:1186-1199): arithmetic // wants numeric operands, bitwise wants integer operands. Without these // `let c: str = a + b;` (str+str) compiled to an integer ADD of the two // header pointers, silent garbage. The ptr-arithmetic special cases // above already returned, so a survivor here is plain value arithmetic. if (op == tkind.TK_PLUS || op == tkind.TK_MINUS || op == tkind.TK_STAR || op == tkind.TK_SLASH || op == tkind.TK_PERCENT) { if ((ltn != nil && !numkindast(c, ltn)) || (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "arithmetic on non-numeric type"); }; return unifyarith(c, ltn, rtn); }; if (op == tkind.TK_AMP || op == tkind.TK_PIPE || op == tkind.TK_CARET || op == tkind.TK_LSHIFT || op == tkind.TK_RSHIFT) { if ((ltn != nil && !intkindast(c, ltn)) || (rtn != nil && !intkindast(c, rtn))) { deffolderr(c, e, "bitwise on non-integer type"); }; 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) { // cstage routes every comparison through unify_arith (check.c:952/ // 955 cbinop), which loud-rejects an error-typed operand paired // with a differing type, e.g. strconv.invalid != i32. wwstage's // unifyarith stays silent on generic typed mismatches (5-lite-b), // but the error-type case is a real cs!=ww edge and must reject to // match cstage (#246, rule 10). Scoped to error operands so the // broad differing-types flip (typeeqast vs cstage type_eq // asymmetry risk) stays out. if (varianterr(c, ltn) || varianterr(c, rtn)) { if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); }; }; // #38/F2 (review item 5): ordered comparisons want numeric // operands (cstage check.c:1198-1199). EQ/NEQ stay UNGATED — // cstage's cbinop equality arm (check.c:1194-1196) gates nothing, // so str==str / ptr==ptr remain valid; aligning those down would // over-reject what cstage accepts. if (op == tkind.TK_LT || op == tkind.TK_LE || op == tkind.TK_GT || op == tkind.TK_GE) { if ((ltn != nil && !numkindast(c, ltn)) || (rtn != nil && !numkindast(c, rtn))) { deffolderr(c, e, "ordered comparison on non-numeric"); }; }; return mktname(c, "bool"); }; if (op == tkind.TK_AND || op == tkind.TK_OR) { // #38/F2 (review item 5): logical operands must be bool (cstage // check.c:1208-1211 gates each side via type_chase_named). cstage // formats per-side with tokname; ww's piecewise cerr can't splice // the op token, so one combined wording — the build-based reject // test gates on rc, not on exact text. if ((ltn != nil && !boolkindast(c, ltn)) || (rtn != nil && !boolkindast(c, rtn))) { deffolderr(c, e, "logical operand is not bool"); }; 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); // #38/F2 (review item 5): unop operand-kind gates the wwstage checker // elided. Mirror cstage cunop (cmd/wcc/check.c:1224-1238): unary +/- // want a numeric operand, ~ wants an integer, ! wants a bool. A nil // opt is an already-errored / inherent-IDENT bail — not re-rejected. if (op == tkind.TK_MINUS || op == tkind.TK_PLUS) { if (opt != nil && !numkindast(c, opt)) { if (op == tkind.TK_MINUS) { deffolderr(c, e, "- on non-numeric"); } else { deffolderr(c, e, "+ on non-numeric"); }; }; return opt; }; if (op == tkind.TK_NOT) { if (opt != nil && !boolkindast(c, opt)) { deffolderr(c, e, "! on non-bool"); }; return mktname(c, "bool"); }; if (op == tkind.TK_TILDE) { if (opt != nil && !intkindast(c, opt)) { deffolderr(c, e, "~ on non-integer"); }; 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) { // Full chase per hop (F2a batch-4 c3-B2): // the one-peel-per-hop walk lost 2-level // alias bases out of the slice/str detect // (cs twin check.c:1198-1201 chases both // hops). NOTE the ww cgen ADDRESS tail // stays loud for ALL alias bases (task // #96) — this aligns the stamped type // only; rows graduate with #96. let bu: *tinfo = tichase(base.type_: *tinfo); if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; }; bu = tichase(bu); 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; }; }; }; }; }; // #206: `&fn` must type as `*fn(...)` — a pointer to the fn's // full signature — not `*`. exprtype's N_IDENT arm // returns a fn decl's lhs (the return type), so the generic // `*opt` below would mistype `&myread` as `*i32`; a `*fn` // laundered into a `*alias` slot then slips past the nominal // pointer-fn reject in isassignable. Synthesize the N_TFN from // the fn decl (N_FNDECL and N_TFN share parseparams' param // shape) so the address-of carries the signature. Mirrors // cstage, where a fn ident already types as TY_FN // (cmd/wcc/check.c:668), so `&fn` is `*fn` natively. if (e.lhs != nil) { if (e.lhs.kind == nkind.N_IDENT) { // #4: curmod preference. A bare `&handler` whose leaf // also names a fn in a LATER module otherwise binds // the foreign signature (scopedefineinmodule prepends); // cstage types a fn ident via scope_lookup_prefer with // cur_mod (cmd/wcc/check.c:1305). let fs: *sym = scopelookupprefer(c.cur, c.curmod, e.lhs.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { if (fs.decl != nil) { if (fs.decl.kind == nkind.N_FNDECL) { let synth: *node = newnode(nkind.N_TFN, "", 0, 0); synth.lhs = fs.decl.lhs; synth.list = fs.decl.list; let pf: *node = newnode(nkind.N_TPTR, "", 0, 0); pf.lhs = synth; return pf; }; }; }; }; }; // #124: a cross-module `&mod.fn` — same synthesis as the // N_IDENT arm, but the fn decl lives in the qualifier module // (scopelookupinmodule). Without this the address-of falls to // the generic `*opt` path and a const `[](str,*fn)` table's // nested cross-module &fn element fails the isassignable // typeeq → confident reject; cstage types `&mod.fn` as `*fn` // natively (a dotted fn ref is TY_FN), so this aligns ww UP to // cstage's actual acceptance reason. if (e.lhs.kind == nkind.N_DOT) { if (e.lhs.lhs != nil && e.lhs.lhs.kind == nkind.N_IDENT) { let fs: *sym = scopelookupinmodule(c.cur, e.lhs.lhs.str, e.lhs.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { if (fs.decl != nil) { if (fs.decl.kind == nkind.N_FNDECL) { let synth: *node = newnode(nkind.N_TFN, "", 0, 0); synth.lhs = fs.decl.lhs; synth.list = fs.decl.list; let pf: *node = newnode(nkind.N_TPTR, "", 0, 0); pf.lhs = synth; return pf; }; }; }; }; }; }; }; // 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) { // rule-9 divergence-doc (drew .ai/drew-14-ruling.md): `str[i] -> // u8` is a deliberate Go-like direct byte-index, a SANCTIONED ww // divergence from Hare's `strings::toutf8(s)[i]` (the Hare // reference checker rejects str-index, harec check.c:362). // lib/strings is load-bearing on it (compare/dup/join). if (streq(u.str, "str")) { // #14: reject INDEX of a bare def-global scalar str — an // inline compile-time constant with no storage to index // (cgen refs an unbacked symbol -> link-fail ww / segfault // cs). let/param/local + string-literal operands stay // valid; only the SK_DEF scalar-str operand is // unindexable. cstage twin: check.c N_INDEX TY_STR arm. if (e.lhs != nil && e.lhs.kind == nkind.N_IDENT) { let s: *sym = scopelookupprefer(c.cur, c.curmod, e.lhs.str); if (s != nil && s.skind == skind.SK_DEF) { cerr("error: cannot index a def-constant str '"); cerr(e.lhs.str); cerr("'; bind it to a `let` (def strings are inline constants, not storage-backed)\n"); c.errs += 1; return nil; }; }; 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) { // #55: bare-leaf value-ident must prefer curmod. Flat-scope // scopelookup bucket-walks and can bind a same-leaf symbol from // the wrong module under a foreign curmod, dragging its decl's // return-type node (e.g. `read` -> io.read under curmod=os, whose // bare `error` then binds strconv.error not io.error). Mirrors // cstage cmd/wcc/check.c:66 scope_lookup_prefer; sibling #56 at // L2439, #53 at L688. Tracked in the cluster note at L685-687. let s: *sym = scopelookupprefer(c.cur, c.curmod, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; // #34: a bare fn-name rvalue types as its FN TYPE, not its return // type. decl.lhs is the RETURN type for an N_FNDECL, so synthesize // the N_TFN over (ret=decl.lhs, params=decl.list) — the shape // assignableaddrfn builds (:3841). Mirrors cstage: build_fn_type at // fn-decl install (cmd/wcc/check.c:2915/2931) stored on the sym and // returned verbatim by cexpr N_IDENT (check.c:1313); harec EXP_ACCESS // yields the fn object's type with NO decay (ref/harec/src/check.c // :341-343; the fn obj is built .storage=STORAGE_FUNCTION at :4279- // 4288, assignable iff dealias-equal fn types, types.c:1001). Was the // root of the #24 fn-family over-rejects (`let p: fn()i32 = g` compared // i32 vs the fn type). cgen lowers a fn rvalue name-keyed via // fnretlookup (cgenexpr.ww:1100), never off this stamp → byte-id-neutral. if (s.skind == skind.SK_FN) { let ft: *node = newnode(nkind.N_TFN, "", 0, 0); ft.lhs = s.decl.lhs; ft.list = s.decl.list; e.type_ = tinfofornode(c, ft): *void; return ft; }; 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) { // B' (#3): an empty `[]` has no element type; // ww gets it only from a let annotation (the // #45 retype). Any other empty alloc has no // hint, so refuse to guess rather than default // to u8 (was a silent u8-default + #5 value-form // miscompile). Align DOWN to harec, which errors // the same way: ref/harec/src/check.c:1801-1802. // The e.type_ != nil guard is the wwstage-only half: // resolvewalk re-types every value node context-free // (L648) AFTER checkletassign already rescued+stamped // this node, so a stamped node is a rescued one — do // not re-error it. cstage cexpr is single-visit (clet // only) so it needs only the allococtx check. if (e != c.allococtx && e.type_ == nil) { deffolderr(c, e, "cannot infer slice element type for alloc([], n) without a type hint; annotate the binding, e.g. let x: []T = alloc([], n)"); return 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) { // #38/F2 (review item 7): UNCONDITIONAL intercept — cstage // gates size/align/offset on NOTHING (cmd/wcc/check.c:1538- // 1578), unlike user-shadowable alloc/abort/assert // (check.c:1625/1740/1755). The removed same-module shadow // gate had no cstage twin (its "Mirrors check.c:907-960" // cite was stale — that range is N_TFN/N_TSTRUCT layout) and // was already dead for primary-file decls (declmod "" → // curmod.len==0 skipped it). if (e.list != nil) { // size/align resolve the arg as a TYPE; an unresolvable // name (`size(localvar)`) is LOUD here, mirroring cstage // resolve_type "unknown type '%s'" (check.c:93) — astsize // otherwise folded the unresolved N_TNAME to 0 silently // (rc=0 wrong binary). offset's arg is a value N_DOT and // keeps its own "no field" diagnostic below. if ((issize || isalign) && e.list.kind == nkind.N_TNAME && tinfofornode(c, e.list) == nil) { cerr(e.list.file); cerr(": error: unknown type '"); cerr(e.list.str); cerr("'\n"); c.errs += 1; }; // 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) { cerr("offset: no field '"); cerr(e.list.str); cerr("'\n"); 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) { // abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT // family (harec ref/harec/src/check.c:877,893), lowered // to rt_abort. Builtin only when no user symbol shadows // the name — the same scopelookupprefer gate as cstage // cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed // call stays on the regular path (#58; the #45 // flat-scope root is filed as task #14). // The TY_ERR stamp on the callee is cgen's routing key // (cstage spells it `n->lhs->type = ty_err`). if (streq(callee.str, "abort") && scopelookupprefer(c.cur, c.curmod, "abort") == nil) { if (e.list != nil) { let mt: *node = exprtype(c, e.list, nil); let conf: bool = false; if (!isassignable(c, mktname(c, "str"), mt, &conf)) { cerr("abort: message must be str\n"); c.errs += 1; }; if (e.list.next != nil) { cerr("abort: at most one arg\n"); c.errs += 1; }; }; let tn: *node = mktname(c, "void"); e.type_ = tinfofornode(c, tn): *void; callee.type_ = c.tc.tyerr: *void; return tn; }; if (streq(callee.str, "assert") && e.list != nil && scopelookupprefer(c.cur, c.curmod, "assert") == nil) { let ct: *node = exprtype(c, e.list, nil); if (ct != nil) { // No alias peel: cstage compares ty_bool by // IDENTITY (cmd/wcc/check.c:1560), so a // `type myb = bool` cond is rejected there — // align down (rule 10). Widening belongs to // the alias-peel choke-point arc (task #5, // #47/#68), both stages together. let cu: *node = unwrapbang(ct); let condok: bool = false; if (cu.kind == nkind.N_TNAME) { if (streq(cu.str, "bool") || streq(cu.str, "untyped_bool")) { condok = true; }; }; if (!condok) { cerr("assert: cond must be bool\n"); c.errs += 1; }; }; if (e.list.next != nil) { let mt: *node = exprtype(c, e.list.next, nil); let conf: bool = false; if (!isassignable(c, mktname(c, "str"), mt, &conf)) { cerr("assert: message must be str\n"); c.errs += 1; }; if (e.list.next.next != nil) { cerr("assert: at most two args\n"); c.errs += 1; }; }; let tn: *node = mktname(c, "void"); e.type_ = tinfofornode(c, tn): *void; callee.type_ = c.tc.tyerr: *void; return tn; }; 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; }; // delete(xs[i]) / delete(xs[lo:hi]) — slice removal, // the delete-half of #35 (insert() is the twin arm // below). Mirrors cstage cmd/wcc/check.c's delete // arm. harec ref/harec/src/check.c:1981-2027 accepts // both an indexing place (EXPR_ACCESS/ACCESS_INDEX) // and a slicing place (EXPR_SLICE — Hare spells it // delete(xs[i..j])); either way the OBJECT must be a // slice. The range form is the fold-5a prereq P2 // (regex.ha:333 delete(jump_idxs[group_level][..])). if (streq(callee.str, "delete")) { let d: *node = e.list; if (d == nil || d.next != nil) { cerr("delete: takes exactly one argument\n"); c.errs += 1; }; if (d != nil) { exprtype(c, d, nil); // harec check.c:2016's reject; wording // adapted to ww's delete: prefix. if (d.kind != nkind.N_INDEX && d.kind != nkind.N_SLICE) { cerr("delete: operand must be an indexing or slicing expression\n"); c.errs += 1; } else { let basetn: *node = exprtype(c, d.lhs, nil); let u: *node = resolvealias(c, unwrapbang(basetn)); // harec check.c:2024 wording; a // fixed-size [N]T base and a str // base land here. if (u == nil || u.kind != nkind.N_TSLICE) { cerr("delete must operate on a slice\n"); c.errs += 1; }; }; }; let tn: *node = mktname(c, "void"); e.type_ = tinfofornode(c, tn): *void; return tn; }; // insert(xs[idx], v) — single-element slice insertion // before idx, delete()'s twin (the insert-half of // #35). Mirrors cstage cmd/wcc/check.c's insert arm. // harec models append/insert in ONE checker arm // (ref/harec/src/check.c:745 check_expr_append_insert; // "insert" at :786): operand 1 must be an indexing // place over a slice; idx == len is a legal // end-insert. The spread form and the with-length // form (harec :821/:837) stay filed on #35; a range // PLACE is not Hare (harec asserts ACCESS_INDEX at // :784) — rejected, no task cite. if (streq(callee.str, "insert")) { let d: *node = e.list; if (d == nil || d.next == nil || d.next.next != nil) { cerr("insert: takes exactly two arguments\n"); c.errs += 1; }; if (d != nil) { exprtype(c, d, nil); if (d.kind == nkind.N_SLICE) { cerr("insert: range place is invalid; operand must be an indexing expression xs[i]\n"); c.errs += 1; } else { if (d.kind != nkind.N_INDEX) { cerr("insert: operand must be an indexing expression xs[i]\n"); c.errs += 1; } else { let basetn: *node = exprtype(c, d.lhs, nil); let u: *node = resolvealias(c, unwrapbang(basetn)); // harec check.c:807 wording; a // fixed-size [N]T base lands here. if (u == nil || u.kind != nkind.N_TSLICE) { cerr("insert must operate on a slice\n"); c.errs += 1; }; }; }; if (d.next != nil) { if (d.next.kind == nkind.N_SPREAD) { cerr("insert: spread form insert(xs[i], vs...) unimplemented (task #35)\n"); c.errs += 1; } else { exprtype(c, d.next, nil); }; }; }; 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; }; // #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). // // #181: a non-named callee (N_UN TK_STAR deref of a *fn local, // `(*f)(...)`; or any other expression-as-callee shape) has no // leaf to resolve here — skip the SK_FN name-lookup and let the // fn-VALUE fallback below peel TPTR / dealias to TFN. Mirrors // harec check_autodereference at ref/harec/src/check.c:1566. // cgen post-#180+#185 already lowers the deref-call correctly, // so lifting the asserttyped bail is silent-SIGSEGV-safe. if (nm.len > 0) { 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; }; }; }; // #208: only a module-qualified callee (SK_USE receiver) // resolves its leaf by name here. A value receiver // (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a // fn-pointer FIELD call whose result type comes from the // FIELD's fn type, not a global-leaf lookup. The old // `scopelookup(c.cur, nm)` else-arm bound whichever // same-leaf global fn headed the flat scope bucket — // order-sensitive (os.read:i64 vs io.read:(i32|eof|closed) // flipped by combined.ww concat order), yielding a // false `return: not assignable`. Leaving s nil falls // through to the fn-VALUE path below (exprtype(callee) → // TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c // :1378-1433 (call result IS the callee type's ret; no // global-leaf path) and harec check_autodereference // (ref/harec/src/check.c:1566-1581). if (ms != nil && (ms.skind == skind.SK_USE || ms.use_alias != 0i32)) { s = scopelookupinmodule(c.cur, callee.lhs.str, 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 || ms.use_alias != 0i32) { let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); if (fs != nil) { if (fs.decl != nil) { // #34: a module-qualified bare fn rvalue `mod.fn` types as // its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is // the RETURN type for an N_FNDECL. Pins 706 (`let p1: fn()i32 // = mod1.ping`). if (fs.skind == skind.SK_FN) { let ft: *node = newnode(nkind.N_TFN, "", 0, 0); ft.lhs = fs.decl.lhs; ft.list = fs.decl.list; e.type_ = tinfofornode(c, ft): *void; return ft; }; 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; }; // A fixed array has no capacity word — .cap is // invalid (drew ruling). cstage check.c errs // symmetrically. c.errs>0 gates cgen off → build // fails (the #11 inferarraylen idiom). if (bu.kind == nkind.N_TARRAY && streq(e.str, "cap")) { cerr("error: no field 'cap' on a fixed-size array (arrays have no capacity; use .len)\n"); c.errs += 1; return nil; }; if (streq(e.str, "cap")) { let tn: *node = mktname(c, "i32"); e.type_ = tinfofornode(c, tn): *void; return tn; }; // rule-9 divergence-doc (drew, task #13): array.ptr ≡ // &A[0], a sanctioned ww spelling / faithful Hare // desugaring (14 live consumers). KEEP — .ptr valid. // See .ai/drew-gapa-ptr-ruling.md. 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; }; // #251: range-check array-typed field inits // (`enc{m=[65,..]}`). The head-stamp above is // field-walk-free — general per-field assignability // is parked behind #23. This is the ISOLATED // array-field accept-if-fits ONLY: it reuses the // stable N_TSTRUCT field-list walk (astoffset // precedent), zero coupling to the parked #23 walk, // and closes the rule-7 silent out-of-range truncate // at this site (cstage check.c:1546 range-checks via // arrlit_init_fits). let stn: *node = resolvealias(c, tn); if (stn != nil && stn.kind == nkind.N_TSTRUCT) { let fi: *node = e.list; for (fi != nil) { if (fi.kind == nkind.N_FIELD && fi.lhs != nil && fi.lhs.kind == nkind.N_ARRLIT) { let ftn: *node = nil; let tf: *node = stn.list; for (tf != nil) { if (tf.kind == nkind.N_TFIELD) { if (streq(tf.str, fi.str)) { ftn = tf.lhs; }; }; tf = tf.next; }; if (ftn != nil) { checkarrlitfits(c, ftn, fi.lhs); }; }; fi = fi.next; }; }; 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) { // #19: N_STRUCTLIT exprtype returns the struct BODY // (N_TSTRUCT) per #66, but the array->slice // isassignable arm typeeqast's the declared element // N_TNAME against this element shape — a TNAME-vs- // TSTRUCT kind mismatch reads confident-false and // rejects. cstage infers the NAMED type here. Capture // the named type for a named struct literal so su.lhs // is the same N_TNAME shape (typeeqast is already // streq-keyed for TNAME). Non-named elements keep the // existing first-element shape. if (it.kind == nkind.N_STRUCTLIT && it.lhs != nil && it.lhs.kind == nkind.N_IDENT) { elt = mktname(c, it.lhs.str); } else { elt = t; }; }; count += 1u64; }; it = it.next; }; // #103/#108: default the inferred element's UNTYPED flavor to its // concrete type, mirroring cstage cmd/wcc/check.c:1856 // `elt = type_default(t)` (type.c:237 untyped_int→int after the // #108 polarity flip). Keeping the raw untyped_int (the prior // documented divergence) sized the synthesized [N]untyped_int // INCONSISTENTLY — untyped_int.size is 0, so the cgarrlitfillbp // STORE strode the 8 sentinel while slotsize (letslotsize slot) // and elemsizeofc (cgindex READ stride) read the 0-size element // → frame under-alloc SEGV + stride-1 index reads, cs≠ww. drew // Hare-fidelity: harec lower_flexible defaults a flexible iconst // to `int` (ref/harec/src/types.c:835). int = machine word (8B); // the old i32 truncated values >2^31 (#263 trap). if (elt != nil && elt.kind == nkind.N_TNAME) { if (streq(elt.str, "untyped_int")) { elt = mktname(c, "int"); } else { if (streq(elt.str, "untyped_float")) { elt = mktname(c, "f64"); } else { if (streq(elt.str, "untyped_str")) { elt = mktname(c, "str"); } else { if (streq(elt.str, "untyped_rune")) { elt = mktname(c, "rune"); } else { if (streq(elt.str, "untyped_bool")) { elt = mktname(c, "bool"); }; }; }; }; }; }; // Empty inferred arrlit: default to int, symmetric with cstage // check.c:1859 empty-fallback ty_int (#103). Was "i32". if (elt == nil) { elt = mktname(c, "int"); }; let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); // #6(niche): stamp the SYNTHESIZED element TNAME so the inferred // array's cgen is IDENTICAL to an explicit [N]T's (whose element is // resolvewalk-stamped). For a scalar element (int/u8) cgen's // fallback reads correctly with a nil elt.type_ (byte-id either // way), but a multi-word element (str, 24B) needs the resolved // element tinfo to emit the 3-word header element load — without it // cgen drops to the 8B-scalar path and `xs[i].len` reads the slot // stride, not the length (silent cs!=ww). Idempotent: elt may be a // real exprtype result whose type_ is already set. if (elt.type_ == nil) { elt.type_ = tinfofornode(c, elt): *void; }; arr.lhs = elt; let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); cn.uval = count; // #6(niche): stamp the SYNTHESIZED count literal. When this arr is // planted on an inferred array global's n.lhs (the array twin of // #135/#150-B), asserttyped walks arr.rhs (this N_INTLIT, an expr // node) and trips `asserttyped: int` — an explicit [N]T's count is // resolvewalk-stamped, the synthesized inferred one wasn't. The // element TNAME (arr.lhs) needs no stamp: N_TNAME is not an // asserttyped expr kind. tinfofornode only resolves type-kind // nodes, so type the count off a fresh `int` TNAME; cgen reads // arr.rhs.uval, so this stamp is byte-id-inert. cn.type_ = tinfofornode(c, mktname(c, "int")): *void; arr.rhs = cn; e.type_ = tinfofornode(c, arr): *void; // #103: stamp the synthesized array NODE too. checkletassign // plants this node on the inferred let's n.lhs; slotsize / // elemsizeofc / letslotsize all read n.lhs.type_, which was nil // here (only e.type_ was set) — falling to the 8 / 0 sentinels. arr.type_ = e.type_; 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; }; }; // Retained divergence: *[N]T does NOT decay here — // `p[lo:hi]` types as [][N]T (C-pointer-slicing), unlike // the index route (idxeffti) and unlike Hare. Loud on the // usual []T annotation; for-range likewise. Team task #18 // (#61-residual A). 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). // #264: consume matchyieldtype's *tinfo DIRECTLY (no tinfofornode // round-trip) — the post-walk call reads the operand's cached // stamp, so the out-of-scope re-derive that clobbered *p/p[i]/*p+1 // is never reached. `retn` captures the pre-walk re-derived type // node (nil on the post-walk cached read) for the assignability // node the consumers (checkletassign/checkretassign) read off this // arm's *node return; see matchyieldtype's docstring + #279. let yt: *tinfo = nil; let retn: *node = nil; let cs: *node = e.list; for (cs != nil) { // cs.str/cs.lhs = the arm's case-binding name + declared // type (the N_MCASE binder); fed to matchyieldtype's #241 // scope-popped `yield ` fallback. let armn: *node = nil; let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn); if (t != nil) { if (yt == nil) { // First yielding arm: it is the match's type; // retn carries its *node for the consumers (#264). yt = t; retn = armn; } else { // #38/F2 (review item 6): cross-arm yield // unification. cstage check.c:2080 rejects an arm // whose yield is neither type_eq nor type_assignable // to the first arm's. ww has tinfo typeeq but no // tinfo type_assignable, so reject only a DEFINITE // coarse-family mismatch (yieldclass) — closing the // catA silent accept (int arm vs str arm reads the // str header through an int-stamped slot at runtime) // while staying lenient on same-family / untyped // promotions cstage admits. Walks ALL arms (no early // break): the post-walk matchyieldtype reads each // arm's cached operand stamp, a pure read (#264). // RETAINED DIVERGENCE (match-yield precision-gap task, // lead #52 - distinct from the enum-reinterpret #52 // note elsewhere): the coarse yieldclass ALSO under- // rejects same-family-but-not-assignable arms cstage // DOES reject (untyped_int vs i64 / untyped_int vs // untyped_float) - a precision gap, not a silent // miscompile of the catA repro; closeable only with a // real tinfo type_assignable. if (!typeeq(yt, t)) { let ca: i32 = yieldclass(yt); let cb: i32 = yieldclass(t); if (ca != 0i32 && cb != 0i32 && ca != cb) { deffolderr(c, cs, "match arm yields an incompatible type"); }; }; }; }; cs = cs.next; }; if (yt == nil) { yt = tinfofornode(c, mktname(c, "void")); }; e.type_ = yt: *void; return retn; }; 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; }; // intkindast / numkindast / boolkindast — operand-kind classifiers for // binoptype/unoptype's cstage-mirrored gates (#38/F2 review item 5). // They resolvealias + unwrapbang to chase the alias (TY_NAMED) wrapper // before classifying — exactly as cstage's type_isint/type_isnum recurse // through TY_NAMED.under (cmd/wcc/type.c:178-180/192-193) and its AND/OR // arm chases via type_chase_named (check.c:1206-1207). Without the chase, // a `type myint = i32` operand would spuriously fail the gate that cstage // (chasing) accepts. nil operands are treated as already-errored upstream // (the unifyarith nil-bail shapes) and NOT re-rejected — the cstage twin's // `l == ty_err` escape. fn intkindast(c: *checker, t: *node) bool = { if (t == nil) { return false; }; let u: *node = resolvealias(c, unwrapbang(t)); if (isinttypeast(u)) { return true; }; // int family + enum + rune if (isuntypedint(u)) { return true; }; if (u != nil && u.kind == nkind.N_TNAME && streq(u.str, "untyped_rune")) { return true; }; return false; }; fn numkindast(c: *checker, t: *node) bool = { if (intkindast(c, t)) { return true; }; if (t == nil) { return false; }; let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return false; }; if (u.kind != nkind.N_TNAME) { return false; }; let s: str = u.str; if (streq(s, "f32")) { return true; }; if (streq(s, "f64")) { return true; }; if (streq(s, "untyped_float")) { return true; }; return false; }; fn boolkindast(c: *checker, t: *node) bool = { if (t == nil) { return false; }; let u: *node = resolvealias(c, unwrapbang(t)); if (u == nil) { return false; }; if (u.kind != nkind.N_TNAME) { return false; }; return streq(u.str, "bool") || streq(u.str, "untyped_bool"); }; 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"); }; // tagshape — #24/#37: the coarse variant-shape bucket of a (resolved) type // node, the AST-side mirror of cgen taggedvariantindext's str/slice shape // fallback (cgenutil.ww:3062-3070, `wantstr`/`wantslice` over typeisstr/ // typeisslice). Three buckets: 2=slice, 1=str, 0=scalar/other (ptr / struct // / tuple / chan / fn / int / enum / ...). Used by the concrete→tagged // aggregate-shape-lenient leg to keep a tagged accept lenient ONLY against a // shape-compatible variant — same classifier cgen boxes with, so the checker // accept and the cgen box agree (rule-12: reuse the in-tree classifier). fn tagshape(t: *node) i32 = { if (t == nil) { return 0i32; }; if (t.kind == nkind.N_TSLICE) { return 2i32; }; if (isstrtname(t)) { return 1i32; }; return 0i32; }; // addrfnptrmatches — true iff `ptr` (after alias-resolve) is a // pointer whose referent resolves to a fn type structurally equal to // `synth` (a synthetic N_TFN built from a fn decl's ret + params). // Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206); // reuses typeeqast — the same structural-fn comparator cstage uses via // type_eq(TY_FN,TY_FN) — for symmetry. fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = { if (ptr == nil) { return false; }; let pu: *node = resolvealias(c, unwrapbang(ptr)); if (pu == nil) { return false; }; if (pu.kind != nkind.N_TPTR) { return false; }; let ref: *node = resolvealias(c, unwrapbang(pu.lhs)); if (ref == nil) { return false; }; if (ref.kind != nkind.N_TFN) { return false; }; return typeeqast(c, synth, ref); }; // assignableaddrfn — project #206 Option C gate. Mirror of cstage // assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally // as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot; // isassignable stays nominal (the pointer-fn arm below confidently // rejects a laundered `*fn` value, like harec types.c:1039-1066). This // admits only the shape harec adopts via its address-of hint (harec // check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature // structurally matches the destination's pointed-to fn alias, or the // single matching ptr-to-fn variant of a tagged dst (>=2 same-sig // variants → ambiguous, reject). Lives at the assignment-boundary // caller sites — not in exprtype — because the alias identity is // nominal-lossy once typed and the direct-&fn shape survives only on // the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape // (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn // decl's lhs/list compares correctly under typeeqast. fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = { if (dst == nil) { return false; }; if (rhs == nil) { return false; }; if (rhs.kind != nkind.N_UN) { return false; }; if (rhs.op != tkind.TK_AMP) { return false; }; let id: *node = rhs.lhs; if (id == nil) { return false; }; if (id.kind != nkind.N_IDENT) { return false; }; // #4: curmod preference. Without it a `&handler` whose leaf also // names a fn in a later module misbinds the foreign fn's signature // here (scopedefineinmodule prepends → chain-first = last module), // silently admitting a *fn into a foreign-sig slot or rejecting a // valid same-module &fn. cstage uses scope_lookup_prefer with // cur_mod (cmd/wcc/check.c:410, assignable_addrfn). let s: *sym = scopelookupprefer(c.cur, c.curmod, id.str); if (s == nil) { return false; }; if (s.skind != skind.SK_FN) { return false; }; if (s.decl == nil) { return false; }; let d: *node = s.decl; if (d.kind != nkind.N_FNDECL) { return false; }; let synth: *node = newnode(nkind.N_TFN, "", 0, 0); synth.lhs = d.lhs; synth.list = d.list; let du: *node = resolvealias(c, unwrapbang(dst)); if (du == nil) { return false; }; if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); }; if (du.kind == nkind.N_TTAGGED) { let nmatch: i32 = 0; let v: *node = du.list; for (v != nil) { if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; }; v = v.next; }; return nmatch == 1; }; return false; }; // 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(c, du, su)) { return true; }; // #258: implicit [N]T -> []T array-to-slice borrow. Hare admits an // array with a defined length wherever its element slice is expected // (ref/harec/src/types.c:1080-1097, the SLICE-dst arm). Element types // must match exactly — no element decay; a mismatch is a CONFIDENT // reject (mirror cstage type.c type_assignable's #258 arm + fallthrough // to 0). The acceptance sites then desugar the array expr to an // explicit full slice via desugararrayslice; cgen is untouched. if (du.kind == nkind.N_TSLICE) { if (su.kind == nkind.N_TARRAY) { if (typeeqast(c, du.lhs, su.lhs)) { return true; }; return false; }; }; // 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; }; // mirror cstage type_isnum(enum)=true (type.c:201 -> // type_isint -> :178 TY_ENUM); an enum variant DOES // accept an untyped int. Without this the #23 fix would // flip an enum-variant union to reject while cstage // accepts = a NEW divergence (A3 trap). if (vu.kind == nkind.N_TENUM) { return true; }; }; v = v.next; }; // #24: a SPREAD variant (`...formattable`) keeps the lenient // escape — cstage flattens spreads at resolve_type so its // type_assignable sees the spread's inlined numeric leaves and // accepts `take(42)` into `(...formattable | bool)`; wwstage // stays AST-keyed (#115) and cannot flatten, so a confident // reject here would OVER-reject what cstage accepts (the new c3 // general call-arg check made this path reachable). Mirror the // tagged→tagged spread escape (:4117). Spread decl-form flatten // is #199b, deferred. for (let p: *node = du.list; p != nil; p = p.next) { if (p.op == tkind.TK_ELLIPSIS) { *confident = false; return true; }; }; // #23: no DIRECT variant accepts an untyped int -> confident // reject (mirror cstage type.c:343 `return 0`). ww does NOT // flatten a nested union variant (#199-alpha non-drill); an int // reachable only via a nested union (e.g. (inner|str), // inner=(int|bool)) would otherwise silently build tag=0/payload // with no inner-tag wrapper = malformed box. *confident is // already true (:3777, untouched on this path) so the caller // sees ok=false,conf=true -> loud errnotassign. DEFERRED // divergence (task #23 / #199b): both stages then over-reject // valid Hare (expand_tagged flattens); faithful flatten+rebox // is post-CSP nominal-identity work. return false; }; // 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; }; }; // #24: untyped int into a known AGGREGATE (slice/array/ptr/fn/chan/ // tuple/struct) — confident reject. `let xs: []int = 5` read the int // as a 24B slice header (the #24 silent-garbage; the catch-all below // left it unconfident → silent accept). cstage type_assignable // rejects untyped_int into a non-numeric aggregate (cmd/wcc/type.c). // The TTAGGED case is handled above; this is the bare aggregate. if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY || du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN || du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE || du.kind == nkind.N_TSTRUCT) { 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; }; }; // A4 (#23 float-twin): (T | ...) tagged dst — accept iff a DIRECT // variant is a float type. cstage type_assignable for an // untyped_float src uses type_isfloat (type.c:376 — f32/f64 ONLY, // NOT type_isnum), so an int/enum variant does NOT accept an // untyped float. No direct float variant -> confident reject // (cstage type.c:316 loop returns 0); ww does NOT flatten a // nested-union float variant (#199-alpha). *confident is already // true (:3777). Without it the catch-all (:3972) over-accepts an // untyped float into ANY tagged (silent tag=0). Faithful // flatten+rebox deferred post-CSP (nominal id, #23/#40). 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 (vu.kind == nkind.N_TNAME) { if (streq(vu.str, "f32")) { return true; }; if (streq(vu.str, "f64")) { return true; }; }; }; v = v.next; }; // #24: spread variant keeps the lenient escape (cstage flattens; // wwstage can't) — same rationale as the untyped-int arm above. for (let p: *node = du.list; p != nil; p = p.next) { if (p.op == tkind.TK_ELLIPSIS) { *confident = false; return true; }; }; return false; }; // #24: untyped float into a known AGGREGATE — confident reject (twin // of the untyped-int aggregate arm above; `let xs: []f64 = 1.5`). if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY || du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN || du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE || du.kind == nkind.N_TSTRUCT) { 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; }; if (v.kind == nkind.N_TCHAN) { return true; }; if (v.kind == nkind.N_TFN) { return true; }; v = v.next; }; // #24: spread variant keeps the lenient escape (cstage flattens; // wwstage can't) — same rationale as the untyped-int arm above. for (let p: *node = du.list; p != nil; p = p.next) { if (p.op == tkind.TK_ELLIPSIS) { *confident = false; return true; }; }; // A5: no nullable (ptr/slice/chan/fn) variant -> confident // reject (cstage type.c:316 loop returns 0; nil accepts only // into ptr/slice/chan/fn per type.c:382-385). *confident is // already true (:3777). Without it the catch-all (:3972) // over-accepts nil into ANY tagged (silent). The trailing // fallthrough below stays for a NON-tagged du (nil into a bare // scalar — a separate non-family over-accept, SIBLINGS). return false; }; *confident = false; return true; }; // Tagged-union variant inclusion: src is one of dst's variants. // #199(α): direct variant only — no transitive drill into a // NAMED-tagged wrapper variant. Cgen has no wrapped-slot layout // (taggedvariantindext returns -1 → tag=0 silent miscompile on // io.underread → (size|io.eof|io.error)). ww-stricter than Hare; // harec keeps the drill at types.c:702-739 (#199b deferred port). // Restores SSoT with `is`/`as` non-recursive lookup (#198 sibling). // Callers compose `let inner: Wrapper = sub; let r: parent = inner;`. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { let v: *node = du.list; for (v != nil) { // Spread `...wrapper` keeps the recursive drill: the // wrapper's flat variants are intentionally inlined into // the parent set, and AST-level params haven't been // expanded yet (cstage flattens at resolve_type; wwstage // stays AST-keyed). Plain wrapper variant gets the // direct-only gate. let vspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *node = resolvealias(c, unwrapbang(v)); let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { if (typeeqast(c, v, src)) { return true; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { return true; }; }; v = v.next; }; // #24: no variant matched. A SCALAR src (known primitive, su is // N_TNAME) is a CONFIDENT reject — `int` into `(str | bool)` (the // #23/#199-α path). An AGGREGATE src (ptr/slice/struct/tuple/chan/ // fn — su.kind != N_TNAME) stays LENIENT: wwstage's nominal-lossy // model can't confirm a cross-module ptr/struct variant (the `stream` // variant is `*vtable` but io.handle's consumer hands a `*io.vtable` // from `&cgoutstream.vt`, or a qualified `io.stream` alias — bare-vs- // qualified NAMED identity, #10/#66; typeeqast can't span it), while // cstage flattens+resolves and ACCEPTS (io.stream → io.handle = // (file | stream)). Pre-c3 the loop accepted ANY src via the first // scalar variant's lenient-true short-circuit; the c3 scalar↔aggregate // reject removed that crutch, exposing the latent nominal gap, so // distinguish by src shape here. The handoff's "preserve concrete→ // tagged accept" path. // // An AGGREGATE src (su.kind != N_TNAME) stays lenient ONLY against a // SHAPE-COMPATIBLE variant — tagshape mirrors cgen taggedvariantindext's // str/slice/scalar-other classifier (cgenutil.ww:3062), so the checker // accept and the cgen box agree (rule-12). A shape-MISMATCHED aggregate // (e.g. a `[]int` slice src into a tagged with no slice variant) is a // CONFIDENT reject, matching cstage's nominal type_assignable; this is // the reachable win (#24-B, rob/lead-ruled shape-narrowing over the // blanket aggregate-lenient). // // RULE-7 TRACKED RESIDUAL (task #37, behind the #10/#66 nominal arc; // NEVER silent): the SAME-COARSE-SHAPE leg still OVER-ACCEPTS a cross- // module SAME-LEAF collision that cstage REJECTS — e.g. `mod1.stream` // (a `*mod1.wbox`, scalar/other shape) passed where `io2.handle = // (io2.file | io2.stream)` is wanted (io2.stream is also scalar/other, // so the shapes match and this stays lenient). cstage rejects it on // NOMINAL identity; wwstage accepts. PROVEN STRUCTURALLY UNREACHABLE // here: the tagged-union variant node is a BARE name (`stream`, // N_TNAME, no module) — BYTE-IDENTICAL for the genuine io2.stream and // the collision mod1.stream — so isassignable, which is AST-NODE-keyed, // has no bit to tell them apart; the distinguishing identity lives only // in the tinfo layer (#66 per-decl TY_NAMED ptr, which cgen's // flatvariantidxt already uses). The reject becomes reachable ONLY when // isassignable is converted to nominal-tinfo keying = the #37 / // #10/#66 work itself, NOT a c3-scope change. (B) shape-narrowing // shrinks the residual from "all aggregate→tagged" to "same-coarse- // shape same-leaf" but cannot close the same-shape ptr↔ptr collision. // This is the LEAF-NAME nominal-collision family also documented at the // tagged→tagged qualleaf bridge (this fn, below) — the eventual #10/#66 // sweep must convert BOTH sites uniformly (enumerate for the sweep: // (i) this concrete→tagged shape-lenient leg, (ii) the tagged→tagged // qualleaf bridge). Pre-c3 the collision was ALSO accepted (call-arg // ran no check; let/return short-circuited on the scalar variant) — c3 // is NEUTRAL on it. if (su.kind != nkind.N_TNAME) { let ss: i32 = tagshape(su); let sp: *node = du.list; for (sp != nil) { let svu: *node = resolvealias(c, unwrapbang(sp)); if (svu != nil) { if (tagshape(svu) == ss) { *confident = false; return true; }; }; sp = sp.next; }; // no shape-compatible variant → confident reject (the #24-B win) return false; }; 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) { // #205: NAMED-variant nominal compare BEFORE permissive // fallthrough. Mirror of cstage type.c type_assignable // tagged→tagged arm (#199 α concrete→tagged sibling). // When src is a NAMED-tagged wrapper and dst has a direct // NAMED-tagged variant equal to src, accept by nominal // identity — wrapper's leaves are NOT direct variants of // dst, so a structural subset walk would reject. SSoT // with `is`/`as` variant lookup (#198 family). let v: *node = du.list; for (v != nil) { let vu: *node = resolvealias(c, unwrapbang(v)); if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { if (typeeqast(c, v, src)) { return true; }; }; }; v = v.next; }; // Spread `...` member on either side: keep the lenient escape. // cstage flattens spreads at resolve_type so its subset loop // never sees one; wwstage stays AST-keyed (#115, check.ww:921), // so a TK_ELLIPSIS variant can reach here. Routing it through // the strict typeeqast subset loop would over-reject a valid // spread-widen cstage accepts → new cs≠ww divergence. Spread // decl-form layout is #199b, deferred. let hasspread: bool = false; for (let p: *node = du.list; p != nil; p = p.next) { if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; }; for (let p: *node = su.list; p != nil; p = p.next) { if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; }; if (hasspread) { *confident = false; return true; }; // Structural subset loop — wwstage was align-DOWN-missing this; // cstage type.c type_assignable tagged→tagged arm (type.c:360-367). // Every src variant must appear in dst, else a loud reject; a // genuine subset accepts. // // Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast // FIRST for the exact / structural variants (e.g. []u8 slices, // nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a // LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs- // qualified spelling mix typeeqast cannot span. A callee returning // an INLINE union spells its variants BARE (utf8.next: // (rune|done|more|invalid)) while the consumer annotates them // QUALIFIED (utf8.done); the module qualifier is unrecoverable for // an inline-union return, so leaf alone decides. Mirrors casecovers' // typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT // compared (cf casevariantpairmatch): module identity is the #10 // gap. Sound for the bootstrap — no two of its variants share a leaf // (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a // known over-accept deferred to #10 / filed in the #4 census. for (let sp: *node = su.list; sp != nil; sp = sp.next) { let ok: bool = false; for (let dp: *node = du.list; dp != nil; dp = dp.next) { if (typeeqast(c, dp, sp)) { ok = true; break; }; let su2: *node = unwrapbang(sp); let du2: *node = unwrapbang(dp); if (su2 != nil && du2 != nil && su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME && streq(qualleaf(su2.str), qualleaf(du2.str))) { ok = true; break; }; }; if (!ok) { return 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; }; }; }; // #206: two pointers whose referents both resolve to fn types are // NOMINALLY assignable only when structurally equal — and that case // already returned true via typeeqast at the top. Reaching here // means the fn signatures differ, or a bare structural `*fn` value // is being laundered into a `*alias` slot: confidently NOT // assignable, mirror of cstage's nominal type_assignable (harec // types.c:1039-1066). The direct `&fn` adopt-the-alias case is // handled at the assignment caller sites via assignableaddrfn, NOT // here. Without this the lenient catch-all below silently accepted // the laundering shape. if (du.kind == nkind.N_TPTR) { if (su.kind == nkind.N_TPTR) { let dref: *node = resolvealias(c, unwrapbang(du.lhs)); let sref: *node = resolvealias(c, unwrapbang(su.lhs)); if (dref != nil) { if (sref != nil) { if (dref.kind == nkind.N_TFN) { if (sref.kind == nkind.N_TFN) { return false; }; }; }; }; }; }; // #34: two bare fn types reaching here are NOT structurally equal // (typeeqast returned true at the top otherwise) — the fn signatures // differ, a confident reject mirroring cstage's structural fn // type_assignable (`init fn() str not assignable to declared fn() i32`; // harec types.c:1001 dealias-equal fn types). Manifests only now that // S1/S2 stamp a bare fn rvalue with its fn type: `let p: fn()i32 = h` // (h: fn()str) was a silent mis-accept via the lenient catch-all below. // The matched-sig case already returned true via typeeqast at the top. // The `&fn` (*fn) vs bare-fn KIND mismatch (du N_TFN, su N_TPTR) is a // different shape, closed by c3's aggregate-kind reject, not here. if (du.kind == nkind.N_TFN) { if (su.kind == nkind.N_TFN) { return false; }; }; // #24: a known scalar primitive vs a known aggregate (slice / array / // ptr / fn / chan / tuple / struct), and two aggregates of DIFFERENT // kinds, are CONFIDENT rejects — mirror cstage type_assignable, which // separates scalar from aggregate and rejects a kind mismatch (the int // read as a 24B slice header was the #24 silent-garbage). du/su are // already alias-RESOLVED (:3943/3944), so `type A = []int` arrives as // N_TSLICE. The array→slice BORROW (su N_TARRAY into du N_TSLICE), // untyped / nil / tagged, and the known-primitive-pair / fn-ptr / fn-fn // shapes all returned above before reaching here. The `&fn` adopt-the- // alias case (a *fn N_TPTR src into a bare-fn N_TFN dst — different // aggregate kinds) is rescued at the let/return/call-arg sites by the // assignableaddrfn UNION, so a reject here is correct (the caller's // union accepts the genuine &fn). SAME-kind aggregate structural // mismatches ([]int vs []str, *u8 vs *i32) stay lenient below — // wwstage's nominal-lossy model can't span them (the #10 gap); cstage // rejects via structural type_assignable, a filed residual under-reject, // NOT a new over-reject. A NAMED struct/alias dst that does NOT resolve // to a known kind stays N_TNAME-non-prim → neither set → lenient. let dprim: bool = du.kind == nkind.N_TNAME && (isnumerictname(du) || isstrtname(du) || streq(du.str, "bool") || streq(du.str, "void")); let sprim: bool = su.kind == nkind.N_TNAME && (isnumerictname(su) || isstrtname(su) || streq(su.str, "bool") || streq(su.str, "void")); let daggr: bool = du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY || du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN || du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE || du.kind == nkind.N_TSTRUCT; let saggr: bool = su.kind == nkind.N_TSLICE || su.kind == nkind.N_TARRAY || su.kind == nkind.N_TPTR || su.kind == nkind.N_TFN || su.kind == nkind.N_TCHAN || su.kind == nkind.N_TTUPLE || su.kind == nkind.N_TSTRUCT; if (sprim && daggr) { return false; }; if (dprim && saggr) { return false; }; // #24: two aggregates of DIFFERENT kinds → confident reject (array/slice // into ptr/fn/chan/tuple is the 24B/16B-header misread). EXEMPT a STRUCT // on either side: wwstage's name-keyed resolvealias mis-resolves a bare // cross-module same-leaf type name to the WRONG module's struct (#224 — // `type s = *vtable` in sa vs `type s = struct{}` in sb; sa.read's bare // `s` param resolves to sb's struct), so a struct-vs-ptr "mismatch" here // is an artifact of the lossy resolution, not a real type error — cstage // resolves `s` correctly and ACCEPTS (test 784). Same nominal-lossy // principle as the concrete→tagged aggregate-lenient arm above; the // struct-into-ptr genuine mismatch stays a filed #224/#10 under-reject. if (daggr && saggr && du.kind != su.kind && du.kind != nkind.N_TSTRUCT && su.kind != nkind.N_TSTRUCT) { 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. // qualleaf — rightmost dotted segment of a (possibly module-qualified) // type name; the whole name when unqualified. fn qualleaf(nm: str) str = { let dotidx: i32 = -1; let i: i32 = 0; for (i < nm.len) { if (nm[i] == 46u8) { dotidx = i; }; i += 1; }; if (dotidx < 0) { return nm; }; let leaf: str; leaf.ptr = nm.ptr + ((dotidx + 1): u64); leaf.len = nm.len - dotidx - 1; return leaf; }; // qualmod — module qualifier of a type name (segment before the // rightmost '.'), or `defmod` when unqualified. fn qualmod(nm: str, defmod: str) str = { let dotidx: i32 = -1; let i: i32 = 0; for (i < nm.len) { if (nm[i] == 46u8) { dotidx = i; }; i += 1; }; if (dotidx < 0) { return defmod; }; let head: str; head.ptr = nm.ptr; head.len = dotidx; return head; }; // casevariantpairmatch — a case pattern names variant `v` of the tagged // union iff their (module, leaf) PAIRS match. Each name reduces to // (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE // name is attributed `unionmod`, the union's defining module. So a // cross-module `case errors.unsupported` matches the bare `unsupported` // in errors.error's body, while a foreign `othermod.unsupported` is // REJECTED (qualifier differs) and Shape A's dotted `errors.error` // variant of io.error keeps matching `case errors.error` (defmod is // NOT forced onto a dotted variant — drew #13). Approximates harec's // resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST // level via the existing nmod/aliassym machinery (cf. #51/#53); the // precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the // exact-string cases first, so this fires only on the qualified-vs-bare // mix. fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = { let vv: *node = unwrapbang(v); let pp: *node = unwrapbang(pat); if (vv == nil) { return false; }; if (pp == nil) { return false; }; if (vv.kind != nkind.N_TNAME) { return false; }; if (pp.kind != nkind.N_TNAME) { return false; }; if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod)); }; // taggeddefmod — defining module of the typedecl whose body IS the // tagged union (follows alias hops via aliassym, the #51/#53 machinery). // Bare variants in that body are defined here: io.error = // !(errors.error | underread | nomem) (module io) -> "io"; errors.error // -> "errors". Falls back to c.curmod when the chain can't resolve. fn taggeddefmod(c: *checker, st: *node) str = { let defmod: str = c.curmod; let cur: *node = unwrapbang(st); for (cur != nil) { if (cur.kind != nkind.N_TNAME) { return defmod; }; let s: *sym = aliassym(c, cur); if (s == nil) { return defmod; }; if (s.decl == nil) { return defmod; }; if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; }; let body: *node = unwrapbang(s.decl.lhs); if (body == nil) { return defmod; }; if (body.kind == nkind.N_TTAGGED) { return defmod; }; cur = body; }; return defmod; }; fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { if (typeeqast(c, cs.lhs, want)) { return true; }; if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { if (typeeqast(c, alt, want)) { return true; }; if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; return false; }; fn errmatchvariant(c: *checker, n: *node, vname: *node) void = { cerr("match: variant not handled"); if (vname != nil) { if (vname.kind == nkind.N_TNAME) { cerr(" ("); cerr(vname.str); cerr(")"); }; }; cerr("\n"); 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`. // // #209: a `...inner` spread variant (v.op == TK_ELLIPSIS, parse.ww:284) // is NOT a variant in itself — its inner tagged union's MEMBERS are. The // AST `tagged.list` keeps the spread unexpanded (only the #61a tinfo // build flattens it), so recurse into the inner union's members, // attributing them the inner union's defining module. Mirrors cstage's // resolve_type flatten (cmd/wcc/check.c:651-674) at the AST layer; the // cgen side already dispatches off the flattened tinfo.params (#61a). fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { if (v.op == tkind.TK_ELLIPSIS) { let inner: *node = resolvealias(c, unwrapbang(v)); if (inner != nil) { if (inner.kind == nkind.N_TTAGGED) { if (casevariantin(c, inner, pat, taggeddefmod(c, v))) { return true; }; v = v.next; continue; }; }; }; if (typeeqast(c, v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; return false; }; fn errbadcase(c: *checker, pat: *node) void = { cerr("case: not a variant of scrutinee"); if (pat != nil) { if (pat.kind == nkind.N_TNAME) { cerr(" ("); cerr(pat.str); cerr(")"); }; }; cerr("\n"); 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); // F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage // types the try-result as the success variant and rejects when it // is not itself a tagged union (check.c "match on non-tagged- // union"); scruttype's IDENT/DOT-only resolution let the form slip // through silently (cs≠ww). The reject below is gated on the // try-form so the lenient-miss contract for other unresolvable // scrutinees is untouched; a tagged success keeps flowing into the // normal exhaustiveness walk, matching cstage's accept. let istry: bool = false; if (st == nil) { if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { istry = true; st = exprtype(c, n.lhs, nil); }; }; let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { if (istry) { cerr("match on non-tagged-union try-result\n"); c.errs += 1; }; return; }; // #13: the union's defining module, so a bare body variant can be // matched against a module-qualified cross-module case pattern (and // a foreign-qualifier pattern correctly rejected). See // casevariantpairmatch. let unionmod: str = taggeddefmod(c, st); // 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(c, u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { if (!casevariantin(c, u, alt, unionmod)) { 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) { checkvariantcovered(c, n, v, unionmod); v = v.next; }; }; // checkvariantcovered — emit "variant not handled" unless some case arm // covers `v`. #209: a `...inner` spread variant expands to its inner // union's members (each attributed the inner union's defining module), // so the phantom spread node is never itself reported uncovered — its // members are checked instead. Mirrors the casevariantin spread recursion // + cstage's flattened u->params exhaustiveness walk (cmd/wcc/check.c: // 1648-1669). Leaf variants keep their NODE so errmatchvariant names them. fn checkvariantcovered(c: *checker, n: *node, v: *node, unionmod: str) void = { if (v.op == tkind.TK_ELLIPSIS) { let inner: *node = resolvealias(c, unwrapbang(v)); if (inner != nil) { if (inner.kind == nkind.N_TTAGGED) { let im: str = taggeddefmod(c, v); let m: *node = inner.list; for (m != nil) { checkvariantcovered(c, n, m, im); m = m.next; }; return; }; }; }; let covered: bool = false; let cs2: *node = n.list; for (cs2 != nil) { if (casecovers(c, cs2, v, unionmod)) { covered = true; cs2 = nil; } else { cs2 = cs2.next; }; }; if (!covered) { errmatchvariant(c, n, v); }; }; // ---- 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 = { cerr(where); cerr(": not assignable"); if (src != nil) { if (src.kind == nkind.N_TNAME) { cerr(" ("); cerr(src.str); cerr(" → "); if (dst != nil) { if (dst.kind == nkind.N_TNAME) { cerr(dst.str); }; }; cerr(")"); }; }; cerr("\n"); c.errs += 1; }; // taggedarrayvariantctor — #5/#60: true when `src` is boxed into the // tagged union `dst` via a variant that chases to TY_ARRAY. The array- // payload box is unwired in cgen (cstage zero-filled the slot at box // materialization — a silent MOVQ $0 payload drop; wwstage only loud at // the dead match arm, errbadcase:4107). Reject the CONSTRUCT until the // faithful array-block-store lands (deferred task #6). The tagged TYPE- // decl with an array variant stays legal — test 944 declares // (void|size|[5]size) and boxes only the narrow `size` variant — only // the array-variant box is refused. Mirrors the concrete→tagged variant // select in isassignable (:3859) and cstage tagged_array_variant so the // variant chosen here is the one the box would materialize. fn taggedarrayvariantctor(c: *checker, dst: *node, src: *node) bool = { if (dst == nil) { return false; }; if (src == nil) { return false; }; let du: *node = resolvealias(c, unwrapbang(dst)); let su: *node = resolvealias(c, unwrapbang(src)); if (du == nil) { return false; }; if (du.kind != nkind.N_TTAGGED) { return false; }; if (su != nil) { if (su.kind == nkind.N_TTAGGED) { return false; }; }; let v: *node = du.list; for (v != nil) { let vspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *node = resolvealias(c, unwrapbang(v)); let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { if (typeeqast(c, v, src)) { return false; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { if (vu != nil) { if (vu.kind == nkind.N_TARRAY) { return true; }; }; return false; }; }; v = v.next; }; return false; }; // checkarrlitfits — #130/#251 array-init accept-if-fits, shared by the // let / def / struct-field init sites. arrtn is the declared [N]T type // node, rhs the N_ARRLIT. Per element: foldable int/rune literal → // defcastfits range-check against T (in-range accept, out-of-range LOUD // reject — rule-7/Drew, Hare range-checks at the literal-value level); // non-foldable → isassignable to the element type. Mirrors cstage // cmd/wcc/check.c arrlit_init_fits. The element WIDTH is driven by the // declared type at cgen, so no element-type restamp is needed — #251 // proved coerce_floatlit's restamp shape does NOT transfer (cgen reads // the declared type's element size, not the literal node's stamp). fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (arrtn == nil) { return; }; if (rhs == nil) { return; }; // #106: chase a TY_NAMED alias (`type A=[2]int`) to the underlying // [N]T before the kind gate — else an alias declared type bails // here and the over-fill (#9) never fires (silent DATA-truncate). // Idempotent on a non-alias (resolvealias returns the node), so a // direct N_TARRAY is untouched. Makes EVERY caller (decl/let/def/ // struct/return/call-arg) alias-aware by construction. arrtn = resolvealias(c, unwrapbang(arrtn)); if (arrtn == nil) { return; }; if (arrtn.kind != nkind.N_TARRAY) { return; }; if (rhs.kind != nkind.N_ARRLIT) { return; }; // #71: more elements than the declared [N] passed every per-element // check below and then smashed the frame at cgen (each element is // stored at its natural offset — the overflow clobbered neighbours // and even the saved BP). Reject loud before the element walk. // A nil or zero length child stays exempt: nil is the un-inferred // [_] sentinel in def/struct-field contexts and 0 doubles as both // [0] and the cstage [_] sentinel (conflation: task #11); the let // paths stamp the real length before reaching here. #141: a def-dim // child (`[MAX]u8`) now folds via arrayelen and is count-checked // like a literal (closes #13's def-dim exemption). // Under-long (count < N, no `...`) stays accepted as before; Hare // rejects it — task #10. let declen: u64 = arrayelen(c, arrtn.rhs); // #9: fire the over-fill whenever the length is EXPLICITLY declared // (arrtn.rhs present) — incl `[0]`. `[_]` leaves arrtn.rhs nil UNTIL // inferarraylen stamps it with the real count (runs first), so a // resolved `[_]` arrives here with cnt == declen (no over-fill). An // explicit `[0]=[1,2]` keeps arrtn.rhs=N_INTLIT(0) → declen 0, cnt 2 → // loud. `[0]=[]` → cnt 0, no error. Replaces the `declen > 0` guard, // the wwstage twin of cstage's dropped `alen > 0`. if (arrtn.rhs != nil) { let cnt: u64 = 0u64; let ce: *node = rhs.list; for (ce != nil) { let cskip: bool = false; if (ce.kind == nkind.N_FIELD) { if (streq(ce.str, "...")) { cskip = true; }; }; if (!cskip) { cnt += 1u64; }; ce = ce.next; }; if (cnt > declen) { cerr("array literal has "); cerr(strconv.u64tos(cnt, strconv.base.DEC)); cerr(" elements but declared array holds "); cerr(strconv.u64tos(declen, strconv.base.DEC)); cerr("\n"); c.errs += 1; return; }; }; let elemtn: *node = arrtn.lhs; let at: *tinfo = tinfofornode(c, arrtn); let et: *tinfo = nil; if (at != nil) { et = at.sub; }; et = tichase(et); let e: *node = 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; }; // #71: a NESTED array-literal element must run the same // count check against the inner [N] — cstage catches the // nested shape through its typed-literal assignability net // (the literal's stamped [2][3]T fails type_assignable), // which wwstage's untyped elements have no analog of; the // silent accept emitted corrupted DATA / smashed frames. // Recursion through the one choke point closes any depth. // #105: a named-alias element type ([2]row) arrives as // N_TNAME and bypassed the kind test — the module // static-DATA emitter then silently TRUNCATED the overlong // inner literal (exit-masked once the #60 read fix removed // the segv). resolvealias is transitive, so alias spellings // of any depth take the same recursion; a direct N_TARRAY // passes through unchanged. let eltr: *node = resolvealias(c, elemtn); if (eltr != nil && eltr.kind == nkind.N_TARRAY && ev != nil && ev.kind == nkind.N_ARRLIT) { checkarrlitfits(c, eltr, ev); e = e.next; continue; }; 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 = "array element out of range\n"; cerr(m); 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) { // #206: direct `&fn` array element. if (!assignableaddrfn(c, elemtn, ev)) { errnotassign(c, elemtn, est, "array element"); return; }; }; }; }; }; }; e = e.next; }; }; // checktuplearrfits — #20/#25/#26: walk a declared tuple type's // element types (ttn.list, each N_TPARAM-wrapped → type on .lhs) // lockstep with an N_TUPLE rhs's values (tup.list, chained directly). // An array-literal element runs the alias-aware over-fill // checkarrlitfits; a nested-tuple element (declared N_TTUPLE vs rhs // N_TUPLE) recurses. Shared by checkletassign (let) and // checkretassign (return). Mirrors cstage's element-wise // type_assignable count-reject; no-ops on scalar elements. fn checktuplearrfits(c: *checker, ttn: *node, tup: *node) void = { if (ttn == nil) { return; }; if (tup == nil) { return; }; // #38/F2 (review item 10): a tuple literal whose arity differs from the // declared tuple is LOUD. cstage type_assignable's tuple arm requires // both param chains to end together (cmd/wcc/type.c:416, consumed as // "init not assignable" at check.c:2386-2391); wwstage's isassignable // has NO tuple arm, so an over/short literal fell to the lenient // catch-all and the lockstep walk below silently ignored the leftovers. // The short direction was the live miscompile (cgen stored a stale, // never-popped register into the missing slot). Count both chains and // reject a mismatch before the per-element fits walk. Recursion through // this one choke point gates every nesting depth. let dn: u64 = 0u64; let dc: *node = ttn.list; for (dc != nil) { dn += 1u64; dc = dc.next; }; let vn: u64 = 0u64; let vc: *node = tup.list; for (vc != nil) { vn += 1u64; vc = vc.next; }; if (dn != vn) { cerr("tuple literal has "); cerr(strconv.u64tos(vn, strconv.base.DEC)); cerr(" elements but declared tuple holds "); cerr(strconv.u64tos(dn, strconv.base.DEC)); cerr("\n"); c.errs += 1; return; }; let dt: *node = ttn.list; let vt: *node = tup.list; for (dt != nil && vt != nil) { if (vt.kind == nkind.N_ARRLIT) { checkarrlitfits(c, dt.lhs, vt); } else { if (vt.kind == nkind.N_TUPLE) { let drt: *node = resolvealias(c, unwrapbang(dt.lhs)); if (drt != nil && drt.kind == nkind.N_TTUPLE) { checktuplearrfits(c, drt, vt); }; }; }; dt = dt.next; vt = vt.next; }; }; // desugararrayslice — #258. The single shared lowering for the implicit // [N]T -> []T borrow. isassignable already admits an array with a defined // length into a matching []T slot (see isassignable's #258 arm); here we // rewrite the array expr to the explicit full slice `arr[0:len(arr)]` (an // N_SLICE over the array base), reusing the existing slice cgen — #252/ // #257/#135 made array bases (incl struct-field arrays) correct. No new // array->slice store cgen; pushargsrev / cgslice / cglet already lower an // N_SLICE identically to cstage, so the borrow header is byte-id across // stages. Twin of cstage cmd/wcc/check.c desugar_arrayslice. // // Returns the (possibly new) node for the caller's tree slot: `val` // unchanged when the shape doesn't match, else a fresh N_SLICE whose base // is `val` (which keeps its stamped array type_). The original sibling // link transfers to the N_SLICE so a desugared call-arg keeps its place. // rejectarrlitborrow — #31/#33 twin of cstage reject_arrlit_borrow. The // array-literal → slice borrow is supported only at a `let` init (where // checkletassign re-stamps + the cgslice N_ARRLIT-base arm spills the // literal to a per-borrow backing slot). In call-arg / return / assign // position there is no addressable backing — loud-reject so the gap is a // compile error, not a dangling-ptr miscompile. Both stages reject here // (rule-10, byte-id-trivial: no asm). Full non-let support is #33. fn rejectarrlitborrow(c: *checker, dsttn: *node, val: *node) bool = { if (val == nil) { return false; }; if (val.kind != nkind.N_ARRLIT) { return false; }; let du: *node = resolvealias(c, unwrapbang(dsttn)); if (du == nil) { return false; }; if (du.kind != nkind.N_TSLICE) { return false; }; let m: str = "array literal cannot borrow as a slice here; bind it to a `let` first\n"; cerr(m); c.errs += 1; return true; }; fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node = { if (dsttn == nil) { return val; }; if (srctn == nil) { return val; }; if (val == nil) { return val; }; let du: *node = resolvealias(c, unwrapbang(dsttn)); let su: *node = resolvealias(c, unwrapbang(srctn)); if (du == nil) { return val; }; if (su == nil) { return val; }; if (du.kind != nkind.N_TSLICE) { return val; }; if (su.kind != nkind.N_TARRAY) { return val; }; if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; let sl: *node = newnode(nkind.N_SLICE, val.file, val.line, val.col); sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) let slt: *node = newnode(nkind.N_TSLICE, "", 0, 0); slt.lhs = su.lhs; sl.type_ = tinfofornode(c, slt): *void; sl.next = val.next; val.next = nil; return sl; }; // calleefndecl — resolve a call's callee to its fn-decl node so the // arg/param lockstep (desugarcallargs) can read declared param types. // Mirrors exprtype's N_CALL resolution (bare-leaf via scopelookupprefer, // module-qualified via the SK_USE receiver). nil for builtins / fn-value // callees — they have no declared param list to drive the #258 desugar, // and the selfhost corpus has no array→slice arg there anyway. fn calleefndecl(c: *checker, callee: *node) *node = { if (callee == nil) { return nil; }; if (callee.kind == nkind.N_IDENT) { let s: *sym = scopelookupprefer(c.cur, c.curmod, callee.str); if (s != nil) { if (s.skind == skind.SK_FN) { return s.decl; }; }; return nil; }; if (callee.kind == nkind.N_DOT) { if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { let ms: *sym = 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) { if (ms.skind == skind.SK_USE || ms.use_alias != 0i32) { let fs: *sym = scopelookupinmodule(c.cur, callee.lhs.str, callee.str); if (fs != nil) { if (fs.skind == skind.SK_FN) { return fs.decl; }; }; }; }; }; }; }; return nil; }; // desugarcallargs — #258 at the call-arg context. Lockstep the call's // args against the callee's declared params and desugar an array arg // passed where a []T param is expected. Mirrors cstage cmd/wcc/check.c's // non-variadic call-arg arm. Variadic (`T...`) slots are skipped (the // arg flows into the gather as an element, not the slice itself). fn desugarcallargs(c: *checker, n: *node) void = { if (n == nil) { return; }; // #24: a 1-arg `free(x)` is the Hare no-op pseudo-builtin (#27), NOT the // rt 2-arg `free(p: *void, n: u64)` that calleefndecl resolves to in the // bundle (lib seeds both: the nil-decl builtin at check.ww:137 AND // rt's @symbol("rt_free") decl). cstage intercepts the builtin by name + // arity BEFORE call resolution (cmd/wcc/check.c:1650, n->list->next == // NULL) and runs NO arg typecheck; exprtype's free arm (:3014) is the // wwstage twin but runs after this seam. Skip so `free(charset)` / // `free(slice)` (regex finish #27) isn't checked against rt_free's *void // param. `free` is the only builtin name with a colliding real decl // (len/alloc/append/delete/insert keep nil decls → calleefndecl bails). if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) { if (streq(n.lhs.str, "free")) { if (n.list != nil) { if (n.list.next == nil) { return; }; }; }; }; }; let decl: *node = calleefndecl(c, n.lhs); // task #51: calleefndecl nils every fn-VALUE callee — a deref `(*fp)(...)` // (N_UN), an SK_VAR fn-ptr ident, a struct-field fn call — so this bail // skips the #258 array→slice desugar AND the general arg typecheck for // them, and cgen then pushes raw array words where the callee reads a 24B // slice header. cstage drives the same arg loop off the callee TYPE // (cmd/wcc/check.c:1828 type_chase_named(cexpr(callee))). The type-keyed // callee path is MECHANISM (not a checker gate) — filed as task #51. if (decl == nil) { return; }; let param: *node = decl.list; let prev: *node = nil; let a: *node = n.list; for (a != nil) { let nexta: *node = a.next; if (param != nil) { if (param.kind == nkind.N_PARAM) { if (param.op != tkind.TK_ELLIPSIS) { let atype: *node = exprtype(c, a, nil); // #29: a rune literal narrowing into an integer param // (`take('b')` where take(b: u8)). Override atype to the // integer target so c3's general isassignable accepts, // mirroring cstage's coarse untyped_rune rule. See // coercerunelit. cgen is byte-id-neutral (narrows by the // param/destination width). let runet: *node = coercerunelit(c, a, param.lhs); if (runet != nil) { atype = runet; }; // #24: GENERAL per-arg assignability — align UP to // cstage check.c:1867-1870, which type_assignables // every non-variadic call arg (`argument type %s not // assignable to %s`). wwstage previously ran NO general // param typecheck (only a narrow #258 array→slice arm), // so any mistyped scalar call-arg silently miscompiled // (an int read as a 24B slice header). The shared // isassignable SUBSUMES that #258 arm: its N_TSLICE/ // N_TARRAY arm is a confident reject on an element // mismatch and an accept on a match (the desugar below // then borrows). Conf-gated + UNIONed with // assignableaddrfn exactly like the let/return sibling // sites (:5151/5154, :5222/5225); the spread-tagged // lenient escape (isassignable :4078) keeps conf=false so // `take(42)` into `(...formattable | bool)` stays accepted. let conf: bool = false; let ok: bool = isassignable(c, param.lhs, atype, &conf); if (!ok) { if (assignableaddrfn(c, param.lhs, a)) { ok = true; }; }; if (conf) { if (!ok) { errnotassign(c, param.lhs, atype, "argument"); }; }; // #12: overlong array-lit CALL-ARG — `g([1,2,3])`. // Reject at CHECK time (clean over-fill msg) instead // of falling to cgen #271's late aggregate-arg loud. // param.lhs is the declared param type (alias-aware // via the resolvealias chase in checkarrlitfits). if (a.kind == nkind.N_ARRLIT) { checkarrlitfits(c, param.lhs, a); }; // #31/#33: bare array-literal arg has no backing // — loud-reject (supported only at a `let`). if (!rejectarrlitborrow(c, param.lhs, a)) { let rep: *node = desugararrayslice(c, param.lhs, atype, a); if (rep != a) { if (prev == nil) { n.list = rep; } else { prev.next = rep; }; a = rep; }; }; }; if (param.op != tkind.TK_ELLIPSIS) { param = param.next; }; }; }; prev = a; a = nexta; }; }; // checkassign — #258 at the assignment context. wwstage runs no other // N_ASSIGN typecheck (cstage's lives in cexpr); this exists solely to // route an array→slice rhs through the shared desugar so w6c_ww emits // the same borrow as w6c (rule-10). No error diagnostics — cstage gates // the shape. fn checkassign(c: *checker, n: *node) void = { if (n == nil) { return; }; if (n.lhs == nil) { return; }; if (n.rhs == nil) { return; }; let ltn: *node = exprtype(c, n.lhs, nil); let rtn: *node = exprtype(c, n.rhs, nil); // #29: a rune literal narrowing into an integer assign/index-store // target (`buf[i] = 'F'`). Override rtn to the integer target so a // general assign typecheck accepts, mirroring cstage's coarse // untyped_rune rule. See coercerunelit. cgen narrows by the destination // width (byte-id-neutral). Removes the getopt `'X': u8` index casts. let runet: *node = coercerunelit(c, n.rhs, ltn); if (runet != nil) { rtn = runet; }; // #24/#36 (rule-7 deferred-divergence, NEVER silent): the ASSIGN seam // does NOT yet route the general conf-gated UNION (isassignable || // assignableaddrfn) that the let / return / call-arg seams run — so a // mistyped bare-assignment `x = some_slice` (a 24B slice header into an // 8B int slot) is still silently accepted here, the one remaining // member of the #24 cat-A. cstage DOES check it (cmd/wcc/check.c:1899, // `cannot assign %s to %s`, every op incl. compound). The union was // implemented + reverted: it correctly closed `x = slice` and matched // cstage on `p += 1`, but surfaced a false over-reject of an EXACT- // signature bare fn assigned to a fn-pointer struct field (lib/log // `r.logger.println = stdprintln`) because typeeqast compares fn types // at the AST level and cannot match a variadic + module-qualified-param // fn signature (the #178 divergence, self-flagged at the typeeqast // N_TFN arm). So the assign seam is BLOCKED on #178 and filed as task // #36 (the bounded #178 typeeqast fn-compare fix, task #35, lands // first). Until then this seam runs only coercerunelit + the #258 // array→slice desugar below. // #31/#33: bare array-literal rhs has no backing — loud-reject // (supported only at a `let`). if (!rejectarrlitborrow(c, ltn, n.rhs)) { n.rhs = desugararrayslice(c, ltn, rtn, n.rhs); }; }; // inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The // parser leaves a `[_]` array's length child nil as the infer sentinel // (parse.ww, mirror cstage parse.c:186). Count the array-literal's // elements (skipping the `...` repeat marker, same walk as the N_ARRLIT // exprtype at L2905) and stamp a synthesized N_INTLIT length node so // tinfofornode / cgen / `.len` all read the real count — the wwstage // analogue of cstage clet's `declared = type_array(.., iu->alen)` patch. // A `[_]T` with no array-literal initialiser can't infer: loud error, // never a silent zero-length array (rule 7). Idempotent (skips once the // length child is set), so the module-level double-call (checkfile's // pre-resolvewalk call + checkletassign here) raises at most one error. fn inferarraylen(c: *checker, n: *node) void = { if (n == nil) { return; }; if (n.lhs == nil) { return; }; if (n.lhs.kind != nkind.N_TARRAY) { return; }; if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred if (n.rhs == nil || n.rhs.kind != nkind.N_ARRLIT) { cerr("error: [_]T needs an array-literal initialiser\n"); c.errs += 1; let z: *node = newnode(nkind.N_INTLIT, "", 0, 0); z.uval = 0u64; n.lhs.rhs = z; // sentinel: idempotent, error already raised return; }; let cnt: u64 = 0u64; let it: *node = n.rhs.list; for (it != nil) { let skip: bool = false; if (it.kind == nkind.N_FIELD) { if (streq(it.str, "...")) { skip = true; }; }; if (!skip) { cnt += 1u64; }; it = it.next; }; let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); cn.uval = cnt; n.lhs.rhs = cn; }; fn checkletassign(c: *checker, n: *node) void = { if (n == nil) { return; }; inferarraylen(c, n); // #7: must run before the n.rhs==nil bail 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. // B' (#3): a `let x: []T = alloc([], n)` is the one context that lets // the empty alloc infer its element type (the #45 retype runs AFTER // exprtype, so flag the exact call node up front; exprtype errors on // any empty alloc that isn't this one). Peel the same ?/! wrapper #45 // peels so the flagged node matches. let octx: *node = nil; if (n.lhs != nil && n.lhs.kind == nkind.N_TSLICE) { let inner: *node = n.rhs; if (inner.kind == nkind.N_TRYPROP) { inner = inner.lhs; } else { if (inner.kind == nkind.N_TRYUNW) { 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) { octx = inner; }; }; }; let savedoctx: *node = c.allococtx; c.allococtx = octx; let src: *node = exprtype(c, n.rhs, nil); c.allococtx = savedoctx; // 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) { // #24 (fold-5 prereq): a struct-lit init's exprtype returns the // decl's BODY node (N_TSTRUCT, L3103 per #66) — but every cgen // local-arm dispatch (cgdot read, cgassign tagged-field store, // cgun addr-of) is N_TNAME-keyed, so planting the body dropped // `p.f` to the module-qualified `MOVQ f(SB)` fallback (link-fail // name-leak; #211 family). Normalize to the synthesized TNAME so // the inferred binding is indistinguishable from the annotated // one downstream. cstage needs no twin: check.c:1477 clet carries // Sym.type (tinfo), and its emission is annotation-invariant // (probed identical asm annotated vs not). if (src != nil && src.kind == nkind.N_TSTRUCT && n.rhs.kind == nkind.N_STRUCTLIT && n.rhs.lhs != nil && n.rhs.lhs.kind == nkind.N_IDENT) { let ltn: *node = mktname(c, n.rhs.lhs.str); ltn.type_ = tinfofornode(c, ltn): *void; n.lhs = ltn; return; }; 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). // #106: an alias-of-array lhs (`type A=[2]int; let g: A = […]`) arrives // as N_TNAME, so the bare N_TARRAY gate skipped it and the over-fill // never fired (silent DATA-truncate). The let path is the one caller // that pre-gates on the declared node's kind (def/return/call-arg pass // it straight to the alias-aware checkarrlitfits); resolve the alias // here too so an alias-of-array routes through the same over-fill. A // direct N_TARRAY is unchanged (resolvealias is idempotent), and the // early return matches the direct-array branch's pre-existing return. let llhs: *node = resolvealias(c, unwrapbang(n.lhs)); if (llhs != nil && llhs.kind == nkind.N_TARRAY && n.rhs.kind == nkind.N_ARRLIT) { checkarrlitfits(c, n.lhs, n.rhs); return; }; // #20: array-typed TUPLE element with an overlong array literal — // `let t:([2]int,i32) = ([1,2,3],5)` was silently accepted (the tuple // position wasn't wired to checkarrlitfits, unlike the direct-array // let above). #26: the walk now lives in checktuplearrfits, which also // recurses into a nested-tuple element (checkarrlitfits chases aliases // #106, recurses nested arrays #251; the helper no-ops on non-array, // non-tuple elements). Mirror cstage's tuple element-wise reject. No // early return — the rest of checkletassign still runs for the tuple. // N_TTUPLE elements wrap their type on .lhs (N_TPARAM chain, // stamptuplebinds:311); the N_TUPLE rhs values chain directly on .list. if (llhs != nil && llhs.kind == nkind.N_TTUPLE && n.rhs.kind == nkind.N_TUPLE) { checktuplearrfits(c, llhs, n.rhs); }; // #25/#31: an array literal initialising a SLICE local. Re-stamp the // literal as [count]T (the slice element) so the #258 borrow's exact- // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the // declared element width. Run the same per-element coercion + range- // check the array path runs (checkarrlitfits against a synthesized // [count]T), then drive isassignable + the borrow off [count]T. Twin of // cstage arrlit_init_fits' slice arm. Local-only (c.cur != c.top): the // borrow runs at runtime; module-level slice-from-arrlit stays #32. if (c.cur != c.top && n.lhs.kind == nkind.N_TSLICE && n.rhs.kind == nkind.N_ARRLIT) { let cnt: u64 = 0u64; let e0: *node = n.rhs.list; for (e0 != nil) { let skip: bool = false; if (e0.kind == nkind.N_FIELD) { if (streq(e0.str, "...")) { skip = true; }; }; if (!skip) { cnt += 1u64; }; e0 = e0.next; }; let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0); cn.uval = cnt; let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0); arr.lhs = n.lhs.lhs; // declared slice element type arr.rhs = cn; checkarrlitfits(c, arr, n.rhs); n.rhs.type_ = tinfofornode(c, arr): *void; // #31: stash the [count]T tnode on the arrlit (arrlit.lhs is free // — the parser sets only .list) so the cgslice N_ARRLIT-base arm // can size the backing NODE-wise via elemsizeofc(base.lhs). wwstage // narrow-primitive tinfos are unsized (i32/u8 .size==0, #8), so the // element width must come from the type NODE, not the tinfo. n.rhs.lhs = arr; src = arr; }; // #29: an un-suffixed rune literal narrowing into an integer let target // (`let b: u8 = 'a'`). Override src to the integer target so isassignable // accepts, mirroring cstage's coarse untyped_rune rule. See coercerunelit. let runet: *node = coercerunelit(c, n.rhs, n.lhs); if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, n.lhs, src, &conf); // #206: direct `&fn` → `*alias` / `(*alias | void)` slot. if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; }; if (!conf) { return; }; if (!ok) { errnotassign(c, n.lhs, src, "let"); }; // #5/#60: reject boxing an array payload into a tagged variant. cerr // alone does NOT fail the build — bump c.errs (errnotassign:4245 idiom). if (taggedarrayvariantctor(c, n.lhs, src)) { cerr("array-typed tagged-union variant construction unwired — reject (task #5 / #60)\n"); c.errs += 1; return; }; // #258: `let s: []T = arr` borrows the array as a full slice. The // desugar lowers to a runtime `arr[0:len]` N_SLICE, so it only // applies to LOCAL lets (a fn body executes the borrow). A MODULE- // level let is static data with no runtime to run the borrow — its // rhs must stay the raw N_ARRLIT so cgen can materialize it as DATA // (#18). cstage splits this by checker: clet (the desugar site, // cmd/wcc/check.c:1993) runs only from cstmt (local), while module- // level lets are checked in check_file pass-2 (check.c:2549) which // never desugars. wwstage runs ONE checkletassign for both (pass-2 // @checkfile + resolvewalk post-order), so mirror cstage's split // here: skip at module scope (c.cur == c.top, the same module-scope // test as L184). Keeps the #130 module-level assignability check // above intact. if (c.cur != c.top) { n.rhs = desugararrayslice(c, n.lhs, src, n.rhs); }; }; 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; }; // #12: overlong array-lit RETURN — `fn f() [2]int = { return [1,2,3]; }`. // #9 wired the over-fill at DECL only; the return position was lenient. // Fire BEFORE the isassignable/conf guards below — a direct [N]T return // type drives isassignable to conf=false (array-length leniency), which // would short-circuit the check (the alias path set conf=true, so neg3 // caught it but the direct neg0 slipped). Alias-aware via the // resolvealias chase at the top of checkarrlitfits. if (n.lhs.kind == nkind.N_ARRLIT) { checkarrlitfits(c, c.fnret, n.lhs); }; // #25: array-typed element of a TUPLE RETURN with an overlong array // literal — `fn f() ([2]int,i32) = { return ([1,2,3],5); }`. #20 // wired the tuple over-fill walk at the LET position only; the // return path runs through checkretassign which never routed tuple // elements through checkarrlitfits. Reuse the #26 helper. Fire // BEFORE the isassignable/conf guards (a tuple return drives // conf=false → short-circuit), same reason as the #12 array arm // above. Alias/!-aware on the fnret tuple type. let rrt: *node = resolvealias(c, unwrapbang(c.fnret)); if (rrt != nil && rrt.kind == nkind.N_TTUPLE && n.lhs.kind == nkind.N_TUPLE) { checktuplearrfits(c, rrt, n.lhs); }; let src: *node = exprtype(c, n.lhs, nil); if (src == nil) { return; }; // #29: a rune literal returned into an integer (or integer-variant) // fnret (`return '\\';` into u8 or (u8 | star | ...)). Override src so // isassignable accepts; cgen boxes via the shape fallback. See // coercerunelit. Removes the fnmatch.ww:126 `'\\': u8` workaround cast. let runet: *node = coercerunelit(c, n.lhs, c.fnret); if (runet != nil) { src = runet; }; let conf: bool = false; let ok: bool = isassignable(c, c.fnret, src, &conf); // #206: direct `&fn` returned into a `*alias` / `(*alias | void)`. if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; }; if (!conf) { return; }; if (!ok) { errnotassign(c, c.fnret, src, "return"); }; // #5/#60: reject returning an array payload into a tagged variant. cerr // alone does NOT fail the build — bump c.errs (errnotassign:4245 idiom). if (taggedarrayvariantctor(c, c.fnret, src)) { cerr("array-typed tagged-union variant construction unwired — reject (task #5 / #60)\n"); c.errs += 1; return; }; // #258: `return arr` borrows the array as a full slice. // #31/#33: bare array-literal has no backing — loud-reject // (supported only at a `let`). if (!rejectarrlitborrow(c, c.fnret, n.lhs)) { n.lhs = desugararrayslice(c, c.fnret, src, n.lhs); }; }; // ---- 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); // F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr // types the ?/! result as the success variant and the non-tagged // gate below then rejects (check.c "is on non-tagged-union"). // scruttype only resolves IDENT/DOT, so the direct try-form slipped // through the lenient-miss contract and wwstage silently ACCEPTED // (cs≠ww). Resolve the try-result here; a tagged success (named // union variant) flows on into the variant checks, matching // cstage's accept. if (st == nil && n.lhs != nil) { if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) { st = exprtype(c, n.lhs, nil); }; }; 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) { cerr("is/as: operand is not a tagged union\n"); c.errs += 1; return; }; let want: *node = n.rhs; if (want == nil) { return; }; // #198: route through tinfo.params (the #61a-flattened chain built // at L1789-1845 N_TTAGGED) — the prior AST u.list walk via // casevariantin false-rejects every variant that arrives via a // `...inner` spread. Mirrors cstage cmd/wcc/check.c:1662-1675 // u->params + type_eq, and matches cgen's own #179 cgmatch / #66 // Phase-N flatvariantidxt lookup (the SSoT cgtagvariantidx already // keys off at cgenexpr.ww:155). project_tinfo_lossy_nominal: name- // keying was the pre-Phase-N workaround for tinfo lossy on nominal // identity; typeeq inside flatvariantidxt now handles NAMED ptr-id. let utinfo: *tinfo = tinfofornode(c, u); let wanttinfo: *tinfo = tinfofornode(c, want); if (utinfo != nil) { if (wanttinfo != nil) { // exact-only (#95-c3): is/as acceptance is nominal variant // membership; the cgen tag-synthesis chain/structural arms must // not widen acceptance here, nor surface the >=2 cgen fatal // during check (rule-10, #107). casevariantin below keeps the // #198 spread fallback. if (flatvariantidxt(utinfo, wanttinfo, true) >= 0) { return; }; }; }; if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; cerr("is/as: not a variant of operand"); if (want.kind == nkind.N_TNAME) { cerr(" ("); cerr(want.str); cerr(")"); }; cerr("\n"); 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 or nkind.N_TRYUNW (the F8 cardinality gate covers // both; the subset walk is ?-only); 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) { // #11a: curmod preference (the #53/#55 family). A bare ident // whose leaf also names a global in a later module otherwise // binds the foreign decl's type, mis-typing the try operand and // either spuriously rejecting valid `?` code or skipping the F8 // reject. Mirrors exprtype's own N_IDENT arm (scopelookupprefer // at :2897); cstage types the operand via cexpr with cur_mod. let s: *sym = scopelookupprefer(c.cur, c.curmod, 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; }; // #11b/task #51: an N_DOT callee `mod.f()?` is looked up by BARE // LEAF here, NOT via the module qualifier (callee.lhs) the way // exprtype's own N_CALL arm does (:3095 scopelookupinmodule) — a // cross-module same-leaf `f` misbinds. The mechanism fix (module- // keyed callee resolution + the deref-callee `(*fp)()?` shape that // returns nil below) rides task #51 with the fn-ptr-callee desugar. if (callee.kind == nkind.N_DOT) { nm = callee.str; }; if (nm.len == 0) { return nil; }; // #11a: curmod preference for the bare-leaf callee. A same-leaf // `op()?` declared in a later module otherwise binds the foreign // op's return type — its error subset then spuriously fails (or // wrongly passes) the enclosing-return check. Mirrors exprtype's // N_CALL arm (scopelookupprefer at :3336). The N_DOT-callee leaf // (callee.str above) still resolves bare-leaf, not via the module // qualifier callee.lhs.str — that module-keyed fix rides task #51. let s: *sym = scopelookupprefer(c.cur, c.curmod, nm); if (s == nil) { return nil; }; if (s.skind != skind.SK_FN) { return nil; }; if (s.decl == nil) { return nil; }; return s.decl.lhs; }; // task #51: a deref callee `(*fp)()?` / SK_VAR fn-ptr ident / struct- // field fn call returns nil here, so checktryprop silently bails and // the F8 multi-success reject is skipped for those shapes. The fix // (type-keyed operand resolution: peel TPTR → TFN.ret) is mechanism, // filed with the fn-ptr-callee desugar as task #51 — NOT a checker gate. return nil; }; // trycountvariants — #38/F3 (review item 8): walk a tagged union's variant // list FLATTENING `...inner` spreads, accumulating the success count and the // has-error flag. cstage counts/checks the resolve_type-FLATTENED u->params // (cmd/wcc/check.c:2160-2165 over the TK_ELLIPSIS-spliced chain); ww's // checktryprop walked the raw AST, so a `(...inner | e)` counted the spread as // ONE success and the F8 multi-success gate never fired — the exact silent // accept the gate exists to catch (a valid bool success then treated as error // by the single-tag-compare cgen). iserror is judged against the OUTER union // (`outer`) throughout — taggedhaserr(outer) holds because the spread's sibling // or a spliced member carries the error, so each flattened member routes // through varianterr, matching cstage's per-param iserror. Mirrors the #209 // spread recursion tinfofornode already runs over vu.params (check.ww:2275). fn trycountvariants(c: *checker, outer: *node, v: *node, nsuccp: *int, haserrp: *bool, depth: i32) void = { for (v != nil) { if (v.op == tkind.TK_ELLIPSIS && depth < 8i32) { let inner: *node = resolvealias(c, unwrapbang(v)); if (inner != nil && inner.kind == nkind.N_TTAGGED) { trycountvariants(c, outer, inner.list, nsuccp, haserrp, depth + 1i32); v = v.next; continue; }; }; if (iserrvariant(c, outer, v)) { *haserrp = true; } else { *nsuccp += 1; }; v = v.next; }; }; 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; }; // F8 interim gate (task #5): try-propagation assumes ONE success // member end-to-end — exprtype collapses to the first non-error // variant and cgen emits a single tag compare, so any OTHER // success member is silently mistaken for an error (? propagates // it; ! aborts on it). One class, both ops (#133 precedent). // Until the honest subset-union result typing lands (task #14, // harec check.c:2759-2835), reject loud. Mirrors cstage check.c // N_TRYPROP/N_TRYUNW. let haserr: bool = false; let nsucc: int = 0; // #38/F3 (review item 8): flatten `...inner` spreads so the F8 // multi-success gate counts the spliced members, not the spread alias. trycountvariants(c, u, u.list, &nsucc, &haserr, 0i32); if (nsucc > 1) { if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); }; cerr(": multi-success union unwired (task #14): bind and match instead\n"); c.errs += 1; return; }; // `!` has no propagation, so no error-subset check (mirrors // cstage's N_TRYPROP-only guard on the subset walk). if (n.kind != nkind.N_TRYPROP) { return; }; 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) { cerr("?: enclosing fn has no tagged-union return\n"); c.errs += 1; return; }; if (r.kind != nkind.N_TTAGGED) { cerr("?: enclosing fn return is not tagged\n"); 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(c, rv, ev)) { found = true; rv = nil; } else { rv = rv.next; }; }; if (!found) { cerr("?: error variant not in enclosing return\n"); 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) { cerr("asserttyped: "); let kn: str = nkname(k); cerr(kn); cerr(" "); if (n.file.len > 0) { cerr(n.file); cerr(":"); let ls: str = strconv.i32tos(n.line, strconv.base.DEC); cerr(ls); }; if (n.str.len > 0) { cerr(" '"); cerr(n.str); cerr("'"); }; cerr("\n"); 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.istest = 0i32; // #15: caller (w6c main) sets it after init c.verbose = 0; c.fnret = nil; let empty: str; c.curmod = empty; c.file = nil; c.allococtx = 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; }; // #15 @test harness — under `w6c_ww -T`, synthesize the entry the // driver would otherwise hand-wire. We sit at the seam between // fn-install (Pass 1, all names now in scope so the synth callees // resolve) and fn-body-resolve (Pass 2 below, which stamps the // appended entry for free). Mirrors harec's checker-side is_test // work — keep @test fns + suppress/own the hosted main // (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is // untouched: the appended N_FNDECL rides cgfn, byte-id by // construction (rule 10; cstage twin at cmd/wcc/check.c). // // #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec // §a): instead of straight-line `foo(); bar();` calls (which abort the // whole run on the first failing @test — old D3), synthesize a value // table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to // the lib/test runner. The runner forks per test and reads the child's // wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the // run proceeds (lib/test/run.ww). Table = harec __test_array reduced to // a value table (D1 no section; D2 real symbols). RETAINED reductions // (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter // (source/collection order; fnmatch is #17 commit-3); D5 no reflective // file:line (the runner prints `name ... ok/FAIL` + a count summary). // Table rides cgen's #117 slice-of-tuple-global path; `run` resolves // bare against the auto-bundled lib/test (synth runs post-pass-1, so // run sits in the same flat "" bucket as the @test fns). if (c.istest != 0) { let pf: str = file.file; let pl: i32 = file.line; let pc: i32 = file.col; // (b) the synth entry OWNS `main` — loud-reject a user one. let u: *node = file.list; for (u != nil) { if (u.kind == nkind.N_FNDECL && u.body != nil && streq(u.str, "main")) { cerr(u.file); cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n"); c.errs += 1; }; // #24(b): the synth table OWNS `__wwtests` — loud-reject a user // decl of that name (mirror the `main` reservation; cstage // cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to // match run()'s `[](str, *fn()void)` param slips the general // call-arg check (a) but still silently shadows the synth table, // so the synth `run(__wwtests)` iterates the user's table, not // the collected @tests — reserve the NAME so the collision is // loud regardless of type. Any decl kind (const/let/fn). if (streq(u.str, "__wwtests")) { cerr(u.file); cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n"); c.errs += 1; }; u = u.next; }; // (c) collect @test fns in file.list order; build one table row // `("", &)` per validated @test fn. let rhead: *node = nil; let rtail: *node = nil; let ntest: i32 = 0; let t: *node = file.list; for (t != nil) { if (t.kind == nkind.N_FNDECL) { let istest: bool = false; let at: *node = t.attr; for (at != nil) { if (at.kind == nkind.N_ATTR && streq(at.str, "test")) { istest = true; }; at = at.next; }; if (istest) { // `fn f() void` parses the explicit void // into t.lhs, so void-returning is lhs==nil // OR an N_TNAME "void". let retvoid: bool = t.lhs == nil || (t.lhs.kind == nkind.N_TNAME && streq(t.lhs.str, "void")); if (t.list != nil || !retvoid) { cerr(t.file); cerr(": error: @test fn '"); cerr(t.str); cerr("' must be fn() void\n"); c.errs += 1; } else { let nm: *node = newnode(nkind.N_STRLIT, pf, pl, pc); nm.str = t.str; let id: *node = newnode(nkind.N_IDENT, pf, pl, pc); id.str = t.str; let amp: *node = newnode(nkind.N_UN, pf, pl, pc); amp.op = tkind.TK_AMP; amp.lhs = id; let row: *node = newnode(nkind.N_TUPLE, pf, pl, pc); row.list = nm; nm.next = amp; if (rhead == nil) { rhead = row; } else { rtail.next = row; }; rtail = row; ntest += 1i32; }; }; }; t = t.next; }; let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc); let tab: *node = nil; if (ntest == 0i32) { // no @test fns in this unit — exit 0, nothing to run. let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc); let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc); zero.uval = 0u64; ret.lhs = zero; body.list = ret; } else { // const __wwtests: [](str, *fn() void) = [rows...]; // wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308). let e0: *node = newnode(nkind.N_TNAME, pf, pl, pc); e0.str = "str"; let p0: *node = newnode(nkind.N_TPARAM, pf, pl, pc); p0.lhs = e0; let vret: *node = newnode(nkind.N_TNAME, pf, pl, pc); vret.str = "void"; let vfn: *node = newnode(nkind.N_TFN, pf, pl, pc); vfn.lhs = vret; let e1: *node = newnode(nkind.N_TPTR, pf, pl, pc); e1.lhs = vfn; let p1: *node = newnode(nkind.N_TPARAM, pf, pl, pc); p1.lhs = e1; let tup: *node = newnode(nkind.N_TTUPLE, pf, pl, pc); tup.list = p0; p0.next = p1; let tsl: *node = newnode(nkind.N_TSLICE, pf, pl, pc); tsl.lhs = tup; let arr: *node = newnode(nkind.N_ARRLIT, pf, pl, pc); arr.list = rhead; tab = newnode(nkind.N_LET, pf, pl, pc); tab.op = tkind.TK_CONST; tab.str = "__wwtests"; tab.lhs = tsl; tab.rhs = arr; // pass 1 already ran; install the table name now so main // resolves it (declmod returns "" for tab.nmod=""). Goes // direct to scopedefineinmodule, NOT installdecl: cstage's // synth install (cmd/wcc/check.c:3079) bypasses the // duplicate-decl reject (#23) the same way, so a pathological // user `const __wwtests` stays a downstream type error in // both stages rather than a dup-diagnostic in only one. scopedefineinmodule(c.top, tab.str, "", skind.SK_VAR, nil, tab); let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc); arg.str = "__wwtests"; let call: *node = newnode(nkind.N_CALL, pf, pl, pc); let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc); cid.str = "run"; call.lhs = cid; call.list = arg; let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc); ret.lhs = call; body.list = ret; }; let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc); m.str = "main"; m.exported = 1i32; let rety: *node = newnode(nkind.N_TNAME, pf, pl, pc); rety.str = "i32"; m.lhs = rety; m.body = body; // Append the table const (if any) then main to file.list tail. No // type_ pre-set on main: installdecl never stamps a fn's type_ on // the ww side — cgfn reads lhs/list on demand. Pass-2 below // resolve-walks the appended bodies. let tl: *node = file.list; if (tl == nil) { if (tab != nil) { file.list = tab; tab.next = m; } else { file.list = m; }; } else { for (tl.next != nil) { tl = tl.next; }; if (tab != nil) { tl.next = tab; tab.next = m; } else { tl.next = m; }; }; }; // 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; switch (k) { case nkind.N_FNDECL: if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); case nkind.N_DEF: // #11: a module-level `def xs: [_]T = arrlit;` must infer // its length BEFORE resolvewalk stamps d.lhs's tinfo, the // def twin of the N_LET arm below. #7 wired only the let // path, so the def path silently stayed length 0 (no DATA, // garbage indexed reads). inferarraylen mutates the N_TARRAY // length child in place (the SSoT all reads resolve through), // so no Sym re-point is needed on this side. inferarraylen(c, d); if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; // #251: `def D: [N]T = [int/rune lits]` array-init // accept-if-fits. The wwstage def path otherwise runs NO // init-assignability check (cstage check.c:2424 does), so // an out-of-range / str element silently over-accepted // (rule-7). Same predicate as the let path; scoped to the // array-init shape (a no-op for every other def). if (d.lhs != nil && d.rhs != nil) { checkarrlitfits(c, d.lhs, 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); }; }; }; case nkind.N_TYPEDECL: if (d.lhs != nil) { resolvewalk(c, d.lhs); }; case nkind.N_LET: // #7: a module-level `let xs: [_]T = arrlit;` must infer // its length BEFORE resolvewalk stamps d.lhs's tinfo — // otherwise the array tinfo caches the alen=0 sentinel and // the patched length child never reaches the size/data // reads. Idempotent with the checkletassign call below. inferarraylen(c, d); 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); // #133: const-fold a const-EXPR rhs (N_BIN / // unary-over-N_BIN / def-ref) to a literal so cgen's // literal-only emitletdataw lays the DATA row + // defaultinferredlets recognises it, mirroring the // N_DEF arm above. GATED on the plain literal fold // missing first (existing literal/unary-literal lets // keep their node, byte-identical) AND the const-fold // succeeding (a str/struct/slice/call/runtime-operand // rhs returns false silently and is left untouched). // Stamp LAST — checkletassign consumes the pre-stamp type. if (d.rhs != nil) { let dv: u64 = 0u64; if (!foldintliteral(d.rhs, &dv)) { if (evaldefconst(c, d.rhs, &dv, 0)) { stampintlit(d.rhs, dv); }; }; }; }; 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; }; // #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully // checked above (pass 2 + pass 3 walked it like every fn) but is NOT // emitted in a non-test build. harec skips append_decl for // FN_TEST && !is_test, so the fn never reaches the codegen decl list; // the body is still checked, only the emission is dropped. ww shares // one file.list across check + cgen (no separate checked-decl list), // so we splice the already-checked @test fns out here, after all // passes — they stay checked, never reach cgen. The -T path is // untouched: its synth main calls the @test fns, so they must remain. // Twin: cmd/wcc/check.c. if (c.istest == 0) { let prev: *node = nil; let e: *node = file.list; for (e != nil) { let istest: bool = false; if (e.kind == nkind.N_FNDECL) { let at: *node = e.attr; for (at != nil) { if (at.kind == nkind.N_ATTR && streq(at.str, "test")) { istest = true; }; at = at.next; }; }; let nx: *node = e.next; if (istest) { if (prev == nil) { file.list = nx; } else { prev.next = nx; }; } else { prev = e; }; e = nx; }; }; let empty: str; c.curmod = empty; }; // io — stream interface (Plan 9 Bio / Hare io::stream shape). // // No closures, no methods. A `stream` is a pointer to a `vtable` of // function pointers (lib/io/stream.ww); read/write/close dispatch // through it. The error channel is the return value — Hare-shaped // tagged unions instead of errno-style integer sentinels. // // This file owns the eof / underread variant tags; lib/io/stream.ww // owns the `vtable` + `stream` + read/write/close dispatchers and the // [[empty]] singleton, and lib/io/types.ww owns the error union, // mode/whence enums, and the reader/writer/closer fn-type aliases. // #94 fold-eFinal collapsed the pre-vtable `stream` struct + `closed` // tag into the single vtable surface; the dispatchers are read/write/ // close (over `stream`), final over `handle` at io fold-2 (#5). package io; // 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. Lifts to `done` with #93. export type eof = 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; // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the // pre-vtable `stream` struct + `closed` tag). Exports: // // vtable a struct of optional fn-pointer slots — reader/writer/ // closer per ref/hare/io/stream.ha:36-42. Hare spells the // nullable as `nullable *T`; ww parser rejects that // spelling (#192), so each slot is a `(*T | void)` tagged // union (Plan-9-lean per the Nullable kept ruling; opt-in // at optionality boundaries only). // stream `*vtable` alias matching Hare's `stream = *vtable` // (ref/hare/io/stream.ha:33). // read / // write / // close the three dispatchers per ref/hare/io/stream.ha:44-68. // Each `match`es the matching vtable slot, returns // `errors.unsupported` (widened twice) when the slot is // void, otherwise calls through the fn pointer. Hare keeps // these private (`st_read` …) behind a `handle`-typed // public `read`/`write`/`close`; ww has no `handle` yet // (io fold-2, #5), so the dispatchers ARE the public // surface and grow the `handle` match when #5 lands. // empty the discard+EOF stream (ref/hare/io/empty.ha:13). Lives // here rather than io.ww so that 900_stdlib can compile // io.ww standalone (io.ww has no cross-file type refs). // // Deferrals (drew-signed): `seeker` lands with io fold-2 (#5) once `off` // + `whence` plug into the signature. Hare's `?`-propagating `close` // body (`c(s)?;`) collapses to a direct `return (*c)(s);` here because // the dispatcher's surface stays Hare-shaped; only the internal // try-prop is omitted (#173 history, now closed — kept direct for the // void-arm fall-through shape). package io; // Q2 layering (ratified): the file-arm of the handle dispatchers routes // to os.{read,write,close,lseek}; ww `os` plays Hare's `sys` role, so // `lib/io import os` is the correct direction (os is the import floor). import os; // ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser // rejects `nullable *T`); each slot is `(*T | void)`. The `seeker` slot // (4th) lands with io fold-2 (#5); `copier` stays deferred. export type vtable = struct { reader: (*reader | void), writer: (*writer | void), closer: (*closer | void), seeker: (*seeker | void), }; // ref/hare/io/stream.ha:33. export type stream = *vtable; // ref/hare/io/stream.ha:44-51. Private stream-arm body behind the // handle-typed [[read]] (#5). The void-arm `return e;` chains two // direct widens: `errors.unsupported → error` via #199 α, then // `error → (size | eof | error)` via #205. The call-arm spells the // fn-ptr call explicitly per #193 (ww requires `(*r)(args)` rather // than implicit `r(args)`). fn st_read(s: stream, buf: []u8) (size | eof | error) = { match (s.reader) { case void => { let u: errors.unsupported; let e: error = u; return e; }; case let r: *reader => return (*r)(s, buf); }; }; // ref/hare/io/stream.ha:53-60. fn st_write(s: stream, buf: []u8) (size | error) = { match (s.writer) { case void => { let u: errors.unsupported; let e: error = u; return e; }; case let w: *writer => return (*w)(s, buf); }; }; // ref/hare/io/stream.ha:62-68. Hare propagates the close error via // `c(s)?;` then falls through to the void arm; ww collapses to a // direct return for the void-arm fall-through shape. The surface // (void | error) matches. fn st_close(s: stream) (void | error) = { match (s.closer) { case void => return void; case let c: *closer => return (*c)(s); }; }; // ref/hare/io/stream.ha:70-77. Clone of st_read's slot-dispatch shape // over the new `seeker` vtable slot. fn st_seek(s: stream, off: off, w: whence) (off | error) = { match (s.seeker) { case void => { let u: errors.unsupported; let e: error = u; return e; }; case let sk: *seeker => return (*sk)(s, off, w); }; }; // ref/hare/io/handle.ha:16-23. The handle-typed public read: the // file-arm routes to os.read (Q2 layering), the stream-arm delegates to // the unchanged st_read vtable dispatch above. export fn read(h: handle, buf: []u8) (size | eof | error) = { match (h) { case let fd: file => { let r: i64 = os.read(fd, buf.ptr, buf.len: u64); if (r < 0) { // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 let u: errors.unsupported; let e: error = u; return e; }; if (r == 0) { let z: eof; return z; }; return r: size; }; case let s: stream => return st_read(s, buf); }; }; // ref/hare/io/handle.ha:27-36. export fn write(h: handle, buf: []u8) (size | error) = { match (h) { case let fd: file => { let r: i64 = os.write(fd, buf.ptr, buf.len: u64); if (r < 0) { // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 let u: errors.unsupported; let e: error = u; return e; }; return r: size; }; case let s: stream => return st_write(s, buf); }; }; // ref/hare/io/handle.ha:44-51. Hare propagates each arm's error via `?` // then falls through; ww returns directly (matching st_close's shape). export fn close(h: handle) (void | error) = { match (h) { case let fd: file => { let r: i32 = os.close(fd); if (r < 0) { // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 let u: errors.unsupported; let e: error = u; return e; }; return void; }; case let s: stream => return st_close(s); }; }; // ref/hare/io/handle.ha:55-62. The io.whence → os.whence cast bridges // the two enum copies (same SET/CUR/END values); os.lseek owns the // syscall. export fn seek(h: handle, off: off, w: whence) (off | error) = { match (h) { case let fd: file => { let r: i64 = os.lseek(fd, off, w: os.whence); if (r < 0) { // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 let u: errors.unsupported; let e: error = u; return e; }; return r: off; }; case let s: stream => return st_seek(s, off, w); }; }; // ref/hare/io/handle.ha:65-67. export fn tell(h: handle) (off | error) = { return seek(h, 0, whence.CUR); }; // ---- empty stream ------------------------------------------------------- // // ref/hare/io/empty.ha:4-17. // // Hare uses `const _empty_vt: vtable = { ... }` + `const empty: *stream`. // ww can't const-init a vtable struct with fn-ptr fields (#118), so the // vtable is a module-level `let` and [[empty]] is a function that wires // the fn-ptr slots on every call and returns the stream pointer. // Single-assignment on the same words: idempotent under re-entry. // Lives in stream.ww (not io.ww) so that 900_stdlib can compile io.ww // standalone without referencing the cross-file vtable/reader/writer types. fn _empty_read(s: stream, buf: []u8) (size | eof | error) = { let e: eof; return e; }; fn _empty_write(s: stream, buf: []u8) (size | error) = { return buf.len: size; }; let _empty_vt: vtable; // empty — a stream that discards all writes (returning their size) and // returns EOF on every read. Mirrors ref/hare/io/empty.ha:13. // #118: const vtable init with fn-ptr fields is unwired (emit_struct_data // needs a two-pass reloc extension, node_fnptr_sym reusable). Using a // mutable let + per-call wiring until #118 lands. export fn empty() stream = { _empty_vt.reader = (&_empty_read): *reader; _empty_vt.writer = (&_empty_write): *writer; return &_empty_vt; }; // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` // composes with every other module's error surface at the tag level. // Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, // ...) live in their own modules; this module ships the generic // conditions Hare's errors:: exports plus the [[errno]] bridge that // wraps a raw [[os.errno]] into a portable [[error]]. package errors; import os; // The named conditions, ordered per ref/hare/errors/common.ha. // The requested resource is not available. export type busy = !void; // An attempt was made to create a resource which already exists. export type exists = !void; // A function was called with an invalid combination of arguments. export type invalid = !void; // The user does not have permission to use this resource. export type noaccess = !void; // An entry was requested which does not exist. export type noentry = !void; // The requested operation caused a numeric overflow condition. export type overflow = !void; // The requested operation is not supported. export type unsupported = !void; // The requested operation timed out. export type timeout = !void; // The requested operation was cancelled. export type cancelled = !void; // A connection attempt was refused. export type refused = !void; // An operation was interrupted. export type interrupted = !void; // The user should attempt an operation again. export type again = !void; // Network unreachable export type netunreachable = !void; // Up to 24 bytes of arbitrary, 8-byte-aligned storage for the opaque // error type's domain-specific data. ref/hare/errors/opaque.ha:41. export type opaque_data = [3]u64; // An "opaque" error wraps an implementation-specific underlying error // behind a function that stringifies it plus a small storage area. // ref/hare/errors/opaque.ha:32. The `strerror` field keeps Hare's `*fn` // pointer (filled via `(&fn): *fn(...)`, mirroring io's `*reader` slot, // lib/io/types.ww:53); only Hare's `const` is dropped (ww has none, cf // lib/io/types.ww:57). export type opaque_ = !struct { strerror: *fn(op: *opaque_data) str, data: opaque_data, }; // A tagged union of all error types. ref/hare/errors/common.ha:43. // Enumerated explicitly rather than spread via Hare's `...error` — // ww's spread-flatten layout is the deferred #199b / #204 fix. export type error = !( busy | exists | invalid | noaccess | noentry | overflow | unsupported | timeout | cancelled | refused | interrupted | again | netunreachable | opaque_ ); // Wraps an [[os.errno]] to produce an [[error]], which may be // [[opaque_]]. ref/hare/errors/rt.ha:9. The mapped errnos become named // conditions; the unmapped tail is carried in an [[opaque_]] whose // stringifier defers to [[os.strerror]]. export fn errno(e: os.errno) error = { // A `!void` condition is returned by instance, not by name: a bare // `return refused;` would emit an undefined symbol reference (the // type, not a value). cf lib/io/stream.ww:53-56. The instance // auto-widens to the [[error]] union on return. switch (e) { case os.ECONNREFUSED: { let r: refused; return r; }; case os.ECANCELED: { let r: cancelled; return r; }; case os.EOVERFLOW: { let r: overflow; return r; }; case os.EACCES: { let r: noaccess; return r; }; case os.EINVAL: { let r: invalid; return r; }; case os.EEXIST: { let r: exists; return r; }; case os.ENOENT: { let r: noentry; return r; }; case os.ETIMEDOUT: { let r: timeout; return r; }; case os.EBUSY: { let r: busy; return r; }; case os.EINTR: { let r: interrupted; return r; }; case os.EAGAIN: { let r: again; return r; }; case os.ENETUNREACH: { let r: netunreachable; return r; }; }; // An unmapped errno falls through the switch into the opaque_ wrap // below. ww switches aren't required exhaustive, so Hare's terminal // `case => void;` (rt.ha:24, present only to satisfy exhaustiveness) // is dropped rather than written as an explicit no-op default — // matching the sibling [[os.strerror]] fall-through. // // ww has no `static assert`; Hare guards size(errno) <= // size(opaque_data) there. The invariant holds structurally: // opaque_data is [3]u64 (24B), errno is one machine int. let err: opaque_; err.strerror = (&rt_strerror): *fn(op: *opaque_data) str; let ptr = (&err.data): *os.errno; *ptr = e; return err; }; // rt_strerror — the [[opaque_]] stringifier for an errno wrapped by // [[errno]]. ref/hare/errors/rt.ha:31. fn rt_strerror(op: *opaque_data) str = { let e = (op): *os.errno; return os.strerror(*e); }; // types — error union, mode/whence enums, reader/writer/closer // fn-type aliases. Project #94 fold-eFinal; the fn-aliases target // `stream` (= `*vtable`, the single io surface). // // Hare splits the io module across stream.ha + types.ha and ww does // the same: lib/io/io.ww owns the `eof` and `underread` tags; // lib/io/stream.ww owns the vtable surface (`vtable`, `stream`, // `read`/`write`/`close` dispatch); this file owns the surrounding // port. The three files share `package io;` so cross-file refs // resolve via the dir-enum concat order (io.ww < stream.ww < types.ww // — `stream` lands before the reader/writer/closer aliases below). // // Deferrals (drew-signed): `copier`, `strerror` (the #5 list's // `handle` + `seeker` landed with the #5 arc and the memio-seeker // port). Hare's `EOF = done` stays as `io.eof` until the `done` // singleton ships (#93). package io; import errors; // ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's // `READ | WRITE` because the ww parser doesn't fold expressions in // enum value positions; the value SSoT stays the same bitfield. export type mode = enum u8 { NONE = 0, READ = 1, WRITE = 2, RDWR = 3, }; // ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; // ww requires one. i32 matches the off type that fold-e2 will plug // into the seeker signature. export type whence = enum i32 { SET = 0, CUR = 1, END = 2, }; // ref/hare/io/+linux/platform_file.ha:13. Hare's `file = int`; on // linux/x86_64 Hare's `int` is 32-bit, but ww's `int` is an 8B machine // word, so a literal `int` here would be width-wrong for a fd. i32 is // width-faithful to Hare's real fd width (USER-ruled 2026-05-31). export type file = i32; // ref/hare/io/arch+x86_64.ha:6. ABI-compatible with POSIX off_t. export type off = i64; // ref/hare/io/handle.ha:12. Hare spells it `(file | *stream)`; ww's // `stream` is already `*vtable` (the #94 collapse absorbed Hare's // `*stream` indirection), so the faithful ww payload is `(file | // stream)`, NOT a literal `*stream` which would be a double-pointer. export type handle = (file | stream); // ref/hare/io/types.ha:11 spreads `...errors::error` into the union; // ww enumerates the tags explicitly (ken's #204-block — spread of an // errors-side tagged into this union triggers a wrapper-vs-flatten // layout mismatch; #199b layout-extension is the deferred fix). The // read/write dispatchers in stream.ww return `errors.unsupported` // when the matching vtable slot is unset, so the variant lands here // directly. `errors.invalid` rides the memio-seeker port (Hare's // memio seek returns errors::invalid on out-of-bounds, // ref/hare/memio/stream.ha:134-136); appended last so existing // member tags stay put. export type error = !(errors.unsupported | underread | nomem | errors.invalid); // ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww // rationale; lifts to `done` with #93. `stream` forward-refs the // `*vtable` alias in stream.ww — the dispatcher receives the vtable // pointer directly (Hare's stream.ha:33 + types.ha:46 collapse). export type reader = fn(s: stream, buf: []u8) (size | eof | error); // ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and // lib/sort/sort.ww:18 drops it the same way. export type writer = fn(s: stream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. export type closer = fn(s: stream) (void | error); // ref/hare/io/types.ha:76. Hare's `fn(s: *stream, off: off, w: whence)`; // ww's `stream` is already `*vtable` (the #94 collapse), so the param is // `stream` directly, mirroring the reader/writer/closer aliases above. export type seeker = fn(s: stream, off: off, w: whence) (off | error); // memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a // single [[io.stream]] (= `*io.vtable`): // // fixed caller owns the buffer, writes stop when full. // dynamic memio owns the buffer, writes grow it; close frees. // // Hare's memio::fixed/dynamic/dynamic_from return a `stream` whose // FIRST field IS the `io::stream` (= `*vtable`). ww mirrors that // intrusively: `stream`'s first field is `vt: io.vtable` (the vtable // embedded INLINE) so a stack `stream` is castable to // `io.stream = *vtable` via `&s.vt` — and the callbacks recover the // outer `stream` by casting the dispatch arg back to `*stream`. Same // intrusive shape as lib/bufio + lib/log over their embedded vtables. // // let s: memio.stream = memio.fixed(buf); // io.write(&s.vt, bytes); // let view: str = memio.string(&s); // // Constructors return the `stream` BY VALUE (Hare shape, // ref/hare/memio/stream.ha:46,58,64): build the struct in a local, // field-assign every slot, and `return r;` (the proven sret round-trip // pinned by test/wcc/925 + 776). NO heap, NO `nomem`: the alloc that // forced an earlier heap return is gone, so the constructor cannot // fail. Caller owns the returned `stream` (stack ownership, no-GC) and // passes `&s.vt` to the io dispatchers — exactly Hare's `&s` into // io::write. // // Subset of Hare's surface: io's variants are {eof, error}, 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 — see [[string]]. Hare's copy callback is absent: // io's vtable has no copier slot yet (deferred with `handle`-typed // io.copy, lib/io/stream.ww header). The seeker is wired — see // [[seekfn]]. // // Cast workaround per #206-payoff (ken: KEEP the explicit casts; they // are cgen-neutral and sidestep the #214 over-acceptance surface). The // `(&fn_name): *io.` cast at each store site is the Hare-faithful // minimum-touch route — the #206 cast-drop is a separate deferred // payoff gated on #214. // // ptr/len/cap kept flat (no `buf: []u8`) per a historical cgen note: // chained-dot writes through a state pointer into a slice subfield // miscompile silently (#195 family); the flat shape sidesteps it. // dynamicfrom uses the slice's `cap` (NOT `len`) to track the // allocated-capacity-to-free on close — passing a half-filled append // slice with len < cap and using only `buf.len` would under-free. package memio; import errors; import io; import os; import rt; // stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at // offset 0 for the intrusive stream→io.stream cast (`&s.vt`) and the // callbacks' reverse `s: *stream` cast. Unified across fixed/dynamic // (Hare keeps a single `stream` over per-mode vtable singletons; ww // wires the per-mode callbacks post-construction instead). ptr/len/cap // flat per the header note. export type stream = struct { vt: io.vtable, ptr: *u8, len: i32, cap: i32, pos: i32, }; // fixed — wire a stream over a caller-supplied buffer. Writes never // grow; they return 0 once `pos` reaches the end of the buffer (Hare // returns `nomem` here; ww surfaces 0 — graduating to Hare's `nomem` // return needs the widen-from-bare-nomem path that #173-family work // gates). // // Mirrors ref/hare/memio/stream.ha:46. export fn fixed(buf: []u8) stream = { let r: stream; r.vt.reader = (&readfn): *io.reader; r.vt.writer = (&fixedwrite): *io.writer; r.vt.seeker = (&seekfn): *io.seeker; r.ptr = buf.ptr; r.len = buf.len; r.cap = buf.len; r.pos = 0; return r; }; // dynamic — wire a stream with no initial buffer. Writes grow the // backing allocation; [[io.close]] frees it. // // Mirrors ref/hare/memio/stream.ha:58. export fn dynamic() stream = { let r: stream; r.vt.reader = (&readfn): *io.reader; r.vt.writer = (&dynamicwrite): *io.writer; r.vt.closer = (&dynamicclose): *io.closer; r.vt.seeker = (&seekfn): *io.seeker; r.ptr = nil; r.len = 0; r.cap = 0; r.pos = 0; return r; }; // dynamicfrom — like [[dynamic]] but seeded with an existing slice. // Ownership transfers; close frees `cap` bytes from the slice's // allocated capacity (NOT logical length). // // Mirrors ref/hare/memio/stream.ha:64. export fn dynamicfrom(buf: []u8) stream = { let r: stream; r.vt.reader = (&readfn): *io.reader; r.vt.writer = (&dynamicwrite): *io.writer; r.vt.closer = (&dynamicclose): *io.closer; r.vt.seeker = (&seekfn): *io.seeker; r.ptr = buf.ptr; r.len = buf.len; r.cap = buf.cap; r.pos = 0; return r; }; // ---- vtable callbacks ---------------------------------------------------- // readfn — recover the stream from the io.stream's `*vtable` via the // intrusive offset-0 cast. Single fn over the common header (Hare's // single `read` at ref/hare/memio/stream.ha:103); fixed and dynamic // share it because the read path is buffer-flavour-agnostic. fn readfn(s: io.stream, buf: []u8) (size | io.eof | io.error) = { let m: *stream = s: *stream; 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: size; }; // seekfn — SET/CUR/END cursor reposition over the common header; // fixed and dynamic share it (Hare wires the same `seek` into both // vtables, ref/hare/memio/stream.ha:26,34). // // Mirrors ref/hare/memio/stream.ha:122-140. len(s.buf) → m.len; // `pos` is i32 while io.off is i64, so the arithmetic runs in i64 and // narrows only after the bounds check. Hare's two-sided check works // in unsigned `size` with a negation dance; the signed i64 spelling // here is the same predicate without it. fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = { let m: *stream = s: *stream; // cstage binop typing is nominal (no alias peel: `off` vs i64 // rejected, wwstage accepts — ww-core #54), so the arithmetic // runs on an i64 copy. let n: i64 = off: i64; let start: i64 = 0; switch (w) { case io.whence.SET: start = 0; case io.whence.CUR: start = m.pos: i64; case io.whence.END: start = m.len: i64; }; if (n < 0) { if (start < -n) { let v: errors.invalid; let e: io.error = v; return e; }; } else { if ((m.len: i64) - start < n) { let v: errors.invalid; let e: io.error = v; return e; }; }; m.pos = (start + n): i32; return m.pos: io.off; }; fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; if (m.pos >= m.len) { return 0: size; }; 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: size; }; fn dynamicwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; 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: size; }; fn dynamicclose(s: io.stream) (void | io.error) = { let m: *stream = s: *stream; 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; }; // Double-and-copy growth with floor at 8. `dynamicgrow`, not Hare's // bare `grow`: cstage bundles all imported modules into a flat TU and // resolves private fns by unqualified name (task #9), so the // module-prefixed name keeps the symmetry with dynamicwrite/dynamicclose. fn dynamicgrow(d: *stream, need: i32) void = { let newcap: i32 = d.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 < d.len) { nbuf[i] = d.ptr[i]; i += 1; }; if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); }; d.ptr = nbuf; d.cap = newcap; }; // ---- accessors over the common `stream` header ----------------------- // // Single fn each: Hare's string/reset/buffer/borrowedread all take // `*stream` and read the flat header. // string — bytes written so far, as a str view (buf[0..pos]). // // Mirrors ref/hare/memio/stream.ha:81 string(in: *stream). Hare returns // (str | utf8::invalid) — the validating constructor. ww returns a bare // `str` per the CLAUDE.md rule-9 frombytes carve-out: utf8.validate at // the IO source is opt-in, never wrapped per-construction; the honest // name reserves a future validating helper. export fn string(s: *stream) str = { let r: str; r.ptr = s.ptr; r.len = s.pos; return r; }; // buffer — borrowed []u8 view of bytes written so far (buf[0..pos]). // // Mirrors ref/hare/memio/stream.ha:74 buffer(in: *stream). export fn buffer(s: *stream) []u8 = { let r: []u8; r.ptr = s.ptr; r.len = s.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. // // Mirrors ref/hare/memio/stream.ha:87 reset(in: *stream). export fn reset(s: *stream) void = { s.pos = 0; s.len = 0; }; // borrowedread — return an `amt`-byte view starting at `pos` without // copying, advancing the cursor. eof if fewer bytes are available. // // Mirrors ref/hare/memio/stream.ha:94 borrowedread(st: *stream, amt). // `amt: i32` (not Hare's `size`) per the i32-index convention. export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { if (s.len - s.pos < amt) { let e: io.eof; return e; }; let r: []u8; r.ptr = s.ptr + (s.pos: u64); r.len = amt; s.pos += amt; return r; }; // 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); }; // ---- 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. // // #38b: cgcall walks the list TWICE — memphase=true first, staging // every MEMORY-class (>48B tagged) arg below all register-class // words, then memphase=false for the register classes. Each phase // skips the other's args; the return value counts only own-phase // slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per // ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). // argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is // widened to when passed to a tagged-union PARAM, or 0 when no register-class // widen happens: param not tagged / not a fixed param, an already-matching- // slot tagged source (natural push), the nullable 8B fold (handled by the // scalar path), or a >48B memory-class slot (staged below the register words). // This is the SSoT the cgcall DRAIN consults for its widen-first POP count; // it MUST agree word-for-word with pushargsrev's widen-PUSH count below // (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain // desyncs — the #30/#48 root was a drain with no widen branch: the widened // box's words were under-drained (#30: a following float read the leftover // payload word) or the float-source box was misclassified as a float arg // (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed // widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall). fn argtaggedwidensz(c: *cgen, arg0: *node, param: *node) i32 = { if (param == nil) { return 0; }; if (param.kind != nkind.N_PARAM) { return 0; }; if (param.op == tkind.TK_ELLIPSIS) { return 0; }; let ptype: *node = param.lhs; if (ptype == nil) { return 0; }; if (!istaggedtype(c, ptype)) { return 0; }; // >48B slot is memory-class: pushargsrev stages it below the register // words and the drain skips it (dmemsz) — never a register widen. if (taggedmemargsize(ptype.type_: *tinfo) > 0) { return 0; }; let arg: *node = taggedcastpeel(c, arg0); let pslot: i32 = slotsize(c, ptype); // Already a matching-slot tagged source → natural push, no widen // (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref). if (arg.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, arg.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { if (slotsize(c, lc.tnode) == pslot) { return 0; }; }; }; }; if (taggedcallslot(c, arg) == pslot) { return 0; }; if (arg.kind == nkind.N_INDEX) { if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; }; if (arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; }; if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; }; // The 8B nullable fold pushes one pointer word the scalar drain path // already pops correctly; only the multi-word tagged widen needs the // drain's dedicated branch. if (pslot < 16) { return 0; }; return pslot; }; fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg == nil) { return 0; }; let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase); // Family C (#35): peel tagged→tagged casts FIRST so every gate // below keys on the operand — an identity cast reduces to the // ident fast path, a widening cast trips the widen branch with // the operand as source. cgexpr on the cast node collapses to // one word (silent word0 push pre-#35). Mirrors cstage's // args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall // pop side counts via pushargsrev's return, so the drain stays // balanced. arg = taggedcastpeel(c, arg); // #38b MEMORY-class detection: keyed off the declared param's // type (so widening into a >48B slot is caught), else the arg's // own stamped type (fn-ptr callee carries no param nodes). let memsz: i32 = 0; let memptype: *node = nil; if (param != nil) { if (param.kind == nkind.N_PARAM) { if (param.op != tkind.TK_ELLIPSIS) { memptype = param.lhs; if (memptype != nil) { memsz = taggedmemargsize(memptype.type_: *tinfo); }; }; }; }; if (memsz == 0) { memsz = taggedmemargsize(arg.type_: *tinfo); }; if (memphase != (memsz > 0)) { return rest; }; if (memsz > 0) { // same-type check — mirror cstage's `(pu == au) || // type_eq(p->type, at)` widen detection. let same: bool = false; let at: *tinfo = arg.type_: *tinfo; if (memptype != nil) { let pt: *tinfo = memptype.type_: *tinfo; let pu: *tinfo = pt; pu = tichase(pu); let au: *tinfo = at; au = tichase(au); if (pu != nil && pu == au) { same = true; }; if (!same && pt != nil && at != nil) { if (typeeq(pt, at)) { same = true; }; }; } else { same = true; }; if (!same) { // Widen via the @tagscr scratch for EVERY source // shape — the direct-push fast arms below stage // exactly 4 words, short of the memsz/8 the drain // accounts for (mirrors cstage cg_widen_tagged_push // dst_is_mem routing). let scroff: i32 = tagscradd(c, memsz); emitline("\tXORQ\tAX, AX\n"); let zz: i32 = 0; for (zz < memsz) { emitline("\tMOVQ\tAX, "); emitoff((scroff + zz): i64); emitline("(BP)\n"); zz += 8; }; cgwidentaggedstore(c, memptype.type_: *tinfo, arg, "BP", scroff, memsz); let pp: i32 = memsz - 8; for (pp >= 0) { emitline("\tMOVQ\t"); emitoff((scroff + pp): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); pp -= 8; }; return rest + memsz / 8; }; // Exact type: raw slot words high→low from the value's // address (local slot, or any aggargsrcaddr-addressable // source: global let, N_DOT chain, array index, deref). // An exact-type CALL source is sret-class (>32B tagged // return) — its result is in memory behind a dest pointer, // not a register cursor; receive-then-push is the // #40-family follow-up. if (arg.kind == nkind.N_CALL) { let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n"; os.write(2, mc.ptr, mc.len: u64); os.exit(1); }; if (arg.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, arg.str); if (lc != nil) { let w: i32 = memsz / 8 - 1; for (w >= 0) { emitline("\tMOVQ\t"); emitoff((lc.off + w*8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); w -= 1; }; return rest + memsz / 8; }; }; if (aggargsrcaddr(c, arg, "SI")) { let w: i32 = memsz / 8 - 1; for (w >= 0) { emitline("\tMOVQ\t"); emitoff((w*8): i64); emitline("(SI), AX\n"); emitline("\tPUSHQ\tAX\n"); w -= 1; }; return rest + memsz / 8; }; // #40/FB3: a place the enumerated arms miss — slice // element, deref-spine element — resolves through the F6 // resolver. AFTER aggargsrcaddr so every pre-#40 shape // keeps its asm; the resolver balances its own pushes, so // the words already staged below stay put. if (cgplaceaddr(c, arg, "SI")) { let w: i32 = memsz / 8 - 1; for (w >= 0) { emitline("\tMOVQ\t"); emitoff((w*8): i64); emitline("(SI), AX\n"); emitline("\tPUSHQ\tAX\n"); w -= 1; }; return rest + memsz / 8; }; let mu: str = "#38b: >48B tagged arg from unsupported source kind (rvalue and unresolvable-place sources unwired)\n"; os.write(2, mu.ptr, mu.len: u64); os.exit(1); }; // 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) { // #55: only treat a tagged ident as // "already tagged" (natural push, no remap) // when its slot MATCHES the param. On slot- // DIFFER the source is a NARROWER union widened // into a wider one — fall to the widen scratch // + tag-remap below (the slot-gated INDEX/DOT/ // STAR arms' twin). Pre-#55 the ungated TRUE // natural-pushed the narrower box's words with // no remap (aligned-tag LUCK, misaligned-tag // wrong). cstage routes every tagged source // through cg_widen_tagged_store (cmd/w6c/ // cgen.c:2982 src_is_tagged). if (istaggedtype(c, lc.tnode)) { if (slotsize(c, lc.tnode) == slotsize(c, ptype)) { aistagged = true; }; }; }; }; // #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; }; }; }; // #22a: t.N tuple-element read leaves the // same AX/DX/CX/R8 box cursor (this arc's // t.N box load) — without this gate the // widening scalar branch clamps the // unresolvable tag to 0 and the callee // reads variant 0. Stamped-carrier (#67) // twin of the N_INDEX arm above; cstage // needs no kind gate (its widen[i] `same` // check is type-keyed on args[i]->type). if (arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { if (slotsize(c, arg) == slotsize(c, ptype)) { aistagged = true; }; }; }; // Family C (#35): a DEREF source is a // tagged box too (mem-based, any size) // — without this gate the widening // scalar branch boxed the box. Same // slotsize key as the N_INDEX/N_DOT // stamped-carrier arms above. if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { 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. // #66: a tuple-typed source has no direct-push shape — the // scalar fast arm would push word 0 only (payload slot 1+ // dropped) and coerce an unresolved tag to 0. Route through // the scratch store, whose #242/#66 tuple arm handles the // literal/cast forms and loud-stops the rest (#72). Mirrors // cstage cg_widen_tagged_push src_is_tuple. let argtup: *tinfo = arg.type_: *tinfo; argtup = tichase(argtup); let argistuple: bool = false; // #55: a tagged SOURCE widened into a wider/reordered tagged param // (slot-DIFFER — the ident/deref/index/dot legs that fell past the // slot-gated aistagged arms above) has no direct-push shape: the // scalar fast arm below would box word0 with a tag clamped to 0, // dropping the real tag + high words and skipping the variant // remap. Route through the @tagscr store, whose ident/memread/ // cursor arms copy the box + tag-remap. Mirrors cstage // cg_widen_tagged_push's unconditional src_is_tagged scratch // routing (cmd/w6c/cgen.c:2982). let argistagged: bool = false; if (argtup != nil) { if (argtup.kind == tykind.TY_TUPLE) { argistuple = true; }; if (argtup.kind == tykind.TY_TAGGED) { argistagged = true; }; }; let pname: str = rhsstructpayload(c, arg); if (pname.len > 0 || argistuple || argistagged) { let ptype: *node = param.lhs; let scroff: i32 = tagscradd(c, widensz); 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; }; // #49: an f64/f32 payload sits in X0 (cgexpr left it // there), not AX — spill it through the stack so the // callee reads the real bits. A plain PUSHQ AX pushed // whatever AX last held (stale for a runtime float // producer; only a const folder leaves the bits in AX // — why #48 with a no-payload-read arm passed but #49 // reading `d == 2.5` did not). Both stages (#263); // cstage cg_widen_tagged_push twin. if (isfloattype(c, arg)) { emitline("\tSUBQ\t$8, SP\n"); let fmov: str = "MOVSD"; if (isf32type(c, arg)) { fmov = "MOVSS"; }; emitline("\t"); emitline(fmov); emitline("\tX0, (SP)\n"); } else { 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; }; }; }; }; // #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call // arg) carries no tnode — resolve esz / base-address from the // checker-stamped element tinfo on base.type_ instead. Cstage // twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror // of the cgslice #252 site. let dotbu: *tinfo = nil; if (base != nil) { if (base.kind == nkind.N_DOT) { dotbu = base.type_: *tinfo; dotbu = tichase(dotbu); };}; // #60 (alias arc #5): alias-NAMED N_IDENT base — the tnode // reads below see only the N_TNAME leaf (esz 1-sentinel, // MOVQ base). Re-key off the chased stamped tinfo, the // pusharg twin of the cgslice fix (cstage pushargs N_SLICE // reads bu = type_chase_named(base->type) uniformly). let basealias: bool = false; let bu60: *tinfo = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { basealias = true; bu60 = tichase(bt60); };}; };}; // esz from the type table for an N_IDENT base (#76; mirrors // the cgindex idiom) or an N_DOT array/slice-field base (#257: // scale by the field's element width, not esz=1 -> silently // wrong for non-u8). Other non-ident bases stay esz=1. // (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects // at the checker, supported only at a `let`; #33.) let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); } else { if (dotbu != nil && dotbu.sub != nil) { esz = dotbu.sub.size: i32; };};}; if (bu60 != nil) { let es60: *tinfo = tichase(bu60.sub); if (es60 != nil) { esz = es60.size: i32; }; }; // base address → push if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarr60: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarr60 = true; }; }; // #60: alias-NAMED base — chased kind (see cgindex twin). if (bu60 != nil) { isarr60 = bu60.kind == tykind.TY_ARRAY; }; if (isarr60) { 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) { let gisarr60: bool = globaltn.kind == nkind.N_TARRAY; // #60: alias-NAMED base — chased kind; runtime- // unreachable until #77/#78 global DATA. if (bu60 != nil) { gisarr60 = bu60.kind == tykind.TY_ARRAY; }; if (gisarr60) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); }; } else { if (dotbaseaddr(c, base, "AX")) { // #257: N_DOT `[N]T`-field base as a call arg → field // ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr // emits. Same choke-point as the cgslice #252 site; // `[]T`/str/`*T` fields fall through to cgexpr. } 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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); } else { // #56: def/const dim — resolve from the stamped // array tinfo (rule-13). The #60 bu60 arm below // covers only NAMED-alias bases (tn.kind == // N_TNAME); a plain `[MAX]u8` base reaches here // with a non-N_INTLIT dim and dropped the // default-hi entirely. cstage reads bu->alen. let abt: *tinfo = tichase(tn.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); 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"); }; };};}; }; // #60: alias-NAMED base default-hi — chased kind // (see the cgslice twin). if (bu60 != nil) { if (bu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(bu60.alen: i64); emitline(", AX\n"); } else { if (bu60.kind == tykind.TY_SLICE || bu60.kind == tykind.TY_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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); } else { // #56: def/const dim on a global array base — // resolve from the stamped array tinfo (rule-13), // twin of the local arg-push arm above. let abt: *tinfo = tichase(globaltn.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); 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 { if (globaltn.kind == nkind.N_TNAME) { if (streq(globaltn.str, "str")) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), CX\n"); emitline("\tMOVQ\t8(CX), AX\n"); }; };};}; // #60: alias-NAMED global base default-hi — chased // kind; runtime-unreachable until #77/#78 global DATA. if (bu60 != nil) { if (bu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(bu60.alen: i64); emitline(", AX\n"); } else { if (bu60.kind == tykind.TY_SLICE || bu60.kind == tykind.TY_STR) { 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; }; }; // #150: a module-global by-value struct arg. localfindnode // above misses it (a global is not a frame slot), and the #271 // arm below excludes a ≤16B struct ident (structident=true), so // pre-fix it fell through to the scalar single-PUSHQ default and // silently dropped word1. cstage's mirror-opposite bug read the // FRAME (localfind→0 off==0 footgun); both stages converge here // on the global-base load: LEAQ name(SB) into BX, then copy ALL // eightbytes high→low (the #256/#129-A.2 struct-global shape; the // isletvar||deflookup gate is the let_islet||def_isstructdef twin // from dotchainaddr). The slot-word count is the stamped tinfo // size (the type table, byte-id with cstage struct_arg_size). if (lc == nil) { let gst: *tinfo = arg.type_: *tinfo; gst = tichase(gst); if (gst != nil) { if (gst.kind == tykind.TY_STRUCT) { let gsz: i32 = gst.size: i32; if (gsz > 0 && gsz <= 16 && (isletvar(c, nm) || deflookup(c, nm))) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), BX\n"); if (gsz > 8) { emitline("\tMOVQ\t8(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); let gnw: i32 = 1; if (gsz > 8) { gnw = 2; }; return rest + gnw; }; }; }; // #151: a module-global by-value slice/str arg — the // slice/str twin of the #150 struct arm above. wwstage's // nodeisslice/nodeisstr (below) are LOCAL-keyed // (localfindnode→nil for a global) so a global slice/str // ident returned false there and fell to the scalar // single-PUSHQ default, dropping len+cap. cstage is // type-keyed (node_isslice/node_isstr on n->type) so it // pushed all 3 header words. cstage emits a DIFFERENT // per-type sequence — mirror EACH for byte-id: a slice via // its dedicated push arm (BX-base per-word, cgen.c:9124); a // str falls to cgexpr's cgslicehdr (CX-base AX/BX/CX, // cgen.c:1866) then the node_isstr triple push. // LET-only (NOT deflookup, unlike the #150 struct arm): // cstage's slice arm gates let_islet (cgen.c:1869) and a // def-str is const-folded by cgexpr (LEAQ _S_0, MOVQ // $len) — it has no name(SB) holder. A deflookup here // would LEAQ an undefined main.(SB) (w6l fails); a // def must fall through to the const-fold path instead. if (gst != nil && isletvar(c, nm)) { if (gst.kind == tykind.TY_SLICE) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), BX\n"); emitline("\tMOVQ\t16(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t8(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); return rest + 3; }; if (gst.kind == tykind.TY_STR) { 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"); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); return rest + 3; }; }; }; }; // #271: aggregate (struct/array) arg from any source the ≤16B // struct-IDENT fast path above doesn't cover — a 16B struct from a // non-ident source, OR any array, OR a struct > 16B. The arg twin // of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push // arm): materialise the source ADDRESS in SI and push its ceil(sz/8) // words high→low (the pop drains word0 into the first arg reg). A // CALL source receives first — ≤24B in AX/DX/CX pushed straight, // >24B sret'd into @aggargscr then pushed from there. Pre-fix every // such source fell to the scalar default (one PUSHQ for a multi-word // aggregate) and stack-imbalanced against the type-based drain. let aggsz: i32 = aggargsizetn(arg.type_: *tinfo); if (aggsz > 0) { // Exclude a ≤16B-struct IDENT — it owns the structparamsize // fast path above (or, when a cross-module same-leaf collision // makes the name-keyed structparamsize miss it, the scalar // default below, byte-id with cstage's 1-word struct push; // #784/#223). The exclusion is TYPE-keyed via the stamped // tinfo, mirroring cstage node_isstructarg (struct_arg_size on // args[i]->type) — a name-keyed gate here re-opens the #211/#13 // name-keyed divergence the cstage type gate doesn't have. let structident: bool = false; if (arg.kind == nkind.N_IDENT) { let st: *tinfo = arg.type_: *tinfo; st = tichase(st); if (st != nil) { if (st.kind == tykind.TY_STRUCT) { if (st.size: i32 <= 16) { structident = true; }; }; }; }; if (!structident) { if (aggargfloatstop(arg)) { let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let nwords: i32 = (aggsz + 7) / 8; if (arg.kind == nkind.N_CALL) { if (callsretsize(c, arg) > 0) { let scr: i32 = localadd(c, "@aggargscr", aggsz, nil); c.sretdestoff = scr; cgexpr(c, arg); c.sretdestoff = 0; let k: i32 = nwords - 1; for (k >= 0) { emitline("\tMOVQ\t"); emitoff((scr + k*8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); k -= 1; }; } else { // ≤24B: producer left AX=word0, // DX=word1, CX=word2. Push high→low so // the pop drains word0 first. cgexpr(c, arg); let k: i32 = nwords - 1; for (k >= 0) { if (k == 2) { emitline("\tPUSHQ\tCX\n"); } else { if (k == 1) { emitline("\tPUSHQ\tDX\n"); } else { emitline("\tPUSHQ\tAX\n"); }; }; k -= 1; }; }; return rest + nwords; }; if (!aggargsrcaddr(c, arg, "SI")) { let msg: str = "#271: aggregate arg from unsupported source kind\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let k: i32 = nwords - 1; for (k >= 0) { emitline("\tMOVQ\t"); emitoff((k*8): i64); emitline("(SI), AX\n"); emitline("\tPUSHQ\tAX\n"); k -= 1; }; return rest + nwords; }; }; // Float arg: cgexpr leaves the value in X0. Push 8 bytes from // X0 via SUBQ+MOVSD so cgcall's pop side can drain into the // XMM stream (X0..X7). f32 still occupies 8B on the stack — // the MOVSS load on the pop side touches only the low 4. let fk: i32 = 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; }; // #68: a tuple-LITERAL arg derives its DECLARED tuple type from the // callee PARAM type node (the #57 decl wire, extended to call-arg // send), so a declared-tagged element's concrete rvalue widens into // the box cursor; the restage + push then key on the param tuple type // node (declared element widths), not the literal's element- // constructed types. Mirrors cstage cgcall paramtup. let paramtt: *node = nil; if (arg.kind == nkind.N_TUPLE) { if (param != nil) { if (param.kind == nkind.N_PARAM && param.lhs != nil) { let ptn: *node = param.lhs; for (ptn != nil && ptn.kind == nkind.N_TNAME) { ptn = aliaslookup(c, ptn.str); }; if (ptn != nil) { if (ptn.kind == nkind.N_TTUPLE) { paramtt = ptn; }; }; }; }; }; if (paramtt != nil) { cgtuplelittocursor(c, arg, paramtt); } else { cgexpr(c, arg); }; // #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr / // the decl-aware fill left the tuple in the return-ABI cursor (AX/DX/ // CX/R8 + X0/X1) for every nodetuplearg producer — call (return ABI), // ident (cgtupleslottocursor), literal (cgtuplelittocursor), `?`/`!` // unwrap (payload shift); 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. When the PARAM tuple type is known (#68), walk its // declared element type nodes (p.lhs); else an N_TUPLE literal's // elements are VALUE exprs classified the way cgtuplelittocursor does. let tuparg: *node = paramtt; if (tuparg == nil) { tuparg = nodetuplearg(c, arg); }; if (tuparg != nil) { let tuplit: bool = false; if (paramtt == nil) { if (tuparg.kind == nkind.N_TUPLE) { tuplit = true; }; }; 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; if (tuplit) { et = p; }; // C-t2 (ken demand 1, rule 7): a COMPOSITE element // (nested tuple/struct/array/tagged) occupies more // than the one GP word this walk counts — the checker // accepts the shape but the cursor transport cannot // carry it; pre-guard it ran WRONG (inner words // skewed). The stamped tinfo classifies both type // nodes and literal value exprs. Mirrors the cstage // restage guard. let eti: *tinfo = et.type_: *tinfo; eti = tichase(eti); if (eti != nil) { let bad: bool = eti.kind == tykind.TY_TUPLE || eti.kind == tykind.TY_STRUCT || eti.kind == tykind.TY_ARRAY; // #68: a declared-tagged element graduates to a real // widen — the decl-aware send (cgtuplelittocursor over // the param tuple type) left the box words in the // cursor, so tupstore below carries them. A tagged // element with no param decl stays rule-7 loud (the // cursor was filled stamped-keyed). if (eti.kind == tykind.TY_TAGGED && paramtt == nil) { bad = true; }; if (bad) { let mne: str = "#32: tuple arg element kind unsupported (nested tuple/struct/array/tagged; rule 7)\n"; os.write(2, mne.ptr, mne.len: u64); os.exit(1); }; }; // eslot — the full slot stride (str/slice header, tagged // box, else 8); on the declared (non-tuplit) path tupeslotn // reads the element TYPE node directly (#68 box-aware, // mirrors cstage tuple_eslot). A literal element rides the // wide-vs-scalar split off its VALUE node. let eslot: i32 = 8; if (tuplit) { let wide: bool = nodeisstr(c, et) || nodeisslice(c, et); if (wide) { eslot = tyslicesize(): i32; }; } else { eslot = tupeslotn(et); }; if (isfloattype(c, et)) { sstot += 1; } else { gptot += eslot / 8; }; tsz += eslot; p = p.next; }; // The producing cursor fill already satisfied #164's caps; // guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]). if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) { 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; if (tuplit) { et = p; }; let eslot: i32 = 8; if (tuplit) { let wide: bool = nodeisstr(c, et) || nodeisslice(c, et); if (wide) { eslot = tyslicesize(): i32; }; } else { eslot = tupeslotn(et); }; tupstore(c, gpcur, ssecur, scr + eoff, eslot, et); if (isfloattype(c, et)) { ssecur += 1; } else { gpcur += eslot / 8; }; eoff += eslot; 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; }; // #32 (C-t2, rule 7): a tuple-typed arg from a source shape whose // cgexpr does NOT fill the return cursor (chain reads, match exprs, // ...) must die loud here — pre-fix it fell to the scalar // single-PUSHQ default and silently skewed every later arg // register. Mirrors the cstage cgcall guard. { let ati: *tinfo = arg.type_: *tinfo; ati = tichase(ati); if (ati != nil) { if (ati.kind == tykind.TY_TUPLE) { let m32: str = "#32: tuple arg from unsupported source shape (call/ident/literal/unwrap only; rule 7)\n"; os.write(2, m32.ptr, m32.len: u64); os.exit(1); }; }; }; 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) { // #38b residual (rule 7): an sret-class call result is in // memory, not the cursor — the @aggargscr-style receive-then- // push is the #40-family follow-up. Mirrors cstage cgen.c // cgcall tagged arg-push gate. if (callsretsize(c, arg) > 0) { let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n"; os.write(2, m38r.ptr, m38r.len: u64); os.exit(1); }; 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. // #22a: N_DOT rides the same arm — the t.N tuple-element box load // (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin // is the generic node_istaggedarg push (cgen.c cgcall); wwstage // keeps the stamped-carrier kind gate (#67 pattern) — the // remaining kinds (deref/cast/unwrap) are word0-only reads today, // filed residual. if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT || (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR)) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); // #35 (Family C): a mem-based read left the box // ADDRESS in AX — push the words from memory // high→low, the mem twin of the cursor push below. // Covers the any-size deref source and the 33-48B // INDEX/DOT reads that loud-stopped here pre-#35. // Mirrors cstage. if (taggedmemread(c, arg)) { let mk35: i32 = isz - 8; for (mk35 >= 0) { emitline("\tMOVQ\t"); emitdispreg(mk35: i64, "AX"); emitline(", DX\n"); emitline("\tPUSHQ\tDX\n"); mk35 -= 8; }; return rest + isz / 8; }; 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); // F7-c1: the local read collapses onto the checker-stamped // n.type_, the same shape cstage's node_isslice uses // (cmd/w6c/cgen.c:182 type_isslice(n->type)). lc.tnode.type_ // (what isslicetype read) and n.type_ are the same tinfo for a // local ident (exprtype N_IDENT stamps n.type_ off the same // decl localfindnode keys on); the localfindnode gate stays to // keep the global-var-not-def case routing to false (out of F7 // scope), so this is a zero-delta mechanic validation. if (lc != nil) { return typeisslice(n.type_: *tinfo); }; // #21: a module-level `def g: []T` ident is not a frame // slot (localfindnode→nil), so the local arm above misses // it and it would fall to the scalar single-PUSHQ default, // dropping len/cap. cstage's node_isslice is type-keyed // (cmd/w6c/cgen.c:226-229 type_isslice(n->type)); align // wwstage UP by reading the checker-stamped .type_ (the def // decl's str/slice type node, check.ww exprtype N_IDENT // arm). A def has no DATA symbol — it rides cgident's // const-fold, so the push site and cgcall's pop sizer flip // together on this shared recognizer (load-bearing). if (deflookup(c, nm)) { return typeisslice(n.type_: *tinfo); }; 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); }; // #45 (F7-c2): `arr[i]` whose element is a slice. cgindex leaves // (AX=ptr, BX=len, CX=cap) for a 24B element, but with no N_INDEX // arm here pushargsrev fell to the scalar default — one PUSHQ AX — // and the cgcall pop under-drained by 2 words (take(rows[1]) pushed // 1 word, callee read garbage len). Read the checker-stamped element // type (exprtype N_INDEX stamps n.type_, check.ww:2777), mirroring // cstage node_isslice = type_isslice(n->type) and the N_INDEX arms of // nodeisstr (below) + nodeisunsigned (:1798). if (k == nkind.N_INDEX) { 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). // // The value-bearing arms (N_IDENT local, N_INDEX, N_DOT) read the // checker-stamped n.type_ — the typed-AST check the prior TODO(#11) // wanted, mirroring cstage node_isstr = type_isstr(n->type). The // remaining N_kind arms (N_STRLIT, N_CALL, N_CAST) carry their own // recognizer because the stamp is on a sub-node (callee return / cast // target), not on `n` itself. Covered: N_STRLIT, N_IDENT (local stamp / // def stamp), N_CALL (return type), N_INDEX (element stamp — #46 F7-c2), // N_DOT (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); // F7-c1: the local read collapses onto the checker-stamped // n.type_, the same shape cstage's node_isstr uses (cmd/w6c/ // cgen.c:213 type_isstr(n->type)). typeisstr chases TY_NAMED so // `!str` aliases (parserr = !str) and `type foo = str;` chains // resolve through exactly as isstrtype(lc.tnode) did — n.type_ // and lc.tnode.type_ are the same tinfo for a local ident // (exprtype N_IDENT stamps n.type_ off the same decl // localfindnode keys on). The localfindnode gate stays to keep // the global-var-not-def case routing to false (out of F7 // scope), so this is a zero-delta mechanic validation. if (lc != nil) { return typeisstr(n.type_: *tinfo); }; // #21: a module-level `def s: str` ident is not a frame slot // (localfindnode→nil), so the local arm above misses it and // it would fall to the scalar single-PUSHQ default, dropping // len/cap. cstage's node_isstr is type-keyed (cmd/w6c/ // cgen.c:213-216 type_isstr(n->type)); align wwstage UP by // reading the checker-stamped .type_ (the def decl's str type // node, check.ww exprtype N_IDENT arm). A def has no DATA // symbol — it rides cgident's const-fold (LEAQ _S_n, MOVQ // $len), so the push site and cgcall's pop sizer flip together // on this shared recognizer (load-bearing). if (deflookup(c, nm)) { return typeisstr(n.type_: *tinfo); }; 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; }; // #46 (F7-c2): `arr[i]` whose element is a str. The prior structural // walk only recognised N_IDENT and N_DOT bases (idxelemtn off the // base's tnode), so a chained / call / slice base (take(m[1][1])) // fell through to `return false` → 1-word push, callee read garbage // .len. Read the checker-stamped element type instead (exprtype // N_INDEX stamps n.type_, check.ww:2777), mirroring cstage node_isstr // = type_isstr(n->type) and nodeisunsigned's N_INDEX arm (:1798). The // base-kind whitelist is gone — every base shape routes through the // one stamp read. if (k == nkind.N_INDEX) { return typeisstr(n.type_: *tinfo); }; // 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; }; // #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. // idxeffti additionally drills `*[N]T` to the pointee array (#61) // so a signed-narrow element behind a pointer-to-array still // sign-extends on load. let ti: *tinfo = idxeffti(t.type_: *tinfo); 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; }; // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). let ti: *tinfo = idxeffti(t.type_: *tinfo); 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; }; // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). let ti: *tinfo = idxeffti(t.type_: *tinfo); 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; }; // tichase — transitive TY_NAMED peel, nil-passthrough. Exact wwstage // twin of cstage type_chase_named (cmd/wcc/type.c:160-162, alias arc // #5): chain-of-aliases stacks TY_NAMED layers, so any single peel // leaves a kind-gated consumer staring at TY_NAMED and falling to a // scalar shape (#60's esz=1/pointer-base SEGV family). One chased // accessor is the only spelled way to dealias; raw `.under` reads // outside it are the lint target (rob F2 ruling). fn tichase(t0: *tinfo) *tinfo = { let t: *tinfo = t0; // peel-ok: chase body for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; return t; }; // 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; u = tichase(u); 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); }; // idxeffti — element-effective tinfo for indexing. `*[N]T` auto-derefs // at an index base, so its esz/element classification must come from // the pointee ARRAY (stride T), not from the pointer (whose .sub is // the whole [N]T — the #61 stride bug, N*size(T) off target per index // step). One peel choke-point: every tinfo-keyed index-element answer // (elemsizeofc / elemissignedc / elemisfloatc / elemisf32c) routes // through here. Mirrors cstage idx_eff (cmd/w6c/cgen.c:1163). fn idxeffti(t0: *tinfo) *tinfo = { let t: *tinfo = t0; t = tichase(t); if (t != nil && t.kind == tykind.TY_PTR) { let p: *tinfo = t.sub; p = tichase(p); if (p != nil && p.kind == tykind.TY_ARRAY) { return p; }; }; return t; }; // idxelemtn — element type-NODE for an indexable base tnode, the // node-keyed companion of idxeffti for the cgen arms that classify // the element structurally (istaggedtype / isstrtype / isslicetype / // isfloattype / tnodestoreop). Same `*[N]T` drill: the pointee array's // OWN element, never the array (#61 — an undrilled elemtn made the // store-width chooser believe the element IS `[N]T` and emit an // N*8-byte aggregate copy from an 8B source: caller-frame smash). // nil for non-indexable kinds (str N_TNAME: callers want nil so // tnodestoreop falls to the u8 byte store). fn idxelemtn(tn: *node) *node = { if (tn == nil) { return nil; }; let k: nkind = tn.kind; if (k != nkind.N_TARRAY && k != nkind.N_TSLICE && k != nkind.N_TPTR) { return nil; }; let elem: *node = tn.lhs; if (k == nkind.N_TPTR && elem != nil && elem.kind == nkind.N_TARRAY) { return elem.lhs; }; return elem; }; // 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. // primsize-ok (#101/#109): elemsizeof is the STRUCTURAL (non- // chasing) sizer by design — its alias-resolving twin elemsizeofc // owns the chase (routed through aliasprimsize at the :1579 leg). // A bare primsize here is correct, not the #101 bug shape. 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; }; // primsize-ok (#101/#109): structural sizer — the chase lives // in elemsizeofc (:1579), not here. See the :1475 leg. 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; }; // #270-2: a NESTED-array element ([N][M]T) — the OUTER index stride // is the WHOLE sub-array [M]T, not the scalar T that elemsizeof // drills down to (the documented elemsizeof FOOTGUN). elemsizeof // returns the inner prim size (4 for [M]u32), so the `direct != 8` // short-circuit below would mis-emit esz=$4 where cstage emits the // sub-array stride $12. Mirror cstage esz = idx_eff(bt)->sub->size // (cmd/w6c/cgen.c:4923): the element-array tinfo's natural size // (sub.size*elen, type.c:121) IS the outer stride. // N_TPTR is excluded (#61): a pointee-array is NOT a nested // element — `p[i]` on `*[N]T` auto-derefs and strides the array's // OWN element (idx_eff peels TY_PTR→TY_ARRAY before the .sub // read). Routing it through this whole-sub-array rule scaled // every index by N*size(T) — the siphash round() corruption. The // `*[N][M]T` outer stride still resolves below via idxeffti // (pointee array's .sub = [M]T, its natural size). let nk: nkind = t.kind; let nest: *node = nil; if (nk == nkind.N_TSLICE) { nest = t.lhs; }; if (nk == nkind.N_TARRAY) { nest = t.lhs; }; if (nest != nil && nest.kind == nkind.N_TARRAY) { let eti: *tinfo = nest.type_: *tinfo; eti = tichase(eti); if (eti != nil) { return eti.size: i32; }; }; // #83/B3: an alias-NAMED INDEXABLE (`type grid = [3]cell`) arrives // as a bare N_TNAME — elemsizeof's name arm knows only str + prims // and answers the 1-sentinel, so the `direct != 8` short-circuit // below returned 1 and every caller strode by one byte (the #60 // esz-1 family, outer-array leg). Answer from the chased stamped // tinfo via idxeffti (which also drills an alias-of-`*[N]T`), // element chased like the #8 leg below. Non-indexable names // (struct/prim/str aliases) fall through to the old paths — // byte-id preserved there. if (nk == nkind.N_TNAME) { let eff: *tinfo = idxeffti(t.type_: *tinfo); if (eff != nil) { if (eff.kind == tykind.TY_ARRAY || eff.kind == tykind.TY_SLICE) { let aes: *tinfo = tichase(eff.sub); if (aes != nil) { return aes.size: i32; }; }; }; }; let direct: i32 = elemsizeof(t); if (direct != 8) { return direct; }; // #8: direct==8 is elemsizeof's "unresolved alias/aggregate" sentinel. // Read the element width off the checker-stamped tinfo, mirroring the // sibling elem*c helpers (elemissignedc :915, elemisfloatc :939, which // already read t.type_.sub) and cstage idx_eff(bt)->sub->size // (cmd/w6c/cgen.c N_INDEX). elemsizeofc was the odd-one-out among the // elem*c family — it derived size purely structurally, so a named-narrow // element (`[N]tkind`, tkind = enum i32) slipped through to a raw 8B slot // instead of its i32 backing (4), wrong-striding both the cgindex READ // and the local-array-init STORE (frame-smash). Peel TY_NAMED on the // indexable and on its element, matching the #270-2 nested-array block // above. Structural slotsize fallback stays for the t.type_==nil case. // idxeffti folds that peel together with the `*[N]T` TY_PTR→ // TY_ARRAY drill (#61) so .sub is the array's element, never the // whole pointee array. let ti: *tinfo = idxeffti(t.type_: *tinfo); if (ti != nil) { let esub: *tinfo = ti.sub; esub = tichase(esub); if (esub != nil) { return esub.size: i32; }; }; let elem: *node = idxelemtn(t); if (elem == nil) { return direct; }; if (elem.kind == nkind.N_TNAME) { let ps: i32 = aliasprimsize(c, 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; // #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped // n.type_, dropping the localfindnode special-case. The prior arm read // the local's declared tnode and returned `false` (signed) for a // module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter // fed to / % >> or a relational got signed IDIV/SAR/JG instead of the // unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/ // cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read // n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M: // the corpus HAS module-global unsigned counters on the divide/shift // path, so the self-compile .s MOVES — every move is toward cstage // (IDIV→DIV where the global's stamp is unsigned) and runtime-correct. if (k == nkind.N_IDENT) { return typeisunsigned(n.type_: *tinfo); }; 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 aliasprimsize(c, tn.str); }; }; }; return 0; }; if (k == nkind.N_CAST) { let tn: *node = n.rhs; if (tn != nil) { if (tn.kind == nkind.N_TNAME) { return aliasprimsize(c, 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; ti = tichase(ti); 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; }; // #92: a struct LITERAL's type ref parses as N_IDENT (expression // position, lib/ww/parse/expr.ww) where type specs parse N_TNAME // — both carry the name in .str. Accept both at the ENTRY only: // iterations past the first walk aliaslookup results, which are // N_TNAME by the loop's own reassignment gate. Consumer census // (commit body): no existing caller can pass N_IDENT. if (tn.kind != nkind.N_TNAME && tn.kind != nkind.N_IDENT) { 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 || cur.kind == nkind.N_IDENT) && 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; }; }; // #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP // eightbytes; a wider slot was silently truncated (payload word // 4+ died in the callee frame). The ≤cap boundary is load-bearing: // (str|nomem)-shaped 32B slots MUST stay register-ABI or every // such consumer in the tree flips. Nullable folds to one word. // Mirrors cstage cg_sret_retsize TY_TAGGED arm. if (istaggedtype(c, r)) { if (isnullabletype(r)) { return 0; }; let tsz38: i32 = slotsize(c, r); if (tsz38 <= TUPLE_GPCAP * 8) { return 0; }; return tsz38; }; if (r.kind == nkind.N_TTUPLE) { // #10: over-cap tuple → sret. Walk the element TYPE nodes // (pt.lhs) over the SAME caps the SEND/receive use; a float = // 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1 // GP word. Return the tuple's natural total size (tinfo.size, // the type table) so the callee returns via sret. Mirrors // cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP // are the shared cap SSoT with the cgreturn SEND emitter. let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; let pt: *node = r.list; for (pt != nil) { let et: *node = pt.lhs; if (isfloattype(c, et)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tupeslotn(et) / 8; }; pt = pt.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { let rti: *tinfo = r.type_: *tinfo; if (rti != nil) { return rti.size: i32; }; }; return 0; }; // #267: arrays ride the struct-return ABI — natural size // (sub.size*len, the type table) gates ≤24 reg / >24 sret, mirroring // cstage cg_sret_retsize TY_ARRAY arm. Pure-int element arrays only; // no float-array-return consumer (structfloatclass stays struct-only). if (r.kind == nkind.N_TARRAY) { let ati: *tinfo = r.type_: *tinfo; ati = tichase(ati); if (ati == nil) { return 0; }; let asz: i32 = ati.size: i32; if (asz <= 24) { return 0; }; return asz; }; if (r.kind != nkind.N_TNAME) { return 0; }; // Primitives / aliased-to-primitives are never sret. if (aliasprimsize(c, r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; // #129: same-module alias wins over any-module struct hit. Without // this, `type stream = *vtable` (io) loses to memio.stream (56B // struct) via structlookup's any-module fallback → spurious sret. // Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name. if (c != nil) { let al: *node = aliassamemod(c, r.str); if (al != nil) { return sretretsize(c, al); }; }; 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); // #129: sretretsize must see the CALLEE's module context so // aliassamemod resolves aliases from the callee's module (not the // caller's). Mirrors cstage operating on resolved Type* objects // (type_chase_named never has this confusion). Swap + restore. let savedmod: str = c.curmod; if (cmod.len > 0) { c.curmod = cmod; }; let r: i32 = sretretsize(c, rtyp); c.curmod = savedmod; return r; }; 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] == '.') { 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; }; // #223: same-module-ONLY struct lookup. structlookup's any-module // fallback returns a foreign same-leaf struct; the cgdot alias-peel // needs to break ONLY on a struct that THIS module defines (a genuine // struct-value receiver), not on a foreign struct that merely shares a // leaf with a same-module alias (io.stream alias vs memio.stream // struct). Returns the struct only when it lives in c.curmod. fn structsamemod(c: *cgen, name: str) *structinfo = { let s: *structinfo = c.structs; for (s != nil) { if (streq(s.sname, name)) { if (streq(s.smod, c.curmod)) { return s; }; }; s = s.sinext; }; return nil; }; // primsize — size in bytes of a primitive type name (or 0 if not // recognised as a primitive — the caller falls back to other paths). // fldnumidx — parse a tuple field name like "0" / "1" / "12" into an // index, or -1 if not all-digits. Used by cgdot to dispatch // `t.0` / `t.1` against an 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; }; // primsize-ok (#101/#109): the primitive-width oracle itself — this // IS the SSoT table aliasprimsize wraps; there is nothing below it to // chase. 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; }; // aliasprimsize — resolved primitive byte width for a type NAME: the // prim width if `nm` is itself a primitive, else chase the alias chain // (aliaslookup) to its bottom and take that prim's width. Returns 0 // when the name doesn't reduce to a width-known primitive (struct / // tagged / `!`/enum-bottom / unresolved). SSoT for the size-use // primsize() family: a bare primsize(name) is alias-blind — a narrow // alias (`type my32 = u32`) returns 0, defaulting the stride/width to // 8 (the #101 struct-fill miscompile: [3]my32 strode 8 not 4, field n // collided with arr[2]). cstage chases my32→u32→4 via type_chase_named // at the twin sites; this is the ww align-up. The bare-primsize GUARD // family (is-primitive dispatch) is the #109 follow-on, NOT routed // here. #101. // primsize-ok (#101/#109): the SSoT chase body itself — primsize is // the leaf-primitive probe this helper wraps, then aliaslookup chases. fn aliasprimsize(c: *cgen, nm: str) i32 = { let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; let cur: *node = aliaslookup(c, nm); for (cur != nil) { if (cur.kind != nkind.N_TNAME) { return 0; }; let p: i32 = primsize(cur.str); if (p > 0) { return p; }; cur = aliaslookup(c, cur.str); }; 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; }; // primsize-ok (#101/#109): this fn IS a prim-resolver // chaser (#33) — primsize is the leaf-primitive probe; // aliaslookup below advances the walk on a miss. 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) { // primsize-ok (#101/#109): a typed-int literal suffix // (`7u32`) is a builtin primitive name by grammar — no // alias can reach here, so there is nothing to chase. 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). typeisint ? size : 0; // bool falls out because typeisint(bool) is false — the same // exclusion the old streq("bool") arm encoded. // B2-c3/B1: the base walk CHASES each hop (was a hand-rolled // 2-peel that ran out at a 3-level alias base or a ptr-to- // 2-level base — clamp kept on a knowable identity cast, // runtime-correct but cs!=ww asm). Aligns up to cstage // castsrcprim post-F1 (type_chase_named at both hops). The // field-u chase is asm-neutral: cs keeps its single peel // there, sound through type_isint's NAMED recursion + the // NAMED tinfo carrying its underlying's size (probe // b1c_fld2lvl byte-id). let bu: *tinfo = nil; if (n.lhs != nil) { bu = n.lhs.type_: *tinfo; }; bu = tichase(bu); if (bu != nil && bu.kind == tykind.TY_PTR) { bu = tichase(bu.sub); }; if (bu != nil && bu.kind == tykind.TY_STRUCT) { let u: *tinfo = n.type_: *tinfo; u = tichase(u); 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 arrays no longer need a slot-size // intercept here: the checker (inferarraylen, check.ww) stamps the // real element count onto the array type's length child before cgen // runs, so slotsize reads it like any explicit `[N]T` (#7). // 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 tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; if (tn == nil) { return 8; }; let s: i32 = slotsize(c, tn); // #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot // to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real // type size. slotsize returns 0 for a void/`done`-aliased scalar // local; localreserve no longer applies a sub-8 floor (it mirrors // cstage's no-floor localslot), so a void slot must be floored here // instead, else its zero width collides with the next local. A // genuine empty struct or [0]T array (also slotsize 0) keeps its 0. if (s == 0) { let ti: *tinfo = tn.type_: *tinfo; if (ti != nil) { ti = tichase(ti); }; if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; }; }; return s; }; // #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. ti = tichase(ti); 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; }; // #75: a struct LOCAL's stack slot is the struct's NATURAL size // rounded up to the 8B slot grain — NOT ti.slotsize, which sums the // slot-padded field widths and over-reserves on sub-8 nested // composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs // cstage $16). cstage reserves the local at f->type->size (cmd/w6c/ // cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same // dual-SSoT leak as #44 (field-offset) / #55, one notion over. The // TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element // stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences. if (kk == tykind.TY_STRUCT) { return ((ti.size + 7u64) & ~7u64): i32; }; if (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. ti = tichase(ti); 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; }; // #44/#55: fi.foff is a VIEW of the checker's already-built NATURAL // `tfield.offset` (check.ww N_TSTRUCT L2234), NOT a second slot-padded // layout recomputed via fieldsize. The two sources diverged iff a struct // had a nested sub-8 composite field (slotsize != size) plus a successor: // the WRITE path used this slot-padded foff, the READ path // (cgplaceaddr/dotbaseaddr) read tfield.offset natural — ww mis-addressed // its own fields. cstage has no structinfo and reads tfield directly // (self-consistently natural); this unifies ww's second source onto it, // preserving cs==ww. Lock-step walk: tstruct.list N_TFIELD AST nodes and // ti.fields tfields share one head-first declared order (both skip // non-TFIELD identically), so they advance in exact step. si.totsize keeps // the slot-padded total (stack-slot allocator's number) from ti.slotsize, // already 8-rounded at check.ww:2259-2261. fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *node) void = { let si: *structinfo = alloc(structinfo{ sname = name, smod = srcmod, })!; if (tstruct.type_ == nil) { let msg: str = "#44/#55: registerstruct: struct node has no stamped tinfo\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let ti: *tinfo = tichase(tstruct.type_: *tinfo); if (ti == nil) { let msg: str = "#44/#55: registerstruct: tichase yielded nil tinfo\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let head: *fieldinfo = nil; let tail: *fieldinfo = nil; let f: *node = tstruct.list; let tf: *tfield = ti.fields; for (f != nil) { if (f.kind == nkind.N_TFIELD) { if (tf == nil) { let msg: str = "#44/#55: registerstruct: AST/tfield walk desync (more TFIELDs than tinfo.fields)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let fi: *fieldinfo = alloc(fieldinfo{ fname = f.str, foff = tf.offset: i32, fsz = tf.type_.size: i32, tnode = f.lhs, })!; if (head == nil) { head = fi; tail = fi; } else { tail.finext = fi; tail = fi; }; tf = tf.tnext; }; f = f.next; }; si.fields = head; si.totsize = ti.slotsize: i32; 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; // #9: an error-struct (`type X = !struct{...}`) carries an // N_TBANG-wrapped body; peel it so X registers like any // struct. cstage is tinfo-based and needs no table, but // wwstage's name-keyed widen dispatch (cgreturn needswiden // → cgwidentaggedstore) resolves struct layout through // c.structs; an unregistered error-struct made the // struct-variant-of-large-union return silently drop its // construction (cs!=ww). The strategic fix is #222's sret // cutover, which deletes this name-keyed dispatch outright; // until then the table must be complete (errors.opaque_ is // the first such type). #10 tracks retiring the table. if (body != nil && body.kind == nkind.N_TBANG) { body = body.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) { // #48: non-ident index base (s.field[i], call()[i], // nested). Only idents carry a declared tnode to walk, // so resolve from the checker-stamped element tinfo // instead — the #45/#67 stamped-carrier pattern; cgmatch // gates on istaggedtype and reads scrutt.type_ directly. // cstage N_MATCH reads s->type for every scrutinee shape // (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and // the variant clamped to 0 + @match_spill mis-sized. if (istaggedtype(c, scrut)) { return scrut; }; 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; }; // idxelemtn: `*[N]T` drills to the pointee array's element (#61). let etn: *node = idxelemtn(btn); 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; }; // Family C (#46): a DEREF scrutinee — stamped-carrier like the // non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant // indices key off scrut.type_; pre-#46 nil here clamped the // variant to 0 and mis-sized @match_spill). if (k == nkind.N_UN && scrut.op == tkind.TK_STAR) { 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 (aliasprimsize(c, 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; }; // aggargsize — byte size of a by-value aggregate (struct OR array) call // arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the // ≤16B-struct structparamsize carve-out doesn't cover: arrays of any // size and structs > 16B. Reads the stamped tinfo size (the type table, // byte-id with cstage Type.size). fn aggargsizetn(t: *tinfo) i32 = { if (t == nil) { return 0; }; let u: *tinfo = t; u = tichase(u); if (u == nil) { return 0; }; if (u.kind == tykind.TY_STRUCT || u.kind == tykind.TY_ARRAY) { return u.size: i32; }; return 0; }; // taggedmemargsize — #38b: a tagged-union arg past the 6-reg register // convention (>48B slot, where the register transport's cap trips) is // MEMORY-class: the caller stages the whole slot on the outgoing stack // below every register-class word and the callee reads it in place at // positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape // per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The // ≤48B register convention is pinned in-tree (test/926 boundary rows). fn taggedmemargsize(t: *tinfo) i32 = { if (t == nil) { return 0; }; let u: *tinfo = t; u = tichase(u); if (u == nil) { return 0; }; if (u.kind != tykind.TY_TAGGED) { return 0; }; if (u.nullable != 0) { return 0; }; // sizelint-ok: 6 SysV int arg regs (DI..R9) x 8B words — the // same register-capacity constant as cstage tagged_arg_size. if (u.size: i32 <= 6 * 8) { return 0; }; return u.size: i32; }; fn nodeisaggarg(n: *node) bool = { if (n == nil) { return false; }; return aggargsizetn(n.type_: *tinfo) > 0; }; // aggargfloatstop — true iff the arg is a ≤16B struct with any float // field (#271/#165). Such a struct from a non-ident source would need // the SSE eightbyte transport the GP aggregate push/drain can't model; // both stages loud-stop on it. Same predicate as the cstage Tfield // fld_isfloat walk (cmd/w6c/cgen.c #271 push arm). fn aggargfloatstop(n: *node) bool = { if (n == nil) { return false; }; let st: *tinfo = n.type_: *tinfo; st = tichase(st); if (st == nil) { return false; }; if (st.kind != tykind.TY_STRUCT) { return false; }; if (st.size: i32 > 16) { return false; }; let f: *tfield = st.fields; for (f != nil) { if (typeisfloat(f.type_)) { return true; }; f = f.tnext; }; return false; }; // 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 (aliasprimsize(c, 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); }; // tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo // (the #49 funnel predicate; #209/#211 discipline — never tnode // names). Mirror of cstage's chase-then-kind test at the fill / // assign aggregate arms. fn tnodeisagg(t: *node) bool = { if (t == nil) { return false; }; let u: *tinfo = t.type_: *tinfo; u = tichase(u); if (u == nil) { return false; }; if (u.kind == tykind.TY_STRUCT) { return true; }; if (u.kind == tykind.TY_ARRAY) { return true; }; return u.kind == tykind.TY_TUPLE; }; // 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. ti = tichase(ti); 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) { // peel-ok: single peel PROBE-CLEARED (batch-2 c3-B2, // 018ef66) — constructible variant params never carry // 2+-level NAMED at this scan; cs twin nullable_ptr_tag // (cmd/w6c/cgen.c:747) keeps the identical single peel. 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, -1 if absent. Used by cgreturn to map bare `return;` // in a tagged-union-returning fn to the void variant's tag. // // #1/#3 (F1 fold): reads the NORMALIZED variant chain (ti.params, which // tinfofornode now never-drops + dedups), NOT the raw AST tagged.list. The // construct/match tag numbering already rides ti.params, so once F1 dedups // it, a deduped union with a void variant (e.g. `(i32|i32|void)`) would // desync its bare-`return;` void tag from match's if this stayed AST-keyed. // Mirrors cstage cg_tag_for_variant(rt, ty_void) (cmd/w6c/cgen.c:900-911): // chase NAMED, scan params, match bare TY_VOID (not `!void`, carried by the // tparam iserror flag). fn voidvariantindex(tagged: *node) i32 = { if (tagged == nil) { return -1; }; let ti: *tinfo = tagged.type_: *tinfo; if (ti == nil) { return -1; }; ti = tichase(ti); 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) { let vt: *tinfo = p.type_; if (vt != nil && vt.kind == tykind.TY_VOID && !p.iserror) { return idx; }; p = p.tnext; 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. #33: untyped values now resolve in flatvariantidxt's // pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match // :801 mirror); the str/slice shape scan below covers the remaining // LOOSE sources (concrete types that typeeq no variant). fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { if (du == nil) { return -1; }; if (rhs == nil) { return -1; }; let ti: *tinfo = du; ti = tichase(ti); if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo, false); if (r >= 0) { return r; }; // Shape fallback: classify rhs as (str, slice, scalar/other) and // pick the first variant of matching shape. Covers LOOSE concrete // sources that typeeq no variant (untyped sources resolve above // via tyassignableuntyped since #33); 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, false); }; // 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). // exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants // only nominal variant membership — pass 1. The chain/structural // tag-synthesis arms below and their >=2 ambiguity os.exit belong to the // cgen WIDEN consumer; routing the checker through them widened is/as // acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two // consumers, two modes — not a wrapper. cstage has no twin: its is/as // gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant // stays full-only there. fn flatvariantidxt(tagged: *tinfo, want: *tinfo, exactonly: bool) i32 = { if (want == nil) { return -1; }; let ti: *tinfo = tagged; ti = tichase(ti); if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; // Pass 1: exact match (NAMED-vs-NAMED typeeq, tagged-vs-tagged, bare // typeeq). Exact matches take precedence and need no guard — distinct // variants don't exact-match the same source. let p: *tparam = ti.params; let idx: i32 = 0; for (p != nil) { if (cgvariantmatch(p.type_, want)) { return idx; }; p = p.tnext; idx += 1; }; if (exactonly) { return -1; }; // Pass 1b (#95): NAMED source, no exact variant — chain membership. // An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a // base), so declaring the variant as `ali` admits any source whose // chain shares a node with ali's. Two linear NAMED chains intersect // iff they share their chased bottom node (.ai/ken-95-oracle.md §1), // so membership reduces to pointer identity of the chased ends — // tichase is the blessed chase, no raw hops. Variants are counted // UNGATED (bare prims are type-table singletons, so a bare variant // node can BE the source's bottom): that keeps the guard ≡ harec's // nassign>=2 → NULL (ref/harec/src/types.c:734-738; the P1-exact // short-circuit is pass 1 above). >=2 chain hits cannot be // disambiguated once the nominal-lossy model collapses the chain — // hard-error (drew's ambiguity proviso extended to the chained // set). cs twin cg_tag_for_variant fused in this commit. if (want.kind == tykind.TY_NAMED) { let sb: *tinfo = tichase(want); if (sb == nil) { return -1; }; let q: *tparam = ti.params; let qi: i32 = 0; let found: i32 = -1; let n: i32 = 0; for (q != nil) { if (q.type_ != nil && tichase(q.type_) == sb) { if (found < 0) { found = qi; }; n += 1; }; q = q.tnext; qi += 1; }; if (n >= 2) { let msg: str = "flatvariantidxt: source alias chain reaches >=2 variants — ambiguous without nominal layout (#95)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (found >= 0) { return found; }; // c2 (#95): no chain hit (found/n are still -1/0 here) — // structural fallback on the chased ends. harec interns bare // composites structurally (type_hash, ref/harec/src/ // types.c:72-81), so a nominally-unrelated structurally- // equal decl DEALIASES TO THE SAME NODE there and the // assignability arm accepts via `to == from` // (types.c:1000-1002) — acceptance is definitional, not an // arm we could misread. Our store does not intern, so the // pointer compare of pass 1b misses it; chased typeeq is the // non-interned rendering of the same rule. EQUALITY only — // no type_is_assignable scalar import. The >=2 hard-error is // the nominal-lossy-model rendering of a case harec cannot // represent (two structurally-identical variants intern to // ONE type — a union cannot contain it twice), not a harec // deviation. cs twin cg_tag_for_variant fused in this commit. q = ti.params; qi = 0; for (q != nil) { if (q.type_ != nil && typeeq(tichase(q.type_), sb)) { if (found < 0) { found = qi; }; n += 1; }; q = q.tnext; qi += 1; }; if (n >= 2) { let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n"; os.write(2, msg2.ptr, msg2.len: u64); os.exit(1); }; return found; }; // Pass 2 (#15): no exact variant matched — structurally match a BARE // source against a NAMED-alias variant (bare *vtable into the // `stream` (= *vtable) variant of `(file | stream)`). The bare side // has no nominal identity, so structure is the only discriminator; // without this the widen found no variant and defaulted to tag 0, // miscompiling emitbytes' io.write(&cgoutstream.vt). Exact-first // (pass 1) keeps a bare `i64` into `(i64 | oserror)` binding the // exact `i64`. drew's proviso: guard the structural fallback like the // #218 nested-widen site — a bare source matching >=2 NAMED variants // needs nominal layout to disambiguate, so hard-error. if (want.kind != tykind.TY_NAMED) { let q: *tparam = ti.params; let qi: i32 = 0; let found: i32 = -1; let n: i32 = 0; for (q != nil) { // Full chase (F2a batch-4 c2): the old one-level // unwrap missed a chained ptr-alias variant // (type a=*X; type b=a) — every pass fell through // and the widen defaulted to tag 0, SILENT. The // TY_NAMED gate keeps bare variants in pass-1's // exact domain; drew's >=2-candidate hard-error // below now guards the CHASED match set. cs twin // cg_tag_for_variant fused in this commit (probe: // both-wrong-identical pre-fix). let pu: *tinfo = q.type_; if (pu != nil && pu.kind == tykind.TY_NAMED && typeeq(tichase(pu), want)) { if (found < 0) { found = qi; }; n += 1; }; q = q.tnext; qi += 1; }; if (n >= 2) { let msg: str = "flatvariantidxt: bare source structurally matches >=2 NAMED variants — ambiguous without nominal layout (#15/#218/#199b/#10)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; return found; }; return -1; }; // tyassignableuntyped — can a value slot of type `dst` HOLD an untyped // source value? The untyped→typed subset of cstage type_assignable // (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill // (type.c:316-324) for a variant that is itself a union. ww's checker // isassignable is node-keyed (AST type exprs), so the tinfo-keyed // widen/match funnel needs this focused mirror (#33). fn tyassignableuntyped(dst: *tinfo, want: *tinfo) bool = { if (dst == nil) { return false; }; if (want == nil) { return false; }; // c4 (rob RE-RULE 2026-06-05): FULL chase, not the one-level unwrap // — harec type_is_assignable dealiases the dst transitively when it // is not tagged and keeps the ORIGINAL dst for the tagged variant // drill, taggedness detected via the full chase (ref/harec/src/ // types.c:989-996). The one-level unwrap left a 2-level named bool // alias variant silently mis-tagged 0 (the str twin was masked by // taggedvariantindext's shape fallback). Aligns ww UP to the cs // twin cmd/wcc/type.c:369-385, harec-shaped since F1 9bd0d8b. let du: *tinfo = tichase(dst); if (du != nil && du.kind == tykind.TY_TAGGED) { // concrete→tagged drill (type.c:316-324): a NESTED tagged // variant compares by type_eq there — never equal to an // untyped source — so only non-tagged variants recurse, on // the variant's ORIGINAL type; the variant's taggedness is // detected via the full chase (was one-level: a 2-level // alias-tagged variant slipped INTO the recursion, diverging // from cs's checker-level #199α reject — see the c4 finding). let p: *tparam = du.params; for (p != nil) { let pu: *tinfo = tichase(p.type_); let nestedtagged: bool = false; if (pu != nil) { if (pu.kind == tykind.TY_TAGGED) { nestedtagged = true; }; }; if (!nestedtagged) { if (tyassignableuntyped(p.type_, want)) { return true; }; }; p = p.tnext; }; return false; }; // type.c:369-385 — INT/FLOAT/RUNE run on the UNPEELED dst exactly // like cs (typeisnum/typeisfloat/typeisint self-recurse TY_NAMED); // STR/BOOL/NIL read the chased du like cs reads its chased du. if (want.kind == tykind.TY_UNTYPED_INT) { return typeisnum(dst); }; if (want.kind == tykind.TY_UNTYPED_FLOAT) { return typeisfloat(dst); }; if (want.kind == tykind.TY_UNTYPED_STR) { if (du == nil) { return false; }; return du.kind == tykind.TY_STR; }; if (want.kind == tykind.TY_UNTYPED_RUNE) { if (typeisint(dst)) { return true; }; return dst.kind == tykind.TY_RUNE; }; if (want.kind == tykind.TY_UNTYPED_BOOL) { if (du == nil) { return false; }; return du.kind == tykind.TY_BOOL; }; if (want.kind == tykind.TY_UNTYPED_NIL) { if (du == nil) { return false; }; if (du.kind == tykind.TY_PTR) { return true; }; if (du.kind == tykind.TY_SLICE) { return true; }; if (du.kind == tykind.TY_CHAN) { return true; }; return du.kind == tykind.TY_FN; }; return false; }; // cgvariantmatch — does a source value of type `want` tag as variant // `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match // (cmd/w6c/cgen.c): // - untyped source → first variant that can HOLD it (cgen.c:801 → // type_assignable; #33 — see tyassignableuntyped) // - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only) // - exactly one NAMED → #218 nominal lost: fall back to structural // equality of the two unwrapped tagged unions, so an outer widen of // a NAMED multi-variant union into an enclosing union computes its // tag (project tinfo_lossy_nominal). Sound only while the model is // nominal-lossy; the collision guard in cgwidentaggedstorebp enforces // the invariant for when #199b/B-full lands true nominal layout. // - neither NAMED → structural typeeq fn cgvariantmatch(vt: *tinfo, want: *tinfo) bool = { if (vt == nil) { return false; }; if (want == nil) { return false; }; // #33: pre-fix an untyped scalar fell past the typeeq passes to // taggedvariantindext's str/slice SHAPE fallback, whose first // non-str/slice variant can be `void` — a bare // `let e: (void | size) = 5` stored tag 0 while the is/as side // resolved `size` to 1 (wwstage-only; cstage resolves here). if (typeisuntyped(want)) { return tyassignableuntyped(vt, want); }; if (vt.kind == tykind.TY_NAMED && want.kind == tykind.TY_NAMED) { return typeeq(vt, want); }; if (vt.kind == tykind.TY_NAMED || want.kind == tykind.TY_NAMED) { let vu: *tinfo = vt; vu = tichase(vu); let wu: *tinfo = want; wu = tichase(wu); if (vu != nil && wu != nil && vu.kind == tykind.TY_TAGGED && wu.kind == tykind.TY_TAGGED) { return typeeq(vu, wu); }; return false; }; return typeeq(vt, want); }; // cgvariantstructmatch — structural equality ignoring nominal identity // (peel NAMED, then typeeq). #218 collision guard: counts how many dst // variants share the source's *shape*; ≥2 means the structural fallback // could not disambiguate them once nominal identity is lost. Mirrors // cstage cg_variant_struct_match (cmd/w6c/cgen.c). fn cgvariantstructmatch(vt: *tinfo, want: *tinfo) bool = { let vu: *tinfo = vt; vu = tichase(vu); let wu: *tinfo = want; wu = tichase(wu); if (vu == nil) { return false; }; if (wu == nil) { return false; }; return typeeq(vu, wu); }; // 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; ti = tichase(ti); 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; su = tichase(su); 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; dt = tichase(dt); if (dt == nil) { return; }; if (dt.kind != tykind.TY_TAGGED) { return; }; let st: *tinfo = su; st = tichase(st); 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_, false); 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_, false); 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) { // #92: bare structlookup missed an alias-named literal // — `ali{...}` into a union fell to the scalar widen // arm, word0-only payload (the class #62 L2 closed for // the N_IDENT-local arm below). Same funnel: the // REGISTERED name keeps every consumer's re-lookup // hitting. Base spellings: structlookup hits at the // chain entry and si.sname == the literal's own name — // same string out, same asm. let si: *structinfo = structlookupchain(c, trefn); if (si != nil) { return si.sname; }; }; 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) { // #62 Layer-2 (F2 ww half): bare structlookup // missed an alias name (ali->base), so the // struct value fell to the SCALAR widen arm — // word0-only box payload, words 1+ zero-filled // (both-wrong-identical with cstage pre-F1, // gate-blind). structlookupchain is the name- // domain twin of cstage's su=type_chase_named // (cg_widen_tagged_store, c138605); returning // the REGISTERED name keeps every consumer's // re-lookup hitting. The variant TAG still // keys on the un-chased stamped type — the // member's nominal identity is the alias. let si: *structinfo = structlookupchain(c, tn); if (si != nil) { return si.sname; }; }; }; }; }; 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) { // #211: a call's result type is the CALLEE's fn-type ret, which // the checker stamps onto this N_CALL node (check.ww N_CALL: both // the SK_FN path and the fn-VALUE/field path set e.type_ = the // result tinfo). Read it directly — a value-receiver fn-ptr FIELD // call `s.f(...)` keyed by the field leaf `f` with the receiver // VARIABLE name as the "module" otherwise mis-binds a same-named // GLOBAL fn of different register shape (silent cs≠ww). cstage's // sister reads u->ret off the callee type (cmd/wcc/check.c:1433, // :1490); harec selects by interned type id, not name (ref/harec/ // src/types.c:714). Mirrors the N_DOT branch below. if (typeistagged(src.type_: *tinfo)) { return true; }; return false; }; if (src.kind == nkind.N_INDEX) { // The checker stamps every N_INDEX node's type_ to the element // tinfo (check.ww:2334-2337 indexresult) for ANY base shape — // N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read // it directly, mirroring cstage cg_widen_tagged_store keying on // src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior // N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases, // so a tagged element materialized via `x.o[i]` (correct AX/DX // slot from cgindex) was then spilled by the scalar-widen arm, // dropping the tag/payload-high word — silent cs≠ww. if (typeistagged(src.type_: *tinfo)) { return true; }; return false; }; // 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; }; }; // Family C (#35/#46): a DEREF source is mem-based (taggedmemread, // any size) — the widen-store's memread arm copies the box from // the address cgexpr leaves in AX. An unwrap (`?`/`!`) source // fills the cursor after the tagged-success payload shift (the // cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm). if (src.kind == nkind.N_UN && src.op == tkind.TK_STAR) { if (typeistagged(src.type_: *tinfo)) { return true; }; }; if (src.kind == nkind.N_TRYPROP || src.kind == nkind.N_TRYUNW) { if (typeistagged(src.type_: *tinfo)) { return true; }; }; return false; }; // taggedmemread — #37: does cgexpr leave this tagged expr's box in // MEMORY (AX = box address) instead of the AX/DX/CX/R8 cursor? True // for an N_INDEX/N_DOT read whose box exceeds the 4-reg cursor — the // same mem-based class as an sret-classified call (which the #38b // gates key separately on callsretsize). Every cursor-spill consumer // must branch on this before reading AX as the tag. Mirrors cstage // cg_tagged_memread. // Family C (#35/#46): a DEREF source is mem-based at ANY size — the // pointer value IS the box address, so cgun skips the scalar load // (which carried only the tag) and consumers copy from memory. ≤32B // INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar); // the nullable one-word fold stays a scalar deref. fn taggedmemread(c: *cgen, e: *node) bool = { if (e == nil) { return false; }; if (e.kind == nkind.N_UN && e.op == tkind.TK_STAR) { let du: *tinfo = e.type_: *tinfo; du = tichase(du); if (du == nil) { return false; }; if (du.kind != tykind.TY_TAGGED) { return false; }; if (du.nullable != 0) { return false; }; return du.size: i32 > 8; }; if (e.kind != nkind.N_INDEX && e.kind != nkind.N_DOT) { return false; }; let u: *tinfo = e.type_: *tinfo; u = tichase(u); if (u == nil) { return false; }; if (u.kind != tykind.TY_TAGGED) { return false; }; return u.size: i32 > TUPLE_GPCAP * 8; }; // taggedcastpeel — Family C (#35): a tagged→tagged cast is transport- // transparent — the operand's box IS the value; transport consumers // (widen-store, arg push) derive the remap from the operand's type. // Peeling exposes the ident/deref carrier their source arms key on; // cgexpr on the cast node itself collapses to one word. Concrete- // variant casts (`7: size`) keep their node for variant-tag lookup; // the nullable one-word fold never spills a cursor — excluded. // Mirrors cstage cg_tagged_castpeel. fn taggedcastpeel(c: *cgen, e: *node) *node = { for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { let cu: *tinfo = e.type_: *tinfo; cu = tichase(cu); if (cu == nil) { return e; }; if (cu.kind != tykind.TY_TAGGED) { return e; }; if (cu.nullable != 0) { return e; }; let iu: *tinfo = e.lhs.type_: *tinfo; iu = tichase(iu); if (iu == nil) { return e; }; if (iu.kind != tykind.TY_TAGGED) { return e; }; if (iu.nullable != 0) { return e; }; e = e.lhs; }; return e; }; // taggedidcastpeel — the IDENTITY-only subset of the peel for // consumers that key variant indices on the scrutinee's own type // (is/as/match): same-type casts are no-ops there, but a WIDENING // cast changes the tag numbering and must NOT be peeled — those die // loud at the consumer's cast catch-all instead. Mirrors cstage // cg_tagged_idcastpeel. fn taggedidcastpeel(c: *cgen, e: *node) *node = { for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) { if (!typeeq(e.type_: *tinfo, e.lhs.type_: *tinfo)) { return e; }; let cu: *tinfo = e.type_: *tinfo; cu = tichase(cu); if (cu == nil) { return e; }; if (cu.kind != tykind.TY_TAGGED) { return e; }; e = e.lhs; }; return e; }; // 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 = { // #37: >32B box — leave its ADDRESS in AX (taggedmemread, the // sret-receive convention); the 4-reg cursor walk below would // truncate past payload word 2. Mirrors cstage's N_DOT // TY_STRUCT/TY_PTR tagged arms. if (slot_sz > TUPLE_GPCAP * 8) { emitline("\tLEAQ\t"); emitdispreg(foff: i64, basereg); emitline(", AX\n"); return; }; // 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 per-size scratch sized at first use (#15/#26c, size-keyed // by #44): sibling sites (cgreturn, pushargsrev, cgindex) hitting // the same slot size reuse the slot; a different size pins its own. let scr: i32 = tagscradd(c, slot_sz); 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; dt = tichase(dt); 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; }; // Family C (#35): a tagged→tagged cast is transport-transparent // — peel it so the ident/deref/memread source arms below see the // carrier and the remap keys on the operand's type. Pre-#35 the // cast node fell to the scalar arm (`let w: un3 = (v: un3)` // stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store. src = taggedcastpeel(c, src); // `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 castsubset: bool = false; let castrhs: *node = src.rhs; if (castrhs != nil) { let castt: *tinfo = castrhs.type_: *tinfo; let castu: *tinfo = castt; castu = tichase(castu); if (castu != nil) { if (castu == dt) { castisdst = true; } else { if (castu.kind == tykind.TY_TAGGED) { if (typeeq(castt, dst)) { castisdst = true; } else { castsubset = true; }; }; }; }; }; // S1/#35 (rule 7): a widen-SUBSET cast — the cast target // is a TAGGED union that is NOT dst (castu tagged && // !typeeq(castt, dst)). The inner-variant tag is never // remapped to dst's index, so the scalar arm below would // emit tag=0: a SILENT mis-tag on any 2nd-variant value // (census S1: `return true: inner`, inner=(int|bool) into // (int|bool|str), ran the int arm not the bool arm). Loud- // align to cstage cgen.c:2721-2723. The !inneristagged gate // matches the src=inner collapse below — a tagged inner is // handled by the #218 nested arm. Faithful remap (read inner // tag, inner-idx→outer-idx) = the #23/#40 widen-subset // feature, deferred post-CSP (needs the nominal variant- // remap table). if (castsubset && !inneristagged) { let m35: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n"; os.write(2, m35.ptr, m35.len: u64); os.exit(1); }; if (castisdst && !inneristagged) { src = inner; }; }; }; }; // #62 Layer-2: ONE source-classify chase at entry (post cast peels, // where src is final) — the per-arm single reads it replaces all // chased the same src.type_. Mirrors cstage cg_widen_tagged_store's // `su = type_chase_named(st)` position (c138605). Tag lookups keep // the un-chased src.type_ (nominal identity is the alias). let su: *tinfo = src.type_: *tinfo; su = tichase(su); // #218: is the source itself a single NESTED variant of dt (its // whole tagged type matches one dt variant), rather than a flattened // SUBSET whose members spread into dt? If so, the inner tagged value // is the payload: store it at slot_off+8 with the outer tag at // slot_off+0, mirroring the scalar/struct/str single-variant arms — // NOT a copy-to-+0 + sub-variant remap. flatvariantidxt's structural // fallback (cgvariantmatch) recovers the index after the nominal- // lossy collapse. Gated on a tagged source so scalar/str/struct // sources keep their existing arms. Mirrors cstage // cg_widen_tagged_store's nested arm (cmd/w6c/cgen.c). let srctagged: bool = (rhstaggedident(c, src) != nil) || rhstaggedabicall(c, src); // #51 (#263): a module-global tagged ident is also a tagged source // (no local slot — handled by the global branch in the nested copy). if (!srctagged) { if (src.kind == nkind.N_IDENT) { if (localfindnode(c, src.str) == nil) { let gtn51: *node = letvartnode(c, src.str); if (gtn51 != nil) { if (istaggedtype(c, gtn51)) { srctagged = true; }; }; }; }; }; if (srctagged) { let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); if (nested >= 0) { // drew collision guard: the structural fallback over- // matches if ≥2 nominally-distinct dt variants share the // source's shape. Unreachable under today's nominal-lossy // model, but INVERTS when #199b/B-full lands the nominal // layer — hard-error NOW so a future collision STOPS the // compiler instead of silently mis-tagging. let nmatch: i32 = 0; let gp: *tparam = dt.params; for (gp != nil) { if (cgvariantstructmatch(gp.type_, src.type_: *tinfo)) { nmatch += 1; }; gp = gp.tnext; }; if (nmatch >= 2) { let msg: str = "cgwidentaggedstore: structural fallback cannot disambiguate nominally-distinct same-shape variants without nominal layout (#218/#199b/B-full)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let ssz: i32 = su.size: i32; emitline("\tXORQ\tAX, AX\n"); let zk: i32 = 0; for (zk < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + zk): i64); emitline("(BP)\n"); zk += 8; }; if (src.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, src.str); if (lc != nil) { let soff: i32 = lc.off; let ck: i32 = 0; for (ck < ssz) { emitline("\tMOVQ\t"); emitoff((soff + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + ck): i64); emitline("(BP)\n"); ck += 8; }; } else { // #51 (#263 ww-runtime-correct): a module-global tagged // ident source — no BP slot. Land g(SB) in SI // (aggargsrcaddr) and copy the inner box to slot+8. // Pre-fix rhstaggedident returned nil for a global, so // srctagged was false and the value fell to the scalar // word0 arm — gi's TAG landed as the payload. cstage // copies frame garbage (cstage half #44). if (aggargsrcaddr(c, src, "SI")) { let ck2: i32 = 0; for (ck2 < ssz) { emitline("\tMOVQ\t"); emitoff(ck2: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + ck2): i64); emitline("(BP)\n"); ck2 += 8; }; }; }; } else { // non-nested SUBSET-widen of a GLOBAL tagged // source still mis-copies (both stages) — task #49. // #38b: an sret-classified call result is in // memory (AX = dest pointer), not the cursor — // the spill below would store the pointer as // the payload. Mem-to-mem widen is #40. if (src.kind == nkind.N_CALL) { if (callsretsize(c, src) > 0) { let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; os.write(2, m40a.ptr, m40a.len: u64); os.exit(1); }; }; if (taggedmemread(c, src)) { // #37: >32B box read — ADDRESS in AX; // copy the inner box from memory into // the payload area. Mirrors cstage. cgexpr(c, src); let mk: i32 = 0; for (mk < ssz) { emitline("\tMOVQ\t"); emitdispreg(mk: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((slot_off + 8 + mk): i64); emitline("(BP)\n"); mk += 8; }; } else { // #37 (rule 7): >32B from a non-mem-based // kind would spill an unfilled cursor. if (ssz > TUPLE_GPCAP * 8) { let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n"; os.write(2, m37a.ptr, m37a.len: u64); os.exit(1); }; // Family C catch-all (rule 7): a tagged cast // surviving taggedcastpeel (cast to a THIRD // union) has no cursor — loud, not word0 // garbage. Mirrors cstage. if (src.kind == nkind.N_CAST) { let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n"; os.write(2, m35a.ptr, m35a.len: u64); os.exit(1); }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); if (ssz > 8) { emitline("\tMOVQ\tDX, "); emitoff((slot_off + 16): i64); emitline("(BP)\n"); }; if (ssz > 16) { emitline("\tMOVQ\tCX, "); emitoff((slot_off + 24): i64); emitline("(BP)\n"); }; if (ssz > 24) { emitline("\tMOVQ\tR8, "); emitoff((slot_off + 32): i64); emitline("(BP)\n"); }; }; }; emitline("\tMOVQ\t$"); emitint(nested: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; }; // 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)) { // #38b: an sret-classified call result is in memory (AX = // dest pointer), not the cursor. #40. if (src.kind == nkind.N_CALL) { if (callsretsize(c, src) > 0) { let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; os.write(2, m40b.ptr, m40b.len: u64); os.exit(1); }; }; // #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left // its ADDRESS in AX; copy the whole box from memory, pad, // tag-remap — the mem-based twin of the ident arm above. // Mirrors cstage cg_widen_tagged_store's memread arm. if (taggedmemread(c, src)) { let ssz37: i32 = su.size: i32; cgexpr(c, src); let mk37: i32 = 0; for (mk37 < ssz37) { emitline("\tMOVQ\t"); emitdispreg(mk37: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((slot_off + mk37): i64); emitline("(BP)\n"); mk37 += 8; }; if (ssz37 < slot_sz) { emitline("\tXORQ\tAX, AX\n"); let pp37: i32 = ssz37; for (pp37 < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + pp37): i64); emitline("(BP)\n"); pp37 += 8; }; }; cgwidentagremap(c, dt, src.type_: *tinfo, slot_off); return; }; // #55: spill the AX/DX/CX/R8 cursor by the SOURCE box width // (su.size), zero-pad to the dst slot, then tag-remap — the // cursor twin of the ident / memread arms above. Pre-#55 it // spilled by slot_sz (reading STALE high regs when the source is // narrower) and skipped the pad + remap, so an INDEX/DOT tagged // element widened into a wider/reordered union pushed a stale word // and the un-remapped source tag (silent cs!=ww). Mirrors cstage // cg_widen_tagged_store's cursor arm + shared pad/remap tail // (cmd/w6c/cgen.c:2695-2714). Same-slot source (ssz==slot_sz) // leaves the pad empty + an identity remap, so the emission is // byte-identical to the prior arm for the exact-slot callers. cgexpr(c, src); let ssz: i32 = su.size: i32; emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); emitline("(BP)\n"); if (ssz > 8) { emitline("\tMOVQ\tDX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); }; if (ssz > 16) { emitline("\tMOVQ\tCX, "); emitoff((slot_off + 16): i64); emitline("(BP)\n"); }; if (ssz > 24) { emitline("\tMOVQ\tR8, "); emitoff((slot_off + 24): i64); emitline("(BP)\n"); }; if (ssz < slot_sz) { emitline("\tXORQ\tAX, AX\n"); let pp: i32 = ssz; for (pp < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + pp): i64); emitline("(BP)\n"); pp += 8; }; }; cgwidentagremap(c, dt, src.type_: *tinfo, slot_off); return; }; // #37 (rule 7): a >32B TAGGED source of a kind the resolver arms // above don't carry (deref/cast/unwrap/...) would fall to the // scalar word0 arm below and silently truncate — keyed on the // stamped src.type_ (kind-blind), the twin of cstage // cg_widen_tagged_store's generic-else bound. Surfaced by // reviewer-37's `let w = *p` probe on a 56B box: cstage loud, // wwstage silent (rule-10 break). if (su != nil && su.kind == tykind.TY_TAGGED && su.size: i32 > TUPLE_GPCAP * 8) { let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n"; os.write(2, m37f.ptr, m37f.len: u64); os.exit(1); }; // Family C catch-all (rule 7): a tagged cast surviving // taggedcastpeel (cast to a THIRD union) would fall to the // scalar arm below and silently truncate — loud. Mirrors // cstage's widen subset-arm cast bound. if (src.kind == nkind.N_CAST && su != nil && su.kind == tykind.TY_TAGGED && su.nullable == 0) { let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n"; os.write(2, m35b.ptr, m35b.len: u64); os.exit(1); }; // #242: tuple payload. Each element rides ONE register-ABI // eightbyte — scalar/float a single 8B word, a slice/str its 3-word // {ptr,len,cap} header (24B) — matching the tagged-return load // (AX=tag, DX=word0, CX=word1, R8=word2) and the cgmlet receive // cursor. NOT the packed-by-size t.N field layout (#238). Mirror of // cstage cg_widen_tagged_store's TY_TUPLE arm. // // #66: the cast-wrapped tuple literal `((a, b): range_alias)` is // the spelling real code uses (regex.ha:213) — the cast targets the // CONCRETE variant, so the widen-cast peel above leaves it intact // and pre-#66 it fell to the scalar arm, silently dropping payload // slot 1+. Peel to the inner tuple here; src.type_ stays the CAST's // type, which resolves the variant tag by exact named match, so the // #241 untyped-element un-matchability does not arise for this form. let tupsrc: *node = nil; let tupcast: bool = false; if (src != nil) { if (src.kind == nkind.N_TUPLE) { tupsrc = src; }; if (src.kind == nkind.N_CAST) { if (src.lhs != nil) { if (src.lhs.kind == nkind.N_TUPLE) { tupsrc = src.lhs; tupcast = true; }; }; }; // #116: a NON-LITERAL tuple-typed source (ident, index, // deref) is no longer loud here — it routes to the // addressable block-copy arm just below the literal arm. // Mirrors cstage cg_widen_tagged_store. }; if (tupsrc != nil) { if (su != nil) { if (su.kind == tykind.TY_TUPLE) { // #242/#241: mirror cstage's loud-stop CONDITION, not its // -1 mechanism (rule 10, align the RICHER side DOWN). // wwstage types `true`/`false` as bool and a suffix-less `7` // as untyped_int, so flatvariantidxt below DOES resolve the // variant — but cstage's cg_tag_for_variant can't type a bare // literal element (#241), returns -1, and loud-stops. A // program cstage rejects, wwstage must also reject. The shape // cstage can't type: a bool literal (N_TRUE/N_FALSE) or a // suffix-less numeric literal (untyped_int/untyped_float). // LIFT BOTH stage guards together when #241 fixes cstage // literal typing -> symmetric accept. BARE form only (#66): // the cast form resolves its tag from the cast's type on // BOTH stages, so bare elements are fine there. let bl: *node = tupsrc.list; for (bl != nil && !tupcast) { let bare: bool = false; if (bl.kind == nkind.N_TRUE) { bare = true; }; if (bl.kind == nkind.N_FALSE) { bare = true; }; if (bl.kind == nkind.N_INTLIT && bl.tsuffix.len == 0) { bare = true; }; if (bl.kind == nkind.N_FLOATLIT && bl.tsuffix.len == 0) { bare = true; }; if (bare) { let ml: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n"; os.write(2, ml.ptr, ml.len: u64); os.exit(1); }; bl = bl.next; }; // #242: resolve the variant tag via the typeeq core // (flatvariantidxt) — NOT taggedvariantindext, whose // str/slice shape fallback would silently pick tag 0 for an // unmatched tuple, diverging from cstage cg_tag_for_variant // (which returns -1) and masking the loud-stop below. let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); // #242: an untyped/literal tuple element (`(true,7)`) leaves // the src tuple un-matchable, so the variant tag can't // resolve — the supported shape is a tuple of TYPED exprs // (strconv parseint `(neg, n)`). Loud-stop rather than // silently mis-tag (rule 7); #241 literal-init family. if (ttag < 0) { let m1: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n"; os.write(2, m1.ptr, m1.len: u64); os.exit(1); }; // #242: this 8B-per-eightbyte packing is correct only when // no two scalar elements share a SysV eightbyte — e.g. // (bool,u64). A (i32,i32,u64) would overflow the union // payload the slotted write assumes. Loud-stop (rule 7); // SysV eightbyte tuple classification is a deferred // follow-up. Symmetric with cstage cg_widen_tagged_store. let ttotal: i32 = 0; let ce: *node = tupsrc.list; for (ce != nil) { // #47 gap-A: a tagged element rides its OWN // box (tag + payload, roundup8) per tuple slot // — NOT one 8B word. Mirror the checker's // N_TTUPLE accumulation (check.ww:1640-1646); // the store loop below boxes it recursively // (two-level widen). Symmetric with cstage. let ceti: *tinfo = ce.type_: *tinfo; ceti = tichase(ceti); if (ceti != nil && ceti.kind == tykind.TY_TAGGED) { ttotal += (ceti.size: i32 + 7) & ~7; } else { if (nodeisstr(c, ce) || nodeisslice(c, ce)) { ttotal += 24; } else { ttotal += 8; }; }; ce = ce.next; }; if (8 + ttotal > slot_sz) { let m2: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n"; os.write(2, m2.ptr, m2.len: u64); os.exit(1); }; emitline("\tXORQ\tAX, AX\n"); let tzk: i32 = 0; for (tzk < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + tzk): i64); emitline("(BP)\n"); tzk += 8; }; let tfoff: i32 = 0; let te: *node = tupsrc.list; for (te != nil) { // #47 gap-A: a tagged element is its own // tag+payload box. Recurse so the inner box // (tag@slot+0, payload@slot+8) is built at the // element's tuple-payload offset, exactly as a // top-level tagged-store does — two-level widen // (inner boxes here, outer tuple tag stamped // below). slot stride = roundup8(box). Symmetric // with cstage cg_widen_tagged_store. let teti: *tinfo = te.type_: *tinfo; teti = tichase(teti); if (teti != nil && teti.kind == tykind.TY_TAGGED) { let ebox: i32 = (teti.size: i32 + 7) & ~7; cgwidentaggedstore(c, te.type_: *tinfo, te, "BP", slot_off + 8 + tfoff, ebox); tfoff += ebox; te = te.next; continue; }; let isflt: bool = isfloattype(c, te); let wide: bool = nodeisstr(c, te) || nodeisslice(c, te); let esz: i32 = 8; let eti: *tinfo = te.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; cgexpr(c, te); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, te)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((slot_off + 8 + tfoff): i64); emitline("(BP)\n"); } else { if (wide) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + tfoff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot_off + 8 + tfoff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((slot_off + 8 + tfoff + 16): i64); emitline("(BP)\n"); } else { let sop: str = tnodestoreop(c, te, esz); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((slot_off + 8 + tfoff): i64); emitline("(BP)\n"); }; }; if (wide) { tfoff += 24; } else { tfoff += 8; }; te = te.next; }; emitline("\tMOVQ\t$"); emitint(ttag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; }; }; // #116: a NON-LITERAL but ADDRESSABLE tuple source — a tuple // IDENT var, a slice/array INDEX (tbl[i]), or a DEREF (*p). The // tuple in memory uses the SAME tupeslot strides as the box // payload the literal loop above fills, so the source-in-memory // layout already equals the box payload layout — the fill is a // flat block-copy of sum(tupeslot) bytes from the source address // into slot_off+8, no re-slotting (a tagged element rides over // as its already-built box). Kept loud (out of scope): a // CALL/sret result (#40-kin) and struct-field / array-literal- // element sources (loud EARLIER at construction, #49 / #270-1c). // A cast wrapping a concrete-variant tuple (`(tbl[i]: ci)`) // survived the widen-cast peel above; its operand is the // addressable expr. Mirror of cstage cg_widen_tagged_store. if (su != nil) { if (su.kind == tykind.TY_TUPLE) { let addrsrc: *node = src; if (addrsrc.kind == nkind.N_CAST) { if (addrsrc.lhs != nil) { addrsrc = addrsrc.lhs; }; }; let okkind: bool = false; if (addrsrc.kind == nkind.N_IDENT) { okkind = true; }; if (addrsrc.kind == nkind.N_INDEX) { okkind = true; }; if (addrsrc.kind == nkind.N_UN) { if (addrsrc.op == tkind.TK_STAR) { okkind = true; }; }; if (!okkind) { let mk: str = "cgwidentaggedstore: tuple-typed source shape unwired (only the bare/cast tuple literal and the addressable ident/index/deref trio carry a full payload; see #116)\n"; os.write(2, mk.ptr, mk.len: u64); os.exit(1); }; let ntag: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); if (ntag < 0) { let mt: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n"; os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; // The tuple's type-table size IS sum(tupeslot) under the 8B-slot // tuple layout (every walk takes its stride from tupeslot; the // type's size is their sum), so the payload byte-count routes // through the type table (rule 13) without re-walking the // elements — and a wwstage tuple tinfo carries no per-element // params list anyway. Mirror of cstage cg_widen_tagged_store. let ntotal: i32 = su.size: i32; if (8 + ntotal > slot_sz) { let mp: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n"; os.write(2, mp.ptr, mp.len: u64); os.exit(1); }; if (!cgplaceaddr(c, addrsrc, "SI")) { let ma: str = "cgwidentaggedstore: addressable tuple source address unresolved (see #116)\n"; os.write(2, ma.ptr, ma.len: u64); os.exit(1); }; emitline("\tXORQ\tAX, AX\n"); let nzk: i32 = 0; for (nzk < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + nzk): i64); emitline("(BP)\n"); nzk += 8; }; let nck: i32 = 0; for (nck < ntotal) { emitline("\tMOVQ\t"); emitoff(nck: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + nck): i64); emitline("(BP)\n"); nck += 8; }; emitline("\tMOVQ\t$"); emitint(ntag: i64); emitline(", "); emitoff(slot_off: i64); emitline("(BP)\n"); return; }; }; // #50 (#263 ww-runtime-correct): a module-GLOBAL struct ident source. // rhsstructpayload below is local/literal-only, so a global struct value // fell to the scalar word0 arm — payload truncated. Land g(SB) in SI and // byte-copy the struct words into slot+8; cstage zero-fills the payload // (cstage half #43). Local/literal sources keep the existing arms (byte-id). if (src.kind == nkind.N_IDENT) { if (localfindnode(c, src.str) == nil) { let gtn: *node = letvartnode(c, src.str); if (gtn != nil) { if (gtn.kind == nkind.N_TNAME) { let gsi: *structinfo = structlookupchain(c, gtn); if (gsi != nil) { emitline("\tXORQ\tAX, AX\n"); let gz: i32 = 0; for (gz < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + gz): i64); emitline("(BP)\n"); gz += 8; }; let gtag: i32 = taggedvariantindext(c, dt, src); if (gtag < 0) { gtag = 0; }; if (aggargsrcaddr(c, src, "SI")) { let gtot: i32 = gsi.totsize; let gk: i32 = 0; for (gk + 8 <= gtot) { emitline("\tMOVQ\t"); emitoff(gk: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8 + gk): i64); emitline("(BP)\n"); gk += 8; }; if (gk < gtot) { let gtail: i32 = gtot - gk; let glop: str = "MOVQ"; if (gtail == 4) { glop = "MOVL"; } else { if (gtail == 1) { glop = "MOVB"; }; }; emitline("\t"); emitline(glop); emitline("\t"); emitoff(gk: i64); emitline("(SI), AX\n"); emitline("\t"); emitline(glop); emitline("\tAX, "); emitoff((slot_off + 8 + gk): i64); emitline("(BP)\n"); }; emitline("\tMOVQ\t$"); emitint(gtag: i64); emitline(", "); emitoff(slot_off: 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) { // #23: delegate to the single fill path. The // inline field loop this replaces was a // parallel fill that drifted: it lacked the // tagged-field widen arm, so a (void|T)-typed // field's raw scalar landed in the field's // TAG word (silent truncation past the first // tagged field, both stages). Delegation also // inherits the nested-struct / call / array- // lit field arms; float / str / slice / // scalar fields emit byte-identically to the // old loop EXCEPT fsz==2 scalars, where the // old loop's fieldstoreop emitted MOVW // against cstage's MOVQ — a latent cs≠ww the // fill's #13-pinned dispatch closes. Mirror // of cstage cg_widen_tagged_store's // N_STRUCTLIT arm. cgstructlitfill(c, si, src, 0, 0, "", slot_off + 8); } 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"); // #227: zero pad words (+16..slot_sz) so a >16B union slot // carries the full dst payload width, not just the 1-word float // value (the BP path never pre-zeroes; a passthrough return or // *u8 reinterpret otherwise reads stack garbage at slot+16/+24). // Symmetric with cstage cg_widen_tagged_store float arm. if (slot_sz > 16) { emitline("\tXORQ\tAX, AX\n"); let zp: i32 = 16; for (zp < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + zp): i64); emitline("(BP)\n"); zp += 8; }; }; // #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; fti = tichase(fti); 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_; fvt = tichase(fvt); 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. #227: zero pad words (+16..slot_sz) — see float // arm above. The BP path never pre-zeroes, so a passthrough return / // *u8 reinterpret of the narrow-tagged value otherwise reads stack // garbage in slot+16/+24. Symmetric with cstage scalar arm. cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); emitline("(BP)\n"); if (slot_sz > 16) { emitline("\tXORQ\tAX, AX\n"); let zp: i32 = 16; for (zp < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((slot_off + zp): i64); emitline("(BP)\n"); zp += 8; }; }; 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; curstruct = tichase(curstruct); if (*outptrroot) { if (curstruct == nil) { return false; }; if (curstruct.kind != tykind.TY_PTR) { return false; }; curstruct = curstruct.sub; curstruct = tichase(curstruct); }; 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_; ft = tichase(ft); 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 (aliasprimsize(c, 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 (aliasprimsize(c, fi.tnode.str) == 0) { let csi: *structinfo = structlookup(c, fi.tnode.str); if (csi != nil) { // Inner struct's ABI size // (maxalign-rounded) — cstage // reads fl->type->size at the // nested-call branch // (cgen.c:2121); check.c:760 // sets that to the // maxalign-rounded ABI extent // (NOT natural). fi.fsz is // wwstage's slot-padded // totsize (round-to-8); the // pre-#169 structnaturalsize // shorts struct{i64,i32} // (natural 12, ABI 16) to // MOVQ+MOVL where cstage // writes MOVQ+MOVQ. let cfsz: i32 = structabisize(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) || isslicetype(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). A slice is // the same 24B {ptr,len,cap} shape, so it rides // this arm; without it the generic scalar tail // stored only the ptr word (#24). 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 if (fi.tnode != nil && fi.tnode.kind == nkind.N_TARRAY && fieldnode.lhs != nil && fieldnode.lhs.kind == nkind.N_ARRLIT) { // #249: array field from an N_ARRLIT. Without this // the generic tail below cgexprs the N_ARRLIT (→ AX) // and stores one sized word, silently DROPPING every // element. Store element-wise at disp+foff+i*esz, // mirroring the N_LET array-init path // (cgenstmt.ww:1393). int/float elements only; // str/slice/struct/tagged elements are the N_LET // multi-word gap — loud rule-7 error (cstage // cg_structlit_fill twin). let elemn: *node = fi.tnode.lhs; let isstrel: bool = isstrtype(c, elemn); let istagel: bool = istaggedtype(c, elemn); let issliceel: bool = isslicetype(c, elemn); // #100: key the loud gate off the CHASED stamped // tinfo (tichase, the tnodeisagg discipline), not // the raw tnode — a bare N_TSLICE kind test / // structlookup leaf-name probe is alias-blind, so // `type el = el0;` bypassed the gate and fell to // the scalar tail (silent wrong, ken kb5_fill2). // Twin of cstage cg_structlit_fill's // type_chase_named key (cgen.c:3179, B5-c1). let isstructel: bool = false; if (elemn != nil) { let eu: *tinfo = tichase(elemn.type_: *tinfo); if (eu != nil) { if (eu.kind == tykind.TY_STRUCT) { isstructel = true; }; }; }; if (isstrel || issliceel || isstructel || istagel) { let e1: str = "ww: struct-literal array field '"; os.write(2, e1.ptr, e1.len: u64); os.write(2, fname.ptr, fname.len: u64); let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n"; os.write(2, e2.ptr, e2.len: u64); os.exit(1); }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = aliasprimsize(c, elemn.str); if (ps > 0) { esz = ps; }; }; }; let mop: str = tnodestoreop(c, elemn, esz); 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 = fieldnode.lhs.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 (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 eoff: i32 = disp + fi.foff + idx * esz; if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); } else { emitline("\t"); emitline(mop); emitline("\tAX, "); }; if (mode == 0) { emitoff(eoff: i64); emitline("(BP)\n"); } else { emitdispreg(eoff: i64, basereg); emitline("\n"); }; idx += 1; e = e.next; }; }; if (repeat) { let total: i32 = idx; if (fi.tnode.rhs != nil) { if (fi.tnode.rhs.kind == nkind.N_INTLIT) { total = fi.tnode.rhs.uval: i32; }; }; for (idx < total) { 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 eoff: i32 = disp + fi.foff + idx * esz; if (isfloatel) { emitline("\t"); emitline(fmov); emitline("\tX0, "); } else { emitline("\t"); emitline(mop); emitline("\tAX, "); }; if (mode == 0) { emitoff(eoff: i64); emitline("(BP)\n"); } else { emitdispreg(eoff: i64, basereg); emitline("\n"); }; idx += 1; }; }; fi = nil; } else if (tnodeisagg(fi.tnode)) { // #49 (f38b/x5f-h): an aggregate (struct/array/ // tuple) field from an ADDRESSABLE source expr — // `outer{.., r = r}` — fell to the scalar tail // below and stored word0 only. Funnel: source // address via aggargsrcaddr (SI), field address // via LEAQ/ADDQ (BX — loaded AFTER the source // walk, which clobbers BX/AX), then aggcopy. // Width = the checker-STAMPED tinfo size (the // cstage fl->type->size SSoT; fi.fsz is the // slot-padded extent and skews on maxalign<8). // Non-addressable aggregate sources (tuple-lit, // >24B/odd-tail call) die loud — pre-#49 the // same silent word0 (rule 7). Mirror of cstage // cg_structlit_fill #49 arm. if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) { let m49g: str = "structlit fill: aggregate field '"; os.write(2, m49g.ptr, m49g.len: u64); os.write(2, fname.ptr, fname.len: u64); let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n"; os.write(2, m49h.ptr, m49h.len: u64); os.exit(1); }; if (mode == 0) { emitline("\tLEAQ\t"); emitoff((disp + fi.foff): i64); emitline("(BP), BX\n"); } else { if (mode == 1) { emitline("\tMOVQ\t"); emitoff(srcoff: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, srcname); emitline("(SB), BX\n"); }; if (disp + fi.foff != 0) { emitline("\tADDQ\t$"); emitint((disp + fi.foff): i64); emitline(", BX\n"); }; }; let agsz49: i32 = 0; if (fi.tnode != nil) { let agti49: *tinfo = fi.tnode.type_: *tinfo; agti49 = tichase(agti49); if (agti49 != nil) { agsz49 = agti49.size: i32; }; }; aggcopy(c, agsz49); 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, cgassign N_IDENT-lhs N_STRUCTLIT, and the C1.25 // @placescr materialise (cg_structlit_fill_bp's named twin). 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; switch (k) { case 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; case 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; case nkind.N_RUNELIT: emitline("\tMOVQ\t$"); emitint(n.uval: i64); emitline(", AX\n"); return; case nkind.N_STRLIT: cgstrlit(c, n); return; case nkind.N_TRUE: emitline("\tMOVQ\t$1, AX\n"); return; case nkind.N_FALSE: emitline("\tMOVQ\t$0, AX\n"); return; case nkind.N_NIL: emitline("\tMOVQ\t$0, AX\n"); return; case 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; case nkind.N_IDENT: cgident(c, n); return; case nkind.N_INDEX: cgindex(c, n); return; case nkind.N_SLICE: cgslice(c, n); return; case nkind.N_MATCH: cgmatch(c, n); return; case nkind.N_CAST: cgcast(c, n); return; case nkind.N_DOT: cgdot(c, n); return; case nkind.N_UN: cgun(c, n); return; case nkind.N_BIN: cgbin(c, n); return; case nkind.N_CALL: cgcall(c, n); return; case nkind.N_ASSIGN: cgassign(c, n); return; case nkind.N_TRYPROP: cgtryprop(c, n); return; case nkind.N_TRYUNW: cgtryunw(c, n); return; case nkind.N_TYPETEST: cgtypetest(c, n); return; case nkind.N_TYPEASSERT: cgtypeassert(c, n); return; case nkind.N_TUPLE: // #241: a literal tuple rvalue `(a, b)` is a value — pack its // elements into the register cursor (mirror cgreturn's N_TUPLE // arm) so a let-bind / destructure consumer reads every element, // not just AX = 0 from the default arm below. cgtuplelittocursor(c, n, nil); return; case: // 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) { // #22a: a STAMPED-CARRIER scrutinee (the #67 matchscrutt // shape — is/as on a tuple element t.N, a struct field, an // indexed element) is not an N_TTAGGED type-AST node; its // tagged type rides .type_. Resolve via the tinfo twin // (flatvariantidxt), the same core the widen-store uses — // cstage cg_tag_for_variant is type-based for every // scrutinee shape, so the AST-keyed -1 here was a silent // tag-0 clamp on wwstage (cs CMPQ $1 vs ww CMPQ $0). let sti: *tinfo = tagged.type_: *tinfo; sti = tichase(sti); if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { return flatvariantidxt(sti, vt.type_: *tinfo, false); }; 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); }; // successtag — index of the success variant of a tagged union. Mirrors // cstage cg_tagged_success_tag (cmd/w6c/cgen.c:857): if any variant is an // error (`!T`), the success tag is the first NON-error variant's index; // else 0 (legacy/no-error). Drives the `?`/`!` success-tag CMPQ + the // payload-shift variant lookup (#52/#216 — replaces the hardcoded tag-0). fn successtag(ou: *tinfo) i64 = { let u: *tinfo = tichase(ou); if (u == nil) { return 0i64; }; if (u.kind != tykind.TY_TAGGED) { return 0i64; }; let haserr: bool = false; let p: *tparam = u.params; for (p != nil) { if (p.iserror) { haserr = true; }; p = p.tnext; }; if (!haserr) { return 0i64; }; let idx: i64 = 0i64; p = u.params; for (p != nil) { if (!p.iserror) { return idx; }; idx += 1i64; p = p.tnext; }; return 0i64; }; // successvariant — the success variant's type_ (the param at successtag). // Lets the #241 tuple/tagged payload-shift sites inspect the SUCCESS // variant's shape, not the (error-first) first param. fn successvariant(ou: *tinfo) *tinfo = { let u: *tinfo = tichase(ou); if (u == nil) { return nil; }; if (u.kind != tykind.TY_TAGGED) { return nil; }; let st: i64 = successtag(ou); let idx: i64 = 0i64; let p: *tparam = u.params; for (p != nil) { if (idx == st) { return p.type_; }; idx += 1i64; p = p.tnext; }; return nil; }; // cgtrytupleshift — #241: if the `?`/`!` operand's success variant is a // tuple, the unwrapped payload is an rvalue tuple that must fill the // register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail // is skipped. Returns true when it emitted the shift. Reads the operand's // stamped tagged result tinfo (n.lhs.type_); the success variant is the // param at successtag (dynamic, #52 — not the hardcoded first param), // matching the dynamic CMPQ $successtag success-tag convention. fn cgtrytupleshift(c: *cgen, n: *node) bool = { if (n.lhs == nil) { return false; }; let ou: *tinfo = n.lhs.type_: *tinfo; ou = tichase(ou); if (ou == nil) { return false; }; if (ou.kind != tykind.TY_TAGGED) { return false; }; if (ou.params == nil) { return false; }; let sv: *tinfo = successvariant(ou); sv = tichase(sv); if (sv == nil) { return false; }; if (sv.kind != tykind.TY_TUPLE) { return false; }; cgtaggedtuplepayloadshift(c, sv); return true; }; // cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!` // operand's success variant (at successtag, #52) is itself a TAGGED union, the // unwrapped value is a NESTED box (ww keeps nested unions // un-flattened) riding the payload words intact — shift past the // outer tag so consumers see the standard AX=tag cursor. The scalar // MOVQ DX,AX tail carried only the inner tag and dropped the payload // (ken unw16). Nullable folds to one word and stays on the scalar // move. Twin of cgtrytupleshift; mirrors cstage N_TRYPROP/N_TRYUNW. fn cgtrytaggedshift(c: *cgen, n: *node) bool = { if (n.lhs == nil) { return false; }; let ou: *tinfo = n.lhs.type_: *tinfo; ou = tichase(ou); if (ou == nil) { return false; }; if (ou.kind != tykind.TY_TAGGED) { return false; }; if (ou.params == nil) { return false; }; let sv: *tinfo = successvariant(ou); sv = tichase(sv); if (sv == nil) { return false; }; if (sv.kind != tykind.TY_TAGGED) { return false; }; if (sv.nullable != 0) { return false; }; emitline("\tMOVQ\tDX, AX\n"); if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); }; if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); }; return true; }; // cgtryunwcursor — Family C (#35/#46): land the `?`/`!` operand's // tagged box in the AX/DX/CX/R8 cursor for the unwrap tail. Non-call // sources don't fill the cursor on their own: an IDENT loads it from // its frame slot, a mem-based read (deref at any size, >32B // INDEX/DOT) from the box address cgexpr leaves in AX. Both were // silent word0 unwraps pre-#35. >32B non-call stays loud (the cursor // cannot carry it; #40 family). Mirrors cstage N_TRYPROP/N_TRYUNW. // cgdotfieldcombine — single-dot field compound combine. The old // field value is in BX, the rhs in AX; the result is left in AX. // PLUSEQ/MINUSEQ preserve the pre-#34 emission (byte-id); the other 8 // ops were silently DROPPED (the arm fell through to a plain store of // the rhs → `s.f = rhs`, #34/#263). SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ // need the lhs in AX and the divisor/count in CX, so swap (rhs AX→CX, // old BX→AX) first. Signed RSHIFTEQ uses SARQ, unsigned SHRQ (#136). // Mirrors cstage cg_dotfield_combine — both stages emit identical asm. fn cgdotfieldcombine(c: *cgen, op: tkind, unsignd: bool) void = { if (op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); return; }; if (op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); return; }; if (op == tkind.TK_STAREQ) { emitline("\tIMULQ\tBX, AX\n"); return; }; if (op == tkind.TK_AMPEQ) { emitline("\tANDQ\tBX, AX\n"); return; }; if (op == tkind.TK_PIPEEQ) { emitline("\tORQ\tBX, AX\n"); return; }; if (op == tkind.TK_CARETEQ) { emitline("\tXORQ\tBX, AX\n"); return; }; if (op == tkind.TK_SLASHEQ) { 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"); }; return; }; if (op == tkind.TK_PERCENTEQ) { 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"); }; emitline("\tMOVQ\tDX, AX\n"); return; }; if (op == tkind.TK_LSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tBX, AX\n"); emitline("\tSHLQ\tCX, AX\n"); return; }; if (op == tkind.TK_RSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tBX, AX\n"); if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; return; }; let m: str = "single-dot field compound: unknown op (#34/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; // cgdotfieldhardstop — loud-stop a single-dot field compound on a // non-integer field (float/str/slice/tagged): the combine arm only // speaks integer ABI; pre-#34 these silently became `s.f = rhs`. // Mirrors the cstage cg_dotfield_hardstop gate. (#34/rule-7) fn cgdotfieldhardstop(c: *cgen, ftn: *node) void = { if (istaggedtype(c, ftn)) { let m: str = "single-dot field compound on tagged field not wired (#34/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isstrtype(c, ftn)) { let m: str = "single-dot field compound on str field not wired (#34/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isslicetype(c, ftn)) { let m: str = "single-dot field compound on slice field not wired (#34/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isfloattype(c, ftn)) { let m: str = "single-dot field compound on float field not wired (#34/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = { let u: *tinfo = nil; if (n.lhs != nil) { u = n.lhs.type_: *tinfo; }; u = tichase(u); let utag: bool = false; if (u != nil) { if (u.kind == tykind.TY_TAGGED && u.nullable == 0) { utag = true; }; }; if (utag && u.size: i32 > TUPLE_GPCAP * 8 && n.lhs.kind != nkind.N_CALL) { let p37: str = "#37: `"; os.write(2, p37.ptr, p37.len: u64); os.write(2, opname.ptr, opname.len: u64); let m37t: str = "` on a >32B mem-based tagged read unwired (#40-family follow-up)\n"; os.write(2, m37t.ptr, m37t.len: u64); os.exit(1); }; if (utag && n.lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.lhs.str); if (lc == nil) { let p35: str = "#35: `"; os.write(2, p35.ptr, p35.len: u64); os.write(2, opname.ptr, opname.len: u64); let m35g: str = "` on a global tagged ident unwired (rule 7)\n"; os.write(2, m35g.ptr, m35g.len: u64); os.exit(1); }; let boff: i32 = lc.off; let bsz: i32 = u.size: i32; if (bsz > 24) { emitline("\tMOVQ\t"); emitoff((boff + 24): i64); emitline("(BP), R8\n"); }; if (bsz > 16) { emitline("\tMOVQ\t"); emitoff((boff + 16): i64); emitline("(BP), CX\n"); }; if (bsz > 8) { emitline("\tMOVQ\t"); emitoff((boff + 8): i64); emitline("(BP), DX\n"); }; emitline("\tMOVQ\t"); emitoff(boff: i64); emitline("(BP), AX\n"); return; }; if (taggedmemread(c, n.lhs)) { let bsz2: i32 = 0; if (u != nil) { bsz2 = u.size: i32; }; cgexpr(c, n.lhs); if (bsz2 > 24) { emitline("\tMOVQ\t24(AX), R8\n"); }; if (bsz2 > 16) { emitline("\tMOVQ\t16(AX), CX\n"); }; if (bsz2 > 8) { emitline("\tMOVQ\t8(AX), DX\n"); }; emitline("\tMOVQ\t(AX), AX\n"); return; }; cgexpr(c, n.lhs); }; // cgtryprop — `e?` propagates the error variant up the stack. // Success tag is dynamic via successtag (#52/#216): the first non-error // variant's index (cstage cg_tagged_success_tag parity), 0 when success-first. fn cgtryprop(c: *cgen, n: *node) void = { // #38b residuals (rule 7): the cursor read below cannot see an // sret-classified call result (AX = dest pointer), and the // propagate-RET cannot speak an sret-classified enclosing return // (the caller reads memory, not the cursor). #40-family follow-ups. // Mirrors cstage cgen.c N_TRYPROP gates. if (n.lhs != nil) { if (n.lhs.kind == nkind.N_CALL) { if (callsretsize(c, n.lhs) > 0) { let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; os.write(2, m38p.ptr, m38p.len: u64); os.exit(1); }; }; }; if (sretretsize(c, c.fnret) > 0) { let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n"; os.write(2, m38q.ptr, m38q.len: u64); os.exit(1); }; // Family C (#35/#46): ident/deref sources land the box in the // cursor here (was a silent word0 unwrap); call sources keep // the plain cgexpr emission byte-for-byte. cgtryunwcursor(c, n, "?"); // Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null // = success (any *T variant), null = error → propagate (RET with // AX=0). The pre-fix path compared AX against successtag and so // treated null as success (inverted). Mirrors cstage cgen.c // N_TRYPROP nullable arm; ww's own cgtypetest carries the twin // (cgenexpr.ww nullable fold). (task #15/F4) if (isnullabletype(n.lhs)) { let nl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(nl); emitline("\n"); emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(nl); return; }; // AX = tag. If not the success tag, this is an error; pop frame and // RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an // error-first union. let cl: str = mklabel(c, "tryprop_ok"); let propu: *tinfo = nil; if (n.lhs != nil) { propu = n.lhs.type_: *tinfo; }; emitline("\tCMPQ\t$"); emitint(successtag(propu)); emitline(", AX\n"); emitline("\tJE\t"); emitline(cl); emitline("\n"); // #173: remap the operand union's error-variant tag to the // enclosing fn return union's variant order before propagating. // When the `?` operand and the enclosing fn return differ in // variant order, the raw operand tag names the WRONG variant in // the return union. Mirrors cstage cmd/w6c/cgen.c:6161-6184. // Payload words DX/CX/R8 ride the RET untouched; only AX (the // tag) is rewritten. Same-order unions map every error variant to // itself → zero instructions, byte-id with the pre-#173 emit. let u: *tinfo = nil; if (n.lhs != nil) { u = n.lhs.type_: *tinfo; }; u = tichase(u); let r: *tinfo = nil; if (c.fnret != nil) { r = c.fnret.type_: *tinfo; }; r = tichase(r); if (u != nil && r != nil && r.kind == tykind.TY_TAGGED && u.params != nil) { let propret: str; propret.ptr = nil; propret.len = 0; let haveret: bool = false; let p: *tparam = u.params; let i: i32 = 0; for (p != nil) { if (p.iserror) { let j: i32 = flatvariantidxt(r, p.type_, false); if (j < 0) { j = 0; }; if (j != i) { let skip: str = mklabel(c, "tryprop_skip"); emitline("\tCMPQ\t$"); emitint(i: i64); emitline(", AX\n"); emitline("\tJNE\t"); emitline(skip); emitline("\n"); emitline("\tMOVQ\t$"); emitint(j: i64); emitline(", AX\n"); if (!haveret) { propret = mklabel(c, "tryprop_ret"); haveret = true; }; emitline("\tJMP\t"); emitline(propret); emitline("\n"); emitlabel(skip); }; }; p = p.tnext; i += 1; }; if (haveret) { emitlabel(propret); }; }; emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(cl); // #241: a tuple success payload is an rvalue tuple — fill the cursor // (shift past the tag) so the destructure / let consumer reads every // element, not just word0. Success variant = successtag (#52). if (cgtrytupleshift(c, n)) { return; }; if (cgtrytaggedshift(c, n)) { return; }; // 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. // #16: read the STAMPED operand's success-variant type (any source // shape, any variant order), not a name-keyed call-only lookup of // the first variant. Mirrors cstage cgen.c:10459-10466 // (success_is_str = type_isstr(succ_t at cg_tagged_success_tag)). let succisstr: bool = false; if (n.lhs != nil) { succisstr = typeisstr(successvariant(n.lhs.type_: *tinfo)); }; 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). Success tag // is dynamic via successtag (#52): the first non-error variant's index // (cstage cg_tagged_success_tag parity), 0 when success-first. fn cgtryunw(c: *cgen, n: *node) void = { // #38b residual (rule 7): see the cgtryprop twin. if (n.lhs != nil) { if (n.lhs.kind == nkind.N_CALL) { if (callsretsize(c, n.lhs) > 0) { let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; os.write(2, m38u.ptr, m38u.len: u64); os.exit(1); }; }; }; // Family C (#35/#46): see the cgtryprop twin. cgtryunwcursor(c, n, "!"); // Nullable `(*T | void)`: AX IS the pointer. Non-null = success // (leave AX as-is), null = error → abort exit(1). The pre-fix // path compared AX against successtag, treating null as success // (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's // own cgtypetest carries the twin. (task #15/F4) if (isnullabletype(n.lhs)) { let nl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(nl); emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(nl); return; }; let cl: str = mklabel(c, "tryunw_ok"); // Success tag is dynamic (successtag / cstage cg_tagged_success_tag // parity, #52): 0 for success-first, the first non-error index for an // error-first union. let unwu: *tinfo = nil; if (n.lhs != nil) { unwu = n.lhs.type_: *tinfo; }; emitline("\tCMPQ\t$"); emitint(successtag(unwu)); emitline(", AX\n"); emitline("\tJE\t"); emitline(cl); emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(cl); // #241: tuple success payload fills the cursor (shift past the tag) — // same rvalue-tuple-into-cursor story as cgtryprop. if (cgtrytupleshift(c, n)) { return; }; if (cgtrytaggedshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) // #16: stamped success-variant type, any source/order (twin of // cgtryprop; cstage cgen.c:10595-10602). let succisstr: bool = false; if (n.lhs != nil) { succisstr = typeisstr(successvariant(n.lhs.type_: *tinfo)); }; 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. // Family C (#35): identity casts are transport no-ops — peel so // the ident emission carries; a WIDENING tagged cast renumbers // the tag the compare keys on and has no wired source arm — // loud below, not a mis-keyed test. Mirrors cstage N_TYPETEST. let lhs: *node = taggedidcastpeel(c, n.lhs); // #38b residual (rule 7): an sret-class call result leaves AX = // dest pointer, not the tag — mem-based test is a #40-family // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. if (lhs != nil) { if (lhs.kind == nkind.N_CALL) { if (callsretsize(c, lhs) > 0) { let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n"; os.write(2, m38t.ptr, m38t.len: u64); os.exit(1); }; }; }; let scrutoff: i32 = 0; let scrutt: *node = nil; let nonident: bool = false; let globalident: bool = false; 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); } else { // #18 is-half (align-UP): global tagged ident — tag word at // g(SB)+0, no BP slot. cstage N_TYPETEST is uniformly cgexpr // (MOVQ g(SB),AX); pre-fix the !nonident emit read 0(BP)=saved BP. let gt: *node = letvartnode(c, lhs.str); scrutt = resolvetagged(c, gt); globalident = true; }; } else { // #45: non-ident scrutinee (xs[i], p.field, call). // cstage N_TYPETEST never spills — cgexpr leaves the // scrutinee's tag word in AX (tagged element/field/ // call reads load the tag first), so compare AX // directly. The `as` twin's @asrt_spill (#200) is // NOT mirrored here: `as` re-reads payload words // after the check; `is` consumes only the tag, and // a spill would diverge from cstage's asm (rule 10). // Pre-#45 this fell through to scrutoff=0 and the // tag read landed on (BP) — the saved-BP word. nonident = true; // Family C catch-all (rule 7): a widening tagged // cast source — loud. Mirrors cstage. { let icu: *tinfo = lhs.type_: *tinfo; icu = tichase(icu); if (lhs.kind == nkind.N_CAST && icu != nil && icu.kind == tykind.TY_TAGGED && icu.nullable == 0) { let m35i: str = "#35: tagged cast source shape unwired at `is` (rule 7)\n"; os.write(2, m35i.ptr, m35i.len: u64); os.exit(1); }; }; cgexpr(c, lhs); // #37: a >32B box read leaves its ADDRESS in AX — // load the tag word from memory before the compare. // Mirrors cstage N_TYPETEST. if (taggedmemread(c, lhs)) { emitline("\tMOVQ\t(AX), AX\n"); }; }; }; let want: i32 = 0; let nullcarrier: *node = scrutt; if (nonident) { // Variant index from the STAMPED scrutinee type (cstage: // u = n->lhs->type) — matchscrutt's node-shape walk can't // carry N_DOT (returns the scrut node, which the // cgtagvariantidx N_TTAGGED gate rejects). flatvariantidx / // flatslicevariantidx read .type_ off any stamped carrier. nullcarrier = lhs; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_TSLICE) { want = flatslicevariantidx(c, lhs, n.rhs.lhs); } else { want = flatvariantidx(c, lhs, n.rhs); }; }; } else { want = cgtagvariantidx(c, scrutt, n.rhs); }; if (!nonident) { if (globalident) { emitline("\tMOVQ\t"); emitsymname(c, lhs.str); emitline("(SB), AX\n"); } else { emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); }; }; let nel: str = mklabel(c, "is_ne"); let dnl: str = mklabel(c, "is_done"); if (isnullabletype(nullcarrier)) { // Nullable `(*T | void)` fold: the word in AX IS the // pointer — discriminate pointer-vs-null, not tag-vs-index // (cstage cgen.c N_TYPETEST nullable arm). `want` stays RAW // here: cstage tests tag == ptr_tag unclamped, so a // no-match (-1) takes the void polarity. emitline("\tCMPQ\t$0, AX\n"); if (want == nullableptrtag(nullcarrier)) { emitline("\tJE\t"); } else { emitline("\tJNE\t"); }; } else { if (want < 0) { want = 0; }; 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; }; fn cgtypeassert(c: *cgen, n: *node) void = { // `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. // Family C (#35): identity-cast peel — see the cgtypetest twin. let lhs: *node = taggedidcastpeel(c, n.lhs); // Enum ↔ integer: reinterpret-only — the value already occupies AX // (or AX:BX for str variants, irrelevant here); no tag/unwrap. Gate // on the stamped operand / result TYPE, not node shape: a constant- // folded enum member (`flag.NOESCAPE`) reaches cgen as an int-literal // node carrying the enum type_, which a node-shape probe missed → // spurious tagged-assertion + exit(1) (#27b). Mirrors cstage // cmd/w6c/cgen.c N_TYPEASSERT (type_chase_named on operand + result). { let su: *tinfo = nil; if (lhs != nil) { su = tichase(lhs.type_: *tinfo); }; let vu: *tinfo = tichase(n.type_: *tinfo); let lenum: bool = false; let renum: bool = false; if (su != nil) { if (su.kind == tykind.TY_ENUM) { lenum = true; }; }; if (vu != nil) { if (vu.kind == tykind.TY_ENUM) { renum = true; }; }; if (lenum || renum) { cgexpr(c, lhs); return; }; }; // #38b residual (rule 7): the spill below reads the cursor, which // an sret-class call result never fills. Mirrors cstage cgen.c // N_TYPEASSERT gate. if (lhs != nil) { if (lhs.kind == nkind.N_CALL) { if (callsretsize(c, lhs) > 0) { let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n"; os.write(2, m38a.ptr, m38a.len: u64); os.exit(1); }; }; }; 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); } else { // #18 as-half (#263 ww-runtime-correct; cstage N_TYPEASSERT on a // global tagged ident spills uninitialized DX as the payload — // task #46). No BP slot: copy the box words from g(SB) into a // fresh @asrt_spill so the tag-check + payload load below index // off memory like a local. cs!=ww residual until #46 lands. let gt: *node = letvartnode(c, lhs.str); scrutt = resolvetagged(c, gt); let gsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@asrt_spill", gsz, nil); emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), AX\n"); let gk: i32 = 0; for (gk < gsz) { emitline("\tMOVQ\t"); emitdispreg(gk: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((scrutoff + gk): i64); emitline("(BP)\n"); gk += 8; }; }; } else { // Non-ident scrutinee (call result, arr[i], p.field, ?, // etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460) // and cstage cmd/w6c/cgen.c:6300-6316: alloc an // `@asrt_spill` slot sized via matchspillsz, evaluate // the LHS, then copy the AX/DX/CX[/R8] return-ABI words // into the slot so the tag-check + payload load indexes // off memory like the IDENT path. Without this, scrutoff // stayed 0 and the tag read fell on (BP) — the saved-BP // word — and the payload read on +8(BP) — the return // address. Bug #200. scrutt = matchscrutt(c, lhs); let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@asrt_spill", spillsz, nil); if (taggedmemread(c, lhs)) { // #37: >32B box read — ADDRESS in AX; copy the // whole box from memory. Mirrors cstage. cgexpr(c, lhs); let ak37: i32 = 0; for (ak37 < spillsz) { emitline("\tMOVQ\t"); emitdispreg(ak37: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((scrutoff + ak37): i64); emitline("(BP)\n"); ak37 += 8; }; } else { // #37 (rule 7): >32B from a non-mem-based kind would // spill an unfilled cursor. Mirrors cstage. if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) { let m37s: str = "#37: `as` on a >32B tagged value from a non-mem-based source unwired (rule 7)\n"; os.write(2, m37s.ptr, m37s.len: u64); os.exit(1); }; // Family C catch-all (rule 7): a widening tagged // cast source — loud. Mirrors cstage. { let acu: *tinfo = lhs.type_: *tinfo; acu = tichase(acu); if (lhs.kind == nkind.N_CAST && acu != nil && acu.kind == tykind.TY_TAGGED && acu.nullable == 0) { let m35s: str = "#35: tagged cast source shape unwired at `as` (rule 7)\n"; os.write(2, m35s.ptr, m35s.len: u64); os.exit(1); }; }; cgexpr(c, lhs); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); emitline("(BP)\n"); if (!isnullabletype(scrutt)) { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); // Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX // → +16 when slot_size > 16. The 4-word case // (R8 → +24, slot_size > 24) is the cgmatch shape // (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990) // but cstage cgtypeassert omits it; preserve the // asymmetry rather than diverge from rule 10 // byte-id. Filed inline as a cstage twin task. if (spillsz > 16) { emitline("\tMOVQ\tCX, "); emitoff((scrutoff + 16): i64); emitline("(BP)\n"); }; }; }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); let okl: str = mklabel(c, "asrt_ok"); // Nullable `(*T | void)`: the slot word IS the pointer, not a tag. // The *T variant asserts non-null, the void variant asserts null; // AX keeps the pointer on the ok path (no slot+8 unwrap — the 8B // nullable slot has no second word). The pre-fix path compared the // POINTER against `want` (so a real pointer aborted, null passed) // and unwrapped a frame word past the slot. `want` stays RAW (no // clamp), mirroring cstage cgen.c N_TYPEASSERT nullable arm and // ww's own cgtypetest nullable fold (cgenexpr.ww). (task #17/F4) if (isnullabletype(scrutt)) { let ptrtag: i32 = nullableptrtag(scrutt); emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); emitline("\tCMPQ\t$0, AX\n"); if (want == ptrtag) { emitline("\tJNE\t"); } else { emitline("\tJE\t"); }; emitline(okl); emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(okl); return; }; if (want < 0) { want = 0; }; 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. Type-keyed on the stamped src/dst types (dst-is-slice + // src-is-str), mirroring cstage cgen.c N_CAST (type_chase_named → // TY_SLICE/TY_STR). The prior local-ident-source gate (#19) missed // every non-local str source — global ident, struct field, call // result — leaving CX = the str's stale word-16 garbage cap. if (isslicetype(c, n.rhs)) { if (isstrtype(c, n.lhs)) { 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; // primsize-ok (#101/#109): this IS an alias chase loop // — primsize is the leaf-primitive break test the loop // wraps (aliaslookup advances the cursor on a miss). 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; // #241: a tuple ident is a value — leave the whole tuple in the // register cursor (`yield t` / `return t` / `let q = t`), not // just word0 in AX. Mirror of cstage cgexpr N_IDENT tuple arm. let itu: *tinfo = nil; if (lc.tnode != nil) { itu = lc.tnode.type_: *tinfo; }; itu = tichase(itu); if (itu != nil) { if (itu.kind == tykind.TY_TUPLE) { cgtupleslottocursor(c, off, itu); return; }; }; // 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"); emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); emitline("\t(CX), X0\n"); return; }; emitline("\tMOVQ\t"); emitsymnamehint(c, nm, c.curmod); 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)) { // C-t3 (#48, rule 7): a GLOBAL tuple as a first-class VALUE // (`let q = g;` / `return g;` / `f(g)`) has no slot-to-cursor // path (cgtupleslottocursor is BP-relative) — pre-fix it fell // to the scalar MOVQ below, loading word0 only, and the // receive read a STALE cursor for words 1+. Element reads // (g.N) are the supported surface. Mirrors the cstage cgexpr // non-local ident guard. let gtt: *node = letvartnode(c, nm); for (gtt != nil && gtt.kind == nkind.N_TNAME) { gtt = aliaslookup(c, gtt.str); }; if (gtt != nil) { if (gtt.kind == nkind.N_TTUPLE) { let mgt: str = "#48: global tuple as a first-class value unwired (element reads only; rule 7)\n"; os.write(2, mgt.ptr, mgt.len: u64); os.exit(1); }; }; 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"); emitsymnamehint(c, nm, c.curmod); 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)) { // #135: an inferred-float global's tnode is the // defaultinferredlets-renamed "f64"/"f32" N_TNAME whose // .type_ is unstamped (cgen can't build tinfo), so the // isfloattype stamp-read misses it. Fall back to the // name keyword (the letfloatprim SSoT letemitsize uses) // so the float load fires for inferred as for explicit. let isf: bool = isfloattype(c, lv.tnode); let is32: bool = isf32type(c, lv.tnode); if (!isf && lv.tnode != nil && lv.tnode.kind == nkind.N_TNAME) { let fsz: i32 = letfloatprim(lv.tnode.str); if (fsz > 0) { isf = true; }; if (fsz == 4) { is32 = true; }; }; if (isf) { let mov: str = "MOVSD"; if (is32) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymnamehint(c, nm, c.curmod); 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"); emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymnamehint(c, nm, c.curmod); 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"); }; }; // dotchainaddr — emit the ADDRESS of a dot/ident lvalue chain into // `dstreg`, dereferencing pointer links mid-chain. Returns true on // success, false if a link isn't a struct / ptr-to-struct it can // resolve. Recursion mirrors the cstage read spine: for `x.f`, recurse // to &x, deref if x is a *struct (so dstreg holds the pointee base), // then add f's offset. Touches ONLY dstreg (no AX, no stack) — same // spill contract as dotbaseaddr. The chained-base arm of dotbaseaddr // (#253) is its sole caller. Cstage twin: cmd/w6c/cgen.c // `cg_dotchain_addr`. fn dotchainaddr(c: *cgen, n: *node, dstreg: str) bool = { if (n == nil) { return false; }; if (n.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.str); if (lc != nil) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), "); emitline(dstreg); emitline("\n"); return true; }; // #256: carry cstage cg_dotchain_addr's `let_islet || // def_isstructdef` guard (never-silent ethos). Unreachable on // valid input — a struct-typed chain root is always local, a // let-global, or a struct def — so this adds zero divergent // asm; it just refuses to LEAQ name(SB) for a name that names // neither. deflookup is the broader def-registry twin (ww has no // struct-specific def predicate; harmless given unreachability). if (isletvar(c, n.str) || deflookup(c, n.str)) { emitline("\tLEAQ\t"); emitsymname(c, n.str); emitline("(SB), "); emitline(dstreg); emitline("\n"); return true; }; return false; }; if (n.kind != nkind.N_DOT) { return false; }; let x: *node = n.lhs; if (x == nil) { return false; }; let xu: *tinfo = x.type_: *tinfo; xu = tichase(xu); if (xu == nil) { return false; }; let xviaptr: bool = false; let st: *tinfo = nil; if (xu.kind == tykind.TY_PTR) { let p: *tinfo = xu.sub; p = tichase(p); if (p != nil) { if (p.kind == tykind.TY_STRUCT) { st = p; xviaptr = true; }; }; } else { if (xu.kind == tykind.TY_STRUCT) { st = xu; }; }; if (st == nil) { return false; }; let f: *tfield = st.fields; let foff: i64 = -1; for (f != nil) { if (streq(f.name, n.str)) { foff = f.offset: i64; break; }; f = f.tnext; }; if (foff < 0) { return false; }; if (!dotchainaddr(c, x, dstreg)) { return false; }; if (xviaptr) { emitline("\tMOVQ\t("); emitline(dstreg); emitline("), "); emitline(dstreg); emitline("\n"); }; if (foff != 0) { emitline("\tADDQ\t$"); emitint(foff); emitline(", "); emitline(dstreg); emitline("\n"); }; return true; }; // dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an // N_DOT with N_IDENT inner OR a chained N_DOT inner (#253: `o.p.m` / // `o.i.m` / `o.a.b.m`). 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). // // #253: a chained inner (`inner` is itself an N_DOT) routes through // dotchainaddr to recover the container base — the pointer VALUE of // inner when inner is a *struct (viaptr), else the ADDRESS of inner — // then adds the field offset. Closes the array-field-base-address // family across every op (index r/w, addr-of, slice, compound). 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; }; let chained: bool = (inner.kind == nkind.N_DOT); if (inner.kind != nkind.N_IDENT && !chained) { 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). N_IDENT-inner only — a chained // inner has a valid stamped type_ and routes through dotchainaddr. let lc: *local = nil; let isglobal: bool = false; if (!chained) { lc = localfindnode(c, inner.str); if (lc == nil) { // #128b is for a module-QUALIFIER inner (mod.arr): inner has no // usable struct type. cstage gates it on inner->type==NULL||ty_err // (cgen.c:2109). Without the gate a typed-struct global inner whose // FIELD shares a name with an unrelated global array hijacks it // (gs.fld -> LEAQ fld(SB)) — #20. A typed inner falls to #249 below. let ibu: *tinfo = tichase(inner.type_: *tinfo); if (ibu == nil || ibu.kind == tykind.TY_ERR) { 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; }; }; // #249 (sibling of #135): inner is a module-GLOBAL struct value // (let/def), not a local — lc is nil but inner.type_ is a valid // struct. Resolve the field below and emit a global base (LEAQ // name(SB)). A non-struct inner (e.g. an SK_USE module qualifier, // type ty_err) falls through the struct gate to `return false`. isglobal = true; }; }; let bu: *tinfo = inner.type_: *tinfo; bu = tichase(bu); if (bu == nil) { return false; }; let viaptr: bool = false; let structt: *tinfo = nil; if (bu.kind == tykind.TY_PTR) { let st: *tinfo = bu.sub; st = tichase(st); 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. ft = tichase(ft); if (ft == nil) { return false; }; if (ft.kind != tykind.TY_ARRAY) { return false; }; // #253: chained inner — compute the container base via the dot-chain // spine (pointer VALUE of inner when viaptr, else its ADDRESS), then // add the field offset. dotchainaddr keeps the spill contract. if (chained) { if (!dotchainaddr(c, inner, dstreg)) { return false; }; if (viaptr) { emitline("\tMOVQ\t("); emitline(dstreg); emitline("), "); emitline(dstreg); emitline("\n"); }; if (foff != 0) { emitline("\tADDQ\t$"); emitint(foff); emitline(", "); emitline(dstreg); emitline("\n"); }; return true; }; let innoff: i64 = 0; if (lc != nil) { innoff = 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 if (isglobal) { // #249: LEAQ name(SB) + field offset. Mirror cstage // cg_dotbase_addr's global value-struct arm. emitline("\tLEAQ\t"); emitsymname(c, inner.str); emitline("(SB), "); 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; }; // aggargsrcaddr — land the ADDRESS of an addressable aggregate (struct/ // array) call-arg source in dstreg (#271, mirror of cstage // aggarg_srcaddr). Reuses the closed #265/#268 let-init-copy dispatch: // local ident slot (LEAQ off(BP)), module-let global (LEAQ name(SB)), // deref operand (cgexpr of the pointer), N_DOT field (dotchainaddr, // #253), N_INDEX element of an N_IDENT array base (the &base[i] spine, // #252/#270). Returns false for an uncovered source kind (caller loud- // stops, rule 7). The CALL source is handled at the push site. fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { if (src.kind == nkind.N_UN) { if (src.op == tkind.TK_STAR) { cgexpr(c, src.lhs); if (!streq(dst, "AX")) { emitline("\tMOVQ\tAX, "); emitline(dst); emitline("\n"); }; return true; }; }; if (src.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, src.str); if (lc != nil) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), "); emitline(dst); emitline("\n"); return true; }; // global value source. Gated to a module-`let` (letvartnode, // the cstage let_islet twin); a const array/struct `def` // aggregate ARG is untested + out of scope (#274; both stages // loud-stop, rule-10 aligned). if (letvartnode(c, src.str) != nil) { emitline("\tLEAQ\t"); emitsymname(c, src.str); emitline("(SB), "); emitline(dst); emitline("\n"); return true; }; return false; }; if (src.kind == nkind.N_DOT) { return dotchainaddr(c, src, dst); }; if (src.kind == nkind.N_INDEX) { let base: *node = src.lhs; let idx: *node = src.rhs; if (base == nil) { return false; }; if (base.kind != nkind.N_IDENT) { return false; }; let bu: *tinfo = base.type_: *tinfo; bu = tichase(bu); if (bu == nil) { return false; }; if (bu.kind != tykind.TY_ARRAY) { return false; }; let esz: i32 = 1; if (bu.sub != nil) { esz = bu.sub.size: i32; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; let boff: *local = localfindnode(c, base.str); if (boff != nil) { emitline("\tLEAQ\t"); emitoff(boff.off: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), BX\n"); }; emitline("\tADDQ\tBX, AX\n"); if (!streq(dst, "AX")) { emitline("\tMOVQ\tAX, "); emitline(dst); emitline("\n"); }; return true; }; return false; }; // aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz // bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail. // Extracted verbatim from the C1.25 assign-resolver tail so every // aggregate copy position (resolver field store, #49 ident reassign, // #49 structlit fill-field) funnels through one loop — close-by- // construction, no per-site width logic to skew. Mirror of cstage // cg_aggcopy. fn aggcopy(c: *cgen, sz: i32) void = { let k: i32 = 0; for (k + 8 <= sz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 8; }; if (k + 4 <= sz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 4; }; if (k + 2 <= sz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 2; }; if (k + 1 <= sz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 1; }; }; // copysrcnatsize — natural byte width of a whole-struct copy's SOURCE // operand, read from the source node's stamped tinfo (tichase peels // NAMED). #71: the four whole-struct field-copy sites must move this many // bytes, NOT structinfo.totsize (slot-padded, round-8) which over-copies // into slot padding and clobbers a natural-offset successor once #44 packs // it there. Reads the source NODE's type, never fi.foff/fi.fsz or // structinfo, so #71 stays independent of #44's registerstruct offset // change. Equals cstage's `str_fu->size` (cmd/w6c/cgen.c:5302 SSoT) — the // field struct's aligned r.size (check.ww N_TSTRUCT), distinct from the // existing structnaturalsize (which reads structinfo max(foff+fsz), a // #44-coupled source). The ragged-tail completeness shared by both stages' // field copies is a separate class, tracked under #73 / the aggcopy // emitter choke-point. fn copysrcnatsize(c: *cgen, src: *node) i32 = { if (src == nil || src.type_ == nil) { let msg: str = "#71: whole-struct copy source has no stamped tinfo\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let ti: *tinfo = tichase(src.type_: *tinfo); if (ti == nil) { let msg: str = "#71: whole-struct copy source tinfo chase yielded nil\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; return ti.size: i32; }; // cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) // expression into dstreg; returns true when the shape is wired, false // otherwise (the caller loud-stops — rule 7, never a silent drop). // F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c // cgplaceaddr: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a // slice/array place, N_DOT struct-field hop with one deref for a // *struct base. C2 (F4 read-walker) adds the N_IDENT root (local / // let / DATA-backed def) so indexed-ident spines resolve too. All // type keys come off the checker-STAMPED tinfo (.type_), never tnode // names — the #209/#211 discipline. Enumerated arms still win at // every dispatch site (they are checked first), so shapes that worked // pre-C1 keep their asm. ADDRESS COMPUTATION ONLY — every call-site // keeps its own load/store/copy emission. Clobbers AX/CX (cgexpr on // index / pointer operands) and balances its own PUSHQ/POPQ; dstreg // must not be AX or CX. fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { if (n == nil) { return false; }; if (n.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.str); if (lc != nil) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), "); emitline(dstreg); emitline("\n"); return true; }; let gok: bool = isletvar(c, n.str); if (!gok) { if (defvarstructinfo(c, n.str) != nil) { gok = true; }; }; if (!gok) { let dtn: *node = defvartnode(c, n.str); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { gok = true; }; }; }; if (gok) { emitline("\tLEAQ\t"); emitsymname(c, n.str); emitline("(SB), "); emitline(dstreg); emitline("\n"); return true; }; return false; }; if (n.kind == nkind.N_UN) { if (n.op != tkind.TK_STAR) { return false; }; // &(*e) is e's value — no load. cgexpr(c, n.lhs); emitline("\tMOVQ\tAX, "); emitline(dstreg); emitline("\n"); return true; }; if (n.kind == nkind.N_INDEX) { let base: *node = n.lhs; let idx: *node = n.rhs; if (base == nil || idx == nil) { return false; }; // C2: any addressable base — recursion decides (deref / // ident / dot / index spine). Ident-rooted shapes with // enumerated arms never reach the resolver (those arms // dispatch first), so their asm is untouched. let bu: *tinfo = base.type_: *tinfo; bu = tichase(bu); if (bu == nil) { return false; }; if (bu.kind != tykind.TY_SLICE && bu.kind != tykind.TY_ARRAY) { return false; }; let et: *tinfo = n.type_: *tinfo; et = tichase(et); if (et == nil) { return false; }; let esz: i32 = et.size: i32; 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 (!cgplaceaddr(c, base, dstreg)) { return false; }; // A slice place holds the {ptr,len,cap} header — the // element base is its .ptr word; an array place IS the // element storage. if (bu.kind == tykind.TY_SLICE) { emitline("\tMOVQ\t("); emitline(dstreg); emitline("), "); emitline(dstreg); emitline("\n"); }; emitline("\tPOPQ\tAX\n"); emitline("\tADDQ\tAX, "); emitline(dstreg); emitline("\n"); return true; }; if (n.kind == nkind.N_DOT) { let base: *node = n.lhs; if (base == nil) { return false; }; let bu: *tinfo = base.type_: *tinfo; bu = tichase(bu); if (bu == nil) { return false; }; let viaptr: bool = false; let st: *tinfo = nil; if (bu.kind == tykind.TY_PTR) { let p: *tinfo = bu.sub; p = tichase(p); if (p != nil) { if (p.kind == tykind.TY_STRUCT) { st = p; viaptr = true; }; }; } else { if (bu.kind == tykind.TY_STRUCT) { st = bu; }; }; if (st == nil) { return false; }; let f: *tfield = st.fields; let foff: i64 = -1; for (f != nil) { if (streq(f.name, n.str)) { foff = f.offset: i64; break; }; f = f.tnext; }; if (foff < 0) { return false; }; if (!cgplaceaddr(c, base, dstreg)) { return false; }; if (viaptr) { emitline("\tMOVQ\t("); emitline(dstreg); emitline("), "); emitline(dstreg); emitline("\n"); }; if (foff != 0) { emitline("\tADDQ\t$"); emitint(foff); emitline(", "); emitline(dstreg); emitline("\n"); }; return true; }; return false; }; 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; // Direct non-ident index bases that match none of the typed arms // below (e.g. a cast-expression base) keep this 8B default — // cs!=ww for narrow elements. Team task #19 (#61-residual B). 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; // #60 (alias arc #5): the base's declared type is an N_IDENT alias // (`type arr = [4]int`). The tnode walks below see only the N_TNAME // leaf — esz fell to the 1-sentinel and the base classified as a // POINTER (MOVQ + no IMULQ → SEGV / prefix-luck reads). Set once // here, consumed by the elem-fact override, the etn fallback and // the LEAQ-vs-MOVQ base classify. let basealias: 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) { // #10: dispatch esz + base-materialization off the // global's RESOLVED type via elemsizeofc, NOT an // N_TARRAY/N_TPTR kind whitelist. A global str (tnode // N_TNAME "str") / slice (N_TSLICE) matched NEITHER old // arm, so esz stayed at the default 8 and the base fell // to the wide-header fallback below (8B stride + full- // word MOVQ) instead of loading the .ptr + an element- // width load. cstage dispatches uniformly off // idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c // N_INDEX); the sister fn cgslice (this file) already // resolves esz via elemsizeofc and the base via // N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align // cgindex UP to that template: any indexable global // resolves esz off the type table, N_TARRAY -> LEAQ (the // symbol IS the storage), every other -> MOVQ name(SB) // (the symbol's first word IS the .ptr). The existing // isglobalptr emission (the loadopsz path below) then // yields the cstage-identical MOVZBQ for a str byte // (esz=1). 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_TARRAY) { isglobalarr = true; } else { isglobalptr = true; }; }; }; // #60: element facts off the chased checker-stamped // tinfos — cstage reads them via type_chase_named/ // idx_eff uniformly (cmd/w6c/cgen.c N_INDEX). let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { basealias = true; }; }; if (basealias) { let et60: *tinfo = tichase(n.type_: *tinfo); if (et60 != nil) { esz = et60.size: i32; signed_elem = typeissigned(et60); float_elem = typeisfloat(et60); f32_elem = typeisf32(et60); }; // global base: array-vs-pointer re-keyed off the // chased BASE kind (cstage isglobal && u->kind == // TY_ARRAY → LEAQ). Runtime-unreachable until the // alias-typed global DATA emit lands (#77/#78); // kept so the read leg is already cs-aligned. if (isglobalarr || isglobalptr) { let bu60: *tinfo = tichase(bt60); if (bu60 != nil) { isglobalarr = bu60.kind == tykind.TY_ARRAY; isglobalptr = !isglobalarr; }; }; }; // Alias-typed ELEMENT under an ident base (`[2]row`, // row = [3]int): elemisarrayc's node walk can't see // through the element's N_TNAME — the chased stamped // element tinfo is the authority (cstage gates on // esubu = type_chase_named(esub) == TY_ARRAY). if (!elem_isarray) { elem_isarray = tinfoisarray(n.type_: *tinfo); }; } 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. // #22 (F7-c3): also stamp elemisstr/elemisslice off the same // chased element tinfo. Without them a chained index whose // element is a str/slice (`m[i][k]` over [N][M]str) loaded // only the ptr word — the 24B/16B header (len/cap) was // dropped (stale BX/CX) → garbage .len downstream. cstage's // idx_eff path classifies the element uniformly via // type_isstr/type_isslice (cmd/w6c/cgen.c); align ww UP by // reading the SAME stamp the esz read above uses. CLASS-N: // the corpus has no chained str/slice element, so the prior // byte-id is preserved (this only fires on the missed shape). let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; signed_elem = typeissigned(et); elemisstr = typeisstr(et); elemisslice = typeisslice(et); float_elem = typeisfloat(et); f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; } else { // Every OTHER non-ident base — N_DOT (`s.arr[i]`), N_UN-deref // (`(*p)[i]`, #61), N_CAST (`(e:*[N]T)[i]`, #19old), N_CALL // (`f()[i]`), N_SLICE (`s[a:b][i]`), N_TYPEASSERT // (`(v as *[N]T)[i]`), … — derives esz/stride/load-width/ // signedness from the checker-stamped index-RESULT tinfo // n.type_ (the element T: u32->4). cstage reads it UNIFORMLY // via idx_eff(base->type)->sub->size with NO node-kind gate // (cmd/w6c/cgen.c:3517-18); wwstage's prior node-kind whitelist // (DOT/UN/CAST only) silently left N_CALL/N_SLICE/N_TYPEASSERT // (and any future base kind) at the 8B default — wrong stride // AND full-word MOVQ load for narrow elements. One stamped-tinfo // read mirrors cstage and closes the class by construction // (#19old + its CALL/SLICE/TYPEASSERT residuals). esz via // dt.size (type table, rule-13). Signedness from the same tinfo // so a signed-narrow element sign-extends on load (loadopsz keys // on (signed,sz); cstage's fldloadop reads it from the element // type — align ww up, #255). The base ADDRESS materialization // below is unchanged; only the stride/load-width was wrong. let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); };}; }; // 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; // idxelemtn drills `*[N]T` to the pointee array's own // element (#61) — an undrilled etn classified the whole // array, missing tagged/str/slice elements behind a // pointer-to-array base. if (bl != nil) { etn = idxelemtn(bl.tnode); } else { etn = idxelemtn(letvartnode(c, base.str)); // #8/GAP-B: a def-global str/slice-array base lives // in c.defs, not c.lets — letvartnode misses it → // etn nil → elem mis-classified scalar, dropping the // 3-word slice-header load (returns element ADDR not // .len; cstage's Type-based classify loads the full // header, the proven let form). defvartnode is the // def-side sister — same fallback as the base-address // resolution at cgenexpr.ww:1762. if (etn == nil) { etn = idxelemtn(defvartnode(c, base.str)); }; }; // #60: an alias-NAMED base has no element tnode // (idxelemtn sees the N_TNAME leaf, nil) — classify // off the stamped index-result n.type_, the same // source the N_DOT/N_INDEX arms read. if (basealias) { etn = n; }; 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); }; // Any NON-ident base (`x.o[i]`, chained `m[i][j]`, `(*p)[i]`, // call `f()[i]`, slice `s[a:b][i]`, typeassert …): the element // tinfo is n.type_ (the checker-stamped indexresult), same source // the esz/str/slice/float flags read above. Mirror cstage // cgen.c:8101 `esubu->kind == TY_TAGGED` — classified for ANY // base, NOT gated on a base-kind whitelist, and NOT excluding the // nullable fold (slot_sz=8 degrades the copy arm to one MOVQ, // matching cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). // #261: without the classify an N_DOT-base tagged element fell to // the scalar loadopsz path and dropped the tag/payload-high word; // #23 (F7-c6): the prior N_DOT/N_INDEX/N_UN whitelist still missed // an N_CALL / N_SLICE base (`f()[i]` / `s[a:b][i]`) → lone // `MOVQ (AX),AX` (tag only) vs cstage's full 4-word cursor // (cs rc=50 / ww rc=40). Dropping the whitelist for the stamp read // closes the base-kind class by construction. if (base.kind != nkind.N_IDENT) { let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { if (typeistagged(dt)) { elem_tagged = true; elem_slot_sz = dt.size: i32; esz = elem_slot_sz; }; }; }; }; // #121 leg (b): whole TUPLE element `let e = tbl[i]`. Classify off // the chased index-result tinfo (n.type_, the same source the N_DOT/ // N_INDEX arms read). gptotal = sum(tupeslot/8); the per-base arms // below fill that many cursor words (tupreg) from the element // address. Mirrors cstage cgen.c N_INDEX TY_TUPLE arms. let elem_tuple: bool = false; let tuple_nwords: i32 = 0; { let eti: *tinfo = tichase(n.type_: *tinfo); if (eti != nil) { if (eti.kind == tykind.TY_TUPLE) { elem_tuple = true; // ww tuples store elements in .tupleelems, not .params. let tpw: *ttupleelem = eti.tupleelems; for (tpw != nil) { tuple_nwords += tupeslot(tpw.type_) / 8; tpw = tpw.tnext; }; };}; }; 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) { // #37: >32B box — ADDRESS in AX (the taggedmemread // convention); the 4-reg cursor walk below would // truncate past payload word 2. Mirrors cstage. if (elem_slot_sz > TUPLE_GPCAP * 8) { emitline("\tMOVQ\tBX, AX\n"); return; }; 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; }; // #121 leg (b): whole TUPLE element — fill gptotal cursor words // from the element address (BX); descending so AX loads last, // mirroring the tagged arm. Over-cap leaves the ADDRESS in AX. if (elem_tuple) { if (tuple_nwords > TUPLE_GPCAP) { emitline("\tMOVQ\tBX, AX\n"); return; }; let kw: i32 = tuple_nwords - 1; for (kw >= 0) { emitline("\tMOVQ\t"); emitdispreg((kw * 8): i64, "BX"); emitline(", "); emitline(tupreg(kw)); emitline("\n"); kw -= 1; }; 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; }; }; // #60: alias-NAMED base — LEAQ-vs-MOVQ off the chased stamped // kind (cstage u->kind == TY_ARRAY, cmd/w6c/cgen.c N_INDEX). if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; }; }; 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) { // #37: >32B box — ADDRESS in AX (the taggedmemread // convention); the 4-reg cursor walk below would // truncate past payload word 2. Mirrors cstage. if (elem_slot_sz > TUPLE_GPCAP * 8) { emitline("\tMOVQ\tBX, AX\n"); return; }; 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; }; // #121 leg (b): whole TUPLE element — twin of the global arm // above (base in BX). Over-cap leaves the ADDRESS in AX. if (elem_tuple) { if (tuple_nwords > TUPLE_GPCAP) { emitline("\tMOVQ\tBX, AX\n"); return; }; let kw: i32 = tuple_nwords - 1; for (kw >= 0) { emitline("\tMOVQ\t"); emitdispreg((kw * 8): i64, "BX"); emitline(", "); emitline(tupreg(kw)); emitline("\n"); kw -= 1; }; 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. #48: the >24 // R8 word was missing ONLY in this fallback arm (both ident // arms have it) — a >24B-slot element via a non-ident base // under-read the cursor and the match spill stored stale R8. // Mirrors cstage cgen.c:9106-9117. // #37: >32B box — AX already holds the element address; // leave it (taggedmemread). Mirrors cstage. if (elem_slot_sz > TUPLE_GPCAP * 8) { return; }; emitline("\tMOVQ\tAX, BX\n"); 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; }; // #121 leg (b) via fallback base: whole TUPLE element. AX holds the // element address — copy to BX (the cursor fill into AX clobbers it), // then fill the gptotal cursor words. Over-cap leaves AX as the addr. if (elem_tuple) { if (tuple_nwords > TUPLE_GPCAP) { return; }; emitline("\tMOVQ\tAX, BX\n"); let kw: i32 = tuple_nwords - 1; for (kw >= 0) { emitline("\tMOVQ\t"); emitdispreg((kw * 8): i64, "BX"); emitline(", "); emitline(tupreg(kw)); emitline("\n"); kw -= 1; }; 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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; // #21: a def/const array dim — non-N_INTLIT — reads its cap // from the stamped array tinfo (rule-13), the cap twin of the // default-hi fix; pre-fix cgbasecap returned false here and the // caller fell to cap=len (cs computes base_cap-lo from bu->alen). let abt: *tinfo = tichase(base.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); emitline(", "); emitline(dst); emitline("\n"); return true; }; return false; }; 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; }; }; // #60: alias-NAMED base — chased kind (cstage cg_base_cap // receives bu pre-chased by the N_SLICE arm, cgen.c:1888). let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { let bu60: *tinfo = tichase(bt60); if (bu60 != nil) { if (bu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(bu60.alen: i64); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (bu60.kind == tykind.TY_SLICE || bu60.kind == tykind.TY_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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; // #21: def/const global array dim cap — twin of the local arm. let abt: *tinfo = tichase(base.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); emitline(", "); emitline(dst); emitline("\n"); return true; }; return false; }; 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; }; // #60: alias-NAMED global base — chased kind; global STR keeps // the #73 cap=len carve-out (cstage isglobal && TY_STR → 0). // Runtime-unreachable until #77/#78 global DATA. let gbt60: *tinfo = base.type_: *tinfo; if (gbt60 != nil) { if (gbt60.kind == tykind.TY_NAMED) { let gbu60: *tinfo = tichase(gbt60); if (gbu60 != nil) { if (gbu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(gbu60.alen: i64); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (gbu60.kind == tykind.TY_SLICE) { 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. // slicebaseesz — element width of a sliceable base, exactly mirroring the // esz cascade cgslice computes inline for ptr-scaling (baselocal/globaltn → // elemsizeofc; N_DOT field → dotbu.sub.size; N_ARRLIT → elemsizeofc; alias- // NAMED override → chased sub.size). The #145 slice-copy-assign arm scales // the COUNT by this same width, so it MUST match cgslice's ptr scaling // byte-for-byte (cgslice supplies the dst ptr). Cstage twin: the N_SLICE-LHS // arm in cgen.c N_ASSIGN reuses the read-path one-liner directly. fn slicebaseesz(c: *cgen, base: *node) i32 = { if (base == nil) { return 1; }; let esz: i32 = 1; if (base.kind == nkind.N_IDENT) { let bl: *local = localfindnode(c, base.str); if (bl != nil) { esz = elemsizeofc(c, bl.tnode); } else { let gt: *node = letvartnode(c, base.str); if (gt != nil) { esz = elemsizeofc(c, gt); }; }; let bt: *tinfo = base.type_: *tinfo; if (bt != nil) { if (bt.kind == tykind.TY_NAMED) { let bu60: *tinfo = tichase(bt); let es60: *tinfo = tichase(bu60.sub); if (es60 != nil) { esz = es60.size: i32; }; };}; } else { if (base.kind == nkind.N_DOT) { let db: *tinfo = tichase(base.type_: *tinfo); if (db != nil) { if (db.sub != nil) { esz = db.sub.size: i32; }; }; } else { if (base.kind == nkind.N_ARRLIT) { esz = elemsizeofc(c, base.lhs); };};}; return esz; }; 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; }; }; }; }; // #252: an N_DOT `[N]T`-field base (`s.obuf[lo:hi]`) carries no // tnode — resolve esz / default-hi / base-address from the checker- // stamped element tinfo on base.type_ instead. Cstage twin reads // base->type (cgen.c N_SLICE esz + bu->kind==TY_ARRAY default-hi). let dotbu: *tinfo = nil; if (base != nil) { if (base.kind == nkind.N_DOT) { dotbu = base.type_: *tinfo; dotbu = tichase(dotbu); };}; // #31: an N_ARRLIT base (the desugared one-step `let xs:[]T=[..]` // borrow — the ONLY context that reaches here; call-arg/return/assign // loud-reject at the checker, #33) has no storage. Its [count]T type // NODE is stashed on base.lhs by checkletassign's #25 re-stamp; size / // count come NODE-wise (elemsizeofc / .rhs intlit), because wwstage // narrow-primitive tinfos are unsized (#8). Cstage twin reads base->type // (its Type IS sized). let arrlittn: *node = nil; if (base != nil) { if (base.kind == nkind.N_ARRLIT) { arrlittn = base.lhs; };}; // #60 (alias arc #5): alias-NAMED N_IDENT base — the tnode reads // below see only the N_TNAME leaf (esz 1-sentinel, MOVQ base, // $0 default-hi, cap=len). All four reads re-key off the chased // stamped tinfo, mirroring cstage N_SLICE's single // bu = type_chase_named(base->type) source (cmd/w6c/cgen.c). let basealias: bool = false; let bu60: *tinfo = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { basealias = true; bu60 = tichase(bt60); };}; };}; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom) or an N_DOT array/slice-field base (#252: scale by // the field's element width, not esz=1 — silently wrong for non-u8). // Other non-ident bases stay esz=1 -> ptr unscaled. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); } else { if (dotbu != nil && dotbu.sub != nil) { esz = dotbu.sub.size: i32; } else { if (arrlittn != nil) { esz = elemsizeofc(c, arrlittn); };};};}; if (bu60 != nil) { let es60: *tinfo = tichase(bu60.sub); if (es60 != nil) { esz = es60.size: i32; }; }; // 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; }; }; // #60: alias-NAMED base — chased kind (see cgindex twin). if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; }; 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). let gisarr: bool = globaltn.kind == nkind.N_TARRAY; // #60: alias-NAMED base — chased kind; runtime-unreachable // until #77/#78 global DATA (see cgindex twin). if (bu60 != nil) { gisarr = bu60.kind == tykind.TY_ARRAY; }; if (gisarr) { 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 && base.kind == nkind.N_ARRLIT && arrlittn != nil) { // #31: materialise the array literal into a FRESH per-borrow // @slicescr stack slot (distinct slot per borrow — a borrow's // backing must outlive the lowering, so it can't share a cached // slot; localalloc is always-fresh, mirror of cstage local_alloc), // fill it via the shared element-fill, then LEAQ the slot as base. // Size/count NODE-wise off the stashed [count]T tnode (#8: tinfo // primitive sizes are 0). Escape (WHY, rob): a `let xs:[]T=[..]; // return xs;` returns a slice into this frame slot, freed on // return = dangling — IDENTICAL to the named-array borrow and // Hare-consistent (no escape analysis / GC / heap promotion; a // local borrowed past its frame is a footgun, not promoted). let cnt: i32 = 0; if (arrlittn.rhs != nil) { if (arrlittn.rhs.kind == nkind.N_INTLIT) { cnt = arrlittn.rhs.uval: i32; }; }; let bsz: i32 = elemsizeofc(c, arrlittn) * cnt; if (bsz < 1) { bsz = 1; }; let scr: i32 = localalloc(c, "@slicescr", bsz, nil); cgarrlitfillbp(c, arrlittn, base, scr); emitline("\tLEAQ\t"); emitoff(scr: i64); emitline("(BP), AX\n"); } else { if (base != nil) { // #252: N_DOT `[N]T`-field base → field ADDRESS via // dotbaseaddr (LEAQ), not the auto-deref VALUE load cgexpr // emits. Sibling of the #135 read-side. if (!dotbaseaddr(c, base, "AX")) { 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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; } else { // #21: a def/const array dim (`[MAX]u8`) is not an // N_INTLIT node, so the literal read above misses it // (MOVQ $0 default-hi -> len 0 / underflow, exit 255). // Read the resolved length from the stamped array // tinfo (rule-13), mirroring cstage's bu->alen. let abt: *tinfo = tichase(base.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); 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; }; };};}; }; // #60: alias-NAMED base default-hi — chased kind (cstage // N_SLICE hi-default reads bu uniformly: TY_ARRAY → $alen, // TY_SLICE/TY_STR → len word at +8). if (!handled && bu60 != nil) { if (bu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(bu60.alen: i64); emitline(", AX\n"); handled = true; } else { if (bu60.kind == tykind.TY_SLICE || bu60.kind == tykind.TY_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 && lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; } else { // #21: a def/const global array dim — non-N_INTLIT, read the // resolved length off the stamped array tinfo (rule-13), the // twin of the local arm above (cstage's bu->alen). let abt: *tinfo = tichase(base.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(abt.alen: i64); 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; };}; // #60: alias-NAMED global base default-hi — chased kind; // runtime-unreachable until #77/#78 global DATA (cstage // emits LEAQ+8 for global SLICE/STR alike). if (!handled && bu60 != nil) { if (bu60.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(bu60.alen: i64); emitline(", AX\n"); handled = true; } else { if (bu60.kind == tykind.TY_SLICE || bu60.kind == tykind.TY_STR) { 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 { if (dotbu != nil && dotbu.kind == tykind.TY_ARRAY) { // #252: default-hi `s.obuf[lo:]` on a struct array-field → // element count from the field's array tinfo. Cstage twin // cgexpr_int(bu->alen) (cgen.c N_SLICE default-hi). emitline("\tMOVQ\t$"); emitint(dotbu.alen: i64); emitline(", AX\n"); } else { if (arrlittn != nil) { // #31: default-hi for the arrlit base = its element count (the // stashed [count]T tnode's .rhs intlit). let hc: i64 = 0i64; if (arrlittn.rhs != nil) { if (arrlittn.rhs.kind == nkind.N_INTLIT) { hc = arrlittn.rhs.uval: i64; }; }; emitline("\tMOVQ\t$"); emitint(hc); emitline(", 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). // Family C (#35): identity-cast peel — see the cgtypetest twin. let scrut: *node = taggedidcastpeel(c, 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 { if (isletvar(c, scrut.str)) { // #87: a tagged-union GLOBAL scrutinee — the box lives // in static DATA at name(SB), not the BP frame. // localfindnode returns nil and the dispatch would read // saved BP as the tag (garbage). The PLAIN-tagged twin // of cstage #78 SB-resolution: LEAQ the address, copy // the box into an @match_spill slot indexed off BP. let gtt: *node = resolvetagged(c, letvartnode(c, scrut.str)); if (gtt != nil && !isnullabletype(gtt)) { scrutt = gtt; let gspill: i32 = matchspillsz(c, gtt); scrutoff = localalloc(c, "@match_spill", gspill, nil); emitline("\tLEAQ\t"); emitsymnamehint(c, scrut.str, c.curmod); emitline("(SB), AX\n"); let gk: i32 = 0; for (gk < gspill) { emitline("\tMOVQ\t"); emitdispreg(gk: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((scrutoff + gk): i64); emitline("(BP)\n"); gk += 8; }; }; }; }; } 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); // #38b: sret-classified tagged call scrutinee — pass // the scrut slot itself as the sret dest and skip the // cursor spill; downstream tag dispatch / case-let // binds already read the slot from memory. Mirrors // cstage cgen.c N_MATCH. let msret: i32 = 0; if (scrut.kind == nkind.N_CALL) { msret = callsretsize(c, scrut); }; if (msret > 0) { c.sretdestoff = scrutoff; cgexpr(c, scrut); c.sretdestoff = 0; } else { if (taggedmemread(c, scrut)) { // #37: >32B box read (insts[pc], t.N) — cgexpr left // its ADDRESS in AX; copy the whole box from memory. // Mirrors cstage cgmatch. cgexpr(c, scrut); let mk37: i32 = 0; for (mk37 < spillsz) { emitline("\tMOVQ\t"); emitdispreg(mk37: i64, "AX"); emitline(", DX\n"); emitline("\tMOVQ\tDX, "); emitoff((scrutoff + mk37): i64); emitline("(BP)\n"); mk37 += 8; }; } else { // #37 (rule 7): a >32B box from a kind with no mem-read // convention would spill the cursor it never filled — // loud, not garbage. Mirrors cstage. if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) { let m37m: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n"; os.write(2, m37m.ptr, m37m.len: u64); os.exit(1); }; // #37 (rule 7) stamped twin: matchscrutt returns nil // for kinds it can't resolve (deref/cast/...), so // spillsz defaults under cap and the guard above is // blind there. cstage sizes the spill from the // stamped s->type, so it louds — key on scrut.type_ // to match. Surfaced by reviewer-37's `match (*p)` // probe on a 56B box. let ms37: *tinfo = scrut.type_: *tinfo; ms37 = tichase(ms37); if (ms37 != nil && ms37.kind == tykind.TY_TAGGED && ms37.size: i32 > TUPLE_GPCAP * 8) { let m37n: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n"; os.write(2, m37n.ptr, m37n.len: u64); os.exit(1); }; // Family C catch-all (rule 7): a widening tagged // cast scrutinee has no cursor — loud. Mirrors // cstage cgmatch. if (scrut.kind == nkind.N_CAST && ms37 != nil && ms37.kind == tykind.TY_TAGGED && ms37.nullable == 0) { let m35m: str = "#35: tagged cast source shape unwired at match (rule 7)\n"; os.write(2, m35m.ptr, m35m.len: u64); os.exit(1); }; 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; let pattype: *tinfo = pat.type_: *tinfo; if (pattype != nil) { // #179: kind-agnostic dispatch on the resolved // tinfo. Cstage cg_tag_for_variant works on // Type, so N_TPTR / N_TFN / N_TPTR(N_TFN) case- // patterns all reach typeeq; the prior pat.kind // gate dropped them to r=-1 → tag 0 collapse. // Slice arm still goes through flatslicevariantidx // (task #19 untyped-elem fallback when typeeq // can't match a (scalar | []T) shape). if (typeisslice(pattype)) { r = flatslicevariantidx(c, scrutt, pat.lhs); } else { r = flatvariantidxt(scrutt.type_: *tinfo, pattype, false); }; }; 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; // Receiver is a NAMED alias chain — peel via aliaslookup // until tn exposes a non-N_TNAME kind (or a struct alias). // Without this, `type vs = *vt` leaves lkind == N_TNAME and // structlookupchain misses (vs is not a struct), so we fall // through to the SB-global fallback and emit a wrong // `MOVQ (SB), AX`. Mirror cstage type_chase_named // (cmd/w6c/cgen.c:144-155); LOOP, not single-peel — Phase-N // builds NAMED chains (project_tinfo_lossy_nominal). Stops // at struct aliases so the existing direct-struct arm below // stays byte-id with pre-fix #22 callers. #191. // // #223: the break is MODULE-AWARE. cstage type_chase_named // follows the resolved NAMED.under pointer (module-correct); // wwstage re-resolves by name (lossy), so a same-module // alias whose leaf collides with a FOREIGN struct of the // same name (io.stream = *vtable vs memio.stream struct) // would wrongly halt the peel at the foreign struct via // structlookup's any-module fallback → field load drops to a // bogus `MOVQ (SB)`. Sibling of #208 (lossy name-keyed // resolution leaking to a global leaf). Break ONLY on a // same-module struct (a genuine struct-value receiver); a // same-module alias keeps peeling; a foreign leaf (in // neither registry for c.curmod) falls back to the prior // any-module heuristic. The broader cross-module same-leaf // STRUCT name-keying at the direct-struct arm below is // filed separately as #224 (not FLIP-triggered). for (tn != nil && tn.kind == nkind.N_TNAME) { if (structsamemod(c, tn.str) != nil) { break; }; let nx: *node = aliassamemod(c, tn.str); if (nx == nil) { if (structlookup(c, tn.str) != nil) { break; }; nx = aliaslookup(c, tn.str); if (nx == nil) { break; }; }; tn = nx; }; if (tn == nil) { return; }; let lkind: nkind = 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 && lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i64; } else { // #56: def/const dim — resolve from the // stamped array tinfo (rule-13), the field- // read twin of the #21 cgslice fix. The dim // node is an N_IDENT(def), not N_INTLIT, so // the literal read above defaults 0; cstage // reads the resolved bu->alen. let abt: *tinfo = tichase(tn.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { alen = abt.alen: 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 { // C-t0/#22: slot stride // (tupeslot accessor). foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; }; if (tp != nil) { let tpt: *node = tp.lhs; // str IS []u8, and a slice is the same 24B // {ptr,len,cap} header — load all three words // into (AX, BX, CX). #28: pre-fix the gate was // str-only, so a SLICE tuple element fell to // the scalar tail below (one ptr word; len/cap // took stale registers). base is BP (frame), // so the triple order has no clobber risk. if (isstrtype(c, tpt) || isslicetype(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; }; // #22a: tagged element — load the box // into the tagged value regs (AX=tag, // DX/CX/R8=payload), the cursor the // is/as spill + match read. Byte-id // twin of cstage's N_DOT TY_TUPLE // tagged arm. if (istaggedtype(c, tpt)) { let eslot: i32 = tupeslotn(tpt); // #37: a >32B box overruns the // 4-reg cursor — leave its // ADDRESS in AX (taggedmemread, // the sret-receive convention); // consumers copy from memory. // Replaces the #22b loud bound. // Mirrors cstage. if (eslot > TUPLE_GPCAP * 8) { emitline("\tLEAQ\t"); emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); return; }; let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((lc.off + foff + k * 8): i64); emitline("(BP), "); emitline(tupreg(k)); emitline("\n"); k += 1; }; return; }; // C-t0: load at the element's NATURAL // width (narrow MOVL/MOVSXD/... at the // slot base), not the 8B slot width — // byte-id twin of cstage's fldloadop // in the N_DOT TY_TUPLE arm. let nsz: i32 = 8; let tpti: *tinfo = tpt.type_: *tinfo; if (tpti != nil) { nsz = tpti.size: i32; }; let op: str = tnodeloadop(c, tpt, nsz); 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 [N]T global pseudo-fields (#7): `.len` is the static // element count (immediate from the array type node's length child); // `.ptr` is the array's base address (LEAQ name(SB)). Without this a // module-level array's `x.len` falls to the module-qualified SB // fallback below and mis-emits `MOVQ len(SB), AX` (linker: undefined // reference to len). Mirror of the local-array arm above and cstage // cg_base_cap's `aimm(bu->alen)` immediate (cgen.c:1692). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let gtn: *node = letvartnode(c, lhs.str); if (gtn != nil) { if (gtn.kind == nkind.N_TARRAY) { if (streq(fld, "ptr")) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), AX\n"); return; }; if (streq(fld, "len")) { let lenn: *node = gtn.rhs; let alen: i64 = 0i64; if (lenn != nil && lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i64; } else { // #56: def/const dim on a let-global array — // resolve from the stamped array tinfo // (rule-13), twin of the local arm above. let abt: *tinfo = tichase(gtn.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { alen = abt.alen: i64; }; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", AX\n"); return; }; }; }; }; }; // GAP-A (#7 def-twin): `def NAME: [N]T = arrlit;` `.len` = static // elem count. The let-global arm above resolves via letvartnode // (c.lets only); a def lives in c.defs, misses it, and falls to the // SB fallback → MOVQ len(SB) (w6l: undefined 'len'). cstage cgen.c // emits MOVQ $alen here. defvartnode is the def-side mirror of // letvartnode (returns dtnode = the N_TARRAY whose .rhs length child // is #11-stamped). `.ptr` mirrors the let-global arm above: the // array's backing pointer ≡ &A[0] = LEAQ name(SB) (drew #13, // .ai/drew-gapa-ptr-ruling.md; GAP-A.ptr now fixed cstage-side too, // so both stages byte-id). `.cap` stays unmirrored — arrays have no // .cap (both checkers reject, task GAP-A.cap). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (streq(fld, "ptr")) { let dtn: *node = defvartnode(c, lhs.str); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), AX\n"); return; }; }; }; if (streq(fld, "len")) { let dtn: *node = defvartnode(c, lhs.str); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { let lenn: *node = dtn.rhs; let alen: i64 = 0i64; if (lenn != nil && lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i64; } else { // #56: def/const dim on a def array — // resolve from the stamped array tinfo // (rule-13), twin of the let-global arm above. let abt: *tinfo = tichase(dtn.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { alen = abt.alen: i64; }; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", AX\n"); return; }; }; }; }; }; // Top-level TUPLE global positional read (C-t3, #48): `g.N` — // LEAQ name(SB) into CX, then load at the element's SLOT offset // (C-t0 layout), the element's natural width. Mirrors the local // N_TTUPLE arm above and cstage's N_DOT TY_TUPLE global base. // Pre-C-t3 the tuple global wasn't in collectlets at all (no // DATA) and the module-leaf fallback mis-emitted the field index // as a symbol (`MOVQ 0(SB), AX`). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let gtt: *node = letvartnode(c, lhs.str); for (gtt != nil && gtt.kind == nkind.N_TNAME) { gtt = aliaslookup(c, gtt.str); }; if (gtt != nil) { if (gtt.kind == nkind.N_TTUPLE) { let gidx: i32 = fldnumidx(fld); if (gidx >= 0) { let gtp: *node = gtt.list; let gfoff: i32 = 0; let gi: i32 = 0; for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { // C-t0/#22: slot stride // (tupeslot accessor). gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; }; if (gtp != nil) { let gpt: *node = gtp.lhs; emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); if (isstrtype(c, gpt)) { // CX (the base) is written LAST so // it survives the +0/+8 reads. emitline("\tMOVQ\t"); emitdispreg((gfoff + 0): i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((gfoff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((gfoff + 16): i64, "CX"); emitline(", CX\n"); return; }; if (isfloattype(c, gpt)) { let mov: str = "MOVSD"; if (isf32type(c, gpt)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(gfoff: i64, "CX"); emitline(", X0\n"); return; }; let gnsz: i32 = 8; let gpti: *tinfo = gpt.type_: *tinfo; if (gpti != nil) { gnsz = gpti.size: i32; }; let gop: str = tnodeloadop(c, gpt, gnsz); emitline("\t"); emitline(gop); emitline("\t"); emitdispreg(gfoff: 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. // C3 (task #8): keyed on the checker-stamped tinfo (lhs.type_ / // idxbase.type_), not tnode KINDs + structlookup-by-name — the // name-key dropped any base typed via an N_TNAME alias (`type // result = []capture`, F10) into the silent fallbacks below. // Mirrors cstage cgen.c case N_DOT N_INDEX-lhs arm 1:1; #209/#211 // name-keyed→tinfo-SSoT cluster. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let elemt: *tinfo = lhs.type_: *tinfo; let elemu: *tinfo = elemt; elemu = tichase(elemu); let st: *tinfo = nil; let viaptr: bool = false; if (elemu != nil) { if (elemu.kind == tykind.TY_PTR) { let pin: *tinfo = elemu.sub; pin = tichase(pin); if (pin != nil) { if (pin.kind == tykind.TY_STRUCT) { st = pin; viaptr = true; };}; } else { if (elemu.kind == tykind.TY_STRUCT) { st = elemu; };}; }; if (st != nil) { let fnd: *tfield = nil; let fwalk: *tfield = st.fields; for (fwalk != nil) { if (streq(fwalk.name, fld)) { fnd = fwalk; break; }; fwalk = fwalk.tnext; }; let bu: *tinfo = idxbase.type_: *tinfo; bu = tichase(bu); let baseisarray: bool = false; let baseok: bool = false; if (bu != nil) { if (bu.kind == tykind.TY_ARRAY) { baseisarray = true; baseok = true; }; if (bu.kind == tykind.TY_SLICE) { baseok = true; }; if (bu.kind == tykind.TY_PTR) { baseok = true; }; }; let lc: *local = localfindnode(c, idxbase.str); // #21 (READ twin of #11): a module-GLOBAL base makes // localfindnode return nil, so this field-offset-aware // branch was skipped and `g[i].field` fell through to // the module-qualified fallback below (garbage — no // main.g load at all). Classify via the let registry / // array-typed def registry (cstage let_islet || // def_isarraydef) and dispatch the base load by shape: // array -> LEAQ name(SB) (the symbol IS the storage), // slice/ptr -> MOVQ name(SB) (the symbol's first word // IS the .ptr). Mirrors cstage cgen.c's #21 arm. let isglobal: bool = false; if (lc == nil) { if (isletvar(c, idxbase.str)) { isglobal = true; } else { let dtn: *node = defvartnode(c, idxbase.str); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { isglobal = true; }; }; }; }; if (fnd != nil && baseok && (lc != nil || isglobal)) { let esz: i32 = elemt.size: i32; cgexpr(c, lhs.rhs); // idx → AX if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobal) { if (baseisarray) { emitline("\tLEAQ\t"); emitsymname(c, idxbase.str); emitline("(SB), BX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, idxbase.str); emitline("(SB), BX\n"); }; } else { 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"); }; let foff: i64 = fnd.offset: i64; let ft: *tinfo = fnd.type_; let fu: *tinfo = ft; fu = tichase(fu); // #270-1a: an `[N]T`-typed field of an // array element (`a[i].m[j]`) — leave the // field's ADDRESS, a base for the outer // index, NEVER deref. AX holds &a[i]; the // field address is &a[i]+foff. The #135 // read-side for `d.m[i]`, applied to an // array-element base. Without this an array // field fell to the scalar load below and loaded // its first 8 bytes as a value → garbage // base → SEGFAULT in the outer index. if (fu != nil && fu.kind == tykind.TY_ARRAY) { if (foff != 0) { emitline("\tADDQ\t$"); emitint(foff); emitline(", AX\n"); }; return; }; if (fu != nil && (fu.kind == tykind.TY_STR || fu.kind == tykind.TY_SLICE)) { // 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(foff + 8, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg(foff + 16, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(foff, "AX"); emitline(", AX\n"); return; }; if (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(foff, "AX"); emitline(", X0\n"); return; }; // #58: a TAGGED field of an indexed array // element (`xs[i].f`). AX holds &xs[i]; load // the box cursor (AX=tag, DX/CX/R8=payload) // mirroring taggedmemread's <=32B convention, // tag LAST (it clobbers the base AX). Without // this arm the field fell to the scalar load // below, reading only the tag word and leaving // the payload cursor (DX) stale (`xs[i].f as // T` read garbage; #38a INDEX-spine residual). // >32B box: a wide-box union (largest variant // >32B) IS constructible via a NARROW variant // (not unbuildable as earlier triage assumed; // #54/#23 fires only on STRUCT-LITERAL // payloads), but the mem-based read (LEAQ // foff(AX),AX) is not yet wired here — so this // arm LOUD-STOPS rather than silently reading a // truncated box (rule 7, the #41 untested-arm // trap), byte-id-neutral. Reachable + pinned // expect-loud (test/wcc/944 cfail rows). When // #114 wires it, that commit replaces this with // the LEAQ box-address emission + a >32B value // pin row. Mirrors cstage cgen.c. if (fu != nil && fu.kind == tykind.TY_TAGGED) { let bsz: i32 = fu.size: i32; if (bsz > TUPLE_GPCAP * 8) { let m58r: str = "#58: >32B tagged-field indexed read unreachable until #114\n"; os.write(2, m58r.ptr, m58r.len: u64); os.exit(1); }; if (bsz > 24) { emitline("\tMOVQ\t"); emitdispreg(foff + 24, "AX"); emitline(", R8\n"); }; if (bsz > 16) { emitline("\tMOVQ\t"); emitdispreg(foff + 16, "AX"); emitline(", CX\n"); }; if (bsz > 8) { emitline("\tMOVQ\t"); emitdispreg(foff + 8, "AX"); emitline(", DX\n"); }; emitline("\tMOVQ\t"); emitdispreg(foff, "AX"); emitline(", AX\n"); return; }; let fsz: i32 = 8; if (ft != nil) { fsz = ft.size: i32; }; let lop: str = loadopsz(typeissigned(ft), fsz); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(foff, "AX"); emitline(", AX\n"); return; }; }; };}; }; }; // #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed // TUPLE element. The struct N_INDEX-lhs block above handles // struct / ptr-to-struct elements; a tuple element fell through // to the read-resolver and died LOUD. Resolve &tbl[i] via the // place-spine (cgplaceaddr, the #116 mechanism) into BX→AX, then // read the field at addr+foff reusing the per-element-kind arms // the local N_TTUPLE block wires: str-triple / float-X0 / fn-or- // scalar loadopsz. Narrow (rob Q3): tagged / nested-aggregate // fields stay LOUD. Mirrors cstage cgen.c leg-(a) arm 1:1. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let eu: *tinfo = tichase(lhs.type_: *tinfo); if (eu != nil) { if (eu.kind == tykind.TY_TUPLE) { let idx: i32 = fldnumidx(fld); if (idx >= 0) { // ww tuples store elements in .tupleelems // (ttupleelem chain), NOT .params (that is // fn-only). foff via tupeslot accumulation = // cstage tuple_eslot, byte-id. let tp: *ttupleelem = eu.tupleelems; let foff: i32 = 0; let i: i32 = 0; for (i < idx) { if (tp == nil) { i = idx; } else { foff += tupeslot(tp.type_); tp = tp.tnext; i += 1; }; }; if (tp != nil) { let ft: *tinfo = tp.type_; let fu: *tinfo = tichase(ft); if (fu != nil && (fu.kind == tykind.TY_TAGGED || fu.kind == tykind.TY_STRUCT || fu.kind == tykind.TY_TUPLE || fu.kind == tykind.TY_ARRAY)) { let mt: str = "#121: aggregate/tagged tuple-element field read off an indexed base unwired\n"; os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; if (!cgplaceaddr(c, lhs, "BX")) { let mp: str = "#121: indexed tuple base not place-resolvable\n"; os.write(2, mp.ptr, mp.len: u64); os.exit(1); }; emitline("\tMOVQ\tBX, AX\n"); if (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(foff: i64, "AX"); emitline(", X0\n"); return; }; if (fu != nil && (fu.kind == tykind.TY_STR || fu.kind == tykind.TY_SLICE)) { emitline("\tMOVQ\t"); emitdispreg((foff + 8): i64, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((foff + 16): i64, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(foff: i64, "AX"); emitline(", AX\n"); return; }; let fsz: i32 = 8; if (ft != nil) { fsz = ft.size: i32; }; let lop: str = loadopsz(typeissigned(ft), fsz); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(foff: i64, "AX"); emitline(", AX\n"); return; }; }; };}; }; }; // 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)); // #229: thread the dotted module (lhs.str), not c.curmod // — the non-preferring emitsymname mis-mangled `aa.v` onto // a same-leaf global. The TY_FN branch above already // threads lhs.str via emitfnname. if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymnamehint(c, fld, lhs.str); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymnamehint(c, fld, lhs.str); 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; }; // tagged leaf (#38a): load the box into the tagged // cursor via cgloadtaggedfield (AX=tag, DX=word0, // R8=word2 before CX=word1 — CX may be the base; // >32B box leaves its ADDRESS in AX, the #37 // convention) — the single-dot tagged-field arm // verbatim. Pre-#38a the scalar loadopsz tail pulled // ONE word (the tag): is-tests passed by tag-luck // while as/match/let consumers read stale payload // registers (ken x5c: o.r.min as size added DX). if (typeistagged(leaftype)) { let tlu: *tinfo = leaftype; tlu = tichase(tlu); let ttsz: i32 = tlu.size: 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"); }; cgloadtaggedfield(c, "CX", totaloff, ttsz); } else { cgloadtaggedfield(c, "BP", rootoff + totaloff, ttsz); }; return; }; if (typeisstr(leaftype) || typeisslice(leaftype)) { // str/slice leaf: load all three header words into // (AX=ptr, BX=len, CX=cap). str IS []u8 — the same 24B // {ptr,len,cap} header. #29: the str leaf used to load // only ptr+len here (cap dropped → a junk strlit before // the chain left CX stale); merged into the slice arm so // both load the full triple, both stages (#263). 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 do not 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`. // A string literal is TY_UNTYPED_STR, so it misses the typed // slice/str gate above and lands here. Evaluate the str-producing // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP // to this shuffle in #14 (it had returned the ptr for `.len`). // C2 (F4): gated to a slice/str/untyped-str STAMPED base (or an // N_STRLIT, which ww types as `str` anyway) — pre-C2 this arm was // shape-blind, so a STRUCT field that merely shares a pseudo-field // NAME behind a non-ident spine took the offset-blind cgexpr path // while cstage (type-gated) routes it to the read-resolver. { let pbu: *tinfo = nil; if (lhs != nil) { pbu = lhs.type_: *tinfo; }; pbu = tichase(pbu); let pbok: bool = false; if (pbu != nil) { if (pbu.kind == tykind.TY_SLICE || pbu.kind == tykind.TY_STR || pbu.kind == tykind.TY_UNTYPED_STR) { pbok = true; }; }; if (lhs != nil) { if (lhs.kind == nkind.N_STRLIT) { pbok = true; }; }; if (pbok) { if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; if (streq(fld, "len")) { cgexpr(c, lhs); emitline("\tMOVQ\tBX, AX\n"); return; }; // .cap on a non-ident base (indexed element `t[i].cap`, // call, dot-slice): cgexpr leaves the full {ptr,len,cap} // header via cgslicehdr — shuffle CX→AX. The shuffle // fires ONLY for a TYPED slice/str base (kind TY_SLICE/ // TY_STR after NAMED-chase) that is NOT a bare string // literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len // (cgen.ww), never a CX cap, so `"abc".cap` must return // AX unshuffled. cstage reaches that outcome by typing // N_STRLIT as untyped_str (cgen.c catch-all, no cap // shuffle); the wwstage checker types N_STRLIT as `str` // instead (check.ww:2322 vs cstage check.c:1079 — // divergence filed separately), so the TY_STR kind-gate // alone would wrongly fire. The N_STRLIT exclusion keeps // this byte-identical with cstage. #13 read-fix, sibling // of the #20 store. if (streq(fld, "cap")) { cgexpr(c, lhs); if (lhs != nil && lhs.kind != nkind.N_STRLIT) { if (pbu != nil && (pbu.kind == tykind.TY_SLICE || pbu.kind == tykind.TY_STR)) { emitline("\tMOVQ\tCX, 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; ct = tichase(ct); let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; cs = tichase(cs); if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr && croot != nil && croot.kind == nkind.N_IDENT && localfindnode(c, croot.str) != nil) { it = lhs.type_: *tinfo; }; it = tichase(it); if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; st = tichase(st); 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 // tagged leaf (#38a): AX holds the // *struct base and the tagged cursor // targets AX (tag) — stage the base in // BX, then cgloadtaggedfield (cstage // chained-*struct twin; ken b8: the // scalar tail read stale DX as payload). if (typeistagged(ft)) { let plu: *tinfo = ft; plu = tichase(plu); emitline("\tMOVQ\tAX, BX\n"); cgloadtaggedfield(c, "BX", tf.offset: i32, plu.size: i32); return; }; // 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 (aliasprimsize(c, 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. C2 (F4): gated to UNTYPED // chains only — pre-C2 it swallowed every unmatched dot-over-dot // chain, turning a TYPED depth-2 read behind an index/deref spine // into a silent global read of a colliding leaf symbol. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let sbt: *tinfo = lhs.type_: *tinfo; if (sbt == nil || sbt.kind == tykind.TY_ERR) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); return; }; }; }; // cstage reads ptr-chained fields (`a.p.f`, any root) in its // chained-*struct arm; wwstage's #70 mirror above is gated // root-local — the uncovered remainder (global / indexed roots) // must not take the resolver (its sequence differs from cstage's // arm → cs≠ww). Loud; the wwstage alignment is filed as task // #37. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let pgu: *tinfo = lhs.type_: *tinfo; pgu = tichase(pgu); if (pgu != nil) { if (pgu.kind == tykind.TY_PTR) { let mp: str = "cgdot: ptr-chained field read unwired in wwstage (task #37)\n"; os.write(2, mp.ptr, mp.len: u64); os.exit(1); }; }; }; }; // C2 read-resolver (F4 + FA3): a TYPED N_DOT read no enumerated // arm matched — depth-2+ chains and slice/str/scalar fields behind // index/deref spines. Address via cgplaceaddr (the C1 resolver), // leaf load emitted here by kind. Leaf kinds with no canonical // register convention in expr position stay LOUD; any shape the // resolver can't address dies LOUD (rule 7) — the pre-C2 tail // silently emitted NOTHING. Mirror of cstage cgen.c case N_DOT // read-resolver tail. { let rdt: *tinfo = n.type_: *tinfo; let rdu: *tinfo = rdt; rdu = tichase(rdu); if (rdu != nil) { if (rdu.kind == tykind.TY_TAGGED) { let mt: str = "read-resolver: tagged field read not wired (rule-7)\n"; os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; // LET-position aggregate leaves route through cglet's // resolver copy (C4, task #7) before cgexpr ever sees // them; this loud guards the remaining non-let expr // positions (no register convention for a >8B leaf), // symmetric with cstage's read-resolver tail. if (rdu.kind == tykind.TY_STRUCT || rdu.kind == tykind.TY_TUPLE) { let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n"; os.write(2, ma.ptr, ma.len: u64); os.exit(1); }; }; if (!cgplaceaddr(c, n, "BX")) { let mu: str = "unsupported field-read shape\n"; os.write(2, mu.ptr, mu.len: u64); os.exit(1); }; if (typeisfloat(rdt)) { let mov: str = "MOVSD"; if (typeisf32(rdt)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t(BX), X0\n"); return; }; if (rdu != nil && rdu.kind == tykind.TY_ARRAY) { // `[N]T` leaf: leave the field ADDRESS — a base for // an outer index, never a value (#270-1a semantics). emitline("\tMOVQ\tBX, AX\n"); return; }; if (rdu != nil && (rdu.kind == tykind.TY_STR || rdu.kind == tykind.TY_SLICE)) { // str IS []u8 — 3-word {ptr,len,cap} into (AX, BX, // CX). BX is the place base, so load .len (which // targets BX) LAST. emitline("\tMOVQ\t(BX), AX\n"); emitline("\tMOVQ\t16(BX), CX\n"); emitline("\tMOVQ\t8(BX), BX\n"); return; }; let lop: str = "MOVQ"; if (rdt != nil) { lop = loadopsz(typeissigned(rdt), rdt.slotsize: i32); }; emitline("\t"); emitline(lop); emitline("\t(BX), AX\n"); 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; }; // #180: address-of a top-level fn name. Twin of // the N_IDENT value-of-fn read-arm in cgident // (LEAQ + emitfnname(c, nm, c.curmod)). Previously // fell through silently — the AX-store at the // assign site picked up whatever AX held. if (fnretlookup(c, nm) != nil) { emitline("\tLEAQ\t"); emitfnname(c, nm, c.curmod); emitline("(SB), 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. // #102 (ken B6-c3 re-attribution): an // alias-NAMED pointee misses the bare // name-keyed lookup, so &p.f fell to the // generic cgplaceaddr route — runtime- // correct but byte-divergent from the // dedicated shape cs pins post-B6-c3. // structlookupchain (#22) chases the alias // chain; plain rows short-circuit at its // structlookup head, byte-id by // construction. 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 = structlookupchain(c, inner); 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. // #102 review-found sibling: same // alias-blind miss one leg below the // &p.f gate — an alias-NAMED value // struct fell to the generic route. // Same chase, same construction. if (lkind == nkind.N_TNAME) { let si: *structinfo = structlookupchain(c, tn); 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; }; // #229: dotted-module value mangle // (basenm), twin of the read — so // &aa.v takes aa's global, not a // same-leaf collision. emitline("\tLEAQ\t"); emitsymnamehint(c, fld, basenm); emitline("(SB), AX\n"); return; }; }; }; }; // C2 (F4 family, reviewer-A route): address-of // through an indexed/deref dot spine — the // arms above root only at idents. Route the // place address through cgplaceaddr (read-twin // in cgdot). Any remaining shape dies LOUD: // the pre-C2 silent drop left stale AX as the // "address" — a gate-blind SEGFAULT at the // deref. Mirror of cstage TK_AMP tail. if (cgplaceaddr(c, opnd, "BX")) { emitline("\tMOVQ\tBX, AX\n"); return; }; let mam: str = "unsupported address-of shape\n"; os.write(2, mam.ptr, mam.len: u64); os.exit(1); }; 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 { // #11: addr-of twin of the #10 cgindex read // fix. Dispatch esz + base load off the // global's RESOLVED type, NOT an N_TARRAY/ // N_TPTR kind whitelist — a global str (tnode // N_TNAME) / slice (N_TSLICE) matched NEITHER // old arm, so esz stayed at the default 8 and // the base fell to the complex-base fallback, // yielding a wide-stride &s[i]. cstage's // TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform: // esz=bu->sub->size, base is_arr?LEAQ:MOVQ // name(SB) (a str/slice's .ptr IS the symbol's // first word). Align UP, mirroring cgindex. let tn: *node = letvartnode(c, base.str); // #94: a def-array base resolves via // defvartnode (the def-side sister), the same // fallback cgindex's read-side already takes // (cgenexpr.ww:1762). Without it &D[i] over a // def array fell to the complex-base value-load // below (MOVQ name(SB) = D[0] not the address), // divergent from cstage's zero-base SEGV — both // wild. The N_TARRAY tnode classifies isglobalarr // → LEAQ name(SB), the cstage-identical base. if (tn == nil) { tn = defvartnode(c, base.str); }; if (tn != nil) { globalname = base.str; esz = elemsizeofc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; } else { isglobalptr = true; }; }; }; // #82: the tnode-kind classify above misses an // alias-typed base (N_TNAME) — base materialized // as MOVQ (element-0 VALUE) instead of LEAQ, a // wild pointer. Re-key arrayness off the chased // checker-stamped base type, the cgindex #60 // idiom (cgenexpr.ww:1800-1820); cstage already // classifies off type_chase_named uniformly // (cmd/w6c/cgen.c:4172-4188). TY_NAMED gate // keeps non-alias rows byte-id by construction. let bt82: *tinfo = base.type_: *tinfo; let basealias82: bool = false; if (bt82 != nil) { if (bt82.kind == tykind.TY_NAMED) { basealias82 = true; }; }; if (basealias82) { let bu82: *tinfo = tichase(bt82); if (bu82 != nil) { if (baselocal != nil) { isarr = bu82.kind == tykind.TY_ARRAY; }; if (isglobalarr || isglobalptr) { isglobalarr = bu82.kind == tykind.TY_ARRAY; isglobalptr = !isglobalarr; }; }; }; } else { if (base.kind == nkind.N_DOT || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // `&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. // N_UN deref base (`&(*p)[i]`, #61 C): same // stamped source; cstage reads base->type // uniformly. 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). // #252: an N_DOT `[N]T`-field base needs the // field ADDRESS (dotbaseaddr LEAQ), not the // auto-deref VALUE load cgexpr emits. Sibling // of the #135 read-side wiring. emitline("\tPUSHQ\tAX\n"); if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; emitline("\tPOPQ\tBX\n"); };};}; emitline("\tADDQ\tBX, AX\n"); return; }; // C2: deref-rooted (`&(*p)`) and other non-ident // operands — resolver-or-loud, the same tail as the // N_DOT arm above (cstage has ONE shared tail for // both). if (cgplaceaddr(c, opnd, "BX")) { emitline("\tMOVQ\tBX, AX\n"); return; }; let mam2: str = "unsupported address-of shape\n"; os.write(2, mam2.ptr, mam2.len: u64); os.exit(1); }; 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) { // #185: deref of *fn — the pointer value IS the fn address. // cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX // would load the first instruction word and a subsequent // CALL would segfault. Mirror ref/harec/src/check.c // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. // #61 C: same skip for an ARRAY pointee — an array value IS // its address everywhere in this cgen (#270-1a), so `*p` on // `*[N]T` leaves AX = p's value. The scalar load below // pulled a[0]'s VALUE and `(*p)[i]` then dereferenced it as // the index base — a wild pointer, SIGSEGV on both stages. let rti: *tinfo = n.type_: *tinfo; rti = tichase(rti); if (rti != nil && rti.kind == tykind.TY_FN) { return; }; if (rti != nil && rti.kind == tykind.TY_ARRAY) { return; }; // Family C (#35/#46): a tagged box behind *p joins the // mem-based class at ANY size (taggedmemread) — AX = p's // value IS the box address. The scalar load below pulled // word0 (the tag) and every cursor consumer transported // garbage payload words — silent-wrong both stages (ken // f35/D3a/D3b). The nullable one-word fold stays a scalar // deref. Mirrors cstage N_UN TK_STAR. if (rti != nil && rti.kind == tykind.TY_TAGGED) { if (rti.nullable == 0 && rti.size: i32 > 8) { return; }; }; // 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; }; // cgstreqpush — push one str operand's (len, then ptr) header words for // the rt_streq content-compare in cgbin. Mirrors cstage cgen.c:4568-4615: // an ident loads its 2-word header (ptr+len, NOT cap) from name(SB) for a // module-global str (#154 let_islet branch) or BP+off for a local; a // non-ident evals via cgexpr (AX=ptr, BX=len) and pushes BX then AX. Push // order is len then ptr so the matching POPQ pops ptr first. fn cgstreqpush(c: *cgen, op: *node) void = { if (op.kind == nkind.N_IDENT) { let nm: str = op.str; let lc: *local = localfindnode(c, nm); if (lc == nil && isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymnamehint(c, nm, c.curmod); emitline("(SB), BX\n"); emitline("\tMOVQ\t8(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); return; }; let off: i32 = 0; if (lc != nil) { off = lc.off; }; emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\t"); emitoff(off: i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); return; }; cgexpr(c, op); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); }; 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; }; // #146 (#154 ww-twin): str ==/!= is a CONTENT compare via rt_streq, // not a ptr compare. Must run before the generic eager-eval tail // below collapses each str header to its ptr word (AX). Push rhs // then lhs (len, ptr each); POPQ DI/SI/DX/CX lands a.ptr,a.len, // b.ptr,b.len per rt/streq.s; CALL rt_streq -> AX in {0,1}; XOR 1 // for !=. The gate reads the checker stamp (typeisstr) exactly like // cstage node_isstr. Byte-identical to cstage cbinop (cmd/w6c/ // cgen.c:4564-4623). cstage was fixed by #154; this is its mirror. if ((n.op == tkind.TK_EQ || n.op == tkind.TK_NEQ) && n.lhs != nil && n.rhs != nil && typeisstr(n.lhs.type_: *tinfo) && typeisstr(n.rhs.type_: *tinfo)) { cgstreqpush(c, n.rhs); cgstreqpush(c, n.lhs); emitline("\tPOPQ\tDI\n\tPOPQ\tSI\n\tPOPQ\tDX\n\tPOPQ\tCX\n"); emitline("\tCALL\trt_streq(SB)\n"); if (n.op == tkind.TK_NEQ) { emitline("\tXORQ\t$1, AX\n"); }; 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) { // #26: chase the alias chain on the literal's type ref so an // alias head (`type pt = point; alloc(pt{...})`) resolves to // the underlying struct's layout — name-keyed structlookup on // the syntactic head missed it (under-alloc $8 + zero field // stores). Mirrors cstage cgen.c:8223-8240 (type_default chases // named before sizing/walking fields); same alias-chase helper // the #22 sites use. si = structlookupchain(c, v.lhs); 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 { // #57 (retained cs!=ww, deferred): scalar/ptr alloc keeps the // default-8 size + MOVQ store; cstage sizes the scalar exactly // ($1/MOVB for u8) and the latent alloc(str|slice) wants 24B not 8. // Byte-id-visible, runtime-benign; not folded into the #26 arm. 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); }; // cgappendgrow — FA1 (#15): append() header-place grow, cgplaceaddr's // append consumer. direct = ident-local header in the frame (BP-disp — // the legacy emission, kept byte-identical); indirect = header address // pre-spilled to @apphdrscr by the resolver. len+=1, &hdr→DI, esz→SI, // CALL rt_ensure. In indirect mode the len bump goes through DI so the // loaded address doubles as the call argument. Mirrors cstage // cg_append_grow. fn cgappendgrow(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32) void = { if (direct) { emitline("\tADDQ\t$1, "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tLEAQ\t"); emitoff(off: i64); emitline("(BP), DI\n"); } else { emitline("\tMOVQ\t"); emitoff(scr: i64); emitline("(BP), DI\n"); emitline("\tADDQ\t$1, 8(DI)\n"); }; emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", SI\n"); emitline("\tCALL\trt_ensure(SB)\n"); }; // cgappendslot — post-rt_ensure slot address: CX = (len-1)*esz, // dst = .ptr + CX. Clobbers AX (the IMUL immediate) and CX, like the // emission it replaces; dst must not be AX or CX. Mirrors cstage // cg_append_slot. fn cgappendslot(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32, dst: str) void = { if (direct) { emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), CX\n"); } else { emitline("\tMOVQ\t"); emitoff(scr: i64); emitline("(BP), "); emitline(dst); emitline("\n"); emitline("\tMOVQ\t8("); emitline(dst); emitline("), 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"); }; if (direct) { emitline("\tMOVQ\t"); emitoff(off: i64); emitline("(BP), "); emitline(dst); emitline("\n"); } else { emitline("\tMOVQ\t("); emitline(dst); emitline("), "); emitline(dst); emitline("\n"); }; emitline("\tADDQ\tCX, "); emitline(dst); emitline("\n"); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: // ; cgexpr → AX // ; PUSHQ AX // ; ADDQ $1, s.len(BP) // ; LEAQ s(BP), DI ; arg1 = &s // ; MOVQ esz, SI ; arg2 = membsz // ; CALL rt_ensure(SB) // ; MOVQ s.len(BP), CX ; CX = new len // ; SUBQ $1, CX ; CX = slot index // ; [IMULQ esz, CX] ; byte offset (esz>1) // ; MOVQ s.ptr(BP), BX // ; ADDQ CX, BX // ; POPQ AX // ; MOV* AX, (BX) ; store (MOVB / MOVQ) // nkind.N_SPREAD wraps the same body in a counted loop over items.len. fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; // FA1 (#15): the old `sn.kind != N_IDENT → return` and // `snlocal == nil → return` gates were SILENT zero-emission // (gate-blind cs≠ww: cstage 0-defaulted the header base and // corrupted the caller frame instead). A non-ident-local target // now resolves its header address through cgplaceaddr; a shape // the resolver can't address is loud. let sndirect: bool = false; let sn_off: i32 = 0; let snlocal: *local = nil; if (sn.kind == nkind.N_IDENT) { snlocal = localfindnode(c, sn.str); if (snlocal != nil) { sndirect = true; sn_off = snlocal.off; }; }; let esz: i32 = 0; let etnode: *node = nil; let sti: *tinfo = nil; if (sndirect) { // #34: esz off the DECLARED slice local's stamped tnode via // elemsizeofc — bare elemsizeof returns the 8 sentinel for a // named tagged/struct element (the #8 family; cgappend was never // upgraded), under-feeding rt_ensure's membsz AND mis-striding // the slot index vs cstage's su->sub->size. esz = elemsizeofc(c, snlocal.tnode); 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; }; sti = snlocal.tnode.type_: *tinfo; }; } else { // FA1: `*p` has no declared tnode — key esz/element kind off // the checker-STAMPED target tinfo (#209/#211 discipline), the // same source cstage reads (sn->type → su->sub->size). sti = sn.type_: *tinfo; let fsti: *tinfo = sti; fsti = tichase(fsti); if (fsti != nil && fsti.sub != nil) { esz = fsti.sub.size: i32; }; if (esz <= 0) { let m15z: str = "#15: append() target element size unresolved (rule-7)\n"; os.write(2, m15z.ptr, m15z.len: u64); os.exit(1); }; }; let store_op: str = tnodestoreop(c, etnode, esz); // #34 element-kind store dispatch: the scalar 1-word store below // silently gutted every wide element (str/slice 24B header, // tagged box, struct body). Kind off the stamped slice tinfo — // the value node's literal tinfo is the #25/#31 esz=0 trap. // Mirrors cstage cgen.c's append arm + the #270/#12/#20 // array-literal element dispatch (cgarrlitfillbp). sti = tichase(sti); let esubti: *tinfo = nil; if (sti != nil) { esubti = sti.sub; }; // FA1: pre-peel handle — the indirect struct-lit fill keys its // structinfo off the NAMED element tinfo's name (the same leaf // structlookupchain resolves from the declared tnode). let esubnamed: *tinfo = esubti; esubti = tichase(esubti); let elstr: bool = esubti != nil && esubti.kind == tykind.TY_STR; let elslice: bool = esubti != nil && esubti.kind == tykind.TY_SLICE; let eltagged: bool = esubti != nil && esubti.kind == tykind.TY_TAGGED; let elstruct: bool = esubti != nil && esubti.kind == tykind.TY_STRUCT; let elwide: bool = elstr || elslice || eltagged || elstruct; if (!elwide && esz > 8) { let m34k: str = "#34: append() element kind unsupported (rule-7)\n"; os.write(2, m34k.ptr, m34k.len: u64); os.exit(1); }; let snscr: i32 = 0; if (!sndirect) { if (!cgplaceaddr(c, sn, "BX")) { let m15p: str = "#15: append() target place unsupported (rule-7)\n"; os.write(2, m15p.ptr, m15p.len: u64); os.exit(1); }; // Spill across rt_ensure: realloc moves .ptr, never the // header, so the slot stays valid for every later reload. // Fresh slot per SITE via localalloc (NOT localadd: its `@` // dedup would share one slot per fn, and a nested // append-through-pointer inside a value expression — // match-yield arm — would clobber the outer's spilled header // address: silent cross-slice corruption. Mirrors cstage's // never-deduping local_alloc at the same point.) snscr = localalloc(c, "@apphdrscr", 8, nil); emitline("\tMOVQ\tBX, "); emitoff(snscr: i64); emitline("(BP)\n"); }; let vn: *node = sn.next; for (vn != nil) { if (vn.kind == nkind.N_SPREAD) { // #34 review: a non-ident/non-local spread source used // to be silently SKIPPED here while cstage fell past // its spread arm into the single-value stores with the // N_SPREAD node (garbage store) — divergent. let it: *node = vn.lhs; // #35: only a {ptr,len,cap}-headered source reads as a // header below; a [N]T array place IS its storage — the // ident path used to read its first 16 data bytes as // ptr/len, silently. Loud until wired (task #27); str // shares the slice header layout. let itu: *tinfo = nil; if (it != nil) { itu = it.type_: *tinfo; }; itu = tichase(itu); if (itu == nil || (itu.kind != tykind.TY_SLICE && itu.kind != tykind.TY_STR)) { let m35p: str = "#35: append() spread source shape unsupported (rule-7)\n"; os.write(2, m35p.ptr, m35p.len: u64); os.exit(1); }; let it_off: i32 = 0; let itscr: i32 = 0; if (it.kind == nkind.N_IDENT) { let itlocal: *local = localfindnode(c, it.str); if (itlocal != nil) { it_off = itlocal.off; }; }; if (it_off == 0) { // #35: place-chain source (deref spine, indexed // chain, global ident — the regex.ha:569/820 dup // shapes) resolves its header ADDRESS through // cgplaceaddr ONCE, pre-grow: the chain's rvalues // run exactly once (the #49 split's pre-grow // half) and every iteration re-reads .ptr/.len // THROUGH the spilled header post-grow (the live // re-derivation half). A header reached through a // buffer the grow reallocs keeps Hare's // stale-base hole — see the #49 comment below. // Rvalue sources (CALL, slicing exprs) have no // place — loud, task #27. Fresh spill slot per // SITE for the same nesting reason as @apphdrscr // above. if (!cgplaceaddr(c, it, "BX")) { let m35q: str = "#35: append() spread source shape unsupported (rule-7)\n"; os.write(2, m35q.ptr, m35q.len: u64); os.exit(1); }; itscr = localalloc(c, "@appsprscr", 8, nil); emitline("\tMOVQ\tBX, "); emitoff(itscr: i64); emitline("(BP)\n"); }; let load_op: str = tnodeloadop(c, etnode, esz); if (!sndirect) { // FA1: no etnode behind `*p` — signedness off the // stamped element tinfo, the predicate cstage's // fldloadop applies to su->sub. load_op = loadopsz(typeissigned(esubti), 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"); if (itscr != 0) { emitline("\tMOVQ\t"); emitoff(itscr: i64); emitline("(BP), DX\n"); emitline("\tMOVQ\t8(DX), DX\n"); } else { emitline("\tMOVQ\t"); emitoff((it_off + 8): i64); emitline("(BP), DX\n"); }; emitline("\tCMPQ\tDX, CX\n"); emitline("\tJGE\t"); emitline(le); emitline("\n"); if (elwide) { // #34: a spread element is already a fully-formed // T in the source slice (tag included), so a // whole-width word-copy is the store — no boxing. // Grow FIRST: rt_ensure may realloc, so both // addresses are recomputed from the slice headers // after the call (i reloads from the counter // slot; CX was clobbered). cgappendgrow(c, sndirect, sn_off, snscr, esz); emitline("\tMOVQ\t(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; if (itscr != 0) { emitline("\tMOVQ\t"); emitoff(itscr: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t(BX), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(it_off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tCX, BX\n"); cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); let wk: i32 = 0; for (wk + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg(wk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(wk: i64, "DX"); emitline("\n"); wk += 8; }; if (wk + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg(wk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(wk: i64, "DX"); emitline("\n"); wk += 4; }; if (wk + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg(wk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(wk: i64, "DX"); emitline("\n"); wk += 2; }; if (wk + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg(wk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(wk: i64, "DX"); emitline("\n"); wk += 1; }; 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; }; if (itscr != 0) { emitline("\tMOVQ\t"); emitoff(itscr: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t(BX), BX\n"); } else { 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"); cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); 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; }; if (elstr || elslice) { // #34: 24B {ptr,len,cap} header. cgexpr leaves // AX/BX/CX; all three must survive rt_ensure. dst // lands in DX, NOT BX — the pops put the element // .len back in BX (the #24 register discipline). cgexpr(c, vn); emitline("\tPUSHQ\tAX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tCX\n"); cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tAX\n"); emitline("\tMOVQ\tAX, (DX)\n"); emitline("\tMOVQ\tBX, 8(DX)\n"); emitline("\tMOVQ\tCX, 16(DX)\n"); vn = vn.next; continue; }; if (eltagged || elstruct) { // #34: no register form survives rt_ensure for these. // struct: grow FIRST, then fill through the dst pointer // (literal fill / ident word-copy). tagged: #50 — the // #12 widen choke-point cgexprs the value internally, // so boxing must run PRE-grow (Hare's argument order: // a `xs.len` read in v sees the pre-append len, like // the scalar arm); box into a frame scratch, grow, // raw-copy the finished box in. // #49 (#35's single-element sibling): a place-chain // source (indexed field `threads[i].root_capture` // regex.ha:819, deref spine, computed index) SPLITS // around the grow per the #49 ruling: the chain's // rvalues (deref-root pointer expr, index expr) // evaluate exactly once PRE-grow — an index reading // the slice header sees the pre-append len, Hare's // argument order — and only the BASE re-derives // POST-grow from the live storage, so a self-append // source re-roots in the post-realloc buffer. harec // resolves an aggregate source address wholly PRE-grow // (gen.c: gen_load returns the address for // STORAGE_STRUCT, gen_store copies after rt.ensure) — // a use-after-free under a reclaiming allocator; per // #263 we align to the runtime-correct side, not the // reference. A pointer ALIASING the grown buffer keeps // Hare's own stale-base hole (sound today only because // rt/malloc.ww never reclaims). Spec not vendored // (ref/hare/docs = man pages only), spec-silence // assumed — re-verify if the spec is ever vendored. // Bounded shapes: root (local/global ident | deref) + // at most one index + trailing direct fields; all else // stays on the #34 loud exit (incl. CALL rvalues, the // #42-style bound). let aplace: bool = false; let afld: i32 = 0; let aidxesz: i32 = 0; let abaseslice: bool = false; let arootoff: i32 = 0; let asroot: i32 = 0; let asoff: i32 = 0; let aroot: *node = nil; let aidx: *node = nil; if (elstruct && vn.kind != nkind.N_STRUCTLIT && vn.kind != nkind.N_IDENT) { let ch: *node = vn; let aok: bool = true; for (aok && ch.kind == nkind.N_DOT) { let ab: *node = ch.lhs; let af: *tfield = nil; if (ab != nil) { let abu: *tinfo = ab.type_: *tinfo; abu = tichase(abu); if (abu != nil && abu.kind == tykind.TY_STRUCT) { let fl: *tfield = abu.fields; for (fl != nil) { if (streq(fl.name, ch.str)) { af = fl; break; }; fl = fl.tnext; }; }; }; if (af == nil) { aok = false; break; }; afld += af.offset: i32; ch = ab; }; if (aok && ch.kind == nkind.N_INDEX) { let ab: *node = ch.lhs; let aet: *tinfo = ch.type_: *tinfo; aet = tichase(aet); let abu: *tinfo = nil; if (ab != nil) { abu = ab.type_: *tinfo; abu = tichase(abu); }; if (ab == nil || abu == nil || aet == nil || (abu.kind != tykind.TY_SLICE && abu.kind != tykind.TY_ARRAY)) { aok = false; } else { abaseslice = abu.kind == tykind.TY_SLICE; aidxesz = aet.size: i32; aidx = ch.rhs; ch = ab; }; }; if (aok) { if (ch.kind == nkind.N_IDENT) { let rl: *local = localfindnode(c, ch.str); if (rl != nil) { arootoff = rl.off; } else { let gok: bool = isletvar(c, ch.str); if (!gok) { if (defvarstructinfo(c, ch.str) != nil) { gok = true; }; }; if (!gok) { let dtn: *node = defvartnode(c, ch.str); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { gok = true; }; }; }; if (!gok) { aok = false; }; }; } else { if (ch.kind != nkind.N_UN || ch.op != tkind.TK_STAR) { aok = false; }; }; }; if (!aok) { let m34p: str = "#34: append() struct element source shape unsupported (rule-7)\n"; os.write(2, m34p.ptr, m34p.len: u64); os.exit(1); }; aroot = ch; if (aroot.kind == nkind.N_UN) { asroot = localadd(c, "@appendsroot", 8, nil); cgexpr(c, aroot.lhs); emitline("\tMOVQ\tAX, "); emitoff(asroot: i64); emitline("(BP)\n"); }; if (aidx != nil) { asoff = localadd(c, "@appendsoff", 8, nil); cgexpr(c, aidx); if (aidxesz > 1) { emitline("\tMOVQ\t$"); emitint(aidxesz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tMOVQ\tAX, "); emitoff(asoff: i64); emitline("(BP)\n"); }; aplace = true; }; if (eltagged) { // Fresh slot per SITE, not the shared per-size // scratch: the box must stay live across // rt_ensure, and a nested append inside the // value expression would clobber a dedup'd // slot (the @apphdrscr rationale; #25/#31). let tgscr: i32 = localalloc(c, "@apptagscr", esz, nil); emitline("\tXORQ\tAX, AX\n"); let zk: i32 = 0; for (zk < esz) { emitline("\tMOVQ\tAX, "); emitoff((tgscr + zk): i64); emitline("(BP)\n"); zk += 8; }; cgwidentaggedstore(c, esubti, vn, "BP", tgscr, esz); cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); let ck: i32 = 0; for (ck < esz) { emitline("\tMOVQ\t"); emitoff((tgscr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 8; }; vn = vn.next; continue; }; if (vn.kind == nkind.N_STRUCTLIT) { // #59 (#50's eval-order kin): resolve the struct // info, then fill the literal into a fresh // per-SITE scratch (must stay live across // rt_ensure, and a nested append in a field expr // would clobber a dedup'd slot — the @apptagscr // rationale, #25/#31) BEFORE the grow, so the // field exprs see the pre-grow len. Then grow, // slot, raw-copy scratch->slot (mirror the #50 // tagged arm above). let esi: *structinfo = structlookupchain(c, etnode); if (esi == nil && !sndirect && esubnamed != nil) { // FA1: no declared tnode to chain through — // the stamped NAMED element tinfo carries the // same leaf structlookupchain would resolve. if (esubnamed.kind == tykind.TY_NAMED) { esi = structlookup(c, esubnamed.name); }; }; if (esi == nil) { let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n"; os.write(2, m34s.ptr, m34s.len: u64); os.exit(1); }; let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil); cgstructlitfillbp(c, esi, vn, stscr); cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); // Descending 8/4/2/1 ladder, not an 8B-word // loop: a plain struct's esz rounds to maxalign // (check.c:916), not 8, so a sub-8B / non-8B- // multiple element packs at its own stride — an // 8B copy of the last slot writes past the slice // buffer (the tagged arm above is safe only // because boxes are 8B-padded; #59). Mirrors the // N_IDENT source arm below. let ck: i32 = 0; for (ck + 8 <= esz) { emitline("\tMOVQ\t"); emitoff((stscr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 8; }; if (ck + 4 <= esz) { emitline("\tMOVL\t"); emitoff((stscr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 4; }; if (ck + 2 <= esz) { emitline("\tMOVW\t"); emitoff((stscr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 2; }; if (ck + 1 <= esz) { emitline("\tMOVB\t"); emitoff((stscr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 1; }; vn = vn.next; continue; }; cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, vn.str); if (sl == nil) { let m34i: str = "#34: append() struct element source ident is not a local (rule-7)\n"; os.write(2, m34i.ptr, m34i.len: u64); os.exit(1); }; let soff: i32 = sl.off; let ck: i32 = 0; for (ck + 8 <= esz) { emitline("\tMOVQ\t"); emitoff((soff + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 8; }; if (ck + 4 <= esz) { emitline("\tMOVL\t"); emitoff((soff + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 4; }; if (ck + 2 <= esz) { emitline("\tMOVW\t"); emitoff((soff + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 2; }; if (ck + 1 <= esz) { emitline("\tMOVB\t"); emitoff((soff + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(ck: i64, "BX"); emitline("\n"); ck += 1; }; vn = vn.next; continue; }; if (aplace) { // phase 2: dst slot to @appendscr, base from // the live storage, stashed offsets back on // top. let pscroff: i32 = localadd(c, "@appendscr", 8, nil); emitline("\tMOVQ\tBX, "); emitoff(pscroff: i64); emitline("(BP)\n"); if (aroot.kind == nkind.N_IDENT) { if (arootoff != 0) { emitline("\tLEAQ\t"); emitoff(arootoff: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, aroot.str); emitline("(SB), BX\n"); }; } else { emitline("\tMOVQ\t"); emitoff(asroot: i64); emitline("(BP), BX\n"); }; if (aidx != nil) { if (abaseslice) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\tMOVQ\t"); emitoff(asoff: i64); emitline("(BP), AX\n"); emitline("\tADDQ\tAX, BX\n"); }; if (afld != 0) { emitline("\tADDQ\t$"); emitint(afld: i64); emitline(", BX\n"); }; emitline("\tMOVQ\t"); emitoff(pscroff: i64); emitline("(BP), DX\n"); let pk: i32 = 0; for (pk + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg(pk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(pk: i64, "DX"); emitline("\n"); pk += 8; }; if (pk + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg(pk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(pk: i64, "DX"); emitline("\n"); pk += 4; }; if (pk + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg(pk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(pk: i64, "DX"); emitline("\n"); pk += 2; }; if (pk + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg(pk: i64, "BX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(pk: i64, "DX"); emitline("\n"); pk += 1; }; vn = vn.next; continue; }; let m34e: str = "#34: append() struct element source shape unsupported (rule-7)\n"; os.write(2, m34e.ptr, m34e.len: u64); os.exit(1); }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\n"); cgappendgrow(c, sndirect, sn_off, snscr, esz); cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; }; return; }; // cgdelete — Hare `delete(xs[i])`: single-element slice removal, the // delete-half of #35. Shift [i+1..len) down one stride, len -= 1, cap // unchanged. The move is a same-type whole-stride byte copy: src and // dst are elements of the SAME slice, so no boxing/coercion exists for // any element kind (str/slice 24B header, tagged box, struct body) — // one word-copy loop serves all kinds, unlike append's value-store // dispatch (#34) which boxes from a foreign source. Ascending j keeps // src (j+1) ahead of dst (j), the safe memmove-down direction. Mirrors // cmd/w6c/cgen.c's N_CALL delete arm instruction-for-instruction // (rule-10 byte-id). fn cgdelete(c: *cgen, n: *node) void = { let d: *node = n.list; // N_INDEX, checker-validated let base: *node = d.lhs; // esz off the STAMPED base tinfo (#34/#48 discipline — never the // value node). Peel TY_NAMED on indexable AND element, mirroring // cstage's type_chase_named on both (#8 family). let sti: *tinfo = base.type_: *tinfo; sti = tichase(sti); let esub: *tinfo = nil; if (sti != nil) { esub = sti.sub; }; esub = tichase(esub); let esz: i32 = 0; if (esub != nil) { esz = esub.size: i32; }; if (esz <= 0) { let m35a: str = "#35: delete() element size unresolved (rule-7)\n"; os.write(2, m35a.ptr, m35a.len: u64); os.exit(1); }; let hdr_lea: bool = false; let hdr_off: i32 = 0; let hdr_ok: bool = false; if (base.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { hdr_lea = true; hdr_off = lc.off; hdr_ok = true; }; }; // (*p)[i]: the header lives behind a local ptr-to-slice — the // regex fold-2b delete_thread shape (threads: *[]thread). if (!hdr_ok && base.kind == nkind.N_UN) { if (base.op == tkind.TK_STAR && base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { let pc: *local = localfindnode(c, base.lhs.str); if (pc != nil) { hdr_off = pc.off; hdr_ok = true; }; }; }; }; if (!hdr_ok) { let m35b: str = "#35: delete() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; os.write(2, m35b.ptr, m35b.len: u64); os.exit(1); }; cgexpr(c, d.rhs); // AX = i emitline("\tPUSHQ\tAX\n"); if (hdr_lea) { emitline("\tLEAQ\t"); } else { emitline("\tMOVQ\t"); }; emitoff(hdr_off: i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); let dll: str = mklabel(c, "del_l"); let dle: str = mklabel(c, "del_e"); emitlabel(dll); emitline("\tMOVQ\t(SP), DX\n"); emitline("\tMOVQ\t8(SP), CX\n"); emitline("\tMOVQ\t8(DX), BX\n"); emitline("\tSUBQ\t$1, BX\n"); emitline("\tCMPQ\tBX, CX\n"); emitline("\tJGE\t"); emitline(dle); emitline("\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t(DX), BX\n"); emitline("\tADDQ\tCX, BX\n"); let dk: i32 = 0; for (dk + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg((esz + dk): i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(dk: i64, "BX"); emitline("\n"); dk += 8; }; if (dk + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg((esz + dk): i64, "BX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(dk: i64, "BX"); emitline("\n"); dk += 4; }; if (dk + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg((esz + dk): i64, "BX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(dk: i64, "BX"); emitline("\n"); dk += 2; }; if (dk + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg((esz + dk): i64, "BX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(dk: i64, "BX"); emitline("\n"); dk += 1; }; emitline("\tADDQ\t$1, 8(SP)\n"); emitline("\tJMP\t"); emitline(dll); emitline("\n"); emitlabel(dle); emitline("\tMOVQ\t(SP), DX\n"); emitline("\tSUBQ\t$1, 8(DX)\n"); emitline("\tADDQ\t$16, SP\n"); }; // cgdeleterange — Hare `delete(xs[lo..hi])` (ww spells the range // `xs[lo:hi]`): range slice removal, the fold-5a prereq P2 // (ref/hare/regex/regex.ha:333 delete(jump_idxs[group_level][..]); // harec ref/harec/src/check.c:1994 EXPR_SLICE). Shift [hi..len) down // count = hi-lo strides, len -= count, cap unchanged; lo defaults 0, // hi defaults len. delete(xs[:]) never enters the copy loop (lo+count // == len at entry) and zeroes len. The per-element move is cgdelete's // same-slice whole-stride word copy with a DYNAMIC src offset // (count*esz via a src register) instead of the constant one-stride. // Ascending j keeps src >= dst, the safe memmove-down direction. // Bounds are implicit (no range check, matching cgdelete and the rest // of cgen). Mirrors cmd/w6c/cgen.c's N_CALL delete range arm // instruction-for-instruction (rule-10 byte-id). fn cgdeleterange(c: *cgen, n: *node) void = { let d: *node = n.list; // N_SLICE, checker-validated let base: *node = d.lhs; // esz off the STAMPED base tinfo (#34/#48 discipline — never the // value node). Peel TY_NAMED on indexable AND element, mirroring // cstage's type_chase_named on both (#8 family). let sti: *tinfo = base.type_: *tinfo; sti = tichase(sti); let esub: *tinfo = nil; if (sti != nil) { esub = sti.sub; }; esub = tichase(esub); let esz: i32 = 0; if (esub != nil) { esz = esub.size: i32; }; if (esz <= 0) { let m35a: str = "#35: delete() element size unresolved (rule-7)\n"; os.write(2, m35a.ptr, m35a.len: u64); os.exit(1); }; let hdr_lea: bool = false; let hdr_off: i32 = 0; let hdr_ok: bool = false; let hdr_idx: bool = false; let osz: i32 = 0; if (base.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { hdr_lea = true; hdr_off = lc.off; hdr_ok = true; }; }; // (*p)[lo:hi]: header behind a local ptr-to-slice — cgdelete's // regex_shape twin. if (!hdr_ok && base.kind == nkind.N_UN) { if (base.op == tkind.TK_STAR && base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { let pc: *local = localfindnode(c, base.lhs.str); if (pc != nil) { hdr_off = pc.off; hdr_ok = true; }; }; }; }; // xs[g][lo:hi]: the header IS element g of an outer local slice — // the fold-5a consumer shape (ref/hare/regex/regex.ha:333 // delete(jump_idxs[group_level][..])). Outer stride = the inner // header type's own table size (sti). if (!hdr_ok && base.kind == nkind.N_INDEX) { if (base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { let oc: *local = localfindnode(c, base.lhs.str); if (oc != nil) { hdr_idx = true; hdr_off = oc.off; if (sti != nil) { osz = sti.size: i32; }; if (osz <= 0) { let m35c: str = "#35: delete() outer element size unresolved (rule-7)\n"; os.write(2, m35c.ptr, m35c.len: u64); os.exit(1); }; hdr_ok = true; }; }; }; }; if (!hdr_ok) { let m35b: str = "#35: delete() range base shape unsupported (rule-7: local slice ident, deref-of-local, or indexed local slice only)\n"; os.write(2, m35b.ptr, m35b.len: u64); os.exit(1); }; if (hdr_idx) { cgexpr(c, base.rhs); if (osz > 1) { emitline("\tMOVQ\t$"); emitint(osz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tMOVQ\t"); emitoff(hdr_off: i64); emitline("(BP), CX\n"); emitline("\tADDQ\tCX, AX\n"); } else { if (hdr_lea) { emitline("\tLEAQ\t"); } else { emitline("\tMOVQ\t"); }; emitoff(hdr_off: i64); emitline("(BP), AX\n"); }; emitline("\tPUSHQ\tAX\n"); if (d.rhs != nil) { cgexpr(c, d.rhs); } else { emitline("\tMOVQ\t$0, AX\n"); }; emitline("\tPUSHQ\tAX\n"); if (d.cond != nil) { cgexpr(c, d.cond); } else { emitline("\tMOVQ\t8(SP), CX\n"); emitline("\tMOVQ\t8(CX), AX\n"); }; emitline("\tMOVQ\t(SP), CX\n"); emitline("\tSUBQ\tCX, AX\n"); emitline("\tPUSHQ\tAX\n"); let rll: str = mklabel(c, "rdl_l"); let rle: str = mklabel(c, "rdl_e"); emitlabel(rll); emitline("\tMOVQ\t16(SP), DX\n"); emitline("\tMOVQ\t8(SP), CX\n"); emitline("\tMOVQ\t(SP), AX\n"); emitline("\tADDQ\tCX, AX\n"); emitline("\tMOVQ\t8(DX), BX\n"); emitline("\tCMPQ\tBX, AX\n"); emitline("\tJGE\t"); emitline(rle); emitline("\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t(DX), BX\n"); emitline("\tADDQ\tCX, BX\n"); emitline("\tMOVQ\t(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tADDQ\tBX, CX\n"); let rk: i32 = 0; for (rk + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg(rk: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(rk: i64, "BX"); emitline("\n"); rk += 8; }; if (rk + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg(rk: i64, "CX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(rk: i64, "BX"); emitline("\n"); rk += 4; }; if (rk + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg(rk: i64, "CX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(rk: i64, "BX"); emitline("\n"); rk += 2; }; if (rk + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg(rk: i64, "CX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(rk: i64, "BX"); emitline("\n"); rk += 1; }; emitline("\tADDQ\t$1, 8(SP)\n"); emitline("\tJMP\t"); emitline(rll); emitline("\n"); emitlabel(rle); emitline("\tMOVQ\t16(SP), DX\n"); emitline("\tMOVQ\t(SP), AX\n"); emitline("\tMOVQ\t8(DX), BX\n"); emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, 8(DX)\n"); emitline("\tADDQ\t$24, SP\n"); }; // cginsert — Hare `insert(xs[idx], v)`: delete()'s twin, the insert-half // of #35. Insert v BEFORE idx; idx==len is a legal end-insert. Lowered // as a DESUGAR to append(xs, v) + a rotate-right of [idx, len): cgappend // contributes grow (rt_ensure) and the whole #34 value-store dispatch // (scalar / str-slice header / tagged widen / struct fill) verbatim — // one boxing choke-point, byte-id by construction — landing v at slot // len-1; the rotate then moves it home through an esz frame scratch. // The rotate is cgdelete's shift loop in reverse (descending j keeps // src j behind dst j+1, the safe memmove-up direction) and, like // delete's, is a same-slice whole-stride raw byte move — no boxing // exists for any element kind. idx evaluates BEFORE the grow (Hare's // left-to-right operand order: insert(xs[len(xs)], v) sees the pre-grow // len); v's evaluation point inherits append's per-kind rules. Bounds // are implicit (no index check, matching delete). Mirrors cmd/w6c/ // cgen.c's N_CALL insert arm instruction-for-instruction (rule-10 // byte-id). fn cginsert(c: *cgen, n: *node) void = { let d: *node = n.list; // N_INDEX, checker-validated let base: *node = d.lhs; let v: *node = d.next; // esz off the STAMPED base tinfo (#34/#48 discipline — never the // value node). Peel TY_NAMED on indexable AND element, mirroring // cstage's type_chase_named on both (#8 family). let sti: *tinfo = base.type_: *tinfo; sti = tichase(sti); let esub: *tinfo = nil; if (sti != nil) { esub = sti.sub; }; esub = tichase(esub); let esz: i32 = 0; if (esub != nil) { esz = esub.size: i32; }; if (esz <= 0) { let m35c: str = "#35: insert() element size unresolved (rule-7)\n"; os.write(2, m35c.ptr, m35c.len: u64); os.exit(1); }; let hdr_lea: bool = false; let hdr_off: i32 = 0; let hdr_ok: bool = false; if (base.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, base.str); if (lc != nil) { hdr_lea = true; hdr_off = lc.off; hdr_ok = true; }; }; // (*p)[i]: the header lives behind a local ptr-to-slice — // delete's regex_shape twin. if (!hdr_ok && base.kind == nkind.N_UN) { if (base.op == tkind.TK_STAR && base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { let pc: *local = localfindnode(c, base.lhs.str); if (pc != nil) { hdr_off = pc.off; hdr_ok = true; }; }; }; }; if (!hdr_ok) { let m35d: str = "#35: insert() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n"; os.write(2, m35d.ptr, m35d.len: u64); os.exit(1); }; // Fresh esz-sized slot per SITE (esz varies; an @-name dedup // would mis-share across element types). Allocated BEFORE the // cgappend body's own scratch allocs — cstage order. let insscr: i32 = localalloc(c, "@insscr", esz, nil); cgexpr(c, d.rhs); // AX = idx emitline("\tPUSHQ\tAX\n"); // Desugar in place and route through cgappend: cgen is // single-pass, base is an lhs node (never on a sibling chain), // and the checker has already validated this call — the mutation // is dead after this emission. The callee rename keeps the AST // consistent with cstage's re-dispatch. n.lhs.str = "append"; n.list = base; base.next = v; cgappend(c, n); if (hdr_lea) { emitline("\tLEAQ\t"); } else { emitline("\tMOVQ\t"); }; emitoff(hdr_off: i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVQ\tAX, DX\n"); emitline("\tMOVQ\t8(DX), 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(DX), BX\n"); emitline("\tADDQ\tCX, BX\n"); let ik: i32 = 0; for (ik + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitoff((insscr + ik): i64); emitline("(BP)\n"); ik += 8; }; if (ik + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitoff((insscr + ik): i64); emitline("(BP)\n"); ik += 4; }; if (ik + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitoff((insscr + ik): i64); emitline("(BP)\n"); ik += 2; }; if (ik + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitoff((insscr + ik): i64); emitline("(BP)\n"); ik += 1; }; emitline("\tMOVQ\t8(DX), AX\n"); emitline("\tSUBQ\t$2, AX\n"); emitline("\tPUSHQ\tAX\n"); let ill: str = mklabel(c, "ins_l"); let ile: str = mklabel(c, "ins_e"); emitlabel(ill); emitline("\tMOVQ\t(SP), CX\n"); emitline("\tMOVQ\t16(SP), DX\n"); emitline("\tCMPQ\tDX, CX\n"); emitline("\tJL\t"); emitline(ile); emitline("\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t8(SP), DX\n"); emitline("\tMOVQ\t(DX), BX\n"); emitline("\tADDQ\tCX, BX\n"); ik = 0; for (ik + 8 <= esz) { emitline("\tMOVQ\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((esz + ik): i64, "BX"); emitline("\n"); ik += 8; }; if (ik + 4 <= esz) { emitline("\tMOVL\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVL\tAX, "); emitdispreg((esz + ik): i64, "BX"); emitline("\n"); ik += 4; }; if (ik + 2 <= esz) { emitline("\tMOVW\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVW\tAX, "); emitdispreg((esz + ik): i64, "BX"); emitline("\n"); ik += 2; }; if (ik + 1 <= esz) { emitline("\tMOVB\t"); emitdispreg(ik: i64, "BX"); emitline(", AX\n"); emitline("\tMOVB\tAX, "); emitdispreg((esz + ik): i64, "BX"); emitline("\n"); ik += 1; }; emitline("\tSUBQ\t$1, (SP)\n"); emitline("\tJMP\t"); emitline(ill); emitline("\n"); emitlabel(ile); emitline("\tMOVQ\t16(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t8(SP), DX\n"); emitline("\tMOVQ\t(DX), BX\n"); emitline("\tADDQ\tCX, BX\n"); ik = 0; for (ik + 8 <= esz) { emitline("\tMOVQ\t"); emitoff((insscr + ik): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(ik: i64, "BX"); emitline("\n"); ik += 8; }; if (ik + 4 <= esz) { emitline("\tMOVL\t"); emitoff((insscr + ik): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitdispreg(ik: i64, "BX"); emitline("\n"); ik += 4; }; if (ik + 2 <= esz) { emitline("\tMOVW\t"); emitoff((insscr + ik): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitdispreg(ik: i64, "BX"); emitline("\n"); ik += 2; }; if (ik + 1 <= esz) { emitline("\tMOVB\t"); emitoff((insscr + ik): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitdispreg(ik: i64, "BX"); emitline("\n"); ik += 1; }; emitline("\tADDQ\t$24, SP\n"); }; 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) { // abort([msg]) / assert(cond[, msg]) — checker-tagged // builtins (callee TY_ERR is the routing key, cstage's // `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645); // lower to rt_abort. A user-shadowed abort/assert is // untagged and stays on the regular call path (#58). let bti: *tinfo = callee.type_: *tinfo; if (bti != nil && bti.kind == tykind.TY_ERR) { if (streq(callee.str, "abort")) { if (n.list != nil) { cgexpr(c, n.list); emitline("\tMOVQ\tAX, DI\n"); emitline("\tMOVQ\tBX, SI\n"); } else { emitline("\tMOVQ\t$0, DI\n"); emitline("\tMOVQ\t$0, SI\n"); }; emitline("\tCALL\trt_abort(SB)\n"); return; }; if (streq(callee.str, "assert") && n.list != nil) { cgexpr(c, n.list); let skip: str = mklabel(c, "as"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(skip); emitline("\n"); let msg: *node = n.list.next; if (msg != nil) { cgexpr(c, msg); emitline("\tMOVQ\tAX, DI\n"); emitline("\tMOVQ\tBX, SI\n"); } else { emitline("\tMOVQ\t$0, DI\n"); emitline("\tMOVQ\t$0, SI\n"); }; emitline("\tCALL\trt_abort(SB)\n"); emitlabel(skip); return; }; }; 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 N_CALL "len" // arm. 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). // // Dispatch (#10/#41): enumerated fast-paths keep their // pre-fix asm (ident local/global, #235 tuple element, #19 // indexed element, TY_ARRAY const fold), then ONE uniform // header-place route via cgplaceaddr (.len at place+8) for // every other slice/str place — the arm enumeration leaked // four siblings (#235 → #19 → F2 → FA2/FB1), each new operand // shape falling to a cgexpr fallback that returned the slice // DATA POINTER as the length. Non-place operands (call // result, slicing expr, string literal — previously the same // silent ptr-garbage) die LOUD per rule 7. if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; let at: *tinfo = a.type_: *tinfo; let u: *tinfo = at; u = tichase(u); let hdrish: bool = false; if (u != nil) { if (u.kind == tykind.TY_SLICE || u.kind == tykind.TY_STR) { hdrish = true; }; }; if (hdrish && 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; }; // #231: str/slice GLOBAL — the // local-only path above lacked it, // so cgexpr fell through and left // AX=.ptr (not .len). The .len word // lives at the global's address+8; // route the LEAQ through the post-#1 // value mangle (c.curmod) so a // private same-leaf global isn't // mis-resolved. if (isletvar(c, a.str)) { emitline("\tLEAQ\t"); emitsymnamehint(c, a.str, c.curmod); emitline("(SB), CX\n"); emitline("\tMOVQ\t8(CX), AX\n"); return; }; // non-local non-let ident (DATA-backed // def): the old arm fell to the silent // cgexpr fallback. Falls to the // resolver route below. }; // #235: len() of a tuple-element slice/str // (`len(t.N)`). Kept as an enumerated arm: // tuples are not resolver-addressable // (cgplaceaddr has no TY_TUPLE hop — that gap // is #238). Load the element's .len word // directly at BP + element_off + 8, mirroring // the N_IDENT slice arm above and the // tuple-field-offset walk (cgenexpr.ww N_TTUPLE). if (hdrish && a.kind == nkind.N_DOT && a.lhs != nil && a.lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.lhs.str); if (lc != nil) { let tn: *node = lc.tnode; for (tn != nil && tn.kind == nkind.N_TNAME) { tn = aliaslookup(c, tn.str); }; if (tn != nil) { if (tn.kind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(a.str); 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 { // C-t0/#22: slot // stride (tupeslot). foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; }; if (tp != nil) { emitline("\tMOVQ\t"); emitoff((lc.off + foff + 8): i64); emitline("(BP), AX\n"); return; }; }; }; }; }; // C-t3 (#48): GLOBAL tuple len(g.N) — // LEAQ name(SB) into CX, .len word at // the SLOT offset + 8. Graduates the // C5 loud-stop this shape previously // hit; twin of the cgdot global-tuple // arm and cstage's #235 global base. if (lc == nil) { let gtn: *node = letvartnode(c, a.lhs.str); for (gtn != nil && gtn.kind == nkind.N_TNAME) { gtn = aliaslookup(c, gtn.str); }; if (gtn != nil) { if (gtn.kind == nkind.N_TTUPLE) { let gidx: i32 = fldnumidx(a.str); if (gidx >= 0) { let gtp: *node = gtn.list; let gfoff: i32 = 0; let gi: i32 = 0; for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { // C-t0/#22: slot // stride (tupeslot). gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; }; if (gtp != nil) { emitline("\tLEAQ\t"); emitsymname(c, a.lhs.str); emitline("(SB), CX\n"); emitline("\tMOVQ\t"); emitdispreg((gfoff + 8): i64, "CX"); emitline(", AX\n"); return; }; }; }; }; }; // struct-field N_DOT (`len(s.field)`): the // old fallback returned .ptr as the length. // Falls to the resolver route below. }; // #19: len() of an INDEXED str/slice element // (`len(xs[i])`). The N_INDEX str/slice load // leaves AX=.ptr, BX=.len, CX=.cap — the bare // cgexpr fallback returned AX (the ptr) AS the // length. Shuffle BX (the len word) into AX, // the same MOVQ BX,AX shape as the #14 .len // pseudo-field fix. Same family as #18 (shared // cstage==wwstage gap, not rule-10). if (hdrish && a.kind == nkind.N_INDEX) { cgexpr(c, a); emitline("\tMOVQ\tBX, AX\n"); return; }; if (u != nil) { if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); emitline(", AX\n"); return; }; }; // #10 (F2) + #41 (FA2/FB1): ONE uniform // header-place route for every other slice/str // place — resolve the operand's header address // (cgplaceaddr: deref / index / dot spines) and // read the .len word at +8. if (hdrish) { if (cgplaceaddr(c, a, "BX")) { emitline("\tMOVQ\t8(BX), AX\n"); return; }; }; let mlen: str = "#10/#41: len() operand shape not place-resolvable (rule-7)\n"; os.write(2, mlen.ptr, mlen.len: u64); os.exit(1); }; }; // free(x) — documented NO-OP, mirror of cstage's cgexpr // N_CALL free arm (#27): ww has no free by design // (rt/alloc.s:30 — the bump allocator cannot reclaim a // mid-chunk pointer; process exit does). The operand is // still evaluated — Hare's free(expr) evaluates expr — // so Hare code ports verbatim with its side effects // intact. Pre-#27 wwstage fell through to a generic // CALL free → undefined reference at link. if (streq(callee.str, "free")) { if (n.list != nil) { if (n.list.next == nil) { cgexpr(c, n.list); return; }; }; }; // delete(xs[i]) / delete(xs[lo:hi]) — #35 // delete-half + fold-5a P2 range form; mirror of // cstage cgen.c's N_CALL delete arm (which branches // on d->kind == N_SLICE internally). if (streq(callee.str, "delete")) { if (n.list != nil) { if (n.list.next == nil) { if (n.list.kind == nkind.N_SLICE) { cgdeleterange(c, n); return; }; cgdelete(c, n); return; }; }; }; // insert(xs[idx], v) — #35 insert-half; mirror of // cstage cgen.c's N_CALL insert arm. if (streq(callee.str, "insert")) { if (n.list != nil) { if (n.list.next != nil) { if (n.list.next.next == nil) { cginsert(c, n); return; }; }; }; }; }; }; // Look up the callee's declared params for tagged-union widening. // fn-pointer calls (callee is a local) don't get widening — the // user must build the tagged value explicitly. // // N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage // only handled N_IDENT, leaving N_DOT calls without widening // detection — pushargsrev then fell through to the N_IDENT-slice // fast path and dropped the variant tag word on widened slice args. // Cstage finds params via the checker-set `n->lhs->type`, sidestepping // the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165). let calleeparams: *node = nil; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { calleeparams = fnparamslookup(c, callee.str); } else { if (callee.kind == nkind.N_DOT) { let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; calleeparams = fnparamslookupmod(c, callee.str, cmod); }; }; }; // Hare-style variadic last param: gather N tail args into a // frame-resident [N]T (vararg_d slot) plus a 24B slice // descriptor (vararg_sl slot), 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. Slot // names come from mklabel (mirrors cstage cgen.c:5427/5431) so // the shared labelseq advances in lockstep — vararg_d only when // nvar>0, vararg_sl always — keeping later match labels aligned. { 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 { // 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 = aliasprimsize(c, velem.str); if (ps > 0) { esz = ps; } else { esz = slotsize(c, velem); }; } else { esz = slotsize(c, velem); }; }; if (esz < 1) { esz = 1; }; // #38b: a >48B tagged variadic ELEMENT would // need the memory convention inside the vararg // gather buffer — unwired (rule 7). cstage twin // guards before its v_is_tagged gather. if (velem != nil) { if (taggedmemargsize(velem.type_: *tinfo) > 0) { let mv: str = "#38b: >48B tagged variadic element unwired\n"; os.write(2, mv.ptr, mv.len: u64); os.exit(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) { // mklabel, not a separate vararg counter, so // labelseq advances with cstage cgen.c:5427 — the // slot name never reaches asm, but the shared // counter numbers later match labels. let dname: str = mklabel(c, "vararg_d"); 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). // vararg_sl always allocated (cstage cgen.c:5431), // bumping labelseq whether or not nvar>0. let sname: str = mklabel(c, "vararg_sl"); 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; }; }; }; }; // #38b: two-phase push — MEMORY-class (>48B tagged) args staged // first so they sit BELOW every register-class word; the pop loop // drains a strict prefix and never touches them. memwords feeds // the caller-cleanup ADDQ (with the mix guard below). let memwords: i32 = pushargsrev(c, n.list, calleeparams, true); let nargs: i32 = pushargsrev(c, n.list, calleeparams, false); // 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; // #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr // slot (the callee writes the struct straight into g's storage). let sretdestn: *node = nil; if (sretcs > 0) { if (c.sretdestnode != nil) { sretdestn = c.sretdestnode; c.sretdestnode = nil; } else { 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 dparam: *node = calleeparams; let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { // #38b: MEMORY-class arg — its words sit below the pop // region and stay on the stack for the callee; nothing to // drain. Same param-keyed-else-arg-keyed detection as // pushargsrev (a widened concrete arg is mem-class only // via its param). let dmemsz: i32 = 0; if (dparam != nil) { if (dparam.kind == nkind.N_PARAM) { if (dparam.op != tkind.TK_ELLIPSIS) { if (dparam.lhs != nil) { dmemsz = taggedmemargsize(dparam.lhs.type_: *tinfo); }; }; }; }; if (dmemsz == 0) { dmemsz = taggedmemargsize(a.type_: *tinfo); }; if (dmemsz > 0) { if (dparam != nil) { dparam = dparam.next; }; a = a.next; continue; }; // Widen-first pop (#30/#48): a concrete arg widened into a // tagged-union param was pushed as slotsize/8 GP words (tag + // payload). Drain those words into the INTEGER arg cursor // BEFORE the float check below — else a widened f64-source box // gets misclassified as a float arg (its tag word drained into // X0, #48), and a float arg FOLLOWING a widened arg reads the // widened box's leftover payload word (#30). Mirror of cstage's // precomputed widen[i] branch (cmd/w6c/cgen.c cgcall, popped // before node_isfloat). argtaggedwidensz is the shared SSoT with // pushargsrev's push count. let dwsz: i32 = argtaggedwidensz(c, a, dparam); if (dwsz >= 16) { let dwb: i32 = dwsz / 8; let dwk: i32 = 0; for (dwk < dwb) { if (intidx < 6) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; } else { stackslots += 1; }; popped += 1; dwk += 1; }; if (dparam != nil) { dparam = dparam.next; }; a = a.next; continue; }; 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 { // #68: drain over the PARAM tuple element widths for a // tuple LITERAL arg (the declared-tagged box words pop // together with the i64 that follows), mirroring the // param-aware send; else the source tuple type. let dptt: *node = nil; if (a.kind == nkind.N_TUPLE) { if (dparam != nil) { if (dparam.kind == nkind.N_PARAM && dparam.lhs != nil) { let dtn2: *node = dparam.lhs; for (dtn2 != nil && dtn2.kind == nkind.N_TNAME) { dtn2 = aliaslookup(c, dtn2.str); }; if (dtn2 != nil) { if (dtn2.kind == nkind.N_TTUPLE) { dptt = dtn2; }; }; }; }; }; let tuparg: *node = dptt; if (tuparg == nil) { tuparg = nodetuplearg(c, a); }; if (tuparg != nil) { // #163/#32: 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, a declared-tagged box its eslot words // (#68). Reg overflow loud-stops (rule 7); the // partial-spill stitch is out of scope (twin of #164). // C-t2: nodetuplearg admits ident/literal/unwrap // sources; a literal's elements are VALUE exprs, // classified the way the @tupargscr restage classified // them (kind-discriminated twin walk). The param-aware // path (dptt) walks declared element TYPE nodes. let tuplit: bool = false; if (dptt == nil) { if (tuparg.kind == nkind.N_TUPLE) { tuplit = true; }; }; let p: *node = tuparg.list; for (p != nil) { let et: *node = p.lhs; if (tuplit) { et = p; }; 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 { // eslot — full slot split; on the declared // path tupeslotn reads the element TYPE node // directly (#68 box-aware), else wide-vs-scalar // off the literal VALUE node (#22 accessor scale). let eb: i32 = 1; if (tuplit) { let wide: bool = nodeisstr(c, et) || nodeisslice(c, et); if (wide) { eb = (tyslicesize() / 8i64): i32; }; } else { eb = tupeslotn(et) / 8; }; 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); } else { // #31 EXCEPTION (align-UP): a module-global float-bearing // struct arg — lc is nil, so the local-only stfc stayed 0 and // the drain fell to all-GP (X0 never loaded). Key the float- // class off the global's declared tnode (letvartnode), the same // SysV classification the local path uses. cstage keys // structfloatclass off the operand TYPE, not a local slot. let gtn: *node = letvartnode(c, a.str); if (gtn != nil) { stfc = structfloatclass(c, gtn); }; }; }; 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; }; // #271: array / >16B-struct / non-ident 16B-struct // aggregate arg — pushargsrev staged ceil(sz/8) words; // drain exactly that many so intidx tracks per-arg // (the ≤16B struct IDENT case is the stfc branch // above). Mirror of cstage node_isaggarg drain arm. let aggsz: i32 = aggargsizetn(a.type_: *tinfo); if (aggsz > 0) { extra = (aggsz + 7) / 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; }; }; }; }; if (dparam != nil) { dparam = dparam.next; }; 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; }; // #38b: MEMORY-class args and register-overflow spill words cannot // coexist — the callee's positive-BP cursor walks params in // declaration order, but the residual region puts spill words // below every mem copy. Loud-stop (rule 7); cgfnparams holds the // mirror check. The merged count feeds the caller-cleanup ADDQ. if (memwords > 0 && stackslots > 0) { let mm: str = "#38b: >48B tagged arg mixed with register-overflow stack args unwired\n"; os.write(2, mm.ptr, mm.len: u64); os.exit(1); }; stackslots += memwords; // `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; }; }; // #181-cgen: a non-named callee (`(*f)(...)` → N_UN TK_STAR, // or any other expression-valued fn) is an indirect call. // cgexpr the callee into AX; CALL AX. Mirrors cstage's // default-fallthrough at cmd/w6c/cgen.c:5918-5921 which // catches every callee shape that isn't a bare-IDENT module // fn or N_DOT module-qualified call. Pre-fix wwstage emitted // `CALL (SB)` (empty symbol) for the N_UN-callee shape — the // IDENT/DOT name-emit branches missed and isfnptrcall stayed // false. if (callee.kind != nkind.N_IDENT && callee.kind != nkind.N_DOT) { 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 { if (sretdestn != nil) { // #220: sret into a GLOBAL — RDI = &g(SB). emitline("\tLEAQ\t"); emitsymname(c, sretdestn.str); emitline("(SB), DI\n"); } 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; // #145 (c1.5a): bulk slice-copy-assign `s.arr[lo:hi] = bs` (LHS is // N_SLICE). No legacy cgassign arm catches N_SLICE — the statement // silently emitted nothing (both stages, byte-id-green, #263-class). // Twin of cstage cgen.c N_ASSIGN N_SLICE arm (full WHY there). // cgexpr(lhs) routes to cgslice and leaves AX = dst ptr (base+lo*esz), // BX = hi-lo (element count), CX = cap; slicebaseesz gives the SAME // esz cgslice scaled the ptr by, so BX*esz is the byte count. Then a // runtime-counted byte-granular copy from bs.ptr — byte loop because // the length is RUNTIME (w6a has no REP/MOVSB). Only plain `=`. The // Hare len(bs)==hi-lo assert is task #149 (rule-7: documented, not a // c2 blocker — appendlit's lengths are equal by construction). if (lhs != nil) { if (lhs.kind == nkind.N_SLICE && n.op == tkind.TK_ASSIGN) { let esz: i32 = slicebaseesz(c, lhs.lhs); cgexpr(c, lhs); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", DX\n"); emitline("\tIMULQ\tDX, BX\n"); }; emitline("\tPUSHQ\tAX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tMOVQ\tAX, SI\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tDI\n"); let top: str = mklabel(c, "scpy"); let end: str = mklabel(c, "scpe"); emitlabel(top); emitline("\tCMPQ\t$0, CX\n"); emitline("\tJLE\t"); emitline(end); emitline("\n"); emitline("\tMOVB\t(SI), AX\n"); emitline("\tMOVB\tAX, (DI)\n"); emitline("\tADDQ\t$1, SI\n"); emitline("\tADDQ\t$1, DI\n"); emitline("\tSUBQ\t$1, CX\n"); emitline("\tJMP\t"); emitline(top); emitline("\n"); emitlabel(end); return; };}; // #20 (task): struct-lit rhs into an INDEXED struct element — // `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a // DEREF place (`*p = pt{...}`) or an indexed-base FIELD place // (`a[i].f = pt{...}`, same class) skips the legacy arms and // routes to the resolver aggregate arm below (the single // @placescr funnel). The legacy arms' rhs handling (#270-1b // ident/dot/deref gate; deref scalar store; the a[i].f // fldstoreop tail) let the lit fall to a scalar tail: // cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one MOVQ zeroed // the place's first word — every field silently dropped, a // leading str header trashed. let placeslit: bool = false; let placedotidx: bool = false; if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil) { if (lhs.lhs.kind == nkind.N_INDEX) { placedotidx = true; }; }; if ((lhs.kind == nkind.N_INDEX || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR) || placedotidx) && n.op == tkind.TK_ASSIGN && n.rhs != nil) { if (n.rhs.kind == nkind.N_STRUCTLIT) { let iet: *tinfo = lhs.type_: *tinfo; iet = tichase(iet); if (iet != nil) { if (iet.kind == tykind.TY_STRUCT) { placeslit = true; }; }; }; }; }; // #49 (#31-A fold): an aggregate pointee diverts the whole // deref-assign to the resolver aggregate arm below — the scalar // tail there stored ONE word of `*p = s` (#31-A); tuple-lit // (#31-E) and call (#31-G) rhs now die loud there instead of // silently truncating. str/slice pointees keep their 3-word arm // (byte-id-pinned). Mirror of cstage deref_agg. let derefagg: bool = false; if (lhs != nil) { if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR && n.op == tkind.TK_ASSIGN) { let du: *tinfo = lhs.type_: *tinfo; du = tichase(du); if (du != nil) { if (du.kind == tykind.TY_STRUCT || du.kind == tykind.TY_ARRAY || du.kind == tykind.TY_TUPLE) { derefagg = true; }; }; }; }; // 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; }; }; }; }; // Task #32: an array-LITERAL rhs at assignment is unwired for // EVERY place kind (ident reassign, index, deref, dot) — only // decl-init fills. Pre-#32 the same scalar tail zeroed one // word silently; die loud until the fill lands. Slice-typed // places are already loud in the checker. if (n.op == tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { if (n.rhs.kind == nkind.N_ARRLIT) { let alt: *tinfo = lhs.type_: *tinfo; alt = tichase(alt); if (alt != nil) { if (alt.kind == tykind.TY_ARRAY) { let mal: str = "array-literal store at assignment unwired (task #32)\n"; os.write(2, mal.ptr, mal.len: u64); os.exit(1); }; }; }; }; // #21: a COMPOUND op on a whole tagged-union IDENT (`g OP= v` // with g:(int|bool)) is nonsense — the ident load-combine-store // tail below reads and writes one word of the {payload,tag} box, // corrupting the tag. Reject loud here, the ident twin of the #18 // deref / #133 index rejects; the byte-id twin of the cstage // guard. Plain `=` (the tagged-ident reassign arm just below) is // untouched. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT && n.op != tkind.TK_ASSIGN) { let itu: *tinfo = tichase(lhs.type_: *tinfo); if (itu != nil) { if (itu.kind == tykind.TY_TAGGED) { let m21: str = "ident compound on tagged not wired (#21/rule-7)\n"; os.write(2, m21.ptr, m21.len: u64); os.exit(1); }; }; }; }; // 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)) { // #38b: an sret-classified tagged CALL // result is in memory, not the cursor — // an exact-type reassign sret's into the // local's own slot; a widening receive // needs mem-to-mem tag-remap (#40). // Mirrors cstage cgen.c N_ASSIGN tagged // arm + the generic sret receive. let asret: i32 = 0; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { asret = callsretsize(c, n.rhs); }; }; if (asret > 0) { let aru: *tinfo = n.rhs.type_: *tinfo; aru = tichase(aru); let alu: *tinfo = lc.tnode.type_: *tinfo; alu = tichase(alu); let aexact: bool = false; if (aru != nil && aru == alu) { aexact = true; } else { if (typeeq(n.rhs.type_: *tinfo, lc.tnode.type_: *tinfo)) { aexact = true; }; }; if (!aexact) { let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; os.write(2, m40d.ptr, m40d.len: u64); os.exit(1); }; c.sretdestoff = lc.off; cgexpr(c, n.rhs); c.sretdestoff = 0; return; }; let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode.type_: *tinfo, n.rhs, "BP", lc.off, lsz); return; }; }; // #38b: sret receive into a tagged GLOBAL // lvalue unwired (rule 7; cstage twin fatals). if (lc == nil && n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { let gru: *tinfo = lhs.type_: *tinfo; gru = tichase(gru); if (gru != nil && gru.kind == tykind.TY_TAGGED && callsretsize(c, n.rhs) > 0) { let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n"; os.write(2, m38g.ptr, m38g.len: u64); os.exit(1); }; }; }; // #32 (#263 ww-runtime-correct): tagged-union GLOBAL reassign // `g = expr`. No BP slot — LEAQ g(SB),BX then the shared widener // stores tag+payload off BX (mirror the local arm above + the // global-struct-field tagged arm at :10220). Pre-fix the generic // scalar store below clobbered the tag word. cstage drops the // store entirely — cs!=ww residual until the cstage half (#41). if (lc == nil) { let gtn: *node = letvartnode(c, lhs.str); if (gtn != nil) { if (istaggedtype(c, gtn)) { let gsz: i32 = slotsize(c, gtn); emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), BX\n"); cgwidentaggedstore(c, gtn.type_: *tinfo, n.rhs, "BX", 0, gsz); 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. // Retained gap: an aggregate >8B rhs (ident, tuple-lit, call) // truncates to one word here — task #31 A/E/G; struct-lit // diverts at the placeslit gate, array-lit dies loud (#32). if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { if (n.op == tkind.TK_ASSIGN && !placeslit && !derefagg) { let inner: *node = lhs.lhs; // #17: tagged-union pointee. The single-store // tail below writes rhs into the tag word only, // dropping the payload and corrupting the union. // Materialise the widened value (tag + payload // words, nullable fold, tag remap) into the shared // @tagscr scratch via cgwidentaggedstore, then // word-copy scratch -> *p. Mirror of the runtime- // index tagged element arm (cgenexpr.ww:8768) and // the cstage twin (cmd/w6c/cgen.c #17 deref arm). let du: *tinfo = tichase(lhs.type_: *tinfo); if (du != nil && du.kind == tykind.TY_TAGGED) { let ssz: i32 = du.size: i32; let scr: i32 = tagscradd(c, ssz); emitline("\tXORQ\tAX, AX\n"); let zk: i32 = 0; for (zk < ssz) { emitline("\tMOVQ\tAX, "); emitoff((scr + zk): i64); emitline("(BP)\n"); zk += 8; }; cgwidentaggedstore(c, du, n.rhs, "BP", scr, ssz); cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); let ck: i32 = 0; for (ck < ssz) { emitline("\tMOVQ\t"); emitoff((scr + ck): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(ck: i64); emitline("(BX)\n"); ck += 8; }; return; }; 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 { // primsize-ok (#101/#109): this site OWNS its own ps==0 // typenodeprimresolved chase below — routing through // aliasprimsize would double-resolve and regress #11. let ps: i32 = primsize(pe.str); // #11: primsize is name-keyed and // misses a `!`/enum/name alias // (`type errno = !i32`); peel it to // the underlying primitive width so // the store narrows, as cstage's // type-resolved pointee does // (cgen.c:4647-4652). Residual: // non-ident pointers + str/float-alias // deref-store widths stay name-blind // (#10 wwstage->tinfo SSoT). if (ps == 0) { let uns: bool = false; typenodeprimresolved(c, pe, &ps, &uns); }; 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) { // Size gate (mirror cstage cgen.c handled=sz∈{1,2,4,8}): // a tagged (or any non-scalar) pointee is not a // meaningful compound target — skip this single-word // store-and-return arm so `*p OP= v` on *tagged falls // through to the assign-resolver's loud TY_TAGGED reject // (#18). Without it the MOVQ default below clobbers the // tag word and returns: a silent miscompile. let psz: i32 = 8; let lt: *tinfo = lhs.type_: *tinfo; if (lt != nil) { psz = lt.size: i32; }; let scalarpointee: bool = (psz == 1 || psz == 2 || psz == 4 || psz == 8); if (n.op != tkind.TK_ASSIGN && scalarpointee) { 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 && !placeslit) { 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; let basealias: bool = false; 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); // idxelemtn drills `*[N]T` to the pointee // array's element (#61): an undrilled elemtn // made the width chooser believe the element // IS the whole array (N*8B aggregate copy // from an 8B source — frame smash). elemtn = idxelemtn(baselocal.tnode); } else { // #11: store/compound twin of the #10 cgindex // read fix. A global str/slice element store hit // the same kind whitelist — N_TNAME (str) / // N_TSLICE matched NEITHER arm, so esz stayed 8 // and the store emitted a full-word MOVQ — an // 8-byte OUT-OF-BOUNDS write past a 1-byte // element — instead of MOVB. cstage // (cmd/w6c/cgen.c N_INDEX store) dispatches esz // off idx_eff->sub->size + the elem-kind flags // off eff->sub uniformly, base is_arr?LEAQ:MOVQ // name(SB). Align UP and resolve elemtn exactly // like the local branch above (element node for // ARRAY/SLICE/PTR; nil for str so tnodestoreop // picks MOVB on the store arm, and the compound // arm's str/slice hard-error still fires on a // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { globalname = bn; esz = elemsizeofc(c, tn); // idxelemtn: `*[N]T` drill, see the // local branch above (#61). elemtn = idxelemtn(tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; } else { isglobalptr = true; }; }; }; } else { if (base.kind == nkind.N_DOT || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // 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). N_UN deref base // (`(*p)[i] = v`, #61 C): same stamped source. 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; }; };};}; }; // #60 (write spine): alias-NAMED N_IDENT base — the // tnode walk above is blind (esz 1-sentinel, elemtn // nil → pointer-treated base, wrong-stride store). // Adopt the stamped-element idiom of the N_DOT/ // N_INDEX arms (lhs.type_ IS the element tinfo); // cstage N_INDEX store reads idx_eff(base->type)->sub // uniformly (cmd/w6c/cgen.c:3517-3518). if (base != nil) { if (base.kind == nkind.N_IDENT) { let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { basealias = true; }; }; if (basealias) { let et60: *tinfo = tichase(lhs.type_: *tinfo); if (et60 != nil) { esz = et60.size: i32; elemtn = lhs; }; // see cgindex twin: cs-aligned, runtime- // unreachable until #77/#78 global DATA. if (isglobalarr || isglobalptr) { let bu60: *tinfo = tichase(bt60); if (bu60 != nil) { isglobalarr = bu60.kind == tykind.TY_ARRAY; isglobalptr = !isglobalarr; }; }; }; }; }; // 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 same-size tagged-arr // stores in the function; first-use sizes the slot // (#15/#26c, size-keyed by #44). if (elemtn != nil) { if (istaggedtype(c, elemtn)) { let slot_sz: i32 = slotsize(c, elemtn); let scroff: i32 = tagscradd(c, slot_sz); // 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; }; }; // #60: alias-NAMED base — chased kind // (see cgindex twin). if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarr = bu60.kind == tykind.TY_ARRAY; }; }; 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 { if (dotbaseaddr(c, base, "BX")) { // #259: N_DOT base resolved inline to // the field address; cgexpr fallback // would auto-deref + load the array // field as a VALUE (broken shape). dst // BX keeps the scaled index live in AX. } 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; }; }; // #234: over-cap sret STORE into an indexed lvalue — // `arr[i] = wide();` STORE-twin of the Fold-B sret RECEIVE // (a937d67). c.sretdestoff is a STATIC BP-relative offset, so // only a CONSTANT index into a LOCAL value array yields a // static dest slot (off + idx*esz) the callee can sret // straight into. Every other indexed form — runtime index, // slice/ptr base, N_DOT-base (`s.arr[i]`), chained (`a[i][k]`), // global base — needs the runtime RDI-pointer dest variant // deferred to #234-tail and HARD-STOPS loud (rule 7). Mirror of // cstage cgen.c (#234) N_INDEX arm. // // Gate ENTRY on callsretsize(n.rhs) — the callee-return-type // SSoT (cgenutil.ww) the receive sites use — NOT on // sretretsize(elemtn): elemtn is only a type node for an // N_IDENT base, but a value node for N_DOT (4695) / chained // (4710), which fell to sretretsize=0 and let those forms // drop SILENTLY through to the truncating store. The callee // return type equals the dest-element type (checker-guaranteed), // so the verdict is byte-identical to cstage's cg_sret_retsize. // The base-shape split below then loud-stops every non-local- // array form, base-kind-independent. if (n.rhs != nil && n.rhs.kind == nkind.N_CALL && callsretsize(c, n.rhs) > 0) { let islocalarr: bool = false; if (baselocal != nil) { let btn: *node = baselocal.tnode; if (btn != nil) { if (btn.kind == nkind.N_TARRAY) { islocalarr = true; }; }; }; let constidx: bool = false; let cidx: i32 = 0; if (idx != nil) { if (idx.kind == nkind.N_INTLIT) { constidx = true; cidx = idx.uval: i32; }; }; if (!islocalarr || !constidx) { let m234: str = "#234-tail: over-cap tuple sret store to non-local/dynamic-index dest unsupported\n"; os.write(2, m234.ptr, m234.len: u64); os.exit(1); }; c.sretdestoff = baselocal.off + cidx * esz; cgexpr(c, n.rhs); c.sretdestoff = 0; return; }; // #121 (write-face of leg-b): a tuple-LITERAL rhs into // an indexed element `a[i] = (3,4)`. A literal has no // source ADDRESS, so the ident/dot/deref copy arm below // can't reach it — it fell to the 1-word scalar store // tail (word0 only; the read-luck masked it until leg-b's // correct read). Materialise the literal into a frame // scratch via the cglet in-cap path (cgtuplelittocursor + // tupstore), then word-copy scratch → &a[i]. NARROW: // N_TTUPLE-literal rhs, N_IDENT base (idxelemtn gives the // element N_TTUPLE), in-cap. Mirror of cstage cgen.c #121 // store arm; the materialise + base-resolve are the // cglet / #270-1b byte-id twins. if (n.rhs.kind == nkind.N_TUPLE && esz > 8 && base != nil && base.kind == nkind.N_IDENT && elemtn != nil && elemtn.kind == nkind.N_TTUPLE && sretretsize(c, elemtn) == 0) { let scr121: i32 = tagscradd(c, esz); cgtuplelittocursor(c, n.rhs, elemtn); let gpc: i32 = 0; let ssc: i32 = 0; let eo: i32 = 0; let q121: *node = elemtn.list; for (q121 != nil) { let qt: *node = q121.lhs; let isflt: bool = isfloattype(c, qt); let es: i32 = tupeslotn(qt); tupstore(c, gpc, ssc, scr121 + eo, es, qt); if (isflt) { ssc += 1; } else { gpc += es / 8; }; eo += es; q121 = q121.next; }; // dest &a[i] → BX (#270-1b base resolve) 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 tn2: *node = baselocal.tnode; let isarr2: bool = false; if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; }; if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; }; }; if (isarr2) { 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")) { } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); emitline("\tADDQ\tAX, BX\n"); let kk2: i32 = 0; for (kk2 < esz) { emitline("\tMOVQ\t"); emitoff((scr121 + kk2): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(kk2: i64); emitline("(BX)\n"); kk2 += 8; }; return; }; // #270-1b: aggregate (struct/array/tuple >8B) element // STORE `a[i] = val`. The scalar store path below copies // only the first 8 bytes (tnodestoreop MOVQ) — a silent // truncation. Compute &a[i] (dest) and the rhs SOURCE // address, then word-copy esz bytes: the WRITE-twin of // the #268 let-init copy loop. Source shapes mirror that // loop (ident, N_DOT field via dotchainaddr, `*p` // deref); struct-lit sources divert at the placeslit // gate above (#20), array-lit dies loud (task #32), and // a by-value call result still falls to the scalar tail // — RAX-only store, task #31-G. esz>8 // non-str/non-slice IS a struct/array/tuple here (the // tagged element already returned above; floats are ≤8). let aggsrc: bool = (n.rhs.kind == nkind.N_IDENT) || (n.rhs.kind == nkind.N_DOT) || (n.rhs.kind == nkind.N_UN && n.rhs.op == tkind.TK_STAR); if (esz > 8 && !isstrtype(c, elemtn) && !isslicetype(c, elemtn) && aggsrc) { 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 tn2: *node = baselocal.tnode; let isarr2: bool = false; if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; }; // #60: alias-NAMED base — chased kind (see cgindex twin). if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; }; }; if (isarr2) { 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")) { // N_DOT array-field base resolved inline. } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPUSHQ\tBX\n"); // spill dest // rhs source address → SI if (n.rhs.kind == nkind.N_UN && n.rhs.op == tkind.TK_STAR) { cgexpr(c, n.rhs.lhs); emitline("\tMOVQ\tAX, SI\n"); } else { if (n.rhs.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, n.rhs.str); if (sl != nil) { emitline("\tLEAQ\t"); emitoff(sl.off: i64); emitline("(BP), SI\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, n.rhs.str); emitline("(SB), SI\n"); }; } else { dotchainaddr(c, n.rhs, "SI"); };}; emitline("\tPOPQ\tBX\n"); // dest let kc: i32 = 0; for (kc + 8 <= esz) { emitline("\tMOVQ\t"); emitoff(kc: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(kc: i64); emitline("(BX)\n"); kc += 8; }; if (kc + 4 <= esz) { emitline("\tMOVL\t"); emitoff(kc: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff(kc: i64); emitline("(BX)\n"); kc += 4; }; if (kc + 2 <= esz) { emitline("\tMOVW\t"); emitoff(kc: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff(kc: i64); emitline("(BX)\n"); kc += 2; }; if (kc + 1 <= esz) { emitline("\tMOVB\t"); emitoff(kc: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff(kc: i64); emitline("(BX)\n"); kc += 1; }; 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; }; }; // #60: alias-NAMED base — chased kind (see cgindex twin). if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; }; }; 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); // idxelemtn drills `*[N]T` to the pointee // array's element (#61): an undrilled elemtn // made the width chooser believe the element // IS the whole array (N*8B aggregate copy // from an 8B source — frame smash). elemtn = idxelemtn(baselocal.tnode); } else { // #11: store/compound twin of the #10 cgindex // read fix. A global str/slice element store hit // the same kind whitelist — N_TNAME (str) / // N_TSLICE matched NEITHER arm, so esz stayed 8 // and the store emitted a full-word MOVQ — an // 8-byte OUT-OF-BOUNDS write past a 1-byte // element — instead of MOVB. cstage // (cmd/w6c/cgen.c N_INDEX store) dispatches esz // off idx_eff->sub->size + the elem-kind flags // off eff->sub uniformly, base is_arr?LEAQ:MOVQ // name(SB). Align UP and resolve elemtn exactly // like the local branch above (element node for // ARRAY/SLICE/PTR; nil for str so tnodestoreop // picks MOVB on the store arm, and the compound // arm's str/slice hard-error still fires on a // []str element). let tn: *node = letvartnode(c, bn); if (tn != nil) { globalname = bn; esz = elemsizeofc(c, tn); // idxelemtn: `*[N]T` drill, see the // local branch above (#61). elemtn = idxelemtn(tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; } else { isglobalptr = true; }; }; }; } else { if (base.kind == nkind.N_DOT || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { // N_UN deref base (`(*p)[i] OP= v`, #61 C): // same stamped source as the store arm. 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; }; };};}; }; // #60 (compound spine): alias-NAMED N_IDENT base — // same stamped-element adoption as the store arm. let basealias: bool = false; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bt60: *tinfo = base.type_: *tinfo; if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) { basealias = true; }; }; if (basealias) { let et60: *tinfo = tichase(lhs.type_: *tinfo); if (et60 != nil) { esz = et60.size: i32; elemtn = lhs; }; if (isglobalarr || isglobalptr) { let bu60: *tinfo = tichase(bt60); if (bu60 != nil) { isglobalarr = bu60.kind == tykind.TY_ARRAY; isglobalptr = !isglobalarr; }; }; }; }; }; // #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; }; }; // #60: alias-NAMED base — chased kind (see cgindex twin). if (basealias) { let bu60: *tinfo = tichase(base.type_: *tinfo); if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; }; }; 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 && !placeslit) { 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; // idxelemtn: `*[N]T` drills to the pointee // array's element (#61). let elemt: *node = idxelemtn(tn); let baseisarray: bool = tn.kind == nkind.N_TARRAY; let snode: *node = nil; 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) { snode = inner; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { snode = elemt; };}; }; // #102 (ken B6-c3 re-attribution): an alias-NAMED // element misses the bare name-keyed lookup, so // `arr[i].f = v` fell to the generic place route — // runtime-correct but byte-divergent from the // dedicated shape cs pins post-B6-c3 (the READ twin // above already chases via tichase, task #8). // structlookupchain (#22) chases the alias chain; // plain rows short-circuit at its structlookup // head, byte-id by construction. esz stays sound: // elemsizeofc reads the chased stamped tinfo (#8). if (snode != nil) { let si: *structinfo = structlookupchain(c, snode); 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; }; // #58: a TAGGED field of an indexed // array element (`xs[i].f = v`). The // scalar store below would write the // raw unboxed rhs into the TAG slot — // never boxing, never writing the // payload (box-corruption, the #38a // write-twin). BOX (mirror the #24 // tagged-field-assign tag lookup, // taggedvariantindext) + STORE spine // (mirror the co-located str/slice // 3-word arm above): cgexpr the // payload, spill across the index/ // address computation, compute // &xs[i]->BX, store the variant tag // (constant) at foff+0 and the scalar // payload at foff+8. Only a SCALAR- // payload variant (box <=16B) store // is wired here. A >16B / multi-word / // float-payload union field IS // constructible (a wide box, built via // a NARROW variant — not unbuildable as // earlier triage assumed; #54/#23 fires // only on STRUCT-LITERAL payloads), but // its box+memcpy store arm is not yet // wired, so it LOUD-STOPS rather than // silently corrupting the box (rule 7, // the #41 untested-arm trap), byte-id- // neutral. Reachable + pinned expect- // loud (test/wcc/944 cfail rows). When // #114 wires them, that commit replaces // these stops with the real box+memcpy // emission + value pin rows. Mirrors // cstage cgen.c. if (istaggedtype(c, fi.tnode)) { let bsz: i32 = slotsize(c, fi.tnode); if (bsz > TUPLE_GPCAP * 8) { let m58s: str = "#58: >32B tagged-field indexed store unreachable until #114\n"; os.write(2, m58s.ptr, m58s.len: u64); os.exit(1); }; if (bsz > 16) { let m58m: str = "#58: multi-word tagged-field indexed store unreachable until #114\n"; os.write(2, m58m.ptr, m58m.len: u64); os.exit(1); }; if (typeisfloat(n.rhs.type_: *tinfo)) { let m58f: str = "#58: float-payload tagged-field indexed store unreachable until #114\n"; os.write(2, m58f.ptr, m58f.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"); }; 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 v58tag: i32 = taggedvariantindext(c, fi.tnode.type_: *tinfo, n.rhs); if (v58tag < 0) { v58tag = 0; }; emitline("\tMOVQ\t$"); emitint(v58tag: i64); emitline(", "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); emitline("\tMOVQ\tAX, "); emitdispreg((fi.foff + 8): i64, "BX"); 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. #33/#263: // all 10 integer ops wired (was 6 → // SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ // silently no-op'd in BOTH stages); // float/str/slice/tagged field hard- // errors LOUD. Mirrors cstage cgen.c // arr[i].field compound twin. if (istaggedtype(c, fi.tnode)) { let m: str = "arr[i].field compound on tagged field not wired (#33/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isstrtype(c, fi.tnode)) { let m: str = "arr[i].field compound on str field not wired (#33/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isslicetype(c, fi.tnode)) { let m: str = "arr[i].field compound on slice field not wired (#33/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (isfloattype(c, fi.tnode)) { let m: str = "arr[i].field compound on float field not wired (#33/rule-7)\n"; os.write(2, m.ptr, m.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"); }; 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"); let unsignd_x: bool = false; if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { unsignd_x = typeisunsigned(fi.tnode.type_: *tinfo); }; }; let wired_x: bool = false; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_SLASHEQ) { if (unsignd_x) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; wired_x = true; }; if (n.op == tkind.TK_PERCENTEQ) { if (unsignd_x) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; emitline("\tMOVQ\tDX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_x = true; }; if (n.op == tkind.TK_RSHIFTEQ) { if (unsignd_x) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired_x = true; }; if (!wired_x) { let m: str = "arr[i].field compound: unknown op (#33/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; 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; }; // #234-tail: via-ptr (`p.f`) sret field STORE. The dest must // be a runtime RDI pointer (the BP-relative c.sretdestoff // can't name `p.f`); deferred. HARD-STOP loud, never fall // through to the truncating generic store (rule 7). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && sretretsize(c, fi.tnode) > 0) { let m234: str = "#234-tail: over-cap tuple sret store to via-ptr field dest unsupported\n"; os.write(2, m234.ptr, m234.len: u64); os.exit(1); }; // 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + 4 <= ssz) { emitline("\tMOVL\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 4; }; if (k + 2 <= ssz) { emitline("\tMOVW\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 2; }; if (k + 1 <= ssz) { emitline("\tMOVB\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 1; }; 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). cgdotfieldhardstop(c, fi.tnode); let uns34: bool = false; if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = typeisunsigned(fi.tnode.type_: *tinfo); }; }; cgdotfieldcombine(c, n.op, uns34); }; 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; }; // #234: over-cap sret STORE into a LOCAL struct field — // `s.f = wide();` where f's type returns via sret // (sretretsize > 0: a >24B struct OR an over-cap tuple). // STORE-twin of the Fold-B sret RECEIVE (a937d67): point // the callee's hidden RDI dest at the field slot // (c.sretdestoff = lc.off + fi.foff) so it writes the // WHOLE value there, never the truncating generic store // below. Mirror of cstage cgen.c (#234) field local arm. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && sretretsize(c, fi.tnode) > 0) { c.sretdestoff = lc.off + fi.foff; cgexpr(c, n.rhs); c.sretdestoff = 0; 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + 4 <= ssz) { emitline("\tMOVL\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= ssz) { emitline("\tMOVW\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= ssz) { emitline("\tMOVB\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); k += 1; }; return; };}; }; if (n.op != tkind.TK_ASSIGN) { // Compound on direct struct-local // scalar field: load current → push // → eval rhs → combine → store // (mirror cgen.c:3477 local arm). let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), BX\n"); emitline("\tPUSHQ\tBX\n"); }; cgexpr(c, n.rhs); if (n.op != tkind.TK_ASSIGN) { emitline("\tPOPQ\tBX\n"); // PLUSEQ commutes; MINUSEQ needs // lhs-rhs (BX old lhs, AX rhs). cgdotfieldhardstop(c, fi.tnode); let uns34: bool = false; if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = typeisunsigned(fi.tnode.type_: *tinfo); }; }; cgdotfieldcombine(c, n.op, uns34); }; // 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. cgdotfieldcombine(c, n.op, false); 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; }; }; if (n.op != tkind.TK_ASSIGN) { // Compound on local `str|slice` pseudo-field: // load current → push → eval rhs → combine → // store (mirror cgen.c:3235 local arm). emitline("\tMOVQ\t"); emitoff((lc.off + delta): i64); emitline("(BP), BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); cgdotfieldcombine(c, n.op, false); emitline("\tMOVQ\tAX, "); emitoff((lc.off + delta): i64); emitline("(BP)\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)) { // #234-tail: GLOBAL (`g.f`) sret field STORE. c.sretdestoff is // BP-relative only and can't name a global slot; the runtime // RDI-pointer dest variant is deferred. HARD-STOP loud, never // the truncating generic store (rule 7). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && sretretsize(c, fi.tnode) > 0) { let m234: str = "#234-tail: over-cap tuple sret store to global field dest unsupported\n"; os.write(2, m234.ptr, m234.len: u64); os.exit(1); }; // 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 && aliasprimsize(c, 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 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + 4 <= ssz) { emitline("\tMOVL\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 4; }; if (k + 2 <= ssz) { emitline("\tMOVW\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 2; }; if (k + 1 <= ssz) { emitline("\tMOVB\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 1; }; return; };}; }; // #129: tagged-union field on global struct. LEAQ // base(SB) into BX then the shared widener handles // every rhs shape. Mirrors cstage cgen.c:4902 // is_global arm. Without this the generic TK_ASSIGN // below truncates to 1 word, silently dropping tag // and payload. if (n.op == tkind.TK_ASSIGN && istaggedtype(c, fi.tnode)) { let fsz: i32 = slotsize(c, fi.tnode); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); cgwidentaggedstore(c, fi.tnode.type_: *tinfo, n.rhs, "BX", fi.foff, fsz); return; }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { // str OR slice field: str IS []u8, so // both store the full {ptr,len,cap} // header (cstage cgen.c:5055 gates // TY_STR||TY_SLICE the same; without the // slice arm this dropped to the 1-word // scalar store below). 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"); cgdotfieldhardstop(c, fi.tnode); let uns34: bool = false; if (fi.tnode != nil) { if (fi.tnode.type_ != nil) { uns34 = typeisunsigned(fi.tnode.type_: *tinfo); }; }; cgdotfieldcombine(c, n.op, uns34); 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; ct = tichase(ct); let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; cs = tichase(cs); if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr && croot != nil && croot.kind == nkind.N_IDENT && localfindnode(c, croot.str) != nil) { it = base.type_: *tinfo; }; it = tichase(it); if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; st = tichase(st); 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) { // tagged leaf (#38a): eval the *struct // base into BX, then the shared widener // (it spills BX across its internal // cgexpr) — same base-then-widen order // as the single-dot via-ptr arm. The // scalar tail below stored ONE sized // word at the field offset: the rhs // landed in the TAG slot (ken b8). if (typeistagged(ft)) { let flu: *tinfo = ft; flu = tichase(flu); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); cgwidentaggedstore(c, flu, n.rhs, "BX", tf.offset: i32, flu.size: i32); return; }; 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; }; // tagged leaf (#38a): full slot rewrite via the shared // widener — the single-dot tagged-field arm verbatim // (cgwidentaggedstore spills the BX base itself). The // scalar tail below stored ONE sized word at the field // offset: the rhs landed in the TAG slot and the payload // kept its old bytes (ken x5d: `o.r.min = 8: size` left // `is size` false). Only plain `=` reaches this walker // (TK_ASSIGN gate above). if (typeistagged(leaftype)) { let wlu: *tinfo = leaftype; wlu = tichase(wlu); let wtsz: i32 = wlu.size: i32; 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"); }; cgwidentaggedstore(c, wlu, n.rhs, "BX", totaloff, wtsz); } else { cgwidentaggedstore(c, wlu, n.rhs, "BP", rootoff + totaloff, wtsz); }; 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; lp = tichase(lp); 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 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + 4 <= ssz) { emitline("\tMOVL\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\tMOVL\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\tMOVL\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; k += 4; }; if (k + 2 <= ssz) { emitline("\tMOVW\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\tMOVW\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\tMOVW\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; k += 2; }; if (k + 1 <= ssz) { emitline("\tMOVB\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\tMOVB\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\tMOVB\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; k += 1; }; 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 (aliasprimsize(c, 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. // C1: a name that is neither a local nor a // let dies LOUD — the pre-C1 return dropped // the whole statement silently (cstage twin: // the N_ASSIGN IDENT-tail fatal). if (!isletvar(c, nm)) { let mi1: str = "unsupported assign target: unresolved identifier '"; os.write(2, mi1.ptr, mi1.len: u64); os.write(2, nm.ptr, nm.len: u64); let mi2: str = "'\n"; os.write(2, mi2.ptr, mi2.len: u64); os.exit(1); }; // 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; }; // #220: `g = f();` where g is a GLOBAL aggregate >24B // (struct or #272 array). No BP slot for the sret dest, // so route RDI to g's symbol; the callee writes straight // into g's storage. The scalar store below would emit a // truncated `MOVQ AX, g(SB)` and drop the body. Mirror // of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && lvftn != nil) { let gsz: i32 = 0; if (lvftn.kind == nkind.N_TNAME) { let gsi: *structinfo = structlookup(c, lvftn.str); if (gsi != nil) { gsz = structabisize(gsi); }; }; if (lvftn.kind == nkind.N_TARRAY) { let gat: *tinfo = lvftn.type_: *tinfo; gat = tichase(gat); if (gat != nil) { gsz = gat.size: i32; }; }; if (gsz > 24) { let rscs: i32 = callsretsize(c, n.rhs); if (rscs > 0) { c.sretdestnode = lhs; cgexpr(c, n.rhs); c.sretdestnode = nil; return; }; }; }; // #272: `g = f();` where g is a GLOBAL ARRAY ≤24B. // The callee leaves AX/DX/CX (#272 reg-return; an array // is never float-class, so AX/DX/CX is always the // transport); the scalar store below would truncate to // MOVQ AX, g(SB). LEAQ the symbol into DI, store the // full+tail words. Mirror of cstage cgen.c ≤24B global // arm. #276: a ≤24B STRUCT global receive can be // float-class (X0/X1) so it stays at its pre-existing // behaviour — no consumer (rule-10 aligned with cstage; // note non-float struct globals also truncate symmetrically // here, byte-id-clean — #276 covers both). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && lvftn != nil && lvftn.kind == nkind.N_TARRAY) { let aggsz: i32 = 0; let aat: *tinfo = lvftn.type_: *tinfo; aat = tichase(aat); if (aat != nil) { aggsz = aat.size: i32; }; if (aggsz > 0 && aggsz <= 24) { cgexpr(c, n.rhs); emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), DI\n"); let full: i32 = aggsz / 8; let tail: i32 = aggsz - full * 8; let i: i32 = 0; for (i < full) { emitline("\tMOVQ\t"); emitline(tupreg(i)); emitline(", "); emitoff((i * 8): i64); emitline("(DI)\n"); i += 1; }; if (tail > 0) { let top: str = "MOVB"; if (tail == 4) { top = "MOVL"; }; if (tail == 2) { top = "MOVW"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(tupreg(full)); emitline(", "); emitoff((full * 8): i64); emitline("(DI)\n"); }; return; }; }; // #49 (global twin): aggregate module-let reassign — // `g = a` / `g = pt{...}`. Same funnel as the local // arm: structlit → mode-2 (DST_GLOBAL) fill; call → // loud (#276: ≤24B struct global receive was a // documented symmetric fall-through, now loud); // addressable rhs → aggargsrcaddr + LEAQ g(SB), BX // + aggcopy. Pre-#49 every shape fell to the scalar // tail below: one MOVQ AX, g(SB). if (n.op == tkind.TK_ASSIGN && lvftn != nil) { let gau: *tinfo = lvftn.type_: *tinfo; gau = tichase(gau); if (gau != nil) { if (gau.kind == tykind.TY_STRUCT || gau.kind == tykind.TY_ARRAY || gau.kind == tykind.TY_TUPLE) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { let gsi49: *structinfo = nil; if (lvftn.kind == nkind.N_TNAME) { gsi49 = structlookup(c, lvftn.str); }; if (gsi49 == nil) { // wwstage-only bail (anonymous // type; the @placescr precedent). let m49d: str = "assign: structlit layout unresolved (rule-7)\n"; os.write(2, m49d.ptr, m49d.len: u64); os.exit(1); }; cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0); return; }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; os.write(2, m49e.ptr, m49e.len: u64); os.exit(1); }; if (aggargsrcaddr(c, n.rhs, "SI")) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), BX\n"); aggcopy(c, gau.size: i32); return; }; let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; os.write(2, m49f.ptr, m49f.len: u64); os.exit(1); }; }; }; 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; }; // #10 Fold B: over-cap tuple reassign `t = f();`. t's // slot (off) IS the caller-prealloc dest; the callee // writes the whole tuple through hidden RDI. Keys on // callsretsize (the shared sret SSoT) for a tuple- // returning call — rettupleof distinguishes it from a // >24B struct, which keeps its own size-aware recv // below. Mirrors the cstage N_ASSIGN-ident over-cap arm. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL) { let rtup: *node = rettupleof(c, n.rhs); if (rtup != nil) { let rscs: i32 = callsretsize(c, n.rhs); if (rscs > 0) { c.sretdestoff = off; cgexpr(c, n.rhs); c.sretdestoff = 0; 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) { // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize (cstage // N_ASSIGN-IDENT branch sets sz = lu->size, // cgen.c:4704; check.c:760 SSoT). The pre- // #169 structnaturalsize shorts struct{i64,i32} // (natural 12, ABI 16) to MOVQ+MOVL where // cstage writes MOVQ+MOVQ. let lcnsz: i32 = structabisize(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; }; }; }; }; }; }; }; // #267: array return-by-value RECV — `c = mk()` where c is // an array local. Arrays ride the struct reg/sret recv path. // >24B sret keys on callsretsize (the shared SSoT, c's slot // IS the prealloc dest); ≤24B arrives in AX/DX/CX, sized // stores. Array natural size (tinfo.size = sub.size*len) // mirrors cstage lu->size. No structfloatclass (pure-int // element arrays). if (lcn != nil && n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL) { let acati: *tinfo = nil; if (lcn.tnode != nil) { acati = lcn.tnode.type_: *tinfo; }; acati = tichase(acati); if (acati != nil && acati.kind == tykind.TY_ARRAY) { let lcsz: i32 = acati.size: i32; if (lcsz > 24) { let rscs: i32 = callsretsize(c, n.rhs); if (rscs > 0) { c.sretdestoff = off; cgexpr(c, n.rhs); c.sretdestoff = 0; return; }; }; 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; }; // #49: aggregate (struct/array/tuple) IDENT // reassignment — any rhs shape that missed the // dedicated arms above (struct-lit fill, call // receive, sret) funnels through the ONE mem-to-mem // copy (aggargsrcaddr → SI, dst → BX, aggcopy), or // dies loud. Pre-#49 it fell to the scalar tail // below and word0-copied `b = a` (cstage cgen.c // N_ASSIGN aggregate-ident twin). Kind keys off the // checker-STAMPED tinfo (#209/#211 discipline). if (n.op == tkind.TK_ASSIGN) { let agu: *tinfo = nil; if (lcn != nil) { if (lcn.tnode != nil) { agu = lcn.tnode.type_: *tinfo; }; }; agu = tichase(agu); if (agu != nil) { if (agu.kind == tykind.TY_STRUCT || agu.kind == tykind.TY_ARRAY || agu.kind == tykind.TY_TUPLE) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { // wwstage-only bail: the fill is // structinfo-keyed; a literal // reaching past the name-keyed arm // above has no registry entry // (anonymous struct type). Loud, // rule 7 (the resolver @placescr // precedent); cstage fills from // Type directly. let m49a: str = "assign: structlit layout unresolved (rule-7)\n"; os.write(2, m49a.ptr, m49a.len: u64); os.exit(1); }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n"; os.write(2, m49b.ptr, m49b.len: u64); os.exit(1); }; if (aggargsrcaddr(c, n.rhs, "SI")) { emitline("\tLEAQ\t"); emitoff(off: i64); emitline("(BP), BX\n"); aggcopy(c, agu.size: i32); return; }; let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n"; os.write(2, m49c.ptr, m49c.len: u64); os.exit(1); }; }; }; 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; }; }; // F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the // enumerated arms above matched — today the deref-rooted spine // `(*p)[i].f = v` / `OP= v`. Base-address derivation routes // through cgplaceaddr; the load/store emission stays here. Any // N_DOT shape the resolver can't address dies LOUD below: the // pre-C1 fall-off-the-function tail silently emitted NOTHING // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. // #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — only // the struct-lit-rhs diversion above reaches here (every other // indexed/deref shape returned from its legacy arm), and the // C1.25 aggregate branch fills via @placescr. if (lhs != nil) { if (lhs.kind == nkind.N_DOT || lhs.kind == nkind.N_INDEX || (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)) { let ft: *tinfo = lhs.type_: *tinfo; let fu: *tinfo = ft; fu = tichase(fu); let fsz: i32 = 8; if (ft != nil) { fsz = ft.size: i32; }; if (typeisfloat(ft)) { let mf: str = "assign-resolver: float field not wired (rule-7)\n"; os.write(2, mf.ptr, mf.len: u64); os.exit(1); }; if (fu != nil) { if (fu.kind == tykind.TY_TAGGED) { let mt: str = "assign-resolver: tagged field not wired (rule-7)\n"; os.write(2, mt.ptr, mt.len: u64); os.exit(1); }; // C1.25 (#23): aggregate field STORE through the // resolver — run_thread's 40B capture store // `(*ts)[i].root_capture = capture{...}`. Dest // address from cgplaceaddr (BX), source address // in SI per rhs shape, then the #270-1b // word-copy tail (SI)→(BX). Pre-C1 a SILENT // no-op; C1 made it loud; this wires it // (loud-first, wire-next). Compound on an // aggregate is meaningless and stays loud. // Mirror of the cstage cgen.c C1.25 arm. if (fu.kind == tykind.TY_STRUCT || fu.kind == tykind.TY_ARRAY || fu.kind == tykind.TY_TUPLE) { if (n.op != tkind.TK_ASSIGN) { let mac: str = "assign-resolver: compound on aggregate field not wired (rule-7)\n"; os.write(2, mac.ptr, mac.len: u64); os.exit(1); }; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { // sret-class needs a runtime-RDI dest // (the #234-tail deferral); the ≤24B // reg-return receive is task #24. The // callee return type equals the field // type (checker-guaranteed), so // callsretsize gives cstage's // cg_sret_retsize(ft) verdict. if (callsretsize(c, n.rhs) > 0) { let mas: str = "assign-resolver: sret call into aggregate field unwired (#234-tail/rule-7)\n"; os.write(2, mas.ptr, mas.len: u64); os.exit(1); }; let ma24: str = "assign-resolver: call result into aggregate field unwired (task #24/rule-7)\n"; os.write(2, ma24.ptr, ma24.len: u64); os.exit(1); }; }; let placed: bool = false; let isslit: bool = false; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_STRUCTLIT && fu.kind == tykind.TY_STRUCT) { isslit = true; }; }; if (isslit) { // @placescr — FRESH slot PER USE (the // @slicescr discipline via localalloc, // NOT the cached @tagscr table: a // cached slot is the #31 multi-live // corruption trap; rob ruling). Funnel // contract, #44 discipline: this arm is // the ONLY @placescr alloc site. Fill // handles nested literals (#18), tagged // fields, TK_ELLIPSIS autofill; the // value sits in memory, so the resolver // below may clobber AX/CX freely. let sname: str; sname.ptr = nil; sname.len = 0; if (ft.kind == tykind.TY_NAMED) { sname = ft.name; }; let si: *structinfo = nil; if (sname.len > 0) { si = structlookup(c, sname); }; if (si == nil) { // wwstage-only bail: the fill is // structinfo-keyed, so an anonymous- // struct field type has no registry // entry (cstage fills from Type // directly). Loud, rule 7. let man: str = "assign-resolver: structlit field layout unresolved (rule-7)\n"; os.write(2, man.ptr, man.len: u64); os.exit(1); }; let scr: i32 = localalloc(c, "@placescr", fsz, nil); cgstructlitfillbp(c, si, n.rhs, scr); placed = cgplaceaddr(c, lhs, "BX"); if (placed) { emitline("\tLEAQ\t"); emitoff(scr: i64); emitline("(BP), SI\n"); }; } else { // Addressable source — ident / global / // N_DOT chain / deref — via the closed // #265/#268 dispatch. Its N_INDEX arm // clobbers BX, so the dest spills around // it (the #270-1b order). Literal // arrays/tuples have no storage address // and stay loud. placed = cgplaceaddr(c, lhs, "BX"); if (placed) { emitline("\tPUSHQ\tBX\n"); if (!aggargsrcaddr(c, n.rhs, "SI")) { let mar: str = "assign-resolver: aggregate rhs shape unwired (rule-7)\n"; os.write(2, mar.ptr, mar.len: u64); os.exit(1); }; emitline("\tPOPQ\tBX\n"); }; }; if (!placed) { let mau: str = "unsupported assign target shape\n"; os.write(2, mau.ptr, mau.len: u64); os.exit(1); }; aggcopy(c, fsz); return; }; }; let fstrsl: bool = false; if (fu != nil) { if (fu.kind == tykind.TY_STR || fu.kind == tykind.TY_SLICE) { fstrsl = true; }; }; if (fstrsl) { if (n.op != tkind.TK_ASSIGN) { let ms: str = "assign-resolver: compound on str/slice field not wired (rule-7)\n"; os.write(2, ms.ptr, ms.len: u64); os.exit(1); }; // str IS []u8: store the whole {ptr,len,cap} // triple from (AX,BX,CX); the place address // goes in DX so the three pops survive // (#1/Phase 3). cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); if (cgplaceaddr(c, lhs, "DX")) { emitline("\tPOPQ\tAX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tAX, (DX)\n"); emitline("\tMOVQ\tBX, 8(DX)\n"); emitline("\tMOVQ\tCX, 16(DX)\n"); return; }; } else { if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); if (cgplaceaddr(c, lhs, "BX")) { emitline("\tPOPQ\tAX\n"); let sop: str = "MOVQ"; if (fsz == 1) { sop = "MOVB"; }; if (fsz == 2) { sop = "MOVW"; }; if (fsz == 4) { sop = "MOVL"; }; emitline("\t"); emitline(sop); emitline("\tAX, (BX)\n"); return; }; } else { // Compound: AX=old, CX=rhs, BX=addr — the same // register roles as the chained-ptr-field // compound template above. cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); if (cgplaceaddr(c, lhs, "BX")) { let lop: str = loadopsz(typeissigned(ft), fsz); emitline("\t"); emitline(lop); emitline("\t(BX), AX\n"); emitline("\tPOPQ\tCX\n"); let unsignd: bool = typeisunsigned(ft); 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) { 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) { 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) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired = true; }; if (!wired) { let mu: str = "assign-resolver: unknown compound op (rule-7)\n"; os.write(2, mu.ptr, mu.len: u64); os.exit(1); }; let sop: str = "MOVQ"; if (fsz == 1) { sop = "MOVB"; }; if (fsz == 2) { sop = "MOVW"; }; if (fsz == 4) { sop = "MOVL"; }; emitline("\t"); emitline(sop); emitline("\tAX, (BX)\n"); return; }; }; }; let mtl: str = "unsupported assign target shape\n"; os.write(2, mtl.ptr, mtl.len: u64); os.exit(1); }; }; // C1 residual (task #22): a non-DOT lvalue no arm above matched // still falls out SILENT here — known member: the str-base element // store family (`s[i] = v`: cstage drops, wwstage emits MOVB; // pre-existing gate-blind divergence) plus tuple-member writes. // The tail goes loud for the remaining kinds with #22. 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) { // #40: at the cap, fail loud in BOTH stages rather than // silently drop the deferred call. cstage's DEFER_MAX was 32 // and also dropped silently past it; the runtime-correct target // is a hard stop at the shared cap (cgen.c twin fatals too). if (c.defertop >= DEFER_MAX) { let msg: str = "cgen: too many defers in one function\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; 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"; }; // tupeslot — THE tuple element-stride accessor (#22): the slot a tuple // element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR // not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B // header, a tagged element its full tag+payload box ((str,str)=48B // predates this; tagged was the one truncated >8B kind — the #237 // fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte. // Every tuple walk (cursor send/receive, t.N read, destructure, sret // classify, DATA emit) takes its stride and its eightbyte count // (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8 // predicates this absorbs were the #22 neighbor-slot/zeros miscompile. // Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin: // tuple_eslot (cmd/w6c/cgen.c). export fn tupeslot(ti: *tinfo) i32 = { let t: *tinfo = ti; t = tichase(t); if (t == nil) { return 8; }; if (t.kind == tykind.TY_VOID) { return 0; }; // a literal tuple's stamped element can be untyped_str (size 0) — // it occupies the str header slot (the C-t2 type_isstr lesson). if (t.kind == tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; if (t.kind == tykind.TY_STR || t.kind == tykind.TY_SLICE || t.kind == tykind.TY_TAGGED) { return ((t.size + 7u64) & ~7u64): i32; }; return 8; }; export fn tupeslotn(n: *node) i32 = { if (n == nil) { return 8; }; return tupeslot(n.type_: *tinfo); }; // 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; }; // nodetuplearg — the tuple node of a call ARG whose cgexpr fills the // return-ABI cursor (#163/#32, C-t2): an N_CALL or `?`/`!` unwrap (the // declared return / success variant via inferletcalltype), an N_IDENT // local (declared tnode, alias-peeled), or an N_TUPLE literal — returned // AS-IS, kind-discriminated at the walks (its elements are VALUE exprs, // classified the way cgtuplelittocursor classifies them, not type // nodes). Mirror of cstage node_tuplearg; the cgcall push site // loud-stops any other tuple-typed source shape (rule 7). rettupleof // stays N_CALL-scoped for the destructure/reassign receive sites. fn nodetuplearg(c: *cgen, a: *node) *node = { if (a == nil) { return nil; }; if (a.kind == nkind.N_TUPLE) { return a; }; if (a.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc == nil) { return nil; }; let tn: *node = lc.tnode; for (tn != nil && tn.kind == nkind.N_TNAME) { tn = aliaslookup(c, tn.str); }; if (tn != nil) { if (tn.kind == nkind.N_TTUPLE) { return tn; }; }; return nil; }; let t: *node = inferletcalltype(c, a); if (t != nil) { if (t.kind == nkind.N_TTUPLE) { return t; }; }; return nil; }; // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A >8B element (slice/str 3-word // {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload // box, #22) stores its eslot/8 words 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, eslot: i32, tn: *node) void = { if (eslot == 0) { return; }; // void element: the checker's 0-slot if (eslot > 8) { let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitline(tupreg(gpcur + k)); emitline(", "); emitoff((off + k * 8): i64); emitline("(BP)\n"); k += 1; }; 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"); }; // tuplitgpwords — INTEGER cursor words an N_TUPLE literal element // occupies. MUST mirror the literal push arms (tuplitpushelem) exactly // — the count drives the POP fill, so a count/push skew silently // shifts every later element (#22 class). A float rides the SSE row // (0 GP words); str/slice push their 3-word header; a tagged element // its tupeslot/8 box words; a void element pushes nothing (the // checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords. // // #57: `dtn` is the DECLARED tuple element TYPE node (nil when the // consumer has none). The N_TUPLE literal's stamped type is // CONSTRUCTED from its elements, so a concrete rvalue under a // declared-TAGGED slot counted ONE word here while the receive walks // the declared eslot — the cursor shifted and every later element // read garbage. Declared-tagged keys the count on the DECLARED box. fn tuplitgpwords(c: *cgen, e: *node, dtn: *node) i32 = { if (dtn != nil) { if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; }; }; if (isfloattype(c, e)) { return 0; }; if (nodeisstr(c, e) || nodeisslice(c, e)) { return (tyslicesize() / 8i64): i32; }; let t: *tinfo = e.type_: *tinfo; t = tichase(t); if (t != nil && (t.kind == tykind.TY_TAGGED || t.kind == tykind.TY_VOID)) { return tupeslotn(e) / 8; }; return 1; }; // tuplitpushelem — evaluate one N_TUPLE literal element and push its // INTEGER cursor words L->R (the pop side fills tupreg in reverse). A // tagged element loads its box words straight from its local slot — // cgexpr's ident load is word0-only for tagged (every tagged consumer // reads memory), so the cursor fill must too. Mirror of cstage // tuple_lit_push_elem — count (tuplitgpwords) and push live or die // together. // // #57: a DECLARED-tagged element whose expr is a concrete rvalue // (`return (5: size, 9)` — cast, literal, call) skipped the widen // entirely: the stamped-keyed arm below saw a scalar and pushed ONE // word, the receiver read the declared box words — silent shift, both // stages, gate-blind (ken /tmp/ken57). Such an element now widens // into the shared tagged scratch (cgwidentaggedstore, the cgreturn // tagged-@retscr shape) and pushes the box words. A tagged->tagged // SUBSET element (eslot mismatch) needs a tag remap on the way into // the slot — loud (rule 7, the #23/#40 widening family). fn tuplitpushelem(c: *cgen, e: *node, dtn: *node) void = { let t: *tinfo = e.type_: *tinfo; t = tichase(t); let etagged: bool = false; if (t != nil) { if (t.kind == tykind.TY_TAGGED) { etagged = true; }; }; if (dtn != nil) { if (istaggedtype(c, dtn) && !etagged) { let eslot: i32 = tupeslotn(dtn); let scr: i32 = tagscradd(c, eslot); emitline("\tXORQ\tAX, AX\n"); let z: i32 = 0; for (z < eslot) { emitline("\tMOVQ\tAX, "); emitoff((scr + z): i64); emitline("(BP)\n"); z += 8; }; cgwidentaggedstore(c, dtn.type_: *tinfo, e, "BP", scr, eslot); let pk: i32 = 0; for (pk < eslot / 8) { emitline("\tMOVQ\t"); emitoff((scr + pk * 8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); pk += 1; }; return; }; if (istaggedtype(c, dtn) && etagged && tupeslotn(dtn) != tupeslotn(e)) { let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n"; os.write(2, m57.ptr, m57.len: u64); os.exit(1); }; }; if (etagged) { let eslot: i32 = tupeslotn(e); let eoff: i32 = 0; if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; if (eoff == 0) { let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7; call-source is task #41, widening #23, deref/cast #35)\n"; os.write(2, m22.ptr, m22.len: u64); os.exit(1); }; let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((eoff + k * 8): i64); emitline("(BP), AX\n"); emitline("\tPUSHQ\tAX\n"); k += 1; }; return; }; cgexpr(c, e); if (t != nil && t.kind == tykind.TY_VOID) { return; }; emitline("\tPUSHQ\tAX\n"); if (nodeisstr(c, e) || nodeisslice(c, e)) { emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tCX\n"); }; }; // cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into // the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/ // R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three // consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves, // which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr // otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal // rvalue tuple bound or destructured read garbage past word0. Byte-identical // extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared // with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't // sret, so the >cap rvalue-tuple materialisation is the #10 follow-up. // // #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE, // nil when it has none — the bare cgexpr route). A declared-TAGGED // element gates the SSE row off (its payload may be float-stamped but // the BOX rides INTEGER eightbytes) and keys count + push on the // declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage // cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem // type in .lhs (the c.fnret.list shape the over-cap arm walks). fn cgtuplelittocursor(c: *cgen, tuple: *node, decl: *node) void = { let dp0: *node = nil; if (decl != nil) { if (decl.kind == nkind.N_TTUPLE) { dp0 = decl.list; }; }; let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; let dp: *node = dp0; let e: *node = tuple.list; for (e != nil) { let dtn: *node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; if (!dtagged && isfloattype(c, e)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tuplitgpwords(c, e, dtn); }; if (dp != nil) { dp = dp.next; }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\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; dp = dp0; e = tuple.list; for (e != nil) { let dtn: *node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; let isflt: bool = !dtagged && isfloattype(c, e); if (isflt) { cgexpr(c, e); 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 { tuplitpushelem(c, e, dtn); }; if (dp != nil) { dp = dp.next; }; 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; dp = dp0; e = tuple.list; for (e != nil) { let dtn: *node = nil; if (dp != nil) { dtn = dp.lhs; }; let dtagged: bool = false; if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; if (!dtagged && 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; }; if (dp != nil) { dp = dp.next; }; e = e.next; }; }; // cgtupleslottocursor — #241: load a tuple already materialised in a BP- // relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union // payload) into the SAME register cursor. The slot uses the register-ABI // stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str // its 3-word header), NOT the packed t.N field layout (#238). All sources // are memory, so each word loads straight into its cursor reg. So `yield t` // / `return t` / `let q = t` over a tuple ident leave the whole tuple in the // cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of // cstage cg_tuple_slot_to_cursor. fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { let gptotal: i32 = 0; let ssecount: i32 = 0; let el: *ttupleelem = tu.tupleelems; for (el != nil) { let et: *tinfo = el.type_; et = tichase(et); if (et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tupeslot(el.type_) / 8; }; el = el.tnext; }; if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let gp: i32 = 0; let sse: i32 = 0; let foff: i32 = 0; el = tu.tupleelems; for (el != nil) { let et: *tinfo = el.type_; et = tichase(et); let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); let eslot: i32 = tupeslot(el.type_); if (isflt) { let mov: str = "MOVSD"; if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), "); emitline(tupsse(sse)); emitline("\n"); sse = sse + 1; foff += 8; } else { let k: i32 = 0; for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k * 8): i64); emitline("(BP), "); emitline(tupreg(gp + k)); emitline("\n"); k += 1; }; gp += eslot / 8; foff += eslot; }; el = el.tnext; }; }; // cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an // rvalue tuple that must fill the register cursor. The tagged return leaves // AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only // word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one // INTEGER reg so element i lands in tupreg(i). Float/slice/str payload // elements ride a different SysV class — loud-stop (rule 7; the per- // eightbyte tagged-tuple-payload classification is the #243 follow-up). // Mirror of cstage cg_tagged_tuple_payload_shift. fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { let words: i32 = 0; let el: *ttupleelem = tup.tupleelems; for (el != nil) { let et: *tinfo = el.type_; et = tichase(et); let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); if (isflt || tupeslot(el.type_) != 8) { let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; words = words + 1; el = el.tnext; }; if (words > 3) { let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let i: i32 = 0; for (i < words) { emitline("\tMOVQ\t"); emitline(tupreg(i + 1)); emitline(", "); emitline(tupreg(i)); emitline("\n"); i = i + 1; }; }; 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. // #242: a bare tuple return packs into the register cursor; a // tuple WRAPPED IN A TAGGED UNION must instead pack into the // union payload (tag + words) — fall through to the tagged path // below, which routes it via cgwidentaggedstore. Without this // guard the bare-tuple arm fired first and dropped the tag, // returning (AX=word0, DX=word1) with no tag word. if (rhs.kind == nkind.N_TUPLE && !istaggedtype(c, c.fnret)) { let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; // #57: count + push key on the DECLARED return-type // element (c.fnret.list) — the literal's stamped type // is element-constructed, so a declared-TAGGED // element's concrete rvalue counted 1 word and skipped // the widen while the caller's receive walks the // declared eslot (2 words sent for a 3-word shape; // ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm // already does (#240/#22b). Mirrors cstage cgreturn. let rp0: *node = nil; if (c.fnret != nil) { if (c.fnret.kind == nkind.N_TTUPLE) { rp0 = c.fnret.list; }; }; let rp: *node = rp0; let e: *node = rhs.list; for (e != nil) { let rdtn: *node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; if (!rdtag && isfloattype(c, e)) { ssecount = ssecount + 1; } else { gptotal = gptotal + tuplitgpwords(c, e, rdtn); }; if (rp != nil) { rp = rp.next; }; e = e.next; }; // #22b: classify and emit MUST agree (the #10 SSoT note // at TUPLE_GPCAP). The over-cap DECISION rides // sretretsize on the DECLARED return type — the same // predicate the prologue (@sretarg) and the caller key // on. The expr-shape count above only pairs the in-cap // push/pop: a declared-tagged element whose expr is the // unwidened payload counts 1 word here vs 2+ declared // eightbytes, so the emit took the register path against // an sret-classified caller — silent garbage, both // stages, gate-blind (probe /tmp/i22b/p2). let overcap: bool = gptotal > TUPLE_GPCAP || ssecount > ssecap; if (c.fnret != nil) { overcap = sretretsize(c, c.fnret) > 0; }; if (overcap) { // #10 Fold A: over-cap tuple returns via sret. The // prologue wired @sretarg (sretretsize agrees on the // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), // holding the caller-prealloc dest. Store each element // through *(@sretarg) // at its packed layout offset (running sum of element // sizes from the return-type tuple node — the t.0/t.1 // positional layout), each at its natural width so a // narrow tail doesn't over-MOVQ (#169); the dest base is // reloaded into DX each step since a wide element's // cgexpr clobbers AX/BX/CX. Then reuse the struct-sret // epilogue. The CALL/receive side stays loud-stopped // (#10 Fold B). Byte-identical to cstage cgen.c // N_RETURN over-cap tuple arm. let saoff: i32 = localfind(c, "@sretarg"); let pt: *node = nil; if (c.fnret != nil) { pt = c.fnret.list; }; let we: *node = rhs.list; let foff: i32 = 0; for (we != nil) { let dt: *tinfo = nil; if (pt != nil) { dt = pt.lhs.type_: *tinfo; }; dt = tichase(dt); if (dt != nil && dt.kind == tykind.TY_TAGGED) { // #22b (task #28): MEMORY-class tagged // element — the whole box copies through // the sret pointer mem-to-mem from the // element's local slot. cgexpr can't // source it: the tagged ident load is // word0-only (every tagged consumer // reads memory) and the AX/DX/CX/R8 box // cursor would collide with the DX // dest-base reload. Ident-only, // mirroring tuplitpushelem; widening / // non-ident sources stay loud (#23/#40 // follow-ups). Mirror of cstage cgen.c // N_RETURN over-cap tagged arm. let eslot: i32 = tupeslotn(pt.lhs); let eu: *tinfo = we.type_: *tinfo; eu = tichase(eu); let eoff: i32 = 0; if (we.kind == nkind.N_IDENT && eu != nil) { if (eu.kind == tykind.TY_TAGGED && tupeslotn(we) == eslot) { eoff = localfind(c, we.str); }; }; if (eoff == 0) { let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return from a non-ident or widening source unwired (ident locals only; rule 7; call-source is task #41, widening #23/#40)\n"; os.write(2, m22b.ptr, m22b.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), DX\n"); let bk: i32 = 0; for (bk < eslot) { emitline("\tMOVQ\t"); emitoff((eoff + bk): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((foff + bk): i64, "DX"); emitline("\n"); bk += 8; }; foff += eslot; we = we.next; if (pt != nil) { pt = pt.next; }; continue; }; let isflt: bool = isfloattype(c, we); let wide: bool = nodeisstr(c, we) || nodeisslice(c, we); let esz: i32 = 8; if (pt != nil) { let eti: *tinfo = pt.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; cgexpr(c, we); emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), DX\n"); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, we)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(foff: i64, "DX"); emitline("\n"); } else { if (wide) { emitline("\tMOVQ\tAX, "); emitdispreg(foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((foff + 16): i64, "DX"); emitline("\n"); } else { let sop: str = tnodestoreop(c, we, esz); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(foff: i64, "DX"); emitline("\n"); }; }; // C-t0/#22: the sret buffer is slot-laid like // every tuple home (checker size, t.N // reader, mlet receive agree) — the stride // is THE accessor's (a declared void // element's 0-slot included; the old // wide?esz:8 advanced 8 where every receive // walks 0). esz keeps the store WIDTH // natural. Mirrors cstage cgen.c N_RETURN // over-cap arm. if (pt != nil) { foff += tupeslotn(pt.lhs); } else { foff += tupeslotn(we); }; we = we.next; if (pt != nil) { pt = pt.next; }; }; emitline("\tMOVQ\t"); emitoff(saoff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; // rule-7 net: register-classified by the declared type // but the expr-shape count overflows the cursor — the // pops below would index past tupreg. Unreachable while // expr counts never exceed declared counts; loud, not // OOB, if a future shape breaks that. Mirrors cstage. if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { let mskew: str = "register-classified tuple return exceeds the cursor (classify/emit skew; rule 7, #22b)\n"; os.write(2, mskew.ptr, mskew.len: u64); os.exit(1); }; let fscr: i32 = 0; if (ssecount > 0) { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; rp = rp0; e = rhs.list; for (e != nil) { let rdtn: *node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; let isflt: bool = !rdtag && isfloattype(c, e); if (isflt) { cgexpr(c, e); 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 { // scalar=AX; slice/str=AX,BX,CX; tagged // box from its slot or widened scratch // (tuplitpushelem) tuplitpushelem(c, e, rdtn); }; if (rp != nil) { rp = rp.next; }; 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; rp = rp0; e = rhs.list; for (e != nil) { let rdtn: *node = nil; if (rp != nil) { rdtn = rp.lhs; }; let rdtag: bool = false; if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; if (!rdtag && 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; }; if (rp != nil) { rp = rp.next; }; 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, R8=v2) — no shuffle, no // tag synthesis. Mirrors cstage cgen.c:8007 passthrough // = istagged && (vu == rt || type_eq(vt, cg_ret_type)). // TYPE-BASED predicate (was name-keyed via fnretlookupmod // IDENT/DOT-only) covers all callee shapes — including // deref-call N_UN(TK_STAR) per #201. Identity-on-peeled // handles the NAMED case (tinfocache memoizes per typedecl, // #191 lineage); the variant-pointer fallback handles the // anonymous case (each anonymous `(A|B)` decl gets its own // NAMED-less tinfo, so identity fails — e.g. cross-module // strings.byteindex returns the same anonymous (i32|void) // as bytes.index). Variant-pointer equality on the params // chain suffices because variants are primitives (single // tctx tinfo) or NAMED (per-decl identity); a full recursive // tinfo structural-eq helper is gated by #178. // #261: N_INDEX of a tagged element (`return x.o[i]`) and // N_DOT of a tagged field both materialize the full tagged // ABI shape via cgexpr (cgindex slot-copy / cgdot field-load, // AX=tag/DX=v0/...), exactly like an N_CALL of a tagged- // returning fn — so a same-type return forwards them // unchanged. cstage gates passthrough purely on the rhs type // (no kind filter, cgen.c:8845); without these kinds an // N_INDEX tagged-element return fell to the scalar-variant // shuffle (MOVQ AX,DX; MOVQ $0,AX), dropping the payload. let forwardtagged: bool = false; if ((rhs.kind == nkind.N_CALL || rhs.kind == nkind.N_INDEX || rhs.kind == nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { let ru: *tinfo = rhs.type_: *tinfo; ru = tichase(ru); let fu: *tinfo = c.fnret.type_: *tinfo; fu = tichase(fu); if (ru != nil && fu != nil && ru.kind == tykind.TY_TAGGED && fu.kind == tykind.TY_TAGGED) { if (ru == fu) { forwardtagged = true; } else if (ru.nullable == fu.nullable) { let pa: *tparam = ru.params; let pb: *tparam = fu.params; let same: bool = true; for (pa != nil && pb != nil) { if (pa.type_ != pb.type_) { same = false; }; pa = pa.tnext; pb = pb.tnext; }; if (same && pa == nil && pb == nil) { forwardtagged = true; }; }; }; }; // #38b: sret-classified tagged return (slot > the // AX/DX/CX/R8 cursor) — write through *(@sretarg) and // return the dest pointer. Three shapes mirror cstage // cgen.c N_RETURN #38b: exact-type N_CALL forward // (c.sretforward), widening from a >32B tagged source // (#40 loud-stop), everything else through // cgwidentaggedstore's non-BP base. if (sretretsize(c, c.fnret) > 0) { let sa38v: i32 = localfind(c, "@sretarg"); if (forwardtagged && rhs.kind == nkind.N_CALL) { // exact-type N_CALL forward: inner sret's // into outer's dest; an N_INDEX/N_DOT // source routes through the widener's // #37 mem-read arm below instead. c.sretforward = 1; cgexpr(c, rhs); emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; let ru38: *tinfo = rhs.type_: *tinfo; ru38 = tichase(ru38); if (ru38 != nil) { // #37 wired the N_INDEX/N_DOT mem-read into // the widener; the remaining >32B kinds stay // loud. if (ru38.kind == tykind.TY_TAGGED && rhs.kind != nkind.N_IDENT && ru38.size: i32 > TUPLE_GPCAP * 8 && !taggedmemread(c, rhs)) { let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; os.write(2, m38e.ptr, m38e.len: u64); os.exit(1); }; }; emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), BX\n"); cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs, "BX", 0, slotsize(c, c.fnret)); emitline("\tMOVQ\t"); emitoff(sa38v: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; // 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; }; // #242: a tuple variant packs into the union // payload via cgwidentaggedstore's TY_TUPLE arm. if (rhs.kind == nkind.N_TUPLE) { needswiden = true; }; // Family C (#35/#46): a mem-based tagged // read (`return *p`, any size) routes // through the widener's memread arm — // the cgexpr fall-through below wrapped // the un-deref'd POINTER as a scalar // payload (silent wrong). Mirrors // cstage cgreturn's widen-store route. if (taggedmemread(c, rhs)) { needswiden = true; }; // S1/#35: a GENUINE-WIDENING tagged source // (stamped type_ TY_TAGGED and != fnret; // exact-type rides forwardtagged/plain above) // routes through the widener — tag remap for // ident/call (#218), #35 widen-subset loud for // cast/dot. Mirrors cstage cgreturn istagged→ // cg_widen_tagged_store (cmd/w6c/cgen.c:13008- // 13011 → :2721). The ru1 != fu1 exclusion keeps // EXACT-type tagged casts on cstage's passthrough // (forwardtagged's own ru==fu equality) so byte-id // holds. let ru1: *tinfo = rhs.type_: *tinfo; ru1 = tichase(ru1); let fu1: *tinfo = nil; if (c.fnret != nil) { fu1 = c.fnret.type_: *tinfo; fu1 = tichase(fu1); }; if (ru1 != nil && fu1 != nil) { if (ru1.kind == tykind.TY_TAGGED && ru1 != fu1) { needswiden = true; }; // S3/#35: a SAME-TYPE tagged CAST (ru1 == fu1, // rhs an N_CAST) routes through the widener too. // cstage keeps the N_CAST out of the srcreg // passthrough (kind != N_CALL/INDEX/DOT) and the // widen branch peels the identity cast internally // (cg_widen_tagged_store cg_tagged_castpeel, // cgen.c:2553), so cs emits the scratch-widen (NOT // passthrough) for `return v: u` over an ident/ // call/dot source. ww's forwardtagged keys on // rhs.kind (N_CAST uncovered), so the same-type // cast fell to the scalar shuffle and synthesized // tag 0 (silent). The N_CAST guard mirrors cstage's // "N_CAST defeats srcreg"; peeling here instead // would reroute a call/dot source to passthrough // and break byte-id. if (rhs.kind == nkind.N_CAST && ru1.kind == tykind.TY_TAGGED && ru1 == fu1) { 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); // Tagged-return ABI loads at most 4 eightbytes // (AX/DX/CX/R8). A union whose slot exceeds 32B // (tag + >3 payload words, e.g. a 32B struct // variant = 40B slot) drops its 5th+ word here — // SYMMETRICALLY with cstage, so byte-id holds and // the tag/early-word read paths are correct. The // dropped tail is #222 (the >4-eightbyte sret ABI // asymmetry); its real fix routes large unions // through a hidden-pointer sret on both paths. // Sound only while consumers never read the tail // (errno's tag/strerror path does not). 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; // #272: >24B sret addressable-source closure — // N_DOT/N_INDEX/deref land their address in SI then // memcpy through *(@sretarg), mirroring cstage cgen.c // N_RETURN sret arm. N_ARRLIT >24B has no consumer // (loud-stops in cstage); not wired here. let addrsrc: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; if (rhs.kind == nkind.N_DOT) { okrhs = true; addrsrc = true; }; if (rhs.kind == nkind.N_INDEX) { okrhs = true; addrsrc = true; }; if (rhs.kind == nkind.N_UN) { if (rhs.op == tkind.TK_STAR) { okrhs = true; addrsrc = true; }; }; if (okrhs) { if (rhs.kind == nkind.N_STRUCTLIT) { // #63: the >24B sret RETURN twin of the :2421 // let-init fix. sretretsize chases the alias for // the size GATE (so this sret arm fires for a >24B // alias struct), but the field-fill resolved the // struct by a bare structlookup(c, sname): for an // alias-NAMED literal (`type biga = big; return // biga{...}`) sname is "biga", unregistered, so // sret_si was nil and the fill was SKIPPED — the // callee returned an uninitialised sret buffer // (SILENT wrong, runtime-0). structlookupchain chases // to the base struct; cs fills via the resolved // Type*, runtime-correct. let trefn: *node = rhs.lhs; let sret_si: *structinfo = structlookupchain(c, trefn); 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 { if (addrsrc) { if (!aggargsrcaddr(c, rhs, "SI")) { let m4: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m4.ptr, m4.len: u64); os.exit(1); }; emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), BX\n"); let k: i32 = 0; for (k + 8 <= scs) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 8; }; for (k + 4 <= scs) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 4; }; for (k < scs) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff(k: i64); emitline("(BX)\n"); k += 1; }; } 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; // #272: struct ≤24B addressable-source closure — // N_DOT/N_INDEX/deref memcpy into @retscr before the // shared structfloatclass tail (mirror cstage cgen.c). let addrsrc: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; // #41 (#263 ww-runtime-correct): a module-global struct // source has no BP slot — route it through the addrsrc // memcpy (LEAQ g(SB),SI via aggargsrcaddr). Pre-fix the // rl==nil N_IDENT arm below emitted nothing → zeroed // @retscr. cstage copies frame garbage (cstage half #42). if (localfindnode(c, rhs.str) == nil) { addrsrc = true; }; }; if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; }; if (rhs.kind == nkind.N_DOT) { okrhs = true; addrsrc = true; }; if (rhs.kind == nkind.N_INDEX) { okrhs = true; addrsrc = true; }; if (rhs.kind == nkind.N_UN) { if (rhs.op == tkind.TK_STAR) { okrhs = true; addrsrc = 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 { if (addrsrc) { // N_DOT / N_INDEX / deref: land src addr in SI, // then memcpy rsz bytes into @retscr (#265/#268 shape). if (!aggargsrcaddr(c, rhs, "SI")) { let m5: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m5.ptr, m5.len: u64); os.exit(1); }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= rsz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= rsz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; } 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; }; }; }; }; // #267: array return-by-value SEND. >24B sret rides the sret // block above (scs = sretretsize keys it, N_IDENT word-copy / // N_CALL forward generic). ≤24B reg-class `return a;` (N_IDENT) // mirrors the struct ≤24B path: zero-pad a 24B scratch, word- // copy the array slot in, ship AX/DX/CX. Array natural size // (tinfo.size = sub.size*len) mirrors cstage rt->size. No // structfloatclass (pure-int arrays); N_CALL forward at reg- // class falls to the default cgexpr passthrough below. if (c.fnret != nil && c.fnret.kind == nkind.N_TARRAY) { // #272: array return-by-value source-shape closure. // Beyond the #267 N_IDENT word-copy, route N_ARRLIT // (literal fill), N_DOT/N_INDEX/deref (aggargsrcaddr + // memcpy) into @retscr — the mirror of cstage cgen.c // N_RETURN ≤24B arm. N_CALL stays on the cgexpr tail (the // callee already left AX/DX/CX). let arrok: bool = false; if (rhs.kind == nkind.N_IDENT) { arrok = true; }; if (rhs.kind == nkind.N_ARRLIT) { arrok = true; }; if (rhs.kind == nkind.N_DOT) { arrok = true; }; if (rhs.kind == nkind.N_INDEX) { arrok = true; }; if (rhs.kind == nkind.N_UN) { if (rhs.op == tkind.TK_STAR) { arrok = true; }; }; let ati: *tinfo = c.fnret.type_: *tinfo; ati = tichase(ati); if (arrok && ati != nil) { let rsz: i32 = ati.size: i32; if (rsz <= 24) { 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_IDENT) { let rl: *local = localfindnode(c, rhs.str); let roff: i32 = 0; if (rl != nil) { roff = rl.off; }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff((roff + 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((roff + 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((roff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; } else { if (rhs.kind == nkind.N_ARRLIT) { // scalar/float element fill; non-scalar // elements loud-stop (rule 7, no consumer). let esubti: *tinfo = nil; if (ati.sub != nil) { esubti = ati.sub; }; esubti = tichase(esubti); let esz: i32 = 8; if (esubti != nil) { esz = esubti.size: i32; }; let badel: bool = false; if (esubti != nil) { if (esubti.kind == tykind.TY_STRUCT) { badel = true; }; if (esubti.kind == tykind.TY_ARRAY) { badel = true; }; if (esubti.kind == tykind.TY_TUPLE) { badel = true; }; if (esubti.kind == tykind.TY_SLICE) { badel = true; }; if (esubti.kind == tykind.TY_STR) { badel = true; }; }; if (badel) { let m2: str = "#272: array-literal return with non-scalar element unsupported (rule 7, no consumer)\n"; os.write(2, m2.ptr, m2.len: u64); os.exit(1); }; let esub: *node = c.fnret.lhs; let isfl: bool = isfloattype(c, esub); let fmov: str = "MOVSD"; if (isf32type(c, esub)) { fmov = "MOVSS"; }; let op: str = "MOVQ"; if (esz == 1) { op = "MOVB"; } else { if (esz == 2) { op = "MOVW"; } else { if (esz == 4) { op = "MOVL"; }; }; }; 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 (isfl) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(op); emitline("\tAX, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); }; idx += 1; e = e.next; }; }; if (repeat) { let total: i32 = rsz / esz; for (idx < total) { if (isfl) { emitline("\t"); emitline(fmov); emitline("\tX0, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); } else { emitline("\t"); emitline(op); emitline("\tAX, "); emitoff((scroff + idx * esz): i64); emitline("(BP)\n"); }; idx += 1; }; }; } else { // N_DOT / N_INDEX / deref: land src addr in SI, // then memcpy rsz bytes into @retscr (#265/#268 // copy shape). Loud-stop unaddressable sources. if (!aggargsrcaddr(c, rhs, "SI")) { let m3: str = "#272: aggregate return from unsupported source kind\n"; os.write(2, m3.ptr, m3.len: u64); os.exit(1); }; let k: i32 = 0; for (k + 8 <= rsz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= rsz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= rsz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= rsz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((scroff + k): i64); emitline("(BP)\n"); k += 1; }; }; }; emitline("\tMOVQ\t"); emitoff(scroff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((scroff + 8): i64); emitline("(BP), DX\n"); emitline("\tMOVQ\t"); emitoff((scroff + 16): i64); emitline("(BP), CX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; }; }; // #272 close-by-construction: addressable aggregate-return // sources (IDENT/STRUCTLIT/ARRLIT/DOT/INDEX/deref) all break in // the arms above; an aggregate N_CALL passes through cgexpr // (callee left AX/DX/CX). Any OTHER aggregate rvalue reaching // here would truncate to AX silently — loud-stop (rule 7), // mirroring cstage cgen.c N_RETURN. { // #277: key on the RESOLVED tinfo, not the syntactic node — a // NAMED-ALIAS aggregate return type (type a=[N]T / type a=struct) // presents as N_TNAME and is TY_ARRAY/TY_STRUCT only after the // alias chase, so the syntactic N_TARRAY/N_TNAME-structlookup arms // above never fire on it. Without this chase it would fall to the // scalar default = silent miscompile (cstage chases via // type_chase_named and stays correct). Loud-stop (rule 7) until // wwstage handles aliases via tinfo-kind dispatch (#277); the >24B // array-literal return (no consumer) also lands here (#276). let aggret: bool = false; if (c.fnret != nil) { let rti: *tinfo = c.fnret.type_: *tinfo; rti = tichase(rti); if (rti != nil) { if (rti.kind == tykind.TY_ARRAY) { aggret = true; }; if (rti.kind == tykind.TY_STRUCT) { aggret = true; }; }; }; if (aggret && rhs.kind != nkind.N_CALL) { let m6: str = "#272/#276/#277: aggregate return reaches scalar default — unclosed shape (named-alias aggregate return or >24B array-literal; wwstage tinfo-dispatch deferred #277)\n"; os.write(2, m6.ptr, m6.len: u64); os.exit(1); }; }; 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)) { // #38b: an sret-classified tagged return (slot > the // AX/DX/CX/R8 cursor) writes the void-variant tag // through *(@sretarg) and returns the dest pointer — // the cursor can't carry the slot and the caller reads // memory. Mirrors cstage cgen.c N_RETURN bare arm. if (sretretsize(c, c.fnret) > 0) { let sa38: i32 = localfind(c, "@sretarg"); let vidx38: i32 = voidvariantindex(c.fnret); if (vidx38 < 0) { vidx38 = 0; }; emitline("\tMOVQ\t"); emitoff(sa38: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t$"); emitint(vidx38: i64); emitline(", (BX)\n"); emitline("\tMOVQ\t"); emitoff(sa38: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); c.lastwasreturn = 1; return; }; 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; }; // cgarrlitfillbp — #31: fill the [count]T destination at BP-relative // `off` from an N_ARRLIT, extracted from the cglet array-init path so // the slice-borrow base materialisation (cgslice N_ARRLIT-base arm) // reuses the IDENTICAL element-store sequence — the frame-order / // store-op guarantee for rule-10 byte-id (ken). `arrtn` is the [count]T // type NODE (cglet n.lhs; cgslice the re-stamped tnode on arrlit.lhs, // #25); `rhs` the literal. Twin of cstage cg_arrlit_fill_bp. fn cgarrlitfillbp(c: *cgen, arrtn: *node, rhs: *node, off: i32) void = { let elemn: *node = arrtn.lhs; // #79 (#60 rider): alias-NAMED [count]T (`type A = [4]u32; let // a: A = [...]`) — arrtn is the N_TNAME leaf: elemn nil, esz // stayed the 8 sentinel and the per-element store strode MOVQ // over a stride-4 slot (saved-BP/RIP smash; masked when esz==8). // This is the STORE half of the #8 pair (the elemsizeofc READ // half chases the ELEMENT via idxeffti; alias-typed INDEXABLES // are chased at its call sites). Synthesise the element node off // the chased stamped sub — the cgforrange FC0 precedent — so the // prim/agg/slice/tagged/narrow dispatch below works unchanged; // stash alen for the `...` repeat bound (cstage cg_arrlit_fill_bp // receives the pre-chased bu and reads bu->alen). let aliasalen: i32 = -1; let ati79: *tinfo = arrtn.type_: *tinfo; if (ati79 != nil) { if (ati79.kind == tykind.TY_NAMED) { let au79: *tinfo = tichase(ati79); if (au79 != nil) { if (au79.kind == tykind.TY_ARRAY && au79.sub != nil) { let en79: *node = newnode(nkind.N_TNAME, arrtn.file, arrtn.line, arrtn.col); en79.str = au79.sub.name; en79.type_ = au79.sub: *void; elemn = en79; aliasalen = au79.alen: i32; };}; };}; 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 = aliasprimsize(c, elemn.str); if (ps > 0) { esz = ps; }; }; }; }; // #270-1c: an AGGREGATE (struct/array/tuple) element of // an array literal — the scalar per-element store below // writes only the first 8 bytes (unpopulated tail). Fill // each element slot from its literal (cgstructlitfillbp) // or source ident (word-copy). esz is the element's // natural size (cstage esub->size). let esubti: *tinfo = nil; if (elemn != nil) { esubti = elemn.type_: *tinfo; }; esubti = tichase(esubti); let isagg: bool = esubti != nil && (esubti.kind == tykind.TY_STRUCT || esubti.kind == tykind.TY_ARRAY || esubti.kind == tykind.TY_TUPLE); if (isagg) { esz = esubti.size: i32; }; // #20/#270 str-slice arm: a slice element (N_TSLICE) is // a 24B {ptr,len,cap} header — it matches no prim/str/agg // branch above, so esz stayed the 8 sentinel (wrong stride, // the -96-vs-80 cs!=ww frame divergence) and the scalar // store dropped .len/.cap. Size it from the stamped tinfo // and route it through the 3-word header store below. let isslicel: bool = esubti != nil && esubti.kind == tykind.TY_SLICE; if (isslicel) { esz = esubti.size: i32; }; // #12: a tagged-union element. NOT folded into isagg — // isagg's body word-copies/fatals and never boxes the // tag+payload; route through the cgwidentaggedstore // choke-point the N_LET tagged path (cgenstmt.ww:1627) // uses. esz must come from the stamped slot size (#8-class // trap, rule-13): the narrow override below only rescues // 1/2/4, so a tagged 16/24B element keeps the wrong 8 // sentinel stride without this. let istaggedel: bool = esubti != nil && esubti.kind == tykind.TY_TAGGED; if (istaggedel) { esz = esubti.size: i32; }; // #8: a named-narrow element (`[N]tk`, tk = enum i32) is // neither a builtin prim (primsize=0 above, so esz stayed // the 8 sentinel) nor an aggregate, so the scalar store kept // an 8B stride/MOVQ and overran the stride-4 frame slot — // smashing the saved BP / return addr (SEGFAULT). Mirror // cstage's uniform lu->sub->size (cgen.c:6387) and the // elemsizeofc read-side fix: take the stamped element tinfo's // size for a narrow scalar (1/2/4). Wider non-prim elements // (tagged/slice/str two-half) stay the documented follow-up // at :1742-1744 — the single-MOVx store below is scalar-only. if (!isstrel && !isagg && esz == 8 && esubti != nil) { let es: i32 = esubti.size: i32; if (es == 1 || es == 2 || es == 4) { esz = es; }; }; 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 { if (isagg) { if (e.kind == nkind.N_STRUCTLIT) { let esi: *structinfo = structlookupchain(c, elemn); cgstructlitfillbp(c, esi, e, off + idx * esz); } else { if (e.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, e.str); let soff: i32 = 0; if (sl != nil) { soff = sl.off; }; let kc: i32 = 0; for (kc + 8 <= esz) { emitline("\tMOVQ\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 8; }; if (kc + 4 <= esz) { emitline("\tMOVL\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVL\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 4; }; if (kc + 2 <= esz) { emitline("\tMOVW\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVW\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 2; }; if (kc + 1 <= esz) { emitline("\tMOVB\t"); emitoff((soff + kc): i64); emitline("(BP), AX\n"); emitline("\tMOVB\tAX, "); emitoff((off + idx * esz + kc): i64); emitline("(BP)\n"); kc += 1; }; } else { let m1c: str = "#270-1c: array-literal aggregate element shape unsupported (rule-7)\n"; os.write(2, m1c.ptr, m1c.len: u64); os.exit(1); }; }; } else { if (istaggedel) { cgwidentaggedstore(c, esubti, e, "BP", off + idx * esz, esz); } else { cgexpr(c, e); if (isstrel || isslicel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + idx * esz + 16): 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; }; }; if (repeat && isagg) { let m1cr: str = "#270-1c: `...` repeat of an aggregate array-literal element not wired (rule-7)\n"; os.write(2, m1cr.ptr, m1cr.len: u64); os.exit(1); }; // #12: `...` re-stores from AX, but cgwidentaggedstore consumed // the node and trashed AX — the repeat-fill would write garbage. // No consumer needs `[N]tagged=[x,...]`. if (repeat && istaggedel) { let m12r: str = "#12: `...` repeat of a tagged-union array-literal element not wired (rule-7)\n"; os.write(2, m12r.ptr, m12r.len: u64); os.exit(1); }; // 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 (arrtn != nil) { if (arrtn.kind == nkind.N_TARRAY) { if (arrtn.rhs != nil) { if (arrtn.rhs.kind == nkind.N_INTLIT) { total = arrtn.rhs.uval: i32; }; }; }; }; // #79: alias arrtn has no length tnode — bound off the // chased tinfo (see the synthesis block at fn top). if (aliasalen >= 0) { total = aliasalen; }; for (idx < total) { if (isstrel || isslicel) { emitline("\tMOVQ\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((off + idx * esz + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((off + idx * esz + 16): 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; }; }; }; // #152: reserve the let's frame slot, emit its initializer against the // PRE-binding locals chain, then link the binding. A self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope because nm is not yet in // c.locals while cgletbody runs (Hare evals the init in the outer scope: // harec check.c clet runs cexpr before scope_define). localreserve bumps // the frame now so off + nested-let offsets stay stable. fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); let tn: *node = n.lhs; if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; let letloc: *local = localreserve(c, nm, sz, tn); cgletbody(c, n, letloc.off); letloc.lnext = c.locals; c.locals = letloc; }; // #152: cgletbody emits the initializer into the reserved slot `off`. // The wrapper cglet reserves the slot BEFORE this runs and links the // binding into c.locals only AFTER, so a self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn cgletbody(c: *cgen, n: *node, off: i32) 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); }; 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; }; nti = tichase(nti); 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. // // #38b: an sret-classified tagged CALL result is in memory, // not the cursor — an exact-type receive falls through to the // generic sret receive below (the let's slot IS the dest); a // widening receive needs mem-to-mem tag-remap (#40, unwired). // Mirrors cstage cgen.c N_LET tagged arm. if (istaggedtype(c, tn)) { let letsret: i32 = 0; if (rhs.kind == nkind.N_CALL) { letsret = callsretsize(c, rhs); }; if (letsret == 0) { cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz); c.lastwasreturn = 0; return; }; let lru: *tinfo = rhs.type_: *tinfo; lru = tichase(lru); let llu: *tinfo = tn.type_: *tinfo; llu = tichase(llu); let exact38: bool = false; if (lru != nil && lru == llu) { exact38 = true; } else { if (typeeq(rhs.type_: *tinfo, tn.type_: *tinfo)) { exact38 = true; }; }; if (!exact38) { let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; os.write(2, m40c.ptr, m40c.len: u64); os.exit(1); }; // fall through to the generic sret receive below. }; // In-cap tuple initialiser (#105 / #164/#107): every in-cap // tuple receive routes here, keyed on the DECLARED TYPE's // register classify (sretretsize == 0, the shared SSoT) — // mirror of cstage cgen.c N_LET tuple arm. 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). Over-cap falls through to // the sret receive below (#240 — an over-cap receive via the // register cursor read garbage past R8). // // C-t1 (#33): the old keys were producer-SHAPE — the mixed // str/scalar arm required s0_is_str != s1_is_str (syntactic) // AND sz==16/32, the rt16 arm required an N_CALL rhs // (rettupleof) — so a scalar-scalar tuple LITERAL `(3, 4)` // matched neither and fell to the generic single-word store, // silently dropping word 1 (#209/#211-class syntactic-vs-type // keying). Alias-peel mirrors cstage's type_chase_named; the // unannotated `let t = f()` shape rides the inferletcalltype // tn above. let ttup: *node = tn; for (ttup != nil && ttup.kind == nkind.N_TNAME) { ttup = aliaslookup(c, ttup.str); }; if (ttup != nil) { if (ttup.kind == nkind.N_TTUPLE && sretretsize(c, ttup) == 0) { // #57: a tuple LITERAL rhs carries the DECLARED // type into the cursor fill — its stamped type // is element-constructed, so a declared-tagged // element's concrete rvalue skipped the widen // and the fill/receive cursor walks skewed // (let-twin of the return-position bug; probe // /tmp/p57/q1_let). Same emission as the cgexpr // route for every declared-tagged-free literal. if (rhs.kind == nkind.N_TUPLE) { cgtuplelittocursor(c, rhs, ttup); } else { cgexpr(c, rhs); }; let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; let q: *node = ttup.list; for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); let eslot: i32 = tupeslotn(qt); tupstore(c, gpcur, ssecur, off + eoff, eslot, qt); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; eoff = eoff + eslot; q = q.next; }; c.lastwasreturn = 0; return; }; // #22a (rule 7, ken R1) wwstage half: an OVER-CAP tuple // init whose rhs is not a CALL has no store path — only // the CALL shape rides the sret receive below; every // other rhs fell past ALL the store arms to NOTHING // (silent uninitialized-frame reads). cgexpr's cursor // materialisers loud most shapes, but their EXPR-shape // counts let a declared-tagged element's unwidened // payload (or a void literal) slip through in-cap // (probe /tmp/i22b/p7) — the let-twin of the #22b // classify/emit skew. Mirrors cstage cgen.c N_LET net. if (ttup.kind == nkind.N_TTUPLE && rhs.kind != nkind.N_CALL && sretretsize(c, ttup) > 0) { cgexpr(c, rhs); let mnet: str = "over-cap tuple initialiser from a non-call source unwired (see #10/#22b)\n"; os.write(2, mnet.ptr, mnet.len: u64); os.exit(1); }; }; // 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/slice element (24B = ptr+len+cap, post-#1) needs all 3 // words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len, // CX=cap) and a single MOVQ from AX would leave .len/.cap 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/slice 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. The str/slice element now stores all 3 // words; [N]tagged element arrays still hit the gap, task #12.) if (rhs.kind == nkind.N_ARRLIT) { cgarrlitfillbp(c, n.lhs, rhs, off); 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) { // #63: an alias-NAMED struct literal (`type rep2 = rep; // let r = rep2{id=6}`) parses its type ref as N_IDENT/N_TNAME // "rep2", but only the base `rep` is registered — bare // structlookup(c, "rep2") returns nil, so the fill never // fired: the slot zeroed + the lit DROPPED (≤8B silent) or // fell to the :2920 LOUD (>8B). structlookupchain chases the // alias chain to the base struct, the #92/W2 SSoT already // adopted at cgenstmt:1974/:2687. cs chases via // type_chase_named, runtime-correct. let trefn: *node = rhs.lhs; let si: *structinfo = structlookupchain(c, trefn); 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; }; }; }; }; }; // #267: array return-by-value RECV ≤24B — `let c = mk()` // where mk returns an array. Arrays ride the struct reg-recv // path (AX/DX/CX, sized tail). >24B sret rides the sret recv // above (callsretsize keyed). Array natural size (tinfo.size // = sub.size*len) mirrors cstage lu->size. No structfloatclass // (pure-int element arrays). if (rhs.kind == nkind.N_CALL && tn != nil && tn.kind == nkind.N_TARRAY) { let ati: *tinfo = tn.type_: *tinfo; ati = tichase(ati); if (ati != nil) { let lsz: i32 = ati.size: i32; 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 // ABI sizes that aren't 8-aligned (e.g. `struct // { i32, i32, i32 }`, maxalign 4 → ABI 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) { // memcpy run sizes on the maxalign-rounded ABI // size — cstage N_LET sets sz = lu->size // (cgen.c:7533, struct-IDENT branch :7869), // check.c:760 SSoT. structnaturalsize would // short struct{i64,i32} (natural 12, ABI 16) // to MOVQ+MOVL where cstage writes MOVQ+MOVQ. let lsz: i32 = structabisize(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; }; }; }; }; }; // #265 fold-1/1b (#268): aggregate let-init copy from an // ADDRESSABLE rhs — `*p` (deref), an array ident `= s` // (struct-ident is the arm above), an N_DOT field `= o.i`, an // N_INDEX element `= a[i]`, T a struct/array >8B. ONE memcpy // loop fed by a per-rhs source-address setup landing the SOURCE // ADDRESS in SI; copy N bytes (the #254 non-slot-padded ABI // extent: structabisize for a struct, tinfo.size for an array) // slot→slot — a MOVQ run plus a sized MOVL/MOVW/MOVB tail. Pre- // fix array-ident/N_DOT truncated to the 8B scalar tail below // and N_INDEX scalar-loaded the element address (segfault). // Mirror of cstage cgen.c N_LET arm (rule-10); the by-value // RETURN ABI is fold-2 (#267). Source-addr setups reuse closed // machinery: LEAQ-slot (ident), the deref operand (cgexpr), // dotchainaddr (#253, N_DOT), the &base[i] spine (#252, // N_INDEX). let aggn: i32 = 0; let aggsi: *structinfo = structlookupchain(c, tn); if (aggsi != nil) { aggn = structabisize(aggsi); } else { let aggti: *tinfo = nil; if (tn != nil) { aggti = tn.type_: *tinfo; }; aggti = tichase(aggti); if (aggti != nil) { if (aggti.kind == tykind.TY_ARRAY) { aggn = aggti.size: i32; }; }; }; if (aggn > 8) { let havesrc: bool = false; if (rhs.kind == nkind.N_UN) { if (rhs.op == tkind.TK_STAR) { cgexpr(c, rhs.lhs); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; }; if (!havesrc) { if (rhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), SI\n"); havesrc = true; } else { // rule-10: the addressable-def set must // equal cstage's let_islet || // def_isarraydef || def_isstructdef — // the laid-out-aggregate globals (#129 // A.2/A.3). Bare deflookup (any def) // over-copies struct-defs on wwstage // only; mirror the defisaddressable // pairing instead. let aggdtn: *node = defvartnode(c, rhs.str); let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil; if (aggdtn != nil) { if (aggdtn.kind == nkind.N_TARRAY) { aggisdef = true; }; }; if (isletvar(c, rhs.str) || aggisdef) { emitline("\tLEAQ\t"); emitsymname(c, rhs.str); emitline("(SB), SI\n"); havesrc = true; }; }; }; }; if (!havesrc) { if (rhs.kind == nkind.N_DOT) { if (dotchainaddr(c, rhs, "SI")) { havesrc = true; }; }; }; if (!havesrc) { if (rhs.kind == nkind.N_INDEX) { let base: *node = rhs.lhs; let idx: *node = rhs.rhs; let bu: *tinfo = nil; if (base != nil) { bu = base.type_: *tinfo; }; bu = tichase(bu); if (base != nil && base.kind == nkind.N_IDENT && bu != nil && bu.kind == tykind.TY_ARRAY) { let esz: i32 = 1; if (bu.sub != nil) { esz = bu.sub.size: i32; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; let bl: *local = localfindnode(c, base.str); if (bl != nil) { emitline("\tLEAQ\t"); emitoff(bl.off: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), BX\n"); }; emitline("\tADDQ\tBX, AX\n"); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; // #270-3a: the index BASE is an N_DOT // array-field (`x.arr[i]`) or a nested N_INDEX // (`a[i][j]`); the N_IDENT-base arm above missed // both, so the copy fell to the 8B truncation // below. Compute &base[idx]: scaled idx on the // stack, then &base via dotbaseaddr (N_DOT field // address) or the &abase[bidx] spine (nested // N_IDENT-array base), then add. if (!havesrc && base != nil && (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX)) { let esz2: i32 = 1; if (bu != nil && bu.sub != nil) { esz2 = bu.sub.size: i32; }; cgexpr(c, idx); if (esz2 > 1) { emitline("\tMOVQ\t$"); emitint(esz2: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); let baseok: bool = false; if (base.kind == nkind.N_DOT) { if (dotbaseaddr(c, base, "AX")) { baseok = true; }; } else { let ab: *node = base.lhs; let bidx: *node = base.rhs; let abu: *tinfo = nil; if (ab != nil) { abu = ab.type_: *tinfo; }; abu = tichase(abu); if (ab != nil && ab.kind == nkind.N_IDENT && abu != nil && abu.kind == tykind.TY_ARRAY) { let aesz: i32 = 1; if (abu.sub != nil) { aesz = abu.sub.size: i32; }; cgexpr(c, bidx); if (aesz > 1) { emitline("\tMOVQ\t$"); emitint(aesz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; let abl: *local = localfindnode(c, ab.str); if (abl != nil) { emitline("\tLEAQ\t"); emitoff(abl.off: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, ab.str); emitline("(SB), BX\n"); }; emitline("\tADDQ\tBX, AX\n"); baseok = true; }; }; emitline("\tPOPQ\tBX\n"); if (baseok) { emitline("\tADDQ\tBX, AX\n"); emitline("\tMOVQ\tAX, SI\n"); havesrc = true; }; }; }; }; // C4 (F5, task #7): the remaining ADDRESSABLE rhs // shapes — a slice-base element (`= xs[0]`; the arms // above have TY_ARRAY/N_DOT/N_INDEX bases but no // TY_SLICE base) and deref-spine leaves // (`= (*ts)[i].cap`) — resolve through cgplaceaddr // (the C1 resolver; enumerated arms dispatch first so // their asm is untouched). Pre-C4 these fell through // to the scalar default's 8B truncation while cstage // emitted NOTHING — gate-blind cs≠ww. if (!havesrc) { if (cgplaceaddr(c, rhs, "SI")) { havesrc = true; }; }; if (havesrc) { let k: i32 = 0; for (k + 8 <= aggn) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= aggn) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVL\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= aggn) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVW\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= aggn) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(SI), AX\n"); emitline("\tMOVB\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 1; }; c.lastwasreturn = 0; return; }; // #38b (rule 7): `?`/`!` over an sret-class call into // an aggregate let — keep the established #38b/#40 // loud-stop marker (mirror of cstage's pre-arm fatal, // cgen.c N_LET; pre-C4 this shape fell through to the // cgtryunw/cgtryprop gates, which the C4 tail below // now pre-empts in let position). if (rhs.kind == nkind.N_TRYUNW || rhs.kind == nkind.N_TRYPROP) { if (rhs.lhs != nil) { if (rhs.lhs.kind == nkind.N_CALL) { if (callsretsize(c, rhs.lhs) > 0) { let m38f: str = "#38b: `?`/`!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; os.write(2, m38f.ptr, m38f.len: u64); os.exit(1); }; }; }; }; // C4: nothing below this arm can initialise a >8B // struct/array slot — the scalar default's 8B store // was a silent truncation (rule 7). let mf5: str = "let: aggregate init from unhandled rhs shape (task #7/rule-7)\n"; os.write(2, mf5.ptr, mf5.len: u64); os.exit(1); }; 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. // #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like // every other composite. They were excluded here, so a // dirtied-stack `let a: [3]int;` read garbage — BOTH stages, // both-wrong-IDENTICAL, gate-blind (#263). Dropping the // exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes // arrays into the zsz>8 / zsz==8 arms below; an 8B array is // already caught by typeis8byteprimitive (TY_ARRAY size==8) → // single MOVQ $0, matching cstage's sz==8 store. // Zero-fill extent. cstage sizes the run on `lu->size` // (the natural ABI size from the type table, cgen.c:8397); // 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 nothing. Source the extent from the // type table's tinfo.size for a struct-typed let to converge; // slot allocation stays on `sz` (frame uses slot-padded slots). // #254: structabisize is NOT a sound ABI-size source here — it // sums fieldsize(), which slot-pads a nested value-struct field // to 8, so a sub-8 outer struct (e.g. `struct{struct{[4]u8}}`, // ABI 4) read 8 and emitted a stray MOVQ $0 cstage doesn't. // fieldsize / registerstruct / frame slot-padding stay // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { // #84: an array's zero-fill extent is its chased ABI // size (cstage `lu->size`), NOT the slot-padded sz from // letslotsize — a non-8-multiple array (e.g. [20]u8 = 20) // would over-zero MOVQ-rounded to 24 and diverge from // cstage's exact 20-byte run. Handles direct N_TARRAY and // alias-to-array (N_TNAME chasing through TY_NAMED) alike. let zti: *tinfo = n.lhs.type_: *tinfo; zti = tichase(zti); if (zti != nil && zti.kind == tykind.TY_ARRAY) { zsz = zti.size: i32; } else { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { let ti: *tinfo = n.lhs.type_: *tinfo; ti = tichase(ti); if (ti != nil) { zsz = ti.size: i32; }; }; }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (zsz == 8) { // #213: an 8B composite (single-field struct / tagged) is // neither an 8B primitive nor zsz>8, so it fell through // un-zeroed while cstage emits MOVQ $0 (cgen.c N_LET // `else if (sz == 8)`); a read-before-init then saw stack // garbage (cs!=ww byte-id + a latent garbage-read). Match // cstage's immediate MOVQ $0, checked BEFORE the run arm // below so an 8B slot stays one immediate store, not // XORQ+MOVQ (rule-10 byte-id). emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (zsz > 0) { // #16: the run arm was gated `zsz > 8`, so a SUB-8 // aggregate (`let c: [3]u8;` = 3, a 3-byte struct, etc.) // matched no arm and fell through un-zeroed — the exact // stack-garbage read ken's bytes verdict pinpointed // (ltrim_cases' `let c: [3]u8;`), BOTH stages, gate-blind // (#263). cstage widened its `!n->rhs && sz > 8` gate to // `sz > 0` symmetrically; the MOVL/MOVB tail already sizes // the run to any 1..7-byte extent. (`[0]T`, zsz == 0, needs // no stores — the lone XORQ is skipped, matching cstage.) 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"); }; // #42: bound the push. The buffers are sized exactly LOOP_MAX, so an // unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write; // fail loud at the cap, both stages (cgen.c twin fatals too). if (c.looptop >= LOOP_MAX) { let msg: str = "cgen: loop nesting too deep\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; 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); // #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out // as cgmlet but the slots already exist (localfind); a `_` / missing // binding (off == 0) SKIPS its store yet still ADVANCES foff so the // next element stays aligned (harec `_`). Byte-identical to the // cstage N_MASSIGN over-cap arm. let sretrecv: i32 = 0; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { sretrecv = callsretsize(c, n.rhs); }; }; // #64: a tuple-LITERAL rhs carries a DECLARED tuple type (built from // the lvalue binding types) into the cursor fill, so a declared-tagged // element's concrete rvalue widens into the box instead of riding the // decl-less stamped-keyed route — the #57 decl wire extended past // cgmlet/cgreturn to destructure-reassign. A `_` lvalue has no local // (no declared type node); its decl element stays nil and the // fill/receive fall back to the rhs literal element's own stamped type // for the cursor stride (harec `_` advance; pinned by the R3 control). let litrhs: bool = false; if (n.rhs != nil) { if (n.rhs.kind == nkind.N_TUPLE) { litrhs = true; }; }; let synthdecl: *node = nil; if (litrhs) { synthdecl = newnode(nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col); let dtail: *node = nil; let lb0: *node = n.list; for (lb0 != nil) { let w: *node = newnode(nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col); w.lhs = nil; if (lb0.kind == nkind.N_IDENT) { let lc0: *local = localfindnode(c, lb0.str); if (lc0 != nil) { w.lhs = lc0.tnode; }; }; w.next = nil; if (dtail == nil) { synthdecl.list = w; } else { dtail.next = w; }; dtail = w; lb0 = lb0.next; }; cgtuplelittocursor(c, n.rhs, synthdecl); } else { if (n.rhs != nil) { cgexpr(c, n.rhs); }; }; if (sretrecv > 0) { let scr: i32 = localfind(c, "@sretscr"); let pt2: *node = nil; if (rettuple != nil) { pt2 = rettuple.list; }; let foff: i32 = 0; let lb: *node = n.list; for (lb != nil) { let tn: *node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); // #22b: the >8B copy-out keys on the ACCESSOR's slot // (str/slice header AND tagged box), not a str/slice // kind test — the tagged element took the scalar arm // (8B silent truncation; unreachable while the SEND // louded, live once #22b unwires it). Byte-id for // str/slice (esz == eslot == 24). Mirrors the cstage // N_MASSIGN sret arm + the R-1 all-three-routings lesson. let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *tinfo = pt2.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let off: i32 = 0; if (lb.kind == nkind.N_IDENT) { off = localfind(c, lb.str); }; if (off != 0) { if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; }; // C-t0: slot stride — must mirror the N_RETURN // over-cap SEND's buffer layout (cstage N_MASSIGN // twin strides tuple_eslot). foff += tupeslotn(tn); lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; c.lastwasreturn = 0; return; }; let ssecap: i32 = TUPLE_SSECAP; // 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; }; // #64: a tuple-LITERAL rhs keys element WIDTH on the DECLARED lvalue // type (synthdecl), not the rettuple (nil for a literal); a `_` slot // (declared type nil) falls back to the rhs literal element's own // stamped type for the cursor stride. let dp: *node = nil; let re: *node = nil; if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; for (l != nil) { let tn: *node = nil; if (litrhs) { if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; }; } else { if (pt != nil) { tn = pt.lhs; }; }; if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; if (dp != nil) { dp = dp.next; }; if (re != nil) { re = re.next; }; }; if (gptotal > TUPLE_GPCAP) { // 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; }; dp = nil; re = nil; if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; for (l != nil) { let tn: *node = nil; if (litrhs) { if (dp != nil && dp.lhs != nil) { tn = dp.lhs; } else { tn = re; }; } else { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(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, eslot, tn); }; if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; if (dp != nil) { dp = dp.next; }; if (re != nil) { re = re.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); // #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd // the whole tuple into the @sretscr discard slot (cgcall sees // callsretsize > 0, no lvalue dest wired). Copy each element out to // its binding slot at the SAME packed offset the SEND wrote (foff += // element size — the t.0/t.1 layout), each at its NATURAL width // (#169). Byte-identical to the cstage N_MLET over-cap arm. let sretrecv: i32 = 0; if (rhs.kind == nkind.N_CALL) { sretrecv = callsretsize(c, rhs); }; // #242: rhs is a tuple already materialised in a local slot (a match- // bound union payload, `let (a,b)=t`), NOT a register-returning call. // cgexpr(tuple ident) loads only word0->AX, so the register cursor // path below reads DX/CX stale. Copy each element from the ident's // slot at the register-ABI 8B stride (24B for a slice/str header) — // the SAME layout the tagged construct + match payload-bind write. // Mirror of cstage cgen.c N_MLET tuple-ident arm. The binding element // types ride l.lhs (stamped by the checker's stamptuplebinds). if (rhs.kind == nkind.N_IDENT) { let rl: *local = localfindnode(c, rhs.str); if (rl != nil) { let rti: *tinfo = rl.tnode.type_: *tinfo; rti = tichase(rti); if (rti != nil) { if (rti.kind == tykind.TY_TUPLE) { let srcoff: i32 = rl.off; let foff: i32 = 0; let lb: *node = n.list; for (lb != nil) { let tn: *node = lb.lhs; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; let eti: *tinfo = nil; if (tn != nil) { eti = tn.type_: *tinfo; }; if (eti != nil) { esz = eti.size: i32; }; let bsz: i32 = 8; if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((srcoff + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; foff += eslot; lb = lb.next; }; c.lastwasreturn = 0; return; }; }; }; }; cgexpr(c, rhs); if (sretrecv > 0) { let scr: i32 = localfind(c, "@sretscr"); let pt2: *node = nil; if (rettuple != nil) { pt2 = rettuple.list; }; let foff: i32 = 0; let lb: *node = n.list; for (lb != nil) { let tn: *node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *tinfo = pt2.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let bsz: i32 = 8; if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { if (eslot > 8) { let k: i32 = 0; for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((off + k): i64); emitline("(BP)\n"); k += 8; }; } else { let lop: str = tnodeloadop(c, tn, esz); let sop: str = tnodestoreop(c, tn, esz); emitline("\t"); emitline(lop); emitline("\t"); emitoff((scr + foff): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; foff += eslot; lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; c.lastwasreturn = 0; return; }; let ssecap: i32 = TUPLE_SSECAP; // 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 { gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; }; if (gptotal > TUPLE_GPCAP) { // 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 eslot: i32 = tupeslotn(tn); let sz: i32 = 8; if (eslot > 8) { sz = eslot; }; let off: i32 = localadd(c, l.str, sz, tn); tupstore(c, gpcur, ssecur, off, eslot, tn); if (isflt) { ssecur = ssecur + 1; } else { gpcur = gpcur + eslot / 8; }; 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, 24 for str/slice (str IS []u8, the slice // header SSoT), tuple → sum of its 8B-floored element slots, 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; }; // #43 (F7-c4): a slice tuple-field carries the 24B header (ptr+len+ // cap), not the 8B scalar default. Without this arm the for-range // destructure over `[N]([]T, U)` strode the tuple at 8 not 24 and // read field-2 at the wrong offset (cs=42/ww=8, the cat-A repro). // tyslicesize() is the slice-header SSoT (rule-13); cstage reads the // same width via tp->type->size (cmd/w6c/cgen.c N_FORRANGE). if (k == nkind.N_TSLICE) { return tyslicesize(): i32; }; // #53: a tagged-union tuple-field carries its full box (tag + widest // payload, slot-padded), NOT the 8B scalar default — a for-range // destructure binding sized 8 loaded only the tag word (cs=56/ww=9, the // cat-A repro). The box width is variant-dependent, so read it from the // checker-stamped tinfo (rule-13): cstage's tp->type->size reads the same // resolved width. The stamp already collapsed any alias, so this also // covers an N_TNAME field naming a tagged union (paramfieldsize's no-`c` // structural contract can't aliaslookup-chase the node). Mirrors the // N_TSLICE arm's type-table SSoT. let tgi: *tinfo = t.type_: *tinfo; if (tgi != nil) { tgi = tichase(tgi); if (tgi != nil && tgi.kind == tykind.TY_TAGGED) { return tgi.size: i32; }; }; // #43 (F7-c4): a nested tuple field sizes as the sum of its element // SLOTS — each element floored UP to one 8B eightbyte (str/slice keep // their 24B header), per the tuple-slot ruling and tupeslot's // roundup8. Recurse structurally so a tuple-of-tuple lands the same // stride cstage's tp->type->size computes. if (k == nkind.N_TTUPLE) { let total: i32 = 0; let dp: *node = t.list; for (dp != nil) { let esz: i32 = paramfieldsize(dp.lhs); total = total + (esz + 7) / 8 * 8; dp = dp.next; }; return total; }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "str")) { return primtypesize("str"): i32; }; // primsize-ok (#101/#109): paramfieldsize is a STRUCTURAL // (no-`c`, no-chase) sizer by design — it takes a *node, not a // *cgen, so it cannot run aliasprimsize's aliaslookup chase // (threading c is the dormant #110). A bare primsize is correct // here, not the #101 narrow-alias bug shape. let ps: i32 = primsize(nm); if (ps > 0) { return ps; }; }; // rule-7: N_TARRAY (an array-typed tuple field) is intentionally not // sized here — and PROVABLY unreachable, not merely latent (#39). // paramfieldsize's only callers are tuple-field contexts (the N_TTUPLE // recursion above + the cgforrange destructure sizers below), and the // checker loud-REJECTS an array/struct/nested-tuple tuple element at // N_TTUPLE resolution (check.ww:2150, "composite element deferred to // task #60"; pinned both-stage by test 832_tuple_elem_overlong). So no // tuple field can carry an N_TARRAY type — this arm cannot be reached // until #60's inline-composite layout lands and lifts that gate. The 8B // fall-through is correct-by-vacuity; reopen WITH #60, adding the arm // (read t.type_.size, twin of the #53 tagged arm above). 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; }; }; }; }; }; // #60 (alias arc #5): alias-NAMED scrutinee (`let a: arr`, arr = // [4]int) — the tnode peek above sees only the N_TNAME leaf: // elemt nil, esz 1-sentinel, neither isarr nor isslicestr, so the // per-iteration base walked the array words as a POINTER (SEGV). // Chase the stamped tinfo (cstage N_FORRANGE u = type_chase_named // (slc->type) feeds esz/alen/base classify uniformly) and // synthesise the element node off .sub — the FC0 non-ident // precedent below. let rti60: *tinfo = nil; if (slctn != nil) { if (slctn.kind == nkind.N_TNAME) { let st60: *tinfo = slctn.type_: *tinfo; if (st60 != nil) { if (st60.kind == tykind.TY_NAMED) { rti60 = tichase(st60); }; }; }; }; if (rti60 != nil && elemt == nil) { if (rti60.sub != nil) { let en60: *node = newnode(nkind.N_TNAME, slctn.file, slctn.line, slctn.col); en60.str = rti60.sub.name; en60.type_ = rti60.sub: *void; elemt = en60; }; }; // 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. // C4 (task #7): elemsizeofc, not elemsizeof — a struct element // (`[]thread`, 16B) hit elemsizeof's 8-sentinel while cstage reads // the stamped slc->type sub size (IMULQ $8 vs $16, gate-blind // cs≠ww). elemsizeofc recovers the width from the stamped tinfo // (the #8 named-narrow precedent). let esz: i32 = elemsizeofc(c, slctn); // #60: alias-NAMED scrutinee — stride off the chased stamped // element (cstage esz = u->sub->size). if (rti60 != nil) { let es60: *tinfo = tichase(rti60.sub); if (es60 != nil) { esz = es60.size: i32; }; }; 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; }; }; // C4 (FC0, task #7): a non-ident scrutinee (`re.charsets`) has no // local tnode — slctn is nil, so esz fell to 1 and the binding // registered typeless (cstage reads the stamped slc->type: esz 24, // slice-header readbacks → cs≠ww). Derive both from the checker- // stamped slc.type_ (tinfo SSoT, the #209/#211 discipline); the // synthesised N_TNAME carries the element tinfo so cgident's // str/slice/float keys read it like a declared local (the str→u8 // synthesis precedent above). if (slctn == nil && slc != nil) { let sti2: *tinfo = slc.type_: *tinfo; sti2 = tichase(sti2); if (sti2 != nil) { if (sti2.kind == tykind.TY_SLICE || sti2.kind == tykind.TY_STR || sti2.kind == tykind.TY_ARRAY) { if (sti2.sub != nil) { esz = sti2.sub.size: i32; let en: *node = newnode(nkind.N_TNAME, slc.file, slc.line, slc.col); en.str = sti2.sub.name; en.type_ = sti2.sub: *void; elemt = en; }; }; }; }; let destruct: bool = (n.list != nil); // .rgi (counter) + .rgl (length) scratch slots. #70: a NON-IDENT // slice/str base (field chain, indexed element, call) also needs // a .rgb base spill — pre-#70 the init stored cgexpr's AX (the // DATA POINTER — a slice-valued cgexpr leaves AX=ptr, BX=len, // CX=cap) into .rgl, and the per-iteration code had no non-ident // base arm, so the bound-reload BX doubled as the base: i was // compared against the POINTER and walked off the end // (regex.finish, SEGV on the first non-empty charsets; empty // slices coincidentally exited on ptr==0 — latent since fold 1, // byte-id both stages). A non-ident ARRAY base is loud (rule 7): // its cgexpr shape is not the slice header. 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); let baseoff: i32 = 0; if (slc != nil) { if (slc.kind != nkind.N_IDENT) { let stu70: *tinfo = slc.type_: *tinfo; stu70 = tichase(stu70); let arr70: bool = false; if (stu70 != nil) { if (stu70.kind == tykind.TY_ARRAY) { arr70 = true; }; }; if (arr70) { let m70: str = "for-range over a non-ident array base unwired (#70)\n"; os.write(2, m70.ptr, m70.len: u64); os.exit(1); }; // #11: cgexpr on a slice DEREF (*p) does not deliver // the AX/BX/CX header convention the spill assumes // (the deref-spine load family) — keep it LOUD until // #11 wires the deref load. if (slc.kind == nkind.N_UN) { if (slc.op == tkind.TK_STAR) { let m11: str = "for-range over a deref base unwired (#11)\n"; os.write(2, m11.ptr, m11.len: u64); os.exit(1); }; }; let bname: str = mkscratchname(c, "rgb"); baseoff = localalloc(c, bname, 8, nil); }; }; // #121 leg (c): for-range over a module-GLOBAL slice/str/array base // SEGV's today — the init + per-iteration base resolution below // assume a frame-local slot (localfindnode), so a global let/def base // reads saved-BP as the .ptr/.len. LOUD-STOP symmetric with cstage // cgen.c (byte-id-neutral; segfault→compile-error is pure // improvement). The fix (the N_INDEX isglobal base resolution ported // into the for-range spine) is a DISTINCT mechanism — filed as a #121 // sibling, off fold-6's path. if (slc != nil) { if (slc.kind == nkind.N_IDENT) { if (localfindnode(c, slc.str) == nil) { let isglob: bool = isletvar(c, slc.str); if (!isglob) { let gdtn: *node = defvartnode(c, slc.str); if (gdtn != nil) { if (gdtn.kind == nkind.N_TARRAY) { isglob = true; }; }; }; if (isglob) { let mc: str = "#121: for-range over a module-global slice/array base unwired (global-base resolution gap)\n"; os.write(2, mc.ptr, mc.len: u64); os.exit(1); }; }; }; }; // 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; }; }; }; // #60: alias-NAMED scrutinee — classify off the chased stamped // kind (cstage gates on u->kind TY_SLICE/TY_STR vs TY_ARRAY). if (rti60 != nil) { if (rti60.kind == tykind.TY_ARRAY) { isarr = true; }; if (rti60.kind == tykind.TY_SLICE) { isslicestr = true; }; if (rti60.kind == tykind.TY_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; }; }; // #60: alias-NAMED scrutinee has no length tnode — bound off // the chased tinfo (cstage aimm(u->alen)). if (rti60 != nil) { alen = rti60.alen: i64; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", "); emitoff(loff: i64); emitline("(BP)\n"); } else { cgexpr(c, slc); if (baseoff != 0) { // #70: slice/str header from cgexpr is AX=ptr, // BX=len, CX=cap — bound is LEN; spill the base ptr // for the per-iteration element address. emitline("\tMOVQ\tBX, "); emitoff(loff: i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff(baseoff: i64); emitline("(BP)\n"); } else { // ident with unresolved type — legacy path, unchanged. 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"); // #42: bound the push. The buffers are sized exactly LOOP_MAX, so an // unguarded push at nesting depth LOOP_MAX+1 is an OOB heap write; // fail loud at the cap, both stages (cgen.c twin fatals too). if (c.looptop >= LOOP_MAX) { let msg: str = "cgen: loop nesting too deep\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; 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"); }; }; } else { // #70: non-ident slice/str base — reload the spilled data // pointer (pre-#70 BX held the bound reload). emitline("\tMOVQ\t"); emitoff(baseoff: 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. // C4 (F5/FC0, task #7): a by-value AGGREGATE element (struct / // tuple / str/slice header, esz > 8) copies its FULL extent — the // single load word truncated it to 8B, so every field past word 0 // (str/slice .len/.cap included) read stale slot bytes // (regex.finish's 24B charset binding, gate-blind cs≠ww). Same // word-run + sized-tail idiom as the cglet aggregate copy. if (!destruct && esz > 8) { let k: i32 = 0; for (k + 8 <= esz) { emitline("\tMOVQ\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= esz) { emitline("\tMOVL\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVL\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= esz) { emitline("\tMOVW\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVW\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= esz) { emitline("\tMOVB\t"); emitoff(k: i64); emitline("(BX), AX\n"); emitline("\tMOVB\tAX, "); emitoff((bind_off[0] + k): i64); emitline("(BP)\n"); k += 1; }; } else { let b: i32 = 0; for (b < nbinds) { // #40 (#263): a str/slice/struct destructure binding // (24B header / aggregate, sz>8) copies its FULL extent // — the single load word truncated a slice binding to // its .ptr, dropping .len/.cap (both stages identically, // byte-id-WRONG; F7-c4 fixed only the STRIDE). Same // word-run + sized-tail idiom as the non-destructure // aggregate copy above. if (bind_sz[b] > 8) { let k: i32 = 0; for (k + 8 <= bind_sz[b]) { emitline("\tMOVQ\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 8; }; if (k + 4 <= bind_sz[b]) { emitline("\tMOVL\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVL\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 4; }; if (k + 2 <= bind_sz[b]) { emitline("\tMOVW\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVW\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 2; }; if (k + 1 <= bind_sz[b]) { emitline("\tMOVB\t"); emitoff((bind_foff[b] + k): i64); emitline("(BX), AX\n"); emitline("\tMOVB\tAX, "); emitoff((bind_off[b] + k): i64); emitline("(BP)\n"); k += 1; }; b += 1; continue; }; 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; // #38b: words consumed by MEMORY-class (>48B tagged) params — // post-walk consistency check against stkcursor. let memwords: 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; }; // #99: chase a TY_NAMED alias (multi-level) to its // underlying tuple — the param twin of the cstage type.c // type_chase_named tuple-arm. A bare (i64,i64) is N_TTUPLE // (no chase); `type tp=(i64,i64)` is an N_TNAME resolved via // aliaslookup. Without the chase the alias fell to the scalar // path → 1 slot, SI dropped, t.1 garbage. Slot size + element // walk source the RESOLVED node; localadd keeps the declared // p.lhs so field reads chase identically to cstage (byte-id). let tt99: *node = nil; if (p.lhs != nil) { tt99 = p.lhs; for (tt99 != nil && tt99.kind == nkind.N_TNAME) { tt99 = aliaslookup(c, tt99.str); }; }; if (p.lhs != nil) { if (tt99 != nil && tt99.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 = tt99.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 eb: i32 = tupeslotn(et) / 8; 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 += tupeslotn(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; // #38b: MEMORY-class (>48B tagged) param — the // caller staged the whole slot below the return // address; read it in place at positive BP // offsets. No spill, no frame growth, zero // prologue bytes. Pre-fix this fell into the // greedy stitch arm below while cstage received // one scalar word (cs≠ww, silent). // ref/qbe/amd64/sysv.c:80-85 / :411-426. if (taggedmemargsize(p.lhs.type_: *tinfo) > 0) { localaddstack(c, nm, p.lhs, 16 + stkcursor*8); stkcursor += nw; memwords += nw; } else { 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 { let aggsz2: i32 = aggargsizetn(p.lhs.type_: *tinfo); if (aggsz2 > 0) { // #271: array / >16B-struct by-value param — // received as ceil(sz/8) GP eightbytes, the // callee twin of the generalised aggregate-arg // push. Mirror of the cstage is_bigagg arm // (regs-fit / partial-stitch / stack-spill). let nw2: i32 = (aggsz2 + 7) / 8; if (idx + nw2 <= 6) { let off: i32 = localadd(c, nm, aggsz2, p.lhs); let w: i32 = 0; for (w < nw2) { emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); emitoff((off + w*8): i64); emitline("(BP)\n"); idx += 1; w += 1; }; } else { if (idx < 6) { let off: i32 = localadd(c, nm, aggsz2, 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 < nw2) { 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 += nw2; };}; } 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; }; // #38b: a MEMORY-class tagged param cannot coexist with stack- // spilled register-class params — both walk the same positive-BP // cursor in declaration order while the caller's residual region // puts spill words below every mem copy. Any non-mem cursor use // leaves stkcursor past the mem words. Mirror of the cgcall // caller-side check; loud-stop (rule 7). if (memwords > 0 && stkcursor != memwords) { let mp: str = "#38b: >48B tagged param mixed with stack-spilled params unwired\n"; os.write(2, mp.ptr, mp.len: u64); os.exit(1); }; }; 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); defaultinferredlets(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 strings; 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] == '.') { 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; }; // #223: same-module-ONLY alias resolution. aliaslookup's any-module // fallback can return a foreign same-leaf alias; the alias-peel in // cgdot needs to know whether THIS module defines the name as an alias // (so the peel continues) without that cross-module fallback. Returns // the alias target only when an alias of `name` lives in c.curmod. fn aliassamemod(c: *cgen, name: str) *node = { 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; }; 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] == '.') { 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 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap) // The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret // classifier (sretretsize over-cap-tuple arm) AND every emit/receive site // (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard). // Classify and emit MUST agree on these, else a tuple gets classified // sret by one and in-reg by the other -> corruption. Mirrors cstage // cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10). def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8 def TUPLE_SSECAP: i32 = 2; // X0,X1 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) // 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, // #220: sret receive into a GLOBAL lvalue. A BP-relative i32 // (sretdestoff) can't name a top-level let, so the lhs IDENT node // is carried and emitted as `LEAQ name(SB), DI`. nil means no // global receiver wired; mutually exclusive with sretdestoff. sretdestnode: *node, 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.sretdestoff = 0; c.sretdestnode = nil; 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; }; // localreserve — localalloc minus the chain-link. #152: cglet reserves // the slot (frame bump + offset) before its initializer emits, then links // the binding into c.locals only AFTER, so a self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { // #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) — // `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to // the old `max(8, round8(sz))` accumulation for every sz>0 (frame // stays 8-aligned, so a 1..8B slot still costs 8); the only change // is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0 // frame instead of over-reserving 8. local.sz is read only by the // @-prefix grow-check in localadd, never for user lets, so storing // the raw sz here is inert. c.frame = (c.frame + sz + 7) & ~7; let off: i32 = 0 - c.frame; let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!; return l; }; // 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; #44 sidesteps the // fatal for the tagged scratch by keying its NAME by size). // Mirrors cstage's cg_tagscr_slot table / cg_retscr / // cg_sretscr same-fn caches in cmd/w6c/cgen.c (#26 / #15 / #44). if (name.len > 0) { if (name[0] == '@') { 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); }; // tagscradd — the ONLY alloc path for the per-fn tagged scratch (#44). // "@tagscr" keys localadd's @-prefix name-dedup by slot size, so a // fn mixing two tagged slot sizes smaller-first (regex compile(): 56B // append-element widen then 64B sret return) no longer trips the // #15/#26c grow-fatal — each distinct size pins its own first-use // slot, in source order in BOTH stages (byte-id). Mirrors cstage // cg_tagscr_slot (cmd/w6c/cgen.c). fn tagscradd(c: *cgen, sz: i32) i32 = { let buf: [32]u8; let pre: str = "@tagscr"; let i: i32 = 0; for (i < pre.len) { buf[i] = pre[i]; i += 1; }; let ns: str = strconv.i64tos(sz: i64, strconv.base.DEC); let n: i32 = ns.len; let k: i32 = 0; for (k < n) { buf[i + k] = ns.ptr[k]; k += 1; }; let total: i32 = i + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let j: i32 = 0; for (j < total) { p[j] = buf[j]; j += 1; }; p[total] = 0u8; let name: str; name.ptr = p.ptr; name.len = total; return localadd(c, name, sz, nil); }; 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 (strings.compare(ln, name) == 0) { 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 cgoutstream: memio.stream; let cgoutmode: i32 = 0; let cgoutinit: i32 = 0; fn cgout_enable() void = { if (cgoutinit == 0) { cgoutstream = memio.dynamic(); cgoutinit = 1; }; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { if (cgoutstream.pos > 0) { os.write(1, cgoutstream.ptr, cgoutstream.pos: u64); memio.reset(&cgoutstream); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { let buf: []u8; buf.ptr = p; buf.len = n: i32; // io.write over the embedded vtable (&cgoutstream.vt = io.stream); // memio.dynamicwrite never errors. Bare-discard mirrors // lib/log/log.ww stdprintln. #94 fold-eFinal. io.write(&cgoutstream.vt, 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] = '.'; i += 1; }; let fname: str = c.fnname; j = 0; for (j < fname.len) { buf[i] = fname[j]; i += 1; j += 1; }; buf[i] = '_'; i += 1; j = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = '_'; 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] = '.'; i += 1; let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = '_'; 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 && lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; } else { // #56: def/const dim — resolve from the stamped array // tinfo (rule-13), the letemitsize twin of the cgdot // .len fix. Pre-fix a non-N_INTLIT dim defaulted alen=1 // → array global mis-sized (one element's worth). let abt: *tinfo = tichase(t.type_: *tinfo); if (abt != nil && abt.kind == tykind.TY_ARRAY) { alen = abt.alen: i32; }; }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = aliasprimsize(c, elemn.str); if (ps > 0) { esz = ps; }; }; }; return alen * esz; }; // C-t3 (#48): tuple global — per-element slot sum (C-t0 // layout: a str/slice its header, everything else one 8B // eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size, // the checker slot sum). Pre-C-t3 the 0 here kept tuple // globals out of collectlets entirely — no DATA emitted, and // the module-leaf fallback mis-emitted the field index as a // symbol (`MOVQ 0(SB), AX`). if (t.kind == nkind.N_TTUPLE) { let tsum: i32 = 0; let p: *node = t.list; for (p != nil) { let et: *node = p.lhs; if (isstrtype(c, et) || isslicetype(c, et)) { tsum += (tyslicesize(): i32); } else { tsum += 8; }; p = p.next; }; return tsum; }; // #87: non-nullable tagged-union global — box size (tag word + // max payload, mirror of the runtime local). Mirrors cstage // let_emit_size TY_TAGGED. if (t.kind == nkind.N_TTAGGED) { // #45 (silent→loud bridge, task #15): a nullable (*T|void) // GLOBAL has no storage path. Returning 0 here made // letcollect + emitletdataw silently skip the decl (no DATA, // no let-registration), so a later match/is/as resolved // 0(BP) or an undefined symbol — a silent miscompile in the // CSP handle-singleton substrate. Die loud at the size/ // storage layer so all three read paths hit one diagnostic; // the full storage + read-class arc is task #15 (CSP-prereq). if (isnullabletype(t)) { let mng: str = "nullable-global storage unimplemented (task #15)\n"; os.write(2, mng.ptr, mng.len: u64); os.exit(1); }; return slotsize(c, t); }; 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; }; // defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs // is an int literal (`let s = 42;`) or a single unary +/-/~ over one // (`let s = -42;`/`~42;`) is stamped by the checker with an // N_TNAME("untyped_int") annotation (d.lhs). letemitsize / emitletdataw / the // cgident global-read arm key on that annotation's name, which letscalarprim // doesn't recognise → the global is dropped from collectlets (no DATAW) and the // read falls to the silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong). // cstage instead type_default's the untyped int to the 8B machine word BEFORE // emit (and folds the unary). Mirror that here at the single global-decl pass: // peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as // foldintliteral) and rewrite the annotation to the concrete machine word `int`. // The inferred decl is then structurally the typed control (`let s: int = -42`), // so all three consumers fire on the existing typed-path code — byte-identical // to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] — // i32 is the #108 truncation trap, opposite polarity. // Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a // nested unary (`- -42`) is a #134-residual (cstage folds it via // foldintliteral's recursion, ww peels one level and leaves it silent) — not // widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a // SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a // NON-literal (`let s = -x`) is not constant; both stay on their current route. // An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to // #135: cstage integer-types the inferred float at the USE site (MOVQ, not // MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a // cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix. // Runs before collectlets in cgfile so the mutated d.lhs is visible to // letpreintern + emitletdataw too. fn defaultinferredlets(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { if (d.lhs != nil && d.rhs != nil && d.lhs.kind == nkind.N_TNAME && streq(d.lhs.str, "untyped_int")) { let opnd: *node = d.rhs; if (opnd.kind == nkind.N_UN && (opnd.op == tkind.TK_PLUS || opnd.op == tkind.TK_MINUS || opnd.op == tkind.TK_TILDE)) { opnd = opnd.lhs; }; if (opnd != nil && opnd.kind == nkind.N_INTLIT) { d.lhs.str = "int"; }; }; // #135: the inferred-FLOAT twin, now unblocked. The carve- // out above (deferred to #135) feared a cs≠ww divergence // because cstage USED to integer-type an inferred float // (MOVQ); #150-B fixed cstage to type_default untyped_float // → f64 and load MOVSD, so defaulting here now CONVERGES. // Without it, letemitsize sees "untyped_float" (not in // letfloatprim) → 0 → the global is dropped from collectlets // (no DATAW) and the read falls to cgident's silent bare // return (X0 untouched). Mirror cstage check.c clet // type_default. if (d.lhs != nil && d.rhs != nil && d.lhs.kind == nkind.N_TNAME && streq(d.lhs.str, "untyped_float")) { let opnd: *node = d.rhs; if (opnd.kind == nkind.N_UN && (opnd.op == tkind.TK_PLUS || opnd.op == tkind.TK_MINUS)) { opnd = opnd.lhs; }; if (opnd != nil && opnd.kind == nkind.N_FLOATLIT) { d.lhs.str = "f64"; }; }; }; d = d.next; }; }; 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`, // which resolves the declared type via type_unwrap — so an alias of // a slice IS a slice. Walks the N_TNAME alias chain exactly as the // sibling letvarisstr does (the structural N_TSLICE node is the // terminator, in place of letvarisstr's "str" name): without this, // a `type S = []T; let g: S = [...]` global misroutes to the str arm // and never reaches emitslicedata, diverging from cstage (#10). 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; for (t != nil) { if (t.kind == nkind.N_TSLICE) { return true; }; if (t.kind != nkind.N_TNAME) { return false; }; let nx: *node = aliaslookup(c, t.str); if (nx == nil) { return false; }; t = nx; }; 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); }; // preinternstrarray — SSoT for the #18 [N]str element-strlit intern // ORDER (element order, then `...` repeat-fill). Shared by letpreintern's // let arm and the #8/GAP-B def arm so both intern labels in the SAME // order emitstrarraydata references them by — a divergent order would // mis-pair the DATAR rows with their _S_ rodata. au is the chased // TY_ARRAY tinfo, r the N_ARRLIT rhs; caller verified the element is str. fn preinternstrarray(c: *cgen, au: *tinfo, r: *node) void = { let alen: i32 = au.alen: i32; let cnt: i32 = 0; let last_ev: *node = nil; let repeat: bool = false; let e: *node = r.list; for (e != nil && cnt < 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) { break; }; if (ev.kind != nkind.N_STRLIT) { break; }; if (ev.str.len > 0) { internstrlit(c, ev.str); }; last_ev = ev; cnt += 1; e = e.next; }; if (repeat && last_ev != nil) { if (last_ev.str.len > 0) { for (cnt < alen) { internstrlit(c, last_ev.str); cnt += 1; }; }; }; }; // 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) { // #8/GAP-B: a `def [N]str` needs the SAME element-strlit // pre-interning as the let [N]str arm (the #18 ordering // contract) so emitstrarraydata's DATAR rows find their _S_ // rodata. letpreintern walked only N_LET; a def's labels were // allocated too late (emitdefconstants pass) → dangling _S_. // Str-array ONLY — def tuple/slice/tagged/scalar-str stay out // of scope (#10/#270 / inline-Sdef). if (d.kind == nkind.N_DEF) { let dr: *node = d.rhs; for (dr != nil && dr.kind == nkind.N_CAST) { dr = dr.lhs; }; if (d.lhs != nil && dr != nil && dr.kind == nkind.N_ARRLIT) { let dau: *tinfo = tichase(d.lhs.type_: *tinfo); if (dau != nil && dau.kind == tykind.TY_ARRAY) { let deu: *tinfo = tichase(dau.sub); if (deu != nil && deu.kind == tykind.TY_STR) { preinternstrarray(c, dau, dr); }; }; }; }; if (d.kind == nkind.N_LET) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; // #18: `let xs: [N]str = […];` — pre-intern each // element's strlit in element order (then repeat-fill) // so emitstrarraydata's DATAR rows find an _S_ rodata // row. Must match that helper's interning order exactly // to keep labels stable. // g-fold #77: gate on the CHASED tinfo kind — the // N_TARRAY tnode test missed alias-typed [N]str // globals, desyncing label order vs cstage. let handled: bool = false; if (d.lhs != nil && r != nil) { let au: *tinfo = tichase(d.lhs.type_: *tinfo); if (au != nil && au.kind == tykind.TY_ARRAY && r.kind == nkind.N_ARRLIT) { let eu: *tinfo = tichase(au.sub); if (eu != nil && eu.kind == tykind.TY_STR) { handled = true; preinternstrarray(c, au, r); }; }; }; // C-t3 (#48): tuple global — pre-intern str-element // literals in element order so emitletdataw's tuple // arm's DATAR rows find their _S_ rodata rows (the // #18 array-arm pattern; cstage let_pre_intern twin). if (!handled && r != nil && d.lhs != nil) { if (r.kind == nkind.N_TUPLE) { let tlt: *node = d.lhs; for (tlt != nil && tlt.kind == nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; if (tlt != nil) { if (tlt.kind == nkind.N_TTUPLE) { handled = true; let tp: *node = tlt.list; let e: *node = r.list; for (e != nil && tp != nil) { let et: *node = tp.lhs; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == nkind.N_STRLIT && (isstrtype(c, et) || isslicetype(c, et))) { if (ev.str.len > 0) { internstrlit(c, ev.str); }; }; }; e = e.next; tp = tp.next; }; }; }; }; }; // #87: tagged global with a str/slice-variant literal init — // pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_ // rodata row (the #48 tuple-arm pattern; cstage letpreintern twin). // #117: slice-of-tuple global — pre-intern each row's // str-element literals in row-then-element order so // emitslicedata's per-row DATAR patches find their _S_ // rodata rows (cstage letpreintern twin). Bounded to // inline N_TTUPLE element types. if (!handled && r != nil && d.lhs != nil) { if (r.kind == nkind.N_ARRLIT && d.lhs.kind == nkind.N_TSLICE) { let tupnode: *node = d.lhs.lhs; if (tupnode != nil && tupnode.kind == nkind.N_TTUPLE) { handled = true; let row: *node = r.list; for (row != nil) { let rw: *node = row; for (rw != nil && rw.kind == nkind.N_CAST) { rw = rw.lhs; }; if (rw != nil && rw.kind == nkind.N_TUPLE) { let tp: *node = tupnode.list; let e: *node = rw.list; for (e != nil && tp != nil) { let et: *node = tp.lhs; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == nkind.N_STRLIT && (isstrtype(c, et) || isslicetype(c, et))) { if (ev.str.len > 0) { internstrlit(c, ev.str); }; }; }; e = e.next; tp = tp.next; }; }; row = row.next; }; }; }; }; if (!handled && r != nil && d.lhs != nil) { let tlt: *node = d.lhs; for (tlt != nil && tlt.kind == nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; if (tlt != nil) { if (tlt.kind == nkind.N_TTAGGED && !isnullabletype(tlt)) { if (r.kind == nkind.N_STRLIT && r.str.len > 0 && (nodeisstr(c, r) || nodeisslice(c, r))) { handled = true; internstrlit(c, r.str); }; }; }; }; if (!handled) { 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) { 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, module: 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(" "); emitsymnamehint(c, name, module); 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; su = tichase(su); 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_; fu = tichase(fu); 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, module: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; su = tichase(su); if (su == nil) { return false; }; if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); emitsymnamehint(c, name, module); 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; au = tichase(au); 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; eu = tichase(eu); 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) { // #13: explicit elements fold normally; a `...` repeat replays the // LAST value; the tail PAST the explicit elements (no `...`) is // ZERO-filled. Pre-fix the default was `last`, so an under-length // literal (`[4]u64 = [1, 2]`) repeated the last value into the tail // instead of zero — cstage already zeroes (Hare: unspecified array // elements are zeroed; the #16-task zero-value ruling); this aligns // wwstage, a gate-blind cs!=ww divergence at the array-global path. let v: u64 = 0u64; if (inrepeat) { v = last; } else { if (e != nil) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { inrepeat = true; v = last; } else { e = e.next; v = last; }; } 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; }; // emitstrarraydata — module-level `let xs: [N]str = […];` static init // (#18). Mirror of cstage emit_strarray_data. A str element carries a // ptr→rodata relocation, not just bytes, so it can't ride // emitarraylitbytes (bytes-only); instead apply the scalar-str-global // pattern (DATAW header with a zero ptr placeholder + inline LE len, // then a per-element DATAR) at offset idx*esz. Each strlit was pre- // interned by letpreintern so its _S_ rodata row exists before this // row's DATAR references it. Always emits into DATAW (writable): A_DATAR // requires a DATAW holder, so both `let` and a read-only `def [N]str` // (#8/GAP-B) park their backing here — the section bit is the reloc- // holder constraint, not a mutability grant (def immutability stays // checker-enforced). Returns false when the element type isn't str. fn emitstrarraydata(c: *cgen, directive: str, name: str, module: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; au = tichase(au); if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; let eu: *tinfo = au.sub; eu = tichase(eu); if (eu == nil) { return false; }; if (eu.kind != tykind.TY_STR) { return false; }; // #8/GAP-B: a str-element array's backing ALWAYS lives in DATAW // (writable section), regardless of the caller's let/def directive — // each element carries an A_DATAR ptr-reloc to its _S_ rodata row, and // w6a requires a DATAR holder be a DATAW slot (asm.c:362). The passed // directive ("DATA" for a def, "DATAW" for a let) is therefore IGNORED // here; the emit below hardcodes DATAW. A `def [N]str` stays immutable // — the checker rejects writes to a def; DATAW is only the reloc-holder // placement, not a mutability grant (rule-8 placement detail). Pre-fix // this gate skipped the def path → no DATA block → w6l undefined // 'main.C' (#270 lineage; int-def is plain DATA, no holder constraint, // so it was unaffected). let esz: i32 = au.sub.size: i32; let alen: i32 = au.alen: i32; let last_ev: *node = nil; let repeat: bool = false; let cnt: i32 = 0; let e: *node = rhs.list; for (e != nil && cnt < 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 (ev.kind != nkind.N_STRLIT) { return false; }; last_ev = ev; cnt += 1; e = e.next; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); let idx: i32 = 0; e = 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; }; let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; let v: u64 = ev.str.len: u64; i = 0; for (i < 8) { emitdatawbyte((v & 255u64): u8); v = v >> 8u64; i += 1; }; i = 16; for (i < esz) { emitdatawbyte(0u8); i += 1; }; idx += 1; e = e.next; }; for (idx < alen) { let v: u64 = 0u64; if (repeat && last_ev != nil) { v = last_ev.str.len: u64; }; let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; i = 0; for (i < 8) { emitdatawbyte((v & 255u64): u8); v = v >> 8u64; i += 1; }; i = 16; for (i < esz) { emitdatawbyte(0u8); i += 1; }; idx += 1; }; emitline("\"\n"); idx = 0; e = 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.str.len > 0) { let lab: str = internstrlit(c, ev.str); emitline("DATAR "); emitsymnamehint(c, name, module); emitline("+"); emitint((idx * esz): i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; idx += 1; e = e.next; }; for (idx < alen) { if (repeat && last_ev != nil && last_ev.str.len > 0) { let lab: str = internstrlit(c, last_ev.str); emitline("DATAR "); emitsymnamehint(c, name, module); emitline("+"); emitint((idx * esz): i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; 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, module: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; au = tichase(au); if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; if (rhs == nil) { let total: u64 = arrt.size; emitline(directive); emitline(" "); emitsymnamehint(c, name, module); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; emitline("\"\n"); return true; }; // str-element arrays carry per-element ptr relocations — handled // by the dedicated DATAW+DATAR helper (#18). if (emitstrarraydata(c, directive, name, module, arrt, rhs)) { return true; }; if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); emitsymnamehint(c, name, module); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); return true; }; // emitslicedata — module-level `let g: []T = [v0,…];` static init (#10 // part a). Mirror of cstage emit_slice_data. A slice literal needs a // writable backing holding the k elements, a 24B header { ptr, len, cap // }, and a DATAR patching the ptr word with the backing's VA. The // backing rides the emitarraylitbytes choke-point via a synthesized // [k]T so int/float/struct/nested-array elements reduce exactly as a // [N]T global's do. Backing symbol = ".d": a second '.' can // never collide with a user global (source identifiers carry no '.'). // Scoped to a writable `let` — A_DATAR's holder must be a DATAW slot // (w6a asm.c:362); read-only `def`, `...` repeat (no target length), // and slice-of-{str,slice,tagged} elements (per-element relocs / #17) // all loud-stop (rule 7, #10 follow-ups). fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, sltnode: *node, rhs: *node) void = { let su: *tinfo = slt; su = tichase(su); // Defensive, mirrors cstage emit_slice_data's // `if (u == NULL || u->kind != TY_SLICE) return 0` (rule-10): the // letvarisslice gate already guarantees a slice, so this is // unreachable — it guards the su.sub deref below if the contract // is ever violated rather than nil-derefing. if (su == nil || su.kind != tykind.TY_SLICE) { return; }; let etype: *tinfo = su.sub; let eu: *tinfo = etype; eu = tichase(eu); // Count elements; reject `...` (a slice literal has no target N). let k: i32 = 0; let e: *node = rhs.list; for (e != nil) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { let m: str = "emitslicedata: '...' repeat has no target length in a slice literal (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; k += 1; e = e.next; }; // #117 aggregate-element arm: a slice of inline (str,*fn)-style // TUPLE rows. The element type-AST node (sltnode.lhs = N_TTUPLE) // drives the per-element slot classification the node-based // emittuplerow helpers expect; cstage drives the same off the tuple // tinfo's params. Bounded to inline N_TTUPLE element types. let tupnode: *node = nil; if (sltnode != nil) { tupnode = sltnode.lhs; }; let istuprow: bool = false; if (eu != nil && eu.kind == tykind.TY_TUPLE && tupnode != nil) { if (tupnode.kind == nkind.N_TTUPLE) { istuprow = true; }; }; if (istuprow) { let stride: i32 = etype.size: i32; // Validate every row before any bytes (two-pass, partial-row // safe). let e2: *node = rhs.list; for (e2 != nil) { let row: *node = e2; for (row != nil && row.kind == nkind.N_CAST) { row = row.lhs; }; let bad: bool = false; if (row == nil) { bad = true; } else if (row.kind != nkind.N_TUPLE) { bad = true; } else if (!tuplerowfoldable(c, tupnode, row)) { bad = true; }; if (bad) { let m: str = "emitslicedata: tuple-row element not a foldable constant ((str,*fn) rows only; #117, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; e2 = e2.next; }; // Backing: k rows, bytes (one DATAW) then per-row relocs. emitline("DATAW "); emitsymnamehint(c, name, module); emitline(".d(SB),\""); e2 = rhs.list; for (e2 != nil) { let row: *node = e2; for (row != nil && row.kind == nkind.N_CAST) { row = row.lhs; }; emittuplerowbytes(c, tupnode, row); e2 = e2.next; }; emitline("\"\n"); let rowoff: i32 = 0; e2 = rhs.list; for (e2 != nil) { let row: *node = e2; for (row != nil && row.kind == nkind.N_CAST) { row = row.lhs; }; emittuplerowrelocs(c, name, module, true, rowoff, tupnode, row); rowoff += stride; e2 = e2.next; }; } else { if (eu != nil) { if (eu.kind == tykind.TY_STR || eu.kind == tykind.TY_SLICE || eu.kind == tykind.TY_TAGGED) { let m: str = "emitslicedata: slice-of-{str,slice,tagged} literal static-init unsupported (#10 follow-up, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; let esz: i32 = etype.size: i32; // Synthesize [k]T to ride the emitarraylitbytes choke-point. let arrt: *tinfo = newtype(tykind.TY_ARRAY); arrt.sub = etype; arrt.alen = k: u64; arrt.size = (k * esz): u64; if (!emitarraylitbytes(c, arrt, rhs, 0)) { let m: str = "emitslicedata: slice-literal element not a foldable constant (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; // Writable backing data. emitline("DATAW "); emitsymnamehint(c, name, module); emitline(".d(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); }; // 24B header: ptr placeholder + LE len + LE cap (both = k). Word // sizes from the type table (rule 13). emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); let i: i32 = 0; let ptrsz: i32 = primtypesize("uintptr"): i32; for (i < ptrsz) { emitdatawbyte(0u8); i += 1; }; let lensz: i32 = primtypesize("size"): i32; i = 0; let kv: u64 = k: u64; for (i < lensz) { emitdatawbyte((kv & 255u64): u8); kv = kv >> 8u64; i += 1; }; i = 0; kv = k: u64; for (i < lensz) { emitdatawbyte((kv & 255u64): u8); kv = kv >> 8u64; i += 1; }; emitline("\"\n"); // Patch the ptr word with the backing VA. emitline("DATAR "); emitsymnamehint(c, name, module); emitline("+0(SB),"); emitsymnamehint(c, name, module); emitline(".d(SB)\n"); }; // emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static // init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): // tag word at +0 (the const-selected variant index, taggedvariantindex — // the routine the runtime widen + match dispatch key on), payload at +8, // zero-padded to the union box size `sz`. int and str-literal variants // are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any // other variant payload returns false and the caller loud-stops (rule 7) — // never the pre-#87 silent no-DATA + garbage read. Mirror of cstage // emit_tagged_data. fn emittaggeddata(c: *cgen, name: str, module: str, tt: *node, rhs: *node, sz: i32) bool = { if (tt == nil) { return false; }; if (rhs == nil) { emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); let zi: i32 = 0; for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; emitline("\"\n"); return true; }; let r: *node = rhs; for (r != nil && r.kind == nkind.N_CAST) { r = r.lhs; }; if (r == nil) { return false; }; // E8/#35: select the variant via flatvariantidx — the EXACT twin of // cstage emit_tagged_data's cg_tag_for_variant (cmd/w6c/cgen.c:15472). // taggedvariantindex adds a str/slice SHAPE fallback (cgenutil.ww:3054) // that cstage does NOT run at this site: a same-type/subset CAST init // (`let g: u = true: u;`) stamps the peeled literal's type_ as the // union itself, so flatvariantidx finds no variant and returns -1, // matching cstage's loud (caller emits "unsupported variant init", // cgen.ww:2818). The shape fallback instead picked the first scalar // variant (tag 0) -> SILENT miscompile (ran the int arm on a bool // value, the S1 coincidence trap). A bare-literal init (`= true` / `= // 7`) keeps its concrete/untyped type and still resolves via // flatvariantidx pass 1 (byte-id with cstage); a str-literal CAST // (`"hi": u`) keeps its str type and resolves to the str variant too. // Faithful tag-remap for a cast-init static global = the deferred // #23/#40 nominal widen feature. let tag: i32 = flatvariantidx(c, tt, r); if (tag < 0) { return false; }; let wide: bool = nodeisstr(c, r) || nodeisslice(c, r); let i: i32 = 0; let acc: u64 = 0u64; if (wide) { if (r.kind != nkind.N_STRLIT) { return false; }; let lv: u64 = r.str.len: u64; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); // tag@0 acc = tag: u64; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // ptr placeholder@8 i = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; // len@16 acc = lv; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // cap@24 (= len for a static str literal, mirroring the box) acc = lv; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // pad to sz i = 32; for (i < sz) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); if (r.str.len > 0) { let lab: str = internstrlit(c, r.str); emitline("DATAR "); emitsymnamehint(c, name, module); emitline("+8(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; return true; }; let v: u64 = 0u64; if (!foldintliteral(r, &v)) { return false; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); // tag@0 acc = tag: u64; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // payload@8 acc = v; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // pad to sz i = 16; for (i < sz) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); return true; }; // nodefnptr — true if `ev` (casts already peeled by the caller) is the // address-of a top-level fn (`&f`). The detect-half of the FIRST &fn→DATAR // reloc machinery (#117 slice-row + #119 scalar-global); mirrors the // address-of-fn codegen arm (fnretlookup at the N_UN TK_AMP ident, // cgenexpr.ww). The reloc target symbol is emitted via emitfnname at the // call site (cstage node_fnptr_sym returns the mangled string directly). fn nodefnptr(c: *cgen, ev: *node) bool = { if (ev == nil) { return false; }; if (ev.kind != nkind.N_UN) { return false; }; if (ev.op != tkind.TK_AMP) { return false; }; let opnd: *node = ev.lhs; if (opnd == nil) { return false; }; // #124: a cross-module `&mod.fn` — opnd is an N_DOT whose base is an // SK_USE module qualifier (not a local / let / def), and whose leaf // resolves to a fn in that module. Mangle via the module ident (not // curmod) at the emit sites so the reloc targets the same TEXT symbol // the runtime `&mod.fn` emits (cgenexpr.ww N_DOT addr-of arm). The // N_DOT arm of the #117/#119 reloc helper. if (opnd.kind == nkind.N_DOT) { if (opnd.lhs == nil) { return false; }; if (opnd.lhs.kind != nkind.N_IDENT) { return false; }; let basenm: str = opnd.lhs.str; if (localfindnode(c, basenm) != nil) { return false; }; if (isletvar(c, basenm)) { return false; }; if (deflookup(c, basenm)) { return false; }; if (fnretlookupmod(c, opnd.str, basenm) == nil) { return false; }; return true; }; if (opnd.kind != nkind.N_IDENT) { return false; }; // #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym // (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542- // 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn // LEAF NAME even when the operand actually resolved to a same-named // global/local VALUE — so `&g` for an `*i64` global `g` colliding with // a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr // into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading // the stamped operand type distinguishes the bare fn rvalue (TY_FN, the // #34 fn-rvalue stamp) from a value ident, closing the leaf-name // collision by construction. (F12 name-keyed overlap noted in the F7 // spec — same predicate-to-stamp shape; fixed once here.) let ou: *tinfo = tichase(opnd.type_: *tinfo); if (ou == nil) { return false; }; return ou.kind == tykind.TY_FN; }; // tuplerowfoldable — validate every cast-peeled element of `rhs` (an // N_TUPLE) reduces to a static row: an int literal (foldintliteral) or a // str literal in a str/slice slot. A tagged element slot has no // static-init shape (tag word + payload widening) — reject so the caller // loud-stops (#22a, rule 7); pre-guard an int init would have emitted one // 8B word into the 16B+ box (silent layout skew). The validate twin of // emittuplerowbytes / emittuplerowrelocs; two-pass keeps a partial row // out of the output (emitarraydata precedent). Factored from emittupledata // so the slice-of-tuple backing (#117) shares it. Mirror of cstage // tuple_row_foldable. fn tuplerowfoldable(c: *cgen, tt: *node, rhs: *node) bool = { let tp: *node = tt.list; let e: *node = rhs.list; for (e != nil) { let et: *node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; { let eti: *tinfo = nil; if (et != nil) { eti = et.type_: *tinfo; }; eti = tichase(eti); if (eti != nil && eti.kind == tykind.TY_TAGGED) { return false; }; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.kind != nkind.N_STRLIT) { return false; }; } else if (nodefnptr(c, ev)) { // #117: a `&fn` element folds to an 8B reloc slot. } else { let v: u64 = 0u64; if (!foldintliteral(ev, &v)) { return false; }; }; e = e.next; if (tp != nil) { tp = tp.next; }; }; return true; }; // emittuplerowbytes — the row's element bytes, concatenated, into the // currently-open DATAW quoted string (no DATAW wrapper, no sym). Slot // layout (C-t0): a scalar element is one 8B LE word; a str/slice element // its 24B header slot (8 zero ptr placeholder + LE len + 8 zero cap). // Caller has already proven the row foldable. Mirror of cstage // emit_tuple_row_bytes. fn emittuplerowbytes(c: *cgen, tt: *node, rhs: *node) void = { let tp: *node = tt.list; let e: *node = rhs.list; for (e != nil) { let et: *node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; let lv: u64 = ev.str.len: u64; i = 0; for (i < 8) { emitdatawbyte((lv & 255u64): u8); lv = lv >> 8u64; i += 1; }; i = 16; let ssz: i32 = primtypesize("str"): i32; for (i < ssz) { emitdatawbyte(0u8); i += 1; }; } else if (nodefnptr(c, ev)) { // #117: a `&fn` element is an 8B zero ptr placeholder; // the reloc is patched in emittuplerowrelocs. let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; } else { let v: u64 = 0u64; foldintliteral(ev, &v); let i: i32 = 0; let nv: u64 = v; for (i < 8) { emitdatawbyte((nv & 255u64): u8); nv = nv >> 8u64; i += 1; }; }; e = e.next; if (tp != nil) { tp = tp.next; }; }; }; // emittuplerowrelocs — the row's DATAR ptr patches, at backing-relative // ++. A str element patches the ptr word with the // interned strlit's VA; the slot stride steps by tyslicesize/tupeslotn. // `backing` writes the ".d" backing label; rowoff lets a slice // backing place k rows contiguously (#117), emittupledata passes 0 (foff // matches the absolute element offset — byte-neutral). Mirror of cstage // emit_tuple_row_relocs. fn emittuplerowrelocs(c: *cgen, name: str, module: str, backing: bool, rowoff: i32, tt: *node, rhs: *node) void = { let foff: i32 = rowoff; let tp: *node = tt.list; let e: *node = rhs.list; for (e != nil) { let et: *node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.str.len > 0) { let lab: str = internstrlit(c, ev.str); emitline("DATAR "); emitsymnamehint(c, name, module); if (backing) { emitline(".d"); }; emitline("+"); emitint(foff: i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; foff += (tyslicesize(): i32); } else { // #117: the `&fn` element's reloc — the FIRST &fn→DATAR // in the emitter; patches the 8B slot at holder+foff // with the fn's TEXT VA via emitfnname. if (nodefnptr(c, ev)) { emitline("DATAR "); emitsymnamehint(c, name, module); if (backing) { emitline(".d"); }; emitline("+"); emitint(foff: i64); emitline("(SB),"); // #124: a cross-module `&mod.fn` operand mangles // the leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (ev.lhs.kind == nkind.N_DOT) { emitfnname(c, ev.lhs.str, ev.lhs.lhs.str); } else { emitfnname(c, ev.lhs.str, c.curmod); }; emitline("(SB)\n"); }; // #22: slot stride via the accessor (tagged is // rejected upstream; non-wide is 8 today — keeps the // stride on the accessor scale). foff += tupeslotn(et); }; e = e.next; if (tp != nil) { tp = tp.next; }; }; }; // emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);` // static init (C-t3, #48). One slot-laid DATAW row (+ DATAR str-element // ptr patches) via the backing-relative emittuplerow helpers; rhs == nil // zero-inits. Unsupported element inits return false and the caller // loud-stops (rule 7 — pre-C-t3 the whole definition was SILENTLY skipped // and reads saw garbage). Mirror of cstage emit_tuple_data. fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { if (tt == nil) { return false; }; if (rhs == nil) { // #22: slot-sum via the accessor so the zero-fill matches // the checker size (cstage zero-emits u->size). let zsz: i32 = 0; let p0: *node = tt.list; for (p0 != nil) { zsz += tupeslotn(p0.lhs); p0 = p0.next; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); let zi: i32 = 0; for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; emitline("\"\n"); return true; }; if (rhs.kind != nkind.N_TUPLE) { return false; }; if (!tuplerowfoldable(c, tt, rhs)) { return false; }; emitline("DATAW "); emitsymnamehint(c, name, module); emitline("(SB),\""); emittuplerowbytes(c, tt, rhs); emitline("\"\n"); emittuplerowrelocs(c, name, module, false, 0, tt, rhs); 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); // g-fold #77: ONE chase at the dispatch entry. The // array gates below keyed on the N_TARRAY tnode — // an alias-typed global's N_TNAME matched no arm // and the skip-policy ate the decl: no DATAW, // undefined reference at link. The str/float/ // struct/slice/tuple gates already alias-walk // (letvaris* / the tlt tnode walk) and stay put. let dti: *tinfo = nil; if (d.lhs != nil) { dti = tichase(d.lhs.type_: *tinfo); }; // C-t3 (#48): tuple global — slot-laid DATAW // row (+ DATAR ptr patches for str elements) // via emittupledata. Unsupported element // inits die LOUD; pre-C-t3 the definition was // silently skipped (no DATA, no diagnostic) // and reads saw garbage. The istup gate also // keeps a tuple out of the sz==8 / str-size // arms below (a 24B tuple == str size). let tlt: *node = d.lhs; for (tlt != nil && tlt.kind == nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; let istup: bool = false; if (tlt != nil) { if (tlt.kind == nkind.N_TTUPLE) { istup = true; }; }; if (istup) { let tr: *node = d.rhs; for (tr != nil) { if (tr.kind != nkind.N_CAST) { break; }; tr = tr.lhs; }; if (!emittupledata(c, nm, d.nmod, tlt, tr)) { let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n"; os.write(2, mtg.ptr, mtg.len: u64); os.exit(1); }; }; // #87: non-nullable tagged-union global — emit the box // mirroring the runtime local (tag + payload). letemitsize // keeps nullable at 0 so the (*T|void) one-word fold stays // on the 8B scalar arm below. The istagged gate also keeps // a tagged box (size can equal str/slice size) out of those. let istagged: bool = false; if (tlt != nil) { if (tlt.kind == nkind.N_TTAGGED) { if (!isnullabletype(tlt)) { istagged = true; }; }; }; if (istagged) { if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) { let mtg: str = "global tagged let: unsupported variant init (int/str literal only; rule 7)\n"; os.write(2, mtg.ptr, mtg.len: u64); os.exit(1); }; }; 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, d.nmod, 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, d.nmod, 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 (dti != nil) { if (dti.kind == tykind.TY_ARRAY) { isarr8 = true; }; }; if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) { let v: u64 = 0u64; let ok: bool = true; let fnp: bool = false; let r: *node = nil; if (d.rhs != nil) { r = 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. // #119: a scalar `&fn` global — the &fn->DATAR // reloc (the #117 helper at its second consumer). if (nodefnptr(c, r)) { fnp = true; } else { ok = foldintliteral(r, &v); }; }; if (fnp) { emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let zi: i32 = 0; for (zi < 8) { emitdatawbyte(0u8); zi += 1; }; emitline("\"\n"); emitline("DATAR "); emitsymnamehint(c, nm, d.nmod); emitline("+0(SB),"); // #124: cross-module `&mod.fn` mangles the // leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (r.lhs.kind == nkind.N_DOT) { emitfnname(c, r.lhs.str, r.lhs.lhs.str); } else { emitfnname(c, r.lhs.str, c.curmod); }; emitline("(SB)\n"); } else if (ok) { emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); 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"); }; }; // #12: this arm is SIZE-keyed (sz == 24), not type-keyed, // so a no-init array global whose bytes sum to str width // (e.g. `let g: [3]u64;`) matched here AND the TY_ARRAY arm // below → two identical `DATAW g` rows (cstage is type- // keyed via let_isstr and emits one). Exclude arrays — the // emitarraydata path owns them — mirroring the existing // isarr8 guard on the sz==8 scalar arm. if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !isarr8 && !letvarisslice(c, nm)) { 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 "); emitsymnamehint(c, nm, d.nmod); 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 "); emitsymnamehint(c, nm, d.nmod); 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 "); emitsymnamehint(c, nm, d.nmod); 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 && !istagged && letvarisslice(c, nm)) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; // #10 part a: slice-literal static init // routes through emitslicedata (header + // writable backing + DATAR). Loud-stops on // the deferred element kinds and the read- // only/`...` shapes (rule 7). if (r != nil && r.kind == nkind.N_ARRLIT) { emitslicedata(c, nm, d.nmod, d.lhs.type_: *tinfo, d.lhs, r); } else { // zero-init: accept no rhs or nil. // Any other rhs is skipped → // undefined symbol at link. let ok: bool = true; if (d.rhs != nil) { ok = false; if (r != nil) { if (r.kind == nkind.N_NIL) { ok = true; }; }; }; if (ok) { emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); 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. // #254: the zero-fill byte count comes from the // type table's tinfo.size (cstage cg_let_emit_size // returns u->size, cgen.c:978), NOT letemitsize/ // si.totsize — registerstruct rounds the nested // value-struct field's slot to 8, so a sub-8 outer // struct (ABI 4) over-emitted DATAW 8 bytes vs // cstage's 4. registerstruct / fieldsize / frame // slot-padding stay UNTOUCHED (field offsets). if (issg) { if (d.rhs == nil) { let zsz: i32 = sz; if (dti != nil) { zsz = dti.size: i32; }; emitline("DATAW "); emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < zsz) { 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 (dti != nil) { if (dti.kind == tykind.TY_ARRAY) { 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; }; }; // #15: a zero-length array (`[0]T`) // has no bytes — cstage emits no DATA // row; wwstage's unguarded emit produced // a spurious `DATAW name(SB),""`. sz // (letemitsize, cgen.ww:2758) is 0 for // [0]T → skip. (Non-empty [N>0] arrays // keep sz>0.) if (route && sz > 0) { emitarraydata(c, "DATAW", nm, d.nmod, dti, 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, d.nmod, 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; su = tichase(su); if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", d.str, d.nmod, 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; au = tichase(au); if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", d.str, d.nmod, at, r); }; // #10: a read-only `def g: []T = [...]` // slice literal can't carry the ptr reloc // emitslicedata needs (DATAR holder must be // DATAW, w6a asm.c:362). Loud-stop, never // silent no-emit. if (au.kind == tykind.TY_SLICE) { let m: str = "emitdefconstants: module-level slice-literal init needs a writable `let` (DATAR holder must be DATAW, w6a asm.c:362); read-only `def` unsupported (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; };}; }; }; 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 "); emitsymnamehint(c, d.str, d.nmod); 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. // fnretlookup — the called fn's declared return type, keyed by NAME // (same-module-first, then first leaf match). The receive sites that // re-derive a call's result SHAPE from this (cglet tagged-store, // cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned // arms) are correct only when the leaf name uniquely picks the callee. // // #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a // VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive // sites keyed on the field leaf `f` with the receiver VARIABLE name as // the "module" (not a real module), so this lookup mis-binds a same-named // GLOBAL fn. When that global's register shape differs from the field's // (scalar global vs tagged field), the slot is stored with the wrong ABI // shape → cstage≠wwstage asm, silent miscompile. The sound fix derives // the result from the FIELD's fn type / the checker-stamped n.type_ (as // cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is // added here: same-shape leaf collisions resolve by name legitimately // today, and a discriminating guard would need the shape-compare that IS // the fix. Masked until #208 landed (the checker rejected the shape // before cgen ran). test/wcc/782 pins the cstage-correct runtime // (cstage-only) and graduates to STAGE_WW on #211 close. 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; }; // #88: the emission side (emitdefconstants' array arm) peels // TY_NAMED off d.lhs.type_ transitively, so an alias-typed def // array HAS a DATA symbol — keying this gate on the unchased // dtnode kind (N_TARRAY) lied it back to the loud error. Chase // the same stamped tinfo so gate == emission set stays exact. let dtn: *node = defvartnode(c, nm); if (dtn != nil) { let du88: *tinfo = tichase(dtn.type_: *tinfo); if (du88 != nil) { if (du88.kind == tykind.TY_ARRAY) { 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; }; // modlookupvalue — value-global variant: mangle ONLY on an exact // (name, hint) match; otherwise empty so the name stays bare. Unlike // modlookupforfn there is NO first-leaf-match fallback — exported value // globals are export-skipped from c.mods (modcollect keeps their bare- // name data ABI), so a first-match fallback would mis-mangle an exported // `v` onto another module's private `v` (#1 cgen value-global module- // qualifier, the cgen residual of #55). Mirrors cstage mod_lookup_value. // // HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a // workaround: if two modules BOTH export the same value leaf, both stay // bare and the linker sees a duplicate symbol. That is a CORRECT, loud, // link-time ABI clash (like C's two-extern-same-name rule), NOT a silent // miscompile. A bare reference can never legitimately resolve to another // module's PRIVATE global, so first-match is never wanted on the value // path; the only ambiguity left is genuine duplicate exports, which // belong to the linker, not to a cgen disambiguation heuristic. fn modlookupvalue(c: *cgen, name: str, hint: str) str = { let empty: str; empty.ptr = nil; empty.len = 0; if (hint.len == 0) { return empty; }; let m: *modent = c.mods; for (m != nil) { if (streq(m.mname, name)) { if (m.nmod.len > 0 && streq(m.nmod, hint)) { return m.nmod; }; }; m = m.mnext; }; return empty; }; // emitsymname — write the asm symbol name for `ident`. Honours, in // order: FFI mapping (@symbol), module mangling (private decls), bare // name. Use everywhere a top-level 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); }; // emitsymnamehint — write the asm symbol name for a value-global // `ident`, threading `hint` the way emitfnname does for fns. // emitsymname's non-hinted modlookup grabs the first // leaf-name match, so two modules with a same-leaf value global (`let v` // in both) collapse onto one DATA label and a bare cross-module read // resolves to the wrong module (#1 cgen value-global module-qualifier, // the cgen residual of #55). Pass c.curmod at a bare reference, the // decl's own module (d.nmod) at a definition label. Routes through // modlookupvalue (exact-or-bare) so an exported global stays bare // instead of mis-mangling onto another module's same-leaf private // global; kept distinct from emitfnname to leave the fn-mangle path // byte-for-byte untouched. fn emitsymnamehint(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 = modlookupvalue(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/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. // // Reads a .ww file, runs the ww-side lexer, prints tokens through // the ww-side tokprint. The 990_selfhost test diffs this output // byte-for-byte against the C-side wwdump on the same file. Any // divergence is a port bug in lex.ww or tok.ww. // // Modes: // wwdump -t file.ww tokens (default) // wwdump -a file.ww AST (not yet implemented; reserved) package main; import os; import tok; import lex; import ast; import parse; import typ; import sym; import check; import cgen; import strconv; // ---- argv helpers ----------------------------------------------------- // argstrlen — strlen on a NUL-terminated *u8. argv strings are always // NUL-terminated (kernel-supplied) so this is safe. fn argstrlen(s: *u8) i32 = { let n: i32 = 0; for (s[n] != 0u8) { n += 1; }; return n; }; fn argstr(p: *u8) str = { let s: str; s.ptr = p; s.len = argstrlen(p); return s; }; // streqlit — compare a NUL-terminated argv entry to a string literal. fn streqlit(p: *u8, lit: str) bool = { let i: i32 = 0; for (i < lit.len) { if (p[i] != lit[i]) { return false; }; i += 1; }; return p[i] == 0u8; }; // ---- main ------------------------------------------------------------- export fn main(argc: i32, argv: **u8) i32 = { let mode: i32 = 116; // 't' let path: *u8 = nil; let i: i32 = 1; for (i < argc) { let a: *u8 = argv[i]; if (streqlit(a, "-t")) { mode = 116; } else { if (streqlit(a, "-a")) { mode = 97; // 'a' } else { if (streqlit(a, "-r")) { mode = 114; // 'r' — resolve / name-check } else { if (streqlit(a, "-c")) { mode = 99; // 'c' — codegen / emit asm } else { if (path == nil) { path = a; };};};};}; i += 1; }; if (path == nil) { os.write(2, "usage: wwdump [-t|-a] file.ww\n".ptr, 30u64); return 2; }; let fdorerr: (i32 | os.oserror) = os.tryopen(argstr(path), os.flag.RDONLY, 0i32); let fd: i32 = -1; match (fdorerr) { case let v: i32 => fd = v; case let e: os.oserror => { os.write(2, "wwdump: cannot open ".ptr, 20u64); os.write(2, path, argstrlen(path): u64); os.write(2, "\n".ptr, 1u64); return 1; }; }; let szr: (i64 | os.oserror) = os.filesize(fd); let sz: i64 = 0i64; match (szr) { case let v: i64 => sz = v; case let e: os.oserror => { os.write(2, "wwdump: filesize failed\n".ptr, 24u64); os.close(fd); return 1; }; }; let buf: []u8 = alloc([], sz: u64)!; buf.len = sz: i32; let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, sz: u64); os.close(fd); let r: i64 = 0i64; match (rr) { case let v: i64 => r = v; case let e: os.oserror => { os.write(2, "wwdump: read failed\n".ptr, 20u64); return 1; }; }; if (r != sz) { os.write(2, "wwdump: short read\n".ptr, 19u64); return 1; }; let l: lex; lexinit(&l, argstr(path), buf.ptr, sz: u64); if (mode == 116) { // '-t' for (true) { let t: tok; lexnext(&l, &t); tokprint(1i32, &t); if (t.kind == tkind.TK_EOF) { break; }; if (t.kind == tkind.TK_ERR) { break; }; }; } else { if (mode == 97) { // '-a' let ps: parser; parserinit(&ps, &l); let f: *node = parsefile(&ps); astprint(1i32, f); } else { if (mode == 114) { // '-r' — name resolve report let ps: parser; parserinit(&ps, &l); let f: *node = parsefile(&ps); // #52: gate the resolve report on parse-stage errors. Without // this a parse-errored decl is silently dropped from the AST // and the report is printed with rc=0. Mirrors w6c main.ww:162 // / cmd/w6c/main.c. if (l.errs > 0 || ps.errs > 0) { return 1; }; let tc: tctx; typesinit(&tc); let ck: checker; checkinit(&ck, &tc); // Quiet by default; flip to 1 when debugging missing names. ck.verbose = 0; checkfile(&ck, f); // (close out the if-else chain — we'll close all braces below) // ": / resolved" os.write(1, argstr(path).ptr, argstrlen(path): u64); os.write(1, ": ".ptr, 2u64); let rs: str = strconv.i64tos(ck.nresolved: i64, strconv.base.DEC); os.write(1, rs.ptr, rs.len: u64); os.write(1, "/".ptr, 1u64); let total: i32 = ck.nresolved + ck.nunresolved; let ts: str = strconv.i64tos(total: i64, strconv.base.DEC); os.write(1, ts.ptr, ts.len: u64); os.write(1, " resolved\n".ptr, 10u64); if (ck.nunresolved > 0) { return 1; }; } else { if (mode == 99) { // '-c' — codegen / emit asm let ps: parser; parserinit(&ps, &l); let f: *node = parsefile(&ps); // #52: gate cgen on parse-stage errors BEFORE check/cgen. // A parse-errored decl is silently dropped from the AST; the // remaining file would otherwise emit asm with rc=0 (silent // miscompile, and the 994 byte-identity probe ships wrong asm // silently). Mirrors w6c main.ww:162 / cmd/w6c/main.c. if (l.errs > 0 || ps.errs > 0) { return 1; }; // #50: mirror w6c — run check before cgen so AST mutations // from #42 (size/align/offset fold) and audit §1.8 (node.type_ // population) land before cgen walks. Without this, wwdump -c // (the byte-identity probe for 994) would diverge from w6c_ww // on any program that uses the size/align/offset typed builtins. 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); };};};}; if (l.errs > 0) { return 1; }; return 0; };