Ver código fonte

feat(codemode): complete regexp and math parity (#37955)

Aiden Cline 3 semanas atrás
pai
commit
6d50023457

+ 7 - 5
packages/codemode/interpreter-support.md

@@ -238,9 +238,9 @@ ultimate source of truth.
       use their epoch time) and reject opaque runtime references as data errors.
 - [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined`
       for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
-      example `Math.sumPrecise is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw,
+      example `Math.sum is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw,
       and unknown `Promise` statics keep their descriptive error.
-- [ ] `Math.sumPrecise`.
+- [x] `Math.sumPrecise` over supported collection iterables, rejecting non-number elements without coercion.
 - [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
 
 ## JSON and console
@@ -275,15 +275,17 @@ ultimate source of truth.
 
 - [x] Literal and `RegExp(pattern, flags)` construction, with or without `new`.
 - [x] `test`, `exec`, and `toString`.
-- [x] Readable `source`, `flags`, `lastIndex`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, and `dotAll`.
+- [x] Readable `source`, `flags`, `lastIndex`, `hasIndices`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`,
+      `unicodeSets`, and `dotAll`.
 - [x] Captures, safe named groups (blocked member names are omitted), match `.index`, and stateful global matching.
 - [x] Integration with supported String methods, including function replacers.
 - [x] Writable `lastIndex`.
-- [ ] `hasIndices`, match `indices`, and `unicodeSets` metadata for the `d` and `v` flags.
-- [ ] `RegExp.escape`.
+- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
+- [x] `RegExp.escape`.
 
 ## Map and Set
 
+- [ ] Static `Map.groupBy`.
 - [x] `new Map()` from entry arrays or another Map.
 - [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`.
 - [x] `new Set()` from arrays, strings, or another Set.

+ 3 - 7
packages/codemode/src/interpreter/methods.ts

@@ -28,7 +28,7 @@ import { invokeJsonMethod } from "../stdlib/json.js"
 import { invokeMathMethod } from "../stdlib/math.js"
 import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js"
 import { invokeObjectMethod } from "../stdlib/object.js"
-import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js"
+import { invokeRegExpMethod, invokeRegExpStatic, matchToValue, toHostRegex } from "../stdlib/regexp.js"
 import { invokeStringStatic } from "../stdlib/string.js"
 import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
 import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js"
@@ -126,12 +126,8 @@ export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unkno
   if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node)
   if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node)
   if (ref.namespace === "Date") return invokeDateStatic(ref.name, args, node)
-  if (
-    ref.namespace === "RegExp" ||
-    ref.namespace === "Map" ||
-    ref.namespace === "Set" ||
-    ref.namespace === "URLSearchParams"
-  ) {
+  if (ref.namespace === "RegExp") return invokeRegExpStatic(ref.name, args, node)
+  if (ref.namespace === "Map" || ref.namespace === "Set" || ref.namespace === "URLSearchParams") {
     throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
   }
   return invokeJsonMethod(ref.name, args, node)

+ 8 - 1
packages/codemode/src/interpreter/runtime.ts

@@ -53,7 +53,13 @@ import { mathConstants, mathMethods } from "../stdlib/math.js"
 import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
 import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js"
 import { promiseStatics } from "../stdlib/promise.js"
-import { escapeRegexHint, regexpMethods, regexpProperties, regexFailureReason } from "../stdlib/regexp.js"
+import {
+  escapeRegexHint,
+  regexpMethods,
+  regexpProperties,
+  regexpStatics,
+  regexFailureReason,
+} from "../stdlib/regexp.js"
 import { stringMethods, stringStatics } from "../stdlib/string.js"
 import {
   urlMethods,
@@ -93,6 +99,7 @@ const globalStaticMembers: Partial<Record<GlobalNamespaceName, Set<string>>> = {
   Array: arrayStatics,
   console: consoleMethods,
   Date: dateStatics,
+  RegExp: regexpStatics,
   URL: urlStatics,
 }
 

+ 22 - 1
packages/codemode/src/stdlib/math.ts

@@ -1,3 +1,13 @@
+import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
+import { spreadItems } from "./collections.js"
+
+// Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types.
+declare global {
+  interface Math {
+    sumPrecise(values: Iterable<number>): number
+  }
+}
+
 export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
 
 export const mathMethods = new Set([
@@ -37,11 +47,23 @@ export const mathMethods = new Set([
   "fround",
   "clz32",
   "imul",
+  "sumPrecise",
 ])
 
 export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
   if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
   if (name === "random") return Math.random()
+  if (name === "sumPrecise") {
+    const items = spreadItems(args[0])
+    if (items === undefined) {
+      throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable collection.", node).as("TypeError")
+    }
+    const numbers = Array.from(items)
+    if (!numbers.every((item): item is number => typeof item === "number")) {
+      throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError")
+    }
+    return Math.sumPrecise(numbers)
+  }
   // Validate only the arguments the method consumes; like JS, extras are ignored
   // (so built-ins work as callbacks receiving (element, index, array)).
   const num = (index: number): number => {
@@ -131,4 +153,3 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
   }
   throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
 }
-import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"

+ 45 - 7
packages/codemode/src/stdlib/regexp.ts

@@ -1,14 +1,33 @@
+import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
+import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
+import { CodeModeRegExp } from "../values.js"
+import { coerceToNumber, coerceToString } from "./value.js"
+
+type MatchValue = Array<unknown> & {
+  index?: number
+  groups?: SafeObject
+  indices?: IndicesValue
+}
+
+type IndicesValue = Array<unknown> & {
+  groups?: SafeObject
+}
+
 export const regexpMethods = new Set(["test", "exec", "toString"])
 
+export const regexpStatics = new Set(["escape"])
+
 export const regexpProperties = new Set([
   "source",
   "flags",
   "lastIndex",
+  "hasIndices",
   "global",
   "ignoreCase",
   "multiline",
   "sticky",
   "unicode",
+  "unicodeSets",
   "dotAll",
 ])
 
@@ -39,18 +58,27 @@ export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFl
 }
 
 export const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
-  const result: Array<unknown> = Array.from(match, (group) => group)
-  if (match.index !== undefined) (result as Record<string, unknown> & Array<unknown>).index = match.index
+  const result: MatchValue = Array.from(match, (group) => group)
+  if (match.index !== undefined) result.index = match.index
   if (match.groups) {
     const groups: SafeObject = Object.create(null) as SafeObject
     for (const [key, group] of Object.entries(match.groups)) {
       if (!isBlockedMember(key)) groups[key] = group
     }
-    ;(result as Record<string, unknown> & Array<unknown>).groups = groups
+    result.groups = groups
   }
+  if (match.indices) result.indices = indicesToValue(match.indices)
   return result
 }
 
+export const invokeRegExpStatic = (name: string, args: Array<unknown>, node: AstNode): string => {
+  if (name !== "escape") throw new InterpreterRuntimeError(`RegExp.${name} is not available in CodeMode.`, node)
+  if (typeof args[0] !== "string") {
+    throw new InterpreterRuntimeError("RegExp.escape expects a string.", node).as("TypeError")
+  }
+  return RegExp.escape(args[0])
+}
+
 export const invokeRegExpMethod = (
   value: CodeModeRegExp,
   name: string,
@@ -85,7 +113,17 @@ const toLength = (value: unknown): number => {
   if (Number.isNaN(number) || number <= 0) return 0
   return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER)
 }
-import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
-import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
-import { CodeModeRegExp } from "../values.js"
-import { coerceToNumber, coerceToString } from "./value.js"
+
+const indicesToValue = (indices: RegExpIndicesArray): IndicesValue => {
+  const result: IndicesValue = Array.from(indices, (range) => (range === undefined ? undefined : [...range]))
+  if (indices.groups) {
+    const groups: SafeObject = Object.create(null) as SafeObject
+    for (const [key, range] of Object.entries(indices.groups)) {
+      if (!isBlockedMember(key)) groups[key] = range === undefined ? undefined : [...range]
+    }
+    result.groups = groups
+    return result
+  }
+  result.groups = undefined
+  return result
+}

+ 10 - 8
packages/codemode/test/parity.test.ts

@@ -856,9 +856,9 @@ describe("coercion parity: ++ and -- use CodeMode numeric coercion", () => {
 
 describe("coercion parity: unknown static members read as undefined", () => {
   test("feature detection on missing statics works like native JS", async () => {
-    expect(await value(`return typeof Math.sumPrecise`)).toBe("undefined")
+    expect(await value(`return typeof Math.sum`)).toBe("undefined")
     expect(await value(`return Object.groupBy === undefined`)).toBe(true)
-    expect(await value(`return RegExp.escape === undefined`)).toBe(true)
+    expect(await value(`return RegExp.quote === undefined`)).toBe(true)
     expect(await value(`return Number.range === undefined`)).toBe(true)
     expect(await value(`return String.raw === undefined`)).toBe(true)
     expect(await value(`return isFinite.something === undefined`)).toBe(true)
@@ -867,26 +867,28 @@ describe("coercion parity: unknown static members read as undefined", () => {
     expect(await value(`return JSON.rawJSON === undefined`)).toBe(true)
     expect(await value(`return URL.createObjectURL === undefined`)).toBe(true)
     expect(await value(`return Map.groupBy === undefined`)).toBe(true)
-    expect(await value(`return Math.sumPrecise?.([1]) ?? "fallback"`)).toBe("fallback")
+    expect(await value(`return Math.sum?.([1]) ?? "fallback"`)).toBe("fallback")
   })
 
   test("known statics still resolve and run", async () => {
     expect(await value(`return typeof Math.max`)).toBe("function")
     expect(await value(`return typeof console.log`)).toBe("function")
     expect(await value(`return typeof Date.now`)).toBe("function")
+    expect(await value(`return typeof Math.sumPrecise`)).toBe("function")
+    expect(await value(`return typeof RegExp.escape`)).toBe("function")
     expect(await value(`return Math.max(1, 2)`)).toBe(2)
+    expect(await value(`return Math.sumPrecise([1, 2])`)).toBe(3)
+    expect(await value(`return RegExp.escape("a.b")`)).toBe("\\x61\\.b")
     expect(await value(`return URL.canParse("https://example.com")`)).toBe(true)
     expect(await value(`return Number.isInteger(3)`)).toBe(true)
     expect(await value(`return Number.MAX_SAFE_INTEGER`)).toBe(Number.MAX_SAFE_INTEGER)
   })
 
   test("calling an unknown static reports a native-style TypeError", async () => {
-    expect(await value(`try { Math.sumPrecise([1]) } catch (e) { return e.name + ": " + e.message }`)).toBe(
-      "TypeError: Math.sumPrecise is not a function.",
-    )
-    expect(await value(`try { Math["sumPrecise"]([1]) } catch (e) { return e.message }`)).toBe(
-      "Math.sumPrecise is not a function.",
+    expect(await value(`try { Math.sum([1]) } catch (e) { return e.name + ": " + e.message }`)).toBe(
+      "TypeError: Math.sum is not a function.",
     )
+    expect(await value(`try { Math["sum"]([1]) } catch (e) { return e.message }`)).toBe("Math.sum is not a function.")
   })
 
   test("blocked members still throw instead of reading as undefined", async () => {

+ 283 - 0
packages/codemode/test/regexp-math-test262.test.ts

@@ -0,0 +1,283 @@
+/*
+ * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
+ * - test/built-ins/RegExp/escape/initial-char-escape.js
+ * - test/built-ins/RegExp/escape/escaped-syntax-characters-simple.js
+ * - test/built-ins/RegExp/escape/escaped-otherpunctuators.js
+ * - test/built-ins/RegExp/escape/escaped-control-characters.js
+ * - test/built-ins/RegExp/escape/escaped-whitespace.js
+ * - test/built-ins/RegExp/escape/escaped-lineterminator.js
+ * - test/built-ins/RegExp/escape/escaped-surrogates.js
+ * - test/built-ins/RegExp/escape/escaped-solidus-character-simple.js
+ * - test/built-ins/RegExp/escape/escaped-utf16encodecodepoint.js
+ * - test/built-ins/RegExp/escape/not-escaped.js
+ * - test/built-ins/RegExp/escape/not-escaped-underscore.js
+ * - test/built-ins/RegExp/escape/non-string-inputs.js
+ * - test/built-ins/RegExp/prototype/hasIndices/this-val-regexp.js
+ * - test/built-ins/RegExp/prototype/unicodeSets/uv-flags-constructor.js
+ * - test/built-ins/RegExp/match-indices/indices-array.js
+ * - test/built-ins/RegExp/match-indices/indices-array-matched.js
+ * - test/built-ins/RegExp/match-indices/indices-array-non-unicode-match.js
+ * - test/built-ins/RegExp/match-indices/indices-array-unmatched.js
+ * - test/built-ins/RegExp/match-indices/indices-array-unicode-match.js
+ * - test/built-ins/RegExp/match-indices/indices-groups-object-undefined.js
+ * - test/built-ins/RegExp/match-indices/indices-groups-object-unmatched.js
+ * - test/built-ins/RegExp/match-indices/no-indices-array.js
+ * - test/built-ins/Math/sumPrecise/takes-iterable.js
+ * - test/built-ins/Math/sumPrecise/sum.js
+ * - test/built-ins/Math/sumPrecise/sum-is-infinite.js
+ * - test/built-ins/Math/sumPrecise/sum-is-minus-zero.js
+ * - test/built-ins/Math/sumPrecise/sum-is-NaN.js
+ * - test/built-ins/Math/sumPrecise/throws-on-non-number.js
+ *
+ * Copyright (C) 2019 Ron Buckton. All rights reserved.
+ * Copyright (C) 2021 Ron Buckton and the V8 project authors. All rights reserved.
+ * Copyright (C) 2024 Kevin Gibbons. All rights reserved.
+ * Copyright (C) 2024 Leo Balter. All rights reserved.
+ * Copyright (C) 2024 Leo Balter, Jordan Harband. All rights reserved.
+ * Copyright 2022 Mathias Bynens. All rights reserved.
+ * Test262 portions are governed by the BSD license in LICENSE.test262.
+ * Generator and custom-iterator cases are omitted because CodeMode exposes only its supported collection iterables;
+ * the Math.sumPrecise iterable case additionally covers Set.
+ */
+import { describe, expect, test } from "bun:test"
+import { Effect } from "effect"
+import { CodeMode } from "../src/index.js"
+
+const value = async (code: string) => {
+  const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
+  if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
+  return result.value
+}
+
+describe("RegExp Test262 parity", () => {
+  test("RegExp.escape encodes syntax, punctuators, whitespace, and surrogates", async () => {
+    expect(
+      await value(`
+        return [
+          RegExp.escape("1+1"),
+          RegExp.escape("foo.bar"),
+          RegExp.escape(".*+?^$|()[]{}"),
+          RegExp.escape(",-=#&!%:;@~'"),
+          RegExp.escape("\\t\\n\\v\\f\\r"),
+          RegExp.escape("\\uFEFF \\u00A0\\u202F"),
+          RegExp.escape("\\u2028\\u2029"),
+          RegExp.escape("\\uD800"),
+          RegExp.escape("\\uDFFF"),
+        ]
+      `),
+    ).toEqual([
+      "\\x31\\+1",
+      "\\x66oo\\.bar",
+      "\\.\\*\\+\\?\\^\\$\\|\\(\\)\\[\\]\\{\\}",
+      "\\x2c\\x2d\\x3d\\x23\\x26\\x21\\x25\\x3a\\x3b\\x40\\x7e\\x27",
+      "\\t\\n\\v\\f\\r",
+      "\\ufeff\\x20\\xa0\\u202f",
+      "\\u2028\\u2029",
+      "\\ud800",
+      "\\udfff",
+    ])
+  })
+
+  test("RegExp.escape rejects non-string inputs", async () => {
+    expect(
+      await value(`
+        const rejects = (input) => {
+          try {
+            RegExp.escape(input)
+            return false
+          } catch (error) {
+            return error.name === "TypeError"
+          }
+        }
+        return [rejects(123), rejects({}), rejects([]), rejects(null), rejects(undefined)]
+      `),
+    ).toEqual([true, true, true, true, true])
+  })
+
+  test("RegExp.escape preserves ordinary ASCII and Unicode code points", async () => {
+    expect(
+      await value(`
+        return [
+          RegExp.escape(""),
+          RegExp.escape(".a1b2c3D4E5F6"),
+          RegExp.escape("_a_1_2"),
+          RegExp.escape("hello_world"),
+          RegExp.escape("//"),
+          RegExp.escape("\\u4F60\\u597D"),
+          RegExp.escape("\\u0393\\u03B5\\u03B9\\u03AC \\u03C3\\u03BF\\u03C5"),
+          RegExp.escape("\\uD835\\uDC01"),
+        ]
+      `),
+    ).toEqual([
+      "",
+      "\\.a1b2c3D4E5F6",
+      "_a_1_2",
+      "\\x68ello_world",
+      "\\/\\/",
+      "\u4f60\u597d",
+      "\u0393\u03b5\u03b9\u03ac\\x20\u03c3\u03bf\u03c5",
+      "\uD835\uDC01",
+    ])
+  })
+
+  test("d flag exposes match indices through exec, match, and matchAll", async () => {
+    expect(
+      await value(`
+        const match = /(?<a>a)(b)?/d.exec("a")
+        const stringMatch = "a".match(/a/d)
+        const all = "a a".matchAll(/a/dg)
+        const blocked = /(?<constructor>a)(?<safe>b)/d.exec("ab")
+        return [
+          /./.hasIndices,
+          /./d.hasIndices,
+          new RegExp(".", "d").hasIndices,
+          match.indices[0],
+          match.indices[1],
+          match.indices[2],
+          match.indices.groups.a,
+          stringMatch.indices[0],
+          all[0].indices[0],
+          all[1].indices[0],
+          Object.keys(blocked.indices.groups),
+        ]
+      `),
+    ).toEqual([false, true, true, [0, 1], [0, 1], null, [0, 1], [0, 1], [0, 1], [2, 3], ["safe"]])
+  })
+
+  test("match indices preserve captures, Unicode offsets, and groups properties", async () => {
+    expect(
+      await value(`
+        const plain = /a/.exec("a")
+        const captured = "abcd".match(/b(c)/d)
+        const unmatched = "bab".match(/(\\w\\w)(\\W)?/d)
+        const nonUnicode = "\\uD835\\uDC01".match(/./d)
+        const unicode = "\\uD835\\uDC01".match(/./du)
+        const noGroups = /./d.exec("a").indices
+        return [
+          Object.hasOwn(plain, "indices"),
+          captured.indices,
+          unmatched.indices,
+          nonUnicode.indices[0],
+          unicode.indices[0],
+          Object.hasOwn(noGroups, "groups"),
+          noGroups.groups,
+        ]
+      `),
+    ).toEqual([
+      false,
+      [
+        [1, 3],
+        [2, 3],
+      ],
+      [[0, 2], [0, 2], null],
+      [0, 1],
+      [0, 2],
+      true,
+      null,
+    ])
+  })
+
+  test("match and matchAll preserve named, unmatched, and blocked index groups", async () => {
+    expect(
+      await value(`
+        const matched = "a".match(/(?<a>a)|(?<x>x)/d).indices.groups
+        const all = "a x".matchAll(/(?<a>a)|(?<x>x)/dg)
+        const blockedMatch = "ab".match(/(?<constructor>a)(?<safe>b)/d).indices.groups
+        const blockedAll = "ab".matchAll(/(?<constructor>a)(?<safe>b)/dg)[0].indices.groups
+        return [
+          matched.a,
+          matched.x,
+          all[0].indices.groups.a,
+          all[0].indices.groups.x,
+          all[1].indices.groups.a,
+          all[1].indices.groups.x,
+          Object.keys(blockedMatch),
+          Object.keys(blockedAll),
+        ]
+      `),
+    ).toEqual([[0, 1], null, [0, 1], null, null, [2, 3], ["safe"], ["safe"]])
+  })
+
+  test("v flag exposes unicodeSets and remains exclusive with u", async () => {
+    expect(
+      await value(`
+        const pattern = new RegExp("[a&&a]", "v")
+        let rejectsUV = false
+        try {
+          new RegExp(".", "uv")
+        } catch (error) {
+          rejectsUV = error.name === "SyntaxError"
+        }
+        return [pattern.unicodeSets, pattern.unicode, pattern.flags, pattern.test("a"), pattern.test("b"), rejectsUV]
+      `),
+    ).toEqual([true, false, "v", true, false, true])
+  })
+
+  test("d and v flags compose", async () => {
+    expect(
+      await value(`
+        const pattern = new RegExp("(?<a>[a&&a])", "dv")
+        const match = pattern.exec("a")
+        return [pattern.hasIndices, pattern.unicodeSets, pattern.flags, match.indices[0], match.indices.groups.a]
+      `),
+    ).toEqual([true, true, "dv", [0, 1], [0, 1]])
+  })
+})
+
+describe("Math.sumPrecise Test262 parity", () => {
+  test("performs maximally precise summation over supported iterables", async () => {
+    expect(
+      await value(`
+        return [
+          Math.sumPrecise([1, 2, 3]),
+          Math.sumPrecise([1e30, 0.1, -1e30]),
+          Math.sumPrecise([1e308, 1e308, 0.1, 0.1, 1e30, 0.1, -1e30, -1e308, -1e308]),
+          Math.sumPrecise([8.98846567431158e307, 8.988465674311579e307, -1.7976931348623157e308]),
+          Math.sumPrecise(new Set([1, 2])),
+          [[1, 2], [3, 4]].map(Math.sumPrecise),
+          Object.is(Math.sumPrecise([]), -0),
+          Object.is(Math.sumPrecise([-0, -0]), -0),
+          Object.is(Math.sumPrecise([-0, 0]), 0),
+        ]
+      `),
+    ).toEqual([6, 0.1, 0.30000000000000004, 9.9792015476736e291, 3, [3, 7], true, true, true])
+  })
+
+  test("handles infinities and NaN", async () => {
+    expect(
+      await value(`
+        return [
+          Math.sumPrecise([Infinity, Infinity]) === Infinity,
+          Math.sumPrecise([-Infinity, -Infinity]) === -Infinity,
+          Number.isNaN(Math.sumPrecise([NaN])),
+          Number.isNaN(Math.sumPrecise([Infinity, -Infinity])),
+        ]
+      `),
+    ).toEqual([true, true, true, true])
+  })
+
+  test("rejects missing, non-iterable, sparse, and non-number inputs", async () => {
+    expect(
+      await value(`
+        const rejects = (input, missing = false) => {
+          try {
+            if (missing) Math.sumPrecise()
+            else Math.sumPrecise(input)
+            return false
+          } catch (error) {
+            return error.name === "TypeError"
+          }
+        }
+        return [
+          rejects(undefined, true),
+          rejects({}),
+          rejects(["1"]),
+          rejects(Array(1)),
+          rejects("12"),
+          rejects(new Map([[1, 2]])),
+          rejects(new URLSearchParams("a=1")),
+        ]
+      `),
+    ).toEqual([true, true, true, true, true, true, true])
+  })
+})