Files
ww/lib/strconv/strconv.ww
2026-05-13 03:10:25 +09:00

189 lines
5.6 KiB
Plaintext

// strconv — number↔string conversions. Decimal i64 to/from a fixed
// buffer. Error shapes mirror Hare's strconv types: (T | invalid |
// overflow) where each error is a named alias over a payload type
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// 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 (we use i32 instead of size).
export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count.
// Hare name; the buffer-in shape is the sanctioned Plan 9 subset of
// Hare's `u64tos(u, base) const str`. Unsigned-only so callers don't
// have to think about wraparound when printing a u64 with the high
// bit set.
export fn u64tos(buf: []u8, v: u64) i32 = {
let tmp: [32]u8;
let i: i32 = 0;
let n: u64 = v;
for (n > 0u64) {
tmp[i] = ((n % 10u64) + 48u64): u8;
n = n / 10u64;
i += 1;
};
if (i == 0) {
tmp[0] = 48u8;
i = 1;
};
let out: i32 = 0;
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
out += 1;
};
return out;
};
export fn i64tos(buf: []u8, v: i64) i32 = {
let neg: bool = false;
let n: i64 = v;
if (n < 0) {
neg = true;
n = -n;
};
let tmp: [32]u8;
let i: i32 = 0;
for (n > 0) {
tmp[i] = ((n % 10) + 48): u8;
n = n / 10;
i += 1;
};
if (i == 0) {
tmp[0] = 48u8;
i = 1;
};
let out: i32 = 0;
if (neg) {
buf[out] = 45u8; // '-'
out += 1;
};
for (i > 0) {
i -= 1;
buf[out] = tmp[i];
out += 1;
};
return out;
};
// stoi64 — Hare-style fallible signed decimal parser. No locale, no
// whitespace, no underscores: a leading '-' is the only non-digit
// accepted, and only at position 0.
export fn stoi64(s: str) (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 v: i64 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c < 48u8) { return i: invalid; };
if (c > 57u8) { return i: invalid; };
v = v * 10 + ((c: i64) - 48);
i += 1;
};
if (neg) { v = -v; };
return v;
};
// stou64 — fallible unsigned decimal parser. No leading sign.
export fn stou64(s: str) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
let v: u64 = 0u64;
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c < 48u8) { return i: invalid; };
if (c > 57u8) { return i: invalid; };
v = v * 10u64 + ((c: u64) - 48u64);
i += 1;
};
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;
};