From 73298656e234d56f0a8c477c643d5bc0e81bc446 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 20 Jul 2026 11:10:46 -0500 Subject: [PATCH 1/3] feat(codemode): complete regexp and math parity --- packages/codemode/interpreter-support.md | 11 +- packages/codemode/src/interpreter/methods.ts | 10 +- packages/codemode/src/interpreter/runtime.ts | 9 +- packages/codemode/src/stdlib/math.ts | 13 + packages/codemode/src/stdlib/regexp.ts | 27 ++ packages/codemode/test/parity.test.ts | 18 +- .../codemode/test/regexp-math-test262.test.ts | 283 ++++++++++++++++++ 7 files changed, 350 insertions(+), 21 deletions(-) create mode 100644 packages/codemode/test/regexp-math-test262.test.ts diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 70a9c1afff34..5d2ef484829e 100644 --- a/packages/codemode/interpreter-support.md +++ b/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,12 +275,13 @@ 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 diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 67f95ac0ec34..f70d09dc7baa 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/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>> = { Array: arrayStatics, console: consoleMethods, Date: dateStatics, + RegExp: regexpStatics, URL: urlStatics, } diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 54fa3be91be1..e0440b92dc9d 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -37,11 +37,23 @@ export const mathMethods = new Set([ "fround", "clz32", "imul", + "sumPrecise", ]) export const invokeMathMethod = (name: string, args: Array, 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 as Math & { sumPrecise(values: Iterable): number }).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 +143,5 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo } throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) } +import { spreadItems } from "./collections.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index bcfedc15ab33..d34cbe31782d 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -1,14 +1,18 @@ 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", ]) @@ -48,9 +52,18 @@ export const matchToValue = (match: RegExpMatchArray): Array => { } ;(result as Record & Array).groups = groups } + if (match.indices) (result as Record & Array).indices = indicesToValue(match.indices) return result } +export const invokeRegExpStatic = (name: string, args: Array, 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,6 +98,20 @@ const toLength = (value: unknown): number => { if (Number.isNaN(number) || number <= 0) return 0 return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) } + +const indicesToValue = (indices: RegExpIndicesArray): Array => { + const result: Array = 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 as Record & Array).groups = groups + return result + } + ;(result as Record & Array).groups = undefined + return result +} import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js" import { CodeModeRegExp } from "../values.js" diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 7167e6471ef6..26c942e23de1 100644 --- a/packages/codemode/test/parity.test.ts +++ b/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 () => { diff --git a/packages/codemode/test/regexp-math-test262.test.ts b/packages/codemode/test/regexp-math-test262.test.ts new file mode 100644 index 000000000000..5fd2100a3782 --- /dev/null +++ b/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)(b)?/d.exec("a") + const stringMatch = "a".match(/a/d) + const all = "a a".matchAll(/a/dg) + const blocked = /(?a)(?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)|(?x)/d).indices.groups + const all = "a x".matchAll(/(?a)|(?x)/dg) + const blockedMatch = "ab".match(/(?a)(?b)/d).indices.groups + const blockedAll = "ab".matchAll(/(?a)(?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])", "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]) + }) +}) From 559b3580d3555bee9d3f680e1af72a1b4667fdaf Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 20 Jul 2026 11:18:33 -0500 Subject: [PATCH 2/3] refactor(codemode): simplify regexp math typings --- packages/codemode/src/stdlib/math.ts | 14 ++++++++--- packages/codemode/src/stdlib/regexp.ts | 35 +++++++++++++++++--------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index e0440b92dc9d..235b6083b5a4 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/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 + } +} + export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"]) export const mathMethods = new Set([ @@ -52,7 +62,7 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo 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 as Math & { sumPrecise(values: Iterable): number }).sumPrecise(numbers) + 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)). @@ -143,5 +153,3 @@ export const invokeMathMethod = (name: string, args: Array, node: AstNo } throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) } -import { spreadItems } from "./collections.js" -import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index d34cbe31782d..f737e1b9936d 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -1,3 +1,18 @@ +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 & { + index?: number + groups?: SafeObject + indices?: IndicesValue +} + +type IndicesValue = Array & { + groups?: SafeObject +} + export const regexpMethods = new Set(["test", "exec", "toString"]) export const regexpStatics = new Set(["escape"]) @@ -43,16 +58,16 @@ export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFl } export const matchToValue = (match: RegExpMatchArray): Array => { - const result: Array = Array.from(match, (group) => group) - if (match.index !== undefined) (result as Record & Array).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 & Array).groups = groups + result.groups = groups } - if (match.indices) (result as Record & Array).indices = indicesToValue(match.indices) + if (match.indices) result.indices = indicesToValue(match.indices) return result } @@ -99,20 +114,16 @@ const toLength = (value: unknown): number => { return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) } -const indicesToValue = (indices: RegExpIndicesArray): Array => { - const result: Array = Array.from(indices, (range) => (range === undefined ? undefined : [...range])) +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 as Record & Array).groups = groups + result.groups = groups return result } - ;(result as Record & Array).groups = undefined + result.groups = undefined return result } -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" From f33293ad879f4311e15daa84d6a924231bc57627 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 20 Jul 2026 11:28:50 -0500 Subject: [PATCH 3/3] docs(codemode): track map groupBy support --- packages/codemode/interpreter-support.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 5d2ef484829e..7229cb44a4a3 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -285,6 +285,7 @@ ultimate source of truth. ## 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.