Files
ww/lib/strconv/strconv.ww
Hojun-Cho be8a662f15 lib/strconv: graduate to owned-str returns with Hare-shape base param
i64tos / u64tos / f64tos return a fresh owned str (caller frees via
os.free) instead of writing into a caller-supplied [N]u8. Adds typed
variants (i32tos / i16tos / i8tos and u32 / u16 / u8) and the missing
base parameter on stoi64 / stou64 + typed parse wrappers.

Base values are exported as plain-i32 `def`s (strconv.DEC,
strconv.HEX_UPPER, ...) rather than a `base` enum: cross-module
`strconv.base.DEC` chains miscompile in the cstage cgen — it emits a
memory load through `base(SB)` rather than inlining the constant.
The Sdef path resolves correctly, so callers say `strconv.DEC` and
both cgens lower to an immediate.

Also renames strings.byteindex / rbyteindex to strings.indexbyte /
rindexbyte, matching bytes.indexbyte and reserving the Hare name
`byteindex` for the future `(str | rune)`-needle shape.

fmt drops printint / printlnint / fprintint — those were stand-ins
for variadic `fmt::println(42)`; with the owned-str graduation the
substitute is one call: `fmt.println(strconv.i64tos(42, strconv.DEC))`.

strerror is sketched in a comment but not shipped — match arms over
the wider `error = !(invalid | overflow)` union still expose a
cstage-vs-wwstage spill divergence.
2026-05-13 03:55:16 +09:00

360 lines
11 KiB
Plaintext

