selfhost: alias-aware istaggedtype for nested-union match

`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.
This commit is contained in:
2026-05-13 04:23:31 +09:00
parent e16634baec
commit 6e7c9e0df4
9 changed files with 893 additions and 59 deletions

View File

@@ -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 a<b, 0 if equal,
// positive if a>b. 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.