lib/strconv: parseint sign+overflow core; stoi64/stou64 fidelity (strconv-int fold-1 C1)

Port ref/hare/strconv/stou.ha:8-65 (rune_to_integer + parseint) and the
stoi64/stou64 fidelity rewrite (stoi.ha:9-17, stou.ha:70-76) over the old
digval loop. parseint is the shared sign + per-digit + multiply-overflow
core returning ((bool, u64) | invalid | overflow); stoi64/stou64 destructure
its `(sign, u)` tuple-in-union result — the shape unblocked by #242/#241.

Wins over the prior ad-hoc parse: leading '+' accepted, '-' on stou64 is
overflow (not silently dropped), wraparound overflow detection (n < old),
and the invalid payload carries the offending byte index per Hare.

Tests: lib/strconv/test/inttest.ww (run via test/wcc/922_strconv_int_run.c),
inline per-case checks mirroring Hare's assert sequences stoi.ha:56-86 /
stou.ha:116-138 (Hare's strconv int tests are flat sequences, not row
tables; feedback_test_match_hare_source). Covers valid dec/hex/oct/bin,
+/- sign, invalid+index, overflow, and U64_MAX / I64_MAX / I64_MIN
boundaries. The I64_MIN expectation is spelled -I64_MAX-1 (Hare's own
two's-complement identity) to isolate the test from #245 (wwstage mis-lexes
the literal 9223372036854775808 -> 0); the parse INPUT is unaffected and
yields the correct value on both stages.

combined.ww regen: strconv is compiler-imported (via fmt), so w6c +
wwdump main.combined.ww are regenerated.
This commit is contained in:
2026-06-01 22:05:24 +09:00
parent 5d023c0ef0
commit 6a5cdbd779
8 changed files with 634 additions and 191 deletions

View File

@@ -8,6 +8,7 @@
package strconv;
import ascii;
import os;
import strings;
@@ -125,59 +126,110 @@ export fn u32tos(v: u32, b: base) str = { return u64tos(v: u64, b); };
export fn u16tos(v: u16, b: base) str = { return u64tos(v: u64, b); };
export fn u8tos(v: u8, b: base) 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: base) i32 = {
if (c >= 48u8) { if (c <= 57u8) { return (c - 48u8): i32; }; };
if (b == base.HEX_LOWER) {
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
// rune_to_integer — digit value of r (0-9 → 0-9; a-z/A-Z → 10-35),
// or void if r is not alphanumeric. Verbatim port of
// ref/hare/strconv/stou.ha:8-15 (ww yields the void variant with a
// bare `return;`, per lib/bytes/bytes.ww:65).
fn rune_to_integer(r: rune) (u64 | void) = {
if (ascii.isdigit(r)) {
return (r: u32 - '0'): u64;
} else if (ascii.isalpha(r) && ascii.islower(r)) {
return (r: u32 - 'a'): u64 + 10;
} else if (ascii.isalpha(r) && ascii.isupper(r)) {
return (r: u32 - 'A'): u64 + 10;
};
if (c >= 65u8) { if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; };
if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; };
return -1;
return;
};
// 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: base) (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: base) (u64 | invalid | overflow) = {
if (s.len == 0) { return 0: invalid; };
// parseint — shared sign+digit+overflow core for stoi64/stou64.
// Verbatim port of ref/hare/strconv/stou.ha:17-65. Divergences:
// - param `base` → `b` (ww: avoid the type/value name collision; the
// file already names the enum arg `b`).
// - Hare's DEFAULT→DEC / HEX_LOWER→HEX base reassignment + the
// base-validity assert collapse into basenum(b), which already maps
// every base to its numeric value {2,8,10,16} (default 10). HEX_LOWER
// thus parses case-insensitively, matching Hare's normalize-then-parse.
// - str is byte-indexable, so Hare's `buf = strings::toutf8(s)` is
// elided (existing file convention, cf. the old stoi64/stou64).
// - n *= base / n += digit spelled as plain assignment (sibling-fn
// convention).
fn parseint(s: str, b: base) ((bool, u64) | invalid | overflow) = {
let nb: u64 = basenum(b): u64;
let v: u64 = 0u64;
if (s.len == 0) {
return 0: invalid;
};
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);
let sign: bool = s[i] == '-';
if (sign || s[i] == '+') {
i += 1;
};
return v;
// Require at least one digit.
if (i == s.len) {
return i: invalid;
};
let n: u64 = 0u64;
// Hare's `for (i < len(buf); i += 1)` (stou.ha:43) → condition-only
// for + tail increment (ww has no 2-clause for; sort.ww:25). Early
// returns exit before the increment, so it's never skipped.
for (i < s.len) {
let digit: u64 = match (rune_to_integer(s[i]: rune)) {
case void => return i: invalid;
case let d: u64 => yield d;
};
if (digit >= nb) {
return i: invalid;
};
let old: u64 = n;
n = n * nb;
n = n + digit;
if (n < old) {
return overflow{};
};
i += 1;
};
return (sign, n);
};
// stoi64 — parse signed base-b number. Verbatim port of
// ref/hare/strconv/stoi.ha:9-17. types.I64_MAX is inlined (the const is
// package-private — see the mulshift32 note in ftos.ww).
export fn stoi64(s: str, b: base) (i64 | invalid | overflow) = {
let (sign, u) = parseint(s, b)?;
// Two's complement: I64_MIN = -I64_MAX - 1. Hare's two if-expressions
// (stoi.ha:12,16) are lowered to statement-if — ww has no
// if-expression (standing divergence, see the note in stof.ww:31).
let max: u64 = 9223372036854775807u64;
if (sign) {
max = max + 1u64;
};
if (u > max) {
return overflow{};
};
let r: i64 = u: i64;
if (sign) {
r = -r;
};
return r;
};
// stou64 — parse unsigned base-b number. Verbatim port of
// ref/hare/strconv/stou.ha:70-76.
export fn stou64(s: str, b: base) (u64 | invalid | overflow) = {
let (sign, u) = parseint(s, b)?;
if (sign) {
return overflow{};
};
return u;
};
export fn stoi32(s: str, b: base) (i32 | invalid | overflow) = {