From f4743dc5d58075eaccde6f4dffd7ef92dddb900c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 03:09:30 +0900 Subject: [PATCH] lib/strconv: add f64tos --- .gitignore | 7 + Makefile | 3 +- lib/strconv/strconv.ww | 81 ++ selfhost/cmd/w6c/main.combined.ww | 1242 +++++++++++++++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 1242 +++++++++++++++++++++++--- selfhost/test/smoke.combined.ww | 81 ++ 6 files changed, 2399 insertions(+), 257 deletions(-) diff --git a/.gitignore b/.gitignore index 59e3938f..35aa6b41 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,13 @@ lib/**/*.combined.ww examples/**/*.o examples/**/*.s examples/**/*.combined.ww + +# `test/wcc/data/` holds .ww fixtures fed to the C-side wcc tests +# (e.g. attest_pass.ww). `ww build` against any of those drops the +# usual triplet next to the source — only the .ww is tracked. +test/wcc/data/*.o +test/wcc/data/*.s +test/wcc/data/*.combined.ww examples/mandelbrot/mandelbrot examples/cmatrix/cmatrix examples/lisp/lisp diff --git a/Makefile b/Makefile index 99602dff..9c65881e 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ - lib/os/os.ww \ + lib/os/os.ww lib/strconv/strconv.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) cd $(BIN) && ./ww build \ @@ -181,6 +181,7 @@ $(BIN)/w6l_ww: selfhost/cmd/w6l/main.ww selfhost/cmd/w6l/sym.ww \ # The driver pulls in lib/os (default search path) and selfhost/cmd/wcc # (for the bump arena). It then orchestrates w6c/w6a/w6l like the C driver. $(BIN)/ww_ww: selfhost/cmd/ww/main.ww selfhost/cmd/wcc/mem.ww lib/os/os.ww \ + lib/strconv/strconv.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) cd $(BIN) && ./ww build \ diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index 3e36ae41..eb05271c 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -105,3 +105,84 @@ export fn stou64(s: str) (u64 | invalid | overflow) = { }; return v; }; + +// f64tos — write `v` in decimal into `buf` and return the byte count. +// Hare name; this is the buffer-in Plan 9 subset of Hare's +// `f64tos(n) const str`. Today's surface: +// +// - finite values only. NaN/±Inf detection needs an f64→u64 bit +// reinterpret cast that the cgen doesn't expose yet. +// - fixed-point only, up to 6 fractional digits. Trailing zeros +// after the decimal point are trimmed. Trailing '.' is dropped. +// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) +// fall back to the literal token "huge". Hare would print these +// in scientific notation via Ryū; we will graduate when the +// compiler grows the bit-reinterpret cast. +// +// Round-trip is therefore lossy past 6 fractional digits; callers +// that need bit-exact recovery should not use this until the +// graduate-to-Ryū step lands. `f64tos(buf, 1.0)` writes "1" (no +// decimal point), `f64tos(buf, 1.5)` writes "1.5", `f64tos(buf, +// 0.1)` writes "0.1". +// +// No float literals in the body — 990's wwdump diff requires this +// file's TK_FLOAT count to match between C and ww front-ends, and +// the ww-side wwdump currently skips TK_FLOAT.fval while the C side +// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: +// build f64 constants via int-to-f64 casts. +export fn f64tos(buf: []u8, v: f64) i32 = { + let out: i32 = 0; + let f: f64 = v; + let zero: f64 = 0: f64; + if (f < zero) { + buf[out] = 45u8; // '-' + out += 1; + f = -f; + }; + // 9e18 is comfortably under I64_MAX (9.22e18). Past this the + // `f: i64` cast wraps and the integer part comes back as garbage. + let cap: f64 = 9000000000000000000i64: f64; + if (f >= cap) { + let s: str = "huge"; + let k: i32 = 0; + for (k < s.len) { buf[out] = s[k]; out += 1; k += 1; }; + return out; + }; + let ip: i64 = f: i64; + // Fractional part scaled to 6 decimal digits, with round-to- + // nearest via +0.5. (f64 compound assigns mis-lower in cgen — + // use the explicit form, as the rest of lib does.) + let frac: f64 = f - (ip: f64); + let scale: f64 = 1000000: f64; + frac = frac * scale; + let half: f64 = (1: f64) / (2: f64); + let fp: i64 = (frac + half): i64; + // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer + // part needs to advance. + if (fp >= 1000000) { + ip += 1; + fp = 0; + }; + let itmp: [32]u8; + let in: i32 = i64tos(itmp[0:32], ip); + let k: i32 = 0; + for (k < in) { buf[out] = itmp[k]; out += 1; k += 1; }; + if (fp == 0) { return out; }; + buf[out] = 46u8; // '.' + out += 1; + let ftmp: [16]u8; + let m: i32 = u64tos(ftmp[0:16], fp: u64); + // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → + // fp=50000, m=5, pad one '0' before "50000"). + let z: i32 = 6 - m; + for (z > 0) { buf[out] = 48u8; out += 1; z -= 1; }; + k = 0; + for (k < m) { buf[out] = ftmp[k]; out += 1; k += 1; }; + // Trim trailing zeros in the fractional part (we know fp != 0, + // so the loop stops before erasing the dot). + for (out > 0) { + if (buf[out - 1] != 48u8) { break; }; + out -= 1; + }; + return out; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 2c553c58..c4dc8aab 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -444,6 +444,87 @@ export fn stou64(s: str) (u64 | invalid | overflow) = { return v; }; +// f64tos — write `v` in decimal into `buf` and return the byte count. +// Hare name; this is the buffer-in Plan 9 subset of Hare's +// `f64tos(n) const str`. Today's surface: +// +// - finite values only. NaN/±Inf detection needs an f64→u64 bit +// reinterpret cast that the cgen doesn't expose yet. +// - fixed-point only, up to 6 fractional digits. Trailing zeros +// after the decimal point are trimmed. Trailing '.' is dropped. +// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) +// fall back to the literal token "huge". Hare would print these +// in scientific notation via Ryū; we will graduate when the +// compiler grows the bit-reinterpret cast. +// +// Round-trip is therefore lossy past 6 fractional digits; callers +// that need bit-exact recovery should not use this until the +// graduate-to-Ryū step lands. `f64tos(buf, 1.0)` writes "1" (no +// decimal point), `f64tos(buf, 1.5)` writes "1.5", `f64tos(buf, +// 0.1)` writes "0.1". +// +// No float literals in the body — 990's wwdump diff requires this +// file's TK_FLOAT count to match between C and ww front-ends, and +// the ww-side wwdump currently skips TK_FLOAT.fval while the C side +// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: +// build f64 constants via int-to-f64 casts. +export fn f64tos(buf: []u8, v: f64) i32 = { + let out: i32 = 0; + let f: f64 = v; + let zero: f64 = 0: f64; + if (f < zero) { + buf[out] = 45u8; // '-' + out += 1; + f = -f; + }; + // 9e18 is comfortably under I64_MAX (9.22e18). Past this the + // `f: i64` cast wraps and the integer part comes back as garbage. + let cap: f64 = 9000000000000000000i64: f64; + if (f >= cap) { + let s: str = "huge"; + let k: i32 = 0; + for (k < s.len) { buf[out] = s[k]; out += 1; k += 1; }; + return out; + }; + let ip: i64 = f: i64; + // Fractional part scaled to 6 decimal digits, with round-to- + // nearest via +0.5. (f64 compound assigns mis-lower in cgen — + // use the explicit form, as the rest of lib does.) + let frac: f64 = f - (ip: f64); + let scale: f64 = 1000000: f64; + frac = frac * scale; + let half: f64 = (1: f64) / (2: f64); + let fp: i64 = (frac + half): i64; + // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer + // part needs to advance. + if (fp >= 1000000) { + ip += 1; + fp = 0; + }; + let itmp: [32]u8; + let in: i32 = i64tos(itmp[0:32], ip); + let k: i32 = 0; + for (k < in) { buf[out] = itmp[k]; out += 1; k += 1; }; + if (fp == 0) { return out; }; + buf[out] = 46u8; // '.' + out += 1; + let ftmp: [16]u8; + let m: i32 = u64tos(ftmp[0:16], fp: u64); + // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → + // fp=50000, m=5, pad one '0' before "50000"). + let z: i32 = 6 - m; + for (z > 0) { buf[out] = 48u8; out += 1; z -= 1; }; + k = 0; + for (k < m) { buf[out] = ftmp[k]; out += 1; k += 1; }; + // Trim trailing zeros in the fractional part (we know fp != 0, + // so the loop stops before erasing the dot). + for (out > 0) { + if (buf[out - 1] != 48u8) { break; }; + out -= 1; + }; + return out; +}; + // MODULE: lex // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. @@ -5179,13 +5260,11 @@ fn nodeisstr(c: *cgen, n: *node) bool = { let nm: str = n.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == nkind.N_TNAME) { - let tnm: str = tn.str; - if (streq(tnm, "str")) { return true; }; - }; - }; + // Use isstrtype so `!str` aliases (parserr = !str) and + // `type foo = str;` chains resolve through. The bare + // `streq("str", ...)` test missed them and dropped the + // MOVQ BX,CX shuffle on returns of str-aliased locals. + if (isstrtype(c, lc.tnode)) { return true; }; }; return false; }; @@ -5673,11 +5752,88 @@ fn primsize(name: str) i32 = { return 0; }; +// variantnamematch — tagged-union variant names are compared as if +// they'd been alias-resolved. Pattern names can be module-qualified +// (`strconv.invalid` from a `case let e: strconv.invalid =>`), +// while the variant's declared name inside its own module is bare +// (`invalid`). With no checker the cgen can't follow imports, so we +// accept exact match plus suffix-after-`.` on either side. Mirrors +// the C cgen's type_eq, which goes through resolved Type pointers. +fn variantnamematch(vname: str, pname: str) bool = { + if (streq(vname, pname)) { return true; }; + // `pname` is qualified, `vname` is bare: drop module prefix. + let i: i32 = 0; + for (i < pname.len) { + if (pname[i] == '.': u8) { + let tail: str; + tail.ptr = pname.ptr + i + 1; + tail.len = pname.len - i - 1; + if (streq(tail, vname)) { return true; }; + }; + i += 1; + }; + // `vname` is qualified, `pname` is bare: same trick in reverse. + let j: i32 = 0; + for (j < vname.len) { + if (vname[j] == '.': u8) { + let tail: str; + tail.ptr = vname.ptr + j + 1; + tail.len = vname.len - j - 1; + if (streq(tail, pname)) { return true; }; + }; + j += 1; + }; + return false; +}; + +// inferletcalltype — for an annotation-less `let x = expr;`, return +// a usable tnode for cgen's struct-aware paths. Today: `let x = +// f()?` infers x's type from the success variant of f's tagged +// return; without this, x has tnode = nil and `x.field` falls into +// the SB-symbol fallback (linker reports `undefined reference to +// `). We don't infer for plain `let x = f()` yet — +// non-tagged returns don't carry their type back the same way. +fn inferletcalltype(c: *cgen, rhs: *node) *node = { + if (rhs == nil) { return nil; }; + // `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged + // return to its success variant; the rhs we want the type of + // is the inner call expression. + let unwrap: bool = false; + let call: *node = rhs; + if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; }; + if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; }; + if (call == nil) { return nil; }; + if (call.kind != nkind.N_CALL) { return nil; }; + let callee: *node = call.lhs; + if (callee == nil) { return nil; }; + let cname: str; + cname.ptr = nil; cname.len = 0; + if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; + if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + if (cname.len == 0) { return nil; }; + let rt: *node = fnretlookup(c, cname); + if (rt == nil) { return nil; }; + if (unwrap) { + // Strip error variants — success type is the first + // variant of the tagged return. + if (rt.kind != nkind.N_TTAGGED) { return nil; }; + return rt.list; + }; + // Plain call: declared return type is the local's type. + return rt; +}; + // letslotsize — slot size for a `let` binding. Like slotsize, but // detects `[_]T = arrlit;` (the type-AST has rhs == nil as the // length-inferred sentinel) and computes count × element-size from // the initialiser. Used by both scanlocals (prologue sizing) and // cglet (slot alloc) so they agree on the frame layout. +// +// `let x = f();` (no annotation): infer from `f`'s declared return +// type so a 24B tagged-union return reserves all three spill slots, +// not the default 8B. Without this, the AX:DX:CX spill in cglet's +// tagged-init branch writes past the local and tramples the next +// slot. export fn letslotsize(c: *cgen, n: *node) i32 = { // `[_]T = arrlit;` — inferred-length array. slotsize would // return elem_size * 1 (treating missing length as 1); intercept @@ -5716,7 +5872,13 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; }; }; - return slotsize(c, n.lhs); + if (n.lhs != nil) { return slotsize(c, n.lhs); }; + // Annotation-less init: defer to the call's return type if we + // can infer it. Tagged-union returns need 24B; everything else + // matches slotsize on the inferred type. + let inferred: *node = inferletcalltype(c, n.rhs); + if (inferred != nil) { return slotsize(c, inferred); }; + return 8; }; fn slotsize(c: *cgen, typn: *node) i32 = { @@ -5768,6 +5930,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = { // Named struct lookup. let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; + // Type alias (`type foo = !str;` / `type foo = bar;`): + // follow it so a tagged-union variant of a !str-aliased + // error type contributes 16 bytes to the max payload + // rather than 8 (the default). + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { + if (aliased.kind == nkind.N_TBANG) { + return slotsize(c, aliased.lhs); + }; + return slotsize(c, aliased); + }; + }; return 8; }; if (k == nkind.N_TARRAY) { @@ -5919,7 +6094,20 @@ fn isstrtype(c: *cgen, t: *node) bool = { if (isstrtyperaw(t)) { return true; }; if (c == nil) { return false; }; let r: *node = resolvetype(c, t); - return isstrtyperaw(r); + if (isstrtyperaw(r)) { return true; }; + // `parserr = !str` — `!T` aliases shouldn't hide their + // underlying type from str-routing. Unwrap and re-check. + if (r != nil) { + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (isstrtyperaw(inner)) { return true; }; + if (inner != nil) { + let r2: *node = resolvetype(c, inner); + if (isstrtyperaw(r2)) { return true; }; + }; + }; + }; + return false; }; fn isslicetyperaw(t: *node) bool = { @@ -6050,6 +6238,48 @@ export fn exprfloatkind(c: *cgen, n: *node) i32 = { }; return 0; }; + if (k == nkind.N_DOT) { + // `p.field` where the struct field is f64/f32. Without this, + // `v.fval: i64` lowers to CVTSI on an integer-load value + // instead of CVTTSD2SI on the X0 the cgdot path actually + // emits for an f64 field. + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { + if (pe.kind == nkind.N_TNAME) { sname = pe.str; }; + }; + }; + }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + if (isf32type(c, fi.tnode)) { return 1; }; + if (isfloattype(c, fi.tnode)) { return 2; }; + return 0; + }; + fi = fi.finext; + }; + }; + }; + }; + return 0; + }; return 0; }; @@ -6118,6 +6348,19 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; + // `T{}` carries its type name on the lhs N_IDENT — the parser + // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. + // Needed so `return eof{};` (variant of a tagged union) resolves + // to the `eof` variant index rather than falling through to the + // "first non-str variant" fallback in taggedvariantindex. + if (rhs.kind == nkind.N_STRUCTLIT) { + let tref: *node = rhs.lhs; + if (tref != nil) { + if (tref.kind == nkind.N_IDENT) { return tref.str; }; + if (tref.kind == nkind.N_TNAME) { return tref.str; }; + }; + return nm; + }; if (rhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { @@ -6143,7 +6386,7 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { let idx: i32 = 0; for (v != nil) { if (v.kind == nkind.N_TNAME) { - if (streq(v.str, wantname)) { return idx; }; + if (variantnamematch(v.str, wantname)) { return idx; }; }; v = v.next; idx += 1; @@ -6268,6 +6511,12 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; + // Default fallback: produce a deterministic AX = 0. Mirrors + // the C cgen's `default: cgexpr_int(c, 0)` branch, which is + // what `return eof{};` (N_STRUCTLIT with an empty !void + // variant) silently relies on — without this AX carries a + // stale value into the tagged-union return shuffle. + emitline("\tMOVQ\t$0, AX\n"); }; // cgtagvariantidx — find the 0-based variant index of `vt` inside the @@ -6692,6 +6941,14 @@ fn cgindex(c: *cgen, n: *node) void = { let esz: i32 = 8; let signed_elem: bool = false; let baselocal: *local = nil; + // Global `[N]T` array or `*T` pointer used as an index base. + // The local-ident lookup above misses it; we need LEAQ name(SB) + // (array, the symbol IS the storage) or MOVQ name(SB) (pointer, + // the symbol holds the address) to feed the addend. + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; @@ -6699,6 +6956,22 @@ fn cgindex(c: *cgen, n: *node) void = { if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); signed_elem = elemissigned(baselocal.tnode); + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeof(tn); + signed_elem = elemissigned(tn); + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeof(tn); + signed_elem = elemissigned(tn); + }; + }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); @@ -6711,6 +6984,31 @@ fn cgindex(c: *cgen, n: *node) void = { emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; + if (isglobalarr || isglobalptr) { + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + }; + emitline("\tADDQ\tAX, BX\n"); + if (esz == 16) { + emitline("\tMOVQ\t8(BX), CX\n"); + emitline("\tMOVQ\t(BX), AX\n"); + emitline("\tMOVQ\tCX, BX\n"); + return; + }; + if (esz == 1) { emitline("\tMOVZBQ\t(BX), AX\n"); } + else { if (esz == 4) { + if (signed_elem) { emitline("\tMOVSXD\t(BX), AX\n"); } + else { emitline("\tMOVL\t(BX), AX\n"); }; + } + else { emitline("\tMOVQ\t(BX), AX\n"); };}; + return; + }; if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; @@ -6950,7 +7248,7 @@ fn cgmatch(c: *cgen, n: *node) void = { let found: bool = false; for (v != nil) { if (v.kind == nkind.N_TNAME) { - if (streq(v.str, patname)) { + if (variantnamematch(v.str, patname)) { want = idx; found = true; v = nil; @@ -7105,6 +7403,19 @@ fn cgdot(c: *cgen, n: *node) void = { emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tCX, 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(fi); emitline("\t"); @@ -7112,7 +7423,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7138,6 +7449,15 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\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(fi); emitline("\t"); @@ -7145,7 +7465,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7332,6 +7652,15 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "CX"); emitline(", BX\n"); + } else { if (isfloattype(c, fi.tnode)) { + // f64/f32 global field: route through X0. + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", X0\n"); } else { let op: str = fieldloadop(fi); emitline("\t"); @@ -7339,7 +7668,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7347,6 +7676,71 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // `xs[i].field` — slice/array/ptr-of-struct element field access. + // Without this the cgen falls through to the module-qualified + // SB fallback below and emits `MOVQ (SB), AX` (linker + // reports `undefined reference to `). cgexpr(c, lhs) + // dispatches to cgindex which leaves the element value in AX + // — for a []*T element that's the *T pointer, so we just chain + // the field load through (AX). + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + let idxbase: *node = lhs.lhs; + if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, idxbase.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let elemt: *node = nil; + let tk: nkind = tn.kind; + if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; + if (tk == nkind.N_TARRAY) { elemt = tn.lhs; }; + if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; + if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { + let inner: *node = elemt.lhs; + if (inner != nil) { if (inner.kind == nkind.N_TNAME) { + let sname: str = inner.str; + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + cgexpr(c, lhs); // AX = *Struct + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + fi = fi.finext; + }; + }; + };}; + };}; + };}; + };}; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7389,7 +7783,6 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { if (streq(fi.fname, fld)) { cgexpr(c, lhs); // AX = ptr to inner struct - let lop: str = fieldloadop(fi); // str field: load both halves. if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); @@ -7400,6 +7793,18 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", AX\n"); return; }; + // f64/f32 chained field: route through X0. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(fi); emitline("\t"); emitline(lop); emitline("\t"); @@ -7413,6 +7818,120 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // 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`. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let inner: *node = lhs.lhs; + let innerfld: str = lhs.str; + if (inner != nil) { if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = tn.kind; + let outname: str; + outname.ptr = nil; outname.len = 0; + let isptr: bool = false; + if (lkind == nkind.N_TNAME) { outname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { if (pe.kind == nkind.N_TNAME) { + outname = pe.str; + isptr = true; + };}; + }; + if (outname.len > 0) { + let osi: *structinfo = structlookup(c, outname); + if (osi != nil) { + let ofi: *fieldinfo = osi.fields; + for (ofi != nil) { + if (streq(ofi.fname, innerfld)) { + let oft: *node = ofi.tnode; + if (oft != nil) { if (oft.kind == nkind.N_TNAME) { + if (primsize(oft.str) == 0) { + let isi: *structinfo = structlookup(c, oft.str); + if (isi != nil) { + let ffi: *fieldinfo = isi.fields; + for (ffi != nil) { + if (streq(ffi.fname, fld)) { + let totoff: i32 = ofi.foff + ffi.foff; + if (isstrtype(c, ffi.tnode)) { + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totoff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg(totoff: i64, "CX"); + emitline(", AX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff((lc.off + totoff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + totoff + 8): i64); + emitline("(BP), BX\n"); + }; + return; + }; + if (isfloattype(c, ffi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(totoff: i64, "BX"); + emitline(", X0\n"); + } else { + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((lc.off + totoff): i64); + emitline("(BP), X0\n"); + }; + return; + }; + let lop: str = fieldloadop(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; + }; + }; + }; + };}; + };}; + }; + }; return; }; @@ -7654,14 +8173,28 @@ fn cgalloc(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, f.lhs); - emitline("\tMOVQ\t(SP), BX\n"); - let sop: str = fieldstoreop(fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitint(fi.foff: i64); - emitline("(BX)\n"); - fi = nil; + // alloc(T{ fval = v }) for f64/f32 field: cgexpr left + // the value in X0, not AX — route the store via MOVSD/MOVSS. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\tMOVQ\t(SP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + } else { + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + }; } else { fi = fi.finext; }; @@ -7844,43 +8377,48 @@ fn cgcall(c: *cgen, n: *node) void = { // 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. + // register class. SysV has only 6 int arg regs (DI/SI/DX/CX/R8/R9); + // the remaining slots stay on the stack and the callee reads them + // via 16+8*k(BP). Caller-cleanup is emitted after the CALL. let intidx: i32 = 0; let fpidx: i32 = 0; let a: *node = n.list; let popped: i32 = 0; + let stackslots: i32 = 0; for (a != nil) { let fk: i32 = exprfloatkind(c, a); if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t(SP), "); - emitline(fargregname(fpidx)); - emitline("\n"); - emitline("\tADDQ\t$8, SP\n"); - fpidx += 1; + 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 { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; - popped += 1; - // Multi-word args (str=2, slice/tagged=3): drain - // the remaining words into successive int regs. let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; - let e: i32 = 0; - for (e < extra) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 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; - e += 1; + w += 1; }; }; a = a.next; @@ -7891,10 +8429,14 @@ fn cgcall(c: *cgen, n: *node) void = { // case here is identical pre-port behaviour. let i: i32 = popped; for (i < nargs) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; i += 1; }; let callee: *node = n.lhs; @@ -7981,6 +8523,14 @@ fn cgcall(c: *cgen, n: *node) void = { }; emitline("(SB)\n"); }; + // Caller cleanup for stack-passed args (args 7+, or any + // overflow past the int/float reg windows). Mirrors C cgen: + // pushed 8 bytes each, ADDQ them off after the CALL. + if (stackslots > 0) { + emitline("\tADDQ\t$"); + emitint((stackslots * 8): i64); + emitline(", SP\n"); + }; // SysV returns 16-byte aggregates in (AX, DX). Our str // convention is (AX, BX), so shuffle for str-returning calls. if (calleename.len > 0) { @@ -8018,6 +8568,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (n.op == tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let elemstr: bool = false; + let elemfloat: bool = false; + let elemf32: bool = false; let storeop: str = "MOVQ"; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { @@ -8030,11 +8582,13 @@ fn cgassign(c: *cgen, n: *node) void = { if (pe != nil) { if (pe.kind == nkind.N_TNAME) { if (streq(pe.str, "str")) { elemstr = true; } + else { if (streq(pe.str, "f64")) { elemfloat = true; } + else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; - }; + }; }; }; }; }; }; @@ -8043,6 +8597,27 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; cgexpr(c, n.rhs); + // `*p = v` for *f64 / *f32: value sits in X0. Spill + // to the stack, evaluate the pointer (clobbers AX), + // then reload X0 and MOVSD/MOVSS through the pointer. + if (elemfloat) { + let mov: str = "MOVSD"; + if (elemf32) { mov = "MOVSS"; }; + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + cgexpr(c, inner); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (BX)\n"); + return; + }; // Push order matches C cgen // (cmd/w6c/cgen.c:1033-1041): PUSHQ AX // (ptr) first, then PUSHQ BX (len) if @@ -8078,12 +8653,30 @@ fn cgassign(c: *cgen, n: *node) void = { let idx: *node = lhs.rhs; let esz: i32 = 8; let baselocal: *local = nil; + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeof(tn); + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeof(tn); + }; + }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); @@ -8100,7 +8693,15 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); // scaled idx - if (baselocal != nil) { + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; @@ -8116,7 +8717,7 @@ fn cgassign(c: *cgen, n: *node) void = { } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); - }; + };};}; emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); // value @@ -8205,6 +8806,21 @@ fn cgassign(c: *cgen, n: *node) void = { 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); @@ -8232,6 +8848,31 @@ fn cgassign(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fld)) { cgexpr(c, n.rhs); + // str field: cgexpr left (AX=ptr, BX=len); + // store both halves at +0/+8. Without this, + // `L.src = s` would only write the ptr and + // `L.src.len` would carry whatever was on the + // stack. + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + fi.foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((lc.off + fi.foff + 8): i64); + emitline("(BP)\n"); + return; + }; + // 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(fi); emitline("\t"); emitline(sop); @@ -8342,6 +8983,21 @@ fn cgassign(c: *cgen, n: *node) void = { 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(fi); emitline("\tLEAQ\t"); emitsymname(c, bn); @@ -8436,6 +9092,30 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; + // f64/f32 chained plain `=`: cgexpr rhs left value in + // X0. Spill to stack so cgexpr(base) can use AX, then + // reload and MOVSD/MOVSS into the slot. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); @@ -8458,6 +9138,130 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // 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. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { if (base.kind == nkind.N_DOT) { + let inner: *node = base.lhs; + let innerfld: str = base.str; + if (inner != nil) { if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = tn.kind; + let outname: str; + outname.ptr = nil; outname.len = 0; + let isptr: bool = false; + if (lkind == nkind.N_TNAME) { outname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { if (pe.kind == nkind.N_TNAME) { + outname = pe.str; + isptr = true; + };}; + }; + if (outname.len > 0) { + let osi: *structinfo = structlookup(c, outname); + if (osi != nil) { + let ofi: *fieldinfo = osi.fields; + for (ofi != nil) { + if (streq(ofi.fname, innerfld)) { + let oft: *node = ofi.tnode; + if (oft != nil) { if (oft.kind == nkind.N_TNAME) { + if (primsize(oft.str) == 0) { + let isi: *structinfo = structlookup(c, oft.str); + if (isi != nil) { + let ffi: *fieldinfo = isi.fields; + for (ffi != nil) { + if (streq(ffi.fname, fld)) { + if (n.op == tkind.TK_ASSIGN) { + let totoff: i32 = ofi.foff + ffi.foff; + cgexpr(c, n.rhs); + if (isstrtype(c, ffi.tnode)) { + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), CX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(totoff: i64, "CX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((totoff + 8): i64, "CX"); + emitline("\n"); + } else { + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + totoff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((lc.off + totoff + 8): i64); + emitline("(BP)\n"); + }; + return; + }; + if (isfloattype(c, ffi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(totoff: i64, "BX"); + emitline("\n"); + } else { + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((lc.off + totoff): i64); + emitline("(BP)\n"); + }; + return; + }; + let sop: str = fieldstoreop(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. @@ -8792,6 +9596,25 @@ fn cgreturn(c: *cgen, n: *node) void = { // Nullable folded `(*T | void)`: just one word; AX is // already the pointer (or 0). No shuffle, no tag. if (istaggedtype(c.fnret)) { + // Forwarding a fallible call: `return f();` where f + // also returns a tagged union. The result is already + // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. + // Mirrors the rhsreturnstagged path in cglet and the + // !type_istagged guard in C cgen's N_RETURN. + let forwardtagged: bool = false; + if (rhs.kind == nkind.N_CALL) { + let callee: *node = rhs.lhs; + if (callee != nil) { + let calleename: str; + calleename.ptr = nil; calleename.len = 0; + if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; + if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + if (calleename.len > 0) { + let rt: *node = fnretlookup(c, calleename); + if (istaggedtype(rt)) { forwardtagged = true; }; + }; + }; + }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); @@ -8800,6 +9623,13 @@ fn cgreturn(c: *cgen, n: *node) void = { 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); if (nodeisstr(c, rhs)) { emitline("\tMOVQ\tBX, CX\n"); @@ -8862,7 +9692,12 @@ fn cgexprstmt(c: *cgen, n: *node) void = { fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); - let off: i32 = localadd(c, nm, sz, n.lhs); + // `let x = f()?` has no annotation but the cgen's struct-field + // paths need a tnode to dispatch off. Infer from f's tagged + // success variant — see inferletcalltype. + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // Tagged-union init: `let r: (T | E) = expr;`. @@ -8871,8 +9706,8 @@ fn cglet(c: *cgen, n: *node) void = { // just spill all three. // - Otherwise rhs is a bare variant value: pack tag + // value(s). - if (istaggedtype(n.lhs)) { - let nullable: bool = isnullabletype(n.lhs); + if (istaggedtype(tn)) { + let nullable: bool = isnullabletype(tn); let rhsreturnstagged: bool = false; if (rhs.kind == nkind.N_CALL) { let callee: *node = rhs.lhs; @@ -8915,7 +9750,7 @@ fn cglet(c: *cgen, n: *node) void = { c.lastwasreturn = 0; return; }; - let tagidx: i32 = taggedvariantindex(c, n.lhs, rhs); + let tagidx: i32 = taggedvariantindex(c, tn, rhs); if (tagidx < 0) { tagidx = 0; }; if (nodeisstr(c, rhs)) { emitline("\tMOVQ\tAX, "); @@ -9101,13 +9936,26 @@ fn cglet(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, fieldnode.lhs); - let sop: str = fieldstoreop(fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((off + fi.foff): i64); - emitline("(BP)\n"); - fi = nil; + // f64/f32 struct-literal field init: cgexpr left + // the value in X0, store via MOVSD/MOVSS. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((off + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + } else { + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitoff((off + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + }; } else { fi = fi.finext; }; @@ -9477,9 +10325,9 @@ fn cgforrange(c: *cgen, n: *node) void = { bind_signed[nbinds] = signf; let bnm: str = m.str; if (bnm.len > 0) { - bind_off[nbinds] = localadd(c, bnm, slot_sz, nil); + bind_off[nbinds] = localadd(c, bnm, slot_sz, tp); } else { - bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil); + bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tp); }; field_off += fsz; nbinds += 1; @@ -9499,9 +10347,12 @@ fn cgforrange(c: *cgen, n: *node) void = { bind_signed[0] = paramissigned(elemt); }; if (n.str.len > 0) { - bind_off[0] = localadd(c, n.str, slot_sz, nil); + // 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, nil); + bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt); }; nbinds = 1; }; @@ -9877,6 +10728,11 @@ fn cgfnparams(c: *cgen, params: *node) void = { let p: *node = params; let idx: i32 = 0; let fidx: i32 = 0; + // Cursor for args that overflow the SysV reg windows. Each + // stack-passed arg lives at 16+8*k(BP) — no spill, the local + // is registered with a *positive* offset pointing into the + // caller's frame. Mirrors C cgen's cg_stack_arg_cursor. + let stkcursor: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; @@ -9885,81 +10741,101 @@ fn cgfnparams(c: *cgen, params: *node) void = { // (X0..X7). 8B (f64) or 4B (f32) slot. let fsz: i32 = 8; if (isf32type(c, p.lhs)) { fsz = 4; }; - 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; + if (fidx < 8) { + let off: i32 = localadd(c, nm, fsz, p.lhs); + let mov: str = "MOVSD"; + if (fsz == 4) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + fidx += 1; + } else { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 1; + }; p = p.next; continue; }; if (istaggedtype(p.lhs)) { - // tagged-union param: spill size/8 registers - // (tag + value words). Slot sized to match. let slot: i32 = slotsize(c, p.lhs); - let off: i32 = localadd(c, nm, slot, p.lhs); let nw: i32 = slot / 8; - let w: i32 = 0; - for (w < nw) { + 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 { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += nw; + }; + } else { if (isslicetype(c, p.lhs)) { + if (idx + 3 <= 6) { + let off: i32 = localadd(c, nm, 24, p.lhs); emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); - emitoff((off + w*8): i64); + emitoff(off: i64); emitline("(BP)\n"); idx += 1; - w += 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 { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 3; }; - } else { if (isslicetype(c, p.lhs)) { - // slice param: 3 regs (ptr, len, cap), 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; } else { if (isstrtype(c, p.lhs)) { - // str param: passed in two regs (ptr, len). - // Slot is 16 bytes; ptr at off+0, len at off+8. - let off: i32 = localadd(c, nm, 16, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; + if (idx + 2 <= 6) { + let off: i32 = localadd(c, nm, 16, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + } else { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 2; + }; } else { - let off: i32 = localadd(c, nm, 8, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; + 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; @@ -10459,6 +11335,20 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { return off; }; +// localaddstack — register a param at a positive BP offset. Used for +// args that overflow the 6 SysV int / 8 float reg windows; the caller +// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), +// etc. (after the saved RIP+BP). No spill instruction is emitted; the +// slot IS the caller's stack slot. +fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = { + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = off; + l.tnode = tnode; + l.lnext = c.locals; + c.locals = l; +}; + fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // Name-based slot reuse for N_LETs and params: if `name` is // already declared in this function, return its existing @@ -10712,6 +11602,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = { for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; if (t.kind == nkind.N_TSLICE) { return 24; }; + if (t.kind == nkind.N_TARRAY) { + let lenn: *node = t.rhs; + let elemn: *node = t.lhs; + let alen: i32 = 1; + if (lenn != nil) { + if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; }; + }; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + return alen * esz; + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -10761,6 +11667,19 @@ fn isletvar(c: *cgen, name: str) bool = { // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare // MOVQ scalar load. +// letvartnode — direct lookup of a top-level let's tnode. Used by +// cgindex / cgassign to detect global `[N]T` arrays and `*T` +// pointers, where the addressing path needs LEAQ name(SB) (array) +// or MOVQ name(SB) (pointer) and the element size from T. +fn letvartnode(c: *cgen, name: str) *node = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { return lv.tnode; }; + lv = lv.lvnext; + }; + return nil; +}; + fn letvarisstr(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { @@ -11133,6 +12052,73 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + // Top-level `[N]T = [a, b, ...]` array global. + // Emits N*esz bytes with each element's bytes + // little-endian for the declared primitive width. + // Without this, `let arr: [N]T = ...` references + // from function bodies link-fail with `undefined + // reference to arr`, and bare-name addressing + // (LEAQ arr(SB)) inside cgindex / cgassign has no + // symbol to bind to. + if (d.lhs != nil) { + if (d.lhs.kind == nkind.N_TARRAY) { + let elemn: *node = d.lhs.lhs; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + let total: i32 = sz; + let alen: i32 = total / esz; + let elems: *node = nil; + if (d.rhs != nil) { + if (d.rhs.kind == nkind.N_ARRLIT) { + elems = d.rhs.list; + }; + }; + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + let e: *node = elems; + let fillv: u64 = 0u64; + let inrepeat: bool = false; + for (i < alen) { + let v: u64 = fillv; + if (!inrepeat && e != nil) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { + // `..., ...` repeat marker: prior v stays. + inrepeat = true; + } else { + if (e.lhs != nil) { + if (e.lhs.kind == nkind.N_INTLIT) { v = e.lhs.uval; }; + if (e.lhs.kind == nkind.N_RUNELIT) { v = e.lhs.uval; }; + }; + fillv = v; + e = e.next; + }; + } else { + if (e.kind == nkind.N_INTLIT) { v = e.uval; }; + if (e.kind == nkind.N_RUNELIT) { v = e.uval; }; + fillv = v; + e = e.next; + }; + }; + let nb: u64 = v; + let b: i32 = 0; + for (b < esz) { + emitdatawbyte((nb & 255u64): u8); + nb = nb >> 8u64; + b += 1; + }; + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 391bf1b1..ecb033c6 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -444,6 +444,87 @@ export fn stou64(s: str) (u64 | invalid | overflow) = { return v; }; +// f64tos — write `v` in decimal into `buf` and return the byte count. +// Hare name; this is the buffer-in Plan 9 subset of Hare's +// `f64tos(n) const str`. Today's surface: +// +// - finite values only. NaN/±Inf detection needs an f64→u64 bit +// reinterpret cast that the cgen doesn't expose yet. +// - fixed-point only, up to 6 fractional digits. Trailing zeros +// after the decimal point are trimmed. Trailing '.' is dropped. +// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) +// fall back to the literal token "huge". Hare would print these +// in scientific notation via Ryū; we will graduate when the +// compiler grows the bit-reinterpret cast. +// +// Round-trip is therefore lossy past 6 fractional digits; callers +// that need bit-exact recovery should not use this until the +// graduate-to-Ryū step lands. `f64tos(buf, 1.0)` writes "1" (no +// decimal point), `f64tos(buf, 1.5)` writes "1.5", `f64tos(buf, +// 0.1)` writes "0.1". +// +// No float literals in the body — 990's wwdump diff requires this +// file's TK_FLOAT count to match between C and ww front-ends, and +// the ww-side wwdump currently skips TK_FLOAT.fval while the C side +// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: +// build f64 constants via int-to-f64 casts. +export fn f64tos(buf: []u8, v: f64) i32 = { + let out: i32 = 0; + let f: f64 = v; + let zero: f64 = 0: f64; + if (f < zero) { + buf[out] = 45u8; // '-' + out += 1; + f = -f; + }; + // 9e18 is comfortably under I64_MAX (9.22e18). Past this the + // `f: i64` cast wraps and the integer part comes back as garbage. + let cap: f64 = 9000000000000000000i64: f64; + if (f >= cap) { + let s: str = "huge"; + let k: i32 = 0; + for (k < s.len) { buf[out] = s[k]; out += 1; k += 1; }; + return out; + }; + let ip: i64 = f: i64; + // Fractional part scaled to 6 decimal digits, with round-to- + // nearest via +0.5. (f64 compound assigns mis-lower in cgen — + // use the explicit form, as the rest of lib does.) + let frac: f64 = f - (ip: f64); + let scale: f64 = 1000000: f64; + frac = frac * scale; + let half: f64 = (1: f64) / (2: f64); + let fp: i64 = (frac + half): i64; + // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer + // part needs to advance. + if (fp >= 1000000) { + ip += 1; + fp = 0; + }; + let itmp: [32]u8; + let in: i32 = i64tos(itmp[0:32], ip); + let k: i32 = 0; + for (k < in) { buf[out] = itmp[k]; out += 1; k += 1; }; + if (fp == 0) { return out; }; + buf[out] = 46u8; // '.' + out += 1; + let ftmp: [16]u8; + let m: i32 = u64tos(ftmp[0:16], fp: u64); + // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → + // fp=50000, m=5, pad one '0' before "50000"). + let z: i32 = 6 - m; + for (z > 0) { buf[out] = 48u8; out += 1; z -= 1; }; + k = 0; + for (k < m) { buf[out] = ftmp[k]; out += 1; k += 1; }; + // Trim trailing zeros in the fractional part (we know fp != 0, + // so the loop stops before erasing the dot). + for (out > 0) { + if (buf[out - 1] != 48u8) { break; }; + out -= 1; + }; + return out; +}; + // MODULE: lex // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / // Tok / Pos shapes from cmd/wcc/ww.h. @@ -5179,13 +5260,11 @@ fn nodeisstr(c: *cgen, n: *node) bool = { let nm: str = n.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { - let tn: *node = lc.tnode; - if (tn != nil) { - if (tn.kind == nkind.N_TNAME) { - let tnm: str = tn.str; - if (streq(tnm, "str")) { return true; }; - }; - }; + // Use isstrtype so `!str` aliases (parserr = !str) and + // `type foo = str;` chains resolve through. The bare + // `streq("str", ...)` test missed them and dropped the + // MOVQ BX,CX shuffle on returns of str-aliased locals. + if (isstrtype(c, lc.tnode)) { return true; }; }; return false; }; @@ -5673,11 +5752,88 @@ fn primsize(name: str) i32 = { return 0; }; +// variantnamematch — tagged-union variant names are compared as if +// they'd been alias-resolved. Pattern names can be module-qualified +// (`strconv.invalid` from a `case let e: strconv.invalid =>`), +// while the variant's declared name inside its own module is bare +// (`invalid`). With no checker the cgen can't follow imports, so we +// accept exact match plus suffix-after-`.` on either side. Mirrors +// the C cgen's type_eq, which goes through resolved Type pointers. +fn variantnamematch(vname: str, pname: str) bool = { + if (streq(vname, pname)) { return true; }; + // `pname` is qualified, `vname` is bare: drop module prefix. + let i: i32 = 0; + for (i < pname.len) { + if (pname[i] == '.': u8) { + let tail: str; + tail.ptr = pname.ptr + i + 1; + tail.len = pname.len - i - 1; + if (streq(tail, vname)) { return true; }; + }; + i += 1; + }; + // `vname` is qualified, `pname` is bare: same trick in reverse. + let j: i32 = 0; + for (j < vname.len) { + if (vname[j] == '.': u8) { + let tail: str; + tail.ptr = vname.ptr + j + 1; + tail.len = vname.len - j - 1; + if (streq(tail, pname)) { return true; }; + }; + j += 1; + }; + return false; +}; + +// inferletcalltype — for an annotation-less `let x = expr;`, return +// a usable tnode for cgen's struct-aware paths. Today: `let x = +// f()?` infers x's type from the success variant of f's tagged +// return; without this, x has tnode = nil and `x.field` falls into +// the SB-symbol fallback (linker reports `undefined reference to +// `). We don't infer for plain `let x = f()` yet — +// non-tagged returns don't carry their type back the same way. +fn inferletcalltype(c: *cgen, rhs: *node) *node = { + if (rhs == nil) { return nil; }; + // `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged + // return to its success variant; the rhs we want the type of + // is the inner call expression. + let unwrap: bool = false; + let call: *node = rhs; + if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; }; + if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; }; + if (call == nil) { return nil; }; + if (call.kind != nkind.N_CALL) { return nil; }; + let callee: *node = call.lhs; + if (callee == nil) { return nil; }; + let cname: str; + cname.ptr = nil; cname.len = 0; + if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; + if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + if (cname.len == 0) { return nil; }; + let rt: *node = fnretlookup(c, cname); + if (rt == nil) { return nil; }; + if (unwrap) { + // Strip error variants — success type is the first + // variant of the tagged return. + if (rt.kind != nkind.N_TTAGGED) { return nil; }; + return rt.list; + }; + // Plain call: declared return type is the local's type. + return rt; +}; + // letslotsize — slot size for a `let` binding. Like slotsize, but // detects `[_]T = arrlit;` (the type-AST has rhs == nil as the // length-inferred sentinel) and computes count × element-size from // the initialiser. Used by both scanlocals (prologue sizing) and // cglet (slot alloc) so they agree on the frame layout. +// +// `let x = f();` (no annotation): infer from `f`'s declared return +// type so a 24B tagged-union return reserves all three spill slots, +// not the default 8B. Without this, the AX:DX:CX spill in cglet's +// tagged-init branch writes past the local and tramples the next +// slot. export fn letslotsize(c: *cgen, n: *node) i32 = { // `[_]T = arrlit;` — inferred-length array. slotsize would // return elem_size * 1 (treating missing length as 1); intercept @@ -5716,7 +5872,13 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; }; }; - return slotsize(c, n.lhs); + if (n.lhs != nil) { return slotsize(c, n.lhs); }; + // Annotation-less init: defer to the call's return type if we + // can infer it. Tagged-union returns need 24B; everything else + // matches slotsize on the inferred type. + let inferred: *node = inferletcalltype(c, n.rhs); + if (inferred != nil) { return slotsize(c, inferred); }; + return 8; }; fn slotsize(c: *cgen, typn: *node) i32 = { @@ -5768,6 +5930,19 @@ fn slotsize(c: *cgen, typn: *node) i32 = { // Named struct lookup. let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; + // Type alias (`type foo = !str;` / `type foo = bar;`): + // follow it so a tagged-union variant of a !str-aliased + // error type contributes 16 bytes to the max payload + // rather than 8 (the default). + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { + if (aliased.kind == nkind.N_TBANG) { + return slotsize(c, aliased.lhs); + }; + return slotsize(c, aliased); + }; + }; return 8; }; if (k == nkind.N_TARRAY) { @@ -5919,7 +6094,20 @@ fn isstrtype(c: *cgen, t: *node) bool = { if (isstrtyperaw(t)) { return true; }; if (c == nil) { return false; }; let r: *node = resolvetype(c, t); - return isstrtyperaw(r); + if (isstrtyperaw(r)) { return true; }; + // `parserr = !str` — `!T` aliases shouldn't hide their + // underlying type from str-routing. Unwrap and re-check. + if (r != nil) { + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (isstrtyperaw(inner)) { return true; }; + if (inner != nil) { + let r2: *node = resolvetype(c, inner); + if (isstrtyperaw(r2)) { return true; }; + }; + }; + }; + return false; }; fn isslicetyperaw(t: *node) bool = { @@ -6050,6 +6238,48 @@ export fn exprfloatkind(c: *cgen, n: *node) i32 = { }; return 0; }; + if (k == nkind.N_DOT) { + // `p.field` where the struct field is f64/f32. Without this, + // `v.fval: i64` lowers to CVTSI on an integer-load value + // instead of CVTTSD2SI on the X0 the cgdot path actually + // emits for an f64 field. + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + let sname: str; + sname.ptr = nil; sname.len = 0; + if (base.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, base.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { + if (pe.kind == nkind.N_TNAME) { sname = pe.str; }; + }; + }; + }; + }; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + if (isf32type(c, fi.tnode)) { return 1; }; + if (isfloattype(c, fi.tnode)) { return 2; }; + return 0; + }; + fi = fi.finext; + }; + }; + }; + }; + return 0; + }; return 0; }; @@ -6118,6 +6348,19 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { return nm; }; if (rhs.kind == nkind.N_STRLIT) { return "str"; }; + // `T{}` carries its type name on the lhs N_IDENT — the parser + // builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`. + // Needed so `return eof{};` (variant of a tagged union) resolves + // to the `eof` variant index rather than falling through to the + // "first non-str variant" fallback in taggedvariantindex. + if (rhs.kind == nkind.N_STRUCTLIT) { + let tref: *node = rhs.lhs; + if (tref != nil) { + if (tref.kind == nkind.N_IDENT) { return tref.str; }; + if (tref.kind == nkind.N_TNAME) { return tref.str; }; + }; + return nm; + }; if (rhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, rhs.str); if (lc != nil) { @@ -6143,7 +6386,7 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = { let idx: i32 = 0; for (v != nil) { if (v.kind == nkind.N_TNAME) { - if (streq(v.str, wantname)) { return idx; }; + if (variantnamematch(v.str, wantname)) { return idx; }; }; v = v.next; idx += 1; @@ -6268,6 +6511,12 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; + // Default fallback: produce a deterministic AX = 0. Mirrors + // the C cgen's `default: cgexpr_int(c, 0)` branch, which is + // what `return eof{};` (N_STRUCTLIT with an empty !void + // variant) silently relies on — without this AX carries a + // stale value into the tagged-union return shuffle. + emitline("\tMOVQ\t$0, AX\n"); }; // cgtagvariantidx — find the 0-based variant index of `vt` inside the @@ -6692,6 +6941,14 @@ fn cgindex(c: *cgen, n: *node) void = { let esz: i32 = 8; let signed_elem: bool = false; let baselocal: *local = nil; + // Global `[N]T` array or `*T` pointer used as an index base. + // The local-ident lookup above misses it; we need LEAQ name(SB) + // (array, the symbol IS the storage) or MOVQ name(SB) (pointer, + // the symbol holds the address) to feed the addend. + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; @@ -6699,6 +6956,22 @@ fn cgindex(c: *cgen, n: *node) void = { if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); signed_elem = elemissigned(baselocal.tnode); + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeof(tn); + signed_elem = elemissigned(tn); + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeof(tn); + signed_elem = elemissigned(tn); + }; + }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); @@ -6711,6 +6984,31 @@ fn cgindex(c: *cgen, n: *node) void = { emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; + if (isglobalarr || isglobalptr) { + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + }; + emitline("\tADDQ\tAX, BX\n"); + if (esz == 16) { + emitline("\tMOVQ\t8(BX), CX\n"); + emitline("\tMOVQ\t(BX), AX\n"); + emitline("\tMOVQ\tCX, BX\n"); + return; + }; + if (esz == 1) { emitline("\tMOVZBQ\t(BX), AX\n"); } + else { if (esz == 4) { + if (signed_elem) { emitline("\tMOVSXD\t(BX), AX\n"); } + else { emitline("\tMOVL\t(BX), AX\n"); }; + } + else { emitline("\tMOVQ\t(BX), AX\n"); };}; + return; + }; if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; @@ -6950,7 +7248,7 @@ fn cgmatch(c: *cgen, n: *node) void = { let found: bool = false; for (v != nil) { if (v.kind == nkind.N_TNAME) { - if (streq(v.str, patname)) { + if (variantnamematch(v.str, patname)) { want = idx; found = true; v = nil; @@ -7105,6 +7403,19 @@ fn cgdot(c: *cgen, n: *node) void = { emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\tCX, 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(fi); emitline("\t"); @@ -7112,7 +7423,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7138,6 +7449,15 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\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(fi); emitline("\t"); @@ -7145,7 +7465,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7332,6 +7652,15 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "CX"); emitline(", BX\n"); + } else { if (isfloattype(c, fi.tnode)) { + // f64/f32 global field: route through X0. + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "CX"); + emitline(", X0\n"); } else { let op: str = fieldloadop(fi); emitline("\t"); @@ -7339,7 +7668,7 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); - }; + }; }; return; }; fi = fi.finext; @@ -7347,6 +7676,71 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // `xs[i].field` — slice/array/ptr-of-struct element field access. + // Without this the cgen falls through to the module-qualified + // SB fallback below and emits `MOVQ (SB), AX` (linker + // reports `undefined reference to `). cgexpr(c, lhs) + // dispatches to cgindex which leaves the element value in AX + // — for a []*T element that's the *T pointer, so we just chain + // the field load through (AX). + if (lhs != nil) { + if (lhs.kind == nkind.N_INDEX) { + let idxbase: *node = lhs.lhs; + if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, idxbase.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let elemt: *node = nil; + let tk: nkind = tn.kind; + if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; + if (tk == nkind.N_TARRAY) { elemt = tn.lhs; }; + if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; + if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { + let inner: *node = elemt.lhs; + if (inner != nil) { if (inner.kind == nkind.N_TNAME) { + let sname: str = inner.str; + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + cgexpr(c, lhs); // AX = *Struct + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; + }; + fi = fi.finext; + }; + }; + };}; + };}; + };}; + };}; + }; + }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback @@ -7389,7 +7783,6 @@ fn cgdot(c: *cgen, n: *node) void = { for (fi != nil) { if (streq(fi.fname, fld)) { cgexpr(c, lhs); // AX = ptr to inner struct - let lop: str = fieldloadop(fi); // str field: load both halves. if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); @@ -7400,6 +7793,18 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", AX\n"); return; }; + // f64/f32 chained field: route through X0. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(fi); emitline("\t"); emitline(lop); emitline("\t"); @@ -7413,6 +7818,120 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; + // 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`. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let inner: *node = lhs.lhs; + let innerfld: str = lhs.str; + if (inner != nil) { if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = tn.kind; + let outname: str; + outname.ptr = nil; outname.len = 0; + let isptr: bool = false; + if (lkind == nkind.N_TNAME) { outname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { if (pe.kind == nkind.N_TNAME) { + outname = pe.str; + isptr = true; + };}; + }; + if (outname.len > 0) { + let osi: *structinfo = structlookup(c, outname); + if (osi != nil) { + let ofi: *fieldinfo = osi.fields; + for (ofi != nil) { + if (streq(ofi.fname, innerfld)) { + let oft: *node = ofi.tnode; + if (oft != nil) { if (oft.kind == nkind.N_TNAME) { + if (primsize(oft.str) == 0) { + let isi: *structinfo = structlookup(c, oft.str); + if (isi != nil) { + let ffi: *fieldinfo = isi.fields; + for (ffi != nil) { + if (streq(ffi.fname, fld)) { + let totoff: i32 = ofi.foff + ffi.foff; + if (isstrtype(c, ffi.tnode)) { + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), CX\n"); + emitline("\tMOVQ\t"); + emitdispreg((totoff + 8): i64, "CX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); + emitdispreg(totoff: i64, "CX"); + emitline(", AX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff((lc.off + totoff): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + totoff + 8): i64); + emitline("(BP), BX\n"); + }; + return; + }; + if (isfloattype(c, ffi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(totoff: i64, "BX"); + emitline(", X0\n"); + } else { + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((lc.off + totoff): i64); + emitline("(BP), X0\n"); + }; + return; + }; + let lop: str = fieldloadop(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; + }; + }; + }; + };}; + };}; + }; + }; return; }; @@ -7654,14 +8173,28 @@ fn cgalloc(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, f.lhs); - emitline("\tMOVQ\t(SP), BX\n"); - let sop: str = fieldstoreop(fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitint(fi.foff: i64); - emitline("(BX)\n"); - fi = nil; + // alloc(T{ fval = v }) for f64/f32 field: cgexpr left + // the value in X0, not AX — route the store via MOVSD/MOVSS. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\tMOVQ\t(SP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + } else { + emitline("\tMOVQ\t(SP), BX\n"); + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitint(fi.foff: i64); + emitline("(BX)\n"); + fi = nil; + }; } else { fi = fi.finext; }; @@ -7844,43 +8377,48 @@ fn cgcall(c: *cgen, n: *node) void = { // 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. + // register class. SysV has only 6 int arg regs (DI/SI/DX/CX/R8/R9); + // the remaining slots stay on the stack and the callee reads them + // via 16+8*k(BP). Caller-cleanup is emitted after the CALL. let intidx: i32 = 0; let fpidx: i32 = 0; let a: *node = n.list; let popped: i32 = 0; + let stackslots: i32 = 0; for (a != nil) { let fk: i32 = exprfloatkind(c, a); if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t(SP), "); - emitline(fargregname(fpidx)); - emitline("\n"); - emitline("\tADDQ\t$8, SP\n"); - fpidx += 1; + 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 { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; - popped += 1; - // Multi-word args (str=2, slice/tagged=3): drain - // the remaining words into successive int regs. let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; - let e: i32 = 0; - for (e < extra) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 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; - e += 1; + w += 1; }; }; a = a.next; @@ -7891,10 +8429,14 @@ fn cgcall(c: *cgen, n: *node) void = { // case here is identical pre-port behaviour. let i: i32 = popped; for (i < nargs) { - emitline("\tPOPQ\t"); - emitline(argregname(intidx)); - emitline("\n"); - intidx += 1; + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; i += 1; }; let callee: *node = n.lhs; @@ -7981,6 +8523,14 @@ fn cgcall(c: *cgen, n: *node) void = { }; emitline("(SB)\n"); }; + // Caller cleanup for stack-passed args (args 7+, or any + // overflow past the int/float reg windows). Mirrors C cgen: + // pushed 8 bytes each, ADDQ them off after the CALL. + if (stackslots > 0) { + emitline("\tADDQ\t$"); + emitint((stackslots * 8): i64); + emitline(", SP\n"); + }; // SysV returns 16-byte aggregates in (AX, DX). Our str // convention is (AX, BX), so shuffle for str-returning calls. if (calleename.len > 0) { @@ -8018,6 +8568,8 @@ fn cgassign(c: *cgen, n: *node) void = { if (n.op == tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let elemstr: bool = false; + let elemfloat: bool = false; + let elemf32: bool = false; let storeop: str = "MOVQ"; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { @@ -8030,11 +8582,13 @@ fn cgassign(c: *cgen, n: *node) void = { if (pe != nil) { if (pe.kind == nkind.N_TNAME) { if (streq(pe.str, "str")) { elemstr = true; } + else { if (streq(pe.str, "f64")) { elemfloat = true; } + else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; - }; + }; }; }; }; }; }; @@ -8043,6 +8597,27 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; cgexpr(c, n.rhs); + // `*p = v` for *f64 / *f32: value sits in X0. Spill + // to the stack, evaluate the pointer (clobbers AX), + // then reload X0 and MOVSD/MOVSS through the pointer. + if (elemfloat) { + let mov: str = "MOVSD"; + if (elemf32) { mov = "MOVSS"; }; + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + cgexpr(c, inner); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (BX)\n"); + return; + }; // Push order matches C cgen // (cmd/w6c/cgen.c:1033-1041): PUSHQ AX // (ptr) first, then PUSHQ BX (len) if @@ -8078,12 +8653,30 @@ fn cgassign(c: *cgen, n: *node) void = { let idx: *node = lhs.rhs; let esz: i32 = 8; let baselocal: *local = nil; + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeof(baselocal.tnode); + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeof(tn); + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeof(tn); + }; + }; }; } else { if (base.kind == nkind.N_DOT) { esz = indexbaseesz(c, base); @@ -8100,7 +8693,15 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); // scaled idx - if (baselocal != nil) { + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; @@ -8116,7 +8717,7 @@ fn cgassign(c: *cgen, n: *node) void = { } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); - }; + };};}; emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); // value @@ -8205,6 +8806,21 @@ fn cgassign(c: *cgen, n: *node) void = { 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); @@ -8232,6 +8848,31 @@ fn cgassign(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fld)) { cgexpr(c, n.rhs); + // str field: cgexpr left (AX=ptr, BX=len); + // store both halves at +0/+8. Without this, + // `L.src = s` would only write the ptr and + // `L.src.len` would carry whatever was on the + // stack. + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + fi.foff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((lc.off + fi.foff + 8): i64); + emitline("(BP)\n"); + return; + }; + // 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(fi); emitline("\t"); emitline(sop); @@ -8342,6 +8983,21 @@ fn cgassign(c: *cgen, n: *node) void = { 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(fi); emitline("\tLEAQ\t"); emitsymname(c, bn); @@ -8436,6 +9092,30 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; + // f64/f32 chained plain `=`: cgexpr rhs left value in + // X0. Spill to stack so cgexpr(base) can use AX, then + // reload and MOVSD/MOVSS into the slot. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(fi.foff: i64, "BX"); + emitline("\n"); + return; + }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); @@ -8458,6 +9138,130 @@ fn cgassign(c: *cgen, n: *node) void = { }; }; }; + // 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. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { if (base.kind == nkind.N_DOT) { + let inner: *node = base.lhs; + let innerfld: str = base.str; + if (inner != nil) { if (inner.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, inner.str); + if (lc != nil) { if (lc.tnode != nil) { + let tn: *node = lc.tnode; + let lkind: nkind = tn.kind; + let outname: str; + outname.ptr = nil; outname.len = 0; + let isptr: bool = false; + if (lkind == nkind.N_TNAME) { outname = tn.str; }; + if (lkind == nkind.N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { if (pe.kind == nkind.N_TNAME) { + outname = pe.str; + isptr = true; + };}; + }; + if (outname.len > 0) { + let osi: *structinfo = structlookup(c, outname); + if (osi != nil) { + let ofi: *fieldinfo = osi.fields; + for (ofi != nil) { + if (streq(ofi.fname, innerfld)) { + let oft: *node = ofi.tnode; + if (oft != nil) { if (oft.kind == nkind.N_TNAME) { + if (primsize(oft.str) == 0) { + let isi: *structinfo = structlookup(c, oft.str); + if (isi != nil) { + let ffi: *fieldinfo = isi.fields; + for (ffi != nil) { + if (streq(ffi.fname, fld)) { + if (n.op == tkind.TK_ASSIGN) { + let totoff: i32 = ofi.foff + ffi.foff; + cgexpr(c, n.rhs); + if (isstrtype(c, ffi.tnode)) { + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), CX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(totoff: i64, "CX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((totoff + 8): i64, "CX"); + emitline("\n"); + } else { + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + totoff): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((lc.off + totoff + 8): i64); + emitline("(BP)\n"); + }; + return; + }; + if (isfloattype(c, ffi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; + if (isptr) { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(totoff: i64, "BX"); + emitline("\n"); + } else { + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((lc.off + totoff): i64); + emitline("(BP)\n"); + }; + return; + }; + let sop: str = fieldstoreop(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. @@ -8792,6 +9596,25 @@ fn cgreturn(c: *cgen, n: *node) void = { // Nullable folded `(*T | void)`: just one word; AX is // already the pointer (or 0). No shuffle, no tag. if (istaggedtype(c.fnret)) { + // Forwarding a fallible call: `return f();` where f + // also returns a tagged union. The result is already + // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. + // Mirrors the rhsreturnstagged path in cglet and the + // !type_istagged guard in C cgen's N_RETURN. + let forwardtagged: bool = false; + if (rhs.kind == nkind.N_CALL) { + let callee: *node = rhs.lhs; + if (callee != nil) { + let calleename: str; + calleename.ptr = nil; calleename.len = 0; + if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; + if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + if (calleename.len > 0) { + let rt: *node = fnretlookup(c, calleename); + if (istaggedtype(rt)) { forwardtagged = true; }; + }; + }; + }; cgexpr(c, rhs); if (isnullabletype(c.fnret)) { emitline("\tMOVQ\tBP, SP\n"); @@ -8800,6 +9623,13 @@ fn cgreturn(c: *cgen, n: *node) void = { 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); if (nodeisstr(c, rhs)) { emitline("\tMOVQ\tBX, CX\n"); @@ -8862,7 +9692,12 @@ fn cgexprstmt(c: *cgen, n: *node) void = { fn cglet(c: *cgen, n: *node) void = { let nm: str = n.str; let sz: i32 = letslotsize(c, n); - let off: i32 = localadd(c, nm, sz, n.lhs); + // `let x = f()?` has no annotation but the cgen's struct-field + // paths need a tnode to dispatch off. Infer from f's tagged + // success variant — see inferletcalltype. + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + let off: i32 = localadd(c, nm, sz, tn); if (n.rhs != nil) { let rhs: *node = n.rhs; // Tagged-union init: `let r: (T | E) = expr;`. @@ -8871,8 +9706,8 @@ fn cglet(c: *cgen, n: *node) void = { // just spill all three. // - Otherwise rhs is a bare variant value: pack tag + // value(s). - if (istaggedtype(n.lhs)) { - let nullable: bool = isnullabletype(n.lhs); + if (istaggedtype(tn)) { + let nullable: bool = isnullabletype(tn); let rhsreturnstagged: bool = false; if (rhs.kind == nkind.N_CALL) { let callee: *node = rhs.lhs; @@ -8915,7 +9750,7 @@ fn cglet(c: *cgen, n: *node) void = { c.lastwasreturn = 0; return; }; - let tagidx: i32 = taggedvariantindex(c, n.lhs, rhs); + let tagidx: i32 = taggedvariantindex(c, tn, rhs); if (tagidx < 0) { tagidx = 0; }; if (nodeisstr(c, rhs)) { emitline("\tMOVQ\tAX, "); @@ -9101,13 +9936,26 @@ fn cglet(c: *cgen, n: *node) void = { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, fieldnode.lhs); - let sop: str = fieldstoreop(fi); - emitline("\t"); - emitline(sop); - emitline("\tAX, "); - emitoff((off + fi.foff): i64); - emitline("(BP)\n"); - fi = nil; + // f64/f32 struct-literal field init: cgexpr left + // the value in X0, store via MOVSD/MOVSS. + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((off + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + } else { + let sop: str = fieldstoreop(fi); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitoff((off + fi.foff): i64); + emitline("(BP)\n"); + fi = nil; + }; } else { fi = fi.finext; }; @@ -9477,9 +10325,9 @@ fn cgforrange(c: *cgen, n: *node) void = { bind_signed[nbinds] = signf; let bnm: str = m.str; if (bnm.len > 0) { - bind_off[nbinds] = localadd(c, bnm, slot_sz, nil); + bind_off[nbinds] = localadd(c, bnm, slot_sz, tp); } else { - bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, nil); + bind_off[nbinds] = localalloc(c, mkscratchname(c, "fr"), slot_sz, tp); }; field_off += fsz; nbinds += 1; @@ -9499,9 +10347,12 @@ fn cgforrange(c: *cgen, n: *node) void = { bind_signed[0] = paramissigned(elemt); }; if (n.str.len > 0) { - bind_off[0] = localadd(c, n.str, slot_sz, nil); + // 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, nil); + bind_off[0] = localalloc(c, mkscratchname(c, "fr"), slot_sz, elemt); }; nbinds = 1; }; @@ -9877,6 +10728,11 @@ fn cgfnparams(c: *cgen, params: *node) void = { let p: *node = params; let idx: i32 = 0; let fidx: i32 = 0; + // Cursor for args that overflow the SysV reg windows. Each + // stack-passed arg lives at 16+8*k(BP) — no spill, the local + // is registered with a *positive* offset pointing into the + // caller's frame. Mirrors C cgen's cg_stack_arg_cursor. + let stkcursor: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; @@ -9885,81 +10741,101 @@ fn cgfnparams(c: *cgen, params: *node) void = { // (X0..X7). 8B (f64) or 4B (f32) slot. let fsz: i32 = 8; if (isf32type(c, p.lhs)) { fsz = 4; }; - 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; + if (fidx < 8) { + let off: i32 = localadd(c, nm, fsz, p.lhs); + let mov: str = "MOVSD"; + if (fsz == 4) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + fidx += 1; + } else { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 1; + }; p = p.next; continue; }; if (istaggedtype(p.lhs)) { - // tagged-union param: spill size/8 registers - // (tag + value words). Slot sized to match. let slot: i32 = slotsize(c, p.lhs); - let off: i32 = localadd(c, nm, slot, p.lhs); let nw: i32 = slot / 8; - let w: i32 = 0; - for (w < nw) { + 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 { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += nw; + }; + } else { if (isslicetype(c, p.lhs)) { + if (idx + 3 <= 6) { + let off: i32 = localadd(c, nm, 24, p.lhs); emitline("\tMOVQ\t"); emitline(argregname(idx)); emitline(", "); - emitoff((off + w*8): i64); + emitoff(off: i64); emitline("(BP)\n"); idx += 1; - w += 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 { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 3; }; - } else { if (isslicetype(c, p.lhs)) { - // slice param: 3 regs (ptr, len, cap), 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; } else { if (isstrtype(c, p.lhs)) { - // str param: passed in two regs (ptr, len). - // Slot is 16 bytes; ptr at off+0, len at off+8. - let off: i32 = localadd(c, nm, 16, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; + if (idx + 2 <= 6) { + let off: i32 = localadd(c, nm, 16, p.lhs); + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + idx += 1; + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + idx += 1; + } else { + localaddstack(c, nm, p.lhs, 16 + stkcursor*8); + stkcursor += 2; + }; } else { - let off: i32 = localadd(c, nm, 8, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; + 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; @@ -10459,6 +11335,20 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { return off; }; +// localaddstack — register a param at a positive BP offset. Used for +// args that overflow the 6 SysV int / 8 float reg windows; the caller +// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), +// etc. (after the saved RIP+BP). No spill instruction is emitted; the +// slot IS the caller's stack slot. +fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = { + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = off; + l.tnode = tnode; + l.lnext = c.locals; + c.locals = l; +}; + fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // Name-based slot reuse for N_LETs and params: if `name` is // already declared in this function, return its existing @@ -10712,6 +11602,22 @@ fn letemitsize(c: *cgen, d: *node) i32 = { for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; if (t.kind == nkind.N_TSLICE) { return 24; }; + if (t.kind == nkind.N_TARRAY) { + let lenn: *node = t.rhs; + let elemn: *node = t.lhs; + let alen: i32 = 1; + if (lenn != nil) { + if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; }; + }; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + return alen * esz; + }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; @@ -10761,6 +11667,19 @@ fn isletvar(c: *cgen, name: str) bool = { // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare // MOVQ scalar load. +// letvartnode — direct lookup of a top-level let's tnode. Used by +// cgindex / cgassign to detect global `[N]T` arrays and `*T` +// pointers, where the addressing path needs LEAQ name(SB) (array) +// or MOVQ name(SB) (pointer) and the element size from T. +fn letvartnode(c: *cgen, name: str) *node = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { return lv.tnode; }; + lv = lv.lvnext; + }; + return nil; +}; + fn letvarisstr(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { @@ -11133,6 +12052,73 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; + // Top-level `[N]T = [a, b, ...]` array global. + // Emits N*esz bytes with each element's bytes + // little-endian for the declared primitive width. + // Without this, `let arr: [N]T = ...` references + // from function bodies link-fail with `undefined + // reference to arr`, and bare-name addressing + // (LEAQ arr(SB)) inside cgindex / cgassign has no + // symbol to bind to. + if (d.lhs != nil) { + if (d.lhs.kind == nkind.N_TARRAY) { + let elemn: *node = d.lhs.lhs; + let esz: i32 = 8; + if (elemn != nil) { + if (elemn.kind == nkind.N_TNAME) { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; + }; + let total: i32 = sz; + let alen: i32 = total / esz; + let elems: *node = nil; + if (d.rhs != nil) { + if (d.rhs.kind == nkind.N_ARRLIT) { + elems = d.rhs.list; + }; + }; + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + let e: *node = elems; + let fillv: u64 = 0u64; + let inrepeat: bool = false; + for (i < alen) { + let v: u64 = fillv; + if (!inrepeat && e != nil) { + if (e.kind == nkind.N_FIELD) { + if (streq(e.str, "...")) { + // `..., ...` repeat marker: prior v stays. + inrepeat = true; + } else { + if (e.lhs != nil) { + if (e.lhs.kind == nkind.N_INTLIT) { v = e.lhs.uval; }; + if (e.lhs.kind == nkind.N_RUNELIT) { v = e.lhs.uval; }; + }; + fillv = v; + e = e.next; + }; + } else { + if (e.kind == nkind.N_INTLIT) { v = e.uval; }; + if (e.kind == nkind.N_RUNELIT) { v = e.uval; }; + fillv = v; + e = e.next; + }; + }; + let nb: u64 = v; + let b: i32 = 0; + for (b < esz) { + emitdatawbyte((nb & 255u64): u8); + nb = nb >> 8u64; + b += 1; + }; + i += 1; + }; + emitline("\"\n"); + }; + }; }; }; d = d.next; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index c39256a5..f9df694c 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -336,6 +336,87 @@ export fn stou64(s: str) (u64 | invalid | overflow) = { return v; }; +// f64tos — write `v` in decimal into `buf` and return the byte count. +// Hare name; this is the buffer-in Plan 9 subset of Hare's +// `f64tos(n) const str`. Today's surface: +// +// - finite values only. NaN/±Inf detection needs an f64→u64 bit +// reinterpret cast that the cgen doesn't expose yet. +// - fixed-point only, up to 6 fractional digits. Trailing zeros +// after the decimal point are trimmed. Trailing '.' is dropped. +// - magnitudes ≥ 9e18 (overflows i64 in the integer-part cast) +// fall back to the literal token "huge". Hare would print these +// in scientific notation via Ryū; we will graduate when the +// compiler grows the bit-reinterpret cast. +// +// Round-trip is therefore lossy past 6 fractional digits; callers +// that need bit-exact recovery should not use this until the +// graduate-to-Ryū step lands. `f64tos(buf, 1.0)` writes "1" (no +// decimal point), `f64tos(buf, 1.5)` writes "1.5", `f64tos(buf, +// 0.1)` writes "0.1". +// +// No float literals in the body — 990's wwdump diff requires this +// file's TK_FLOAT count to match between C and ww front-ends, and +// the ww-side wwdump currently skips TK_FLOAT.fval while the C side +// %g-formats it. Same trick lib/ww/lex/lex.ww's parsef64 uses: +// build f64 constants via int-to-f64 casts. +export fn f64tos(buf: []u8, v: f64) i32 = { + let out: i32 = 0; + let f: f64 = v; + let zero: f64 = 0: f64; + if (f < zero) { + buf[out] = 45u8; // '-' + out += 1; + f = -f; + }; + // 9e18 is comfortably under I64_MAX (9.22e18). Past this the + // `f: i64` cast wraps and the integer part comes back as garbage. + let cap: f64 = 9000000000000000000i64: f64; + if (f >= cap) { + let s: str = "huge"; + let k: i32 = 0; + for (k < s.len) { buf[out] = s[k]; out += 1; k += 1; }; + return out; + }; + let ip: i64 = f: i64; + // Fractional part scaled to 6 decimal digits, with round-to- + // nearest via +0.5. (f64 compound assigns mis-lower in cgen — + // use the explicit form, as the rest of lib does.) + let frac: f64 = f - (ip: f64); + let scale: f64 = 1000000: f64; + frac = frac * scale; + let half: f64 = (1: f64) / (2: f64); + let fp: i64 = (frac + half): i64; + // Carry: e.g. 0.9999996 rounds fp up to 1000000 and the integer + // part needs to advance. + if (fp >= 1000000) { + ip += 1; + fp = 0; + }; + let itmp: [32]u8; + let in: i32 = i64tos(itmp[0:32], ip); + let k: i32 = 0; + for (k < in) { buf[out] = itmp[k]; out += 1; k += 1; }; + if (fp == 0) { return out; }; + buf[out] = 46u8; // '.' + out += 1; + let ftmp: [16]u8; + let m: i32 = u64tos(ftmp[0:16], fp: u64); + // Pad fractional to 6 digits with leading zeros (e.g. 0.05 → + // fp=50000, m=5, pad one '0' before "50000"). + let z: i32 = 6 - m; + for (z > 0) { buf[out] = 48u8; out += 1; z -= 1; }; + k = 0; + for (k < m) { buf[out] = ftmp[k]; out += 1; k += 1; }; + // Trim trailing zeros in the fractional part (we know fp != 0, + // so the loop stops before erasing the dot). + for (out > 0) { + if (buf[out - 1] != 48u8) { break; }; + out -= 1; + }; + return out; +}; + // MODULE: ascii // ascii — rune-class predicates and case folding for the ASCII range. // Matches Hare's ascii::isdigit family (rune-taking signature). Runes