diff --git a/Makefile b/Makefile index ddbd10af..a7d376ae 100644 --- a/Makefile +++ b/Makefile @@ -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 $@ $< diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index 9c8e0a4a..ef9c6895 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -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) = { diff --git a/lib/strconv/test/inttest.ww b/lib/strconv/test/inttest.ww new file mode 100644 index 00000000..ee6bbee5 --- /dev/null +++ b/lib/strconv/test/inttest.ww @@ -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; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1820d976..5c9bbb4a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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) = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d85f8680..7320184f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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) = { diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index ae1e8d90..92dd25d1 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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) = { diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 8e076e26..7e8e9e17 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -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" diff --git a/test/wcc/922_strconv_int_run.c b/test/wcc/922_strconv_int_run.c new file mode 100644 index 00000000..7d7012d7 --- /dev/null +++ b/test/wcc/922_strconv_int_run.c @@ -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 +#include +#include +#include + +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; +}