diff --git a/Makefile b/Makefile index bb1ff8f2..af1c224a 100644 --- a/Makefile +++ b/Makefile @@ -1066,8 +1066,9 @@ $(BIN)/test_loopcap_run: test/wcc/989_loopcap_run.c \ # 989_enumcap_run (#65, F13 c3): the driver's directory enumerator grows # dynamically (mirror cstage realloc-doubling) instead of silently capping at -# 256 files. Builds on both driver twins and asserts a byte-identical -# combined.ww on a >256-file module (rule-10). See the test header. +# 256 files. Builds on both driver twins (`--sep`) and asserts a byte-identical +# per-package sep unit (bigmod.unit.ww) on a >256-file module (rule-10). See +# the test header. $(BIN)/test_enumcap_run: test/wcc/989_enumcap_run.c \ $(BIN)/ww $(BIN)/ww_ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ @@ -1826,9 +1827,9 @@ $(BIN)/test_type_value_shadow_run: test/wcc/788_type_value_shadow_run.c \ # #223: a struct-field access through a pointer-ALIAS receiver whose # leaf collides with another module's same-leaf STRUCT. cstage driver -# build + run for runtime, raw w6c vs w6c_ww .s cmp on the combined.ww -# for rule-10 byte-id (the #223 discriminator). Builds its own 2-module -# fixtures in a private mktemp dir — not a selfhost-driver test. +# build + run for runtime, cs-sep vs ww-sep .s cmp (concatenated +# main.sepwork/*.s) for rule-10 byte-id (the #223 discriminator). Builds its +# own 2-module fixtures in a private mktemp dir — not a selfhost-driver test. $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collide_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ @@ -1837,8 +1838,8 @@ $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collid # #13: cross-module decomposition of an imported union's variants # (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built -# it. cstage driver build + run pins routing; raw w6c_ww on the -# combined.ww (must now accept) + cs==ww byte-id is the discriminator. +# it. cstage driver build + run pins routing; ww-sep build (w6c_ww must +# now accept) + cs-sep==ww-sep byte-id is the discriminator. # Builds its own 2-module fixtures in a private mktemp dir. $(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ @@ -1851,8 +1852,8 @@ $(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \ # to an N_DOT chain the checker's resolve_type had no arm for); the fix # flattens the chain to one N_TNAME at the struct-lit handoff in BOTH # stages. cstage driver build + run pins parse + runtime field values; -# raw w6c vs w6c_ww on the combined.ww is the rule-10 byte-id gate. -# Builds its own 2-module fixtures in a private mktemp dir. +# cs-sep vs ww-sep .s cmp (concatenated main.sepwork/*.s) is the rule-10 +# byte-id gate. Builds its own 2-module fixtures in a private mktemp dir. $(BIN)/test_xmod_qualstructlit_run: test/wcc/848_xmod_qualstructlit_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ @@ -1866,7 +1867,7 @@ $(BIN)/test_xmod_qualstructlit_run: test/wcc/848_xmod_qualstructlit_run.c \ # ("case: not a variant" / "variant not handled"), so any fmt-importing # unit was wwstage-uncompilable. Fix flattens the spread in the checker's # match validity + exhaustiveness walk (selfhost/cmd/wcc/check.ww). The -# w6c_ww-accepts + cs==ww byte-id on the driver combined is the +# w6c_ww-accepts + cs-sep==ww-sep byte-id on the driver sep build is the # discriminator. Self-contained single-file probe. $(BIN)/test_spread_variant_match: test/wcc/792_spread_variant_match.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ @@ -1882,7 +1883,7 @@ $(BIN)/test_widen_pad_zero_run: test/wcc/793_widen_pad_zero_run.c \ # #55: bare value-ident in an imported module must resolve same-module- # preferred (exprtype N_IDENT scopelookup -> scopelookupprefer). cstage -# driver build + run pins routing; raw w6c_ww on the combined must ACCEPT +# driver build + run pins routing; the ww-sep build (w6c_ww) must ACCEPT # (pre-fix it rejected the wrong-module-typed ident). Builds its own # 2-module fixtures in a private mktemp dir. $(BIN)/test_xmod_ident_prefer: test/wcc/794_xmod_ident_prefer.c \ @@ -2944,7 +2945,7 @@ $(BIN)/test_self_rebuild: test/wcc/995_self_rebuild.c $(BIN)/ww_ww \ $(BIN)/test_selfcheck: test/wcc/950_selfcheck.c $(BIN)/wwdump_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< -$(BIN)/test_asserttyped_gap: test/wcc/901_asserttyped_gap.c $(BIN)/wwdump_ww | $(BIN) +$(BIN)/test_asserttyped_gap: test/wcc/901_asserttyped_gap.c $(BIN)/ww $(BIN)/wwdump_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< $(BIN)/test_dyn_ww: test/wcc/996_dyn_ww.c $(BIN)/ww $(BIN)/w6l $(BIN)/w6l_ww \ @@ -3589,9 +3590,10 @@ test-commit: all sizelint peellint $(TESTS) # w6c_ww and diffs the emitted .s. Catches a cs!=ww emission divergence # (rule 10) in seconds — the per-fold inner-loop check Rob's cadence # calls for. NOT a substitute for the full 990-997 byte-id gate, which -# self-compiles main.combined.ww: ALWAYS run `make test` before landing +# self-compiles the tools via `--sep`: ALWAYS run `make test` before landing # a cgen/ABI/compiler-lib fold. w6c consumes a flat unit (no import -# resolution), so FIXTURE must be import-free or a pre-built .combined.ww. +# resolution), so FIXTURE must be import-free or a pre-resolved sep unit +# (.sepwork/__root.unit.ww). # make smoke # default: rt/malloc.ww # make smoke FIXTURE=rt/ensure.ww # any self-contained .ww FIXTURE ?= rt/malloc.ww diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 9623a31a..abac3b32 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -218,10 +218,9 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) * but ww corpus settled on the un-underscored shape). */ if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0) continue; - /* skip "*.combined.ww" — driver-generated concat - * artifacts (the previous build leaves them next to - * the source). They look like .ww but parse-erroring - * when re-included. */ + /* skip "*.combined.ww" — legacy amalgamator artifacts + * (pre-#87): they look like .ww but parse-error when + * re-included, and the sep driver no longer writes them. */ if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0) continue; if (n + 1 > cap) { @@ -240,7 +239,7 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) * ww build — separate-compilation driver (task #46/c3). * ==================================================================== * This is the SOLE build path (E3-C1 flip, task #87): the legacy - * single-file amalgamator (build_one/expand) is gone. Each imported + * single-file amalgamator is gone. Each imported * package's `.wwi` interface is materialized and every package is * compiled on its own (`w6c -c`), then the `.o` set is flat-linked. * @@ -313,10 +312,10 @@ sep_fname(const struct sepgraph *g, int pi, const char *scratch, /* unit_has_package — does `path` declare `package ;` ANYWHERE? * #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import - * from one satisfied by an INLINE package in the same unit. Unlike - * peek_package (stops at the FIRST package decl), this scans every line - * — single-file multi-package fixtures carry several `package` decls. The - * comment-skip line scan + name match mirror peek_package, uncapped. The + * from one satisfied by an INLINE package in the same unit. Unlike a + * first-package-decl scan, this checks every line — single-file + * multi-package fixtures carry several `package` decls. The + * comment-skip line scan + name match are uncapped. The * wwstage twin unithaspackage must stay byte-identical (rule 10). */ static int unit_has_package(const char *path, const char *leaf) @@ -348,8 +347,9 @@ unit_has_package(const char *path, const char *leaf) /* Scan one source file for top-level `import IDENT;`, resolving each. A * DIRECTORY import is a package boundary: add it as a direct dep of pkg * `pi`. A FILE import is an intra-package split — fold its imports into - * `pi` (its bytes join pi's body at emit time). Mirrors expand's scan - * but collects package PATHS instead of concatenating bytes (§1.1). */ + * `pi` (its bytes join pi's body at emit time). Collects package PATHS + * rather than concatenating bytes the way the legacy amalgamator did + * (§1.1). */ static int sep_scan_file(struct sepgraph *g, int pi, const char *file, const char *searchpath, struct ImportSet *filevisit) @@ -383,9 +383,9 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, /* #16 ENFORCE-driver: an unresolvable import is a hard error, * not a silent skip — EXCEPT when the package is defined INLINE * in the same unit (single-file multi-package; leaf = the last - * dotted component). E3-C1: the combined path that used to own - * the genuine-missing case is gone, so the sep producer enforces - * it here (INV-2, by construction). Mirrors the deleted expand. */ + * dotted component). E3-C1 (#87): the legacy amalgamator that + * used to own the genuine-missing case is gone, so the sep + * producer enforces it here (INV-2, by construction). */ if (!locate_import(searchpath, path_form, ipath, sizeof ipath, &is_dir)) { const char *dot = strrchr(name, '.'); @@ -888,8 +888,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, else if (access("lib", 0) == 0) srcdir = "lib"; else srcdir = libdir; } - /* search path: source-dir, then -I dirs, then srcdir (mirrors - * build_one). */ + /* search path: source-dir, then -I dirs, then srcdir. */ char srcd[1024]; if (entry_is_dir) { snprintf(srcd, sizeof srcd, "%s", src); @@ -938,9 +937,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, /* #79 (-T): lib/test is the synth main's `test.run` callee but @test * files never `import test;`. Inject it as a direct dep of the root so * sep_scan_pkg pulls test + its transitive deps; the producer adds -T - * to the root and `test.run` links against test's `.a`. Mirrors the - * combined path's auto-bundle (build_one is_test) and the sep_scan_file - * dedup-guarded dep append. */ + * to the root and `test.run` links against test's `.a` — via the + * sep_scan_file dedup-guarded dep append. */ if (is_test) { char tpath[1024]; int tdir = 0; @@ -1014,8 +1012,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *out, /* wrap each DEP package's `.o` in its own deterministic `.a` * (5a). The ROOT stays a positional `.o` (force-loaded — it's * the build target, always fully linked), so `main` is defined - * before any archive is processed, matching build_one's root - * treatment. The link consumes `.o`/`.a`, never `.wwi`. */ + * before any archive is processed. The link consumes `.o`/`.a`, + * never `.wwi`. */ if (pi != root) { char apath[1024]; sep_fname(g, pi, scratch, ".a", apath, sizeof apath); @@ -1114,7 +1112,7 @@ basename_no_ext(const char *path, char *out, size_t outsz) * . → cwd as a directory * foo (bare) → walk cwd:incs:WW_LIB; first hit is dir or file. * Sets *is_dir on hit. Dir resolution drives directory-as-module - * enumeration in build_one. */ + * enumeration in build_one_sep. */ static int resolve_module(const char *name, const char *incs, char *out, size_t outsz, int *is_dir) @@ -1317,8 +1315,8 @@ do_run(int argc, char **argv) } char tmp[1024]; snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid()); - /* objstem = tmp → intermediates land at /tmp/ww_run_.{s,o, - * combined.ww}, never next to the source (T3). */ + /* objstem = tmp → intermediates land under /tmp/ww_run_.sepwork/, + * never next to the source (T3). */ if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0) return 1; /* exec the built binary with any trailing argv as its argv. */ @@ -1347,9 +1345,10 @@ do_test(int argc, char **argv) const char *src = NULL; char incs[2048] = {0}; /* -c (compile-only, Go's `go test -c`) + -o build the test - * binary (and its lib/test-inclusive combined, via build_one's + * binary (and its lib/test-inclusive sep unit, via build_one_sep's * is_test auto-bundle + the T3 objstem redirect) WITHOUT running it — - * the byte-id gates feed .combined.ww to raw w6c -T / w6c_ww -T. + * the byte-id gates feed .sepwork/__root.unit.ww to raw + * w6c -T / w6c_ww -T. * -T stays internal to w6c; the driver never sees it. -l/-L carry no * meaning for a test build, so they (and any unknown flag) are rejected * rather than silently swallowed — byte-identical wording to the diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww deleted file mode 100644 index 73e8c0bf..00000000 --- a/selfhost/cmd/w6a/main.combined.ww +++ /dev/null @@ -1,5187 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module-reset -// selfhost/cmd/w6a/opcodes.ww — types + constants shared across the -// w6a port. Mirrors cmd/w6a/a.h and cmd/w6c/6.out.h. - -package w6a; - -// ---- registers + operand kinds (from 6.out.h) ------------------------- -// These must stay numerically aligned with the C enum so that ww-cgen -// output (which reads them via `D_AX(SB)` etc.) lands on the same -// integers when read by ww-w6a. -def D_NONE: i32 = 0; - -def D_AX: i32 = 1; -def D_CX: i32 = 2; -def D_DX: i32 = 3; -def D_BX: i32 = 4; -def D_SP: i32 = 5; -def D_BP: i32 = 6; -def D_SI: i32 = 7; -def D_DI: i32 = 8; -def D_R8: i32 = 9; -def D_R9: i32 = 10; -def D_R10: i32 = 11; -def D_R11: i32 = 12; -def D_R12: i32 = 13; -def D_R13: i32 = 14; -def D_R14: i32 = 15; -def D_R15: i32 = 16; - -def D_X0: i32 = 17; -def D_X1: i32 = 18; -def D_X2: i32 = 19; -def D_X3: i32 = 20; -def D_X4: i32 = 21; -def D_X5: i32 = 22; -def D_X6: i32 = 23; -def D_X7: i32 = 24; -def D_X8: i32 = 25; -def D_X9: i32 = 26; -def D_X10: i32 = 27; -def D_X11: i32 = 28; -def D_X12: i32 = 29; -def D_X13: i32 = 30; -def D_X14: i32 = 31; -def D_X15: i32 = 32; - -def D_PSP: i32 = 33; -def D_PFP: i32 = 34; -def D_PSB: i32 = 35; - -def D_CONST: i32 = 36; -def D_BRANCH: i32 = 37; -def D_EXTERN: i32 = 38; -def D_INDIR: i32 = 39; - -// ---- opcodes ---------------------------------------------------------- -def A_NOP: i32 = 0; -def A_TEXT: i32 = 1; -def A_DATA: i32 = 2; -def A_GLOBL: i32 = 3; -def A_END: i32 = 4; - -def A_MOVQ: i32 = 5; -def A_MOVL: i32 = 6; -def A_MOVB: i32 = 7; -def A_MOVZBQ: i32 = 8; -def A_MOVSXD: i32 = 9; -def A_MOVW: i32 = 62; -def A_MOVZWQ: i32 = 63; -def A_MOVSWQ: i32 = 64; -def A_MOVSBQ: i32 = 65; - -def A_MOVSD: i32 = 10; -def A_ADDSD: i32 = 11; -def A_SUBSD: i32 = 12; -def A_MULSD: i32 = 13; -def A_DIVSD: i32 = 14; -def A_UCOMISD: i32 = 15; -def A_CVTTSD2SI: i32 = 16; -def A_CVTSI2SD: i32 = 17; - -def A_MOVSS: i32 = 18; -def A_ADDSS: i32 = 19; -def A_SUBSS: i32 = 20; -def A_MULSS: i32 = 21; -def A_DIVSS: i32 = 22; -def A_UCOMISS: i32 = 23; -def A_CVTTSS2SI: i32 = 24; -def A_CVTSI2SS: i32 = 25; -def A_CVTSD2SS: i32 = 26; -def A_CVTSS2SD: i32 = 27; - -def A_ADDQ: i32 = 28; -def A_SUBQ: i32 = 29; -def A_IMULQ: i32 = 30; -def A_IDIVQ: i32 = 31; -def A_DIVQ: i32 = 32; -def A_NEGQ: i32 = 33; -def A_NOTQ: i32 = 34; -def A_ANDQ: i32 = 35; -def A_ORQ: i32 = 36; -def A_XORQ: i32 = 37; -def A_SHLQ: i32 = 38; -def A_SHRQ: i32 = 39; -def A_CMPQ: i32 = 40; - -def A_PUSHQ: i32 = 41; -def A_POPQ: i32 = 42; -def A_LEAQ: i32 = 43; - -def A_CALL: i32 = 44; -def A_RET: i32 = 45; -def A_JMP: i32 = 46; -def A_JE: i32 = 47; -def A_JNE: i32 = 48; -def A_JL: i32 = 49; -def A_JLE: i32 = 50; -def A_JG: i32 = 51; -def A_JGE: i32 = 52; -def A_JB: i32 = 53; -def A_JBE: i32 = 54; -def A_JA: i32 = 55; -def A_JAE: i32 = 56; -def A_JZ: i32 = 57; -def A_JNZ: i32 = 58; -// 67 (next free above A_CQO=66): appended so the existing A_MOV*/ -// A_SYSCALL/A_DATAW/A_DATAR/A_CQO numbers stay put. Jump on -// parity (PF=1): UCOMISD unordered (#97). -def A_JP: i32 = 67; -// #136: arithmetic right-shift, sign-extends MSB. SHR injects -// zeros and is wrong for signed operands; cgen routes signed -// `>>` / `>>=` through SAR after this opcode landed. -def A_SARQ: i32 = 68; - -def A_SYSCALL: i32 = 59; - -// Writable data + reloc-only data. Mirror cmd/w6c/6.out.h. -// A_DATAW: bytes land in .data (RW) instead of .text. -// A_DATAR: record an R_X86_64_64 reloc at a .data slot, patched -// to a target symbol's runtime VA at link time. -def A_DATAW: i32 = 60; -def A_DATAR: i32 = 61; - -// REX.W 99 — sign-extend RAX into RDX:RAX. Pairs with IDIVQ for -// signed division; pendant to the MOVQ $0, DX zero-fill that pairs -// with DIVQ. -def A_CQO: i32 = 66; - -// ---- structs (mirror cmd/w6a/a.h) -------------------------------------- - -type aoperand = struct { - atype: i32, // D_NONE / D_AX..D_R15 / D_CONST / D_INDIR / D_EXTERN / D_BRANCH - reg: i32, - offset: i64, - asym: str, -}; - -// `from` and `to` are pointer-to-aoperand (rather than embedded). -// The C cgen doesn't support chained-dot through embedded value -// fields, so allocating each operand once per prog lets us write -// `p.to.atype` directly. -type aprog = struct { - as_: i32, - from: *aoperand, - to: *aoperand, - line: i32, - label: str, - link: *aprog, - bytes: *u8, // payload for A_DATA - nbytes: u64, -}; - -type asym = struct { - name: str, - defined: i32, - istext: i32, - isdata: i32, // mutually exclusive with istext; DATAW symbols - isglobal: i32, - addr: u64, // offset within its section (.text or .data) - idx: i32, - snext: *asym, -}; - -type areloc = struct { - off: u64, - section: i32, // 0 = .text, 1 = .data - kind: i32, - asy: *asym, - addend: i64, - rnext: *areloc, -}; - -type afixup = struct { - off: u64, // where the rel32 lands in .text - label: str, - fnext: *afixup, -}; - -type asm_ = struct { - file: str, - src: *u8, - srclen: u64, - pos: u64, - line: i32, - - head: *aprog, - tail: *aprog, - - text: *u8, - textcap: u64, - textlen: u64, - - // Writable .data. Empty unless any DATAW directive was seen; - // obj.ww emits the extra section conditionally so .o output - // stays byte-identical for inputs that don't use DATAW (test - // 991 byte-diff invariant). - data: *u8, - datacap: u64, - datalen: u64, - - syms: *asym, - relocs: *areloc, - fixups: *afixup, - - errs: i32, -}; - -//ww:module-reset -// selfhost/cmd/w6a/lex.ww — port of cmd/w6a/lex.c. -// -// Character-level helpers for w6a's line-oriented parser. The parser -// itself is in parse.ww; here we keep tokenisers for identifiers and -// numbers so parse.ww stays focused on syntax. - -package w6a; - -export fn isidstart(c: i32) bool = { - if (c == 95) { return true; }; - if (c >= 65) { if (c <= 90) { return true; }; }; // A-Z - if (c >= 97) { if (c <= 122) { return true; }; }; // a-z - return false; -}; - -export fn isidcont(c: i32) bool = { - if (isidstart(c)) { return true; }; - if (c >= 48) { if (c <= 57) { return true; }; }; // 0-9 - if (c == 46) { return true; }; // . - return false; -}; - -// parsenum — read a leading [+-]?[0x|0X|0]?digits from p[0..n-1]. -// Returns (value, consumed). Stops at first non-digit. -// Plain Plan 9-style: $123 / $0x1f / $-7. Decimal default; 0x prefix -// for hex; 0 prefix for octal when followed by a digit (else just 0). -export fn parsenum(p: *u8, n: u64) (i64, u64) = { - // strtoll(s, end, 0) semantics, matching the C twin cmd/w6a/lex.c:30: - // skip leading whitespace, optional sign, base-0 prefix detection - // (0x -> hex, leading 0 -> octal, else decimal). w6c never emits the - // `$ 5` / `$08` edge shapes; this aligns the hand-written-asm path - // with cstage so the two assemblers agree byte-for-byte (#62). - let i: u64 = 0u64; - for (i < n) { - if (p[i] != 32u8) { if (p[i] != 9u8) { break; }; }; - i += 1u64; - }; - let neg: bool = false; - if (i < n) { - if (p[i] == 45u8) { neg = true; i += 1u64; } - else { if (p[i] == 43u8) { i += 1u64; }; }; - }; - let base: i64 = 10i64; - if (i < n) { - if (p[i] == 48u8) { // leading '0' -> octal, unless '0x'/'0X' - if (i + 1u64 < n) { - if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } - else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } - else { base = 8i64; i += 1u64; }; }; - } else { base = 8i64; i += 1u64; }; - }; - }; - let v: i64 = 0i64; - let scan: bool = true; - for (scan) { - if (i >= n) { scan = false; } - else { - let c: u8 = p[i]; - let d: i64 = -1i64; - if (c >= 48u8) { if (c <= 57u8) { d = (c - 48u8): i64; }; }; - if (d < 0i64) { - if (base == 16i64) { - if (c >= 97u8) { if (c <= 102u8) { d = (c - 97u8): i64 + 10i64; }; }; - if (c >= 65u8) { if (c <= 70u8) { d = (c - 65u8): i64 + 10i64; }; }; - }; - }; - if (d < 0i64) { scan = false; } - else { if (d >= base) { scan = false; } - else { - v = v * base + d; - i += 1u64; - }; }; - }; - }; - if (neg) { v = -v; }; - return v, i; -}; - -//ww:module-reset -// selfhost/cmd/w6a/parse.ww — port of cmd/w6a/parse.c. -// -// Line-oriented parser for the asm subset emitted by w6c. -// Grammar: -// line := blank | comment | label | text | instr -// blank := /^\s*$/ -// comment := /^\s*\/\/.*$/ -// label := /^IDENT:$/ -// text := TEXT name,$framesize -// instr := \tMNEM\t[OP1[, OP2]] -// OP := $NUM | REG | NUM(REG) | (REG) | name(SB) | label - -package w6a; - -import os; -import strings; -import lex; -import opcodes; - -fn streqlit(p: *u8, n: u64, lit: str) bool = { - if (n != lit.len: u64) { return false; }; - let i: u64 = 0u64; - for (i < n) { - let li: i32 = i: i32; - if (p[i] != lit[li]) { return false; }; - i += 1u64; - }; - return true; -}; - -// opcodelookup — name (length-bounded *u8) → A_*. Returns 0 (A_NOP) -// if not found. -fn opcodelookup(p: *u8, n: u64) i32 = { - if (streqlit(p, n, "MOVQ")) { return A_MOVQ; }; - if (streqlit(p, n, "MOVL")) { return A_MOVL; }; - if (streqlit(p, n, "MOVW")) { return A_MOVW; }; - if (streqlit(p, n, "MOVB")) { return A_MOVB; }; - if (streqlit(p, n, "MOVZBQ")) { return A_MOVZBQ; }; - if (streqlit(p, n, "MOVZWQ")) { return A_MOVZWQ; }; - if (streqlit(p, n, "MOVSXD")) { return A_MOVSXD; }; - if (streqlit(p, n, "MOVSWQ")) { return A_MOVSWQ; }; - if (streqlit(p, n, "MOVSBQ")) { return A_MOVSBQ; }; - if (streqlit(p, n, "MOVSD")) { return A_MOVSD; }; - if (streqlit(p, n, "ADDSD")) { return A_ADDSD; }; - if (streqlit(p, n, "SUBSD")) { return A_SUBSD; }; - if (streqlit(p, n, "MULSD")) { return A_MULSD; }; - if (streqlit(p, n, "DIVSD")) { return A_DIVSD; }; - if (streqlit(p, n, "UCOMISD")) { return A_UCOMISD; }; - if (streqlit(p, n, "CVTTSD2SI")) { return A_CVTTSD2SI; }; - if (streqlit(p, n, "CVTSI2SD")) { return A_CVTSI2SD; }; - if (streqlit(p, n, "MOVSS")) { return A_MOVSS; }; - if (streqlit(p, n, "ADDSS")) { return A_ADDSS; }; - if (streqlit(p, n, "SUBSS")) { return A_SUBSS; }; - if (streqlit(p, n, "MULSS")) { return A_MULSS; }; - if (streqlit(p, n, "DIVSS")) { return A_DIVSS; }; - if (streqlit(p, n, "UCOMISS")) { return A_UCOMISS; }; - if (streqlit(p, n, "CVTTSS2SI")) { return A_CVTTSS2SI; }; - if (streqlit(p, n, "CVTSI2SS")) { return A_CVTSI2SS; }; - if (streqlit(p, n, "CVTSD2SS")) { return A_CVTSD2SS; }; - if (streqlit(p, n, "CVTSS2SD")) { return A_CVTSS2SD; }; - if (streqlit(p, n, "ADDQ")) { return A_ADDQ; }; - if (streqlit(p, n, "SUBQ")) { return A_SUBQ; }; - if (streqlit(p, n, "IMULQ")) { return A_IMULQ; }; - if (streqlit(p, n, "IDIVQ")) { return A_IDIVQ; }; - if (streqlit(p, n, "DIVQ")) { return A_DIVQ; }; - if (streqlit(p, n, "CQO")) { return A_CQO; }; - if (streqlit(p, n, "NEGQ")) { return A_NEGQ; }; - if (streqlit(p, n, "NOTQ")) { return A_NOTQ; }; - if (streqlit(p, n, "ANDQ")) { return A_ANDQ; }; - if (streqlit(p, n, "ORQ")) { return A_ORQ; }; - if (streqlit(p, n, "XORQ")) { return A_XORQ; }; - if (streqlit(p, n, "SHLQ")) { return A_SHLQ; }; - if (streqlit(p, n, "SHRQ")) { return A_SHRQ; }; - if (streqlit(p, n, "SARQ")) { return A_SARQ; }; - if (streqlit(p, n, "CMPQ")) { return A_CMPQ; }; - if (streqlit(p, n, "PUSHQ")) { return A_PUSHQ; }; - if (streqlit(p, n, "POPQ")) { return A_POPQ; }; - if (streqlit(p, n, "LEAQ")) { return A_LEAQ; }; - if (streqlit(p, n, "CALL")) { return A_CALL; }; - if (streqlit(p, n, "RET")) { return A_RET; }; - if (streqlit(p, n, "JMP")) { return A_JMP; }; - if (streqlit(p, n, "JE")) { return A_JE; }; - if (streqlit(p, n, "JNE")) { return A_JNE; }; - if (streqlit(p, n, "JL")) { return A_JL; }; - if (streqlit(p, n, "JLE")) { return A_JLE; }; - if (streqlit(p, n, "JG")) { return A_JG; }; - if (streqlit(p, n, "JGE")) { return A_JGE; }; - if (streqlit(p, n, "JB")) { return A_JB; }; - if (streqlit(p, n, "JBE")) { return A_JBE; }; - if (streqlit(p, n, "JA")) { return A_JA; }; - if (streqlit(p, n, "JAE")) { return A_JAE; }; - if (streqlit(p, n, "JZ")) { return A_JZ; }; - if (streqlit(p, n, "JNZ")) { return A_JNZ; }; - if (streqlit(p, n, "JP")) { return A_JP; }; - if (streqlit(p, n, "SYSCALL")) { return A_SYSCALL; }; - if (streqlit(p, n, "TEXT")) { return A_TEXT; }; - if (streqlit(p, n, "DATA")) { return A_DATA; }; - if (streqlit(p, n, "DATAW")) { return A_DATAW; }; - if (streqlit(p, n, "DATAR")) { return A_DATAR; }; - return A_NOP; -}; - -// reglookup — name → D_*. Returns D_NONE if not found. -fn reglookup(p: *u8, n: u64) i32 = { - if (streqlit(p, n, "AX")) { return D_AX; }; - if (streqlit(p, n, "BX")) { return D_BX; }; - if (streqlit(p, n, "CX")) { return D_CX; }; - if (streqlit(p, n, "DX")) { return D_DX; }; - if (streqlit(p, n, "SP")) { return D_SP; }; - if (streqlit(p, n, "BP")) { return D_BP; }; - if (streqlit(p, n, "SI")) { return D_SI; }; - if (streqlit(p, n, "DI")) { return D_DI; }; - if (streqlit(p, n, "R8")) { return D_R8; }; - if (streqlit(p, n, "R9")) { return D_R9; }; - if (streqlit(p, n, "R10")) { return D_R10; }; - if (streqlit(p, n, "R11")) { return D_R11; }; - if (streqlit(p, n, "R12")) { return D_R12; }; - if (streqlit(p, n, "R13")) { return D_R13; }; - if (streqlit(p, n, "R14")) { return D_R14; }; - if (streqlit(p, n, "R15")) { return D_R15; }; - if (streqlit(p, n, "X0")) { return D_X0; }; - if (streqlit(p, n, "X1")) { return D_X1; }; - if (streqlit(p, n, "X2")) { return D_X2; }; - if (streqlit(p, n, "X3")) { return D_X3; }; - if (streqlit(p, n, "X4")) { return D_X4; }; - if (streqlit(p, n, "X5")) { return D_X5; }; - if (streqlit(p, n, "X6")) { return D_X6; }; - if (streqlit(p, n, "X7")) { return D_X7; }; - if (streqlit(p, n, "X8")) { return D_X8; }; - if (streqlit(p, n, "X9")) { return D_X9; }; - if (streqlit(p, n, "X10")) { return D_X10; }; - if (streqlit(p, n, "X11")) { return D_X11; }; - if (streqlit(p, n, "X12")) { return D_X12; }; - if (streqlit(p, n, "X13")) { return D_X13; }; - if (streqlit(p, n, "X14")) { return D_X14; }; - if (streqlit(p, n, "X15")) { return D_X15; }; - if (streqlit(p, n, "SB")) { return D_PSB; }; - if (streqlit(p, n, "FP")) { return D_PFP; }; - return D_NONE; -}; - -export fn init(a: *asm_, file: str, src: *u8, len: u64) void = { - a.file = file; - a.src = src; - a.srclen = len; - a.pos = 0u64; - a.line = 1; - a.head = nil; - a.tail = nil; - a.text = nil; - a.textcap = 0u64; - a.textlen = 0u64; - a.syms = nil; - a.relocs = nil; - a.fixups = nil; - a.errs = 0; -}; - -export fn intern(a: *asm_, name: str) *asym = { - let s: *asym = a.syms; - for (s != nil) { - if (strings.compare(s.name, name) == 0) { return s; }; - s = s.snext; - }; - let n: *asym = alloc(asym { name = name, snext = a.syms })!; - a.syms = n; - return n; -}; - -fn perr(a: *asm_, msg: str) void = { - let pfx: str = "w6a: "; - os.write(2, pfx.ptr, pfx.len: u64); - let f: str = a.file; - os.write(2, f.ptr, f.len: u64); - let sep: str = ": "; - os.write(2, sep.ptr, sep.len: u64); - os.write(2, msg.ptr, msg.len: u64); - let nl: str = "\n"; - os.write(2, nl.ptr, nl.len: u64); - a.errs += 1; -}; - -// dupstr — copy n bytes from p into a fresh heap str. -fn dupstr(p: *u8, n: u64) str = { - let view: str; - view.ptr = p; - view.len = n: i32; - return strings.dup(view); -}; - -// ---- line iteration & whitespace -------------------------------------- - -// Read next line into a fresh heap buffer; returns (ptr, len) or (nil,0) -// at EOF. Advances a.pos past the newline. -fn nextline(a: *asm_) (*u8, u64) = { - if (a.pos >= a.srclen) { return nil, 0u64; }; - let start: u64 = a.pos; - for (a.pos < a.srclen) { - if (a.src[a.pos] == '\n') { a.pos = a.pos; a.pos += 0u64; } // no-op; explicit break via condition - else { a.pos += 1u64; continue; }; - // hit newline - let n: u64 = a.pos - start; - let buf: []u8 = alloc([], n + 1u64)!; - let i: u64 = 0u64; - for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; - buf[n] = 0u8; - a.pos += 1u64; // skip newline - return buf.ptr, n; - }; - // EOF without trailing newline - let n: u64 = a.pos - start; - if (n == 0u64) { return nil, 0u64; }; - let buf: []u8 = alloc([], n + 1u64)!; - let i: u64 = 0u64; - for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; - buf[n] = 0u8; - return buf.ptr, n; -}; - -fn skipws(p: *u8, off: u64, n: u64) u64 = { - let i: u64 = off; - for (i < n) { - if (p[i] != ' ') { if (p[i] != '\t') { return i; }; }; - i += 1u64; - }; - return i; -}; - -// parseoperand — parse one operand from p[off..n), populate out. -// Returns new offset (clamped to n on error). -fn parseoperand(a: *asm_, p: *u8, offin: u64, n: u64, out: *aoperand) u64 = { - let off: u64 = skipws(p, offin, n); - out.atype = D_NONE; - out.reg = 0; - out.offset = 0i64; - let empty: str; - empty.ptr = nil; empty.len = 0; - out.asym = empty; - if (off >= n) { return off; }; - let c0: u8 = p[off]; - - // $NUM - if (c0 == '$') { - off += 1u64; - let v: i64; - let used: u64; - v, used = parsenum(p + off, n - off); - out.atype = D_CONST; - out.offset = v; - return off + used; - }; - - // (REG) - if (c0 == '(') { - off += 1u64; - let rstart: u64 = off; - for (off < n) { - if (p[off] == ')') { off = off; off += 0u64; } // no-op marker - else { off += 1u64; continue; }; - let rn: u64 = off - rstart; - let r: i32 = reglookup(p + rstart, rn); - if (r == 0) { perr(a, "bad register in indirect"); return n; }; - out.atype = D_INDIR; - out.reg = r; - out.offset = 0i64; - return off + 1u64; // past ')' - }; - perr(a, "missing ')' in indirect"); - return n; - }; - - // number(REG) — possibly signed — or bare $NUM-less constant - let cur: u64 = off; - let isnum: bool = false; - if (cur < n) { - // cstage parse.c:189-190 strips '-' then requires isdigit(*p); - // a bare `-(BP)` is NOT a number operand and falls through to a - // loud reject. ww treated '-' alone as a number → silent 0(BP). - // Require a following digit to match (#62). - if (p[cur] == '-') { - if (cur + 1u64 < n) { - if (p[cur + 1u64] >= '0') { if (p[cur + 1u64] <= '9') { - isnum = true; - }; }; - }; - } - else { if (p[cur] >= '0') { if (p[cur] <= '9') { isnum = true; }; }; }; - }; - if (isnum) { - let v: i64; - let used: u64; - v, used = parsenum(p + off, n - off); - let after: u64 = off + used; - if (after < n) { if (p[after] == '(') { - let rstart: u64 = after + 1u64; - let cur2: u64 = rstart; - for (cur2 < n) { - if (p[cur2] == ')') { cur2 = cur2; cur2 += 0u64; } - else { cur2 += 1u64; continue; }; - let rn: u64 = cur2 - rstart; - let r: i32 = reglookup(p + rstart, rn); - if (r == 0) { perr(a, "bad register"); return n; }; - out.atype = D_INDIR; - out.reg = r; - out.offset = v; - return cur2 + 1u64; - }; - perr(a, "missing ')'"); - return n; - };}; - out.atype = D_CONST; - out.offset = v; - return after; - }; - - // IDENT — register, symbol(SB), symbol+disp(SB), or branch label - if (isidstart(c0: i32)) { - let istart: u64 = off; - for (off < n) { - if (isidcont(p[off]: i32)) { off += 1u64; continue; }; - off = off; off += 0u64; // loop break - let in_: u64 = off - istart; - // Optional `+disp` between the ident and `(SB)`. Used - // by DATAR to address bytes within a previously-defined - // .data slot (e.g. `DATAR s+8(SB),...`). - let symdisp: i64 = 0i64; - if (off < n) { if (p[off] == '+') { - off += 1u64; - let v: i64; - let used: u64; - v, used = parsenum(p + off, n - off); - symdisp = v; - off += used; - };}; - // IDENT(SB) — external - if (off < n) { if (p[off] == '(') { - let rstart: u64 = off + 1u64; - let cur2: u64 = rstart; - for (cur2 < n) { - if (p[cur2] == ')') { cur2 = cur2; cur2 += 0u64; } - else { cur2 += 1u64; continue; }; - let rn: u64 = cur2 - rstart; - let r: i32 = reglookup(p + rstart, rn); - if (r == D_PSB) { - out.atype = D_EXTERN; - out.asym = dupstr(p + istart, in_); - out.offset = symdisp; - } else { - out.atype = D_INDIR; - out.reg = r; - out.offset = 0i64; - }; - return cur2 + 1u64; - }; - perr(a, "missing ')'"); - return n; - };}; - let r: i32 = reglookup(p + istart, in_); - if (r != D_NONE) { - out.atype = r; - return off; - }; - out.atype = D_BRANCH; - out.asym = dupstr(p + istart, in_); - return off; - }; - // EOF inside ident - let in_: u64 = off - istart; - let r: i32 = reglookup(p + istart, in_); - if (r != D_NONE) { out.atype = r; return off; }; - out.atype = D_BRANCH; - out.asym = dupstr(p + istart, in_); - return off; - }; - - perr(a, "unrecognised operand"); - return n; -}; - -// Append a fresh aprog to the list with given opcode and label. -fn addprog(a: *asm_, opc: i32, lbl: str) *aprog = { - let pr: *aprog = alloc(aprog { as_ = opc, line = a.line, label = lbl })!; - pr.from = alloc(aoperand { })!; - pr.to = alloc(aoperand { })!; - if (a.head == nil) { a.head = pr; } - else { a.tail.link = pr; }; - a.tail = pr; - return pr; -}; - -export fn parse(a: *asm_) i32 = { - let pending: str; - pending.ptr = nil; pending.len = 0; - - for (true) { - let line: *u8; - let n: u64; - line, n = nextline(a); - if (line == nil) { return a.errs; }; - - // skip leading ws - let i: u64 = skipws(line, 0u64, n); - // blank or //-comment - if (i >= n) { a.line += 1; continue; }; - if (i + 1u64 < n) { - if (line[i] == '/') { if (line[i + 1u64] == '/') { - a.line += 1; continue; - };}; - }; - - // Label? IDENT: starting at column 0 (no leading tab). - // Only if the identifier is followed by ':'. Otherwise, fall - // through to mnemonic parsing so e.g. `TEXT foo,$0` (which - // also starts with an idchar in column 0) gets parsed. - if (line[0u64] != '\t') { - if (isidstart(line[i]: i32)) { - let q: u64 = i; - let scanid: bool = true; - for (scanid) { - if (q >= n) { scanid = false; } - else { if (isidcont(line[q]: i32)) { q += 1u64; } - else { scanid = false; }; }; - }; - if (q < n) { if (line[q] == ':') { - let nm: str = dupstr(line + i, q - i); - // Pending label gets a NOP prog so addresses pin. - if (pending.len > 0) { - let np: *aprog = addprog(a, A_NOP, pending); - }; - pending = nm; - a.line += 1; - continue; - };}; - // not a label — fall through to mnemonic parse - }; - }; - - // MNEMONIC at the start of the rest. Scan to first ws/EOL. - let mstart: u64 = i; - let m: u64 = mstart; - let scan: bool = true; - for (scan) { - if (m >= n) { scan = false; } - else { if (line[m] == ' ') { scan = false; } - else { if (line[m] == '\t') { scan = false; } - else { m += 1u64; }; }; }; - }; - let mlen: u64 = m - mstart; - let opc: i32 = opcodelookup(line + mstart, mlen); - if (opc == 0) { - if (mlen > 0u64) { - perr(a, "unknown opcode"); - }; - pending.ptr = nil; pending.len = 0; - a.line += 1; continue; - }; - - let pr: *aprog = addprog(a, opc, pending); - pending.ptr = nil; pending.len = 0; - - // Skip ws after mnemonic - let r0: u64 = skipws(line, m, n); - - if (opc == A_TEXT) { - // TEXT name,$framesize — find first ',' as the end of name. - let q: u64 = r0; - let commapos: u64 = n; - let scant: bool = true; - for (scant) { - if (q >= n) { scant = false; } - else { if (line[q] == ',') { commapos = q; scant = false; } - else { q += 1u64; }; }; - }; - let toop: *aoperand = pr.to; - toop.atype = D_EXTERN; - toop.asym = dupstr(line + r0, commapos - r0); - if (commapos < n) { - let p2: u64 = commapos + 1u64; - p2 = skipws(line, p2, n); - if (p2 < n) { if (line[p2] == '$') { p2 += 1u64; }; }; - let v: i64; - let used: u64; - v, used = parsenum(line + p2, n - p2); - let fromop: *aoperand = pr.from; - fromop.atype = D_CONST; - fromop.offset = v; - }; - a.line += 1; continue; - }; - - if (opc == A_DATA || opc == A_DATAW) { - // DATA / DATAW name(SB),"escaped bytes" — same syntax, - // different destination section (.text vs .data). - let q: u64 = r0; - let lparen: u64 = n; - let scand: bool = true; - for (scand) { - if (q >= n) { scand = false; } - else { if (line[q] == '(') { lparen = q; scand = false; } - else { q += 1u64; }; }; - }; - let toop: *aoperand = pr.to; - toop.atype = D_EXTERN; - toop.asym = dupstr(line + r0, lparen - r0); - // Skip past `(SB)` to land just after ')'. - let p2: u64 = lparen; - let scand2: bool = true; - for (scand2) { - if (p2 >= n) { scand2 = false; } - else { if (line[p2] == ')') { p2 += 1u64; scand2 = false; } - else { p2 += 1u64; }; }; - }; - // Skip ws / ',' / tab between `)` and the `"`. - let scand3: bool = true; - for (scand3) { - if (p2 >= n) { scand3 = false; } - else { if (line[p2] == ' ') { p2 += 1u64; } - else { if (line[p2] == ',') { p2 += 1u64; } - else { if (line[p2] == '\t') { p2 += 1u64; } - else { scand3 = false; }; }; }; }; - }; - if (p2 >= n) { perr(a, "DATA missing payload"); a.line += 1; continue; }; - if (line[p2] != '"') { perr(a, "DATA expects \"...\""); a.line += 1; continue; }; - p2 += 1u64; // past opening " - // Parse escape sequence into a fresh growable buffer. - let cap: u64 = 32u64; - let blen: u64 = 0u64; - let dbuf: []u8 = alloc([], cap)!; - for (p2 < n) { - if (line[p2] == '"') { p2 = p2; p2 += 0u64; p2 = n + 1u64; } - else { - let ch: u8 = line[p2]; - p2 += 1u64; - if (ch == '\\') { - if (p2 < n) { - let e: u8 = line[p2]; - p2 += 1u64; - if (e == 'n') { ch = '\n'; } - else { if (e == 't') { ch = '\t'; } - else { if (e == 'r') { ch = '\r'; } - else { if (e == '\\') { ch = '\\'; } - else { if (e == '"') { ch = '"'; } - else { if (e == '0') { ch = '\0'; } - else { if (e == 'x') { - if (p2 + 1u64 < n) { - let hi: u8 = line[p2]; - let lo: u8 = line[p2 + 1u64]; - p2 += 2u64; - let h: u8 = 0u8; - let l: u8 = 0u8; - if (hi <= 57u8) { h = hi - 48u8; } - else { h = (hi | 32u8) - 97u8 + 10u8; }; - if (lo <= 57u8) { l = lo - 48u8; } - else { l = (lo | 32u8) - 97u8 + 10u8; }; - ch = (h << 4u8) | l; - }; - } - else { ch = e; };};};};};};}; - }; - }; - if (blen + 1u64 > cap) { - let ncap: u64 = cap * 2u64; - let nb: []u8 = alloc([], ncap)!; - let bi: u64 = 0u64; - for (bi < blen) { nb[bi] = dbuf[bi]; bi += 1u64; }; - dbuf = nb; - cap = ncap; - }; - dbuf[blen] = ch; - blen += 1u64; - }; - }; - pr.bytes = dbuf.ptr; - pr.nbytes = blen; - a.line += 1; continue; - }; - - // Generic instruction: 0/1/2 operands separated by ','. - // Find top-level comma. - let comma: i64 = -1i64; - let q: u64 = r0; - for (q < n) { - if (line[q] == ',') { - if (comma < 0i64) { comma = q: i64; }; - }; - q += 1u64; - }; - if (comma >= 0i64) { - let cu: u64 = comma: u64; - parseoperand(a, line, r0, cu, pr.from); - parseoperand(a, line + (cu + 1u64), 0u64, n - (cu + 1u64), pr.to); - } else { if (r0 < n) { - parseoperand(a, line, r0, n, pr.to); - };}; - - a.line += 1; - }; - return a.errs; -}; - -//ww:module-reset -// selfhost/cmd/w6a/asm.ww — port of cmd/w6a/asm.c. -// -// Encode the parsed aprog list into amd64 machine bytes, appending to -// asm_.text. Relocations for CALL/branch targets that resolve to -// externals are queued in asm_.relocs. -// -// Encoding subset matches what w6c emits — see cmd/w6a/asm.c for the -// authoritative list. Helpers (rcode/rhi/modrm/emitrex etc.) are -// fully ported; encode itself is still a stub pending the full -// switch over A_*. - -package w6a; - -import os; -import rt; -import opcodes; -import strings; - -// ---- text buffer growth ------------------------------------------------ - -export fn emitbyte(a: *asm_, b: u8) void = { - if (a.textlen + 1u64 > a.textcap) { - let nc: u64 = a.textcap; - if (nc == 0u64) { nc = 4096u64; }; - nc = nc * 2u64; - let nb: []u8 = alloc([], nc)!; - let i: u64 = 0u64; - for (i < a.textlen) { nb[i] = a.text[i]; i += 1u64; }; - a.text = nb.ptr; - a.textcap = nc; - }; - a.text[a.textlen] = b; - a.textlen += 1u64; -}; - -export fn emitu32(a: *asm_, v: u32) void = { - emitbyte(a, (v & 255u32): u8); - emitbyte(a, ((v >> 8u32) & 255u32): u8); - emitbyte(a, ((v >> 16u32) & 255u32): u8); - emitbyte(a, ((v >> 24u32) & 255u32): u8); -}; - -export fn addreloc(a: *asm_, off: u64, kind: i32, s: *asym, add: i64) void = { - let r: *areloc = alloc(areloc { off = off, section = 0, kind = kind, asy = s, addend = add, rnext = a.relocs })!; - a.relocs = r; -}; - -// Record a relocation that lives in the .data section. Used by -// DATAR to patch a 64-bit slot with a symbol's runtime VA. obj.ww -// separates these into .rela.data when emitting the .o. -export fn addrelocdata(a: *asm_, off: u64, kind: i32, s: *asym, add: i64) void = { - let r: *areloc = alloc(areloc { off = off, section = 1, kind = kind, asy = s, addend = add, rnext = a.relocs })!; - a.relocs = r; -}; - -// Append one byte to the writable .data buffer. Mirrors emitbyte -// but targets a.data instead of a.text. -export fn emitdatabyte(a: *asm_, b: u8) void = { - if (a.datalen + 1u64 > a.datacap) { - let nc: u64 = a.datacap; - if (nc == 0u64) { nc = 256u64; }; - nc = nc * 2u64; - let nb: []u8 = alloc([], nc)!; - let i: u64 = 0u64; - for (i < a.datalen) { nb[i] = a.data[i]; i += 1u64; }; - a.data = nb.ptr; - a.datacap = nc; - }; - a.data[a.datalen] = b; - a.datalen += 1u64; -}; - -// ---- register codes ---------------------------------------------------- - -// Low 3 bits of register encoding. -fn rcode(r: i32) i32 = { - if (r == D_AX) { return 0; }; if (r == D_CX) { return 1; }; - if (r == D_DX) { return 2; }; if (r == D_BX) { return 3; }; - if (r == D_SP) { return 4; }; if (r == D_BP) { return 5; }; - if (r == D_SI) { return 6; }; if (r == D_DI) { return 7; }; - if (r == D_R8) { return 0; }; if (r == D_R9) { return 1; }; - if (r == D_R10) { return 2; }; if (r == D_R11) { return 3; }; - if (r == D_R12) { return 4; }; if (r == D_R13) { return 5; }; - if (r == D_R14) { return 6; }; if (r == D_R15) { return 7; }; - if (r == D_X0) { return 0; }; if (r == D_X1) { return 1; }; - if (r == D_X2) { return 2; }; if (r == D_X3) { return 3; }; - if (r == D_X4) { return 4; }; if (r == D_X5) { return 5; }; - if (r == D_X6) { return 6; }; if (r == D_X7) { return 7; }; - if (r == D_X8) { return 0; }; if (r == D_X9) { return 1; }; - if (r == D_X10) { return 2; }; if (r == D_X11) { return 3; }; - if (r == D_X12) { return 4; }; if (r == D_X13) { return 5; }; - if (r == D_X14) { return 6; }; if (r == D_X15) { return 7; }; - return 0; -}; - -// 1 if r needs the REX high bit (R8..R15 or X8..X15). -fn rhi(r: i32) i32 = { - if (r >= D_R8) { if (r <= D_R15) { return 1; }; }; - if (r >= D_X8) { if (r <= D_X15) { return 1; }; }; - return 0; -}; - -fn isxmm(r: i32) bool = { - if (r >= D_X0) { if (r <= D_X15) { return true; }; }; - return false; -}; - -// ModR/M byte builder. -fn modrmbyte(mod: i32, reg: i32, rm: i32) u8 = { - return (((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7)): u8; -}; - -// REX prefix; W=1 for 64-bit operand size. -fn emitrex(a: *asm_, regbit: i32, rmbit: i32, w: i32) void = { - let b: u8 = 64u8; // 0x40 - if (w != 0) { b = b | 8u8; }; - if (regbit != 0) { b = b | 4u8; }; - if (rmbit != 0) { b = b | 1u8; }; - if (b != 64u8) { emitbyte(a, b); } - else { if (w != 0) { emitbyte(a, b); }; }; -}; - -// ModR/M + (optional) SIB + displacement for [base+disp]. -// Special-cases SP (needs SIB) and BP (forces explicit disp). -fn emitmodrmmem(a: *asm_, regfield: i32, base: i32, disp: i64) void = { - let rm: i32 = rcode(base); - let needsib: bool = (rm == 4); - let forceddisp: bool = false; - if (rm == 5) { if (disp == 0i64) { forceddisp = true; }; }; - - let mod: i32 = 2; - if (disp == 0i64) { - if (!forceddisp) { mod = 0; } - else { mod = 1; }; - } else { - if (disp >= -128i64) { if (disp <= 127i64) { mod = 1; }; }; - }; - - emitbyte(a, modrmbyte(mod, regfield, rm)); - if (needsib) { - emitbyte(a, 36u8); // 0x24: scale=0 idx=4(none) base=4 - }; - if (mod == 1) { - emitbyte(a, (disp: u64 & 255u64): u8); - } else { if (mod == 2) { - emitu32(a, disp: u32); - };}; -}; - -// reg→reg "src, dst" generic encoding (89 /r, 01 /r, etc.). -fn encoderr(a: *asm_, opcode: u8, src: i32, dst: i32) void = { - emitrex(a, rhi(src), rhi(dst), 1); - emitbyte(a, opcode); - emitbyte(a, modrmbyte(3, rcode(src), rcode(dst))); -}; - -// reg→mem(base, disp) (e.g. MOVQ src reg into mem; opcode = 0x89). -fn encoderm(a: *asm_, opcode: u8, srcreg: i32, base: i32, disp: i64) void = { - emitrex(a, rhi(srcreg), rhi(base), 1); - emitbyte(a, opcode); - emitmodrmmem(a, rcode(srcreg), base, disp); -}; - -// mem(base, disp) → reg (e.g. MOVQ mem into reg; opcode = 0x8B). -fn encodemr(a: *asm_, opcode: u8, dstreg: i32, base: i32, disp: i64) void = { - emitrex(a, rhi(dstreg), rhi(base), 1); - emitbyte(a, opcode); - emitmodrmmem(a, rcode(dstreg), base, disp); -}; - -// OPCODE /n imm32 reg form (e.g. ADDQ $imm, reg). -fn encoderiimm32(a: *asm_, opcode: u8, subop: i32, dst: i32, imm: i32) void = { - emitrex(a, 0, rhi(dst), 1); - emitbyte(a, opcode); - emitbyte(a, modrmbyte(3, subop, rcode(dst))); - emitu32(a, imm: u32); -}; - -// Unary on reg: F7 /n reg, etc. -fn encodeunary(a: *asm_, opcode: u8, subop: i32, dst: i32) void = { - emitrex(a, 0, rhi(dst), 1); - emitbyte(a, opcode); - emitbyte(a, modrmbyte(3, subop, rcode(dst))); -}; - -// SSE2 helpers. Plan 9 syntax: source first, destination second. -// For ADDSD-style ops we put dst in the reg field, src in r/m. -fn sserr(a: *asm_, prefix: u8, op2: u8, regop: i32, rmop: i32) void = { - if (prefix != 0u8) { emitbyte(a, prefix); }; - emitrex(a, rhi(regop), rhi(rmop), 0); - emitbyte(a, 15u8); // 0x0F - emitbyte(a, op2); - emitbyte(a, modrmbyte(3, rcode(regop), rcode(rmop))); -}; - -fn ssemrload(a: *asm_, prefix: u8, op2: u8, regop: i32, base: i32, disp: i64) void = { - if (prefix != 0u8) { emitbyte(a, prefix); }; - emitrex(a, rhi(regop), rhi(base), 0); - emitbyte(a, 15u8); - emitbyte(a, op2); - emitmodrmmem(a, rcode(regop), base, disp); -}; - -// REX.W variant of sse_rr (CVTTSD2SI / CVTSI2SD). -fn sserrw(a: *asm_, prefix: u8, op2: u8, regop: i32, rmop: i32) void = { - if (prefix != 0u8) { emitbyte(a, prefix); }; - emitrex(a, rhi(regop), rhi(rmop), 1); - emitbyte(a, 15u8); - emitbyte(a, op2); - emitbyte(a, modrmbyte(3, rcode(regop), rcode(rmop))); -}; - -// ---- label resolution / fixups ---------------------------------------- - -fn resolvelabel(a: *asm_, name: str) u64 = { - let s: *asym = a.syms; - for (s != nil) { - if (s.defined != 0) { if (strings.compare(s.name, name) == 0) { return s.addr; }; }; - s = s.snext; - }; - return 0u64; -}; - -fn labeldefined(a: *asm_, name: str) bool = { - let s: *asym = a.syms; - for (s != nil) { - if (s.defined != 0) { if (strings.compare(s.name, name) == 0) { return true; }; }; - s = s.snext; - }; - return false; -}; - -// ---- fixup helper ----------------------------------------------------- - -fn addfixup(a: *asm_, off: u64, label: str) void = { - let f: *afixup = alloc(afixup { off = off, label = label, fnext = a.fixups })!; - a.fixups = f; -}; - -fn isgpr(t: i32) bool = { - if (t >= D_AX) { if (t <= D_R15) { return true; }; }; - return false; -}; - -// `intern` lives in parse.ww — flat-scope concat lets us call it -// directly without an @symbol declaration here. - -// ---- encode ---------------------------------------------------------- - -export fn encode(a: *asm_) i32 = { - let p: *aprog = a.head; - for (p != nil) { - // Define any pending label at the current PC. - if (p.label.len > 0) { - let s: *asym = intern(a, p.label); - s.defined = 1; - s.istext = 1; - s.addr = a.textlen; - }; - let op: i32 = p.as_; - - if (op == A_NOP) { - p = p.link; continue; - }; - if (op == A_TEXT) { - let s: *asym = intern(a, p.to.asym); - s.defined = 1; - s.istext = 1; - s.isglobal = 1; - s.addr = a.textlen; - p = p.link; continue; - }; - if (op == A_DATA) { - let s: *asym = intern(a, p.to.asym); - s.defined = 1; - s.istext = 1; - s.isglobal = 1; - s.addr = a.textlen; - let i: u64 = 0u64; - for (i < p.nbytes) { emitbyte(a, p.bytes[i]); i += 1u64; }; - p = p.link; continue; - }; - if (op == A_DATAW) { - // Writable variant: bytes go into .data instead of - // .text. obj.ww emits the extra section conditionally - // on datalen > 0 so .o output stays byte-identical - // for inputs that don't use DATAW. - let s: *asym = intern(a, p.to.asym); - s.defined = 1; - s.isdata = 1; - s.isglobal = 1; - s.addr = a.datalen; - let i: u64 = 0u64; - for (i < p.nbytes) { emitdatabyte(a, p.bytes[i]); i += 1u64; }; - p = p.link; continue; - }; - if (op == A_DATAR) { - // DATAR slot+off(SB), target(SB) — record an - // R_X86_64_64 relocation at slot+off in .data - // pointing at target. Slot must already be defined - // by a prior DATAW. - let holder: *asym = intern(a, p.from.asym); - // #59: a DATAR slot with no prior DATAW definition must be - // a loud error, not a silently dropped relocation (the .o - // would ship a zero-filled pointer slot — a null pointer at - // link/runtime). Mirrors cmd/w6a/asm.c:363 (errs++ → main - // returns 1 before opening the output, so no .o is written). - if (holder.defined == 0 || holder.isdata == 0) { - let msg: str = "w6a: DATAR slot not yet defined as DATAW: "; - os.write(2, msg.ptr, msg.len: u64); - os.write(2, p.from.asym.ptr, p.from.asym.len: u64); - os.write(2, "\n".ptr, 1u64); - a.errs += 1; - p = p.link; continue; - }; - let target: *asym = intern(a, p.to.asym); - let reloff: u64 = holder.addr + p.from.offset: u64; - addrelocdata(a, reloff, 1 /* R_X86_64_64 */, - target, 0i64); - p = p.link; continue; - }; - if (op == A_RET) { - emitbyte(a, 195u8); // 0xC3 - p = p.link; continue; - }; - if (op == A_SYSCALL) { - emitbyte(a, 15u8); - emitbyte(a, 5u8); - p = p.link; continue; - }; - if (op == A_PUSHQ) { - if (rhi(p.to.atype) != 0) { emitbyte(a, 65u8); }; // 0x41 - emitbyte(a, (80 + rcode(p.to.atype)): u8); // 0x50 - p = p.link; continue; - }; - if (op == A_POPQ) { - if (rhi(p.to.atype) != 0) { emitbyte(a, 65u8); }; - emitbyte(a, (88 + rcode(p.to.atype)): u8); // 0x58 - p = p.link; continue; - }; - if (op == A_NEGQ) { encodeunary(a, 247u8, 3, p.to.atype); p = p.link; continue; }; - if (op == A_NOTQ) { encodeunary(a, 247u8, 2, p.to.atype); p = p.link; continue; }; - if (op == A_IDIVQ) { encodeunary(a, 247u8, 7, p.to.atype); p = p.link; continue; }; - if (op == A_DIVQ) { encodeunary(a, 247u8, 6, p.to.atype); p = p.link; continue; }; - if (op == A_CQO) { - emitbyte(a, 72u8); // REX.W (0x48) - emitbyte(a, 153u8); // 0x99 - p = p.link; continue; - }; - - if (op == A_MOVQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - let v: i64 = p.from.offset; - if (v >= -2147483648i64) { if (v <= 2147483647i64) { - encoderiimm32(a, 199u8, 0, tt, v: i32); - p = p.link; continue; - };}; - // movabs r64, imm64: REX.W B8+rd imm64 - emitrex(a, 0, rhi(tt), 1); - emitbyte(a, (184 + rcode(tt)): u8); - let k: i32 = 0; - for (k < 8) { - emitbyte(a, ((v: u64 >> (k: u64 * 8u64)) & 255u64): u8); - k += 1; - }; - p = p.link; continue; - };}; - if (isgpr(ft)) { if (isgpr(tt)) { - encoderr(a, 137u8, ft, tt); // 0x89 - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - encodemr(a, 139u8, tt, p.from.reg, p.from.offset); // 0x8B - p = p.link; continue; - };}; - if (isgpr(ft)) { if (tt == D_INDIR) { - encoderm(a, 137u8, ft, p.to.reg, p.to.offset); - p = p.link; continue; - };}; - if (ft == D_CONST) { if (tt == D_INDIR) { - emitrex(a, 0, rhi(p.to.reg), 1); - emitbyte(a, 199u8); - emitmodrmmem(a, 0, p.to.reg, p.to.offset); - emitu32(a, p.from.offset: u32); - p = p.link; continue; - };}; - if (ft == D_EXTERN) { if (isgpr(tt)) { - // RIP-relative load: 48 8B /r mod=00 rm=5 disp32 - emitrex(a, rhi(tt), 0, 1); - emitbyte(a, 139u8); - emitbyte(a, modrmbyte(0, rcode(tt), 5)); - let reloff: u64 = a.textlen; - emitu32(a, 0u32); - let s: *asym = intern(a, p.from.asym); - addreloc(a, reloff, 2, s, -4i64); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (tt == D_EXTERN) { - // RIP-relative store: 48 89 /r mod=00 rm=5 disp32 - emitrex(a, rhi(ft), 0, 1); - emitbyte(a, 137u8); - emitbyte(a, modrmbyte(0, rcode(ft), 5)); - let reloff: u64 = a.textlen; - emitu32(a, 0u32); - let s: *asym = intern(a, p.to.asym); - addreloc(a, reloff, 2, s, -4i64); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVQ shape\n".ptr, 27u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVB) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (isgpr(ft)) { if (tt == D_INDIR) { - emitrex(a, rhi(ft), rhi(p.to.reg), 0); - emitbyte(a, 136u8); // 0x88 - emitmodrmmem(a, rcode(ft), p.to.reg, p.to.offset); - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 0); - emitbyte(a, 138u8); // 0x8A - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVB shape\n".ptr, 27u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVW) { - // 16-bit MOV: 0x66 operand-size prefix + the 32-bit - // MOV opcodes 0x89 / 0x8B. No REX.W. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (isgpr(ft)) { if (tt == D_INDIR) { - emitbyte(a, 102u8); // 0x66 - emitrex(a, rhi(ft), rhi(p.to.reg), 0); - emitbyte(a, 137u8); // 0x89 - emitmodrmmem(a, rcode(ft), p.to.reg, p.to.offset); - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitbyte(a, 102u8); // 0x66 - emitrex(a, rhi(tt), rhi(p.from.reg), 0); - emitbyte(a, 139u8); // 0x8B - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVW shape\n".ptr, 27u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVZWQ) { - // MOVZX r64, r/m16 — 0F B7 /r with REX.W. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 1); - emitbyte(a, 15u8); - emitbyte(a, 183u8); // 0xB7 - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVZWQ shape\n".ptr, 29u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVSWQ) { - // MOVSX r64, r/m16 — 0F BF /r with REX.W. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 1); - emitbyte(a, 15u8); - emitbyte(a, 191u8); // 0xBF - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(ft), 1); - emitbyte(a, 15u8); - emitbyte(a, 191u8); // 0xBF - emitbyte(a, modrmbyte(3, rcode(tt), rcode(ft))); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVSWQ shape\n".ptr, 29u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVSBQ) { - // MOVSX r64, r/m8 — 0F BE /r with REX.W. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 1); - emitbyte(a, 15u8); - emitbyte(a, 190u8); // 0xBE - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(ft), 1); - emitbyte(a, 15u8); - emitbyte(a, 190u8); // 0xBE - emitbyte(a, modrmbyte(3, rcode(tt), rcode(ft))); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVSBQ shape\n".ptr, 29u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVZBQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 1); - emitbyte(a, 15u8); - emitbyte(a, 182u8); // 0xB6 - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVZBQ shape\n".ptr, 29u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVL) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (isgpr(ft)) { if (tt == D_INDIR) { - emitrex(a, rhi(ft), rhi(p.to.reg), 0); - emitbyte(a, 137u8); - emitmodrmmem(a, rcode(ft), p.to.reg, p.to.offset); - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 0); - emitbyte(a, 139u8); - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (isgpr(tt)) { - emitrex(a, rhi(ft), rhi(tt), 0); - emitbyte(a, 137u8); - emitbyte(a, modrmbyte(3, rcode(ft), rcode(tt))); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVL shape\n".ptr, 27u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVSXD) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(p.from.reg), 1); - emitbyte(a, 99u8); // 0x63 - emitmodrmmem(a, rcode(tt), p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (isgpr(tt)) { - emitrex(a, rhi(tt), rhi(ft), 1); - emitbyte(a, 99u8); // 0x63 - emitbyte(a, modrmbyte(3, rcode(tt), rcode(ft))); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVSXD shape\n".ptr, 29u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_MOVSD) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (isxmm(ft)) { if (isxmm(tt)) { - sserr(a, 242u8, 16u8, tt, ft); - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isxmm(tt)) { - ssemrload(a, 242u8, 16u8, tt, p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isxmm(ft)) { if (tt == D_INDIR) { - ssemrload(a, 242u8, 17u8, ft, p.to.reg, p.to.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVSD shape\n".ptr, 28u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_ADDSD) { sserr(a, 242u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_SUBSD) { sserr(a, 242u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_MULSD) { sserr(a, 242u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_DIVSD) { sserr(a, 242u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_UCOMISD) { sserr(a, 102u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTTSD2SI) { sserrw(a, 242u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTSI2SD) { sserrw(a, 242u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; - - if (op == A_MOVSS) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (isxmm(ft)) { if (isxmm(tt)) { - sserr(a, 243u8, 16u8, tt, ft); p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isxmm(tt)) { - ssemrload(a, 243u8, 16u8, tt, p.from.reg, p.from.offset); - p = p.link; continue; - };}; - if (isxmm(ft)) { if (tt == D_INDIR) { - ssemrload(a, 243u8, 17u8, ft, p.to.reg, p.to.offset); - p = p.link; continue; - };}; - os.write(2, "w6a: unsupported MOVSS shape\n".ptr, 28u64); - a.errs += 1; - p = p.link; continue; - }; - - if (op == A_ADDSS) { sserr(a, 243u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_SUBSS) { sserr(a, 243u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_MULSS) { sserr(a, 243u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_DIVSS) { sserr(a, 243u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_UCOMISS) { sserr(a, 0u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTTSS2SI) { sserrw(a, 243u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTSI2SS) { sserrw(a, 243u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTSD2SS) { sserr(a, 242u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; - if (op == A_CVTSS2SD) { sserr(a, 243u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; - - if (op == A_ADDQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 0, tt, p.from.offset: i32); // 0x81 - p = p.link; continue; - };}; - if (ft == D_CONST) { if (tt == D_INDIR) { - emitrex(a, 0, rhi(p.to.reg), 1); - emitbyte(a, 129u8); - emitmodrmmem(a, 0, p.to.reg, p.to.offset); - emitu32(a, p.from.offset: u32); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (tt == D_INDIR) { - encoderm(a, 1u8, ft, p.to.reg, p.to.offset); - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - encodemr(a, 3u8, tt, p.from.reg, p.from.offset); - p = p.link; continue; - };}; - encoderr(a, 1u8, ft, tt); - p = p.link; continue; - }; - - if (op == A_SUBQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 5, tt, p.from.offset: i32); - p = p.link; continue; - };}; - if (ft == D_CONST) { if (tt == D_INDIR) { - emitrex(a, 0, rhi(p.to.reg), 1); - emitbyte(a, 129u8); - emitmodrmmem(a, 5, p.to.reg, p.to.offset); - emitu32(a, p.from.offset: u32); - p = p.link; continue; - };}; - if (isgpr(ft)) { if (tt == D_INDIR) { - encoderm(a, 41u8, ft, p.to.reg, p.to.offset); // 0x29 - p = p.link; continue; - };}; - if (ft == D_INDIR) { if (isgpr(tt)) { - encodemr(a, 43u8, tt, p.from.reg, p.from.offset); // 0x2B - p = p.link; continue; - };}; - encoderr(a, 41u8, ft, tt); - p = p.link; continue; - }; - - if (op == A_ANDQ) { - // AND r/m64, imm32 — 0x81 /4 (REX.W). Without the - // D_CONST path encoderr would silently emit 0x21 - // with garbage reg fields. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 4, tt, p.from.offset: i32); - p = p.link; continue; - };}; - encoderr(a, 33u8, ft, tt); // 0x21 - p = p.link; continue; - }; - if (op == A_ORQ) { - // OR r/m64, imm32 — 0x81 /1 (REX.W). Mirrors ANDQ. - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 1, tt, p.from.offset: i32); - p = p.link; continue; - };}; - encoderr(a, 9u8, ft, tt); // 0x09 - p = p.link; continue; - }; - if (op == A_XORQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 6, tt, p.from.offset: i32); - p = p.link; continue; - };}; - encoderr(a, 49u8, ft, tt); // 0x31 - p = p.link; continue; - }; - if (op == A_IMULQ) { - emitrex(a, rhi(p.to.atype), rhi(p.from.atype), 1); - emitbyte(a, 15u8); - emitbyte(a, 175u8); // 0xAF - emitbyte(a, modrmbyte(3, rcode(p.to.atype), rcode(p.from.atype))); - p = p.link; continue; - }; - if (op == A_SHLQ) { encodeunary(a, 211u8, 4, p.to.atype); p = p.link; continue; }; // 0xD3 - if (op == A_SHRQ) { encodeunary(a, 211u8, 5, p.to.atype); p = p.link; continue; }; - // #136: SAR r/m64, CL — REX.W + D3 /7 (arithmetic right - // shift, sign-extends MSB; cstage twin cmd/w6a/asm.c). - if (op == A_SARQ) { encodeunary(a, 211u8, 7, p.to.atype); p = p.link; continue; }; - if (op == A_CMPQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_CONST) { if (isgpr(tt)) { - encoderiimm32(a, 129u8, 7, tt, p.from.offset: i32); - p = p.link; continue; - };}; - encoderr(a, 57u8, ft, tt); // 0x39 - p = p.link; continue; - }; - - if (op == A_LEAQ) { - let ft: i32 = p.from.atype; - let tt: i32 = p.to.atype; - if (ft == D_INDIR) { if (isgpr(tt)) { - encodemr(a, 141u8, tt, p.from.reg, p.from.offset); // 0x8D - p = p.link; continue; - };}; - if (ft == D_EXTERN) { if (isgpr(tt)) { - emitrex(a, rhi(tt), 0, 1); - emitbyte(a, 141u8); - emitbyte(a, modrmbyte(0, rcode(tt), 5)); - let reloff: u64 = a.textlen; - emitu32(a, 0u32); - let s: *asym = intern(a, p.from.asym); - addreloc(a, reloff, 2, s, -4i64); - p = p.link; continue; - };}; - p = p.link; continue; - }; - - if (op == A_CALL) { - let tt: i32 = p.to.atype; - if (tt == D_EXTERN) { - emitbyte(a, 232u8); // 0xE8 - let reloff: u64 = a.textlen; - emitu32(a, 0u32); - let s: *asym = intern(a, p.to.asym); - addreloc(a, reloff, 4, s, -4i64); - p = p.link; continue; - }; - if (tt == D_BRANCH) { - emitbyte(a, 232u8); - addfixup(a, a.textlen, p.to.asym); - emitu32(a, 0u32); - p = p.link; continue; - }; - if (isgpr(tt)) { - if (rhi(tt) != 0) { emitbyte(a, 65u8); }; - emitbyte(a, 255u8); // 0xFF - emitbyte(a, modrmbyte(3, 2, rcode(tt))); - p = p.link; continue; - }; - p = p.link; continue; - }; - - if (op == A_JMP) { - emitbyte(a, 233u8); // 0xE9 - addfixup(a, a.textlen, p.to.asym); - emitu32(a, 0u32); - p = p.link; continue; - }; - - // Conditional jumps. 0x0F + cc + rel32. - let cc: u8 = 0u8; - let isjcc: bool = true; - switch (op) { - case A_JE: cc = 132u8; // 0x84 - case A_JZ: cc = 132u8; - case A_JNE: cc = 133u8; - case A_JNZ: cc = 133u8; - case A_JL: cc = 140u8; - case A_JLE: cc = 142u8; - case A_JG: cc = 143u8; - case A_JGE: cc = 141u8; - case A_JB: cc = 130u8; - case A_JBE: cc = 134u8; - case A_JA: cc = 135u8; - case A_JAE: cc = 131u8; - case A_JP: cc = 138u8; // 0x8A, UCOMISD unordered (#97) - case: isjcc = false; - }; - if (isjcc) { - emitbyte(a, 15u8); - emitbyte(a, cc); - addfixup(a, a.textlen, p.to.asym); - emitu32(a, 0u32); - p = p.link; continue; - }; - - os.write(2, "w6a: unsupported opcode\n".ptr, 23u64); - a.errs += 1; - p = p.link; - }; - - // Second pass: patch fixups (forward label refs). - let f: *afixup = a.fixups; - for (f != nil) { - if (!labeldefined(a, f.label)) { - os.write(2, "w6a: undefined label '".ptr, 21u64); - let lbl: str = f.label; - os.write(2, lbl.ptr, lbl.len: u64); - os.write(2, "'\n".ptr, 2u64); - a.errs += 1; - f = f.fnext; - continue; - }; - let target: u64 = resolvelabel(a, f.label); - let rel: i64 = target: i64 - (f.off: i64 + 4i64); - let rel32: u32 = rel: u32; - a.text[f.off] = (rel32 & 255u32): u8; - a.text[f.off + 1u64] = ((rel32 >> 8u32) & 255u32): u8; - a.text[f.off + 2u64] = ((rel32 >> 16u32) & 255u32): u8; - a.text[f.off + 3u64] = ((rel32 >> 24u32) & 255u32): u8; - f = f.fnext; - }; - return a.errs; -}; - -//ww:module-reset -// selfhost/cmd/w6a/obj.ww — port of cmd/w6a/obj.c. -// -// Emit a tiny ELF64 relocatable object. Layout (in file order): -// [0] ELF header -// [1] Section .text (program bytes) -// [2] Section .rela.text (relocations) -// [3] Section .symtab -// [4] Section .strtab -// [5] Section .shstrtab -// [6] Section header table -// -// Symtab indices: 0 = STN_UNDEF, 1.. = our syms. Only GLOBAL symbols. - -package w6a; - -import os; -import opcodes; - -// Local wrappers around os.writeall's tagged return — collapse the -// (i64 | oserror) back to a boolean / int sentinel for the -// length-checked / fire-and-forget write patterns below. -fn wrn(fd: i32, p: *u8, n: u64, want: i64) bool = { - let r: (i64 | os.oserror) = os.writeall(fd, p, n); - match (r) { - case let v: i64 => return v == want; - case let e: os.oserror => return false; - }; - return false; -}; - -fn wrdrop(fd: i32, p: *u8, n: u64) void = { - let r: (i64 | os.oserror) = os.writeall(fd, p, n); - match (r) { - case let v: i64 => { }; - case let e: os.oserror => { }; - }; -}; - -// ---- ELF constants ---------------------------------------------------- -def ELFCLASS64: u8 = 2u8; -def ELFDATA2LSB: u8 = 1u8; -def EV_CURRENT_W: u32 = 1u32; -def ET_REL_W: u16 = 1u16; -def EM_X86_64_W: u16 = 62u16; - -def SHT_NULL_C: u32 = 0u32; -def SHT_PROGBITS_C: u32 = 1u32; -def SHT_SYMTAB_C: u32 = 2u32; -def SHT_STRTAB_C: u32 = 3u32; -def SHT_RELA_C: u32 = 4u32; - -def SHF_WRITE: u64 = 1u64; -def SHF_ALLOC: u64 = 2u64; -def SHF_EXECINSTR: u64 = 4u64; -def SHF_INFO_LINK: u64 = 64u64; // 0x40 - -def STB_GLOBAL: u8 = 1u8; -def STT_NOTYPE: u8 = 0u8; -def STT_OBJECT: u8 = 1u8; -def STT_FUNC: u8 = 2u8; - -// Sizes of fixed structures. -def EHDR_SZ: u64 = 64u64; -def SHDR_SZ: u64 = 64u64; -def SYM_SZ: u64 = 24u64; -def RELA_SZ: u64 = 24u64; - -// ---- LE byte writers (own the bytes — write into a *u8 + offset) ---- - -fn wru8(p: *u8, off: u64, v: u8) void = { p[off] = v; }; -fn wru16(p: *u8, off: u64, v: u16) void = { - p[off] = (v & 255u16): u8; - p[off + 1u64] = ((v >> 8u16) & 255u16): u8; -}; -fn wru32(p: *u8, off: u64, v: u32) void = { - p[off] = (v & 255u32): u8; - p[off + 1u64] = ((v >> 8u32) & 255u32): u8; - p[off + 2u64] = ((v >> 16u32) & 255u32): u8; - p[off + 3u64] = ((v >> 24u32) & 255u32): u8; -}; -fn wru64(p: *u8, off: u64, v: u64) void = { - wru32(p, off, (v & 4294967295u64): u32); - wru32(p, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); -}; - -// ---- growable byte buffer --------------------------------------------- - -type buf = struct { - p: *u8, - n: u64, - cap: u64, -}; - -fn bufinit(b: *buf) void = { - b.cap = 256u64; - b.n = 0u64; - let np: []u8 = alloc([], b.cap)!; - b.p = np.ptr; -}; - -fn bufgrow(b: *buf, need: u64) void = { - if (b.n + need <= b.cap) { return; }; - let nc: u64 = b.cap; - for (nc < b.n + need) { nc = nc * 2u64; }; - let np: []u8 = alloc([], nc)!; - let i: u64 = 0u64; - for (i < b.n) { np[i] = b.p[i]; i += 1u64; }; - b.p = np.ptr; - b.cap = nc; -}; - -fn bufputb(b: *buf, src: *u8, n: u64) void = { - bufgrow(b, n); - let i: u64 = 0u64; - for (i < n) { b.p[b.n + i] = src[i]; i += 1u64; }; - b.n += n; -}; - -// Write a NUL-terminated C-string copy of `s` into b. Returns offset -// where it started (suitable for st_name / sh_name fields). -fn bufputcstr(b: *buf, s: str) u32 = { - let off: u32 = b.n: u32; - bufgrow(b, s.len: u64 + 1u64); - let i: i32 = 0; - for (i < s.len) { b.p[b.n] = s[i]; b.n += 1u64; i += 1; }; - b.p[b.n] = 0u8; - b.n += 1u64; - return off; -}; - -// ---- emitelf --------------------------------------------------------- - -export fn emitelf(a: *asm_, fd: i32) i32 = { - let shstr: buf; bufinit(&shstr); - let str_: buf; bufinit(&str_); - let sym: buf; bufinit(&sym); - let rela: buf; bufinit(&rela); - let relad: buf; bufinit(&relad); - - // Index 0 = empty. - let zero: u8 = 0u8; - bufputb(&shstr, &zero, 1u64); - bufputb(&str_, &zero, 1u64); - - let hasdata: bool = a.datalen > 0u64; - let hasdatarelocs: bool = false; - let rscan: *areloc = a.relocs; - for (rscan != nil) { - if (rscan.section == 1) { hasdatarelocs = true; }; - rscan = rscan.rnext; - }; - - // Section indices (mirror cmd/w6a/obj.c): - // without data, without data-relocs: - // 1=.text 2=.rela.text 3=.symtab 4=.strtab 5=.shstrtab - // with data, no data-relocs: - // 1=.text 2=.rela.text 3=.data 4=.symtab 5=.strtab 6=.shstrtab - // with data + data-relocs: - // 1=.text 2=.rela.text 3=.data 4=.rela.data 5=.symtab - // 6=.strtab 7=.shstrtab - let SH_TEXT: u16 = 1u16; - let SH_DATA: u16 = 0u16; - let SH_RELAD: u16 = 0u16; - let SH_SYMTAB: u16 = 3u16; - if (hasdata) { - SH_DATA = 3u16; - if (hasdatarelocs) { - SH_RELAD = 4u16; - SH_SYMTAB = 5u16; - } else { - SH_SYMTAB = 4u16; - }; - }; - let SH_STRTAB: u16 = SH_SYMTAB + 1u16; - let SH_SHSTR: u16 = SH_STRTAB + 1u16; - - // Section name offsets. Append .data / .rela.data only when - // used so the .shstrtab buffer stays byte-identical for the - // no-DATAW case (test 991 byte-diff invariant). - let shntext: u32 = bufputcstr(&shstr, ".text"); - let shnrela: u32 = bufputcstr(&shstr, ".rela.text"); - let shndata: u32 = 0u32; - let shnrelad: u32 = 0u32; - if (hasdata) { shndata = bufputcstr(&shstr, ".data"); }; - if (hasdata) { if (hasdatarelocs) { - shnrelad = bufputcstr(&shstr, ".rela.data"); - };}; - let shnsymtab: u32 = bufputcstr(&shstr, ".symtab"); - let shnstrtab: u32 = bufputcstr(&shstr, ".strtab"); - let shnshstrtab: u32 = bufputcstr(&shstr, ".shstrtab"); - - // Symbol 0 — STN_UNDEF (24 zero bytes). - let zsym: [24]u8; - let zi: i32 = 0; - for (zi < 24) { zsym[zi] = 0u8; zi += 1; }; - bufputb(&sym, zsym.ptr, 24u64); - - // Build symbols. - let idx: i32 = 1; - let s: *asym = a.syms; - for (s != nil) { - let entry: [24]u8; - let ei: i32 = 0; - for (ei < 24) { entry[ei] = 0u8; ei += 1; }; - let stname: u32 = bufputcstr(&str_, s.name); - wru32(entry.ptr, 0u64, stname); - if (s.defined != 0) { - if (s.isdata != 0) { - wru8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_OBJECT)); - wru16(entry.ptr, 6u64, SH_DATA); - } else { - wru8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_FUNC)); - wru16(entry.ptr, 6u64, SH_TEXT); - }; - wru64(entry.ptr, 8u64, s.addr); - } else { - wru8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_NOTYPE)); - wru16(entry.ptr, 6u64, 0u16); - }; - bufputb(&sym, entry.ptr, 24u64); - s.idx = idx; - idx += 1; - s = s.snext; - }; - - // Build relocations — split into text vs data buffers. - let r: *areloc = a.relocs; - for (r != nil) { - let entry: [24]u8; - wru64(entry.ptr, 0u64, r.off); - let rinfo: u64 = (r.asy.idx: u64 << 32u64) | (r.kind: u64 & 4294967295u64); - wru64(entry.ptr, 8u64, rinfo); - wru64(entry.ptr, 16u64, r.addend: u64); - if (r.section == 1) { - bufputb(&relad, entry.ptr, 24u64); - } else { - bufputb(&rela, entry.ptr, 24u64); - }; - r = r.rnext; - }; - - // File offsets. - let off: u64 = EHDR_SZ; - let offtext: u64 = off; off = off + a.textlen; - let offrela: u64 = off; off = off + rela.n; - let offdata: u64 = off; if (hasdata) { off = off + a.datalen; }; - let offrelad: u64 = off; if (hasdata) { if (hasdatarelocs) { - off = off + relad.n; - };}; - let offsym: u64 = off; off = off + sym.n; - let offstr: u64 = off; off = off + str_.n; - let offshstr: u64 = off; off = off + shstr.n; - for ((off & 7u64) != 0u64) { off += 1u64; }; - let offshdr: u64 = off; - let NSECT: u16 = 6u16; - if (hasdata) { - if (hasdatarelocs) { NSECT = 8u16; } - else { NSECT = 7u16; }; - }; - - // ---- Ehdr ---- - let eh: [64]u8; - let i: i32 = 0; - for (i < 64) { eh[i] = 0u8; i += 1; }; - eh[0] = 127u8; // 0x7f - eh[1] = 'E'; - eh[2] = 'L'; - eh[3] = 'F'; - eh[4] = ELFCLASS64; - eh[5] = ELFDATA2LSB; - eh[6] = EV_CURRENT_W: u8; - wru16(eh.ptr, 16u64, ET_REL_W); - wru16(eh.ptr, 18u64, EM_X86_64_W); - wru32(eh.ptr, 20u64, EV_CURRENT_W); - wru64(eh.ptr, 24u64, 0u64); // e_entry - wru64(eh.ptr, 32u64, 0u64); // e_phoff - wru64(eh.ptr, 40u64, offshdr); // e_shoff - wru32(eh.ptr, 48u64, 0u32); // e_flags - wru16(eh.ptr, 52u64, 64u16); // e_ehsize - wru16(eh.ptr, 54u64, 0u16); // e_phentsize - wru16(eh.ptr, 56u64, 0u16); // e_phnum - wru16(eh.ptr, 58u64, 64u16); // e_shentsize - wru16(eh.ptr, 60u64, NSECT); // e_shnum - wru16(eh.ptr, 62u64, SH_SHSTR); // e_shstrndx - - if (!wrn(fd, eh.ptr, 64u64, 64i64)) { return -1; }; - if (a.textlen > 0u64) { - if (!wrn(fd, a.text, a.textlen, a.textlen: i64)) { return -1; }; - }; - if (rela.n > 0u64) { - if (!wrn(fd, rela.p, rela.n, rela.n: i64)) { return -1; }; - }; - if (hasdata) { - if (a.datalen > 0u64) { - if (!wrn(fd, a.data, a.datalen, a.datalen: i64)) { return -1; }; - }; - if (hasdatarelocs) { - if (relad.n > 0u64) { - if (!wrn(fd, relad.p, relad.n, relad.n: i64)) { return -1; }; - }; - }; - }; - if (sym.n > 0u64) { - if (!wrn(fd, sym.p, sym.n, sym.n: i64)) { return -1; }; - }; - if (str_.n > 0u64) { - if (!wrn(fd, str_.p, str_.n, str_.n: i64)) { return -1; }; - }; - if (shstr.n > 0u64) { - if (!wrn(fd, shstr.p, shstr.n, shstr.n: i64)) { return -1; }; - }; - - // Pad to 8 before shdrs. - let written: u64 = EHDR_SZ + a.textlen + rela.n + sym.n + str_.n + shstr.n; - if (hasdata) { - written += a.datalen; - if (hasdatarelocs) { written += relad.n; }; - }; - for ((written & 7u64) != 0u64) { - wrdrop(fd, &zero, 1u64); - written += 1u64; - }; - - // Section header table — 6 headers of 64 bytes each = 384 bytes. - let shbuf: [64]u8; - // SHT_NULL - let sn: i32 = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wrdrop(fd, shbuf.ptr, 64u64); - // .text - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shntext); - wru32(shbuf.ptr, 4u64, SHT_PROGBITS_C); - wru64(shbuf.ptr, 8u64, SHF_ALLOC | SHF_EXECINSTR); - wru64(shbuf.ptr, 24u64, offtext); - wru64(shbuf.ptr, 32u64, a.textlen); - wru64(shbuf.ptr, 48u64, 1u64); // sh_addralign - wrdrop(fd, shbuf.ptr, 64u64); - // .rela.text - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shnrela); - wru32(shbuf.ptr, 4u64, SHT_RELA_C); - wru64(shbuf.ptr, 8u64, SHF_INFO_LINK); - wru64(shbuf.ptr, 24u64, offrela); - wru64(shbuf.ptr, 32u64, rela.n); - wru32(shbuf.ptr, 40u64, SH_SYMTAB: u32); // sh_link - wru32(shbuf.ptr, 44u64, 1u32); // sh_info = .text idx - wru64(shbuf.ptr, 48u64, 8u64); - wru64(shbuf.ptr, 56u64, RELA_SZ); - wrdrop(fd, shbuf.ptr, 64u64); - if (hasdata) { - // .data - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shndata); - wru32(shbuf.ptr, 4u64, SHT_PROGBITS_C); - wru64(shbuf.ptr, 8u64, SHF_ALLOC | SHF_WRITE); - wru64(shbuf.ptr, 24u64, offdata); - wru64(shbuf.ptr, 32u64, a.datalen); - wru64(shbuf.ptr, 48u64, 8u64); // sh_addralign - wrdrop(fd, shbuf.ptr, 64u64); - if (hasdatarelocs) { - // .rela.data - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shnrelad); - wru32(shbuf.ptr, 4u64, SHT_RELA_C); - wru64(shbuf.ptr, 8u64, SHF_INFO_LINK); - wru64(shbuf.ptr, 24u64, offrelad); - wru64(shbuf.ptr, 32u64, relad.n); - wru32(shbuf.ptr, 40u64, SH_SYMTAB: u32); - wru32(shbuf.ptr, 44u64, SH_DATA: u32); // applies to .data - wru64(shbuf.ptr, 48u64, 8u64); - wru64(shbuf.ptr, 56u64, RELA_SZ); - wrdrop(fd, shbuf.ptr, 64u64); - }; - }; - // .symtab - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shnsymtab); - wru32(shbuf.ptr, 4u64, SHT_SYMTAB_C); - wru64(shbuf.ptr, 24u64, offsym); - wru64(shbuf.ptr, 32u64, sym.n); - wru32(shbuf.ptr, 40u64, SH_STRTAB: u32); // sh_link - wru32(shbuf.ptr, 44u64, 1u32); // sh_info = one local (STN_UNDEF) - wru64(shbuf.ptr, 48u64, 8u64); - wru64(shbuf.ptr, 56u64, SYM_SZ); - wrdrop(fd, shbuf.ptr, 64u64); - // .strtab - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shnstrtab); - wru32(shbuf.ptr, 4u64, SHT_STRTAB_C); - wru64(shbuf.ptr, 24u64, offstr); - wru64(shbuf.ptr, 32u64, str_.n); - wru64(shbuf.ptr, 48u64, 1u64); - wrdrop(fd, shbuf.ptr, 64u64); - // .shstrtab - sn = 0; - for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; - wru32(shbuf.ptr, 0u64, shnshstrtab); - wru32(shbuf.ptr, 4u64, SHT_STRTAB_C); - wru64(shbuf.ptr, 24u64, offshstr); - wru64(shbuf.ptr, 32u64, shstr.n); - wru64(shbuf.ptr, 48u64, 1u64); - wrdrop(fd, shbuf.ptr, 64u64); - - return 0; -}; - -//ww:module-reset -// selfhost/cmd/w6a/main.ww — port of cmd/w6a/main.c. -// -// w6a = amd64 assembler. Read .s, parse, encode, emit ELF .o. -// -// w6a_ww -o file.o file.s - -package main; - -import os; -import rt; -import strings; -import opcodes; -import lex; -import parse; -import asm; -import obj; - -fn cstreq(a: *u8, lit: str) bool = { - let n: u64 = lit.len: u64; - let i: u64 = 0u64; - for (i < n) { - let li: i32 = i: i32; - if (a[i] != lit[li]) { return false; }; - i += 1u64; - }; - if (a[i] != 0u8) { return false; }; - return true; -}; - -fn cstrlen(p: *u8) u64 = { - let n: u64 = 0u64; - for (p[n] != 0u8) { n += 1u64; }; - return n; -}; - -// pathstr — view a NUL-terminated *u8 as a str. Bridges argv-style -// callers to lib/os entrypoints (str post-task-#23). -fn pathstr(p: *u8) str = { - let r: str; - r.ptr = p; - r.len = cstrlen(p): i32; - return r; -}; - -// Slurp the whole file into a fresh buffer. -fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil, 0u64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - let n: i64 = 0i64; - match (szr) { - case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return nil, 0u64; }; - }; - let nz: u64 = n: u64; - let buf: []u8 = alloc([], nz + 1u64)!; - buf.len = (nz + 1u64): i32; - let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nz); - os.close(fd); - let got: i64 = 0i64; - match (rr) { - case let v: i64 => got = v; - case let e: os.oserror => return nil, 0u64; - }; - if (got != n) { return nil, 0u64; }; - buf[nz] = 0u8; - return buf.ptr, nz; -}; - -export fn main(argc: i32, argv: **u8) i32 = { - let src: *u8 = nil; - let out: *u8 = nil; - - let i: i32 = 1; - for (i < argc) { - let a: *u8 = argv[i]; - if (cstreq(a, "-o")) { - i += 1; - if (i >= argc) { - os.write(2, "w6a: -o requires arg\n".ptr, 20u64); - return 2; - }; - out = argv[i]; - } else { if (a[0u64] == 45u8) { - os.write(2, "w6a: unknown flag\n".ptr, 17u64); - return 2; - } else { - if (src != nil) { - os.write(2, "w6a: only one input\n".ptr, 19u64); - return 2; - }; - src = a; - }; }; - i += 1; - }; - - if (src == nil) { - os.write(2, "usage: w6a_ww -o file.o file.s\n".ptr, 30u64); - return 2; - }; - if (out == nil) { - os.write(2, "w6a: missing -o\n".ptr, 15u64); - return 2; - }; - - let buf: *u8; - let blen: u64; - buf, blen = slurp(src); - if (buf == nil) { - os.write(2, "w6a: cannot read input\n".ptr, 22u64); - return 1; - }; - - let s: asm_; - let nlen: u64 = cstrlen(src); - let view: str; - view.ptr = src; - view.len = nlen: i32; - let fname: str = strings.dup(view); - init(&s, fname, buf, blen); - - if (parse(&s) != 0) { return 1; }; - if (encode(&s) != 0) { return 1; }; - - // Open output for write. - let fd: i32 = os.open(pathstr(out), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { - os.write(2, "w6a: cannot open output\n".ptr, 23u64); - return 1; - }; - let rc: i32 = emitelf(&s, fd); - os.close(fd); - return rc; -}; - diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww deleted file mode 100644 index 3dce3b36..00000000 --- a/selfhost/cmd/w6c/main.combined.ww +++ /dev/null @@ -1,46365 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module strconv -// 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; -}; - -//ww:module math -// 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); -}; - -//ww:module math -// 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; -}; - -//ww:module strconv -// 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; -}; - -//ww:module strconv -// 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, -]; - -//ww:module ascii -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -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); -}; - -//ww:module strconv -// 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) -}; - -//ww:module strconv -// 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], -]; - -//ww:module strconv -// strconv — number↔string conversions. -// -// Mirrors Hare's strconv:: surface. The *tos functions return a -// `const str` view into a module-level buffer that is overwritten on -// the next call to the same function; callers must copy the bytes if -// they need to outlive the next invocation. See [[strings.dup]] to -// duplicate. Matches Hare's strconv::*tos semantics. - -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(""); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -import os; -import strconv; - -// ---- 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. -export 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 ------------------------------------------------------------- - -export 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 - usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias - // in `str` (M1 #22); "" otherwise - imported: i32, // M1 #22: decl reached via `//ww:module ` import - // boundary (vs root/primary); gates root-only bare main -}; - -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="", usepath="", imported=0})!; - 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); -}; - -//ww:module syntax -// lib/ww/syntax/decl.ww — declaration parsing, split out of parse.ww. - -package syntax; - -import os; -import strings; - -// `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; - // M1 #22: accumulate the full dotted import path (n.usepath) for the - // checker's path-keyed module match; n.str stays the leaf alias the - // user writes (`utf8.x`). - let leaf: str; - expectident(p, &leaf); - let path: str = leaf; - for (p.curkind == tkind.TK_DOT) { - advance(p); // past `.` - expectident(p, &leaf); - path = strings.concat(path, ".", leaf); - }; - n.str = leaf; - n.usepath = path; - 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); - // A value-LESS `def X: T;` is an interface prototype (an aggregate- - // init `.wwi` def whose DATA lives in the defining package — BUG-2 / - // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; - // the checker fold pass and cgen emit_defs/emit_lets already guard - // the rhs==nil shape. - if (accepttok(p, tkind.TK_ASSIGN)) { - 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; -}; - - -//ww:module syntax -// lib/ww/syntax/expr.ww — expression parsing, split out of parse.ww. - -package syntax; - -import os; - -// 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; - // Parse each element BEFORE the RPAREN-break so `(a,)` - // (a single elem + trailing comma) is a loud parse error; - // a trailing comma is legal only after >=2 elems. Mirror - // cstage cmd/wcc/parse.c:552-558 loop order. - for (true) { - let en: *node = parseexpr(p); - tail.next = en; - tail = en; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { 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; -}; - -// #76: flatten a qualified `pkg.Type` N_DOT chain into its source-order -// dotted string ("a.b.C"), byte-identical to cstage parsetype's aprintf -// accumulation (cmd/wcc/parse.c) and to flattendotstr in parse.c. -fn flattendotstr(n: *node) str = { - if (n.kind == nkind.N_IDENT) { return n.str; }; - return joindotted(flattendotstr(n.lhs), n.str); -}; - -// #76: an N_DOT chain qualifies as a type path only when it is rooted at -// a bare IDENT and every component is an identifier. cstage folds only -// peek==TK_IDENT dots in parseprimary, so its chain is inherently -// all-ident; ww folds ALL dots (incl tuple `t.0`) in parsepostfix, so we -// guard out numeric-first-byte components here to restore exact symmetry. -fn isqualpath(n: *node) bool = { - if (n.kind != nkind.N_DOT) { return false; }; - let cur: *node = n; - for (cur.kind == nkind.N_DOT) { - if (cur.str.len < 1) { return false; }; - let c0: u8 = cur.str[0]; - if (c0 >= '0') { if (c0 <= '9') { return false; }; }; - cur = cur.lhs; - }; - return cur.kind == nkind.N_IDENT; -}; - -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; - }; - // #76: qualified struct literal `pkg.Type{...}`. cstage folds - // the dotted chain in parseprimary (peek) and hits `{` there; - // the ww parser has no token-peek and folds dots HERE, so the - // struct-lit handoff lives here too. Fire only on an all-ident - // N_DOT chain (isqualpath); bare `Foo{` stays in parseprimary. - // Build the literal IN-LOOP and `continue` (req a) so trailing - // ops (`.f`, `[i]`, `()`, `as T`) still apply, matching cstage's - // return-to-outer-parsepostfix. The flattened N_TNAME (req c: - // source order) lets the checker's existing N_TNAME arm resolve. - if (p.curkind == tkind.TK_LBRACE) { - if (isqualpath(cur)) { - let tn: *node = newnode(nkind.N_TNAME, pf, pl, pc); - tn.str = flattendotstr(cur); - advance(p); - let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); - s.lhs = tn; - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACE) { - if (p.curkind == tkind.TK_EOF) { break; }; - 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; - cur = s; - 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; -}; - - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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; -}; - -export 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, - // a `//ww:module ` directive was seen in the last skipped run; - // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). - modpathset: i32, - modpath: str, - // a `//ww:module-reset ` directive was seen; the next TK_MODRESET - // carries this dotted path so the sep primary body mangles on the path, - // not its leaf clause (#57). - modresetpathset: i32, - modresetpath: str, -}; - -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; - l.modpathset = 0; - l.modresetpathset = 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 pre: str = "ww:module"; - let di: i32 = 0; - let matched: bool = true; - for (di < pre.len) { - if (lpeek(l, di: u64) != pre[di]: i32) { - matched = false; break; - }; - di += 1; - }; - if (matched) { - let nx: i32 = lpeek(l, pre.len: u64); - if (nx == '-') { - let rest: str = "-reset"; - let rj: i32 = 0; - let rm: bool = true; - for (rj < rest.len) { - if (lpeek(l, (pre.len + rj): u64) - != rest[rj]: i32) { - rm = false; break; - }; - rj += 1; - }; - if (rm) { - let af: i32 = lpeek(l, - (pre.len + rest.len): u64); - if (af == '\n') { l.modreset = 1; } - else { if (af < 0) { l.modreset = 1; } - else { if (af == ' ' || af == '\t') { - // `//ww:module-reset ` — sep - // primary body tagged by its full - // dotted import path (#57). - let k: u64 = - (pre.len + rest.len): u64; - for (true) { - let sc: i32 = lpeek(l, k); - if (sc == ' ' || sc == '\t') { - k += 1u64; continue; - }; - break; - }; - let s0: u64 = k; - for (true) { - let pc: i32 = lpeek(l, k); - if (pc < 0) { break; }; - if (pc == '\n' || pc == '\r' - || pc == ' ' - || pc == '\t') { - break; - }; - k += 1u64; - }; - l.modreset = 1; - if (k > s0) { - let view: str; - view.ptr = - l.src + l.lpos + s0; - view.len = (k - s0): i32; - l.modresetpath = - strings.dup(view); - l.modresetpathset = 1; - }; - }; }; }; - }; - } else { if (nx == ' ' || nx == '\t') { - // `//ww:module ` — M1 import boundary. - let k: u64 = pre.len: u64; - for (true) { - let sc: i32 = lpeek(l, k); - if (sc == ' ' || sc == '\t') { - k += 1u64; continue; - }; - break; - }; - let s0: u64 = k; - for (true) { - let pc: i32 = lpeek(l, k); - if (pc < 0) { break; }; - if (pc == '\n' || pc == '\r' - || pc == ' ' || pc == '\t') { - break; - }; - k += 1u64; - }; - if (k > s0) { - let view: str; - view.ptr = l.src + l.lpos + s0; - view.len = (k - s0): i32; - l.modpath = strings.dup(view); - l.modpathset = 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); - // path-carrying reset → text=path (#57); bare reset → text empty - if (l.modresetpathset != 0) { - l.modresetpathset = 0; - out.text = l.modresetpath; - }; - return; - }; - if (l.modpathset != 0) { - l.modpathset = 0; - emitsimple(&start, tkind.TK_MODPATH, out); - out.text = l.modpath; - 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); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -// Sibling files (expr, stmt, decl) are the same `package syntax`, -// auto-resolved via task #22 dir-enum of lib/ww/syntax/. -import os; -import strings; - -export 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, - // M1 #22: active `//ww:module ` dotted import path. While set, - // decls stamp nmod=pathmod and imported=1, and the in-file `package` - // clause is an assertion. "" means inactive (root/primary). - pathmod: str, - // #57: active `//ww:module-reset ` dotted path. Mangles decls on - // the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main - // rule stay intact), and the in-file `package` clause asserts (leaf == - // last component) instead of overwriting curmod. "" means inactive. - resetmod: 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; - p.pathmod = ""; - p.resetmod = ""; - 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"); - if (p.pathmod.len != 0 || p.resetmod.len != 0) { - // M1 #22: while an import path is active the in-file - // `package` clause is an ASSERTION — its leaf must - // equal the path's last component; it does NOT - // overwrite the path-derived module. #57 extends this - // to the sep primary-reset path (resetmod): the dotted - // reset path is authoritative, the clause asserts. - let active: str = p.pathmod; - if (p.pathmod.len == 0) { active = p.resetmod; }; - let (pre, post) = strings.rcut(active, "."); - let last: str = post; - if (post.len == 0) { last = active; }; - if (strings.compare(name, last) != 0) { - errmsg(p, "package does not match import path"); - }; - } else { - p.curmod = name; - }; - continue; - }; - // `//ww:module ` — M1 #22 import boundary. The following - // file's decls mangle on the full dotted import path, not the - // leaf `package` clause, and are flagged imported (gates the - // root-only bare-`main` rule, #32). - if (p.curkind == tkind.TK_MODPATH) { - p.pathmod = p.curtext; - p.curmod = p.curtext; - p.resetmod = ""; - advance(p); - 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) { - // #57: a path-carrying reset (sep primary body) mangles decls - // on the dotted path so definer == importer, but leaves - // imported==0 (curmod set, pathmod "") so -c primary-ness and - // the #32 bare-main rule are intact; the body's `package` - // clause then asserts (resetmod). A bare reset is the - // root/package-less boundary: curmod "" → bare, today's path. - let rp: str = p.curtext; - advance(p); - p.pathmod = ""; - if (rp.len != 0) { - p.curmod = rp; - p.resetmod = rp; - } else { - let empty: str; - empty.ptr = nil; - empty.len = 0; - p.curmod = empty; - p.resetmod = ""; - }; - 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) { - // M1 #22: flag decls reached via an import-path boundary - // (gates the root-only bare-`main` rule, #32). - if (p.pathmod.len != 0) { d.imported = 1; }; - if (head == nil) { - head = d; - tail = d; - } else { - tail.next = d; - tail = d; - }; - }; - }; - f.list = head; - return f; -}; - -//ww:module syntax -// lib/ww/syntax/stmt.ww — statement parsing, split out of parse.ww. - -package syntax; - -import os; - -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; -}; - - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -export 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, -}; - -export 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; - -export 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 (a same-name `import X;` -// SK_USE shadowing a struct X declared in another module), the resolver -// needs the type entry — the struct's mod may differ from the leaf 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; -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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. - -export 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_MODPATH = 88, // `//ww:module ` driver import - // boundary; decls mangle on the path, not the - // leaf `package` clause (M1 #22; cstage twin) - TK_LAST = 89, -}; - -// ---- 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, -}; - -export 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_MODPATH: return "//ww:module"; - - 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'); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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. -export 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 ----------------------------------------- - -export type tfield = struct { - name: str, - type_: *tinfo, - offset: u64, - tnext: *tfield, -}; - -export 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). -export type ttupleelem = struct { - type_: *tinfo, - offset: u64, - tnext: *ttupleelem, -}; - -export 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. -export 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 ------------------------------- - -export 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 -}; - -//ww:module-reset -// 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 syntax; -import strconv; - -type checker = struct { - tc: *syntax.tctx, - top: *syntax.scope, - cur: *syntax.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: *syntax.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: *syntax.node, // N_FILE root; used by checkmoduleshadow - // to consult the declaring source's own - // `use` directives. - allococtx: *syntax.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: *syntax.tinfo, n: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.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 = { - syntax.scopedefine(c.top, "void", syntax.skind.SK_TYPE, c.tc.tyvoid, nil); - syntax.scopedefine(c.top, "bool", syntax.skind.SK_TYPE, c.tc.tybool, nil); - syntax.scopedefine(c.top, "rune", syntax.skind.SK_TYPE, c.tc.tyrune, nil); - syntax.scopedefine(c.top, "i8", syntax.skind.SK_TYPE, c.tc.tyi8, nil); - syntax.scopedefine(c.top, "i16", syntax.skind.SK_TYPE, c.tc.tyi16, nil); - syntax.scopedefine(c.top, "i32", syntax.skind.SK_TYPE, c.tc.tyi32, nil); - syntax.scopedefine(c.top, "i64", syntax.skind.SK_TYPE, c.tc.tyi64, nil); - syntax.scopedefine(c.top, "u8", syntax.skind.SK_TYPE, c.tc.tyu8, nil); - syntax.scopedefine(c.top, "u16", syntax.skind.SK_TYPE, c.tc.tyu16, nil); - syntax.scopedefine(c.top, "u32", syntax.skind.SK_TYPE, c.tc.tyu32, nil); - syntax.scopedefine(c.top, "u64", syntax.skind.SK_TYPE, c.tc.tyu64, nil); - syntax.scopedefine(c.top, "int", syntax.skind.SK_TYPE, c.tc.tyint, nil); - syntax.scopedefine(c.top, "uint", syntax.skind.SK_TYPE, c.tc.tyuint, nil); - syntax.scopedefine(c.top, "uintptr", syntax.skind.SK_TYPE, c.tc.tyuintptr, nil); - syntax.scopedefine(c.top, "f32", syntax.skind.SK_TYPE, c.tc.tyf32, nil); - syntax.scopedefine(c.top, "f64", syntax.skind.SK_TYPE, c.tc.tyf64, nil); - syntax.scopedefine(c.top, "str", syntax.skind.SK_TYPE, c.tc.tystr, nil); - syntax.scopedefine(c.top, "never", syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0); - tnvoid.str = "void"; - let bang: *syntax.node = syntax.newnode(syntax.nkind.N_TBANG, empty, 0, 0); - bang.lhs = tnvoid; - let nomemdecl: *syntax.node = syntax.newnode(syntax.nkind.N_TYPEDECL, empty, 0, 0); - nomemdecl.str = "nomem"; - nomemdecl.lhs = bang; - syntax.scopedefine(c.top, "nomem", syntax.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. - syntax.scopedefine(c.top, "len", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "alloc", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "free", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "append", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "delete", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "insert", syntax.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. - syntax.scopedefine(c.top, "size", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "align", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "offset", syntax.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: *syntax.node, d: *syntax.node) str = { - let empty: str; - if (d == nil) { return empty; }; - if (d.nmod.len == 0) { return empty; }; - if (file == nil) { return empty; }; - let u: *syntax.node = file.list; - for (u != nil) { - // M1 #22: a decl is imported iff some `use` directive's full - // dotted import path equals the decl's module (now the path). - // Single-level packages have usepath == leaf so this is - // unchanged; nested (`encoding.utf8`) match here, not on leaf. - if (u.kind == syntax.nkind.N_USE) { - if (syntax.streq(u.usepath, d.nmod)) { return d.nmod; }; - }; - u = u.next; - }; - return empty; -}; - -// usepath — map a `use` alias (leaf bareword the user writes, `utf8`) -// to the full dotted import path it binds (`encoding.utf8`), for the -// module-qualified resolution and codegen hint (M1 #22). Single-level -// packages have usepath == alias so the result is unchanged. The -// current tree has one occurrence per leaf, so the map is unambiguous. -fn usepathfor(file: *syntax.node, alias: str) str = { - let empty: str; - if (file == nil) { return empty; }; - if (alias.len == 0) { return empty; }; - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_USE) { - if (syntax.streq(u.str, alias)) { - if (u.usepath.len != 0) { return u.usepath; }; - return u.str; - }; - }; - u = u.next; - }; - return empty; -}; - -// modkeyfor — usepathfor with leaf-alias fallback: the module key for a -// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). -fn modkeyfor(c: *checker, alias: str) str = { - let mk: str = usepathfor(c.file, alias); - if (mk.len == 0) { return alias; }; - return mk; -}; - -// 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: *syntax.node, modtag: str, name: str) bool = { - if (file == nil) { return false; }; - if (name.len == 0) { return false; }; - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.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 (syntax.streq(u.nmod, u.usepath)) { - 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 (syntax.streq(um, modtag)) { m = true; }; }; - if (m) { - if (syntax.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: *syntax.scope = c.cur; - for (s != nil) { - let r: *syntax.sym = syntax.scopelookuplocal(s, name); - if (r != nil) { - if (r.skind == syntax.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: *syntax.node, d: *syntax.node) void = { - if (d == nil) { return; }; - let k: syntax.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 == syntax.nkind.N_USE) { - // M1 #22: self-import ⟺ the imported path equals the use's own - // (owning) module path. Compares paths, not leaves. - if (mod.len != 0 && syntax.streq(d.usepath, 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: *syntax.sym = syntax.scopelookuplocal(c.top, nm); - if (prev != nil) { if (prev.skind != syntax.skind.SK_USE) { - prev.use_alias = 1i32; - return; - }; }; - syntax.scopedefine(c.top, nm, syntax.skind.SK_USE, nil, d); return; - }; - if (k == syntax.nkind.N_DEF) { installtop(c, d, nm, mod, syntax.skind.SK_DEF, "def"); return; }; - if (k == syntax.nkind.N_TYPEDECL) { installtop(c, d, nm, mod, syntax.skind.SK_TYPE, "type"); return; }; - if (k == syntax.nkind.N_FNDECL) { installtop(c, d, nm, mod, syntax.skind.SK_FN, "fn"); return; }; - if (k == syntax.nkind.N_LET) { installtop(c, d, nm, mod, syntax.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: *syntax.node, nm: str, mod: str, k: syntax.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: *syntax.sym = syntax.scopelookuplocal(c.top, nm); - if (puse != nil) { if (puse.skind == syntax.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 (syntax.scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; }; - let prev: *syntax.sym = syntax.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: *syntax.node, elems: *syntax.node, - define: bool, what: str) void = { - let b: *syntax.node = binds; - let pt: *syntax.node = elems; - for (b != nil) { - let et: *syntax.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); - syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b); - }; - }; - if (b.type_ == nil) { - let src: *syntax.node = b.lhs; - if (src == nil) { src = et; }; - if (src != nil) { - let ti: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.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 == syntax.nkind.N_MATCH) { checkmatchexhaust(c, n); }; - if (k == syntax.nkind.N_TRYPROP) { checktryprop(c, n); }; - if (k == syntax.nkind.N_TRYUNW) { checktryprop(c, n); }; - if (k == syntax.nkind.N_TYPETEST) { checkisas(c, n); }; - if (k == syntax.nkind.N_TYPEASSERT) { checkisas(c, n); }; - if (k == syntax.nkind.N_LET) { checkletassign(c, n); }; - if (k == syntax.nkind.N_RETURN) { checkretassign(c, n); }; - - // `use IDENT;` — name is a module label, not a free ident. - if (k == syntax.nkind.N_USE) { return; }; - - if (k == syntax.nkind.N_IDENT) { - let nm: str = n.str; - if (nm.len > 0) { - let s: *syntax.sym = syntax.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 == syntax.nkind.N_TNAME) { - let nm: str = n.str; - if (nm.len > 0) { - let s: *syntax.sym = syntax.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: *syntax.sym = syntax.scopelookup(c.cur, head); - if (m != nil) { - let leaf: str; - leaf.ptr = nm.ptr + (dot + 1): u64; - leaf.len = nm.len - (dot + 1); - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 == syntax.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: *syntax.node = nil; - let it: *syntax.node = exprtype(c, n.lhs, nil); - if (it != nil) { - let et: *syntax.node = nil; - if (it.kind == syntax.nkind.N_TSLICE) { et = it.lhs; }; - if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; }; - if (et != nil) { if (et.kind == syntax.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: *syntax.node = nil; - let it: *syntax.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 == syntax.nkind.N_TSLICE) { et = it.lhs; }; - if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; }; - }; - if (et != nil) { - let bn: *syntax.node = syntax.newnode(syntax.nkind.N_LET, n.file, n.line, n.col); - bn.str = bnm; - bn.lhs = et; - syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, bn); - } else { - syntax.scopedefine(c.cur, bnm, syntax.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 == syntax.nkind.N_MCASE) { - if (n.lhs != nil) { resolvewalk(c, n.lhs); }; - let outer: *syntax.scope = c.cur; - c.cur = syntax.newscope(outer); - let nm: str = n.str; - if (nm.len > 0) { - checkmoduleshadow(c, nm, "binding"); - syntax.scopedefine(c.cur, nm, syntax.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 == syntax.nkind.N_BLOCK) { - let outer: *syntax.scope = c.cur; - c.cur = syntax.newscope(outer); - let m: *syntax.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 == syntax.nkind.N_MLET) { - if (n.rhs != nil) { resolvewalk(c, n.rhs); }; - let pt: *syntax.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: *syntax.node = exprtype(c, n.rhs, nil); - if (rty != nil) { if (rty.kind == syntax.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 == syntax.nkind.N_MASSIGN) { - if (n.rhs != nil) { resolvewalk(c, n.rhs); }; - let pt: *syntax.node = nil; - // #242: consume the rhs tuple type for ANY rhs (see N_MLET). - if (n.rhs != nil) { - let rty: *syntax.node = exprtype(c, n.rhs, nil); - if (rty != nil && rty.kind == syntax.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: *syntax.node = n.list; - for (l != nil) { resolvewalk(c, l); l = l.next; }; - stamptuplebinds(c, n.list, pt, false, ""); - return; - }; - - if (k == syntax.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: *syntax.node = exprtype(c, n, nil); - return; - }; - - if (k == syntax.nkind.N_FIELD) { - if (n.lhs != nil) { resolvewalk(c, n.lhs); }; - return; - }; - - if (k == syntax.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: *syntax.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 == syntax.nkind.N_TNAME || k == syntax.nkind.N_TPTR || - k == syntax.nkind.N_TSLICE || k == syntax.nkind.N_TCHAN || - k == syntax.nkind.N_TBANG || k == syntax.nkind.N_TARRAY || - k == syntax.nkind.N_TFN || k == syntax.nkind.N_TSTRUCT || - k == syntax.nkind.N_TTUPLE || k == syntax.nkind.N_TTAGGED || - k == syntax.nkind.N_TENUM) { - if (n.type_ == nil) { - let ti: *syntax.tinfo = tinfofornode(c, n); - if (ti != nil) { n.type_ = ti: *void; }; - }; - }; - - if (k == syntax.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 == syntax.nkind.N_LET) { - let nm: str = n.str; - if (nm.len > 0) { - checkmoduleshadow(c, nm, "let"); - let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); - // catB-22: flag a `const` binding so checkassign can - // reject a later reassignment. The parser stamps - // n.op = TK_CONST (parse/stmt.ww). Mirror cstage - // cmd/wcc/check.c:2408. - if (s != nil) { - if (n.op == syntax.tkind.TK_CONST) { s.is_const = 1i32; }; - }; - }; - }; - - // #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 == syntax.nkind.N_LET) { coercefloatlit(c, n.rhs, n.lhs); }; - if (k == syntax.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 == syntax.nkind.N_CALL) { desugarcallargs(c, n); }; - if (k == syntax.nkind.N_ASSIGN) { checkassign(c, n); }; - - if (k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT || - k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT || - k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE || - k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT || - k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN || - k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL || - k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST || - k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT || - k == syntax.nkind.N_RECV || - k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD || - k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP || - k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST || - k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD || - k == syntax.nkind.N_MATCH) { - let _t: *syntax.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: *syntax.node) *syntax.node = { - if (n == nil) { return nil; }; - if (n.kind == syntax.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: *syntax.node) *syntax.sym = { - if (n == nil) { return nil; }; - if (n.kind != syntax.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: *syntax.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 = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 = syntax.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 != syntax.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: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, nm); - if (sm != nil) { s = sm; }; - }; - }; - }; - if (s == nil) { return nil; }; - if (s.skind != syntax.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: *syntax.node) *syntax.node = { - let cur: *syntax.node = n; - for (cur != nil) { - if (cur.kind != syntax.nkind.N_TNAME) { return cur; }; - let s: *syntax.sym = aliassym(c, cur); - if (s == nil) { return cur; }; - let body: *syntax.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: *syntax.node, b: *syntax.node) bool = { - let aa: *syntax.node = unwrapbang(a); - let bb: *syntax.node = unwrapbang(b); - if (aa == nil) { return bb == nil; }; - if (bb == nil) { return false; }; - // Identity fast-path, mirroring cstage type_eq's first line - // (cmd/wcc/type.c:250 `if (a == b) return 1`). Enum (and struct/ - // array) type nodes are shared from their decl, so two references to - // the SAME `os.flag` resolve to one N_TENUM node; without this the - // catch-all below returns false and the #26 reject fires on a - // same-enum binop like `os.flag.WRONLY | os.flag.CREATE` (w6l), which - // cstage accepts via this identity check. - if (aa == bb) { return true; }; - if (aa.kind != bb.kind) { return false; }; - let k: syntax.nkind = aa.kind; - if (k == syntax.nkind.N_TNAME) { - if (syntax.streq(aa.str, bb.str)) { return true; }; - let sa: *syntax.sym = aliassym(c, aa); - let sb: *syntax.sym = aliassym(c, bb); - if (sa != nil && sa == sb) { return true; }; - return false; - }; - if (k == syntax.nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.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: *syntax.node = aa.list; - let pb: *syntax.node = bb.list; - for (pa != nil) { - if (pb == nil) { return false; }; - let ac: bool = syntax.streq(pa.str, "..."); - let bc: bool = syntax.streq(pb.str, "..."); - if (ac != bc) { return false; }; - if (!ac) { - let va: bool = pa.op == syntax.tkind.TK_ELLIPSIS; - let vb: bool = pb.op == syntax.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 == syntax.nkind.N_TTUPLE) { - let pa: *syntax.node = aa.list; - let pb: *syntax.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 == syntax.nkind.N_TTAGGED) { - let pa: *syntax.node = aa.list; - let pb: *syntax.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 == syntax.tkind.TK_ELLIPSIS) { return false; }; - if (pb.op == syntax.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: *syntax.node) bool = { - if (v == nil) { return false; }; - if (v.kind == syntax.nkind.N_TBANG) { return true; }; - if (v.kind == syntax.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: *syntax.sym = syntax.scopelookup(c.cur, v.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_TYPE) { - if (s.decl != nil) { - if (s.decl.lhs != nil) { - if (s.decl.lhs.kind == syntax.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: *syntax.node) bool = { - let v: *syntax.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: *syntax.node, v: *syntax.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: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind == syntax.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: *syntax.sym = syntax.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 == syntax.nkind.N_DOT) { - if (e.lhs == nil) { return nil; }; - if (e.lhs.kind != syntax.nkind.N_IDENT) { return nil; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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) *syntax.node = { - let n: *syntax.node = syntax.newnode(syntax.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 (syntax.streq(nm, "void")) { return 0i64; }; - if (syntax.streq(nm, "bool")) { return 1i64; }; - if (syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; }; - if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; }; - if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; }; - if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64")) { return 8i64; }; - if (syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.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 (syntax.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: *syntax.node) i64 = { - if (t == nil) { return 1i64; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TBANG) { return astalign(c, t.lhs); }; - if (k == syntax.nkind.N_TPTR) { return 8i64; }; - if (k == syntax.nkind.N_TSLICE) { return 8i64; }; - if (k == syntax.nkind.N_TCHAN) { return 8i64; }; - if (k == syntax.nkind.N_TFN) { return 8i64; }; - if (k == syntax.nkind.N_TARRAY) { return astalign(c, t.lhs); }; - if (k == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.align: i64; }; - return 8i64; - }; - if (k == syntax.nkind.N_TTUPLE) { - let m: i64 = 1i64; - let p: *syntax.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 == syntax.nkind.N_TSTRUCT) { - let m: i64 = 1i64; - let f: *syntax.node = t.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let fa: i64 = astalign(c, f.lhs); - if (fa > m) { m = fa; }; - }; - f = f.next; - }; - return m; - }; - if (k == syntax.nkind.N_TENUM) { - if (t.lhs != nil) { return astalign(c, t.lhs); }; - return 4i64; - }; - if (k == syntax.nkind.N_TNAME) { - let nm: str = t.str; - if (syntax.streq(nm, "void") || syntax.streq(nm, "bool") || syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; }; - if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; }; - if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; }; - if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64") || syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.streq(nm, "size") || syntax.streq(nm, "str")) { return 8i64; }; - let resolved: *syntax.node = resolvealias(c, t); - if (resolved != nil && resolved != t) { - return astalign(c, resolved); - }; - }; - return 1i64; -}; - -fn astsize(c: *checker, t: *syntax.node) i64 = { - if (t == nil) { return 0i64; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TBANG) { return astsize(c, t.lhs); }; - if (k == syntax.nkind.N_TPTR) { return 8i64; }; - if (k == syntax.nkind.N_TSLICE) { return tyslicesize(); }; - if (k == syntax.nkind.N_TCHAN) { return 8i64; }; - if (k == syntax.nkind.N_TFN) { return 8i64; }; - if (k == syntax.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 == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.size: i64; }; - return 0i64; - }; - if (k == syntax.nkind.N_TSTRUCT) { - let off: i64 = 0i64; - let maxal: i64 = 1i64; - let f: *syntax.node = t.list; - for (f != nil) { - if (f.kind == syntax.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 == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.size: i64; }; - return 0i64; - }; - if (k == syntax.nkind.N_TENUM) { - if (t.lhs != nil) { return astsize(c, t.lhs); }; - return 4i64; - }; - if (k == syntax.nkind.N_TNAME) { - let nm: str = t.str; - let ps: i64 = primtypesize(nm); - if (ps >= 0i64) { return ps; }; - let resolved: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - let k: syntax.nkind = u.kind; - if (k == syntax.nkind.N_TNAME) { - if (syntax.streq(u.str, "opaque")) { return true; }; - return false; - }; - if (k == syntax.nkind.N_TARRAY) { return astunsized(c, u.lhs); }; - if (k == syntax.nkind.N_TTUPLE) { - let p: *syntax.node = u.list; - for (p != nil) { - if (astunsized(c, p.lhs)) { return true; }; - p = p.next; - }; - return false; - }; - if (k == syntax.nkind.N_TSTRUCT) { - let f: *syntax.node = u.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - if (astunsized(c, f.lhs)) { return true; }; - }; - f = f.next; - }; - return false; - }; - if (k == syntax.nkind.N_TTAGGED) { - let v: *syntax.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: *syntax.node, bname: str, btype: *syntax.node, - nodeout: **syntax.node) *syntax.tinfo = { - if (body == nil) { return nil; }; - let k: syntax.nkind = body.kind; - if (k == syntax.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 == syntax.nkind.N_IDENT && bname.len > 0 - && syntax.streq(body.lhs.str, bname)) { - *nodeout = btype; - }; - return body.lhs.type_: *syntax.tinfo; - }; - let t: *syntax.node = exprtype(c, body.lhs, nil); - if (t != nil) { - *nodeout = t; - return tinfofornode(c, t); - }; - if (body.lhs.kind == syntax.nkind.N_IDENT && bname.len > 0 - && syntax.streq(body.lhs.str, bname)) { - *nodeout = btype; - return tinfofornode(c, btype); - }; - return nil; - }; - if (k == syntax.nkind.N_MATCH) { return nil; }; - if (k == syntax.nkind.N_BLOCK) { - let s: *syntax.node = body.list; - for (s != nil) { - let t: *syntax.tinfo = matchyieldtype(c, s, bname, btype, nodeout); - if (t != nil) { return t; }; - s = s.next; - }; - return nil; - }; - if (k == syntax.nkind.N_IF) { - let t: *syntax.tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); - if (t != nil) { return t; }; - return matchyieldtype(c, body.els, bname, btype, nodeout); - }; - if (k == syntax.nkind.N_FOR || k == syntax.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: *syntax.tinfo) i32 = { - let u: *syntax.tinfo = tichase(t); - if (u == nil) { return 0i32; }; - if (syntax.typeisnum(u)) { return 1i32; }; - if (syntax.typeisstr(u)) { return 2i32; }; - if (u.kind == syntax.tykind.TY_BOOL) { return 3i32; }; - if (u.kind == syntax.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: *syntax.node) i64 = { - if (dot == nil) { return -1i64; }; - if (dot.kind != syntax.nkind.N_DOT) { return -1i64; }; - let recv: *syntax.node = scruttype(c, dot.lhs); - if (recv == nil) { return -1i64; }; - let rtyp: *syntax.node = resolvealias(c, unwrapbang(recv)); - if (rtyp == nil) { return -1i64; }; - if (rtyp.kind == syntax.nkind.N_TPTR) { - rtyp = resolvealias(c, unwrapbang(rtyp.lhs)); - }; - if (rtyp == nil) { return -1i64; }; - if (rtyp.kind != syntax.nkind.N_TSTRUCT) { return -1i64; }; - let off: i64 = 0i64; - let f: *syntax.node = rtyp.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let fa: i64 = astalign(c, f.lhs); - off = (off + fa - 1i64) & ~(fa - 1i64); - if (syntax.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: *syntax.node, v: i64) void = { - n.kind = syntax.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: syntax.tkind, a: u64, b: u64, out: *u64) bool = { - if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; }; - if (op == syntax.tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == syntax.tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; }; - if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == syntax.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: *syntax.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: *syntax.tinfo, v: u64) bool = { - if (!syntax.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 (syntax.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: *syntax.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: syntax.nkind = n.kind; - if (k == syntax.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 == syntax.tkind.TK_SLASH || n.op == syntax.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 == syntax.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 == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (n.op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (n.op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - deffolderr(c, n, "def value: unsupported unary op"); - return false; - }; - if (k == syntax.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: *syntax.tinfo = (n.type_): *syntax.tinfo; - if (!defcastfits(t, v)) { - deffolderr(c, n, "def value: narrowing cast loses value"); - return false; - }; - *out = v; - return true; - }; - if (k == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.str); - if (s == nil) { return false; }; - if (s.skind != syntax.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 == syntax.nkind.N_DOT) { - if (n.lhs == nil) { return false; }; - if (n.lhs.kind != syntax.nkind.N_IDENT) { return false; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); - if (s == nil) { return false; }; - if (s.skind != syntax.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: *syntax.node, v: u64) void = { - n.kind = syntax.nkind.N_INTLIT; - n.uval = v; - n.op = syntax.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: *syntax.node) u64 = { - if (rhs == nil) { return 0u64; }; - if (rhs.kind == syntax.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: *syntax.node, until: *syntax.node, e: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; }; - if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_UN) { - let v: u64 = 0u64; - if (!enumvalfold(body, until, e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - if (k == syntax.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 == syntax.nkind.N_IDENT) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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: *syntax.node) void = { - let under: *syntax.tinfo = c.tc.tyi32; - if (n.lhs != nil) { - let s: *syntax.tinfo = tinfofornode(c, n.lhs); - if (s != nil) { under = s; }; - }; - let m: *syntax.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: *syntax.node, ti: *syntax.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: *syntax.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: *syntax.tinfo = pt; - t = tichase(t); - if (t == nil) { return 8u64; }; - let pk: syntax.tykind = t.kind; - if (pk == syntax.tykind.TY_VOID) { return 0u64; }; - if (pk == syntax.tykind.TY_STR) { return t.size; }; - if (pk == syntax.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 == syntax.tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; - if (pk == syntax.tykind.TY_PTR || pk == syntax.tykind.TY_FN || - pk == syntax.tykind.TY_CHAN || pk == syntax.tykind.TY_I64 || - pk == syntax.tykind.TY_U64 || pk == syntax.tykind.TY_INT || - pk == syntax.tykind.TY_UINT || pk == syntax.tykind.TY_UINTPTR || - pk == syntax.tykind.TY_SIZE || pk == syntax.tykind.TY_F64) { return 8u64; }; - if (pk == syntax.tykind.TY_BOOL || pk == syntax.tykind.TY_RUNE || - pk == syntax.tykind.TY_I8 || pk == syntax.tykind.TY_I16 || - pk == syntax.tykind.TY_I32 || pk == syntax.tykind.TY_U8 || - pk == syntax.tykind.TY_U16 || pk == syntax.tykind.TY_U32 || - pk == syntax.tykind.TY_F32 || pk == syntax.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: *syntax.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: *syntax.tinfo = ft; - t = tichase(t); - if (t == nil) { return 8u64; }; - let fk: syntax.tykind = t.kind; - if (fk == syntax.tykind.TY_STRUCT) { return t.slotsize; }; - if (fk == syntax.tykind.TY_ARRAY) { return t.slotsize; }; - if (fk == syntax.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 == syntax.tykind.TY_SLICE) { return t.size; }; - if (fk == syntax.tykind.TY_PTR || fk == syntax.tykind.TY_FN || - fk == syntax.tykind.TY_CHAN) { return 8u64; }; - if (fk == syntax.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 == syntax.tykind.TY_TUPLE) { return t.slotsize; }; - // Primitives keep natural width inside structs (matches - // cgenutil fieldsize: primsize, not pad-to-8). - if (fk == syntax.tykind.TY_BOOL || fk == syntax.tykind.TY_RUNE || - fk == syntax.tykind.TY_I8 || fk == syntax.tykind.TY_I16 || - fk == syntax.tykind.TY_I32 || fk == syntax.tykind.TY_I64 || - fk == syntax.tykind.TY_U8 || fk == syntax.tykind.TY_U16 || - fk == syntax.tykind.TY_U32 || fk == syntax.tykind.TY_U64 || - fk == syntax.tykind.TY_INT || fk == syntax.tykind.TY_UINT || - fk == syntax.tykind.TY_UINTPTR || fk == syntax.tykind.TY_SIZE || - fk == syntax.tykind.TY_F32 || fk == syntax.tykind.TY_F64 || - fk == syntax.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: *syntax.tparam, vt: *syntax.tinfo) bool = { - if (vt == nil) { return false; }; - let p: *syntax.tparam = head; - for (p != nil) { - if (p.type_ != nil) { - if (syntax.typeeq(p.type_, vt)) { return true; }; - }; - p = p.tnext; - }; - return false; -}; - -fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = { - if (n == nil) { return nil; }; - let cached: *syntax.tinfo = syntax.tinfocachelookup(c.tc, n); - if (cached != nil) { return cached; }; - let r: *syntax.tinfo = nil; - let k: syntax.nkind = n.kind; - switch (k) { - case syntax.nkind.N_TNAME: - let nm: str = n.str; - if (syntax.streq(nm, "void")) { r = c.tc.tyvoid; }; - if (syntax.streq(nm, "bool")) { r = c.tc.tybool; }; - if (syntax.streq(nm, "rune")) { r = c.tc.tyrune; }; - if (syntax.streq(nm, "i8")) { r = c.tc.tyi8; }; - if (syntax.streq(nm, "i16")) { r = c.tc.tyi16; }; - if (syntax.streq(nm, "i32")) { r = c.tc.tyi32; }; - if (syntax.streq(nm, "i64")) { r = c.tc.tyi64; }; - if (syntax.streq(nm, "u8")) { r = c.tc.tyu8; }; - if (syntax.streq(nm, "u16")) { r = c.tc.tyu16; }; - if (syntax.streq(nm, "u32")) { r = c.tc.tyu32; }; - if (syntax.streq(nm, "u64")) { r = c.tc.tyu64; }; - if (syntax.streq(nm, "int")) { r = c.tc.tyint; }; - if (syntax.streq(nm, "uint")) { r = c.tc.tyuint; }; - if (syntax.streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; - if (syntax.streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 - if (syntax.streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) - if (syntax.streq(nm, "f32")) { r = c.tc.tyf32; }; - if (syntax.streq(nm, "f64")) { r = c.tc.tyf64; }; - if (syntax.streq(nm, "str")) { r = c.tc.tystr; }; - if (syntax.streq(nm, "never")) { r = c.tc.tynever; }; - if (syntax.streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; }; - if (syntax.streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; }; - if (syntax.streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; }; - if (syntax.streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; }; - if (syntax.streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; }; - if (syntax.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: *syntax.sym = aliassym(c, n); - if (s != nil) { - if (s.type_ != nil) { - r = s.type_; - } else { - let body: *syntax.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: *syntax.tinfo = syntax.typenamed(s.name, nil); - s.type_ = named; - named.resolving = 1; - let under: *syntax.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 syntax.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 syntax.nkind.N_TPTR: - r = syntax.typeptr(tinfofornode(c, n.lhs)); - case syntax.nkind.N_TSLICE: - r = syntax.typeslice(tinfofornode(c, n.lhs)); - case syntax.nkind.N_TCHAN: - r = syntax.typechan(tinfofornode(c, n.lhs)); - case syntax.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 == syntax.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: *syntax.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 = syntax.typearray(sub, elen); - case syntax.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 = syntax.newtype(syntax.tykind.TY_FN); - r.size = 8u64; - r.align = 8u64; - r.slotsize = 8u64; - syntax.tinfocachebind(c.tc, n, r); - r.ret = tinfofornode(c, n.lhs); - case syntax.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 = syntax.newtype(syntax.tykind.TY_ENUM); - let storage: *syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_TUPLE); - syntax.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: *syntax.ttupleelem = nil; - let tet: *syntax.ttupleelem = nil; - let slottotal: u64 = 0u64; - let maxal: u64 = 1u64; - let p: *syntax.node = n.list; - for (p != nil) { - let pt: *syntax.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: *syntax.tinfo = tichase(pt); - if (cu != nil && (cu.kind == syntax.tykind.TY_ARRAY - || cu.kind == syntax.tykind.TY_STRUCT - || cu.kind == syntax.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: *syntax.ttupleelem = alloc(syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_STRUCT); - syntax.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: *syntax.tfield = nil; - let ft_: *syntax.tfield = nil; - let off: u64 = 0u64; - let maxalign: u64 = 1u64; - let soff: u64 = 0u64; - let f: *syntax.node = n.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let ft: *syntax.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: *syntax.tfield = alloc(syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_TAGGED); - syntax.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: *syntax.tparam = nil; - let tail: *syntax.tparam = nil; - let maxsz: u64 = 0u64; - let al: u64 = 8u64; - let nv: i32 = 0; - let v: *syntax.node = n.list; - for (v != nil) { - let vt: *syntax.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 == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.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 == syntax.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: *syntax.tparam = vu.params; - for (src != nil) { - let st: *syntax.tinfo = src.type_; - if (st != nil && st.kind == syntax.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: *syntax.tparam = alloc(syntax.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 == syntax.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: *syntax.tparam = alloc(syntax.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: *syntax.tparam = head; - let pb: *syntax.tparam = head.tnext; - let aptr: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_PTR); - let bptr: bool = (pb.type_ != nil && pb.type_.kind == syntax.tykind.TY_PTR); - let avoid: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_VOID && !pa.iserror); - let bvoid: bool = (pb.type_ != nil && pb.type_.kind == syntax.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; }; - syntax.tinfocachebind(c.tc, n, r); - }; - return r; -}; - -// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode -// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec -// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is -// loud (#26), aligning wwstage down to cstage; the only promotion left is -// the one-sided alias-vs-base chase (cstage check.c:1060-1067). -// -// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or 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, so a nil operand never reaches the loud reject below. -fn unifyarith(c: *checker, e: *syntax.node, ltn: *syntax.node, rtn: *syntax.node) *syntax.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; }; - }; - // A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296), - // assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's - // unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed - // side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence, - // exprtype :2927), so the literal does not reach the isuntyped arms - // above. Mirror cstage here off the operand node so the rune-literal / - // integer mix stays valid (35 selfhost+lib sites) under the #26 reject. - if (e != nil) { - let lr: bool = e.lhs != nil && e.lhs.kind == syntax.nkind.N_RUNELIT; - let rr: bool = e.rhs != nil && e.rhs.kind == syntax.nkind.N_RUNELIT; - if (lr && isinttypeast(rtn)) { return rtn; }; - if (rr && isinttypeast(ltn)) { return ltn; }; - }; - if (typeeqast(c, ltn, rtn)) { return ltn; }; - // One-sided alias vs its (transitive) base promotes to the alias - // side; alias-vs-different-alias stays rejected even when the bases - // agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and - // harec type_promote (ref/harec/src/check.c:1083-1105). The error - // axis (varianterr) must agree on both sides so a `!i32` alias does - // NOT promote against a plain i32 (#246, 949_errtype_compare). - // A user alias is an SK_TYPE sym WITH a decl body; the primitives are - // SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone - // would mis-flag i32 as "named". This decl != nil gate is the wwstage - // analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc). - let ls: *syntax.sym = aliassym(c, unwrapbang(ltn)); - let rs: *syntax.sym = aliassym(c, unwrapbang(rtn)); - let la: bool = ls != nil && ls.decl != nil; - let ra: bool = rs != nil && rs.decl != nil; - if (!(la && ra)) { - let da: *syntax.node = resolvealias(c, ltn); - let db: *syntax.node = resolvealias(c, rtn); - if (da != nil && db != nil - && varianterr(c, ltn) == varianterr(c, rtn) - && typeeqast(c, da, db)) { - if (la) { return ltn; }; - return rtn; - }; - }; - // ww requires explicit integer conversion in binary ops (Go-faithful, - // user-blessed #26): a typed/typed operand mismatch is loud, NOT - // promoted. Diverges from Hare type_promote's implicit signed-widen - // (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage - // already rejects the same shape (cmd/wcc/check.c:1068); this aligns - // wwstage down. A nil operand is an inherent-IDENT bail (see header) — - // propagate it, never reject. - if (ltn != nil && rtn != nil) { - deffolderr(c, e, "operands have differing types"); - }; - 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: *syntax.node, target: *syntax.node) void = { - if (e == nil) { return; }; - if (target == nil) { return; }; - let tu: *syntax.node = resolvealias(c, unwrapbang(target)); - if (tu == nil) { return; }; - if (tu.kind != syntax.nkind.N_TNAME) { return; }; - if (!syntax.streq(tu.str, "f32")) { return; }; - if (e.kind == syntax.nkind.N_FLOATLIT) { - if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) { - let f32t: *syntax.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: *syntax.node, target: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind != syntax.nkind.N_RUNELIT) { return nil; }; - if (target == nil) { return nil; }; - let tu: *syntax.node = resolvealias(c, unwrapbang(target)); - if (tu == nil) { return nil; }; - if (isinttypeast(tu)) { return tu; }; - if (tu.kind == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = tu.list; - for (v != nil) { - let vu: *syntax.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: *syntax.node) *syntax.node = { - let op: syntax.tkind = e.op; - let ltn: *syntax.node = exprtype(c, e.lhs, nil); - let rtn: *syntax.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 == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS) { - if (ltn != nil && ltn.kind == syntax.nkind.N_TPTR && intkindast(c, rtn)) { - return ltn; - }; - }; - if (op == syntax.tkind.TK_PLUS) { - if (intkindast(c, ltn) && rtn != nil && rtn.kind == syntax.nkind.N_TPTR) { - return rtn; - }; - }; - if (op == syntax.tkind.TK_MINUS) { - if (ltn != nil && rtn != nil - && ltn.kind == syntax.nkind.N_TPTR && rtn.kind == syntax.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 == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS || - op == syntax.tkind.TK_STAR || op == syntax.tkind.TK_SLASH || - op == syntax.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, e, ltn, rtn); - }; - if (op == syntax.tkind.TK_AMP || op == syntax.tkind.TK_PIPE || - op == syntax.tkind.TK_CARET || op == syntax.tkind.TK_LSHIFT || - op == syntax.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, e, ltn, rtn); - }; - if (op == syntax.tkind.TK_EQ || op == syntax.tkind.TK_NEQ || - op == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE || - op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) { - // cstage routes every comparison through unify_arith (check.c:1195/ - // 1200 cbinop), which loud-rejects a differing-types pair, e.g. - // strconv.invalid != i32. wwstage routes the ORDERED arm through - // unifyarith below (#26), but EQ/NEQ stays here on the error-type- - // only reject: a full unifyarith route for EQ/NEQ would expose the - // typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr - // equality. The error-type case (#246, rule 10) is the one real - // cs!=ww edge EQ/NEQ must still reject. - 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 == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE || - op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) { - if ((ltn != nil && !numkindast(c, ltn)) || - (rtn != nil && !numkindast(c, rtn))) { - deffolderr(c, e, "ordered comparison on non-numeric"); - }; - // #26: an ordered comparison routes through unifyarith for - // the differing-types reject — cstage's cbinop sends every - // comparison through unify_arith (cmd/wcc/check.c:1195/1200), - // so `int < len(s):i32` is loud there. Guarded off error - // operands (the #246 block above already rejects those, so - // this avoids a double diagnostic) and scoped to ordered, - // keeping the typeeqast-vs-type_eq asymmetry risk off the - // untouched EQ/NEQ arm. - if (!varianterr(c, ltn) && !varianterr(c, rtn)) { - unifyarith(c, e, ltn, rtn); - }; - }; - return mktname(c, "bool"); - }; - if (op == syntax.tkind.TK_AND || op == syntax.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: *syntax.node) *syntax.node = { - let op: syntax.tkind = e.op; - let opt: *syntax.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 == syntax.tkind.TK_MINUS || op == syntax.tkind.TK_PLUS) { - if (opt != nil && !numkindast(c, opt)) { - if (op == syntax.tkind.TK_MINUS) { - deffolderr(c, e, "- on non-numeric"); - } else { - deffolderr(c, e, "+ on non-numeric"); - }; - }; - return opt; - }; - if (op == syntax.tkind.TK_NOT) { - if (opt != nil && !boolkindast(c, opt)) { - deffolderr(c, e, "! on non-bool"); - }; - return mktname(c, "bool"); - }; - if (op == syntax.tkind.TK_TILDE) { - if (opt != nil && !intkindast(c, opt)) { - deffolderr(c, e, "~ on non-integer"); - }; - return opt; - }; - if (op == syntax.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: *syntax.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 != syntax.nkind.N_TPTR) { return nil; }; - return u.lhs; - }; - if (op == syntax.tkind.TK_AMP) { - if (e.lhs != nil && e.lhs.kind == syntax.nkind.N_DOT) { - let fld: str = e.lhs.str; - if (syntax.streq(fld, "len") || syntax.streq(fld, "cap")) { - let base: *syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = bu.sub; }; - bu = tichase(bu); - if (bu != nil) { - if (bu.kind == syntax.tykind.TY_SLICE || bu.kind == syntax.tykind.TY_STR) { - let pp: *syntax.node = syntax.newnode(syntax.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 == syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (fs != nil) { - if (fs.skind == syntax.skind.SK_FN) { - if (fs.decl != nil) { - if (fs.decl.kind == syntax.nkind.N_FNDECL) { - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = fs.decl.lhs; - synth.list = fs.decl.list; - let pf: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_DOT) { - if (e.lhs.lhs != nil && e.lhs.lhs.kind == syntax.nkind.N_IDENT) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); - if (fs != nil) { - if (fs.skind == syntax.skind.SK_FN) { - if (fs.decl != nil) { - if (fs.decl.kind == syntax.nkind.N_FNDECL) { - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = fs.decl.lhs; - synth.list = fs.decl.list; - let pf: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.node) *syntax.node = { - let basetn: *syntax.node = exprtype(c, e.lhs, nil); - let _idx: *syntax.node = exprtype(c, e.rhs, nil); - let u: *syntax.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 == syntax.nkind.N_TSLICE) { return u.lhs; }; - if (u.kind == syntax.nkind.N_TARRAY) { return u.lhs; }; - if (u.kind == syntax.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 (syntax.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 == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (s != nil && s.skind == syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = u.lhs; - let iu: *syntax.node = resolvealias(c, unwrapbang(inner)); - if (iu != nil && iu.kind == syntax.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: *syntax.node, hint: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - let k: syntax.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 == syntax.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: *syntax.node = mktname(c, e.tsuffix); - let ti: *syntax.tinfo = tinfofornode(c, suf); - if (ti != nil) { - e.type_ = ti: *void; - return suf; - }; - }; - let tn: *syntax.node = mktname(c, "untyped_int"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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: *syntax.node = mktname(c, e.tsuffix); - let ti: *syntax.tinfo = tinfofornode(c, suf); - if (ti != nil) { - e.type_ = ti: *void; - return suf; - }; - }; - let tn: *syntax.node = mktname(c, "untyped_float"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_STRLIT) { - let tn: *syntax.node = mktname(c, "str"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_RUNELIT) { - let tn: *syntax.node = mktname(c, "rune"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_TRUE) { - let tn: *syntax.node = mktname(c, "bool"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_FALSE) { - let tn: *syntax.node = mktname(c, "bool"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_VOIDLIT) { - let tn: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_NIL) { - let tn: *syntax.node = mktname(c, "untyped_nil"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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: *syntax.sym = syntax.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 == syntax.skind.SK_FN) { - let ft: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { - e.type_ = ti: *void; - t.type_ = ti: *void; - }; - }; - }; - return t; - }; - if (k == syntax.nkind.N_BIN) { - let tn: *syntax.node = binoptype(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_UN) { - let tn: *syntax.node = unoptype(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_INDEX) { - let tn: *syntax.node = indexresult(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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 == syntax.nkind.N_CALL) { - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(callee.str, "alloc")) { - let shadowed: bool = false; - if (c.curmod.len > 0) { - if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { - shadowed = true; - }; - }; - if (!shadowed) { - if (e.list != nil) { - // Slice form: `alloc([], n)`. - if (e.list.kind == syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = mktname(c, "u8"); - let nome: *syntax.node = mktname(c, "nomem"); - sl.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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: *syntax.node = exprtype(c, e.list, nil); - let ptr: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, "", 0, 0); - ptr.lhs = argt; - let nome: *syntax.node = mktname(c, "nomem"); - ptr.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_IDENT) { - let bname: str = callee.str; - let issize: bool = syntax.streq(bname, "size"); - let isalign: bool = syntax.streq(bname, "align"); - let isoffset: bool = syntax.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 == syntax.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:1570/1602 which stamps - // ty_untyped_int after the fold AND returns it - // to the caller (the Hare-correct type), so a - // `let x: size = size(T)` binding is assignable. - let utn: *syntax.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, "untyped_int"); - }; - 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, "untyped_int"); - }; - // 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 == syntax.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, "untyped_int"); - }; - }; - }; - }; - }; - // 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 == syntax.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 (syntax.streq(callee.str, "abort") - && syntax.scopelookupprefer(c.cur, c.curmod, "abort") == nil) { - if (e.list != nil) { - let mt: *syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - callee.type_ = c.tc.tyerr: *void; - return tn; - }; - if (syntax.streq(callee.str, "assert") && e.list != nil - && syntax.scopelookupprefer(c.cur, c.curmod, "assert") == nil) { - let ct: *syntax.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: *syntax.node = unwrapbang(ct); - let condok: bool = false; - if (cu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(cu.str, "bool") - || syntax.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: *syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - callee.type_ = c.tc.tyerr: *void; - return tn; - }; - if (syntax.streq(callee.str, "len")) { - let tn: *syntax.node = mktname(c, "i32"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (syntax.streq(callee.str, "append") || syntax.streq(callee.str, "free")) { - let tn: *syntax.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 (syntax.streq(callee.str, "delete")) { - let d: *syntax.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 != syntax.nkind.N_INDEX && d.kind != syntax.nkind.N_SLICE) { - cerr("delete: operand must be an indexing or slicing expression\n"); - c.errs += 1; - } else { - let basetn: *syntax.node = exprtype(c, d.lhs, nil); - let u: *syntax.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 != syntax.nkind.N_TSLICE) { - cerr("delete must operate on a slice\n"); - c.errs += 1; - }; - }; - }; - let tn: *syntax.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 (syntax.streq(callee.str, "insert")) { - let d: *syntax.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 == syntax.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 != syntax.nkind.N_INDEX) { - cerr("insert: operand must be an indexing expression xs[i]\n"); - c.errs += 1; - } else { - let basetn: *syntax.node = exprtype(c, d.lhs, nil); - let u: *syntax.node = resolvealias(c, unwrapbang(basetn)); - // harec check.c:807 wording; a - // fixed-size [N]T base lands here. - if (u == nil || u.kind != syntax.nkind.N_TSLICE) { - cerr("insert must operate on a slice\n"); - c.errs += 1; - }; - }; - }; - if (d.next != nil) { - if (d.next.kind == syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - }; - let nm: str; - nm.ptr = nil; nm.len = 0; - if (callee.kind == syntax.nkind.N_IDENT) { nm = callee.str; }; - if (callee.kind == syntax.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: *syntax.sym = nil; - if (callee.kind == syntax.nkind.N_IDENT) { - s = syntax.scopelookupprefer(c.cur, c.curmod, nm); - } else { - let ms: *syntax.sym = nil; - if (callee.lhs != nil && callee.lhs.kind == syntax.nkind.N_IDENT) { - ms = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.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 == syntax.skind.SK_USE || ms.use_alias != 0i32)) { - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); - // Module-qualified callee whose leaf isn't scope-keyed - // under its module: align to cstage, which stamps ty_err - // here and lets cgen emit the call (cmd/wcc/check.c:1834- - // 1843; rule-10). This now tolerates only a genuinely- - // extern leaf (raw w6c on a single module-qualified file - // with no driver concat), the module-qualified arm of #27. - // The -T synth's `test.run` NO LONGER lands here: #80 - // PREPENDS the synth `use test;` before Pass 1, so declmod - // keys lib/test's `run` under "test" (not "") and the synth - // `test.run` type-resolves — the E1 bridge is closed. cgen - // keys the CALL off run's `//ww:module test` directive - // either way, so the resolution change is asm-neutral. - if (s == nil) { - e.type_ = c.tc.tyerr: *void; - callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) - return nil; - }; - }; - }; - if (s != nil) { if (s.skind == syntax.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: *syntax.node = resolvealias(c, unwrapbang(exprtype(c, callee, nil))); - for (ct != nil && ct.kind == syntax.nkind.N_TPTR) { - ct = resolvealias(c, unwrapbang(ct.lhs)); - }; - if (ct != nil) { if (ct.kind == syntax.nkind.N_TFN) { - let res: *syntax.node = ct.lhs; - e.type_ = tinfofornode(c, res): *void; - return res; - }; }; - return nil; - }; - if (k == syntax.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: *syntax.node = e.lhs; - if (lhsn != nil) { if (lhsn.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, lhsn.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.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 == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 == syntax.skind.SK_FN) { - let ft: *syntax.node = syntax.newnode(syntax.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: *syntax.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 == syntax.skind.SK_TYPE) { if (ms.decl != nil) { - let body: *syntax.node = ms.decl.lhs; - let ub: *syntax.node = resolvealias(c, unwrapbang(body)); - if (ub != nil) { if (ub.kind == syntax.nkind.N_TENUM) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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: *syntax.node = exprtype(c, lhsn, nil); - if (basetn != nil) { - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu != nil) { if (bu.kind == syntax.nkind.N_TPTR) { - bu = resolvealias(c, unwrapbang(bu.lhs)); - }; }; - if (bu != nil) { if (bu.kind == syntax.nkind.N_TENUM) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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 == syntax.nkind.N_TNAME) && syntax.streq(bu.str, "str"); - if (bu.kind == syntax.nkind.N_TSLICE || bu.kind == syntax.nkind.N_TARRAY || isstr) { - if (syntax.streq(e.str, "len")) { - let tn: *syntax.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 == syntax.nkind.N_TARRAY && syntax.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 (syntax.streq(e.str, "cap")) { - let tn: *syntax.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 (syntax.streq(e.str, "ptr")) { - let elem: *syntax.node = bu.lhs; - if (isstr) { elem = mktname(c, "u8"); }; - let pp: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_TSTRUCT) { - let f: *syntax.node = bu.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { if (syntax.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 == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(e.str); - if (idx >= 0) { - let p: *syntax.node = bu.list; - for (idx > 0 && p != nil) { - p = p.next; - idx -= 1; - }; - if (p != nil) { - let pt: *syntax.node = p.lhs; - e.type_ = tinfofornode(c, pt): *void; - return pt; - }; - }; - }; }; - }; - return nil; - }; - if (k == syntax.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 == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (ms != nil) { if (ms.skind == syntax.skind.SK_TYPE) { if (ms.decl != nil) { - let tn: *syntax.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: *syntax.node = mktname(c, e.lhs.str); - let nti: *syntax.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: *syntax.node = resolvealias(c, tn); - if (stn != nil && stn.kind == syntax.nkind.N_TSTRUCT) { - let fi: *syntax.node = e.list; - for (fi != nil) { - if (fi.kind == syntax.nkind.N_FIELD - && fi.lhs != nil - && fi.lhs.kind == syntax.nkind.N_ARRLIT) { - let ftn: *syntax.node = nil; - let tf: *syntax.node = stn.list; - for (tf != nil) { - if (tf.kind == syntax.nkind.N_TFIELD) { - if (syntax.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 == syntax.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: *syntax.node = nil; - let count: u64 = 0u64; - let it: *syntax.node = e.list; - for (it != nil) { - let skip: bool = false; - if (it.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(it.str, "...")) { skip = true; }; - }; - if (!skip) { - let t: *syntax.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 == syntax.nkind.N_STRUCTLIT - && it.lhs != nil - && it.lhs.kind == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(elt.str, "untyped_int")) { - elt = mktname(c, "int"); - } else { if (syntax.streq(elt.str, "untyped_float")) { - elt = mktname(c, "f64"); - } else { if (syntax.streq(elt.str, "untyped_str")) { - elt = mktname(c, "str"); - } else { if (syntax.streq(elt.str, "untyped_rune")) { - elt = mktname(c, "rune"); - } else { if (syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu == nil) { return nil; }; - if (bu.kind == syntax.nkind.N_TARRAY) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = bu.lhs; - e.type_ = tinfofornode(c, sl): *void; - return sl; - }; - if (bu.kind == syntax.nkind.N_TSLICE) { - e.type_ = tinfofornode(c, basetn): *void; - return basetn; - }; - if (bu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(bu.str, "str")) { - let tn: *syntax.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 == syntax.nkind.N_TPTR && bu.lhs != nil) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = bu.lhs; - e.type_ = tinfofornode(c, sl): *void; - return sl; - }; - return nil; - }; - if (k == syntax.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: *syntax.node = nil; - let tail: *syntax.node = nil; - let it: *syntax.node = e.list; - for (it != nil) { - let elemt: *syntax.node = exprtype(c, it, nil); - let w: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, "", 0, 0); - w.lhs = elemt; - if (head == nil) { head = w; } - else { tail.next = w; }; - tail = w; - it = it.next; - }; - let tt: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, "", 0, 0); - tt.list = head; - e.type_ = tinfofornode(c, tt): *void; - return tt; - }; - if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu == nil) { return nil; }; - if (bu.kind == syntax.nkind.N_TCHAN) { - e.type_ = tinfofornode(c, bu.lhs): *void; - return bu.lhs; - }; - return nil; - }; - if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; - return t; - }; - if (k == syntax.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: *syntax.tinfo = nil; - let retn: *syntax.node = nil; - let cs: *syntax.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: *syntax.node = nil; - let t: *syntax.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 (!syntax.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 == syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, v): *void; - return v; - }; - let t: *syntax.node = exprtype(c, e.lhs, nil); - if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; - return t; - }; - if (k == syntax.nkind.N_TRYPROP) { - // success unwrap: the success-variant type of operand's - // tagged union. - let opt: *syntax.node = exprtype(c, e.lhs, nil); - let ou: *syntax.node = resolvealias(c, unwrapbang(opt)); - if (ou == nil) { return nil; }; - if (ou.kind != syntax.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: *syntax.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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let ou: *syntax.node = resolvealias(c, unwrapbang(opt)); - if (ou == nil) { return nil; }; - if (ou.kind != syntax.nkind.N_TTAGGED) { return nil; }; - if (taggedhaserr(c, ou)) { - let v: *syntax.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 == syntax.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 == syntax.nkind.N_TYPETEST) { - // `e is T` → bool - let tn: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(t.str, "untyped_int"); -}; - -fn isuntypedfloat(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(t.str, "untyped_float"); -}; - -fn isuntypednil(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind == syntax.nkind.N_TENUM) { return true; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = t.str; - if (syntax.streq(s, "i8")) { return true; }; - if (syntax.streq(s, "i16")) { return true; }; - if (syntax.streq(s, "i32")) { return true; }; - if (syntax.streq(s, "i64")) { return true; }; - if (syntax.streq(s, "u8")) { return true; }; - if (syntax.streq(s, "u16")) { return true; }; - if (syntax.streq(s, "u32")) { return true; }; - if (syntax.streq(s, "u64")) { return true; }; - if (syntax.streq(s, "int")) { return true; }; - if (syntax.streq(s, "uint")) { return true; }; - if (syntax.streq(s, "uintptr")) { return true; }; - if (syntax.streq(s, "size")) { return true; }; - if (syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (isinttypeast(u)) { return true; }; // int family + enum + rune - if (isuntypedint(u)) { return true; }; - if (u != nil && u.kind == syntax.nkind.N_TNAME && syntax.streq(u.str, "untyped_rune")) { - return true; - }; - return false; -}; - -fn numkindast(c: *checker, t: *syntax.node) bool = { - if (intkindast(c, t)) { return true; }; - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - if (u.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = u.str; - if (syntax.streq(s, "f32")) { return true; }; - if (syntax.streq(s, "f64")) { return true; }; - if (syntax.streq(s, "untyped_float")) { return true; }; - return false; -}; - -fn boolkindast(c: *checker, t: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - if (u.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(u.str, "bool") || syntax.streq(u.str, "untyped_bool"); -}; - -fn isnumerictname(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = t.str; - if (syntax.streq(s, "i8")) { return true; }; - if (syntax.streq(s, "i16")) { return true; }; - if (syntax.streq(s, "i32")) { return true; }; - if (syntax.streq(s, "i64")) { return true; }; - if (syntax.streq(s, "u8")) { return true; }; - if (syntax.streq(s, "u16")) { return true; }; - if (syntax.streq(s, "u32")) { return true; }; - if (syntax.streq(s, "u64")) { return true; }; - if (syntax.streq(s, "int")) { return true; }; - if (syntax.streq(s, "uint")) { return true; }; - if (syntax.streq(s, "uintptr")) { return true; }; - if (syntax.streq(s, "size")) { return true; }; - if (syntax.streq(s, "rune")) { return true; }; - if (syntax.streq(s, "f32")) { return true; }; - if (syntax.streq(s, "f64")) { return true; }; - return false; -}; - -fn isstrtname(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.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: *syntax.node) i32 = { - if (t == nil) { return 0i32; }; - if (t.kind == syntax.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: *syntax.node, synth: *syntax.node) bool = { - if (ptr == nil) { return false; }; - let pu: *syntax.node = resolvealias(c, unwrapbang(ptr)); - if (pu == nil) { return false; }; - if (pu.kind != syntax.nkind.N_TPTR) { return false; }; - let ref: *syntax.node = resolvealias(c, unwrapbang(pu.lhs)); - if (ref == nil) { return false; }; - if (ref.kind != syntax.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: *syntax.node, rhs: *syntax.node) bool = { - if (dst == nil) { return false; }; - if (rhs == nil) { return false; }; - if (rhs.kind != syntax.nkind.N_UN) { return false; }; - if (rhs.op != syntax.tkind.TK_AMP) { return false; }; - let id: *syntax.node = rhs.lhs; - if (id == nil) { return false; }; - if (id.kind != syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, id.str); - if (s == nil) { return false; }; - if (s.skind != syntax.skind.SK_FN) { return false; }; - if (s.decl == nil) { return false; }; - let d: *syntax.node = s.decl; - if (d.kind != syntax.nkind.N_FNDECL) { return false; }; - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = d.lhs; - synth.list = d.list; - let du: *syntax.node = resolvealias(c, unwrapbang(dst)); - if (du == nil) { return false; }; - if (du.kind == syntax.nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); }; - if (du.kind == syntax.nkind.N_TTAGGED) { - let nmatch: i32 = 0; - let v: *syntax.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: *syntax.node, src: *syntax.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: *syntax.node = resolvealias(c, unwrapbang(dst)); - let su: *syntax.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 == syntax.nkind.N_TSLICE) { - if (su.kind == syntax.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 == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.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 == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(du.str, "bool")) { return false; }; - if (syntax.streq(du.str, "void")) { return false; }; - if (syntax.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 == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT) { - return false; - }; - // Unknown shapes: stay quiet. - *confident = false; - return true; - }; - if (isuntypedfloat(su)) { - if (isnumerictname(du)) { return true; }; - if (du.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(du.str, "bool")) { return false; }; - if (syntax.streq(du.str, "void")) { return false; }; - if (syntax.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 == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - if (vu != nil) { - if (vu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(vu.str, "f32")) { return true; }; - if (syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT) { - return false; - }; - *confident = false; - return true; - }; - if (isuntypednil(su)) { - // nil → ptr/slice/chan/fn/nullable - if (du.kind == syntax.nkind.N_TPTR) { return true; }; - if (du.kind == syntax.nkind.N_TSLICE) { return true; }; - if (du.kind == syntax.nkind.N_TCHAN) { return true; }; - if (du.kind == syntax.nkind.N_TFN) { return true; }; - // nullable `(*T | void)` — already accepted by typeeqast - // when matched whole; nil is OK there too. - if (du.kind == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - if (v.kind == syntax.nkind.N_TPTR) { return true; }; - if (v.kind == syntax.nkind.N_TSLICE) { return true; }; - if (v.kind == syntax.nkind.N_TCHAN) { return true; }; - if (v.kind == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TTAGGED && su.kind != syntax.nkind.N_TTAGGED) { - let v: *syntax.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 == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - let vtagged: bool = false; - if (vu != nil) { if (vu.kind == syntax.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 != syntax.nkind.N_TNAME) { - let ss: i32 = tagshape(su); - let sp: *syntax.node = du.list; - for (sp != nil) { - let svu: *syntax.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 == syntax.nkind.N_TTAGGED && su.kind == syntax.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: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - if (vu != nil) { - if (vu.kind == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.tkind.TK_ELLIPSIS) { hasspread = true; }; - }; - for (let p: *syntax.node = su.list; p != nil; p = p.next) { - if (p.op == syntax.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: *syntax.node = su.list; sp != nil; sp = sp.next) { - let ok: bool = false; - for (let dp: *syntax.node = du.list; dp != nil; dp = dp.next) { - if (typeeqast(c, dp, sp)) { ok = true; break; }; - let su2: *syntax.node = unwrapbang(sp); - let du2: *syntax.node = unwrapbang(dp); - if (su2 != nil && du2 != nil && - su2.kind == syntax.nkind.N_TNAME && du2.kind == syntax.nkind.N_TNAME && - syntax.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 == syntax.nkind.N_TTAGGED && du.kind != syntax.nkind.N_TTAGGED) { - return false; - }; - // Two known primitives with different names are confidently - // incompatible. `i32 ↔ bool`, `str ↔ i32`, etc. - if (du.kind == syntax.nkind.N_TNAME && su.kind == syntax.nkind.N_TNAME) { - let known_d: bool = isnumerictname(du) || isstrtname(du); - if (!known_d) { if (syntax.streq(du.str, "bool")) { known_d = true; }; }; - if (!known_d) { if (syntax.streq(du.str, "void")) { known_d = true; }; }; - let known_s: bool = isnumerictname(su) || isstrtname(su); - if (!known_s) { if (syntax.streq(su.str, "bool")) { known_s = true; }; }; - if (!known_s) { if (syntax.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 == syntax.nkind.N_TPTR) { - if (su.kind == syntax.nkind.N_TPTR) { - let dref: *syntax.node = resolvealias(c, unwrapbang(du.lhs)); - let sref: *syntax.node = resolvealias(c, unwrapbang(su.lhs)); - if (dref != nil) { - if (sref != nil) { - if (dref.kind == syntax.nkind.N_TFN) { - if (sref.kind == syntax.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 == syntax.nkind.N_TFN) { - if (su.kind == syntax.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 == syntax.nkind.N_TNAME - && (isnumerictname(du) || isstrtname(du) - || syntax.streq(du.str, "bool") || syntax.streq(du.str, "void")); - let sprim: bool = su.kind == syntax.nkind.N_TNAME - && (isnumerictname(su) || isstrtname(su) - || syntax.streq(su.str, "bool") || syntax.streq(su.str, "void")); - let daggr: bool = du.kind == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT; - let saggr: bool = su.kind == syntax.nkind.N_TSLICE || su.kind == syntax.nkind.N_TARRAY - || su.kind == syntax.nkind.N_TPTR || su.kind == syntax.nkind.N_TFN - || su.kind == syntax.nkind.N_TCHAN || su.kind == syntax.nkind.N_TTUPLE - || su.kind == syntax.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 != syntax.nkind.N_TSTRUCT && su.kind != syntax.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: *syntax.node, pat: *syntax.node, unionmod: str) bool = { - let vv: *syntax.node = unwrapbang(v); - let pp: *syntax.node = unwrapbang(pat); - if (vv == nil) { return false; }; - if (pp == nil) { return false; }; - if (vv.kind != syntax.nkind.N_TNAME) { return false; }; - if (pp.kind != syntax.nkind.N_TNAME) { return false; }; - if (!syntax.streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; - return syntax.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: *syntax.node) str = { - let defmod: str = c.curmod; - let cur: *syntax.node = unwrapbang(st); - for (cur != nil) { - if (cur.kind != syntax.nkind.N_TNAME) { return defmod; }; - let s: *syntax.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: *syntax.node = unwrapbang(s.decl.lhs); - if (body == nil) { return defmod; }; - if (body.kind == syntax.nkind.N_TTAGGED) { return defmod; }; - cur = body; - }; - return defmod; -}; - -fn casecovers(c: *checker, cs: *syntax.node, want: *syntax.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: *syntax.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: *syntax.node, vname: *syntax.node) void = { - cerr("match: variant not handled"); - if (vname != nil) { - if (vname.kind == syntax.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: *syntax.node, pat: *syntax.node, unionmod: str) bool = { - let v: *syntax.node = tagged.list; - for (v != nil) { - if (v.op == syntax.tkind.TK_ELLIPSIS) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil) { - if (inner.kind == syntax.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: *syntax.node) void = { - cerr("case: not a variant of scrutinee"); - if (pat != nil) { - if (pat.kind == syntax.nkind.N_TNAME) { - cerr(" ("); - cerr(pat.str); - cerr(")"); - }; - }; - cerr("\n"); - c.errs += 1; -}; - -fn checkmatchexhaust(c: *checker, n: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) { - istry = true; - st = exprtype(c, n.lhs, nil); - }; - }; - let u: *syntax.node = resolvealias(c, unwrapbang(st)); - if (u == nil) { return; }; - if (u.kind != syntax.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: *syntax.node = n.list; - for (cs0 != nil) { - if (cs0.lhs != nil) { - if (!casevariantin(c, u, cs0.lhs, unionmod)) { - errbadcase(c, cs0.lhs); - }; - let alt: *syntax.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: *syntax.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: *syntax.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: *syntax.node, v: *syntax.node, unionmod: str) void = { - if (v.op == syntax.tkind.TK_ELLIPSIS) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil) { - if (inner.kind == syntax.nkind.N_TTAGGED) { - let im: str = taggeddefmod(c, v); - let m: *syntax.node = inner.list; - for (m != nil) { - checkvariantcovered(c, n, m, im); - m = m.next; - }; - return; - }; - }; - }; - let covered: bool = false; - let cs2: *syntax.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: *syntax.node, src: *syntax.node, where: str) void = { - cerr(where); - cerr(": not assignable"); - if (src != nil) { - if (src.kind == syntax.nkind.N_TNAME) { - cerr(" ("); - cerr(src.str); - cerr(" → "); - if (dst != nil) { - if (dst.kind == syntax.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: *syntax.node, src: *syntax.node) bool = { - if (dst == nil) { return false; }; - if (src == nil) { return false; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dst)); - let su: *syntax.node = resolvealias(c, unwrapbang(src)); - if (du == nil) { return false; }; - if (du.kind != syntax.nkind.N_TTAGGED) { return false; }; - if (su != nil) { if (su.kind == syntax.nkind.N_TTAGGED) { return false; }; }; - let v: *syntax.node = du.list; - for (v != nil) { - let vspread: bool = (v.op == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - let vtagged: bool = false; - if (vu != nil) { if (vu.kind == syntax.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 == syntax.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: *syntax.node, rhs: *syntax.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 != syntax.nkind.N_TARRAY) { return; }; - if (rhs.kind != syntax.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: *syntax.node = rhs.list; - for (ce != nil) { - let cskip: bool = false; - if (ce.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = arrtn.lhs; - let at: *syntax.tinfo = tinfofornode(c, arrtn); - let et: *syntax.tinfo = nil; - if (at != nil) { et = at.sub; }; - et = tichase(et); - let e: *syntax.node = rhs.list; - for (e != nil) { - let skip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { skip = true; }; - }; - if (!skip) { - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.node = resolvealias(c, elemtn); - if (eltr != nil && eltr.kind == syntax.nkind.N_TARRAY - && ev != nil && ev.kind == syntax.nkind.N_ARRLIT) { - checkarrlitfits(c, eltr, ev); - e = e.next; - continue; - }; - let v: u64 = 0u64; - let folded: bool = false; - if (et != nil) { - if (syntax.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: *syntax.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: *syntax.node, tup: *syntax.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: *syntax.node = ttn.list; - for (dc != nil) { dn += 1u64; dc = dc.next; }; - let vn: u64 = 0u64; - let vc: *syntax.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: *syntax.node = ttn.list; - let vt: *syntax.node = tup.list; - for (dt != nil && vt != nil) { - if (vt.kind == syntax.nkind.N_ARRLIT) { - checkarrlitfits(c, dt.lhs, vt); - } else { - if (vt.kind == syntax.nkind.N_TUPLE) { - let drt: *syntax.node = resolvealias(c, unwrapbang(dt.lhs)); - if (drt != nil && drt.kind == syntax.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: *syntax.node, val: *syntax.node) bool = { - if (val == nil) { return false; }; - if (val.kind != syntax.nkind.N_ARRLIT) { return false; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dsttn)); - if (du == nil) { return false; }; - if (du.kind != syntax.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: *syntax.node, srctn: *syntax.node, val: *syntax.node) *syntax.node = { - if (dsttn == nil) { return val; }; - if (srctn == nil) { return val; }; - if (val == nil) { return val; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dsttn)); - let su: *syntax.node = resolvealias(c, unwrapbang(srctn)); - if (du == nil) { return val; }; - if (su == nil) { return val; }; - if (du.kind != syntax.nkind.N_TSLICE) { return val; }; - if (su.kind != syntax.nkind.N_TARRAY) { return val; }; - if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_SLICE, val.file, val.line, val.col); - sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) - let slt: *syntax.node = syntax.newnode(syntax.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: *syntax.node) *syntax.node = { - if (callee == nil) { return nil; }; - if (callee.kind == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_FN) { return s.decl; }; - }; - return nil; - }; - if (callee.kind == syntax.nkind.N_DOT) { - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, callee.lhs.str); - if (mu != nil) { ms = mu; }; - }; - if (ms != nil) { - if (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); - if (fs != nil) { - if (fs.skind == syntax.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: *syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(n.lhs.str, "free")) { - if (n.list != nil) { if (n.list.next == nil) { return; }; }; - }; - }; }; - let decl: *syntax.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: *syntax.node = decl.list; - let prev: *syntax.node = nil; - let a: *syntax.node = n.list; - for (a != nil) { - let nexta: *syntax.node = a.next; - if (param != nil) { - if (param.kind == syntax.nkind.N_PARAM) { - if (param.op != syntax.tkind.TK_ELLIPSIS) { - let atype: *syntax.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: *syntax.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 == syntax.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: *syntax.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 != syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - if (n.rhs == nil) { return; }; - // catB-22: reject reassigning a `const`-flagged binding (the - // is_const bit set at the let-install above). A bare `_` discard - // lvalue carries an empty str and is skipped. Mirror cstage - // cmd/wcc/check.c:1889-1896. - if (n.lhs.kind == syntax.nkind.N_IDENT) { - if (n.lhs.str.len > 0) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.lhs.str); - if (s != nil) { - if (s.is_const != 0i32) { - cerr("error: cannot assign to const binding\n"); - c.errs += 1; - }; - }; - }; - }; - let ltn: *syntax.node = exprtype(c, n.lhs, nil); - let rtn: *syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - if (n.lhs.kind != syntax.nkind.N_TARRAY) { return; }; - if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred - if (n.rhs == nil || n.rhs.kind != syntax.nkind.N_ARRLIT) { - cerr("error: [_]T needs an array-literal initialiser\n"); - c.errs += 1; - let z: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - z.uval = 0u64; - n.lhs.rhs = z; // sentinel: idempotent, error already raised - return; - }; - let cnt: u64 = 0u64; - let it: *syntax.node = n.rhs.list; - for (it != nil) { - let skip: bool = false; - if (it.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(it.str, "...")) { skip = true; }; - }; - if (!skip) { cnt += 1u64; }; - it = it.next; - }; - let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - cn.uval = cnt; - n.lhs.rhs = cn; -}; - -fn checkletassign(c: *checker, n: *syntax.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: *syntax.node = nil; - if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_TSLICE) { - let inner: *syntax.node = n.rhs; - if (inner.kind == syntax.nkind.N_TRYPROP) { - inner = inner.lhs; - } else { if (inner.kind == syntax.nkind.N_TRYUNW) { - inner = inner.lhs; - }; }; - if (inner != nil && inner.kind == syntax.nkind.N_CALL) { - let callee: *syntax.node = inner.lhs; - let a0: *syntax.node = inner.list; - let a1: *syntax.node = nil; - let a2: *syntax.node = nil; - if (a0 != nil) { a1 = a0.next; }; - if (a1 != nil) { a2 = a1.next; }; - if (callee != nil - && callee.kind == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0 != nil && a0.kind == syntax.nkind.N_ARRLIT - && a0.list == nil - && a1 != nil && a2 == nil) { - octx = inner; - }; - }; - }; - let savedoctx: *syntax.node = c.allococtx; - c.allococtx = octx; - let src: *syntax.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 == syntax.nkind.N_TSTRUCT - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && n.rhs.lhs != nil && n.rhs.lhs.kind == syntax.nkind.N_IDENT) { - let ltn: *syntax.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 == syntax.nkind.N_TSLICE) { - let wrapped: bool = false; - let inner: *syntax.node = n.rhs; - if (inner.kind == syntax.nkind.N_TRYPROP) { - wrapped = true; - inner = inner.lhs; - } else { if (inner.kind == syntax.nkind.N_TRYUNW) { - wrapped = true; - inner = inner.lhs; - }; }; - if (inner != nil && inner.kind == syntax.nkind.N_CALL) { - let callee: *syntax.node = inner.lhs; - let a0: *syntax.node = inner.list; - let a1: *syntax.node = nil; - let a2: *syntax.node = nil; - if (a0 != nil) { a1 = a0.next; }; - if (a1 != nil) { a2 = a1.next; }; - if (callee != nil - && callee.kind == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0 != nil && a0.kind == syntax.nkind.N_ARRLIT - && a0.list == nil - && a1 != nil && a2 == nil) { - let shadowed: bool = false; - if (c.curmod.len > 0) { - if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { - shadowed = true; - }; - }; - if (!shadowed) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = n.lhs.lhs; - if (wrapped) { - src = sl; - } else { - let nome: *syntax.node = mktname(c, "nomem"); - sl.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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: *syntax.node = resolvealias(c, unwrapbang(n.lhs)); - if (llhs != nil && llhs.kind == syntax.nkind.N_TARRAY && n.rhs.kind == syntax.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 == syntax.nkind.N_TTUPLE - && n.rhs.kind == syntax.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 == syntax.nkind.N_TSLICE - && n.rhs.kind == syntax.nkind.N_ARRLIT) { - let cnt: u64 = 0u64; - let e0: *syntax.node = n.rhs.list; - for (e0 != nil) { - let skip: bool = false; - if (e0.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e0.str, "...")) { skip = true; }; - }; - if (!skip) { cnt += 1u64; }; - e0 = e0.next; - }; - let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - cn.uval = cnt; - let arr: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { - // bare `return;` — mirror cstage cmd/wcc/check.c:2428-2439: - // the value-less return has type void, then type_assignable - // (c.fnret, void). A void fnret or a tagged union carrying a - // void variant accepts; a non-void scalar fnret rejects (the - // value-less return would RET a garbage register). isassignable - // reaches the same verdict (void→void typeeqast; void→tagged via - // the void variant; void→i32 a confident primitive mismatch). - if (c.fnret == nil) { return; }; - let vt: *syntax.node = mktname(c, "void"); - let vconf: bool = false; - let vok: bool = isassignable(c, c.fnret, vt, &vconf); - if (vconf) { if (!vok) { errnotassign(c, c.fnret, vt, "return"); }; }; - 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 == syntax.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: *syntax.node = resolvealias(c, unwrapbang(c.fnret)); - if (rrt != nil && rrt.kind == syntax.nkind.N_TTUPLE - && n.lhs.kind == syntax.nkind.N_TUPLE) { - checktuplearrfits(c, rrt, n.lhs); - }; - let src: *syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - // e is in n.lhs (value), T is in n.rhs (type expr). - let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) { - st = exprtype(c, n.lhs, nil); - }; - }; - let u: *syntax.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 == syntax.nkind.N_TYPEASSERT) { - let v: *syntax.node = resolvealias(c, unwrapbang(n.rhs)); - let lhsenum: bool = false; - let rhsenum: bool = false; - if (u != nil) { if (u.kind == syntax.nkind.N_TENUM) { lhsenum = true; }; }; - if (v != nil) { if (v.kind == syntax.nkind.N_TENUM) { rhsenum = true; }; }; - if (lhsenum || rhsenum) { - if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; }; - }; - }; - if (u.kind != syntax.nkind.N_TTAGGED) { - cerr("is/as: operand is not a tagged union\n"); - c.errs += 1; - return; - }; - let want: *syntax.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: *syntax.tinfo = tinfofornode(c, u); - let wanttinfo: *syntax.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 == syntax.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: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind == syntax.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: *syntax.sym = syntax.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 == syntax.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: *syntax.node = e.lhs; - if (callee == nil) { return nil; }; - let nm: str; - nm.ptr = nil; nm.len = 0; - if (callee.kind == syntax.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 == syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm); - if (s == nil) { return nil; }; - if (s.skind != syntax.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: *syntax.node, v: *syntax.node, nsuccp: *int, - haserrp: *bool, depth: i32) void = { - for (v != nil) { - if (v.op == syntax.tkind.TK_ELLIPSIS && depth < 8i32) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil && inner.kind == syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - let t: *syntax.node = exprtypeoftry(c, n.lhs); - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return; }; - if (u.kind != syntax.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 == syntax.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 != syntax.nkind.N_TRYPROP) { return; }; - if (!haserr) { return; }; - // Enclosing fn must return a tagged union with each operand - // error variant present. - let r: *syntax.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 != syntax.nkind.N_TTAGGED) { - cerr("?: enclosing fn return is not tagged\n"); - c.errs += 1; - return; - }; - let ev: *syntax.node = u.list; - for (ev != nil) { - if (iserrvariant(c, u, ev)) { - let found: bool = false; - let rv: *syntax.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: *syntax.node) void = { - let p: *syntax.node = params; - for (p != nil) { - if (p.kind == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - if (p.lhs != nil && p.lhs.kind != syntax.nkind.N_TSLICE) { - let sl: *syntax.node = syntax.newnode(syntax.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"); - syntax.scopedefine(c.cur, nm, syntax.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: *syntax.node) void = { - let outer: *syntax.scope = c.cur; - c.cur = syntax.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: *syntax.node = fnnode.list; - for (p != nil) { - if (p.kind == syntax.nkind.N_PARAM) { - if (p.lhs != nil) { resolvewalk(c, p.lhs); }; - }; - p = p.next; - }; - let prevret: *syntax.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: *syntax.node) bool = { - if (id == nil) { return false; }; - if (id.kind != syntax.nkind.N_IDENT) { return false; }; - if (!syntax.streq(id.str, "abort") && !syntax.streq(id.str, "assert")) { - return false; - }; - return syntax.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: *syntax.node, indot: bool) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - let isexpr: bool = - k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT || - k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT || - k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE || - k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT || - k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN || - k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL || - k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST || - k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT || - k == syntax.nkind.N_RECV || k == syntax.nkind.N_DOT || - k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD || - k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP || - k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST || - k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD || - k == syntax.nkind.N_MATCH; - let skip: bool = false; - if (isexpr && k == syntax.nkind.N_IDENT) { - if (indot) { skip = true; }; - if (!skip) { - let s: *syntax.sym = syntax.scopelookup(c.cur, n.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_USE) { skip = true; }; - if (s.decl == nil) { skip = true; }; - }; - }; - if (!skip) { if (isassertfam(c, n)) { skip = true; }; }; - }; - if (isexpr && k == syntax.nkind.N_CALL) { - if (isassertfam(c, n.lhs)) { skip = true; }; - }; - if (isexpr && !skip) { - if (n.type_ == nil) { - cerr("asserttyped: "); - let kn: str = syntax.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 == syntax.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: *syntax.node = n.list; - for (m != nil) { - asserttyped(c, m, false); - m = m.next; - }; -}; - -export fn checkinit(c: *checker, tc: *syntax.tctx) void = { - c.tc = tc; - c.top = syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - if (file.kind != syntax.nkind.N_FILE) { return; }; - c.file = file; - - // #80: under -T, PREPEND the synth `use test;` BEFORE Pass 1 so declmod - // (called per-decl during install) sees a matching `use test` when it - // keys lib/test's `run` — keying it under mod="test", not "". This is a - // pure ORDER fix: the synth N_USE was appended AFTER install (below), - // too late for declmod, so `run` keyed under "" — leaving the synth - // `test.run` ty_err (the E1 bridge) and colliding with a user root - // `fn run` (also mod=""). Decoupled from cgen: the symbol mangle keys - // off d.module (the //ww:module directive, cgen mod_collect), NOT this - // scope keying — cgen already emits the correct CALL test.run. Twin of - // cstage cmd/wcc/check.c. - if (c.istest != 0) { - let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); - usenode.str = "test"; - usenode.usepath = "test"; - usenode.next = file.list; - file.list = usenode; - }; - - // Pass 1: install all top-level names. - let d: *syntax.node = file.list; - for (d != nil) { - installdecl(c, file, d); - d = d.next; - }; - - // Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is - // the ROOT-unit main (imported==0) — it alone lowers to the bare - // `main` symbol. An IMPORTED package's `main` (imported==1) mangles on - // its path (`foo.bar.main`) and may coexist, closing the old dup-main - // collision by construction (#31). Two ROOT entries still collide on - // the bare symbol → reject loud (rule 7). Walks USER decls only — runs - // before the -T synth main is appended below. Twin of cmd/wcc/check.c. - let firstmain: *syntax.node = nil; - let mm: *syntax.node = file.list; - for (mm != nil) { - let ismain: bool = (mm.kind == syntax.nkind.N_FNDECL - || mm.kind == syntax.nkind.N_LET || mm.kind == syntax.nkind.N_DEF - || mm.kind == syntax.nkind.N_TYPEDECL) && syntax.streq(mm.str, "main"); - if (ismain && mm.imported == 0) { - if (firstmain == nil) { - firstmain = mm; - } else { - cerr(mm.file); - cerr(": error: duplicate entry main: only one root main may exist (#32)\n"); - c.errs += 1; - }; - }; - mm = mm.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: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_FNDECL && u.body != nil - && syntax.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 (syntax.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: *syntax.node = nil; - let rtail: *syntax.node = nil; - let ntest: i32 = 0; - let t: *syntax.node = file.list; - for (t != nil) { - if (t.kind == syntax.nkind.N_FNDECL) { - let istest: bool = false; - let at: *syntax.node = t.attr; - for (at != nil) { - if (at.kind == syntax.nkind.N_ATTR - && syntax.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 == syntax.nkind.N_TNAME - && syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_STRLIT, pf, pl, pc); - nm.str = t.str; - let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - id.str = t.str; - let amp: *syntax.node = syntax.newnode(syntax.nkind.N_UN, pf, pl, pc); - amp.op = syntax.tkind.TK_AMP; - amp.lhs = id; - let row: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, pf, pl, pc); - let tab: *syntax.node = nil; - if (ntest == 0i32) { - // no @test fns in this unit — exit 0, nothing to run. - let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); - let zero: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc); - e0.str = "str"; - let p0: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc); - p0.lhs = e0; - let vret: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc); - vret.str = "void"; - let vfn: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, pf, pl, pc); - vfn.lhs = vret; - let e1: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, pf, pl, pc); - e1.lhs = vfn; - let p1: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc); - p1.lhs = e1; - let tup: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, pf, pl, pc); - tup.list = p0; - p0.next = p1; - let tsl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, pf, pl, pc); - tsl.lhs = tup; - let arr: *syntax.node = syntax.newnode(syntax.nkind.N_ARRLIT, pf, pl, pc); - arr.list = rhead; - tab = syntax.newnode(syntax.nkind.N_LET, pf, pl, pc); - tab.op = syntax.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. - syntax.scopedefineinmodule(c.top, tab.str, "", syntax.skind.SK_VAR, nil, tab); - - let arg: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - arg.str = "__wwtests"; - let call: *syntax.node = syntax.newnode(syntax.nkind.N_CALL, pf, pl, pc); - // QUALIFIED test.run (N_DOT base ident `test`, member `run`): - // the sep producer sees lib/test as a real imported package, - // so the call must carry the module qualifier; the combined - // path tags auto-bundled lib/test `//ww:module test` too, so - // it resolves+mangles identically (test.run). Cstage twin: - // cmd/wcc/check.c synth. The base ident binds through the - // synth N_USE below. - let dot: *syntax.node = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc); - let did: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - did.str = "test"; - dot.lhs = did; - dot.str = "run"; - call.lhs = dot; - call.list = arg; - let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); - ret.lhs = call; - body.list = ret; - // #80: the synth `use test;` (the N_DOT base qualifier + the - // usepathfor source mapping `test`→its path) is now PREPENDED - // before Pass 1 at the top of checkfile, so declmod keys - // lib/test's `run` under "test" and the synth `test.run` - // type-resolves. It is installed (SK_USE) by Pass 1's N_USE arm. - }; - let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc); - m.str = "main"; - m.exported = 1i32; - let rety: *syntax.node = syntax.newnode(syntax.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: *syntax.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: syntax.nkind = d.kind; - switch (k) { - case syntax.nkind.N_FNDECL: - if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type - resolvefnbody(c, d); - case syntax.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 syntax.nkind.N_TYPEDECL: - if (d.lhs != nil) { resolvewalk(c, d.lhs); }; - case syntax.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: *syntax.node = nil; - let e: *syntax.node = file.list; - for (e != nil) { - let istest: bool = false; - if (e.kind == syntax.nkind.N_FNDECL) { - let at: *syntax.node = e.attr; - for (at != nil) { - if (at.kind == syntax.nkind.N_ATTR - && syntax.streq(at.str, "test")) { - istest = true; - }; - at = at.next; - }; - }; - let nx: *syntax.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; - -}; - -//ww:module io -// 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; - -//ww:module io -// 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; -}; - -//ww:module errors -// 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); -}; - -//ww:module io -// 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); - -//ww:module memio -// 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; a write to a full buffer returns nomem, matching Hare -// (ref/hare/memio/stream.ha:44,161). A zero-length write always -// succeeds with 0 (the empty-input short-circuit, stream.ha:157). -// -// 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 (buf.len == 0) { return 0: size; }; // ref/hare/memio/stream.ha:157 - if (m.pos >= m.len) { // ref/hare/memio/stream.ha:161 - let nm: nomem; - let e: io.error = nm; - return e; - }; - 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; -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node) *syntax.node = { - let s: *syntax.node = syntax.newnode(syntax.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: *syntax.node, nfixed_out: *i32) *syntax.node = { - *nfixed_out = 0; - let p: *syntax.node = ps; - for (p != nil) { - if (p.kind == syntax.nkind.N_PARAM) { - if (p.op == syntax.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: *syntax.node, nfixed_out: *i32) *syntax.node = { - *nfixed_out = 0; - if (callee == nil) { return nil; }; - let ps: *syntax.node = nil; - if (callee.kind == syntax.nkind.N_IDENT) { - if (callee.str.len == 0) { return nil; }; - ps = fnparamslookup(c, callee.str); - } else { if (callee.kind == syntax.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 == syntax.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: *syntax.node, param: *syntax.node) i32 = { - if (param == nil) { return 0; }; - if (param.kind != syntax.nkind.N_PARAM) { return 0; }; - if (param.op == syntax.tkind.TK_ELLIPSIS) { return 0; }; - let ptype: *syntax.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_: *syntax.tinfo) > 0) { return 0; }; - let arg: *syntax.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 == syntax.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 == syntax.nkind.N_INDEX) { - if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; - }; - if (arg.kind == syntax.nkind.N_DOT) { - if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; - }; - if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.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: *syntax.node, param: *syntax.node, memphase: bool) i32 = { - if (arg == nil) { return 0; }; - let nextparam: *syntax.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: *syntax.node = nil; - if (param != nil) { if (param.kind == syntax.nkind.N_PARAM) { - if (param.op != syntax.tkind.TK_ELLIPSIS) { - memptype = param.lhs; - if (memptype != nil) { - memsz = taggedmemargsize(memptype.type_: *syntax.tinfo); - }; - }; - }; }; - if (memsz == 0) { - memsz = taggedmemargsize(arg.type_: *syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - if (memptype != nil) { - let pt: *syntax.tinfo = memptype.type_: *syntax.tinfo; - let pu: *syntax.tinfo = pt; - pu = tichase(pu); - let au: *syntax.tinfo = at; - au = tichase(au); - if (pu != nil && pu == au) { same = true; }; - if (!same && pt != nil && at != nil) { - if (syntax.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_: *syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - widensz = 0; - } else { - let ptype: *syntax.node = param.lhs; - if (istaggedtype(c, ptype)) { - let aistagged: bool = false; - if (arg.kind == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) { - if (istaggedtype(c, arg)) { - if (slotsize(c, arg) == slotsize(c, ptype)) { - aistagged = true; - }; - }; - }; - if (!aistagged) { - widensz = slotsize(c, ptype); - let tagged: *syntax.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: *syntax.tinfo = arg.type_: *syntax.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 == syntax.tykind.TY_TUPLE) { - argistuple = true; - }; - if (argtup.kind == syntax.tykind.TY_TAGGED) { - argistagged = true; - }; - }; - let pname: str = rhsstructpayload(c, arg); - if (pname.len > 0 || argistuple || argistagged) { - let ptype: *syntax.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_: *syntax.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 == syntax.nkind.N_SLICE) { - let base: *syntax.node = arg.lhs; - let lo: *syntax.node = arg.rhs; - let hi: *syntax.node = arg.cond; - let baselocal: *local = nil; - let globaltn: *syntax.node = nil; - let globalname: str; - globalname.ptr = nil; globalname.len = 0; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - baselocal = localfindnode(c, bn); - if (baselocal == nil) { - let gt: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - dotbu = base.type_: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.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: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - // base address → push - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - let isarr60: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { isarr60 = true; }; - }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (bu60 != nil) { isarr60 = bu60.kind == syntax.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 == syntax.nkind.N_TARRAY; - // #60: alias-NAMED base — chased kind; runtime- - // unreachable until #77/#78 global DATA. - if (bu60 != nil) { gisarr60 = bu60.kind == syntax.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: *syntax.node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - }; - }; - } else { if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - } else { if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.tykind.TY_STR) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - };}; - }; - } else { if (globaltn != nil) { - if (globaltn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = globaltn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(globaltn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - }; - }; - } else { if (globaltn.kind == syntax.nkind.N_TSLICE) { - emitline("\tLEAQ\t"); - emitsymname(c, globalname); - emitline("(SB), CX\n"); - emitline("\tMOVQ\t8(CX), AX\n"); - } else { if (globaltn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - gst = tichase(gst); - if (gst != nil) { if (gst.kind == syntax.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 == syntax.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 == syntax.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_: *syntax.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 == syntax.nkind.N_IDENT) { - let st: *syntax.tinfo = arg.type_: *syntax.tinfo; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.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 == syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - if (syntax.typeisf32(at)) { fk = 1; } - else { if (syntax.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: *syntax.node = nil; - if (arg.kind == syntax.nkind.N_TUPLE) { if (param != nil) { - if (param.kind == syntax.nkind.N_PARAM && param.lhs != nil) { - let ptn: *syntax.node = param.lhs; - for (ptn != nil && ptn.kind == syntax.nkind.N_TNAME) { - ptn = aliaslookup(c, ptn.str); - }; - if (ptn != nil) { if (ptn.kind == syntax.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: *syntax.node = paramtt; - if (tuparg == nil) { tuparg = nodetuplearg(c, arg); }; - if (tuparg != nil) { - let tuplit: bool = false; - if (paramtt == nil) { if (tuparg.kind == syntax.nkind.N_TUPLE) { tuplit = true; }; }; - let gptot: i32 = 0; - let sstot: i32 = 0; - let tsz: i32 = 0; - let p: *syntax.node = tuparg.list; - for (p != nil) { - let et: *syntax.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: *syntax.tinfo = et.type_: *syntax.tinfo; - eti = tichase(eti); - if (eti != nil) { - let bad: bool = eti.kind == syntax.tykind.TY_TUPLE - || eti.kind == syntax.tykind.TY_STRUCT - || eti.kind == syntax.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 == syntax.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: *syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - ati = tichase(ati); - if (ati != nil) { - if (ati.kind == syntax.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 == syntax.nkind.N_INDEX || arg.kind == syntax.nkind.N_DOT - || (arg.kind == syntax.nkind.N_UN && arg.op == syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - if (n.kind != syntax.nkind.N_CALL) { return 0; }; - let callee: *syntax.node = n.lhs; - if (callee == nil) { return 0; }; - if (callee.kind != syntax.nkind.N_IDENT) { return 0; }; - let rtyp: *syntax.node = fnretlookup(c, callee.str); - if (!istaggedtype(c, rtyp)) { return 0; }; - return slotsize(c, rtyp); -}; - -fn nodeisslice(c: *cgen, n: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.nkind = n.kind; - if (k == syntax.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 syntax.typeisslice(n.type_: *syntax.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 syntax.typeisslice(n.type_: *syntax.tinfo); - }; - return false; - }; - if (k == syntax.nkind.N_SLICE) { return true; }; - if (k == syntax.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 == syntax.nkind.N_CALL) { - let callee: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod); - return isslicetype(c, rtyp); - }; - if (callee.kind == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - let rtyp: *syntax.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 == syntax.nkind.N_DOT) { - return syntax.typeisslice(n.type_: *syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisslice(n.type_: *syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.nkind = n.kind; - if (k == syntax.nkind.N_STRLIT) { return true; }; - if (k == syntax.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 syntax.typeisstr(n.type_: *syntax.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 syntax.typeisstr(n.type_: *syntax.tinfo); - }; - return false; - }; - if (k == syntax.nkind.N_CALL) { - let callee: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - let rtyp: *syntax.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 == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - let rtyp: *syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisstr(n.type_: *syntax.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 == syntax.nkind.N_DOT) { - return syntax.typeisstr(n.type_: *syntax.tinfo); - }; - if (k == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeis8byteprim(t.type_: *syntax.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: *syntax.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: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). - let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). - let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let k: syntax.nkind = t.kind; - let elem: *syntax.node = nil; - if (k == syntax.nkind.N_TPTR) { elem = t.lhs; }; - if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; }; - if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; }; - if (elem == nil) { return false; }; - if (k == syntax.nkind.N_TPTR) { - if (elem.kind == syntax.nkind.N_TARRAY) { - if (elem.lhs != nil) { elem = elem.lhs; }; - }; - }; - return elem.kind == syntax.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: *syntax.tinfo) *syntax.tinfo = { - let t: *syntax.tinfo = t0; - // peel-ok: chase body - for (t != nil && t.kind == syntax.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: *syntax.tinfo) bool = { - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return false; }; - return u.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeissigned(t.type_: *syntax.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: *syntax.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: *syntax.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: *syntax.node) str = { - if (tnode == nil) { return "MOVQ"; }; - let ti: *syntax.tinfo = tnode.type_: *syntax.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 = syntax.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: *syntax.tinfo) *syntax.tinfo = { - let t: *syntax.tinfo = t0; - t = tichase(t); - if (t != nil && t.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = t.sub; - p = tichase(p); - if (p != nil && p.kind == syntax.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: *syntax.node) *syntax.node = { - if (tn == nil) { return nil; }; - let k: syntax.nkind = tn.kind; - if (k != syntax.nkind.N_TARRAY && k != syntax.nkind.N_TSLICE - && k != syntax.nkind.N_TPTR) { return nil; }; - let elem: *syntax.node = tn.lhs; - if (k == syntax.nkind.N_TPTR && elem != nil - && elem.kind == syntax.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: *syntax.node) i32 = { - if (t == nil) { return 1; }; - let k: syntax.nkind = t.kind; - let elem: *syntax.node = nil; - if (k == syntax.nkind.N_TPTR) { elem = t.lhs; }; - if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; }; - if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; }; - if (k == syntax.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 (syntax.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 == syntax.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 == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; - if (elem.kind == syntax.nkind.N_TNAME) { - let nm: str = elem.str; - // str element is 16B (ptr+len). primsize returns 0 for it. - if (syntax.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: *syntax.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: syntax.nkind = t.kind; - let nest: *syntax.node = nil; - if (nk == syntax.nkind.N_TSLICE) { nest = t.lhs; }; - if (nk == syntax.nkind.N_TARRAY) { nest = t.lhs; }; - if (nest != nil && nest.kind == syntax.nkind.N_TARRAY) { - let eti: *syntax.tinfo = nest.type_: *syntax.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 == syntax.nkind.N_TNAME) { - let eff: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (eff != nil) { - if (eff.kind == syntax.tykind.TY_ARRAY - || eff.kind == syntax.tykind.TY_SLICE) { - let aes: *syntax.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: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti != nil) { - let esub: *syntax.tinfo = ti.sub; - esub = tichase(esub); - if (esub != nil) { return esub.size: i32; }; - }; - let elem: *syntax.node = idxelemtn(t); - if (elem == nil) { return direct; }; - if (elem.kind == syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.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 == syntax.nkind.N_IDENT) { - return syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_DOT) { - return syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_CAST) { - if (n.rhs == nil) { return false; }; - return syntax.typeisunsigned(n.rhs.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_BIN) { - if (nodeisunsigned(c, n.lhs)) { return true; }; - return nodeisunsigned(c, n.rhs); - }; - if (k == syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisunsigned(n.type_: *syntax.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 == syntax.nkind.N_CALL) { - return syntax.typeisunsigned(n.type_: *syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - let k: syntax.nkind = n.kind; - if (k == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, n.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); }; - }; - }; - return 0; - }; - if (k == syntax.nkind.N_CAST) { - let tn: *syntax.node = n.rhs; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); }; - }; - return 0; - }; - if (k == syntax.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: *syntax.tinfo = fi.tnode.type_: *syntax.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: *syntax.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 != syntax.nkind.N_TNAME && tn.kind != syntax.nkind.N_IDENT) { return nil; }; - let si: *structinfo = structlookup(c, tn.str); - if (si != nil) { return si; }; - let cur: *syntax.node = tn; - for (cur != nil - && (cur.kind == syntax.nkind.N_TNAME || cur.kind == syntax.nkind.N_IDENT) - && si == nil) { - let aliased: *syntax.node = aliaslookup(c, cur.str); - if (aliased == nil) { cur = nil; } - else { - if (aliased.kind == syntax.nkind.N_TNAME) { - si = structlookup(c, aliased.str); - cur = aliased; - } else { cur = nil; }; - }; - }; - return si; -}; - -export fn sretretsize(c: *cgen, t: *syntax.node) i32 = { - if (t == nil) { return 0; }; - let r: *syntax.node = t; - if (r.kind == syntax.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 == syntax.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: *syntax.node = r.list; - for (pt != nil) { - let et: *syntax.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: *syntax.tinfo = r.type_: *syntax.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 == syntax.nkind.N_TARRAY) { - let ati: *syntax.tinfo = r.type_: *syntax.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 != syntax.nkind.N_TNAME) { return 0; }; - // Primitives / aliased-to-primitives are never sret. - if (aliasprimsize(c, r.str) > 0) { return 0; }; - if (syntax.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: *syntax.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: *syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - if (n.kind != syntax.nkind.N_CALL) { return 0; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cn = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cn = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cn.len == 0) { return 0; }; - let rtyp: *syntax.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 (syntax.streq(s.sname, name)) { - if (syntax.streq(s.smod, c.curmod)) { return s; }; - }; - s = s.sinext; - }; - s = c.structs; - for (s != nil) { - let sn: str = s.sname; - if (syntax.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); - // M1 #22: the embedded qualifier (`utf8`) is the use ALIAS; - // the struct's smod is now the dotted import PATH - // (`encoding.utf8`). Map alias→path before comparing. - let pkgmod: str = usehint(c, pkg); - let b: *structinfo = c.structs; - for (b != nil) { - if (syntax.streq(b.sname, leaf)) { - if (syntax.streq(b.smod, pkgmod)) { - 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 (syntax.streq(s.sname, name)) { - if (syntax.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 (syntax.streq(name, "u8")) { return 1; }; - if (syntax.streq(name, "i8")) { return 1; }; - if (syntax.streq(name, "bool")) { return 1; }; - if (syntax.streq(name, "u16")) { return 2; }; - if (syntax.streq(name, "i16")) { return 2; }; - if (syntax.streq(name, "u32")) { return 4; }; - if (syntax.streq(name, "i32")) { return 4; }; - if (syntax.streq(name, "f32")) { return 4; }; - if (syntax.streq(name, "u64")) { return 8; }; - if (syntax.streq(name, "i64")) { return 8; }; - if (syntax.streq(name, "uint")) { return 8; }; - if (syntax.streq(name, "int")) { return 8; }; - if (syntax.streq(name, "uintptr")) { return 8; }; - if (syntax.streq(name, "size")) { return 8; }; - if (syntax.streq(name, "f64")) { return 8; }; - if (syntax.streq(name, "rune")) { return 4; }; - if (syntax.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: *syntax.node = aliaslookup(c, nm); - for (cur != nil) { - if (cur.kind != syntax.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: *syntax.node, - sz_out: *i32, unsigned_out: *bool) void = { - *sz_out = 0; - *unsigned_out = false; - let cur: *syntax.node = t; - for (cur != nil) { - let k: syntax.nkind = cur.kind; - if (k == syntax.nkind.N_TBANG) { cur = cur.lhs; } - else { if (k == syntax.nkind.N_TENUM) { cur = cur.lhs; } - else { if (k == syntax.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 (syntax.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 = syntax.typeisunsigned(cur.type_: *syntax.tinfo); - return; - }; - let al: *syntax.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: *syntax.node, - sz_out: *i32, unsigned_out: *bool) void = { - *sz_out = 0; - *unsigned_out = false; - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - if (k == syntax.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 = syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - }; - return; - }; - if (k == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, n.str); - if (lc != nil) { - typenodeprimresolved(c, lc.tnode, - sz_out, unsigned_out); - }; - return; - }; - if (k == syntax.nkind.N_CAST) { - typenodeprimresolved(c, n.rhs, sz_out, unsigned_out); - return; - }; - if (k == syntax.nkind.N_UN) { - exprprimresolved(c, n.lhs, sz_out, unsigned_out); - return; - }; - if (k == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { bu = n.lhs.type_: *syntax.tinfo; }; - bu = tichase(bu); - if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = tichase(bu.sub); }; - if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) { - let u: *syntax.tinfo = n.type_: *syntax.tinfo; - u = tichase(u); - if (syntax.typeisint(u)) { - *sz_out = u.size: i32; - *unsigned_out = syntax.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 (syntax.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 (syntax.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 (syntax.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: *syntax.node) *syntax.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: *syntax.node = rhs; - if (rhs.kind == syntax.nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; }; - if (rhs.kind == syntax.nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; }; - if (call == nil) { return nil; }; - if (call.kind != syntax.nkind.N_CALL) { return nil; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cname = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cname = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cname.len == 0) { return nil; }; - let rtyp: *syntax.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 != syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = tn.type_: *syntax.tinfo; - if (ti != nil) { ti = tichase(ti); }; - if (ti != nil && ti.kind == syntax.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: *syntax.node) i32 = { - if (typn == nil) { return 8; }; - let ti: *syntax.tinfo = typn.type_: *syntax.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: syntax.tykind = ti.kind; - if (kk == syntax.tykind.TY_VOID) { return 0; }; - if (kk == syntax.tykind.TY_PTR || kk == syntax.tykind.TY_SLICE || - kk == syntax.tykind.TY_CHAN || kk == syntax.tykind.TY_FN || - kk == syntax.tykind.TY_STR || kk == syntax.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 == syntax.tykind.TY_STRUCT) { - return ((ti.size + 7u64) & ~7u64): i32; - }; - if (kk == syntax.tykind.TY_TUPLE || kk == syntax.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: *syntax.node) i32 = { - if (tnode == nil) { return 8; }; - let ti: *syntax.tinfo = tnode.type_: *syntax.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: syntax.tykind = ti.kind; - if (k == syntax.tykind.TY_STRUCT) { return ti.slotsize: i32; }; - if (k == syntax.tykind.TY_ARRAY) { return ti.slotsize: i32; }; - if (k == syntax.tykind.TY_TAGGED) { return ti.size: i32; }; - if (k == syntax.tykind.TY_SLICE) { return ti.size: i32; }; - if (k == syntax.tykind.TY_PTR || k == syntax.tykind.TY_FN || - k == syntax.tykind.TY_CHAN) { return 8; }; - if (k == syntax.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: *syntax.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: *syntax.tinfo = tichase(tstruct.type_: *syntax.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: *syntax.node = tstruct.list; - let tf: *syntax.tfield = ti.fields; - for (f != nil) { - if (f.kind == syntax.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: *syntax.node) void = { - c.structs = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.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 == syntax.nkind.N_TBANG) { - body = body.lhs; - }; - if (body != nil) { - if (body.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisstr(t.type_: *syntax.tinfo); -}; - -fn isslicetype(c: *cgen, t: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisslice(t.type_: *syntax.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: *syntax.node) *syntax.node = { - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return nil; }; - if (r.kind == syntax.nkind.N_TBANG) { - let inner: *syntax.node = r.lhs; - if (inner == nil) { return nil; }; - r = resolvetype(c, inner); - if (r == nil) { return nil; }; - }; - if (r.kind == syntax.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: *syntax.node) *syntax.node = { - if (scrut == nil) { return nil; }; - let k: syntax.nkind = scrut.kind; - if (k == syntax.nkind.N_IDENT) { return nil; }; - if (k == syntax.nkind.N_CALL) { - let callee: *syntax.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 == syntax.nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == syntax.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 == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cnm.len > 0) { - let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); - if (rtyp != nil) { return resolvetagged(c, rtyp); }; - }; - }; - return nil; - }; - if (k == syntax.nkind.N_INDEX) { - let ibase: *syntax.node = scrut.lhs; - if (ibase == nil) { return nil; }; - if (ibase.kind != syntax.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: *syntax.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: *syntax.node = idxelemtn(btn); - if (etn == nil) { return nil; }; - return resolvetagged(c, etn); - }; - if (k == syntax.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 == syntax.nkind.N_UN && scrut.op == syntax.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: *syntax.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: *syntax.node) i32 = { - if (c == nil) { return 0; }; - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return 0; }; - if (r.kind != syntax.nkind.N_TNAME) { return 0; }; - let nm: str = r.str; - if (syntax.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: *syntax.tinfo) i32 = { - if (t == nil) { return 0; }; - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return 0; }; - if (u.kind == syntax.tykind.TY_STRUCT || u.kind == syntax.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: *syntax.tinfo) i32 = { - if (t == nil) { return 0; }; - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return 0; }; - if (u.kind != syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - return aggargsizetn(n.type_: *syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let st: *syntax.tinfo = n.type_: *syntax.tinfo; - st = tichase(st); - if (st == nil) { return false; }; - if (st.kind != syntax.tykind.TY_STRUCT) { return false; }; - if (st.size: i32 > 16) { return false; }; - let f: *syntax.tfield = st.fields; - for (f != nil) { - if (syntax.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: *syntax.node) i32 = { - if (c == nil) { return 0; }; - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return 0; }; - if (r.kind != syntax.nkind.N_TNAME) { return 0; }; - let nm: str = r.str; - if (syntax.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: *syntax.node = resolvetype(c, fi.tnode); - if (rf != nil) { - if (rf.kind == syntax.nkind.N_TARRAY) { return 0; }; - if (rf.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeistagged(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.tinfo = t.type_: *syntax.tinfo; - u = tichase(u); - if (u == nil) { return false; }; - if (u.kind == syntax.tykind.TY_STRUCT) { return true; }; - if (u.kind == syntax.tykind.TY_ARRAY) { return true; }; - return u.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisfloat(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisf32(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisnullable(t.type_: *syntax.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: *syntax.node) i32 = { - if (t == nil) { return 0; }; - let ti: *syntax.tinfo = t.type_: *syntax.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 != syntax.tykind.TY_TAGGED) { return 0; }; - let p: *syntax.tparam = ti.params; - let i: i32 = 0; - for (p != nil) { - let vt: *syntax.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 == syntax.tykind.TY_NAMED) { vt = vt.under; }; - if (vt != nil) { - if (vt.kind == syntax.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: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo; - if (ti == nil) { return -1; }; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let p: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - if (vt != nil && vt.kind == syntax.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: *syntax.node, rhs: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - return taggedvariantindext(c, tagged.type_: *syntax.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: *syntax.tinfo, rhs: *syntax.node) i32 = { - if (du == nil) { return -1; }; - if (rhs == nil) { return -1; }; - let ti: *syntax.tinfo = du; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let r: i32 = flatvariantidxt(ti, rhs.type_: *syntax.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: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - let visstr: bool = syntax.typeisstr(vt); - let visslice: bool = syntax.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: *syntax.node, pat: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - if (pat == nil) { return -1; }; - return flatvariantidxt(tagged.type_: *syntax.tinfo, pat.type_: *syntax.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: *syntax.tinfo, want: *syntax.tinfo, exactonly: bool) i32 = { - if (want == nil) { return -1; }; - let ti: *syntax.tinfo = tagged; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.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: *syntax.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 == syntax.tykind.TY_NAMED) { - let sb: *syntax.tinfo = tichase(want); - if (sb == nil) { return -1; }; - let q: *syntax.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 && syntax.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 != syntax.tykind.TY_NAMED) { - let q: *syntax.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: *syntax.tinfo = q.type_; - if (pu != nil && pu.kind == syntax.tykind.TY_NAMED - && syntax.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: *syntax.tinfo, want: *syntax.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: *syntax.tinfo = tichase(dst); - if (du != nil && du.kind == syntax.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: *syntax.tparam = du.params; - for (p != nil) { - let pu: *syntax.tinfo = tichase(p.type_); - let nestedtagged: bool = false; - if (pu != nil) { - if (pu.kind == syntax.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 == syntax.tykind.TY_UNTYPED_INT) { return syntax.typeisnum(dst); }; - if (want.kind == syntax.tykind.TY_UNTYPED_FLOAT) { return syntax.typeisfloat(dst); }; - if (want.kind == syntax.tykind.TY_UNTYPED_STR) { - if (du == nil) { return false; }; - return du.kind == syntax.tykind.TY_STR; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_RUNE) { - if (syntax.typeisint(dst)) { return true; }; - return dst.kind == syntax.tykind.TY_RUNE; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_BOOL) { - if (du == nil) { return false; }; - return du.kind == syntax.tykind.TY_BOOL; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_NIL) { - if (du == nil) { return false; }; - if (du.kind == syntax.tykind.TY_PTR) { return true; }; - if (du.kind == syntax.tykind.TY_SLICE) { return true; }; - if (du.kind == syntax.tykind.TY_CHAN) { return true; }; - return du.kind == syntax.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: *syntax.tinfo, want: *syntax.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 (syntax.typeisuntyped(want)) { return tyassignableuntyped(vt, want); }; - if (vt.kind == syntax.tykind.TY_NAMED && want.kind == syntax.tykind.TY_NAMED) { - return syntax.typeeq(vt, want); - }; - if (vt.kind == syntax.tykind.TY_NAMED || want.kind == syntax.tykind.TY_NAMED) { - let vu: *syntax.tinfo = vt; - vu = tichase(vu); - let wu: *syntax.tinfo = want; - wu = tichase(wu); - if (vu != nil && wu != nil - && vu.kind == syntax.tykind.TY_TAGGED - && wu.kind == syntax.tykind.TY_TAGGED) { - return syntax.typeeq(vu, wu); - }; - return false; - }; - return syntax.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: *syntax.tinfo, want: *syntax.tinfo) bool = { - let vu: *syntax.tinfo = vt; - vu = tichase(vu); - let wu: *syntax.tinfo = want; - wu = tichase(wu); - if (vu == nil) { return false; }; - if (wu == nil) { return false; }; - return syntax.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: *syntax.node, elem: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let want: *syntax.tinfo = nil; - if (elem != nil) { want = elem.type_: *syntax.tinfo; }; - let fallback: i32 = -1; - let p: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - if (vt != nil) { - if (syntax.typeisslice(vt)) { - if (fallback < 0) { fallback = idx; }; - if (want != nil) { - let su: *syntax.tinfo = vt; - su = tichase(su); - if (su != nil) { - if (syntax.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: *syntax.tinfo, su: *syntax.tinfo, slot_off: i32) void = { - let dt: *syntax.tinfo = du; - dt = tichase(dt); - if (dt == nil) { return; }; - if (dt.kind != syntax.tykind.TY_TAGGED) { return; }; - let st: *syntax.tinfo = su; - st = tichase(st); - if (st == nil) { return; }; - if (st.kind != syntax.tykind.TY_TAGGED) { return; }; - let identity: bool = true; - let p: *syntax.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: *syntax.node) str = { - let empty: str; - empty.ptr = nil; empty.len = 0; - if (src == nil) { return empty; }; - if (src.kind == syntax.nkind.N_STRUCTLIT) { - let trefn: *syntax.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 == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, src.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.node) *syntax.node = { - if (src == nil) { return nil; }; - if (src.kind != syntax.nkind.N_IDENT) { return nil; }; - let lc: *local = localfindnode(c, src.str); - if (lc == nil) { return nil; }; - let tn: *syntax.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: *syntax.node) bool = { - if (src == nil) { return false; }; - if (src.kind == syntax.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 (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; }; - return false; - }; - if (src.kind == syntax.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 (syntax.typeistagged(src.type_: *syntax.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 == syntax.nkind.N_DOT) { - if (syntax.typeistagged(src.type_: *syntax.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 == syntax.nkind.N_UN && src.op == syntax.tkind.TK_STAR) { - if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; }; - }; - if (src.kind == syntax.nkind.N_TRYPROP || src.kind == syntax.nkind.N_TRYUNW) { - if (syntax.typeistagged(src.type_: *syntax.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: *syntax.node) bool = { - if (e == nil) { return false; }; - if (e.kind == syntax.nkind.N_UN && e.op == syntax.tkind.TK_STAR) { - let du: *syntax.tinfo = e.type_: *syntax.tinfo; - du = tichase(du); - if (du == nil) { return false; }; - if (du.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (du.nullable != 0) { return false; }; - return du.size: i32 > 8; - }; - if (e.kind != syntax.nkind.N_INDEX && e.kind != syntax.nkind.N_DOT) { return false; }; - let u: *syntax.tinfo = e.type_: *syntax.tinfo; - u = tichase(u); - if (u == nil) { return false; }; - if (u.kind != syntax.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: *syntax.node) *syntax.node = { - for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) { - let cu: *syntax.tinfo = e.type_: *syntax.tinfo; - cu = tichase(cu); - if (cu == nil) { return e; }; - if (cu.kind != syntax.tykind.TY_TAGGED) { return e; }; - if (cu.nullable != 0) { return e; }; - let iu: *syntax.tinfo = e.lhs.type_: *syntax.tinfo; - iu = tichase(iu); - if (iu == nil) { return e; }; - if (iu.kind != syntax.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: *syntax.node) *syntax.node = { - for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) { - if (!syntax.typeeq(e.type_: *syntax.tinfo, e.lhs.type_: *syntax.tinfo)) { return e; }; - let cu: *syntax.tinfo = e.type_: *syntax.tinfo; - cu = tichase(cu); - if (cu == nil) { return e; }; - if (cu.kind != syntax.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: *syntax.tinfo, src: *syntax.node, - basereg: str, slot_off: i32, slot_sz: i32) void = { - if (syntax.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: *syntax.tinfo, src: *syntax.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: *syntax.tinfo = dst; - dt = tichase(dt); - if (dt == nil) { return; }; - if (dt.kind != syntax.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 == syntax.nkind.N_CAST) { - if (src.lhs != nil) { - let inner: *syntax.node = src.lhs; - let inneristagged: bool = false; - if (inner.kind == syntax.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: *syntax.node = src.rhs; - if (castrhs != nil) { - let castt: *syntax.tinfo = castrhs.type_: *syntax.tinfo; - let castu: *syntax.tinfo = castt; - castu = tichase(castu); - if (castu != nil) { - if (castu == dt) { - castisdst = true; - } else { if (castu.kind == syntax.tykind.TY_TAGGED) { - if (syntax.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: *syntax.tinfo = src.type_: *syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, src.str) == nil) { - let gtn51: *syntax.node = letvartnode(c, src.str); - if (gtn51 != nil) { if (istaggedtype(c, gtn51)) { srctagged = true; }; }; - }; - }; }; - if (srctagged) { - let nested: i32 = flatvariantidxt(dt, src.type_: *syntax.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: *syntax.tparam = dt.params; - for (gp != nil) { - if (cgvariantstructmatch(gp.type_, - src.type_: *syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.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_: *syntax.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 == syntax.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_: *syntax.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_: *syntax.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 == syntax.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 == syntax.nkind.N_CAST && su != nil - && su.kind == syntax.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: *syntax.node = nil; - let tupcast: bool = false; - if (src != nil) { - if (src.kind == syntax.nkind.N_TUPLE) { tupsrc = src; }; - if (src.kind == syntax.nkind.N_CAST) { - if (src.lhs != nil) { - if (src.lhs.kind == syntax.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 == syntax.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: *syntax.node = tupsrc.list; - for (bl != nil && !tupcast) { - let bare: bool = false; - if (bl.kind == syntax.nkind.N_TRUE) { bare = true; }; - if (bl.kind == syntax.nkind.N_FALSE) { bare = true; }; - if (bl.kind == syntax.nkind.N_INTLIT && bl.tsuffix.len == 0) { - bare = true; - }; - if (bl.kind == syntax.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_: *syntax.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: *syntax.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: *syntax.tinfo = ce.type_: *syntax.tinfo; - ceti = tichase(ceti); - if (ceti != nil && ceti.kind == syntax.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: *syntax.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: *syntax.tinfo = te.type_: *syntax.tinfo; - teti = tichase(teti); - if (teti != nil && teti.kind == syntax.tykind.TY_TAGGED) { - let ebox: i32 = (teti.size: i32 + 7) & ~7; - cgwidentaggedstore(c, te.type_: *syntax.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: *syntax.tinfo = te.type_: *syntax.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 == syntax.tykind.TY_TUPLE) { - let addrsrc: *syntax.node = src; - if (addrsrc.kind == syntax.nkind.N_CAST) { - if (addrsrc.lhs != nil) { addrsrc = addrsrc.lhs; }; - }; - let okkind: bool = false; - if (addrsrc.kind == syntax.nkind.N_IDENT) { okkind = true; }; - if (addrsrc.kind == syntax.nkind.N_INDEX) { okkind = true; }; - if (addrsrc.kind == syntax.nkind.N_UN) { - if (addrsrc.op == syntax.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_: *syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, src.str) == nil) { - let gtn: *syntax.node = letvartnode(c, src.str); - if (gtn != nil) { if (gtn.kind == syntax.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 == syntax.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: *syntax.tinfo = src.type_: *syntax.tinfo; - if (syntax.typeisf32(srct)) { fkind = 1; } - else { if (syntax.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: *syntax.tinfo = dt; - fti = tichase(fti); - if (fti != nil) { if (fti.kind == syntax.tykind.TY_TAGGED) { - let fp: *syntax.tparam = fti.params; - let fidx: i32 = 0; - for (fp != nil) { - let fvt: *syntax.tinfo = fp.type_; - fvt = tichase(fvt); - if (fvt != nil) { - if (syntax.typeisfloat(fvt)) { - if (syntax.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: *syntax.node, - outrootname: *str, outrootoff: *i32, outtotaloff: *i32, - outleaftype: **syntax.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 != syntax.nkind.N_DOT) { return false; }; - let stk: [16]*syntax.node; - let nsteps: i32 = 0; - let cur: *syntax.node = n; - for (cur != nil) { - if (cur.kind != syntax.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 != syntax.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 == syntax.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 == syntax.nkind.N_TPTR) { - let pe: *syntax.node = lc.tnode.lhs; - if (pe != nil) { - if (pe.kind == syntax.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: *syntax.tinfo = cur.type_: *syntax.tinfo; - curstruct = tichase(curstruct); - if (*outptrroot) { - if (curstruct == nil) { return false; }; - if (curstruct.kind != syntax.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 != syntax.tykind.TY_STRUCT) { return false; }; - if (stk[i] == nil) { return false; }; - let stepnm: str = stk[i].str; - let tf: *syntax.tfield = curstruct.fields; - let found: *syntax.tfield = nil; - for (tf != nil) { - if (syntax.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: *syntax.tinfo = found.type_; - ft = tichase(ft); - if (ft == nil) { return false; }; - if (ft.kind == syntax.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 (syntax.streq(pseudo, "ptr")) { delta = 0; } - else { if (syntax.streq(pseudo, "len")) { delta = 8; } - else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; }; - if (delta < 0) { return false; }; - *outtotaloff = *outtotaloff + foff; - *outslicedelta = delta; - return true; - }; - if (ft.kind == syntax.tykind.TY_SLICE) { - if (i != 1) { return false; }; - let pseudo: str = stk[0].str; - let delta: i32 = -1; - if (syntax.streq(pseudo, "ptr")) { delta = 0; } - else { if (syntax.streq(pseudo, "len")) { delta = 8; } - else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; }; - if (delta < 0) { return false; }; - *outtotaloff = *outtotaloff + foff; - *outslicedelta = delta; - return true; - }; - if (ft.kind != syntax.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: *syntax.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 == syntax.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: *syntax.node = lit.list; - for (fieldnode != nil) { - if (fieldnode.kind == syntax.nkind.N_FIELD) { - let fname: str = fieldnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.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_: *syntax.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 == syntax.nkind.N_STRUCTLIT) { - if (fi.tnode != nil) { - if (fi.tnode.kind == syntax.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 == syntax.nkind.N_CALL) { - if (fi.tnode != nil) { - if (fi.tnode.kind == syntax.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 == syntax.nkind.N_TARRAY - && fieldnode.lhs != nil - && fieldnode.lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = tichase(elemn.type_: *syntax.tinfo); - if (eu != nil) { - if (eu.kind == syntax.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 == syntax.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: *syntax.node = fieldnode.lhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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 == syntax.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: *syntax.tinfo = fi.tnode.type_: *syntax.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: *syntax.node, bpoff: i32) void = { - if (si == nil) { return; }; - cgstructlitfill(c, si, lit, 0, 0, "", bpoff); -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - - switch (k) { - case syntax.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 syntax.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 syntax.nkind.N_RUNELIT: - emitline("\tMOVQ\t$"); - emitint(n.uval: i64); - emitline(", AX\n"); - return; - case syntax.nkind.N_STRLIT: cgstrlit(c, n); return; - case syntax.nkind.N_TRUE: - emitline("\tMOVQ\t$1, AX\n"); - return; - case syntax.nkind.N_FALSE: - emitline("\tMOVQ\t$0, AX\n"); - return; - case syntax.nkind.N_NIL: - emitline("\tMOVQ\t$0, AX\n"); - return; - case syntax.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 syntax.nkind.N_IDENT: cgident(c, n); return; - case syntax.nkind.N_INDEX: cgindex(c, n); return; - case syntax.nkind.N_SLICE: cgslice(c, n); return; - case syntax.nkind.N_MATCH: cgmatch(c, n); return; - case syntax.nkind.N_CAST: cgcast(c, n); return; - case syntax.nkind.N_DOT: cgdot(c, n); return; - case syntax.nkind.N_UN: cgun(c, n); return; - case syntax.nkind.N_BIN: cgbin(c, n); return; - case syntax.nkind.N_CALL: cgcall(c, n); return; - case syntax.nkind.N_ASSIGN: cgassign(c, n); return; - case syntax.nkind.N_TRYPROP: cgtryprop(c, n); return; - case syntax.nkind.N_TRYUNW: cgtryunw(c, n); return; - case syntax.nkind.N_TYPETEST: cgtypetest(c, n); return; - case syntax.nkind.N_TYPEASSERT: cgtypeassert(c, n); return; - case syntax.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: *syntax.node, vt: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - if (vt == nil) { return -1; }; - if (tagged.kind != syntax.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: *syntax.tinfo = tagged.type_: *syntax.tinfo; - sti = tichase(sti); - if (sti != nil && sti.kind == syntax.tykind.TY_TAGGED && vt.type_ != nil) { - return flatvariantidxt(sti, vt.type_: *syntax.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 == syntax.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: *syntax.tinfo) i64 = { - let u: *syntax.tinfo = tichase(ou); - if (u == nil) { return 0i64; }; - if (u.kind != syntax.tykind.TY_TAGGED) { return 0i64; }; - let haserr: bool = false; - let p: *syntax.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: *syntax.tinfo) *syntax.tinfo = { - let u: *syntax.tinfo = tichase(ou); - if (u == nil) { return nil; }; - if (u.kind != syntax.tykind.TY_TAGGED) { return nil; }; - let st: i64 = successtag(ou); - let idx: i64 = 0i64; - let p: *syntax.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: *syntax.node) bool = { - if (n.lhs == nil) { return false; }; - let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - ou = tichase(ou); - if (ou == nil) { return false; }; - if (ou.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (ou.params == nil) { return false; }; - let sv: *syntax.tinfo = successvariant(ou); - sv = tichase(sv); - if (sv == nil) { return false; }; - if (sv.kind != syntax.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: *syntax.node) bool = { - if (n.lhs == nil) { return false; }; - let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - ou = tichase(ou); - if (ou == nil) { return false; }; - if (ou.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (ou.params == nil) { return false; }; - let sv: *syntax.tinfo = successvariant(ou); - sv = tichase(sv); - if (sv == nil) { return false; }; - if (sv.kind != syntax.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: syntax.tkind, unsignd: bool) void = { - if (op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_MINUSEQ) { - emitline("\tSUBQ\tAX, BX\n"); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - if (op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tBX, AX\n"); return; }; - if (op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tMOVQ\tBX, AX\n"); - emitline("\tSHLQ\tCX, AX\n"); - return; - }; - if (op == syntax.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: *syntax.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: *syntax.node, opname: str) void = { - let u: *syntax.tinfo = nil; - if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; }; - u = tichase(u); - let utag: bool = false; - if (u != nil) { - if (u.kind == syntax.tykind.TY_TAGGED && u.nullable == 0) { - utag = true; - }; - }; - if (utag && u.size: i32 > TUPLE_GPCAP * 8 - && n.lhs.kind != syntax.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 == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { propu = n.lhs.type_: *syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; }; - u = tichase(u); - let r: *syntax.tinfo = nil; - if (c.fnret != nil) { r = c.fnret.type_: *syntax.tinfo; }; - r = tichase(r); - if (u != nil && r != nil && r.kind == syntax.tykind.TY_TAGGED && u.params != nil) { - let propret: str; - propret.ptr = nil; propret.len = 0; - let haveret: bool = false; - let p: *syntax.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 = syntax.typeisstr(successvariant(n.lhs.type_: *syntax.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: *syntax.node) void = { - // #38b residual (rule 7): see the cgtryprop twin. - if (n.lhs != nil) { - if (n.lhs.kind == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { unwu = n.lhs.type_: *syntax.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 = syntax.typeisstr(successvariant(n.lhs.type_: *syntax.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: *syntax.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: *syntax.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 == syntax.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: *syntax.node = nil; - let nonident: bool = false; - let globalident: bool = false; - if (lhs != nil) { - if (lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - icu = tichase(icu); - if (lhs.kind == syntax.nkind.N_CAST && icu != nil - && icu.kind == syntax.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: *syntax.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 == syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = nil; - if (lhs != nil) { su = tichase(lhs.type_: *syntax.tinfo); }; - let vu: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - let lenum: bool = false; - let renum: bool = false; - if (su != nil) { if (su.kind == syntax.tykind.TY_ENUM) { lenum = true; }; }; - if (vu != nil) { if (vu.kind == syntax.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 == syntax.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: *syntax.node = nil; - if (lhs != nil) { - if (lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - acu = tichase(acu); - if (lhs.kind == syntax.nkind.N_CAST && acu != nil - && acu.kind == syntax.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: *syntax.node) void = { - let srcfk: i32 = 0; - if (n.lhs != nil) { - let st: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(st)) { srcfk = 1; } - else { if (syntax.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: *syntax.node = n.rhs; - for (leaf_tn != nil) { - let lk: syntax.nkind = leaf_tn.kind; - if (lk == syntax.nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; } - else { if (lk == syntax.nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; } - else { if (lk == syntax.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: *syntax.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 == syntax.nkind.N_TNAME) { - is_bool = syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = nil; - if (lc.tnode != nil) { itu = lc.tnode.type_: *syntax.tinfo; }; - itu = tichase(itu); - if (itu != nil) { if (itu.kind == syntax.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: *syntax.node = deflookuprhs(c, nm); - if (drhs != nil) { - if (drhs.kind == syntax.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"); - emitfnname(c, nm, c.curmod); - emitline("(SB), CX\n"); - emitline("\t"); - emitline(mov); - emitline("\t(CX), X0\n"); - return; - }; - emitline("\tMOVQ\t"); - emitfnname(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: *syntax.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: *syntax.node = letvartnode(c, nm); - for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) { - gtt = aliaslookup(c, gtt.str); - }; - if (gtt != nil) { - if (gtt.kind == syntax.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"); - emitfnname(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: *syntax.node = nil; - let lv: *letvar = c.lets; - for (lv != nil) { - if (syntax.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 == syntax.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"); - emitfnname(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 (syntax.streq(glop, "MOVQ")) { - emitline("\tMOVQ\t"); - emitfnname(c, nm, c.curmod); - emitline("(SB), AX\n"); - } else { - emitline("\tLEAQ\t"); - emitfnname(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 (!syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; - if (!syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; - if (!syntax.streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; - if (syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; - if (syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; - if (syntax.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: *syntax.node, dstreg: str) bool = { - if (n == nil) { return false; }; - if (n.kind == syntax.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 != syntax.nkind.N_DOT) { return false; }; - let x: *syntax.node = n.lhs; - if (x == nil) { return false; }; - let xu: *syntax.tinfo = x.type_: *syntax.tinfo; - xu = tichase(xu); - if (xu == nil) { return false; }; - let xviaptr: bool = false; - let st: *syntax.tinfo = nil; - if (xu.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = xu.sub; - p = tichase(p); - if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) { - st = p; - xviaptr = true; - }; }; - } else { if (xu.kind == syntax.tykind.TY_STRUCT) { - st = xu; - }; }; - if (st == nil) { return false; }; - let f: *syntax.tfield = st.fields; - let foff: i64 = -1; - for (f != nil) { - if (syntax.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: *syntax.node, dstreg: str) bool = { - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_DOT) { return false; }; - let inner: *syntax.node = base.lhs; - if (inner == nil) { return false; }; - let chained: bool = (inner.kind == syntax.nkind.N_DOT); - if (inner.kind != syntax.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: *syntax.tinfo = tichase(inner.type_: *syntax.tinfo); - if (ibu == nil || ibu.kind == syntax.tykind.TY_ERR) { - let gt: *syntax.node = letvartnode(c, base.str); - if (gt != nil && gt.kind == syntax.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: *syntax.tinfo = inner.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - let viaptr: bool = false; - let structt: *syntax.tinfo = nil; - if (bu.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = bu.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - structt = st; - viaptr = true; - }; }; - } else { if (bu.kind == syntax.tykind.TY_STRUCT) { - structt = bu; - }; }; - if (structt == nil) { return false; }; - let f: *syntax.tfield = structt.fields; - let foff: i64 = -1; - let ft: *syntax.tinfo = nil; - for (f != nil) { - if (syntax.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 != syntax.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: *syntax.node, dst: str) bool = { - if (src.kind == syntax.nkind.N_UN) { - if (src.op == syntax.tkind.TK_STAR) { - cgexpr(c, src.lhs); - if (!syntax.streq(dst, "AX")) { - emitline("\tMOVQ\tAX, "); - emitline(dst); - emitline("\n"); - }; - return true; - }; - }; - if (src.kind == syntax.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 == syntax.nkind.N_DOT) { - return dotchainaddr(c, src, dst); - }; - if (src.kind == syntax.nkind.N_INDEX) { - let base: *syntax.node = src.lhs; - let idx: *syntax.node = src.rhs; - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_IDENT) { return false; }; - let bu: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - if (bu.kind != syntax.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 (!syntax.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: *syntax.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: *syntax.tinfo = tichase(src.type_: *syntax.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: *syntax.node, dstreg: str) bool = { - if (n == nil) { return false; }; - if (n.kind == syntax.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: *syntax.node = defvartnode(c, n.str); - if (dtn != nil) { - if (dtn.kind == syntax.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 == syntax.nkind.N_UN) { - if (n.op != syntax.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 == syntax.nkind.N_INDEX) { - let base: *syntax.node = n.lhs; - let idx: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - if (bu.kind != syntax.tykind.TY_SLICE && bu.kind != syntax.tykind.TY_ARRAY) { - return false; - }; - let et: *syntax.tinfo = n.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = n.lhs; - if (base == nil) { return false; }; - let bu: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - let viaptr: bool = false; - let st: *syntax.tinfo = nil; - if (bu.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = bu.sub; - p = tichase(p); - if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) { - st = p; - viaptr = true; - }; }; - } else { if (bu.kind == syntax.tykind.TY_STRUCT) { - st = bu; - }; }; - if (st == nil) { return false; }; - let f: *syntax.tfield = st.fields; - let foff: i64 = -1; - for (f != nil) { - if (syntax.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: *syntax.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: *syntax.node = n.lhs; - let idx: *syntax.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 == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - if (et60 != nil) { - esz = et60.size: i32; - signed_elem = syntax.typeissigned(et60); - float_elem = syntax.typeisfloat(et60); - f32_elem = syntax.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: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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_: *syntax.tinfo); - }; - } else { if (base.kind == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - if (et != nil) { - esz = et.size: i32; - signed_elem = syntax.typeissigned(et); - elemisstr = syntax.typeisstr(et); - elemisslice = syntax.typeisslice(et); - float_elem = syntax.typeisfloat(et); - f32_elem = syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; signed_elem = syntax.typeissigned(dt); elemisstr = syntax.typeisstr(dt); elemisslice = syntax.typeisslice(dt); float_elem = syntax.typeisfloat(dt); f32_elem = syntax.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 == syntax.nkind.N_IDENT) { - let bl: *local = baselocal; - let etn: *syntax.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 != syntax.nkind.N_IDENT) { - let dt: *syntax.tinfo = n.type_: *syntax.tinfo; - if (dt != nil) { - if (syntax.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: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - if (eti != nil) { if (eti.kind == syntax.tykind.TY_TUPLE) { - elem_tuple = true; - // ww tuples store elements in .tupleelems, not .params. - let tpw: *syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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: *syntax.node, dst: str) bool = { - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_IDENT) { return false; }; - let baselocal: *local = localfindnode(c, base.str); - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - if (tn == nil) { return false; }; - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - return false; - }; - if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 16): i64); - emitline("(BP), "); - emitline(dst); - emitline("\n"); - return true; - }; - if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.tykind.TY_NAMED) { - let bu60: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - if (bu60.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.tykind.TY_STR) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 16): i64); - emitline("(BP), "); - emitline(dst); - emitline("\n"); - return true; - }; - }; - };}; - return false; - }; - let gt: *syntax.node = letvartnode(c, base.str); - if (gt == nil) { return false; }; - if (gt.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = gt.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - return false; - }; - if (gt.kind == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (gbt60 != nil) { if (gbt60.kind == syntax.tykind.TY_NAMED) { - let gbu60: *syntax.tinfo = tichase(gbt60); - if (gbu60 != nil) { - if (gbu60.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(gbu60.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - if (gbu60.kind == syntax.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: *syntax.node) i32 = { - if (base == nil) { return 1; }; - let esz: i32 = 1; - if (base.kind == syntax.nkind.N_IDENT) { - let bl: *local = localfindnode(c, base.str); - if (bl != nil) { - esz = elemsizeofc(c, bl.tnode); - } else { - let gt: *syntax.node = letvartnode(c, base.str); - if (gt != nil) { esz = elemsizeofc(c, gt); }; - }; - let bt: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt != nil) { if (bt.kind == syntax.tykind.TY_NAMED) { - let bu60: *syntax.tinfo = tichase(bt); - let es60: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - };}; - } else { if (base.kind == syntax.nkind.N_DOT) { - let db: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (db != nil) { if (db.sub != nil) { esz = db.sub.size: i32; }; }; - } else { if (base.kind == syntax.nkind.N_ARRLIT) { - esz = elemsizeofc(c, base.lhs); - };};}; - return esz; -}; - -fn cgslice(c: *cgen, n: *syntax.node) void = { - let base: *syntax.node = n.lhs; - let lo: *syntax.node = n.rhs; - let hi: *syntax.node = n.cond; - let baselocal: *local = nil; - let globaltn: *syntax.node = nil; - let globalname: str; - globalname.ptr = nil; globalname.len = 0; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - baselocal = localfindnode(c, base.str); - if (baselocal == nil) { - let gt: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - dotbu = base.type_: *syntax.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: *syntax.node = nil; - if (base != nil) { if (base.kind == syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.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: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - // base address - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; - }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (bu60 != nil) { isarray = bu60.kind == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node = baselocal.tnode; - let handled: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - handled = true; - }; - }; - } else { if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - handled = true; - } else { if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - handled = true; - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = globaltn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - handled = true; - }; - }; - } else { if (globaltn.kind == syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - handled = true; - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.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 == syntax.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: *syntax.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: *syntax.node = taggedidcastpeel(c, n.lhs); - let scrutoff: i32 = 0; - let scrutt: *syntax.node = nil; - if (scrut != nil) { - if (scrut.kind == syntax.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: *syntax.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"); - emitfnname(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 { - // M1 (#25): match on a tagged field of a BARE-ident VALUE - // struct reads the box IN PLACE at base.off + field.offset - // — the box (tag@+0, word0@+8, word1@+16) is contiguous in - // the parent frame, so no @match_spill copy. Verbatim mirror - // of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241- - // 10296): gate on N_DOT with a bare-IDENT base whose stamped - // type chases to TY_STRUCT and whose field resolves by name. - // A *ptr-field base (`match (h.e)`, h:*struct) chases to - // TY_PTR, the by-name scan misses, and it falls to the spill - // arm below — exactly as cstage, no separate deref guard. - // #29: the in-place arm is also gated on a CONFIRMED-LOCAL - // base (bglobal below). A global VALUE-struct base - // (`match (g.field)`) has localfind==0, so the M1 base.off - // would land in the saved-BP/return-addr region — both - // stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The - // global-base predicate `localfind==0 && isletvar` (cstage - // twin: let_islet, cgen.c:2000) routes it through to the - // spill `else`, which resolves g(SB). align-DOWN to cstage - // (rule 10): both stages emit TEXT $32 on the local-field - // case and the same g(SB) spill on the global-field case. - let mfld: *syntax.tfield = nil; - if (scrut.kind == syntax.nkind.N_DOT && scrut.lhs != nil - && scrut.lhs.kind == syntax.nkind.N_IDENT - && scrut.lhs.type_ != nil) { - let bu: *syntax.tinfo = tichase(scrut.lhs.type_: *syntax.tinfo); - if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) { - let fl: *syntax.tfield = bu.fields; - for (fl != nil) { - if (syntax.streq(fl.name, scrut.str)) { - mfld = fl; - break; - }; - fl = fl.tnext; - }; - }; - }; - let bglobal: bool = false; - if (mfld != nil) { - bglobal = (localfind(c, scrut.lhs.str) == 0) - && isletvar(c, scrut.lhs.str); - }; - if (mfld != nil && !bglobal) { - let foff: i32 = mfld.offset: i32; - scrutoff = localfind(c, scrut.lhs.str) + foff; - scrutt = matchscrutt(c, scrut); - } 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 == syntax.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: *syntax.tinfo = scrut.type_: *syntax.tinfo; - ms37 = tichase(ms37); - if (ms37 != nil && ms37.kind == syntax.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 == syntax.nkind.N_CAST && ms37 != nil - && ms37.kind == syntax.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: *syntax.node = n.list; - for (cs != nil) { - let nxt: str = mklabel(c, "match_next"); - let pat: *syntax.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 == syntax.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: *syntax.tinfo = pat.type_: *syntax.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 (syntax.typeisslice(pattype)) { - r = flatslicevariantidx(c, scrutt, pat.lhs); - } else { - r = flatvariantidxt(scrutt.type_: *syntax.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 == syntax.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: *syntax.node) void = { - let lhs: *syntax.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: *syntax.node = lhs; - if (dotlhs != nil) { - if (dotlhs.kind == syntax.nkind.N_UN) { - if (dotlhs.op == syntax.tkind.TK_STAR) { - if (dotlhs.lhs != nil) { - if (dotlhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) { - etname = lhs.str; - }; - if (lhs.kind == syntax.nkind.N_DOT) { - if (lhs.lhs != nil) { - if (lhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) { - let nm: str = dotlhs.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let tn: *syntax.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 == syntax.nkind.N_TNAME) { - if (structsamemod(c, tn.str) != nil) { break; }; - let nx: *syntax.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: syntax.nkind = tn.kind; - // Pointer-to-struct: deref then field load. - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.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 (syntax.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 == syntax.nkind.N_TARRAY) { - if (syntax.streq(fld, "ptr")) { - emitline("\tLEAQ\t"); - emitoff(lc.off: i64); - emitline("(BP), AX\n"); - return; - }; - if (syntax.streq(fld, "len")) { - let lenn: *syntax.node = tn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(fld); - if (idx >= 0) { - let tp: *syntax.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: *syntax.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: *syntax.tinfo = tpt.type_: *syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let innerkind: syntax.nkind = syntax.nkind.N_NONE; - if (inner != nil) { innerkind = inner.kind; }; - let innerstr: bool = false; - if (innerkind == syntax.nkind.N_TNAME) { - if (syntax.streq(inner.str, "str")) { innerstr = true; }; - }; - if (innerkind == syntax.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 == syntax.nkind.N_IDENT) { - let drhs: *syntax.node = deflookuprhs(c, lhs.str); - if (drhs != nil) { - if (drhs.kind == syntax.nkind.N_STRLIT) { - let bytes: str = drhs.str; - if (syntax.streq(fld, "ptr")) { - let lab: str = internstrlit(c, bytes); - emitline("\tLEAQ\t"); - emitbytes( lab.ptr, lab.len: u64); - emitline("(SB), AX\n"); - return; - }; - if (syntax.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 == syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.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 (syntax.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 == syntax.nkind.N_IDENT) { - let gtn: *syntax.node = letvartnode(c, lhs.str); - if (gtn != nil) { - if (gtn.kind == syntax.nkind.N_TARRAY) { - if (syntax.streq(fld, "ptr")) { - emitline("\tLEAQ\t"); - emitsymname(c, lhs.str); - emitline("(SB), AX\n"); - return; - }; - if (syntax.streq(fld, "len")) { - let lenn: *syntax.node = gtn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(gtn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(fld, "ptr")) { - let dtn: *syntax.node = defvartnode(c, lhs.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { - emitline("\tLEAQ\t"); - emitsymname(c, lhs.str); - emitline("(SB), AX\n"); - return; - }; - }; - }; - if (syntax.streq(fld, "len")) { - let dtn: *syntax.node = defvartnode(c, lhs.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = dtn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_IDENT) { - let gtt: *syntax.node = letvartnode(c, lhs.str); - for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) { - gtt = aliaslookup(c, gtt.str); - }; - if (gtt != nil) { - if (gtt.kind == syntax.nkind.N_TTUPLE) { - let gidx: i32 = fldnumidx(fld); - if (gidx >= 0) { - let gtp: *syntax.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: *syntax.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: *syntax.tinfo = gpt.type_: *syntax.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 == syntax.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 (syntax.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 == syntax.nkind.N_INDEX) { - let idxbase: *syntax.node = lhs.lhs; - if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) { - let elemt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - let elemu: *syntax.tinfo = elemt; - elemu = tichase(elemu); - let st: *syntax.tinfo = nil; - let viaptr: bool = false; - if (elemu != nil) { - if (elemu.kind == syntax.tykind.TY_PTR) { - let pin: *syntax.tinfo = elemu.sub; - pin = tichase(pin); - if (pin != nil) { if (pin.kind == syntax.tykind.TY_STRUCT) { - st = pin; - viaptr = true; - };}; - } else { if (elemu.kind == syntax.tykind.TY_STRUCT) { - st = elemu; - };}; - }; - if (st != nil) { - let fnd: *syntax.tfield = nil; - let fwalk: *syntax.tfield = st.fields; - for (fwalk != nil) { - if (syntax.streq(fwalk.name, fld)) { fnd = fwalk; break; }; - fwalk = fwalk.tnext; - }; - let bu: *syntax.tinfo = idxbase.type_: *syntax.tinfo; - bu = tichase(bu); - let baseisarray: bool = false; - let baseok: bool = false; - if (bu != nil) { - if (bu.kind == syntax.tykind.TY_ARRAY) { baseisarray = true; baseok = true; }; - if (bu.kind == syntax.tykind.TY_SLICE) { baseok = true; }; - if (bu.kind == syntax.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: *syntax.node = defvartnode(c, idxbase.str); - if (dtn != nil) { - if (dtn.kind == syntax.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: *syntax.tinfo = fnd.type_; - let fu: *syntax.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 == syntax.tykind.TY_ARRAY) { - if (foff != 0) { - emitline("\tADDQ\t$"); - emitint(foff); - emitline(", AX\n"); - }; - return; - }; - if (fu != nil && (fu.kind == syntax.tykind.TY_STR - || fu.kind == syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.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 == syntax.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(syntax.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 == syntax.nkind.N_INDEX) { - let eu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (eu != nil) { if (eu.kind == syntax.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: *syntax.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: *syntax.tinfo = tp.type_; - let fu: *syntax.tinfo = tichase(ft); - if (fu != nil && (fu.kind == syntax.tykind.TY_TAGGED - || fu.kind == syntax.tykind.TY_STRUCT - || fu.kind == syntax.tykind.TY_TUPLE - || fu.kind == syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(ft)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(foff: i64, "AX"); - emitline(", X0\n"); - return; - }; - if (fu != nil && (fu.kind == syntax.tykind.TY_STR - || fu.kind == syntax.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(syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, lhs.str)); - if (frt != nil) { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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: *syntax.node = deflookuprhsmod(c, fld, usehint(c, lhs.str)); - if (drhs != nil) { - if (drhs.kind == syntax.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 (syntax.streq(mqop, "MOVQ")) { - emitline("\tMOVQ\t"); - emitfnname(c, fld, usehint(c, lhs.str)); - emitline("(SB), AX\n"); - } else { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_DOT) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 (syntax.typeistagged(leaftype)) { - let tlu: *syntax.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 (syntax.typeisstr(leaftype) || syntax.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 (syntax.typeisfloat(leaftype)) { - let mov: str = "MOVSD"; - if (syntax.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(syntax.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: *syntax.tinfo = nil; - if (lhs != nil) { pbu = lhs.type_: *syntax.tinfo; }; - pbu = tichase(pbu); - let pbok: bool = false; - if (pbu != nil) { - if (pbu.kind == syntax.tykind.TY_SLICE - || pbu.kind == syntax.tykind.TY_STR - || pbu.kind == syntax.tykind.TY_UNTYPED_STR) { pbok = true; }; - }; - if (lhs != nil) { - if (lhs.kind == syntax.nkind.N_STRLIT) { pbok = true; }; - }; - if (pbok) { - if (syntax.streq(fld, "ptr")) { cgexpr(c, lhs); return; }; - if (syntax.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 (syntax.streq(fld, "cap")) { - cgexpr(c, lhs); - if (lhs != nil && lhs.kind != syntax.nkind.N_STRLIT) { - if (pbu != nil && (pbu.kind == syntax.tykind.TY_SLICE - || pbu.kind == syntax.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 == syntax.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: *syntax.node = lhs; - let allptr: bool = true; - for (croot != nil && croot.kind == syntax.nkind.N_DOT) { - let ct: *syntax.tinfo = croot.type_: *syntax.tinfo; - ct = tichase(ct); - let okp: bool = false; - if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) { - let cs: *syntax.tinfo = ct.sub; - cs = tichase(cs); - if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; }; - }; }; - if (!okp) { allptr = false; }; - croot = croot.lhs; - }; - let it: *syntax.tinfo = nil; - if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT && - localfindnode(c, croot.str) != nil) { - it = lhs.type_: *syntax.tinfo; - }; - it = tichase(it); - if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = it.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - let tf: *syntax.tfield = st.fields; - for (tf != nil) { - if (syntax.streq(tf.name, fld)) { - let ft: *syntax.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 (syntax.typeistagged(ft)) { - let plu: *syntax.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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(ft)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(tf.offset: i64, "AX"); - emitline(", X0\n"); - return; - }; - let lop: str = loadopsz(syntax.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 == syntax.nkind.N_DOT) { - let inner: *syntax.node = lhs.lhs; - let innerfld: str = lhs.str; - if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = tn.kind; - let outname: str; - outname.ptr = nil; outname.len = 0; - let isptr: bool = false; - if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) { - let oft: *syntax.node = ofi.tnode; - if (oft != nil) { if (oft.kind == syntax.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 (syntax.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 == syntax.nkind.N_DOT) { - let sbt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (sbt == nil || sbt.kind == syntax.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 == syntax.nkind.N_DOT) { - let pgu: *syntax.tinfo = lhs.type_: *syntax.tinfo; - pgu = tichase(pgu); - if (pgu != nil) { - if (pgu.kind == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - let rdu: *syntax.tinfo = rdt; - rdu = tichase(rdu); - if (rdu != nil) { - if (rdu.kind == syntax.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 == syntax.tykind.TY_STRUCT - || rdu.kind == syntax.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 (syntax.typeisfloat(rdt)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(rdt)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t(BX), X0\n"); - return; - }; - if (rdu != nil && rdu.kind == syntax.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 == syntax.tykind.TY_STR - || rdu.kind == syntax.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(syntax.typeissigned(rdt), rdt.slotsize: i32); - }; - emitline("\t"); - emitline(lop); - emitline("\t(BX), AX\n"); - return; - }; -}; - -fn cgun(c: *cgen, n: *syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(lt)) { fk = 1; } - else { if (syntax.typeisfloat(lt)) { fk = 2; }; }; - }; - if (n.op == syntax.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 == syntax.tkind.TK_AMP) { - let opnd: *syntax.node = n.lhs; - if (opnd != nil) { - if (opnd.kind == syntax.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 == syntax.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 == syntax.nkind.N_DOT) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 == syntax.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: *syntax.node = lc.tnode; - let lkind: syntax.nkind = syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.nkind.N_TNAME) { - let si: *structinfo = structlookupchain(c, tn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.streq(fld, "cap")) { delta = 16; }; - if (delta >= 0) { - let isslor: bool = false; - if (lkind == syntax.nkind.N_TSLICE) { isslor = true; }; - if (lkind == syntax.nkind.N_TNAME) { - if (syntax.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 (syntax.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 (syntax.streq(fld, "ptr")) { gdelta = 0; }; - if (syntax.streq(fld, "len")) { gdelta = 8; }; - // str IS []u8: &str.cap is valid too, not slice-only - // — mirrors cstage (#1/Phase 3, #11). - if (syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, basenm)); - if (frt != nil) { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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"); - emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_INDEX) { - // &base[i] = base + i*esz, no dereference. - let base: *syntax.node = opnd.lhs; - let idx: *syntax.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 == syntax.nkind.N_IDENT) { - baselocal = localfindnode(c, base.str); - if (baselocal != nil) { - esz = elemsizeofc(c, baselocal.tnode); - let tn: *syntax.node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - let basealias82: bool = false; - if (bt82 != nil) { - if (bt82.kind == syntax.tykind.TY_NAMED) { basealias82 = true; }; - }; - if (basealias82) { - let bu82: *syntax.tinfo = tichase(bt82); - if (bu82 != nil) { - if (baselocal != nil) { - isarr = bu82.kind == syntax.tykind.TY_ARRAY; - }; - if (isglobalarr || isglobalptr) { - isglobalarr = bu82.kind == syntax.tykind.TY_ARRAY; - isglobalptr = !isglobalarr; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.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: *syntax.tinfo = opnd.type_: *syntax.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 == syntax.tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; - if (n.op == syntax.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 == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil && rti.kind == syntax.tykind.TY_FN) { return; }; - if (rti != nil && rti.kind == syntax.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 == syntax.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 == syntax.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: *syntax.node) void = { - if (op.kind == syntax.nkind.N_IDENT) { - let nm: str = op.str; - let lc: *local = localfindnode(c, nm); - if (lc == nil && isletvar(c, nm)) { - emitline("\tLEAQ\t"); - emitfnname(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: *syntax.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 == syntax.tkind.TK_AND || n.op == syntax.tkind.TK_OR) { - let prefix: str = "andend"; - let jshrt: str = "JE"; - if (n.op == syntax.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 == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_NEQ) - && n.lhs != nil && n.rhs != nil - && syntax.typeisstr(n.lhs.type_: *syntax.tinfo) - && syntax.typeisstr(n.rhs.type_: *syntax.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 == syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(llt)) { lfk = 1; } - else { if (syntax.typeisfloat(llt)) { lfk = 2; }; }; - }; - let rfk: i32 = 0; - if (n.rhs != nil) { - let rrt: *syntax.tinfo = n.rhs.type_: *syntax.tinfo; - if (syntax.typeisf32(rrt)) { rfk = 1; } - else { if (syntax.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 == syntax.tkind.TK_PLUS || - n.op == syntax.tkind.TK_MINUS || - n.op == syntax.tkind.TK_STAR || - n.op == syntax.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 == syntax.tkind.TK_MINUS) { op = "SUBSD"; }; - if (n.op == syntax.tkind.TK_STAR) { op = "MULSD"; }; - if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSD"; }; - if (fk == 1) { - if (n.op == syntax.tkind.TK_PLUS) { op = "ADDSS"; }; - if (n.op == syntax.tkind.TK_MINUS) { op = "SUBSS"; }; - if (n.op == syntax.tkind.TK_STAR) { op = "MULSS"; }; - if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSS"; }; - }; - emitline("\t"); emitline(op); emitline("\tX1, X0\n"); - return; - }; - let isfcmp: bool = false; - if (n.op == syntax.tkind.TK_EQ) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_NEQ) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_LT) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_LE) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_GT) { isfcmp = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_LT || - n.op == syntax.tkind.TK_LE) { - // unordered -> false; otherwise the ordered Jcc decides. - let jcc: str = "JE"; - if (n.op == syntax.tkind.TK_LT) { jcc = "JB"; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_LSHIFT) { - emitline("\tMOVQ\tBX, CX\n"); - emitline("\tSHLQ\tCX, AX\n"); - return; - }; - if (n.op == syntax.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 == syntax.tkind.TK_EQ) { iscmp = true; jcc = "JE"; }; - if (n.op == syntax.tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; }; - if (n.op == syntax.tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; - if (n.op == syntax.tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; - if (n.op == syntax.tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; - if (n.op == syntax.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: *syntax.node) void = { - let v: *syntax.node = n.list; - let sz: i32 = 8; - let si: *structinfo = nil; - if (v.kind == syntax.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 == syntax.nkind.N_STRUCTLIT) { - if (si != nil) { - let f: *syntax.node = v.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_FIELD) { - let fname: str = f.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.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: *syntax.node) void = { - let sn: *syntax.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 == syntax.nkind.N_IDENT) { - snlocal = localfindnode(c, sn.str); - if (snlocal != nil) { - sndirect = true; - sn_off = snlocal.off; - }; - }; - let esz: i32 = 0; - let etnode: *syntax.node = nil; - let sti: *syntax.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: syntax.nkind = snlocal.tnode.kind; - if (stk == syntax.nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; - if (stk == syntax.nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; - if (stk == syntax.nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; - sti = snlocal.tnode.type_: *syntax.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_: *syntax.tinfo; - let fsti: *syntax.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: *syntax.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: *syntax.tinfo = esubti; - esubti = tichase(esubti); - let elstr: bool = esubti != nil && esubti.kind == syntax.tykind.TY_STR; - let elslice: bool = esubti != nil && esubti.kind == syntax.tykind.TY_SLICE; - let eltagged: bool = esubti != nil && esubti.kind == syntax.tykind.TY_TAGGED; - let elstruct: bool = esubti != nil && esubti.kind == syntax.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: *syntax.node = sn.next; - for (vn != nil) { - if (vn.kind == syntax.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: *syntax.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: *syntax.tinfo = nil; - if (it != nil) { itu = it.type_: *syntax.tinfo; }; - itu = tichase(itu); - if (itu == nil || (itu.kind != syntax.tykind.TY_SLICE - && itu.kind != syntax.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 == syntax.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(syntax.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: *syntax.node = nil; - let aidx: *syntax.node = nil; - if (elstruct && vn.kind != syntax.nkind.N_STRUCTLIT && vn.kind != syntax.nkind.N_IDENT) { - let ch: *syntax.node = vn; - let aok: bool = true; - for (aok && ch.kind == syntax.nkind.N_DOT) { - let ab: *syntax.node = ch.lhs; - let af: *syntax.tfield = nil; - if (ab != nil) { - let abu: *syntax.tinfo = ab.type_: *syntax.tinfo; - abu = tichase(abu); - if (abu != nil && abu.kind == syntax.tykind.TY_STRUCT) { - let fl: *syntax.tfield = abu.fields; - for (fl != nil) { - if (syntax.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 == syntax.nkind.N_INDEX) { - let ab: *syntax.node = ch.lhs; - let aet: *syntax.tinfo = ch.type_: *syntax.tinfo; - aet = tichase(aet); - let abu: *syntax.tinfo = nil; - if (ab != nil) { - abu = ab.type_: *syntax.tinfo; - abu = tichase(abu); - }; - if (ab == nil || abu == nil || aet == nil - || (abu.kind != syntax.tykind.TY_SLICE && abu.kind != syntax.tykind.TY_ARRAY)) { - aok = false; - } else { - abaseslice = abu.kind == syntax.tykind.TY_SLICE; - aidxesz = aet.size: i32; - aidx = ch.rhs; - ch = ab; - }; - }; - if (aok) { - if (ch.kind == syntax.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: *syntax.node = defvartnode(c, ch.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { gok = true; }; - }; - }; - if (!gok) { aok = false; }; - }; - } else { - if (ch.kind != syntax.nkind.N_UN || ch.op != syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_INDEX, checker-validated - let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_SLICE, checker-validated - let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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 == syntax.nkind.N_INDEX) { - if (base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_INDEX, checker-validated - let base: *syntax.node = d.lhs; - let v: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.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: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.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: *syntax.tinfo = callee.type_: *syntax.tinfo; - if (bti != nil && bti.kind == syntax.tykind.TY_ERR) { - if (syntax.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 (syntax.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: *syntax.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 (syntax.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 (syntax.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 (syntax.streq(callee.str, "len")) { - if (n.list != nil) { - let a: *syntax.node = n.list; - let at: *syntax.tinfo = a.type_: *syntax.tinfo; - let u: *syntax.tinfo = at; - u = tichase(u); - let hdrish: bool = false; - if (u != nil) { - if (u.kind == syntax.tykind.TY_SLICE - || u.kind == syntax.tykind.TY_STR) { - hdrish = true; - }; - }; - if (hdrish && a.kind == syntax.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"); - emitfnname(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 == syntax.nkind.N_DOT - && a.lhs != nil - && a.lhs.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.lhs.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(a.str); - if (idx >= 0) { - let tp: *syntax.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: *syntax.node = letvartnode(c, a.lhs.str); - for (gtn != nil && gtn.kind == syntax.nkind.N_TNAME) { - gtn = aliaslookup(c, gtn.str); - }; - if (gtn != nil) { - if (gtn.kind == syntax.nkind.N_TTUPLE) { - let gidx: i32 = fldnumidx(a.str); - if (gidx >= 0) { - let gtp: *syntax.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 == syntax.nkind.N_INDEX) { - cgexpr(c, a); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - if (u != nil) { - if (u.kind == syntax.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 (syntax.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 (syntax.streq(callee.str, "delete")) { - if (n.list != nil) { - if (n.list.next == nil) { - if (n.list.kind == syntax.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 (syntax.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: *syntax.node = nil; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - calleeparams = fnparamslookup(c, callee.str); - } else { if (callee.kind == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.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: *syntax.node = callee_variadic_param(c, callee, &nfixed_v); - if (varp != nil) { - let nargs0: i32 = 0; - let aw: *syntax.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: *syntax.node = n.list; - let kk: i32 = 0; - for (kk < nfixed_v) { - aaf = aaf.next; - kk += 1; - }; - if (aaf != nil) { - if (aaf.kind == syntax.nkind.N_SPREAD) { - forwarding = true; - }; - }; - }; - if (forwarding) { - let prev: *syntax.node = nil; - let cur2: *syntax.node = n.list; - let kk2: i32 = 0; - for (kk2 < nfixed_v) { - prev = cur2; - cur2 = cur2.next; - kk2 += 1; - }; - let inner: *syntax.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: *syntax.node = varp.lhs; - if (varp.lhs != nil - && varp.lhs.kind == syntax.nkind.N_TSLICE) { - velem = varp.lhs.lhs; - }; - let esz: i32 = 8; - if (velem != nil) { - if (velem.kind == syntax.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_: *syntax.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: *syntax.node = n.list; - let kk3: i32 = 0; - for (kk3 < nfixed_v) { - aa2 = aa2.next; - kk3 += 1; - }; - let j: i32 = 0; - let prevarg: *syntax.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_: *syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.node = n.list; - let dparam: *syntax.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 == syntax.nkind.N_PARAM) { - if (dparam.op != syntax.tkind.TK_ELLIPSIS) { - if (dparam.lhs != nil) { - dmemsz = taggedmemargsize(dparam.lhs.type_: *syntax.tinfo); - }; - }; - }; }; - if (dmemsz == 0) { - dmemsz = taggedmemargsize(a.type_: *syntax.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: *syntax.tinfo = a.type_: *syntax.tinfo; - if (syntax.typeisf32(at)) { fk = 1; } - else { if (syntax.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: *syntax.node = nil; - if (a.kind == syntax.nkind.N_TUPLE) { if (dparam != nil) { - if (dparam.kind == syntax.nkind.N_PARAM && dparam.lhs != nil) { - let dtn2: *syntax.node = dparam.lhs; - for (dtn2 != nil && dtn2.kind == syntax.nkind.N_TNAME) { - dtn2 = aliaslookup(c, dtn2.str); - }; - if (dtn2 != nil) { if (dtn2.kind == syntax.nkind.N_TTUPLE) { dptt = dtn2; }; }; - }; - }; }; - let tuparg: *syntax.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 == syntax.nkind.N_TUPLE) { tuplit = true; }; }; - let p: *syntax.node = tuparg.list; - for (p != nil) { - let et: *syntax.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 == syntax.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: *syntax.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_: *syntax.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 == syntax.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 != syntax.nkind.N_IDENT - && callee.kind != syntax.nkind.N_DOT) { - isfnptrcall = true; - }; - if (callee.kind == syntax.nkind.N_DOT) { - let base: *syntax.node = callee.lhs; - let fld: str = callee.str; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - let lkind: syntax.nkind = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == syntax.nkind.N_TNAME) { sname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.streq(fn_, fld)) { - let ft: *syntax.node = fi.tnode; - if (ft != nil) { - if (ft.kind == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { - hint = usehint(c, 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: *syntax.node) void = { - let lhs: *syntax.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 == syntax.nkind.N_SLICE - && n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil) { - if (lhs.lhs.kind == syntax.nkind.N_INDEX) { - placedotidx = true; - }; - }; - if ((lhs.kind == syntax.nkind.N_INDEX - || (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR) - || placedotidx) - && n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil) { - if (n.rhs.kind == syntax.nkind.N_STRUCTLIT) { - let iet: *syntax.tinfo = lhs.type_: *syntax.tinfo; - iet = tichase(iet); - if (iet != nil) { - if (iet.kind == syntax.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 == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR - && n.op == syntax.tkind.TK_ASSIGN) { - let du: *syntax.tinfo = lhs.type_: *syntax.tinfo; - du = tichase(du); - if (du != nil) { - if (du.kind == syntax.tykind.TY_STRUCT - || du.kind == syntax.tykind.TY_ARRAY - || du.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (lhs.str.len == 0) { - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { - if (n.rhs.kind == syntax.nkind.N_ARRLIT) { - let alt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - alt = tichase(alt); - if (alt != nil) { - if (alt.kind == syntax.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 == syntax.nkind.N_IDENT && n.op != syntax.tkind.TK_ASSIGN) { - let itu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (itu != nil) { - if (itu.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (n.op == syntax.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 == syntax.nkind.N_CALL) { - asret = callsretsize(c, n.rhs); - }; - }; - if (asret > 0) { - let aru: *syntax.tinfo = n.rhs.type_: *syntax.tinfo; - aru = tichase(aru); - let alu: *syntax.tinfo = lc.tnode.type_: *syntax.tinfo; - alu = tichase(alu); - let aexact: bool = false; - if (aru != nil && aru == alu) { aexact = true; } - else { - if (syntax.typeeq(n.rhs.type_: *syntax.tinfo, - lc.tnode.type_: *syntax.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_: *syntax.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 == syntax.nkind.N_CALL) { - let gru: *syntax.tinfo = lhs.type_: *syntax.tinfo; - gru = tichase(gru); - if (gru != nil && gru.kind == syntax.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: *syntax.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_: *syntax.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 == syntax.nkind.N_UN) { - if (lhs.op == syntax.tkind.TK_STAR) { - if (n.op == syntax.tkind.TK_ASSIGN && !placeslit - && !derefagg) { - let inner: *syntax.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: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (du != nil && du.kind == syntax.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 == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { - if (pe.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(pe.str, "str")) { elemstr = true; } - else { if (syntax.streq(pe.str, "f64")) { elemfloat = true; } - else { if (syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (lhs.op == syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (lt != nil) { psz = lt.size: i32; }; - let scalarpointee: bool = (psz == 1 || psz == 2 || psz == 4 || psz == 8); - if (n.op != syntax.tkind.TK_ASSIGN && scalarpointee) { - let inner: *syntax.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: *syntax.node = nil; - if (inner != nil) { - if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.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 = syntax.typeisunsigned(pe.type_: *syntax.tinfo); - }; - if (!unsignd) { - unsignd = nodeisunsigned(c, n.rhs); - }; - if (n.op == syntax.tkind.TK_SLASHEQ || n.op == syntax.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 == syntax.tkind.TK_PERCENTEQ) { - emitline("\tMOVQ\tDX, AX\n"); - }; - emitline("\t"); - emitline(storeop); - emitline("\tAX, (BX)\n"); - return; - }; - let combineop: str = "MOVQ"; - if (n.op == syntax.tkind.TK_PLUSEQ) { combineop = "ADDQ"; } - else { if (n.op == syntax.tkind.TK_MINUSEQ) { combineop = "SUBQ"; } - else { if (n.op == syntax.tkind.TK_STAREQ) { combineop = "IMULQ"; } - else { if (n.op == syntax.tkind.TK_AMPEQ) { combineop = "ANDQ"; } - else { if (n.op == syntax.tkind.TK_PIPEEQ) { combineop = "ORQ"; } - else { if (n.op == syntax.tkind.TK_CARETEQ) { combineop = "XORQ"; } - else { if (n.op == syntax.tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; } - else { if (n.op == syntax.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 == syntax.nkind.N_INDEX && !placeslit) { - if (n.op == syntax.tkind.TK_ASSIGN) { - let base: *syntax.node = lhs.lhs; - let idx: *syntax.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: *syntax.node = nil; - let basealias: bool = false; - if (base != nil) { - if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) { - isglobalarr = true; - } else { - isglobalptr = true; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; - } else { if (base.kind == syntax.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: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.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: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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_: *syntax.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: *syntax.node = baselocal.tnode; - let isarr: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - isarr = true; - }; - }; - // #60: alias-NAMED base — chased kind - // (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr = bu60.kind == syntax.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 == syntax.nkind.N_CALL - && callsretsize(c, n.rhs) > 0) { - let islocalarr: bool = false; - if (baselocal != nil) { - let btn: *syntax.node = baselocal.tnode; - if (btn != nil) { - if (btn.kind == syntax.nkind.N_TARRAY) { islocalarr = true; }; - }; - }; - let constidx: bool = false; - let cidx: i32 = 0; - if (idx != nil) { - if (idx.kind == syntax.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 == syntax.nkind.N_TUPLE && esz > 8 - && base != nil && base.kind == syntax.nkind.N_IDENT - && elemtn != nil && elemtn.kind == syntax.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: *syntax.node = elemtn.list; - for (q121 != nil) { - let qt: *syntax.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: *syntax.node = baselocal.tnode; - let isarr2: bool = false; - if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; }; - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_IDENT) - || (n.rhs.kind == syntax.nkind.N_DOT) - || (n.rhs.kind == syntax.nkind.N_UN - && n.rhs.op == syntax.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: *syntax.node = baselocal.tnode; - let isarr2: bool = false; - if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_UN - && n.rhs.op == syntax.tkind.TK_STAR) { - cgexpr(c, n.rhs.lhs); - emitline("\tMOVQ\tAX, SI\n"); - } else { if (n.rhs.kind == syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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 != syntax.tkind.TK_ASSIGN) { - let base: *syntax.node = lhs.lhs; - let idx: *syntax.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: *syntax.node = nil; - if (base != nil) { - if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) { - isglobalarr = true; - } else { - isglobalptr = true; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.tkind.TK_STAR)) { - // N_UN deref base (`(*p)[i] OP= v`, #61 C): - // same stamped source as the store arm. - let dt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; - } else { if (base.kind == syntax.nkind.N_INDEX) { - let et: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (et60 != nil) { - esz = et60.size: i32; - elemtn = lhs; - }; - if (isglobalarr || isglobalptr) { - let bu60: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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 = syntax.typeisunsigned(elemtn.type_: *syntax.tinfo); - }; - }; - let wired: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == syntax.nkind.N_INDEX && !placeslit) { - let idxbase: *syntax.node = lhs.lhs.lhs; - let idx: *syntax.node = lhs.lhs.rhs; - let fld2: str = lhs.str; - if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) { - if (idx != nil) { - let lc: *local = localfindnode(c, idxbase.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - // idxelemtn: `*[N]T` drills to the pointee - // array's element (#61). - let elemt: *syntax.node = idxelemtn(tn); - let baseisarray: bool = tn.kind == syntax.nkind.N_TARRAY; - let snode: *syntax.node = nil; - let viaptr: bool = false; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = elemt.lhs; - if (inner != nil) { if (inner.kind == syntax.nkind.N_TNAME) { - snode = inner; - viaptr = true; - };}; - } else { if (elemt.kind == syntax.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 (syntax.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 == syntax.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 (syntax.typeisfloat(n.rhs.type_: *syntax.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_: *syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); - }; - }; - let wired_x: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR) { - if (base.lhs != nil) { - if (base.lhs.kind == syntax.nkind.N_IDENT) { - base = base.lhs; - }; - }; - }; - }; - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = syntax.nkind.N_NONE; - if (tn != nil) { lkind = tn.kind; }; - // Pointer-to-struct: deref then store. - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.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_: *syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 != syntax.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 != syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); }; }; - cgdotfieldcombine(c, n.op, uns34); - }; - if (n.op == syntax.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 == syntax.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 (syntax.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 == syntax.tkind.TK_ASSIGN - && istaggedtype(c, fi.tnode)) { - let fsz: i32 = slotsize(c, fi.tnode); - cgwidentaggedstore(c, fi.tnode.type_: *syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 != syntax.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 != syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.streq(fld, "cap")) { delta = 16; }; - if (delta >= 0) { - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let innerkind: syntax.nkind = syntax.nkind.N_NONE; - if (inner != nil) { innerkind = inner.kind; }; - let innerstr: bool = false; - if (innerkind == syntax.nkind.N_TNAME) { - if (syntax.streq(inner.str, "str")) { innerstr = true; }; - }; - if (innerkind == syntax.nkind.N_TSLICE) { innerstr = true; }; - if (innerstr) { - if (n.op != syntax.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 != syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.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 (syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.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_: *syntax.tinfo, - n.rhs, "BX", fi.foff, fsz); - return; - }; - if (n.op == syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.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: *syntax.node = base; - let allptr: bool = true; - for (croot != nil && croot.kind == syntax.nkind.N_DOT) { - let ct: *syntax.tinfo = croot.type_: *syntax.tinfo; - ct = tichase(ct); - let okp: bool = false; - if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) { - let cs: *syntax.tinfo = ct.sub; - cs = tichase(cs); - if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; }; - }; }; - if (!okp) { allptr = false; }; - croot = croot.lhs; - }; - let it: *syntax.tinfo = nil; - if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT && - localfindnode(c, croot.str) != nil) { - it = base.type_: *syntax.tinfo; - }; - it = tichase(it); - if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = it.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - let tf: *syntax.tfield = st.fields; - for (tf != nil) { - if (syntax.streq(tf.name, fld)) { - let ft: *syntax.tinfo = tf.type_; - if (n.op == syntax.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 (syntax.typeistagged(ft)) { - let flu: *syntax.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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.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 != syntax.tkind.TK_ASSIGN) { - if (syntax.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 (syntax.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 (syntax.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 (syntax.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 = syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == syntax.nkind.N_DOT - && n.op == syntax.tkind.TK_ASSIGN) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 (syntax.typeistagged(leaftype)) { - let wlu: *syntax.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 (syntax.typeisstr(leaftype) || syntax.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: *syntax.tinfo = leaftype; - lp = tichase(lp); - if (lp != nil) { - if (lp.kind == syntax.tykind.TY_STRUCT) { leafstruct = true; }; - }; - if (leaftype.kind == syntax.tykind.TY_NAMED) { - leafname = leaftype.name; - }; - }; - if (n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.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 == syntax.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 (syntax.typeisfloat(leaftype)) { - let mov: str = "MOVSD"; - if (syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - let inner: *syntax.node = base.lhs; - let innerfld: str = base.str; - if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = tn.kind; - let outname: str; - outname.ptr = nil; outname.len = 0; - let isptr: bool = false; - if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) { - let oft: *syntax.node = ofi.tnode; - if (oft != nil) { if (oft.kind == syntax.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 (syntax.streq(ffi.fname, fld)) { - if (n.op == syntax.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 == syntax.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: *syntax.node = nil; - for (lvf != nil) { - if (syntax.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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; }; - if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; }; - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil) { - let gsz: i32 = 0; - if (lvftn.kind == syntax.nkind.N_TNAME) { - let gsi: *structinfo = structlookup(c, lvftn.str); - if (gsi != nil) { gsz = structabisize(gsi); }; - }; - if (lvftn.kind == syntax.nkind.N_TARRAY) { - let gat: *syntax.tinfo = lvftn.type_: *syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil - && lvftn.kind == syntax.nkind.N_TARRAY) { - let aggsz: i32 = 0; - let aat: *syntax.tinfo = lvftn.type_: *syntax.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 == syntax.tkind.TK_ASSIGN && lvftn != nil) { - let gau: *syntax.tinfo = lvftn.type_: *syntax.tinfo; - gau = tichase(gau); - if (gau != nil) { - if (gau.kind == syntax.tykind.TY_STRUCT - || gau.kind == syntax.tykind.TY_ARRAY - || gau.kind == syntax.tykind.TY_TUPLE) { - if (n.rhs != nil && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { - let gsi49: *structinfo = nil; - if (lvftn.kind == syntax.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 == syntax.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 == syntax.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 (syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tSHLQ\tCX, BX\n"); - } - else { if (n.op == syntax.tkind.TK_RSHIFTEQ) { - // #136: signed RSHIFTEQ → SARQ. - let unsignd_r: bool = false; - if (lvftn != nil) { - if (lvftn.type_ != nil) { - unsignd_r = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) { - let unsignd: bool = false; - if (lvftn != nil) { - unsignd = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL) { - let rtup: *syntax.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: *syntax.node = lcn.tnode; - let lcsname: str; - lcsname.ptr = nil; lcsname.len = 0; - if (lctn != nil) { - if (lctn.kind == syntax.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 == syntax.tkind.TK_ASSIGN) { - if (n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL) { - let acati: *syntax.tinfo = nil; - if (lcn.tnode != nil) { acati = lcn.tnode.type_: *syntax.tinfo; }; - acati = tichase(acati); - if (acati != nil && acati.kind == syntax.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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; }; - if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; }; - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN) { - let agu: *syntax.tinfo = nil; - if (lcn != nil) { - if (lcn.tnode != nil) { - agu = lcn.tnode.type_: *syntax.tinfo; - }; - }; - agu = tichase(agu); - if (agu != nil) { - if (agu.kind == syntax.tykind.TY_STRUCT - || agu.kind == syntax.tykind.TY_ARRAY - || agu.kind == syntax.tykind.TY_TUPLE) { - if (n.rhs != nil && n.rhs.kind == syntax.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 == syntax.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 == syntax.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 (syntax.streq(llop, "MOVQ")) { - if (n.op == syntax.tkind.TK_PLUSEQ) { - emitline("\tADDQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - return; - }; - if (n.op == syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tSHLQ\tCX, BX\n"); - }; - if (n.op == syntax.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 = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) { - let unsignd: bool = false; - if (lcn != nil) { - if (lcn.tnode != nil) { - unsignd = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT || lhs.kind == syntax.nkind.N_INDEX - || (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR)) { - let ft: *syntax.tinfo = lhs.type_: *syntax.tinfo; - let fu: *syntax.tinfo = ft; - fu = tichase(fu); - let fsz: i32 = 8; - if (ft != nil) { fsz = ft.size: i32; }; - if (syntax.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 == syntax.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 == syntax.tykind.TY_STRUCT - || fu.kind == syntax.tykind.TY_ARRAY - || fu.kind == syntax.tykind.TY_TUPLE) { - if (n.op != syntax.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 == syntax.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 == syntax.nkind.N_STRUCTLIT - && fu.kind == syntax.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 == syntax.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 == syntax.tykind.TY_STR - || fu.kind == syntax.tykind.TY_SLICE) { - fstrsl = true; - }; - }; - if (fstrsl) { - if (n.op != syntax.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 == syntax.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(syntax.typeissigned(ft), fsz); - emitline("\t"); - emitline(lop); - emitline("\t(BX), AX\n"); - emitline("\tPOPQ\tCX\n"); - let unsignd: bool = syntax.typeisunsigned(ft); - let wired: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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; -}; - - - -//ww:module-reset -// 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 syntax; -import strconv; - -// ---- statement cgen -------------------------------------------------- - -fn cgstmt(c: *cgen, n: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - - if (k == syntax.nkind.N_BLOCK) { cgblock(c, n); return; }; - - if (k == syntax.nkind.N_RETURN) { cgreturn(c, n); return; }; - - if (k == syntax.nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; }; - - if (k == syntax.nkind.N_LET) { cglet(c, n); return; }; - - if (k == syntax.nkind.N_IF) { cgif(c, n); return; }; - - if (k == syntax.nkind.N_FOR) { cgfor(c, n); return; }; - - if (k == syntax.nkind.N_FORRANGE) { cgforrange(c, n); return; }; - - if (k == syntax.nkind.N_SWITCH) { cgswitch(c, n); return; }; - - if (k == syntax.nkind.N_MASSIGN) { cgmassign(c, n); return; }; - - if (k == syntax.nkind.N_MLET) { cgmlet(c, n); return; }; - - if (k == syntax.nkind.N_BREAK) { cgbreak(c, n); return; }; - if (k == syntax.nkind.N_CONTINUE) { cgcontinue(c, n); return; }; - - if (k == syntax.nkind.N_YIELD) { cgyield(c, n); return; }; - - if (k == syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.tinfo) i32 = { - let t: *syntax.tinfo = ti; - t = tichase(t); - if (t == nil) { return 8; }; - if (t.kind == syntax.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 == syntax.tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; - if (t.kind == syntax.tykind.TY_STR || t.kind == syntax.tykind.TY_SLICE || - t.kind == syntax.tykind.TY_TAGGED) { - return ((t.size + 7u64) & ~7u64): i32; - }; - return 8; -}; - -export fn tupeslotn(n: *syntax.node) i32 = { - if (n == nil) { return 8; }; - return tupeslot(n.type_: *syntax.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: *syntax.node) *syntax.node = { - if (rhs == nil) { return nil; }; - if (rhs.kind != syntax.nkind.N_CALL) { return nil; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cnm = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cnm = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cnm.len == 0) { return nil; }; - let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); - if (rtyp == nil) { return nil; }; - if (rtyp.kind != syntax.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: *syntax.node) *syntax.node = { - if (a == nil) { return nil; }; - if (a.kind == syntax.nkind.N_TUPLE) { return a; }; - if (a.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.str); - if (lc == nil) { return nil; }; - let tn: *syntax.node = lc.tnode; - for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TTUPLE) { return tn; }; - }; - return nil; - }; - let t: *syntax.node = inferletcalltype(c, a); - if (t != nil) { - if (t.kind == syntax.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: *syntax.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 = syntax.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: *syntax.node, dtn: *syntax.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: *syntax.tinfo = e.type_: *syntax.tinfo; - t = tichase(t); - if (t != nil && (t.kind == syntax.tykind.TY_TAGGED || - t.kind == syntax.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: *syntax.node, dtn: *syntax.node) void = { - let t: *syntax.tinfo = e.type_: *syntax.tinfo; - t = tichase(t); - let etagged: bool = false; - if (t != nil) { if (t.kind == syntax.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_: *syntax.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 == syntax.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 == syntax.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: *syntax.node, decl: *syntax.node) void = { - let dp0: *syntax.node = nil; - if (decl != nil) { - if (decl.kind == syntax.nkind.N_TTUPLE) { dp0 = decl.list; }; - }; - let ssecap: i32 = TUPLE_SSECAP; - let gptotal: i32 = 0; - let ssecount: i32 = 0; - let dp: *syntax.node = dp0; - let e: *syntax.node = tuple.list; - for (e != nil) { - let dtn: *syntax.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: *syntax.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: *syntax.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: *syntax.tinfo) void = { - let gptotal: i32 = 0; - let ssecount: i32 = 0; - let el: *syntax.ttupleelem = tu.tupleelems; - for (el != nil) { - let et: *syntax.tinfo = el.type_; - et = tichase(et); - if (et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.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: *syntax.tinfo = el.type_; - et = tichase(et); - let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64); - let eslot: i32 = tupeslot(el.type_); - if (isflt) { - let mov: str = "MOVSD"; - if (et.kind == syntax.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: *syntax.tinfo) void = { - let words: i32 = 0; - let el: *syntax.ttupleelem = tup.tupleelems; - for (el != nil) { - let et: *syntax.tinfo = el.type_; - et = tichase(et); - let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.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: *syntax.node) void = { - rundefers(c); - let rhs: *syntax.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 == syntax.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: *syntax.node = nil; - if (c.fnret != nil) { - if (c.fnret.kind == syntax.nkind.N_TTUPLE) { - rp0 = c.fnret.list; - }; - }; - let rp: *syntax.node = rp0; - let e: *syntax.node = rhs.list; - for (e != nil) { - let rdtn: *syntax.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: *syntax.node = nil; - if (c.fnret != nil) { pt = c.fnret.list; }; - let we: *syntax.node = rhs.list; - let foff: i32 = 0; - for (we != nil) { - let dt: *syntax.tinfo = nil; - if (pt != nil) { dt = pt.lhs.type_: *syntax.tinfo; }; - dt = tichase(dt); - if (dt != nil && dt.kind == syntax.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: *syntax.tinfo = we.type_: *syntax.tinfo; - eu = tichase(eu); - let eoff: i32 = 0; - if (we.kind == syntax.nkind.N_IDENT && eu != nil) { - if (eu.kind == syntax.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: *syntax.tinfo = pt.lhs.type_: *syntax.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: *syntax.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: *syntax.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 == syntax.nkind.N_CALL || rhs.kind == syntax.nkind.N_INDEX || rhs.kind == syntax.nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { - let ru: *syntax.tinfo = rhs.type_: *syntax.tinfo; - ru = tichase(ru); - let fu: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; - fu = tichase(fu); - if (ru != nil && fu != nil && ru.kind == syntax.tykind.TY_TAGGED && fu.kind == syntax.tykind.TY_TAGGED) { - if (ru == fu) { - forwardtagged = true; - } else if (ru.nullable == fu.nullable) { - let pa: *syntax.tparam = ru.params; - let pb: *syntax.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 == syntax.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: *syntax.tinfo = rhs.type_: *syntax.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 == syntax.tykind.TY_TAGGED - && rhs.kind != syntax.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_: *syntax.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 == syntax.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: *syntax.tinfo = rhs.type_: *syntax.tinfo; - ru1 = tichase(ru1); - let fu1: *syntax.tinfo = nil; - if (c.fnret != nil) { - fu1 = c.fnret.type_: *syntax.tinfo; - fu1 = tichase(fu1); - }; - if (ru1 != nil && fu1 != nil) { - if (ru1.kind == syntax.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 == syntax.nkind.N_CAST && ru1.kind == syntax.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_: *syntax.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: *syntax.tinfo = rhs.type_: *syntax.tinfo; - if (syntax.typeisf32(rety)) { rfk = 1; } - else { if (syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { okrhs = true; }; - if (rhs.kind == syntax.nkind.N_STRUCTLIT) { okrhs = true; }; - if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = true; }; - }; - if (okrhs) { - if (rhs.kind == syntax.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: *syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_STRUCTLIT) { - okrhs = true; - }; - if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_ARRLIT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_DOT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { arrok = true; }; - }; - let ati: *syntax.tinfo = c.fnret.type_: *syntax.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 == syntax.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 == syntax.nkind.N_ARRLIT) { - // scalar/float element fill; non-scalar - // elements loud-stop (rule 7, no consumer). - let esubti: *syntax.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 == syntax.tykind.TY_STRUCT) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_ARRAY) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_TUPLE) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_SLICE) { badel = true; }; - if (esubti.kind == syntax.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: *syntax.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: *syntax.node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil) { - if (rti.kind == syntax.tykind.TY_ARRAY) { aggret = true; }; - if (rti.kind == syntax.tykind.TY_STRUCT) { aggret = true; }; - }; - }; - if (aggret && rhs.kind != syntax.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: *syntax.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: *syntax.node, rhs: *syntax.node, off: i32) void = { - let elemn: *syntax.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: *syntax.tinfo = arrtn.type_: *syntax.tinfo; - if (ati79 != nil) { if (ati79.kind == syntax.tykind.TY_NAMED) { - let au79: *syntax.tinfo = tichase(ati79); - if (au79 != nil) { if (au79.kind == syntax.tykind.TY_ARRAY - && au79.sub != nil) { - let en79: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.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: *syntax.tinfo = nil; - if (elemn != nil) { esubti = elemn.type_: *syntax.tinfo; }; - esubti = tichase(esubti); - let isagg: bool = esubti != nil - && (esubti.kind == syntax.tykind.TY_STRUCT - || esubti.kind == syntax.tykind.TY_ARRAY - || esubti.kind == syntax.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 == syntax.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 == syntax.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: *syntax.node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - repeat = true; - isellip = true; - }; - }; - if (isellip) { - e = nil; - } else { - if (isagg) { - if (e.kind == syntax.nkind.N_STRUCTLIT) { - let esi: *structinfo = structlookupchain(c, elemn); - cgstructlitfillbp(c, esi, e, off + idx * esz); - } else { if (e.kind == syntax.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 == syntax.nkind.N_TARRAY) { - if (arrtn.rhs != nil) { - if (arrtn.rhs.kind == syntax.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: *syntax.node) void = { - let nm: str = n.str; - let sz: i32 = letslotsize(c, n); - let tn: *syntax.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: *syntax.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: *syntax.node = n.lhs; - if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; - if (n.rhs != nil) { - let rhs: *syntax.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: *syntax.node = nil; - let viatryunw: bool = false; - let viatryprop: bool = false; - if (rhs.kind == syntax.nkind.N_TRYUNW) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.nkind.N_CALL) { - scall = rhs.lhs; - viatryunw = true; - }; - }; - } else { if (rhs.kind == syntax.nkind.N_TRYPROP) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.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 == syntax.nkind.N_TSLICE && sz == tyslicesize(): i32) { - let callee: *syntax.node = scall.lhs; - let a0: *syntax.node = scall.list; - let a1: *syntax.node = nil; - let a2: *syntax.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 == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0.kind == syntax.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: *syntax.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: *syntax.tinfo = nil; - if (c.fnret != nil) { nti = c.fnret.type_: *syntax.tinfo; }; - nti = tichase(nti); - if (nti != nil) { if (nti.kind == syntax.tykind.TY_TAGGED) { - let np: *syntax.tparam = nti.params; - let nidx2: i32 = 0; - for (np != nil) { - let nvt: *syntax.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 == syntax.nkind.N_CALL) { - letsret = callsretsize(c, rhs); - }; - if (letsret == 0) { - cgwidentaggedstore(c, tn.type_: *syntax.tinfo, rhs, - "BP", off, sz); - c.lastwasreturn = 0; - return; - }; - let lru: *syntax.tinfo = rhs.type_: *syntax.tinfo; - lru = tichase(lru); - let llu: *syntax.tinfo = tn.type_: *syntax.tinfo; - llu = tichase(llu); - let exact38: bool = false; - if (lru != nil && lru == llu) { exact38 = true; } - else { - if (syntax.typeeq(rhs.type_: *syntax.tinfo, tn.type_: *syntax.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: *syntax.node = tn; - for (ttup != nil && ttup.kind == syntax.nkind.N_TNAME) { - ttup = aliaslookup(c, ttup.str); - }; - if (ttup != nil) { - if (ttup.kind == syntax.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 == syntax.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: *syntax.node = ttup.list; - for (q != nil) { - let qt: *syntax.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 == syntax.nkind.N_TTUPLE - && rhs.kind != syntax.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 == syntax.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 == syntax.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: *syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_CALL) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (tn != nil) { - if (tn.kind == syntax.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 == syntax.nkind.N_CALL && tn != nil - && tn.kind == syntax.nkind.N_TARRAY) { - let ati: *syntax.tinfo = tn.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.tinfo = nil; - if (tn != nil) { aggti = tn.type_: *syntax.tinfo; }; - aggti = tichase(aggti); - if (aggti != nil) { - if (aggti.kind == syntax.tykind.TY_ARRAY) { - aggn = aggti.size: i32; - }; - }; - }; - if (aggn > 8) { - let havesrc: bool = false; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { - cgexpr(c, rhs.lhs); - emitline("\tMOVQ\tAX, SI\n"); - havesrc = true; - }; - }; - if (!havesrc) { if (rhs.kind == syntax.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: *syntax.node = defvartnode(c, rhs.str); - let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil; - if (aggdtn != nil) { - if (aggdtn.kind == syntax.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 == syntax.nkind.N_DOT) { - if (dotchainaddr(c, rhs, "SI")) { - havesrc = true; - }; - }; }; - if (!havesrc) { if (rhs.kind == syntax.nkind.N_INDEX) { - let base: *syntax.node = rhs.lhs; - let idx: *syntax.node = rhs.rhs; - let bu: *syntax.tinfo = nil; - if (base != nil) { bu = base.type_: *syntax.tinfo; }; - bu = tichase(bu); - if (base != nil && base.kind == syntax.nkind.N_IDENT - && bu != nil && bu.kind == syntax.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 == syntax.nkind.N_DOT - || base.kind == syntax.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 == syntax.nkind.N_DOT) { - if (dotbaseaddr(c, base, "AX")) { - baseok = true; - }; - } else { - let ab: *syntax.node = base.lhs; - let bidx: *syntax.node = base.rhs; - let abu: *syntax.tinfo = nil; - if (ab != nil) { abu = ab.type_: *syntax.tinfo; }; - abu = tichase(abu); - if (ab != nil && ab.kind == syntax.nkind.N_IDENT - && abu != nil && abu.kind == syntax.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 == syntax.nkind.N_TRYUNW - || rhs.kind == syntax.nkind.N_TRYPROP) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - zti = tichase(zti); - if (zti != nil && zti.kind == syntax.tykind.TY_ARRAY) { - zsz = zti.size: i32; - } else { if (n.lhs.kind == syntax.nkind.N_TNAME) { - let szi: *structinfo = structlookupchain(c, n.lhs); - if (szi != nil) { - let ti: *syntax.tinfo = n.lhs.type_: *syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.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 == syntax.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 == syntax.nkind.N_TUPLE) { litrhs = true; }; }; - let synthdecl: *syntax.node = nil; - if (litrhs) { - synthdecl = syntax.newnode(syntax.nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col); - let dtail: *syntax.node = nil; - let lb0: *syntax.node = n.list; - for (lb0 != nil) { - let w: *syntax.node = syntax.newnode(syntax.nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col); - w.lhs = nil; - if (lb0.kind == syntax.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: *syntax.node = nil; - if (rettuple != nil) { pt2 = rettuple.list; }; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.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: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo; - if (eti != nil) { esz = eti.size: i32; }; - }; - let off: i32 = 0; - if (lb.kind == syntax.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: *syntax.node = n.list; - let pt: *syntax.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: *syntax.node = nil; - let re: *syntax.node = nil; - if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; - for (l != nil) { - let tn: *syntax.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: *syntax.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 == syntax.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: *syntax.node) void = { - let rhs: *syntax.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: *syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { - let rl: *local = localfindnode(c, rhs.str); - if (rl != nil) { - let rti: *syntax.tinfo = rl.tnode.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil) { if (rti.kind == syntax.tykind.TY_TUPLE) { - let srcoff: i32 = rl.off; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.node = lb.lhs; - let isflt: bool = isfloattype(c, tn); - let eslot: i32 = tupeslotn(tn); - let esz: i32 = 8; - let eti: *syntax.tinfo = nil; - if (tn != nil) { eti = tn.type_: *syntax.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: *syntax.node = nil; - if (rettuple != nil) { pt2 = rettuple.list; }; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.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: *syntax.tinfo = pt2.lhs.type_: *syntax.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: *syntax.node = n.list; - let pt: *syntax.node = nil; - if (rettuple != nil) { pt = rettuple.list; }; - for (l != nil) { - let tn: *syntax.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: *syntax.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: *syntax.node) i32 = { - if (t == nil) { return 8; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TPTR) { return 8; }; - if (k == syntax.nkind.N_TFN) { return 8; }; - if (k == syntax.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 == syntax.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: *syntax.tinfo = t.type_: *syntax.tinfo; - if (tgi != nil) { - tgi = tichase(tgi); - if (tgi != nil && tgi.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let total: i32 = 0; - let dp: *syntax.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 == syntax.nkind.N_TNAME) { - let nm: str = t.str; - if (syntax.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: *syntax.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: *syntax.node) void = { - let slc: *syntax.node = n.lhs; - let slclocal: *local = nil; - let slctn: *syntax.node = nil; - if (slc != nil) { - if (slc.kind == syntax.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: *syntax.node = nil; - if (slctn != nil) { - let sk: syntax.nkind = slctn.kind; - if (sk == syntax.nkind.N_TSLICE) { elemt = slctn.lhs; }; - if (sk == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(slctn.str, "str")) { - let sti: *syntax.tinfo = slctn.type_: *syntax.tinfo; - if (sti != nil) { - if (sti.sub != nil) { - let u8n: *syntax.node = syntax.newnode(syntax.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: *syntax.tinfo = nil; - if (slctn != nil) { - if (slctn.kind == syntax.nkind.N_TNAME) { - let st60: *syntax.tinfo = slctn.type_: *syntax.tinfo; - if (st60 != nil) { - if (st60.kind == syntax.tykind.TY_NAMED) { rti60 = tichase(st60); }; - }; - }; - }; - if (rti60 != nil && elemt == nil) { - if (rti60.sub != nil) { - let en60: *syntax.node = syntax.newnode(syntax.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: *syntax.tinfo = tichase(rti60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TTUPLE) { - let total: i32 = 0; - let p: *syntax.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: *syntax.tinfo = slc.type_: *syntax.tinfo; - sti2 = tichase(sti2); - if (sti2 != nil) { - if (sti2.kind == syntax.tykind.TY_SLICE - || sti2.kind == syntax.tykind.TY_STR - || sti2.kind == syntax.tykind.TY_ARRAY) { - if (sti2.sub != nil) { - esz = sti2.sub.size: i32; - let en: *syntax.node = syntax.newnode(syntax.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 != syntax.nkind.N_IDENT) { - let stu70: *syntax.tinfo = slc.type_: *syntax.tinfo; - stu70 = tichase(stu70); - let arr70: bool = false; - if (stu70 != nil) { - if (stu70.kind == syntax.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 == syntax.nkind.N_UN) { - if (slc.op == syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, slc.str) == nil) { - let isglob: bool = isletvar(c, slc.str); - if (!isglob) { - let gdtn: *syntax.node = defvartnode(c, slc.str); - if (gdtn != nil) { - if (gdtn.kind == syntax.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: *syntax.node = nil; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TTUPLE) { tp = elemt.list; }; - }; - let field_off: i32 = 0; - let m: *syntax.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: *syntax.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: syntax.nkind = slctn.kind; - if (tk == syntax.nkind.N_TSLICE) { isslicestr = true; }; - if (tk == syntax.nkind.N_TARRAY) { isarr = true; }; - if (tk == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { isarr = true; }; - if (rti60.kind == syntax.tykind.TY_SLICE) { isslicestr = true; }; - if (rti60.kind == syntax.tykind.TY_STR) { isslicestr = true; }; - }; - if (isslicestr) { - if (slc.kind == syntax.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 == syntax.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 == syntax.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: *syntax.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: *syntax.node = nil; - - let cs: *syntax.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: *syntax.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: *syntax.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: *syntax.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; -}; - - - -//ww:module-reset -// 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 syntax; -import strconv; - - -// ---- function-level cgen --------------------------------------------- - -fn cgfnparams(c: *cgen, params: *syntax.node) void = { - let p: *syntax.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 == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - let tn: *syntax.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: *syntax.node = nil; - if (p.lhs != nil) { - tt99 = p.lhs; - for (tt99 != nil && tt99.kind == syntax.nkind.N_TNAME) { - tt99 = aliaslookup(c, tt99.str); - }; - }; - if (p.lhs != nil) { if (tt99 != nil && tt99.kind == syntax.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: *syntax.node = tt99.list; - for (te != nil) { - let et: *syntax.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_: *syntax.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_: *syntax.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_: *syntax.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 == syntax.nkind.N_BLOCK) { - let s: *syntax.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 module hint (this fn's own module) the call sites use. M1 #32: - // the ROOT-unit main (imported==0) is the bare `_start` entry — emit - // it bare directly, mirroring collectmods' skip. Irreducibly name- - // based: a `package main` primary's decls are MODULED "main" (the - // package clause sets curmod; cmd/ww/main.c:394), so main is NOT a - // bare-module decl — dropping this carve-out mangles it to `main.main` - // (undefined `main`). #84's bare-module handling is the orthogonal - // axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved - // in modlookupforfn, NOT here. - // #99: gate on sepisdep — under sep a dep unit's main is imported==0 - // (path-carrying `//ww:module-reset`, #57); only the root/link-entry - // unit (wwiout==nil, #69) keeps the bare label. - emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { - emitbytes(fn_.str.ptr, fn_.str.len: u64); - } else { - 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: *syntax.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: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_FNDECL) { - // #22 M3: emit code ONLY for this package's own decls. A - // `.wwi` dep fn is a body-less prototype that already skips - // (the body != nil gate); the explicit imported gate also - // covers a dep fn that still carries a body in a transitional - // unit, keeping the "emit iff imported==0" rule exception-free. - if (!(c.sepmode != 0 && d.imported != 0)) { - if (d.body != nil) { - cgfn(c, d); - }; - }; - }; - d = d.next; - }; - letpreintern(c, file); - emitdatasection(c); - emitdefconstants(c, file); - emitletdataw(c, file); -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node, // the rhs type expr - aanext: *aliasent, -}; - -fn collectaliases(c: *cgen, file: *syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0); - tnvoid.str = "void"; - let bang: *syntax.node = syntax.newnode(syntax.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: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.node = d.lhs; - if (body != nil) { - if (body.kind != syntax.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) *syntax.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 (syntax.streq(a.aname, name)) { - if (syntax.streq(a.amod, c.curmod)) { return a.target; }; - }; - a = a.aanext; - }; - a = c.aliases; - for (a != nil) { - if (syntax.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); - // M1 #22: map the embedded use ALIAS (`utf8`) to the - // dotted import PATH the decl's module now carries. - let pkgmod: str = usehint(c, pkg); - let b: *aliasent = c.aliases; - for (b != nil) { - if (syntax.streq(b.aname, leaf)) { - if (syntax.streq(b.amod, pkgmod)) { - 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) *syntax.node = { - let a: *aliasent = c.aliases; - for (a != nil) { - if (syntax.streq(a.aname, name)) { - if (syntax.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: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; }; - if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_UN) { - let v: u64; - if (!foldintliteral(e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - return false; -}; - -fn enumevalmember(prev: *enummember, e: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - if (foldintliteral(e, out)) { return true; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_IDENT) { - let m: *enummember = prev; - for (m != nil) { - if (syntax.streq(m.mname, e.str)) { - *out = m.mval; - return true; - }; - m = m.emnext; - }; - return false; - }; - if (k == syntax.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: syntax.tkind = e.op; - if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; }; - if (op == syntax.tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == syntax.tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; }; - if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == syntax.tkind.TK_RSHIFT) { *out = a >> b; return true; }; - return false; - }; - if (k == syntax.nkind.N_UN) { - let v: u64; - if (!enumevalmember(prev, e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - return false; -}; - -fn collectenums(c: *cgen, file: *syntax.node) void = { - c.enums = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.node = d.lhs; - if (body != nil) { - if (body.kind == syntax.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: *syntax.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 (syntax.streq(e.ename, name)) { - if (syntax.streq(e.emod, c.curmod)) { return e; }; - }; - e = e.etnext; - }; - e = c.enums; - for (e != nil) { - if (syntax.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); - // M1 #22: map the embedded use ALIAS (`utf8`) to the - // dotted import PATH the decl's module now carries. - let pkgmod: str = usehint(c, pkg); - let b: *enumtype = c.enums; - for (b != nil) { - if (syntax.streq(b.ename, leaf)) { - if (syntax.streq(b.emod, pkgmod)) { - 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 (syntax.streq(e.ename, name)) { - if (syntax.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 (syntax.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: *syntax.node) *syntax.node = { - let cur: *syntax.node = t; - let depth: i32 = 0; - for (depth < 16) { - if (cur == nil) { return nil; }; - if (cur.kind != syntax.nkind.N_TNAME) { return cur; }; - let nm: str = cur.str; - let next: *syntax.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: *syntax.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: *syntax.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: *syntax.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 - uses: *modent, // M1 #22: N_USE alias → dotted import path, - // for the qualified-ref codegen hint - // (mname=alias, nmod=path) - 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: *syntax.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: []*syntax.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: *syntax.node, - sretforward: i32, - // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ - // DATA ONLY for this package's own (imported==0) decls; treat every - // `.wwi`-sourced (imported==1) dep decl as an external. Off on the - // combined path (every existing invocation) so M3 is a pure addition. - // NOT reset by cgeninit (which runs per-fn) — set once in main and - // must survive to the post-loop emitletdataw/emitdefconstants pass, - // like strlits/ffis. Symmetric with cstage Cg.sep_mode. - sepmode: i32, - // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. - // Set from `wwiout != nil` in main — the producer passes -I (.wwi - // output) to DEP units only; the root's .wwi is stripped (#69), so - // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- - // out: a dep's `fn main` mangles on its path like any decl; only the - // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). - // Symmetric with cstage Cg.sep_isdep. - sepisdep: 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: *syntax.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: []*syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.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 (syntax.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 (syntax.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 (syntax.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 (syntax.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 (syntax.streq(bs, bytes)) { - return s.label; - }; - s = s.slnext; - }; - // New label "._S_" (bare "_S_" when curmod empty). - // #49: per-unit prefix so two str-bearing packages don't both emit - // `_S_0`.. and collide at w6l link. Pure function of the module path - // (matching mklabel's spelling), so the self-host fixed-point holds. - 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; }; - buf[i] = 95u8; i += 1; buf[i] = 83u8; i += 1; buf[i] = 95u8; i += 1; // "_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[i + dk] = ns.ptr[dk]; dk += 1; }; - c.strlitseq += 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 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 (syntax.streq(nm, "bool")) { return true; }; - if (syntax.streq(nm, "rune")) { return true; }; - if (syntax.streq(nm, "i8")) { return true; }; - if (syntax.streq(nm, "i16")) { return true; }; - if (syntax.streq(nm, "i32")) { return true; }; - if (syntax.streq(nm, "i64")) { return true; }; - if (syntax.streq(nm, "u8")) { return true; }; - if (syntax.streq(nm, "u16")) { return true; }; - if (syntax.streq(nm, "u32")) { return true; }; - if (syntax.streq(nm, "u64")) { return true; }; - if (syntax.streq(nm, "int")) { return true; }; - if (syntax.streq(nm, "uint")) { return true; }; - if (syntax.streq(nm, "uintptr")) { return true; }; - if (syntax.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 (syntax.streq(nm, "f32")) { return 4; }; - if (syntax.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: *syntax.node) i32 = { - if (d == nil) { return 0; }; - let t: *syntax.node = d.lhs; - for (t != nil) { - if (t.kind == syntax.nkind.N_TPTR) { return 8; }; - if (t.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; - if (t.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = t.rhs; - let elemn: *syntax.node = t.lhs; - let alen: i32 = 1; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(t.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - alen = abt.alen: i32; - }; - }; - let esz: i32 = 8; - if (elemn != nil) { - if (elemn.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let tsum: i32 = 0; - let p: *syntax.node = t.list; - for (p != nil) { - let et: *syntax.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 == syntax.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 != syntax.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 (syntax.streq(nm, "str")) { return primtypesize("str"): i32; }; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.totsize; }; - let next: *syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_LET) { - if (d.lhs != nil && d.rhs != nil - && d.lhs.kind == syntax.nkind.N_TNAME - && syntax.streq(d.lhs.str, "untyped_int")) { - let opnd: *syntax.node = d.rhs; - if (opnd.kind == syntax.nkind.N_UN - && (opnd.op == syntax.tkind.TK_PLUS - || opnd.op == syntax.tkind.TK_MINUS - || opnd.op == syntax.tkind.TK_TILDE)) { - opnd = opnd.lhs; - }; - if (opnd != nil - && opnd.kind == syntax.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 == syntax.nkind.N_TNAME - && syntax.streq(d.lhs.str, "untyped_float")) { - let opnd: *syntax.node = d.rhs; - if (opnd.kind == syntax.nkind.N_UN - && (opnd.op == syntax.tkind.TK_PLUS - || opnd.op == syntax.tkind.TK_MINUS)) { - opnd = opnd.lhs; - }; - if (opnd != nil - && opnd.kind == syntax.nkind.N_FLOATLIT) { - d.lhs.str = "f64"; - }; - }; - }; - d = d.next; - }; -}; - -fn collectlets(c: *cgen, file: *syntax.node) void = { - c.lets = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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 (syntax.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) *syntax.node = { - let lv: *letvar = c.lets; - for (lv != nil) { - if (syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nm: str = t.str; - if (syntax.streq(nm, "str")) { return true; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind == syntax.nkind.N_TSLICE) { return true; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return 0; }; - let fsz: i32 = letfloatprim(t.str); - if (fsz > 0) { return fsz; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nm: str = t.str; - if (structlookup(c, nm) != nil) { return true; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return nil; }; - let nm: str = t.str; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si; }; - let nx: *syntax.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 (syntax.streq(e.dname, name)) { - let t: *syntax.node = e.dtnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return nil; }; - let nm: str = t.str; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si; }; - let nx: *syntax.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) *syntax.node = { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.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: *syntax.tinfo, r: *syntax.node) void = { - let alen: i32 = au.alen: i32; - let cnt: i32 = 0; - let last_ev: *syntax.node = nil; - let repeat: bool = false; - let e: *syntax.node = r.list; - for (e != nil && cnt < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - repeat = true; - break; - }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev == nil) { break; }; - if (ev.kind != syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - // #49: strlit labels allocated here (static-data initialisers) take - // the OWNING decl's module prefix, not the stale last-fn curmod. - // Save/restore so the later emit passes — which read curmod for - // fn-ptr relocs — see the same value they did before; letpreintern - // itself only interns, so driving curmod here has no other effect. - let savedmod: str = c.curmod; - let d: *syntax.node = file.list; - for (d != nil) { - c.curmod = d.nmod; - // #22 M3: skip imported deps so the strlit table (and its _S_ - // sequence) is a pure function of THIS package's own decls. A - // dep's body initializer would intern here, but its `.wwi` (init - // stripped) would not — gating on imported keeps the - // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- - // module str-def splicing rides the def registry (interned at the - // use site, not here), so it is unaffected. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - // #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 == syntax.nkind.N_DEF) { - let dr: *syntax.node = d.rhs; - for (dr != nil && dr.kind == syntax.nkind.N_CAST) { - dr = dr.lhs; - }; - if (d.lhs != nil && dr != nil - && dr.kind == syntax.nkind.N_ARRLIT) { - let dau: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); - if (dau != nil && dau.kind == syntax.tykind.TY_ARRAY) { - let deu: *syntax.tinfo = tichase(dau.sub); - if (deu != nil && deu.kind == syntax.tykind.TY_STR) { - preinternstrarray(c, dau, dr); - }; - }; - }; - }; - if (d.kind == syntax.nkind.N_LET) { - let r: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); - if (au != nil && au.kind == syntax.tykind.TY_ARRAY - && r.kind == syntax.nkind.N_ARRLIT) { - let eu: *syntax.tinfo = tichase(au.sub); - if (eu != nil && eu.kind == syntax.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 == syntax.nkind.N_TUPLE) { - let tlt: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTUPLE) { - handled = true; - let tp: *syntax.node = tlt.list; - let e: *syntax.node = r.list; - for (e != nil && tp != nil) { - let et: *syntax.node = tp.lhs; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev != nil) { - if (ev.kind == syntax.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 == syntax.nkind.N_ARRLIT - && d.lhs.kind == syntax.nkind.N_TSLICE) { - let tupnode: *syntax.node = d.lhs.lhs; - if (tupnode != nil && tupnode.kind == syntax.nkind.N_TTUPLE) { - handled = true; - let row: *syntax.node = r.list; - for (row != nil) { - let rw: *syntax.node = row; - for (rw != nil && rw.kind == syntax.nkind.N_CAST) { - rw = rw.lhs; - }; - if (rw != nil && rw.kind == syntax.nkind.N_TUPLE) { - let tp: *syntax.node = tupnode.list; - let e: *syntax.node = rw.list; - for (e != nil && tp != nil) { - let et: *syntax.node = tp.lhs; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev != nil) { - if (ev.kind == syntax.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: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTAGGED && !isnullabletype(tlt)) { - if (r.kind == syntax.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 == syntax.nkind.N_STRLIT) { - if (r.str.len > 0) { - internstrlit(c, r.str); - }; - }; - }; - }; - }; - }; - d = d.next; - }; - c.curmod = savedmod; -}; - -// 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: *syntax.node) bool = { - let isf32: bool = (sz == 4); - let bits: u64 = 0u64; - let neg: bool = false; - if (rhs != nil) { - let r: *syntax.node = rhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - if (r != nil) { - if (r.kind == syntax.nkind.N_UN) { - if (r.op == syntax.tkind.TK_MINUS) { - neg = true; - r = r.lhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - } else { if (r.op == syntax.tkind.TK_PLUS) { - r = r.lhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - };}; - }; - }; - if (r == nil) { return false; }; - if (r.kind != syntax.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(" "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node, - base: u64) bool = { - let su: *syntax.tinfo = structt; - su = tichase(su); - if (su == nil) { return false; }; - if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; - let pos: u64 = base; - let f: *syntax.tfield = su.fields; - for (f != nil) { - let fstart: u64 = base + f.offset; - for (pos < fstart) { - emitdatawbyte(0u8); - pos = pos + 1u64; - }; - let v: *syntax.node = nil; - if (rhs != nil) { - let fnod: *syntax.node = rhs.list; - for (fnod != nil) { - if (syntax.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: *syntax.node = v; - for (vr != nil && vr.kind == syntax.nkind.N_CAST) { vr = vr.lhs; }; - let fu: *syntax.tinfo = f.type_; - fu = tichase(fu); - // #19 option A: a non-nullable tagged-union field rides the shared - // (tag,payload) core at the field slot size, mirroring the scalar - // tagged global — NOT the int emitter. Wide/struct/non-foldable - // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil - // here (the absent-field zero-fill is handled above). - if (fu != nil && fu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(fu)) { - if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { - let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); - }; - pos = fstart + fsz: u64; - f = f.tnext; - continue; - }; - if (fu != nil && fu.kind == syntax.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 != syntax.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 == syntax.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 != syntax.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 (syntax.typeisfloat(f.type_)) { - let isf32: bool = (fsz == 4); - let neg: bool = false; - let fr: *syntax.node = vr; - if (fr != nil) { if (fr.kind == syntax.nkind.N_UN) { - if (fr.op == syntax.tkind.TK_MINUS) { - neg = true; - fr = fr.lhs; - for (fr != nil && fr.kind == syntax.nkind.N_CAST) { fr = fr.lhs; }; - } else { if (fr.op == syntax.tkind.TK_PLUS) { - fr = fr.lhs; - for (fr != nil && fr.kind == syntax.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 != syntax.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: *syntax.tinfo, rhs: *syntax.node) bool = { - let su: *syntax.tinfo = structt; - su = tichase(su); - if (su == nil) { return false; }; - if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; - emitline(directive); - emitline(" "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node, - emit_phase: i32) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - let esz: i32 = au.sub.size: i32; - let alen: i32 = au.alen: i32; - let eu: *syntax.tinfo = au.sub; - eu = tichase(eu); - - // #19 option A: a non-nullable tagged-union element rides the shared - // (tag,payload) core (emittaggedbytes) at the full slot stride esz, - // mirroring the scalar tagged global — NOT the int emitter, which - // mis-folds the payload into the tag word. A wide/struct/non-foldable - // payload element loud-rejects (task #30 sub-item, rule 7). A nullable - // `(*T|void)` element is a 1-word fold, not a tag box — left to the - // existing int path (task #15). - if (eu != nil && eu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(eu)) { - let idx: i32 = 0; - let last_ev: *syntax.node = nil; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - if (!emittaggedbytes(c, au.sub, e, esz, 0)) { - let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); - }; - last_ev = e; - 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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - emittaggedbytes(c, au.sub, e, esz, 1); - idx += 1; - e = e.next; - }; - for (idx < alen) { - if (repeat && last_ev != nil) { - emittaggedbytes(c, au.sub, last_ev, esz, 1); - } else { - let bb: i32 = 0; - for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; - }; - idx += 1; - }; - return true; - }; - - if (eu != nil && eu.kind == syntax.tykind.TY_STRUCT) { - // Validate: every element must be N_STRUCTLIT (after N_CAST). - let idx: i32 = 0; - let last_ev: *syntax.node = nil; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.tykind.TY_ARRAY) { - let idx: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 (syntax.typeisfloat(au.sub)) { - let isf32: bool = syntax.typeisf32(au.sub); - // Validate. - let idx: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { - if (ev.op == syntax.tkind.TK_MINUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - } else { if (ev.op == syntax.tkind.TK_PLUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - };}; - };}; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - let neg: bool = false; - if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { - if (ev.op == syntax.tkind.TK_MINUS) { - neg = true; - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - } else { if (ev.op == syntax.tkind.TK_PLUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.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: *syntax.node = rhs.list; - let last: u64 = 0u64; - let repeat: bool = false; - // Validate first. - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - inrepeat = true; - v = last; - } else { - e = e.next; - v = last; - }; - } else { - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.tinfo, rhs: *syntax.node) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - let eu: *syntax.tinfo = au.sub; - eu = tichase(eu); - if (eu == nil) { return false; }; - if (eu.kind != syntax.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: *syntax.node = nil; - let repeat: bool = false; - let cnt: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && cnt < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.nkind.N_STRLIT) { return false; }; - last_ev = ev; - cnt += 1; - e = e.next; - }; - - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - let idx: i32 = 0; - e = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev.str.len > 0) { - let lab: str = internstrlit(c, ev.str); - emitline("DATAR "); - emitfnname(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 "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - if (rhs == nil) { - let total: u64 = arrt.size; - emitline(directive); - emitline(" "); - emitfnname(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(" "); - emitfnname(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: *syntax.tinfo, - sltnode: *syntax.node, rhs: *syntax.node) void = { - let su: *syntax.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 != syntax.tykind.TY_SLICE) { return; }; - let etype: *syntax.tinfo = su.sub; - let eu: *syntax.tinfo = etype; - eu = tichase(eu); - // Count elements; reject `...` (a slice literal has no target N). - let k: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = nil; - if (sltnode != nil) { tupnode = sltnode.lhs; }; - let istuprow: bool = false; - if (eu != nil && eu.kind == syntax.tykind.TY_TUPLE && tupnode != nil) { - if (tupnode.kind == syntax.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: *syntax.node = rhs.list; - for (e2 != nil) { - let row: *syntax.node = e2; - for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; }; - let bad: bool = false; - if (row == nil) { bad = true; } - else if (row.kind != syntax.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 "); - emitfnname(c, name, module); - emitline(".d(SB),\""); - e2 = rhs.list; - for (e2 != nil) { - let row: *syntax.node = e2; - for (row != nil && row.kind == syntax.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: *syntax.node = e2; - for (row != nil && row.kind == syntax.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 == syntax.tykind.TY_STR || eu.kind == syntax.tykind.TY_SLICE - || eu.kind == syntax.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: *syntax.tinfo = syntax.newtype(syntax.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 "); - emitfnname(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 "); - emitfnname(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 "); - emitfnname(c, name, module); - emitline("+0(SB),"); - emitfnname(c, name, module); - emitline(".d(SB)\n"); -}; - -// emittaggedbytes — raw sz-byte static-init payload for a tagged-union -// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO -// directive open/close, NO reloc — emits exactly sz bytes via -// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a -// foldable int payload only; a wide (str/slice) payload needs a DATAR the -// raw core cannot place inside an already-open aggregate directive, and a -// struct/non-foldable payload has no scalar form — both return false -// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct -// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated -// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. -// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata -// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) -// share this raw core so a nested tagged member rides the SAME -// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage -// emit_tagged_bytes. -fn emittaggedbytes(c: *cgen, tti: *syntax.tinfo, rhs: *syntax.node, sz: i32, emit_phase: i32) bool = { - if (tti == nil) { return false; }; - if (rhs == nil) { - if (emit_phase == 1) { - let zi: i32 = 0; - for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; - }; - return true; - }; - let r: *syntax.node = rhs; - for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; }; - if (r == nil) { return false; }; - let tag: i32 = flatvariantidxt(tti, r.type_: *syntax.tinfo, false); - if (tag < 0) { return false; }; - if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; - let v: u64 = 0u64; - if (!foldintliteral(r, &v)) { return false; }; - if (emit_phase == 1) { - let acc: u64 = 0u64; - let i: i32 = 0; - acc = tag: u64; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - acc = v; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - i = 16; - for (i < sz) { emitdatawbyte(0u8); i += 1; }; - }; - return true; -}; - -// 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: *syntax.node, rhs: *syntax.node, sz: i32) bool = { - if (tt == nil) { return false; }; - if (rhs == nil) { - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1); - emitline("\"\n"); - return true; - }; - let r: *syntax.node = rhs; - for (r != nil && r.kind == syntax.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 != syntax.nkind.N_STRLIT) { return false; }; - let lv: u64 = r.str.len: u64; - emitline("DATAW "); - emitfnname(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 "); - emitfnname(c, name, module); - emitline("+8(SB),"); - emitbytes( lab.ptr, lab.len: u64); - emitline("(SB)\n"); - }; - return true; - }; - // int/zero payload via the shared raw core; validate (phase 0) BEFORE - // opening the directive so a non-foldable rhs returns false without - // leaving a half-written DATAW. - if (!emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 0)) { return false; }; - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 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: *syntax.node) bool = { - if (ev == nil) { return false; }; - if (ev.kind != syntax.nkind.N_UN) { return false; }; - if (ev.op != syntax.tkind.TK_AMP) { return false; }; - let opnd: *syntax.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 == syntax.nkind.N_DOT) { - if (opnd.lhs == nil) { return false; }; - if (opnd.lhs.kind != syntax.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 != syntax.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: *syntax.tinfo = tichase(opnd.type_: *syntax.tinfo); - if (ou == nil) { return false; }; - return ou.kind == syntax.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: *syntax.node, rhs: *syntax.node) bool = { - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - { - let eti: *syntax.tinfo = nil; - if (et != nil) { eti = et.type_: *syntax.tinfo; }; - eti = tichase(eti); - if (eti != nil && eti.kind == syntax.tykind.TY_TAGGED) { - return false; - }; - }; - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - if (wide) { - if (ev.kind != syntax.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: *syntax.node, rhs: *syntax.node) void = { - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.node, rhs: *syntax.node) void = { - let foff: i32 = rowoff; - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 "); - emitfnname(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 "); - emitfnname(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 == syntax.nkind.N_DOT) { - emitfnname(c, ev.lhs.str, usehint(c, 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: *syntax.node, rhs: *syntax.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: *syntax.node = tt.list; - for (p0 != nil) { - zsz += tupeslotn(p0.lhs); - p0 = p0.next; - }; - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - let zi: i32 = 0; - for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; - emitline("\"\n"); - return true; - }; - if (rhs.kind != syntax.nkind.N_TUPLE) { return false; }; - if (!tuplerowfoldable(c, tt, rhs)) { return false; }; - emitline("DATAW "); - emitfnname(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: *syntax.node) void = { - let d: *syntax.node = file.list; - for (d != nil) { - // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is - // initializer-less; emitting a DATAW for it would DUPLICATE the - // definition that lives in the dep's own .o → link collision. - // Gate on the explicit imported flag (NOT no-rhs: a package's OWN - // init-less let must still zero-init). Symmetric with M2's - // producer imported==0 filter — same predicate both ways. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - if (d.kind == syntax.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: *syntax.tinfo = nil; - if (d.lhs != nil) { dti = tichase(d.lhs.type_: *syntax.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: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - let istup: bool = false; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTUPLE) { - istup = true; - }; - }; - if (istup) { - let tr: *syntax.node = d.rhs; - for (tr != nil) { - if (tr.kind != syntax.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 == syntax.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: *syntax.node = d.rhs; - if (r != nil) { - if (r.kind == syntax.nkind.N_STRUCTLIT) { - let st: *syntax.tinfo = d.lhs.type_: *syntax.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 == syntax.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: *syntax.node = nil; - if (d.rhs != nil) { - r = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 "); - emitfnname(c, nm, d.nmod); - emitline("(SB),\""); - let zi: i32 = 0; - for (zi < 8) { emitdatawbyte(0u8); zi += 1; }; - emitline("\"\n"); - emitline("DATAR "); - emitfnname(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 == syntax.nkind.N_DOT) { - emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); - } else { - emitfnname(c, r.lhs.str, c.curmod); - }; - emitline("(SB)\n"); - } else if (ok) { - emitline("DATAW "); - emitfnname(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: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 == syntax.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 "); - emitfnname(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 "); - emitfnname(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 == syntax.nkind.N_NIL) { ok = true; }; - if (r.kind == syntax.nkind.N_STRLIT) { - if (r.str.len == 0) { ok = true; }; - }; - }; - }; - if (ok) { - emitline("DATAW "); - emitfnname(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: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 == syntax.nkind.N_ARRLIT) { - emitslicedata(c, nm, d.nmod, - d.lhs.type_: *syntax.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 == syntax.nkind.N_NIL) { ok = true; }; - }; - }; - if (ok) { - emitline("DATAW "); - emitfnname(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 "); - emitfnname(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 == syntax.tykind.TY_ARRAY) { - let rh: *syntax.node = d.rhs; - let route: bool = false; - if (rh == nil) { route = true; }; - if (rh != nil) { - if (rh.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = file.list; - for (d != nil) { - // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / - // struct / array) must NOT re-emit — the dep's own .o owns the - // symbol. Str defs are inline-spliced (never emitted here), so - // they need no gate; the def registry stays populated for imported - // decls so the target's LOAD paths still resolve the extern. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - if (d.kind == syntax.nkind.N_DEF) { - let r: *syntax.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: *syntax.node = d.lhs; - for (dt != nil) { - if (dt.kind != syntax.nkind.N_TNAME) { dfsz = 0; break; }; - let fsz: i32 = letfloatprim(dt.str); - if (fsz > 0) { dfsz = fsz; break; }; - let nx: *syntax.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 == syntax.nkind.N_STRUCTLIT) { - let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; - let su: *syntax.tinfo = st; - su = tichase(su); - if (su != nil) { - if (su.kind == syntax.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 == syntax.nkind.N_ARRLIT) { - let at: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; - let au: *syntax.tinfo = at; - au = tichase(au); - if (au != nil) { - if (au.kind == syntax.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 == syntax.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 "); - emitfnname(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: *syntax.node, - params: *syntax.node, - frnext: *fnret, -}; - -fn collectfnrets(c: *cgen, file: *syntax.node) void = { - c.fnrets = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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) *syntax.node = { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, c.curmod)) { return f.rtype; }; - }; - f = f.frnext; - }; - f = c.fnrets; - for (f != nil) { - if (syntax.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) *syntax.node = { - // M1 #22 (#199b): the qualifier may be the import ALIAS the user - // wrote (`utf8`); fn decls register f.fmod under the dotted import - // PATH (`encoding.utf8`). Map alias->path so a nested-package callee - // matches its own module instead of falling back to the name-only - // pass — which a same-leaf caller-module fn (e.g. strings.next vs - // utf8.next) otherwise wins, resolving a match scrutinee to the - // caller's union and collapsing arms 2+. usehint is idempotent on a - // path / c.curmod (returns the input when no `use` matches), so the - // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident - // c.curmod callers are unaffected. The choke-point twin of the - // struct/alias/enum usehint splitters (cgen.ww:128/319, - // cgenutil.ww:2173) — closes the whole fnret class by construction. - let mk: str = usehint(c, mod); - if (mk.len > 0) { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, mk)) { 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) *syntax.node = { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, c.curmod)) { return f.params; }; - }; - f = f.frnext; - }; - f = c.fnrets; - for (f != nil) { - if (syntax.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 (syntax.streq(f.fname, name)) { - if (syntax.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) *syntax.node = { - // M1 #22 (#199b): map import alias -> dotted path, identical to - // fnretlookupmod (the param-side twin). usehint is idempotent on a - // path / c.curmod so existing callers are unaffected. - let mk: str = usehint(c, mod); - if (mk.len > 0) { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, mk)) { 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: *syntax.node, - dtnode: *syntax.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: *syntax.node) void = { - c.defs = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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 (syntax.streq(e.dname, name)) { - if (syntax.streq(e.dmod, c.curmod)) { return true; }; - }; - e = e.dnext; - }; - e = c.defs; - for (e != nil) { - if (syntax.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) *syntax.node = { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.streq(e.dname, name)) { - if (syntax.streq(e.dmod, c.curmod)) { return e.drhs; }; - }; - e = e.dnext; - }; - e = c.defs; - for (e != nil) { - if (syntax.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) *syntax.node = { - if (mod.len > 0) { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.streq(e.dname, name)) { - if (syntax.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: *syntax.node) bool = { - let r: *syntax.node = rhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - if (r != nil) { - if (r.kind == syntax.nkind.N_UN) { - if (r.op == syntax.tkind.TK_MINUS) { - r = r.lhs; - for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; - } else { if (r.op == syntax.tkind.TK_PLUS) { - r = r.lhs; - for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; - }; }; - }; - }; - if (r == nil) { return false; }; - return r.kind == syntax.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: *syntax.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: *syntax.node = defvartnode(c, nm); - if (dtn != nil) { - let du88: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo); - if (du88 != nil) { if (du88.kind == syntax.tykind.TY_ARRAY) { return true; }; }; - }; - let drhs: *syntax.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`) - omod: str, // owning module of a `use` decl (#40); unused for mods - mnext: *modent, -}; - -fn collectmods(c: *cgen, file: *syntax.node) void = { - c.mods = nil; - c.uses = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - // M1 #22: record alias→path for the qualified-ref hint. - if (d.kind == syntax.nkind.N_USE) { - if (d.usepath.len > 0) { - let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, mnext=c.uses})!; - c.uses = um; - }; - }; - // 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 == syntax.nkind.N_FNDECL) { - // Fns mangle regardless of export status — covers - // lib/os.read vs lib/io.read collision. - let isffi: bool = false; - let a: *syntax.node = d.attr; - for (a != nil) { - if (a.kind == syntax.nkind.N_ATTR) { - let an: str = a.str; - if (syntax.streq(an, "symbol")) { isffi = true; }; - }; - a = a.next; - }; - if (!isffi) { - // #84: register bare-module (package-less `//ww:module- - // reset`) fns TOO (nmod.len==0) — previously the - // `nmod.len > 0` gate skipped ALL bare fns, so a bare fn - // never entered c.mods and a bare-ident ref to it first- - // matched an imported same-leaf fn (modlookupforfn) → a - // #40 residual / #263-class silent dead-dup. With the bare - // entry present, modlookupforfn prefers it on an empty - // hint. main is the linker entry convention: a `package - // main` primary's decls are MODULED "main" so main is NOT - // bare here — skip it (cgfn force-emits the bare `main` - // label). M1 #32: only ROOT main (imported==0) stays bare; - // an IMPORTED `fn main` mangles on its path. Mirrors - // cstage cgen.c mod_collect. - // #99: under sep a dep unit's main is imported==0 too (its - // body is composed with a path-carrying `//ww:module-reset`, - // #57), so imported==0 no longer means "root unit" per-unit. - // sepisdep (wwiout==nil <=> root/link-entry unit, #69) - // re-mangles a dep's main; only the root's stays bare. - if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - }; - // §7-A / #53: exported non-fn decls (def/type/let) path-qualify - // like fns — `export def MAX` becomes `.MAX`, not a bare - // `MAX` two packages could clash on under sep-compile. No export - // guard: every decl with a module mangles identically. - // #85 (deferred): the `nmod.len > 0` gate below retains the - // bare-NON-fn skip — the sibling of #84 (a package-less let/def/ - // type leaf colliding with an imported same-kind export) needs a - // different fix (modlookup is hint-LESS) and has no corpus repro; - // retained until then (rule-11). - if (d.kind == syntax.nkind.N_DEF) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - if (d.kind == syntax.nkind.N_TYPEDECL) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - if (d.kind == syntax.nkind.N_LET) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=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 (syntax.streq(m.mname, name)) { return m.nmod; }; - m = m.mnext; - }; - let empty: str; - empty.ptr = nil; - empty.len = 0; - return empty; -}; - -// usehint — M1 #22: map a qualified-ref alias (`utf8`) to its dotted -// import path (`encoding.utf8`) so the codegen hint keys the path-keyed -// mods map. For single-level packages alias == path (no-op). Returns the -// alias unchanged when no matching `use` exists. -// -// NOT file-global (#40): two modules in one unit may bind the same leaf -// alias to different paths (module one's `import a.math` vs module two's -// `import b.math`, both alias `math`). The `use` declared in the SAME -// module as the reference (c.curmod) is authoritative; preferring it -// routes each `math.pick()` to its own package. Falls back to any -// matching alias when c.curmod has no own import. Mirrors the checker's -// use_path curmod-preference (cstage check.c, M1 55f54fb). -fn usehint(c: *cgen, alias: str) str = { - let m: *modent = c.uses; - let any: str; - any.ptr = nil; - any.len = 0; - for (m != nil) { - if (syntax.streq(m.mname, alias)) { - if (syntax.streq(m.omod, c.curmod)) { return m.nmod; }; - if (any.ptr == nil && any.len == 0) { any = m.nmod; }; - }; - m = m.mnext; - }; - if (any.ptr != nil || any.len != 0) { return any; }; - return alias; -}; - -// 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 (syntax.streq(m.mname, name)) { - if (m.nmod.len == 0) { - // #84: a bare-module entry (now registered). A bare-ident - // ref (hint.len==0: the caller is itself in the bare/root - // module) resolves to it — bare wins over an imported - // same-leaf fn, by construction, order-independent. A - // non-empty hint named a module, so a bare decl is - // irrelevant: skip it, leaving the legacy first-imported- - // match untouched (byte-id-neutral for moduled callers). - if (hint.len == 0) { - let empty: str; - empty.ptr = nil; - empty.len = 0; - return empty; - }; - } else { - if (hint.len > 0 && syntax.streq(m.nmod, hint)) { - return m.nmod; - }; - if (first.len == 0 && first.ptr == nil) { - first = m.nmod; - }; - }; - }; - m = m.mnext; - }; - return first; -}; - -// emitsymname — write the asm symbol name for `ident`. Honours, in -// order: FFI mapping (@symbol), module mangling (private decls), bare -// name. Use everywhere a top-level non-fn name is emitted before `(SB)` -// — DATA labels for top-level lets/defs, address-of-let, etc. Fn names -// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates -// cross-module same-leaf fn exports. -fn emitsymname(c: *cgen, ident: str) void = { - let resolved: str = ffiresolve(c, ident); - if (resolved.ptr != ident.ptr) { - // FFI hit — emit the mapped linker symbol verbatim. - emitbytes( resolved.ptr, resolved.len: u64); - return; - }; - let mod: str = modlookup(c, ident); - if (mod.len > 0) { - emitbytes( mod.ptr, mod.len: u64); - emitbytes( ".".ptr, 1u64); - }; - emitbytes( ident.ptr, ident.len: u64); -}; - -// emitfnname — write the asm symbol name for a fn `ident`, threading -// `hint` (the explicit module from a `mod.fn` use site, or c.curmod -// for bare-IDENT calls) through modlookupforfn. Same FFI override -// semantics as emitsymname; same dot-separator format. Use at every -// CALL / LEAQ-of-fn / TEXT-def site. -fn emitfnname(c: *cgen, ident: str, hint: str) void = { - let resolved: str = ffiresolve(c, ident); - if (resolved.ptr != ident.ptr) { - emitbytes( resolved.ptr, resolved.len: u64); - return; - }; - let mod: str = modlookupforfn(c, ident, hint); - if (mod.len > 0) { - emitbytes( mod.ptr, mod.len: u64); - emitbytes( ".".ptr, 1u64); - }; - emitbytes( ident.ptr, ident.len: u64); -}; - -// ---- FFI map --------------------------------------------------------- - -fn fficollect(c: *cgen, file: *syntax.node) void = { - c.ffis = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_FNDECL) { - let a: *syntax.node = d.attr; - for (a != nil) { - if (a.kind == syntax.nkind.N_ATTR) { - let aname: str = a.str; - if (syntax.streq(aname, "symbol")) { - let symnode: *syntax.node = a.list; - if (symnode != nil) { - if (symnode.kind == syntax.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 (syntax.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 "?"; -}; - -//ww:module-reset -// wwi.ww — `.wwi` export-data producer (w6c_ww -I). M2 DEAD-CODE: writes -// a re-parseable ww-prototype rendering of a package's EXPORTED surface. -// Nothing consumes `.wwi` yet (combined.ww stays the live path); the only -// caller is the new -I flag, off on every existing invocation. -// -// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10 -// requirement (`.wwi` is a new cross-stage byte-id substrate). Specs: -// .ai/rob-M2-spec.md + .ai/drew-M2-checkexported.md. -// -// - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the -// tinfo — tinfo collapses nominal pkg.Name identity. -// - check_exported_type rides the producer entry (flag-gated), so M2 -// stays dead on the normal `.s` path. It rejects exactly one thing: -// an exported signature naming a non-exported nominal type. -// - A `.wwi` is ONE package's interface; the M2 gate feeds a driver- -// combined unit, so the emit filters to PRIMARY decls (imported==0). - -// Imports mirror check.ww (os/tok/strconv only): node/nkind/sym/skind/ -// scopelookuptype/streq/checker/tkind resolve bare in the selfhost's flat -// bundled scope, exactly as check.ww references them. -package wcc; - -import os; -import syntax; -import strconv; - -// --- byte writers ------------------------------------------------------ - -fn wputs(fd: i32, s: str) void = { - os.write(fd, s.ptr, s.len: u64); -}; - -fn wputb(fd: i32, b: u8) void = { - let buf: [1]u8; - buf[0] = b; - os.write(fd, buf.ptr, 1u64); -}; - -fn wquote(fd: i32, s: str) void = { - wputb(fd, '"'); - let i: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - if (c == '"') { - wputs(fd, "\\\""); - } else { if (c == '\\') { - wputs(fd, "\\\\"); - } else { if (c == '\n') { - wputs(fd, "\\n"); - } else { if (c == '\t') { - wputs(fd, "\\t"); - } 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 { - wputb(fd, c); - };};};};}; - i += 1; - }; - wputb(fd, '"'); -}; - -// --- check_exported_type (drew) --------------------------------------- -// -// Resolve an N_TNAME to its type sym via the public sym helpers (mirror -// of cstage wwi_typesym / scope_lookup_type) WITHOUT the side effects of -// the checker's aliassym (no on-demand resolve, no double error). A -// primitive/keyword resolves to no SK_TYPE → leaf. By producer time -// checkfile has finished and c.cur == c.top. - -fn wwitypesym(c: *checker, nm: str) *syntax.sym = { - let empty: str; - let s: *syntax.sym = syntax.scopelookuptype(c.cur, empty, nm); - if (s == nil) { - let dotidx: i32 = -1; - let i: i32 = 0; - for (i < nm.len) { - if (nm[i] == 46u8) { dotidx = i; }; - i += 1; - }; - if (dotidx >= 0) { - let leaf: str; - leaf.ptr = nm.ptr + ((dotidx + 1): u64); - leaf.len = nm.len - dotidx - 1; - s = syntax.scopelookuptype(c.cur, empty, leaf); - }; - }; - if (s == nil) { return nil; }; - if (s.skind != syntax.skind.SK_TYPE) { return nil; }; - return s; -}; - -fn wwireject(d: *syntax.node, nm: str) void = { - wputs(2, d.file); - wputs(2, ":"); - wputs(2, strconv.u64tos(d.line: u64, strconv.base.DEC)); - wputs(2, ":"); - wputs(2, strconv.u64tos(d.col: u64, strconv.base.DEC)); - wputs(2, ": error: exported declaration references unexported type '"); - wputs(2, nm); - wputs(2, "'\n"); -}; - -// Returns 1 if a non-exported nominal was named (loud), else 0. -fn wwichecktype(c: *checker, d: *syntax.node, t: *syntax.node) i32 = { - if (t == nil) { return 0; }; - // rule-10: the wwstage checker wraps N_TTUPLE.list elements in - // N_TPARAM (ast.ww:101); cstage keeps the type-AST pristine. Unwrap - // transparently so the recursion sees the same shape cstage walks. - if (t.kind == syntax.nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); }; - let bad: i32 = 0; - if (t.kind == syntax.nkind.N_TNAME) { - let s: *syntax.sym = wwitypesym(c, t.str); - // sym.exported is vestigial (never set); the nominal's export - // status lives on its decl node, parser-set. - if (s != nil) { - if (s.decl != nil) { - if (s.decl.kind == syntax.nkind.N_TYPEDECL) { - // file.len == 0 ⇒ a checkinit-synthesized - // predeclared builtin (the ONLY empty-source - // decls: nomemdecl check.ww:126, the `void` - // TNAME check.ww:122), not a user nominal — ww's - // analogue of harec's STORAGE_NOMEM leaf-arm, and - // why cstage (lookup_builtin, no scope SK_TYPE) - // needs no such guard. - if (s.decl.file.len > 0) { - if (s.decl.exported == 0) { - wwireject(d, t.str); - bad = 1; - }; - }; - }; - }; - }; - } else { if ( - t.kind == syntax.nkind.N_TPTR || - t.kind == syntax.nkind.N_TSLICE || - t.kind == syntax.nkind.N_TBANG || - t.kind == syntax.nkind.N_TCHAN - ) { - bad = bad | wwichecktype(c, d, t.lhs); - } else { if (t.kind == syntax.nkind.N_TARRAY) { - // element only; the length is a const-expr, not a type. - bad = bad | wwichecktype(c, d, t.lhs); - } else { if (t.kind == syntax.nkind.N_TFN) { - let p: *syntax.node = t.list; - for (p != nil) { - bad = bad | wwichecktype(c, d, p.lhs); - p = p.next; - }; - bad = bad | wwichecktype(c, d, t.lhs); - } else { if (t.kind == syntax.nkind.N_TSTRUCT) { - let f: *syntax.node = t.list; - for (f != nil) { - bad = bad | wwichecktype(c, d, f.lhs); - f = f.next; - }; - } else { if (t.kind == syntax.nkind.N_TTAGGED || t.kind == syntax.nkind.N_TTUPLE) { - let e: *syntax.node = t.list; - for (e != nil) { - bad = bad | wwichecktype(c, d, e); - e = e.next; - }; - } else { if (t.kind == syntax.nkind.N_TENUM) { - // the inline enum body is the definition, not a reference; - // recurse only its storage type (members are values). - bad = bad | wwichecktype(c, d, t.lhs); - };};};};};};}; - return bad; -}; - -fn wwicheckdecl(c: *checker, d: *syntax.node) i32 = { - let bad: i32 = 0; - if (d.kind == syntax.nkind.N_FNDECL) { - let p: *syntax.node = d.list; - for (p != nil) { - bad = bad | wwichecktype(c, d, p.lhs); - p = p.next; - }; - bad = bad | wwichecktype(c, d, d.lhs); // ret - } else { if (d.kind == syntax.nkind.N_TYPEDECL) { - bad = bad | wwichecktype(c, d, d.lhs); - } else { if (d.kind == syntax.nkind.N_DEF) { - // the declared type; the rhs const value is not a type. - bad = bad | wwichecktype(c, d, d.lhs); - } else { if (d.kind == syntax.nkind.N_LET) { - bad = bad | wwichecktype(c, d, d.lhs); - };};};}; - return bad; -}; - -// --- type-expr + const-expr unparser (rob §2.2/§2.4) ------------------ - -// The lexer's rune escapes stop at \xHH (no \u/\U — task #50), so a -// codepoint above 0xff can't render as a re-parseable rune literal; fail -// loud rather than emit a malformed one. No exported def names such a rune -// today (RUNE_MAX is written as an int-cast for the same reason). -fn wwirune(fd: i32, cp: u64) void = { - if (cp > 255u64) { - wputs(2, "wwi: rune codepoint exceeds \\xHH (task #50)\n"); - os.exit(1); - }; - let c: u8 = cp: u8; - wputb(fd, '\''); - if (c == '\'') { - wputs(fd, "\\'"); - } else { if (c == '\\') { - wputs(fd, "\\\\"); - } else { if (c == '\n') { - wputs(fd, "\\n"); - } else { if (c == '\t') { - wputs(fd, "\\t"); - } else { if (c < 32u8 || c >= 127u8) { - 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 { - wputb(fd, c); - };};};};}; - wputb(fd, '\''); -}; - -fn wwiexpr(fd: i32, e: *syntax.node) void = { - if (e == nil) { return; }; - if (e.kind == syntax.nkind.N_INTLIT) { - wputs(fd, strconv.u64tos(e.uval, strconv.base.DEC)); - if (e.tsuffix.len > 0) { wputs(fd, e.tsuffix); }; - } else { if (e.kind == syntax.nkind.N_IDENT || e.kind == syntax.nkind.N_TNAME) { - wputs(fd, e.str); - } else { if (e.kind == syntax.nkind.N_DOT) { - wwiexpr(fd, e.lhs); - wputb(fd, '.'); - wputs(fd, e.str); - } else { if (e.kind == syntax.nkind.N_TRUE) { - wputs(fd, "true"); - } else { if (e.kind == syntax.nkind.N_FALSE) { - wputs(fd, "false"); - } else { if (e.kind == syntax.nkind.N_NIL) { - wputs(fd, "nil"); - } else { if (e.kind == syntax.nkind.N_STRLIT) { - wquote(fd, e.str); - } else { if (e.kind == syntax.nkind.N_RUNELIT) { - // A bare rune literal in a const-expr (types.RUNE_MIN = '\0'); - // casts never reach here — the checker folds a const cast to an - // integer literal before the producer runs (RUNE_MAX - // `0x10ffff: rune` emits as the plain int 1114111). - wwirune(fd, e.uval); - } else { if (e.kind == syntax.nkind.N_BIN) { - wwiexpr(fd, e.lhs); - wputb(fd, 32u8); - wputs(fd, syntax.tokname(e.op)); - wputb(fd, 32u8); - wwiexpr(fd, e.rhs); - } else { if (e.kind == syntax.nkind.N_UN) { - wputs(fd, syntax.tokname(e.op)); - wwiexpr(fd, e.lhs); - } else { - wputs(2, "wwi: unhandled const-expr node kind\n"); - os.exit(1); - };};};};};};};};};}; -}; - -fn wwiparam(fd: i32, p: *syntax.node) void = { - if (syntax.streq(p.str, "...")) { // C-style FFI `...` - wputs(fd, "..."); - return; - }; - if (p.str.len > 0) { - wputs(fd, p.str); - wputs(fd, ": "); - }; - // rule-10: the wwstage checker desugars a Hare variadic `T...` param - // in place to `[]T` (lhs becomes N_TSLICE); cstage leaves lhs == T. - // Peel the inserted slice so both stages emit the surface `T...`. - if (p.op == syntax.tkind.TK_ELLIPSIS && p.lhs != nil && p.lhs.kind == syntax.nkind.N_TSLICE) { - wwitype(fd, p.lhs.lhs); - } else { - wwitype(fd, p.lhs); - }; - if (p.op == syntax.tkind.TK_ELLIPSIS) { // Hare `T...` variadic - wputs(fd, "..."); - }; -}; - -fn wwitype(fd: i32, t: *syntax.node) void = { - if (t == nil) { // absent return type spells void - wputs(fd, "void"); - return; - }; - // rule-10: unwrap the wwstage-only N_TPARAM tuple-element wrapper - // (ast.ww:101) so the unparse matches cstage's pristine type-AST. - if (t.kind == syntax.nkind.N_TPARAM) { wwitype(fd, t.lhs); return; }; - if (t.kind == syntax.nkind.N_TNAME) { - if (t.str.len > 0) { wputs(fd, t.str); } else { wputs(fd, "void"); }; - } else { if (t.kind == syntax.nkind.N_TPTR) { - wputb(fd, '*'); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TSLICE) { - wputs(fd, "[]"); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TARRAY) { - wputb(fd, '['); - if (t.rhs != nil) { wwiexpr(fd, t.rhs); } else { wputb(fd, '_'); }; - wputb(fd, ']'); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TBANG) { - wputb(fd, '!'); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TCHAN) { - wputs(fd, "chan "); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TFN) { - wputs(fd, "fn("); - let p: *syntax.node = t.list; - for (p != nil) { - if (p != t.list) { wputs(fd, ", "); }; - wwiparam(fd, p); - p = p.next; - }; - wputs(fd, ") "); - wwitype(fd, t.lhs); - } else { if (t.kind == syntax.nkind.N_TSTRUCT) { - wputs(fd, "struct { "); - let f: *syntax.node = t.list; - for (f != nil) { - if (f != t.list) { wputs(fd, ", "); }; - if (f.str.len > 0) { - wputs(fd, f.str); - wputs(fd, ": "); - }; - wwitype(fd, f.lhs); - f = f.next; - }; - wputs(fd, " }"); - } else { if (t.kind == syntax.nkind.N_TTUPLE) { - wputb(fd, '('); - let e: *syntax.node = t.list; - for (e != nil) { - if (e != t.list) { wputs(fd, ", "); }; - wwitype(fd, e); - e = e.next; - }; - wputb(fd, ')'); - } else { if (t.kind == syntax.nkind.N_TTAGGED) { - wputb(fd, '('); - let e: *syntax.node = t.list; - for (e != nil) { - if (e != t.list) { wputs(fd, " | "); }; - // #95: re-emit the spread marker purely syntactically; - // the consumer's type-store flattens - // (ref/hare/hare/unparse/type.ha:290-300). - if (e.op == syntax.tkind.TK_ELLIPSIS) { wputs(fd, "..."); }; - wwitype(fd, e); - e = e.next; - }; - wputb(fd, ')'); - } else { if (t.kind == syntax.nkind.N_TENUM) { - wputs(fd, "enum "); - if (t.lhs != nil) { - wwitype(fd, t.lhs); - wputb(fd, 32u8); - }; - wputs(fd, "{ "); - let m: *syntax.node = t.list; - for (m != nil) { - if (m != t.list) { wputs(fd, ", "); }; - wputs(fd, m.str); - if (m.lhs != nil) { - wputs(fd, " = "); - wwiexpr(fd, m.lhs); - }; - m = m.next; - }; - wputs(fd, " }"); - } else { - wputs(2, "wwi: unhandled type node kind\n"); - os.exit(1); - };};};};};};};};};};}; -}; - -// Only codegen/link-relevant attributes round-trip into the `.wwi`. Today -// that is exactly @symbol (the FFI link-symbol override, read back at cgen -// fficollect — dropping it makes sep-compile emit `CALL malloc` for a -// `@symbol("rt_malloc")` fn). Named for the class so @align/@offset would -// slot in here IF ww ever grows field-layout attributes — it has none -// today (task #47 report). @test never reaches a `.wwi` (test fns are not -// export-marked), so it needs no exclusion arm. -fn wwiattrrelevant(nm: str) bool = { - return syntax.streq(nm, "symbol"); -}; - -fn wwiattrs(fd: i32, d: *syntax.node) void = { - let a: *syntax.node = d.attr; - for (a != nil) { - if (a.kind == syntax.nkind.N_ATTR && wwiattrrelevant(a.str)) { - wputb(fd, '@'); - wputs(fd, a.str); - if (a.list != nil) { - wputb(fd, '('); - let arg: *syntax.node = a.list; - for (arg != nil) { - if (arg != a.list) { wputs(fd, ", "); }; - wwiexpr(fd, arg); - arg = arg.next; - }; - wputb(fd, ')'); - }; - wputb(fd, 32u8); - }; - a = a.next; - }; -}; - -fn wwidecl(fd: i32, d: *syntax.node) void = { - if (d.kind == syntax.nkind.N_FNDECL) { - wwiattrs(fd, d); - wputs(fd, "export fn "); - wputs(fd, d.str); - wputb(fd, '('); - let p: *syntax.node = d.list; - for (p != nil) { - if (p != d.list) { wputs(fd, ", "); }; - wwiparam(fd, p); - p = p.next; - }; - wputs(fd, ") "); - wwitype(fd, d.lhs); - wputs(fd, ";\n"); - } else { if (d.kind == syntax.nkind.N_TYPEDECL) { - wputs(fd, "export type "); - wputs(fd, d.str); - wputs(fd, " = "); - wwitype(fd, d.lhs); - wputs(fd, ";\n"); - } else { if (d.kind == syntax.nkind.N_DEF) { - wputs(fd, "export def "); - wputs(fd, d.str); - wputs(fd, ": "); - wwitype(fd, d.lhs); - // An aggregate initializer (N_STRUCTLIT/N_ARRLIT) is a DATA-global - // (#52): emit a value-LESS prototype `export def X: T;`. The - // defining package's own .o emits the struct/array DATA; the - // importer registers the def by TYPE only (DATA-suppression - // already in cgen) and field/element reads become external refs - // the linker fills. Boundary (rule 7): this gives up importer- - // side const-fold of an aggregate def's FIELDS — a no-op, ww - // never folds struct-literal field access, and an aggregate-def - // field demanded in a const-fold context stays a LOUD error (#71). - if (d.rhs != nil && (d.rhs.kind == syntax.nkind.N_STRUCTLIT || - d.rhs.kind == syntax.nkind.N_ARRLIT)) { - wputs(fd, ";\n"); - } else { - wputs(fd, " = "); - wwiexpr(fd, d.rhs); - wputs(fd, ";\n"); - }; - } else { if (d.kind == syntax.nkind.N_LET) { - wputs(fd, "export let "); - wputs(fd, d.str); - wputs(fd, ": "); - if (d.lhs == nil) { - wputs(2, "wwi: exported let has no declared type\n"); - os.exit(1); - }; - wwitype(fd, d.lhs); - wputs(fd, ";\n"); - };};};}; -}; - -// --- deterministic ordering (rob §3) ---------------------------------- - -fn wwiprimary(n: *syntax.node) bool = { - // imported==1 marks a decl reached through a `//ww:module ` - // boundary (an imported module's concatenated section). - if (n == nil) { return false; }; - return n.imported == 0; -}; - -fn wwiisdecl(d: *syntax.node) bool = { - return d.kind == syntax.nkind.N_FNDECL || d.kind == syntax.nkind.N_TYPEDECL || - d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_LET; -}; - -// strcmp — byte lexicographic, mirror C strcmp sign (<0/0/>0). Both -// stages key the sort identically, so the `.wwi` order is deterministic. -fn wwistrcmp(a: str, b: str) i32 = { - let i: i32 = 0; - for (i < a.len && i < b.len) { - let ca: i32 = a[i]: i32; - let cb: i32 = b[i]: i32; - if (ca != cb) { return ca - cb; }; - i += 1; - }; - return a.len - b.len; -}; - -// Selection sort over parallel (key, node) arrays. Total order keyed on -// the symbol name; ties broken by original index — so the result is -// stable regardless of any same-name collision, matching cstage's qsort -// + idx tiebreak. -fn wwisortdecls(keys: []str, nodes: []*syntax.node, n: i32) void = { - let i: i32 = 0; - for (i < n) { - let best: i32 = i; - let j: i32 = i + 1; - for (j < n) { - let r: i32 = wwistrcmp(keys[j], keys[best]); - if (r < 0) { best = j; }; - j += 1; - }; - if (best != i) { - let tk: str = keys[i]; keys[i] = keys[best]; keys[best] = tk; - let tn: *syntax.node = nodes[i]; nodes[i] = nodes[best]; nodes[best] = tn; - }; - i += 1; - }; -}; - -export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { - // §5: check_exported_type FIRST, before any byte — a producer - // without it can emit a dangling `.wwi`. - let bad: i32 = 0; - let d: *syntax.node = file.list; - for (d != nil) { - if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { - bad = bad | wwicheckdecl(c, d); - }; - d = d.next; - }; - if (bad != 0) { return 1i32; }; - - let fd: i32 = os.open(path, - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { - wputs(2, "w6c: cannot open "); - wputs(2, path); - wputs(2, "\n"); - return 1i32; - }; - - // package line: leaf of the first primary decl's module tag. - let pkg: str = "main"; - let pd: *syntax.node = file.list; - for (pd != nil) { - if (wwiprimary(pd) && pd.nmod.len > 0) { - let dotidx: i32 = -1; - let i: i32 = 0; - for (i < pd.nmod.len) { - if (pd.nmod[i] == 46u8) { dotidx = i; }; - i += 1; - }; - if (dotidx >= 0) { - let leaf: str; - leaf.ptr = pd.nmod.ptr + ((dotidx + 1): u64); - leaf.len = pd.nmod.len - dotidx - 1; - pkg = leaf; - } else { - pkg = pd.nmod; - }; - pd = nil; - } else { - pd = pd.next; - }; - }; - wputs(fd, "package "); - wputs(fd, pkg); - wputs(fd, ";\n"); - - // imports — primary N_USE, byte-sorted by import path. - let nuse: i32 = 0; - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_USE && wwiprimary(u)) { nuse += 1; }; - u = u.next; - }; - if (nuse > 0) { - let upaths: []str = alloc([], nuse: u64)!; - upaths.len = nuse; - let unodes: []*syntax.node = alloc([], nuse: u64)!; - unodes.len = nuse; - let k: i32 = 0; - u = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_USE && wwiprimary(u)) { - if (u.usepath.len > 0) { upaths[k] = u.usepath; } else { upaths[k] = u.str; }; - unodes[k] = u; - k += 1; - }; - u = u.next; - }; - wwisortdecls(upaths, unodes, nuse); - let i: i32 = 0; - for (i < nuse) { - wputs(fd, "import "); - wputs(fd, upaths[i]); - wputs(fd, ";\n"); - i += 1; - }; - }; - - // decls — exported primary, byte-sorted by symbol name. - let ndecl: i32 = 0; - d = file.list; - for (d != nil) { - if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { ndecl += 1; }; - d = d.next; - }; - if (ndecl > 0) { - let dkeys: []str = alloc([], ndecl: u64)!; - dkeys.len = ndecl; - let dnodes: []*syntax.node = alloc([], ndecl: u64)!; - dnodes.len = ndecl; - let k: i32 = 0; - d = file.list; - for (d != nil) { - if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { - dkeys[k] = d.str; - dnodes[k] = d; - k += 1; - }; - d = d.next; - }; - wwisortdecls(dkeys, dnodes, ndecl); - let i: i32 = 0; - for (i < ndecl) { - wwidecl(fd, dnodes[i]); - i += 1; - }; - }; - - os.close(fd); - return 0i32; -}; - -//ww:module-reset -// selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c. -// -// w6c = amd64 compiler. Read .ww, parse, codegen, emit Plan 9 amd64 -// asm to stdout (or the file given by -o). -// -// w6c_ww -o file.s file.ww -// -// The cgen routines in selfhost/cmd/wcc/cgen.ww write directly to -// fd 1 via os.write(1, ...). For -o, we open the output file and -// dup2 it onto fd 1 before invoking cgfile. This is the same trick -// the bootstrap uses with shell redirection, just in-process. - -package main; - -import os; -import rt; -import strings; -import syntax; -import check; -import cgen; -import wwi; - -fn cstreq(a: *u8, lit: str) bool = { - let n: u64 = lit.len: u64; - let i: u64 = 0u64; - for (i < n) { - let li: i32 = i: i32; - if (a[i] != lit[li]) { return false; }; - i += 1u64; - }; - if (a[i] != 0u8) { return false; }; - return true; -}; - -fn cstrlen(p: *u8) u64 = { - let n: u64 = 0u64; - for (p[n] != 0u8) { n += 1u64; }; - return n; -}; - -// pathstr — view a NUL-terminated *u8 as a str. lib/os entrypoints -// take str post-task-#23; this bridges call sites that still hold -// C-string paths (argv entries, arena-allocated buffers). -fn pathstr(p: *u8) str = { - let r: str; - r.ptr = p; - r.len = cstrlen(p): i32; - return r; -}; - -fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil, 0u64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - let n: i64 = 0i64; - match (szr) { - case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return nil, 0u64; }; - }; - let nz: u64 = n: u64; - let buf: []u8 = alloc([], nz + 1u64)!; - buf.len = (nz + 1u64): i32; - let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nz); - os.close(fd); - let got: i64 = 0i64; - match (rr) { - case let v: i64 => got = v; - case let e: os.oserror => return nil, 0u64; - }; - if (got != n) { return nil, 0u64; }; - buf[nz] = 0u8; - return buf.ptr, nz; -}; - -export fn main(argc: i32, argv: **u8) i32 = { - let src: *u8 = nil; - let out: *u8 = nil; - let wwiout: *u8 = nil; // -I : M2 export-data producer - let testmode: i32 = 0i32; // #15: `-T` test-mode - let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- - // only codegen (emit imported==0 decls - // only; treat `.wwi` deps as external) - - let i: i32 = 1; - for (i < argc) { - let a: *u8 = argv[i]; - if (cstreq(a, "-o")) { - i += 1; - if (i >= argc) { - let m: str = "w6c: -o requires arg\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - out = argv[i]; - } else { if (cstreq(a, "-I")) { - i += 1; - if (i >= argc) { - let m: str = "w6c: -I requires arg\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - wwiout = argv[i]; - } else { if (cstreq(a, "-T")) { - testmode = 1i32; - } else { if (cstreq(a, "-c")) { - sepmode = 1i32; - } else { if (a[0u64] == 45u8) { - let m: str = "w6c: unknown flag\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - } else { - if (src != nil) { - let m: str = "w6c: only one input\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - src = a; - }; }; }; }; }; - i += 1; - }; - - if (src == nil) { - let m: str = "usage: w6c_ww [-T] [-o out.s] file.ww\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - - let buf: *u8; - let blen: u64; - buf, blen = slurp(src); - if (buf == nil) { - let m: str = "w6c: cannot read input\n"; - os.write(2, m.ptr, m.len: u64); - return 1; - }; - - // Redirect fd 1 to the output file before any cgen emit runs. - // cgen.ww writes directly to fd 1; dup2 lets us reuse it without - // threading a file descriptor through the emit helpers. - if (out != nil) { - let ofd: i32 = os.open(pathstr(out), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (ofd < 0) { - let m: str = "w6c: cannot open output\n"; - os.write(2, m.ptr, m.len: u64); - return 1; - }; - if (os.dup2(ofd, 1i32) < 0) { - let m: str = "w6c: dup2 failed\n"; - os.write(2, m.ptr, m.len: u64); - os.close(ofd); - return 1; - }; - os.close(ofd); - }; - - let nlen: u64 = cstrlen(src); - let view: str; - view.ptr = src; - view.len = nlen: i32; - let fname: str = strings.dup(view); - - let l: lex; - lexinit(&l, fname, buf, blen); - - let ps: parser; - parserinit(&ps, &l); - let f: *node = parsefile(&ps); - // Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's - // `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches - // cgen and emits junk asm with a zero exit (silent miscompile). - if (l.errs > 0 || ps.errs > 0) { return 1; }; - - // #50: run check before cgen so AST mutations from #42 (size/align/ - // offset fold) and the audit §1.8 node.type_ population land before - // cgen walks the file. Mirrors cmd/w6c/main.c:73-75. Five precondition - // fixes for fixture cleanliness: #51 cross-module type refs, #52 - // enum↔int reinterpret, #53 N_BLOCK scoping, #55 nominal-first variant - // compare, #56 bare-leaf same-module preference. - let tc: tctx; - typesinit(&tc); - let ck: checker; - checkinit(&ck, &tc); - ck.istest = testmode; - checkfile(&ck, f); - if (ck.errs > 0) { return 1; }; - - // M2 export-data: write the `.wwi` after a clean check, before cgen. - // Dead on the live path (no existing invocation passes -I); the - // producer's check_exported_type may reject a dangling export. - if (wwiout != nil) { - if (wwiemit(&ck, f, pathstr(wwiout)) != 0) { return 1; }; - }; - - let cg: cgen; - cgeninit(&cg); - cg.sepmode = sepmode; - // #99: wwiout != nil <=> this is a sep DEP unit (the producer passes - // -I to deps only; the root's .wwi is stripped per #69). Gates the - // bare-`main` carve-out so only the root/link-entry main stays bare. - if (wwiout != nil) { cg.sepisdep = 1i32; }; - cgfile(&cg, f); - return 0; -}; - diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww deleted file mode 100644 index 3cee9fde..00000000 --- a/selfhost/cmd/w6l/main.combined.ww +++ /dev/null @@ -1,5426 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module-reset -// selfhost/cmd/w6l/sym.ww — port of cmd/w6l/sym.c. -// -// Linker symbol table. Singly-linked list, usually a few hundred -// entries; hashing isn't worth it yet. - -package w6l; - -import strings; - -type lsym = struct { - name: str, - val: u64, // offset within combined .text (or .data when - // indata=1) once linked - defined: i32, // 1 if some lobj defines this symbol - indata: i32, // 1 if defined in .data (writable globals) - owner: *lobj, - idxinowner: i32, - // Dynamic-linking fields. Set by resolve when an undefined sym - // is provided by some loaded lso. pltidx and dynsymidx default - // to -1 (set explicitly by resolve; alloc-zeroing gives 0, not -1). - isdyn: i32, - dynlib: *lso, - dynversion: str, // matched export's version; len 0 if none - pltidx: i32, - dynsymidx: i32, - snext: *lsym, -}; - -type lrel = struct { - off: u64, // offset within the relocation's section - section: i32, // 0 = .text, 1 = .data - kind: i32, // R_X86_64_* - sym: *lsym, - addend: i64, - rnext: *lrel, -}; - -type lobj = struct { - path: str, - buf: *u8, // object bytes - len: u64, - textoff: u64, // offset of .text in combined output - textsize: u64, - dataoff: u64, // offset of .data in combined output - datasize: u64, // bytes contributed to combined .data (0 if none) - onext: *lobj, -}; - -// lexport — one entry per GLOBAL/WEAK symbol exported by a loaded .so. -// Stored as a chain in the order the .so's dynsym presents them, so -// soprovides_v's first-match semantics agree with the C version. -type lexport = struct { - name: str, - version: str, // len 0 for unversioned globals - enext: *lexport, -}; - -type lso = struct { - path: str, // full filesystem path used to load - soname: str, // DT_SONAME, or basename if missing - exports: *lexport, // dynsym-order chain of exported names - sonext: *lso, -}; - -type lnk = struct { - objs: *lobj, - sos: *lso, - syms: *lsym, - rels: *lrel, - text: *u8, // combined .text - textcap: u64, - textlen: u64, - // Combined .data (writable). Empty unless any input .o has a - // .data PROGBITS section. - data: *u8, - datacap: u64, - datalen: u64, - errs: i32, - dynn: i32, // number of syms routed through PLT -}; - -export fn intern(l: *lnk, name: str) *lsym = { - let s: *lsym = l.syms; - for (s != nil) { - if (strings.compare(s.name, name) == 0) { return s; }; - s = s.snext; - }; - let n: *lsym = alloc(lsym { name = name, snext = l.syms })!; - l.syms = n; - return n; -}; - -export fn lookup(l: *lnk, name: str) *lsym = { - let s: *lsym = l.syms; - for (s != nil) { - if (strings.compare(s.name, name) == 0) { return s; }; - s = s.snext; - }; - return nil; -}; - -//ww:module-reset -// selfhost/cmd/w6l/obj.ww — port of cmd/w6l/obj.c. -// -// Loads relocatable ELF64 .o files emitted by w6a, appends .text to -// the combined image, and pulls in symbols + relocations with -// offsets adjusted to the combined section. -// -// Also handles SysV `ar` archives (libwwrt.a). The two-pass loader -// indexes members on the first pass and iteratively pulls members -// that define currently-undefined symbols on subsequent passes. - -package w6l; - -import os; -import rt; -import strings; -import sym; - -def ET_REL: i32 = 1; -def EM_X86_64: i32 = 62; -def SHT_PROGBITS: i32 = 1; -def SHT_SYMTAB: i32 = 2; -def SHT_STRTAB: i32 = 3; -def SHT_RELA: i32 = 4; - -// ---- little-endian byte readers ---------------------------------------- -// w6a/w6l use straight LE on amd64. Reading via byte offsets keeps us off -// the cgen's u16 field-load story for now (MOVZBQ exists; MOVZWQ doesn't). - -fn rdu16(p: *u8, off: u64) u16 = { - let b0: u16 = p[off]: u16; - let b1: u16 = p[off + 1u64]: u16; - return b0 | (b1 << 8u16); -}; - -fn rdu32(p: *u8, off: u64) u32 = { - let b0: u32 = p[off]: u32; - let b1: u32 = p[off + 1u64]: u32; - let b2: u32 = p[off + 2u64]: u32; - let b3: u32 = p[off + 3u64]: u32; - return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32); -}; - -fn rdu64(p: *u8, off: u64) u64 = { - let lo: u64 = rdu32(p, off): u64; - let hi: u64 = rdu32(p, off + 4u64): u64; - return lo | (hi << 32u64); -}; - -// ---- ELF64 section header offsets (40 bytes total) -------------------- -def SHDR_SIZE: u64 = 64u64; // sizeof(Shdr) per ELF64 spec -def SHDR_NAME: u64 = 0u64; -def SHDR_TYPE: u64 = 4u64; -def SHDR_OFFSET: u64 = 24u64; -def SHDR_SIZE_F: u64 = 32u64; -def SHDR_LINK: u64 = 40u64; - -// ELF64 ehdr field offsets -def EHDR_SIZE: u64 = 64u64; -def EHDR_TYPE: u64 = 16u64; -def EHDR_MACHINE: u64 = 18u64; -def EHDR_SHOFF: u64 = 40u64; -def EHDR_SHENTSIZE: u64 = 58u64; -def EHDR_SHNUM: u64 = 60u64; -def EHDR_SHSTRNDX: u64 = 62u64; - -// ELF64 sym entry: 24 bytes -def SYM_SIZE: u64 = 24u64; -def SYM_NAME: u64 = 0u64; -def SYM_INFO: u64 = 4u64; -def SYM_SHNDX: u64 = 6u64; -def SYM_VALUE: u64 = 8u64; - -// ELF64 RELA entry: 24 bytes -def RELA_SIZE: u64 = 24u64; -def RELA_OFFSET: u64 = 0u64; -def RELA_INFO: u64 = 8u64; -def RELA_ADDEND: u64 = 16u64; - -// ---- file slurp -------------------------------------------------------- - -fn slurp(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil, 0u64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - let n: i64 = 0i64; - match (szr) { - case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return nil, 0u64; }; - }; - let buf: []u8 = alloc([], n: u64)!; - buf.len = n: i32; - let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, n: u64); - os.close(fd); - let got: i64 = 0i64; - match (rr) { - case let v: i64 => got = v; - case let e: os.oserror => return nil, 0u64; - }; - if (got != n) { return nil, 0u64; }; - return buf.ptr, n: u64; -}; - -// ---- text buffer growth ------------------------------------------------ - -fn emittext(l: *lnk, src: *u8, n: u64) void = { - if (l.textlen + n > l.textcap) { - let nc: u64 = l.textcap; - if (nc == 0u64) { nc = 4096u64; }; - for (nc < l.textlen + n) { nc = nc * 2u64; }; - // Grow by mmap'ing a fresh region and copying. The old buffer - // is leaked into the page allocator; for a linker run this is - // trivial waste. - let nb: []u8 = alloc([], nc)!; - let i: u64 = 0u64; - for (i < l.textlen) { - nb[i] = l.text[i]; - i += 1u64; - }; - l.text = nb.ptr; - l.textcap = nc; - }; - let i: u64 = 0u64; - for (i < n) { - l.text[l.textlen + i] = src[i]; - i += 1u64; - }; - l.textlen += n; -}; - -fn emitdata(l: *lnk, src: *u8, n: u64) void = { - if (l.datalen + n > l.datacap) { - let nc: u64 = l.datacap; - if (nc == 0u64) { nc = 256u64; }; - for (nc < l.datalen + n) { nc = nc * 2u64; }; - let nb: []u8 = alloc([], nc)!; - let i: u64 = 0u64; - for (i < l.datalen) { - nb[i] = l.data[i]; - i += 1u64; - }; - l.data = nb.ptr; - l.datacap = nc; - }; - let i: u64 = 0u64; - for (i < n) { - l.data[l.datalen + i] = src[i]; - i += 1u64; - }; - l.datalen += n; -}; - -// ---- C-string helpers -------------------------------------------------- - -fn cstrlen(p: *u8) u64 = { - let n: u64 = 0u64; - for (p[n] != 0u8) { n += 1u64; }; - return n; -}; - -// pathstr — view a NUL-terminated *u8 as a str. Bridges argv-style -// callers to lib/os entrypoints (str post-task-#23). Shared with -// main.ww and dyn.ww via the w6l bundle. -fn pathstr(p: *u8) str = { - let r: str; - r.ptr = p; - r.len = cstrlen(p): i32; - return r; -}; - -fn cstreq(p: *u8, lit: str) bool = { - let n: u64 = lit.len: u64; - let i: u64 = 0u64; - for (i < n) { - let li: i32 = i: i32; - if (p[i] != lit[li]) { return false; }; - i += 1u64; - }; - if (p[i] != 0u8) { return false; }; - return true; -}; - -// Build a ww str from a NUL-terminated *u8 (for passing to intern). -fn cstrtostr(p: *u8) str = { - let n: u64 = cstrlen(p); - let view: str; - view.ptr = p; - view.len = n: i32; - return strings.dup(view); -}; - -// ---- archive (SysV ar) types and helpers ------------------------------- -// -// Each archive member starts with a 60-byte ar_hdr. The fields we care -// about are the first byte (member type) and the size at offset 48 (a -// 10-byte, space-padded decimal). Member bodies are 2-byte aligned. - -type defent = struct { - name: str, - dnext: *defent, -}; - -type armember = struct { - data: *u8, // owned heap copy of the member's ELF bytes - size: u64, - defs: *defent, // linked list of defined globals - loaded: i32, - mnext: *armember, -}; - -fn isarchive(p: *u8, len: u64) bool = { - if (len < 8u64) { return false; }; - if (p[0u64] != '!' || p[1u64] != '<' || p[2u64] != 'a' || p[3u64] != 'r' || - p[4u64] != 'c' || p[5u64] != 'h' || p[6u64] != '>' || - p[7u64] != '\n') { - return false; - }; - return true; -}; - -// arfield — parse a space-padded decimal integer of width n. -fn arfield(p: *u8, n: u64) u64 = { - let v: u64 = 0u64; - let i: u64 = 0u64; - for (i < n) { - let c: u8 = p[i]; - if (c < 48u8) { return v; }; // space, NUL, etc. - if (c > 57u8) { return v; }; - v = v * 10u64 + ((c - 48u8): u64); - i += 1u64; - }; - return v; -}; - -// elfglobals — return a linked list of names of globally-defined -// (STB_GLOBAL) symbols whose section is `.text`. Names are owned -// heap copies, so the source ELF buffer can be freed afterward. -fn elfglobals(buf: *u8, len: u64) *defent = { - if (len < EHDR_SIZE) { return nil; }; - if (buf[0u64] != 127u8) { return nil; }; - if (buf[1u64] != 'E') { return nil; }; - if (buf[2u64] != 'L') { return nil; }; - if (buf[3u64] != 'F') { return nil; }; - - let shoff: u64 = rdu64(buf, EHDR_SHOFF); - let shnum: u32 = rdu16(buf, EHDR_SHNUM): u32; - let shstrndx: u32 = rdu16(buf, EHDR_SHSTRNDX): u32; - - let shstrshoff: u64 = rdu64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); - let shstr: *u8 = buf + shstrshoff; - - let idxtext: i32 = -1; - let idxdata: i32 = -1; - let idxsymtab: i32 = -1; - let i: u32 = 0u32; - for (i < shnum) { - let secoff: u64 = shoff + (i: u64) * SHDR_SIZE; - let shtype: u32 = rdu32(buf, secoff + SHDR_TYPE); - let shname: u32 = rdu32(buf, secoff + SHDR_NAME); - let nm: *u8 = shstr + (shname: u64); - if (shtype == SHT_PROGBITS: u32) { - if (cstreq(nm, ".text")) { idxtext = i: i32; }; - if (cstreq(nm, ".data")) { idxdata = i: i32; }; - }; - if (shtype == SHT_SYMTAB: u32) { idxsymtab = i: i32; }; - i += 1u32; - }; - if (idxtext < 0) { return nil; }; - if (idxsymtab < 0) { return nil; }; - - let symsh: u64 = shoff + (idxsymtab: u64) * SHDR_SIZE; - let symoff: u64 = rdu64(buf, symsh + SHDR_OFFSET); - let symsize: u64 = rdu64(buf, symsh + SHDR_SIZE_F); - let symlink: u32 = rdu32(buf, symsh + SHDR_LINK); - let nsyms: u64 = symsize / SYM_SIZE; - - let strsh: u64 = shoff + (symlink: u64) * SHDR_SIZE; - let stroff: u64 = rdu64(buf, strsh + SHDR_OFFSET); - let strtab: *u8 = buf + stroff; - - let head: *defent = nil; - let si: u64 = 1u64; - for (si < nsyms) { - let symp: u64 = symoff + si * SYM_SIZE; - let stname: u32 = rdu32(buf, symp + SYM_NAME); - let stinfo: u8 = buf[symp + SYM_INFO]; - let stshndx: u16 = rdu16(buf, symp + SYM_SHNDX); - let bind: u32 = (stinfo: u32) >> 4u32; - // STB_GLOBAL = 1; defined in .text or .data. Both are - // included so an archive member that owns a data global - // gets pulled in when something references it. - if (bind == 1u32) { - if (stshndx != 0u16) { - let intext: bool = (stshndx: i32) == idxtext; - let indt: bool = false; - if (idxdata >= 0) { - indt = (stshndx: i32) == idxdata; - }; - if (intext || indt) { - let nmp: *u8 = strtab + (stname: u64); - if (nmp[0u64] != 0u8) { - let nm: str = cstrtostr(nmp); - let de: *defent = alloc(defent { name = nm, dnext = head })!; - head = de; - }; - }; - }; - }; - si += 1u64; - }; - return head; -}; - -// memberdefinesundef — true if any of m's defined globals matches a -// currently-undefined symbol in the linker's symbol table. Names not -// already interned are uninteresting (the link doesn't need them yet). -fn memberdefinesundef(l: *lnk, m: *armember) bool = { - let de: *defent = m.defs; - for (de != nil) { - let s: *lsym = lookup(l, de.name); - if (s != nil) { - if (s.defined == 0) { return true; }; - }; - de = de.dnext; - }; - return false; -}; - -// loadarchive — port of cmd/w6l/obj.c:load_archive. -// -// Pass 1 indexes every regular member. Pass 2 iteratively pulls in any -// member that supplies a currently-undefined symbol; each pull may -// introduce fresh undefs, so we loop until quiescent. -fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = { - let head: *armember = nil; - let tail: *armember = nil; - let pos: u64 = 8u64; // past "!\n" - for (pos + 60u64 <= len) { - let hdrsize: u64 = arfield(buf + pos + 48u64, 10u64); - let hdrend: u64 = pos + 60u64; - if (hdrend + hdrsize > len) { break; }; - let first: u8 = buf[pos]; - // Skip the symbol table ('/'), long-name table ('//'), and - // any padding entries (NUL or space leading byte). - if (first != '/' && first != 0u8 && first != ' ') { - let m: *armember = alloc(armember { size = hdrsize })!; - let mbs: []u8 = alloc([], hdrsize)!; - let mb: *u8 = mbs.ptr; - let i: u64 = 0u64; - for (i < hdrsize) { - mb[i] = buf[hdrend + i]; - i += 1u64; - }; - m.data = mb; - m.defs = elfglobals(mb, hdrsize); - if (head == nil) { head = m; } - else { tail.mnext = m; }; - tail = m; - }; - pos = hdrend + hdrsize; - if ((hdrsize & 1u64) != 0u64) { pos = pos + 1u64; }; - }; - - let changed: i32 = 1; - for (changed != 0) { - changed = 0; - let m: *armember = head; - for (m != nil) { - if (m.loaded == 0) { - if (memberdefinesundef(l, m)) { - if (loadimage(l, path, m.data, m.size) == 0) { - m.loaded = 1; - changed = 1; - }; - }; - }; - m = m.mnext; - }; - }; - - // Pass 3 (#31, task #62): catch a dup the selective pull MASKED. A - // member pass 2 left unloaded yet defining a name some other object - // already `defined` is exactly the cross-package collision archive - // selective-pull would silently skip. lookup (never intern) keeps - // the pull machinery untouched → byte-id-neutral on every clean - // link. Pulled-member and direct-`.o` dups stay caught in loadimage. - // ww has no weak symbols → a genuine dup. (The bare message matches - // loadimage's; the w6l path+name text divergence is #61, separate.) - let dm: *armember = head; - for (dm != nil) { - if (dm.loaded == 0) { - let de: *defent = dm.defs; - for (de != nil) { - let s: *lsym = lookup(l, de.name); - if (s != nil) { - if (s.defined != 0) { - let msg: str = "w6l: duplicate symbol\n"; - os.write(2, msg.ptr, msg.len: u64); - l.errs += 1; - }; - }; - de = de.dnext; - }; - }; - dm = dm.mnext; - }; - return 0; -}; - -// ---- main loader ------------------------------------------------------- - -export fn load(l: *lnk, path: *u8) i32 = { - let bufp: *u8; - let buflen: u64; - bufp, buflen = slurp(path); - if (bufp == nil) { - let m: str = "w6l: cannot read object\n"; - os.write(2, m.ptr, m.len: u64); - return -1; - }; - if (isarchive(bufp, buflen)) { - return loadarchive(l, path, bufp, buflen); - }; - return loadimage(l, path, bufp, buflen); -}; - -fn loadimage(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = { - if (len < EHDR_SIZE) { return -1; }; - // magic: 0x7f, 'E', 'L', 'F' - if (buf[0u64] != 127u8) { return -1; }; - if (buf[1u64] != 'E') { return -1; }; - if (buf[2u64] != 'L') { return -1; }; - if (buf[3u64] != 'F') { return -1; }; - if (buf[4u64] != 2u8) { return -1; }; // ELFCLASS64 - if (rdu16(buf, EHDR_TYPE) != ET_REL: u16) { return -1; }; - if (rdu16(buf, EHDR_MACHINE) != EM_X86_64: u16) { return -1; }; - - let shoff: u64 = rdu64(buf, EHDR_SHOFF); - let shnum: u32 = rdu16(buf, EHDR_SHNUM): u32; - let shstrndx: u32 = rdu16(buf, EHDR_SHSTRNDX): u32; - - let shstrshoff: u64 = rdu64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); - let shstr: *u8 = buf + shstrshoff; - - // find .text, .data, .symtab, .rela.text, .rela.data - let idxtext: i32 = -1; - let idxdata: i32 = -1; - let idxsymtab: i32 = -1; - let idxrela: i32 = -1; - let idxrelad: i32 = -1; - let i: u32 = 0u32; - for (i < shnum) { - let secoff: u64 = shoff + (i: u64) * SHDR_SIZE; - let shtype: u32 = rdu32(buf, secoff + SHDR_TYPE); - let shname: u32 = rdu32(buf, secoff + SHDR_NAME); - let nm: *u8 = shstr + (shname: u64); - if (shtype == SHT_PROGBITS: u32) { - if (cstreq(nm, ".text")) { idxtext = i: i32; }; - if (cstreq(nm, ".data")) { idxdata = i: i32; }; - }; - if (shtype == SHT_SYMTAB: u32) { idxsymtab = i: i32; }; - if (shtype == SHT_RELA: u32) { - if (cstreq(nm, ".rela.text")) { idxrela = i: i32; }; - if (cstreq(nm, ".rela.data")) { idxrelad = i: i32; }; - }; - i += 1u32; - }; - if (idxtext < 0) { - let m: str = "w6l: missing .text\n"; - os.write(2, m.ptr, m.len: u64); - return -1; - }; - if (idxsymtab < 0) { - let m: str = "w6l: missing .symtab\n"; - os.write(2, m.ptr, m.len: u64); - return -1; - }; - - let textsh: u64 = shoff + (idxtext: u64) * SHDR_SIZE; - let textoff: u64 = rdu64(buf, textsh + SHDR_OFFSET); - let textsize: u64 = rdu64(buf, textsh + SHDR_SIZE_F); - - let symsh: u64 = shoff + (idxsymtab: u64) * SHDR_SIZE; - let symoff: u64 = rdu64(buf, symsh + SHDR_OFFSET); - let symsize: u64 = rdu64(buf, symsh + SHDR_SIZE_F); - let symlink: u32 = rdu32(buf, symsh + SHDR_LINK); - let nsyms: u64 = symsize / SYM_SIZE; - - let strsh: u64 = shoff + (symlink: u64) * SHDR_SIZE; - let stroff: u64 = rdu64(buf, strsh + SHDR_OFFSET); - let strtab: *u8 = buf + stroff; - - let datasize: u64 = 0u64; - let dataoff: u64 = 0u64; - if (idxdata >= 0) { - let datash: u64 = shoff + (idxdata: u64) * SHDR_SIZE; - dataoff = rdu64(buf, datash + SHDR_OFFSET); - datasize = rdu64(buf, datash + SHDR_SIZE_F); - }; - - // Track this object. - let ob: *lobj = alloc(lobj { - path = cstrtostr(path), - buf = buf, - len = len, - textoff = l.textlen, - textsize = textsize, - dataoff = l.datalen, - datasize = datasize, - onext = l.objs, - })!; - l.objs = ob; - - // Append .text bytes to the combined image. - emittext(l, buf + textoff, textsize); - // Append .data bytes (if present) to the combined .data buffer. - if (idxdata >= 0) { - if (datasize > 0u64) { - emitdata(l, buf + dataoff, datasize); - }; - }; - - // Walk symbols. We don't keep a per-object map[] of *lsym. Instead - // the reloc loop re-walks symtab and re-interns by name. Simpler - // than dancing around the cgen's u64-shift gaps. - let si: u64 = 1u64; // skip index 0 (always undef sentinel) - for (si < nsyms) { - let symp: u64 = symoff + si * SYM_SIZE; - let stname: u32 = rdu32(buf, symp + SYM_NAME); - let stshndx: u16 = rdu16(buf, symp + SYM_SHNDX); - let stvalue: u64 = rdu64(buf, symp + SYM_VALUE); - let nmp: *u8 = strtab + (stname: u64); - if (nmp[0u64] != 0u8) { - let nm: str = cstrtostr(nmp); - let gs: *lsym = intern(l, nm); - if (stshndx != 0u16) { - let intext: bool = (stshndx: i32) == idxtext; - let indt: bool = false; - if (idxdata >= 0) { - indt = (stshndx: i32) == idxdata; - }; - if (intext && indt) { indt = false; }; - if (intext) { - if (gs.defined != 0) { - let m: str = "w6l: duplicate symbol\n"; - os.write(2, m.ptr, m.len: u64); - l.errs += 1; - } else { - gs.defined = 1; - gs.owner = ob; - gs.idxinowner = si: i32; - gs.val = ob.textoff + stvalue; - }; - }; - if (indt) { - if (gs.defined != 0) { - let m: str = "w6l: duplicate symbol\n"; - os.write(2, m.ptr, m.len: u64); - l.errs += 1; - } else { - gs.defined = 1; - gs.indata = 1; - gs.owner = ob; - gs.idxinowner = si: i32; - gs.val = ob.dataoff + stvalue; - }; - }; - }; - }; - si += 1u64; - }; - - // Per-object relocation collection. - if (idxrela >= 0) { - let relash: u64 = shoff + (idxrela: u64) * SHDR_SIZE; - let relaoff: u64 = rdu64(buf, relash + SHDR_OFFSET); - let relasize: u64 = rdu64(buf, relash + SHDR_SIZE_F); - let nrel: u64 = relasize / RELA_SIZE; - let ri: u64 = 0u64; - for (ri < nrel) { - let rp: u64 = relaoff + ri * RELA_SIZE; - let roff: u64 = rdu64(buf, rp + RELA_OFFSET); - let rinfo: u64 = rdu64(buf, rp + RELA_INFO); - let raddend: u64 = rdu64(buf, rp + RELA_ADDEND); - let rsymidx: u32 = (rinfo >> 32u64): u32; - let rkind: i32 = ((rinfo & 4294967295u64): u32): i32; - let nr: *lrel = alloc(lrel { - off = ob.textoff + roff, - section = 0, - kind = rkind, - addend = raddend: i64, - rnext = l.rels, - })!; - // Look up the referenced sym by name (re-walk symtab). - if ((rsymidx: u64) < nsyms) { - let sp: u64 = symoff + (rsymidx: u64) * SYM_SIZE; - let sname: u32 = rdu32(buf, sp + SYM_NAME); - let snm: *u8 = strtab + (sname: u64); - if (snm[0u64] != 0u8) { - let nm: str = cstrtostr(snm); - nr.sym = intern(l, nm); - }; - }; - l.rels = nr; - ri += 1u64; - }; - }; - - // Data-reloc collection. Offsets land in .data, shifted by - // this object's data_off so they index the combined buffer. - if (idxrelad >= 0) { - let relash: u64 = shoff + (idxrelad: u64) * SHDR_SIZE; - let relaoff: u64 = rdu64(buf, relash + SHDR_OFFSET); - let relasize: u64 = rdu64(buf, relash + SHDR_SIZE_F); - let nrel: u64 = relasize / RELA_SIZE; - let ri: u64 = 0u64; - for (ri < nrel) { - let rp: u64 = relaoff + ri * RELA_SIZE; - let roff: u64 = rdu64(buf, rp + RELA_OFFSET); - let rinfo: u64 = rdu64(buf, rp + RELA_INFO); - let raddend: u64 = rdu64(buf, rp + RELA_ADDEND); - let rsymidx: u32 = (rinfo >> 32u64): u32; - let rkind: i32 = ((rinfo & 4294967295u64): u32): i32; - let nr: *lrel = alloc(lrel { - off = ob.dataoff + roff, - section = 1, - kind = rkind, - addend = raddend: i64, - rnext = l.rels, - })!; - if ((rsymidx: u64) < nsyms) { - let sp: u64 = symoff + (rsymidx: u64) * SYM_SIZE; - let sname: u32 = rdu32(buf, sp + SYM_NAME); - let snm: *u8 = strtab + (sname: u64); - if (snm[0u64] != 0u8) { - let nm: str = cstrtostr(snm); - nr.sym = intern(l, nm); - }; - }; - l.rels = nr; - ri += 1u64; - }; - }; - - return 0; -}; - -//ww:module-reset -// selfhost/cmd/w6l/dyn.ww — port of cmd/w6l/dyn.c. -// -// Load a shared object (ET_DYN) so the linker knows which symbols it -// exports and which DT_NEEDED entry to record. We do not pull bytes -// from the .so; the dynamic loader maps it at runtime. -// -// Each call appends one lso to lnk->sos. l_so_provides_v answers -// "does this .so export the named symbol, and at which version?" — -// l_resolve uses that to promote unresolved references to dynamic. - -package w6l; - -import os; -import rt; -import strings; -import sym; - -def ET_DYN_SO: u16 = 3u16; -def EM_X86_64_SO: u16 = 62u16; - -def SHT_DYNAMIC: u32 = 6u32; -def SHT_DYNSYM: u32 = 11u32; -// GNU extensions, sh_type values. -def SHT_GNU_VERDEF: u32 = 1879048189u32; // 0x6ffffffd -def SHT_GNU_VERNEED: u32 = 1879048190u32; // 0x6ffffffe -def SHT_GNU_VERSYM: u32 = 1879048191u32; // 0x6fffffff - -def DT_NULL_TAG: i64 = 0i64; -def DT_SONAME_TAG: i64 = 14i64; - -// Versym special values. -def VER_NDX_LOCAL_C: u16 = 0u16; -def VER_NDX_GLOBAL_C: u16 = 1u16; -def VERSYM_HIDDEN_C: u16 = 32768u16; // 0x8000 -def VERSYM_VERSION_C: u16 = 32767u16; // 0x7fff - -// ELF64 ehdr field offsets (subset) -def EH_SHOFF: u64 = 40u64; -def EH_ETYPE: u64 = 16u64; -def EH_EMACHINE: u64 = 18u64; -def EH_SHENTSIZE: u64 = 58u64; -def EH_SHNUM: u64 = 60u64; -def EH_SHSTRNDX: u64 = 62u64; - -// ELF64 Shdr (64 bytes) -def SH_SIZE: u64 = 64u64; -def SH_TYPE: u64 = 4u64; -def SH_OFFSET: u64 = 24u64; -def SH_SIZE_F: u64 = 32u64; -def SH_LINK: u64 = 40u64; -def SH_ENTSIZE: u64 = 56u64; - -// ELF64 Sym (24 bytes) -def SY_SIZE: u64 = 24u64; -def SY_NAME: u64 = 0u64; -def SY_INFO: u64 = 4u64; -def SY_SHNDX: u64 = 6u64; - -// ELF64 Dyn (16 bytes) -def DY_SIZE: u64 = 16u64; -def DY_TAG: u64 = 0u64; -def DY_VAL: u64 = 8u64; - -// Verdef (20 bytes) -def VD_SIZE: u64 = 20u64; -def VD_NDX: u64 = 4u64; -def VD_CNT: u64 = 6u64; -def VD_AUX: u64 = 12u64; -def VD_NEXT: u64 = 16u64; - -// Verdaux (8 bytes) -def VA_NAME: u64 = 0u64; -def VA_NEXT: u64 = 4u64; - -// ---- little-endian byte readers --------------------------------------- - -fn du16(p: *u8, off: u64) u16 = { - let b0: u16 = p[off]: u16; - let b1: u16 = p[off + 1u64]: u16; - return b0 | (b1 << 8u16); -}; - -fn du32(p: *u8, off: u64) u32 = { - let b0: u32 = p[off]: u32; - let b1: u32 = p[off + 1u64]: u32; - let b2: u32 = p[off + 2u64]: u32; - let b3: u32 = p[off + 3u64]: u32; - return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32); -}; - -fn du64(p: *u8, off: u64) u64 = { - let lo: u64 = du32(p, off): u64; - let hi: u64 = du32(p, off + 4u64): u64; - return lo | (hi << 32u64); -}; - -fn di64(p: *u8, off: u64) i64 = { - return du64(p, off): i64; -}; - -// ---- C-string helpers -------------------------------------------------- - -fn dcstrlen(p: *u8) u64 = { - let n: u64 = 0u64; - for (p[n] != 0u8) { n += 1u64; }; - return n; -}; - -fn dcstrtostr(p: *u8) str = { - let n: u64 = dcstrlen(p); - let view: str; - view.ptr = p; - view.len = n: i32; - return strings.dup(view); -}; - -// basename: scan for last '/' and return pointer past it. -fn dbasename(p: *u8) *u8 = { - let n: u64 = dcstrlen(p); - let i: u64 = n; - for (i > 0u64) { - i -= 1u64; - if (p[i] == '/') { - return p + i + 1u64; - }; - }; - return p; -}; - -// ---- file slurp -------------------------------------------------------- - -fn slurpso(path: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil, 0u64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - let n: i64 = 0i64; - match (szr) { - case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return nil, 0u64; }; - }; - let buf: []u8 = alloc([], n: u64)!; - buf.len = n: i32; - let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, n: u64); - os.close(fd); - let got: i64 = 0i64; - match (rr) { - case let v: i64 => got = v; - case let e: os.oserror => return nil, 0u64; - }; - if (got != n) { return nil, 0u64; }; - return buf.ptr, n: u64; -}; - -// ---- verdef helpers ---------------------------------------------------- - -// vdnameat — walk verdef records and return the name (as *u8 into -// the .so's verstr buffer) for the entry whose vd_ndx == ndx. The name -// is the first Verdaux's vda_name (subsequent auxes are predecessor -// names). Returns nil if no entry matches. -fn vdnameat(buf: *u8, verdefoff: u64, verdefsize: u64, - verstr: *u8, ndx: u16) *u8 = { - let off: u64 = 0u64; - for (off < verdefsize) { - let vdp: u64 = verdefoff + off; - let vdndx: u16 = du16(buf, vdp + VD_NDX); - let vdaux: u32 = du32(buf, vdp + VD_AUX); - let vdnext: u32 = du32(buf, vdp + VD_NEXT); - if (vdndx == ndx) { - let auxp: u64 = vdp + (vdaux: u64); - let vdaname: u32 = du32(buf, auxp + VA_NAME); - return verstr + (vdaname: u64); - }; - if (vdnext == 0u32) { return nil; }; - off += vdnext: u64; - }; - return nil; -}; - -// ---- entry points ------------------------------------------------------ - -export fn loadso(l: *lnk, path: *u8) i32 = { - let buf: *u8; - let blen: u64; - buf, blen = slurpso(path); - if (buf == nil) { - os.write(2, "w6l: cannot read .so\n".ptr, 20u64); - return -1; - }; - if (blen < 64u64) { - os.write(2, "w6l: short ELF\n".ptr, 14u64); - return -1; - }; - if (buf[0u64] != 127u8) { return soerr("not ELF"); }; - if (buf[1u64] != 'E') { return soerr("not ELF"); }; - if (buf[2u64] != 'L') { return soerr("not ELF"); }; - if (buf[3u64] != 'F') { return soerr("not ELF"); }; - if (buf[4u64] != 2u8) { return soerr("not ELFCLASS64"); }; - if (du16(buf, EH_EMACHINE) != EM_X86_64_SO) { - return soerr("not amd64"); - }; - if (du16(buf, EH_ETYPE) != ET_DYN_SO) { - return soerr("not ET_DYN"); - }; - - let shoff: u64 = du64(buf, EH_SHOFF); - let shnum: u32 = du16(buf, EH_SHNUM): u32; - if (shoff == 0u64) { return soerr("stripped .so unsupported"); }; - if (shnum == 0u32) { return soerr("stripped .so unsupported"); }; - - // Locate the four sections we care about. - let idxdynsym: i32 = -1; - let idxdynamic: i32 = -1; - let idxversym: i32 = -1; - let idxverdef: i32 = -1; - let i: u32 = 0u32; - for (i < shnum) { - let shp: u64 = shoff + (i: u64) * SH_SIZE; - let shtype: u32 = du32(buf, shp + SH_TYPE); - if (shtype == SHT_DYNSYM) { idxdynsym = i: i32; }; - if (shtype == SHT_DYNAMIC) { idxdynamic = i: i32; }; - if (shtype == SHT_GNU_VERSYM) { idxversym = i: i32; }; - if (shtype == SHT_GNU_VERDEF) { idxverdef = i: i32; }; - i += 1u32; - }; - if (idxdynsym < 0) { - return soerr("no .dynsym"); - }; - - let dynsymsh: u64 = shoff + (idxdynsym: u64) * SH_SIZE; - let dynsymoff: u64 = du64(buf, dynsymsh + SH_OFFSET); - let dynsymsize: u64 = du64(buf, dynsymsh + SH_SIZE_F); - let dynsymlink: u32 = du32(buf, dynsymsh + SH_LINK); - let nsyms: u64 = dynsymsize / SY_SIZE; - - let dynstrsh: u64 = shoff + (dynsymlink: u64) * SH_SIZE; - let dynstroff: u64 = du64(buf, dynstrsh + SH_OFFSET); - let dynstr: *u8 = buf + dynstroff; - - // SONAME: .dynamic strings live in the section pointed at by its - // sh_link (almost always .dynstr). - let sonamecs: *u8 = nil; - if (idxdynamic >= 0) { - let dynsh: u64 = shoff + (idxdynamic: u64) * SH_SIZE; - let dynoff: u64 = du64(buf, dynsh + SH_OFFSET); - let dynsize: u64 = du64(buf, dynsh + SH_SIZE_F); - let dynlink: u32 = du32(buf, dynsh + SH_LINK); - let dstrsh: u64 = shoff + (dynlink: u64) * SH_SIZE; - let dstroff: u64 = du64(buf, dstrsh + SH_OFFSET); - let dstr: *u8 = buf + dstroff; - let nd: u64 = dynsize / DY_SIZE; - let di: u64 = 0u64; - for (di < nd) { - let dp: u64 = dynoff + di * DY_SIZE; - let dtag: i64 = di64(buf, dp + DY_TAG); - if (dtag == DT_NULL_TAG) { - di = nd; // break - } else { - if (dtag == DT_SONAME_TAG) { - let dval: u64 = du64(buf, dp + DY_VAL); - sonamecs = dstr + dval; - di = nd; // break - } else { - di += 1u64; - }; - }; - }; - }; - if (sonamecs == nil) { - sonamecs = dbasename(path); - }; - - // Versym is one u16 per dynsym entry. - let versymoff: u64 = 0u64; - let hasversym: i32 = 0; - if (idxversym >= 0) { - let vssh: u64 = shoff + (idxversym: u64) * SH_SIZE; - versymoff = du64(buf, vssh + SH_OFFSET); - hasversym = 1; - }; - - // Verdef section bounds + the .dynstr-like string section it uses. - let verdefoff: u64 = 0u64; - let verdefsize: u64 = 0u64; - let verstr: *u8 = nil; - if (idxverdef >= 0) { - let vdsh: u64 = shoff + (idxverdef: u64) * SH_SIZE; - verdefoff = du64(buf, vdsh + SH_OFFSET); - verdefsize = du64(buf, vdsh + SH_SIZE_F); - let vdlink: u32 = du32(buf, vdsh + SH_LINK); - let vstrsh: u64 = shoff + (vdlink: u64) * SH_SIZE; - let vstroff: u64 = du64(buf, vstrsh + SH_OFFSET); - verstr = buf + vstroff; - }; - - // Build the lso. Exports are appended in dynsym order so - // soprovides_v's first-match semantics match the C version. - let so: *lso = alloc(lso { path = dcstrtostr(path), soname = dcstrtostr(sonamecs) })!; - let tail: *lexport = nil; - - let si: u64 = 1u64; - for (si < nsyms) { - let sp: u64 = dynsymoff + si * SY_SIZE; - let stshndx: u16 = du16(buf, sp + SY_SHNDX); - if (stshndx == 0u16) { si += 1u64; } else { - let stinfo: u8 = buf[sp + SY_INFO]; - let bind: u32 = (stinfo: u32) >> 4u32; - if (bind != 1u32) { if (bind != 2u32) { - // not GLOBAL/WEAK - si += 1u64; - continue; - }; }; - let stname: u32 = du32(buf, sp + SY_NAME); - let nmp: *u8 = dynstr + (stname: u64); - if (nmp[0u64] == 0u8) { - si += 1u64; - continue; - }; - - // Determine version. Skip non-default (hidden) and - // local entries. - let vernamecs: *u8 = nil; - let keep: i32 = 1; - if (hasversym != 0) { - let v: u16 = du16(buf, versymoff + si * 2u64); - if ((v & VERSYM_HIDDEN_C) != 0u16) { - keep = 0; // non-default - } else { - let vidx: u16 = v & VERSYM_VERSION_C; - switch (vidx) { - case VER_NDX_LOCAL_C: - keep = 0; // not exported - case VER_NDX_GLOBAL_C,1u16: - // glibc's BASE entry (vidx==1): treat as - // unversioned. (The C version notes that - // vidx==1 in Verdef maps to the SONAME BASE.) - vernamecs = nil; - case: - if (verstr != nil) { - let nm: *u8 = vdnameat(buf, verdefoff, verdefsize, verstr, vidx); - vernamecs = nm; - }; - }; - }; - }; - - if (keep != 0) { - let e: *lexport = alloc(lexport { name = dcstrtostr(nmp) })!; - if (vernamecs != nil) { - e.version = dcstrtostr(vernamecs); - }; - if (tail == nil) { - so.exports = e; - } else { - tail.enext = e; - }; - tail = e; - }; - si += 1u64; - }; - }; - - so.sonext = l.sos; - l.sos = so; - return 0; -}; - -fn soerr(msg: str) i32 = { - os.write(2, "w6l: ".ptr, 4u64); - os.write(2, msg.ptr, msg.len: u64); - os.write(2, "\n".ptr, 1u64); - return -1; -}; - -// soprovides — 1 if so exports name, 0 otherwise. -export fn soprovides(so: *lso, name: str) i32 = { - if (so == nil) { return 0; }; - let e: *lexport = so.exports; - for (e != nil) { - if (strings.compare(e.name, name) == 0) { return 1; }; - e = e.enext; - }; - return 0; -}; - -// soversion — the version of so's export named `name`, or an empty -// str (ptr=nil, len=0) if the export is unversioned or not present. -export fn soversion(so: *lso, name: str) str = { - let result: str; - result.ptr = nil; - result.len = 0i32; - if (so == nil) { return result; }; - let e: *lexport = so.exports; - for (e != nil) { - if (strings.compare(e.name, name) == 0) { - result.ptr = e.version.ptr; - result.len = e.version.len; - return result; - }; - e = e.enext; - }; - return result; -}; - -//ww:module-reset -// selfhost/cmd/w6l/pass.ww — port of cmd/w6l/pass.c. -// -// Resolution + relocation. l_resolve flags every undefined symbol -// referenced by a relocation, and promotes those provided by some -// loaded .so to "dynamic" with a freshly-assigned PLT slot. -// l_relocate walks the rel list and patches the .text bytes in place -// once the final virtual base is known. Dynamic refs are deferred: -// their site is patched later in dynout, once the PLT vaddr is known. -// -// Supported relocation kinds: PC32 (=2), PLT32 (=4); both are 32-bit -// PC-relative displacements (PLT32 == PC32 for static). - -package w6l; - -import os; -import sym; -import dyn; - -def R_X86_64_64: i32 = 1; -def R_X86_64_PC32: i32 = 2; -def R_X86_64_PLT32: i32 = 4; - -export fn resolve(l: *lnk) i32 = { - // Initialise dynamic-linking sentinels. alloc(T{})! zeroes, so - // isdyn/dynlib start clean — but pltidx and dynsymidx - // must be -1, not 0. - let si: *lsym = l.syms; - for (si != nil) { - si.pltidx = -1; - si.dynsymidx = -1; - si = si.snext; - }; - - // Promote each undefined sym that some lso exports to dynamic - // and hand it a PLT slot. Iteration order over the relocation - // list determines slot numbering and is stable across runs. - let r: *lrel = l.rels; - for (r != nil) { - if (r.sym != nil) { - if (r.sym.defined == 0) { - if (r.sym.isdyn == 0) { - let so: *lso = l.sos; - for (so != nil) { - if (soprovides(so, r.sym.name) != 0) { - r.sym.isdyn = 1; - r.sym.dynlib = so; - r.sym.pltidx = l.dynn; - l.dynn += 1; - so = nil; // break - } else { - so = so.sonext; - }; - }; - }; - }; - }; - r = r.rnext; - }; - - // What remains undefined truly is undefined. - let r2: *lrel = l.rels; - for (r2 != nil) { - if (r2.sym != nil) { - if (r2.sym.defined == 0) { - if (r2.sym.isdyn == 0) { - os.write(2, "w6l: undefined reference to '".ptr, 28u64); - let nm: str = r2.sym.name; - os.write(2, nm.ptr, nm.len: u64); - os.write(2, "'\n".ptr, 2u64); - l.errs += 1; - }; - }; - }; - r2 = r2.rnext; - }; - return l.errs; -}; - -fn patchu32(p: *u8, v: u32) void = { - p[0] = (v & 255u32): u8; - p[1] = ((v >> 8u32) & 255u32): u8; - p[2] = ((v >> 16u32) & 255u32): u8; - p[3] = ((v >> 24u32) & 255u32): u8; -}; - -fn patchu64(p: *u8, v: u64) void = { - let i: i32 = 0; - for (i < 8) { - p[i] = ((v >> (i: u64 * 8u64)) & 255u64): u8; - i += 1; - }; -}; - -export fn relocate(l: *lnk, textva: u64, datava: u64) i32 = { - let r: *lrel = l.rels; - for (r != nil) { - if (r.sym != nil) { - // Dynamic refs are patched later in dynout once the - // PLT vaddr is known. - if (r.sym.isdyn != 0) { - r = r.rnext; - continue; - }; - if (r.sym.defined != 0) { - let symva: u64 = textva + r.sym.val; - if (r.sym.indata != 0) { symva = datava + r.sym.val; }; - let k: i32 = r.kind; - switch (k) { - case R_X86_64_PC32,R_X86_64_PLT32: - let site: u64 = textva + r.off; - let rel: i64 = (symva: i64 - site: i64) + r.addend; - patchu32(l.text + r.off, rel: u32); - case R_X86_64_64: - // Absolute 64-bit. Currently used only - // for DATAR slots in .data. - let v: u64 = (symva: i64 + r.addend): u64; - if (r.section == 1) { - patchu64(l.data + r.off, v); - } else { - patchu64(l.text + r.off, v); - }; - case: - os.write(2, "w6l: unsupported reloc kind\n".ptr, 27u64); - l.errs += 1; - }; - }; - }; - r = r.rnext; - }; - return l.errs; -}; - -//ww:module-reset -// selfhost/cmd/w6l/dynout.ww — port of cmd/w6l/dynout.c. -// -// Emit a dynamic-linked ELF executable. The shape is the simplest -// valid one: PT_INTERP + PT_DYNAMIC + DT_BIND_NOW so the loader -// resolves every PLT slot at startup (no lazy binding, no PLT0 -// trampoline). SysV .hash, not .gnu.hash. Non-PIE, fixed base. -// -// Layout: -// [0] Ehdr -// [64] Phdrs (PT_LOAD R+X, PT_LOAD R+W, PT_INTERP, PT_DYNAMIC) -// [interp_off] "/lib64/ld-linux-x86-64.so.2\0" -// [dynstr_off] .dynstr -// [dynsym_off] .dynsym -// [hash_off] .hash -// [versym_off] .gnu.version -// [verneed_off] .gnu.version_r -// [relaplt_off] .rela.plt -// [pad to 0x1000] -// [text_off] .text -// [plt_off] .plt -// [pad to next page] -// [gotplt_off] .got.plt (writable; mapped by PT_LOAD #2) -// [dynamic_off] .dynamic (writable; covered by PT_DYNAMIC) - -package w6l; - -import os; -import rt; -import strings; -import sym; - -// ELF constants -def ET_EXEC_D: u16 = 2u16; -def EM_X86_64_D: u16 = 62u16; -def EV_CURRENT_D: u32 = 1u32; -def ELFCLASS64_D: u8 = 2u8; -def ELFDATA2LSB_D: u8 = 1u8; - -def PT_LOAD_D: u32 = 1u32; -def PT_DYNAMIC_D: u32 = 2u32; -def PT_INTERP_D: u32 = 3u32; -def PF_X_D: u32 = 1u32; -def PF_W_D: u32 = 2u32; -def PF_R_D: u32 = 4u32; - -def DT_NULL: i64 = 0i64; -def DT_NEEDED: i64 = 1i64; -def DT_PLTRELSZ: i64 = 2i64; -def DT_PLTGOT: i64 = 3i64; -def DT_HASH: i64 = 4i64; -def DT_STRTAB: i64 = 5i64; -def DT_SYMTAB: i64 = 6i64; -def DT_STRSZ: i64 = 10i64; -def DT_SYMENT: i64 = 11i64; -def DT_PLTREL: i64 = 20i64; -def DT_RELA: i64 = 7i64; -def DT_JMPREL: i64 = 23i64; -def DT_BIND_NOW: i64 = 24i64; -def DT_VERSYM: i64 = 1879048176i64; // 0x6ffffff0 -def DT_VERNEED: i64 = 1879048190i64; // 0x6ffffffe -def DT_VERNEEDNUM: i64 = 1879048191i64; // 0x6fffffff - -def VER_NDX_LOCAL_D: u16 = 0u16; -def VER_NDX_GLOBAL_D: u16 = 1u16; - -def R_X86_64_PC32_D: i32 = 2; -def R_X86_64_PLT32_D: i32 = 4; -def R_X86_64_JUMP_SLOT_D: u32 = 7u32; - -def STB_GLOBAL_D: u8 = 1u8; -def STT_FUNC_D: u8 = 2u8; - -def PLT_STUB_BYTES_D: u64 = 8u64; -def PAGE: u64 = 4096u64; - -def INTERP: str = "/lib64/ld-linux-x86-64.so.2"; - -// ---- byte writers ------------------------------------------------------ - -fn dwr8(buf: *u8, off: u64, v: u8) void = { - buf[off] = v; -}; - -fn dwr16(buf: *u8, off: u64, v: u16) void = { - buf[off] = (v & 255u16): u8; - buf[off + 1u64] = ((v >> 8u16) & 255u16): u8; -}; - -fn dwr32(buf: *u8, off: u64, v: u32) void = { - buf[off] = (v & 255u32): u8; - buf[off + 1u64] = ((v >> 8u32) & 255u32): u8; - buf[off + 2u64] = ((v >> 16u32) & 255u32): u8; - buf[off + 3u64] = ((v >> 24u32) & 255u32): u8; -}; - -fn dwr64(buf: *u8, off: u64, v: u64) void = { - dwr32(buf, off, (v & 4294967295u64): u32); - dwr32(buf, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); -}; - -fn dwri64(buf: *u8, off: u64, v: i64) void = { - dwr64(buf, off, v: u64); -}; - -fn dwri32(buf: *u8, off: u64, v: i32) void = { - dwr32(buf, off, v: u32); -}; - -// ---- byte readers ------------------------------------------------------ - -fn drdu16(p: *u8, off: u64) u16 = { - let b0: u16 = p[off]: u16; - let b1: u16 = p[off + 1u64]: u16; - return b0 | (b1 << 8u16); -}; - -fn drdu32(p: *u8, off: u64) u32 = { - let b0: u32 = p[off]: u32; - let b1: u32 = p[off + 1u64]: u32; - let b2: u32 = p[off + 2u64]: u32; - let b3: u32 = p[off + 3u64]: u32; - return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32); -}; - -fn drdi32(p: *u8, off: u64) i32 = { - return drdu32(p, off): i32; -}; - -fn dbcopy(dst: *u8, off: u64, src: *u8, n: u64) void = { - let i: u64 = 0u64; - for (i < n) { - dst[off + i] = src[i]; - i += 1u64; - }; -}; - -// elfhash — SysV ELF hash. Used for .gnu.version_r's vna_hash. -fn elfhash(name: str) u32 = { - let h: u32 = 0u32; - let i: i32 = 0; - for (i < name.len) { - let c: u32 = (name[i]: u8): u32; - h = (h << 4u32) + c; - let g: u32 = h & 4026531840u32; // 0xf0000000 - if (g != 0u32) { h = h ^ (g >> 24u32); }; - h = h & ~g; - i += 1; - }; - return h; -}; - -fn alignup(off: u64, a: u64) u64 = { - return (off + a - 1u64) & ~(a - 1u64); -}; - -// ---- main entry -------------------------------------------------------- - -export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { - // .data shares the R+W PT_LOAD with .got.plt and .dynamic. - // Placed after .dynamic so the segment is one contiguous run; - // relocate runs from here so the dyn layout's datava lands in - // patched offsets. - let n: i32 = l.dynn; - let nu: u64 = n: u64; - - // ---- collect dyn syms into a plt_idx-indexed array ---- - let dynsyms: []*lsym = alloc([], nu)!; - let s: *lsym = l.syms; - for (s != nil) { - if (s.isdyn != 0) { - if (s.pltidx >= 0) { - if (s.pltidx < n) { - dynsyms[s.pltidx] = s; - }; - }; - }; - s = s.snext; - }; - let i: i32 = 0; - for (i < n) { - if (dynsyms[i] == nil) { - os.write(2, "w6l: dynout: no sym for plt_idx\n".ptr, 31u64); - return 1; - }; - i += 1; - }; - - // ---- collect used .so's (in l.sos order) ---- - let maxsos: i32 = 0; - let so: *lso = l.sos; - for (so != nil) { maxsos += 1; so = so.sonext; }; - let sosused: []*lso = alloc([], maxsos: u64)!; - let nsos: i32 = 0; - so = l.sos; - for (so != nil) { - let used: i32 = 0; - let j: i32 = 0; - for (j < n) { - let dsm: *lsym = dynsyms[j]; - let dl: *lso = dsm.dynlib; - if (dl == so) { used = 1; j = n; } - else { j += 1; }; - }; - if (used != 0) { - sosused[nsos] = so; - nsos += 1; - }; - so = so.sonext; - }; - - // ---- build flat version table grouped by vlib ---- - // vlib_sos_idx[k] = sos_used index for vlib k. - // vlib_first[k] = ver index of first version under vlib k. - // vlib_count[k] = number of versions under vlib k. - // ver_lib_idx[v] = vlib index that version v belongs to. - // ver_name_ptr_arr[v] = name's *u8 (interned in the .so's verdef strings). - // ver_name_len_buf[v] = name length (i32). - // ver_dynstr_off[v] = offset within .dynstr (assigned after layout). - // ver_vna_other[v] = versym index (starting at 2). - - let vlibsosidxbuf: []u8 = alloc([], (maxsos: u64) * 4u64)!; - let vlibfirstbuf: []u8 = alloc([], (maxsos: u64) * 4u64)!; - let vlibcountbuf: []u8 = alloc([], (maxsos: u64) * 4u64)!; - let nvlibs: i32 = 0; - - let verlibidxbuf: []u8 = alloc([], nu * 4u64)!; - let vernameptrarr: []*u8 = alloc([], nu)!; - let vernamelenbuf: []u8 = alloc([], nu * 4u64)!; - let verdynstroff: []u8 = alloc([], nu * 4u64)!; - let vervnaother: []u8 = alloc([], nu * 2u64)!; - let nvers: i32 = 0; - - let si: i32 = 0; - for (si < nsos) { - let curso: *lso = sosused[si]; - let has: i32 = 0; - let j: i32 = 0; - for (j < n) { - let dsm: *lsym = dynsyms[j]; - let dl: *lso = dsm.dynlib; - if (dl == curso) { - let nm0: str = dsm.name; - let dv: str = soversion(curso, nm0); - if (dv.len > 0) { - has = 1; j = n; - } else { j += 1; }; - } else { j += 1; }; - }; - if (has != 0) { - dwr32(vlibsosidxbuf.ptr, (nvlibs: u64) * 4u64, si: u32); - dwr32(vlibfirstbuf.ptr, (nvlibs: u64) * 4u64, nvers: u32); - let added: i32 = 0; - let jj: i32 = 0; - for (jj < n) { - let dsm2: *lsym = dynsyms[jj]; - let dl2: *lso = dsm2.dynlib; - if (dl2 == curso) { - let nm2: str = dsm2.name; - let vname: str = soversion(curso, nm2); - if (vname.len > 0) { - let seen: i32 = 0; - let k: i32 = 0; - for (k < added) { - let kk: i32 = nvers - added + k; - let existing: str; - existing.ptr = vernameptrarr[kk]; - existing.len = drdi32(vernamelenbuf.ptr, (kk: u64) * 4u64); - if (strings.compare(existing, vname) == 0) { - seen = 1; k = added; - } else { k += 1; }; - }; - if (seen == 0) { - dwr32(verlibidxbuf.ptr, (nvers: u64) * 4u64, nvlibs: u32); - vernameptrarr[nvers] = vname.ptr; - dwri32(vernamelenbuf.ptr, (nvers: u64) * 4u64, vname.len); - nvers += 1; - added += 1; - }; - }; - }; - jj += 1; - }; - dwr32(vlibcountbuf.ptr, (nvlibs: u64) * 4u64, added: u32); - nvlibs += 1; - }; - si += 1; - }; - - // Assign vna_other indices starting at 2, walking vlib then per-version. - let nextvna: u16 = 2u16; - let vi: i32 = 0; - for (vi < nvlibs) { - let first: i32 = drdi32(vlibfirstbuf.ptr, (vi: u64) * 4u64); - let cnt: i32 = drdi32(vlibcountbuf.ptr, (vi: u64) * 4u64); - let k: i32 = 0; - for (k < cnt) { - dwr16(vervnaother.ptr, ((first + k): u64) * 2u64, nextvna); - nextvna += 1u16; - k += 1; - }; - vi += 1; - }; - - // ---- compute dynstr size ---- - let dynstrsz: u64 = 1u64; // leading NUL - let pi: i32 = 0; - for (pi < nsos) { - let so4: *lso = sosused[pi]; - dynstrsz += so4.soname.len: u64; - dynstrsz += 1u64; - pi += 1; - }; - pi = 0; - for (pi < n) { - let dsm4: *lsym = dynsyms[pi]; - dynstrsz += dsm4.name.len: u64; - dynstrsz += 1u64; - pi += 1; - }; - pi = 0; - for (pi < nvers) { - let nmlen: i32 = drdi32(vernamelenbuf.ptr, (pi: u64) * 4u64); - dynstrsz += nmlen: u64; - dynstrsz += 1u64; - pi += 1; - }; - - // ---- fill dynstr ---- - let dynstr: []u8 = alloc([], dynstrsz)!; - let dynstrpos: u64 = 1u64; // past leading NUL - - let sonamestr: []u8 = alloc([], (nsos: u64) * 4u64)!; - pi = 0; - for (pi < nsos) { - dwr32(sonamestr.ptr, (pi: u64) * 4u64, dynstrpos: u32); - let so2: *lso = sosused[pi]; - let snm: str = so2.soname; - dbcopy(dynstr.ptr, dynstrpos, snm.ptr, snm.len: u64); - dynstrpos += snm.len: u64; - dynstr[dynstrpos] = 0u8; - dynstrpos += 1u64; - pi += 1; - }; - let symnamestr: []u8 = alloc([], nu * 4u64)!; - pi = 0; - for (pi < n) { - dwr32(symnamestr.ptr, (pi: u64) * 4u64, dynstrpos: u32); - let dsm: *lsym = dynsyms[pi]; - let snm: str = dsm.name; - dbcopy(dynstr.ptr, dynstrpos, snm.ptr, snm.len: u64); - dynstrpos += snm.len: u64; - dynstr[dynstrpos] = 0u8; - dynstrpos += 1u64; - pi += 1; - }; - pi = 0; - for (pi < nvers) { - dwr32(verdynstroff.ptr, (pi: u64) * 4u64, dynstrpos: u32); - let nmp: *u8 = vernameptrarr[pi]; - let nmlen: i32 = drdi32(vernamelenbuf.ptr, (pi: u64) * 4u64); - dbcopy(dynstr.ptr, dynstrpos, nmp, nmlen: u64); - dynstrpos += nmlen: u64; - dynstr[dynstrpos] = 0u8; - dynstrpos += 1u64; - pi += 1; - }; - - // ---- per-dyn-sym versym index ---- - let versymfor: []u8 = alloc([], nu * 2u64)!; - pi = 0; - for (pi < n) { - let dsm3: *lsym = dynsyms[pi]; - let dl3: *lso = dsm3.dynlib; - let nm3: str = dsm3.name; - let vname: str = soversion(dl3, nm3); - if (vname.len == 0) { - dwr16(versymfor.ptr, (pi: u64) * 2u64, VER_NDX_GLOBAL_D); - } else { - let matched: i32 = 0; - let vk: i32 = 0; - for (vk < nvers) { - let vlx: i32 = drdi32(verlibidxbuf.ptr, (vk: u64) * 4u64); - let sosx: i32 = drdi32(vlibsosidxbuf.ptr, (vlx: u64) * 4u64); - if (sosused[sosx] == dl3) { - let exi: str; - exi.ptr = vernameptrarr[vk]; - exi.len = drdi32(vernamelenbuf.ptr, (vk: u64) * 4u64); - if (strings.compare(exi, vname) == 0) { - let other: u16 = drdu16(vervnaother.ptr, (vk: u64) * 2u64); - dwr16(versymfor.ptr, (pi: u64) * 2u64, other); - matched = 1; - vk = nvers; - } else { vk += 1; }; - } else { vk += 1; }; - }; - if (matched == 0) { - dwr16(versymfor.ptr, (pi: u64) * 2u64, VER_NDX_GLOBAL_D); - }; - }; - pi += 1; - }; - - // ---- compute byte sizes ---- - let ehdrsz: u64 = 64u64; - let nphdrs: u64 = 4u64; - let phdrsz: u64 = nphdrs * 56u64; - let interpsz: u64 = (INTERP.len: u64) + 1u64; - - let nsymstotal: u64 = 1u64 + nu; - let dynsymsz: u64 = nsymstotal * 24u64; - - let nbuckets: u32 = 1u32; - let nchain: u32 = nsymstotal: u32; - let hashsz: u64 = (2u64 + (nbuckets: u64) + (nchain: u64)) * 4u64; - - let relapltsz: u64 = nu * 24u64; - let pltsz: u64 = nu * PLT_STUB_BYTES_D; - let gotpltsz: u64 = (3u64 + nu) * 8u64; - let versymsz: u64 = nsymstotal * 2u64; - - let verneedsz: u64 = 0u64; - let vli: i32 = 0; - for (vli < nvlibs) { - let cnt: i32 = drdi32(vlibcountbuf.ptr, (vli: u64) * 4u64); - verneedsz += 16u64 + 16u64 * (cnt: u64); - vli += 1; - }; - - let withver: i32 = 0; - if (nvlibs > 0) { withver = 1; }; - let extra: u64 = 0u64; - if (withver != 0) { extra = 3u64; }; - let ndyn: u64 = (nsos: u64) + 11u64 + extra; - let dynamicsz: u64 = ndyn * 16u64; - - // ---- compute file offsets ---- - let off: u64 = ehdrsz + phdrsz; - let interpoff: u64 = off; off += interpsz; - off = alignup(off, 8u64); - let dynstroff: u64 = off; off += dynstrsz; - off = alignup(off, 8u64); - let dynsymoff: u64 = off; off += dynsymsz; - let hashoff: u64 = off; off += hashsz; - off = alignup(off, 2u64); - let versymoff: u64 = off; off += versymsz; - off = alignup(off, 4u64); - let verneedoff: u64 = off; off += verneedsz; - off = alignup(off, 8u64); - let relapltoff: u64 = off; off += relapltsz; - - let textoff: u64 = alignup(off, PAGE); - let pltoff: u64 = textoff + l.textlen; - let rxend: u64 = pltoff + pltsz; - - let gotpltoff: u64 = alignup(rxend, PAGE); - let dynamicoff: u64 = gotpltoff + gotpltsz; - let dataoff: u64 = dynamicoff + dynamicsz; - let fileend: u64 = dataoff + l.datalen; - - let interpva: u64 = base + interpoff; - let dynstrva: u64 = base + dynstroff; - let dynsymva: u64 = base + dynsymoff; - let hashva: u64 = base + hashoff; - let versymva: u64 = base + versymoff; - let verneedva: u64 = base + verneedoff; - let relapltva: u64 = base + relapltoff; - let textva: u64 = base + textoff; - let pltva: u64 = base + pltoff; - let gotpltva: u64 = base + gotpltoff; - let dynamicva: u64 = base + dynamicoff; - let datava: u64 = base + dataoff; - - // Apply relocations now that the dyn layout's textva/datava are - // pinned. main.ww defers this so each path uses its own VAs. - if (relocate(l, textva, datava) != 0) { return 1; }; - - // BSS optimisation — same trailing-zero scan as out.ww. - let bsslen: u64 = 0u64; - if (l.datalen > 0u64) { - for (bsslen < l.datalen) { - let b: u8 = l.data[l.datalen - 1u64 - bsslen]; - if (b != 0u8) { break; }; - bsslen += 1u64; - }; - }; - let datafilelen: u64 = l.datalen - bsslen; - let filedataend: u64 = dataoff + datafilelen; - - // ---- build .dynsym ---- - let dynsymbuf: []u8 = alloc([], dynsymsz)!; - pi = 0; - for (pi < n) { - let eoff: u64 = (1u64 + (pi: u64)) * 24u64; - dwr32(dynsymbuf.ptr, eoff + 0u64, drdu32(symnamestr.ptr, (pi: u64) * 4u64)); - dwr8(dynsymbuf.ptr, eoff + 4u64, (STB_GLOBAL_D << 4u8) | (STT_FUNC_D & 15u8)); - dwr8(dynsymbuf.ptr, eoff + 5u64, 0u8); - dwr16(dynsymbuf.ptr, eoff + 6u64, 0u16); - dwr64(dynsymbuf.ptr, eoff + 8u64, 0u64); - dwr64(dynsymbuf.ptr, eoff + 16u64, 0u64); - pi += 1; - }; - - // ---- build .hash (SysV, 1 bucket) ---- - let hashbuf: []u8 = alloc([], hashsz)!; - dwr32(hashbuf.ptr, 0u64, nbuckets); - dwr32(hashbuf.ptr, 4u64, nchain); - let bucket0: u32 = 0u32; - if (nsymstotal > 1u64) { bucket0 = 1u32; }; - dwr32(hashbuf.ptr, 8u64, bucket0); - let ci: u64 = 1u64; - for (ci < nsymstotal) { - let nxt: u32 = 0u32; - if (ci + 1u64 < nsymstotal) { nxt = (ci + 1u64): u32; }; - dwr32(hashbuf.ptr, 8u64 + (nbuckets: u64) * 4u64 + ci * 4u64, nxt); - ci += 1u64; - }; - - // ---- build .rela.plt ---- - let relapltbuf: []u8 = alloc([], relapltsz)!; - pi = 0; - for (pi < n) { - let roff: u64 = (pi: u64) * 24u64; - dwr64(relapltbuf.ptr, roff + 0u64, gotpltva + (3u64 + (pi: u64)) * 8u64); - let info: u64 = ((1u64 + (pi: u64)) << 32u64) | (R_X86_64_JUMP_SLOT_D: u64); - dwr64(relapltbuf.ptr, roff + 8u64, info); - dwri64(relapltbuf.ptr, roff + 16u64, 0i64); - pi += 1; - }; - - // ---- build .gnu.version (u16 per dynsym entry) ---- - let versymbuf: []u8 = alloc([], versymsz)!; - dwr16(versymbuf.ptr, 0u64, VER_NDX_LOCAL_D); - pi = 0; - for (pi < n) { - dwr16(versymbuf.ptr, 2u64 + (pi: u64) * 2u64, drdu16(versymfor.ptr, (pi: u64) * 2u64)); - pi += 1; - }; - - // ---- build .gnu.version_r ---- - let verneedbuf: []u8 = alloc([], verneedsz)!; - if (verneedsz > 0u64) { - let vnoff: u64 = 0u64; - vli = 0; - for (vli < nvlibs) { - let sosidx: i32 = drdi32(vlibsosidxbuf.ptr, (vli: u64) * 4u64); - let first: i32 = drdi32(vlibfirstbuf.ptr, (vli: u64) * 4u64); - let cnt: i32 = drdi32(vlibcountbuf.ptr, (vli: u64) * 4u64); - let vnstart: u64 = vnoff; - dwr16(verneedbuf.ptr, vnoff + 0u64, 1u16); - dwr16(verneedbuf.ptr, vnoff + 2u64, cnt: u16); - dwr32(verneedbuf.ptr, vnoff + 4u64, drdu32(sonamestr.ptr, (sosidx: u64) * 4u64)); - dwr32(verneedbuf.ptr, vnoff + 8u64, 16u32); - vnoff += 16u64; - let k: i32 = 0; - for (k < cnt) { - let vk: i32 = first + k; - let nm: str; - nm.ptr = vernameptrarr[vk]; - nm.len = drdi32(vernamelenbuf.ptr, (vk: u64) * 4u64); - let h: u32 = elfhash(nm); - dwr32(verneedbuf.ptr, vnoff + 0u64, h); - dwr16(verneedbuf.ptr, vnoff + 4u64, 0u16); - dwr16(verneedbuf.ptr, vnoff + 6u64, drdu16(vervnaother.ptr, (vk: u64) * 2u64)); - dwr32(verneedbuf.ptr, vnoff + 8u64, drdu32(verdynstroff.ptr, (vk: u64) * 4u64)); - let nxt: u32 = 0u32; - if (k + 1 < cnt) { nxt = 16u32; }; - dwr32(verneedbuf.ptr, vnoff + 12u64, nxt); - vnoff += 16u64; - k += 1; - }; - let vnnxt: u32 = 0u32; - if (vli + 1 < nvlibs) { vnnxt = (vnoff - vnstart): u32; }; - dwr32(verneedbuf.ptr, vnstart + 12u64, vnnxt); - vli += 1; - }; - }; - - // ---- build .plt ---- - let pltbuf: []u8 = alloc([], pltsz)!; - pi = 0; - for (pi < n) { - let poff: u64 = (pi: u64) * PLT_STUB_BYTES_D; - let stubva: u64 = pltva + poff; - let nextip: u64 = stubva + 6u64; - let slotva: u64 = gotpltva + (3u64 + (pi: u64)) * 8u64; - let disp: i64 = (slotva: i64) - (nextip: i64); - dwr8(pltbuf.ptr, poff + 0u64, 255u8); - dwr8(pltbuf.ptr, poff + 1u64, 37u8); - dwr32(pltbuf.ptr, poff + 2u64, (disp: i32): u32); - pi += 1; - }; - - // ---- build .got.plt ---- - let gotpltbuf: []u8 = alloc([], gotpltsz)!; - dwr64(gotpltbuf.ptr, 0u64, dynamicva); - - // ---- build .dynamic ---- - let dynamicbuf: []u8 = alloc([], dynamicsz)!; - let dk: u64 = 0u64; - pi = 0; - for (pi < nsos) { - dwri64(dynamicbuf.ptr, dk * 16u64 + 0u64, DT_NEEDED); - dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, drdu32(sonamestr.ptr, (pi: u64) * 4u64): u64); - dk += 1u64; - pi += 1; - }; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_HASH); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, hashva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_STRTAB); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, dynstrva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_SYMTAB); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, dynsymva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_STRSZ); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, dynstrsz); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_SYMENT); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, 24u64); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_PLTGOT); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, gotpltva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_PLTRELSZ); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, relapltsz); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_PLTREL); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, DT_RELA: u64);dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_JMPREL); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, relapltva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_BIND_NOW); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, 0u64); dk += 1u64; - if (withver != 0) { - dwri64(dynamicbuf.ptr, dk * 16u64, DT_VERSYM); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, versymva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_VERNEED); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, verneedva); dk += 1u64; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_VERNEEDNUM); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, nvlibs: u64);dk += 1u64; - }; - dwri64(dynamicbuf.ptr, dk * 16u64, DT_NULL); dwr64(dynamicbuf.ptr, dk * 16u64 + 8u64, 0u64); dk += 1u64; - if (dk != ndyn) { - os.write(2, "w6l: dynamic entry count mismatch\n".ptr, 33u64); - return 1; - }; - - // ---- patch .text relocs targeting dynamic syms ---- - let r: *lrel = l.rels; - for (r != nil) { - if (r.sym != nil) { - let rsym: *lsym = r.sym; - if (rsym.isdyn != 0) { - if (r.kind != R_X86_64_PC32_D) { - if (r.kind != R_X86_64_PLT32_D) { - os.write(2, "w6l: dynamic reloc kind unsupported\n".ptr, 35u64); - return 1; - }; - }; - let site: u64 = textva + r.off; - let stub: u64 = pltva + (rsym.pltidx: u64) * PLT_STUB_BYTES_D; - let disp: i64 = (stub: i64) - (site: i64) + r.addend; - dwr32(l.text, r.off, (disp: i32): u32); - }; - }; - r = r.rnext; - }; - - // ---- assemble file buffer ---- - let filebuf: []u8 = alloc([], fileend)!; - filebuf.len = fileend: i32; - - // Ehdr - dwr8(filebuf.ptr, 0u64, 127u8); - dwr8(filebuf.ptr, 1u64, 69u8); - dwr8(filebuf.ptr, 2u64, 76u8); - dwr8(filebuf.ptr, 3u64, 70u8); - dwr8(filebuf.ptr, 4u64, ELFCLASS64_D); - dwr8(filebuf.ptr, 5u64, ELFDATA2LSB_D); - dwr8(filebuf.ptr, 6u64, EV_CURRENT_D: u8); - dwr16(filebuf.ptr, 16u64, ET_EXEC_D); - dwr16(filebuf.ptr, 18u64, EM_X86_64_D); - dwr32(filebuf.ptr, 20u64, EV_CURRENT_D); - // #63: main computed `entry` as BASE + CODE_VA_OFF (== PAGE) + val - // assuming .text sits at file offset 0x1000, but textoff above is - // recomputed and overflows 0x1000 once the dynamic headers exceed the - // first page (e.g. ~100 dyn syms). Rebase e_entry onto the actual textoff - // so it points at the real _start instead of into the headers. Both - // stages (#263). PAGE is dynout-local; equals main.ww's CODE_VA_OFF. - dwr64(filebuf.ptr, 24u64, entry - PAGE + textoff); - dwr64(filebuf.ptr, 32u64, ehdrsz); - dwr64(filebuf.ptr, 40u64, 0u64); - dwr32(filebuf.ptr, 48u64, 0u32); - dwr16(filebuf.ptr, 52u64, ehdrsz: u16); - dwr16(filebuf.ptr, 54u64, 56u16); - dwr16(filebuf.ptr, 56u64, nphdrs: u16); - dwr16(filebuf.ptr, 58u64, 0u16); - dwr16(filebuf.ptr, 60u64, 0u16); - dwr16(filebuf.ptr, 62u64, 0u16); - - // Phdrs at offset 64. - let p0: u64 = 64u64; - dwr32(filebuf.ptr, p0 + 0u64, PT_LOAD_D); - dwr32(filebuf.ptr, p0 + 4u64, PF_R_D | PF_X_D); - dwr64(filebuf.ptr, p0 + 8u64, 0u64); - dwr64(filebuf.ptr, p0 + 16u64, base); - dwr64(filebuf.ptr, p0 + 24u64, base); - dwr64(filebuf.ptr, p0 + 32u64, rxend); - dwr64(filebuf.ptr, p0 + 40u64, rxend); - dwr64(filebuf.ptr, p0 + 48u64, PAGE); - - let p1: u64 = 64u64 + 56u64; - dwr32(filebuf.ptr, p1 + 0u64, PT_LOAD_D); - dwr32(filebuf.ptr, p1 + 4u64, PF_R_D | PF_W_D); - dwr64(filebuf.ptr, p1 + 8u64, gotpltoff); - dwr64(filebuf.ptr, p1 + 16u64, gotpltva); - dwr64(filebuf.ptr, p1 + 24u64, gotpltva); - // filesz trims the .data trailing zeros (BSS); memsz covers - // .got.plt + .dynamic + the full .data so the loader zero-fills. - dwr64(filebuf.ptr, p1 + 32u64, filedataend - gotpltoff); - dwr64(filebuf.ptr, p1 + 40u64, fileend - gotpltoff); - dwr64(filebuf.ptr, p1 + 48u64, PAGE); - - let p2: u64 = 64u64 + 112u64; - dwr32(filebuf.ptr, p2 + 0u64, PT_INTERP_D); - dwr32(filebuf.ptr, p2 + 4u64, PF_R_D); - dwr64(filebuf.ptr, p2 + 8u64, interpoff); - dwr64(filebuf.ptr, p2 + 16u64, interpva); - dwr64(filebuf.ptr, p2 + 24u64, interpva); - dwr64(filebuf.ptr, p2 + 32u64, interpsz); - dwr64(filebuf.ptr, p2 + 40u64, interpsz); - dwr64(filebuf.ptr, p2 + 48u64, 1u64); - - let p3: u64 = 64u64 + 168u64; - dwr32(filebuf.ptr, p3 + 0u64, PT_DYNAMIC_D); - dwr32(filebuf.ptr, p3 + 4u64, PF_R_D | PF_W_D); - dwr64(filebuf.ptr, p3 + 8u64, dynamicoff); - dwr64(filebuf.ptr, p3 + 16u64, dynamicva); - dwr64(filebuf.ptr, p3 + 24u64, dynamicva); - dwr64(filebuf.ptr, p3 + 32u64, dynamicsz); - dwr64(filebuf.ptr, p3 + 40u64, dynamicsz); - dwr64(filebuf.ptr, p3 + 48u64, 8u64); - - // Sections. - dbcopy(filebuf.ptr, interpoff, INTERP.ptr, INTERP.len: u64); - dwr8(filebuf.ptr, interpoff + (INTERP.len: u64), 0u8); - dbcopy(filebuf.ptr, dynstroff, dynstr.ptr, dynstrsz); - dbcopy(filebuf.ptr, dynsymoff, dynsymbuf.ptr, dynsymsz); - dbcopy(filebuf.ptr, hashoff, hashbuf.ptr, hashsz); - dbcopy(filebuf.ptr, versymoff, versymbuf.ptr, versymsz); - if (verneedsz > 0u64) { - dbcopy(filebuf.ptr, verneedoff, verneedbuf.ptr, verneedsz); - }; - dbcopy(filebuf.ptr, relapltoff, relapltbuf.ptr, relapltsz); - if (l.textlen > 0u64) { - dbcopy(filebuf.ptr, textoff, l.text, l.textlen); - }; - dbcopy(filebuf.ptr, pltoff, pltbuf.ptr, pltsz); - dbcopy(filebuf.ptr, gotpltoff, gotpltbuf.ptr, gotpltsz); - dbcopy(filebuf.ptr, dynamicoff, dynamicbuf.ptr, dynamicsz); - if (datafilelen > 0u64) { - dbcopy(filebuf.ptr, dataoff, l.data, datafilelen); - }; - - let wr: (i64 | os.oserror) = os.writeall(fd, filebuf.ptr, filedataend); - match (wr) { - case let v: i64 => { if (v != filedataend: i64) { return 1; }; }; - case let e: os.oserror => return 1; - }; - return 0; -}; - -//ww:module-reset -// selfhost/cmd/w6l/out.ww — port of cmd/w6l/out.c. -// -// Emit a static ELF64 executable. File layout (per the C original): -// [0..64) Ehdr -// [64..120) Phdr (one PT_LOAD) -// [120..0x1000) zero pad -// [0x1000..) .text bytes -// Single PT_LOAD covers the whole file, R+X. No interpreter, no .bss. - -package w6l; - -import os; -import rt; -import sym; -import dynout; - -def ET_EXEC: u16 = 2u16; -def EM_X86_64_W: u16 = 62u16; -def EV_CURRENT: u32 = 1u32; -def ELFCLASS64: u8 = 2u8; -def ELFDATA2LSB: u8 = 1u8; -def PT_LOAD: u32 = 1u32; -def PF_X: u32 = 1u32; -def PF_W: u32 = 2u32; -def PF_R: u32 = 4u32; - -def TEXT_OFF: u64 = 4096u64; // 0x1000 -def PAGE_SZ: u64 = 4096u64; - -// ---- little-endian byte writers ---------------------------------------- - -fn wru16(buf: *u8, off: u64, v: u16) void = { - buf[off] = (v & 255u16): u8; - buf[off + 1u64] = ((v >> 8u16) & 255u16): u8; -}; - -fn wru32(buf: *u8, off: u64, v: u32) void = { - buf[off] = (v & 255u32): u8; - buf[off + 1u64] = ((v >> 8u32) & 255u32): u8; - buf[off + 2u64] = ((v >> 16u32) & 255u32): u8; - buf[off + 3u64] = ((v >> 24u32) & 255u32): u8; -}; - -fn wru64(buf: *u8, off: u64, v: u64) void = { - wru32(buf, off, (v & 4294967295u64): u32); - wru32(buf, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); -}; - -// ---- emit --------------------------------------------------------------- - -export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { - // Dispatch: any loaded shared object plus any dynamic ref means - // we owe the loader a real PT_INTERP/PT_DYNAMIC binary. - if (l.sos != nil) { - if (l.dynn > 0) { - return emitdynelf(l, fd, base, entry); - }; - }; - - let hasdata: bool = l.datalen > 0u64; - let rxend: u64 = TEXT_OFF + l.textlen; - // .data lands at the next page boundary so the loader can give - // it fresh R+W permissions without overlapping the R+X mapping. - let dataoff: u64 = 0u64; - let datava: u64 = 0u64; - if (hasdata) { - dataoff = (rxend + PAGE_SZ - 1u64) & ~(PAGE_SZ - 1u64); - datava = base + dataoff; - }; - - // Apply relocations now that the layout's textva/datava are - // known. Deferred from main.ww so the dyn path uses its own - // datava. - if (relocate(l, base + TEXT_OFF, datava) != 0) { return -1; }; - - // BSS optimisation: trailing zero bytes in .data can be left - // out of the file. The loader zero-fills the gap between - // p_filesz and p_memsz. Scan after l_relocate has applied any - // DATAR patches — anything still zero at the tail genuinely is - // zero-init. Matches cmd/w6l/out.c byte-for-byte. - let bsslen: u64 = 0u64; - if (hasdata) { - for (bsslen < l.datalen) { - let b: u8 = l.data[l.datalen - 1u64 - bsslen]; - if (b != 0u8) { break; }; - bsslen += 1u64; - }; - }; - let datafilelen: u64 = l.datalen - bsslen; - - // One contiguous header buffer covering [0..0x1000), then .text. - let hdr: []u8 = alloc([], TEXT_OFF)!; - hdr.len = TEXT_OFF: i32; - - // --- Ehdr (64 bytes) --- - hdr[0u64] = 127u8; // 0x7f - hdr[1u64] = 'E'; - hdr[2u64] = 'L'; - hdr[3u64] = 'F'; - hdr[4u64] = ELFCLASS64; - hdr[5u64] = ELFDATA2LSB; - hdr[6u64] = EV_CURRENT: u8; - wru16(hdr.ptr, 16u64, ET_EXEC); // e_type - wru16(hdr.ptr, 18u64, EM_X86_64_W); // e_machine - wru32(hdr.ptr, 20u64, EV_CURRENT); // e_version - wru64(hdr.ptr, 24u64, entry); // e_entry - wru64(hdr.ptr, 32u64, 64u64); // e_phoff = sizeof(Ehdr) - wru64(hdr.ptr, 40u64, 0u64); // e_shoff - wru32(hdr.ptr, 48u64, 0u32); // e_flags - wru16(hdr.ptr, 52u64, 64u16); // e_ehsize - wru16(hdr.ptr, 54u64, 56u16); // e_phentsize - if (hasdata) { wru16(hdr.ptr, 56u64, 2u16); } - else { wru16(hdr.ptr, 56u64, 1u16); }; - wru16(hdr.ptr, 58u64, 0u16); // e_shentsize - wru16(hdr.ptr, 60u64, 0u16); // e_shnum - wru16(hdr.ptr, 62u64, 0u16); // e_shstrndx - - // --- Phdr #1 (R+X) at offset 64 --- - wru32(hdr.ptr, 64u64, PT_LOAD); - wru32(hdr.ptr, 68u64, PF_R | PF_X); - wru64(hdr.ptr, 72u64, 0u64); // p_offset - wru64(hdr.ptr, 80u64, base); // p_vaddr - wru64(hdr.ptr, 88u64, base); // p_paddr - wru64(hdr.ptr, 96u64, rxend); // p_filesz - wru64(hdr.ptr, 104u64, rxend); // p_memsz - wru64(hdr.ptr, 112u64, TEXT_OFF); // p_align - - if (hasdata) { - // --- Phdr #2 (R+W) at offset 64+56=120 --- - wru32(hdr.ptr, 120u64, PT_LOAD); - wru32(hdr.ptr, 124u64, PF_R | PF_W); - wru64(hdr.ptr, 128u64, dataoff); // p_offset - wru64(hdr.ptr, 136u64, base + dataoff); // p_vaddr - wru64(hdr.ptr, 144u64, base + dataoff); // p_paddr - wru64(hdr.ptr, 152u64, datafilelen); // p_filesz - wru64(hdr.ptr, 160u64, l.datalen); // p_memsz - wru64(hdr.ptr, 168u64, PAGE_SZ); // p_align - }; - - // Write [0..0x1000) then .text. - let r1: (i64 | os.oserror) = os.writeall(fd, hdr.ptr, TEXT_OFF); - let n1: i64 = 0i64; - match (r1) { - case let v: i64 => n1 = v; - case let e: os.oserror => return -1; - }; - if (n1 != TEXT_OFF: i64) { return -1; }; - if (l.textlen > 0u64) { - let r2: (i64 | os.oserror) = os.writeall(fd, l.text, l.textlen); - let n2: i64 = 0i64; - match (r2) { - case let v: i64 => n2 = v; - case let e: os.oserror => return -1; - }; - if (n2 != l.textlen: i64) { return -1; }; - }; - if (hasdata && datafilelen > 0u64) { - // Pad to the page-aligned data offset, then write only - // the non-zero prefix of .data. The rest is BSS — the - // loader zero-fills from p_filesz to p_memsz. - let here: u64 = TEXT_OFF + l.textlen; - let zero: u8 = 0u8; - for (here < dataoff) { - let r3: (i64 | os.oserror) = os.writeall(fd, &zero, 1u64); - match (r3) { - case let v: i64 => { }; - case let e: os.oserror => return -1; - }; - here += 1u64; - }; - let r4: (i64 | os.oserror) = os.writeall(fd, l.data, datafilelen); - let n4: i64 = 0i64; - match (r4) { - case let v: i64 => n4 = v; - case let e: os.oserror => return -1; - }; - if (n4 != datafilelen: i64) { return -1; }; - }; - return 0; -}; - -//ww:module-reset -// selfhost/cmd/w6l/main.ww — port of cmd/w6l/main.c. -// -// w6l = amd64 linker. Reads relocatable ELF .o files, SysV `ar` -// archives, and shared objects (ET_DYN). Resolves symbols, applies -// relocations, writes a static or dynamic-linked ELF executable. -// -// w6l_ww -o out [-L...] [-l...] file1.o file2.o ... - -package main; - -import os; -import rt; -import strings; -import sym; -import obj; -import dyn; -import pass; -import out; - -def BASE: u64 = 4194304u64; // 0x400000 -def CODE_VA_OFF: u64 = 4096u64; // .text starts at base + 0x1000 - -fn mklnk() *lnk = { - let l: *lnk = alloc(lnk { })!; - return l; -}; - -// `cstreq` lives in obj.ww — same bundle, single definition. - -// `cstrlen` lives in obj.ww — same bundle, single definition. - -// Build "/lib." into dst (NUL-terminated). Returns total -// length excluding NUL. dst must be large enough. -fn buildpath(dst: *u8, dir: *u8, name: *u8, ext: str) u64 = { - let i: u64 = 0u64; - let dn: u64 = cstrlen(dir); - let nn: u64 = cstrlen(name); - let k: u64 = 0u64; - for (k < dn) { dst[i] = dir[k]; i += 1u64; k += 1u64; }; - dst[i] = '/'; - i += 1u64; - dst[i] = 'l'; - i += 1u64; - dst[i] = 'i'; - i += 1u64; - dst[i] = 'b'; - i += 1u64; - k = 0u64; - for (k < nn) { dst[i] = name[k]; i += 1u64; k += 1u64; }; - k = 0u64; - for (k < ext.len: u64) { - let li: i32 = k: i32; - dst[i] = ext[li]; - i += 1u64; - k += 1u64; - }; - dst[i] = 0u8; - return i; -}; - -// Append decimal n to dst at offset i. Returns new offset. -fn appenddec(dst: *u8, i: u64, n: u64) u64 = { - if (n == 0u64) { - dst[i] = 48u8; - return i + 1u64; - }; - let buf: [16]u8; - let k: u64 = 0u64; - let v: u64 = n; - for (v > 0u64) { - buf[k] = (v % 10u64): u8 + 48u8; - v = v / 10u64; - k += 1u64; - }; - let oi: u64 = i; - for (k > 0u64) { - k -= 1u64; - dst[oi] = buf[k]; - oi += 1u64; - }; - return oi; -}; - -fn buildpathv(dst: *u8, dir: *u8, name: *u8, v: u64) u64 = { - let i: u64 = 0u64; - let dn: u64 = cstrlen(dir); - let nn: u64 = cstrlen(name); - let k: u64 = 0u64; - for (k < dn) { dst[i] = dir[k]; i += 1u64; k += 1u64; }; - dst[i] = '/'; i += 1u64; - dst[i] = 'l'; i += 1u64; - dst[i] = 'i'; i += 1u64; - dst[i] = 'b'; i += 1u64; - k = 0u64; - for (k < nn) { dst[i] = name[k]; i += 1u64; k += 1u64; }; - dst[i] = '.'; i += 1u64; dst[i] = 's'; i += 1u64; dst[i] = 'o'; i += 1u64; dst[i] = '.'; i += 1u64; - i = appenddec(dst, i, v); - dst[i] = 0u8; - return i; -}; - -// islinkable: read first 8 bytes; require !\n or \x7fELF. -fn islinkable(path: *u8) bool = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return false; }; - let mp: [8]u8; - let n: i64 = os.read(fd, &mp[0], 8u64); - os.close(fd); - if (n < 4i64) { return false; }; - // hasprefix length-bails when the header is shorter than the magic, - // matching the old n>=8 archive guard (str len < 8 => no match). - let hn: i32 = n: i32; - let hdr: str = strings.frombytes(mp[0:hn]); - if (strings.hasprefix(hdr, "!\n")) { return true; }; - if (strings.hasprefix(hdr, "\x7fELF")) { return true; }; - return false; -}; - -// Walk libdirs[0..n) trying lib.so, then lib.so.{0..8}, -// then lib.a. Return a heap-allocated NUL-terminated path on -// success, nil on miss. -fn resolvelib(name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = { - let bufp: []u8 = alloc([], 1024u64)!; - bufp.len = 1024; - let i: i32 = 0; - for (i < nlibdirs) { - let dir: *u8 = libdirs[i]; - let _l1: u64 = buildpath(bufp.ptr, dir, name, ".so"); - if (islinkable(bufp.ptr)) { - let pl: u64 = cstrlen(bufp.ptr); - let p: []u8 = alloc([], pl + 1u64)!; - let k: u64 = 0u64; - for (k <= pl) { p[k] = bufp[k]; k += 1u64; }; - return p.ptr; - }; - let v: u64 = 0u64; - for (v <= 8u64) { - let _l2: u64 = buildpathv(bufp.ptr, dir, name, v); - if (islinkable(bufp.ptr)) { - let pl2: u64 = cstrlen(bufp.ptr); - let p2: []u8 = alloc([], pl2 + 1u64)!; - let k2: u64 = 0u64; - for (k2 <= pl2) { p2[k2] = bufp[k2]; k2 += 1u64; }; - return p2.ptr; - }; - v += 1u64; - }; - let _l3: u64 = buildpath(bufp.ptr, dir, name, ".a"); - if (islinkable(bufp.ptr)) { - let pl3: u64 = cstrlen(bufp.ptr); - let p3: []u8 = alloc([], pl3 + 1u64)!; - let k3: u64 = 0u64; - for (k3 <= pl3) { p3[k3] = bufp[k3]; k3 += 1u64; }; - return p3.ptr; - }; - i += 1; - }; - return nil; -}; - -// Read first 20 bytes; return 1 for ET_DYN .so, 0 for ar/.o. -fn isso(path: *u8) i32 = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return 0; }; - let mp: [20]u8; - let n: i64 = os.read(fd, &mp[0], 20u64); - os.close(fd); - if (n < 20i64) { return 0; }; - let hdr: str = strings.frombytes(mp[0:4]); - if (!strings.hasprefix(hdr, "\x7fELF")) { return 0; }; - // e_type at offset 16, u16 little-endian - let t: u16 = (mp[16u64]: u16) | ((mp[17u64]: u16) << 8u16); - if (t == 3u16) { return 1; }; - return 0; -}; - -export fn main(argc: i32, argv: **u8) i32 = { - let outpath: *u8 = nil; - // argc is the upper bound on flags/inputs (one each per argv slot), - // mirroring cstage's calloc(argc, ...). No fixed cap. - let inputs: []*u8 = alloc([], argc: u64)!; - inputs.len = argc; - let ninputs: i32 = 0; - let libdirs: []*u8 = alloc([], argc: u64)!; - libdirs.len = argc; - let nlibdirs: i32 = 0; - let lflags: []*u8 = alloc([], argc: u64)!; - lflags.len = argc; - let nlflags: i32 = 0; - - let i: i32 = 1; - for (i < argc) { - let a: *u8 = argv[i]; - if (cstreq(a, "-o")) { - i += 1; - if (i >= argc) { - let m: str = "w6l: -o requires argument\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - outpath = argv[i]; - } else { if (cstreq(a, "-L")) { - i += 1; - if (i >= argc) { - let m: str = "w6l: -L requires argument\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - libdirs[nlibdirs] = argv[i]; - nlibdirs += 1; - } else { if (cstreq(a, "-l")) { - i += 1; - if (i >= argc) { - let m: str = "w6l: -l requires argument\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - lflags[nlflags] = argv[i]; - nlflags += 1; - } else { if (a[0u64] == '-') { - // -L joined form. - if (a[1u64] == 'L') { - if (a[2u64] != 0u8) { - libdirs[nlibdirs] = a + 2u64; - nlibdirs += 1; - } else { - let m: str = "w6l: bare -L\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - } else { if (a[1u64] == 'l') { - if (a[2u64] != 0u8) { - lflags[nlflags] = a + 2u64; - nlflags += 1; - } else { - let m: str = "w6l: bare -l\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - } else { - let m: str = "w6l: unknown flag\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - };}; - } else { - inputs[ninputs] = a; - ninputs += 1; - };};};}; - i += 1; - }; - - if (outpath == nil) { - let m: str = "usage: w6l_ww -o exe [-L...] [-l...] file1.o [file2.o...]\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - if (ninputs == 0) { - let m: str = "w6l: no inputs\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - - let l: *lnk = mklnk(); - - // Seed _start so libwwrt-style start.o is recognised as wanted. - intern(l, "_start"); - - // Load positional inputs first (preserving order). - let k: i32 = 0; - for (k < ninputs) { - if (load(l, inputs[k]) != 0) { - return 1; - }; - k += 1; - }; - - // Then resolve -l flags and load each. Archives append; shared - // objects register their exports. - let lf: i32 = 0; - for (lf < nlflags) { - let p: *u8 = resolvelib(lflags[lf], libdirs.ptr, nlibdirs); - if (p == nil) { - let m: str = "w6l: cannot find -l"; - os.write(2, m.ptr, m.len: u64); - let nm: *u8 = lflags[lf]; - os.write(2, nm, cstrlen(nm)); - let nl: str = "\n"; - os.write(2, nl.ptr, nl.len: u64); - return 1; - }; - if (isso(p) != 0) { - if (loadso(l, p) != 0) { return 1; }; - } else { - if (load(l, p) != 0) { return 1; }; - }; - lf += 1; - }; - - if (resolve(l) != 0) { return 1; }; - // Relocation is deferred to the emit functions — each path - // knows its own layout (textva, datava); the static and dyn - // paths place .data at different VAs. - - let entrysym: *lsym = lookup(l, "_start"); - if (entrysym == nil) { entrysym = lookup(l, "main"); } - else { if (entrysym.defined == 0) { entrysym = lookup(l, "main"); }; }; - if (entrysym == nil) { - let m: str = "w6l: no _start or main symbol\n"; - os.write(2, m.ptr, m.len: u64); - return 1; - }; - if (entrysym.defined == 0) { - let m: str = "w6l: no _start or main symbol\n"; - os.write(2, m.ptr, m.len: u64); - return 1; - }; - - let flags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC; - let fd: i32 = os.open(pathstr(outpath), flags, 493i32); // 0o755 - if (fd < 0) { - let m: str = "w6l: cannot open output\n"; - os.write(2, m.ptr, m.len: u64); - return 1; - }; - - let entryva: u64 = BASE + CODE_VA_OFF + entrysym.val; - let rc: i32 = emitelf(l, fd, BASE, entryva); - os.close(fd); - return rc; -}; - diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww deleted file mode 100644 index f4a9e756..00000000 --- a/selfhost/cmd/ww/main.combined.ww +++ /dev/null @@ -1,5639 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module-reset -// selfhost/cmd/ww/main.ww — port of cmd/ww/main.c. -// -// The user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue: -// -// ww build foo.ww → w6c foo.ww > foo.s ; w6a foo.s > foo.o ; -// w6l -o foo foo.o libwwrt.a -// ww run foo.ww → build then exec -// ww version → print version -// -// Tool paths default to siblings of $0 so a fresh build runs out of -// out/bin/. Env-var overrides (WW_W6C / WW_W6A / WW_W6L / WW_LIB) are -// not yet supported in this port; the bootstrap doesn't need them. - -package main; - -import os; -import rt; -import strings; - -// All path/string scratch buffers go on the runtime page allocator. -// One page is plenty for any path we build. PATH_MAX lives in lib/os -// (os.PATH_MAX: i32 = 4096) — the duplicate u64 def was dropped to close -// the cgen #127 mod-mangle attribution bug consumer per rule-7. -def CMD_MAX: u64 = 8192u64; - -// cerr — bare stderr fragment writer for the driver'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 (the selfhost ww-driver build is a single main.ww; -// err.c's bare-message path is not ported into this tree). -fn cerr(m: str) void = { - os.write(2, m.ptr, m.len: u64); -}; - -// ---- C-string helpers -------------------------------------------------- - -fn cstrlen(p: *u8) u64 = { - let n: u64 = 0u64; - for (p[n] != 0u8) { n += 1u64; }; - return n; -}; - -// pathstr — view a NUL-terminated *u8 as a str. Bridges the -// driver's argv-style *u8 paths to lib/os entrypoints (str -// post-task-#23). -fn pathstr(p: *u8) str = { - let r: str; - r.ptr = p; - r.len = cstrlen(p): i32; - return r; -}; - -fn cstreq(a: *u8, b: *u8) bool = { - return strings.compare(pathstr(a), pathstr(b)) == 0; -}; - -// cstreqlit — compare a NUL-terminated *u8 to a ww string literal. -fn cstreqlit(a: *u8, lit: str) bool = { - return strings.compare(pathstr(a), lit) == 0; -}; - -// memcpy -fn bytecpy(dst: *u8, src: *u8, n: u64) void = { - let i: u64 = 0u64; - for (i < n) { - dst[i] = src[i]; - i += 1u64; - }; -}; - -// Copy a NUL-terminated *u8 into dst starting at off; return the new -// offset (without writing a NUL). -fn cstrinto(dst: *u8, off: u64, src: *u8) u64 = { - let i: u64 = 0u64; - for (src[i] != 0u8) { - dst[off + i] = src[i]; - i += 1u64; - }; - return off + i; -}; - -// Same, but for a ww `str` (no NUL on the source side; we copy len bytes). -fn strinto(dst: *u8, off: u64, src: str) u64 = { - let n: i32 = src.len; - let i: i32 = 0; - for (i < n) { - let iu: u64 = i: u64; - dst[off + iu] = src[i]; - i += 1; - }; - let nu: u64 = n: u64; - return off + nu; -}; - -// Write a single byte, return new offset. -fn byteinto(dst: *u8, off: u64, c: u8) u64 = { - dst[off] = c; - return off + 1u64; -}; - -// NUL-terminate at off and return the same off (handy when passing the -// buffer to a syscall that expects a C-string). -fn cstrseal(dst: *u8, off: u64) void = { - dst[off] = 0u8; -}; - -// ---- Tool-path resolution --------------------------------------------- - -// dirname-equivalent: copy argv[0] up to (but not including) the last -// '/' into dst, NUL-terminated. If no slash, write ".". -fn selfdirinto(dst: *u8, dstsz: u64, argv0: *u8) void = { - let n: u64 = cstrlen(argv0); - let cut: u64 = 0u64; - let i: u64 = 0u64; - for (i < n) { - if (argv0[i] == 47u8) { cut = i; }; // '/' - i += 1u64; - }; - if (cut == 0u64) { - dst[0u64] = 46u8; // '.' - dst[1u64] = 0u8; - return; - }; - if (cut + 1u64 >= dstsz) { cut = dstsz - 2u64; }; - bytecpy(dst, argv0, cut); - dst[cut] = 0u8; -}; - -// Build "$dir/$name" (NUL-terminated) into a fresh page-sized buffer. -fn joinpath(dir: *u8, name: *u8) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; - let off: u64 = cstrinto(buf.ptr, 0u64, dir); - off = byteinto(buf.ptr, off, 47u8); - off = cstrinto(buf.ptr, off, name); - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// Same, but the second component is a ww `str` literal. -fn joinpathlit(dir: *u8, name: str) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; - let off: u64 = cstrinto(buf.ptr, 0u64, dir); - off = byteinto(buf.ptr, off, 47u8); - off = strinto(buf.ptr, off, name); - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// ---- Subprocess plumbing ---------------------------------------------- - -// procrun — fork, execve `path` with `argv` (NULL-terminated), wait. -// Returns the child's real exit code on clean exit, 1 on signal kill, -// -1 on fork/wait failure. Mirrors cmd/ww/main.c:do_run WEXITSTATUS: -// the build-step callers only test `!= 0`, so propagating the exact -// non-zero code leaves them unaffected while `dorun` reports the true -// program exit status (was collapsing every non-zero exit to 1; fix #16). -fn procrun(path: *u8, argv: **u8) i32 = { - let pid: i32 = os.fork(); - if (pid < 0) { - cerr("ww: fork failed\n"); - return -1; - }; - if (pid == 0) { - os.execve(pathstr(path), argv, nil: **u8); - cerr("ww: execve failed\n"); - os.exit(127); - }; - let status: i32 = 0; - let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); - if (r < 0) { - cerr("ww: wait4 failed\n"); - return -1; - }; - // Linux wait status: low byte = signal (0 if exited cleanly), - // next byte = exit code. - if ((status & 127i32) != 0) { return 1; }; - let code: i32 = (status >> 8i32) & 255i32; - return code; -}; - -// ---- `use` resolution + source concatenation -------------------------- -// -// Recursive expansion: for each `use IDENT;` we find at the top of -// `path`, resolve via the colon-separated `dirs`, expand the imported -// file first, then append our own bytes. Already-visited paths are -// skipped (linear scan; typical builds visit a handful of modules). - -type strnode = struct { - s: str, - snext: *strnode, -}; - -type expctx = struct { - out: i32, // fd we're writing the combined source to - dirs: *u8, // ":"-separated search path (NUL-terminated) - visit: *strnode, -}; - -fn visitseen(c: *expctx, path: str) bool = { - let n: *strnode = c.visit; - for (n != nil) { - if (n.s.len == path.len) { - let i: i32 = 0; - let eq: bool = true; - for (i < path.len) { - if (n.s[i] != path[i]) { eq = false; i = path.len; } - else { i += 1; }; - }; - if (eq) { return true; }; - }; - n = n.snext; - }; - return false; -}; - -fn visitadd(c: *expctx, path: str) void = { - let n: *strnode = alloc(strnode{s=path, snext=c.visit})!; - c.visit = n; -}; - -// Translate dots in an `import` name to slashes for path lookup. -// `encoding.utf8` → `encoding/utf8`. Mirrors Hare's hare(1) -// use-path → fs-path mapping -// (ref/hare/hare/module/srcs.ha:78 builds the same shape via -// path::push per ident part). -fn importpathform(name: *u8, namelen: u64) *u8 = { - let buf: []u8 = alloc([], namelen + 1u64)!; - let i: u64 = 0u64; - for (i < namelen) { - if (name[i] == 46u8) { buf[i] = 47u8; } // '.' -> '/' - else { buf[i] = name[i]; }; - i += 1u64; - }; - buf[namelen] = 0u8; - return buf.ptr; -}; - -// Try // as a directory (wantdir != 0), else /.ww -// as a file. Sets *isdir on hit. Symmetric with cstage locate_import_in -// for byte-id driver output (rule 10). The legacy //.ww -// form was dropped in task #22 — directory-as-module enumeration -// replaces it, mirroring ref/hare/hare/module/srcs.ha (Hare has no -// `foo/foo.ha` fallback; a module IS the directory). -fn locatein(dir: *u8, dirlen: u64, - pathform: *u8, pflen: u64, isdir: *i32, wantdir: i32) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - let off: u64 = 0u64; - let i: u64 = 0u64; - for (i < dirlen) { buf[off + i] = dir[i]; i += 1u64; }; - off += dirlen; - buf[off] = 47u8; off += 1u64; // '/' - i = 0u64; - for (i < pflen) { buf[off + i] = pathform[i]; i += 1u64; }; - off += pflen; - if (wantdir != 0i32) { - buf[off] = 0u8; - let fi: os.filestat; - let r: (void | os.oserror) = os.stat(&fi, pathstr(buf.ptr)); - match (r) { - case void => { - let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT - if (t == os.mode.DIR: u32) { - *isdir = 1; - return buf.ptr; - }; - }; - case let e: os.oserror => void; - }; - return nil; - }; - buf[off] = 46u8; off += 1u64; // '.' - buf[off] = 119u8; off += 1u64; // 'w' - buf[off] = 119u8; off += 1u64; // 'w' - buf[off] = 0u8; - if (os.access(pathstr(buf.ptr), 0i32) == 0) { - *isdir = 0; - return buf.ptr; - }; - return nil; -}; - -// Walk a colon-separated dirlist, return first hit or nil. Sets -// *isdir on hit. -// -// #98: "a module IS the directory" — a directory-package on ANY entry -// wins over a same-named sibling FILE on an EARLIER entry. The driver -// builds the searchpath srcd-first; a co-located `lib//test.ww` -// entry makes srcd = lib/, so a self-named `import ` would -// else file-hit the sibling lib//.ww and (under --sep) fold -// inline under the wrong module-reset. Two passes — directories first, -// files only if no directory matches anywhere — let lib// resolve -// as the dir while a genuine leaf package with no directory (e.g. -// lib/encoding/hex imported bare as `hex`, reachable only via its file -// in srcd) still resolves in the file pass. Latent: a dir-package now -// beats an earlier-entry same-named sibling FILE — loud-failing, none -// in the corpus; tracked as #101. -fn locateimport(dirs: *u8, name: *u8, namelen: u64, - isdir: *i32) *u8 = { - let pathform: *u8 = importpathform(name, namelen); - let pflen: u64 = cstrlen(pathform); - let total: u64 = cstrlen(dirs); - let wantdir: i32 = 1i32; - for (wantdir >= 0i32) { - let p: u64 = 0u64; - for (p < total) { - let q: u64 = p; - for (q < total) { - if (dirs[q] == ':') { break; }; - q += 1u64; - }; - let seglen: u64 = q - p; - if (seglen > 0u64) { - let hit: *u8 = locatein(dirs + p, seglen, - pathform, pflen, isdir, wantdir); - if (hit != nil) { return hit; }; - }; - p = q + 1u64; - }; - wantdir -= 1i32; - }; - return nil; -}; - -// Filter for dir enumeration: keep `*.ww` minus `*test.ww` and the -// `*.combined.ww` driver-generated concat artifacts (the previous -// build leaves them in the source tree; they parse-error when -// re-included). Returns true to keep. -fn dirfilekeep(name: *u8, nlen: u64) bool = { - // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but - // would pass strings.hassuffix(".ww"); preserves cstage parity. - if (nlen <= 3u64) { return false; }; - let s: str; - s.ptr = name; - s.len = nlen: i32; - if (!strings.hassuffix(s, ".ww")) { return false; }; - if (strings.hassuffix(s, "test.ww")) { return false; }; - // ".combined.ww" — full 12-char match mirrors cstage - // cmd/ww/main.c enumerate_dir_ww strcmp (rule 10). - if (strings.hassuffix(s, ".combined.ww")) { return false; }; - return true; -}; - -// Byte-wise memcmp returning < 0, 0, > 0. Rule-10 byte-id requires -// cstage and wwstage sort the same way; memcmp is the -// locale-independent total order (mirrors ref/hare/sort/cmp/cmp.ha -// strs). -fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = { - let n: u64 = alen; - if (blen < n) { n = blen; }; - let i: u64 = 0u64; - for (i < n) { - let av: i32 = (a[i]): i32; - let bv: i32 = (b[i]): i32; - if (av < bv) { return -1; }; - if (av > bv) { return 1; }; - i += 1u64; - }; - if (alen < blen) { return -1; }; - if (alen > blen) { return 1; }; - return 0; -}; - -// enumeratedir — list *.ww entries of `dirpath` (less *test.ww and -// *.combined.ww), byte-sort. Returns (names[], nnames) with each -// name a NUL-terminated heap copy. -fn enumeratedir(dirpath: *u8) (**u8, i32) = { - let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil: **u8, 0; }; - // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, - // cmd/ww/main.c:209). The old fixed 256-name cap silently dropped every - // eligible file past it, diverging the combined.ww from cstage on a - // module dir with >256 sources. - let cap: i32 = 8; - let names: []*u8 = alloc([], cap: u64)!; - let nlens: []u64 = alloc([], cap: u64)!; - let n: i32 = 0; - let buf: []u8 = alloc([], 8192u64)!; - buf.len = 8192; - let r: i64 = os.getdents64(fd, buf.ptr, 8192u64); - for (r > 0i64) { - let off: u64 = 0u64; - let ru: u64 = r: u64; - for (off < ru) { - let blo: u64 = (buf[off + 16u64]): u64; - let bhi: u64 = (buf[off + 17u64]): u64; - let reclen: u64 = blo + (bhi * 256u64); - let nm: *u8 = buf.ptr + off + 19u64; - let nl: u64 = cstrlen(nm); - if (dirfilekeep(nm, nl)) { - if (n >= cap) { - let ncap: i32 = cap * 2; - let nn: []*u8 = alloc([], ncap: u64)!; - let nl2: []u64 = alloc([], ncap: u64)!; - let k: i32 = 0; - for (k < n) { - nn[k] = names[k]; - nl2[k] = nlens[k]; - k += 1; - }; - names = nn; - nlens = nl2; - cap = ncap; - }; - let cp: []u8 = alloc([], nl + 1u64)!; - let i: u64 = 0u64; - for (i < nl) { cp[i] = nm[i]; i += 1u64; }; - cp[nl] = 0u8; - names[n] = cp.ptr; - nlens[n] = nl; - n += 1; - }; - off += reclen; - }; - r = os.getdents64(fd, buf.ptr, 8192u64); - }; - os.close(fd); - - // Insertion sort, byte-wise. n is small (≤16 in practice). - let i: i32 = 1; - for (i < n) { - let j: i32 = i; - for (j > 0) { - if (bytecmp(names[j - 1], nlens[j - 1], - names[j], nlens[j]) <= 0) { j = 0; } - else { - let t: *u8 = names[j]; - names[j] = names[j - 1]; - names[j - 1] = t; - let tl: u64 = nlens[j]; - nlens[j] = nlens[j - 1]; - nlens[j - 1] = tl; - j -= 1; - }; - }; - i += 1; - }; - return names.ptr, n; -}; - -// ---- file slurp ------------------------------------------------------- - -fn slurp(pathcs: *u8) (*u8, u64) = { - let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil, 0u64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - let n: i64 = 0i64; - match (szr) { - case let v: i64 => n = v; - case let e: os.oserror => { os.close(fd); return nil, 0u64; }; - }; - let nu: u64 = n: u64; - let buf: []u8 = alloc([], nu + 1u64)!; - buf.len = (nu + 1u64): i32; - let rr: (i64 | os.oserror) = os.readall(fd, buf.ptr, nu); - os.close(fd); - let got: i64 = 0i64; - match (rr) { - case let v: i64 => got = v; - case let e: os.oserror => return nil, 0u64; - }; - if (got != n) { return nil, 0u64; }; - buf[nu] = 0u8; - return buf.ptr, nu; -}; - -fn isidentbyte(c: u8) bool = { - if (c >= 'a' && c <= 'z') { return true; }; - if (c >= 'A' && c <= 'Z') { return true; }; - if (c >= '0' && c <= '9') { return true; }; - if (c == '_') { return true; }; - if (c == '.') { return true; }; - return false; -}; - -// Scan one `import IDENT;` line out of [start, end). Returns the start -// of the ident and its length, or (nil, 0) if no `import` here. The -// caller passes a slice of the source: src points at the line start. -fn scanuse(src: *u8, len: u64) (*u8, u64) = { - let i: u64 = 0u64; - // skip leading whitespace - for (i < len) { - if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; - i += 1u64; - }; - // i+7>len guard kept: hasprefix("import") covers the 6 spell-out - // bytes, but the sep read at src[i+6] still needs i+6 < len. - if (i + 7u64 > len) { return nil, 0u64; }; - let rest: str; - rest.ptr = src + i; - rest.len = (len - i): i32; - if (!strings.hasprefix(rest, "import")) { return nil, 0u64; }; - let sep: u8 = src[i + 6u64]; - if (sep != 32u8) { if (sep != 9u8) { return nil, 0u64; }; }; - i += 7u64; - for (i < len) { - if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; - i += 1u64; - }; - let idstart: u64 = i; - for (i < len) { - if (!isidentbyte(src[i])) { break; }; - i += 1u64; - }; - let idlen: u64 = i - idstart; - if (idlen == 0u64) { return nil, 0u64; }; - return src + idstart, idlen; -}; - -// expand — emit one file's bytes verbatim into the combined stream, -// after recursive-expanding its top-of-file `import X;` imports. -// Each source declares its own `package ;` (parser stamps -// decls). -fn expand(c: *expctx, pathcs: *u8, modpath: str) void = { - let plen: u64 = cstrlen(pathcs); - let view: str; - view.ptr = pathcs; - view.len = plen: i32; - let pathstr: str = strings.dup(view); - if (visitseen(c, pathstr)) { return; }; - visitadd(c, pathstr); - - let bufp: *u8; - let blen: u64; - bufp, blen = slurp(pathcs); - if (bufp == nil) { - cerr("ww: cannot read source\n"); - return; - }; - - // Pass 1: scan top-of-file `import X;` lines, recursively expand. - let i: u64 = 0u64; - for (i < blen) { - let j: u64 = i; - for (j < blen) { - if (bufp[j] == 10u8) { break; }; // '\n' - j += 1u64; - }; - let idp: *u8; - let idn: u64; - idp, idn = scanuse(bufp + i, j - i); - if (idp != nil) { - let isdir: i32 = 0; - let ipath: *u8 = locateimport(c.dirs, idp, idn, - &isdir); - if (ipath != nil) { - // M1 #22 (isdir-gated, rob-ratified): only DIRECTORY - // imports are package boundaries that path-mangle. A - // single-file import (`import opcodes;` → opcodes.ww - // declaring `package w6a`) is an intra-package file-split: - // it keeps its in-file `package` clause as its module - // (no directive → reset → package-clause mangling). - if (isdir != 0) { - let mv: str; - mv.ptr = idp; - mv.len = idn: i32; - let modstr: str = strings.dup(mv); - expanddir(c, ipath, modstr); - } else { - expand(c, ipath, ""); - }; - } else { - // #16 ENFORCE-driver (rob A): a locate-miss is - // legal when the package is defined INLINE in the - // same unit (single-file multi-package). leaf = the - // last dotted component of the import name; if an - // inline `package ` exists -> silent skip - // (the checker binds it), else fatal. cstage twin in - // cmd/ww/main.c; fatal text identical. - let lstart: u64 = 0u64; - let lk: u64 = 0u64; - for (lk < idn) { - if (idp[lk] == 46u8) { lstart = lk + 1u64; }; // '.' - lk += 1u64; - }; - let leafp: *u8 = idp + lstart; - let leafn: u64 = idn - lstart; - if (!unithaspackage(bufp, blen, leafp, leafn)) { - cerr("ww: cannot find package "); - os.write(2, idp, idn); - cerr("\n"); - os.exit(1); - }; - }; - }; - i = j + 1u64; - }; - - // #16 option-B: a package-less file's decls would otherwise inherit - // the preceding bundled module's sticky curmod (parser parse.ww). Emit - // a curmod-reset boundary directive so the lexer/parser attribute the - // file to the primary module ("") — fixes the self-import false-fire - // and the leaked-prefix bug, codegen-neutral (bare symbols kept; not - // `package main`, which would main-prefix them). A packaged file's own - // `package` decl already sets curmod, so it needs nothing — keeping - // the directive out of every tracked combined.ww. (Task #11.) - if (modpath.len != 0) { - // M1 #22: import-reached file carries its full dotted path so - // codegen mangles symbols on the path, not the leaf clause. - let dm: str = "//ww:module "; - os.writeall(c.out, dm.ptr, dm.len: u64); - os.writeall(c.out, modpath.ptr, modpath.len: u64); - os.writeall(c.out, "\n".ptr, 1u64); - } else { - // Root/primary: always reset so a preceding imported section's - // sticky pathmod is cleared; a packaged primary's own `package` - // clause then sets curmod fresh (pathmod NULL → real clause). - let d: str = "//ww:module-reset\n"; - os.writeall(c.out, d.ptr, d.len: u64); - }; - - os.writeall(c.out, bufp, blen); - os.writeall(c.out, "\n".ptr, 1u64); -}; - -// Scan `pathcs` for its first non-comment-non-blank line; if it -// starts with `package ;` return the package name as a fresh -// heap-allocated NUL-terminated *u8, else nil. Same shape as cstage -// peek_package. -// -// Reads the WHOLE file (via slurp), not a fixed prefix: cstage's -// peek_package scans line-by-line with fgets and no total cap, stopping -// at the first non-comment-non-blank line. A prior 2048-byte read cap -// here diverged from cstage on files whose `package` decl sits behind a -// >2048-byte comment header (strconv decimal/ftos/stof, memio) — -// returning nil and making the #16 D-i injection asymmetric (rule-10 -// break, byte-id divergence in the regenerated combined). The corpus has -// no line >2047 chars, so a whole-file line scan matches cstage's -// per-line fgets byte-for-byte on every real input. -fn peekpackage(pathcs: *u8) *u8 = { - let bufp: *u8; - let nu: u64; - bufp, nu = slurp(pathcs); - if (bufp == nil) { return nil; }; - let p: u64 = 0u64; - for (p < nu) { - let q: u64 = p; - for (q < nu) { - if (bufp[q] == 10u8) { break; }; // '\n' - q += 1u64; - }; - let s: u64 = p; - for (s < q) { - if (bufp[s] != 32u8) { - if (bufp[s] != 9u8) { break; }; - }; - s += 1u64; - }; - if (s < q) { - let line: str; - line.ptr = bufp + s; - line.len = (q - s): i32; - // hasprefix("//") subsumes the old s+1= 97u8) { if (ch <= 122u8) { isalpha = true; }; }; - if (ch >= 65u8) { if (ch <= 90u8) { isalpha = true; }; }; - if (ch >= 48u8) { if (ch <= 57u8) { isalpha = true; }; }; - if (ch == 95u8) { isalpha = true; }; - if (!isalpha) { break; }; - t += 1u64; - }; - let plen: u64 = t - start; - if (plen == 0u64) { return nil; }; - let r: []u8 = alloc([], plen + 1u64)!; - let k: u64 = 0u64; - for (k < plen) { r[k] = bufp[start + k]; k += 1u64; }; - r[plen] = 0u8; - return r.ptr; - }; - }; - return nil; - }; - p = q + 1u64; - }; - return nil; -}; - -// unithaspackage — does the unit buffer declare `package ;` ANYWHERE? -// #16 ENFORCE-driver (rob A) cstage unit_has_package twin: distinguishes a -// genuinely-missing import from one satisfied by an INLINE package in the -// same single-file multi-package unit (`package aa; ... package main; -// import aa;`). Scans EVERY line (comment-skip) — not just the first -// package decl (peekpackage stops there). Decision byte-identical to -// cstage so the skip/fatal choice + driver output match (rule 10). -fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = { - let p: u64 = 0u64; - for (p < buflen) { - let q: u64 = p; - for (q < buflen) { if (buf[q] == 10u8) { break; }; q += 1u64; }; - let s: u64 = p; - for (s < q) { - if (buf[s] != 32u8) { if (buf[s] != 9u8) { break; }; }; - s += 1u64; - }; - if (s < q) { - let line: str; - line.ptr = buf + s; - line.len = (q - s): i32; - if (strings.hasprefix(line, "//")) { p = q + 1u64; continue; }; - if (s + 8u64 <= q) { - if (strings.hasprefix(line, "package")) { - let sep: u8 = buf[s + 7u64]; - let oksep: bool = false; - if (sep == 32u8) { oksep = true; } - else { if (sep == 9u8) { oksep = true; }; }; - if (oksep) { - let t: u64 = s + 8u64; - for (t < q) { - if (buf[t] != 32u8) { if (buf[t] != 9u8) { break; }; }; - t += 1u64; - }; - let m: u64 = 0u64; - let eq: bool = true; - for (m < leafn) { - if (t + m >= q) { eq = false; break; }; - if (buf[t + m] != leafp[m]) { eq = false; break; }; - m += 1u64; - }; - if (eq) { - let after: u64 = t + leafn; - let term: bool = false; - if (after >= q) { term = true; } - else { - let c: u8 = buf[after]; - if (c == 59u8) { term = true; } // ';' - else { if (c == 32u8) { term = true; } - else { if (c == 9u8) { term = true; }; }; }; - }; - if (term) { return true; }; - }; - }; - }; - }; - }; - p = q + 1u64; - }; - return false; -}; - -// Strict-same-package error helper. Bundled here per task #22 -// brief — failure mode is dir-enum's own. -fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = { - cerr("ww: "); - os.write(2, file, cstrlen(file)); - cerr(": package "); - os.write(2, pkg, cstrlen(pkg)); - cerr(" differs from "); - os.write(2, dirpkg, cstrlen(dirpkg)); - cerr(" in same module dir "); - os.write(2, dirpath, cstrlen(dirpath)); - cerr("\n"); - os.exit(1); -}; - -// expanddir — enumerate /*.ww (skip *test.ww and -// *.combined.ww), byte-sort, recurse into each. Mirrors -// ref/hare/hare/module/srcs.ha:183 `_findsrcs` minus tag handling. -// The visited set keys on concrete file paths so multi-file modules -// are pulled once. Strict-same-package: all enumerated files must -// declare the same `package ;` (task #23 subset; failure -// mode native to dir-enum). -fn expanddir(c: *expctx, dirpath: *u8, modpath: str) void = { - let names: **u8; - let n: i32; - names, n = enumeratedir(dirpath); - let dlen: u64 = cstrlen(dirpath); - let dirpkg: *u8 = nil; - let i: i32 = 0; - for (i < n) { - let nlen: u64 = cstrlen(names[i]); - let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; - let k: u64 = 0u64; - for (k < dlen) { fp[k] = dirpath[k]; k += 1u64; }; - fp[dlen] = 47u8; // '/' - k = 0u64; - for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; - fp[dlen + 1u64 + nlen] = 0u8; - let pkg: *u8 = peekpackage(fp.ptr); - if (pkg != nil) { - if (dirpkg == nil) { dirpkg = pkg; } - else { if (!cstreq(dirpkg, pkg)) { - strictpkgmismatch(fp.ptr, pkg, dirpkg, dirpath); - }; }; - }; - expand(c, fp.ptr, modpath); - i += 1; - }; -}; - -// ---- Build pipeline --------------------------------------------------- - -// Strip the trailing ".ww" off `src` (a NUL-terminated path) into -// `stem`, NUL-terminated. If there's no .ww, the stem is the whole -// path. -fn makestem(stem: *u8, src: *u8) void = { - let n: u64 = cstrlen(src); - let stop: u64 = n; - if (n >= 3u64) { - if (src[n - 3u64] == 46u8) { // '.' - if (src[n - 2u64] == 119u8) { // 'w' - if (src[n - 1u64] == 119u8) { // 'w' - stop = n - 3u64; - }; - }; - }; - }; - let i: u64 = 0u64; - for (i < stop) { stem[i] = src[i]; i += 1u64; }; - stem[stop] = 0u8; -}; - -// Append a literal suffix to `stem` (which already lives in a buffer). -fn appendlit(stem: *u8, suffix: str) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; - let off: u64 = cstrinto(buf.ptr, 0u64, stem); - off = strinto(buf.ptr, off, suffix); - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// linker flags (-L, -l) grouped as one struct so buildone's -// param list stays readable. (The earlier note here claimed a 6-argument -// wwstage calling-convention cap; that is stale — emittuplerowrelocs in -// cgen.ww takes 7 params and self-compiles green, and buildone itself now -// takes 7.) -type lflags = struct { - libdirs: **u8, - nlibdirs: i32, - libs: **u8, - nlibs: i32, -}; - -// ---- ww build --sep — M3-tail separate-compilation driver ------------ -// -// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path -// materializes each imported package's `.wwi` interface and compiles -// every package on its own (`w6c -c`), then flat-links the `.o` set. -// combined.ww stays the DEFAULT live path; --sep is additive. -// -// Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND -// producer (writes this package's `.wwi` via -I). Reverse-topo order -// guarantees a package's deps' `.wwi` exist before it compiles. -// -// The load-bearing rule (#56, rob-resolved): every dep is tagged by its -// FULL DOTTED import path on prepend (`//ww:module `), so the -// definer's qualified symbol (#53) equals the consumer's qualified -// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE -// closure of a package's deps (lead-ratified): a dep's interface can -// name a transitive dep's type, so the consuming unit needs the whole -// closure for resolution. The unit composition is byte-identical to the -// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`. - -def SEP_MAXPKG: i32 = 256; - -type seppkg = struct { - path: *u8, // dotted import path, NUL-term; root path[0]==0 - entry: *u8, // resolved package dir (or file, file root), NUL-term - isdir: i32, - deps: []i32, // direct-dep indices into sepgraph.pkg - ndeps: i32, - color: i32, // tri-color DFS: 0 white, 1 gray, 2 black -}; - -type sepgraph = struct { - pkg: []seppkg, // alloc'd SEP_MAXPKG - n: i32, -}; - -// Find a package by dotted path, or add it. Returns index, -1 if full. -fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { - let i: i32 = 0; - for (i < g.n) { - if (cstreq(g.pkg[i].path, path)) { return i; }; - i += 1; - }; - if (g.n >= SEP_MAXPKG) { - cerr("ww --sep: too many packages\n"); - return -1; - }; - let plen: u64 = cstrlen(path); - let elen: u64 = cstrlen(entry); - g.pkg[g.n].path = arenadupcstr(path, plen); - g.pkg[g.n].entry = arenadupcstr(entry, elen); - g.pkg[g.n].isdir = isdir; - let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; - dslot.len = SEP_MAXPKG; - g.pkg[g.n].deps = dslot; - g.pkg[g.n].ndeps = 0; - g.pkg[g.n].color = 0; - let r: i32 = g.n; - g.n += 1; - return r; -}; - -// Build "/" NUL-term; base = path, or "__root" -// for the empty root path. -fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; - let off: u64 = cstrinto(buf.ptr, 0u64, scratch); - off = byteinto(buf.ptr, off, 47u8); // '/' - if (g.pkg[pi].path[0u64] != 0u8) { - off = cstrinto(buf.ptr, off, g.pkg[pi].path); - } else { - off = strinto(buf.ptr, off, "__root"); - }; - off = strinto(buf.ptr, off, suffix); - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// Scan one source file for top-level `import IDENT;`. A DIRECTORY import -// is a package boundary: add as a direct dep of pi. A FILE import is an -// intra-package split: fold its imports into pi. Mirrors cstage -// sep_scan_file (collects PATHS, not bytes). -fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, - fv: *expctx) i32 = { - let fview: str; - fview.ptr = file; - fview.len = cstrlen(file): i32; - let fdup: str = strings.dup(fview); - if (visitseen(fv, fdup)) { return 0; }; - visitadd(fv, fdup); - let bufp: *u8; - let blen: u64; - bufp, blen = slurp(file); - if (bufp == nil) { - cerr("ww --sep: cannot read source\n"); - return -1; - }; - let i: u64 = 0u64; - for (i < blen) { - let j: u64 = i; - for (j < blen) { - if (bufp[j] == 10u8) { break; }; // '\n' - j += 1u64; - }; - let idp: *u8; - let idn: u64; - idp, idn = scanuse(bufp + i, j - i); - if (idp != nil) { - let isdir: i32 = 0; - let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); - if (ipath != nil) { - if (isdir != 0) { - let nm: []u8 = alloc([], idn + 1u64)!; - let k: u64 = 0u64; - for (k < idn) { nm[k] = idp[k]; k += 1u64; }; - nm[idn] = 0u8; - let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); - if (di < 0) { return -1; }; - let seen: bool = false; - let m: i32 = 0; - for (m < g.pkg[pi].ndeps) { - if (g.pkg[pi].deps[m] == di) { seen = true; }; - m += 1; - }; - if (!seen) { - if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; }; - g.pkg[pi].deps[g.pkg[pi].ndeps] = di; - g.pkg[pi].ndeps += 1; - }; - } else { - if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) { - return -1; - }; - }; - }; - }; - i = j + 1u64; - }; - return 0; -}; - -// Discover pi's direct deps + recurse. Enumerate the package's own -// files (dir → *.ww less *test.ww; file → the file) and scan each. -// `color` doubles as a scanned-marker (2); reset to white before topo. -fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { - if (g.pkg[pi].color == 2) { return 0; }; - g.pkg[pi].color = 2; - let fv: expctx; - fv.out = -1; - fv.dirs = searchpath; - fv.visit = nil; - let rc: i32 = 0; - if (g.pkg[pi].isdir != 0) { - let names: **u8; - let n: i32; - names, n = enumeratedir(g.pkg[pi].entry); - let dlen: u64 = cstrlen(g.pkg[pi].entry); - let i: i32 = 0; - for (i < n) { - if (rc == 0) { - let nlen: u64 = cstrlen(names[i]); - let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; - let k: u64 = 0u64; - for (k < dlen) { fp[k] = g.pkg[pi].entry[k]; k += 1u64; }; - fp[dlen] = 47u8; // '/' - k = 0u64; - for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; - fp[dlen + 1u64 + nlen] = 0u8; - rc = sepscanfile(g, pi, fp.ptr, searchpath, &fv); - }; - i += 1; - }; - } else { - rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv); - }; - if (rc < 0) { return rc; }; - let k: i32 = 0; - for (k < g.pkg[pi].ndeps) { - if (sepscanpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; }; - k += 1; - }; - return 0; -}; - -// Print one cycle-chain node: a package path, or "(root)" for the -// empty root path. -fn sepcyclenode(p: *u8) void = { - if (p[0] == 0u8) { cerr("(root)"); } else { cerr(pathstr(p)); }; -}; - -// DFS post-order over the dep DAG → reverse-topo (deps before importer). -// Tri-color: a gray back-edge is a loud dep-cycle reject naming the chain -// (Hare deps.ha:243); stack[0..depth) is the live DFS path, so the cycle -// runs from pi's first occurrence on it to the top, closing on pi. -// Cite Hare gather (deps.ha:123). -fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32, - stack: []i32, depth: i32) i32 = { - if (g.pkg[pi].color == 2) { return 0; }; - if (g.pkg[pi].color == 1) { - let j: i32 = 0; - for (j < depth && stack[j] != pi) { j += 1; }; - cerr("ww --sep: dependency cycle: "); - let s: i32 = j; - for (s < depth) { - sepcyclenode(g.pkg[stack[s]].path); - cerr(" -> "); - s += 1; - }; - sepcyclenode(g.pkg[pi].path); - cerr("\n"); - return -1; - }; - g.pkg[pi].color = 1; - stack[depth] = pi; - let k: i32 = 0; - for (k < g.pkg[pi].ndeps) { - if (septopovisit(g, g.pkg[pi].deps[k], order, no, stack, depth + 1) < 0) { - return -1; - }; - k += 1; - }; - g.pkg[pi].color = 2; - order[*no] = pi; - *no += 1; - return 0; -}; - -// Mark pi's transitive deps (excluding pi) in inset[]. -fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { - let k: i32 = 0; - for (k < g.pkg[pi].ndeps) { - let di: i32 = g.pkg[pi].deps[k]; - if (inset[di] == 0u8) { - inset[di] = 1u8; - sepmarkdeps(g, di, inset); - }; - k += 1; - }; -}; - -// Emit one of pi's own source files into the sep-unit under the -// //ww:module-reset primary boundary (so -c emits its decls, imported -// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); -// FILE imports fold in (intra-package split). -fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { - let pview: str; - pview.ptr = path; - pview.len = cstrlen(path): i32; - let pdup: str = strings.dup(pview); - if (visitseen(visit, pdup)) { return; }; - visitadd(visit, pdup); - let bufp: *u8; - let blen: u64; - bufp, blen = slurp(path); - if (bufp == nil) { - cerr("ww --sep: cannot read source\n"); - return; - }; - let i: u64 = 0u64; - for (i < blen) { - let j: u64 = i; - for (j < blen) { - if (bufp[j] == 10u8) { break; }; // '\n' - j += 1u64; - }; - let idp: *u8; - let idn: u64; - idp, idn = scanuse(bufp + i, j - i); - if (idp != nil) { - let isdir: i32 = 0; - let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); - if (ipath != nil) { - if (isdir == 0) { - sepemitbody(fd, ipath, visit, searchpath, modpath); - }; - }; - }; - i = j + 1u64; - }; - // #57: tag the primary body by its full dotted import path so the - // definer mangles == the importer reference; a root build (path "") - // stays a bare reset (keeps bare main). - if (modpath != nil && modpath[0u64] != 0u8) { - let dm: str = "//ww:module-reset "; - os.writeall(fd, dm.ptr, dm.len: u64); - os.writeall(fd, modpath, cstrlen(modpath)); - os.writeall(fd, "\n".ptr, 1u64); - } else { - let d: str = "//ww:module-reset\n"; - os.writeall(fd, d.ptr, d.len: u64); - }; - os.writeall(fd, bufp, blen); - os.writeall(fd, "\n".ptr, 1u64); -}; - -fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8, modpath: *u8) void = { - let names: **u8; - let n: i32; - names, n = enumeratedir(dir); - let dlen: u64 = cstrlen(dir); - let i: i32 = 0; - for (i < n) { - let nlen: u64 = cstrlen(names[i]); - let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; - let k: u64 = 0u64; - for (k < dlen) { fp[k] = dir[k]; k += 1u64; }; - fp[dlen] = 47u8; // '/' - k = 0u64; - for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; - fp[dlen + 1u64 + nlen] = 0u8; - sepemitbody(fd, fp.ptr, visit, searchpath, modpath); - i += 1; - }; -}; - -// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s -// (reverse-topo order, each tagged by its dotted path), then pi's own -// body under //ww:module-reset. -fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, - norder: i32, searchpath: *u8, unitf: *u8) i32 = { - let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (u < 0) { - cerr("ww --sep: cannot open unit\n"); - return -1; - }; - let inset: []u8 = alloc([], g.n: u64)!; - inset.len = g.n; - let z: i32 = 0; - for (z < g.n) { inset[z] = 0u8; z += 1; }; - sepmarkdeps(g, pi, inset); - let oi: i32 = 0; - for (oi < norder) { - let dj: i32 = order[oi]; - if (dj != pi) { - if (inset[dj] != 0u8) { - let wwi: *u8 = sepfname(g, dj, scratch, ".wwi"); - let wb: *u8; - let wn: u64; - wb, wn = slurp(wwi); - if (wb == nil) { - cerr("ww --sep: missing wwi\n"); - os.close(u); - return -1; - }; - let dm: str = "//ww:module "; - os.writeall(u, dm.ptr, dm.len: u64); - os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path)); - os.writeall(u, "\n".ptr, 1u64); - os.writeall(u, wb, wn); - os.writeall(u, "\n".ptr, 1u64); - }; - }; - oi += 1; - }; - let bv: expctx; - bv.out = u; - bv.dirs = searchpath; - bv.visit = nil; - if (g.pkg[pi].isdir != 0) { - sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); - } else { - sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); - }; - os.close(u); - return 0; -}; - -// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic -// single-member SysV ar archive at `apath` wrapping the `.o` at -// `objpath`. No armap / long-name table: w6l reads each member's ELF -// .symtab directly and skips '/'-named members, so a package `.a` is -// just the global magic + one 60-byte member header + the `.o` bytes -// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed -// member name make the bytes a pure function of the `.o` content → -// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b -// cache key. -fn archiveo(objpath: *u8, apath: *u8) i32 = { - let objp: *u8; - let objn: u64; - objp, objn = slurp(objpath); - if (objp == nil) { - cerr("ww --sep: cannot read object for archive\n"); - return -1; - }; - let pad: u64 = 0u64; - if ((objn & 1u64) != 0u64) { pad = 1u64; }; - // sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT - // constants, not type sizes (CLAUDE.md rule 13 carve-out). - let total: u64 = 8u64 + 60u64 + objn + pad; - let outs: []u8 = alloc([], total)!; - let out: *u8 = outs.ptr; - - // 60-byte member header at offset 8, ASCII space-filled, fields - // left-justified; the 8-byte global magic precedes it. strinto - // copies a str's bytes (the working i32-index idiom) — a direct - // `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm. - let h: u64 = 8u64; - let j: u64 = 0u64; - for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill - strinto(out, 0u64, "!\n"); // global magic - strinto(out, h, "pkg.o/"); // name (GNU '/' terminator) - out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism) - out[h + 28u64] = 48u8; // uid "0" - out[h + 34u64] = 48u8; // gid "0" - strinto(out, h + 40u64, "100644"); // mode (fixed octal) - // size: decimal byte-count of the .o, left-justified at [48..58) - if (objn == 0u64) { - out[h + 48u64] = 48u8; - } else { - let ndig: u64 = 0u64; - let t: u64 = objn; - for (t > 0u64) { ndig += 1u64; t = t / 10u64; }; - let d: u64 = ndig; - t = objn; - for (t > 0u64) { - d -= 1u64; - out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8; - t = t / 10u64; - }; - }; - out[h + 58u64] = 96u8; // member-header magic 0x60 - out[h + 59u64] = 10u8; // 0x0a - - // the .o bytes, then a '\n' pad iff the size is odd (2-byte align). - let k: u64 = 0u64; - for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; }; - if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; }; - - let fd: i32 = os.open(pathstr(apath), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { - cerr("ww --sep: cannot open archive\n"); - return -1; - }; - os.writeall(fd, out, total); - os.close(fd); - return 0; -}; - -// ---- 5b content-keyed package cache (#63) ---------------------------- -// -// Twin of cstage's pkgcache_root/md5_file/sep_manifest/cache_lookup/ -// cache_store (cmd/ww/main.c). A per-package cache under WW_PKGCACHE -// (default out/.pkgcache; gitignored, make clean wipes $(OUT)). Dev-inner- -// loop convenience only: every gate cold-compiles, so the cache changes no -// gate output. rule-10: md5 is NOT reimplemented here — both stages shell to -// the host md5sum, so a HIT's reused P.wwi/P.o stay byte-identical cs==ww. - -fn pkgcacheroot() *u8 = { - match (os.getenv("WW_PKGCACHE")) { - case let s: str => - if (s.len > 0) { - let buf: []u8 = alloc([], (s.len: u64) + 1u64)!; - let i: i32 = 0; - for (i < s.len) { buf[i] = s[i]; i += 1; }; - buf[s.len] = 0u8; - return buf.ptr; - }; - case void => void; - }; - let d: []u8 = alloc([], 64u64)!; - d.len = 64; - let o: u64 = strinto(d.ptr, 0u64, "out/.pkgcache"); - cstrseal(d.ptr, o); - return d.ptr; -}; - -fn pkgcachedir(cacheroot: *u8, g: *sepgraph, pi: i32) *u8 = { - if (g.pkg[pi].path[0u64] == 0u8) { - return joinpathlit(cacheroot, "__root"); - }; - return joinpathlit(cacheroot, pathstr(g.pkg[pi].path)); -}; - -// md5appendhex — host md5sum of `path`, captured via a shell redirect to -// `tmp` (cache is gate-cold → host-tool dep sanctioned, rule-7). Appends the -// hex digest to `dst` at `off`; returns the new offset, or 0 on failure -// (caller treats 0 as a non-cacheable miss). Mirrors cstage md5_file's -// copy-until-whitespace parse so the digest text is byte-identical. -fn md5appendhex(dst: *u8, off: u64, path: *u8, tmp: *u8) u64 = { - let cmd: []u8 = alloc([], 8192u64)!; - cmd.len = 8192; - let co: u64 = strinto(cmd.ptr, 0u64, "md5sum '"); - co = cstrinto(cmd.ptr, co, path); - co = strinto(cmd.ptr, co, "' > '"); - co = cstrinto(cmd.ptr, co, tmp); - co = strinto(cmd.ptr, co, "'"); - cstrseal(cmd.ptr, co); - let argv: []*u8 = alloc([], 4u64)!; - argv.len = 4; - argv[0] = "sh\0".ptr; - argv[1] = "-c\0".ptr; - argv[2] = cmd.ptr; - argv[3] = nil; - if (procrun("/bin/sh\0".ptr, argv.ptr) != 0) { return 0u64; }; - let (buf, n) = slurp(tmp); - if (buf == nil) { return 0u64; }; - let i: u64 = 0u64; - for (i < n) { - let c: u8 = buf[i]; - if (c == 32u8 || c == 9u8 || c == 10u8) { break; }; - dst[off + i] = c; - i += 1u64; - }; - if (i == 0u64) { return 0u64; }; - return off + i; -}; - -// sepmanifest — assemble package pi's content-key manifest (D4) into `out` -// as deterministic text (see cstage sep_manifest). Returns the manifest -// length, or 0 on any md5 failure (caller → cold compile). The md5 capture -// tmp lives in `scratch` (which exists), not the cache dir (created lazily). -fn sepmanifest(g: *sepgraph, pi: i32, scratch: *u8, c6: *u8, a6: *u8, - out: *u8) u64 = { - let tmp: *u8 = sepfname(g, pi, scratch, ".md5tmp"); - let off: u64 = strinto(out, 0u64, "src"); - if (g.pkg[pi].isdir != 0) { - let (names, n) = enumeratedir(g.pkg[pi].entry); - let i: i32 = 0; - for (i < n) { - let fp: []u8 = alloc([], os.PATH_MAX: u64)!; - fp.len = os.PATH_MAX; - let fo: u64 = cstrinto(fp.ptr, 0u64, g.pkg[pi].entry); - fo = byteinto(fp.ptr, fo, 47u8); // '/' - fo = cstrinto(fp.ptr, fo, names[i]); - cstrseal(fp.ptr, fo); - off = byteinto(out, off, 32u8); // ' ' - let no: u64 = md5appendhex(out, off, fp.ptr, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - i += 1; - }; - } else { - off = byteinto(out, off, 32u8); - let no: u64 = md5appendhex(out, off, g.pkg[pi].entry, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - }; - off = byteinto(out, off, 10u8); // '\n' - - // dep lines, sorted by dep path (insertion sort, mirrors enumeratedir). - let nd: i32 = g.pkg[pi].ndeps; - let idx: []i32 = alloc([], SEP_MAXPKG: u64)!; - idx.len = SEP_MAXPKG; - let i: i32 = 0; - for (i < nd) { idx[i] = g.pkg[pi].deps[i]; i += 1; }; - i = 1; - for (i < nd) { - let j: i32 = i; - for (j > 0) { - let a: *u8 = g.pkg[idx[j - 1]].path; - let b: *u8 = g.pkg[idx[j]].path; - if (bytecmp(a, cstrlen(a), b, cstrlen(b)) <= 0) { j = 0; } - else { - let t: i32 = idx[j]; - idx[j] = idx[j - 1]; - idx[j - 1] = t; - j -= 1; - }; - }; - i += 1; - }; - i = 0; - for (i < nd) { - let di: i32 = idx[i]; - let wwip: *u8 = sepfname(g, di, scratch, ".wwi"); - off = strinto(out, off, "dep "); - off = cstrinto(out, off, g.pkg[di].path); - off = byteinto(out, off, 32u8); - let no: u64 = md5appendhex(out, off, wwip, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - i += 1; - }; - - off = strinto(out, off, "w6c "); - let no: u64 = md5appendhex(out, off, c6, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - off = strinto(out, off, "w6a "); - no = md5appendhex(out, off, a6, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - off = strinto(out, off, "flags -c -I\n"); - return off; -}; - -// copyfile — byte-copy src→dst (TRUNC). Returns 0 on success, -1 on failure. -fn copyfile(src: *u8, dst: *u8) i32 = { - let (buf, n) = slurp(src); - if (buf == nil) { return -1; }; - let fd: i32 = os.open(pathstr(dst), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { return -1; }; - os.writeall(fd, buf, n); - os.close(fd); - return 0; -}; - -// cachelookup — HIT iff the manifest equals the stored P.key byte-for-byte -// AND both cached artifacts exist; on HIT copy them into the scratch -// wwi/objf so the producer loop can skip compose+w6c+w6a. -fn cachelookup(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8, - mlen: u64, wwi: *u8, objf: *u8) i32 = { - let dir: *u8 = pkgcachedir(cacheroot, g, pi); - let keyp: *u8 = joinpathlit(dir, "P.key"); - let cwwi: *u8 = joinpathlit(dir, "P.wwi"); - let cobj: *u8 = joinpathlit(dir, "P.o"); - let (stored, sn) = slurp(keyp); - if (stored == nil) { return 0; }; - if (sn != mlen) { return 0; }; - let i: u64 = 0u64; - for (i < mlen) { - if (stored[i] != manifest[i]) { return 0; }; - i += 1u64; - }; - if (os.access(pathstr(cwwi), 0i32) != 0) { return 0; }; - if (os.access(pathstr(cobj), 0i32) != 0) { return 0; }; - if (copyfile(cwwi, wwi) != 0) { return 0; }; - if (copyfile(cobj, objf) != 0) { return 0; }; - return 1; -}; - -// cachestore — on MISS persist the artifacts then the key (key LAST: a crash -// mid-store never leaves a key without its artifacts; the next run re-misses). -fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8, - mlen: u64, wwi: *u8, objf: *u8) void = { - let dir: *u8 = pkgcachedir(cacheroot, g, pi); - match (os.mkdirs(pathstr(dir), 493i32)) { // 0o755 - case void => void; - case let e: os.oserror => void; // best-effort; copyfile surfaces a real failure - }; - let cwwi: *u8 = joinpathlit(dir, "P.wwi"); - let cobj: *u8 = joinpathlit(dir, "P.o"); - let keyp: *u8 = joinpathlit(dir, "P.key"); - if (copyfile(wwi, cwwi) != 0) { return; }; - if (copyfile(objf, cobj) != 0) { return; }; - let fd: i32 = os.open(pathstr(keyp), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { return; }; - os.writeall(fd, manifest, mlen); - os.close(fd); -}; - -// buildonesep — the --sep orchestration: discover deps, reverse-topo, -// the transitive producer loop (one `w6c -c -I` per package, dep-first, -// each `.o` wrapped in its own deterministic per-package `.a`), then a -// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a -// cold `.sepwork` scratch dir. Twin of cstage build_one_sep. -fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, - objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = { - let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); - let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); - let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); - - // Default lib search path: /../../lib - let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!; - dotdotlib.len = os.PATH_MAX; - { - let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir); - off = strinto(dotdotlib.ptr, off, "/../../lib"); - cstrseal(dotdotlib.ptr, off); - }; - - // Source directory (mirrors buildone). - let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!; - srcd.len = os.PATH_MAX; - if (entryisdir != 0) { - let slen: u64 = cstrlen(src); - let k: u64 = 0u64; - for (k < slen) { srcd[k] = src[k]; k += 1u64; }; - for (slen > 1u64) { - if (srcd[slen - 1u64] != 47u8) { break; }; - slen -= 1u64; - }; - srcd[slen] = 0u8; - } else { - let slen: u64 = cstrlen(src); - let last: u64 = slen; - let found: bool = false; - let i: u64 = slen; - for (i > 0u64) { - i -= 1u64; - if (src[i] == 47u8) { last = i; found = true; i = 0u64; }; - }; - if (found) { - let k: u64 = 0u64; - for (k < last) { srcd[k] = src[k]; k += 1u64; }; - srcd[last] = 0u8; - } else { - srcd[0] = 46u8; // '.' - srcd[1] = 0u8; - }; - }; - - // searchpath = srcd + ':' + incs + ':' + dotdotlib. - let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!; - searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32; - { - let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr); - off = byteinto(searchpath.ptr, off, 58u8); // ':' - if (incs[0u64] != 0u8) { - off = cstrinto(searchpath.ptr, off, incs); - off = byteinto(searchpath.ptr, off, 58u8); - }; - off = cstrinto(searchpath.ptr, off, dotdotlib.ptr); - cstrseal(searchpath.ptr, off); - }; - - // Stem for the scratch dir (mirrors buildone). - let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; - stem.len = os.PATH_MAX; - if (entryisdir != 0) { - let dlen: u64 = cstrlen(srcd.ptr); - let bo: u64 = basenameoff(srcd.ptr, dlen); - let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr); - stem[off] = 47u8; off += 1u64; // '/' - let i: u64 = bo; - for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; }; - cstrseal(stem.ptr, off); - } else { - makestem(stem.ptr, src); - }; - let effstem: *u8 = stem.ptr; - if (objstem != nil) { effstem = objstem; }; - let scratch: *u8 = appendlit(effstem, ".sepwork"); - - // rm -rf scratch (cold); recreate. Reuse os.removeall if present; - // here we mkdir and rely on TRUNC opens to overwrite stale files. - os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd) - - // libwwrt.a path: /../lib/libwwrt.a - let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; - libwwrt.len = os.PATH_MAX; - { - let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir); - off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a"); - cstrseal(libwwrt.ptr, off); - }; - - // Discover. - let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!; - pkgslot.len = SEP_MAXPKG; - let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; - let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir); - if (root < 0) { return 1; }; - // #79 (-T): lib/test is the synth main's `test.run` callee but @test - // files never `import test;`. Inject it as a direct dep of the root so - // sepscanpkg pulls test + its transitive deps; the producer adds -T to - // the root and `test.run` links against test's `.a`. Mirrors the - // combined path's auto-bundle (buildone istest) and sepscanfile dedup. - if (istest != 0) { - let td: i32 = 0; - let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); - if (tp != nil) { - let ti: i32 = sepfindoradd(g, "test\0".ptr, tp, td); - if (ti < 0) { return 1; }; - let seen: bool = false; - let m: i32 = 0; - for (m < g.pkg[root].ndeps) { - if (g.pkg[root].deps[m] == ti) { seen = true; }; - m += 1; - }; - if (!seen) { - if (g.pkg[root].ndeps < SEP_MAXPKG) { - g.pkg[root].deps[g.pkg[root].ndeps] = ti; - g.pkg[root].ndeps += 1; - }; - }; - }; - }; - if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; }; - - // Reset colors, reverse-topo. - let ci: i32 = 0; - for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; - let order: []i32 = alloc([], g.n: u64)!; - order.len = g.n; - let stack: []i32 = alloc([], g.n: u64)!; - stack.len = g.n; - let norder: i32 = 0; - if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; }; - - // Producer loop — dep-first, one `w6c -c -I` per package. - let cacheroot: *u8 = pkgcacheroot(); - let oi: i32 = 0; - for (oi < norder) { - let pi: i32 = order[oi]; - let unitf: *u8 = sepfname(g, pi, scratch, ".unit.ww"); - let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); - let asmf: *u8 = sepfname(g, pi, scratch, ".s"); - let objf: *u8 = sepfname(g, pi, scratch, ".o"); - // 5b: skip compose+w6c+w6a on a content-key HIT. The root is - // never cached — it is the build target, always recompiled. - let manbuf: []u8 = alloc([], 16384u64)!; - manbuf.len = 16384; - let mlen: u64 = 0u64; - let cacheable: i32 = 0; - if (pi != root) { - mlen = sepmanifest(g, pi, scratch, c6, a6, manbuf.ptr); - if (mlen > 0u64) { cacheable = 1; }; - }; - let fresh: i32 = 0; - if (cacheable != 0) { - fresh = cachelookup(cacheroot, g, pi, manbuf.ptr, mlen, wwi, objf); - }; - if (fresh == 0) { - if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { - return 1; - }; - { - // BUG-1 (#69): -I is purely the root's UNUSED - // `.wwi` output path, but it triggers wwiemit -> - // checkexportedtype on the root. A terminal binary's - // root legitimately has `export fn` over an unexported - // LOCAL type (the root is never imported), which the - // export-check rejects. Build a shorter root argv - // without the -I/wwi pair; root's `.wwi` is unconsumed. - // #79: the root carries -T under `ww test --sep` so w6c - // synthesizes the test main; deps never get -T. - let roott: bool = (pi == root) && (istest != 0); - let alen: u64 = 8u64; - if (pi == root) { alen = 6u64; if (roott) { alen = 7u64; }; }; - let argv: []*u8 = alloc([], alen)!; - argv.len = (alen: i32); - argv[0] = "w6c\0".ptr; - let k: u64 = 1u64; - if (roott) { argv[k] = "-T\0".ptr; k += 1u64; }; - argv[k] = "-c\0".ptr; k += 1u64; - if (pi != root) { - argv[k] = "-I\0".ptr; k += 1u64; - argv[k] = wwi; k += 1u64; - }; - argv[k] = "-o\0".ptr; k += 1u64; - argv[k] = asmf; k += 1u64; - argv[k] = unitf; k += 1u64; - argv[k] = nil; - if (procrun(c6, argv.ptr) != 0) { - cerr("ww --sep: w6c failed\n"); - return 1; - }; - }; - { - let argv: []*u8 = alloc([], 5u64)!; - argv.len = 5; - argv[0] = "w6a\0".ptr; - argv[1] = "-o\0".ptr; - argv[2] = objf; - argv[3] = asmf; - argv[4] = nil; - if (procrun(a6, argv.ptr) != 0) { - cerr("ww --sep: w6a failed\n"); - return 1; - }; - }; - if (cacheable != 0) { - cachestore(cacheroot, g, pi, manbuf.ptr, mlen, wwi, objf); - }; - }; - // Wrap each DEP package's `.o` in its own deterministic `.a` - // (5a). The ROOT stays a positional `.o` (force-loaded — it's - // the build target), so `main` is defined before any archive is - // processed (mirrors buildone's root treatment). The link - // consumes `.o`/`.a`, never `.wwi`. - if (pi != root) { - let apath: *u8 = sepfname(g, pi, scratch, ".a"); - if (archiveo(objf, apath) != 0) { - cerr("ww --sep: archive failed\n"); - return 1; - }; - }; - oi += 1; - }; - - // Reverse-topo link of the per-package `.a` set: root.a first - // (order[norder-1]), deps after, then libwwrt.a (which still - // selectively pulls only the runtime members a live undef needs). - // argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs - // + 2*nlibs + 1 nil. - let nldirs: i32 = 0; - let nllibs: i32 = 0; - let ldirs: **u8 = nil; - let llibs: **u8 = nil; - if (lf != nil) { - nldirs = lf.nlibdirs; - nllibs = lf.nlibs; - ldirs = lf.libdirs; - llibs = lf.libs; - }; - let total: i32 = 3 + norder + 1 + 2 * nldirs + 2 * nllibs + 1; - let largv: []*u8 = alloc([], total: u64)!; - largv.len = total; - largv[0] = "w6l\0".ptr; - largv[1] = "-o\0".ptr; - largv[2] = out; - let pos: i32 = 3; - let li: i32 = norder - 1; - for (li >= 0) { - // root: positional `.o` (force-load); deps: `.a` (selective). - let suf: str = ".a"; - if (order[li] == root) { suf = ".o"; }; - largv[pos] = sepfname(g, order[li], scratch, suf); - pos += 1; - li -= 1; - }; - largv[pos] = libwwrt.ptr; pos += 1; - let k: i32 = 0; - for (k < nldirs) { - largv[pos] = "-L\0".ptr; - largv[pos + 1] = ldirs[k]; - pos += 2; - k += 1; - }; - k = 0; - for (k < nllibs) { - largv[pos] = "-l\0".ptr; - largv[pos + 1] = llibs[k]; - pos += 2; - k += 1; - }; - largv[pos] = nil; - if (procrun(l6, largv.ptr) != 0) { - cerr("ww --sep: w6l failed\n"); - return 1; - }; - return 0; -}; - -// buildone — compile `src` (file or directory) into the executable -// named `out`. -// selfdir: NUL-terminated dir containing this driver and the -// wwstage tools (w6c_ww/w6a_ww/w6l_ww) -// src: NUL-terminated entry path (file or directory). -// entryisdir: non-zero when src is a module directory. -// out: NUL-terminated desired output path -// objstem: when non-nil, redirects the .s/.o/.combined.ww side -// files to live beside this stem instead of next to the -// source (T3 / task #15). `ww run` and `ww build -o` pass -// it so concurrent builds never share next-to-source fixed -// paths; nil keeps the old next-to-source layout (the make -// tracked-combined.ww regen + #110 gate depend on it). Twin -// of cmd/ww/main.c build_one's objstem. -// incs: NUL-terminated colon-list of -I dirs (may be empty) -// lf: extra linker flags (-L, -l); may be nil -// -// The ww-side driver shells to the ww-side tools so a `ww_ww build` -// touches no C-built code at runtime. The C `ww` driver in cmd/ww/ -// still drives the C-built w6c/w6a/w6l. Test 993 pins the two -// pipelines to byte-identical output on a corpus. -fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, istest: i32) i32 = { - let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); - let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); - let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); - - // Default lib search path: /../../lib - let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!; - dotdotlib.len = os.PATH_MAX; - { - let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir); - off = strinto(dotdotlib.ptr, off, "/../../lib"); - cstrseal(dotdotlib.ptr, off); - }; - - // Compute the source directory. For a file entry: bytes of `src` - // up to the last '/' (or "." when src has no '/'). For a dir - // entry: the dir itself (less trailing slashes). Hare's CWD-first - // convention assumes you're running from the module dir; our - // wrappers don't cd, so dirname(src) stands in as the closest - // analog. Source-dir wins ties over the system path (cc -I.). - let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!; - srcd.len = os.PATH_MAX; - if (entryisdir != 0) { - let slen: u64 = cstrlen(src); - let k: u64 = 0u64; - for (k < slen) { srcd[k] = src[k]; k += 1u64; }; - for (slen > 1u64) { - if (srcd[slen - 1u64] != 47u8) { break; }; - slen -= 1u64; - }; - srcd[slen] = 0u8; - } else { - let slen: u64 = cstrlen(src); - let last: u64 = slen; - let found: bool = false; - let i: u64 = slen; - for (i > 0u64) { - i -= 1u64; - if (src[i] == 47u8) { // '/' - last = i; - found = true; - i = 0u64; - }; - }; - if (found) { - let k: u64 = 0u64; - for (k < last) { srcd[k] = src[k]; k += 1u64; }; - srcd[last] = 0u8; - } else { - srcd[0] = 46u8; // '.' - srcd[1] = 0u8; - }; - }; - - // Compose searchpath: srcd + ':' + incs + ':' + dotdotlib. - let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!; - searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32; - { - let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr); - off = byteinto(searchpath.ptr, off, 58u8); // ':' - if (incs[0u64] != 0u8) { - off = cstrinto(searchpath.ptr, off, incs); - off = byteinto(searchpath.ptr, off, 58u8); // ':' - }; - off = cstrinto(searchpath.ptr, off, dotdotlib.ptr); - cstrseal(searchpath.ptr, off); - }; - - // Stem for .s/.o/.combined.ww side files. Dir entry: /; - // file entry: src stripped of .ww. - let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; - stem.len = os.PATH_MAX; - if (entryisdir != 0) { - let dlen: u64 = cstrlen(srcd.ptr); - let bo: u64 = basenameoff(srcd.ptr, dlen); - let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr); - stem[off] = 47u8; off += 1u64; // '/' - let i: u64 = bo; - for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; }; - cstrseal(stem.ptr, off); - } else { - makestem(stem.ptr, src); - }; - let effstem: *u8 = stem.ptr; - if (objstem != nil) { effstem = objstem; }; - let asmf: *u8 = appendlit(effstem, ".s"); - let objf: *u8 = appendlit(effstem, ".o"); - let combined: *u8 = appendlit(effstem, ".combined.ww"); - - // libwwrt.a path: /../lib/libwwrt.a - let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; - libwwrt.len = os.PATH_MAX; - { - let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir); - off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a"); - cstrseal(libwwrt.ptr, off); - }; - - // Step 1: expand imports into the combined file. Dir entry → - // enumerate the module dir; file entry → start at the file. - let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (cf < 0) { - cerr("ww: cannot open combined\n"); - return 1; - }; - { - let c: expctx; - c.out = cf; - c.dirs = searchpath.ptr; - c.visit = nil; - // #17 auto-bundle lib/test: the -T synth's main calls lib/test's - // run(), but @test files don't `import test;`. Pull it like an - // implicit import through the same locate+expand path (the visit - // set dedupes a fixture that imports it explicitly). cstage twin - // in cmd/ww/main.c buildone. - if (istest != 0) { - let td: i32 = 0; - let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); - if (tp != nil) { - if (td != 0) { expanddir(&c, tp, "test"); } - else { expand(&c, tp, ""); }; - }; - }; - if (entryisdir != 0) { expanddir(&c, srcd.ptr, ""); } - else { expand(&c, src, ""); }; - }; - os.close(cf); - - // Step 2: w6c [-T] -o .s .combined.ww - { - let argv: []*u8 = alloc([], 6u64)!; - let p: i32 = 0; - argv[p] = "w6c\0".ptr; p += 1; - if (istest != 0) { argv[p] = "-T\0".ptr; p += 1; }; - argv[p] = "-o\0".ptr; p += 1; - argv[p] = asmf; p += 1; - argv[p] = combined; p += 1; - argv[p] = nil; p += 1; - argv.len = p; - if (procrun(c6, argv.ptr) != 0) { - cerr("ww: w6c failed\n"); - return 1; - }; - }; - - // Step 3: w6a -o .o .s - { - let argv: []*u8 = alloc([], 5u64)!; - argv.len = 5; - argv[0] = "w6a\0".ptr; - argv[1] = "-o\0".ptr; - argv[2] = objf; - argv[3] = asmf; - argv[4] = nil; - if (procrun(a6, argv.ptr) != 0) { - cerr("ww: w6a failed\n"); - return 1; - }; - }; - - // Step 4: w6l -o .o libwwrt.a [-L...] [-l...] - { - let nldirs: i32 = 0; - let nllibs: i32 = 0; - let ldirs: **u8 = nil; - let llibs: **u8 = nil; - if (lf != nil) { - nldirs = lf.nlibdirs; - nllibs = lf.nlibs; - ldirs = lf.libdirs; - llibs = lf.libs; - }; - // argv slots: 5 fixed (w6l, -o, out, objf, libwwrt) - // + 2 * nlibdirs (-L, dir) - // + 2 * nlibs (-l, name) - // + 1 nil terminator. - let total: i32 = 5 + 2 * nldirs + 2 * nllibs + 1; - let argv: []*u8 = alloc([], total: u64)!; - argv.len = total; - argv[0] = "w6l\0".ptr; - argv[1] = "-o\0".ptr; - argv[2] = out; - argv[3] = objf; - argv[4] = libwwrt.ptr; - let pos: i32 = 5; - let k: i32 = 0; - for (k < nldirs) { - argv[pos] = "-L\0".ptr; - argv[pos + 1] = ldirs[k]; - pos += 2; - k += 1; - }; - k = 0; - for (k < nllibs) { - argv[pos] = "-l\0".ptr; - argv[pos + 1] = llibs[k]; - pos += 2; - k += 1; - }; - argv[pos] = nil; - if (procrun(l6, argv.ptr) != 0) { - cerr("ww: w6l failed\n"); - return 1; - }; - }; - return 0; -}; - -// ---- Module-by-name resolution ---------------------------------------- -// -// Mirrors cmd/ww/main.c:resolvemodule. Maps a name like "foo", "lib/foo", -// "foo.ww", or "." to a concrete .ww file path: -// 1. literal .ww that exists → use as-is -// 2. "." → /.ww → that, if it exists -// 3. /.ww → that, if it exists -// 4. walk search path (cwd:incs:/../../lib): -// /.ww or //.ww - -fn cstrendswithlit(p: *u8, lit: str) bool = { - return strings.hassuffix(pathstr(p), lit); -}; - -// basenameoff — return the offset of the last path segment within `p` -// (i.e. one past the final '/'). Returns 0 if there's no slash. -fn basenameoff(p: *u8, plen: u64) u64 = { - let start: u64 = 0u64; - let i: u64 = 0u64; - for (i < plen) { - if (p[i] == '/') { start = i + 1u64; }; - i += 1u64; - }; - return start; -}; - -// arenadupcstr — copy `plen` bytes from `src` into a fresh NUL-sealed -// heap buffer. -fn arenadupcstr(src: *u8, plen: u64) *u8 = { - let buf: []u8 = alloc([], plen + 1u64)!; - let i: u64 = 0u64; - for (i < plen) { buf[i] = src[i]; i += 1u64; }; - buf[plen] = 0u8; - return buf.ptr; -}; - -// buildsearchpath — compose the colon-separated lookup path used by -// resolvemodule's case (4). Order: "." : : /../../lib -fn buildsearchpath(selfdir: *u8, incs: *u8) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; - let off: u64 = 0u64; - buf[off] = 46u8; off += 1u64; // '.' - if (incs != nil) { - if (incs[0u64] != 0u8) { - buf[off] = 58u8; off += 1u64; // ':' - off = cstrinto(buf.ptr, off, incs); - }; - }; - buf[off] = 58u8; off += 1u64; - off = cstrinto(buf.ptr, off, selfdir); - off = strinto(buf.ptr, off, "/../../lib"); - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// resolvemodule — map a name like "foo", "lib/foo", "foo.ww", or -// "." to a concrete entry path. Sets *isdir when the entry is a -// module directory (caller will dir-enumerate). -fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = { - let nlen: u64 = cstrlen(name); - - // (1) Literal file that exists → use as-is. - if (cstrendswithlit(name, ".ww")) { - if (os.access(pathstr(name), 0i32) == 0) { - *isdir = 0; - return arenadupcstr(name, nlen); - }; - }; - - // (2) Existing path → use as-is, dir vs file via stat. - let fi: os.filestat; - let sr: (void | os.oserror) = os.stat(&fi, pathstr(name)); - let found: bool = false; - let foundisdir: i32 = 0; - match (sr) { - case void => { - let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT - if (t == os.mode.DIR: u32) { foundisdir = 1; }; - found = true; - }; - case let e: os.oserror => void; - }; - if (found) { - *isdir = foundisdir; - return arenadupcstr(name, nlen); - }; - - // (3) Search-path lookup with dot-to-slash path translation. - let search: *u8 = buildsearchpath(selfdir, incs); - return locateimport(search, name, nlen, isdir); -}; - -// ---- Subcommand handlers ---------------------------------------------- - -fn writeusage(fd: i32) void = { - let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [path] compile module to a static binary (path defaults to cwd)\n run [path] ... build then exec, passing extra args to the program\n test [path] build and run *_test.ww in the module (path defaults to cwd)\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's .ww\n"; - os.write(fd, s.ptr, s.len: u64); -}; - -fn doversion() i32 = { - os.write(1, "ww 0.0\n".ptr, 7u64); - return 0; -}; - -// Compute the basename of src (without trailing ".ww") into a fresh -// buffer. Used as the default output path for `ww build`. -fn defaultoutpath(src: *u8) *u8 = { - let n: u64 = cstrlen(src); - let start: u64 = 0u64; - let i: u64 = 0u64; - for (i < n) { - if (src[i] == 47u8) { start = i + 1u64; }; // '/' - i += 1u64; - }; - let out: []u8 = alloc([], (os.PATH_MAX: u64))!; - out.len = os.PATH_MAX; - let off: u64 = 0u64; - let j: u64 = start; - for (j < n) { - out[off] = src[j]; - off += 1u64; - j += 1u64; - }; - // Strip ".ww" if present. - if (off >= 3u64) { - if (out[off - 3u64] == 46u8) { - if (out[off - 2u64] == 119u8) { - if (out[off - 1u64] == 119u8) { - off -= 3u64; - }; - }; - }; - }; - cstrseal(out.ptr, off); - return out.ptr; -}; - -fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { - let src: *u8 = nil; - let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path - let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 - let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; - incs.len = ((os.PATH_MAX: u64) * 2u64): i32; - let incoff: u64 = 0u64; - cstrseal(incs.ptr, 0u64); - - let maxlflags: i32 = 32; - let libdirs: []*u8 = alloc([], maxlflags: u64)!; - libdirs.len = maxlflags; - let nlibdirs: i32 = 0; - let libs: []*u8 = alloc([], maxlflags: u64)!; - libs.len = maxlflags; - let nlibs: i32 = 0; - - let i: i32 = start; - for (i < argc) { - let p: *u8 = argv[i]; - if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "--sep")) { // M3-tail separate-compile - wantsep = 1; - } else { if (p[1u64] == 73u8) { // '-I' - let dir: *u8 = nil; - if (p[2u64] != 0u8) { - dir = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww build: -I needs an argument\n"); - return 2; - }; - i += 1; - dir = argv[i]; - }; - if (incoff > 0u64) { - incs[incoff] = 58u8; // ':' - incoff += 1u64; - }; - incoff = cstrinto(incs.ptr, incoff, dir); - cstrseal(incs.ptr, incoff); - } else { if (p[1u64] == 76u8) { // '-L' - let dir: *u8 = nil; - if (p[2u64] != 0u8) { - dir = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww build: -L needs an argument\n"); - return 2; - }; - i += 1; - dir = argv[i]; - }; - if (nlibdirs >= maxlflags) { - cerr("ww build: too many -L\n"); - return 2; - }; - libdirs[nlibdirs] = dir; - nlibdirs += 1; - } else { if (p[1u64] == 108u8) { // '-l' - let nm: *u8 = nil; - if (p[2u64] != 0u8) { - nm = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww build: -l needs an argument\n"); - return 2; - }; - i += 1; - nm = argv[i]; - }; - if (nlibs >= maxlflags) { - cerr("ww build: too many -l\n"); - return 2; - }; - libs[nlibs] = nm; - nlibs += 1; - } else { if (p[1u64] == 111u8) { // '-o' - if (p[2u64] != 0u8) { - outflag = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww build: -o needs an argument\n"); - return 2; - }; - i += 1; - outflag = argv[i]; - }; - } else { - cerr("ww build: unknown flag\n"); - return 2; - }; }; }; }; }; - } else { - if (src == nil) { src = p; }; - }; - i += 1; - }; - - if (src == nil) { - // default to cwd module - let dot: [2]u8 = ['.': u8, 0u8]; - src = &dot[0]; - }; - let isdir: i32 = 0; - let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); - if (resolved == nil) { - cerr("ww build: cannot find module\n"); - return 1; - }; - let out: *u8 = nil; - let objstem: *u8 = nil; - if (outflag != nil) { - // -o sets both the binary path and the intermediate stem so - // artifacts land beside the requested output (T3). - out = outflag; - objstem = outflag; - } else { if (isdir != 0) { - let rlen: u64 = cstrlen(resolved); - for (rlen > 1u64) { - if (resolved[rlen - 1u64] != 47u8) { break; }; - rlen -= 1u64; - }; - let bo: u64 = basenameoff(resolved, rlen); - let outbuf: []u8 = alloc([], (os.PATH_MAX: u64))!; - outbuf.len = os.PATH_MAX; - out = outbuf.ptr; - let i: u64 = bo; - let off: u64 = 0u64; - for (i < rlen) { out[off] = resolved[i]; off += 1u64; i += 1u64; }; - cstrseal(out, off); - } else { - out = defaultoutpath(resolved); - }; }; - let lf: lflags; - lf.libdirs = libdirs.ptr; - lf.nlibdirs = nlibdirs; - lf.libs = libs.ptr; - lf.nlibs = nlibs; - if (wantsep != 0) { - return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); - }; - return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); -}; - -// Format the scratch path /tmp/ww_run_ into buf. Returns NUL- -// terminated buf. Pid is folded in decimal manually since we don't -// import strconv. -fn makeruntmp(buf: *u8) void = { - let off: u64 = 0u64; - off = strinto(buf, off, "/tmp/ww_run_"); - let pid: i32 = os.getpid(); - // itoa for non-negative pid - let dig: [16]u8; - let n: i32 = 0; - if (pid <= 0) { - dig[n] = 48u8; // '0' - n += 1; - } else { - let v: i32 = pid; - for (v > 0) { - dig[n] = ((v % 10) + 48): u8; - n += 1; - v = v / 10; - }; - }; - let k: i32 = n - 1; - for (k >= 0) { - buf[off] = dig[k]; - off += 1u64; - k -= 1; - }; - cstrseal(buf, off); -}; - -fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { - let src: *u8 = nil; - let passstart: i32 = -1; // first argv idx to pass through to program - let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; - incs.len = ((os.PATH_MAX: u64) * 2u64): i32; - let incoff: u64 = 0u64; - cstrseal(incs.ptr, 0u64); - - let maxlflags: i32 = 32; - let libdirs: []*u8 = alloc([], maxlflags: u64)!; - libdirs.len = maxlflags; - let nlibdirs: i32 = 0; - let libs: []*u8 = alloc([], maxlflags: u64)!; - libs.len = maxlflags; - let nlibs: i32 = 0; - - let i: i32 = start; - for (i < argc) { - if (passstart >= 0) { i = argc; } // stop, leave rest for exec - else { - let p: *u8 = argv[i]; - if (p[0u64] == 45u8) { - if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10) - cerr("ww run: --sep is only valid with build\n"); - return 2; - } else { if (p[1u64] == 73u8) { - let dir: *u8 = nil; - if (p[2u64] != 0u8) { - dir = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww run: -I needs an argument\n"); - return 2; - }; - i += 1; - dir = argv[i]; - }; - if (incoff > 0u64) { - incs[incoff] = 58u8; - incoff += 1u64; - }; - incoff = cstrinto(incs.ptr, incoff, dir); - cstrseal(incs.ptr, incoff); - } else { if (p[1u64] == 76u8) { - let dir: *u8 = nil; - if (p[2u64] != 0u8) { - dir = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww run: -L needs an argument\n"); - return 2; - }; - i += 1; - dir = argv[i]; - }; - if (nlibdirs >= maxlflags) { - cerr("ww run: too many -L\n"); - return 2; - }; - libdirs[nlibdirs] = dir; - nlibdirs += 1; - } else { if (p[1u64] == 108u8) { - let nm: *u8 = nil; - if (p[2u64] != 0u8) { - nm = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww run: -l needs an argument\n"); - return 2; - }; - i += 1; - nm = argv[i]; - }; - if (nlibs >= maxlflags) { - cerr("ww run: too many -l\n"); - return 2; - }; - libs[nlibs] = nm; - nlibs += 1; - } else { if (p[1u64] == 111u8) { // '-o' - // run always execs the temp binary; -o is accepted+ - // ignored, mirroring the C driver's shared flag parser. - if (p[2u64] == 0u8) { - if (i + 1 >= argc) { - cerr("ww run: -o needs an argument\n"); - return 2; - }; - i += 1; - }; - } else { - cerr("ww run: unknown flag\n"); - return 2; - }; }; }; }; }; - i += 1; - } else { - if (src == nil) { - src = p; - i += 1; - } else { - passstart = i; // remaining args go to the program - }; - }; - }; - }; - - if (src == nil) { - let dot: [2]u8 = ['.': u8, 0u8]; - src = &dot[0]; - }; - let isdir: i32 = 0; - let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); - if (resolved == nil) { - cerr("ww run: cannot find module\n"); - return 1; - }; - - let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; - tmp.len = os.PATH_MAX; - makeruntmp(tmp.ptr); - let lf: lflags; - lf.libdirs = libdirs.ptr; - lf.nlibdirs = nlibdirs; - lf.libs = libs.ptr; - lf.nlibs = nlibs; - // objstem = tmp → intermediates at /tmp/ww_run_.{s,o,combined.ww}, - // never next to the source (T3). - if (buildone(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) { - os.remove(pathstr(tmp.ptr)); - return 1; - }; - - // exec with [tmp, argv[passstart..argc), nil] - let nextra: i32 = 0; - if (passstart >= 0) { nextra = argc - passstart; }; - let total: i32 = nextra + 2; - let execargv: []*u8 = alloc([], total: u64)!; - execargv.len = total; - execargv[0] = tmp.ptr; - let k: i32 = 0; - for (k < nextra) { - execargv[k + 1] = argv[passstart + k]; - k += 1; - }; - execargv[nextra + 1] = nil; - let rc: i32 = procrun(tmp.ptr, execargv.ptr); - os.remove(pathstr(tmp.ptr)); - return rc; -}; - -// ---- ww test ---------------------------------------------------------- -// -// Mirrors cmd/ww/main.c:dotest. Two modes: -// single-file: build+run a literal *.ww file, return its exit code -// directory: open the dir, getdents64, build+run each *_test.ww, -// report ok/FAIL per file, return 0 iff all pass. - -fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8, usesep: i32) i32 = { - let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; - tmp.len = os.PATH_MAX; - // -o redirects the binary + its combined (objstem, T3) to ; the - // default temp keeps the combined next to the source as before. - let outp: *u8 = nil; - let objstem: *u8 = nil; - if (outstem != nil) { - outp = outstem; - objstem = outstem; - } else { - makeruntmp(tmp.ptr); - outp = tmp.ptr; - }; - // #79 E1: --sep routes the test build through the separate-compilation - // producer (buildonesep, istest=1) instead of the amalgamator. - let bres: i32 = 0; - if (usesep != 0) { - let lf: lflags; - lf.libdirs = nil; - lf.nlibdirs = 0; - lf.libs = nil; - lf.nlibs = 0; - bres = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32); - } else { - bres = buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32); - }; - if (bres != 0) { - if (outstem == nil) { os.remove(pathstr(outp)); }; - return 1; - }; - if (compileonly != 0) { return 0; }; - // #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run() - // reads it via os.args. procrun execve's a NUL-terminated argv, so the - // terminator (not .len) bounds the vector. cstage twin: run_test_bin. - let execargv: []*u8 = alloc([], 3u64)!; - execargv[0] = outp; - if (pattern != nil) { - execargv.len = 3; - execargv[1] = pattern; - execargv[2] = nil; - } else { - execargv.len = 2; - execargv[1] = nil; - }; - let rc: i32 = procrun(outp, execargv.ptr); - if (outstem == nil) { os.remove(pathstr(outp)); }; - return rc; -}; - -fn rundirtests(selfdir: *u8, dir: *u8) i32 = { - let fd: i32 = os.open(pathstr(dir), os.flag.RDONLY, 0i32); - if (fd < 0) { - cerr("ww test: cannot open directory\n"); - return 1; - }; - - let pass: i32 = 0; - let fail: i32 = 0; - let seen: i32 = 0; // #64: count *_test.ww matches for the zero-test gate - let buf: []u8 = alloc([], 8192u64)!; - buf.len = 8192; - let dirlen: u64 = cstrlen(dir); - - let n: i64 = os.getdents64(fd, buf.ptr, 8192u64); - for (n > 0i64) { - let off: u64 = 0u64; - let nu: u64 = n: u64; - for (off < nu) { - // d_reclen at offset+16 (u16 LE), d_name at offset+19 (cstr) - let blo: u64 = (buf[off + 16u64]): u64; - let bhi: u64 = (buf[off + 17u64]): u64; - let reclen: u64 = blo + (bhi * 256u64); - let name: *u8 = buf.ptr + off + 19u64; - if (cstrendswithlit(name, "_test.ww")) { - seen += 1; - let nlen: u64 = cstrlen(name); - // path = / - let path: []u8 = alloc([], (os.PATH_MAX: u64))!; - path.len = os.PATH_MAX; - let poff: u64 = cstrinto(path.ptr, 0u64, dir); - path[poff] = 47u8; poff += 1u64; - let i: u64 = 0u64; - for (i < nlen) { path[poff + i] = name[i]; i += 1u64; }; - poff += nlen; - cstrseal(path.ptr, poff); - // incs = so test files can `use ` siblings - let tincs: []u8 = alloc([], (os.PATH_MAX: u64))!; - tincs.len = os.PATH_MAX; - let ic: u64 = cstrinto(tincs.ptr, 0u64, dir); - cstrseal(tincs.ptr, ic); - let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; - tmp.len = os.PATH_MAX; - makeruntmp(tmp.ptr); - let bres: i32 = buildone(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32); - if (bres != 0) { - fail += 1; - cerr("FAIL "); - os.write(2, name, nlen); - cerr(" (build)\n"); - } else { - let execargv: []*u8 = alloc([], 2u64)!; - execargv.len = 2; - execargv[0] = tmp.ptr; - execargv[1] = nil; - let rc: i32 = procrun(tmp.ptr, execargv.ptr); - if (rc == 0) { - pass += 1; - os.write(1, "ok ".ptr, 5u64); - os.write(1, name, nlen); - os.write(1, "\n".ptr, 1u64); - } else { - fail += 1; - cerr("FAIL "); - os.write(2, name, nlen); - cerr("\n"); - }; - }; - os.remove(pathstr(tmp.ptr)); - }; - off += reclen; - }; - n = os.getdents64(fd, buf.ptr, 8192u64); - }; - os.close(fd); - // #64: a directory with no *_test.ww files is a loud failure, not a - // silent rc=0 false-green. Mirrors cstage do_test (cmd/ww/main.c:945) - // "ww test: no *_test.ww files in %s". - if (seen == 0) { - cerr("ww test: no *_test.ww files in "); - os.write(2, dir, dirlen); - cerr("\n"); - return 1; - }; - if (fail == 0) { return 0; }; - return 1; -}; - -fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { - // -I parsing mirrors dobuild/dorun so a single-file test can resolve - // transitive imports (e.g. 905_nkname asttest → tok); coupled to the - // -T flip (task #5/#10). - let target: *u8 = nil; - // #17: optional 2nd positional = fnmatch name-filter pattern, forwarded - // to the test binary as argv[1] (single-file/module only; dir-mode - // rejects). cstage twin: do_test `pattern`. - let patarg: *u8 = nil; - let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; - incs.len = ((os.PATH_MAX: u64) * 2u64): i32; - let incoff: u64 = 0u64; - cstrseal(incs.ptr, 0u64); - - // -c (compile-only) + -o : build the test binary + its lib/test- - // inclusive combined WITHOUT running it, for the byte-id gates. cstage - // twin: cmd/ww/main.c do_test (error wording identical). - let compileonly: i32 = 0; - let outstem: *u8 = nil; - // #79 E1: --sep routes a single-file test through the separate- - // compilation producer (buildonesep, istest=1). Dir-mode --sep is - // deferred to E2. cstage twin: do_test use_sep. - let usesep: i32 = 0; - let i: i32 = start; - for (i < argc) { - let p: *u8 = argv[i]; - if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "--sep")) { // #79: separate-compile test - usesep = 1; - } else { if (p[1u64] == 73u8) { // '-I' - let dir: *u8 = nil; - if (p[2u64] != 0u8) { - dir = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww test: -I needs an argument\n"); - return 2; - }; - i += 1; - dir = argv[i]; - }; - if (incoff > 0u64) { - incs[incoff] = 58u8; // ':' - incoff += 1u64; - }; - incoff = cstrinto(incs.ptr, incoff, dir); - cstrseal(incs.ptr, incoff); - } else { if (p[1u64] == 99u8 && p[2u64] == 0u8) { // "-c" - compileonly = 1; - } else { if (p[1u64] == 111u8) { // '-o' - if (p[2u64] != 0u8) { - outstem = p + 2u64; - } else { - if (i + 1 >= argc) { - cerr("ww test: -o needs an argument\n"); - return 2; - }; - i += 1; - outstem = argv[i]; - }; - } else { - cerr("ww test: unknown flag\n"); - return 2; - }; }; }; }; // #79: extra close for the --sep else - } else { - if (target == nil) { target = p; } - else { if (patarg == nil) { patarg = p; }; }; - }; - i += 1; - }; - if (target == nil) { - let dot: [2]u8 = ['.': u8, 0u8]; - target = &dot[0]; - }; - - // single-file mode: literal *.ww that exists - if (cstrendswithlit(target, ".ww")) { - if (os.access(pathstr(target), 0i32) == 0) { - return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg, usesep); - }; - }; - - if (compileonly != 0 || outstem != nil) { - cerr("ww test: -c/-o need a single test file\n"); - return 2; - }; - // #79 E1: dir-mode --sep deferred to E2; single-file proves the path. - if (usesep != 0) { - cerr("ww test: --sep needs a single test file\n"); - return 2; - }; - // #17: a name-filter pattern is per-binary; dir mode builds one binary - // per *_test.ww, so a single pattern can't route. cstage twin parity. - if (patarg != nil) { - cerr("ww test: pattern needs a single test file\n"); - return 2; - }; - // otherwise treat target as a directory; enumerate *_test.ww - return rundirtests(selfdir, target); -}; - -// ---- Entry ------------------------------------------------------------- - -export fn main(argc: i32, argv: **u8) i32 = { - if (argc < 1) { - writeusage(2); - return 2; - }; - - // selfdir = dirname(argv[0]) - let selfdir: []u8 = alloc([], (os.PATH_MAX: u64))!; - selfdir.len = os.PATH_MAX; - selfdirinto(selfdir.ptr, (os.PATH_MAX: u64), argv[0]); - - if (argc < 2) { - writeusage(2); - return 2; - }; - let cmd: *u8 = argv[1]; - if (cstreqlit(cmd, "-V")) { return doversion(); }; - if (cstreqlit(cmd, "version")) { return doversion(); }; - if (cstreqlit(cmd, "-h")) { - writeusage(1); - return 0; - }; - if (cstreqlit(cmd, "--help")) { - writeusage(1); - return 0; - }; - if (cstreqlit(cmd, "build")) { - return dobuild(selfdir.ptr, argv, argc, 2); - }; - if (cstreqlit(cmd, "run")) { - return dorun(selfdir.ptr, argv, argc, 2); - }; - if (cstreqlit(cmd, "test")) { - return dotest(selfdir.ptr, argv, argc, 2); - }; - cerr("ww: unknown subcommand\n"); - writeusage(2); - return 2; -}; - diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index bc396813..eee5e374 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -195,7 +195,7 @@ type strnode = struct { }; type expctx = struct { - out: i32, // fd we're writing the combined source to + out: i32, // fd we're writing the sep unit source to dirs: *u8, // ":"-separated search path (NUL-terminated) visit: *strnode, }; @@ -326,9 +326,9 @@ fn locateimport(dirs: *u8, name: *u8, namelen: u64, }; // Filter for dir enumeration: keep `*.ww` minus `*test.ww` and the -// `*.combined.ww` driver-generated concat artifacts (the previous -// build leaves them in the source tree; they parse-error when -// re-included). Returns true to keep. +// `*.combined.ww` legacy amalgamator artifacts (pre-#87): they +// parse-error when re-included, and the sep driver no longer writes +// them. Returns true to keep. fn dirfilekeep(name: *u8, nlen: u64) bool = { // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but // would pass strings.hassuffix(".ww"); preserves cstage parity. @@ -372,7 +372,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { if (fd < 0) { return nil: **u8, 0; }; // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, // cmd/ww/main.c:209). The old fixed 256-name cap silently dropped every - // eligible file past it, diverging the combined.ww from cstage on a + // eligible file past it, diverging the package unit from cstage on a // module dir with >256 sources. let cap: i32 = 8; let names: []*u8 = alloc([], cap: u64)!; @@ -542,11 +542,10 @@ fn appendlit(stem: *u8, suffix: str) *u8 = { return buf.ptr; }; -// linker flags (-L, -l) grouped as one struct so buildone's +// linker flags (-L, -l) grouped as one struct so buildonesep's // param list stays readable. (The earlier note here claimed a 6-argument // wwstage calling-convention cap; that is stale — emittuplerowrelocs in -// cgen.ww takes 7 params and self-compiles green, and buildone itself now -// takes 7.) +// cgen.ww takes 7 params and self-compiles green.) type lflags = struct { libdirs: **u8, nlibdirs: i32, @@ -559,7 +558,7 @@ type lflags = struct { // Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path // materializes each imported package's `.wwi` interface and compiles // every package on its own (`w6c -c`), then flat-links the `.o` set. -// combined.ww stays the DEFAULT live path; --sep is additive. +// Separate compilation is the SOLE build path (E3-C1 flip, task #87). // // Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND // producer (writes this package's `.wwi` via -I). Reverse-topo order @@ -638,7 +637,7 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { // genuinely-missing import from one satisfied by an INLINE package in the // same single-file multi-package unit (`package aa; ... package main; // import aa;`). Scans EVERY line (comment-skip) — not just the first -// package decl (peekpackage stops there). Decision byte-identical to +// package decl. Decision byte-identical to // cstage so the skip/fatal choice + driver output match (rule 10). fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = { let p: u64 = 0u64; @@ -756,10 +755,10 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, // when the package is defined INLINE in the same unit // (single-file multi-package). leaf = last dotted // component; inline `package ` -> skip (the - // checker binds it), else fatal. E3-C1: the combined - // path that owned the genuine-missing case is gone, so - // the sep producer enforces it here (INV-2). cstage - // twin; fatal text identical. + // checker binds it), else fatal. E3-C1 (#87): the + // legacy amalgamator that owned the genuine-missing + // case is gone, so the sep producer enforces it here + // (INV-2). cstage twin; fatal text identical. let lstart: u64 = 0u64; let lk: u64 = 0u64; for (lk < idn) { @@ -1377,7 +1376,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, cstrseal(dotdotlib.ptr, off); }; - // Source directory (mirrors buildone). + // Source directory. let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!; srcd.len = os.PATH_MAX; if (entryisdir != 0) { @@ -1422,7 +1421,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, cstrseal(searchpath.ptr, off); }; - // Stem for the scratch dir (mirrors buildone). + // Stem for the scratch dir. let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; stem.len = os.PATH_MAX; if (entryisdir != 0) { @@ -1462,8 +1461,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // #79 (-T): lib/test is the synth main's `test.run` callee but @test // files never `import test;`. Inject it as a direct dep of the root so // sepscanpkg pulls test + its transitive deps; the producer adds -T to - // the root and `test.run` links against test's `.a`. Mirrors the - // combined path's auto-bundle (buildone istest) and sepscanfile dedup. + // the root and `test.run` links against test's `.a` — via the + // sepscanfile dedup-guarded dep append. if (istest != 0) { let td: i32 = 0; let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); @@ -1575,7 +1574,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // Wrap each DEP package's `.o` in its own deterministic `.a` // (5a). The ROOT stays a positional `.o` (force-loaded — it's // the build target), so `main` is defined before any archive is - // processed (mirrors buildone's root treatment). The link + // processed. The link // consumes `.o`/`.a`, never `.wwi`. if (pi != root) { let apath: *u8 = sepfname(g, pi, scratch, ".a"); @@ -2069,7 +2068,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; - // objstem = tmp → intermediates at /tmp/ww_run_.{s,o,combined.ww}, + // objstem = tmp → intermediates under /tmp/ww_run_.sepwork/, // never next to the source (T3). if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) { os.remove(pathstr(tmp.ptr)); @@ -2104,8 +2103,8 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; - // -o redirects the binary + its combined (objstem, T3) to ; the - // default temp keeps the combined next to the source as before. + // -o redirects the binary + its sepwork intermediates (objstem, T3) to + // ; the default temp keeps them next to the source as before. let outp: *u8 = nil; let objstem: *u8 = nil; if (outstem != nil) { @@ -2248,7 +2247,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cstrseal(incs.ptr, 0u64); // -c (compile-only) + -o : build the test binary + its lib/test- - // inclusive combined WITHOUT running it, for the byte-id gates. cstage + // inclusive sep unit WITHOUT running it, for the byte-id gates. cstage // twin: cmd/ww/main.c do_test (error wording identical). let compileonly: i32 = 0; let outstem: *u8 = nil; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww deleted file mode 100644 index a15e69f8..00000000 --- a/selfhost/cmd/wwdump/main.combined.ww +++ /dev/null @@ -1,45692 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module strconv -// 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; -}; - -//ww:module math -// 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); -}; - -//ww:module math -// 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; -}; - -//ww:module strconv -// 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; -}; - -//ww:module strconv -// 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, -]; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module ascii -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -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); -}; - -//ww:module strconv -// 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) -}; - -//ww:module strconv -// 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], -]; - -//ww:module strconv -// strconv — number↔string conversions. -// -// Mirrors Hare's strconv:: surface. The *tos functions return a -// `const str` view into a module-level buffer that is overwritten on -// the next call to the same function; callers must copy the bytes if -// they need to outlive the next invocation. See [[strings.dup]] to -// duplicate. Matches Hare's strconv::*tos semantics. - -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(""); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -import os; -import strconv; - -// ---- 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. -export 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 ------------------------------------------------------------- - -export 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 - usepath: str, // on an N_USE: full dotted IMPORT path vs leaf alias - // in `str` (M1 #22); "" otherwise - imported: i32, // M1 #22: decl reached via `//ww:module ` import - // boundary (vs root/primary); gates root-only bare main -}; - -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="", usepath="", imported=0})!; - 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); -}; - -//ww:module syntax -// lib/ww/syntax/decl.ww — declaration parsing, split out of parse.ww. - -package syntax; - -import os; -import strings; - -// `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; - // M1 #22: accumulate the full dotted import path (n.usepath) for the - // checker's path-keyed module match; n.str stays the leaf alias the - // user writes (`utf8.x`). - let leaf: str; - expectident(p, &leaf); - let path: str = leaf; - for (p.curkind == tkind.TK_DOT) { - advance(p); // past `.` - expectident(p, &leaf); - path = strings.concat(path, ".", leaf); - }; - n.str = leaf; - n.usepath = path; - 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); - // A value-LESS `def X: T;` is an interface prototype (an aggregate- - // init `.wwi` def whose DATA lives in the defining package — BUG-2 / - // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; - // the checker fold pass and cgen emit_defs/emit_lets already guard - // the rhs==nil shape. - if (accepttok(p, tkind.TK_ASSIGN)) { - 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; -}; - - -//ww:module syntax -// lib/ww/syntax/expr.ww — expression parsing, split out of parse.ww. - -package syntax; - -import os; - -// 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; - // Parse each element BEFORE the RPAREN-break so `(a,)` - // (a single elem + trailing comma) is a loud parse error; - // a trailing comma is legal only after >=2 elems. Mirror - // cstage cmd/wcc/parse.c:552-558 loop order. - for (true) { - let en: *node = parseexpr(p); - tail.next = en; - tail = en; - if (!accepttok(p, tkind.TK_COMMA)) { break; }; - if (p.curkind == tkind.TK_RPAREN) { 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; -}; - -// #76: flatten a qualified `pkg.Type` N_DOT chain into its source-order -// dotted string ("a.b.C"), byte-identical to cstage parsetype's aprintf -// accumulation (cmd/wcc/parse.c) and to flattendotstr in parse.c. -fn flattendotstr(n: *node) str = { - if (n.kind == nkind.N_IDENT) { return n.str; }; - return joindotted(flattendotstr(n.lhs), n.str); -}; - -// #76: an N_DOT chain qualifies as a type path only when it is rooted at -// a bare IDENT and every component is an identifier. cstage folds only -// peek==TK_IDENT dots in parseprimary, so its chain is inherently -// all-ident; ww folds ALL dots (incl tuple `t.0`) in parsepostfix, so we -// guard out numeric-first-byte components here to restore exact symmetry. -fn isqualpath(n: *node) bool = { - if (n.kind != nkind.N_DOT) { return false; }; - let cur: *node = n; - for (cur.kind == nkind.N_DOT) { - if (cur.str.len < 1) { return false; }; - let c0: u8 = cur.str[0]; - if (c0 >= '0') { if (c0 <= '9') { return false; }; }; - cur = cur.lhs; - }; - return cur.kind == nkind.N_IDENT; -}; - -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; - }; - // #76: qualified struct literal `pkg.Type{...}`. cstage folds - // the dotted chain in parseprimary (peek) and hits `{` there; - // the ww parser has no token-peek and folds dots HERE, so the - // struct-lit handoff lives here too. Fire only on an all-ident - // N_DOT chain (isqualpath); bare `Foo{` stays in parseprimary. - // Build the literal IN-LOOP and `continue` (req a) so trailing - // ops (`.f`, `[i]`, `()`, `as T`) still apply, matching cstage's - // return-to-outer-parsepostfix. The flattened N_TNAME (req c: - // source order) lets the checker's existing N_TNAME arm resolve. - if (p.curkind == tkind.TK_LBRACE) { - if (isqualpath(cur)) { - let tn: *node = newnode(nkind.N_TNAME, pf, pl, pc); - tn.str = flattendotstr(cur); - advance(p); - let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); - s.lhs = tn; - let head: *node = nil; - let tail: *node = nil; - for (p.curkind != tkind.TK_RBRACE) { - if (p.curkind == tkind.TK_EOF) { break; }; - 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; - cur = s; - 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; -}; - - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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; -}; - -export 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, - // a `//ww:module ` directive was seen in the last skipped run; - // lexnext emits TK_MODPATH carrying this dotted path (M1 #22). - modpathset: i32, - modpath: str, - // a `//ww:module-reset ` directive was seen; the next TK_MODRESET - // carries this dotted path so the sep primary body mangles on the path, - // not its leaf clause (#57). - modresetpathset: i32, - modresetpath: str, -}; - -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; - l.modpathset = 0; - l.modresetpathset = 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 pre: str = "ww:module"; - let di: i32 = 0; - let matched: bool = true; - for (di < pre.len) { - if (lpeek(l, di: u64) != pre[di]: i32) { - matched = false; break; - }; - di += 1; - }; - if (matched) { - let nx: i32 = lpeek(l, pre.len: u64); - if (nx == '-') { - let rest: str = "-reset"; - let rj: i32 = 0; - let rm: bool = true; - for (rj < rest.len) { - if (lpeek(l, (pre.len + rj): u64) - != rest[rj]: i32) { - rm = false; break; - }; - rj += 1; - }; - if (rm) { - let af: i32 = lpeek(l, - (pre.len + rest.len): u64); - if (af == '\n') { l.modreset = 1; } - else { if (af < 0) { l.modreset = 1; } - else { if (af == ' ' || af == '\t') { - // `//ww:module-reset ` — sep - // primary body tagged by its full - // dotted import path (#57). - let k: u64 = - (pre.len + rest.len): u64; - for (true) { - let sc: i32 = lpeek(l, k); - if (sc == ' ' || sc == '\t') { - k += 1u64; continue; - }; - break; - }; - let s0: u64 = k; - for (true) { - let pc: i32 = lpeek(l, k); - if (pc < 0) { break; }; - if (pc == '\n' || pc == '\r' - || pc == ' ' - || pc == '\t') { - break; - }; - k += 1u64; - }; - l.modreset = 1; - if (k > s0) { - let view: str; - view.ptr = - l.src + l.lpos + s0; - view.len = (k - s0): i32; - l.modresetpath = - strings.dup(view); - l.modresetpathset = 1; - }; - }; }; }; - }; - } else { if (nx == ' ' || nx == '\t') { - // `//ww:module ` — M1 import boundary. - let k: u64 = pre.len: u64; - for (true) { - let sc: i32 = lpeek(l, k); - if (sc == ' ' || sc == '\t') { - k += 1u64; continue; - }; - break; - }; - let s0: u64 = k; - for (true) { - let pc: i32 = lpeek(l, k); - if (pc < 0) { break; }; - if (pc == '\n' || pc == '\r' - || pc == ' ' || pc == '\t') { - break; - }; - k += 1u64; - }; - if (k > s0) { - let view: str; - view.ptr = l.src + l.lpos + s0; - view.len = (k - s0): i32; - l.modpath = strings.dup(view); - l.modpathset = 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); - // path-carrying reset → text=path (#57); bare reset → text empty - if (l.modresetpathset != 0) { - l.modresetpathset = 0; - out.text = l.modresetpath; - }; - return; - }; - if (l.modpathset != 0) { - l.modpathset = 0; - emitsimple(&start, tkind.TK_MODPATH, out); - out.text = l.modpath; - 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); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -// Sibling files (expr, stmt, decl) are the same `package syntax`, -// auto-resolved via task #22 dir-enum of lib/ww/syntax/. -import os; -import strings; - -export 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, - // M1 #22: active `//ww:module ` dotted import path. While set, - // decls stamp nmod=pathmod and imported=1, and the in-file `package` - // clause is an assertion. "" means inactive (root/primary). - pathmod: str, - // #57: active `//ww:module-reset ` dotted path. Mangles decls on - // the path WITHOUT imported=1 (primary-ness for -c and the #32 bare-main - // rule stay intact), and the in-file `package` clause asserts (leaf == - // last component) instead of overwriting curmod. "" means inactive. - resetmod: 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; - p.pathmod = ""; - p.resetmod = ""; - 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"); - if (p.pathmod.len != 0 || p.resetmod.len != 0) { - // M1 #22: while an import path is active the in-file - // `package` clause is an ASSERTION — its leaf must - // equal the path's last component; it does NOT - // overwrite the path-derived module. #57 extends this - // to the sep primary-reset path (resetmod): the dotted - // reset path is authoritative, the clause asserts. - let active: str = p.pathmod; - if (p.pathmod.len == 0) { active = p.resetmod; }; - let (pre, post) = strings.rcut(active, "."); - let last: str = post; - if (post.len == 0) { last = active; }; - if (strings.compare(name, last) != 0) { - errmsg(p, "package does not match import path"); - }; - } else { - p.curmod = name; - }; - continue; - }; - // `//ww:module ` — M1 #22 import boundary. The following - // file's decls mangle on the full dotted import path, not the - // leaf `package` clause, and are flagged imported (gates the - // root-only bare-`main` rule, #32). - if (p.curkind == tkind.TK_MODPATH) { - p.pathmod = p.curtext; - p.curmod = p.curtext; - p.resetmod = ""; - advance(p); - 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) { - // #57: a path-carrying reset (sep primary body) mangles decls - // on the dotted path so definer == importer, but leaves - // imported==0 (curmod set, pathmod "") so -c primary-ness and - // the #32 bare-main rule are intact; the body's `package` - // clause then asserts (resetmod). A bare reset is the - // root/package-less boundary: curmod "" → bare, today's path. - let rp: str = p.curtext; - advance(p); - p.pathmod = ""; - if (rp.len != 0) { - p.curmod = rp; - p.resetmod = rp; - } else { - let empty: str; - empty.ptr = nil; - empty.len = 0; - p.curmod = empty; - p.resetmod = ""; - }; - 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) { - // M1 #22: flag decls reached via an import-path boundary - // (gates the root-only bare-`main` rule, #32). - if (p.pathmod.len != 0) { d.imported = 1; }; - if (head == nil) { - head = d; - tail = d; - } else { - tail.next = d; - tail = d; - }; - }; - }; - f.list = head; - return f; -}; - -//ww:module syntax -// lib/ww/syntax/stmt.ww — statement parsing, split out of parse.ww. - -package syntax; - -import os; - -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; -}; - - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -// Symbol kinds — must stay numerically aligned with cmd/wcc/ww.h Skind. -export 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, -}; - -export 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; - -export 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 (a same-name `import X;` -// SK_USE shadowing a struct X declared in another module), the resolver -// needs the type entry — the struct's mod may differ from the leaf 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; -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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. - -export 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_MODPATH = 88, // `//ww:module ` driver import - // boundary; decls mangle on the path, not the - // leaf `package` clause (M1 #22; cstage twin) - TK_LAST = 89, -}; - -// ---- 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, -}; - -export 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_MODPATH: return "//ww:module"; - - 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'); -}; - -//ww:module syntax -// lib/ww/syntax/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 syntax; - -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. -export 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 ----------------------------------------- - -export type tfield = struct { - name: str, - type_: *tinfo, - offset: u64, - tnext: *tfield, -}; - -export 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). -export type ttupleelem = struct { - type_: *tinfo, - offset: u64, - tnext: *ttupleelem, -}; - -export 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. -export 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 ------------------------------- - -export 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 -}; - -//ww:module-reset -// 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 syntax; -import strconv; - -type checker = struct { - tc: *syntax.tctx, - top: *syntax.scope, - cur: *syntax.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: *syntax.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: *syntax.node, // N_FILE root; used by checkmoduleshadow - // to consult the declaring source's own - // `use` directives. - allococtx: *syntax.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: *syntax.tinfo, n: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.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 = { - syntax.scopedefine(c.top, "void", syntax.skind.SK_TYPE, c.tc.tyvoid, nil); - syntax.scopedefine(c.top, "bool", syntax.skind.SK_TYPE, c.tc.tybool, nil); - syntax.scopedefine(c.top, "rune", syntax.skind.SK_TYPE, c.tc.tyrune, nil); - syntax.scopedefine(c.top, "i8", syntax.skind.SK_TYPE, c.tc.tyi8, nil); - syntax.scopedefine(c.top, "i16", syntax.skind.SK_TYPE, c.tc.tyi16, nil); - syntax.scopedefine(c.top, "i32", syntax.skind.SK_TYPE, c.tc.tyi32, nil); - syntax.scopedefine(c.top, "i64", syntax.skind.SK_TYPE, c.tc.tyi64, nil); - syntax.scopedefine(c.top, "u8", syntax.skind.SK_TYPE, c.tc.tyu8, nil); - syntax.scopedefine(c.top, "u16", syntax.skind.SK_TYPE, c.tc.tyu16, nil); - syntax.scopedefine(c.top, "u32", syntax.skind.SK_TYPE, c.tc.tyu32, nil); - syntax.scopedefine(c.top, "u64", syntax.skind.SK_TYPE, c.tc.tyu64, nil); - syntax.scopedefine(c.top, "int", syntax.skind.SK_TYPE, c.tc.tyint, nil); - syntax.scopedefine(c.top, "uint", syntax.skind.SK_TYPE, c.tc.tyuint, nil); - syntax.scopedefine(c.top, "uintptr", syntax.skind.SK_TYPE, c.tc.tyuintptr, nil); - syntax.scopedefine(c.top, "f32", syntax.skind.SK_TYPE, c.tc.tyf32, nil); - syntax.scopedefine(c.top, "f64", syntax.skind.SK_TYPE, c.tc.tyf64, nil); - syntax.scopedefine(c.top, "str", syntax.skind.SK_TYPE, c.tc.tystr, nil); - syntax.scopedefine(c.top, "never", syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0); - tnvoid.str = "void"; - let bang: *syntax.node = syntax.newnode(syntax.nkind.N_TBANG, empty, 0, 0); - bang.lhs = tnvoid; - let nomemdecl: *syntax.node = syntax.newnode(syntax.nkind.N_TYPEDECL, empty, 0, 0); - nomemdecl.str = "nomem"; - nomemdecl.lhs = bang; - syntax.scopedefine(c.top, "nomem", syntax.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. - syntax.scopedefine(c.top, "len", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "alloc", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "free", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "append", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "delete", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "insert", syntax.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. - syntax.scopedefine(c.top, "size", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "align", syntax.skind.SK_FN, nil, nil); - syntax.scopedefine(c.top, "offset", syntax.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: *syntax.node, d: *syntax.node) str = { - let empty: str; - if (d == nil) { return empty; }; - if (d.nmod.len == 0) { return empty; }; - if (file == nil) { return empty; }; - let u: *syntax.node = file.list; - for (u != nil) { - // M1 #22: a decl is imported iff some `use` directive's full - // dotted import path equals the decl's module (now the path). - // Single-level packages have usepath == leaf so this is - // unchanged; nested (`encoding.utf8`) match here, not on leaf. - if (u.kind == syntax.nkind.N_USE) { - if (syntax.streq(u.usepath, d.nmod)) { return d.nmod; }; - }; - u = u.next; - }; - return empty; -}; - -// usepath — map a `use` alias (leaf bareword the user writes, `utf8`) -// to the full dotted import path it binds (`encoding.utf8`), for the -// module-qualified resolution and codegen hint (M1 #22). Single-level -// packages have usepath == alias so the result is unchanged. The -// current tree has one occurrence per leaf, so the map is unambiguous. -fn usepathfor(file: *syntax.node, alias: str) str = { - let empty: str; - if (file == nil) { return empty; }; - if (alias.len == 0) { return empty; }; - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_USE) { - if (syntax.streq(u.str, alias)) { - if (u.usepath.len != 0) { return u.usepath; }; - return u.str; - }; - }; - u = u.next; - }; - return empty; -}; - -// modkeyfor — usepathfor with leaf-alias fallback: the module key for a -// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22). -fn modkeyfor(c: *checker, alias: str) str = { - let mk: str = usepathfor(c.file, alias); - if (mk.len == 0) { return alias; }; - return mk; -}; - -// 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: *syntax.node, modtag: str, name: str) bool = { - if (file == nil) { return false; }; - if (name.len == 0) { return false; }; - let u: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.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 (syntax.streq(u.nmod, u.usepath)) { - 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 (syntax.streq(um, modtag)) { m = true; }; }; - if (m) { - if (syntax.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: *syntax.scope = c.cur; - for (s != nil) { - let r: *syntax.sym = syntax.scopelookuplocal(s, name); - if (r != nil) { - if (r.skind == syntax.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: *syntax.node, d: *syntax.node) void = { - if (d == nil) { return; }; - let k: syntax.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 == syntax.nkind.N_USE) { - // M1 #22: self-import ⟺ the imported path equals the use's own - // (owning) module path. Compares paths, not leaves. - if (mod.len != 0 && syntax.streq(d.usepath, 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: *syntax.sym = syntax.scopelookuplocal(c.top, nm); - if (prev != nil) { if (prev.skind != syntax.skind.SK_USE) { - prev.use_alias = 1i32; - return; - }; }; - syntax.scopedefine(c.top, nm, syntax.skind.SK_USE, nil, d); return; - }; - if (k == syntax.nkind.N_DEF) { installtop(c, d, nm, mod, syntax.skind.SK_DEF, "def"); return; }; - if (k == syntax.nkind.N_TYPEDECL) { installtop(c, d, nm, mod, syntax.skind.SK_TYPE, "type"); return; }; - if (k == syntax.nkind.N_FNDECL) { installtop(c, d, nm, mod, syntax.skind.SK_FN, "fn"); return; }; - if (k == syntax.nkind.N_LET) { installtop(c, d, nm, mod, syntax.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: *syntax.node, nm: str, mod: str, k: syntax.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: *syntax.sym = syntax.scopelookuplocal(c.top, nm); - if (puse != nil) { if (puse.skind == syntax.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 (syntax.scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; }; - let prev: *syntax.sym = syntax.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: *syntax.node, elems: *syntax.node, - define: bool, what: str) void = { - let b: *syntax.node = binds; - let pt: *syntax.node = elems; - for (b != nil) { - let et: *syntax.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); - syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b); - }; - }; - if (b.type_ == nil) { - let src: *syntax.node = b.lhs; - if (src == nil) { src = et; }; - if (src != nil) { - let ti: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.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 == syntax.nkind.N_MATCH) { checkmatchexhaust(c, n); }; - if (k == syntax.nkind.N_TRYPROP) { checktryprop(c, n); }; - if (k == syntax.nkind.N_TRYUNW) { checktryprop(c, n); }; - if (k == syntax.nkind.N_TYPETEST) { checkisas(c, n); }; - if (k == syntax.nkind.N_TYPEASSERT) { checkisas(c, n); }; - if (k == syntax.nkind.N_LET) { checkletassign(c, n); }; - if (k == syntax.nkind.N_RETURN) { checkretassign(c, n); }; - - // `use IDENT;` — name is a module label, not a free ident. - if (k == syntax.nkind.N_USE) { return; }; - - if (k == syntax.nkind.N_IDENT) { - let nm: str = n.str; - if (nm.len > 0) { - let s: *syntax.sym = syntax.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 == syntax.nkind.N_TNAME) { - let nm: str = n.str; - if (nm.len > 0) { - let s: *syntax.sym = syntax.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: *syntax.sym = syntax.scopelookup(c.cur, head); - if (m != nil) { - let leaf: str; - leaf.ptr = nm.ptr + (dot + 1): u64; - leaf.len = nm.len - (dot + 1); - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 == syntax.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: *syntax.node = nil; - let it: *syntax.node = exprtype(c, n.lhs, nil); - if (it != nil) { - let et: *syntax.node = nil; - if (it.kind == syntax.nkind.N_TSLICE) { et = it.lhs; }; - if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; }; - if (et != nil) { if (et.kind == syntax.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: *syntax.node = nil; - let it: *syntax.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 == syntax.nkind.N_TSLICE) { et = it.lhs; }; - if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; }; - }; - if (et != nil) { - let bn: *syntax.node = syntax.newnode(syntax.nkind.N_LET, n.file, n.line, n.col); - bn.str = bnm; - bn.lhs = et; - syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, bn); - } else { - syntax.scopedefine(c.cur, bnm, syntax.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 == syntax.nkind.N_MCASE) { - if (n.lhs != nil) { resolvewalk(c, n.lhs); }; - let outer: *syntax.scope = c.cur; - c.cur = syntax.newscope(outer); - let nm: str = n.str; - if (nm.len > 0) { - checkmoduleshadow(c, nm, "binding"); - syntax.scopedefine(c.cur, nm, syntax.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 == syntax.nkind.N_BLOCK) { - let outer: *syntax.scope = c.cur; - c.cur = syntax.newscope(outer); - let m: *syntax.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 == syntax.nkind.N_MLET) { - if (n.rhs != nil) { resolvewalk(c, n.rhs); }; - let pt: *syntax.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: *syntax.node = exprtype(c, n.rhs, nil); - if (rty != nil) { if (rty.kind == syntax.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 == syntax.nkind.N_MASSIGN) { - if (n.rhs != nil) { resolvewalk(c, n.rhs); }; - let pt: *syntax.node = nil; - // #242: consume the rhs tuple type for ANY rhs (see N_MLET). - if (n.rhs != nil) { - let rty: *syntax.node = exprtype(c, n.rhs, nil); - if (rty != nil && rty.kind == syntax.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: *syntax.node = n.list; - for (l != nil) { resolvewalk(c, l); l = l.next; }; - stamptuplebinds(c, n.list, pt, false, ""); - return; - }; - - if (k == syntax.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: *syntax.node = exprtype(c, n, nil); - return; - }; - - if (k == syntax.nkind.N_FIELD) { - if (n.lhs != nil) { resolvewalk(c, n.lhs); }; - return; - }; - - if (k == syntax.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: *syntax.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 == syntax.nkind.N_TNAME || k == syntax.nkind.N_TPTR || - k == syntax.nkind.N_TSLICE || k == syntax.nkind.N_TCHAN || - k == syntax.nkind.N_TBANG || k == syntax.nkind.N_TARRAY || - k == syntax.nkind.N_TFN || k == syntax.nkind.N_TSTRUCT || - k == syntax.nkind.N_TTUPLE || k == syntax.nkind.N_TTAGGED || - k == syntax.nkind.N_TENUM) { - if (n.type_ == nil) { - let ti: *syntax.tinfo = tinfofornode(c, n); - if (ti != nil) { n.type_ = ti: *void; }; - }; - }; - - if (k == syntax.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 == syntax.nkind.N_LET) { - let nm: str = n.str; - if (nm.len > 0) { - checkmoduleshadow(c, nm, "let"); - let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); - // catB-22: flag a `const` binding so checkassign can - // reject a later reassignment. The parser stamps - // n.op = TK_CONST (parse/stmt.ww). Mirror cstage - // cmd/wcc/check.c:2408. - if (s != nil) { - if (n.op == syntax.tkind.TK_CONST) { s.is_const = 1i32; }; - }; - }; - }; - - // #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 == syntax.nkind.N_LET) { coercefloatlit(c, n.rhs, n.lhs); }; - if (k == syntax.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 == syntax.nkind.N_CALL) { desugarcallargs(c, n); }; - if (k == syntax.nkind.N_ASSIGN) { checkassign(c, n); }; - - if (k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT || - k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT || - k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE || - k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT || - k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN || - k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL || - k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST || - k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT || - k == syntax.nkind.N_RECV || - k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD || - k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP || - k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST || - k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD || - k == syntax.nkind.N_MATCH) { - let _t: *syntax.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: *syntax.node) *syntax.node = { - if (n == nil) { return nil; }; - if (n.kind == syntax.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: *syntax.node) *syntax.sym = { - if (n == nil) { return nil; }; - if (n.kind != syntax.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: *syntax.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 = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 = syntax.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 != syntax.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: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, nm); - if (sm != nil) { s = sm; }; - }; - }; - }; - if (s == nil) { return nil; }; - if (s.skind != syntax.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: *syntax.node) *syntax.node = { - let cur: *syntax.node = n; - for (cur != nil) { - if (cur.kind != syntax.nkind.N_TNAME) { return cur; }; - let s: *syntax.sym = aliassym(c, cur); - if (s == nil) { return cur; }; - let body: *syntax.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: *syntax.node, b: *syntax.node) bool = { - let aa: *syntax.node = unwrapbang(a); - let bb: *syntax.node = unwrapbang(b); - if (aa == nil) { return bb == nil; }; - if (bb == nil) { return false; }; - // Identity fast-path, mirroring cstage type_eq's first line - // (cmd/wcc/type.c:250 `if (a == b) return 1`). Enum (and struct/ - // array) type nodes are shared from their decl, so two references to - // the SAME `os.flag` resolve to one N_TENUM node; without this the - // catch-all below returns false and the #26 reject fires on a - // same-enum binop like `os.flag.WRONLY | os.flag.CREATE` (w6l), which - // cstage accepts via this identity check. - if (aa == bb) { return true; }; - if (aa.kind != bb.kind) { return false; }; - let k: syntax.nkind = aa.kind; - if (k == syntax.nkind.N_TNAME) { - if (syntax.streq(aa.str, bb.str)) { return true; }; - let sa: *syntax.sym = aliassym(c, aa); - let sb: *syntax.sym = aliassym(c, bb); - if (sa != nil && sa == sb) { return true; }; - return false; - }; - if (k == syntax.nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; - if (k == syntax.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: *syntax.node = aa.list; - let pb: *syntax.node = bb.list; - for (pa != nil) { - if (pb == nil) { return false; }; - let ac: bool = syntax.streq(pa.str, "..."); - let bc: bool = syntax.streq(pb.str, "..."); - if (ac != bc) { return false; }; - if (!ac) { - let va: bool = pa.op == syntax.tkind.TK_ELLIPSIS; - let vb: bool = pb.op == syntax.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 == syntax.nkind.N_TTUPLE) { - let pa: *syntax.node = aa.list; - let pb: *syntax.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 == syntax.nkind.N_TTAGGED) { - let pa: *syntax.node = aa.list; - let pb: *syntax.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 == syntax.tkind.TK_ELLIPSIS) { return false; }; - if (pb.op == syntax.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: *syntax.node) bool = { - if (v == nil) { return false; }; - if (v.kind == syntax.nkind.N_TBANG) { return true; }; - if (v.kind == syntax.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: *syntax.sym = syntax.scopelookup(c.cur, v.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_TYPE) { - if (s.decl != nil) { - if (s.decl.lhs != nil) { - if (s.decl.lhs.kind == syntax.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: *syntax.node) bool = { - let v: *syntax.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: *syntax.node, v: *syntax.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: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind == syntax.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: *syntax.sym = syntax.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 == syntax.nkind.N_DOT) { - if (e.lhs == nil) { return nil; }; - if (e.lhs.kind != syntax.nkind.N_IDENT) { return nil; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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) *syntax.node = { - let n: *syntax.node = syntax.newnode(syntax.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 (syntax.streq(nm, "void")) { return 0i64; }; - if (syntax.streq(nm, "bool")) { return 1i64; }; - if (syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; }; - if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; }; - if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; }; - if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64")) { return 8i64; }; - if (syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.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 (syntax.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: *syntax.node) i64 = { - if (t == nil) { return 1i64; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TBANG) { return astalign(c, t.lhs); }; - if (k == syntax.nkind.N_TPTR) { return 8i64; }; - if (k == syntax.nkind.N_TSLICE) { return 8i64; }; - if (k == syntax.nkind.N_TCHAN) { return 8i64; }; - if (k == syntax.nkind.N_TFN) { return 8i64; }; - if (k == syntax.nkind.N_TARRAY) { return astalign(c, t.lhs); }; - if (k == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.align: i64; }; - return 8i64; - }; - if (k == syntax.nkind.N_TTUPLE) { - let m: i64 = 1i64; - let p: *syntax.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 == syntax.nkind.N_TSTRUCT) { - let m: i64 = 1i64; - let f: *syntax.node = t.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let fa: i64 = astalign(c, f.lhs); - if (fa > m) { m = fa; }; - }; - f = f.next; - }; - return m; - }; - if (k == syntax.nkind.N_TENUM) { - if (t.lhs != nil) { return astalign(c, t.lhs); }; - return 4i64; - }; - if (k == syntax.nkind.N_TNAME) { - let nm: str = t.str; - if (syntax.streq(nm, "void") || syntax.streq(nm, "bool") || syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; }; - if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; }; - if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; }; - if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64") || syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.streq(nm, "size") || syntax.streq(nm, "str")) { return 8i64; }; - let resolved: *syntax.node = resolvealias(c, t); - if (resolved != nil && resolved != t) { - return astalign(c, resolved); - }; - }; - return 1i64; -}; - -fn astsize(c: *checker, t: *syntax.node) i64 = { - if (t == nil) { return 0i64; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TBANG) { return astsize(c, t.lhs); }; - if (k == syntax.nkind.N_TPTR) { return 8i64; }; - if (k == syntax.nkind.N_TSLICE) { return tyslicesize(); }; - if (k == syntax.nkind.N_TCHAN) { return 8i64; }; - if (k == syntax.nkind.N_TFN) { return 8i64; }; - if (k == syntax.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 == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.size: i64; }; - return 0i64; - }; - if (k == syntax.nkind.N_TSTRUCT) { - let off: i64 = 0i64; - let maxal: i64 = 1i64; - let f: *syntax.node = t.list; - for (f != nil) { - if (f.kind == syntax.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 == syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { return ti.size: i64; }; - return 0i64; - }; - if (k == syntax.nkind.N_TENUM) { - if (t.lhs != nil) { return astsize(c, t.lhs); }; - return 4i64; - }; - if (k == syntax.nkind.N_TNAME) { - let nm: str = t.str; - let ps: i64 = primtypesize(nm); - if (ps >= 0i64) { return ps; }; - let resolved: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - let k: syntax.nkind = u.kind; - if (k == syntax.nkind.N_TNAME) { - if (syntax.streq(u.str, "opaque")) { return true; }; - return false; - }; - if (k == syntax.nkind.N_TARRAY) { return astunsized(c, u.lhs); }; - if (k == syntax.nkind.N_TTUPLE) { - let p: *syntax.node = u.list; - for (p != nil) { - if (astunsized(c, p.lhs)) { return true; }; - p = p.next; - }; - return false; - }; - if (k == syntax.nkind.N_TSTRUCT) { - let f: *syntax.node = u.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - if (astunsized(c, f.lhs)) { return true; }; - }; - f = f.next; - }; - return false; - }; - if (k == syntax.nkind.N_TTAGGED) { - let v: *syntax.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: *syntax.node, bname: str, btype: *syntax.node, - nodeout: **syntax.node) *syntax.tinfo = { - if (body == nil) { return nil; }; - let k: syntax.nkind = body.kind; - if (k == syntax.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 == syntax.nkind.N_IDENT && bname.len > 0 - && syntax.streq(body.lhs.str, bname)) { - *nodeout = btype; - }; - return body.lhs.type_: *syntax.tinfo; - }; - let t: *syntax.node = exprtype(c, body.lhs, nil); - if (t != nil) { - *nodeout = t; - return tinfofornode(c, t); - }; - if (body.lhs.kind == syntax.nkind.N_IDENT && bname.len > 0 - && syntax.streq(body.lhs.str, bname)) { - *nodeout = btype; - return tinfofornode(c, btype); - }; - return nil; - }; - if (k == syntax.nkind.N_MATCH) { return nil; }; - if (k == syntax.nkind.N_BLOCK) { - let s: *syntax.node = body.list; - for (s != nil) { - let t: *syntax.tinfo = matchyieldtype(c, s, bname, btype, nodeout); - if (t != nil) { return t; }; - s = s.next; - }; - return nil; - }; - if (k == syntax.nkind.N_IF) { - let t: *syntax.tinfo = matchyieldtype(c, body.body, bname, btype, nodeout); - if (t != nil) { return t; }; - return matchyieldtype(c, body.els, bname, btype, nodeout); - }; - if (k == syntax.nkind.N_FOR || k == syntax.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: *syntax.tinfo) i32 = { - let u: *syntax.tinfo = tichase(t); - if (u == nil) { return 0i32; }; - if (syntax.typeisnum(u)) { return 1i32; }; - if (syntax.typeisstr(u)) { return 2i32; }; - if (u.kind == syntax.tykind.TY_BOOL) { return 3i32; }; - if (u.kind == syntax.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: *syntax.node) i64 = { - if (dot == nil) { return -1i64; }; - if (dot.kind != syntax.nkind.N_DOT) { return -1i64; }; - let recv: *syntax.node = scruttype(c, dot.lhs); - if (recv == nil) { return -1i64; }; - let rtyp: *syntax.node = resolvealias(c, unwrapbang(recv)); - if (rtyp == nil) { return -1i64; }; - if (rtyp.kind == syntax.nkind.N_TPTR) { - rtyp = resolvealias(c, unwrapbang(rtyp.lhs)); - }; - if (rtyp == nil) { return -1i64; }; - if (rtyp.kind != syntax.nkind.N_TSTRUCT) { return -1i64; }; - let off: i64 = 0i64; - let f: *syntax.node = rtyp.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let fa: i64 = astalign(c, f.lhs); - off = (off + fa - 1i64) & ~(fa - 1i64); - if (syntax.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: *syntax.node, v: i64) void = { - n.kind = syntax.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: syntax.tkind, a: u64, b: u64, out: *u64) bool = { - if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; }; - if (op == syntax.tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == syntax.tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; }; - if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == syntax.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: *syntax.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: *syntax.tinfo, v: u64) bool = { - if (!syntax.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 (syntax.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: *syntax.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: syntax.nkind = n.kind; - if (k == syntax.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 == syntax.tkind.TK_SLASH || n.op == syntax.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 == syntax.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 == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (n.op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (n.op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - deffolderr(c, n, "def value: unsupported unary op"); - return false; - }; - if (k == syntax.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: *syntax.tinfo = (n.type_): *syntax.tinfo; - if (!defcastfits(t, v)) { - deffolderr(c, n, "def value: narrowing cast loses value"); - return false; - }; - *out = v; - return true; - }; - if (k == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.str); - if (s == nil) { return false; }; - if (s.skind != syntax.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 == syntax.nkind.N_DOT) { - if (n.lhs == nil) { return false; }; - if (n.lhs.kind != syntax.nkind.N_IDENT) { return false; }; - let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str); - if (s == nil) { return false; }; - if (s.skind != syntax.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: *syntax.node, v: u64) void = { - n.kind = syntax.nkind.N_INTLIT; - n.uval = v; - n.op = syntax.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: *syntax.node) u64 = { - if (rhs == nil) { return 0u64; }; - if (rhs.kind == syntax.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: *syntax.node, until: *syntax.node, e: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; }; - if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_UN) { - let v: u64 = 0u64; - if (!enumvalfold(body, until, e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - if (k == syntax.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 == syntax.nkind.N_IDENT) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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: *syntax.node) void = { - let under: *syntax.tinfo = c.tc.tyi32; - if (n.lhs != nil) { - let s: *syntax.tinfo = tinfofornode(c, n.lhs); - if (s != nil) { under = s; }; - }; - let m: *syntax.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: *syntax.node, ti: *syntax.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: *syntax.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: *syntax.tinfo = pt; - t = tichase(t); - if (t == nil) { return 8u64; }; - let pk: syntax.tykind = t.kind; - if (pk == syntax.tykind.TY_VOID) { return 0u64; }; - if (pk == syntax.tykind.TY_STR) { return t.size; }; - if (pk == syntax.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 == syntax.tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; - if (pk == syntax.tykind.TY_PTR || pk == syntax.tykind.TY_FN || - pk == syntax.tykind.TY_CHAN || pk == syntax.tykind.TY_I64 || - pk == syntax.tykind.TY_U64 || pk == syntax.tykind.TY_INT || - pk == syntax.tykind.TY_UINT || pk == syntax.tykind.TY_UINTPTR || - pk == syntax.tykind.TY_SIZE || pk == syntax.tykind.TY_F64) { return 8u64; }; - if (pk == syntax.tykind.TY_BOOL || pk == syntax.tykind.TY_RUNE || - pk == syntax.tykind.TY_I8 || pk == syntax.tykind.TY_I16 || - pk == syntax.tykind.TY_I32 || pk == syntax.tykind.TY_U8 || - pk == syntax.tykind.TY_U16 || pk == syntax.tykind.TY_U32 || - pk == syntax.tykind.TY_F32 || pk == syntax.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: *syntax.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: *syntax.tinfo = ft; - t = tichase(t); - if (t == nil) { return 8u64; }; - let fk: syntax.tykind = t.kind; - if (fk == syntax.tykind.TY_STRUCT) { return t.slotsize; }; - if (fk == syntax.tykind.TY_ARRAY) { return t.slotsize; }; - if (fk == syntax.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 == syntax.tykind.TY_SLICE) { return t.size; }; - if (fk == syntax.tykind.TY_PTR || fk == syntax.tykind.TY_FN || - fk == syntax.tykind.TY_CHAN) { return 8u64; }; - if (fk == syntax.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 == syntax.tykind.TY_TUPLE) { return t.slotsize; }; - // Primitives keep natural width inside structs (matches - // cgenutil fieldsize: primsize, not pad-to-8). - if (fk == syntax.tykind.TY_BOOL || fk == syntax.tykind.TY_RUNE || - fk == syntax.tykind.TY_I8 || fk == syntax.tykind.TY_I16 || - fk == syntax.tykind.TY_I32 || fk == syntax.tykind.TY_I64 || - fk == syntax.tykind.TY_U8 || fk == syntax.tykind.TY_U16 || - fk == syntax.tykind.TY_U32 || fk == syntax.tykind.TY_U64 || - fk == syntax.tykind.TY_INT || fk == syntax.tykind.TY_UINT || - fk == syntax.tykind.TY_UINTPTR || fk == syntax.tykind.TY_SIZE || - fk == syntax.tykind.TY_F32 || fk == syntax.tykind.TY_F64 || - fk == syntax.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: *syntax.tparam, vt: *syntax.tinfo) bool = { - if (vt == nil) { return false; }; - let p: *syntax.tparam = head; - for (p != nil) { - if (p.type_ != nil) { - if (syntax.typeeq(p.type_, vt)) { return true; }; - }; - p = p.tnext; - }; - return false; -}; - -fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = { - if (n == nil) { return nil; }; - let cached: *syntax.tinfo = syntax.tinfocachelookup(c.tc, n); - if (cached != nil) { return cached; }; - let r: *syntax.tinfo = nil; - let k: syntax.nkind = n.kind; - switch (k) { - case syntax.nkind.N_TNAME: - let nm: str = n.str; - if (syntax.streq(nm, "void")) { r = c.tc.tyvoid; }; - if (syntax.streq(nm, "bool")) { r = c.tc.tybool; }; - if (syntax.streq(nm, "rune")) { r = c.tc.tyrune; }; - if (syntax.streq(nm, "i8")) { r = c.tc.tyi8; }; - if (syntax.streq(nm, "i16")) { r = c.tc.tyi16; }; - if (syntax.streq(nm, "i32")) { r = c.tc.tyi32; }; - if (syntax.streq(nm, "i64")) { r = c.tc.tyi64; }; - if (syntax.streq(nm, "u8")) { r = c.tc.tyu8; }; - if (syntax.streq(nm, "u16")) { r = c.tc.tyu16; }; - if (syntax.streq(nm, "u32")) { r = c.tc.tyu32; }; - if (syntax.streq(nm, "u64")) { r = c.tc.tyu64; }; - if (syntax.streq(nm, "int")) { r = c.tc.tyint; }; - if (syntax.streq(nm, "uint")) { r = c.tc.tyuint; }; - if (syntax.streq(nm, "uintptr")) { r = c.tc.tyuintptr; }; - if (syntax.streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2 - if (syntax.streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a) - if (syntax.streq(nm, "f32")) { r = c.tc.tyf32; }; - if (syntax.streq(nm, "f64")) { r = c.tc.tyf64; }; - if (syntax.streq(nm, "str")) { r = c.tc.tystr; }; - if (syntax.streq(nm, "never")) { r = c.tc.tynever; }; - if (syntax.streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; }; - if (syntax.streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; }; - if (syntax.streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; }; - if (syntax.streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; }; - if (syntax.streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; }; - if (syntax.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: *syntax.sym = aliassym(c, n); - if (s != nil) { - if (s.type_ != nil) { - r = s.type_; - } else { - let body: *syntax.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: *syntax.tinfo = syntax.typenamed(s.name, nil); - s.type_ = named; - named.resolving = 1; - let under: *syntax.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 syntax.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 syntax.nkind.N_TPTR: - r = syntax.typeptr(tinfofornode(c, n.lhs)); - case syntax.nkind.N_TSLICE: - r = syntax.typeslice(tinfofornode(c, n.lhs)); - case syntax.nkind.N_TCHAN: - r = syntax.typechan(tinfofornode(c, n.lhs)); - case syntax.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 == syntax.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: *syntax.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 = syntax.typearray(sub, elen); - case syntax.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 = syntax.newtype(syntax.tykind.TY_FN); - r.size = 8u64; - r.align = 8u64; - r.slotsize = 8u64; - syntax.tinfocachebind(c.tc, n, r); - r.ret = tinfofornode(c, n.lhs); - case syntax.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 = syntax.newtype(syntax.tykind.TY_ENUM); - let storage: *syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_TUPLE); - syntax.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: *syntax.ttupleelem = nil; - let tet: *syntax.ttupleelem = nil; - let slottotal: u64 = 0u64; - let maxal: u64 = 1u64; - let p: *syntax.node = n.list; - for (p != nil) { - let pt: *syntax.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: *syntax.tinfo = tichase(pt); - if (cu != nil && (cu.kind == syntax.tykind.TY_ARRAY - || cu.kind == syntax.tykind.TY_STRUCT - || cu.kind == syntax.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: *syntax.ttupleelem = alloc(syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_STRUCT); - syntax.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: *syntax.tfield = nil; - let ft_: *syntax.tfield = nil; - let off: u64 = 0u64; - let maxalign: u64 = 1u64; - let soff: u64 = 0u64; - let f: *syntax.node = n.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { - let ft: *syntax.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: *syntax.tfield = alloc(syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_TAGGED); - syntax.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: *syntax.tparam = nil; - let tail: *syntax.tparam = nil; - let maxsz: u64 = 0u64; - let al: u64 = 8u64; - let nv: i32 = 0; - let v: *syntax.node = n.list; - for (v != nil) { - let vt: *syntax.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 == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.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 == syntax.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: *syntax.tparam = vu.params; - for (src != nil) { - let st: *syntax.tinfo = src.type_; - if (st != nil && st.kind == syntax.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: *syntax.tparam = alloc(syntax.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 == syntax.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: *syntax.tparam = alloc(syntax.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: *syntax.tparam = head; - let pb: *syntax.tparam = head.tnext; - let aptr: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_PTR); - let bptr: bool = (pb.type_ != nil && pb.type_.kind == syntax.tykind.TY_PTR); - let avoid: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_VOID && !pa.iserror); - let bvoid: bool = (pb.type_ != nil && pb.type_.kind == syntax.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; }; - syntax.tinfocachebind(c.tc, n, r); - }; - return r; -}; - -// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode -// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec -// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is -// loud (#26), aligning wwstage down to cstage; the only promotion left is -// the one-sided alias-vs-base chase (cstage check.c:1060-1067). -// -// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or 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, so a nil operand never reaches the loud reject below. -fn unifyarith(c: *checker, e: *syntax.node, ltn: *syntax.node, rtn: *syntax.node) *syntax.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; }; - }; - // A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296), - // assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's - // unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed - // side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence, - // exprtype :2927), so the literal does not reach the isuntyped arms - // above. Mirror cstage here off the operand node so the rune-literal / - // integer mix stays valid (35 selfhost+lib sites) under the #26 reject. - if (e != nil) { - let lr: bool = e.lhs != nil && e.lhs.kind == syntax.nkind.N_RUNELIT; - let rr: bool = e.rhs != nil && e.rhs.kind == syntax.nkind.N_RUNELIT; - if (lr && isinttypeast(rtn)) { return rtn; }; - if (rr && isinttypeast(ltn)) { return ltn; }; - }; - if (typeeqast(c, ltn, rtn)) { return ltn; }; - // One-sided alias vs its (transitive) base promotes to the alias - // side; alias-vs-different-alias stays rejected even when the bases - // agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and - // harec type_promote (ref/harec/src/check.c:1083-1105). The error - // axis (varianterr) must agree on both sides so a `!i32` alias does - // NOT promote against a plain i32 (#246, 949_errtype_compare). - // A user alias is an SK_TYPE sym WITH a decl body; the primitives are - // SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone - // would mis-flag i32 as "named". This decl != nil gate is the wwstage - // analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc). - let ls: *syntax.sym = aliassym(c, unwrapbang(ltn)); - let rs: *syntax.sym = aliassym(c, unwrapbang(rtn)); - let la: bool = ls != nil && ls.decl != nil; - let ra: bool = rs != nil && rs.decl != nil; - if (!(la && ra)) { - let da: *syntax.node = resolvealias(c, ltn); - let db: *syntax.node = resolvealias(c, rtn); - if (da != nil && db != nil - && varianterr(c, ltn) == varianterr(c, rtn) - && typeeqast(c, da, db)) { - if (la) { return ltn; }; - return rtn; - }; - }; - // ww requires explicit integer conversion in binary ops (Go-faithful, - // user-blessed #26): a typed/typed operand mismatch is loud, NOT - // promoted. Diverges from Hare type_promote's implicit signed-widen - // (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage - // already rejects the same shape (cmd/wcc/check.c:1068); this aligns - // wwstage down. A nil operand is an inherent-IDENT bail (see header) — - // propagate it, never reject. - if (ltn != nil && rtn != nil) { - deffolderr(c, e, "operands have differing types"); - }; - 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: *syntax.node, target: *syntax.node) void = { - if (e == nil) { return; }; - if (target == nil) { return; }; - let tu: *syntax.node = resolvealias(c, unwrapbang(target)); - if (tu == nil) { return; }; - if (tu.kind != syntax.nkind.N_TNAME) { return; }; - if (!syntax.streq(tu.str, "f32")) { return; }; - if (e.kind == syntax.nkind.N_FLOATLIT) { - if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) { - let f32t: *syntax.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: *syntax.node, target: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind != syntax.nkind.N_RUNELIT) { return nil; }; - if (target == nil) { return nil; }; - let tu: *syntax.node = resolvealias(c, unwrapbang(target)); - if (tu == nil) { return nil; }; - if (isinttypeast(tu)) { return tu; }; - if (tu.kind == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = tu.list; - for (v != nil) { - let vu: *syntax.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: *syntax.node) *syntax.node = { - let op: syntax.tkind = e.op; - let ltn: *syntax.node = exprtype(c, e.lhs, nil); - let rtn: *syntax.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 == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS) { - if (ltn != nil && ltn.kind == syntax.nkind.N_TPTR && intkindast(c, rtn)) { - return ltn; - }; - }; - if (op == syntax.tkind.TK_PLUS) { - if (intkindast(c, ltn) && rtn != nil && rtn.kind == syntax.nkind.N_TPTR) { - return rtn; - }; - }; - if (op == syntax.tkind.TK_MINUS) { - if (ltn != nil && rtn != nil - && ltn.kind == syntax.nkind.N_TPTR && rtn.kind == syntax.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 == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS || - op == syntax.tkind.TK_STAR || op == syntax.tkind.TK_SLASH || - op == syntax.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, e, ltn, rtn); - }; - if (op == syntax.tkind.TK_AMP || op == syntax.tkind.TK_PIPE || - op == syntax.tkind.TK_CARET || op == syntax.tkind.TK_LSHIFT || - op == syntax.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, e, ltn, rtn); - }; - if (op == syntax.tkind.TK_EQ || op == syntax.tkind.TK_NEQ || - op == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE || - op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) { - // cstage routes every comparison through unify_arith (check.c:1195/ - // 1200 cbinop), which loud-rejects a differing-types pair, e.g. - // strconv.invalid != i32. wwstage routes the ORDERED arm through - // unifyarith below (#26), but EQ/NEQ stays here on the error-type- - // only reject: a full unifyarith route for EQ/NEQ would expose the - // typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr - // equality. The error-type case (#246, rule 10) is the one real - // cs!=ww edge EQ/NEQ must still reject. - 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 == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE || - op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) { - if ((ltn != nil && !numkindast(c, ltn)) || - (rtn != nil && !numkindast(c, rtn))) { - deffolderr(c, e, "ordered comparison on non-numeric"); - }; - // #26: an ordered comparison routes through unifyarith for - // the differing-types reject — cstage's cbinop sends every - // comparison through unify_arith (cmd/wcc/check.c:1195/1200), - // so `int < len(s):i32` is loud there. Guarded off error - // operands (the #246 block above already rejects those, so - // this avoids a double diagnostic) and scoped to ordered, - // keeping the typeeqast-vs-type_eq asymmetry risk off the - // untouched EQ/NEQ arm. - if (!varianterr(c, ltn) && !varianterr(c, rtn)) { - unifyarith(c, e, ltn, rtn); - }; - }; - return mktname(c, "bool"); - }; - if (op == syntax.tkind.TK_AND || op == syntax.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: *syntax.node) *syntax.node = { - let op: syntax.tkind = e.op; - let opt: *syntax.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 == syntax.tkind.TK_MINUS || op == syntax.tkind.TK_PLUS) { - if (opt != nil && !numkindast(c, opt)) { - if (op == syntax.tkind.TK_MINUS) { - deffolderr(c, e, "- on non-numeric"); - } else { - deffolderr(c, e, "+ on non-numeric"); - }; - }; - return opt; - }; - if (op == syntax.tkind.TK_NOT) { - if (opt != nil && !boolkindast(c, opt)) { - deffolderr(c, e, "! on non-bool"); - }; - return mktname(c, "bool"); - }; - if (op == syntax.tkind.TK_TILDE) { - if (opt != nil && !intkindast(c, opt)) { - deffolderr(c, e, "~ on non-integer"); - }; - return opt; - }; - if (op == syntax.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: *syntax.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 != syntax.nkind.N_TPTR) { return nil; }; - return u.lhs; - }; - if (op == syntax.tkind.TK_AMP) { - if (e.lhs != nil && e.lhs.kind == syntax.nkind.N_DOT) { - let fld: str = e.lhs.str; - if (syntax.streq(fld, "len") || syntax.streq(fld, "cap")) { - let base: *syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = bu.sub; }; - bu = tichase(bu); - if (bu != nil) { - if (bu.kind == syntax.tykind.TY_SLICE || bu.kind == syntax.tykind.TY_STR) { - let pp: *syntax.node = syntax.newnode(syntax.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 == syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (fs != nil) { - if (fs.skind == syntax.skind.SK_FN) { - if (fs.decl != nil) { - if (fs.decl.kind == syntax.nkind.N_FNDECL) { - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = fs.decl.lhs; - synth.list = fs.decl.list; - let pf: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_DOT) { - if (e.lhs.lhs != nil && e.lhs.lhs.kind == syntax.nkind.N_IDENT) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str); - if (fs != nil) { - if (fs.skind == syntax.skind.SK_FN) { - if (fs.decl != nil) { - if (fs.decl.kind == syntax.nkind.N_FNDECL) { - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = fs.decl.lhs; - synth.list = fs.decl.list; - let pf: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.node) *syntax.node = { - let basetn: *syntax.node = exprtype(c, e.lhs, nil); - let _idx: *syntax.node = exprtype(c, e.rhs, nil); - let u: *syntax.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 == syntax.nkind.N_TSLICE) { return u.lhs; }; - if (u.kind == syntax.nkind.N_TARRAY) { return u.lhs; }; - if (u.kind == syntax.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 (syntax.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 == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (s != nil && s.skind == syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = u.lhs; - let iu: *syntax.node = resolvealias(c, unwrapbang(inner)); - if (iu != nil && iu.kind == syntax.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: *syntax.node, hint: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - let k: syntax.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 == syntax.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: *syntax.node = mktname(c, e.tsuffix); - let ti: *syntax.tinfo = tinfofornode(c, suf); - if (ti != nil) { - e.type_ = ti: *void; - return suf; - }; - }; - let tn: *syntax.node = mktname(c, "untyped_int"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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: *syntax.node = mktname(c, e.tsuffix); - let ti: *syntax.tinfo = tinfofornode(c, suf); - if (ti != nil) { - e.type_ = ti: *void; - return suf; - }; - }; - let tn: *syntax.node = mktname(c, "untyped_float"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_STRLIT) { - let tn: *syntax.node = mktname(c, "str"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_RUNELIT) { - let tn: *syntax.node = mktname(c, "rune"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_TRUE) { - let tn: *syntax.node = mktname(c, "bool"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_FALSE) { - let tn: *syntax.node = mktname(c, "bool"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_VOIDLIT) { - let tn: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_NIL) { - let tn: *syntax.node = mktname(c, "untyped_nil"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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: *syntax.sym = syntax.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 == syntax.skind.SK_FN) { - let ft: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.tinfo = tinfofornode(c, t); - if (ti != nil) { - e.type_ = ti: *void; - t.type_ = ti: *void; - }; - }; - }; - return t; - }; - if (k == syntax.nkind.N_BIN) { - let tn: *syntax.node = binoptype(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_UN) { - let tn: *syntax.node = unoptype(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.nkind.N_INDEX) { - let tn: *syntax.node = indexresult(c, e); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (k == syntax.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 == syntax.nkind.N_CALL) { - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(callee.str, "alloc")) { - let shadowed: bool = false; - if (c.curmod.len > 0) { - if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { - shadowed = true; - }; - }; - if (!shadowed) { - if (e.list != nil) { - // Slice form: `alloc([], n)`. - if (e.list.kind == syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = mktname(c, "u8"); - let nome: *syntax.node = mktname(c, "nomem"); - sl.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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: *syntax.node = exprtype(c, e.list, nil); - let ptr: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, "", 0, 0); - ptr.lhs = argt; - let nome: *syntax.node = mktname(c, "nomem"); - ptr.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_IDENT) { - let bname: str = callee.str; - let issize: bool = syntax.streq(bname, "size"); - let isalign: bool = syntax.streq(bname, "align"); - let isoffset: bool = syntax.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 == syntax.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:1570/1602 which stamps - // ty_untyped_int after the fold AND returns it - // to the caller (the Hare-correct type), so a - // `let x: size = size(T)` binding is assignable. - let utn: *syntax.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, "untyped_int"); - }; - 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, "untyped_int"); - }; - // 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 == syntax.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, "untyped_int"); - }; - }; - }; - }; - }; - // 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 == syntax.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 (syntax.streq(callee.str, "abort") - && syntax.scopelookupprefer(c.cur, c.curmod, "abort") == nil) { - if (e.list != nil) { - let mt: *syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - callee.type_ = c.tc.tyerr: *void; - return tn; - }; - if (syntax.streq(callee.str, "assert") && e.list != nil - && syntax.scopelookupprefer(c.cur, c.curmod, "assert") == nil) { - let ct: *syntax.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: *syntax.node = unwrapbang(ct); - let condok: bool = false; - if (cu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(cu.str, "bool") - || syntax.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: *syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - callee.type_ = c.tc.tyerr: *void; - return tn; - }; - if (syntax.streq(callee.str, "len")) { - let tn: *syntax.node = mktname(c, "i32"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - if (syntax.streq(callee.str, "append") || syntax.streq(callee.str, "free")) { - let tn: *syntax.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 (syntax.streq(callee.str, "delete")) { - let d: *syntax.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 != syntax.nkind.N_INDEX && d.kind != syntax.nkind.N_SLICE) { - cerr("delete: operand must be an indexing or slicing expression\n"); - c.errs += 1; - } else { - let basetn: *syntax.node = exprtype(c, d.lhs, nil); - let u: *syntax.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 != syntax.nkind.N_TSLICE) { - cerr("delete must operate on a slice\n"); - c.errs += 1; - }; - }; - }; - let tn: *syntax.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 (syntax.streq(callee.str, "insert")) { - let d: *syntax.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 == syntax.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 != syntax.nkind.N_INDEX) { - cerr("insert: operand must be an indexing expression xs[i]\n"); - c.errs += 1; - } else { - let basetn: *syntax.node = exprtype(c, d.lhs, nil); - let u: *syntax.node = resolvealias(c, unwrapbang(basetn)); - // harec check.c:807 wording; a - // fixed-size [N]T base lands here. - if (u == nil || u.kind != syntax.nkind.N_TSLICE) { - cerr("insert must operate on a slice\n"); - c.errs += 1; - }; - }; - }; - if (d.next != nil) { - if (d.next.kind == syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, tn): *void; - return tn; - }; - }; - let nm: str; - nm.ptr = nil; nm.len = 0; - if (callee.kind == syntax.nkind.N_IDENT) { nm = callee.str; }; - if (callee.kind == syntax.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: *syntax.sym = nil; - if (callee.kind == syntax.nkind.N_IDENT) { - s = syntax.scopelookupprefer(c.cur, c.curmod, nm); - } else { - let ms: *syntax.sym = nil; - if (callee.lhs != nil && callee.lhs.kind == syntax.nkind.N_IDENT) { - ms = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.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 == syntax.skind.SK_USE || ms.use_alias != 0i32)) { - s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm); - // Module-qualified callee whose leaf isn't scope-keyed - // under its module: align to cstage, which stamps ty_err - // here and lets cgen emit the call (cmd/wcc/check.c:1834- - // 1843; rule-10). This now tolerates only a genuinely- - // extern leaf (raw w6c on a single module-qualified file - // with no driver concat), the module-qualified arm of #27. - // The -T synth's `test.run` NO LONGER lands here: #80 - // PREPENDS the synth `use test;` before Pass 1, so declmod - // keys lib/test's `run` under "test" (not "") and the synth - // `test.run` type-resolves — the E1 bridge is closed. cgen - // keys the CALL off run's `//ww:module test` directive - // either way, so the resolution change is asm-neutral. - if (s == nil) { - e.type_ = c.tc.tyerr: *void; - callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it) - return nil; - }; - }; - }; - if (s != nil) { if (s.skind == syntax.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: *syntax.node = resolvealias(c, unwrapbang(exprtype(c, callee, nil))); - for (ct != nil && ct.kind == syntax.nkind.N_TPTR) { - ct = resolvealias(c, unwrapbang(ct.lhs)); - }; - if (ct != nil) { if (ct.kind == syntax.nkind.N_TFN) { - let res: *syntax.node = ct.lhs; - e.type_ = tinfofornode(c, res): *void; - return res; - }; }; - return nil; - }; - if (k == syntax.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: *syntax.node = e.lhs; - if (lhsn != nil) { if (lhsn.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, lhsn.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.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 == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 == syntax.skind.SK_FN) { - let ft: *syntax.node = syntax.newnode(syntax.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: *syntax.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 == syntax.skind.SK_TYPE) { if (ms.decl != nil) { - let body: *syntax.node = ms.decl.lhs; - let ub: *syntax.node = resolvealias(c, unwrapbang(body)); - if (ub != nil) { if (ub.kind == syntax.nkind.N_TENUM) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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: *syntax.node = exprtype(c, lhsn, nil); - if (basetn != nil) { - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu != nil) { if (bu.kind == syntax.nkind.N_TPTR) { - bu = resolvealias(c, unwrapbang(bu.lhs)); - }; }; - if (bu != nil) { if (bu.kind == syntax.nkind.N_TENUM) { - let prev: u64 = (-1i64): u64; - let m: *syntax.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 (syntax.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 == syntax.nkind.N_TNAME) && syntax.streq(bu.str, "str"); - if (bu.kind == syntax.nkind.N_TSLICE || bu.kind == syntax.nkind.N_TARRAY || isstr) { - if (syntax.streq(e.str, "len")) { - let tn: *syntax.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 == syntax.nkind.N_TARRAY && syntax.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 (syntax.streq(e.str, "cap")) { - let tn: *syntax.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 (syntax.streq(e.str, "ptr")) { - let elem: *syntax.node = bu.lhs; - if (isstr) { elem = mktname(c, "u8"); }; - let pp: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_TSTRUCT) { - let f: *syntax.node = bu.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_TFIELD) { if (syntax.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 == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(e.str); - if (idx >= 0) { - let p: *syntax.node = bu.list; - for (idx > 0 && p != nil) { - p = p.next; - idx -= 1; - }; - if (p != nil) { - let pt: *syntax.node = p.lhs; - e.type_ = tinfofornode(c, pt): *void; - return pt; - }; - }; - }; }; - }; - return nil; - }; - if (k == syntax.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 == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str); - if (ms != nil) { if (ms.skind == syntax.skind.SK_TYPE) { if (ms.decl != nil) { - let tn: *syntax.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: *syntax.node = mktname(c, e.lhs.str); - let nti: *syntax.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: *syntax.node = resolvealias(c, tn); - if (stn != nil && stn.kind == syntax.nkind.N_TSTRUCT) { - let fi: *syntax.node = e.list; - for (fi != nil) { - if (fi.kind == syntax.nkind.N_FIELD - && fi.lhs != nil - && fi.lhs.kind == syntax.nkind.N_ARRLIT) { - let ftn: *syntax.node = nil; - let tf: *syntax.node = stn.list; - for (tf != nil) { - if (tf.kind == syntax.nkind.N_TFIELD) { - if (syntax.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 == syntax.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: *syntax.node = nil; - let count: u64 = 0u64; - let it: *syntax.node = e.list; - for (it != nil) { - let skip: bool = false; - if (it.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(it.str, "...")) { skip = true; }; - }; - if (!skip) { - let t: *syntax.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 == syntax.nkind.N_STRUCTLIT - && it.lhs != nil - && it.lhs.kind == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(elt.str, "untyped_int")) { - elt = mktname(c, "int"); - } else { if (syntax.streq(elt.str, "untyped_float")) { - elt = mktname(c, "f64"); - } else { if (syntax.streq(elt.str, "untyped_str")) { - elt = mktname(c, "str"); - } else { if (syntax.streq(elt.str, "untyped_rune")) { - elt = mktname(c, "rune"); - } else { if (syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu == nil) { return nil; }; - if (bu.kind == syntax.nkind.N_TARRAY) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = bu.lhs; - e.type_ = tinfofornode(c, sl): *void; - return sl; - }; - if (bu.kind == syntax.nkind.N_TSLICE) { - e.type_ = tinfofornode(c, basetn): *void; - return basetn; - }; - if (bu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(bu.str, "str")) { - let tn: *syntax.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 == syntax.nkind.N_TPTR && bu.lhs != nil) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = bu.lhs; - e.type_ = tinfofornode(c, sl): *void; - return sl; - }; - return nil; - }; - if (k == syntax.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: *syntax.node = nil; - let tail: *syntax.node = nil; - let it: *syntax.node = e.list; - for (it != nil) { - let elemt: *syntax.node = exprtype(c, it, nil); - let w: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, "", 0, 0); - w.lhs = elemt; - if (head == nil) { head = w; } - else { tail.next = w; }; - tail = w; - it = it.next; - }; - let tt: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, "", 0, 0); - tt.list = head; - e.type_ = tinfofornode(c, tt): *void; - return tt; - }; - if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let bu: *syntax.node = resolvealias(c, unwrapbang(basetn)); - if (bu == nil) { return nil; }; - if (bu.kind == syntax.nkind.N_TCHAN) { - e.type_ = tinfofornode(c, bu.lhs): *void; - return bu.lhs; - }; - return nil; - }; - if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; - return t; - }; - if (k == syntax.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: *syntax.tinfo = nil; - let retn: *syntax.node = nil; - let cs: *syntax.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: *syntax.node = nil; - let t: *syntax.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 (!syntax.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 == syntax.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: *syntax.node = mktname(c, "void"); - e.type_ = tinfofornode(c, v): *void; - return v; - }; - let t: *syntax.node = exprtype(c, e.lhs, nil); - if (t != nil) { e.type_ = tinfofornode(c, t): *void; }; - return t; - }; - if (k == syntax.nkind.N_TRYPROP) { - // success unwrap: the success-variant type of operand's - // tagged union. - let opt: *syntax.node = exprtype(c, e.lhs, nil); - let ou: *syntax.node = resolvealias(c, unwrapbang(opt)); - if (ou == nil) { return nil; }; - if (ou.kind != syntax.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: *syntax.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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil); - let ou: *syntax.node = resolvealias(c, unwrapbang(opt)); - if (ou == nil) { return nil; }; - if (ou.kind != syntax.nkind.N_TTAGGED) { return nil; }; - if (taggedhaserr(c, ou)) { - let v: *syntax.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 == syntax.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 == syntax.nkind.N_TYPETEST) { - // `e is T` → bool - let tn: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(t.str, "untyped_int"); -}; - -fn isuntypedfloat(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(t.str, "untyped_float"); -}; - -fn isuntypednil(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind == syntax.nkind.N_TENUM) { return true; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = t.str; - if (syntax.streq(s, "i8")) { return true; }; - if (syntax.streq(s, "i16")) { return true; }; - if (syntax.streq(s, "i32")) { return true; }; - if (syntax.streq(s, "i64")) { return true; }; - if (syntax.streq(s, "u8")) { return true; }; - if (syntax.streq(s, "u16")) { return true; }; - if (syntax.streq(s, "u32")) { return true; }; - if (syntax.streq(s, "u64")) { return true; }; - if (syntax.streq(s, "int")) { return true; }; - if (syntax.streq(s, "uint")) { return true; }; - if (syntax.streq(s, "uintptr")) { return true; }; - if (syntax.streq(s, "size")) { return true; }; - if (syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (isinttypeast(u)) { return true; }; // int family + enum + rune - if (isuntypedint(u)) { return true; }; - if (u != nil && u.kind == syntax.nkind.N_TNAME && syntax.streq(u.str, "untyped_rune")) { - return true; - }; - return false; -}; - -fn numkindast(c: *checker, t: *syntax.node) bool = { - if (intkindast(c, t)) { return true; }; - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - if (u.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = u.str; - if (syntax.streq(s, "f32")) { return true; }; - if (syntax.streq(s, "f64")) { return true; }; - if (syntax.streq(s, "untyped_float")) { return true; }; - return false; -}; - -fn boolkindast(c: *checker, t: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return false; }; - if (u.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.streq(u.str, "bool") || syntax.streq(u.str, "untyped_bool"); -}; - -fn isnumerictname(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let s: str = t.str; - if (syntax.streq(s, "i8")) { return true; }; - if (syntax.streq(s, "i16")) { return true; }; - if (syntax.streq(s, "i32")) { return true; }; - if (syntax.streq(s, "i64")) { return true; }; - if (syntax.streq(s, "u8")) { return true; }; - if (syntax.streq(s, "u16")) { return true; }; - if (syntax.streq(s, "u32")) { return true; }; - if (syntax.streq(s, "u64")) { return true; }; - if (syntax.streq(s, "int")) { return true; }; - if (syntax.streq(s, "uint")) { return true; }; - if (syntax.streq(s, "uintptr")) { return true; }; - if (syntax.streq(s, "size")) { return true; }; - if (syntax.streq(s, "rune")) { return true; }; - if (syntax.streq(s, "f32")) { return true; }; - if (syntax.streq(s, "f64")) { return true; }; - return false; -}; - -fn isstrtname(t: *syntax.node) bool = { - if (t == nil) { return false; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - return syntax.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: *syntax.node) i32 = { - if (t == nil) { return 0i32; }; - if (t.kind == syntax.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: *syntax.node, synth: *syntax.node) bool = { - if (ptr == nil) { return false; }; - let pu: *syntax.node = resolvealias(c, unwrapbang(ptr)); - if (pu == nil) { return false; }; - if (pu.kind != syntax.nkind.N_TPTR) { return false; }; - let ref: *syntax.node = resolvealias(c, unwrapbang(pu.lhs)); - if (ref == nil) { return false; }; - if (ref.kind != syntax.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: *syntax.node, rhs: *syntax.node) bool = { - if (dst == nil) { return false; }; - if (rhs == nil) { return false; }; - if (rhs.kind != syntax.nkind.N_UN) { return false; }; - if (rhs.op != syntax.tkind.TK_AMP) { return false; }; - let id: *syntax.node = rhs.lhs; - if (id == nil) { return false; }; - if (id.kind != syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, id.str); - if (s == nil) { return false; }; - if (s.skind != syntax.skind.SK_FN) { return false; }; - if (s.decl == nil) { return false; }; - let d: *syntax.node = s.decl; - if (d.kind != syntax.nkind.N_FNDECL) { return false; }; - let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0); - synth.lhs = d.lhs; - synth.list = d.list; - let du: *syntax.node = resolvealias(c, unwrapbang(dst)); - if (du == nil) { return false; }; - if (du.kind == syntax.nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); }; - if (du.kind == syntax.nkind.N_TTAGGED) { - let nmatch: i32 = 0; - let v: *syntax.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: *syntax.node, src: *syntax.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: *syntax.node = resolvealias(c, unwrapbang(dst)); - let su: *syntax.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 == syntax.nkind.N_TSLICE) { - if (su.kind == syntax.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 == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.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 == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(du.str, "bool")) { return false; }; - if (syntax.streq(du.str, "void")) { return false; }; - if (syntax.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 == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT) { - return false; - }; - // Unknown shapes: stay quiet. - *confident = false; - return true; - }; - if (isuntypedfloat(su)) { - if (isnumerictname(du)) { return true; }; - if (du.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(du.str, "bool")) { return false; }; - if (syntax.streq(du.str, "void")) { return false; }; - if (syntax.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 == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - if (vu != nil) { - if (vu.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(vu.str, "f32")) { return true; }; - if (syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT) { - return false; - }; - *confident = false; - return true; - }; - if (isuntypednil(su)) { - // nil → ptr/slice/chan/fn/nullable - if (du.kind == syntax.nkind.N_TPTR) { return true; }; - if (du.kind == syntax.nkind.N_TSLICE) { return true; }; - if (du.kind == syntax.nkind.N_TCHAN) { return true; }; - if (du.kind == syntax.nkind.N_TFN) { return true; }; - // nullable `(*T | void)` — already accepted by typeeqast - // when matched whole; nil is OK there too. - if (du.kind == syntax.nkind.N_TTAGGED) { - let v: *syntax.node = du.list; - for (v != nil) { - if (v.kind == syntax.nkind.N_TPTR) { return true; }; - if (v.kind == syntax.nkind.N_TSLICE) { return true; }; - if (v.kind == syntax.nkind.N_TCHAN) { return true; }; - if (v.kind == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.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 == syntax.nkind.N_TTAGGED && su.kind != syntax.nkind.N_TTAGGED) { - let v: *syntax.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 == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - let vtagged: bool = false; - if (vu != nil) { if (vu.kind == syntax.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 != syntax.nkind.N_TNAME) { - let ss: i32 = tagshape(su); - let sp: *syntax.node = du.list; - for (sp != nil) { - let svu: *syntax.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 == syntax.nkind.N_TTAGGED && su.kind == syntax.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: *syntax.node = du.list; - for (v != nil) { - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - if (vu != nil) { - if (vu.kind == syntax.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: *syntax.node = du.list; p != nil; p = p.next) { - if (p.op == syntax.tkind.TK_ELLIPSIS) { hasspread = true; }; - }; - for (let p: *syntax.node = su.list; p != nil; p = p.next) { - if (p.op == syntax.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: *syntax.node = su.list; sp != nil; sp = sp.next) { - let ok: bool = false; - for (let dp: *syntax.node = du.list; dp != nil; dp = dp.next) { - if (typeeqast(c, dp, sp)) { ok = true; break; }; - let su2: *syntax.node = unwrapbang(sp); - let du2: *syntax.node = unwrapbang(dp); - if (su2 != nil && du2 != nil && - su2.kind == syntax.nkind.N_TNAME && du2.kind == syntax.nkind.N_TNAME && - syntax.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 == syntax.nkind.N_TTAGGED && du.kind != syntax.nkind.N_TTAGGED) { - return false; - }; - // Two known primitives with different names are confidently - // incompatible. `i32 ↔ bool`, `str ↔ i32`, etc. - if (du.kind == syntax.nkind.N_TNAME && su.kind == syntax.nkind.N_TNAME) { - let known_d: bool = isnumerictname(du) || isstrtname(du); - if (!known_d) { if (syntax.streq(du.str, "bool")) { known_d = true; }; }; - if (!known_d) { if (syntax.streq(du.str, "void")) { known_d = true; }; }; - let known_s: bool = isnumerictname(su) || isstrtname(su); - if (!known_s) { if (syntax.streq(su.str, "bool")) { known_s = true; }; }; - if (!known_s) { if (syntax.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 == syntax.nkind.N_TPTR) { - if (su.kind == syntax.nkind.N_TPTR) { - let dref: *syntax.node = resolvealias(c, unwrapbang(du.lhs)); - let sref: *syntax.node = resolvealias(c, unwrapbang(su.lhs)); - if (dref != nil) { - if (sref != nil) { - if (dref.kind == syntax.nkind.N_TFN) { - if (sref.kind == syntax.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 == syntax.nkind.N_TFN) { - if (su.kind == syntax.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 == syntax.nkind.N_TNAME - && (isnumerictname(du) || isstrtname(du) - || syntax.streq(du.str, "bool") || syntax.streq(du.str, "void")); - let sprim: bool = su.kind == syntax.nkind.N_TNAME - && (isnumerictname(su) || isstrtname(su) - || syntax.streq(su.str, "bool") || syntax.streq(su.str, "void")); - let daggr: bool = du.kind == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY - || du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN - || du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE - || du.kind == syntax.nkind.N_TSTRUCT; - let saggr: bool = su.kind == syntax.nkind.N_TSLICE || su.kind == syntax.nkind.N_TARRAY - || su.kind == syntax.nkind.N_TPTR || su.kind == syntax.nkind.N_TFN - || su.kind == syntax.nkind.N_TCHAN || su.kind == syntax.nkind.N_TTUPLE - || su.kind == syntax.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 != syntax.nkind.N_TSTRUCT && su.kind != syntax.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: *syntax.node, pat: *syntax.node, unionmod: str) bool = { - let vv: *syntax.node = unwrapbang(v); - let pp: *syntax.node = unwrapbang(pat); - if (vv == nil) { return false; }; - if (pp == nil) { return false; }; - if (vv.kind != syntax.nkind.N_TNAME) { return false; }; - if (pp.kind != syntax.nkind.N_TNAME) { return false; }; - if (!syntax.streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; - return syntax.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: *syntax.node) str = { - let defmod: str = c.curmod; - let cur: *syntax.node = unwrapbang(st); - for (cur != nil) { - if (cur.kind != syntax.nkind.N_TNAME) { return defmod; }; - let s: *syntax.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: *syntax.node = unwrapbang(s.decl.lhs); - if (body == nil) { return defmod; }; - if (body.kind == syntax.nkind.N_TTAGGED) { return defmod; }; - cur = body; - }; - return defmod; -}; - -fn casecovers(c: *checker, cs: *syntax.node, want: *syntax.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: *syntax.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: *syntax.node, vname: *syntax.node) void = { - cerr("match: variant not handled"); - if (vname != nil) { - if (vname.kind == syntax.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: *syntax.node, pat: *syntax.node, unionmod: str) bool = { - let v: *syntax.node = tagged.list; - for (v != nil) { - if (v.op == syntax.tkind.TK_ELLIPSIS) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil) { - if (inner.kind == syntax.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: *syntax.node) void = { - cerr("case: not a variant of scrutinee"); - if (pat != nil) { - if (pat.kind == syntax.nkind.N_TNAME) { - cerr(" ("); - cerr(pat.str); - cerr(")"); - }; - }; - cerr("\n"); - c.errs += 1; -}; - -fn checkmatchexhaust(c: *checker, n: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) { - istry = true; - st = exprtype(c, n.lhs, nil); - }; - }; - let u: *syntax.node = resolvealias(c, unwrapbang(st)); - if (u == nil) { return; }; - if (u.kind != syntax.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: *syntax.node = n.list; - for (cs0 != nil) { - if (cs0.lhs != nil) { - if (!casevariantin(c, u, cs0.lhs, unionmod)) { - errbadcase(c, cs0.lhs); - }; - let alt: *syntax.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: *syntax.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: *syntax.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: *syntax.node, v: *syntax.node, unionmod: str) void = { - if (v.op == syntax.tkind.TK_ELLIPSIS) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil) { - if (inner.kind == syntax.nkind.N_TTAGGED) { - let im: str = taggeddefmod(c, v); - let m: *syntax.node = inner.list; - for (m != nil) { - checkvariantcovered(c, n, m, im); - m = m.next; - }; - return; - }; - }; - }; - let covered: bool = false; - let cs2: *syntax.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: *syntax.node, src: *syntax.node, where: str) void = { - cerr(where); - cerr(": not assignable"); - if (src != nil) { - if (src.kind == syntax.nkind.N_TNAME) { - cerr(" ("); - cerr(src.str); - cerr(" → "); - if (dst != nil) { - if (dst.kind == syntax.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: *syntax.node, src: *syntax.node) bool = { - if (dst == nil) { return false; }; - if (src == nil) { return false; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dst)); - let su: *syntax.node = resolvealias(c, unwrapbang(src)); - if (du == nil) { return false; }; - if (du.kind != syntax.nkind.N_TTAGGED) { return false; }; - if (su != nil) { if (su.kind == syntax.nkind.N_TTAGGED) { return false; }; }; - let v: *syntax.node = du.list; - for (v != nil) { - let vspread: bool = (v.op == syntax.tkind.TK_ELLIPSIS); - let vu: *syntax.node = resolvealias(c, unwrapbang(v)); - let vtagged: bool = false; - if (vu != nil) { if (vu.kind == syntax.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 == syntax.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: *syntax.node, rhs: *syntax.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 != syntax.nkind.N_TARRAY) { return; }; - if (rhs.kind != syntax.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: *syntax.node = rhs.list; - for (ce != nil) { - let cskip: bool = false; - if (ce.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = arrtn.lhs; - let at: *syntax.tinfo = tinfofornode(c, arrtn); - let et: *syntax.tinfo = nil; - if (at != nil) { et = at.sub; }; - et = tichase(et); - let e: *syntax.node = rhs.list; - for (e != nil) { - let skip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { skip = true; }; - }; - if (!skip) { - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.node = resolvealias(c, elemtn); - if (eltr != nil && eltr.kind == syntax.nkind.N_TARRAY - && ev != nil && ev.kind == syntax.nkind.N_ARRLIT) { - checkarrlitfits(c, eltr, ev); - e = e.next; - continue; - }; - let v: u64 = 0u64; - let folded: bool = false; - if (et != nil) { - if (syntax.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: *syntax.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: *syntax.node, tup: *syntax.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: *syntax.node = ttn.list; - for (dc != nil) { dn += 1u64; dc = dc.next; }; - let vn: u64 = 0u64; - let vc: *syntax.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: *syntax.node = ttn.list; - let vt: *syntax.node = tup.list; - for (dt != nil && vt != nil) { - if (vt.kind == syntax.nkind.N_ARRLIT) { - checkarrlitfits(c, dt.lhs, vt); - } else { - if (vt.kind == syntax.nkind.N_TUPLE) { - let drt: *syntax.node = resolvealias(c, unwrapbang(dt.lhs)); - if (drt != nil && drt.kind == syntax.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: *syntax.node, val: *syntax.node) bool = { - if (val == nil) { return false; }; - if (val.kind != syntax.nkind.N_ARRLIT) { return false; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dsttn)); - if (du == nil) { return false; }; - if (du.kind != syntax.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: *syntax.node, srctn: *syntax.node, val: *syntax.node) *syntax.node = { - if (dsttn == nil) { return val; }; - if (srctn == nil) { return val; }; - if (val == nil) { return val; }; - let du: *syntax.node = resolvealias(c, unwrapbang(dsttn)); - let su: *syntax.node = resolvealias(c, unwrapbang(srctn)); - if (du == nil) { return val; }; - if (su == nil) { return val; }; - if (du.kind != syntax.nkind.N_TSLICE) { return val; }; - if (su.kind != syntax.nkind.N_TARRAY) { return val; }; - if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_SLICE, val.file, val.line, val.col); - sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) - let slt: *syntax.node = syntax.newnode(syntax.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: *syntax.node) *syntax.node = { - if (callee == nil) { return nil; }; - if (callee.kind == syntax.nkind.N_IDENT) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_FN) { return s.decl; }; - }; - return nil; - }; - if (callee.kind == syntax.nkind.N_DOT) { - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str); - if (ms != nil && ms.skind != syntax.skind.SK_USE) { - let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, callee.lhs.str); - if (mu != nil) { ms = mu; }; - }; - if (ms != nil) { - if (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32) { - let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str); - if (fs != nil) { - if (fs.skind == syntax.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: *syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(n.lhs.str, "free")) { - if (n.list != nil) { if (n.list.next == nil) { return; }; }; - }; - }; }; - let decl: *syntax.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: *syntax.node = decl.list; - let prev: *syntax.node = nil; - let a: *syntax.node = n.list; - for (a != nil) { - let nexta: *syntax.node = a.next; - if (param != nil) { - if (param.kind == syntax.nkind.N_PARAM) { - if (param.op != syntax.tkind.TK_ELLIPSIS) { - let atype: *syntax.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: *syntax.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 == syntax.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: *syntax.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 != syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - if (n.rhs == nil) { return; }; - // catB-22: reject reassigning a `const`-flagged binding (the - // is_const bit set at the let-install above). A bare `_` discard - // lvalue carries an empty str and is skipped. Mirror cstage - // cmd/wcc/check.c:1889-1896. - if (n.lhs.kind == syntax.nkind.N_IDENT) { - if (n.lhs.str.len > 0) { - let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.lhs.str); - if (s != nil) { - if (s.is_const != 0i32) { - cerr("error: cannot assign to const binding\n"); - c.errs += 1; - }; - }; - }; - }; - let ltn: *syntax.node = exprtype(c, n.lhs, nil); - let rtn: *syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { return; }; - if (n.lhs.kind != syntax.nkind.N_TARRAY) { return; }; - if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred - if (n.rhs == nil || n.rhs.kind != syntax.nkind.N_ARRLIT) { - cerr("error: [_]T needs an array-literal initialiser\n"); - c.errs += 1; - let z: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - z.uval = 0u64; - n.lhs.rhs = z; // sentinel: idempotent, error already raised - return; - }; - let cnt: u64 = 0u64; - let it: *syntax.node = n.rhs.list; - for (it != nil) { - let skip: bool = false; - if (it.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(it.str, "...")) { skip = true; }; - }; - if (!skip) { cnt += 1u64; }; - it = it.next; - }; - let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - cn.uval = cnt; - n.lhs.rhs = cn; -}; - -fn checkletassign(c: *checker, n: *syntax.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: *syntax.node = nil; - if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_TSLICE) { - let inner: *syntax.node = n.rhs; - if (inner.kind == syntax.nkind.N_TRYPROP) { - inner = inner.lhs; - } else { if (inner.kind == syntax.nkind.N_TRYUNW) { - inner = inner.lhs; - }; }; - if (inner != nil && inner.kind == syntax.nkind.N_CALL) { - let callee: *syntax.node = inner.lhs; - let a0: *syntax.node = inner.list; - let a1: *syntax.node = nil; - let a2: *syntax.node = nil; - if (a0 != nil) { a1 = a0.next; }; - if (a1 != nil) { a2 = a1.next; }; - if (callee != nil - && callee.kind == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0 != nil && a0.kind == syntax.nkind.N_ARRLIT - && a0.list == nil - && a1 != nil && a2 == nil) { - octx = inner; - }; - }; - }; - let savedoctx: *syntax.node = c.allococtx; - c.allococtx = octx; - let src: *syntax.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 == syntax.nkind.N_TSTRUCT - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && n.rhs.lhs != nil && n.rhs.lhs.kind == syntax.nkind.N_IDENT) { - let ltn: *syntax.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 == syntax.nkind.N_TSLICE) { - let wrapped: bool = false; - let inner: *syntax.node = n.rhs; - if (inner.kind == syntax.nkind.N_TRYPROP) { - wrapped = true; - inner = inner.lhs; - } else { if (inner.kind == syntax.nkind.N_TRYUNW) { - wrapped = true; - inner = inner.lhs; - }; }; - if (inner != nil && inner.kind == syntax.nkind.N_CALL) { - let callee: *syntax.node = inner.lhs; - let a0: *syntax.node = inner.list; - let a1: *syntax.node = nil; - let a2: *syntax.node = nil; - if (a0 != nil) { a1 = a0.next; }; - if (a1 != nil) { a2 = a1.next; }; - if (callee != nil - && callee.kind == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0 != nil && a0.kind == syntax.nkind.N_ARRLIT - && a0.list == nil - && a1 != nil && a2 == nil) { - let shadowed: bool = false; - if (c.curmod.len > 0) { - if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) { - shadowed = true; - }; - }; - if (!shadowed) { - let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0); - sl.lhs = n.lhs.lhs; - if (wrapped) { - src = sl; - } else { - let nome: *syntax.node = mktname(c, "nomem"); - sl.next = nome; - let tt: *syntax.node = syntax.newnode(syntax.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: *syntax.node = resolvealias(c, unwrapbang(n.lhs)); - if (llhs != nil && llhs.kind == syntax.nkind.N_TARRAY && n.rhs.kind == syntax.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 == syntax.nkind.N_TTUPLE - && n.rhs.kind == syntax.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 == syntax.nkind.N_TSLICE - && n.rhs.kind == syntax.nkind.N_ARRLIT) { - let cnt: u64 = 0u64; - let e0: *syntax.node = n.rhs.list; - for (e0 != nil) { - let skip: bool = false; - if (e0.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e0.str, "...")) { skip = true; }; - }; - if (!skip) { cnt += 1u64; }; - e0 = e0.next; - }; - let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0); - cn.uval = cnt; - let arr: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - if (n.lhs == nil) { - // bare `return;` — mirror cstage cmd/wcc/check.c:2428-2439: - // the value-less return has type void, then type_assignable - // (c.fnret, void). A void fnret or a tagged union carrying a - // void variant accepts; a non-void scalar fnret rejects (the - // value-less return would RET a garbage register). isassignable - // reaches the same verdict (void→void typeeqast; void→tagged via - // the void variant; void→i32 a confident primitive mismatch). - if (c.fnret == nil) { return; }; - let vt: *syntax.node = mktname(c, "void"); - let vconf: bool = false; - let vok: bool = isassignable(c, c.fnret, vt, &vconf); - if (vconf) { if (!vok) { errnotassign(c, c.fnret, vt, "return"); }; }; - 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 == syntax.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: *syntax.node = resolvealias(c, unwrapbang(c.fnret)); - if (rrt != nil && rrt.kind == syntax.nkind.N_TTUPLE - && n.lhs.kind == syntax.nkind.N_TUPLE) { - checktuplearrfits(c, rrt, n.lhs); - }; - let src: *syntax.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: *syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - // e is in n.lhs (value), T is in n.rhs (type expr). - let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) { - st = exprtype(c, n.lhs, nil); - }; - }; - let u: *syntax.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 == syntax.nkind.N_TYPEASSERT) { - let v: *syntax.node = resolvealias(c, unwrapbang(n.rhs)); - let lhsenum: bool = false; - let rhsenum: bool = false; - if (u != nil) { if (u.kind == syntax.nkind.N_TENUM) { lhsenum = true; }; }; - if (v != nil) { if (v.kind == syntax.nkind.N_TENUM) { rhsenum = true; }; }; - if (lhsenum || rhsenum) { - if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; }; - }; - }; - if (u.kind != syntax.nkind.N_TTAGGED) { - cerr("is/as: operand is not a tagged union\n"); - c.errs += 1; - return; - }; - let want: *syntax.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: *syntax.tinfo = tinfofornode(c, u); - let wanttinfo: *syntax.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 == syntax.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: *syntax.node) *syntax.node = { - if (e == nil) { return nil; }; - if (e.kind == syntax.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: *syntax.sym = syntax.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 == syntax.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: *syntax.node = e.lhs; - if (callee == nil) { return nil; }; - let nm: str; - nm.ptr = nil; nm.len = 0; - if (callee.kind == syntax.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 == syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm); - if (s == nil) { return nil; }; - if (s.skind != syntax.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: *syntax.node, v: *syntax.node, nsuccp: *int, - haserrp: *bool, depth: i32) void = { - for (v != nil) { - if (v.op == syntax.tkind.TK_ELLIPSIS && depth < 8i32) { - let inner: *syntax.node = resolvealias(c, unwrapbang(v)); - if (inner != nil && inner.kind == syntax.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: *syntax.node) void = { - if (n == nil) { return; }; - let t: *syntax.node = exprtypeoftry(c, n.lhs); - let u: *syntax.node = resolvealias(c, unwrapbang(t)); - if (u == nil) { return; }; - if (u.kind != syntax.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 == syntax.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 != syntax.nkind.N_TRYPROP) { return; }; - if (!haserr) { return; }; - // Enclosing fn must return a tagged union with each operand - // error variant present. - let r: *syntax.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 != syntax.nkind.N_TTAGGED) { - cerr("?: enclosing fn return is not tagged\n"); - c.errs += 1; - return; - }; - let ev: *syntax.node = u.list; - for (ev != nil) { - if (iserrvariant(c, u, ev)) { - let found: bool = false; - let rv: *syntax.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: *syntax.node) void = { - let p: *syntax.node = params; - for (p != nil) { - if (p.kind == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - if (p.lhs != nil && p.lhs.kind != syntax.nkind.N_TSLICE) { - let sl: *syntax.node = syntax.newnode(syntax.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"); - syntax.scopedefine(c.cur, nm, syntax.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: *syntax.node) void = { - let outer: *syntax.scope = c.cur; - c.cur = syntax.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: *syntax.node = fnnode.list; - for (p != nil) { - if (p.kind == syntax.nkind.N_PARAM) { - if (p.lhs != nil) { resolvewalk(c, p.lhs); }; - }; - p = p.next; - }; - let prevret: *syntax.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: *syntax.node) bool = { - if (id == nil) { return false; }; - if (id.kind != syntax.nkind.N_IDENT) { return false; }; - if (!syntax.streq(id.str, "abort") && !syntax.streq(id.str, "assert")) { - return false; - }; - return syntax.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: *syntax.node, indot: bool) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - let isexpr: bool = - k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT || - k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT || - k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE || - k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT || - k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN || - k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL || - k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST || - k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT || - k == syntax.nkind.N_RECV || k == syntax.nkind.N_DOT || - k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD || - k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP || - k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST || - k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD || - k == syntax.nkind.N_MATCH; - let skip: bool = false; - if (isexpr && k == syntax.nkind.N_IDENT) { - if (indot) { skip = true; }; - if (!skip) { - let s: *syntax.sym = syntax.scopelookup(c.cur, n.str); - if (s != nil) { - if (s.skind == syntax.skind.SK_USE) { skip = true; }; - if (s.decl == nil) { skip = true; }; - }; - }; - if (!skip) { if (isassertfam(c, n)) { skip = true; }; }; - }; - if (isexpr && k == syntax.nkind.N_CALL) { - if (isassertfam(c, n.lhs)) { skip = true; }; - }; - if (isexpr && !skip) { - if (n.type_ == nil) { - cerr("asserttyped: "); - let kn: str = syntax.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 == syntax.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: *syntax.node = n.list; - for (m != nil) { - asserttyped(c, m, false); - m = m.next; - }; -}; - -export fn checkinit(c: *checker, tc: *syntax.tctx) void = { - c.tc = tc; - c.top = syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - if (file.kind != syntax.nkind.N_FILE) { return; }; - c.file = file; - - // #80: under -T, PREPEND the synth `use test;` BEFORE Pass 1 so declmod - // (called per-decl during install) sees a matching `use test` when it - // keys lib/test's `run` — keying it under mod="test", not "". This is a - // pure ORDER fix: the synth N_USE was appended AFTER install (below), - // too late for declmod, so `run` keyed under "" — leaving the synth - // `test.run` ty_err (the E1 bridge) and colliding with a user root - // `fn run` (also mod=""). Decoupled from cgen: the symbol mangle keys - // off d.module (the //ww:module directive, cgen mod_collect), NOT this - // scope keying — cgen already emits the correct CALL test.run. Twin of - // cstage cmd/wcc/check.c. - if (c.istest != 0) { - let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col); - usenode.str = "test"; - usenode.usepath = "test"; - usenode.next = file.list; - file.list = usenode; - }; - - // Pass 1: install all top-level names. - let d: *syntax.node = file.list; - for (d != nil) { - installdecl(c, file, d); - d = d.next; - }; - - // Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is - // the ROOT-unit main (imported==0) — it alone lowers to the bare - // `main` symbol. An IMPORTED package's `main` (imported==1) mangles on - // its path (`foo.bar.main`) and may coexist, closing the old dup-main - // collision by construction (#31). Two ROOT entries still collide on - // the bare symbol → reject loud (rule 7). Walks USER decls only — runs - // before the -T synth main is appended below. Twin of cmd/wcc/check.c. - let firstmain: *syntax.node = nil; - let mm: *syntax.node = file.list; - for (mm != nil) { - let ismain: bool = (mm.kind == syntax.nkind.N_FNDECL - || mm.kind == syntax.nkind.N_LET || mm.kind == syntax.nkind.N_DEF - || mm.kind == syntax.nkind.N_TYPEDECL) && syntax.streq(mm.str, "main"); - if (ismain && mm.imported == 0) { - if (firstmain == nil) { - firstmain = mm; - } else { - cerr(mm.file); - cerr(": error: duplicate entry main: only one root main may exist (#32)\n"); - c.errs += 1; - }; - }; - mm = mm.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: *syntax.node = file.list; - for (u != nil) { - if (u.kind == syntax.nkind.N_FNDECL && u.body != nil - && syntax.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 (syntax.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: *syntax.node = nil; - let rtail: *syntax.node = nil; - let ntest: i32 = 0; - let t: *syntax.node = file.list; - for (t != nil) { - if (t.kind == syntax.nkind.N_FNDECL) { - let istest: bool = false; - let at: *syntax.node = t.attr; - for (at != nil) { - if (at.kind == syntax.nkind.N_ATTR - && syntax.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 == syntax.nkind.N_TNAME - && syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_STRLIT, pf, pl, pc); - nm.str = t.str; - let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - id.str = t.str; - let amp: *syntax.node = syntax.newnode(syntax.nkind.N_UN, pf, pl, pc); - amp.op = syntax.tkind.TK_AMP; - amp.lhs = id; - let row: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, pf, pl, pc); - let tab: *syntax.node = nil; - if (ntest == 0i32) { - // no @test fns in this unit — exit 0, nothing to run. - let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); - let zero: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc); - e0.str = "str"; - let p0: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc); - p0.lhs = e0; - let vret: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc); - vret.str = "void"; - let vfn: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, pf, pl, pc); - vfn.lhs = vret; - let e1: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, pf, pl, pc); - e1.lhs = vfn; - let p1: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc); - p1.lhs = e1; - let tup: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, pf, pl, pc); - tup.list = p0; - p0.next = p1; - let tsl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, pf, pl, pc); - tsl.lhs = tup; - let arr: *syntax.node = syntax.newnode(syntax.nkind.N_ARRLIT, pf, pl, pc); - arr.list = rhead; - tab = syntax.newnode(syntax.nkind.N_LET, pf, pl, pc); - tab.op = syntax.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. - syntax.scopedefineinmodule(c.top, tab.str, "", syntax.skind.SK_VAR, nil, tab); - - let arg: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - arg.str = "__wwtests"; - let call: *syntax.node = syntax.newnode(syntax.nkind.N_CALL, pf, pl, pc); - // QUALIFIED test.run (N_DOT base ident `test`, member `run`): - // the sep producer sees lib/test as a real imported package, - // so the call must carry the module qualifier; the combined - // path tags auto-bundled lib/test `//ww:module test` too, so - // it resolves+mangles identically (test.run). Cstage twin: - // cmd/wcc/check.c synth. The base ident binds through the - // synth N_USE below. - let dot: *syntax.node = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc); - let did: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); - did.str = "test"; - dot.lhs = did; - dot.str = "run"; - call.lhs = dot; - call.list = arg; - let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc); - ret.lhs = call; - body.list = ret; - // #80: the synth `use test;` (the N_DOT base qualifier + the - // usepathfor source mapping `test`→its path) is now PREPENDED - // before Pass 1 at the top of checkfile, so declmod keys - // lib/test's `run` under "test" and the synth `test.run` - // type-resolves. It is installed (SK_USE) by Pass 1's N_USE arm. - }; - let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc); - m.str = "main"; - m.exported = 1i32; - let rety: *syntax.node = syntax.newnode(syntax.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: *syntax.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: syntax.nkind = d.kind; - switch (k) { - case syntax.nkind.N_FNDECL: - if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type - resolvefnbody(c, d); - case syntax.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 syntax.nkind.N_TYPEDECL: - if (d.lhs != nil) { resolvewalk(c, d.lhs); }; - case syntax.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: *syntax.node = nil; - let e: *syntax.node = file.list; - for (e != nil) { - let istest: bool = false; - if (e.kind == syntax.nkind.N_FNDECL) { - let at: *syntax.node = e.attr; - for (at != nil) { - if (at.kind == syntax.nkind.N_ATTR - && syntax.streq(at.str, "test")) { - istest = true; - }; - at = at.next; - }; - }; - let nx: *syntax.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; - -}; - -//ww:module io -// 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; - -//ww:module io -// 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; -}; - -//ww:module errors -// 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); -}; - -//ww:module io -// 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); - -//ww:module memio -// 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; a write to a full buffer returns nomem, matching Hare -// (ref/hare/memio/stream.ha:44,161). A zero-length write always -// succeeds with 0 (the empty-input short-circuit, stream.ha:157). -// -// 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 (buf.len == 0) { return 0: size; }; // ref/hare/memio/stream.ha:157 - if (m.pos >= m.len) { // ref/hare/memio/stream.ha:161 - let nm: nomem; - let e: io.error = nm; - return e; - }; - 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; -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node) *syntax.node = { - let s: *syntax.node = syntax.newnode(syntax.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: *syntax.node, nfixed_out: *i32) *syntax.node = { - *nfixed_out = 0; - let p: *syntax.node = ps; - for (p != nil) { - if (p.kind == syntax.nkind.N_PARAM) { - if (p.op == syntax.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: *syntax.node, nfixed_out: *i32) *syntax.node = { - *nfixed_out = 0; - if (callee == nil) { return nil; }; - let ps: *syntax.node = nil; - if (callee.kind == syntax.nkind.N_IDENT) { - if (callee.str.len == 0) { return nil; }; - ps = fnparamslookup(c, callee.str); - } else { if (callee.kind == syntax.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 == syntax.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: *syntax.node, param: *syntax.node) i32 = { - if (param == nil) { return 0; }; - if (param.kind != syntax.nkind.N_PARAM) { return 0; }; - if (param.op == syntax.tkind.TK_ELLIPSIS) { return 0; }; - let ptype: *syntax.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_: *syntax.tinfo) > 0) { return 0; }; - let arg: *syntax.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 == syntax.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 == syntax.nkind.N_INDEX) { - if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; - }; - if (arg.kind == syntax.nkind.N_DOT) { - if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; - }; - if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.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: *syntax.node, param: *syntax.node, memphase: bool) i32 = { - if (arg == nil) { return 0; }; - let nextparam: *syntax.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: *syntax.node = nil; - if (param != nil) { if (param.kind == syntax.nkind.N_PARAM) { - if (param.op != syntax.tkind.TK_ELLIPSIS) { - memptype = param.lhs; - if (memptype != nil) { - memsz = taggedmemargsize(memptype.type_: *syntax.tinfo); - }; - }; - }; }; - if (memsz == 0) { - memsz = taggedmemargsize(arg.type_: *syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - if (memptype != nil) { - let pt: *syntax.tinfo = memptype.type_: *syntax.tinfo; - let pu: *syntax.tinfo = pt; - pu = tichase(pu); - let au: *syntax.tinfo = at; - au = tichase(au); - if (pu != nil && pu == au) { same = true; }; - if (!same && pt != nil && at != nil) { - if (syntax.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_: *syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - widensz = 0; - } else { - let ptype: *syntax.node = param.lhs; - if (istaggedtype(c, ptype)) { - let aistagged: bool = false; - if (arg.kind == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) { - if (istaggedtype(c, arg)) { - if (slotsize(c, arg) == slotsize(c, ptype)) { - aistagged = true; - }; - }; - }; - if (!aistagged) { - widensz = slotsize(c, ptype); - let tagged: *syntax.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: *syntax.tinfo = arg.type_: *syntax.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 == syntax.tykind.TY_TUPLE) { - argistuple = true; - }; - if (argtup.kind == syntax.tykind.TY_TAGGED) { - argistagged = true; - }; - }; - let pname: str = rhsstructpayload(c, arg); - if (pname.len > 0 || argistuple || argistagged) { - let ptype: *syntax.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_: *syntax.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 == syntax.nkind.N_SLICE) { - let base: *syntax.node = arg.lhs; - let lo: *syntax.node = arg.rhs; - let hi: *syntax.node = arg.cond; - let baselocal: *local = nil; - let globaltn: *syntax.node = nil; - let globalname: str; - globalname.ptr = nil; globalname.len = 0; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - baselocal = localfindnode(c, bn); - if (baselocal == nil) { - let gt: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - dotbu = base.type_: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.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: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - // base address → push - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - let isarr60: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { isarr60 = true; }; - }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (bu60 != nil) { isarr60 = bu60.kind == syntax.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 == syntax.nkind.N_TARRAY; - // #60: alias-NAMED base — chased kind; runtime- - // unreachable until #77/#78 global DATA. - if (bu60 != nil) { gisarr60 = bu60.kind == syntax.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: *syntax.node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - }; - }; - } else { if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - } else { if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.tykind.TY_STR) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - };}; - }; - } else { if (globaltn != nil) { - if (globaltn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = globaltn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(globaltn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - }; - }; - } else { if (globaltn.kind == syntax.nkind.N_TSLICE) { - emitline("\tLEAQ\t"); - emitsymname(c, globalname); - emitline("(SB), CX\n"); - emitline("\tMOVQ\t8(CX), AX\n"); - } else { if (globaltn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - gst = tichase(gst); - if (gst != nil) { if (gst.kind == syntax.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 == syntax.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 == syntax.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_: *syntax.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 == syntax.nkind.N_IDENT) { - let st: *syntax.tinfo = arg.type_: *syntax.tinfo; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.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 == syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - if (syntax.typeisf32(at)) { fk = 1; } - else { if (syntax.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: *syntax.node = nil; - if (arg.kind == syntax.nkind.N_TUPLE) { if (param != nil) { - if (param.kind == syntax.nkind.N_PARAM && param.lhs != nil) { - let ptn: *syntax.node = param.lhs; - for (ptn != nil && ptn.kind == syntax.nkind.N_TNAME) { - ptn = aliaslookup(c, ptn.str); - }; - if (ptn != nil) { if (ptn.kind == syntax.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: *syntax.node = paramtt; - if (tuparg == nil) { tuparg = nodetuplearg(c, arg); }; - if (tuparg != nil) { - let tuplit: bool = false; - if (paramtt == nil) { if (tuparg.kind == syntax.nkind.N_TUPLE) { tuplit = true; }; }; - let gptot: i32 = 0; - let sstot: i32 = 0; - let tsz: i32 = 0; - let p: *syntax.node = tuparg.list; - for (p != nil) { - let et: *syntax.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: *syntax.tinfo = et.type_: *syntax.tinfo; - eti = tichase(eti); - if (eti != nil) { - let bad: bool = eti.kind == syntax.tykind.TY_TUPLE - || eti.kind == syntax.tykind.TY_STRUCT - || eti.kind == syntax.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 == syntax.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: *syntax.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: *syntax.tinfo = arg.type_: *syntax.tinfo; - ati = tichase(ati); - if (ati != nil) { - if (ati.kind == syntax.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 == syntax.nkind.N_INDEX || arg.kind == syntax.nkind.N_DOT - || (arg.kind == syntax.nkind.N_UN && arg.op == syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - if (n.kind != syntax.nkind.N_CALL) { return 0; }; - let callee: *syntax.node = n.lhs; - if (callee == nil) { return 0; }; - if (callee.kind != syntax.nkind.N_IDENT) { return 0; }; - let rtyp: *syntax.node = fnretlookup(c, callee.str); - if (!istaggedtype(c, rtyp)) { return 0; }; - return slotsize(c, rtyp); -}; - -fn nodeisslice(c: *cgen, n: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.nkind = n.kind; - if (k == syntax.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 syntax.typeisslice(n.type_: *syntax.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 syntax.typeisslice(n.type_: *syntax.tinfo); - }; - return false; - }; - if (k == syntax.nkind.N_SLICE) { return true; }; - if (k == syntax.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 == syntax.nkind.N_CALL) { - let callee: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod); - return isslicetype(c, rtyp); - }; - if (callee.kind == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - let rtyp: *syntax.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 == syntax.nkind.N_DOT) { - return syntax.typeisslice(n.type_: *syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisslice(n.type_: *syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.nkind = n.kind; - if (k == syntax.nkind.N_STRLIT) { return true; }; - if (k == syntax.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 syntax.typeisstr(n.type_: *syntax.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 syntax.typeisstr(n.type_: *syntax.tinfo); - }; - return false; - }; - if (k == syntax.nkind.N_CALL) { - let callee: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - let rtyp: *syntax.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 == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - let rtyp: *syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisstr(n.type_: *syntax.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 == syntax.nkind.N_DOT) { - return syntax.typeisstr(n.type_: *syntax.tinfo); - }; - if (k == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeis8byteprim(t.type_: *syntax.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: *syntax.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: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). - let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - // idxeffti = TY_NAMED peel + the `*[N]T` drill (#61). - let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti == nil) { return false; }; - return syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let k: syntax.nkind = t.kind; - let elem: *syntax.node = nil; - if (k == syntax.nkind.N_TPTR) { elem = t.lhs; }; - if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; }; - if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; }; - if (elem == nil) { return false; }; - if (k == syntax.nkind.N_TPTR) { - if (elem.kind == syntax.nkind.N_TARRAY) { - if (elem.lhs != nil) { elem = elem.lhs; }; - }; - }; - return elem.kind == syntax.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: *syntax.tinfo) *syntax.tinfo = { - let t: *syntax.tinfo = t0; - // peel-ok: chase body - for (t != nil && t.kind == syntax.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: *syntax.tinfo) bool = { - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return false; }; - return u.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeissigned(t.type_: *syntax.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: *syntax.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: *syntax.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: *syntax.node) str = { - if (tnode == nil) { return "MOVQ"; }; - let ti: *syntax.tinfo = tnode.type_: *syntax.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 = syntax.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: *syntax.tinfo) *syntax.tinfo = { - let t: *syntax.tinfo = t0; - t = tichase(t); - if (t != nil && t.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = t.sub; - p = tichase(p); - if (p != nil && p.kind == syntax.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: *syntax.node) *syntax.node = { - if (tn == nil) { return nil; }; - let k: syntax.nkind = tn.kind; - if (k != syntax.nkind.N_TARRAY && k != syntax.nkind.N_TSLICE - && k != syntax.nkind.N_TPTR) { return nil; }; - let elem: *syntax.node = tn.lhs; - if (k == syntax.nkind.N_TPTR && elem != nil - && elem.kind == syntax.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: *syntax.node) i32 = { - if (t == nil) { return 1; }; - let k: syntax.nkind = t.kind; - let elem: *syntax.node = nil; - if (k == syntax.nkind.N_TPTR) { elem = t.lhs; }; - if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; }; - if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; }; - if (k == syntax.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 (syntax.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 == syntax.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 == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; - if (elem.kind == syntax.nkind.N_TNAME) { - let nm: str = elem.str; - // str element is 16B (ptr+len). primsize returns 0 for it. - if (syntax.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: *syntax.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: syntax.nkind = t.kind; - let nest: *syntax.node = nil; - if (nk == syntax.nkind.N_TSLICE) { nest = t.lhs; }; - if (nk == syntax.nkind.N_TARRAY) { nest = t.lhs; }; - if (nest != nil && nest.kind == syntax.nkind.N_TARRAY) { - let eti: *syntax.tinfo = nest.type_: *syntax.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 == syntax.nkind.N_TNAME) { - let eff: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (eff != nil) { - if (eff.kind == syntax.tykind.TY_ARRAY - || eff.kind == syntax.tykind.TY_SLICE) { - let aes: *syntax.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: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo); - if (ti != nil) { - let esub: *syntax.tinfo = ti.sub; - esub = tichase(esub); - if (esub != nil) { return esub.size: i32; }; - }; - let elem: *syntax.node = idxelemtn(t); - if (elem == nil) { return direct; }; - if (elem.kind == syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let k: syntax.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 == syntax.nkind.N_IDENT) { - return syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_DOT) { - return syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_CAST) { - if (n.rhs == nil) { return false; }; - return syntax.typeisunsigned(n.rhs.type_: *syntax.tinfo); - }; - if (k == syntax.nkind.N_BIN) { - if (nodeisunsigned(c, n.lhs)) { return true; }; - return nodeisunsigned(c, n.rhs); - }; - if (k == syntax.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 == syntax.nkind.N_INDEX) { - return syntax.typeisunsigned(n.type_: *syntax.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 == syntax.nkind.N_CALL) { - return syntax.typeisunsigned(n.type_: *syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - let k: syntax.nkind = n.kind; - if (k == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, n.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); }; - }; - }; - return 0; - }; - if (k == syntax.nkind.N_CAST) { - let tn: *syntax.node = n.rhs; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); }; - }; - return 0; - }; - if (k == syntax.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: *syntax.tinfo = fi.tnode.type_: *syntax.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: *syntax.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 != syntax.nkind.N_TNAME && tn.kind != syntax.nkind.N_IDENT) { return nil; }; - let si: *structinfo = structlookup(c, tn.str); - if (si != nil) { return si; }; - let cur: *syntax.node = tn; - for (cur != nil - && (cur.kind == syntax.nkind.N_TNAME || cur.kind == syntax.nkind.N_IDENT) - && si == nil) { - let aliased: *syntax.node = aliaslookup(c, cur.str); - if (aliased == nil) { cur = nil; } - else { - if (aliased.kind == syntax.nkind.N_TNAME) { - si = structlookup(c, aliased.str); - cur = aliased; - } else { cur = nil; }; - }; - }; - return si; -}; - -export fn sretretsize(c: *cgen, t: *syntax.node) i32 = { - if (t == nil) { return 0; }; - let r: *syntax.node = t; - if (r.kind == syntax.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 == syntax.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: *syntax.node = r.list; - for (pt != nil) { - let et: *syntax.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: *syntax.tinfo = r.type_: *syntax.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 == syntax.nkind.N_TARRAY) { - let ati: *syntax.tinfo = r.type_: *syntax.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 != syntax.nkind.N_TNAME) { return 0; }; - // Primitives / aliased-to-primitives are never sret. - if (aliasprimsize(c, r.str) > 0) { return 0; }; - if (syntax.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: *syntax.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: *syntax.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: *syntax.node) i32 = { - if (n == nil) { return 0; }; - if (n.kind != syntax.nkind.N_CALL) { return 0; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cn = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cn = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cn.len == 0) { return 0; }; - let rtyp: *syntax.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 (syntax.streq(s.sname, name)) { - if (syntax.streq(s.smod, c.curmod)) { return s; }; - }; - s = s.sinext; - }; - s = c.structs; - for (s != nil) { - let sn: str = s.sname; - if (syntax.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); - // M1 #22: the embedded qualifier (`utf8`) is the use ALIAS; - // the struct's smod is now the dotted import PATH - // (`encoding.utf8`). Map alias→path before comparing. - let pkgmod: str = usehint(c, pkg); - let b: *structinfo = c.structs; - for (b != nil) { - if (syntax.streq(b.sname, leaf)) { - if (syntax.streq(b.smod, pkgmod)) { - 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 (syntax.streq(s.sname, name)) { - if (syntax.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 (syntax.streq(name, "u8")) { return 1; }; - if (syntax.streq(name, "i8")) { return 1; }; - if (syntax.streq(name, "bool")) { return 1; }; - if (syntax.streq(name, "u16")) { return 2; }; - if (syntax.streq(name, "i16")) { return 2; }; - if (syntax.streq(name, "u32")) { return 4; }; - if (syntax.streq(name, "i32")) { return 4; }; - if (syntax.streq(name, "f32")) { return 4; }; - if (syntax.streq(name, "u64")) { return 8; }; - if (syntax.streq(name, "i64")) { return 8; }; - if (syntax.streq(name, "uint")) { return 8; }; - if (syntax.streq(name, "int")) { return 8; }; - if (syntax.streq(name, "uintptr")) { return 8; }; - if (syntax.streq(name, "size")) { return 8; }; - if (syntax.streq(name, "f64")) { return 8; }; - if (syntax.streq(name, "rune")) { return 4; }; - if (syntax.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: *syntax.node = aliaslookup(c, nm); - for (cur != nil) { - if (cur.kind != syntax.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: *syntax.node, - sz_out: *i32, unsigned_out: *bool) void = { - *sz_out = 0; - *unsigned_out = false; - let cur: *syntax.node = t; - for (cur != nil) { - let k: syntax.nkind = cur.kind; - if (k == syntax.nkind.N_TBANG) { cur = cur.lhs; } - else { if (k == syntax.nkind.N_TENUM) { cur = cur.lhs; } - else { if (k == syntax.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 (syntax.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 = syntax.typeisunsigned(cur.type_: *syntax.tinfo); - return; - }; - let al: *syntax.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: *syntax.node, - sz_out: *i32, unsigned_out: *bool) void = { - *sz_out = 0; - *unsigned_out = false; - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - if (k == syntax.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 = syntax.typeisunsigned(n.type_: *syntax.tinfo); - }; - }; - return; - }; - if (k == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, n.str); - if (lc != nil) { - typenodeprimresolved(c, lc.tnode, - sz_out, unsigned_out); - }; - return; - }; - if (k == syntax.nkind.N_CAST) { - typenodeprimresolved(c, n.rhs, sz_out, unsigned_out); - return; - }; - if (k == syntax.nkind.N_UN) { - exprprimresolved(c, n.lhs, sz_out, unsigned_out); - return; - }; - if (k == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { bu = n.lhs.type_: *syntax.tinfo; }; - bu = tichase(bu); - if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = tichase(bu.sub); }; - if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) { - let u: *syntax.tinfo = n.type_: *syntax.tinfo; - u = tichase(u); - if (syntax.typeisint(u)) { - *sz_out = u.size: i32; - *unsigned_out = syntax.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 (syntax.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 (syntax.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 (syntax.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: *syntax.node) *syntax.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: *syntax.node = rhs; - if (rhs.kind == syntax.nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; }; - if (rhs.kind == syntax.nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; }; - if (call == nil) { return nil; }; - if (call.kind != syntax.nkind.N_CALL) { return nil; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cname = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cname = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cname.len == 0) { return nil; }; - let rtyp: *syntax.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 != syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = tn.type_: *syntax.tinfo; - if (ti != nil) { ti = tichase(ti); }; - if (ti != nil && ti.kind == syntax.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: *syntax.node) i32 = { - if (typn == nil) { return 8; }; - let ti: *syntax.tinfo = typn.type_: *syntax.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: syntax.tykind = ti.kind; - if (kk == syntax.tykind.TY_VOID) { return 0; }; - if (kk == syntax.tykind.TY_PTR || kk == syntax.tykind.TY_SLICE || - kk == syntax.tykind.TY_CHAN || kk == syntax.tykind.TY_FN || - kk == syntax.tykind.TY_STR || kk == syntax.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 == syntax.tykind.TY_STRUCT) { - return ((ti.size + 7u64) & ~7u64): i32; - }; - if (kk == syntax.tykind.TY_TUPLE || kk == syntax.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: *syntax.node) i32 = { - if (tnode == nil) { return 8; }; - let ti: *syntax.tinfo = tnode.type_: *syntax.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: syntax.tykind = ti.kind; - if (k == syntax.tykind.TY_STRUCT) { return ti.slotsize: i32; }; - if (k == syntax.tykind.TY_ARRAY) { return ti.slotsize: i32; }; - if (k == syntax.tykind.TY_TAGGED) { return ti.size: i32; }; - if (k == syntax.tykind.TY_SLICE) { return ti.size: i32; }; - if (k == syntax.tykind.TY_PTR || k == syntax.tykind.TY_FN || - k == syntax.tykind.TY_CHAN) { return 8; }; - if (k == syntax.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: *syntax.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: *syntax.tinfo = tichase(tstruct.type_: *syntax.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: *syntax.node = tstruct.list; - let tf: *syntax.tfield = ti.fields; - for (f != nil) { - if (f.kind == syntax.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: *syntax.node) void = { - c.structs = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.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 == syntax.nkind.N_TBANG) { - body = body.lhs; - }; - if (body != nil) { - if (body.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisstr(t.type_: *syntax.tinfo); -}; - -fn isslicetype(c: *cgen, t: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisslice(t.type_: *syntax.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: *syntax.node) *syntax.node = { - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return nil; }; - if (r.kind == syntax.nkind.N_TBANG) { - let inner: *syntax.node = r.lhs; - if (inner == nil) { return nil; }; - r = resolvetype(c, inner); - if (r == nil) { return nil; }; - }; - if (r.kind == syntax.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: *syntax.node) *syntax.node = { - if (scrut == nil) { return nil; }; - let k: syntax.nkind = scrut.kind; - if (k == syntax.nkind.N_IDENT) { return nil; }; - if (k == syntax.nkind.N_CALL) { - let callee: *syntax.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 == syntax.nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == syntax.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 == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cnm.len > 0) { - let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); - if (rtyp != nil) { return resolvetagged(c, rtyp); }; - }; - }; - return nil; - }; - if (k == syntax.nkind.N_INDEX) { - let ibase: *syntax.node = scrut.lhs; - if (ibase == nil) { return nil; }; - if (ibase.kind != syntax.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: *syntax.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: *syntax.node = idxelemtn(btn); - if (etn == nil) { return nil; }; - return resolvetagged(c, etn); - }; - if (k == syntax.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 == syntax.nkind.N_UN && scrut.op == syntax.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: *syntax.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: *syntax.node) i32 = { - if (c == nil) { return 0; }; - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return 0; }; - if (r.kind != syntax.nkind.N_TNAME) { return 0; }; - let nm: str = r.str; - if (syntax.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: *syntax.tinfo) i32 = { - if (t == nil) { return 0; }; - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return 0; }; - if (u.kind == syntax.tykind.TY_STRUCT || u.kind == syntax.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: *syntax.tinfo) i32 = { - if (t == nil) { return 0; }; - let u: *syntax.tinfo = t; - u = tichase(u); - if (u == nil) { return 0; }; - if (u.kind != syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - return aggargsizetn(n.type_: *syntax.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: *syntax.node) bool = { - if (n == nil) { return false; }; - let st: *syntax.tinfo = n.type_: *syntax.tinfo; - st = tichase(st); - if (st == nil) { return false; }; - if (st.kind != syntax.tykind.TY_STRUCT) { return false; }; - if (st.size: i32 > 16) { return false; }; - let f: *syntax.tfield = st.fields; - for (f != nil) { - if (syntax.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: *syntax.node) i32 = { - if (c == nil) { return 0; }; - let r: *syntax.node = resolvetype(c, t); - if (r == nil) { return 0; }; - if (r.kind != syntax.nkind.N_TNAME) { return 0; }; - let nm: str = r.str; - if (syntax.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: *syntax.node = resolvetype(c, fi.tnode); - if (rf != nil) { - if (rf.kind == syntax.nkind.N_TARRAY) { return 0; }; - if (rf.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeistagged(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - let u: *syntax.tinfo = t.type_: *syntax.tinfo; - u = tichase(u); - if (u == nil) { return false; }; - if (u.kind == syntax.tykind.TY_STRUCT) { return true; }; - if (u.kind == syntax.tykind.TY_ARRAY) { return true; }; - return u.kind == syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisfloat(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisf32(t.type_: *syntax.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: *syntax.node) bool = { - if (t == nil) { return false; }; - return syntax.typeisnullable(t.type_: *syntax.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: *syntax.node) i32 = { - if (t == nil) { return 0; }; - let ti: *syntax.tinfo = t.type_: *syntax.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 != syntax.tykind.TY_TAGGED) { return 0; }; - let p: *syntax.tparam = ti.params; - let i: i32 = 0; - for (p != nil) { - let vt: *syntax.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 == syntax.tykind.TY_NAMED) { vt = vt.under; }; - if (vt != nil) { - if (vt.kind == syntax.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: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo; - if (ti == nil) { return -1; }; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let p: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - if (vt != nil && vt.kind == syntax.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: *syntax.node, rhs: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - return taggedvariantindext(c, tagged.type_: *syntax.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: *syntax.tinfo, rhs: *syntax.node) i32 = { - if (du == nil) { return -1; }; - if (rhs == nil) { return -1; }; - let ti: *syntax.tinfo = du; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let r: i32 = flatvariantidxt(ti, rhs.type_: *syntax.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: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - let visstr: bool = syntax.typeisstr(vt); - let visslice: bool = syntax.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: *syntax.node, pat: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - if (pat == nil) { return -1; }; - return flatvariantidxt(tagged.type_: *syntax.tinfo, pat.type_: *syntax.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: *syntax.tinfo, want: *syntax.tinfo, exactonly: bool) i32 = { - if (want == nil) { return -1; }; - let ti: *syntax.tinfo = tagged; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.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: *syntax.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 == syntax.tykind.TY_NAMED) { - let sb: *syntax.tinfo = tichase(want); - if (sb == nil) { return -1; }; - let q: *syntax.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 && syntax.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 != syntax.tykind.TY_NAMED) { - let q: *syntax.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: *syntax.tinfo = q.type_; - if (pu != nil && pu.kind == syntax.tykind.TY_NAMED - && syntax.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: *syntax.tinfo, want: *syntax.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: *syntax.tinfo = tichase(dst); - if (du != nil && du.kind == syntax.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: *syntax.tparam = du.params; - for (p != nil) { - let pu: *syntax.tinfo = tichase(p.type_); - let nestedtagged: bool = false; - if (pu != nil) { - if (pu.kind == syntax.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 == syntax.tykind.TY_UNTYPED_INT) { return syntax.typeisnum(dst); }; - if (want.kind == syntax.tykind.TY_UNTYPED_FLOAT) { return syntax.typeisfloat(dst); }; - if (want.kind == syntax.tykind.TY_UNTYPED_STR) { - if (du == nil) { return false; }; - return du.kind == syntax.tykind.TY_STR; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_RUNE) { - if (syntax.typeisint(dst)) { return true; }; - return dst.kind == syntax.tykind.TY_RUNE; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_BOOL) { - if (du == nil) { return false; }; - return du.kind == syntax.tykind.TY_BOOL; - }; - if (want.kind == syntax.tykind.TY_UNTYPED_NIL) { - if (du == nil) { return false; }; - if (du.kind == syntax.tykind.TY_PTR) { return true; }; - if (du.kind == syntax.tykind.TY_SLICE) { return true; }; - if (du.kind == syntax.tykind.TY_CHAN) { return true; }; - return du.kind == syntax.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: *syntax.tinfo, want: *syntax.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 (syntax.typeisuntyped(want)) { return tyassignableuntyped(vt, want); }; - if (vt.kind == syntax.tykind.TY_NAMED && want.kind == syntax.tykind.TY_NAMED) { - return syntax.typeeq(vt, want); - }; - if (vt.kind == syntax.tykind.TY_NAMED || want.kind == syntax.tykind.TY_NAMED) { - let vu: *syntax.tinfo = vt; - vu = tichase(vu); - let wu: *syntax.tinfo = want; - wu = tichase(wu); - if (vu != nil && wu != nil - && vu.kind == syntax.tykind.TY_TAGGED - && wu.kind == syntax.tykind.TY_TAGGED) { - return syntax.typeeq(vu, wu); - }; - return false; - }; - return syntax.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: *syntax.tinfo, want: *syntax.tinfo) bool = { - let vu: *syntax.tinfo = vt; - vu = tichase(vu); - let wu: *syntax.tinfo = want; - wu = tichase(wu); - if (vu == nil) { return false; }; - if (wu == nil) { return false; }; - return syntax.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: *syntax.node, elem: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo; - ti = tichase(ti); - if (ti == nil) { return -1; }; - if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; }; - let want: *syntax.tinfo = nil; - if (elem != nil) { want = elem.type_: *syntax.tinfo; }; - let fallback: i32 = -1; - let p: *syntax.tparam = ti.params; - let idx: i32 = 0; - for (p != nil) { - let vt: *syntax.tinfo = p.type_; - if (vt != nil) { - if (syntax.typeisslice(vt)) { - if (fallback < 0) { fallback = idx; }; - if (want != nil) { - let su: *syntax.tinfo = vt; - su = tichase(su); - if (su != nil) { - if (syntax.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: *syntax.tinfo, su: *syntax.tinfo, slot_off: i32) void = { - let dt: *syntax.tinfo = du; - dt = tichase(dt); - if (dt == nil) { return; }; - if (dt.kind != syntax.tykind.TY_TAGGED) { return; }; - let st: *syntax.tinfo = su; - st = tichase(st); - if (st == nil) { return; }; - if (st.kind != syntax.tykind.TY_TAGGED) { return; }; - let identity: bool = true; - let p: *syntax.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: *syntax.node) str = { - let empty: str; - empty.ptr = nil; empty.len = 0; - if (src == nil) { return empty; }; - if (src.kind == syntax.nkind.N_STRUCTLIT) { - let trefn: *syntax.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 == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, src.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.node) *syntax.node = { - if (src == nil) { return nil; }; - if (src.kind != syntax.nkind.N_IDENT) { return nil; }; - let lc: *local = localfindnode(c, src.str); - if (lc == nil) { return nil; }; - let tn: *syntax.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: *syntax.node) bool = { - if (src == nil) { return false; }; - if (src.kind == syntax.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 (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; }; - return false; - }; - if (src.kind == syntax.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 (syntax.typeistagged(src.type_: *syntax.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 == syntax.nkind.N_DOT) { - if (syntax.typeistagged(src.type_: *syntax.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 == syntax.nkind.N_UN && src.op == syntax.tkind.TK_STAR) { - if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; }; - }; - if (src.kind == syntax.nkind.N_TRYPROP || src.kind == syntax.nkind.N_TRYUNW) { - if (syntax.typeistagged(src.type_: *syntax.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: *syntax.node) bool = { - if (e == nil) { return false; }; - if (e.kind == syntax.nkind.N_UN && e.op == syntax.tkind.TK_STAR) { - let du: *syntax.tinfo = e.type_: *syntax.tinfo; - du = tichase(du); - if (du == nil) { return false; }; - if (du.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (du.nullable != 0) { return false; }; - return du.size: i32 > 8; - }; - if (e.kind != syntax.nkind.N_INDEX && e.kind != syntax.nkind.N_DOT) { return false; }; - let u: *syntax.tinfo = e.type_: *syntax.tinfo; - u = tichase(u); - if (u == nil) { return false; }; - if (u.kind != syntax.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: *syntax.node) *syntax.node = { - for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) { - let cu: *syntax.tinfo = e.type_: *syntax.tinfo; - cu = tichase(cu); - if (cu == nil) { return e; }; - if (cu.kind != syntax.tykind.TY_TAGGED) { return e; }; - if (cu.nullable != 0) { return e; }; - let iu: *syntax.tinfo = e.lhs.type_: *syntax.tinfo; - iu = tichase(iu); - if (iu == nil) { return e; }; - if (iu.kind != syntax.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: *syntax.node) *syntax.node = { - for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) { - if (!syntax.typeeq(e.type_: *syntax.tinfo, e.lhs.type_: *syntax.tinfo)) { return e; }; - let cu: *syntax.tinfo = e.type_: *syntax.tinfo; - cu = tichase(cu); - if (cu == nil) { return e; }; - if (cu.kind != syntax.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: *syntax.tinfo, src: *syntax.node, - basereg: str, slot_off: i32, slot_sz: i32) void = { - if (syntax.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: *syntax.tinfo, src: *syntax.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: *syntax.tinfo = dst; - dt = tichase(dt); - if (dt == nil) { return; }; - if (dt.kind != syntax.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 == syntax.nkind.N_CAST) { - if (src.lhs != nil) { - let inner: *syntax.node = src.lhs; - let inneristagged: bool = false; - if (inner.kind == syntax.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: *syntax.node = src.rhs; - if (castrhs != nil) { - let castt: *syntax.tinfo = castrhs.type_: *syntax.tinfo; - let castu: *syntax.tinfo = castt; - castu = tichase(castu); - if (castu != nil) { - if (castu == dt) { - castisdst = true; - } else { if (castu.kind == syntax.tykind.TY_TAGGED) { - if (syntax.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: *syntax.tinfo = src.type_: *syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, src.str) == nil) { - let gtn51: *syntax.node = letvartnode(c, src.str); - if (gtn51 != nil) { if (istaggedtype(c, gtn51)) { srctagged = true; }; }; - }; - }; }; - if (srctagged) { - let nested: i32 = flatvariantidxt(dt, src.type_: *syntax.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: *syntax.tparam = dt.params; - for (gp != nil) { - if (cgvariantstructmatch(gp.type_, - src.type_: *syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.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_: *syntax.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 == syntax.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_: *syntax.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_: *syntax.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 == syntax.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 == syntax.nkind.N_CAST && su != nil - && su.kind == syntax.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: *syntax.node = nil; - let tupcast: bool = false; - if (src != nil) { - if (src.kind == syntax.nkind.N_TUPLE) { tupsrc = src; }; - if (src.kind == syntax.nkind.N_CAST) { - if (src.lhs != nil) { - if (src.lhs.kind == syntax.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 == syntax.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: *syntax.node = tupsrc.list; - for (bl != nil && !tupcast) { - let bare: bool = false; - if (bl.kind == syntax.nkind.N_TRUE) { bare = true; }; - if (bl.kind == syntax.nkind.N_FALSE) { bare = true; }; - if (bl.kind == syntax.nkind.N_INTLIT && bl.tsuffix.len == 0) { - bare = true; - }; - if (bl.kind == syntax.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_: *syntax.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: *syntax.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: *syntax.tinfo = ce.type_: *syntax.tinfo; - ceti = tichase(ceti); - if (ceti != nil && ceti.kind == syntax.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: *syntax.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: *syntax.tinfo = te.type_: *syntax.tinfo; - teti = tichase(teti); - if (teti != nil && teti.kind == syntax.tykind.TY_TAGGED) { - let ebox: i32 = (teti.size: i32 + 7) & ~7; - cgwidentaggedstore(c, te.type_: *syntax.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: *syntax.tinfo = te.type_: *syntax.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 == syntax.tykind.TY_TUPLE) { - let addrsrc: *syntax.node = src; - if (addrsrc.kind == syntax.nkind.N_CAST) { - if (addrsrc.lhs != nil) { addrsrc = addrsrc.lhs; }; - }; - let okkind: bool = false; - if (addrsrc.kind == syntax.nkind.N_IDENT) { okkind = true; }; - if (addrsrc.kind == syntax.nkind.N_INDEX) { okkind = true; }; - if (addrsrc.kind == syntax.nkind.N_UN) { - if (addrsrc.op == syntax.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_: *syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, src.str) == nil) { - let gtn: *syntax.node = letvartnode(c, src.str); - if (gtn != nil) { if (gtn.kind == syntax.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 == syntax.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: *syntax.tinfo = src.type_: *syntax.tinfo; - if (syntax.typeisf32(srct)) { fkind = 1; } - else { if (syntax.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: *syntax.tinfo = dt; - fti = tichase(fti); - if (fti != nil) { if (fti.kind == syntax.tykind.TY_TAGGED) { - let fp: *syntax.tparam = fti.params; - let fidx: i32 = 0; - for (fp != nil) { - let fvt: *syntax.tinfo = fp.type_; - fvt = tichase(fvt); - if (fvt != nil) { - if (syntax.typeisfloat(fvt)) { - if (syntax.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: *syntax.node, - outrootname: *str, outrootoff: *i32, outtotaloff: *i32, - outleaftype: **syntax.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 != syntax.nkind.N_DOT) { return false; }; - let stk: [16]*syntax.node; - let nsteps: i32 = 0; - let cur: *syntax.node = n; - for (cur != nil) { - if (cur.kind != syntax.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 != syntax.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 == syntax.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 == syntax.nkind.N_TPTR) { - let pe: *syntax.node = lc.tnode.lhs; - if (pe != nil) { - if (pe.kind == syntax.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: *syntax.tinfo = cur.type_: *syntax.tinfo; - curstruct = tichase(curstruct); - if (*outptrroot) { - if (curstruct == nil) { return false; }; - if (curstruct.kind != syntax.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 != syntax.tykind.TY_STRUCT) { return false; }; - if (stk[i] == nil) { return false; }; - let stepnm: str = stk[i].str; - let tf: *syntax.tfield = curstruct.fields; - let found: *syntax.tfield = nil; - for (tf != nil) { - if (syntax.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: *syntax.tinfo = found.type_; - ft = tichase(ft); - if (ft == nil) { return false; }; - if (ft.kind == syntax.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 (syntax.streq(pseudo, "ptr")) { delta = 0; } - else { if (syntax.streq(pseudo, "len")) { delta = 8; } - else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; }; - if (delta < 0) { return false; }; - *outtotaloff = *outtotaloff + foff; - *outslicedelta = delta; - return true; - }; - if (ft.kind == syntax.tykind.TY_SLICE) { - if (i != 1) { return false; }; - let pseudo: str = stk[0].str; - let delta: i32 = -1; - if (syntax.streq(pseudo, "ptr")) { delta = 0; } - else { if (syntax.streq(pseudo, "len")) { delta = 8; } - else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; }; - if (delta < 0) { return false; }; - *outtotaloff = *outtotaloff + foff; - *outslicedelta = delta; - return true; - }; - if (ft.kind != syntax.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: *syntax.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 == syntax.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: *syntax.node = lit.list; - for (fieldnode != nil) { - if (fieldnode.kind == syntax.nkind.N_FIELD) { - let fname: str = fieldnode.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.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_: *syntax.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 == syntax.nkind.N_STRUCTLIT) { - if (fi.tnode != nil) { - if (fi.tnode.kind == syntax.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 == syntax.nkind.N_CALL) { - if (fi.tnode != nil) { - if (fi.tnode.kind == syntax.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 == syntax.nkind.N_TARRAY - && fieldnode.lhs != nil - && fieldnode.lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = tichase(elemn.type_: *syntax.tinfo); - if (eu != nil) { - if (eu.kind == syntax.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 == syntax.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: *syntax.node = fieldnode.lhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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 == syntax.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: *syntax.tinfo = fi.tnode.type_: *syntax.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: *syntax.node, bpoff: i32) void = { - if (si == nil) { return; }; - cgstructlitfill(c, si, lit, 0, 0, "", bpoff); -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - - switch (k) { - case syntax.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 syntax.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 syntax.nkind.N_RUNELIT: - emitline("\tMOVQ\t$"); - emitint(n.uval: i64); - emitline(", AX\n"); - return; - case syntax.nkind.N_STRLIT: cgstrlit(c, n); return; - case syntax.nkind.N_TRUE: - emitline("\tMOVQ\t$1, AX\n"); - return; - case syntax.nkind.N_FALSE: - emitline("\tMOVQ\t$0, AX\n"); - return; - case syntax.nkind.N_NIL: - emitline("\tMOVQ\t$0, AX\n"); - return; - case syntax.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 syntax.nkind.N_IDENT: cgident(c, n); return; - case syntax.nkind.N_INDEX: cgindex(c, n); return; - case syntax.nkind.N_SLICE: cgslice(c, n); return; - case syntax.nkind.N_MATCH: cgmatch(c, n); return; - case syntax.nkind.N_CAST: cgcast(c, n); return; - case syntax.nkind.N_DOT: cgdot(c, n); return; - case syntax.nkind.N_UN: cgun(c, n); return; - case syntax.nkind.N_BIN: cgbin(c, n); return; - case syntax.nkind.N_CALL: cgcall(c, n); return; - case syntax.nkind.N_ASSIGN: cgassign(c, n); return; - case syntax.nkind.N_TRYPROP: cgtryprop(c, n); return; - case syntax.nkind.N_TRYUNW: cgtryunw(c, n); return; - case syntax.nkind.N_TYPETEST: cgtypetest(c, n); return; - case syntax.nkind.N_TYPEASSERT: cgtypeassert(c, n); return; - case syntax.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: *syntax.node, vt: *syntax.node) i32 = { - if (tagged == nil) { return -1; }; - if (vt == nil) { return -1; }; - if (tagged.kind != syntax.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: *syntax.tinfo = tagged.type_: *syntax.tinfo; - sti = tichase(sti); - if (sti != nil && sti.kind == syntax.tykind.TY_TAGGED && vt.type_ != nil) { - return flatvariantidxt(sti, vt.type_: *syntax.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 == syntax.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: *syntax.tinfo) i64 = { - let u: *syntax.tinfo = tichase(ou); - if (u == nil) { return 0i64; }; - if (u.kind != syntax.tykind.TY_TAGGED) { return 0i64; }; - let haserr: bool = false; - let p: *syntax.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: *syntax.tinfo) *syntax.tinfo = { - let u: *syntax.tinfo = tichase(ou); - if (u == nil) { return nil; }; - if (u.kind != syntax.tykind.TY_TAGGED) { return nil; }; - let st: i64 = successtag(ou); - let idx: i64 = 0i64; - let p: *syntax.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: *syntax.node) bool = { - if (n.lhs == nil) { return false; }; - let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - ou = tichase(ou); - if (ou == nil) { return false; }; - if (ou.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (ou.params == nil) { return false; }; - let sv: *syntax.tinfo = successvariant(ou); - sv = tichase(sv); - if (sv == nil) { return false; }; - if (sv.kind != syntax.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: *syntax.node) bool = { - if (n.lhs == nil) { return false; }; - let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - ou = tichase(ou); - if (ou == nil) { return false; }; - if (ou.kind != syntax.tykind.TY_TAGGED) { return false; }; - if (ou.params == nil) { return false; }; - let sv: *syntax.tinfo = successvariant(ou); - sv = tichase(sv); - if (sv == nil) { return false; }; - if (sv.kind != syntax.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: syntax.tkind, unsignd: bool) void = { - if (op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_MINUSEQ) { - emitline("\tSUBQ\tAX, BX\n"); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - if (op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tBX, AX\n"); return; }; - if (op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tBX, AX\n"); return; }; - if (op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tMOVQ\tBX, AX\n"); - emitline("\tSHLQ\tCX, AX\n"); - return; - }; - if (op == syntax.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: *syntax.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: *syntax.node, opname: str) void = { - let u: *syntax.tinfo = nil; - if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; }; - u = tichase(u); - let utag: bool = false; - if (u != nil) { - if (u.kind == syntax.tykind.TY_TAGGED && u.nullable == 0) { - utag = true; - }; - }; - if (utag && u.size: i32 > TUPLE_GPCAP * 8 - && n.lhs.kind != syntax.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 == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { propu = n.lhs.type_: *syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; }; - u = tichase(u); - let r: *syntax.tinfo = nil; - if (c.fnret != nil) { r = c.fnret.type_: *syntax.tinfo; }; - r = tichase(r); - if (u != nil && r != nil && r.kind == syntax.tykind.TY_TAGGED && u.params != nil) { - let propret: str; - propret.ptr = nil; propret.len = 0; - let haveret: bool = false; - let p: *syntax.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 = syntax.typeisstr(successvariant(n.lhs.type_: *syntax.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: *syntax.node) void = { - // #38b residual (rule 7): see the cgtryprop twin. - if (n.lhs != nil) { - if (n.lhs.kind == syntax.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: *syntax.tinfo = nil; - if (n.lhs != nil) { unwu = n.lhs.type_: *syntax.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 = syntax.typeisstr(successvariant(n.lhs.type_: *syntax.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: *syntax.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: *syntax.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 == syntax.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: *syntax.node = nil; - let nonident: bool = false; - let globalident: bool = false; - if (lhs != nil) { - if (lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - icu = tichase(icu); - if (lhs.kind == syntax.nkind.N_CAST && icu != nil - && icu.kind == syntax.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: *syntax.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 == syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = nil; - if (lhs != nil) { su = tichase(lhs.type_: *syntax.tinfo); }; - let vu: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - let lenum: bool = false; - let renum: bool = false; - if (su != nil) { if (su.kind == syntax.tykind.TY_ENUM) { lenum = true; }; }; - if (vu != nil) { if (vu.kind == syntax.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 == syntax.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: *syntax.node = nil; - if (lhs != nil) { - if (lhs.kind == syntax.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: *syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - acu = tichase(acu); - if (lhs.kind == syntax.nkind.N_CAST && acu != nil - && acu.kind == syntax.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: *syntax.node) void = { - let srcfk: i32 = 0; - if (n.lhs != nil) { - let st: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(st)) { srcfk = 1; } - else { if (syntax.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: *syntax.node = n.rhs; - for (leaf_tn != nil) { - let lk: syntax.nkind = leaf_tn.kind; - if (lk == syntax.nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; } - else { if (lk == syntax.nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; } - else { if (lk == syntax.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: *syntax.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 == syntax.nkind.N_TNAME) { - is_bool = syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = nil; - if (lc.tnode != nil) { itu = lc.tnode.type_: *syntax.tinfo; }; - itu = tichase(itu); - if (itu != nil) { if (itu.kind == syntax.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: *syntax.node = deflookuprhs(c, nm); - if (drhs != nil) { - if (drhs.kind == syntax.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"); - emitfnname(c, nm, c.curmod); - emitline("(SB), CX\n"); - emitline("\t"); - emitline(mov); - emitline("\t(CX), X0\n"); - return; - }; - emitline("\tMOVQ\t"); - emitfnname(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: *syntax.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: *syntax.node = letvartnode(c, nm); - for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) { - gtt = aliaslookup(c, gtt.str); - }; - if (gtt != nil) { - if (gtt.kind == syntax.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"); - emitfnname(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: *syntax.node = nil; - let lv: *letvar = c.lets; - for (lv != nil) { - if (syntax.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 == syntax.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"); - emitfnname(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 (syntax.streq(glop, "MOVQ")) { - emitline("\tMOVQ\t"); - emitfnname(c, nm, c.curmod); - emitline("(SB), AX\n"); - } else { - emitline("\tLEAQ\t"); - emitfnname(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 (!syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; - if (!syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; - if (!syntax.streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; - if (syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; - if (syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; - if (syntax.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: *syntax.node, dstreg: str) bool = { - if (n == nil) { return false; }; - if (n.kind == syntax.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 != syntax.nkind.N_DOT) { return false; }; - let x: *syntax.node = n.lhs; - if (x == nil) { return false; }; - let xu: *syntax.tinfo = x.type_: *syntax.tinfo; - xu = tichase(xu); - if (xu == nil) { return false; }; - let xviaptr: bool = false; - let st: *syntax.tinfo = nil; - if (xu.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = xu.sub; - p = tichase(p); - if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) { - st = p; - xviaptr = true; - }; }; - } else { if (xu.kind == syntax.tykind.TY_STRUCT) { - st = xu; - }; }; - if (st == nil) { return false; }; - let f: *syntax.tfield = st.fields; - let foff: i64 = -1; - for (f != nil) { - if (syntax.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: *syntax.node, dstreg: str) bool = { - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_DOT) { return false; }; - let inner: *syntax.node = base.lhs; - if (inner == nil) { return false; }; - let chained: bool = (inner.kind == syntax.nkind.N_DOT); - if (inner.kind != syntax.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: *syntax.tinfo = tichase(inner.type_: *syntax.tinfo); - if (ibu == nil || ibu.kind == syntax.tykind.TY_ERR) { - let gt: *syntax.node = letvartnode(c, base.str); - if (gt != nil && gt.kind == syntax.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: *syntax.tinfo = inner.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - let viaptr: bool = false; - let structt: *syntax.tinfo = nil; - if (bu.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = bu.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - structt = st; - viaptr = true; - }; }; - } else { if (bu.kind == syntax.tykind.TY_STRUCT) { - structt = bu; - }; }; - if (structt == nil) { return false; }; - let f: *syntax.tfield = structt.fields; - let foff: i64 = -1; - let ft: *syntax.tinfo = nil; - for (f != nil) { - if (syntax.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 != syntax.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: *syntax.node, dst: str) bool = { - if (src.kind == syntax.nkind.N_UN) { - if (src.op == syntax.tkind.TK_STAR) { - cgexpr(c, src.lhs); - if (!syntax.streq(dst, "AX")) { - emitline("\tMOVQ\tAX, "); - emitline(dst); - emitline("\n"); - }; - return true; - }; - }; - if (src.kind == syntax.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 == syntax.nkind.N_DOT) { - return dotchainaddr(c, src, dst); - }; - if (src.kind == syntax.nkind.N_INDEX) { - let base: *syntax.node = src.lhs; - let idx: *syntax.node = src.rhs; - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_IDENT) { return false; }; - let bu: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - if (bu.kind != syntax.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 (!syntax.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: *syntax.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: *syntax.tinfo = tichase(src.type_: *syntax.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: *syntax.node, dstreg: str) bool = { - if (n == nil) { return false; }; - if (n.kind == syntax.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: *syntax.node = defvartnode(c, n.str); - if (dtn != nil) { - if (dtn.kind == syntax.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 == syntax.nkind.N_UN) { - if (n.op != syntax.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 == syntax.nkind.N_INDEX) { - let base: *syntax.node = n.lhs; - let idx: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - if (bu.kind != syntax.tykind.TY_SLICE && bu.kind != syntax.tykind.TY_ARRAY) { - return false; - }; - let et: *syntax.tinfo = n.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = n.lhs; - if (base == nil) { return false; }; - let bu: *syntax.tinfo = base.type_: *syntax.tinfo; - bu = tichase(bu); - if (bu == nil) { return false; }; - let viaptr: bool = false; - let st: *syntax.tinfo = nil; - if (bu.kind == syntax.tykind.TY_PTR) { - let p: *syntax.tinfo = bu.sub; - p = tichase(p); - if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) { - st = p; - viaptr = true; - }; }; - } else { if (bu.kind == syntax.tykind.TY_STRUCT) { - st = bu; - }; }; - if (st == nil) { return false; }; - let f: *syntax.tfield = st.fields; - let foff: i64 = -1; - for (f != nil) { - if (syntax.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: *syntax.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: *syntax.node = n.lhs; - let idx: *syntax.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 == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - if (et60 != nil) { - esz = et60.size: i32; - signed_elem = syntax.typeissigned(et60); - float_elem = syntax.typeisfloat(et60); - f32_elem = syntax.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: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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_: *syntax.tinfo); - }; - } else { if (base.kind == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - if (et != nil) { - esz = et.size: i32; - signed_elem = syntax.typeissigned(et); - elemisstr = syntax.typeisstr(et); - elemisslice = syntax.typeisslice(et); - float_elem = syntax.typeisfloat(et); - f32_elem = syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; signed_elem = syntax.typeissigned(dt); elemisstr = syntax.typeisstr(dt); elemisslice = syntax.typeisslice(dt); float_elem = syntax.typeisfloat(dt); f32_elem = syntax.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 == syntax.nkind.N_IDENT) { - let bl: *local = baselocal; - let etn: *syntax.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 != syntax.nkind.N_IDENT) { - let dt: *syntax.tinfo = n.type_: *syntax.tinfo; - if (dt != nil) { - if (syntax.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: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); - if (eti != nil) { if (eti.kind == syntax.tykind.TY_TUPLE) { - elem_tuple = true; - // ww tuples store elements in .tupleelems, not .params. - let tpw: *syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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: *syntax.node, dst: str) bool = { - if (base == nil) { return false; }; - if (base.kind != syntax.nkind.N_IDENT) { return false; }; - let baselocal: *local = localfindnode(c, base.str); - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - if (tn == nil) { return false; }; - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - return false; - }; - if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 16): i64); - emitline("(BP), "); - emitline(dst); - emitline("\n"); - return true; - }; - if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.tykind.TY_NAMED) { - let bu60: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - if (bu60.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.tykind.TY_STR) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 16): i64); - emitline("(BP), "); - emitline(dst); - emitline("\n"); - return true; - }; - }; - };}; - return false; - }; - let gt: *syntax.node = letvartnode(c, base.str); - if (gt == nil) { return false; }; - if (gt.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = gt.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - return false; - }; - if (gt.kind == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - if (gbt60 != nil) { if (gbt60.kind == syntax.tykind.TY_NAMED) { - let gbu60: *syntax.tinfo = tichase(gbt60); - if (gbu60 != nil) { - if (gbu60.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(gbu60.alen: i64); - emitline(", "); - emitline(dst); - emitline("\n"); - return true; - }; - if (gbu60.kind == syntax.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: *syntax.node) i32 = { - if (base == nil) { return 1; }; - let esz: i32 = 1; - if (base.kind == syntax.nkind.N_IDENT) { - let bl: *local = localfindnode(c, base.str); - if (bl != nil) { - esz = elemsizeofc(c, bl.tnode); - } else { - let gt: *syntax.node = letvartnode(c, base.str); - if (gt != nil) { esz = elemsizeofc(c, gt); }; - }; - let bt: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt != nil) { if (bt.kind == syntax.tykind.TY_NAMED) { - let bu60: *syntax.tinfo = tichase(bt); - let es60: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - };}; - } else { if (base.kind == syntax.nkind.N_DOT) { - let db: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (db != nil) { if (db.sub != nil) { esz = db.sub.size: i32; }; }; - } else { if (base.kind == syntax.nkind.N_ARRLIT) { - esz = elemsizeofc(c, base.lhs); - };};}; - return esz; -}; - -fn cgslice(c: *cgen, n: *syntax.node) void = { - let base: *syntax.node = n.lhs; - let lo: *syntax.node = n.rhs; - let hi: *syntax.node = n.cond; - let baselocal: *local = nil; - let globaltn: *syntax.node = nil; - let globalname: str; - globalname.ptr = nil; globalname.len = 0; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - baselocal = localfindnode(c, base.str); - if (baselocal == nil) { - let gt: *syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - dotbu = base.type_: *syntax.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: *syntax.node = nil; - if (base != nil) { if (base.kind == syntax.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: *syntax.tinfo = nil; - if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { if (bt60.kind == syntax.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: *syntax.tinfo = tichase(bu60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - // base address - if (baselocal != nil) { - let tn: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; - }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (bu60 != nil) { isarray = bu60.kind == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node = baselocal.tnode; - let handled: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = tn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - handled = true; - }; - }; - } else { if (tn.kind == syntax.nkind.N_TSLICE) { - emitline("\tMOVQ\t"); - emitoff((baselocal.off + 8): i64); - emitline("(BP), AX\n"); - handled = true; - } else { if (tn.kind == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - handled = true; - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = globaltn.rhs; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(abt.alen: i64); - emitline(", AX\n"); - handled = true; - }; - }; - } else { if (globaltn.kind == syntax.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 == syntax.tykind.TY_ARRAY) { - emitline("\tMOVQ\t$"); - emitint(bu60.alen: i64); - emitline(", AX\n"); - handled = true; - } else { if (bu60.kind == syntax.tykind.TY_SLICE - || bu60.kind == syntax.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 == syntax.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 == syntax.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: *syntax.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: *syntax.node = taggedidcastpeel(c, n.lhs); - let scrutoff: i32 = 0; - let scrutt: *syntax.node = nil; - if (scrut != nil) { - if (scrut.kind == syntax.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: *syntax.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"); - emitfnname(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 { - // M1 (#25): match on a tagged field of a BARE-ident VALUE - // struct reads the box IN PLACE at base.off + field.offset - // — the box (tag@+0, word0@+8, word1@+16) is contiguous in - // the parent frame, so no @match_spill copy. Verbatim mirror - // of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241- - // 10296): gate on N_DOT with a bare-IDENT base whose stamped - // type chases to TY_STRUCT and whose field resolves by name. - // A *ptr-field base (`match (h.e)`, h:*struct) chases to - // TY_PTR, the by-name scan misses, and it falls to the spill - // arm below — exactly as cstage, no separate deref guard. - // #29: the in-place arm is also gated on a CONFIRMED-LOCAL - // base (bglobal below). A global VALUE-struct base - // (`match (g.field)`) has localfind==0, so the M1 base.off - // would land in the saved-BP/return-addr region — both - // stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The - // global-base predicate `localfind==0 && isletvar` (cstage - // twin: let_islet, cgen.c:2000) routes it through to the - // spill `else`, which resolves g(SB). align-DOWN to cstage - // (rule 10): both stages emit TEXT $32 on the local-field - // case and the same g(SB) spill on the global-field case. - let mfld: *syntax.tfield = nil; - if (scrut.kind == syntax.nkind.N_DOT && scrut.lhs != nil - && scrut.lhs.kind == syntax.nkind.N_IDENT - && scrut.lhs.type_ != nil) { - let bu: *syntax.tinfo = tichase(scrut.lhs.type_: *syntax.tinfo); - if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) { - let fl: *syntax.tfield = bu.fields; - for (fl != nil) { - if (syntax.streq(fl.name, scrut.str)) { - mfld = fl; - break; - }; - fl = fl.tnext; - }; - }; - }; - let bglobal: bool = false; - if (mfld != nil) { - bglobal = (localfind(c, scrut.lhs.str) == 0) - && isletvar(c, scrut.lhs.str); - }; - if (mfld != nil && !bglobal) { - let foff: i32 = mfld.offset: i32; - scrutoff = localfind(c, scrut.lhs.str) + foff; - scrutt = matchscrutt(c, scrut); - } 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 == syntax.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: *syntax.tinfo = scrut.type_: *syntax.tinfo; - ms37 = tichase(ms37); - if (ms37 != nil && ms37.kind == syntax.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 == syntax.nkind.N_CAST && ms37 != nil - && ms37.kind == syntax.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: *syntax.node = n.list; - for (cs != nil) { - let nxt: str = mklabel(c, "match_next"); - let pat: *syntax.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 == syntax.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: *syntax.tinfo = pat.type_: *syntax.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 (syntax.typeisslice(pattype)) { - r = flatslicevariantidx(c, scrutt, pat.lhs); - } else { - r = flatvariantidxt(scrutt.type_: *syntax.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 == syntax.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: *syntax.node) void = { - let lhs: *syntax.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: *syntax.node = lhs; - if (dotlhs != nil) { - if (dotlhs.kind == syntax.nkind.N_UN) { - if (dotlhs.op == syntax.tkind.TK_STAR) { - if (dotlhs.lhs != nil) { - if (dotlhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) { - etname = lhs.str; - }; - if (lhs.kind == syntax.nkind.N_DOT) { - if (lhs.lhs != nil) { - if (lhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) { - let nm: str = dotlhs.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - let tn: *syntax.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 == syntax.nkind.N_TNAME) { - if (structsamemod(c, tn.str) != nil) { break; }; - let nx: *syntax.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: syntax.nkind = tn.kind; - // Pointer-to-struct: deref then field load. - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.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 (syntax.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 == syntax.nkind.N_TARRAY) { - if (syntax.streq(fld, "ptr")) { - emitline("\tLEAQ\t"); - emitoff(lc.off: i64); - emitline("(BP), AX\n"); - return; - }; - if (syntax.streq(fld, "len")) { - let lenn: *syntax.node = tn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(fld); - if (idx >= 0) { - let tp: *syntax.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: *syntax.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: *syntax.tinfo = tpt.type_: *syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let innerkind: syntax.nkind = syntax.nkind.N_NONE; - if (inner != nil) { innerkind = inner.kind; }; - let innerstr: bool = false; - if (innerkind == syntax.nkind.N_TNAME) { - if (syntax.streq(inner.str, "str")) { innerstr = true; }; - }; - if (innerkind == syntax.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 == syntax.nkind.N_IDENT) { - let drhs: *syntax.node = deflookuprhs(c, lhs.str); - if (drhs != nil) { - if (drhs.kind == syntax.nkind.N_STRLIT) { - let bytes: str = drhs.str; - if (syntax.streq(fld, "ptr")) { - let lab: str = internstrlit(c, bytes); - emitline("\tLEAQ\t"); - emitbytes( lab.ptr, lab.len: u64); - emitline("(SB), AX\n"); - return; - }; - if (syntax.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 == syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.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 (syntax.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 == syntax.nkind.N_IDENT) { - let gtn: *syntax.node = letvartnode(c, lhs.str); - if (gtn != nil) { - if (gtn.kind == syntax.nkind.N_TARRAY) { - if (syntax.streq(fld, "ptr")) { - emitline("\tLEAQ\t"); - emitsymname(c, lhs.str); - emitline("(SB), AX\n"); - return; - }; - if (syntax.streq(fld, "len")) { - let lenn: *syntax.node = gtn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(gtn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (syntax.streq(fld, "ptr")) { - let dtn: *syntax.node = defvartnode(c, lhs.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { - emitline("\tLEAQ\t"); - emitsymname(c, lhs.str); - emitline("(SB), AX\n"); - return; - }; - }; - }; - if (syntax.streq(fld, "len")) { - let dtn: *syntax.node = defvartnode(c, lhs.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = dtn.rhs; - let alen: i64 = 0i64; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.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 == syntax.nkind.N_IDENT) { - let gtt: *syntax.node = letvartnode(c, lhs.str); - for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) { - gtt = aliaslookup(c, gtt.str); - }; - if (gtt != nil) { - if (gtt.kind == syntax.nkind.N_TTUPLE) { - let gidx: i32 = fldnumidx(fld); - if (gidx >= 0) { - let gtp: *syntax.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: *syntax.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: *syntax.tinfo = gpt.type_: *syntax.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 == syntax.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 (syntax.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 == syntax.nkind.N_INDEX) { - let idxbase: *syntax.node = lhs.lhs; - if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) { - let elemt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - let elemu: *syntax.tinfo = elemt; - elemu = tichase(elemu); - let st: *syntax.tinfo = nil; - let viaptr: bool = false; - if (elemu != nil) { - if (elemu.kind == syntax.tykind.TY_PTR) { - let pin: *syntax.tinfo = elemu.sub; - pin = tichase(pin); - if (pin != nil) { if (pin.kind == syntax.tykind.TY_STRUCT) { - st = pin; - viaptr = true; - };}; - } else { if (elemu.kind == syntax.tykind.TY_STRUCT) { - st = elemu; - };}; - }; - if (st != nil) { - let fnd: *syntax.tfield = nil; - let fwalk: *syntax.tfield = st.fields; - for (fwalk != nil) { - if (syntax.streq(fwalk.name, fld)) { fnd = fwalk; break; }; - fwalk = fwalk.tnext; - }; - let bu: *syntax.tinfo = idxbase.type_: *syntax.tinfo; - bu = tichase(bu); - let baseisarray: bool = false; - let baseok: bool = false; - if (bu != nil) { - if (bu.kind == syntax.tykind.TY_ARRAY) { baseisarray = true; baseok = true; }; - if (bu.kind == syntax.tykind.TY_SLICE) { baseok = true; }; - if (bu.kind == syntax.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: *syntax.node = defvartnode(c, idxbase.str); - if (dtn != nil) { - if (dtn.kind == syntax.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: *syntax.tinfo = fnd.type_; - let fu: *syntax.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 == syntax.tykind.TY_ARRAY) { - if (foff != 0) { - emitline("\tADDQ\t$"); - emitint(foff); - emitline(", AX\n"); - }; - return; - }; - if (fu != nil && (fu.kind == syntax.tykind.TY_STR - || fu.kind == syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.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 == syntax.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(syntax.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 == syntax.nkind.N_INDEX) { - let eu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (eu != nil) { if (eu.kind == syntax.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: *syntax.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: *syntax.tinfo = tp.type_; - let fu: *syntax.tinfo = tichase(ft); - if (fu != nil && (fu.kind == syntax.tykind.TY_TAGGED - || fu.kind == syntax.tykind.TY_STRUCT - || fu.kind == syntax.tykind.TY_TUPLE - || fu.kind == syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(ft)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(foff: i64, "AX"); - emitline(", X0\n"); - return; - }; - if (fu != nil && (fu.kind == syntax.tykind.TY_STR - || fu.kind == syntax.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(syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, lhs.str)); - if (frt != nil) { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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: *syntax.node = deflookuprhsmod(c, fld, usehint(c, lhs.str)); - if (drhs != nil) { - if (drhs.kind == syntax.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 (syntax.streq(mqop, "MOVQ")) { - emitline("\tMOVQ\t"); - emitfnname(c, fld, usehint(c, lhs.str)); - emitline("(SB), AX\n"); - } else { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_DOT) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 (syntax.typeistagged(leaftype)) { - let tlu: *syntax.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 (syntax.typeisstr(leaftype) || syntax.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 (syntax.typeisfloat(leaftype)) { - let mov: str = "MOVSD"; - if (syntax.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(syntax.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: *syntax.tinfo = nil; - if (lhs != nil) { pbu = lhs.type_: *syntax.tinfo; }; - pbu = tichase(pbu); - let pbok: bool = false; - if (pbu != nil) { - if (pbu.kind == syntax.tykind.TY_SLICE - || pbu.kind == syntax.tykind.TY_STR - || pbu.kind == syntax.tykind.TY_UNTYPED_STR) { pbok = true; }; - }; - if (lhs != nil) { - if (lhs.kind == syntax.nkind.N_STRLIT) { pbok = true; }; - }; - if (pbok) { - if (syntax.streq(fld, "ptr")) { cgexpr(c, lhs); return; }; - if (syntax.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 (syntax.streq(fld, "cap")) { - cgexpr(c, lhs); - if (lhs != nil && lhs.kind != syntax.nkind.N_STRLIT) { - if (pbu != nil && (pbu.kind == syntax.tykind.TY_SLICE - || pbu.kind == syntax.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 == syntax.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: *syntax.node = lhs; - let allptr: bool = true; - for (croot != nil && croot.kind == syntax.nkind.N_DOT) { - let ct: *syntax.tinfo = croot.type_: *syntax.tinfo; - ct = tichase(ct); - let okp: bool = false; - if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) { - let cs: *syntax.tinfo = ct.sub; - cs = tichase(cs); - if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; }; - }; }; - if (!okp) { allptr = false; }; - croot = croot.lhs; - }; - let it: *syntax.tinfo = nil; - if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT && - localfindnode(c, croot.str) != nil) { - it = lhs.type_: *syntax.tinfo; - }; - it = tichase(it); - if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = it.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - let tf: *syntax.tfield = st.fields; - for (tf != nil) { - if (syntax.streq(tf.name, fld)) { - let ft: *syntax.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 (syntax.typeistagged(ft)) { - let plu: *syntax.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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(ft)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(tf.offset: i64, "AX"); - emitline(", X0\n"); - return; - }; - let lop: str = loadopsz(syntax.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 == syntax.nkind.N_DOT) { - let inner: *syntax.node = lhs.lhs; - let innerfld: str = lhs.str; - if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = tn.kind; - let outname: str; - outname.ptr = nil; outname.len = 0; - let isptr: bool = false; - if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) { - let oft: *syntax.node = ofi.tnode; - if (oft != nil) { if (oft.kind == syntax.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 (syntax.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 == syntax.nkind.N_DOT) { - let sbt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (sbt == nil || sbt.kind == syntax.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 == syntax.nkind.N_DOT) { - let pgu: *syntax.tinfo = lhs.type_: *syntax.tinfo; - pgu = tichase(pgu); - if (pgu != nil) { - if (pgu.kind == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - let rdu: *syntax.tinfo = rdt; - rdu = tichase(rdu); - if (rdu != nil) { - if (rdu.kind == syntax.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 == syntax.tykind.TY_STRUCT - || rdu.kind == syntax.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 (syntax.typeisfloat(rdt)) { - let mov: str = "MOVSD"; - if (syntax.typeisf32(rdt)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t(BX), X0\n"); - return; - }; - if (rdu != nil && rdu.kind == syntax.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 == syntax.tykind.TY_STR - || rdu.kind == syntax.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(syntax.typeissigned(rdt), rdt.slotsize: i32); - }; - emitline("\t"); - emitline(lop); - emitline("\t(BX), AX\n"); - return; - }; -}; - -fn cgun(c: *cgen, n: *syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(lt)) { fk = 1; } - else { if (syntax.typeisfloat(lt)) { fk = 2; }; }; - }; - if (n.op == syntax.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 == syntax.tkind.TK_AMP) { - let opnd: *syntax.node = n.lhs; - if (opnd != nil) { - if (opnd.kind == syntax.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 == syntax.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 == syntax.nkind.N_DOT) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 == syntax.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: *syntax.node = lc.tnode; - let lkind: syntax.nkind = syntax.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 == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.nkind.N_TNAME) { - let si: *structinfo = structlookupchain(c, tn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.streq(fld, "cap")) { delta = 16; }; - if (delta >= 0) { - let isslor: bool = false; - if (lkind == syntax.nkind.N_TSLICE) { isslor = true; }; - if (lkind == syntax.nkind.N_TNAME) { - if (syntax.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 (syntax.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 (syntax.streq(fld, "ptr")) { gdelta = 0; }; - if (syntax.streq(fld, "len")) { gdelta = 8; }; - // str IS []u8: &str.cap is valid too, not slice-only - // — mirrors cstage (#1/Phase 3, #11). - if (syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, basenm)); - if (frt != nil) { - emitline("\tLEAQ\t"); - emitfnname(c, fld, usehint(c, 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"); - emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_INDEX) { - // &base[i] = base + i*esz, no dereference. - let base: *syntax.node = opnd.lhs; - let idx: *syntax.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 == syntax.nkind.N_IDENT) { - baselocal = localfindnode(c, base.str); - if (baselocal != nil) { - esz = elemsizeofc(c, baselocal.tnode); - let tn: *syntax.node = baselocal.tnode; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - let basealias82: bool = false; - if (bt82 != nil) { - if (bt82.kind == syntax.tykind.TY_NAMED) { basealias82 = true; }; - }; - if (basealias82) { - let bu82: *syntax.tinfo = tichase(bt82); - if (bu82 != nil) { - if (baselocal != nil) { - isarr = bu82.kind == syntax.tykind.TY_ARRAY; - }; - if (isglobalarr || isglobalptr) { - isglobalarr = bu82.kind == syntax.tykind.TY_ARRAY; - isglobalptr = !isglobalarr; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.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: *syntax.tinfo = opnd.type_: *syntax.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 == syntax.tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; - if (n.op == syntax.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 == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil && rti.kind == syntax.tykind.TY_FN) { return; }; - if (rti != nil && rti.kind == syntax.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 == syntax.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 == syntax.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: *syntax.node) void = { - if (op.kind == syntax.nkind.N_IDENT) { - let nm: str = op.str; - let lc: *local = localfindnode(c, nm); - if (lc == nil && isletvar(c, nm)) { - emitline("\tLEAQ\t"); - emitfnname(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: *syntax.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 == syntax.tkind.TK_AND || n.op == syntax.tkind.TK_OR) { - let prefix: str = "andend"; - let jshrt: str = "JE"; - if (n.op == syntax.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 == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_NEQ) - && n.lhs != nil && n.rhs != nil - && syntax.typeisstr(n.lhs.type_: *syntax.tinfo) - && syntax.typeisstr(n.rhs.type_: *syntax.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 == syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - if (syntax.typeisf32(llt)) { lfk = 1; } - else { if (syntax.typeisfloat(llt)) { lfk = 2; }; }; - }; - let rfk: i32 = 0; - if (n.rhs != nil) { - let rrt: *syntax.tinfo = n.rhs.type_: *syntax.tinfo; - if (syntax.typeisf32(rrt)) { rfk = 1; } - else { if (syntax.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 == syntax.tkind.TK_PLUS || - n.op == syntax.tkind.TK_MINUS || - n.op == syntax.tkind.TK_STAR || - n.op == syntax.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 == syntax.tkind.TK_MINUS) { op = "SUBSD"; }; - if (n.op == syntax.tkind.TK_STAR) { op = "MULSD"; }; - if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSD"; }; - if (fk == 1) { - if (n.op == syntax.tkind.TK_PLUS) { op = "ADDSS"; }; - if (n.op == syntax.tkind.TK_MINUS) { op = "SUBSS"; }; - if (n.op == syntax.tkind.TK_STAR) { op = "MULSS"; }; - if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSS"; }; - }; - emitline("\t"); emitline(op); emitline("\tX1, X0\n"); - return; - }; - let isfcmp: bool = false; - if (n.op == syntax.tkind.TK_EQ) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_NEQ) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_LT) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_LE) { isfcmp = true; }; - if (n.op == syntax.tkind.TK_GT) { isfcmp = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_LT || - n.op == syntax.tkind.TK_LE) { - // unordered -> false; otherwise the ordered Jcc decides. - let jcc: str = "JE"; - if (n.op == syntax.tkind.TK_LT) { jcc = "JB"; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; }; - if (n.op == syntax.tkind.TK_LSHIFT) { - emitline("\tMOVQ\tBX, CX\n"); - emitline("\tSHLQ\tCX, AX\n"); - return; - }; - if (n.op == syntax.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 == syntax.tkind.TK_EQ) { iscmp = true; jcc = "JE"; }; - if (n.op == syntax.tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; }; - if (n.op == syntax.tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; - if (n.op == syntax.tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; - if (n.op == syntax.tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; - if (n.op == syntax.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: *syntax.node) void = { - let v: *syntax.node = n.list; - let sz: i32 = 8; - let si: *structinfo = nil; - if (v.kind == syntax.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 == syntax.nkind.N_STRUCTLIT) { - if (si != nil) { - let f: *syntax.node = v.list; - for (f != nil) { - if (f.kind == syntax.nkind.N_FIELD) { - let fname: str = f.str; - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.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: *syntax.node) void = { - let sn: *syntax.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 == syntax.nkind.N_IDENT) { - snlocal = localfindnode(c, sn.str); - if (snlocal != nil) { - sndirect = true; - sn_off = snlocal.off; - }; - }; - let esz: i32 = 0; - let etnode: *syntax.node = nil; - let sti: *syntax.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: syntax.nkind = snlocal.tnode.kind; - if (stk == syntax.nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; - if (stk == syntax.nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; - if (stk == syntax.nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; - sti = snlocal.tnode.type_: *syntax.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_: *syntax.tinfo; - let fsti: *syntax.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: *syntax.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: *syntax.tinfo = esubti; - esubti = tichase(esubti); - let elstr: bool = esubti != nil && esubti.kind == syntax.tykind.TY_STR; - let elslice: bool = esubti != nil && esubti.kind == syntax.tykind.TY_SLICE; - let eltagged: bool = esubti != nil && esubti.kind == syntax.tykind.TY_TAGGED; - let elstruct: bool = esubti != nil && esubti.kind == syntax.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: *syntax.node = sn.next; - for (vn != nil) { - if (vn.kind == syntax.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: *syntax.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: *syntax.tinfo = nil; - if (it != nil) { itu = it.type_: *syntax.tinfo; }; - itu = tichase(itu); - if (itu == nil || (itu.kind != syntax.tykind.TY_SLICE - && itu.kind != syntax.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 == syntax.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(syntax.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: *syntax.node = nil; - let aidx: *syntax.node = nil; - if (elstruct && vn.kind != syntax.nkind.N_STRUCTLIT && vn.kind != syntax.nkind.N_IDENT) { - let ch: *syntax.node = vn; - let aok: bool = true; - for (aok && ch.kind == syntax.nkind.N_DOT) { - let ab: *syntax.node = ch.lhs; - let af: *syntax.tfield = nil; - if (ab != nil) { - let abu: *syntax.tinfo = ab.type_: *syntax.tinfo; - abu = tichase(abu); - if (abu != nil && abu.kind == syntax.tykind.TY_STRUCT) { - let fl: *syntax.tfield = abu.fields; - for (fl != nil) { - if (syntax.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 == syntax.nkind.N_INDEX) { - let ab: *syntax.node = ch.lhs; - let aet: *syntax.tinfo = ch.type_: *syntax.tinfo; - aet = tichase(aet); - let abu: *syntax.tinfo = nil; - if (ab != nil) { - abu = ab.type_: *syntax.tinfo; - abu = tichase(abu); - }; - if (ab == nil || abu == nil || aet == nil - || (abu.kind != syntax.tykind.TY_SLICE && abu.kind != syntax.tykind.TY_ARRAY)) { - aok = false; - } else { - abaseslice = abu.kind == syntax.tykind.TY_SLICE; - aidxesz = aet.size: i32; - aidx = ch.rhs; - ch = ab; - }; - }; - if (aok) { - if (ch.kind == syntax.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: *syntax.node = defvartnode(c, ch.str); - if (dtn != nil) { - if (dtn.kind == syntax.nkind.N_TARRAY) { gok = true; }; - }; - }; - if (!gok) { aok = false; }; - }; - } else { - if (ch.kind != syntax.nkind.N_UN || ch.op != syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_INDEX, checker-validated - let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_SLICE, checker-validated - let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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 == syntax.nkind.N_INDEX) { - if (base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = n.list; // N_INDEX, checker-validated - let base: *syntax.node = d.lhs; - let v: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo; - sti = tichase(sti); - let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) { - if (base.lhs.kind == syntax.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: *syntax.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: *syntax.node = n.lhs; - if (callee != nil) { - if (callee.kind == syntax.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: *syntax.tinfo = callee.type_: *syntax.tinfo; - if (bti != nil && bti.kind == syntax.tykind.TY_ERR) { - if (syntax.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 (syntax.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: *syntax.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 (syntax.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 (syntax.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 (syntax.streq(callee.str, "len")) { - if (n.list != nil) { - let a: *syntax.node = n.list; - let at: *syntax.tinfo = a.type_: *syntax.tinfo; - let u: *syntax.tinfo = at; - u = tichase(u); - let hdrish: bool = false; - if (u != nil) { - if (u.kind == syntax.tykind.TY_SLICE - || u.kind == syntax.tykind.TY_STR) { - hdrish = true; - }; - }; - if (hdrish && a.kind == syntax.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"); - emitfnname(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 == syntax.nkind.N_DOT - && a.lhs != nil - && a.lhs.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.lhs.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TTUPLE) { - let idx: i32 = fldnumidx(a.str); - if (idx >= 0) { - let tp: *syntax.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: *syntax.node = letvartnode(c, a.lhs.str); - for (gtn != nil && gtn.kind == syntax.nkind.N_TNAME) { - gtn = aliaslookup(c, gtn.str); - }; - if (gtn != nil) { - if (gtn.kind == syntax.nkind.N_TTUPLE) { - let gidx: i32 = fldnumidx(a.str); - if (gidx >= 0) { - let gtp: *syntax.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 == syntax.nkind.N_INDEX) { - cgexpr(c, a); - emitline("\tMOVQ\tBX, AX\n"); - return; - }; - if (u != nil) { - if (u.kind == syntax.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 (syntax.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 (syntax.streq(callee.str, "delete")) { - if (n.list != nil) { - if (n.list.next == nil) { - if (n.list.kind == syntax.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 (syntax.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: *syntax.node = nil; - if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { - calleeparams = fnparamslookup(c, callee.str); - } else { if (callee.kind == syntax.nkind.N_DOT) { - let cmod: str; - cmod.ptr = nil; cmod.len = 0; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.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: *syntax.node = callee_variadic_param(c, callee, &nfixed_v); - if (varp != nil) { - let nargs0: i32 = 0; - let aw: *syntax.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: *syntax.node = n.list; - let kk: i32 = 0; - for (kk < nfixed_v) { - aaf = aaf.next; - kk += 1; - }; - if (aaf != nil) { - if (aaf.kind == syntax.nkind.N_SPREAD) { - forwarding = true; - }; - }; - }; - if (forwarding) { - let prev: *syntax.node = nil; - let cur2: *syntax.node = n.list; - let kk2: i32 = 0; - for (kk2 < nfixed_v) { - prev = cur2; - cur2 = cur2.next; - kk2 += 1; - }; - let inner: *syntax.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: *syntax.node = varp.lhs; - if (varp.lhs != nil - && varp.lhs.kind == syntax.nkind.N_TSLICE) { - velem = varp.lhs.lhs; - }; - let esz: i32 = 8; - if (velem != nil) { - if (velem.kind == syntax.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_: *syntax.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: *syntax.node = n.list; - let kk3: i32 = 0; - for (kk3 < nfixed_v) { - aa2 = aa2.next; - kk3 += 1; - }; - let j: i32 = 0; - let prevarg: *syntax.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_: *syntax.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: *syntax.node = syntax.newnode(syntax.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: *syntax.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: *syntax.node = n.list; - let dparam: *syntax.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 == syntax.nkind.N_PARAM) { - if (dparam.op != syntax.tkind.TK_ELLIPSIS) { - if (dparam.lhs != nil) { - dmemsz = taggedmemargsize(dparam.lhs.type_: *syntax.tinfo); - }; - }; - }; }; - if (dmemsz == 0) { - dmemsz = taggedmemargsize(a.type_: *syntax.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: *syntax.tinfo = a.type_: *syntax.tinfo; - if (syntax.typeisf32(at)) { fk = 1; } - else { if (syntax.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: *syntax.node = nil; - if (a.kind == syntax.nkind.N_TUPLE) { if (dparam != nil) { - if (dparam.kind == syntax.nkind.N_PARAM && dparam.lhs != nil) { - let dtn2: *syntax.node = dparam.lhs; - for (dtn2 != nil && dtn2.kind == syntax.nkind.N_TNAME) { - dtn2 = aliaslookup(c, dtn2.str); - }; - if (dtn2 != nil) { if (dtn2.kind == syntax.nkind.N_TTUPLE) { dptt = dtn2; }; }; - }; - }; }; - let tuparg: *syntax.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 == syntax.nkind.N_TUPLE) { tuplit = true; }; }; - let p: *syntax.node = tuparg.list; - for (p != nil) { - let et: *syntax.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 == syntax.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: *syntax.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_: *syntax.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 == syntax.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 != syntax.nkind.N_IDENT - && callee.kind != syntax.nkind.N_DOT) { - isfnptrcall = true; - }; - if (callee.kind == syntax.nkind.N_DOT) { - let base: *syntax.node = callee.lhs; - let fld: str = callee.str; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - let lkind: syntax.nkind = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == syntax.nkind.N_TNAME) { sname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.streq(fn_, fld)) { - let ft: *syntax.node = fi.tnode; - if (ft != nil) { - if (ft.kind == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { - hint = usehint(c, 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: *syntax.node) void = { - let lhs: *syntax.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 == syntax.nkind.N_SLICE - && n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil) { - if (lhs.lhs.kind == syntax.nkind.N_INDEX) { - placedotidx = true; - }; - }; - if ((lhs.kind == syntax.nkind.N_INDEX - || (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR) - || placedotidx) - && n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil) { - if (n.rhs.kind == syntax.nkind.N_STRUCTLIT) { - let iet: *syntax.tinfo = lhs.type_: *syntax.tinfo; - iet = tichase(iet); - if (iet != nil) { - if (iet.kind == syntax.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 == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR - && n.op == syntax.tkind.TK_ASSIGN) { - let du: *syntax.tinfo = lhs.type_: *syntax.tinfo; - du = tichase(du); - if (du != nil) { - if (du.kind == syntax.tykind.TY_STRUCT - || du.kind == syntax.tykind.TY_ARRAY - || du.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (lhs.str.len == 0) { - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) { - if (n.rhs.kind == syntax.nkind.N_ARRLIT) { - let alt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - alt = tichase(alt); - if (alt != nil) { - if (alt.kind == syntax.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 == syntax.nkind.N_IDENT && n.op != syntax.tkind.TK_ASSIGN) { - let itu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (itu != nil) { - if (itu.kind == syntax.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 == syntax.nkind.N_IDENT) { - if (n.op == syntax.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 == syntax.nkind.N_CALL) { - asret = callsretsize(c, n.rhs); - }; - }; - if (asret > 0) { - let aru: *syntax.tinfo = n.rhs.type_: *syntax.tinfo; - aru = tichase(aru); - let alu: *syntax.tinfo = lc.tnode.type_: *syntax.tinfo; - alu = tichase(alu); - let aexact: bool = false; - if (aru != nil && aru == alu) { aexact = true; } - else { - if (syntax.typeeq(n.rhs.type_: *syntax.tinfo, - lc.tnode.type_: *syntax.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_: *syntax.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 == syntax.nkind.N_CALL) { - let gru: *syntax.tinfo = lhs.type_: *syntax.tinfo; - gru = tichase(gru); - if (gru != nil && gru.kind == syntax.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: *syntax.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_: *syntax.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 == syntax.nkind.N_UN) { - if (lhs.op == syntax.tkind.TK_STAR) { - if (n.op == syntax.tkind.TK_ASSIGN && !placeslit - && !derefagg) { - let inner: *syntax.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: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (du != nil && du.kind == syntax.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 == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { - if (pe.kind == syntax.nkind.N_TNAME) { - if (syntax.streq(pe.str, "str")) { elemstr = true; } - else { if (syntax.streq(pe.str, "f64")) { elemfloat = true; } - else { if (syntax.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 == syntax.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 == syntax.nkind.N_UN) { - if (lhs.op == syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (lt != nil) { psz = lt.size: i32; }; - let scalarpointee: bool = (psz == 1 || psz == 2 || psz == 4 || psz == 8); - if (n.op != syntax.tkind.TK_ASSIGN && scalarpointee) { - let inner: *syntax.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: *syntax.node = nil; - if (inner != nil) { - if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - if (tn.kind == syntax.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 = syntax.typeisunsigned(pe.type_: *syntax.tinfo); - }; - if (!unsignd) { - unsignd = nodeisunsigned(c, n.rhs); - }; - if (n.op == syntax.tkind.TK_SLASHEQ || n.op == syntax.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 == syntax.tkind.TK_PERCENTEQ) { - emitline("\tMOVQ\tDX, AX\n"); - }; - emitline("\t"); - emitline(storeop); - emitline("\tAX, (BX)\n"); - return; - }; - let combineop: str = "MOVQ"; - if (n.op == syntax.tkind.TK_PLUSEQ) { combineop = "ADDQ"; } - else { if (n.op == syntax.tkind.TK_MINUSEQ) { combineop = "SUBQ"; } - else { if (n.op == syntax.tkind.TK_STAREQ) { combineop = "IMULQ"; } - else { if (n.op == syntax.tkind.TK_AMPEQ) { combineop = "ANDQ"; } - else { if (n.op == syntax.tkind.TK_PIPEEQ) { combineop = "ORQ"; } - else { if (n.op == syntax.tkind.TK_CARETEQ) { combineop = "XORQ"; } - else { if (n.op == syntax.tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; } - else { if (n.op == syntax.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 == syntax.nkind.N_INDEX && !placeslit) { - if (n.op == syntax.tkind.TK_ASSIGN) { - let base: *syntax.node = lhs.lhs; - let idx: *syntax.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: *syntax.node = nil; - let basealias: bool = false; - if (base != nil) { - if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) { - isglobalarr = true; - } else { - isglobalptr = true; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; - } else { if (base.kind == syntax.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: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.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: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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_: *syntax.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: *syntax.node = baselocal.tnode; - let isarr: bool = false; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TARRAY) { - isarr = true; - }; - }; - // #60: alias-NAMED base — chased kind - // (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr = bu60.kind == syntax.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 == syntax.nkind.N_CALL - && callsretsize(c, n.rhs) > 0) { - let islocalarr: bool = false; - if (baselocal != nil) { - let btn: *syntax.node = baselocal.tnode; - if (btn != nil) { - if (btn.kind == syntax.nkind.N_TARRAY) { islocalarr = true; }; - }; - }; - let constidx: bool = false; - let cidx: i32 = 0; - if (idx != nil) { - if (idx.kind == syntax.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 == syntax.nkind.N_TUPLE && esz > 8 - && base != nil && base.kind == syntax.nkind.N_IDENT - && elemtn != nil && elemtn.kind == syntax.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: *syntax.node = elemtn.list; - for (q121 != nil) { - let qt: *syntax.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: *syntax.node = baselocal.tnode; - let isarr2: bool = false; - if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; }; - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_IDENT) - || (n.rhs.kind == syntax.nkind.N_DOT) - || (n.rhs.kind == syntax.nkind.N_UN - && n.rhs.op == syntax.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: *syntax.node = baselocal.tnode; - let isarr2: bool = false; - if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_UN - && n.rhs.op == syntax.tkind.TK_STAR) { - cgexpr(c, n.rhs.lhs); - emitline("\tMOVQ\tAX, SI\n"); - } else { if (n.rhs.kind == syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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 != syntax.tkind.TK_ASSIGN) { - let base: *syntax.node = lhs.lhs; - let idx: *syntax.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: *syntax.node = nil; - if (base != nil) { - if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) { - isglobalarr = true; - } else { - isglobalptr = true; - }; - }; - }; - } else { if (base.kind == syntax.nkind.N_DOT - || (base.kind == syntax.nkind.N_UN - && base.op == syntax.tkind.TK_STAR)) { - // N_UN deref base (`(*p)[i] OP= v`, #61 C): - // same stamped source as the store arm. - let dt: *syntax.tinfo = lhs.type_: *syntax.tinfo; - if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; - } else { if (base.kind == syntax.nkind.N_INDEX) { - let et: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let bt60: *syntax.tinfo = base.type_: *syntax.tinfo; - if (bt60 != nil) { - if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; }; - }; - if (basealias) { - let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); - if (et60 != nil) { - esz = et60.size: i32; - elemtn = lhs; - }; - if (isglobalarr || isglobalptr) { - let bu60: *syntax.tinfo = tichase(bt60); - if (bu60 != nil) { - isglobalarr = bu60.kind == syntax.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: *syntax.node = baselocal.tnode; - let isarray: bool = false; - if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; }; - // #60: alias-NAMED base — chased kind (see cgindex twin). - if (basealias) { - let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); - if (bu60 != nil) { isarray = bu60.kind == syntax.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 = syntax.typeisunsigned(elemtn.type_: *syntax.tinfo); - }; - }; - let wired: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == syntax.nkind.N_INDEX && !placeslit) { - let idxbase: *syntax.node = lhs.lhs.lhs; - let idx: *syntax.node = lhs.lhs.rhs; - let fld2: str = lhs.str; - if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) { - if (idx != nil) { - let lc: *local = localfindnode(c, idxbase.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - // idxelemtn: `*[N]T` drills to the pointee - // array's element (#61). - let elemt: *syntax.node = idxelemtn(tn); - let baseisarray: bool = tn.kind == syntax.nkind.N_TARRAY; - let snode: *syntax.node = nil; - let viaptr: bool = false; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = elemt.lhs; - if (inner != nil) { if (inner.kind == syntax.nkind.N_TNAME) { - snode = inner; - viaptr = true; - };}; - } else { if (elemt.kind == syntax.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 (syntax.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 == syntax.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 (syntax.typeisfloat(n.rhs.type_: *syntax.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_: *syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); - }; - }; - let wired_x: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_x = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.nkind.N_UN) { - if (base.op == syntax.tkind.TK_STAR) { - if (base.lhs != nil) { - if (base.lhs.kind == syntax.nkind.N_IDENT) { - base = base.lhs; - }; - }; - }; - }; - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = syntax.nkind.N_NONE; - if (tn != nil) { lkind = tn.kind; }; - // Pointer-to-struct: deref then store. - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (inner != nil) { - if (inner.kind == syntax.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 (syntax.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 == syntax.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_: *syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 != syntax.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 != syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo); }; }; - cgdotfieldcombine(c, n.op, uns34); - }; - if (n.op == syntax.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 == syntax.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 (syntax.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 == syntax.tkind.TK_ASSIGN - && istaggedtype(c, fi.tnode)) { - let fsz: i32 = slotsize(c, fi.tnode); - cgwidentaggedstore(c, fi.tnode.type_: *syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 != syntax.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 != syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; }; - if (syntax.streq(fld, "len")) { delta = 8; }; - if (syntax.streq(fld, "cap")) { delta = 16; }; - if (delta >= 0) { - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - let innerkind: syntax.nkind = syntax.nkind.N_NONE; - if (inner != nil) { innerkind = inner.kind; }; - let innerstr: bool = false; - if (innerkind == syntax.nkind.N_TNAME) { - if (syntax.streq(inner.str, "str")) { innerstr = true; }; - }; - if (innerkind == syntax.nkind.N_TSLICE) { innerstr = true; }; - if (innerstr) { - if (n.op != syntax.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 != syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.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 (syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_STRUCTLIT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_IDENT - && fi.tnode != nil - && fi.tnode.kind == syntax.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 == syntax.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_: *syntax.tinfo, - n.rhs, "BX", fi.foff, fsz); - return; - }; - if (n.op == syntax.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 = syntax.typeisunsigned(fi.tnode.type_: *syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { - if (base.kind == syntax.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: *syntax.node = base; - let allptr: bool = true; - for (croot != nil && croot.kind == syntax.nkind.N_DOT) { - let ct: *syntax.tinfo = croot.type_: *syntax.tinfo; - ct = tichase(ct); - let okp: bool = false; - if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) { - let cs: *syntax.tinfo = ct.sub; - cs = tichase(cs); - if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; }; - }; }; - if (!okp) { allptr = false; }; - croot = croot.lhs; - }; - let it: *syntax.tinfo = nil; - if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT && - localfindnode(c, croot.str) != nil) { - it = base.type_: *syntax.tinfo; - }; - it = tichase(it); - if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) { - let st: *syntax.tinfo = it.sub; - st = tichase(st); - if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) { - let tf: *syntax.tfield = st.fields; - for (tf != nil) { - if (syntax.streq(tf.name, fld)) { - let ft: *syntax.tinfo = tf.type_; - if (n.op == syntax.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 (syntax.typeistagged(ft)) { - let flu: *syntax.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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) { - let mov: str = "MOVSD"; - if (syntax.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 != syntax.tkind.TK_ASSIGN) { - if (syntax.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 (syntax.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 (syntax.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 (syntax.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 = syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; - if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil - && lhs.lhs.kind == syntax.nkind.N_DOT - && n.op == syntax.tkind.TK_ASSIGN) { - let rootname: str = ""; - let rootoff: i32 = 0; - let totaloff: i32 = 0; - let leaftype: *syntax.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 (syntax.typeistagged(leaftype)) { - let wlu: *syntax.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 (syntax.typeisstr(leaftype) || syntax.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: *syntax.tinfo = leaftype; - lp = tichase(lp); - if (lp != nil) { - if (lp.kind == syntax.tykind.TY_STRUCT) { leafstruct = true; }; - }; - if (leaftype.kind == syntax.tykind.TY_NAMED) { - leafname = leaftype.name; - }; - }; - if (n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.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 == syntax.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 (syntax.typeisfloat(leaftype)) { - let mov: str = "MOVSD"; - if (syntax.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 == syntax.nkind.N_DOT) { - let base: *syntax.node = lhs.lhs; - let fld: str = lhs.str; - if (base != nil) { if (base.kind == syntax.nkind.N_DOT) { - let inner: *syntax.node = base.lhs; - let innerfld: str = base.str; - if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, inner.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *syntax.node = lc.tnode; - let lkind: syntax.nkind = tn.kind; - let outname: str; - outname.ptr = nil; outname.len = 0; - let isptr: bool = false; - if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let pe: *syntax.node = tn.lhs; - if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) { - let oft: *syntax.node = ofi.tnode; - if (oft != nil) { if (oft.kind == syntax.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 (syntax.streq(ffi.fname, fld)) { - if (n.op == syntax.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 == syntax.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: *syntax.node = nil; - for (lvf != nil) { - if (syntax.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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; }; - if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; }; - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil) { - let gsz: i32 = 0; - if (lvftn.kind == syntax.nkind.N_TNAME) { - let gsi: *structinfo = structlookup(c, lvftn.str); - if (gsi != nil) { gsz = structabisize(gsi); }; - }; - if (lvftn.kind == syntax.nkind.N_TARRAY) { - let gat: *syntax.tinfo = lvftn.type_: *syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil - && lvftn.kind == syntax.nkind.N_TARRAY) { - let aggsz: i32 = 0; - let aat: *syntax.tinfo = lvftn.type_: *syntax.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 == syntax.tkind.TK_ASSIGN && lvftn != nil) { - let gau: *syntax.tinfo = lvftn.type_: *syntax.tinfo; - gau = tichase(gau); - if (gau != nil) { - if (gau.kind == syntax.tykind.TY_STRUCT - || gau.kind == syntax.tykind.TY_ARRAY - || gau.kind == syntax.tykind.TY_TUPLE) { - if (n.rhs != nil && n.rhs.kind == syntax.nkind.N_STRUCTLIT) { - let gsi49: *structinfo = nil; - if (lvftn.kind == syntax.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 == syntax.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 == syntax.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 (syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } - else { if (n.op == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tSHLQ\tCX, BX\n"); - } - else { if (n.op == syntax.tkind.TK_RSHIFTEQ) { - // #136: signed RSHIFTEQ → SARQ. - let unsignd_r: bool = false; - if (lvftn != nil) { - if (lvftn.type_ != nil) { - unsignd_r = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) { - let unsignd: bool = false; - if (lvftn != nil) { - unsignd = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil - && n.rhs.kind == syntax.nkind.N_CALL) { - let rtup: *syntax.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: *syntax.node = lcn.tnode; - let lcsname: str; - lcsname.ptr = nil; lcsname.len = 0; - if (lctn != nil) { - if (lctn.kind == syntax.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 == syntax.tkind.TK_ASSIGN) { - if (n.rhs != nil - && n.rhs.kind == syntax.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 == syntax.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 == syntax.tkind.TK_ASSIGN - && n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL) { - let acati: *syntax.tinfo = nil; - if (lcn.tnode != nil) { acati = lcn.tnode.type_: *syntax.tinfo; }; - acati = tichase(acati); - if (acati != nil && acati.kind == syntax.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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; }; - if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; }; - if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN) { - let agu: *syntax.tinfo = nil; - if (lcn != nil) { - if (lcn.tnode != nil) { - agu = lcn.tnode.type_: *syntax.tinfo; - }; - }; - agu = tichase(agu); - if (agu != nil) { - if (agu.kind == syntax.tykind.TY_STRUCT - || agu.kind == syntax.tykind.TY_ARRAY - || agu.kind == syntax.tykind.TY_TUPLE) { - if (n.rhs != nil && n.rhs.kind == syntax.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 == syntax.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 == syntax.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 (syntax.streq(llop, "MOVQ")) { - if (n.op == syntax.tkind.TK_PLUSEQ) { - emitline("\tADDQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - return; - }; - if (n.op == syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }; - if (n.op == syntax.tkind.TK_LSHIFTEQ) { - emitline("\tMOVQ\tAX, CX\n"); - emitline("\tSHLQ\tCX, BX\n"); - }; - if (n.op == syntax.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 = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) { - let unsignd: bool = false; - if (lcn != nil) { - if (lcn.tnode != nil) { - unsignd = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT || lhs.kind == syntax.nkind.N_INDEX - || (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR)) { - let ft: *syntax.tinfo = lhs.type_: *syntax.tinfo; - let fu: *syntax.tinfo = ft; - fu = tichase(fu); - let fsz: i32 = 8; - if (ft != nil) { fsz = ft.size: i32; }; - if (syntax.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 == syntax.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 == syntax.tykind.TY_STRUCT - || fu.kind == syntax.tykind.TY_ARRAY - || fu.kind == syntax.tykind.TY_TUPLE) { - if (n.op != syntax.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 == syntax.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 == syntax.nkind.N_STRUCTLIT - && fu.kind == syntax.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 == syntax.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 == syntax.tykind.TY_STR - || fu.kind == syntax.tykind.TY_SLICE) { - fstrsl = true; - }; - }; - if (fstrsl) { - if (n.op != syntax.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 == syntax.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(syntax.typeissigned(ft), fsz); - emitline("\t"); - emitline(lop); - emitline("\t(BX), AX\n"); - emitline("\tPOPQ\tCX\n"); - let unsignd: bool = syntax.typeisunsigned(ft); - let wired: bool = false; - if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; - if (n.op == syntax.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; -}; - - - -//ww:module-reset -// 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 syntax; -import strconv; - -// ---- statement cgen -------------------------------------------------- - -fn cgstmt(c: *cgen, n: *syntax.node) void = { - if (n == nil) { return; }; - let k: syntax.nkind = n.kind; - - if (k == syntax.nkind.N_BLOCK) { cgblock(c, n); return; }; - - if (k == syntax.nkind.N_RETURN) { cgreturn(c, n); return; }; - - if (k == syntax.nkind.N_EXPRSTMT) { cgexprstmt(c, n); return; }; - - if (k == syntax.nkind.N_LET) { cglet(c, n); return; }; - - if (k == syntax.nkind.N_IF) { cgif(c, n); return; }; - - if (k == syntax.nkind.N_FOR) { cgfor(c, n); return; }; - - if (k == syntax.nkind.N_FORRANGE) { cgforrange(c, n); return; }; - - if (k == syntax.nkind.N_SWITCH) { cgswitch(c, n); return; }; - - if (k == syntax.nkind.N_MASSIGN) { cgmassign(c, n); return; }; - - if (k == syntax.nkind.N_MLET) { cgmlet(c, n); return; }; - - if (k == syntax.nkind.N_BREAK) { cgbreak(c, n); return; }; - if (k == syntax.nkind.N_CONTINUE) { cgcontinue(c, n); return; }; - - if (k == syntax.nkind.N_YIELD) { cgyield(c, n); return; }; - - if (k == syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.tinfo) i32 = { - let t: *syntax.tinfo = ti; - t = tichase(t); - if (t == nil) { return 8; }; - if (t.kind == syntax.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 == syntax.tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; - if (t.kind == syntax.tykind.TY_STR || t.kind == syntax.tykind.TY_SLICE || - t.kind == syntax.tykind.TY_TAGGED) { - return ((t.size + 7u64) & ~7u64): i32; - }; - return 8; -}; - -export fn tupeslotn(n: *syntax.node) i32 = { - if (n == nil) { return 8; }; - return tupeslot(n.type_: *syntax.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: *syntax.node) *syntax.node = { - if (rhs == nil) { return nil; }; - if (rhs.kind != syntax.nkind.N_CALL) { return nil; }; - let callee: *syntax.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 == syntax.nkind.N_IDENT) { - cnm = callee.str; - cmod = c.curmod; - }; - if (callee.kind == syntax.nkind.N_DOT) { - cnm = callee.str; - if (callee.lhs != nil) { - if (callee.lhs.kind == syntax.nkind.N_IDENT) { - cmod = callee.lhs.str; - }; - }; - }; - if (cnm.len == 0) { return nil; }; - let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); - if (rtyp == nil) { return nil; }; - if (rtyp.kind != syntax.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: *syntax.node) *syntax.node = { - if (a == nil) { return nil; }; - if (a.kind == syntax.nkind.N_TUPLE) { return a; }; - if (a.kind == syntax.nkind.N_IDENT) { - let lc: *local = localfindnode(c, a.str); - if (lc == nil) { return nil; }; - let tn: *syntax.node = lc.tnode; - for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { - tn = aliaslookup(c, tn.str); - }; - if (tn != nil) { - if (tn.kind == syntax.nkind.N_TTUPLE) { return tn; }; - }; - return nil; - }; - let t: *syntax.node = inferletcalltype(c, a); - if (t != nil) { - if (t.kind == syntax.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: *syntax.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 = syntax.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: *syntax.node, dtn: *syntax.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: *syntax.tinfo = e.type_: *syntax.tinfo; - t = tichase(t); - if (t != nil && (t.kind == syntax.tykind.TY_TAGGED || - t.kind == syntax.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: *syntax.node, dtn: *syntax.node) void = { - let t: *syntax.tinfo = e.type_: *syntax.tinfo; - t = tichase(t); - let etagged: bool = false; - if (t != nil) { if (t.kind == syntax.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_: *syntax.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 == syntax.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 == syntax.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: *syntax.node, decl: *syntax.node) void = { - let dp0: *syntax.node = nil; - if (decl != nil) { - if (decl.kind == syntax.nkind.N_TTUPLE) { dp0 = decl.list; }; - }; - let ssecap: i32 = TUPLE_SSECAP; - let gptotal: i32 = 0; - let ssecount: i32 = 0; - let dp: *syntax.node = dp0; - let e: *syntax.node = tuple.list; - for (e != nil) { - let dtn: *syntax.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: *syntax.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: *syntax.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: *syntax.tinfo) void = { - let gptotal: i32 = 0; - let ssecount: i32 = 0; - let el: *syntax.ttupleelem = tu.tupleelems; - for (el != nil) { - let et: *syntax.tinfo = el.type_; - et = tichase(et); - if (et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.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: *syntax.tinfo = el.type_; - et = tichase(et); - let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.tykind.TY_F64); - let eslot: i32 = tupeslot(el.type_); - if (isflt) { - let mov: str = "MOVSD"; - if (et.kind == syntax.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: *syntax.tinfo) void = { - let words: i32 = 0; - let el: *syntax.ttupleelem = tup.tupleelems; - for (el != nil) { - let et: *syntax.tinfo = el.type_; - et = tichase(et); - let isflt: bool = et != nil && (et.kind == syntax.tykind.TY_F32 || et.kind == syntax.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: *syntax.node) void = { - rundefers(c); - let rhs: *syntax.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 == syntax.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: *syntax.node = nil; - if (c.fnret != nil) { - if (c.fnret.kind == syntax.nkind.N_TTUPLE) { - rp0 = c.fnret.list; - }; - }; - let rp: *syntax.node = rp0; - let e: *syntax.node = rhs.list; - for (e != nil) { - let rdtn: *syntax.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: *syntax.node = nil; - if (c.fnret != nil) { pt = c.fnret.list; }; - let we: *syntax.node = rhs.list; - let foff: i32 = 0; - for (we != nil) { - let dt: *syntax.tinfo = nil; - if (pt != nil) { dt = pt.lhs.type_: *syntax.tinfo; }; - dt = tichase(dt); - if (dt != nil && dt.kind == syntax.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: *syntax.tinfo = we.type_: *syntax.tinfo; - eu = tichase(eu); - let eoff: i32 = 0; - if (we.kind == syntax.nkind.N_IDENT && eu != nil) { - if (eu.kind == syntax.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: *syntax.tinfo = pt.lhs.type_: *syntax.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: *syntax.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: *syntax.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 == syntax.nkind.N_CALL || rhs.kind == syntax.nkind.N_INDEX || rhs.kind == syntax.nkind.N_DOT) && rhs.type_ != nil && c.fnret != nil && c.fnret.type_ != nil) { - let ru: *syntax.tinfo = rhs.type_: *syntax.tinfo; - ru = tichase(ru); - let fu: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; - fu = tichase(fu); - if (ru != nil && fu != nil && ru.kind == syntax.tykind.TY_TAGGED && fu.kind == syntax.tykind.TY_TAGGED) { - if (ru == fu) { - forwardtagged = true; - } else if (ru.nullable == fu.nullable) { - let pa: *syntax.tparam = ru.params; - let pb: *syntax.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 == syntax.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: *syntax.tinfo = rhs.type_: *syntax.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 == syntax.tykind.TY_TAGGED - && rhs.kind != syntax.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_: *syntax.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 == syntax.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: *syntax.tinfo = rhs.type_: *syntax.tinfo; - ru1 = tichase(ru1); - let fu1: *syntax.tinfo = nil; - if (c.fnret != nil) { - fu1 = c.fnret.type_: *syntax.tinfo; - fu1 = tichase(fu1); - }; - if (ru1 != nil && fu1 != nil) { - if (ru1.kind == syntax.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 == syntax.nkind.N_CAST && ru1.kind == syntax.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_: *syntax.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: *syntax.tinfo = rhs.type_: *syntax.tinfo; - if (syntax.typeisf32(rety)) { rfk = 1; } - else { if (syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { okrhs = true; }; - if (rhs.kind == syntax.nkind.N_STRUCTLIT) { okrhs = true; }; - if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { okrhs = true; addrsrc = true; }; - }; - if (okrhs) { - if (rhs.kind == syntax.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: *syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_STRUCTLIT) { - okrhs = true; - }; - if (rhs.kind == syntax.nkind.N_DOT) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { okrhs = true; addrsrc = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_ARRLIT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_DOT) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_INDEX) { arrok = true; }; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { arrok = true; }; - }; - let ati: *syntax.tinfo = c.fnret.type_: *syntax.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 == syntax.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 == syntax.nkind.N_ARRLIT) { - // scalar/float element fill; non-scalar - // elements loud-stop (rule 7, no consumer). - let esubti: *syntax.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 == syntax.tykind.TY_STRUCT) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_ARRAY) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_TUPLE) { badel = true; }; - if (esubti.kind == syntax.tykind.TY_SLICE) { badel = true; }; - if (esubti.kind == syntax.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: *syntax.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: *syntax.node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.tinfo = c.fnret.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil) { - if (rti.kind == syntax.tykind.TY_ARRAY) { aggret = true; }; - if (rti.kind == syntax.tykind.TY_STRUCT) { aggret = true; }; - }; - }; - if (aggret && rhs.kind != syntax.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: *syntax.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: *syntax.node, rhs: *syntax.node, off: i32) void = { - let elemn: *syntax.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: *syntax.tinfo = arrtn.type_: *syntax.tinfo; - if (ati79 != nil) { if (ati79.kind == syntax.tykind.TY_NAMED) { - let au79: *syntax.tinfo = tichase(ati79); - if (au79 != nil) { if (au79.kind == syntax.tykind.TY_ARRAY - && au79.sub != nil) { - let en79: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.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: *syntax.tinfo = nil; - if (elemn != nil) { esubti = elemn.type_: *syntax.tinfo; }; - esubti = tichase(esubti); - let isagg: bool = esubti != nil - && (esubti.kind == syntax.tykind.TY_STRUCT - || esubti.kind == syntax.tykind.TY_ARRAY - || esubti.kind == syntax.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 == syntax.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 == syntax.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: *syntax.node = rhs.list; - for (e != nil) { - let isellip: bool = false; - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - repeat = true; - isellip = true; - }; - }; - if (isellip) { - e = nil; - } else { - if (isagg) { - if (e.kind == syntax.nkind.N_STRUCTLIT) { - let esi: *structinfo = structlookupchain(c, elemn); - cgstructlitfillbp(c, esi, e, off + idx * esz); - } else { if (e.kind == syntax.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 == syntax.nkind.N_TARRAY) { - if (arrtn.rhs != nil) { - if (arrtn.rhs.kind == syntax.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: *syntax.node) void = { - let nm: str = n.str; - let sz: i32 = letslotsize(c, n); - let tn: *syntax.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: *syntax.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: *syntax.node = n.lhs; - if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; - if (n.rhs != nil) { - let rhs: *syntax.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: *syntax.node = nil; - let viatryunw: bool = false; - let viatryprop: bool = false; - if (rhs.kind == syntax.nkind.N_TRYUNW) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.nkind.N_CALL) { - scall = rhs.lhs; - viatryunw = true; - }; - }; - } else { if (rhs.kind == syntax.nkind.N_TRYPROP) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.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 == syntax.nkind.N_TSLICE && sz == tyslicesize(): i32) { - let callee: *syntax.node = scall.lhs; - let a0: *syntax.node = scall.list; - let a1: *syntax.node = nil; - let a2: *syntax.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 == syntax.nkind.N_IDENT - && syntax.streq(callee.str, "alloc") - && a0.kind == syntax.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: *syntax.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: *syntax.tinfo = nil; - if (c.fnret != nil) { nti = c.fnret.type_: *syntax.tinfo; }; - nti = tichase(nti); - if (nti != nil) { if (nti.kind == syntax.tykind.TY_TAGGED) { - let np: *syntax.tparam = nti.params; - let nidx2: i32 = 0; - for (np != nil) { - let nvt: *syntax.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 == syntax.nkind.N_CALL) { - letsret = callsretsize(c, rhs); - }; - if (letsret == 0) { - cgwidentaggedstore(c, tn.type_: *syntax.tinfo, rhs, - "BP", off, sz); - c.lastwasreturn = 0; - return; - }; - let lru: *syntax.tinfo = rhs.type_: *syntax.tinfo; - lru = tichase(lru); - let llu: *syntax.tinfo = tn.type_: *syntax.tinfo; - llu = tichase(llu); - let exact38: bool = false; - if (lru != nil && lru == llu) { exact38 = true; } - else { - if (syntax.typeeq(rhs.type_: *syntax.tinfo, tn.type_: *syntax.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: *syntax.node = tn; - for (ttup != nil && ttup.kind == syntax.nkind.N_TNAME) { - ttup = aliaslookup(c, ttup.str); - }; - if (ttup != nil) { - if (ttup.kind == syntax.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 == syntax.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: *syntax.node = ttup.list; - for (q != nil) { - let qt: *syntax.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 == syntax.nkind.N_TTUPLE - && rhs.kind != syntax.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 == syntax.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 == syntax.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: *syntax.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 == syntax.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 == syntax.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 == syntax.nkind.N_CALL) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (tn != nil) { - if (tn.kind == syntax.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 == syntax.nkind.N_CALL && tn != nil - && tn.kind == syntax.nkind.N_TARRAY) { - let ati: *syntax.tinfo = tn.type_: *syntax.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 == syntax.nkind.N_IDENT) { - let sname: str; - sname.ptr = nil; sname.len = 0; - if (tn != nil) { - if (tn.kind == syntax.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: *syntax.tinfo = nil; - if (tn != nil) { aggti = tn.type_: *syntax.tinfo; }; - aggti = tichase(aggti); - if (aggti != nil) { - if (aggti.kind == syntax.tykind.TY_ARRAY) { - aggn = aggti.size: i32; - }; - }; - }; - if (aggn > 8) { - let havesrc: bool = false; - if (rhs.kind == syntax.nkind.N_UN) { - if (rhs.op == syntax.tkind.TK_STAR) { - cgexpr(c, rhs.lhs); - emitline("\tMOVQ\tAX, SI\n"); - havesrc = true; - }; - }; - if (!havesrc) { if (rhs.kind == syntax.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: *syntax.node = defvartnode(c, rhs.str); - let aggisdef: bool = defvarstructinfo(c, rhs.str) != nil; - if (aggdtn != nil) { - if (aggdtn.kind == syntax.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 == syntax.nkind.N_DOT) { - if (dotchainaddr(c, rhs, "SI")) { - havesrc = true; - }; - }; }; - if (!havesrc) { if (rhs.kind == syntax.nkind.N_INDEX) { - let base: *syntax.node = rhs.lhs; - let idx: *syntax.node = rhs.rhs; - let bu: *syntax.tinfo = nil; - if (base != nil) { bu = base.type_: *syntax.tinfo; }; - bu = tichase(bu); - if (base != nil && base.kind == syntax.nkind.N_IDENT - && bu != nil && bu.kind == syntax.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 == syntax.nkind.N_DOT - || base.kind == syntax.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 == syntax.nkind.N_DOT) { - if (dotbaseaddr(c, base, "AX")) { - baseok = true; - }; - } else { - let ab: *syntax.node = base.lhs; - let bidx: *syntax.node = base.rhs; - let abu: *syntax.tinfo = nil; - if (ab != nil) { abu = ab.type_: *syntax.tinfo; }; - abu = tichase(abu); - if (ab != nil && ab.kind == syntax.nkind.N_IDENT - && abu != nil && abu.kind == syntax.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 == syntax.nkind.N_TRYUNW - || rhs.kind == syntax.nkind.N_TRYPROP) { - if (rhs.lhs != nil) { - if (rhs.lhs.kind == syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo; - zti = tichase(zti); - if (zti != nil && zti.kind == syntax.tykind.TY_ARRAY) { - zsz = zti.size: i32; - } else { if (n.lhs.kind == syntax.nkind.N_TNAME) { - let szi: *structinfo = structlookupchain(c, n.lhs); - if (szi != nil) { - let ti: *syntax.tinfo = n.lhs.type_: *syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.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 == syntax.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 == syntax.nkind.N_TUPLE) { litrhs = true; }; }; - let synthdecl: *syntax.node = nil; - if (litrhs) { - synthdecl = syntax.newnode(syntax.nkind.N_TTUPLE, n.rhs.file, n.rhs.line, n.rhs.col); - let dtail: *syntax.node = nil; - let lb0: *syntax.node = n.list; - for (lb0 != nil) { - let w: *syntax.node = syntax.newnode(syntax.nkind.N_TUPLE, n.rhs.file, n.rhs.line, n.rhs.col); - w.lhs = nil; - if (lb0.kind == syntax.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: *syntax.node = nil; - if (rettuple != nil) { pt2 = rettuple.list; }; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.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: *syntax.tinfo = pt2.lhs.type_: *syntax.tinfo; - if (eti != nil) { esz = eti.size: i32; }; - }; - let off: i32 = 0; - if (lb.kind == syntax.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: *syntax.node = n.list; - let pt: *syntax.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: *syntax.node = nil; - let re: *syntax.node = nil; - if (litrhs) { dp = synthdecl.list; re = n.rhs.list; }; - for (l != nil) { - let tn: *syntax.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: *syntax.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 == syntax.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: *syntax.node) void = { - let rhs: *syntax.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: *syntax.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 == syntax.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 == syntax.nkind.N_IDENT) { - let rl: *local = localfindnode(c, rhs.str); - if (rl != nil) { - let rti: *syntax.tinfo = rl.tnode.type_: *syntax.tinfo; - rti = tichase(rti); - if (rti != nil) { if (rti.kind == syntax.tykind.TY_TUPLE) { - let srcoff: i32 = rl.off; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.node = lb.lhs; - let isflt: bool = isfloattype(c, tn); - let eslot: i32 = tupeslotn(tn); - let esz: i32 = 8; - let eti: *syntax.tinfo = nil; - if (tn != nil) { eti = tn.type_: *syntax.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: *syntax.node = nil; - if (rettuple != nil) { pt2 = rettuple.list; }; - let foff: i32 = 0; - let lb: *syntax.node = n.list; - for (lb != nil) { - let tn: *syntax.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: *syntax.tinfo = pt2.lhs.type_: *syntax.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: *syntax.node = n.list; - let pt: *syntax.node = nil; - if (rettuple != nil) { pt = rettuple.list; }; - for (l != nil) { - let tn: *syntax.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: *syntax.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: *syntax.node) i32 = { - if (t == nil) { return 8; }; - let k: syntax.nkind = t.kind; - if (k == syntax.nkind.N_TPTR) { return 8; }; - if (k == syntax.nkind.N_TFN) { return 8; }; - if (k == syntax.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 == syntax.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: *syntax.tinfo = t.type_: *syntax.tinfo; - if (tgi != nil) { - tgi = tichase(tgi); - if (tgi != nil && tgi.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let total: i32 = 0; - let dp: *syntax.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 == syntax.nkind.N_TNAME) { - let nm: str = t.str; - if (syntax.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: *syntax.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: *syntax.node) void = { - let slc: *syntax.node = n.lhs; - let slclocal: *local = nil; - let slctn: *syntax.node = nil; - if (slc != nil) { - if (slc.kind == syntax.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: *syntax.node = nil; - if (slctn != nil) { - let sk: syntax.nkind = slctn.kind; - if (sk == syntax.nkind.N_TSLICE) { elemt = slctn.lhs; }; - if (sk == syntax.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 == syntax.nkind.N_TNAME) { - if (syntax.streq(slctn.str, "str")) { - let sti: *syntax.tinfo = slctn.type_: *syntax.tinfo; - if (sti != nil) { - if (sti.sub != nil) { - let u8n: *syntax.node = syntax.newnode(syntax.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: *syntax.tinfo = nil; - if (slctn != nil) { - if (slctn.kind == syntax.nkind.N_TNAME) { - let st60: *syntax.tinfo = slctn.type_: *syntax.tinfo; - if (st60 != nil) { - if (st60.kind == syntax.tykind.TY_NAMED) { rti60 = tichase(st60); }; - }; - }; - }; - if (rti60 != nil && elemt == nil) { - if (rti60.sub != nil) { - let en60: *syntax.node = syntax.newnode(syntax.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: *syntax.tinfo = tichase(rti60.sub); - if (es60 != nil) { esz = es60.size: i32; }; - }; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TTUPLE) { - let total: i32 = 0; - let p: *syntax.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: *syntax.tinfo = slc.type_: *syntax.tinfo; - sti2 = tichase(sti2); - if (sti2 != nil) { - if (sti2.kind == syntax.tykind.TY_SLICE - || sti2.kind == syntax.tykind.TY_STR - || sti2.kind == syntax.tykind.TY_ARRAY) { - if (sti2.sub != nil) { - esz = sti2.sub.size: i32; - let en: *syntax.node = syntax.newnode(syntax.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 != syntax.nkind.N_IDENT) { - let stu70: *syntax.tinfo = slc.type_: *syntax.tinfo; - stu70 = tichase(stu70); - let arr70: bool = false; - if (stu70 != nil) { - if (stu70.kind == syntax.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 == syntax.nkind.N_UN) { - if (slc.op == syntax.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 == syntax.nkind.N_IDENT) { - if (localfindnode(c, slc.str) == nil) { - let isglob: bool = isletvar(c, slc.str); - if (!isglob) { - let gdtn: *syntax.node = defvartnode(c, slc.str); - if (gdtn != nil) { - if (gdtn.kind == syntax.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: *syntax.node = nil; - if (elemt != nil) { - if (elemt.kind == syntax.nkind.N_TTUPLE) { tp = elemt.list; }; - }; - let field_off: i32 = 0; - let m: *syntax.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: *syntax.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: syntax.nkind = slctn.kind; - if (tk == syntax.nkind.N_TSLICE) { isslicestr = true; }; - if (tk == syntax.nkind.N_TARRAY) { isarr = true; }; - if (tk == syntax.nkind.N_TNAME) { - if (syntax.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 == syntax.tykind.TY_ARRAY) { isarr = true; }; - if (rti60.kind == syntax.tykind.TY_SLICE) { isslicestr = true; }; - if (rti60.kind == syntax.tykind.TY_STR) { isslicestr = true; }; - }; - if (isslicestr) { - if (slc.kind == syntax.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 == syntax.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 == syntax.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: *syntax.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: *syntax.node = nil; - - let cs: *syntax.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: *syntax.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: *syntax.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: *syntax.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; -}; - - - -//ww:module-reset -// 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 syntax; -import strconv; - - -// ---- function-level cgen --------------------------------------------- - -fn cgfnparams(c: *cgen, params: *syntax.node) void = { - let p: *syntax.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 == syntax.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 == syntax.tkind.TK_ELLIPSIS) { - let tn: *syntax.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: *syntax.node = nil; - if (p.lhs != nil) { - tt99 = p.lhs; - for (tt99 != nil && tt99.kind == syntax.nkind.N_TNAME) { - tt99 = aliaslookup(c, tt99.str); - }; - }; - if (p.lhs != nil) { if (tt99 != nil && tt99.kind == syntax.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: *syntax.node = tt99.list; - for (te != nil) { - let et: *syntax.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_: *syntax.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_: *syntax.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_: *syntax.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 == syntax.nkind.N_BLOCK) { - let s: *syntax.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 module hint (this fn's own module) the call sites use. M1 #32: - // the ROOT-unit main (imported==0) is the bare `_start` entry — emit - // it bare directly, mirroring collectmods' skip. Irreducibly name- - // based: a `package main` primary's decls are MODULED "main" (the - // package clause sets curmod; cmd/ww/main.c:394), so main is NOT a - // bare-module decl — dropping this carve-out mangles it to `main.main` - // (undefined `main`). #84's bare-module handling is the orthogonal - // axis (a package-LESS `//ww:module-reset` fn, nmod empty), resolved - // in modlookupforfn, NOT here. - // #99: gate on sepisdep — under sep a dep unit's main is imported==0 - // (path-carrying `//ww:module-reset`, #57); only the root/link-entry - // unit (wwiout==nil, #69) keeps the bare label. - emitline("TEXT "); - if (syntax.streq(fn_.str, "main") && fn_.imported == 0 && c.sepisdep == 0) { - emitbytes(fn_.str.ptr, fn_.str.len: u64); - } else { - 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: *syntax.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: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_FNDECL) { - // #22 M3: emit code ONLY for this package's own decls. A - // `.wwi` dep fn is a body-less prototype that already skips - // (the body != nil gate); the explicit imported gate also - // covers a dep fn that still carries a body in a transitional - // unit, keeping the "emit iff imported==0" rule exception-free. - if (!(c.sepmode != 0 && d.imported != 0)) { - if (d.body != nil) { - cgfn(c, d); - }; - }; - }; - d = d.next; - }; - letpreintern(c, file); - emitdatasection(c); - emitdefconstants(c, file); - emitletdataw(c, file); -}; - -//ww:module-reset -// 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 syntax; -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: *syntax.node, // the rhs type expr - aanext: *aliasent, -}; - -fn collectaliases(c: *cgen, file: *syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0); - tnvoid.str = "void"; - let bang: *syntax.node = syntax.newnode(syntax.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: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.node = d.lhs; - if (body != nil) { - if (body.kind != syntax.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) *syntax.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 (syntax.streq(a.aname, name)) { - if (syntax.streq(a.amod, c.curmod)) { return a.target; }; - }; - a = a.aanext; - }; - a = c.aliases; - for (a != nil) { - if (syntax.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); - // M1 #22: map the embedded use ALIAS (`utf8`) to the - // dotted import PATH the decl's module now carries. - let pkgmod: str = usehint(c, pkg); - let b: *aliasent = c.aliases; - for (b != nil) { - if (syntax.streq(b.aname, leaf)) { - if (syntax.streq(b.amod, pkgmod)) { - 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) *syntax.node = { - let a: *aliasent = c.aliases; - for (a != nil) { - if (syntax.streq(a.aname, name)) { - if (syntax.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: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; }; - if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; }; - if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; }; - if (k == syntax.nkind.N_UN) { - let v: u64; - if (!foldintliteral(e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - return false; -}; - -fn enumevalmember(prev: *enummember, e: *syntax.node, out: *u64) bool = { - if (e == nil) { return false; }; - if (foldintliteral(e, out)) { return true; }; - let k: syntax.nkind = e.kind; - if (k == syntax.nkind.N_IDENT) { - let m: *enummember = prev; - for (m != nil) { - if (syntax.streq(m.mname, e.str)) { - *out = m.mval; - return true; - }; - m = m.emnext; - }; - return false; - }; - if (k == syntax.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: syntax.tkind = e.op; - if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; }; - if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; }; - if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; }; - if (op == syntax.tkind.TK_SLASH) { - if (b == 0u64) { return false; }; - *out = a / b; return true; - }; - if (op == syntax.tkind.TK_PERCENT) { - if (b == 0u64) { return false; }; - *out = a % b; return true; - }; - if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; }; - if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; }; - if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; }; - if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; }; - if (op == syntax.tkind.TK_RSHIFT) { *out = a >> b; return true; }; - return false; - }; - if (k == syntax.nkind.N_UN) { - let v: u64; - if (!enumevalmember(prev, e.lhs, &v)) { return false; }; - let op: syntax.tkind = e.op; - if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; - if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; - if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; - return false; - }; - return false; -}; - -fn collectenums(c: *cgen, file: *syntax.node) void = { - c.enums = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_TYPEDECL) { - let body: *syntax.node = d.lhs; - if (body != nil) { - if (body.kind == syntax.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: *syntax.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 (syntax.streq(e.ename, name)) { - if (syntax.streq(e.emod, c.curmod)) { return e; }; - }; - e = e.etnext; - }; - e = c.enums; - for (e != nil) { - if (syntax.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); - // M1 #22: map the embedded use ALIAS (`utf8`) to the - // dotted import PATH the decl's module now carries. - let pkgmod: str = usehint(c, pkg); - let b: *enumtype = c.enums; - for (b != nil) { - if (syntax.streq(b.ename, leaf)) { - if (syntax.streq(b.emod, pkgmod)) { - 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 (syntax.streq(e.ename, name)) { - if (syntax.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 (syntax.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: *syntax.node) *syntax.node = { - let cur: *syntax.node = t; - let depth: i32 = 0; - for (depth < 16) { - if (cur == nil) { return nil; }; - if (cur.kind != syntax.nkind.N_TNAME) { return cur; }; - let nm: str = cur.str; - let next: *syntax.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: *syntax.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: *syntax.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: *syntax.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 - uses: *modent, // M1 #22: N_USE alias → dotted import path, - // for the qualified-ref codegen hint - // (mname=alias, nmod=path) - 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: *syntax.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: []*syntax.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: *syntax.node, - sretforward: i32, - // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ - // DATA ONLY for this package's own (imported==0) decls; treat every - // `.wwi`-sourced (imported==1) dep decl as an external. Off on the - // combined path (every existing invocation) so M3 is a pure addition. - // NOT reset by cgeninit (which runs per-fn) — set once in main and - // must survive to the post-loop emitletdataw/emitdefconstants pass, - // like strlits/ffis. Symmetric with cstage Cg.sep_mode. - sepmode: i32, - // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. - // Set from `wwiout != nil` in main — the producer passes -I (.wwi - // output) to DEP units only; the root's .wwi is stripped (#69), so - // wwiout==nil <=> root/link-entry unit. Gates the bare-`main` carve- - // out: a dep's `fn main` mangles on its path like any decl; only the - // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). - // Symmetric with cstage Cg.sep_isdep. - sepisdep: 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: *syntax.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: []*syntax.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: *syntax.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: *syntax.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: *syntax.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: *syntax.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 (syntax.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 (syntax.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 (syntax.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 (syntax.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 (syntax.streq(bs, bytes)) { - return s.label; - }; - s = s.slnext; - }; - // New label "._S_" (bare "_S_" when curmod empty). - // #49: per-unit prefix so two str-bearing packages don't both emit - // `_S_0`.. and collide at w6l link. Pure function of the module path - // (matching mklabel's spelling), so the self-host fixed-point holds. - 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; }; - buf[i] = 95u8; i += 1; buf[i] = 83u8; i += 1; buf[i] = 95u8; i += 1; // "_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[i + dk] = ns.ptr[dk]; dk += 1; }; - c.strlitseq += 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 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 (syntax.streq(nm, "bool")) { return true; }; - if (syntax.streq(nm, "rune")) { return true; }; - if (syntax.streq(nm, "i8")) { return true; }; - if (syntax.streq(nm, "i16")) { return true; }; - if (syntax.streq(nm, "i32")) { return true; }; - if (syntax.streq(nm, "i64")) { return true; }; - if (syntax.streq(nm, "u8")) { return true; }; - if (syntax.streq(nm, "u16")) { return true; }; - if (syntax.streq(nm, "u32")) { return true; }; - if (syntax.streq(nm, "u64")) { return true; }; - if (syntax.streq(nm, "int")) { return true; }; - if (syntax.streq(nm, "uint")) { return true; }; - if (syntax.streq(nm, "uintptr")) { return true; }; - if (syntax.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 (syntax.streq(nm, "f32")) { return 4; }; - if (syntax.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: *syntax.node) i32 = { - if (d == nil) { return 0; }; - let t: *syntax.node = d.lhs; - for (t != nil) { - if (t.kind == syntax.nkind.N_TPTR) { return 8; }; - if (t.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; - if (t.kind == syntax.nkind.N_TARRAY) { - let lenn: *syntax.node = t.rhs; - let elemn: *syntax.node = t.lhs; - let alen: i32 = 1; - if (lenn != nil && lenn.kind == syntax.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: *syntax.tinfo = tichase(t.type_: *syntax.tinfo); - if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { - alen = abt.alen: i32; - }; - }; - let esz: i32 = 8; - if (elemn != nil) { - if (elemn.kind == syntax.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 == syntax.nkind.N_TTUPLE) { - let tsum: i32 = 0; - let p: *syntax.node = t.list; - for (p != nil) { - let et: *syntax.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 == syntax.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 != syntax.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 (syntax.streq(nm, "str")) { return primtypesize("str"): i32; }; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si.totsize; }; - let next: *syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_LET) { - if (d.lhs != nil && d.rhs != nil - && d.lhs.kind == syntax.nkind.N_TNAME - && syntax.streq(d.lhs.str, "untyped_int")) { - let opnd: *syntax.node = d.rhs; - if (opnd.kind == syntax.nkind.N_UN - && (opnd.op == syntax.tkind.TK_PLUS - || opnd.op == syntax.tkind.TK_MINUS - || opnd.op == syntax.tkind.TK_TILDE)) { - opnd = opnd.lhs; - }; - if (opnd != nil - && opnd.kind == syntax.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 == syntax.nkind.N_TNAME - && syntax.streq(d.lhs.str, "untyped_float")) { - let opnd: *syntax.node = d.rhs; - if (opnd.kind == syntax.nkind.N_UN - && (opnd.op == syntax.tkind.TK_PLUS - || opnd.op == syntax.tkind.TK_MINUS)) { - opnd = opnd.lhs; - }; - if (opnd != nil - && opnd.kind == syntax.nkind.N_FLOATLIT) { - d.lhs.str = "f64"; - }; - }; - }; - d = d.next; - }; -}; - -fn collectlets(c: *cgen, file: *syntax.node) void = { - c.lets = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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 (syntax.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) *syntax.node = { - let lv: *letvar = c.lets; - for (lv != nil) { - if (syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nm: str = t.str; - if (syntax.streq(nm, "str")) { return true; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind == syntax.nkind.N_TSLICE) { return true; }; - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return 0; }; - let fsz: i32 = letfloatprim(t.str); - if (fsz > 0) { return fsz; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return false; }; - let nm: str = t.str; - if (structlookup(c, nm) != nil) { return true; }; - let nx: *syntax.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 (syntax.streq(lv.name, name)) { - let t: *syntax.node = lv.tnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return nil; }; - let nm: str = t.str; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si; }; - let nx: *syntax.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 (syntax.streq(e.dname, name)) { - let t: *syntax.node = e.dtnode; - for (t != nil) { - if (t.kind != syntax.nkind.N_TNAME) { return nil; }; - let nm: str = t.str; - let si: *structinfo = structlookup(c, nm); - if (si != nil) { return si; }; - let nx: *syntax.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) *syntax.node = { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.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: *syntax.tinfo, r: *syntax.node) void = { - let alen: i32 = au.alen: i32; - let cnt: i32 = 0; - let last_ev: *syntax.node = nil; - let repeat: bool = false; - let e: *syntax.node = r.list; - for (e != nil && cnt < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - repeat = true; - break; - }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev == nil) { break; }; - if (ev.kind != syntax.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: *syntax.node) void = { - if (file == nil) { return; }; - // #49: strlit labels allocated here (static-data initialisers) take - // the OWNING decl's module prefix, not the stale last-fn curmod. - // Save/restore so the later emit passes — which read curmod for - // fn-ptr relocs — see the same value they did before; letpreintern - // itself only interns, so driving curmod here has no other effect. - let savedmod: str = c.curmod; - let d: *syntax.node = file.list; - for (d != nil) { - c.curmod = d.nmod; - // #22 M3: skip imported deps so the strlit table (and its _S_ - // sequence) is a pure function of THIS package's own decls. A - // dep's body initializer would intern here, but its `.wwi` (init - // stripped) would not — gating on imported keeps the - // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- - // module str-def splicing rides the def registry (interned at the - // use site, not here), so it is unaffected. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - // #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 == syntax.nkind.N_DEF) { - let dr: *syntax.node = d.rhs; - for (dr != nil && dr.kind == syntax.nkind.N_CAST) { - dr = dr.lhs; - }; - if (d.lhs != nil && dr != nil - && dr.kind == syntax.nkind.N_ARRLIT) { - let dau: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); - if (dau != nil && dau.kind == syntax.tykind.TY_ARRAY) { - let deu: *syntax.tinfo = tichase(dau.sub); - if (deu != nil && deu.kind == syntax.tykind.TY_STR) { - preinternstrarray(c, dau, dr); - }; - }; - }; - }; - if (d.kind == syntax.nkind.N_LET) { - let r: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); - if (au != nil && au.kind == syntax.tykind.TY_ARRAY - && r.kind == syntax.nkind.N_ARRLIT) { - let eu: *syntax.tinfo = tichase(au.sub); - if (eu != nil && eu.kind == syntax.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 == syntax.nkind.N_TUPLE) { - let tlt: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTUPLE) { - handled = true; - let tp: *syntax.node = tlt.list; - let e: *syntax.node = r.list; - for (e != nil && tp != nil) { - let et: *syntax.node = tp.lhs; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev != nil) { - if (ev.kind == syntax.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 == syntax.nkind.N_ARRLIT - && d.lhs.kind == syntax.nkind.N_TSLICE) { - let tupnode: *syntax.node = d.lhs.lhs; - if (tupnode != nil && tupnode.kind == syntax.nkind.N_TTUPLE) { - handled = true; - let row: *syntax.node = r.list; - for (row != nil) { - let rw: *syntax.node = row; - for (rw != nil && rw.kind == syntax.nkind.N_CAST) { - rw = rw.lhs; - }; - if (rw != nil && rw.kind == syntax.nkind.N_TUPLE) { - let tp: *syntax.node = tupnode.list; - let e: *syntax.node = rw.list; - for (e != nil && tp != nil) { - let et: *syntax.node = tp.lhs; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { - ev = ev.lhs; - }; - if (ev != nil) { - if (ev.kind == syntax.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: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTAGGED && !isnullabletype(tlt)) { - if (r.kind == syntax.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 == syntax.nkind.N_STRLIT) { - if (r.str.len > 0) { - internstrlit(c, r.str); - }; - }; - }; - }; - }; - }; - d = d.next; - }; - c.curmod = savedmod; -}; - -// 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: *syntax.node) bool = { - let isf32: bool = (sz == 4); - let bits: u64 = 0u64; - let neg: bool = false; - if (rhs != nil) { - let r: *syntax.node = rhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - if (r != nil) { - if (r.kind == syntax.nkind.N_UN) { - if (r.op == syntax.tkind.TK_MINUS) { - neg = true; - r = r.lhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - } else { if (r.op == syntax.tkind.TK_PLUS) { - r = r.lhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - };}; - }; - }; - if (r == nil) { return false; }; - if (r.kind != syntax.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(" "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node, - base: u64) bool = { - let su: *syntax.tinfo = structt; - su = tichase(su); - if (su == nil) { return false; }; - if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; - let pos: u64 = base; - let f: *syntax.tfield = su.fields; - for (f != nil) { - let fstart: u64 = base + f.offset; - for (pos < fstart) { - emitdatawbyte(0u8); - pos = pos + 1u64; - }; - let v: *syntax.node = nil; - if (rhs != nil) { - let fnod: *syntax.node = rhs.list; - for (fnod != nil) { - if (syntax.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: *syntax.node = v; - for (vr != nil && vr.kind == syntax.nkind.N_CAST) { vr = vr.lhs; }; - let fu: *syntax.tinfo = f.type_; - fu = tichase(fu); - // #19 option A: a non-nullable tagged-union field rides the shared - // (tag,payload) core at the field slot size, mirroring the scalar - // tagged global — NOT the int emitter. Wide/struct/non-foldable - // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil - // here (the absent-field zero-fill is handled above). - if (fu != nil && fu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(fu)) { - if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { - let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); - }; - pos = fstart + fsz: u64; - f = f.tnext; - continue; - }; - if (fu != nil && fu.kind == syntax.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 != syntax.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 == syntax.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 != syntax.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 (syntax.typeisfloat(f.type_)) { - let isf32: bool = (fsz == 4); - let neg: bool = false; - let fr: *syntax.node = vr; - if (fr != nil) { if (fr.kind == syntax.nkind.N_UN) { - if (fr.op == syntax.tkind.TK_MINUS) { - neg = true; - fr = fr.lhs; - for (fr != nil && fr.kind == syntax.nkind.N_CAST) { fr = fr.lhs; }; - } else { if (fr.op == syntax.tkind.TK_PLUS) { - fr = fr.lhs; - for (fr != nil && fr.kind == syntax.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 != syntax.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: *syntax.tinfo, rhs: *syntax.node) bool = { - let su: *syntax.tinfo = structt; - su = tichase(su); - if (su == nil) { return false; }; - if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; - emitline(directive); - emitline(" "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node, - emit_phase: i32) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - let esz: i32 = au.sub.size: i32; - let alen: i32 = au.alen: i32; - let eu: *syntax.tinfo = au.sub; - eu = tichase(eu); - - // #19 option A: a non-nullable tagged-union element rides the shared - // (tag,payload) core (emittaggedbytes) at the full slot stride esz, - // mirroring the scalar tagged global — NOT the int emitter, which - // mis-folds the payload into the tag word. A wide/struct/non-foldable - // payload element loud-rejects (task #30 sub-item, rule 7). A nullable - // `(*T|void)` element is a 1-word fold, not a tag box — left to the - // existing int path (task #15). - if (eu != nil && eu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(eu)) { - let idx: i32 = 0; - let last_ev: *syntax.node = nil; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - if (!emittaggedbytes(c, au.sub, e, esz, 0)) { - let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); - }; - last_ev = e; - 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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - emittaggedbytes(c, au.sub, e, esz, 1); - idx += 1; - e = e.next; - }; - for (idx < alen) { - if (repeat && last_ev != nil) { - emittaggedbytes(c, au.sub, last_ev, esz, 1); - } else { - let bb: i32 = 0; - for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; - }; - idx += 1; - }; - return true; - }; - - if (eu != nil && eu.kind == syntax.tykind.TY_STRUCT) { - // Validate: every element must be N_STRUCTLIT (after N_CAST). - let idx: i32 = 0; - let last_ev: *syntax.node = nil; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.tykind.TY_ARRAY) { - let idx: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 (syntax.typeisfloat(au.sub)) { - let isf32: bool = syntax.typeisf32(au.sub); - // Validate. - let idx: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { - if (ev.op == syntax.tkind.TK_MINUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - } else { if (ev.op == syntax.tkind.TK_PLUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - };}; - };}; - if (ev == nil) { return false; }; - if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - let neg: bool = false; - if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { - if (ev.op == syntax.tkind.TK_MINUS) { - neg = true; - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - } else { if (ev.op == syntax.tkind.TK_PLUS) { - ev = ev.lhs; - for (ev != nil && ev.kind == syntax.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: *syntax.node = rhs.list; - let last: u64 = 0u64; - let repeat: bool = false; - // Validate first. - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { - inrepeat = true; - v = last; - } else { - e = e.next; - v = last; - }; - } else { - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.tinfo, rhs: *syntax.node) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - let eu: *syntax.tinfo = au.sub; - eu = tichase(eu); - if (eu == nil) { return false; }; - if (eu.kind != syntax.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: *syntax.node = nil; - let repeat: bool = false; - let cnt: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil && cnt < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { repeat = true; break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - if (ev.kind != syntax.nkind.N_STRLIT) { return false; }; - last_ev = ev; - cnt += 1; - e = e.next; - }; - - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - let idx: i32 = 0; - e = rhs.list; - for (e != nil && idx < alen) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 == syntax.nkind.N_FIELD) { - if (syntax.streq(e.str, "...")) { break; }; - }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev.str.len > 0) { - let lab: str = internstrlit(c, ev.str); - emitline("DATAR "); - emitfnname(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 "); - emitfnname(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: *syntax.tinfo, rhs: *syntax.node) bool = { - let au: *syntax.tinfo = arrt; - au = tichase(au); - if (au == nil) { return false; }; - if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; - if (rhs == nil) { - let total: u64 = arrt.size; - emitline(directive); - emitline(" "); - emitfnname(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(" "); - emitfnname(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: *syntax.tinfo, - sltnode: *syntax.node, rhs: *syntax.node) void = { - let su: *syntax.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 != syntax.tykind.TY_SLICE) { return; }; - let etype: *syntax.tinfo = su.sub; - let eu: *syntax.tinfo = etype; - eu = tichase(eu); - // Count elements; reject `...` (a slice literal has no target N). - let k: i32 = 0; - let e: *syntax.node = rhs.list; - for (e != nil) { - if (e.kind == syntax.nkind.N_FIELD) { - if (syntax.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: *syntax.node = nil; - if (sltnode != nil) { tupnode = sltnode.lhs; }; - let istuprow: bool = false; - if (eu != nil && eu.kind == syntax.tykind.TY_TUPLE && tupnode != nil) { - if (tupnode.kind == syntax.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: *syntax.node = rhs.list; - for (e2 != nil) { - let row: *syntax.node = e2; - for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; }; - let bad: bool = false; - if (row == nil) { bad = true; } - else if (row.kind != syntax.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 "); - emitfnname(c, name, module); - emitline(".d(SB),\""); - e2 = rhs.list; - for (e2 != nil) { - let row: *syntax.node = e2; - for (row != nil && row.kind == syntax.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: *syntax.node = e2; - for (row != nil && row.kind == syntax.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 == syntax.tykind.TY_STR || eu.kind == syntax.tykind.TY_SLICE - || eu.kind == syntax.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: *syntax.tinfo = syntax.newtype(syntax.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 "); - emitfnname(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 "); - emitfnname(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 "); - emitfnname(c, name, module); - emitline("+0(SB),"); - emitfnname(c, name, module); - emitline(".d(SB)\n"); -}; - -// emittaggedbytes — raw sz-byte static-init payload for a tagged-union -// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO -// directive open/close, NO reloc — emits exactly sz bytes via -// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a -// foldable int payload only; a wide (str/slice) payload needs a DATAR the -// raw core cannot place inside an already-open aggregate directive, and a -// struct/non-foldable payload has no scalar form — both return false -// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct -// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated -// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. -// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata -// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) -// share this raw core so a nested tagged member rides the SAME -// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage -// emit_tagged_bytes. -fn emittaggedbytes(c: *cgen, tti: *syntax.tinfo, rhs: *syntax.node, sz: i32, emit_phase: i32) bool = { - if (tti == nil) { return false; }; - if (rhs == nil) { - if (emit_phase == 1) { - let zi: i32 = 0; - for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; - }; - return true; - }; - let r: *syntax.node = rhs; - for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; }; - if (r == nil) { return false; }; - let tag: i32 = flatvariantidxt(tti, r.type_: *syntax.tinfo, false); - if (tag < 0) { return false; }; - if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; - let v: u64 = 0u64; - if (!foldintliteral(r, &v)) { return false; }; - if (emit_phase == 1) { - let acc: u64 = 0u64; - let i: i32 = 0; - acc = tag: u64; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - acc = v; - i = 0; - for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; - i = 16; - for (i < sz) { emitdatawbyte(0u8); i += 1; }; - }; - return true; -}; - -// 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: *syntax.node, rhs: *syntax.node, sz: i32) bool = { - if (tt == nil) { return false; }; - if (rhs == nil) { - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1); - emitline("\"\n"); - return true; - }; - let r: *syntax.node = rhs; - for (r != nil && r.kind == syntax.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 != syntax.nkind.N_STRLIT) { return false; }; - let lv: u64 = r.str.len: u64; - emitline("DATAW "); - emitfnname(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 "); - emitfnname(c, name, module); - emitline("+8(SB),"); - emitbytes( lab.ptr, lab.len: u64); - emitline("(SB)\n"); - }; - return true; - }; - // int/zero payload via the shared raw core; validate (phase 0) BEFORE - // opening the directive so a non-foldable rhs returns false without - // leaving a half-written DATAW. - if (!emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 0)) { return false; }; - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 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: *syntax.node) bool = { - if (ev == nil) { return false; }; - if (ev.kind != syntax.nkind.N_UN) { return false; }; - if (ev.op != syntax.tkind.TK_AMP) { return false; }; - let opnd: *syntax.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 == syntax.nkind.N_DOT) { - if (opnd.lhs == nil) { return false; }; - if (opnd.lhs.kind != syntax.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 != syntax.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: *syntax.tinfo = tichase(opnd.type_: *syntax.tinfo); - if (ou == nil) { return false; }; - return ou.kind == syntax.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: *syntax.node, rhs: *syntax.node) bool = { - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; - if (ev == nil) { return false; }; - { - let eti: *syntax.tinfo = nil; - if (et != nil) { eti = et.type_: *syntax.tinfo; }; - eti = tichase(eti); - if (eti != nil && eti.kind == syntax.tykind.TY_TAGGED) { - return false; - }; - }; - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - if (wide) { - if (ev.kind != syntax.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: *syntax.node, rhs: *syntax.node) void = { - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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: *syntax.node, rhs: *syntax.node) void = { - let foff: i32 = rowoff; - let tp: *syntax.node = tt.list; - let e: *syntax.node = rhs.list; - for (e != nil) { - let et: *syntax.node = nil; - if (tp != nil) { et = tp.lhs; }; - let ev: *syntax.node = e; - for (ev != nil && ev.kind == syntax.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 "); - emitfnname(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 "); - emitfnname(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 == syntax.nkind.N_DOT) { - emitfnname(c, ev.lhs.str, usehint(c, 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: *syntax.node, rhs: *syntax.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: *syntax.node = tt.list; - for (p0 != nil) { - zsz += tupeslotn(p0.lhs); - p0 = p0.next; - }; - emitline("DATAW "); - emitfnname(c, name, module); - emitline("(SB),\""); - let zi: i32 = 0; - for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; - emitline("\"\n"); - return true; - }; - if (rhs.kind != syntax.nkind.N_TUPLE) { return false; }; - if (!tuplerowfoldable(c, tt, rhs)) { return false; }; - emitline("DATAW "); - emitfnname(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: *syntax.node) void = { - let d: *syntax.node = file.list; - for (d != nil) { - // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is - // initializer-less; emitting a DATAW for it would DUPLICATE the - // definition that lives in the dep's own .o → link collision. - // Gate on the explicit imported flag (NOT no-rhs: a package's OWN - // init-less let must still zero-init). Symmetric with M2's - // producer imported==0 filter — same predicate both ways. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - if (d.kind == syntax.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: *syntax.tinfo = nil; - if (d.lhs != nil) { dti = tichase(d.lhs.type_: *syntax.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: *syntax.node = d.lhs; - for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { - tlt = aliaslookup(c, tlt.str); - }; - let istup: bool = false; - if (tlt != nil) { - if (tlt.kind == syntax.nkind.N_TTUPLE) { - istup = true; - }; - }; - if (istup) { - let tr: *syntax.node = d.rhs; - for (tr != nil) { - if (tr.kind != syntax.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 == syntax.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: *syntax.node = d.rhs; - if (r != nil) { - if (r.kind == syntax.nkind.N_STRUCTLIT) { - let st: *syntax.tinfo = d.lhs.type_: *syntax.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 == syntax.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: *syntax.node = nil; - if (d.rhs != nil) { - r = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 "); - emitfnname(c, nm, d.nmod); - emitline("(SB),\""); - let zi: i32 = 0; - for (zi < 8) { emitdatawbyte(0u8); zi += 1; }; - emitline("\"\n"); - emitline("DATAR "); - emitfnname(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 == syntax.nkind.N_DOT) { - emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); - } else { - emitfnname(c, r.lhs.str, c.curmod); - }; - emitline("(SB)\n"); - } else if (ok) { - emitline("DATAW "); - emitfnname(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: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 == syntax.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 "); - emitfnname(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 "); - emitfnname(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 == syntax.nkind.N_NIL) { ok = true; }; - if (r.kind == syntax.nkind.N_STRLIT) { - if (r.str.len == 0) { ok = true; }; - }; - }; - }; - if (ok) { - emitline("DATAW "); - emitfnname(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: *syntax.node = d.rhs; - for (r != nil) { - if (r.kind != syntax.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 == syntax.nkind.N_ARRLIT) { - emitslicedata(c, nm, d.nmod, - d.lhs.type_: *syntax.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 == syntax.nkind.N_NIL) { ok = true; }; - }; - }; - if (ok) { - emitline("DATAW "); - emitfnname(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 "); - emitfnname(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 == syntax.tykind.TY_ARRAY) { - let rh: *syntax.node = d.rhs; - let route: bool = false; - if (rh == nil) { route = true; }; - if (rh != nil) { - if (rh.kind == syntax.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: *syntax.node) void = { - let d: *syntax.node = file.list; - for (d != nil) { - // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / - // struct / array) must NOT re-emit — the dep's own .o owns the - // symbol. Str defs are inline-spliced (never emitted here), so - // they need no gate; the def registry stays populated for imported - // decls so the target's LOAD paths still resolve the extern. - if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; - if (d.kind == syntax.nkind.N_DEF) { - let r: *syntax.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: *syntax.node = d.lhs; - for (dt != nil) { - if (dt.kind != syntax.nkind.N_TNAME) { dfsz = 0; break; }; - let fsz: i32 = letfloatprim(dt.str); - if (fsz > 0) { dfsz = fsz; break; }; - let nx: *syntax.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 == syntax.nkind.N_STRUCTLIT) { - let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; - let su: *syntax.tinfo = st; - su = tichase(su); - if (su != nil) { - if (su.kind == syntax.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 == syntax.nkind.N_ARRLIT) { - let at: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; - let au: *syntax.tinfo = at; - au = tichase(au); - if (au != nil) { - if (au.kind == syntax.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 == syntax.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 "); - emitfnname(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: *syntax.node, - params: *syntax.node, - frnext: *fnret, -}; - -fn collectfnrets(c: *cgen, file: *syntax.node) void = { - c.fnrets = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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) *syntax.node = { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, c.curmod)) { return f.rtype; }; - }; - f = f.frnext; - }; - f = c.fnrets; - for (f != nil) { - if (syntax.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) *syntax.node = { - // M1 #22 (#199b): the qualifier may be the import ALIAS the user - // wrote (`utf8`); fn decls register f.fmod under the dotted import - // PATH (`encoding.utf8`). Map alias->path so a nested-package callee - // matches its own module instead of falling back to the name-only - // pass — which a same-leaf caller-module fn (e.g. strings.next vs - // utf8.next) otherwise wins, resolving a match scrutinee to the - // caller's union and collapsing arms 2+. usehint is idempotent on a - // path / c.curmod (returns the input when no `use` matches), so the - // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident - // c.curmod callers are unaffected. The choke-point twin of the - // struct/alias/enum usehint splitters (cgen.ww:128/319, - // cgenutil.ww:2173) — closes the whole fnret class by construction. - let mk: str = usehint(c, mod); - if (mk.len > 0) { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, mk)) { 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) *syntax.node = { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, c.curmod)) { return f.params; }; - }; - f = f.frnext; - }; - f = c.fnrets; - for (f != nil) { - if (syntax.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 (syntax.streq(f.fname, name)) { - if (syntax.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) *syntax.node = { - // M1 #22 (#199b): map import alias -> dotted path, identical to - // fnretlookupmod (the param-side twin). usehint is idempotent on a - // path / c.curmod so existing callers are unaffected. - let mk: str = usehint(c, mod); - if (mk.len > 0) { - let f: *fnret = c.fnrets; - for (f != nil) { - if (syntax.streq(f.fname, name)) { - if (syntax.streq(f.fmod, mk)) { 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: *syntax.node, - dtnode: *syntax.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: *syntax.node) void = { - c.defs = nil; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.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 (syntax.streq(e.dname, name)) { - if (syntax.streq(e.dmod, c.curmod)) { return true; }; - }; - e = e.dnext; - }; - e = c.defs; - for (e != nil) { - if (syntax.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) *syntax.node = { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.streq(e.dname, name)) { - if (syntax.streq(e.dmod, c.curmod)) { return e.drhs; }; - }; - e = e.dnext; - }; - e = c.defs; - for (e != nil) { - if (syntax.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) *syntax.node = { - if (mod.len > 0) { - let e: *defent = c.defs; - for (e != nil) { - if (syntax.streq(e.dname, name)) { - if (syntax.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: *syntax.node) bool = { - let r: *syntax.node = rhs; - for (r != nil) { - if (r.kind != syntax.nkind.N_CAST) { break; }; - r = r.lhs; - }; - if (r != nil) { - if (r.kind == syntax.nkind.N_UN) { - if (r.op == syntax.tkind.TK_MINUS) { - r = r.lhs; - for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; - } else { if (r.op == syntax.tkind.TK_PLUS) { - r = r.lhs; - for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; - }; }; - }; - }; - if (r == nil) { return false; }; - return r.kind == syntax.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: *syntax.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: *syntax.node = defvartnode(c, nm); - if (dtn != nil) { - let du88: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo); - if (du88 != nil) { if (du88.kind == syntax.tykind.TY_ARRAY) { return true; }; }; - }; - let drhs: *syntax.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`) - omod: str, // owning module of a `use` decl (#40); unused for mods - mnext: *modent, -}; - -fn collectmods(c: *cgen, file: *syntax.node) void = { - c.mods = nil; - c.uses = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - // M1 #22: record alias→path for the qualified-ref hint. - if (d.kind == syntax.nkind.N_USE) { - if (d.usepath.len > 0) { - let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, mnext=c.uses})!; - c.uses = um; - }; - }; - // 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 == syntax.nkind.N_FNDECL) { - // Fns mangle regardless of export status — covers - // lib/os.read vs lib/io.read collision. - let isffi: bool = false; - let a: *syntax.node = d.attr; - for (a != nil) { - if (a.kind == syntax.nkind.N_ATTR) { - let an: str = a.str; - if (syntax.streq(an, "symbol")) { isffi = true; }; - }; - a = a.next; - }; - if (!isffi) { - // #84: register bare-module (package-less `//ww:module- - // reset`) fns TOO (nmod.len==0) — previously the - // `nmod.len > 0` gate skipped ALL bare fns, so a bare fn - // never entered c.mods and a bare-ident ref to it first- - // matched an imported same-leaf fn (modlookupforfn) → a - // #40 residual / #263-class silent dead-dup. With the bare - // entry present, modlookupforfn prefers it on an empty - // hint. main is the linker entry convention: a `package - // main` primary's decls are MODULED "main" so main is NOT - // bare here — skip it (cgfn force-emits the bare `main` - // label). M1 #32: only ROOT main (imported==0) stays bare; - // an IMPORTED `fn main` mangles on its path. Mirrors - // cstage cgen.c mod_collect. - // #99: under sep a dep unit's main is imported==0 too (its - // body is composed with a path-carrying `//ww:module-reset`, - // #57), so imported==0 no longer means "root unit" per-unit. - // sepisdep (wwiout==nil <=> root/link-entry unit, #69) - // re-mangles a dep's main; only the root's stays bare. - if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - }; - // §7-A / #53: exported non-fn decls (def/type/let) path-qualify - // like fns — `export def MAX` becomes `.MAX`, not a bare - // `MAX` two packages could clash on under sep-compile. No export - // guard: every decl with a module mangles identically. - // #85 (deferred): the `nmod.len > 0` gate below retains the - // bare-NON-fn skip — the sibling of #84 (a package-less let/def/ - // type leaf colliding with an imported same-kind export) needs a - // different fix (modlookup is hint-LESS) and has no corpus repro; - // retained until then (rule-11). - if (d.kind == syntax.nkind.N_DEF) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - if (d.kind == syntax.nkind.N_TYPEDECL) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; - c.mods = m; - }; - }; - if (d.kind == syntax.nkind.N_LET) { - if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=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 (syntax.streq(m.mname, name)) { return m.nmod; }; - m = m.mnext; - }; - let empty: str; - empty.ptr = nil; - empty.len = 0; - return empty; -}; - -// usehint — M1 #22: map a qualified-ref alias (`utf8`) to its dotted -// import path (`encoding.utf8`) so the codegen hint keys the path-keyed -// mods map. For single-level packages alias == path (no-op). Returns the -// alias unchanged when no matching `use` exists. -// -// NOT file-global (#40): two modules in one unit may bind the same leaf -// alias to different paths (module one's `import a.math` vs module two's -// `import b.math`, both alias `math`). The `use` declared in the SAME -// module as the reference (c.curmod) is authoritative; preferring it -// routes each `math.pick()` to its own package. Falls back to any -// matching alias when c.curmod has no own import. Mirrors the checker's -// use_path curmod-preference (cstage check.c, M1 55f54fb). -fn usehint(c: *cgen, alias: str) str = { - let m: *modent = c.uses; - let any: str; - any.ptr = nil; - any.len = 0; - for (m != nil) { - if (syntax.streq(m.mname, alias)) { - if (syntax.streq(m.omod, c.curmod)) { return m.nmod; }; - if (any.ptr == nil && any.len == 0) { any = m.nmod; }; - }; - m = m.mnext; - }; - if (any.ptr != nil || any.len != 0) { return any; }; - return alias; -}; - -// 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 (syntax.streq(m.mname, name)) { - if (m.nmod.len == 0) { - // #84: a bare-module entry (now registered). A bare-ident - // ref (hint.len==0: the caller is itself in the bare/root - // module) resolves to it — bare wins over an imported - // same-leaf fn, by construction, order-independent. A - // non-empty hint named a module, so a bare decl is - // irrelevant: skip it, leaving the legacy first-imported- - // match untouched (byte-id-neutral for moduled callers). - if (hint.len == 0) { - let empty: str; - empty.ptr = nil; - empty.len = 0; - return empty; - }; - } else { - if (hint.len > 0 && syntax.streq(m.nmod, hint)) { - return m.nmod; - }; - if (first.len == 0 && first.ptr == nil) { - first = m.nmod; - }; - }; - }; - m = m.mnext; - }; - return first; -}; - -// emitsymname — write the asm symbol name for `ident`. Honours, in -// order: FFI mapping (@symbol), module mangling (private decls), bare -// name. Use everywhere a top-level non-fn name is emitted before `(SB)` -// — DATA labels for top-level lets/defs, address-of-let, etc. Fn names -// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates -// cross-module same-leaf fn exports. -fn emitsymname(c: *cgen, ident: str) void = { - let resolved: str = ffiresolve(c, ident); - if (resolved.ptr != ident.ptr) { - // FFI hit — emit the mapped linker symbol verbatim. - emitbytes( resolved.ptr, resolved.len: u64); - return; - }; - let mod: str = modlookup(c, ident); - if (mod.len > 0) { - emitbytes( mod.ptr, mod.len: u64); - emitbytes( ".".ptr, 1u64); - }; - emitbytes( ident.ptr, ident.len: u64); -}; - -// emitfnname — write the asm symbol name for a fn `ident`, threading -// `hint` (the explicit module from a `mod.fn` use site, or c.curmod -// for bare-IDENT calls) through modlookupforfn. Same FFI override -// semantics as emitsymname; same dot-separator format. Use at every -// CALL / LEAQ-of-fn / TEXT-def site. -fn emitfnname(c: *cgen, ident: str, hint: str) void = { - let resolved: str = ffiresolve(c, ident); - if (resolved.ptr != ident.ptr) { - emitbytes( resolved.ptr, resolved.len: u64); - return; - }; - let mod: str = modlookupforfn(c, ident, hint); - if (mod.len > 0) { - emitbytes( mod.ptr, mod.len: u64); - emitbytes( ".".ptr, 1u64); - }; - emitbytes( ident.ptr, ident.len: u64); -}; - -// ---- FFI map --------------------------------------------------------- - -fn fficollect(c: *cgen, file: *syntax.node) void = { - c.ffis = nil; - if (file == nil) { return; }; - let d: *syntax.node = file.list; - for (d != nil) { - if (d.kind == syntax.nkind.N_FNDECL) { - let a: *syntax.node = d.attr; - for (a != nil) { - if (a.kind == syntax.nkind.N_ATTR) { - let aname: str = a.str; - if (syntax.streq(aname, "symbol")) { - let symnode: *syntax.node = a.list; - if (symnode != nil) { - if (symnode.kind == syntax.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 (syntax.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 "?"; -}; - -//ww:module-reset -// 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 syntax; -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; -}; - diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww deleted file mode 100644 index d19a21a5..00000000 --- a/selfhost/test/smoke.combined.ww +++ /dev/null @@ -1,6633 +0,0 @@ -//ww:module time -// 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; -}; - -//ww:module rt -// 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; - -//ww:module os -// os — process and filesystem facade. The body of each call lands -// either in libwwrt.a (rt_syscall trampoline) or libc bindings, -// depending on how the program was linked. - -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; -}; - -//ww:module strconv -// 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; -}; - -//ww:module math -// 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); -}; - -//ww:module math -// 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; -}; - -//ww:module strconv -// 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; -}; - -//ww:module strconv -// 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, -]; - -//ww:module types -// 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; - -export def I8_MAX: i8 = 127; -export def I16_MAX: i16 = 32767; -export def I32_MAX: i32 = 2147483647; -export def I64_MAX: i64 = 9223372036854775807; - -export def I8_MIN: i8 = -128; -export def I16_MIN: i16 = -32768; -export def I32_MIN: i32 = -2147483648; -export def I64_MIN: i64 = -9223372036854775808; - -export def U8_MAX: u8 = 255; -export def U16_MAX: u16 = 65535; -export def U32_MAX: u32 = 4294967295; -export def U64_MAX: u64 = 18446744073709551615; - -export def U8_MIN: u8 = 0; -export def U16_MIN: u16 = 0; -export def U32_MIN: u32 = 0; -export 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. -export def INT_MAX: int = (1 << (size(int)*8 - 1)) - 1; -export def INT_MIN: int = -1 << (size(int)*8 - 1); -export def UINT_MIN: uint = 0; -export def UINT_MAX: uint = ~(0: uint); - -// size is 8B on amd64; no cast needed (size ∈ unsigned class per #113). -export def SIZE_MIN: size = U64_MIN; -export def SIZE_MAX: size = U64_MAX; - -// uintptr not in the unsigned class, so the cast is required (Hare's form). -export def UINTPTR_MIN: uintptr = U64_MIN: uintptr; -export def UINTPTR_MAX: uintptr = U64_MAX: uintptr; - -export def RUNE_MIN: rune = '\0'; -// Hare spells this `'\U0010ffff'` (ref/hare/types/limits.ha:57); ww's lexer -// has no \U/\u escape (cmd/wcc/lex.c escape(): \xHH only, max 0xFF) so the -// codepoint is written as an int-cast — same value, forced spelling. #50. -export def RUNE_MAX: rune = 0x10ffff: rune; - -//ww:module bytes -// 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. -// - peektoken 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 nexttoken / peektoken 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 nexttoken yields the -// last token, last nexttoken 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; -}; - -// peektoken — next token without advancing the cursor. Returns done -// once `s.delim` has been zeroed by a prior past-end nexttoken. -// ref/hare/bytes/tokenize.ha:91. -export fn peektoken(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; -}; - -// nexttoken — 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 nexttoken(s: *tokenizer) ([]u8 | done) = { - let b: []u8; - match (peektoken(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; -}; - -// remainingtokens — the unconsumed portion of `s.in`. Read-only view. -// ref/hare/bytes/tokenize.ha:145. -export fn remainingtokens(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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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 (nexttoken(&tok)) { - case let s: []u8 => { append(toks, s); }; - case done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case done => void; - case let pk: []u8 => { - let r: []u8 = remainingtokens(&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); - }; - }; -}; - -//ww:module encoding.utf8 -// 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; -}; - - -//ww:module strings -// 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 -// nexttoken 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...); -}; - -// nexttoken — current token, advancing the cursor. -// ref/hare/strings/tokenize.ha:62. -export fn nexttoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.nexttoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// peektoken — current token without advancing. -// ref/hare/strings/tokenize.ha:71. -export fn peektoken(s: *tokenizer) (str | bytes.done) = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - match (bytes.peektoken(b)) { - case let v: []u8 => return frombytes(v); - case bytes.done => { let d: bytes.done; return d; }; - }; -}; - -// remainingtokens — unconsumed portion of the input ahead of the -// cursor. ref/hare/strings/tokenize.ha:79. -export fn remainingtokens(s: *tokenizer) str = { - let b: *bytes.tokenizer = s: *bytes.tokenizer; - return frombytes(bytes.remainingtokens(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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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 (nexttoken(&tok)) { - case let s: str => { append(toks, s); }; - case bytes.done => { return toks; }; - }; - i += 1; - }; - match (peektoken(&tok)) { - case bytes.done => void; - case let pk: str => { - let r: str = remainingtokens(&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); -}; - -//ww:module ascii -// ascii — rune-class predicates and case folding for the ASCII range. -// Matches Hare's ascii::isdigit family (rune-taking signature). Runes -// outside 0..127 always answer `false`. The lexer hot path uses these -// inline; they are expected to inline to a couple of compares. - -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); -}; - -//ww:module strconv -// 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) -}; - -//ww:module strconv -// 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], -]; - -//ww:module strconv -// strconv — number↔string conversions. -// -// Mirrors Hare's strconv:: surface. The *tos functions return a -// `const str` view into a module-level buffer that is overwritten on -// the next call to the same function; callers must copy the bytes if -// they need to outlive the next invocation. See [[strings.dup]] to -// duplicate. Matches Hare's strconv::*tos semantics. - -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(""); -}; - -//ww:module-reset -// selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. -// -// Exercises the patterns the real ww-side compiler port will use: -// - bump arena allocator (mem.ww shape) -// - error idiom (T | str) -// - struct of fn pointers + ctx pointer (the io.stream-style -// polymorphism we use instead of interfaces) -// - byte-level scanning that mirrors the hot path inside lex.ww -// - strconv round-trip via the real stdlib -// -// `main` returns 42 when every check passes, 1..N on failure -// indicating which probe broke. The 990_selfhost test asserts 42. -// -// Note: only stack-local mutable state. Top-level `let` mutation -// requires a writable .data segment in w6l, which is a separate -// task; until then we exercise polymorphism via ctx pointers, which -// is what the real port wants anyway. - -package test; - -import os; -import strconv; -import ascii; - -// --- bump arena --------------------------------------------------------- - -type arena = struct { - buf: *u8, - off: u64, - cap: u64, -}; - -// In-place init. Returning a 24-byte struct by value isn't yet -// supported in w6c (SysV requires a hidden return-slot pointer for -// structs >16 bytes), so we initialize through a pointer like the -// real compiler does today. -fn arena_init(a: *arena, buf: *u8, cap: u64) void = { - a.buf = buf; - a.off = 0u64; - a.cap = cap; -}; - -fn arena_alloc(a: *arena, n: u64) *u8 = { - if (n > a.cap - a.off) { return nil; }; - let p: *u8 = a.buf + a.off; - a.off += n; - return p; -}; - -// --- (i32 | str) error idiom ------------------------------------------- - -fn checked_div(num: i32, den: i32) (i32 | str) = { - if (den == 0) { return "div by zero"; }; - return num / den; -}; - -// --- struct-of-fn-pointer polymorphism --------------------------------- -// -// A trivial "writer" abstraction: a function pointer plus a context. -// This mirrors how io.stream / Plan 9 Bio work. The ctx pointer lets -// the implementation own its own state without a global. - -type counter = struct { - n: i32, -}; - -type writer = struct { - ctx: *void, - emit: fn(ctx: *void, b: u8) void, -}; - -fn count_emit(ctx: *void, b: u8) void = { - let c: *counter = ctx: *counter; - c.n += 1; -}; - -// --- size/align/offset typed-builtin fixtures (#42) -------------------- - -type point = struct { - x: i32, - y: i32, -}; - -// Mixed-alignment struct: i8 lays at 0, then i64 needs to skip to -// offset 8 (the i64's natural align). Probe asserts both ends. -type mixalign = struct { - tag: i8, - val: i64, -}; - -// --- byte scanner like lex.ww's hot path ------------------------------- - -fn count_digits(s: str) i32 = { - let i: i32 = 0; - let n: i32 = 0; - for (i < s.len) { - let c: u8 = s[i]; - if (c >= 48u8) { - if (c <= 57u8) { n += 1; }; - }; - i += 1; - }; - return n; -}; - -// --- entry -------------------------------------------------------------- - -export fn main() i32 = { - // Probe 1 — arena hands out distinct pointers, refuses oversize. - let buf: [256]u8; - let a: arena; - arena_init(&a, buf.ptr, 256u64); - let p1: *u8 = arena_alloc(&a, 32u64); - let p2: *u8 = arena_alloc(&a, 32u64); - if (p1 == nil) { return 1; }; - if (p2 == nil) { return 2; }; - if (p1 == p2) { return 3; }; - let p3: *u8 = arena_alloc(&a, 1024u64); - if (p3 != nil) { return 4; }; - - // Probe 2 — error union both ways. - let r_ok: (i32 | str) = checked_div(84, 2); - let r_bad: (i32 | str) = checked_div(1, 0); - let acc: i32 = 0; - match (r_ok) { - case let v: i32 => acc = v; - case let e: str => return 5; - }; - if (acc != 42) { return 6; }; - match (r_bad) { - case let v: i32 => return 7; - case let e: str => acc = e.len: i32; - }; - if (acc != 11) { return 8; }; // len("div by zero") == 11 - - // Probe 3 — struct-of-fn-pointer dispatch via ctx pointer. - let c: counter = counter { n = 0 }; - let w: writer = writer { ctx = (&c): *void, emit = count_emit }; - w.emit(w.ctx, 65u8); - w.emit(w.ctx, 66u8); - w.emit(w.ctx, 67u8); - if (c.n != 3) { return 9; }; - - // Probe 4 — byte scan over a literal. - let dn: i32 = count_digits("ww123abc"); - if (dn != 3) { return 10; }; - - // Probe 5 — strconv round-trip via the real stdlib. - let s: str = strconv.i64tos(4242i64, strconv.base.DEC); - if (s.len != 4) { return 11; }; - if (s.ptr[0] != 52u8) { return 12; }; // '4' - if (s.ptr[3] != 50u8) { return 13; }; // '2' - - // Probe 6 — ascii classifications (rune-taking, Hare-shaped). - if (!ascii.isdigit(53)) { return 14; }; // '5' - if (ascii.isdigit(65)) { return 15; }; // 'A' is not a digit - if (!ascii.isalpha(122)) { return 16; }; // 'z' - if (!ascii.isxdigit(70)) { return 17; }; // 'F' - if (ascii.isxdigit(71)) { return 18; }; // 'G' is not hex - if (ascii.tolower(65) != 97) { return 19; }; // 'A' -> 'a' - if (ascii.toupper(122) != 90) { return 20; }; // 'z' -> 'Z' - - // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline - // always exists on Linux, no write side, and is non-empty. - let path: str = "/proc/self/cmdline"; - // Use raw os.open here (returns i32 with -errno) for the same - // reason as os.read below: probe 6 in 990_selfhost compiles - // smoke.ww standalone (no `use` expansion), so cross-module type - // references like `os.oserror` and `os.flag` don't resolve at - // that step. RDONLY is 0; passing the literal keeps the call - // site standalone-compilable to byte-identical asm on both - // compilers. - let fd: i32 = os.open(path, 0, 0i32); - if (fd < 0) { return 21; }; - let rbuf: [128]u8; - // Use raw os.read here (single syscall, plain i64) instead of - // os.readall: the 990 cgen-match probe compiles smoke.ww - // standalone without `use os;` expansion, so cross-module type - // references like `os.oserror` can't be resolved. - let n: i64 = os.read(fd, rbuf.ptr, 128u64); - os.close(fd); - if (n <= 0i64) { return 22; }; - - // Probe 8 — size(T) / align(T) / offset(e.f) typed-builtin folds - // (#42). Each call folds to an N_INTLIT at check time; cgen - // materialises the literal as a plain `MOVQ $N, AX`. Mirrors - // cstage cmd/wcc/check.c:907-960 byte-for-byte on this corpus. - if (size(str) != 24) { return 23; }; // str IS []u8: {ptr,len,cap} 24B (#1/Phase 3) - if (size(i64) != 8) { return 24; }; - if (size(i32) != 4) { return 25; }; - if (align(i64) != 8) { return 26; }; - if (align(i32) != 4) { return 27; }; - // Initialize struct locals explicitly so the cgen path doesn't - // drift from cstage on bare `let X: T;` zero-init (pre-existing - // wwstage divergence outside #42). - let pt: point = point { x = 0, y = 0 }; - if (offset(pt.x) != 0) { return 28; }; - if (offset(pt.y) != 4) { return 29; }; - let mx: mixalign = mixalign { tag = 0i8, val = 0i64 }; - if (offset(mx.tag) != 0) { return 30; }; - if (offset(mx.val) != 8) { return 31; }; // align-padded to 8 - // Width breadth: smallest prim, ptr, slice, struct (8B + padded), - // covering astsize's TPTR/TSLICE/TNAME-resolve-to-struct arms. - if (size(i8) != 1) { return 32; }; - if (align(i8) != 1) { return 33; }; - if (size(*i32) != 8) { return 34; }; - if (size([]i32) != 24) { return 35; }; - if (size(point) != 8) { return 36; }; - if (size(mixalign) != 16) { return 37; }; - - return 42; -}; - diff --git a/test/wcc/901_asserttyped_gap.c b/test/wcc/901_asserttyped_gap.c index b5419190..a097a3ab 100644 --- a/test/wcc/901_asserttyped_gap.c +++ b/test/wcc/901_asserttyped_gap.c @@ -12,13 +12,20 @@ * without gating on the bail's exit so a regression names its class. * * This probe runs the wwstage checker (wwdump_ww -c) over the - * gap-bearing combined.ww corpus, counts the per-file asserttyped - * diagnostics on stderr, and pins each count against the manifest - * below. A fresh nil-gap (count up) or a regressed fixed class (count - * up from 0) fails loud; a fold that closes a class drives its count - * down, which fails until the manifest is edited to match. Every class - * has reached all-zero, so the manifest is now the all-closed floor - * that holds the armed bail green. + * gap-bearing corpus, counts the per-file asserttyped diagnostics on + * stderr, and pins each count against the manifest below. A fresh + * nil-gap (count up) or a regressed fixed class (count up from 0) fails + * loud; a fold that closes a class drives its count down, which fails + * until the manifest is edited to match. Every class has reached + * all-zero, so the manifest is now the all-closed floor that holds the + * armed bail green. + * + * #90 sep-feed: the E3 flip retired the combined.ww amalgamator, so the + * library/selfhost gap fixtures (A-D) feed their RESOLVED sep unit (see + * resolveunit — `ww build --sep` composes .sepwork/__root.unit.ww + * from the dep `.wwi` stubs + the root body) rather than a pre-built + * .combined.ww. The single-file, import-free test fixtures (E-G) + * carry no imports and stay raw-fed. * * Gap classes (counts verified empirically at this revision): * A module-qual N_DOT call result checked_test 0 (closed) @@ -39,6 +46,7 @@ #include #include #include +#include #include static int @@ -82,6 +90,52 @@ slurp(const char *path, char **outbuf, size_t *outlen) return 0; } +/* resolveunit — compose 's RESOLVED translation unit the way the + * sep driver does. Mirrors 990_selfhost's resolveunit: copy the fixture's + * whole package directory into a private /tmp dir (so a white-box `_test` + * file's sibling package sources resolve, and the sepwork lands off the repo + * tree), `ww build --sep `, then read .sepwork/__root.unit.ww — + * the single self-contained unit w6c/wwdump consume for the root. Feeding the + * raw module file instead would leave its import refs (os/fmt/strconv …) + * unresolved, a partial unit the asserttyped invariant must never see. The + * link step fails for an import-only module (no main), but the unit is + * written before the link, so we detect the artifact rather than gating on + * the build exit. Writes the owning tmpdir into `td` (caller rm -rf's it) and + * the unit path into `out`. Returns 0 on success, -1 otherwise. */ +static int +resolveunit(const char *bin, const char *cwd, const char *fixture, + int idx, char *td, size_t tdsz, char *out, size_t outsz) +{ + char cmd[2048]; + snprintf(td, tdsz, "/tmp/atgap_ru_%d_%d", getpid(), idx); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + const char *slash = strrchr(fixture, '/'); + const char *base = slash ? slash + 1 : fixture; + char dir[1024]; + if (slash) { + size_t dl = (size_t)(slash - fixture); + if (dl >= sizeof dir) return -1; + memcpy(dir, fixture, dl); + dir[dl] = '\0'; + } else { + dir[0] = '.'; dir[1] = '\0'; + } + snprintf(cmd, sizeof cmd, "cp %s/%s/*.ww %s/ 2>/dev/null", cwd, dir, td); + runwait(cmd); + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/ww build --sep %s >/dev/null 2>&1", + td, bin, base); + runwait(cmd); + char stem[1024]; + snprintf(stem, sizeof stem, "%s/%s", td, base); + char *dot = strrchr(stem, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + snprintf(out, outsz, "%s.sepwork/__root.unit.ww", stem); + return access(out, 0) == 0 ? 0 : -1; +} + /* Count line-leading "asserttyped:" diagnostics; each warn is one such * line and no other wwdump output carries the prefix. */ static int @@ -109,30 +163,47 @@ main(void) char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; - struct { const char *rel; const char *cls; int want; } manifest[] = { - { "lib/math/checked/checked_test.combined.ww", - "A module-qual N_DOT call result", 0 }, - { "selfhost/test/smoke.combined.ww", - "B fn-ptr struct-field call", 0 }, - { "lib/encoding/utf8/utf8.combined.ww", - "C abort intrinsic callee", 0 }, - { "lib/fnmatch/fnmatchtest.combined.ww", - "D module-leaf == type/fn name", 0 }, - { "lib/math/random/random_test.combined.ww", - "D module-leaf == type/fn name", 0 }, + /* sep=1 fixtures feed their resolved sep unit (resolveunit); sep=0 + * fixtures are import-free single files fed raw. */ + struct { const char *rel; const char *cls; int want; int sep; } manifest[] = { + { "lib/math/checked/checked_test.ww", + "A module-qual N_DOT call result", 0, 1 }, + { "selfhost/test/smoke.ww", + "B fn-ptr struct-field call", 0, 1 }, + { "lib/encoding/utf8/utf8.ww", + "C abort intrinsic callee", 0, 1 }, + { "lib/fnmatch/fnmatchtest.ww", + "D module-leaf == type/fn name", 0, 1 }, + { "lib/math/random/random_test.ww", + "D module-leaf == type/fn name", 0, 1 }, { "test/wcc/901_enum_corpus.ww", - "E computed enum-member value-expr", 0 }, + "E computed enum-member value-expr", 0, 0 }, { "test/wcc/901_forrange_tuple.ww", - "F for-range tuple-destructure bind", 0 }, + "F for-range tuple-destructure bind", 0, 0 }, { "test/wcc/901_massign_blank.ww", - "G tuple multi-assign discard `_`", 0 }, - { NULL, NULL, 0 }, + "G tuple multi-assign discard `_`", 0, 0 }, + { NULL, NULL, 0, 0 }, }; int fail = 0, n = 0; for (int i = 0; manifest[i].rel; i++) { char src[2048], errf[64], cmd[4096]; - snprintf(src, sizeof src, "%s/%s", cwd, manifest[i].rel); + char td[64] = {0}, unit[1280]; + if (manifest[i].sep) { + if (resolveunit(bin, cwd, manifest[i].rel, i, td, + sizeof td, unit, sizeof unit) != 0) { + fprintf(stderr, "asserttyped_gap FAIL: %s [%s] " + "no resolved sep unit\n", + manifest[i].rel, manifest[i].cls); + if (td[0]) { snprintf(cmd, sizeof cmd, + "rm -rf %s", td); runwait(cmd); } + fail++; n++; + continue; + } + snprintf(src, sizeof src, "%s", unit); + } else { + snprintf(src, sizeof src, "%s/%s", cwd, manifest[i].rel); + } snprintf(errf, sizeof errf, "/tmp/atgap_%d_%d.err", getpid(), i); snprintf(cmd, sizeof cmd, "timeout 180 %s/wwdump_ww -c %s >/dev/null 2>%s", @@ -152,6 +223,8 @@ main(void) fail++; } unlink(errf); + if (td[0]) { snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); } } if (fail) { diff --git a/test/wcc/990_selfhost.c b/test/wcc/990_selfhost.c index e522f51e..95740e57 100644 --- a/test/wcc/990_selfhost.c +++ b/test/wcc/990_selfhost.c @@ -113,7 +113,6 @@ probe_smoke(const char *bin) char tmp[1024]; snprintf(tmp, sizeof tmp, "%s/smoke.s", tmpdir); unlink(tmp); snprintf(tmp, sizeof tmp, "%s/smoke.o", tmpdir); unlink(tmp); - snprintf(tmp, sizeof tmp, "%s/smoke.combined.ww", tmpdir); unlink(tmp); rmdir(tmpdir); if (rc != 42) { fprintf(stderr, "smoke FAIL: exit=%d (want 42; the number "