From 76e7bfbb4e3a6114a33c7dba666fdd26698bedc5 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Wed, 14 May 2025 15:53:58 -0400 Subject: [PATCH] runtime: move atoi to internal/runtime/strconv Moving to a smaller package allows its use in other internal/runtime packages. This isn't internal/strconvlite since it can't be used directly by strconv. For #73193. Change-Id: I6a6a636c9c8b3f06b5fd6c07fe9dd5a7a37d1429 Reviewed-on: https://go-review.googlesource.com/c/go/+/672697 Reviewed-by: Michael Knyszek LUCI-TryBot-Result: Go LUCI Auto-Submit: Michael Pratt --- src/cmd/internal/objabi/pkgspecial.go | 1 + src/go/build/deps_test.go | 1 + src/internal/runtime/math/math.go | 3 + src/internal/runtime/strconv/atoi.go | 76 ++++++++++++++++ src/internal/runtime/strconv/atoi_test.go | 105 ++++++++++++++++++++++ src/runtime/export_test.go | 2 - src/runtime/malloc.go | 4 +- src/runtime/mgcpacer.go | 6 +- src/runtime/mheap.go | 2 +- src/runtime/os_linux.go | 3 +- src/runtime/proc.go | 3 +- src/runtime/runtime1.go | 7 +- src/runtime/string.go | 83 ++--------------- src/runtime/string_test.go | 92 ------------------- 14 files changed, 208 insertions(+), 180 deletions(-) create mode 100644 src/internal/runtime/strconv/atoi.go create mode 100644 src/internal/runtime/strconv/atoi_test.go diff --git a/src/cmd/internal/objabi/pkgspecial.go b/src/cmd/internal/objabi/pkgspecial.go index 55b66b6055..d4773b1ecf 100644 --- a/src/cmd/internal/objabi/pkgspecial.go +++ b/src/cmd/internal/objabi/pkgspecial.go @@ -53,6 +53,7 @@ var runtimePkgs = []string{ "internal/runtime/gc", "internal/runtime/maps", "internal/runtime/math", + "internal/runtime/strconv", "internal/runtime/sys", "internal/runtime/syscall", diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index b261af47e2..4f366b34a1 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -97,6 +97,7 @@ var depsRules = ` < internal/runtime/gc < internal/runtime/math < internal/runtime/maps + < internal/runtime/strconv < runtime < sync/atomic < internal/sync diff --git a/src/internal/runtime/math/math.go b/src/internal/runtime/math/math.go index e0fdc3438d..7b616cff79 100644 --- a/src/internal/runtime/math/math.go +++ b/src/internal/runtime/math/math.go @@ -8,7 +8,10 @@ import "internal/goarch" const ( MaxUint32 = ^uint32(0) + MaxUint64 = ^uint64(0) MaxUintptr = ^uintptr(0) + + MaxInt64 = int64(MaxUint64 >> 1) ) // MulUintptr returns a * b and whether the multiplication overflowed. diff --git a/src/internal/runtime/strconv/atoi.go b/src/internal/runtime/strconv/atoi.go new file mode 100644 index 0000000000..87b3faf6d5 --- /dev/null +++ b/src/internal/runtime/strconv/atoi.go @@ -0,0 +1,76 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package strconv + +import ( + "internal/runtime/math" +) + +// Atoi64 parses an int64 from a string s. +// The bool result reports whether s is a number +// representable by a value of type int64. +func Atoi64(s string) (int64, bool) { + if s == "" { + return 0, false + } + + neg := false + if s[0] == '-' { + neg = true + s = s[1:] + } + + un := uint64(0) + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return 0, false + } + if un > math.MaxUint64/10 { + // overflow + return 0, false + } + un *= 10 + un1 := un + uint64(c) - '0' + if un1 < un { + // overflow + return 0, false + } + un = un1 + } + + if !neg && un > uint64(math.MaxInt64) { + return 0, false + } + if neg && un > uint64(math.MaxInt64)+1 { + return 0, false + } + + n := int64(un) + if neg { + n = -n + } + + return n, true +} + +// Atoi is like Atoi64 but for integers +// that fit into an int. +func Atoi(s string) (int, bool) { + if n, ok := Atoi64(s); n == int64(int(n)) { + return int(n), ok + } + return 0, false +} + +// Atoi32 is like Atoi but for integers +// that fit into an int32. +func Atoi32(s string) (int32, bool) { + if n, ok := Atoi64(s); n == int64(int32(n)) { + return int32(n), ok + } + return 0, false +} + diff --git a/src/internal/runtime/strconv/atoi_test.go b/src/internal/runtime/strconv/atoi_test.go new file mode 100644 index 0000000000..49cd6f160a --- /dev/null +++ b/src/internal/runtime/strconv/atoi_test.go @@ -0,0 +1,105 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package strconv_test + +import ( + "internal/runtime/strconv" + "testing" +) + +const intSize = 32 << (^uint(0) >> 63) + +type atoi64Test struct { + in string + out int64 + ok bool +} + +var atoi64tests = []atoi64Test{ + {"", 0, false}, + {"0", 0, true}, + {"-0", 0, true}, + {"1", 1, true}, + {"-1", -1, true}, + {"12345", 12345, true}, + {"-12345", -12345, true}, + {"012345", 12345, true}, + {"-012345", -12345, true}, + {"12345x", 0, false}, + {"-12345x", 0, false}, + {"98765432100", 98765432100, true}, + {"-98765432100", -98765432100, true}, + {"20496382327982653440", 0, false}, + {"-20496382327982653440", 0, false}, + {"9223372036854775807", 1<<63 - 1, true}, + {"-9223372036854775807", -(1<<63 - 1), true}, + {"9223372036854775808", 0, false}, + {"-9223372036854775808", -1 << 63, true}, + {"9223372036854775809", 0, false}, + {"-9223372036854775809", 0, false}, +} + +func TestAtoi(t *testing.T) { + switch intSize { + case 32: + for i := range atoi32tests { + test := &atoi32tests[i] + out, ok := strconv.Atoi(test.in) + if test.out != int32(out) || test.ok != ok { + t.Errorf("Atoi(%q) = (%v, %v) want (%v, %v)", + test.in, out, ok, test.out, test.ok) + } + } + case 64: + for i := range atoi64tests { + test := &atoi64tests[i] + out, ok := strconv.Atoi(test.in) + if test.out != int64(out) || test.ok != ok { + t.Errorf("Atoi(%q) = (%v, %v) want (%v, %v)", + test.in, out, ok, test.out, test.ok) + } + } + } +} + +type atoi32Test struct { + in string + out int32 + ok bool +} + +var atoi32tests = []atoi32Test{ + {"", 0, false}, + {"0", 0, true}, + {"-0", 0, true}, + {"1", 1, true}, + {"-1", -1, true}, + {"12345", 12345, true}, + {"-12345", -12345, true}, + {"012345", 12345, true}, + {"-012345", -12345, true}, + {"12345x", 0, false}, + {"-12345x", 0, false}, + {"987654321", 987654321, true}, + {"-987654321", -987654321, true}, + {"2147483647", 1<<31 - 1, true}, + {"-2147483647", -(1<<31 - 1), true}, + {"2147483648", 0, false}, + {"-2147483648", -1 << 31, true}, + {"2147483649", 0, false}, + {"-2147483649", 0, false}, +} + +func TestAtoi32(t *testing.T) { + for i := range atoi32tests { + test := &atoi32tests[i] + out, ok := strconv.Atoi32(test.in) + if test.out != out || test.ok != ok { + t.Errorf("Atoi32(%q) = (%v, %v) want (%v, %v)", + test.in, out, ok, test.out, test.ok) + } + } +} + diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go index e7f5d426e4..a9cc767e30 100644 --- a/src/runtime/export_test.go +++ b/src/runtime/export_test.go @@ -35,8 +35,6 @@ var ReadRandomFailed = &readRandomFailed var Fastlog2 = fastlog2 -var Atoi = atoi -var Atoi32 = atoi32 var ParseByteCount = parseByteCount var Nanotime = nanotime diff --git a/src/runtime/malloc.go b/src/runtime/malloc.go index ffaf92debc..ccdebb26fb 100644 --- a/src/runtime/malloc.go +++ b/src/runtime/malloc.go @@ -630,7 +630,7 @@ func mallocinit() { } // Initialize the memory limit here because the allocator is going to look at it // but we haven't called gcinit yet and we're definitely going to allocate memory before then. - gcController.memoryLimit.Store(maxInt64) + gcController.memoryLimit.Store(math.MaxInt64) } // sysAlloc allocates heap arena space for at least n bytes. The @@ -1816,7 +1816,7 @@ func profilealloc(mp *m, x unsafe.Pointer, size uintptr) { func nextSample() int64 { if MemProfileRate == 0 { // Basically never sample. - return maxInt64 + return math.MaxInt64 } if MemProfileRate == 1 { // Sample immediately. diff --git a/src/runtime/mgcpacer.go b/src/runtime/mgcpacer.go index 2e05244d95..044792d6bd 100644 --- a/src/runtime/mgcpacer.go +++ b/src/runtime/mgcpacer.go @@ -8,6 +8,8 @@ import ( "internal/cpu" "internal/goexperiment" "internal/runtime/atomic" + "internal/runtime/math" + "internal/runtime/strconv" _ "unsafe" // for go:linkname ) @@ -1311,7 +1313,7 @@ func readGOGC() int32 { if p == "off" { return -1 } - if n, ok := atoi32(p); ok { + if n, ok := strconv.Atoi32(p); ok { return n } return 100 @@ -1355,7 +1357,7 @@ func setMemoryLimit(in int64) (out int64) { func readGOMEMLIMIT() int64 { p := gogetenv("GOMEMLIMIT") if p == "" || p == "off" { - return maxInt64 + return math.MaxInt64 } n, ok := parseByteCount(p) if !ok { diff --git a/src/runtime/mheap.go b/src/runtime/mheap.go index 5a27ab5e78..c5e55f583e 100644 --- a/src/runtime/mheap.go +++ b/src/runtime/mheap.go @@ -1324,7 +1324,7 @@ HaveSpan: // that we expect to page in. inuse := gcController.mappedReady.Load() // Be careful about overflow, especially with uintptrs. Even on 32-bit platforms - // someone can set a really big memory limit that isn't maxInt64. + // someone can set a really big memory limit that isn't math.MaxInt64. if uint64(scav)+inuse > uint64(limit) { bytesToScavenge = uintptr(uint64(scav) + inuse - uint64(limit)) forceScavenge = true diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index f24d18027b..3071e32202 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -8,6 +8,7 @@ import ( "internal/abi" "internal/goarch" "internal/runtime/atomic" + "internal/runtime/strconv" "internal/runtime/syscall" "unsafe" ) @@ -341,7 +342,7 @@ func getHugePageSize() uintptr { return 0 } n-- // remove trailing newline - v, ok := atoi(slicebytetostringtmp((*byte)(ptr), int(n))) + v, ok := strconv.Atoi(slicebytetostringtmp((*byte)(ptr), int(n))) if !ok || v < 0 { v = 0 } diff --git a/src/runtime/proc.go b/src/runtime/proc.go index 1ca800c5fd..5d3e4e4953 100644 --- a/src/runtime/proc.go +++ b/src/runtime/proc.go @@ -11,6 +11,7 @@ import ( "internal/goos" "internal/runtime/atomic" "internal/runtime/exithook" + "internal/runtime/strconv" "internal/runtime/sys" "internal/stringslite" "unsafe" @@ -900,7 +901,7 @@ func schedinit() { lock(&sched.lock) sched.lastpoll.Store(nanotime()) procs := ncpu - if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { + if n, ok := strconv.Atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 { procs = n } if procresize(procs) != nil { diff --git a/src/runtime/runtime1.go b/src/runtime/runtime1.go index 2132ceecb2..5beaa4dd74 100644 --- a/src/runtime/runtime1.go +++ b/src/runtime/runtime1.go @@ -8,6 +8,7 @@ import ( "internal/bytealg" "internal/goarch" "internal/runtime/atomic" + "internal/runtime/strconv" "unsafe" ) @@ -526,13 +527,13 @@ func parsegodebug(godebug string, seen map[string]bool) { // is int, not int32, and should only be updated // if specified in GODEBUG. if seen == nil && key == "memprofilerate" { - if n, ok := atoi(value); ok { + if n, ok := strconv.Atoi(value); ok { MemProfileRate = n } } else { for _, v := range dbgvars { if v.name == key { - if n, ok := atoi32(value); ok { + if n, ok := strconv.Atoi32(value); ok { if seen == nil && v.value != nil { *v.value = n } else if v.atomic != nil { @@ -572,7 +573,7 @@ func setTraceback(level string) { fallthrough default: t = tracebackAll - if n, ok := atoi(level); ok && n == int(uint32(n)) { + if n, ok := strconv.Atoi(level); ok && n == int(uint32(n)) { t |= uint32(n) << tracebackShift } } diff --git a/src/runtime/string.go b/src/runtime/string.go index 7bb9d58de0..44d586bc53 100644 --- a/src/runtime/string.go +++ b/src/runtime/string.go @@ -8,6 +8,8 @@ import ( "internal/abi" "internal/bytealg" "internal/goarch" + "internal/runtime/math" + "internal/runtime/strconv" "internal/runtime/sys" "unsafe" ) @@ -391,77 +393,6 @@ func gostringn(p *byte, l int) string { return s } -const ( - maxUint64 = ^uint64(0) - maxInt64 = int64(maxUint64 >> 1) -) - -// atoi64 parses an int64 from a string s. -// The bool result reports whether s is a number -// representable by a value of type int64. -func atoi64(s string) (int64, bool) { - if s == "" { - return 0, false - } - - neg := false - if s[0] == '-' { - neg = true - s = s[1:] - } - - un := uint64(0) - for i := 0; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - return 0, false - } - if un > maxUint64/10 { - // overflow - return 0, false - } - un *= 10 - un1 := un + uint64(c) - '0' - if un1 < un { - // overflow - return 0, false - } - un = un1 - } - - if !neg && un > uint64(maxInt64) { - return 0, false - } - if neg && un > uint64(maxInt64)+1 { - return 0, false - } - - n := int64(un) - if neg { - n = -n - } - - return n, true -} - -// atoi is like atoi64 but for integers -// that fit into an int. -func atoi(s string) (int, bool) { - if n, ok := atoi64(s); n == int64(int(n)) { - return int(n), ok - } - return 0, false -} - -// atoi32 is like atoi but for integers -// that fit into an int32. -func atoi32(s string) (int32, bool) { - if n, ok := atoi64(s); n == int64(int32(n)) { - return int32(n), ok - } - return 0, false -} - // parseByteCount parses a string that represents a count of bytes. // // s must match the following regular expression: @@ -483,7 +414,7 @@ func parseByteCount(s string) (int64, bool) { // Handle the easy non-suffix case. last := s[len(s)-1] if last >= '0' && last <= '9' { - n, ok := atoi64(s) + n, ok := strconv.Atoi64(s) if !ok || n < 0 { return 0, false } @@ -498,7 +429,7 @@ func parseByteCount(s string) (int64, bool) { // The one before that must always be a digit or 'i'. if c := s[len(s)-2]; c >= '0' && c <= '9' { // Trivial 'B' suffix. - n, ok := atoi64(s[:len(s)-1]) + n, ok := strconv.Atoi64(s[:len(s)-1]) if !ok || n < 0 { return 0, false } @@ -529,17 +460,17 @@ func parseByteCount(s string) (int64, bool) { for i := 0; i < power; i++ { m *= 1024 } - n, ok := atoi64(s[:len(s)-3]) + n, ok := strconv.Atoi64(s[:len(s)-3]) if !ok || n < 0 { return 0, false } un := uint64(n) - if un > maxUint64/m { + if un > math.MaxUint64/m { // Overflow. return 0, false } un *= m - if un > uint64(maxInt64) { + if un > uint64(math.MaxInt64) { // Overflow. return 0, false } diff --git a/src/runtime/string_test.go b/src/runtime/string_test.go index cfc0ad7cde..522a502a1c 100644 --- a/src/runtime/string_test.go +++ b/src/runtime/string_test.go @@ -390,98 +390,6 @@ func TestString2Slice(t *testing.T) { const intSize = 32 << (^uint(0) >> 63) -type atoi64Test struct { - in string - out int64 - ok bool -} - -var atoi64tests = []atoi64Test{ - {"", 0, false}, - {"0", 0, true}, - {"-0", 0, true}, - {"1", 1, true}, - {"-1", -1, true}, - {"12345", 12345, true}, - {"-12345", -12345, true}, - {"012345", 12345, true}, - {"-012345", -12345, true}, - {"12345x", 0, false}, - {"-12345x", 0, false}, - {"98765432100", 98765432100, true}, - {"-98765432100", -98765432100, true}, - {"20496382327982653440", 0, false}, - {"-20496382327982653440", 0, false}, - {"9223372036854775807", 1<<63 - 1, true}, - {"-9223372036854775807", -(1<<63 - 1), true}, - {"9223372036854775808", 0, false}, - {"-9223372036854775808", -1 << 63, true}, - {"9223372036854775809", 0, false}, - {"-9223372036854775809", 0, false}, -} - -func TestAtoi(t *testing.T) { - switch intSize { - case 32: - for i := range atoi32tests { - test := &atoi32tests[i] - out, ok := runtime.Atoi(test.in) - if test.out != int32(out) || test.ok != ok { - t.Errorf("atoi(%q) = (%v, %v) want (%v, %v)", - test.in, out, ok, test.out, test.ok) - } - } - case 64: - for i := range atoi64tests { - test := &atoi64tests[i] - out, ok := runtime.Atoi(test.in) - if test.out != int64(out) || test.ok != ok { - t.Errorf("atoi(%q) = (%v, %v) want (%v, %v)", - test.in, out, ok, test.out, test.ok) - } - } - } -} - -type atoi32Test struct { - in string - out int32 - ok bool -} - -var atoi32tests = []atoi32Test{ - {"", 0, false}, - {"0", 0, true}, - {"-0", 0, true}, - {"1", 1, true}, - {"-1", -1, true}, - {"12345", 12345, true}, - {"-12345", -12345, true}, - {"012345", 12345, true}, - {"-012345", -12345, true}, - {"12345x", 0, false}, - {"-12345x", 0, false}, - {"987654321", 987654321, true}, - {"-987654321", -987654321, true}, - {"2147483647", 1<<31 - 1, true}, - {"-2147483647", -(1<<31 - 1), true}, - {"2147483648", 0, false}, - {"-2147483648", -1 << 31, true}, - {"2147483649", 0, false}, - {"-2147483649", 0, false}, -} - -func TestAtoi32(t *testing.T) { - for i := range atoi32tests { - test := &atoi32tests[i] - out, ok := runtime.Atoi32(test.in) - if test.out != out || test.ok != ok { - t.Errorf("atoi32(%q) = (%v, %v) want (%v, %v)", - test.in, out, ok, test.out, test.ok) - } - } -} - func TestParseByteCount(t *testing.T) { for _, test := range []struct { in string -- 2.51.0