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
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue