vendor tsgo
This commit is contained in:
177
tools/tsgo/internal/jsnum/jsnum.go
Normal file
177
tools/tsgo/internal/jsnum/jsnum.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package jsnum provides JS-like number handling.
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxSafeInteger Number = 1<<53 - 1
|
||||
MinSafeInteger Number = -MaxSafeInteger
|
||||
)
|
||||
|
||||
// Number represents a JS-like number.
|
||||
//
|
||||
// All operations that can be performed directly on this type
|
||||
// (e.g., conversion, arithmetic, etc.) behave as they would in JavaScript,
|
||||
// but any other operation should use this type's methods,
|
||||
// not the "math" package and conversions.
|
||||
type Number float64
|
||||
|
||||
func NaN() Number {
|
||||
return Number(math.NaN())
|
||||
}
|
||||
|
||||
func (n Number) IsNaN() bool {
|
||||
return math.IsNaN(float64(n))
|
||||
}
|
||||
|
||||
func Inf(sign int) Number {
|
||||
return Number(math.Inf(sign))
|
||||
}
|
||||
|
||||
func (n Number) IsInf() bool {
|
||||
return math.IsInf(float64(n), 0)
|
||||
}
|
||||
|
||||
func isNonFinite(x float64) bool {
|
||||
// This is equivalent to checking `math.IsNaN(x) || math.IsInf(x, 0)` in one operation.
|
||||
const mask = 0x7FF0000000000000
|
||||
return math.Float64bits(x)&mask == mask
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-touint32
|
||||
func (x Number) toUint32() uint32 {
|
||||
// The only difference between ToUint32 and ToInt32 is the interpretation of the bits.
|
||||
return uint32(x.toInt32())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-toint32
|
||||
func (n Number) toInt32() int32 {
|
||||
x := float64(n)
|
||||
|
||||
// Fast path: if the number is in the range (-2^31, 2^32), i.e. an SMI,
|
||||
// then we don't need to do any special mapping.
|
||||
if smi := int32(x); float64(smi) == x {
|
||||
return smi
|
||||
}
|
||||
|
||||
// 2. If number is not finite or number is either +0𝔽 or -0𝔽, return +0𝔽.
|
||||
// Zero was covered by the test above.
|
||||
if isNonFinite(x) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Let int be truncate(ℝ(number)).
|
||||
x = math.Trunc(x)
|
||||
// Let int32bit be int modulo 2**32.
|
||||
x = math.Mod(x, 1<<32)
|
||||
// If int32bit ≥ 2**31, return 𝔽(int32bit - 2**32); otherwise return 𝔽(int32bit).
|
||||
return int32(int64(x))
|
||||
}
|
||||
|
||||
func (x Number) toShiftCount() uint32 {
|
||||
return x.toUint32() & 31
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-signedRightShift
|
||||
func (x Number) SignedRightShift(y Number) Number {
|
||||
return Number(x.toInt32() >> y.toShiftCount())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-unsignedRightShift
|
||||
func (x Number) UnsignedRightShift(y Number) Number {
|
||||
return Number(x.toUint32() >> y.toShiftCount())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-leftShift
|
||||
func (x Number) LeftShift(y Number) Number {
|
||||
return Number(x.toInt32() << y.toShiftCount())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-bitwiseNOT
|
||||
func (x Number) BitwiseNOT() Number {
|
||||
return Number(^x.toInt32())
|
||||
}
|
||||
|
||||
// The below are implemented by https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numberbitwiseop.
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-bitwiseOR
|
||||
func (x Number) BitwiseOR(y Number) Number {
|
||||
return Number(x.toInt32() | y.toInt32())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-bitwiseAND
|
||||
func (x Number) BitwiseAND(y Number) Number {
|
||||
return Number(x.toInt32() & y.toInt32())
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-bitwiseXOR
|
||||
func (x Number) BitwiseXOR(y Number) Number {
|
||||
return Number(x.toInt32() ^ y.toInt32())
|
||||
}
|
||||
|
||||
func (x Number) trunc() Number {
|
||||
return Number(math.Trunc(float64(x)))
|
||||
}
|
||||
|
||||
func (x Number) Floor() Number {
|
||||
return Number(math.Floor(float64(x)))
|
||||
}
|
||||
|
||||
func (x Number) Abs() Number {
|
||||
return Number(math.Abs(float64(x)))
|
||||
}
|
||||
|
||||
var negativeZero = Number(math.Copysign(0, -1))
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-remainder
|
||||
func (n Number) Remainder(d Number) Number {
|
||||
switch {
|
||||
case n.IsNaN() || d.IsNaN():
|
||||
return NaN()
|
||||
case n.IsInf():
|
||||
return NaN()
|
||||
case d.IsInf():
|
||||
return n
|
||||
case d == 0:
|
||||
return NaN()
|
||||
case n == 0:
|
||||
return n
|
||||
}
|
||||
return Number(math.Mod(float64(n), float64(d)))
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-exponentiate
|
||||
func (base Number) Exponentiate(exponent Number) Number {
|
||||
switch {
|
||||
case (base == 1 || base == -1) && exponent.IsInf():
|
||||
return NaN()
|
||||
case base == 1 && exponent.IsNaN():
|
||||
return NaN()
|
||||
}
|
||||
|
||||
b := float64(base)
|
||||
e := float64(exponent)
|
||||
|
||||
// For integer base ** integer exponent where the result exceeds 53 bits,
|
||||
// math.Pow can be off by multiple ULPs vs JS engines. Use exact big.Int
|
||||
// arithmetic and IEEE 754 round-to-nearest-even conversion instead.
|
||||
// The ES spec (§6.1.6.1.3) says exponentiate returns an
|
||||
// "implementation-approximated" value, so engines are allowed to differ.
|
||||
// This won't exactly match every engine (V8's fdlibm-compiled pow can
|
||||
// round halfway ties differently), but will always be within 1 ULP
|
||||
// (unit in the last place, i.e. the least significant bit of the result).
|
||||
if b >= math.MinInt64 && b <= math.MaxInt64 && b == math.Trunc(b) &&
|
||||
e >= 0 && e <= math.MaxInt64 && e == math.Trunc(e) && !math.IsInf(e, 0) {
|
||||
magnitude := e * math.Log2(math.Abs(b))
|
||||
if magnitude > 53 && magnitude <= math.Log2(math.MaxFloat64) {
|
||||
ri := new(big.Int).Exp(big.NewInt(int64(b)), big.NewInt(int64(e)), nil)
|
||||
result, _ := new(big.Float).SetPrec(256).SetInt(ri).Float64()
|
||||
return Number(result)
|
||||
}
|
||||
}
|
||||
|
||||
return Number(math.Pow(b, e))
|
||||
}
|
||||
740
tools/tsgo/internal/jsnum/jsnum_test.go
Normal file
740
tools/tsgo/internal/jsnum/jsnum_test.go
Normal file
@@ -0,0 +1,740 @@
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/jstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func assertEqualNumber(t *testing.T, got, want Number) {
|
||||
t.Helper()
|
||||
|
||||
if got.IsNaN() || want.IsNaN() {
|
||||
assert.Equal(t, got.IsNaN(), want.IsNaN(), "got: %v, want: %v", got, want)
|
||||
} else {
|
||||
assert.Equal(t, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// assertWithinOneULP checks that got and want are either equal or differ by
|
||||
// at most 1 ULP (unit in the last place).
|
||||
func assertWithinOneULP(t *testing.T, got, want Number) {
|
||||
t.Helper()
|
||||
|
||||
if got.IsNaN() || want.IsNaN() {
|
||||
assert.Equal(t, got.IsNaN(), want.IsNaN(), "got: %v, want: %v", got, want)
|
||||
return
|
||||
}
|
||||
|
||||
if got == want {
|
||||
return
|
||||
}
|
||||
|
||||
gotBits := math.Float64bits(float64(got))
|
||||
wantBits := math.Float64bits(float64(want))
|
||||
if gotBits == wantBits {
|
||||
return
|
||||
}
|
||||
|
||||
var ulpDist uint64
|
||||
if gotBits > wantBits {
|
||||
ulpDist = gotBits - wantBits
|
||||
} else {
|
||||
ulpDist = wantBits - gotBits
|
||||
}
|
||||
|
||||
if ulpDist > 1 {
|
||||
t.Errorf("got %v (%016x), want %v (%016x) within 1 ULP (off by %d ULPs)",
|
||||
got, gotBits, want, wantBits, ulpDist)
|
||||
}
|
||||
}
|
||||
|
||||
func numberFromBits(b uint64) Number {
|
||||
return Number(math.Float64frombits(b))
|
||||
}
|
||||
|
||||
func numberToBits(n Number) uint64 {
|
||||
return math.Float64bits(float64(n))
|
||||
}
|
||||
|
||||
type binaryInput struct {
|
||||
X [2]uint32 `json:"x"`
|
||||
Y [2]uint32 `json:"y"`
|
||||
}
|
||||
|
||||
type binaryResult struct {
|
||||
X [2]uint32 `json:"x"`
|
||||
Y [2]uint32 `json:"y"`
|
||||
Result [2]uint32 `json:"result"`
|
||||
}
|
||||
|
||||
type unaryInput struct {
|
||||
X [2]uint32 `json:"x"`
|
||||
}
|
||||
|
||||
type unaryResult struct {
|
||||
X [2]uint32 `json:"x"`
|
||||
Result [2]uint32 `json:"result"`
|
||||
}
|
||||
|
||||
func numToUint32s(n Number) [2]uint32 {
|
||||
bits := numberToBits(n)
|
||||
return [2]uint32{uint32(bits), uint32(bits >> 32)}
|
||||
}
|
||||
|
||||
func uint32sToNum(a [2]uint32) Number {
|
||||
bits := uint64(a[0]) | uint64(a[1])<<32
|
||||
return numberFromBits(bits)
|
||||
}
|
||||
|
||||
// evalBinaryOp evaluates a binary JS expression on all cases using Node.js.
|
||||
// Skips the calling test if Node.js is not available.
|
||||
func evalBinaryOp(t *testing.T, op string, xs, ys []Number) []Number {
|
||||
t.Helper()
|
||||
jstest.SkipIfNoNodeJS(t)
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
inputs := make([]binaryInput, len(xs))
|
||||
for i := range xs {
|
||||
inputs[i] = binaryInput{X: numToUint32s(xs[i]), Y: numToUint32s(ys[i])}
|
||||
}
|
||||
|
||||
jsonInput, err := json.Marshal(inputs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
inputPath := filepath.Join(tmpdir, "input.json")
|
||||
err = os.WriteFile(inputPath, jsonInput, 0o644)
|
||||
assert.NilError(t, err)
|
||||
|
||||
script := fmt.Sprintf(`
|
||||
import fs from 'fs';
|
||||
|
||||
function fromBits(bits) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Uint32Array(buffer))[0] = bits[0];
|
||||
(new Uint32Array(buffer))[1] = bits[1];
|
||||
return new Float64Array(buffer)[0];
|
||||
}
|
||||
|
||||
function toBits(number) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Float64Array(buffer))[0] = number;
|
||||
return [(new Uint32Array(buffer))[0], (new Uint32Array(buffer))[1]];
|
||||
}
|
||||
|
||||
export default function(inputFile) {
|
||||
const input = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||
return input.map(({x, y}) => {
|
||||
const a = fromBits(x);
|
||||
const b = fromBits(y);
|
||||
return { x, y, result: toBits(%s) };
|
||||
});
|
||||
};
|
||||
`, op)
|
||||
|
||||
results, err := jstest.EvalNodeScript[[]binaryResult](t, script, tmpdir, inputPath)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(results), len(xs))
|
||||
|
||||
out := make([]Number, len(results))
|
||||
for i, r := range results {
|
||||
out[i] = uint32sToNum(r.Result)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// evalUnaryOp evaluates a unary JS expression on all cases using Node.js.
|
||||
// Skips the calling test if Node.js is not available.
|
||||
func evalUnaryOp(t *testing.T, op string, xs []Number) []Number {
|
||||
t.Helper()
|
||||
jstest.SkipIfNoNodeJS(t)
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
inputs := make([]unaryInput, len(xs))
|
||||
for i, x := range xs {
|
||||
inputs[i] = unaryInput{X: numToUint32s(x)}
|
||||
}
|
||||
|
||||
jsonInput, err := json.Marshal(inputs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
inputPath := filepath.Join(tmpdir, "input.json")
|
||||
err = os.WriteFile(inputPath, jsonInput, 0o644)
|
||||
assert.NilError(t, err)
|
||||
|
||||
script := fmt.Sprintf(`
|
||||
import fs from 'fs';
|
||||
|
||||
function fromBits(bits) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Uint32Array(buffer))[0] = bits[0];
|
||||
(new Uint32Array(buffer))[1] = bits[1];
|
||||
return new Float64Array(buffer)[0];
|
||||
}
|
||||
|
||||
function toBits(number) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Float64Array(buffer))[0] = number;
|
||||
return [(new Uint32Array(buffer))[0], (new Uint32Array(buffer))[1]];
|
||||
}
|
||||
|
||||
export default function(inputFile) {
|
||||
const input = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||
return input.map(({x}) => {
|
||||
const a = fromBits(x);
|
||||
return { x, result: toBits(%s) };
|
||||
});
|
||||
};
|
||||
`, op)
|
||||
|
||||
results, err := jstest.EvalNodeScript[[]unaryResult](t, script, tmpdir, inputPath)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(results), len(xs))
|
||||
|
||||
out := make([]Number, len(results))
|
||||
for i, r := range results {
|
||||
out[i] = uint32sToNum(r.Result)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var toInt32Tests = []struct {
|
||||
name string
|
||||
input Number
|
||||
want int32
|
||||
bench bool
|
||||
}{
|
||||
{"0.0", 0, 0, true},
|
||||
{"-0.0", Number(negativeZero), 0, false},
|
||||
{"NaN", NaN(), 0, true},
|
||||
{"+Inf", Inf(1), 0, true},
|
||||
{"-Inf", Inf(-1), 0, true},
|
||||
{"MaxInt32", Number(math.MaxInt32), math.MaxInt32, false},
|
||||
{"MaxInt32+1", Number(int64(math.MaxInt32) + 1), math.MinInt32, true},
|
||||
{"MinInt32", Number(math.MinInt32), math.MinInt32, false},
|
||||
{"MinInt32-1", Number(int64(math.MinInt32) - 1), math.MaxInt32, true},
|
||||
{"MIN_SAFE_INTEGER", MinSafeInteger, 1, false},
|
||||
{"MIN_SAFE_INTEGER-1", MinSafeInteger - 1, 0, false},
|
||||
{"MIN_SAFE_INTEGER+1", MinSafeInteger + 1, 2, false},
|
||||
{"MAX_SAFE_INTEGER", MaxSafeInteger, -1, true},
|
||||
{"MAX_SAFE_INTEGER-1", MaxSafeInteger - 1, -2, true},
|
||||
{"MAX_SAFE_INTEGER+1", MaxSafeInteger + 1, 0, true},
|
||||
{"-8589934590", -8589934590, 2, false},
|
||||
{"0xDEADBEEF", 0xDEADBEEF, -559038737, true},
|
||||
{"4294967808", 4294967808, 512, false},
|
||||
{"-0.4", -0.4, 0, false},
|
||||
{"SmallestNonzeroFloat64", math.SmallestNonzeroFloat64, 0, false},
|
||||
{"-SmallestNonzeroFloat64", -math.SmallestNonzeroFloat64, 0, false},
|
||||
{"MaxFloat64", math.MaxFloat64, 0, false},
|
||||
{"-MaxFloat64", -math.MaxFloat64, 0, false},
|
||||
{"Largest subnormal number", numberFromBits(0x000FFFFFFFFFFFFF), 0, false},
|
||||
{"Smallest positive normal number", numberFromBits(0x0010000000000000), 0, false},
|
||||
{"Largest normal number", math.MaxFloat64, 0, false},
|
||||
{"-Largest normal number", -math.MaxFloat64, 0, false},
|
||||
{"1.0", 1.0, 1, false},
|
||||
{"-1.0", -1.0, -1, false},
|
||||
{"1e308", 1e308, 0, false},
|
||||
{"-1e308", -1e308, 0, false},
|
||||
{"math.Pi", math.Pi, 3, false},
|
||||
{"-math.Pi", -math.Pi, -3, false},
|
||||
{"math.E", math.E, 2, false},
|
||||
{"-math.E", -math.E, -2, false},
|
||||
{"0.5", 0.5, 0, false},
|
||||
{"-0.5", -0.5, 0, false},
|
||||
{"0.49999999999999994", 0.49999999999999994, 0, false},
|
||||
{"-0.49999999999999994", -0.49999999999999994, 0, false},
|
||||
{"0.5000000000000001", 0.5000000000000001, 0, false},
|
||||
{"-0.5000000000000001", -0.5000000000000001, 0, false},
|
||||
{"2^31 + 0.5", 2147483648.5, -2147483648, false},
|
||||
{"-2^31 - 0.5", -2147483648.5, -2147483648, false},
|
||||
{"2^40", 1099511627776, 0, false},
|
||||
{"-2^40", -1099511627776, 0, false},
|
||||
{"TypeFlagsNarrowable", 536624127, 536624127, true},
|
||||
}
|
||||
|
||||
func TestToInt32(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
inputs := make([]Number, len(toInt32Tests))
|
||||
zeros := make([]Number, len(toInt32Tests))
|
||||
for i, test := range toInt32Tests {
|
||||
inputs[i] = test.input
|
||||
}
|
||||
for _, test := range toInt32Tests {
|
||||
t.Run(fmt.Sprintf("%s (%v)", test.name, float64(test.input)), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.input.toInt32()
|
||||
assert.Equal(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a | b", inputs, zeros)
|
||||
for i, test := range toInt32Tests {
|
||||
t.Run(fmt.Sprintf("%s (%v)", test.name, float64(test.input)), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, Number(test.input.toInt32()), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkToInt32(b *testing.B) {
|
||||
for _, test := range toInt32Tests {
|
||||
if !test.bench {
|
||||
continue
|
||||
}
|
||||
|
||||
b.Run(fmt.Sprintf("%s (%v)", test.name, float64(test.input)), func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
test.input.toInt32()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitwiseNOT(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, want Number
|
||||
}{
|
||||
// Original pairs: ~(-2147483649) == ~(2147483647)
|
||||
{Number(-2147483649), -2147483648},
|
||||
{Number(2147483647), -2147483648},
|
||||
// Original pairs: ~(-4294967296) == ~(0)
|
||||
{Number(-4294967296), -1},
|
||||
{0, -1},
|
||||
// Original pairs: ~(2147483648) == ~(-2147483648)
|
||||
{Number(2147483648), 2147483647},
|
||||
{Number(-2147483648), 2147483647},
|
||||
// Original pairs: ~(4294967296) == ~(0)
|
||||
{Number(4294967296), -1},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("~%v", test.x), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.BitwiseNOT()
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalUnaryOp(t, "~a", xs)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("~%v", test.x), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.BitwiseNOT(), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBitwiseAND(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{0, 0, 0},
|
||||
{0, 1, 0},
|
||||
{1, 0, 0},
|
||||
{1, 1, 1},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v & %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.BitwiseAND(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a & b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v & %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.BitwiseAND(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBitwiseOR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{0, 0, 0},
|
||||
{0, 1, 1},
|
||||
{1, 0, 1},
|
||||
{1, 1, 1},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v | %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.BitwiseOR(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a | b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v | %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.BitwiseOR(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBitwiseXOR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{0, 0, 0},
|
||||
{0, 1, 1},
|
||||
{1, 0, 1},
|
||||
{1, 1, 0},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v ^ %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.BitwiseXOR(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a ^ b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v ^ %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.BitwiseXOR(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSignedRightShift(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{1, 0, 1},
|
||||
{1, 1, 0},
|
||||
{1, 2, 0},
|
||||
{1, 31, 0},
|
||||
{1, 32, 1},
|
||||
{-4, 0, -4},
|
||||
{-4, 1, -2},
|
||||
{-4, 2, -1},
|
||||
{-4, 3, -1},
|
||||
{-4, 4, -1},
|
||||
{-4, 31, -1},
|
||||
{-4, 32, -4},
|
||||
{-4, 33, -2},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v >> %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.SignedRightShift(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a >> b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v >> %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.SignedRightShift(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnsignedRightShift(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{1, 0, 1},
|
||||
{1, 1, 0},
|
||||
{1, 2, 0},
|
||||
{1, 31, 0},
|
||||
{1, 32, 1},
|
||||
{-4, 0, 4294967292},
|
||||
{-4, 1, 2147483646},
|
||||
{-4, 2, 1073741823},
|
||||
{-4, 3, 536870911},
|
||||
{-4, 4, 268435455},
|
||||
{-4, 31, 1},
|
||||
{-4, 32, 4294967292},
|
||||
{-4, 33, 2147483646},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v >>> %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.UnsignedRightShift(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a >>> b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v >>> %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.UnsignedRightShift(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeftShift(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{1, 0, 1},
|
||||
{1, 1, 2},
|
||||
{1, 2, 4},
|
||||
{1, 31, -2147483648},
|
||||
{1, 32, 1},
|
||||
{-4, 0, -4},
|
||||
{-4, 1, -8},
|
||||
{-4, 2, -16},
|
||||
{-4, 3, -32},
|
||||
{-4, 31, 0},
|
||||
{-4, 32, -4},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v << %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.LeftShift(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a << b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v << %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.LeftShift(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemainder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{NaN(), 1, NaN()},
|
||||
{1, NaN(), NaN()},
|
||||
{Inf(1), 1, NaN()},
|
||||
{Inf(-1), 1, NaN()},
|
||||
{123, Inf(1), 123},
|
||||
{123, Inf(-1), 123},
|
||||
{123, 0, NaN()},
|
||||
{123, negativeZero, NaN()},
|
||||
{0, 123, 0},
|
||||
{negativeZero, 123, negativeZero},
|
||||
// Normal cases
|
||||
{10, 3, 1},
|
||||
{-10, 3, -1},
|
||||
{10, -3, 1},
|
||||
{-10, -3, -1},
|
||||
{5.5, 2, 1.5},
|
||||
{-5.5, 2, -1.5},
|
||||
{1, 0.5, 0},
|
||||
{-1, 0.5, negativeZero},
|
||||
{1.5, 1, 0.5},
|
||||
{-1.5, 1, -0.5},
|
||||
// Edge cases that prove the bug in the manual formula:
|
||||
// The manual formula n - d*(n/d).trunc() accumulates floating-point
|
||||
// rounding errors that IEEE 754 fmod (math.Mod) avoids.
|
||||
{7, 0.1, Number(math.Mod(7, 0.1))},
|
||||
{7, 0.2, Number(math.Mod(7, 0.2))},
|
||||
{7, 0.3, Number(math.Mod(7, 0.3))},
|
||||
{100, 0.3, Number(math.Mod(100, 0.3))},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v %% %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.Remainder(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a % b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v %% %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, test.x.Remainder(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExponentiate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
x, y, want Number
|
||||
}{
|
||||
{2, 3, 8},
|
||||
{Inf(1), 3, Inf(1)},
|
||||
{Inf(1), -5, 0},
|
||||
{Inf(-1), 3, Inf(-1)},
|
||||
{Inf(-1), 4, Inf(1)},
|
||||
{Inf(-1), -3, negativeZero},
|
||||
{Inf(-1), -4, 0},
|
||||
{0, 3, 0},
|
||||
{0, -10, Inf(1)},
|
||||
{negativeZero, 3, negativeZero},
|
||||
{negativeZero, 4, 0},
|
||||
{negativeZero, -3, Inf(-1)},
|
||||
{negativeZero, -4, Inf(1)},
|
||||
{3, Inf(1), Inf(1)},
|
||||
{-3, Inf(1), Inf(1)},
|
||||
{3, Inf(-1), 0},
|
||||
{-3, Inf(-1), 0},
|
||||
{NaN(), 3, NaN()},
|
||||
{1, Inf(1), NaN()},
|
||||
{1, Inf(-1), NaN()},
|
||||
{-1, Inf(1), NaN()},
|
||||
{-1, Inf(-1), NaN()},
|
||||
{1, NaN(), NaN()},
|
||||
// Cases where math.Pow diverges from V8 by >1 ULP.
|
||||
// Expected values are the correctly-rounded IEEE 754 results
|
||||
// computed via exact integer arithmetic (big.Int).
|
||||
// Cross-engine testing (V8, SpiderMonkey, QuickJS, XS via jsvu)
|
||||
// confirmed these match the majority of JS engines.
|
||||
{10, 308, numberFromBits(0x7fe1ccf385ebc8a0)},
|
||||
{5, 210, numberFromBits(0x5e68557f31326bbb)},
|
||||
{10, 200, numberFromBits(0x6974e718d7d7625a)},
|
||||
}
|
||||
|
||||
xs := make([]Number, len(tests))
|
||||
ys := make([]Number, len(tests))
|
||||
for i, test := range tests {
|
||||
xs[i] = test.x
|
||||
ys[i] = test.y
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v ** %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := test.x.Exponentiate(test.y)
|
||||
assertEqualNumber(t, got, test.want)
|
||||
})
|
||||
}
|
||||
|
||||
// The ES spec says exponentiate is "implementation-approximated".
|
||||
// Different JS engines (V8, SpiderMonkey, JSC) use different pow
|
||||
// implementations that can differ by 1 ULP. Allow that tolerance.
|
||||
t.Run("Node", func(t *testing.T) {
|
||||
jsResults := evalBinaryOp(t, "a ** b", xs, ys)
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v ** %v", test.x, test.y), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertWithinOneULP(t, test.x.Exponentiate(test.y), jsResults[i])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkExponentiate(b *testing.B) {
|
||||
cases := []struct {
|
||||
name string
|
||||
base Number
|
||||
exponent Number
|
||||
}{
|
||||
{"2**10_exact", 2, 10}, // small, fits in 53 bits → math.Pow
|
||||
{"2**53_exact", 2, 53}, // boundary, exactly 53 bits → math.Pow
|
||||
{"10**20_bigint", 10, 20}, // exceeds 53 bits → big.Int
|
||||
{"10**308_bigint", 10, 308}, // large exponent → big.Int
|
||||
{"3**34_bigint", 3, 34}, // medium → big.Int
|
||||
{"0.5**-0.5_mathpow", 0.5, -0.5}, // non-integer → math.Pow
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
b.Run(c.name, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
c.base.Exponentiate(c.exponent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
66
tools/tsgo/internal/jsnum/pseudobigint.go
Normal file
66
tools/tsgo/internal/jsnum/pseudobigint.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PseudoBigInt represents a JS-like bigint. The zero state of the struct represents the value 0.
|
||||
type PseudoBigInt struct {
|
||||
Negative bool // true if the value is a non-zero negative number.
|
||||
Base10Value string // The absolute value in base 10 with no leading zeros. The value zero is represented as an empty string.
|
||||
}
|
||||
|
||||
func NewPseudoBigInt(value string, negative bool) PseudoBigInt {
|
||||
value = strings.TrimLeft(value, "0")
|
||||
return PseudoBigInt{Negative: negative && len(value) != 0, Base10Value: value}
|
||||
}
|
||||
|
||||
func (value PseudoBigInt) String() string {
|
||||
if len(value.Base10Value) == 0 {
|
||||
return "0"
|
||||
}
|
||||
if value.Negative {
|
||||
return "-" + value.Base10Value
|
||||
}
|
||||
return value.Base10Value
|
||||
}
|
||||
|
||||
func (value PseudoBigInt) Sign() int {
|
||||
if len(value.Base10Value) == 0 {
|
||||
return 0
|
||||
}
|
||||
if value.Negative {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func ParseValidBigInt(text string) PseudoBigInt {
|
||||
text, negative := strings.CutPrefix(text, "-")
|
||||
return NewPseudoBigInt(ParsePseudoBigInt(text), negative)
|
||||
}
|
||||
|
||||
func ParsePseudoBigInt(stringValue string) string {
|
||||
stringValue = strings.TrimSuffix(stringValue, "n")
|
||||
var b1 byte
|
||||
if len(stringValue) > 1 {
|
||||
b1 = stringValue[1]
|
||||
}
|
||||
switch b1 {
|
||||
case 'b', 'B', 'o', 'O', 'x', 'X':
|
||||
// Not decimal.
|
||||
default:
|
||||
stringValue = strings.TrimLeft(stringValue, "0")
|
||||
if stringValue == "" {
|
||||
return "0"
|
||||
}
|
||||
return stringValue
|
||||
}
|
||||
bi, ok := new(big.Int).SetString(stringValue, 0)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Failed to parse big int: %q", stringValue))
|
||||
}
|
||||
return bi.String() // !!!
|
||||
}
|
||||
77
tools/tsgo/internal/jsnum/pseudobigint_test.go
Normal file
77
tools/tsgo/internal/jsnum/pseudobigint_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestParsePseudoBigInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var testNumbers []Number
|
||||
for i := range int64(1e3) {
|
||||
testNumbers = append(testNumbers, Number(i))
|
||||
}
|
||||
for bits := range 53 {
|
||||
testNumbers = append(testNumbers, Number(int64(1<<bits)), Number(int64(1<<bits)-1))
|
||||
}
|
||||
|
||||
t.Run("strip base-10 strings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, testNumber := range testNumbers {
|
||||
for leadingZeros := range 10 {
|
||||
assert.Equal(
|
||||
t,
|
||||
ParsePseudoBigInt(strings.Repeat("0", leadingZeros)+testNumber.String()+"n"),
|
||||
testNumber.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse non-decimal bases (small numbers)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type tc struct {
|
||||
lit string
|
||||
out string
|
||||
}
|
||||
cases := []tc{
|
||||
// binary
|
||||
{lit: "0b0n", out: "0"},
|
||||
{lit: "0b1n", out: "1"},
|
||||
{lit: "0b1010n", out: "10"},
|
||||
{lit: "0b1010_0101n", out: "165"},
|
||||
{lit: "0B1101n", out: "13"}, // uppercase prefix
|
||||
|
||||
// octal
|
||||
{lit: "0o0n", out: "0"},
|
||||
{lit: "0o7n", out: "7"},
|
||||
{lit: "0o755n", out: "493"},
|
||||
{lit: "0o7_5_5n", out: "493"},
|
||||
{lit: "0O12n", out: "10"}, // uppercase prefix
|
||||
|
||||
// hex
|
||||
{lit: "0x0n", out: "0"},
|
||||
{lit: "0xFn", out: "15"},
|
||||
{lit: "0xFFn", out: "255"},
|
||||
{lit: "0xF_Fn", out: "255"},
|
||||
{lit: "0X1Fn", out: "31"}, // uppercase prefix
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
got := ParsePseudoBigInt(c.lit)
|
||||
assert.Equal(t, got, c.out, "literal: %q", c.lit)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("can parse large literals", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, ParsePseudoBigInt("123456789012345678901234567890n"), "123456789012345678901234567890")
|
||||
assert.Equal(t, ParsePseudoBigInt("0b1100011101110100100001111111101101100001101110011111000001110111001001110001111110000101011010010n"), "123456789012345678901234567890")
|
||||
assert.Equal(t, ParsePseudoBigInt("0o143564417755415637016711617605322n"), "123456789012345678901234567890")
|
||||
assert.Equal(t, ParsePseudoBigInt("0x18ee90ff6c373e0ee4e3f0ad2n"), "123456789012345678901234567890")
|
||||
})
|
||||
}
|
||||
164
tools/tsgo/internal/jsnum/ryu_test.go
Normal file
164
tools/tsgo/internal/jsnum/ryu_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package jsnum
|
||||
|
||||
// Copyright 2018 Ulf Adams
|
||||
//
|
||||
// The contents of this file may be used under the terms of the Apache License,
|
||||
// Version 2.0.
|
||||
//
|
||||
// (See accompanying file LICENSE-Apache or copy at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0)
|
||||
//
|
||||
// Alternatively, the contents of this file may be used under the terms of
|
||||
// the Boost Software License, Version 1.0.
|
||||
// (See accompanying file LICENSE-Boost or copy at
|
||||
// https://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, this software
|
||||
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied.
|
||||
|
||||
// Copied from https://github.com/ulfjack/ryu/blob/1264a946ba66eab320e927bfd2362e0c8580c42f/ryu/tests/d2s_test.cc
|
||||
// Modified to fit Number::toString's output.
|
||||
|
||||
func ieeeParts2Double(sign bool, ieeeExponent uint32, ieeeMantissa uint64) Number {
|
||||
if ieeeExponent > 2047 {
|
||||
panic("ieeeExponent > 2047")
|
||||
}
|
||||
if ieeeMantissa > maxMantissa {
|
||||
panic("ieeeMantissa > maxMantissa")
|
||||
}
|
||||
signBit := uint64(0)
|
||||
if sign {
|
||||
signBit = 1
|
||||
}
|
||||
return numberFromBits((signBit << 63) | (uint64(ieeeExponent) << 52) | ieeeMantissa)
|
||||
}
|
||||
|
||||
const maxMantissa = (1 << 53) - 1
|
||||
|
||||
var ryuTests = []stringTest{
|
||||
{2.2250738585072014e-308, "2.2250738585072014e-308"},
|
||||
{numberFromBits(0x7fefffffffffffff), "1.7976931348623157e+308"},
|
||||
{numberFromBits(1), "5e-324"},
|
||||
{2.98023223876953125e-8, "2.9802322387695312e-8"},
|
||||
{-2.109808898695963e16, "-21098088986959630"},
|
||||
{4.940656e-318, "4.940656e-318"},
|
||||
{1.18575755e-316, "1.18575755e-316"},
|
||||
{2.989102097996e-312, "2.989102097996e-312"},
|
||||
{9.0608011534336e15, "9060801153433600"},
|
||||
{4.708356024711512e18, "4708356024711512000"},
|
||||
{9.409340012568248e18, "9409340012568248000"},
|
||||
{1.2345678, "1.2345678"},
|
||||
{numberFromBits(0x4830F0CF064DD592), "5.764607523034235e+39"},
|
||||
{numberFromBits(0x4840F0CF064DD592), "1.152921504606847e+40"},
|
||||
{numberFromBits(0x4850F0CF064DD592), "2.305843009213694e+40"},
|
||||
{1.2, "1.2"},
|
||||
{1.23, "1.23"},
|
||||
{1.234, "1.234"},
|
||||
{1.2345, "1.2345"},
|
||||
{1.23456, "1.23456"},
|
||||
{1.234567, "1.234567"},
|
||||
{1.2345678, "1.2345678"},
|
||||
{1.23456789, "1.23456789"},
|
||||
{1.234567895, "1.234567895"},
|
||||
{1.2345678901, "1.2345678901"},
|
||||
{1.23456789012, "1.23456789012"},
|
||||
{1.234567890123, "1.234567890123"},
|
||||
{1.2345678901234, "1.2345678901234"},
|
||||
{1.23456789012345, "1.23456789012345"},
|
||||
{1.234567890123456, "1.234567890123456"},
|
||||
{1.2345678901234567, "1.2345678901234567"},
|
||||
{4.294967294, "4.294967294"},
|
||||
{4.294967295, "4.294967295"},
|
||||
{4.294967296, "4.294967296"},
|
||||
{4.294967297, "4.294967297"},
|
||||
{4.294967298, "4.294967298"},
|
||||
{ieeeParts2Double(false, 4, 0), "1.7800590868057611e-307"},
|
||||
{ieeeParts2Double(false, 6, maxMantissa), "2.8480945388892175e-306"},
|
||||
{ieeeParts2Double(false, 41, 0), "2.446494580089078e-296"},
|
||||
{ieeeParts2Double(false, 40, maxMantissa), "4.8929891601781557e-296"},
|
||||
{ieeeParts2Double(false, 1077, 0), "18014398509481984"},
|
||||
{ieeeParts2Double(false, 1076, maxMantissa), "36028797018963964"},
|
||||
{ieeeParts2Double(false, 307, 0), "2.900835519859558e-216"},
|
||||
{ieeeParts2Double(false, 306, maxMantissa), "5.801671039719115e-216"},
|
||||
{ieeeParts2Double(false, 934, 0x000FA7161A4D6E0C), "3.196104012172126e-27"},
|
||||
{9007199254740991.0, "9007199254740991"},
|
||||
{9007199254740992.0, "9007199254740992"},
|
||||
{1.0e+0, "1"},
|
||||
{1.2e+1, "12"},
|
||||
{1.23e+2, "123"},
|
||||
{1.234e+3, "1234"},
|
||||
{1.2345e+4, "12345"},
|
||||
{1.23456e+5, "123456"},
|
||||
{1.234567e+6, "1234567"},
|
||||
{1.2345678e+7, "12345678"},
|
||||
{1.23456789e+8, "123456789"},
|
||||
{1.23456789e+9, "1234567890"},
|
||||
{1.234567895e+9, "1234567895"},
|
||||
{1.2345678901e+10, "12345678901"},
|
||||
{1.23456789012e+11, "123456789012"},
|
||||
{1.234567890123e+12, "1234567890123"},
|
||||
{1.2345678901234e+13, "12345678901234"},
|
||||
{1.23456789012345e+14, "123456789012345"},
|
||||
{1.234567890123456e+15, "1234567890123456"},
|
||||
{1.0e+0, "1"},
|
||||
{1.0e+1, "10"},
|
||||
{1.0e+2, "100"},
|
||||
{1.0e+3, "1000"},
|
||||
{1.0e+4, "10000"},
|
||||
{1.0e+5, "100000"},
|
||||
{1.0e+6, "1000000"},
|
||||
{1.0e+7, "10000000"},
|
||||
{1.0e+8, "100000000"},
|
||||
{1.0e+9, "1000000000"},
|
||||
{1.0e+10, "10000000000"},
|
||||
{1.0e+11, "100000000000"},
|
||||
{1.0e+12, "1000000000000"},
|
||||
{1.0e+13, "10000000000000"},
|
||||
{1.0e+14, "100000000000000"},
|
||||
{1.0e+15, "1000000000000000"},
|
||||
{1000000000000001, "1000000000000001"},
|
||||
{1000000000000010, "1000000000000010"},
|
||||
{1000000000000100, "1000000000000100"},
|
||||
{1000000000001000, "1000000000001000"},
|
||||
{1000000000010000, "1000000000010000"},
|
||||
{1000000000100000, "1000000000100000"},
|
||||
{1000000001000000, "1000000001000000"},
|
||||
{1000000010000000, "1000000010000000"},
|
||||
{1000000100000000, "1000000100000000"},
|
||||
{1000001000000000, "1000001000000000"},
|
||||
{1000010000000000, "1000010000000000"},
|
||||
{1000100000000000, "1000100000000000"},
|
||||
{1001000000000000, "1001000000000000"},
|
||||
{1010000000000000, "1010000000000000"},
|
||||
{1100000000000000, "1100000000000000"},
|
||||
{8.0, "8"},
|
||||
{64.0, "64"},
|
||||
{512.0, "512"},
|
||||
{8192.0, "8192"},
|
||||
{65536.0, "65536"},
|
||||
{524288.0, "524288"},
|
||||
{8388608.0, "8388608"},
|
||||
{67108864.0, "67108864"},
|
||||
{536870912.0, "536870912"},
|
||||
{8589934592.0, "8589934592"},
|
||||
{68719476736.0, "68719476736"},
|
||||
{549755813888.0, "549755813888"},
|
||||
{8796093022208.0, "8796093022208"},
|
||||
{70368744177664.0, "70368744177664"},
|
||||
{562949953421312.0, "562949953421312"},
|
||||
{9007199254740992.0, "9007199254740992"},
|
||||
{8.0e+3, "8000"},
|
||||
{64.0e+3, "64000"},
|
||||
{512.0e+3, "512000"},
|
||||
{8192.0e+3, "8192000"},
|
||||
{65536.0e+3, "65536000"},
|
||||
{524288.0e+3, "524288000"},
|
||||
{8388608.0e+3, "8388608000"},
|
||||
{67108864.0e+3, "67108864000"},
|
||||
{536870912.0e+3, "536870912000"},
|
||||
{8589934592.0e+3, "8589934592000"},
|
||||
{68719476736.0e+3, "68719476736000"},
|
||||
{549755813888.0e+3, "549755813888000"},
|
||||
{8796093022208.0e+3, "8796093022208000"},
|
||||
}
|
||||
341
tools/tsgo/internal/jsnum/string.go
Normal file
341
tools/tsgo/internal/jsnum/string.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-tostring
|
||||
func (n Number) String() string {
|
||||
switch {
|
||||
case n.IsNaN():
|
||||
return "NaN"
|
||||
case n.IsInf():
|
||||
if n < 0 {
|
||||
return "-Infinity"
|
||||
}
|
||||
return "Infinity"
|
||||
}
|
||||
|
||||
// Fast path: for safe integers, directly convert to string.
|
||||
if MinSafeInteger <= n && n <= MaxSafeInteger {
|
||||
if i := int64(n); float64(i) == float64(n) {
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, the Go json package handles this correctly.
|
||||
b, _ := json.Marshal(float64(n))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/abstract-operations.html#sec-stringtonumber
|
||||
func FromString(s string) Number {
|
||||
// Implementing StringToNumber exactly as written in the spec involves
|
||||
// writing a parser, along with the conversion of the parsed AST into the
|
||||
// actual value.
|
||||
//
|
||||
// We've already implemented a number parser in the scanner, but we can't
|
||||
// import it here. We also do not have the conversion implemented since we
|
||||
// previously just wrote `+literal` and let the runtime handle it.
|
||||
//
|
||||
// The strategy below is to instead break the number apart and fix it up
|
||||
// such that Go's own parsing functionality can handle it. This won't be
|
||||
// the fastest method, but it saves us from writing the full parser and
|
||||
// conversion logic.
|
||||
|
||||
s = strings.TrimFunc(s, isStrWhiteSpace)
|
||||
|
||||
switch s {
|
||||
case "":
|
||||
return 0
|
||||
case "Infinity", "+Infinity":
|
||||
return Inf(1)
|
||||
case "-Infinity":
|
||||
return Inf(-1)
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if !isNumberRune(r) {
|
||||
return NaN()
|
||||
}
|
||||
}
|
||||
|
||||
if n, ok := tryParseInt(s); ok {
|
||||
return n
|
||||
}
|
||||
|
||||
// Cut this off first so we can ensure -0 is returned as -0.
|
||||
s, negative := strings.CutPrefix(s, "-")
|
||||
|
||||
if !negative {
|
||||
s, _ = strings.CutPrefix(s, "+")
|
||||
}
|
||||
|
||||
if first, _ := utf8.DecodeRuneInString(s); !stringutil.IsDigit(first) && first != '.' {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
f := parseFloatString(s)
|
||||
if math.IsNaN(f) {
|
||||
return NaN()
|
||||
}
|
||||
|
||||
sign := 1.0
|
||||
if negative {
|
||||
sign = -1.0
|
||||
}
|
||||
return Number(math.Copysign(f, sign))
|
||||
}
|
||||
|
||||
func isStrWhiteSpace(r rune) bool {
|
||||
// This is different than stringutil.IsWhiteSpaceLike.
|
||||
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-LineTerminator
|
||||
// https://tc39.es/ecma262/2024/multipage/ecmascript-language-lexical-grammar.html#prod-WhiteSpace
|
||||
|
||||
switch r {
|
||||
// LineTerminator
|
||||
case '\n', '\r', 0x2028, 0x2029:
|
||||
return true
|
||||
// WhiteSpace
|
||||
case '\t', '\v', '\f', 0xFEFF:
|
||||
return true
|
||||
}
|
||||
|
||||
// WhiteSpace
|
||||
return unicode.Is(unicode.Zs, r)
|
||||
}
|
||||
|
||||
var errUnknownPrefix = errors.New("unknown number prefix")
|
||||
|
||||
func tryParseInt(s string) (Number, bool) {
|
||||
var i int64
|
||||
var err error
|
||||
var hasIntResult bool
|
||||
|
||||
if len(s) > 2 {
|
||||
prefix, rest := s[:2], s[2:]
|
||||
switch prefix {
|
||||
case "0b", "0B":
|
||||
if !isAllBinaryDigits(rest) {
|
||||
return NaN(), true
|
||||
}
|
||||
i, err = strconv.ParseInt(rest, 2, 64)
|
||||
hasIntResult = true
|
||||
case "0o", "0O":
|
||||
if !isAllOctalDigits(rest) {
|
||||
return NaN(), true
|
||||
}
|
||||
i, err = strconv.ParseInt(rest, 8, 64)
|
||||
hasIntResult = true
|
||||
case "0x", "0X":
|
||||
if !isAllHexDigits(rest) {
|
||||
return NaN(), true
|
||||
}
|
||||
i, err = strconv.ParseInt(rest, 16, 64)
|
||||
hasIntResult = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasIntResult {
|
||||
// StringToNumber does not parse leading zeros as octal.
|
||||
s = trimLeadingZeros(s)
|
||||
if !isAllDigits(s) {
|
||||
return 0, false
|
||||
}
|
||||
i, err = strconv.ParseInt(s, 10, 64)
|
||||
hasIntResult = true
|
||||
}
|
||||
|
||||
if hasIntResult && err == nil {
|
||||
return Number(i), true
|
||||
}
|
||||
|
||||
// Using this to parse large integers.
|
||||
bi, ok := new(big.Int).SetString(s, 0)
|
||||
if !ok {
|
||||
return NaN(), true
|
||||
}
|
||||
|
||||
f, _ := bi.Float64()
|
||||
return Number(f), true
|
||||
}
|
||||
|
||||
func parseFloatString(s string) float64 {
|
||||
var hasDot, hasExp bool
|
||||
|
||||
// <a>
|
||||
// <a>.<b>
|
||||
// <a>.<b>e<c>
|
||||
// <a>e<c>
|
||||
var a, b, c, rest string
|
||||
|
||||
a, rest, hasDot = strings.Cut(s, ".")
|
||||
if hasDot {
|
||||
// <a>.<b>
|
||||
// <a>.<b>e<c>
|
||||
b, c, hasExp = cutAny(rest, "eE")
|
||||
} else {
|
||||
// <a>
|
||||
// <a>e<c>
|
||||
a, c, hasExp = cutAny(s, "eE")
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(a) + len(b) + len(c) + 3)
|
||||
|
||||
if a == "" {
|
||||
if hasDot && b == "" {
|
||||
return math.NaN()
|
||||
}
|
||||
if hasExp && c == "" {
|
||||
return math.NaN()
|
||||
}
|
||||
sb.WriteString("0")
|
||||
} else {
|
||||
a = trimLeadingZeros(a)
|
||||
if !isAllDigits(a) {
|
||||
return math.NaN()
|
||||
}
|
||||
sb.WriteString(a)
|
||||
}
|
||||
|
||||
if hasDot {
|
||||
sb.WriteString(".")
|
||||
if b == "" {
|
||||
sb.WriteString("0")
|
||||
} else {
|
||||
b = trimTrailingZeros(b)
|
||||
if !isAllDigits(b) {
|
||||
return math.NaN()
|
||||
}
|
||||
sb.WriteString(b)
|
||||
}
|
||||
}
|
||||
|
||||
if hasExp {
|
||||
sb.WriteString("e")
|
||||
|
||||
c, negative := strings.CutPrefix(c, "-")
|
||||
if negative {
|
||||
sb.WriteString("-")
|
||||
} else {
|
||||
c, _ = strings.CutPrefix(c, "+")
|
||||
}
|
||||
c = trimLeadingZeros(c)
|
||||
if !isAllDigits(c) {
|
||||
return math.NaN()
|
||||
}
|
||||
sb.WriteString(c)
|
||||
}
|
||||
|
||||
return stringToFloat64(sb.String())
|
||||
}
|
||||
|
||||
func cutAny(s string, cutset string) (before, after string, found bool) {
|
||||
if i := strings.IndexAny(s, cutset); i >= 0 {
|
||||
before = s[:i]
|
||||
afterAndFound := s[i:]
|
||||
_, size := utf8.DecodeRuneInString(afterAndFound)
|
||||
after = afterAndFound[size:]
|
||||
return before, after, true
|
||||
}
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
func trimLeadingZeros(s string) string {
|
||||
if strings.HasPrefix(s, "0") {
|
||||
s = strings.TrimLeft(s, "0")
|
||||
if s == "" {
|
||||
return "0"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func trimTrailingZeros(s string) string {
|
||||
if strings.HasSuffix(s, "0") {
|
||||
s = strings.TrimRight(s, "0")
|
||||
if s == "" {
|
||||
return "0"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func stringToFloat64(s string) float64 {
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f
|
||||
} else {
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
func isAllDigits(s string) bool {
|
||||
for _, r := range s {
|
||||
if !stringutil.IsDigit(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isAllBinaryDigits(s string) bool {
|
||||
for _, r := range s {
|
||||
if r != '0' && r != '1' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isAllOctalDigits(s string) bool {
|
||||
for _, r := range s {
|
||||
if !stringutil.IsOctalDigit(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isAllHexDigits(s string) bool {
|
||||
for _, r := range s {
|
||||
if !stringutil.IsHexDigit(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isNumberRune(r rune) bool {
|
||||
if stringutil.IsDigit(r) {
|
||||
return true
|
||||
}
|
||||
|
||||
if 'a' <= r && r <= 'f' {
|
||||
return true
|
||||
}
|
||||
|
||||
if 'A' <= r && r <= 'F' {
|
||||
return true
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '.', '-', '+', 'x', 'X', 'o', 'O':
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
366
tools/tsgo/internal/jsnum/string_test.go
Normal file
366
tools/tsgo/internal/jsnum/string_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package jsnum
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/jstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type stringTest struct {
|
||||
number Number
|
||||
str string
|
||||
}
|
||||
|
||||
var stringTests = slices.Concat([]stringTest{
|
||||
{NaN(), "NaN"},
|
||||
{Inf(1), "Infinity"},
|
||||
{Inf(-1), "-Infinity"},
|
||||
{0, "0"},
|
||||
{negativeZero, "0"},
|
||||
{1, "1"},
|
||||
{-1, "-1"},
|
||||
{0.3, "0.3"},
|
||||
{-0.3, "-0.3"},
|
||||
{1.5, "1.5"},
|
||||
{-1.5, "-1.5"},
|
||||
{1e308, "1e+308"},
|
||||
{-1e308, "-1e+308"},
|
||||
{math.Pi, "3.141592653589793"},
|
||||
{-math.Pi, "-3.141592653589793"},
|
||||
{MaxSafeInteger, "9007199254740991"},
|
||||
{MinSafeInteger, "-9007199254740991"},
|
||||
{numberFromBits(0x000FFFFFFFFFFFFF), "2.225073858507201e-308"},
|
||||
{numberFromBits(0x0010000000000000), "2.2250738585072014e-308"},
|
||||
{1234567.8, "1234567.8"},
|
||||
{19686109595169230000, "19686109595169230000"},
|
||||
{123.456, "123.456"},
|
||||
{-123.456, "-123.456"},
|
||||
{444123, "444123"},
|
||||
{-444123, "-444123"},
|
||||
{444123.789123456789875436, "444123.7891234568"},
|
||||
{-444123.78963636363636363636, "-444123.7896363636"},
|
||||
{1e21, "1e+21"},
|
||||
{1e20, "100000000000000000000"},
|
||||
}, ryuTests)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range stringTests {
|
||||
fInput := float64(test.number)
|
||||
|
||||
t.Run(fmt.Sprintf("%v", fInput), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, test.number.String(), test.str)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var fromStringTests = []stringTest{
|
||||
{NaN(), " NaN"},
|
||||
{Inf(1), "Infinity "},
|
||||
{Inf(-1), " -Infinity"},
|
||||
{1, "1."},
|
||||
{1, "1.0 "},
|
||||
{1, "+1"},
|
||||
{1, "+1."},
|
||||
{1, "+1.0"},
|
||||
{NaN(), "whoops"},
|
||||
{0, ""},
|
||||
{0, "0"},
|
||||
{0, "0."},
|
||||
{0, "0.0"},
|
||||
{0, "0.0000"},
|
||||
{0, ".0000"},
|
||||
{negativeZero, "-0"},
|
||||
{negativeZero, "-0."},
|
||||
{negativeZero, "-0.0"},
|
||||
{negativeZero, "-.0"},
|
||||
{NaN(), "."},
|
||||
{NaN(), "e"},
|
||||
{NaN(), ".e"},
|
||||
{NaN(), "+"},
|
||||
{0, "0X0"},
|
||||
{NaN(), "e0"},
|
||||
{NaN(), "E0"},
|
||||
{NaN(), "1e"},
|
||||
{NaN(), "1e+"},
|
||||
{NaN(), "1e-"},
|
||||
{1, "1e+0"},
|
||||
{NaN(), "++0"},
|
||||
{NaN(), "0_0"},
|
||||
{Inf(1), "1e1000"},
|
||||
{Inf(-1), "-1e1000"},
|
||||
{0, ".0e0"},
|
||||
{NaN(), "0e++0"},
|
||||
{10, "0XA"},
|
||||
{0b1010, "0b1010"},
|
||||
{0b1010, "0B1010"},
|
||||
{0o12, "0o12"},
|
||||
{0o12, "0O12"},
|
||||
{0x123456789abcdef0, "0x123456789abcdef0"},
|
||||
{0x123456789abcdef0, "0X123456789ABCDEF0"},
|
||||
{18446744073709552000, "0X10000000000000000"},
|
||||
{18446744073709597000, "0X1000000000000A801"},
|
||||
{NaN(), "0B0.0"},
|
||||
{1.231235345083403e+91, "12312353450834030486384068034683603046834603806830644850340602384608368034634603680348603864"},
|
||||
{NaN(), "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8OOOOOOOOOOOOOOOOOOO"},
|
||||
{Inf(1), "+Infinity"},
|
||||
{1234.56, " \t1234.56 "},
|
||||
{NaN(), "\u200b"},
|
||||
{0, " "},
|
||||
{0, "\n"},
|
||||
{0, "\r"},
|
||||
{0, "\r\n"},
|
||||
{0, "\u2028"},
|
||||
{0, "\u2029"},
|
||||
{0, "\t"},
|
||||
{0, "\v"},
|
||||
{0, "\f"},
|
||||
{0, "\uFEFF"},
|
||||
{0, "\u00A0"},
|
||||
{10000000000000000000, "010000000000000000000"},
|
||||
{NaN(), "0x1.fffffffffffffp1023"}, // Make sure Go's extended float syntax doesn't work.
|
||||
{NaN(), "0X_1FFFP-16"},
|
||||
{NaN(), "1_000"}, // NumberToString doesn't handle underscores.
|
||||
{0, "0x0"},
|
||||
{0, "0X0"},
|
||||
{NaN(), "0xOOPS"},
|
||||
{0xABCDEF, "0xABCDEF"},
|
||||
{0xABCDEF, "0xABCDEF"},
|
||||
{0, "0o0"},
|
||||
{0, "0O0"},
|
||||
{NaN(), "0o8"},
|
||||
{NaN(), "0O8"},
|
||||
{0o12345, "0o12345"},
|
||||
{0o12345, "0O12345"},
|
||||
{0, "0b0"},
|
||||
{0, "0B0"},
|
||||
{NaN(), "0b2"},
|
||||
{NaN(), "0b2"},
|
||||
{0b10101, "0b10101"},
|
||||
{0b10101, "0B10101"},
|
||||
{NaN(), "1.f"},
|
||||
{NaN(), "1.e"},
|
||||
{NaN(), "1.0ef"},
|
||||
{NaN(), "1.0e"},
|
||||
{NaN(), ".f"},
|
||||
{NaN(), ".e"},
|
||||
{NaN(), ".0ef"},
|
||||
{NaN(), ".0e"},
|
||||
{NaN(), "a.f"},
|
||||
{NaN(), "a.e"},
|
||||
{NaN(), "a.0ef"},
|
||||
{NaN(), "a.0e"},
|
||||
}
|
||||
|
||||
func TestFromString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("stringTests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, test := range stringTests {
|
||||
t.Run(test.str, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, FromString(test.str), test.number)
|
||||
assertEqualNumber(t, FromString(test.str+" "), test.number)
|
||||
assertEqualNumber(t, FromString(" "+test.str), test.number)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fromStringTests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, test := range fromStringTests {
|
||||
t.Run(test.str, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, FromString(test.str), test.number)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range stringTests {
|
||||
t.Run(test.str, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, FromString(test.str).String(), test.str)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringJS(t *testing.T) {
|
||||
t.Parallel()
|
||||
jstest.SkipIfNoNodeJS(t)
|
||||
|
||||
t.Run("stringTests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// These tests should roundtrip both ways.
|
||||
stringTestsResults := getStringResultsFromJS(t, stringTests)
|
||||
for i, test := range stringTests {
|
||||
t.Run(fmt.Sprintf("%v", float64(test.number)), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, stringTestsResults[i].number, test.number)
|
||||
assert.Equal(t, stringTestsResults[i].str, test.str)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fromStringTests", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// These tests should convert the string to the same number.
|
||||
fromStringTestsResults := getStringResultsFromJS(t, fromStringTests)
|
||||
for i, test := range fromStringTests {
|
||||
t.Run(fmt.Sprintf("fromString %q", test.str), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertEqualNumber(t, fromStringTestsResults[i].number, test.number)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func isFuzzing() bool {
|
||||
return flag.CommandLine.Lookup("test.fuzz").Value.String() != ""
|
||||
}
|
||||
|
||||
func FuzzStringJS(f *testing.F) {
|
||||
jstest.SkipIfNoNodeJS(f)
|
||||
|
||||
if isFuzzing() {
|
||||
// Avoid running anything other than regressions in the fuzzing mode.
|
||||
for _, test := range stringTests {
|
||||
f.Add(float64(test.number))
|
||||
}
|
||||
for _, test := range fromStringTests {
|
||||
f.Add(float64(test.number))
|
||||
}
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, f float64) {
|
||||
n := Number(f)
|
||||
nStr := n.String()
|
||||
|
||||
results := getStringResultsFromJS(t, []stringTest{{number: n, str: nStr}})
|
||||
assert.Equal(t, len(results), 1)
|
||||
|
||||
nToJSStr := results[0].str
|
||||
nStrToJSNumber := results[0].number
|
||||
|
||||
assert.Equal(t, nStr, nToJSStr)
|
||||
assertEqualNumber(t, n, nStrToJSNumber)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzFromStringJS(f *testing.F) {
|
||||
jstest.SkipIfNoNodeJS(f)
|
||||
|
||||
if isFuzzing() {
|
||||
// Avoid running anything other than regressions in the fuzzing mode.
|
||||
for _, test := range stringTests {
|
||||
f.Add(test.str)
|
||||
}
|
||||
for _, test := range fromStringTests {
|
||||
f.Add(test.str)
|
||||
}
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
if len(s) > 350 {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
n := FromString(s)
|
||||
results := getStringResultsFromJS(t, []stringTest{{str: s}})
|
||||
assert.Equal(t, len(results), 1)
|
||||
assertEqualNumber(t, n, results[0].number)
|
||||
})
|
||||
}
|
||||
|
||||
func getStringResultsFromJS(t testing.TB, tests []stringTest) []stringTest {
|
||||
t.Helper()
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
type data struct {
|
||||
Bits [2]uint32 `json:"bits"`
|
||||
Str string `json:"str"`
|
||||
}
|
||||
|
||||
inputData := make([]data, len(tests))
|
||||
for i, test := range tests {
|
||||
inputData[i] = data{
|
||||
Bits: numberToUint32Array(test.number),
|
||||
Str: test.str,
|
||||
}
|
||||
}
|
||||
|
||||
jsonInput, err := json.Marshal(inputData)
|
||||
assert.NilError(t, err)
|
||||
|
||||
jsonInputPath := filepath.Join(tmpdir, "input.json")
|
||||
err = os.WriteFile(jsonInputPath, jsonInput, 0o644)
|
||||
assert.NilError(t, err)
|
||||
|
||||
script := `
|
||||
import fs from 'fs';
|
||||
|
||||
function fromBits(bits) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Uint32Array(buffer))[0] = bits[0];
|
||||
(new Uint32Array(buffer))[1] = bits[1];
|
||||
return new Float64Array(buffer)[0];
|
||||
}
|
||||
|
||||
function toBits(number) {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
(new Float64Array(buffer))[0] = number;
|
||||
return [(new Uint32Array(buffer))[0], (new Uint32Array(buffer))[1]];
|
||||
}
|
||||
|
||||
export default function(inputFile) {
|
||||
const input = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||
|
||||
const output = input.map((input) => ({
|
||||
str: ""+fromBits(input.bits),
|
||||
bits: toBits(+input.str),
|
||||
}));
|
||||
|
||||
return output;
|
||||
};
|
||||
`
|
||||
|
||||
outputData, err := jstest.EvalNodeScript[[]data](t, script, tmpdir, jsonInputPath)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(outputData), len(tests))
|
||||
|
||||
output := make([]stringTest, len(tests))
|
||||
for i, outputDatum := range outputData {
|
||||
output[i] = stringTest{
|
||||
number: uint32ArrayToNumber(outputDatum.Bits),
|
||||
str: outputDatum.Str,
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func numberToUint32Array(n Number) [2]uint32 {
|
||||
bits := numberToBits(n)
|
||||
return [2]uint32{uint32(bits), uint32(bits >> 32)}
|
||||
}
|
||||
|
||||
func uint32ArrayToNumber(a [2]uint32) Number {
|
||||
bits := uint64(a[0]) | uint64(a[1])<<32
|
||||
return numberFromBits(bits)
|
||||
}
|
||||
Reference in New Issue
Block a user