From 6e7c9e0df4eef5cb2ffe68ce7504786f0aab6957 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 04:23:31 +0900 Subject: [PATCH] selfhost: alias-aware istaggedtype for nested-union match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type error = !(invalid | overflow)` miscompiled — istaggedtype only matched N_TTAGGED directly, so an `e: error` param spilled as 8B scalar and the match's slot+8 read trailed into saved BP. Mirror isstrtype's alias+bang unwrap; add resolvetagged() for is/as/match sites that need the inner N_TTAGGED. Frame scan counts via slotsize so wwstage stays byte-identical to cstage. Unblocks lib/strconv.strerror. --- lib/strconv/strconv.ww | 16 +- selfhost/cmd/w6c/main.combined.ww | 306 +++++++++++++++++++++++++-- selfhost/cmd/wcc/cgendecl.ww | 4 +- selfhost/cmd/wcc/cgenexpr.ww | 8 +- selfhost/cmd/wcc/cgenstmt.ww | 10 +- selfhost/cmd/wcc/cgenutil.ww | 48 ++++- selfhost/cmd/wwdump/main.combined.ww | 306 +++++++++++++++++++++++++-- selfhost/test/smoke.combined.ww | 236 ++++++++++++++++++++- test/wcc/700_e2e.c | 18 ++ 9 files changed, 893 insertions(+), 59 deletions(-) diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index 974de163..abdeaffb 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -8,6 +8,7 @@ // today). Graduate to the static-buffer shape once that lands. use os; +use strings; // invalid — input wasn't a valid number in the requested format. // Payload is the byte index of the first offending position. @@ -352,8 +353,13 @@ export fn f64tos(v: f64) str = { return r; }; -// strerror — Hare has strconv::strerror; ww doesn't ship it yet -// because a `match (e) { case invalid => ... }` arm over the wider -// `error = !(invalid | overflow)` union exposes a cstage-vs-wwstage -// cgen divergence (one cgen spills the unused payload slot, the -// other elides it). Restore once the cgens converge. +// strerror — convert an strconv error to a user-readable string. +// Returns owned str; release via os.free. Mirrors Hare's +// strconv::strerror. +export fn strerror(e: error) str = { + match (e) { + case let v: invalid => return strings.dup("input is not a valid number"); + case let v: overflow => return strings.dup("input number doesn't fit target type"); + }; + return strings.dup(""); +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6a15ab94..c4af8a90 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -335,6 +335,226 @@ export fn freearena(a: *arena) void = { }; }; +// MODULE: strings +// strings — operations over the immutable str type ({ *u8, len }). +// Mirrors Hare's strings::; `len` and `is-empty` aren't functions +// (callers use `s.len` and `s.len == 0` directly). + +use os; + +// compare — bytewise three-way comparison: negative if ab. Matches Hare's strings::compare. ASCII-order, not +// locale-aware. Callers that just need equality use `compare(a, b) == 0`. +export fn compare(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return (a[i]: i32) - (b[i]: i32); }; + i += 1; + }; + return a.len - b.len; +}; + +export fn hasprefix(s: str, p: str) bool = { + if (p.len > s.len) { return false; }; + let i: i32 = 0; + for (i < p.len) { + if (s[i] != p[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn hassuffix(s: str, suf: str) bool = { + if (suf.len > s.len) { return false; }; + let off: i32 = s.len - suf.len; + let i: i32 = 0; + for (i < suf.len) { + if (s[off + i] != suf[i]) { return false; }; + i += 1; + }; + return true; +}; + +// indexbyte — first byte position of byte `c` in `s`. Mirrors +// Hare's strings::byteindex when the needle is a single ASCII rune, +// renamed to match bytes.indexbyte and to disambiguate from Hare's +// `byteindex(haystack, needle: (str | rune))` which we don't have +// the union-arg ABI for yet. +export fn indexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == c) { return i; }; + i += 1; + }; + return; +}; + +// rindexbyte — last byte position of byte `c` in `s`. +export fn rindexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = s.len - 1; + for (i >= 0) { + if (s[i] == c) { return i; }; + i -= 1; + }; + return; +}; + +// index — first index of `sub` in `s`. Naive scan; fine for short +// patterns and small strings, which dominate config and CLI parsing. +// Empty `sub` matches at 0. +export fn index(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return 0; }; + if (sub.len > s.len) { return; }; + let last: i32 = s.len - sub.len; + let i: i32 = 0; + for (i <= last) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i += 1; + }; + return; +}; + +export fn contains(s: str, sub: str) bool = { + let r: (i32 | void) = index(s, sub); + match (r) { + case let i: i32 => return true; + case void => return false; + }; + return false; +}; + +// concat — joins two strings into a fresh str. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::concat shape. +export fn concat(a: str, b: str) str = { + let total: i32 = a.len + b.len; + let buf: *u8 = os.alloc(total: u64): *u8; + let i: i32 = 0; + for (i < a.len) { buf[i] = a[i]; i += 1; }; + let j: i32 = 0; + for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; + let r: str; + r.ptr = buf; + r.len = total; + return r; +}; + +// dup — duplicate a string into a fresh allocation. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::dup shape — Hare returns `(str | nomem)`, ww doesn't +// have nomem (os.alloc aborts on OOM), so we return plain `str`. +// +// Empty input yields a `{nil, 0}` str — Hare returns the static empty +// string; same observable result. +export fn dup(s: str) str = { + let r: str; + r.ptr = nil; + r.len = 0; + if (s.len == 0) { return r; }; + let buf: *u8 = os.alloc(s.len: u64): *u8; + let i: i32 = 0; + for (i < s.len) { buf[i] = s[i]; i += 1; }; + r.ptr = buf; + r.len = s.len; + return r; +}; + +// rindex — last index of `sub` in `s`. Mirrors Hare's strings::rindex +// (slice case). Empty `sub` matches at s.len. +export fn rindex(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return s.len; }; + if (sub.len > s.len) { return; }; + let i: i32 = s.len - sub.len; + for (i >= 0) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i -= 1; + }; + return; +}; + +// sub — borrowed substring `s[start..end]`. Mirrors Hare's +// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of- +// range indices are clamped silently here, where Hare aborts. +export fn sub(s: str, start: i32, end: i32) str = { + let lo: i32 = start; + let hi: i32 = end; + if (lo < 0) { lo = 0; }; + if (hi > s.len) { hi = s.len; }; + if (hi < lo) { hi = lo; }; + let r: str; + r.ptr = s.ptr + (lo: u64); + r.len = hi - lo; + return r; +}; + +// trimprefix — `s` with `pre` stripped from the front, or `s` +// unchanged if it doesn't start with `pre`. Returns a borrowed view. +// Mirrors Hare's strings::trimprefix. +export fn trimprefix(s: str, pre: str) str = { + if (!hasprefix(s, pre)) { return s; }; + let r: str; + r.ptr = s.ptr + (pre.len: u64); + r.len = s.len - pre.len; + return r; +}; + +// trimsuffix — `s` with `suf` stripped from the end, or `s` unchanged +// if it doesn't end with `suf`. Returns a borrowed view. Mirrors +// Hare's strings::trimsuffix. +export fn trimsuffix(s: str, suf: str) str = { + if (!hassuffix(s, suf)) { return s; }; + let r: str; + r.ptr = s.ptr; + r.len = s.len - suf.len; + return r; +}; + +// ltrimbyte / rtrimbyte / trimbyte — strip occurrences of a single +// byte from the left, right, or both ends. Returns a borrowed view. +// Hare's strings::ltrim / rtrim / trim take a rune varargs set; ww's +// subset takes a single byte (the common ASCII case). +export fn ltrimbyte(s: str, c: u8) str = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] != c) { break; }; + i += 1; + }; + let r: str; + r.ptr = s.ptr + (i: u64); + r.len = s.len - i; + return r; +}; + +export fn rtrimbyte(s: str, c: u8) str = { + let n: i32 = s.len; + for (n > 0) { + if (s[n - 1] != c) { break; }; + n -= 1; + }; + let r: str; + r.ptr = s.ptr; + r.len = n; + return r; +}; + +export fn trimbyte(s: str, c: u8) str = { + return rtrimbyte(ltrimbyte(s, c), c); +}; + // MODULE: strconv // strconv — number↔string conversions. // @@ -346,6 +566,7 @@ export fn freearena(a: *arena) void = { // today). Graduate to the static-buffer shape once that lands. use os; +use strings; // invalid — input wasn't a valid number in the requested format. // Payload is the byte index of the first offending position. @@ -690,11 +911,16 @@ export fn f64tos(v: f64) str = { return r; }; -// strerror — Hare has strconv::strerror; ww doesn't ship it yet -// because a `match (e) { case invalid => ... }` arm over the wider -// `error = !(invalid | overflow)` union exposes a cstage-vs-wwstage -// cgen divergence (one cgen spills the unused payload slot, the -// other elides it). Restore once the cgens converge. +// strerror — convert an strconv error to a user-readable string. +// Returns owned str; release via os.free. Mirrors Hare's +// strconv::strerror. +export fn strerror(e: error) str = { + match (e) { + case let v: invalid => return strings.dup("input is not a valid number"); + case let v: overflow => return strings.dup("input number doesn't fit target type"); + }; + return strings.dup(""); +}; // MODULE: lex // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / @@ -5426,7 +5652,7 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + if (isslicetype(c, lc.tnode) || istaggedtype(c, lc.tnode)) { emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), AX\n"); @@ -6361,12 +6587,56 @@ fn isslicetype(c: *cgen, t: *node) bool = { return isslicetyperaw(r); }; -fn istaggedtype(t: *node) bool = { +fn istaggedtyperaw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TTAGGED) { return true; }; return false; }; +// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil +// if `t` doesn't ultimately denote a tagged union. Follows N_TNAME +// aliases (via resolvetype) and unwraps one leading N_TBANG so +// `type error = !(invalid | overflow);` resolves to its inner +// `(invalid | overflow)` node. Use at sites that read variant lists +// or detect nullable folding off a scrutinee — cgmatch, cgtypetest, +// cgtypeassert — so aliased `!(A|B)` shapes still dispatch. +export fn resolvetagged(c: *cgen, t: *node) *node = { + let r: *node = resolvetype(c, t); + if (r == nil) { return nil; }; + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (inner == nil) { return nil; }; + r = resolvetype(c, inner); + if (r == nil) { return nil; }; + }; + if (r.kind == nkind.N_TTAGGED) { return r; }; + return nil; +}; + +// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its +// underlying decl, then unwrap a leading N_TBANG so `type error = +// !(invalid | overflow);` is still recognised as tagged. Without the +// bang unwrap the prologue treats the param as scalar (8B), spilling +// only DI and losing the value-word SI; the match read of slot+8 then +// trails into saved BP. +fn istaggedtype(c: *cgen, t: *node) bool = { + if (istaggedtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (istaggedtyperaw(r)) { return true; }; + if (r != nil) { + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (istaggedtyperaw(inner)) { return true; }; + if (inner != nil) { + let r2: *node = resolvetype(c, inner); + if (istaggedtyperaw(r2)) { return true; }; + }; + }; + }; + return false; +}; + // isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. fn isf32typeraw(t: *node) bool = { if (t == nil) { return false; }; @@ -6891,7 +7161,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -6979,7 +7249,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -7393,7 +7663,7 @@ fn cgmatch(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, scrut.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; } else { // Non-ident scrutinee (call result, ?, etc.). Spill into a @@ -7415,7 +7685,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); - if (rt != nil) { scrutt = resolvetype(c, rt); }; + if (rt != nil) { scrutt = resolvetagged(c, rt); }; }; }; }; @@ -9833,7 +10103,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // For other variants, cgexpr leaves AX, shuffle DX←AX. // Nullable folded `(*T | void)`: just one word; AX is // already the pointer (or 0). No shuffle, no tag. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { // Forwarding a fallible call: `return f();` where f // also returns a tagged union. The result is already // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. @@ -9849,7 +10119,7 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { forwardtagged = true; }; + if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; }; @@ -9890,7 +10160,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // Bare `return;` from a tagged-union-returning fn is // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -9944,7 +10214,7 @@ fn cglet(c: *cgen, n: *node) void = { // just spill all three. // - Otherwise rhs is a bare variant value: pack tag + // value(s). - if (istaggedtype(tn)) { + if (istaggedtype(c, tn)) { let nullable: bool = isnullabletype(tn); let rhsreturnstagged: bool = false; if (rhs.kind == nkind.N_CALL) { @@ -9956,7 +10226,7 @@ fn cglet(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { rhsreturnstagged = true; }; + if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; }; }; }; @@ -10998,7 +11268,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; - if (istaggedtype(p.lhs)) { + if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; if (idx + nw <= 6) { @@ -11116,7 +11386,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } + if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } else { frame += 8; }; }; }; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index a2947394..e4936ae9 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -199,7 +199,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; - if (istaggedtype(p.lhs)) { + if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; if (idx + nw <= 6) { @@ -317,7 +317,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } + if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } else { frame += 8; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index b2b4a866..07154f37 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -242,7 +242,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -330,7 +330,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -744,7 +744,7 @@ fn cgmatch(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, scrut.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; } else { // Non-ident scrutinee (call result, ?, etc.). Spill into a @@ -766,7 +766,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); - if (rt != nil) { scrutt = resolvetype(c, rt); }; + if (rt != nil) { scrutt = resolvetagged(c, rt); }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 0be233c9..64a261f3 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -142,7 +142,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // For other variants, cgexpr leaves AX, shuffle DX←AX. // Nullable folded `(*T | void)`: just one word; AX is // already the pointer (or 0). No shuffle, no tag. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { // Forwarding a fallible call: `return f();` where f // also returns a tagged union. The result is already // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. @@ -158,7 +158,7 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { forwardtagged = true; }; + if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; }; @@ -199,7 +199,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // Bare `return;` from a tagged-union-returning fn is // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -253,7 +253,7 @@ fn cglet(c: *cgen, n: *node) void = { // just spill all three. // - Otherwise rhs is a bare variant value: pack tag + // value(s). - if (istaggedtype(tn)) { + if (istaggedtype(c, tn)) { let nullable: bool = isnullabletype(tn); let rhsreturnstagged: bool = false; if (rhs.kind == nkind.N_CALL) { @@ -265,7 +265,7 @@ fn cglet(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { rhsreturnstagged = true; }; + if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 97de3c9c..73db176c 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -118,7 +118,7 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + if (isslicetype(c, lc.tnode) || istaggedtype(c, lc.tnode)) { emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), AX\n"); @@ -1053,12 +1053,56 @@ fn isslicetype(c: *cgen, t: *node) bool = { return isslicetyperaw(r); }; -fn istaggedtype(t: *node) bool = { +fn istaggedtyperaw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TTAGGED) { return true; }; return false; }; +// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil +// if `t` doesn't ultimately denote a tagged union. Follows N_TNAME +// aliases (via resolvetype) and unwraps one leading N_TBANG so +// `type error = !(invalid | overflow);` resolves to its inner +// `(invalid | overflow)` node. Use at sites that read variant lists +// or detect nullable folding off a scrutinee — cgmatch, cgtypetest, +// cgtypeassert — so aliased `!(A|B)` shapes still dispatch. +export fn resolvetagged(c: *cgen, t: *node) *node = { + let r: *node = resolvetype(c, t); + if (r == nil) { return nil; }; + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (inner == nil) { return nil; }; + r = resolvetype(c, inner); + if (r == nil) { return nil; }; + }; + if (r.kind == nkind.N_TTAGGED) { return r; }; + return nil; +}; + +// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its +// underlying decl, then unwrap a leading N_TBANG so `type error = +// !(invalid | overflow);` is still recognised as tagged. Without the +// bang unwrap the prologue treats the param as scalar (8B), spilling +// only DI and losing the value-word SI; the match read of slot+8 then +// trails into saved BP. +fn istaggedtype(c: *cgen, t: *node) bool = { + if (istaggedtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (istaggedtyperaw(r)) { return true; }; + if (r != nil) { + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (istaggedtyperaw(inner)) { return true; }; + if (inner != nil) { + let r2: *node = resolvetype(c, inner); + if (istaggedtyperaw(r2)) { return true; }; + }; + }; + }; + return false; +}; + // isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. fn isf32typeraw(t: *node) bool = { if (t == nil) { return false; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 8df65e01..67cb53be 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -335,6 +335,226 @@ export fn freearena(a: *arena) void = { }; }; +// MODULE: strings +// strings — operations over the immutable str type ({ *u8, len }). +// Mirrors Hare's strings::; `len` and `is-empty` aren't functions +// (callers use `s.len` and `s.len == 0` directly). + +use os; + +// compare — bytewise three-way comparison: negative if ab. Matches Hare's strings::compare. ASCII-order, not +// locale-aware. Callers that just need equality use `compare(a, b) == 0`. +export fn compare(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return (a[i]: i32) - (b[i]: i32); }; + i += 1; + }; + return a.len - b.len; +}; + +export fn hasprefix(s: str, p: str) bool = { + if (p.len > s.len) { return false; }; + let i: i32 = 0; + for (i < p.len) { + if (s[i] != p[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn hassuffix(s: str, suf: str) bool = { + if (suf.len > s.len) { return false; }; + let off: i32 = s.len - suf.len; + let i: i32 = 0; + for (i < suf.len) { + if (s[off + i] != suf[i]) { return false; }; + i += 1; + }; + return true; +}; + +// indexbyte — first byte position of byte `c` in `s`. Mirrors +// Hare's strings::byteindex when the needle is a single ASCII rune, +// renamed to match bytes.indexbyte and to disambiguate from Hare's +// `byteindex(haystack, needle: (str | rune))` which we don't have +// the union-arg ABI for yet. +export fn indexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == c) { return i; }; + i += 1; + }; + return; +}; + +// rindexbyte — last byte position of byte `c` in `s`. +export fn rindexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = s.len - 1; + for (i >= 0) { + if (s[i] == c) { return i; }; + i -= 1; + }; + return; +}; + +// index — first index of `sub` in `s`. Naive scan; fine for short +// patterns and small strings, which dominate config and CLI parsing. +// Empty `sub` matches at 0. +export fn index(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return 0; }; + if (sub.len > s.len) { return; }; + let last: i32 = s.len - sub.len; + let i: i32 = 0; + for (i <= last) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i += 1; + }; + return; +}; + +export fn contains(s: str, sub: str) bool = { + let r: (i32 | void) = index(s, sub); + match (r) { + case let i: i32 => return true; + case void => return false; + }; + return false; +}; + +// concat — joins two strings into a fresh str. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::concat shape. +export fn concat(a: str, b: str) str = { + let total: i32 = a.len + b.len; + let buf: *u8 = os.alloc(total: u64): *u8; + let i: i32 = 0; + for (i < a.len) { buf[i] = a[i]; i += 1; }; + let j: i32 = 0; + for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; + let r: str; + r.ptr = buf; + r.len = total; + return r; +}; + +// dup — duplicate a string into a fresh allocation. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::dup shape — Hare returns `(str | nomem)`, ww doesn't +// have nomem (os.alloc aborts on OOM), so we return plain `str`. +// +// Empty input yields a `{nil, 0}` str — Hare returns the static empty +// string; same observable result. +export fn dup(s: str) str = { + let r: str; + r.ptr = nil; + r.len = 0; + if (s.len == 0) { return r; }; + let buf: *u8 = os.alloc(s.len: u64): *u8; + let i: i32 = 0; + for (i < s.len) { buf[i] = s[i]; i += 1; }; + r.ptr = buf; + r.len = s.len; + return r; +}; + +// rindex — last index of `sub` in `s`. Mirrors Hare's strings::rindex +// (slice case). Empty `sub` matches at s.len. +export fn rindex(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return s.len; }; + if (sub.len > s.len) { return; }; + let i: i32 = s.len - sub.len; + for (i >= 0) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i -= 1; + }; + return; +}; + +// sub — borrowed substring `s[start..end]`. Mirrors Hare's +// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of- +// range indices are clamped silently here, where Hare aborts. +export fn sub(s: str, start: i32, end: i32) str = { + let lo: i32 = start; + let hi: i32 = end; + if (lo < 0) { lo = 0; }; + if (hi > s.len) { hi = s.len; }; + if (hi < lo) { hi = lo; }; + let r: str; + r.ptr = s.ptr + (lo: u64); + r.len = hi - lo; + return r; +}; + +// trimprefix — `s` with `pre` stripped from the front, or `s` +// unchanged if it doesn't start with `pre`. Returns a borrowed view. +// Mirrors Hare's strings::trimprefix. +export fn trimprefix(s: str, pre: str) str = { + if (!hasprefix(s, pre)) { return s; }; + let r: str; + r.ptr = s.ptr + (pre.len: u64); + r.len = s.len - pre.len; + return r; +}; + +// trimsuffix — `s` with `suf` stripped from the end, or `s` unchanged +// if it doesn't end with `suf`. Returns a borrowed view. Mirrors +// Hare's strings::trimsuffix. +export fn trimsuffix(s: str, suf: str) str = { + if (!hassuffix(s, suf)) { return s; }; + let r: str; + r.ptr = s.ptr; + r.len = s.len - suf.len; + return r; +}; + +// ltrimbyte / rtrimbyte / trimbyte — strip occurrences of a single +// byte from the left, right, or both ends. Returns a borrowed view. +// Hare's strings::ltrim / rtrim / trim take a rune varargs set; ww's +// subset takes a single byte (the common ASCII case). +export fn ltrimbyte(s: str, c: u8) str = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] != c) { break; }; + i += 1; + }; + let r: str; + r.ptr = s.ptr + (i: u64); + r.len = s.len - i; + return r; +}; + +export fn rtrimbyte(s: str, c: u8) str = { + let n: i32 = s.len; + for (n > 0) { + if (s[n - 1] != c) { break; }; + n -= 1; + }; + let r: str; + r.ptr = s.ptr; + r.len = n; + return r; +}; + +export fn trimbyte(s: str, c: u8) str = { + return rtrimbyte(ltrimbyte(s, c), c); +}; + // MODULE: strconv // strconv — number↔string conversions. // @@ -346,6 +566,7 @@ export fn freearena(a: *arena) void = { // today). Graduate to the static-buffer shape once that lands. use os; +use strings; // invalid — input wasn't a valid number in the requested format. // Payload is the byte index of the first offending position. @@ -690,11 +911,16 @@ export fn f64tos(v: f64) str = { return r; }; -// strerror — Hare has strconv::strerror; ww doesn't ship it yet -// because a `match (e) { case invalid => ... }` arm over the wider -// `error = !(invalid | overflow)` union exposes a cstage-vs-wwstage -// cgen divergence (one cgen spills the unused payload slot, the -// other elides it). Restore once the cgens converge. +// strerror — convert an strconv error to a user-readable string. +// Returns owned str; release via os.free. Mirrors Hare's +// strconv::strerror. +export fn strerror(e: error) str = { + match (e) { + case let v: invalid => return strings.dup("input is not a valid number"); + case let v: overflow => return strings.dup("input number doesn't fit target type"); + }; + return strings.dup(""); +}; // MODULE: lex // lib/ww/lex/tok.ww — port of cmd/wcc/tok.c plus the Tkind / @@ -5426,7 +5652,7 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; - if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) { + if (isslicetype(c, lc.tnode) || istaggedtype(c, lc.tnode)) { emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), AX\n"); @@ -6361,12 +6587,56 @@ fn isslicetype(c: *cgen, t: *node) bool = { return isslicetyperaw(r); }; -fn istaggedtype(t: *node) bool = { +fn istaggedtyperaw(t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TTAGGED) { return true; }; return false; }; +// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil +// if `t` doesn't ultimately denote a tagged union. Follows N_TNAME +// aliases (via resolvetype) and unwraps one leading N_TBANG so +// `type error = !(invalid | overflow);` resolves to its inner +// `(invalid | overflow)` node. Use at sites that read variant lists +// or detect nullable folding off a scrutinee — cgmatch, cgtypetest, +// cgtypeassert — so aliased `!(A|B)` shapes still dispatch. +export fn resolvetagged(c: *cgen, t: *node) *node = { + let r: *node = resolvetype(c, t); + if (r == nil) { return nil; }; + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (inner == nil) { return nil; }; + r = resolvetype(c, inner); + if (r == nil) { return nil; }; + }; + if (r.kind == nkind.N_TTAGGED) { return r; }; + return nil; +}; + +// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its +// underlying decl, then unwrap a leading N_TBANG so `type error = +// !(invalid | overflow);` is still recognised as tagged. Without the +// bang unwrap the prologue treats the param as scalar (8B), spilling +// only DI and losing the value-word SI; the match read of slot+8 then +// trails into saved BP. +fn istaggedtype(c: *cgen, t: *node) bool = { + if (istaggedtyperaw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (istaggedtyperaw(r)) { return true; }; + if (r != nil) { + if (r.kind == nkind.N_TBANG) { + let inner: *node = r.lhs; + if (istaggedtyperaw(inner)) { return true; }; + if (inner != nil) { + let r2: *node = resolvetype(c, inner); + if (istaggedtyperaw(r2)) { return true; }; + }; + }; + }; + return false; +}; + // isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. fn isf32typeraw(t: *node) bool = { if (t == nil) { return false; }; @@ -6891,7 +7161,7 @@ fn cgtypetest(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -6979,7 +7249,7 @@ fn cgtypeassert(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; }; }; @@ -7393,7 +7663,7 @@ fn cgmatch(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, scrut.str); if (lc != nil) { scrutoff = lc.off; - scrutt = resolvetype(c, lc.tnode); + scrutt = resolvetagged(c, lc.tnode); }; } else { // Non-ident scrutinee (call result, ?, etc.). Spill into a @@ -7415,7 +7685,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; if (cnm.len > 0) { let rt: *node = fnretlookup(c, cnm); - if (rt != nil) { scrutt = resolvetype(c, rt); }; + if (rt != nil) { scrutt = resolvetagged(c, rt); }; }; }; }; @@ -9833,7 +10103,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // For other variants, cgexpr leaves AX, shuffle DX←AX. // Nullable folded `(*T | void)`: just one word; AX is // already the pointer (or 0). No shuffle, no tag. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { // Forwarding a fallible call: `return f();` where f // also returns a tagged union. The result is already // in (AX=tag, DX=v0, CX=v1) — no shuffle, no tag. @@ -9849,7 +10119,7 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { forwardtagged = true; }; + if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; }; @@ -9890,7 +10160,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // Bare `return;` from a tagged-union-returning fn is // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. - if (istaggedtype(c.fnret)) { + if (istaggedtype(c, c.fnret)) { if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -9944,7 +10214,7 @@ fn cglet(c: *cgen, n: *node) void = { // just spill all three. // - Otherwise rhs is a bare variant value: pack tag + // value(s). - if (istaggedtype(tn)) { + if (istaggedtype(c, tn)) { let nullable: bool = isnullabletype(tn); let rhsreturnstagged: bool = false; if (rhs.kind == nkind.N_CALL) { @@ -9956,7 +10226,7 @@ fn cglet(c: *cgen, n: *node) void = { if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; if (calleename.len > 0) { let rt: *node = fnretlookup(c, calleename); - if (istaggedtype(rt)) { rhsreturnstagged = true; }; + if (istaggedtype(c, rt)) { rhsreturnstagged = true; }; }; }; }; @@ -10998,7 +11268,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { p = p.next; continue; }; - if (istaggedtype(p.lhs)) { + if (istaggedtype(c, p.lhs)) { let slot: i32 = slotsize(c, p.lhs); let nw: i32 = slot / 8; if (idx + nw <= 6) { @@ -11116,7 +11386,7 @@ fn cgfn(c: *cgen, fn_: *node) void = { let frame: i32 = 0; for (scanp != nil) { if (scanp.kind == nkind.N_PARAM) { - if (istaggedtype(scanp.lhs)) { frame += 24; } + if (istaggedtype(c, scanp.lhs)) { frame += slotsize(c, scanp.lhs); } else { if (isslicetype(c, scanp.lhs)) { frame += 24; } else { if (isstrtype(c, scanp.lhs)) { frame += 16; } else { frame += 8; }; }; }; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index b218a6a4..146b2683 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -227,6 +227,226 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(nr.GETDENTS64, fd: i64, buf: i64, n: i64); }; +// MODULE: strings +// strings — operations over the immutable str type ({ *u8, len }). +// Mirrors Hare's strings::; `len` and `is-empty` aren't functions +// (callers use `s.len` and `s.len == 0` directly). + +use os; + +// compare — bytewise three-way comparison: negative if ab. Matches Hare's strings::compare. ASCII-order, not +// locale-aware. Callers that just need equality use `compare(a, b) == 0`. +export fn compare(a: str, b: str) i32 = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return (a[i]: i32) - (b[i]: i32); }; + i += 1; + }; + return a.len - b.len; +}; + +export fn hasprefix(s: str, p: str) bool = { + if (p.len > s.len) { return false; }; + let i: i32 = 0; + for (i < p.len) { + if (s[i] != p[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn hassuffix(s: str, suf: str) bool = { + if (suf.len > s.len) { return false; }; + let off: i32 = s.len - suf.len; + let i: i32 = 0; + for (i < suf.len) { + if (s[off + i] != suf[i]) { return false; }; + i += 1; + }; + return true; +}; + +// indexbyte — first byte position of byte `c` in `s`. Mirrors +// Hare's strings::byteindex when the needle is a single ASCII rune, +// renamed to match bytes.indexbyte and to disambiguate from Hare's +// `byteindex(haystack, needle: (str | rune))` which we don't have +// the union-arg ABI for yet. +export fn indexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == c) { return i; }; + i += 1; + }; + return; +}; + +// rindexbyte — last byte position of byte `c` in `s`. +export fn rindexbyte(s: str, c: u8) (i32 | void) = { + let i: i32 = s.len - 1; + for (i >= 0) { + if (s[i] == c) { return i; }; + i -= 1; + }; + return; +}; + +// index — first index of `sub` in `s`. Naive scan; fine for short +// patterns and small strings, which dominate config and CLI parsing. +// Empty `sub` matches at 0. +export fn index(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return 0; }; + if (sub.len > s.len) { return; }; + let last: i32 = s.len - sub.len; + let i: i32 = 0; + for (i <= last) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i += 1; + }; + return; +}; + +export fn contains(s: str, sub: str) bool = { + let r: (i32 | void) = index(s, sub); + match (r) { + case let i: i32 => return true; + case void => return false; + }; + return false; +}; + +// concat — joins two strings into a fresh str. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::concat shape. +export fn concat(a: str, b: str) str = { + let total: i32 = a.len + b.len; + let buf: *u8 = os.alloc(total: u64): *u8; + let i: i32 = 0; + for (i < a.len) { buf[i] = a[i]; i += 1; }; + let j: i32 = 0; + for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; + let r: str; + r.ptr = buf; + r.len = total; + return r; +}; + +// dup — duplicate a string into a fresh allocation. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::dup shape — Hare returns `(str | nomem)`, ww doesn't +// have nomem (os.alloc aborts on OOM), so we return plain `str`. +// +// Empty input yields a `{nil, 0}` str — Hare returns the static empty +// string; same observable result. +export fn dup(s: str) str = { + let r: str; + r.ptr = nil; + r.len = 0; + if (s.len == 0) { return r; }; + let buf: *u8 = os.alloc(s.len: u64): *u8; + let i: i32 = 0; + for (i < s.len) { buf[i] = s[i]; i += 1; }; + r.ptr = buf; + r.len = s.len; + return r; +}; + +// rindex — last index of `sub` in `s`. Mirrors Hare's strings::rindex +// (slice case). Empty `sub` matches at s.len. +export fn rindex(s: str, sub: str) (i32 | void) = { + if (sub.len == 0) { return s.len; }; + if (sub.len > s.len) { return; }; + let i: i32 = s.len - sub.len; + for (i >= 0) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i -= 1; + }; + return; +}; + +// sub — borrowed substring `s[start..end]`. Mirrors Hare's +// strings::sub. Caller must ensure 0 <= start <= end <= s.len; out-of- +// range indices are clamped silently here, where Hare aborts. +export fn sub(s: str, start: i32, end: i32) str = { + let lo: i32 = start; + let hi: i32 = end; + if (lo < 0) { lo = 0; }; + if (hi > s.len) { hi = s.len; }; + if (hi < lo) { hi = lo; }; + let r: str; + r.ptr = s.ptr + (lo: u64); + r.len = hi - lo; + return r; +}; + +// trimprefix — `s` with `pre` stripped from the front, or `s` +// unchanged if it doesn't start with `pre`. Returns a borrowed view. +// Mirrors Hare's strings::trimprefix. +export fn trimprefix(s: str, pre: str) str = { + if (!hasprefix(s, pre)) { return s; }; + let r: str; + r.ptr = s.ptr + (pre.len: u64); + r.len = s.len - pre.len; + return r; +}; + +// trimsuffix — `s` with `suf` stripped from the end, or `s` unchanged +// if it doesn't end with `suf`. Returns a borrowed view. Mirrors +// Hare's strings::trimsuffix. +export fn trimsuffix(s: str, suf: str) str = { + if (!hassuffix(s, suf)) { return s; }; + let r: str; + r.ptr = s.ptr; + r.len = s.len - suf.len; + return r; +}; + +// ltrimbyte / rtrimbyte / trimbyte — strip occurrences of a single +// byte from the left, right, or both ends. Returns a borrowed view. +// Hare's strings::ltrim / rtrim / trim take a rune varargs set; ww's +// subset takes a single byte (the common ASCII case). +export fn ltrimbyte(s: str, c: u8) str = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] != c) { break; }; + i += 1; + }; + let r: str; + r.ptr = s.ptr + (i: u64); + r.len = s.len - i; + return r; +}; + +export fn rtrimbyte(s: str, c: u8) str = { + let n: i32 = s.len; + for (n > 0) { + if (s[n - 1] != c) { break; }; + n -= 1; + }; + let r: str; + r.ptr = s.ptr; + r.len = n; + return r; +}; + +export fn trimbyte(s: str, c: u8) str = { + return rtrimbyte(ltrimbyte(s, c), c); +}; + // MODULE: strconv // strconv — number↔string conversions. // @@ -238,6 +458,7 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // today). Graduate to the static-buffer shape once that lands. use os; +use strings; // invalid — input wasn't a valid number in the requested format. // Payload is the byte index of the first offending position. @@ -582,11 +803,16 @@ export fn f64tos(v: f64) str = { return r; }; -// strerror — Hare has strconv::strerror; ww doesn't ship it yet -// because a `match (e) { case invalid => ... }` arm over the wider -// `error = !(invalid | overflow)` union exposes a cstage-vs-wwstage -// cgen divergence (one cgen spills the unused payload slot, the -// other elides it). Restore once the cgens converge. +// strerror — convert an strconv error to a user-readable string. +// Returns owned str; release via os.free. Mirrors Hare's +// strconv::strerror. +export fn strerror(e: error) str = { + match (e) { + case let v: invalid => return strings.dup("input is not a valid number"); + case let v: overflow => return strings.dup("input number doesn't fit target type"); + }; + return strings.dup(""); +}; // MODULE: ascii // ascii — rune-class predicates and case folding for the ASCII range. diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 2f42b694..6d2367d2 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -470,6 +470,24 @@ static const struct row rows[] = { " };\n" " return 0;\n" "};", 40 }, + /* named alias over `!(A | B)`: param spill must size to the + * flattened union (8B tag + 8B payload), not 8B-scalar. Pinned + * the cstage/wwstage divergence where wwstage's istaggedtype + * was alias-blind and the slot+8 read trailed into saved BP. */ + { "type invalid = !i32;\n" + "type overflow = !void;\n" + "type error = !(invalid | overflow);\n" + "fn errcode(e: error) i32 = {\n" + " match (e) {\n" + " case let v: invalid => return v: i32;\n" + " case let v: overflow => return 99;\n" + " };\n" + " return 0;\n" + "};\n" + "fn main() i32 = {\n" + " let e: error = 7: invalid;\n" + " return errcode(e);\n" + "};", 7 }, /* str-typed variant payload: let-init with a string literal, * match-binding loads ptr+len from the slot */ { "fn main() i32 = {\n"