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

@@ -388,7 +388,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_intdiv_signed \
$(BIN)/test_strings_run \
$(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \
$(BIN)/test_decimal_run $(BIN)/test_stof_run $(BIN)/test_ftos_run \
$(BIN)/test_decimal_run $(BIN)/test_strconv_int_run \
$(BIN)/test_stof_run $(BIN)/test_ftos_run \
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
$(BIN)/test_errno_run \
$(BIN)/test_base32_run $(BIN)/test_base64_run \
@@ -1492,6 +1493,10 @@ $(BIN)/test_decimal_run: test/wcc/922_decimal_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_strconv_int_run: test/wcc/922_strconv_int_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_stof_run: test/wcc/909_stof_run.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

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) = {

173
lib/strconv/test/inttest.ww Normal file
View File

@@ -0,0 +1,173 @@
// inttest — exercises lib/strconv integer parse: parseint / stoi64 /
// stou64 / the iN/uN width wrappers. Run with
// `out/bin/ww run lib/strconv/test/inttest.ww`. Same
// signalled-then-fail()-with-+10 pattern as decimaltest / bytestest:
// a non-zero exit code (signalled+10) pinpoints the failing case.
//
// Verbatim port of ref/hare/strconv/stoi.ha:56-86 (stoi/stoi_bases) and
// stou.ha:116-138 (stou/stou_bases). Hare's strconv integer tests are
// flat assert SEQUENCES, not row-array tables — mirrored here as inline
// per-case checks (feedback_test_match_hare_source: inline @test-fn for
// verbatim ports). Each case sets `signalled` first so a failure's exit
// code identifies the exact assertion.
//
// Lives in lib/strconv/test/ (not lib/strconv/) so `import strconv`
// resolves to the lib/strconv DIRECTORY (pulls the full package), not
// the strconv.ww FILE — same rationale as 922_decimal_run.
//
// Hex/oct/bin expected values are written in decimal (ww has no 0x/0o/0b
// literal form for the expectation side); the original Hare radix form
// is noted inline.
package strconv;
import strconv;
import os;
let signalled: i32 = 0;
fn fail() void = { os.exit(signalled + 10); };
fn cki(id: i32, s: str, b: base, want: i64) void = {
signalled = id;
match (stoi64(s, b)) {
case let v: i64 => if (v != want) { fail(); };
case let e: invalid => fail();
case let e: overflow => fail();
};
};
fn cki_inv(id: i32, s: str, b: base, idx: i32) void = {
signalled = id;
match (stoi64(s, b)) {
case let v: i64 => fail();
case let e: invalid => if (e: i32 != idx) { fail(); };
case let e: overflow => fail();
};
};
fn cki_ovf(id: i32, s: str, b: base) void = {
signalled = id;
match (stoi64(s, b)) {
case let v: i64 => fail();
case let e: invalid => fail();
case let e: overflow => { };
};
};
fn cku(id: i32, s: str, b: base, want: u64) void = {
signalled = id;
match (stou64(s, b)) {
case let v: u64 => if (v != want) { fail(); };
case let e: invalid => fail();
case let e: overflow => fail();
};
};
fn cku_inv(id: i32, s: str, b: base, idx: i32) void = {
signalled = id;
match (stou64(s, b)) {
case let v: u64 => fail();
case let e: invalid => if (e: i32 != idx) { fail(); };
case let e: overflow => fail();
};
};
fn cku_ovf(id: i32, s: str, b: base) void = {
signalled = id;
match (stou64(s, b)) {
case let v: u64 => fail();
case let e: invalid => fail();
case let e: overflow => { };
};
};
fn cki32_ovf(id: i32, s: str, b: base) void = {
signalled = id;
match (stoi32(s, b)) {
case let v: i32 => fail();
case let e: invalid => fail();
case let e: overflow => { };
};
};
fn cki32(id: i32, s: str, b: base, want: i32) void = {
signalled = id;
match (stoi32(s, b)) {
case let v: i32 => if (v != want) { fail(); };
case let e: invalid => fail();
case let e: overflow => fail();
};
};
// ref/hare/strconv/stoi.ha:56-79.
@test fn test_stoi64() void = {
cki_inv(1, "", base.DEC, 0);
cki_inv(2, "abc", base.DEC, 0);
cki_inv(3, "1a", base.DEC, 1);
cki_inv(4, "+", base.DEC, 1);
cki_inv(5, "-+", base.DEC, 1);
cki_inv(6, "-z", base.DEC, 1);
cki_ovf(7, "9223372036854775808", base.DEC);
cki_ovf(8, "-9223372036854775809", base.DEC);
cki(9, "0", base.DEC, 0);
cki(10, "1", base.DEC, 1);
cki(11, "+1", base.DEC, 1);
cki(12, "-1", base.DEC, -1);
cki(13, "9223372036854775807", base.DEC, 9223372036854775807i64);
// I64_MIN. Spelled -I64_MAX-1 (Hare's own two's-complement identity,
// stoi64 comment in stoi.ha:11) because the wwstage mis-lexes the
// direct literal -9223372036854775808 (and types.I64_MIN) to 0 —
// proj #245. The INPUT string is unaffected; stoi64 parses it to the
// correct value on both stages. This isolates the parse test from #245.
cki(14, "-9223372036854775808", base.DEC, -9223372036854775807i64 - 1i64);
// width wrapper boundaries (ref/hare/strconv/stoi.ha:74-78).
cki32_ovf(15, "2147483648", base.DEC);
cki32_ovf(16, "-2147483649", base.DEC);
cki32(17, "2147483647", base.DEC, 2147483647i32);
cki32(18, "-2147483648", base.DEC, -2147483648i32);
};
// ref/hare/strconv/stoi.ha:81-86.
@test fn test_stoi64_bases() void = {
cki(20, "-7f", base.HEX, -127i64); // -0x7f
cki(21, "7F", base.HEX, 127i64); // 0x7f
cki(22, "37", base.OCT, 31i64); // 0o37
cki(23, "-110101", base.BIN, -53i64); // -0b110101
};
// ref/hare/strconv/stou.ha:116-130.
@test fn test_stou64() void = {
cku_inv(30, "", base.DEC, 0);
cku_inv(31, "+", base.DEC, 1);
cku_inv(32, "+a", base.DEC, 1);
cku_inv(33, "abc", base.DEC, 0);
cku_inv(34, "1a", base.DEC, 1);
cku_ovf(35, "18446744073709551616", base.DEC);
cku_ovf(36, "184467440737095516150", base.DEC);
cku_ovf(37, "-1", base.DEC);
cku(38, "0", base.DEC, 0u64);
cku(39, "1", base.DEC, 1u64);
cku(40, "18446744073709551615", base.DEC, 18446744073709551615u64);
};
// ref/hare/strconv/stou.ha:132-138.
@test fn test_stou64_bases() void = {
cku(41, "f", base.HEX_LOWER, 15u64); // 0xf
cku(42, "7f", base.HEX, 127u64); // 0x7f
cku(43, "7F", base.HEX, 127u64); // 0x7f
cku(44, "37", base.OCT, 31u64); // 0o37
cku(45, "110101", base.BIN, 53u64); // 0b110101
};
export fn main() i32 = {
test_stoi64();
test_stoi64_bases();
test_stou64();
test_stou64_bases();
return 0;
};

View File

@@ -5968,6 +5968,7 @@ let powers_of_ten: [596][2]u64 = [
package strconv;
import ascii;
import os;
import strings;
@@ -6085,59 +6086,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) = {

View File

@@ -5968,6 +5968,7 @@ let powers_of_ten: [596][2]u64 = [
package strconv;
import ascii;
import os;
import strings;
@@ -6085,59 +6086,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) = {

View File

@@ -5968,6 +5968,7 @@ let powers_of_ten: [596][2]u64 = [
package strconv;
import ascii;
import os;
import strings;
@@ -6085,59 +6086,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) = {

View File

@@ -1258,8 +1258,11 @@ static const struct row rows[] = {
" };\n"
" return acc;\n"
"};", 35 }, /* 42 + (-7) + 0 (invalid at index 0 in \"abc\") */
/* strconv.stou64: success path; leading-sign rejected with
* invalid carrying the offending index. */
/* strconv.stou64: success path; a leading '-' is rejected with
* overflow per Hare (ref/hare/strconv/stou.ha:72-74 — parseint
* accepts the sign, stou64 then rejects sign==true as overflow).
* Updated from the prior ad-hoc parse, which mis-reported it as
* invalid(index 0) (strconv-int fold-1 fidelity fix). */
{ "import strconv;\n"
"type r_t = (u64 | strconv.invalid | strconv.overflow);\n"
"fn main() i32 = {\n"
@@ -1272,12 +1275,12 @@ static const struct row rows[] = {
" case let e: strconv.overflow => acc += -200;\n"
" };\n"
" match (r2) {\n"
" case let v: u64 => acc += -100;\n"
" case let e: strconv.invalid => acc += e: i32;\n"
" case let e: strconv.overflow => acc += -200;\n"
" case let v: u64 => acc += 100;\n"
" case let e: strconv.invalid => acc += 200;\n"
" case let e: strconv.overflow => acc += 7;\n"
" };\n"
" return acc;\n"
"};", 123 }, /* 123 + 0 (invalid at index 0 in \"-1\") */
"};", 130 }, /* 123 + 7: \"-1\" is overflow (not invalid/success) */
/* strings.byteindex with (str | rune) needle: returns (i32 | void). */
{ "import strings;\n"
"fn pick(r: (i32 | void), miss: i32) i32 = {\n"

View File

@@ -0,0 +1,54 @@
/*
* 922_strconv_int_run — execute the lib/strconv/test/inttest fixture
* under the C-side `ww run` driver and assert exit 0.
*
* Same thin-wrapper shape as 922_decimal_run / 909_stof_run: inttest.ww
* carries its own `export fn main()` that drives the @test fns and
* signals which case failed via the exit code (signalled + 10), so a
* non-zero exit pinpoints the failing scenario.
*
* The fixture lives in lib/strconv/test/ (not lib/strconv/) so
* `import strconv` resolves to the lib/strconv DIRECTORY rather than
* shadowing on the strconv.ww FILE — same rationale as 922_decimal_run.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
const char *src = "lib/strconv/test/inttest.ww";
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "strconv_int_run FAIL: %s exited %d\n", src, rc);
return 1;
}
printf("strconv_int_run: %s ok\n", src);
return 0;
}