feat(codemode): complete regexp and math parity (#37955)
This commit is contained in:
parent
e8b19afa8f
commit
6d50023457
7 changed files with 378 additions and 29 deletions
|
|
@ -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
packages/codemode/test/regexp-math-test262.test.ts
Normal file
283
packages/codemode/test/regexp-math-test262.test.ts
Normal file
|
|
@ -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])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue