Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9b548b4

Browse files
jakebaileysandersn
andauthoredJan 15, 2025
Reimplement number/string conversions per ECMAScript spec (microsoft#215)
Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent fae85c0 commit 9b548b4

File tree

6 files changed

+921
-27
lines changed

6 files changed

+921
-27
lines changed
 

‎.github/codeql/codeql-configuration.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ paths-ignore:
44
- _submodules/**
55
- '**/testdata/**'
66
- 'internal/bundled/libs/**'
7+
8+
query-filters:
9+
# This query takes too long on complicated string manipulations
10+
- exclude:
11+
id: go/unsafe-quoting

‎internal/jsnum/jsnum.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package jsnum
33

44
import (
55
"math"
6-
"strconv"
76
)
87

98
const (
@@ -19,12 +18,6 @@ const (
1918
// not the "math" package and conversions.
2019
type Number float64
2120

22-
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-tostring
23-
func (n Number) String() string {
24-
// !!! verify that this is actually the same as JS.
25-
return strconv.FormatFloat(float64(n), 'g', -1, 64)
26-
}
27-
2821
func NaN() Number {
2922
return Number(math.NaN())
3023
}
@@ -41,20 +34,6 @@ func (n Number) IsInf() bool {
4134
return math.IsInf(float64(n), 0)
4235
}
4336

44-
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-stringtonumber
45-
func FromString(s string) Number {
46-
// !!! verify that this is actually the same as JS.
47-
floatValue, err := strconv.ParseFloat(s, 64)
48-
if err == nil {
49-
return Number(floatValue)
50-
}
51-
intValue, err := strconv.ParseInt(s, 0, 64)
52-
if err == nil {
53-
return Number(intValue)
54-
}
55-
return NaN()
56-
}
57-
5837
func isNonFinite(x float64) bool {
5938
// This is equivalent to checking `math.IsNaN(x) || math.IsInf(x, 0)` in one operation.
6039
const mask = 0x7FF0000000000000

‎internal/jsnum/jsnum_test.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,22 @@ import (
1010

1111
func assertEqualNumber(t *testing.T, got, want Number) {
1212
t.Helper()
13-
if want.IsNaN() {
14-
assert.Assert(t, got.IsNaN())
13+
14+
if got.IsNaN() || want.IsNaN() {
15+
assert.Equal(t, got.IsNaN(), want.IsNaN(), "got: %v, want: %v", got, want)
1516
} else {
1617
assert.Equal(t, got, want)
1718
}
1819
}
1920

21+
func numberFromBits(b uint64) Number {
22+
return Number(math.Float64frombits(b))
23+
}
24+
25+
func numberToBits(n Number) uint64 {
26+
return math.Float64bits(float64(n))
27+
}
28+
2029
var toInt32Tests = []struct {
2130
name string
2231
input Number
@@ -46,8 +55,8 @@ var toInt32Tests = []struct {
4655
{"-SmallestNonzeroFloat64", -math.SmallestNonzeroFloat64, 0, false},
4756
{"MaxFloat64", math.MaxFloat64, 0, false},
4857
{"-MaxFloat64", -math.MaxFloat64, 0, false},
49-
{"Largest subnormal number", Number(math.Float64frombits(0x000FFFFFFFFFFFFF)), 0, false},
50-
{"Smallest positive normal number", Number(math.Float64frombits(0x0010000000000000)), 0, false},
58+
{"Largest subnormal number", numberFromBits(0x000FFFFFFFFFFFFF), 0, false},
59+
{"Smallest positive normal number", numberFromBits(0x0010000000000000), 0, false},
5160
{"Largest normal number", math.MaxFloat64, 0, false},
5261
{"-Largest normal number", -math.MaxFloat64, 0, false},
5362
{"1.0", 1.0, 1, false},
@@ -75,7 +84,7 @@ func TestToInt32(t *testing.T) {
7584
t.Parallel()
7685

7786
for _, test := range toInt32Tests {
78-
t.Run(fmt.Sprintf("%s (%v)", test.name, test.input), func(t *testing.T) {
87+
t.Run(fmt.Sprintf("%s (%v)", test.name, float64(test.input)), func(t *testing.T) {
7988
t.Parallel()
8089
assert.Equal(t, test.input.toInt32(), test.want)
8190
})
@@ -90,7 +99,7 @@ func BenchmarkToInt32(b *testing.B) {
9099
continue
91100
}
92101

93-
b.Run(fmt.Sprintf("%s (%v)", test.name, test.input), func(b *testing.B) {
102+
b.Run(fmt.Sprintf("%s (%v)", test.name, float64(test.input)), func(b *testing.B) {
94103
for range b.N {
95104
sink = test.input.toInt32()
96105
}

‎internal/jsnum/ryu_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package jsnum
2+
3+
// Copyright 2018 Ulf Adams
4+
//
5+
// The contents of this file may be used under the terms of the Apache License,
6+
// Version 2.0.
7+
//
8+
// (See accompanying file LICENSE-Apache or copy at
9+
// http://www.apache.org/licenses/LICENSE-2.0)
10+
//
11+
// Alternatively, the contents of this file may be used under the terms of
12+
// the Boost Software License, Version 1.0.
13+
// (See accompanying file LICENSE-Boost or copy at
14+
// https://www.boost.org/LICENSE_1_0.txt)
15+
//
16+
// Unless required by applicable law or agreed to in writing, this software
17+
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18+
// KIND, either express or implied.
19+
20+
// Copied from https://github.com/ulfjack/ryu/blob/1264a946ba66eab320e927bfd2362e0c8580c42f/ryu/tests/d2s_test.cc
21+
// Modified to fit Number::toString's output.
22+
23+
func ieeeParts2Double(sign bool, ieeeExponent uint32, ieeeMantissa uint64) Number {
24+
if ieeeExponent > 2047 {
25+
panic("ieeeExponent > 2047")
26+
}
27+
if ieeeMantissa > maxMantissa {
28+
panic("ieeeMantissa > maxMantissa")
29+
}
30+
signBit := uint64(0)
31+
if sign {
32+
signBit = 1
33+
}
34+
return numberFromBits((signBit << 63) | (uint64(ieeeExponent) << 52) | ieeeMantissa)
35+
}
36+
37+
const maxMantissa = (1 << 53) - 1
38+
39+
var ryuTests = []stringTest{
40+
{2.2250738585072014e-308, "2.2250738585072014e-308"},
41+
{numberFromBits(0x7fefffffffffffff), "1.7976931348623157e+308"},
42+
{numberFromBits(1), "5e-324"},
43+
{2.98023223876953125e-8, "2.9802322387695312e-8"},
44+
{-2.109808898695963e16, "-21098088986959630"},
45+
{4.940656e-318, "4.940656e-318"},
46+
{1.18575755e-316, "1.18575755e-316"},
47+
{2.989102097996e-312, "2.989102097996e-312"},
48+
{9.0608011534336e15, "9060801153433600"},
49+
{4.708356024711512e18, "4708356024711512000"},
50+
{9.409340012568248e18, "9409340012568248000"},
51+
{1.2345678, "1.2345678"},
52+
{numberFromBits(0x4830F0CF064DD592), "5.764607523034235e+39"},
53+
{numberFromBits(0x4840F0CF064DD592), "1.152921504606847e+40"},
54+
{numberFromBits(0x4850F0CF064DD592), "2.305843009213694e+40"},
55+
{1.2, "1.2"},
56+
{1.23, "1.23"},
57+
{1.234, "1.234"},
58+
{1.2345, "1.2345"},
59+
{1.23456, "1.23456"},
60+
{1.234567, "1.234567"},
61+
{1.2345678, "1.2345678"},
62+
{1.23456789, "1.23456789"},
63+
{1.234567895, "1.234567895"},
64+
{1.2345678901, "1.2345678901"},
65+
{1.23456789012, "1.23456789012"},
66+
{1.234567890123, "1.234567890123"},
67+
{1.2345678901234, "1.2345678901234"},
68+
{1.23456789012345, "1.23456789012345"},
69+
{1.234567890123456, "1.234567890123456"},
70+
{1.2345678901234567, "1.2345678901234567"},
71+
{4.294967294, "4.294967294"},
72+
{4.294967295, "4.294967295"},
73+
{4.294967296, "4.294967296"},
74+
{4.294967297, "4.294967297"},
75+
{4.294967298, "4.294967298"},
76+
{ieeeParts2Double(false, 4, 0), "1.7800590868057611e-307"},
77+
{ieeeParts2Double(false, 6, maxMantissa), "2.8480945388892175e-306"},
78+
{ieeeParts2Double(false, 41, 0), "2.446494580089078e-296"},
79+
{ieeeParts2Double(false, 40, maxMantissa), "4.8929891601781557e-296"},
80+
{ieeeParts2Double(false, 1077, 0), "18014398509481984"},
81+
{ieeeParts2Double(false, 1076, maxMantissa), "36028797018963964"},
82+
{ieeeParts2Double(false, 307, 0), "2.900835519859558e-216"},
83+
{ieeeParts2Double(false, 306, maxMantissa), "5.801671039719115e-216"},
84+
{ieeeParts2Double(false, 934, 0x000FA7161A4D6E0C), "3.196104012172126e-27"},
85+
{9007199254740991.0, "9007199254740991"},
86+
{9007199254740992.0, "9007199254740992"},
87+
{1.0e+0, "1"},
88+
{1.2e+1, "12"},
89+
{1.23e+2, "123"},
90+
{1.234e+3, "1234"},
91+
{1.2345e+4, "12345"},
92+
{1.23456e+5, "123456"},
93+
{1.234567e+6, "1234567"},
94+
{1.2345678e+7, "12345678"},
95+
{1.23456789e+8, "123456789"},
96+
{1.23456789e+9, "1234567890"},
97+
{1.234567895e+9, "1234567895"},
98+
{1.2345678901e+10, "12345678901"},
99+
{1.23456789012e+11, "123456789012"},
100+
{1.234567890123e+12, "1234567890123"},
101+
{1.2345678901234e+13, "12345678901234"},
102+
{1.23456789012345e+14, "123456789012345"},
103+
{1.234567890123456e+15, "1234567890123456"},
104+
{1.0e+0, "1"},
105+
{1.0e+1, "10"},
106+
{1.0e+2, "100"},
107+
{1.0e+3, "1000"},
108+
{1.0e+4, "10000"},
109+
{1.0e+5, "100000"},
110+
{1.0e+6, "1000000"},
111+
{1.0e+7, "10000000"},
112+
{1.0e+8, "100000000"},
113+
{1.0e+9, "1000000000"},
114+
{1.0e+10, "10000000000"},
115+
{1.0e+11, "100000000000"},
116+
{1.0e+12, "1000000000000"},
117+
{1.0e+13, "10000000000000"},
118+
{1.0e+14, "100000000000000"},
119+
{1.0e+15, "1000000000000000"},
120+
{1000000000000001, "1000000000000001"},
121+
{1000000000000010, "1000000000000010"},
122+
{1000000000000100, "1000000000000100"},
123+
{1000000000001000, "1000000000001000"},
124+
{1000000000010000, "1000000000010000"},
125+
{1000000000100000, "1000000000100000"},
126+
{1000000001000000, "1000000001000000"},
127+
{1000000010000000, "1000000010000000"},
128+
{1000000100000000, "1000000100000000"},
129+
{1000001000000000, "1000001000000000"},
130+
{1000010000000000, "1000010000000000"},
131+
{1000100000000000, "1000100000000000"},
132+
{1001000000000000, "1001000000000000"},
133+
{1010000000000000, "1010000000000000"},
134+
{1100000000000000, "1100000000000000"},
135+
{8.0, "8"},
136+
{64.0, "64"},
137+
{512.0, "512"},
138+
{8192.0, "8192"},
139+
{65536.0, "65536"},
140+
{524288.0, "524288"},
141+
{8388608.0, "8388608"},
142+
{67108864.0, "67108864"},
143+
{536870912.0, "536870912"},
144+
{8589934592.0, "8589934592"},
145+
{68719476736.0, "68719476736"},
146+
{549755813888.0, "549755813888"},
147+
{8796093022208.0, "8796093022208"},
148+
{70368744177664.0, "70368744177664"},
149+
{562949953421312.0, "562949953421312"},
150+
{9007199254740992.0, "9007199254740992"},
151+
{8.0e+3, "8000"},
152+
{64.0e+3, "64000"},
153+
{512.0e+3, "512000"},
154+
{8192.0e+3, "8192000"},
155+
{65536.0e+3, "65536000"},
156+
{524288.0e+3, "524288000"},
157+
{8388608.0e+3, "8388608000"},
158+
{67108864.0e+3, "67108864000"},
159+
{536870912.0e+3, "536870912000"},
160+
{8589934592.0e+3, "8589934592000"},
161+
{68719476736.0e+3, "68719476736000"},
162+
{549755813888.0e+3, "549755813888000"},
163+
{8796093022208.0e+3, "8796093022208000"},
164+
}

‎internal/jsnum/string.go

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
package jsnum
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"math"
7+
"math/big"
8+
"strconv"
9+
"strings"
10+
"unicode"
11+
"unicode/utf8"
12+
13+
"github.com/microsoft/typescript-go/internal/stringutil"
14+
)
15+
16+
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-tostring
17+
func (n Number) String() string {
18+
switch {
19+
case n.IsNaN():
20+
return "NaN"
21+
case n.IsInf():
22+
if n < 0 {
23+
return "-Infinity"
24+
}
25+
return "Infinity"
26+
}
27+
28+
// Fast path: for safe integers, directly convert to string.
29+
if MinSafeInteger <= n && n <= MaxSafeInteger {
30+
if i := int64(n); float64(i) == float64(n) {
31+
return strconv.FormatInt(i, 10)
32+
}
33+
}
34+
35+
// Otherwise, the Go json package handles this correctly.
36+
b, _ := json.Marshal(float64(n)) //nolint:errchkjson
37+
return string(b)
38+
}
39+
40+
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-stringtonumber
41+
func FromString(s string) Number {
42+
// Implementing StringToNumber exactly as written in the spec involves
43+
// writing a parser, along with the conversion of the parsed AST into the
44+
// actual value.
45+
//
46+
// We've already implemented a number parser in the scanner, but we can't
47+
// import it here. We also do not have the conversion implemented since we
48+
// previously just wrote `+literal` and let the runtime handle it.
49+
//
50+
// The strategy below is to instead break the number apart and fix it up
51+
// such that Go's own parsing functionality can handle it. This won't be
52+
// the fastest method, but it saves us from writing the full parser and
53+
// conversion logic.
54+
55+
s = strings.TrimFunc(s, isStrWhiteSpace)
56+
57+
switch s {
58+
case "":
59+
return 0
60+
case "Infinity", "+Infinity":
61+
return Inf(1)
62+
case "-Infinity":
63+
return Inf(-1)
64+
}
65+
66+
for _, r := range s {
67+
if !isNumberRune(r) {
68+
return NaN()
69+
}
70+
}
71+
72+
if n, ok := tryParseInt(s); ok {
73+
return n
74+
}
75+
76+
// Cut this off first so we can ensure -0 is returned as -0.
77+
s, negative := strings.CutPrefix(s, "-")
78+
79+
if !negative {
80+
s, _ = strings.CutPrefix(s, "+")
81+
}
82+
83+
if first, _ := utf8.DecodeRuneInString(s); !stringutil.IsDigit(first) && first != '.' {
84+
return NaN()
85+
}
86+
87+
f := parseFloatString(s)
88+
if math.IsNaN(f) {
89+
return NaN()
90+
}
91+
92+
sign := 1.0
93+
if negative {
94+
sign = -1.0
95+
}
96+
return Number(math.Copysign(f, sign))
97+
}
98+
99+
func isStrWhiteSpace(r rune) bool {
100+
// This is different than stringutil.IsWhiteSpaceLike.
101+
102+
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-LineTerminator
103+
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-WhiteSpace
104+
105+
switch r {
106+
// LineTerminator
107+
case '\n', '\r', 0x2028, 0x2029:
108+
return true
109+
// WhiteSpace
110+
case '\t', '\v', '\f', 0xFEFF:
111+
return true
112+
}
113+
114+
// WhiteSpace
115+
return unicode.Is(unicode.Zs, r)
116+
}
117+
118+
var errUnknownPrefix = errors.New("unknown number prefix")
119+
120+
func tryParseInt(s string) (Number, bool) {
121+
var i int64
122+
var err error
123+
var hasIntResult bool
124+
125+
if len(s) > 2 {
126+
prefix, rest := s[:2], s[2:]
127+
switch prefix {
128+
case "0b", "0B":
129+
if !isAllBinaryDigits(rest) {
130+
return NaN(), true
131+
}
132+
i, err = strconv.ParseInt(rest, 2, 64)
133+
hasIntResult = true
134+
case "0o", "0O":
135+
if !isAllOctalDigits(rest) {
136+
return NaN(), true
137+
}
138+
i, err = strconv.ParseInt(rest, 8, 64)
139+
hasIntResult = true
140+
case "0x", "0X":
141+
if !isAllHexDigits(rest) {
142+
return NaN(), true
143+
}
144+
i, err = strconv.ParseInt(rest, 16, 64)
145+
hasIntResult = true
146+
}
147+
}
148+
149+
if !hasIntResult {
150+
// StringToNumber does not parse leading zeros as octal.
151+
s = trimLeadingZeros(s)
152+
if !isAllDigits(s) {
153+
return 0, false
154+
}
155+
i, err = strconv.ParseInt(s, 10, 64)
156+
hasIntResult = true
157+
}
158+
159+
if hasIntResult && err == nil {
160+
return Number(i), true
161+
}
162+
163+
// Using this to parse large integers.
164+
bi, ok := new(big.Int).SetString(s, 0)
165+
if !ok {
166+
return NaN(), true
167+
}
168+
169+
f, _ := bi.Float64()
170+
return Number(f), true
171+
}
172+
173+
func parseFloatString(s string) float64 {
174+
var hasDot, hasExp bool
175+
176+
// <a>
177+
// <a>.<b>
178+
// <a>.<b>e<c>
179+
// <a>e<c>
180+
var a, b, c, rest string
181+
182+
a, rest, hasDot = strings.Cut(s, ".")
183+
if hasDot {
184+
// <a>.<b>
185+
// <a>.<b>e<c>
186+
b, c, hasExp = cutAny(rest, "eE")
187+
} else {
188+
// <a>
189+
// <a>e<c>
190+
a, c, hasExp = cutAny(s, "eE")
191+
}
192+
193+
var sb strings.Builder
194+
sb.Grow(len(a) + len(b) + len(c) + 3)
195+
196+
if a == "" {
197+
if hasDot && b == "" {
198+
return math.NaN()
199+
}
200+
if hasExp && c == "" {
201+
return math.NaN()
202+
}
203+
sb.WriteString("0")
204+
} else {
205+
a = trimLeadingZeros(a)
206+
if !isAllDigits(a) {
207+
return math.NaN()
208+
}
209+
sb.WriteString(a)
210+
}
211+
212+
if hasDot {
213+
sb.WriteString(".")
214+
if b == "" {
215+
sb.WriteString("0")
216+
} else {
217+
b = trimTrailingZeros(b)
218+
if !isAllDigits(b) {
219+
return math.NaN()
220+
}
221+
sb.WriteString(b)
222+
}
223+
}
224+
225+
if hasExp {
226+
sb.WriteString("e")
227+
228+
c, negative := strings.CutPrefix(c, "-")
229+
if negative {
230+
sb.WriteString("-")
231+
} else {
232+
c, _ = strings.CutPrefix(c, "+")
233+
}
234+
c = trimLeadingZeros(c)
235+
if !isAllDigits(c) {
236+
return math.NaN()
237+
}
238+
sb.WriteString(c)
239+
}
240+
241+
return stringToFloat64(sb.String())
242+
}
243+
244+
func cutAny(s string, cutset string) (before, after string, found bool) {
245+
if i := strings.IndexAny(s, cutset); i >= 0 {
246+
before = s[:i]
247+
afterAndFound := s[i:]
248+
_, size := utf8.DecodeRuneInString(afterAndFound)
249+
after = afterAndFound[size:]
250+
return before, after, true
251+
}
252+
return s, "", false
253+
}
254+
255+
func trimLeadingZeros(s string) string {
256+
if strings.HasPrefix(s, "0") {
257+
s = strings.TrimLeft(s, "0")
258+
if s == "" {
259+
return "0"
260+
}
261+
}
262+
return s
263+
}
264+
265+
func trimTrailingZeros(s string) string {
266+
if strings.HasSuffix(s, "0") {
267+
s = strings.TrimRight(s, "0")
268+
if s == "" {
269+
return "0"
270+
}
271+
}
272+
return s
273+
}
274+
275+
func stringToFloat64(s string) float64 {
276+
if f, err := strconv.ParseFloat(s, 64); err == nil {
277+
return f
278+
} else {
279+
if errors.Is(err, strconv.ErrRange) {
280+
return f
281+
}
282+
}
283+
return math.NaN()
284+
}
285+
286+
func isAllDigits(s string) bool {
287+
for _, r := range s {
288+
if !stringutil.IsDigit(r) {
289+
return false
290+
}
291+
}
292+
return true
293+
}
294+
295+
func isAllBinaryDigits(s string) bool {
296+
for _, r := range s {
297+
if r != '0' && r != '1' {
298+
return false
299+
}
300+
}
301+
return true
302+
}
303+
304+
func isAllOctalDigits(s string) bool {
305+
for _, r := range s {
306+
if !stringutil.IsOctalDigit(r) {
307+
return false
308+
}
309+
}
310+
return true
311+
}
312+
313+
func isAllHexDigits(s string) bool {
314+
for _, r := range s {
315+
if !stringutil.IsHexDigit(r) {
316+
return false
317+
}
318+
}
319+
return true
320+
}
321+
322+
func isNumberRune(r rune) bool {
323+
if stringutil.IsDigit(r) {
324+
return true
325+
}
326+
327+
if 'a' <= r && r <= 'f' {
328+
return true
329+
}
330+
331+
if 'A' <= r && r <= 'F' {
332+
return true
333+
}
334+
335+
switch r {
336+
case '.', '-', '+', 'x', 'X', 'o', 'O':
337+
return true
338+
}
339+
340+
return false
341+
}

‎internal/jsnum/string_test.go

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
package jsnum
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"flag"
7+
"fmt"
8+
"math"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"slices"
13+
"testing"
14+
15+
"gotest.tools/v3/assert"
16+
)
17+
18+
type stringTest struct {
19+
number Number
20+
str string
21+
}
22+
23+
var stringTests = slices.Concat([]stringTest{
24+
{NaN(), "NaN"},
25+
{Inf(1), "Infinity"},
26+
{Inf(-1), "-Infinity"},
27+
{0, "0"},
28+
{negativeZero, "0"},
29+
{1, "1"},
30+
{-1, "-1"},
31+
{0.3, "0.3"},
32+
{-0.3, "-0.3"},
33+
{1.5, "1.5"},
34+
{-1.5, "-1.5"},
35+
{1e308, "1e+308"},
36+
{-1e308, "-1e+308"},
37+
{math.Pi, "3.141592653589793"},
38+
{-math.Pi, "-3.141592653589793"},
39+
{MaxSafeInteger, "9007199254740991"},
40+
{MinSafeInteger, "-9007199254740991"},
41+
{numberFromBits(0x000FFFFFFFFFFFFF), "2.225073858507201e-308"},
42+
{numberFromBits(0x0010000000000000), "2.2250738585072014e-308"},
43+
{1234567.8, "1234567.8"},
44+
{19686109595169230000, "19686109595169230000"},
45+
{123.456, "123.456"},
46+
{-123.456, "-123.456"},
47+
{444123, "444123"},
48+
{-444123, "-444123"},
49+
{444123.789123456789875436, "444123.7891234568"},
50+
{-444123.78963636363636363636, "-444123.7896363636"},
51+
{1e21, "1e+21"},
52+
{1e20, "100000000000000000000"},
53+
}, ryuTests)
54+
55+
func TestString(t *testing.T) {
56+
t.Parallel()
57+
58+
for _, test := range stringTests {
59+
fInput := float64(test.number)
60+
61+
t.Run(fmt.Sprintf("%v", fInput), func(t *testing.T) {
62+
t.Parallel()
63+
assert.Equal(t, test.number.String(), test.str)
64+
})
65+
}
66+
}
67+
68+
var fromStringTests = []stringTest{
69+
{NaN(), " NaN"},
70+
{Inf(1), "Infinity "},
71+
{Inf(-1), " -Infinity"},
72+
{1, "1."},
73+
{1, "1.0 "},
74+
{1, "+1"},
75+
{1, "+1."},
76+
{1, "+1.0"},
77+
{NaN(), "whoops"},
78+
{0, ""},
79+
{0, "0"},
80+
{0, "0."},
81+
{0, "0.0"},
82+
{0, "0.0000"},
83+
{0, ".0000"},
84+
{negativeZero, "-0"},
85+
{negativeZero, "-0."},
86+
{negativeZero, "-0.0"},
87+
{negativeZero, "-.0"},
88+
{NaN(), "."},
89+
{NaN(), "e"},
90+
{NaN(), ".e"},
91+
{NaN(), "+"},
92+
{0, "0X0"},
93+
{NaN(), "e0"},
94+
{NaN(), "E0"},
95+
{NaN(), "1e"},
96+
{NaN(), "1e+"},
97+
{NaN(), "1e-"},
98+
{1, "1e+0"},
99+
{NaN(), "++0"},
100+
{NaN(), "0_0"},
101+
{Inf(1), "1e1000"},
102+
{Inf(-1), "-1e1000"},
103+
{0, ".0e0"},
104+
{NaN(), "0e++0"},
105+
{10, "0XA"},
106+
{0b1010, "0b1010"},
107+
{0b1010, "0B1010"},
108+
{0o12, "0o12"},
109+
{0o12, "0O12"},
110+
{0x123456789abcdef0, "0x123456789abcdef0"},
111+
{0x123456789abcdef0, "0X123456789ABCDEF0"},
112+
{18446744073709552000, "0X10000000000000000"},
113+
{18446744073709597000, "0X1000000000000A801"},
114+
{NaN(), "0B0.0"},
115+
{1.231235345083403e+91, "12312353450834030486384068034683603046834603806830644850340602384608368034634603680348603864"},
116+
{NaN(), "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8OOOOOOOOOOOOOOOOOOO"},
117+
{Inf(1), "+Infinity"},
118+
{1234.56, " \t1234.56 "},
119+
{NaN(), "\u200b"},
120+
{0, " "},
121+
{0, "\n"},
122+
{0, "\r"},
123+
{0, "\r\n"},
124+
{0, "\u2028"},
125+
{0, "\u2029"},
126+
{0, "\t"},
127+
{0, "\v"},
128+
{0, "\f"},
129+
{0, "\uFEFF"},
130+
{0, "\u00A0"},
131+
{10000000000000000000, "010000000000000000000"},
132+
{NaN(), "0x1.fffffffffffffp1023"}, // Make sure Go's extended float syntax doesn't work.
133+
{NaN(), "0X_1FFFP-16"},
134+
{NaN(), "1_000"}, // NumberToString doesn't handle underscores.
135+
{0, "0x0"},
136+
{0, "0X0"},
137+
{NaN(), "0xOOPS"},
138+
{0xABCDEF, "0xABCDEF"},
139+
{0xABCDEF, "0xABCDEF"},
140+
{0, "0o0"},
141+
{0, "0O0"},
142+
{NaN(), "0o8"},
143+
{NaN(), "0O8"},
144+
{0o12345, "0o12345"},
145+
{0o12345, "0O12345"},
146+
{0, "0b0"},
147+
{0, "0B0"},
148+
{NaN(), "0b2"},
149+
{NaN(), "0b2"},
150+
{0b10101, "0b10101"},
151+
{0b10101, "0B10101"},
152+
{NaN(), "1.f"},
153+
{NaN(), "1.e"},
154+
{NaN(), "1.0ef"},
155+
{NaN(), "1.0e"},
156+
{NaN(), ".f"},
157+
{NaN(), ".e"},
158+
{NaN(), ".0ef"},
159+
{NaN(), ".0e"},
160+
{NaN(), "a.f"},
161+
{NaN(), "a.e"},
162+
{NaN(), "a.0ef"},
163+
{NaN(), "a.0e"},
164+
}
165+
166+
func TestFromString(t *testing.T) {
167+
t.Parallel()
168+
169+
t.Run("stringTests", func(t *testing.T) {
170+
t.Parallel()
171+
for _, test := range stringTests {
172+
t.Run(test.str, func(t *testing.T) {
173+
t.Parallel()
174+
assertEqualNumber(t, FromString(test.str), test.number)
175+
assertEqualNumber(t, FromString(test.str+" "), test.number)
176+
assertEqualNumber(t, FromString(" "+test.str), test.number)
177+
})
178+
}
179+
})
180+
181+
t.Run("fromStringTests", func(t *testing.T) {
182+
t.Parallel()
183+
for _, test := range fromStringTests {
184+
t.Run(test.str, func(t *testing.T) {
185+
t.Parallel()
186+
assertEqualNumber(t, FromString(test.str), test.number)
187+
})
188+
}
189+
})
190+
}
191+
192+
func TestStringRoundtrip(t *testing.T) {
193+
t.Parallel()
194+
195+
for _, test := range stringTests {
196+
t.Run(test.str, func(t *testing.T) {
197+
t.Parallel()
198+
assert.Equal(t, FromString(test.str).String(), test.str)
199+
})
200+
}
201+
}
202+
203+
func getNodeExe(t testing.TB) string {
204+
t.Helper()
205+
206+
const exeName = "node"
207+
exe, err := exec.LookPath(exeName)
208+
if err != nil {
209+
t.Skipf("%s not found: %v", exeName, err)
210+
}
211+
return exe
212+
}
213+
214+
func TestStringJS(t *testing.T) {
215+
t.Parallel()
216+
217+
exe := getNodeExe(t)
218+
219+
t.Run("stringTests", func(t *testing.T) {
220+
t.Parallel()
221+
222+
// These tests should roundtrip both ways.
223+
stringTestsResults := getStringResultsFromJS(t, exe, stringTests)
224+
for i, test := range stringTests {
225+
t.Run(fmt.Sprintf("%v", float64(test.number)), func(t *testing.T) {
226+
t.Parallel()
227+
assertEqualNumber(t, stringTestsResults[i].number, test.number)
228+
assert.Equal(t, stringTestsResults[i].str, test.str)
229+
})
230+
}
231+
})
232+
233+
t.Run("fromStringTests", func(t *testing.T) {
234+
t.Parallel()
235+
236+
// These tests should convert the string to the same number.
237+
fromStringTestsResults := getStringResultsFromJS(t, exe, fromStringTests)
238+
for i, test := range fromStringTests {
239+
t.Run(fmt.Sprintf("fromString %q", test.str), func(t *testing.T) {
240+
t.Parallel()
241+
assertEqualNumber(t, fromStringTestsResults[i].number, test.number)
242+
})
243+
}
244+
})
245+
}
246+
247+
func isFuzzing() bool {
248+
return flag.CommandLine.Lookup("test.fuzz").Value.String() != ""
249+
}
250+
251+
func FuzzStringJS(f *testing.F) {
252+
exe := getNodeExe(f)
253+
254+
if isFuzzing() {
255+
// Avoid running anything other than regressions in the fuzzing mode.
256+
for _, test := range stringTests {
257+
f.Add(float64(test.number))
258+
}
259+
for _, test := range fromStringTests {
260+
f.Add(float64(test.number))
261+
}
262+
}
263+
264+
f.Fuzz(func(t *testing.T, f float64) {
265+
n := Number(f)
266+
nStr := n.String()
267+
268+
results := getStringResultsFromJS(t, exe, []stringTest{{number: n, str: nStr}})
269+
assert.Equal(t, len(results), 1)
270+
271+
nToJSStr := results[0].str
272+
nStrToJSNumber := results[0].number
273+
274+
assert.Equal(t, nStr, nToJSStr)
275+
assertEqualNumber(t, n, nStrToJSNumber)
276+
})
277+
}
278+
279+
func FuzzFromStringJS(f *testing.F) {
280+
exe := getNodeExe(f)
281+
282+
if isFuzzing() {
283+
// Avoid running anything other than regressions in the fuzzing mode.
284+
for _, test := range stringTests {
285+
f.Add(test.str)
286+
}
287+
for _, test := range fromStringTests {
288+
f.Add(test.str)
289+
}
290+
}
291+
292+
f.Fuzz(func(t *testing.T, s string) {
293+
if len(s) > 350 {
294+
t.Skip()
295+
}
296+
297+
n := FromString(s)
298+
results := getStringResultsFromJS(t, exe, []stringTest{{str: s}})
299+
assert.Equal(t, len(results), 1)
300+
assertEqualNumber(t, n, results[0].number)
301+
})
302+
}
303+
304+
func getStringResultsFromJS(t testing.TB, exe string, tests []stringTest) []stringTest {
305+
t.Helper()
306+
tmpdir := t.TempDir()
307+
308+
type data struct {
309+
Bits [2]uint32 `json:"bits"`
310+
Str string `json:"str"`
311+
}
312+
313+
inputData := make([]data, len(tests))
314+
for i, test := range tests {
315+
inputData[i] = data{
316+
Bits: numberToUint32Array(test.number),
317+
Str: test.str,
318+
}
319+
}
320+
321+
jsonInput, err := json.Marshal(inputData)
322+
assert.NilError(t, err)
323+
324+
jsonInputPath := filepath.Join(tmpdir, "input.json")
325+
err = os.WriteFile(jsonInputPath, jsonInput, 0o644)
326+
assert.NilError(t, err)
327+
328+
script := `
329+
const fs = require('fs');
330+
331+
function fromBits(bits) {
332+
const buffer = new ArrayBuffer(8);
333+
(new Uint32Array(buffer))[0] = bits[0];
334+
(new Uint32Array(buffer))[1] = bits[1];
335+
return new Float64Array(buffer)[0];
336+
}
337+
338+
function toBits(number) {
339+
const buffer = new ArrayBuffer(8);
340+
(new Float64Array(buffer))[0] = number;
341+
return [(new Uint32Array(buffer))[0], (new Uint32Array(buffer))[1]];
342+
}
343+
344+
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
345+
346+
const output = input.map((input) => ({
347+
str: ""+fromBits(input.bits),
348+
bits: toBits(+input.str),
349+
}));
350+
351+
process.stdout.write(JSON.stringify(output));
352+
`
353+
354+
scriptPath := filepath.Join(tmpdir, "script.cjs")
355+
err = os.WriteFile(scriptPath, []byte(script), 0o644)
356+
assert.NilError(t, err)
357+
358+
execCmd := exec.Command(exe, scriptPath, jsonInputPath)
359+
360+
stdout, err := execCmd.Output()
361+
if err != nil {
362+
var exitErr *exec.ExitError
363+
if errors.As(err, &exitErr) {
364+
t.Fatalf("failed to execute: %v\n%s", err, exitErr.Stderr)
365+
} else {
366+
t.Fatalf("failed to execute: %v", err)
367+
}
368+
}
369+
370+
var outputData []data
371+
372+
err = json.Unmarshal(stdout, &outputData)
373+
assert.NilError(t, err)
374+
375+
assert.Equal(t, len(outputData), len(tests))
376+
377+
output := make([]stringTest, len(tests))
378+
for i, outputDatum := range outputData {
379+
output[i] = stringTest{
380+
number: uint32ArrayToNumber(outputDatum.Bits),
381+
str: outputDatum.Str,
382+
}
383+
}
384+
385+
return output
386+
}
387+
388+
func numberToUint32Array(n Number) [2]uint32 {
389+
bits := numberToBits(n)
390+
return [2]uint32{uint32(bits), uint32(bits >> 32)}
391+
}
392+
393+
func uint32ArrayToNumber(a [2]uint32) Number {
394+
bits := uint64(a[0]) | uint64(a[1])<<32
395+
return numberFromBits(bits)
396+
}

0 commit comments

Comments
 (0)
Please sign in to comment.