// strconv — number↔string conversions.
//
// Mirrors Hare's strconv:: surface. The *tos functions return a fresh
// owned `str`; release via os.free(r.ptr, r.len: u64) when done.
// Hare returns `const str` into a static buffer; ww allocates per
// call because the wwstage cgen doesn't currently support mutating a
// module-level `*u8` (so a lazy-init shared buffer isn't expressible
// today). Graduate to the static-buffer shape once that lands.
use os;
// invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position.
// Mirrors Hare's strconv::invalid = !size.
export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type.
// Mirrors Hare's strconv::overflow = !void.
export type overflow = !void;
// error — any error from a strconv call. Mirrors Hare's strconv::error.
export type error = !(invalid | overflow);
// base — numeric base for parsing/formatting. Plain i32 (not a named
// enum) because cross-module `strconv.base.DEC` chains miscompile in
// the cstage cgen — it emits a memory load through `base(SB)` rather
// than inlining the enum value. Hare names them as `strconv::base`
// enum values; we expose them as module-level `def`s so callers say
// `strconv.DEC` and the cgen inlines the immediate.
//
// HEX is HEX_UPPER; HEX_LOWER is a separate pseudo-base that produces
// lowercase a-f digits.
export def DEFAULT: i32 = 0;
export def BIN: i32 = 2;
export def OCT: i32 = 8;
export def DEC: i32 = 10;
export def HEX_UPPER: i32 = 16;
export def HEX: i32 = 16;
export def HEX_LOWER: i32 = 17;
fn basenum(b: i32) i64 = {
if (b == BIN) { return 2; };
if (b == OCT) { return 8; };
if (b == HEX) { return 16; };
if (b == HEX_UPPER) { return 16; };
if (b == HEX_LOWER) { return 16; };
return 10; // DEC and DEFAULT
};
fn basedigit(d: i64, b: i32) u8 = {
if (d < 10) { return (d + 48): u8; };
let off: i64 = d - 10;
if (b == HEX_LOWER) { return (off + 97): u8; };
return (off + 65): u8;
};
// u64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free(r.ptr, r.len: u64). Mirrors Hare's
// strconv::u64tos (Hare returns const str into a static buffer).
export fn u64tos(v: u64, b: i32) str = {
let nb: u64 = basenum(b): u64;
let tmp: [65]u8;
let i: i32 = 0;
let n: u64 = v;
if (n == 0u64) { tmp[0] = 48u8; i = 1; };
for (n > 0u64) {
let d: i64 = (n % nb): i64;
tmp[i] = basedigit(d, b);
n = n / nb;
i += 1;
};
let buf: *u8 = os.alloc(i: u64): *u8;
let out: i32 = 0;
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.len = out;
return r;
};
// i64tos — convert v to a base-b numeric string. Returns owned str;
// release via os.free. Mirrors Hare's strconv::i64tos.
export fn i64tos(v: i64, b: i32) str = {
let neg: bool = false;
let n: i64 = v;
if (n < 0) { neg = true; n = -n; };
let nb: i64 = basenum(b);
let tmp: [65]u8;
let i: i32 = 0;
if (n == 0) { tmp[0] = 48u8; i = 1; };
for (n > 0) {
let d: i64 = n % nb;
tmp[i] = basedigit(d, b);
n = n / nb;
i += 1;
};
let extra: i32 = 0;
if (neg) { extra = 1; };
let total: i32 = i + extra;
let buf: *u8 = os.alloc(total: u64): *u8;
let out: i32 = 0;
if (neg) { buf[out] = 45u8; out += 1; }; // '-'
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
out += 1;
};
let r: str;
r.ptr = buf;
r.len = out;
return r;
};
export fn i32tos(v: i32, b: i32) str = { return i64tos(v: i64, b); };
export fn i16tos(v: i16, b: i32) str = { return i64tos(v: i64, b); };
export fn i8tos(v: i8, b: i32) str = { return i64tos(v: i64, b); };
export fn u32tos(v: u32, b: i32) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: i32) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: i32) str = { return u64tos(v: u64, b); };
// digval — value of digit byte `c` under base `b`, or -1 if not a
// valid digit. Letters are accepted case-insensitively under HEX /
// HEX_UPPER; only lowercase under HEX_LOWER.
fn digval(c: u8, b: i32) i32 = {
if (c >= 48u8) { if (c <= 57u8) { return (c - 48u8): i32; }; };
if (b == HEX_LOWER) {
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
};
if (c >= 65u8) { if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; };
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
};
// stoi64 — parse signed base-b number. Mirrors Hare's strconv::stoi64.
// No locale, no whitespace, no underscores: optional leading '-' then
// digits. Returns invalid with the offending index or overflow on
// out-of-range.
export fn stoi64(s: str, b: i32) (i64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let i: i32 = 0;
let neg: bool = false;
if (s[0] == 45u8) { neg = true; i = 1; };
if (i >= s.len) { return i: invalid; };
let nb: i32 = basenum(b): i32;
let v: i64 = 0;
for (i < s.len) {
let c: u8 = s[i];
let d: i32 = digval(c, b);
if (d < 0) { return i: invalid; };
if (d >= nb) { return i: invalid; };
v = v * (nb: i64) + (d: i64);
i += 1;
};
if (neg) { v = -v; };
return v;
};
// stou64 — parse unsigned base-b number. Mirrors Hare's strconv::stou64.
export fn stou64(s: str, b: i32) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let nb: u64 = basenum(b): u64;
let v: u64 = 0u64;
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
let d: i32 = digval(c, b);
if (d < 0) { return i: invalid; };
if ((d: u64) >= nb) { return i: invalid; };
v = v * nb + (d: u64);
i += 1;
};
return v;
};
export fn stoi32(s: str, b: i32) (i32 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
if (v > 2147483647i64) { return overflow{}; };
if (v < -2147483648i64) { return overflow{}; };
return v: i32;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid; // unreachable; appeases the path-cov checker
};
export fn stoi16(s: str, b: i32) (i16 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
if (v > 32767i64) { return overflow{}; };
if (v < -32768i64) { return overflow{}; };
return v: i16;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid;
};
export fn stoi8(s: str, b: i32) (i8 | invalid | overflow) = {
let r = stoi64(s, b);
match (r) {
case let v: i64 => {
if (v > 127i64) { return overflow{}; };
if (v < -128i64) { return overflow{}; };
return v: i8;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid;
};
export fn stou32(s: str, b: i32) (u32 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
if (v > 4294967295u64) { return overflow{}; };
return v: u32;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid;
};
export fn stou16(s: str, b: i32) (u16 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
if (v > 65535u64) { return overflow{}; };
return v: u16;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid;
};
export fn stou8(s: str, b: i32) (u8 | invalid | overflow) = {
let r = stou64(s, b);
match (r) {
case let v: u64 => {
if (v > 255u64) { return overflow{}; };
return v: u8;
};
case let e: invalid => return e;
case let e: overflow => return e;
};
return 0: invalid;
};
// f64tos — convert v to a decimal string. Returns owned str; release
// via os.free. Mirrors Hare's strconv::f64tos (current ww impl is
// fixed-point only, max 6 fractional digits, no NaN/Inf support —
// see graduate-to-Ryū note below).
//
// 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.
//
// 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(v: f64) str = {
let tmp: [64]u8;
let out: i32 = 0;
let f: f64 = v;
let zero: f64 = 0: f64;
if (f < zero) {
tmp[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) { tmp[out] = s[k]; out += 1; k += 1; };
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
let r: str;
r.ptr = buf;
r.len = out;
return r;
};
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 intstr: str = i64tos(ip, DEC);
let k: i32 = 0;
for (k < intstr.len) { tmp[out] = intstr.ptr[k]; out += 1; k += 1; };
os.free(intstr.ptr: *void, intstr.len: u64);
if (fp != 0) {
tmp[out] = 46u8; // '.'
out += 1;
let fracstr: str = u64tos(fp: u64, DEC);
// Pad fractional to 6 digits with leading zeros (e.g. 0.05 →
// fp=50000, fracstr="50000", pad one '0' before).
let z: i32 = 6 - fracstr.len;
for (z > 0) { tmp[out] = 48u8; out += 1; z -= 1; };
k = 0;
for (k < fracstr.len) { tmp[out] = fracstr.ptr[k]; out += 1; k += 1; };
os.free(fracstr.ptr: *void, fracstr.len: u64);
// Trim trailing zeros in the fractional part.
for (out > 0) {
if (tmp[out - 1] != 48u8) { break; };
out -= 1;
};
};
let buf: *u8 = os.alloc(out: u64): *u8;
let q: i32 = 0;
for (q < out) { buf[q] = tmp[q]; q += 1; };
let r: str;
r.ptr = buf;
r.len = out;
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.