mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(codemode): coerce built-in arguments as in JS instead of requiring numbers and strings (#50492)
This commit is contained in:
@@ -124,8 +124,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `Boolean`, `Number`, `String`, `parseInt`, `parseFloat`, `isFinite`, `isNaN`, and URI helpers as callbacks.
|
||||
- [x] Built-in method references as callbacks, such as `values.map(Math.abs)`, `records.map(JSON.stringify)`,
|
||||
`items.forEach(console.log)`, and `Promise.resolve(-1).then(Math.abs)`. Extra callback arguments a built-in
|
||||
does not consume are ignored, like JS; consumed arguments stay strictly validated (`Math.floor` still rejects a
|
||||
string). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
|
||||
does not consume are ignored, like JS, and consumed arguments coerce, like JS (`"3.7".replace(/\d\.\d/,
|
||||
Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
|
||||
because `includes` is called without a string `this`.
|
||||
- [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`),
|
||||
and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Headers`, `Promise`) throw a `TypeError`,
|
||||
@@ -205,8 +205,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
applied to functions and namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
|
||||
- [ ] ToPrimitive on object operands: operators, `Error(message)`, `Date` arguments, and `parseInt` radix should call
|
||||
`valueOf`/`toString` in spec order and surface their throws.
|
||||
- [ ] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via `toString`) become string keys; only
|
||||
strings and numbers are accepted.
|
||||
- [x] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via their built-in string form) become
|
||||
string keys.
|
||||
|
||||
## Promises and tools
|
||||
|
||||
@@ -319,10 +319,12 @@ reject }` object.
|
||||
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
|
||||
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
|
||||
form, like `JSON.stringify`.
|
||||
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
|
||||
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
|
||||
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
|
||||
`undefined`; `includes()` still requires a value.
|
||||
- [x] Numeric arguments coerce as in JS (ToIntegerOrInfinity): `indexOf(x, "1")`, `slice("1", "3")`, `at(null)`,
|
||||
`flat(1.9)`, `with(1.5, v)`, `Math.max("3", "2")`, `parseInt("11", "2")`, `(1.5).toFixed("2")`,
|
||||
`String.fromCharCode("65")`, and the Uint8Array equivalents. `join(sep)` and `JSON.parse(text)` apply ToString
|
||||
(`join(null)` is `"1null2"`, `JSON.parse(123)` is `123`). `Array.from({ length: "2" })` applies ToLength; a
|
||||
promise source still throws with an `await` hint rather than JS's silent `[]`. A program object's own
|
||||
`valueOf`/`toString` is not consulted yet (see ToPrimitive above).
|
||||
|
||||
## Strings
|
||||
|
||||
|
||||
@@ -422,6 +422,12 @@ export const coerceToNumber = (value: Value): number => {
|
||||
return value instanceof ToolReference ? Number.NaN : Number(value)
|
||||
}
|
||||
|
||||
/** ToIntegerOrInfinity: NaN is 0, fractions truncate. */
|
||||
export const coerceToInteger = (value: Value): number => {
|
||||
const number = coerceToNumber(value)
|
||||
return Number.isNaN(number) ? 0 : Math.trunc(number)
|
||||
}
|
||||
|
||||
/** Values that cannot cross the data boundary: opaque machinery and host-backed wrappers. */
|
||||
export const isRuntimeReference = (value: Value): boolean =>
|
||||
value instanceof Opaque || value instanceof Wrapper || value instanceof ToolReference
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
GeneratorObj,
|
||||
hostIterator,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
coerceToInteger,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
@@ -20,11 +22,11 @@ import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { compareText } from "../tool-runtime.js"
|
||||
|
||||
const arrayLikeSource = (source: Value): { readonly length: number; readonly source: Obj } => {
|
||||
if (source instanceof Obj && typeof get(source, "length") === "number") {
|
||||
const length = get(source, "length") as number
|
||||
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
|
||||
checkArrayLength(normalized)
|
||||
return { length: normalized, source }
|
||||
// JS would treat a promise as an empty array-like; that would hide a missing `await`.
|
||||
if (source instanceof Obj && !(source instanceof PromiseObj)) {
|
||||
const length = Math.max(0, coerceToInteger(get(source, "length")))
|
||||
checkArrayLength(length)
|
||||
return { length, source }
|
||||
}
|
||||
throw invalidData(`Array.from expects an iterable or array-like value, received ${describeValue(source)}.`)
|
||||
}
|
||||
@@ -122,13 +124,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
])
|
||||
|
||||
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") {
|
||||
throw typeError(`Array.${name} expects ${label} to be a number.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
|
||||
// Callback methods fix the iteration length while reading existing elements live.
|
||||
const iterate = (
|
||||
name: string,
|
||||
@@ -153,13 +149,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
"join",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "join").items
|
||||
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
|
||||
throw typeError("Array.join expects zero arguments or one string separator.")
|
||||
}
|
||||
const joined = target
|
||||
.map((item) => coerceToString(item ?? ""))
|
||||
.join(args.length === 0 ? "," : (args[0] as string))
|
||||
const joined = self(thisValue, "join")
|
||||
.items.map((item) => coerceToString(item ?? ""))
|
||||
.join(args[0] === undefined ? "," : coerceToString(args[0]))
|
||||
checkStringLength(joined.length)
|
||||
return joined
|
||||
},
|
||||
@@ -176,40 +168,23 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
"includes",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "includes").items
|
||||
if (args.length === 0 || args.length > 2) {
|
||||
throw typeError("Array.includes expects a value and optional start index.")
|
||||
}
|
||||
return target.includes(args[0], optNumber("includes", args[1], "start index"))
|
||||
return self(thisValue, "includes").items.includes(args[0], optNumber(args[1]))
|
||||
},
|
||||
],
|
||||
[
|
||||
"indexOf",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
self(thisValue, "indexOf").items.indexOf(args[0], optNumber("indexOf", args[1], "start index")),
|
||||
],
|
||||
["indexOf", 1, (thisValue, args) => self(thisValue, "indexOf").items.indexOf(args[0], optNumber(args[1]))],
|
||||
[
|
||||
"lastIndexOf",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "lastIndexOf").items
|
||||
return args[1] === undefined
|
||||
? target.lastIndexOf(args[0])
|
||||
: target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index"))
|
||||
return args[1] === undefined ? target.lastIndexOf(args[0]) : target.lastIndexOf(args[0], optNumber(args[1]))
|
||||
},
|
||||
],
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber("at", args[0], "index") ?? 0)],
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber(args[0]) ?? 0)],
|
||||
[
|
||||
"slice",
|
||||
2,
|
||||
(thisValue, args) =>
|
||||
wrap(
|
||||
self(thisValue, "slice").items.slice(
|
||||
optNumber("slice", args[0], "start"),
|
||||
optNumber("slice", args[1], "end"),
|
||||
),
|
||||
),
|
||||
(thisValue, args) => wrap(self(thisValue, "slice").items.slice(optNumber(args[0]), optNumber(args[1]))),
|
||||
],
|
||||
[
|
||||
"concat",
|
||||
@@ -228,7 +203,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
(thisValue, args) => {
|
||||
const flatten = (items: Array<Value>, depth: number): Array<Value> =>
|
||||
items.flatMap((item) => (item instanceof Arr && depth > 0 ? flatten(item.items, depth - 1) : [item]))
|
||||
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
|
||||
const flattened = flatten(self(thisValue, "flat").items, optNumber(args[0]) ?? 1)
|
||||
checkArrayLength(flattened.length)
|
||||
return wrap(flattened)
|
||||
},
|
||||
@@ -274,7 +249,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "with").items
|
||||
const index = optNumber("with", args[0], "index") ?? 0
|
||||
const index = optNumber(args[0]) ?? 0
|
||||
const resolved = index < 0 ? target.length + index : index
|
||||
if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.")
|
||||
const copied = [...target]
|
||||
@@ -309,9 +284,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "splice")
|
||||
if (args.length === 0) return wrap(target.items.splice(0, 0))
|
||||
const start = optNumber("splice", args[0], "start") ?? 0
|
||||
const start = optNumber(args[0]) ?? 0
|
||||
if (args.length === 1) return wrap(target.items.splice(start))
|
||||
const deleteCount = optNumber("splice", args[1], "delete count") ?? 0
|
||||
const deleteCount = optNumber(args[1]) ?? 0
|
||||
const inserted = args.slice(2)
|
||||
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result")
|
||||
return wrap(target.items.splice(start, deleteCount, ...inserted))
|
||||
@@ -323,9 +298,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
(thisValue, args) => {
|
||||
const copied = [...self(thisValue, "toSpliced").items]
|
||||
if (args.length === 0) return wrap(copied)
|
||||
const start = optNumber("toSpliced", args[0], "start") ?? 0
|
||||
const start = optNumber(args[0]) ?? 0
|
||||
if (args.length === 1) copied.splice(start)
|
||||
else copied.splice(start, optNumber("toSpliced", args[1], "delete count") ?? 0, ...args.slice(2))
|
||||
else copied.splice(start, optNumber(args[1]) ?? 0, ...args.slice(2))
|
||||
return wrap(copied)
|
||||
},
|
||||
],
|
||||
@@ -335,7 +310,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "fill")
|
||||
rejectCircularInsertion(target, args[0], "Array.fill result")
|
||||
target.items.fill(args[0], optNumber("fill", args[1], "start"), optNumber("fill", args[2], "end"))
|
||||
target.items.fill(args[0], optNumber(args[1]), optNumber(args[2]))
|
||||
return target
|
||||
},
|
||||
],
|
||||
@@ -344,11 +319,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "copyWithin")
|
||||
target.items.copyWithin(
|
||||
optNumber("copyWithin", args[0], "target index") ?? 0,
|
||||
optNumber("copyWithin", args[1], "start") ?? 0,
|
||||
optNumber("copyWithin", args[2], "end"),
|
||||
)
|
||||
target.items.copyWithin(optNumber(args[0]) ?? 0, optNumber(args[1]) ?? 0, optNumber(args[2]))
|
||||
return target
|
||||
},
|
||||
],
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Bytes,
|
||||
hostIterator,
|
||||
Obj,
|
||||
coerceToInteger,
|
||||
coerceToNumber,
|
||||
coerceToString,
|
||||
type Value,
|
||||
@@ -75,44 +76,28 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
])
|
||||
|
||||
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: Value, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") throw typeError(`Uint8Array.${name} expects ${label} to be a number.`)
|
||||
return value
|
||||
}
|
||||
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
|
||||
const wrapAll = (items: Array<Value>) => new Arr(builtins.Array, items)
|
||||
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
|
||||
methods(builtins, proto, [
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber("at", args[0], "index") ?? 0)],
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber(args[0]) ?? 0)],
|
||||
[
|
||||
"slice",
|
||||
2,
|
||||
(thisValue, args) =>
|
||||
wrap(
|
||||
self(thisValue, "slice").bytes.slice(
|
||||
optNumber("slice", args[0], "start"),
|
||||
optNumber("slice", args[1], "end"),
|
||||
),
|
||||
),
|
||||
(thisValue, args) => wrap(self(thisValue, "slice").bytes.slice(optNumber(args[0]), optNumber(args[1]))),
|
||||
],
|
||||
// A view on the same bytes, as in JS: writes through one are visible through the other.
|
||||
[
|
||||
"subarray",
|
||||
2,
|
||||
(thisValue, args) =>
|
||||
wrap(
|
||||
self(thisValue, "subarray").bytes.subarray(
|
||||
optNumber("subarray", args[0], "start"),
|
||||
optNumber("subarray", args[1], "end"),
|
||||
),
|
||||
),
|
||||
(thisValue, args) => wrap(self(thisValue, "subarray").bytes.subarray(optNumber(args[0]), optNumber(args[1]))),
|
||||
],
|
||||
[
|
||||
"set",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "set")
|
||||
const offset = optNumber("set", args[1], "offset") ?? 0
|
||||
const offset = optNumber(args[1]) ?? 0
|
||||
return Effect.map(collectBytes(ctx, args[0], "Uint8Array.set"), (source) => {
|
||||
if (!Number.isInteger(offset) || offset < 0 || source.length + offset > target.bytes.length) {
|
||||
throw rangeError("Uint8Array.set: the source does not fit at that offset.")
|
||||
@@ -127,11 +112,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "fill")
|
||||
target.bytes.fill(
|
||||
coerceToNumber(args[0]),
|
||||
optNumber("fill", args[1], "start"),
|
||||
optNumber("fill", args[2], "end"),
|
||||
)
|
||||
target.bytes.fill(coerceToNumber(args[0]), optNumber(args[1]), optNumber(args[2]))
|
||||
return target
|
||||
},
|
||||
],
|
||||
@@ -147,8 +128,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
[
|
||||
"indexOf",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber("indexOf", args[1], "start index")),
|
||||
(thisValue, args) => self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber(args[1])),
|
||||
],
|
||||
[
|
||||
"lastIndexOf",
|
||||
@@ -157,26 +137,19 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const target = self(thisValue, "lastIndexOf").bytes
|
||||
return args[1] === undefined
|
||||
? target.lastIndexOf(coerceToNumber(args[0]))
|
||||
: target.lastIndexOf(coerceToNumber(args[0]), optNumber("lastIndexOf", args[1], "start index"))
|
||||
: target.lastIndexOf(coerceToNumber(args[0]), optNumber(args[1]))
|
||||
},
|
||||
],
|
||||
[
|
||||
"includes",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
self(thisValue, "includes").bytes.includes(
|
||||
coerceToNumber(args[0]),
|
||||
optNumber("includes", args[1], "start index"),
|
||||
),
|
||||
(thisValue, args) => self(thisValue, "includes").bytes.includes(coerceToNumber(args[0]), optNumber(args[1])),
|
||||
],
|
||||
[
|
||||
"join",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
|
||||
throw typeError("Uint8Array.join expects zero arguments or one string separator.")
|
||||
}
|
||||
const joined = self(thisValue, "join").bytes.join(args.length === 0 ? "," : (args[0] as string))
|
||||
const joined = self(thisValue, "join").bytes.join(args[0] === undefined ? "," : coerceToString(args[0]))
|
||||
checkStringLength(joined.length)
|
||||
return joined
|
||||
},
|
||||
|
||||
@@ -3,10 +3,10 @@ import { methods } from "../interpreter/native.js"
|
||||
import { applyCollectionCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { checkStringLength } from "../interpreter/limits.js"
|
||||
import { syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { syntaxError } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { fromJson, toJson } from "../data.js"
|
||||
import { get, keys, Arr, Obj, record, remove, set, type Value } from "../interpreter/objects.js"
|
||||
import { get, keys, Arr, Obj, coerceToString, record, remove, set, type Value } from "../interpreter/objects.js"
|
||||
|
||||
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const json = new Obj(ctx.builtins.Object)
|
||||
@@ -18,8 +18,7 @@ export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
}
|
||||
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
|
||||
const text = coerceToString(args[0])
|
||||
|
||||
const parsed = (() => {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { constants, type Method, methods } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { Obj, type Value } from "../interpreter/objects.js"
|
||||
import { Obj, coerceToNumber } from "../interpreter/objects.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
@@ -14,31 +14,18 @@ declare global {
|
||||
|
||||
// Validate only the arguments a method consumes; like JS, extras are ignored
|
||||
// (so built-ins work as callbacks receiving (element, index, array)).
|
||||
const number = (name: string, args: Array<Value>, index: number): number => {
|
||||
if (index >= args.length) return Number.NaN
|
||||
const arg = args[index]
|
||||
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
|
||||
return arg
|
||||
}
|
||||
|
||||
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(number(name, args, 0))]
|
||||
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(coerceToNumber(args[0]))]
|
||||
|
||||
const binary = (name: string, op: (a: number, b: number) => number): Method => [
|
||||
name,
|
||||
2,
|
||||
(_, args) => op(number(name, args, 0), number(name, args, 1)),
|
||||
(_, args) => op(coerceToNumber(args[0]), coerceToNumber(args[1])),
|
||||
]
|
||||
|
||||
const variadic = (name: string, op: (...values: Array<number>) => number): Method => [
|
||||
name,
|
||||
2,
|
||||
(_, args) =>
|
||||
op(
|
||||
...args.map((arg) => {
|
||||
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
|
||||
return arg
|
||||
}),
|
||||
),
|
||||
(_, args) => op(...args.map(coerceToNumber)),
|
||||
]
|
||||
|
||||
export const mathGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { constructor, constants, methods } from "../interpreter/native.js"
|
||||
import { coerceToString, type Value } from "../interpreter/objects.js"
|
||||
import { coerceToNumber, coerceToString, type Value } from "../interpreter/objects.js"
|
||||
import { rangeError, typeError } from "../interpreter/model.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coercion } from "./value.js"
|
||||
@@ -30,11 +30,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
"parseInt",
|
||||
2,
|
||||
(_, args) => {
|
||||
const radix = args[1]
|
||||
if (radix !== undefined && typeof radix !== "number") {
|
||||
throw typeError("Number.parseInt expects a numeric radix.")
|
||||
}
|
||||
return parseInt(coerceToString(args[0]), radix)
|
||||
return parseInt(coerceToString(args[0]), coerceToNumber(args[1]))
|
||||
},
|
||||
],
|
||||
["parseFloat", 1, (_, args) => parseFloat(coerceToString(args[0]))],
|
||||
@@ -44,25 +40,17 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
if (typeof thisValue === "number") return thisValue
|
||||
throw typeError(`Number.prototype.${name} requires that 'this' be a Number.`)
|
||||
}
|
||||
const optNum = (name: string, arg: Value): number | undefined => {
|
||||
if (arg === undefined) return undefined
|
||||
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
|
||||
return arg
|
||||
}
|
||||
const optNum = (arg: Value): number | undefined => (arg === undefined ? undefined : coerceToNumber(arg))
|
||||
methods(builtins, builtins.Number, [
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum(args[0]))],
|
||||
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
|
||||
[
|
||||
"toExponential",
|
||||
1,
|
||||
(thisValue, args) => self(thisValue, "toExponential").toExponential(optNum("toExponential", args[0])),
|
||||
],
|
||||
["toExponential", 1, (thisValue, args) => self(thisValue, "toExponential").toExponential(optNum(args[0]))],
|
||||
[
|
||||
"toPrecision",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const value = self(thisValue, "toPrecision")
|
||||
const digits = optNum("toPrecision", args[0])
|
||||
const digits = optNum(args[0])
|
||||
return digits === undefined ? value.toString() : value.toPrecision(digits)
|
||||
},
|
||||
],
|
||||
@@ -71,7 +59,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const value = self(thisValue, "toString")
|
||||
const radix = optNum("toString", args[0])
|
||||
const radix = optNum(args[0])
|
||||
if (radix !== undefined && (radix < 2 || radix > 36)) {
|
||||
throw rangeError("Number.toString radix must be between 2 and 36.")
|
||||
}
|
||||
|
||||
@@ -93,15 +93,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const codeUnits = (name: string, op: (...codes: Array<number>) => string): Method => [
|
||||
name,
|
||||
1,
|
||||
(_, args) =>
|
||||
op(
|
||||
...args.map((arg) => {
|
||||
if (typeof arg !== "number") {
|
||||
throw typeError(`String.${name} expects number arguments.`)
|
||||
}
|
||||
return arg
|
||||
}),
|
||||
),
|
||||
(_, args) => op(...args.map(coerceToNumber)),
|
||||
]
|
||||
methods(builtins, string, [
|
||||
codeUnits("fromCharCode", String.fromCharCode),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fn } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { coerceToNumber, coerceToString, type Native, type Value } from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
@@ -20,11 +19,7 @@ const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<Value>): Val
|
||||
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
|
||||
if (name === "isNaN") return Number.isNaN(coerceToNumber(raw))
|
||||
if (name === "parseInt") {
|
||||
const radix = args[1]
|
||||
if (radix !== undefined && typeof radix !== "number") {
|
||||
throw typeError("parseInt expects a numeric radix.")
|
||||
}
|
||||
return parseInt(coerceToString(raw), radix)
|
||||
return parseInt(coerceToString(raw), coerceToNumber(args[1]))
|
||||
}
|
||||
if (name === "parseFloat") return parseFloat(coerceToString(raw))
|
||||
return coerceToString(raw)
|
||||
|
||||
@@ -271,10 +271,8 @@ describe("still-rejected callables get the wrap hint", () => {
|
||||
test("built-in references work as replacers", async () => {
|
||||
// Like real JS: JSON.stringify(match, offset, string) quotes the match.
|
||||
expect(await value(`return "abc".replace(/b/, JSON.stringify)`)).toBe('a"b"c')
|
||||
// Math methods stay strict about consumed arguments: a match string is not coerced.
|
||||
expect((await error(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).message).toContain(
|
||||
"Math.floor expects number arguments",
|
||||
)
|
||||
// Math methods coerce the match string, as in JS.
|
||||
expect(await value(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).toBe("3")
|
||||
})
|
||||
|
||||
test("non-callables still get the plain callback error", async () => {
|
||||
|
||||
@@ -839,6 +839,36 @@ describe("coercion parity: global isFinite and isNaN", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("coercion parity: built-in arguments coerce as in JS", () => {
|
||||
test("numeric arguments apply ToIntegerOrInfinity", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
[1, 2, 3].indexOf(2, "1"), [1, 2, 3].lastIndexOf(3, "5"), [1, 2, 3].includes(1, "1"),
|
||||
[1, 2, 3, 4].slice("1", "3"), [1, 2, 3].at(null), [1, 2, 3].at(1.7),
|
||||
[1, [2, [3]]].flat(1.9), [1, 2, 3].with(1.5, 9), [1, 2, 3, 4].splice("1", "2"),
|
||||
Math.max("3", "2"), Math.floor(null), Math.hypot("3", "4"),
|
||||
parseInt("11", "2"), Number.parseInt("ff", "16"), (1.5).toFixed("2"), (255).toString("16"),
|
||||
String.fromCharCode("65", 66.9), new Uint8Array([1, 2, 3]).indexOf(2, "1"),
|
||||
]
|
||||
`),
|
||||
).toEqual([1, 2, false, [2, 3], 1, 2, [1, 2, [3]], [1, 9, 3], [2, 3], 3, 0, 5, 3, 255, "1.50", "ff", "AB", 1])
|
||||
})
|
||||
|
||||
test("join separators, JSON.parse text, and Array.from length coerce", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
[1, 2].join(null), [1, 2].join(0), [1, 2].join(undefined), new Uint8Array([1, 2]).join(null),
|
||||
JSON.parse(123), JSON.parse(true),
|
||||
Array.from({ length: "2" }), Array.from({ length: 2.5 }), Array.from({ length: -1 }), Array.from({}),
|
||||
]
|
||||
`),
|
||||
).toEqual(["1null2", "102", "1,2", "1null2", 123, true, [null, null], [null, null], [], []])
|
||||
expect((await error(`return JSON.parse(undefined)`)).message).toContain("JSON")
|
||||
})
|
||||
})
|
||||
|
||||
describe("coercion parity: arrays coerce to numbers through their string form", () => {
|
||||
test("arrays with objects become NaN instead of crashing on host ToPrimitive", async () => {
|
||||
expect(await value(`let x = [{}]; x++; return Number.isNaN(x)`)).toBe(true)
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
built-ins/Array/prototype/at/index-argument-tointeger.js # Array.at expects index to be a number.
|
||||
built-ins/Array/prototype/at/index-non-numeric-argument-tointeger.js # Array.at expects index to be a number.
|
||||
built-ins/Array/prototype/concat/S15.4.4.4_A2_T1.js # Array.prototype.concat called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/concat/S15.4.4.4_A2_T2.js # Array.prototype.concat called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/concat/S15.4.4.4_A3_T1.js # arr.hasOwnProperty("1") must return true Expected SameValue(«false», «true») to be true
|
||||
built-ins/Array/prototype/concat/S15.4.4.4_A3_T2.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
|
||||
built-ins/Array/prototype/concat/S15.4.4.4_A3_T3.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
|
||||
built-ins/Array/prototype/concat/create-ctor-non-object.js # a.concat() throws a TypeError exception Expected a TypeError to be thrown but no exception was throw
|
||||
built-ins/Array/prototype/copyWithin/coerced-values-end.js # Array.copyWithin expects end to be a number.
|
||||
built-ins/Array/prototype/copyWithin/coerced-values-start-change-target.js # Array.copyWithin expects start to be a number.
|
||||
built-ins/Array/prototype/copyWithin/coerced-values-start.js # Array.copyWithin expects start to be a number.
|
||||
built-ins/Array/prototype/copyWithin/coerced-values-target.js # Array.copyWithin expects target index to be a number.
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a TypeError
|
||||
@@ -23,7 +19,6 @@ built-ins/Array/prototype/every/15.4.4.16-8-5.js # … cannot be constructed: u
|
||||
built-ins/Array/prototype/every/15.4.4.16-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/every/15.4.4.16-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/every/15.4.4.16-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/fill/coerced-indexes.js # Array.fill expects start to be a number.
|
||||
built-ins/Array/prototype/fill/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/fill/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/filter/15.4.4.20-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
@@ -37,7 +32,6 @@ built-ins/Array/prototype/filter/15.4.4.20-6-8.js # … cannot be constructed:
|
||||
built-ins/Array/prototype/filter/15.4.4.20-9-6.js # resArr.length Expected SameValue(«3», «4») to be true
|
||||
built-ins/Array/prototype/filter/15.4.4.20-9-c-i-8.js # newArr.length Expected SameValue(«0», «1») to be true
|
||||
built-ins/Array/prototype/filter/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
|
||||
built-ins/Array/prototype/flat/non-numeric-depth-should-not-throw.js # Array.flat expects depth to be a number.
|
||||
built-ins/Array/prototype/flat/non-object-ctor-throws.js # null value Expected a TypeError to be thrown but no exception was thrown at all
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-7-5.js # callCnt Expected SameValue(«4», «5») to be true
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-7-c-i-8.js # testResult !== true
|
||||
@@ -50,57 +44,30 @@ built-ins/Array/prototype/forEach/15.4.4.18-8-6.js # … cannot be constructed:
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-8-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/includes/length-zero-returns-false.js # Array.includes expects a value and optional start index.
|
||||
built-ins/Array/prototype/includes/no-arg.js # Array.includes expects a value and optional start index.
|
||||
built-ins/Array/prototype/includes/return-abrupt-tointeger-fromindex.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/includes/tointeger-fromindex.js # Array.includes expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-1.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-15.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-16.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-17.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-18.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-19.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-20.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-21.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-22.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-23.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-24.js # toStringAccessed
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-3.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-5-5.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-7.js # Array assignment result contains a circular value.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-3.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-6.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-b-i-7.js # [, , , ].indexOf(true) Expected SameValue(«-1», «0») to be true
|
||||
built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js # Array.indexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A1.2_T2.js # Array.join expects zero arguments or one string separator.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A2_T1.js # Array.prototype.join called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A2_T4.js # Array.prototype.join called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js # Array.join expects zero arguments or one string separator.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T2.js # Array.join expects zero arguments or one string separator.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A3.2_T2.js # x.join() must return "*" Expected SameValue(«"[object Object]"», «"*"») to be true
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A4_T3.js # Array.prototype.join called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A5_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.join() === "0,1". Actual: 0,
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-15.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-16.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-17.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-18.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-19.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-20.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-21.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-22.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-23.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-24.js # toStringAccessed
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js # a.lastIndexOf(2,undefined) Expected SameValue(«1», «-1») to be true
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-7.js # Array assignment result contains a circular value.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-3.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-6.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-b-i-7.js # [, , , ].lastIndexOf(true) Expected SameValue(«-1», «0») to be true
|
||||
built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/map/15.4.4.19-8-6.js # resArr[4] Expected SameValue(«undefined», «1») to be true
|
||||
built-ins/Array/prototype/map/15.4.4.19-8-c-i-8.js # newArr[1] Expected SameValue(«13», «true») to be true
|
||||
built-ins/Array/prototype/map/15.4.4.19-9-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
@@ -175,7 +142,6 @@ built-ins/Array/prototype/shift/S15.4.4.9_A2_T5.js # Array.prototype.shift call
|
||||
built-ins/Array/prototype/shift/S15.4.4.9_A3_T3.js # Array.prototype.shift called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/shift/S15.4.4.9_A4_T1.js # #2: Array.prototype[1] = 1; x = [0]; x.length = 2; x.shift(); x[0] === 1. Actual: undefined
|
||||
built-ins/Array/prototype/shift/S15.4.4.9_A4_T2.js # Array.prototype.shift called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/slice/S15.4.4.10_A2.1_T5.js # Array.slice expects start to be a number.
|
||||
built-ins/Array/prototype/slice/S15.4.4.10_A2.2_T5.js # Array.slice expects end to be a number.
|
||||
built-ins/Array/prototype/slice/S15.4.4.10_A2_T1.js # Array.prototype.slice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/slice/S15.4.4.10_A2_T2.js # Array.prototype.slice called on incompatible receiver a data object.
|
||||
@@ -206,7 +172,6 @@ built-ins/Array/prototype/sort/S15.4.4.11_A4_T3.js # Array.prototype.sort calle
|
||||
built-ins/Array/prototype/sort/S15.4.4.11_A6_T2.js # Array.prototype.sort called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/sort/bug_596_1.js # #1: [object, object].sort(); counter < 2. Actual: 0
|
||||
built-ins/Array/prototype/sort/precise-prototype-element.js # Expected SameValue(«undefined», «4») to be true
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A2.1_T5.js # Array.splice expects start to be a number.
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A2.2_T5.js # Array.splice expects delete count to be a number.
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A2_T1.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A2_T2.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
|
||||
Reference in New Issue
Block a user