diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index dbcdf211eb7..84cab537556 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -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 diff --git a/packages/codemode/src/interpreter/objects.ts b/packages/codemode/src/interpreter/objects.ts index 84134eaa0e8..a46c5c3c5f1 100644 --- a/packages/codemode/src/interpreter/objects.ts +++ b/packages/codemode/src/interpreter/objects.ts @@ -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 diff --git a/packages/codemode/src/stdlib/array.ts b/packages/codemode/src/stdlib/array.ts index b8110568a7c..922d9a1473b 100644 --- a/packages/codemode/src/stdlib/array.ts +++ b/packages/codemode/src/stdlib/array.ts @@ -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 = (ctx: Interpreter) => { ]) 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 = (ctx: Interpreter) => { "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 = (ctx: Interpreter) => { "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 = (ctx: Interpreter) => { (thisValue, args) => { const flatten = (items: Array, depth: number): Array => 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 = (ctx: Interpreter) => { 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 = (ctx: Interpreter) => { (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 = (ctx: Interpreter) => { (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 = (ctx: Interpreter) => { (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 = (ctx: Interpreter) => { 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 }, ], diff --git a/packages/codemode/src/stdlib/bytes.ts b/packages/codemode/src/stdlib/bytes.ts index a9d60ec9f53..255fa625ba4 100644 --- a/packages/codemode/src/stdlib/bytes.ts +++ b/packages/codemode/src/stdlib/bytes.ts @@ -11,6 +11,7 @@ import { Bytes, hostIterator, Obj, + coerceToInteger, coerceToNumber, coerceToString, type Value, @@ -75,44 +76,28 @@ export const uint8ArrayGlobal = (ctx: Interpreter) => { ]) 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) => 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 = (ctx: Interpreter) => { 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 = (ctx: Interpreter) => { [ "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 = (ctx: Interpreter) => { 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 }, diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index 8a87463e513..335e2514f98 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -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 = (ctx: Interpreter) => { const json = new Obj(ctx.builtins.Object) @@ -18,8 +18,7 @@ export const jsonGlobal = (ctx: Interpreter) => { } const parse = (ctx: Interpreter, args: Array): Effect.Effect => { - const text = args[0] - if (typeof text !== "string") throw typeError("JSON.parse expects a string.") + const text = coerceToString(args[0]) const parsed = (() => { try { diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 23f35d50a52..8c1d15de01e 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -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, 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): 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 = (ctx: Interpreter) => { diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts index 369e66755de..4cb90ce151d 100644 --- a/packages/codemode/src/stdlib/number.ts +++ b/packages/codemode/src/stdlib/number.ts @@ -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 = (ctx: Interpreter) => { "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 = (ctx: Interpreter) => { 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 = (ctx: Interpreter) => { 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.") } diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index 84f69da4e12..0e27e920e1f 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -93,15 +93,7 @@ export const stringGlobal = (ctx: Interpreter) => { const codeUnits = (name: string, op: (...codes: Array) => 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), diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index 4308ad791da..0669f0b3350 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -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 = (ctx: Interpreter, name: Coercion, args: Array): 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) diff --git a/packages/codemode/test/callbacks.test.ts b/packages/codemode/test/callbacks.test.ts index 60bb8dacef1..9722ae19a1a 100644 --- a/packages/codemode/test/callbacks.test.ts +++ b/packages/codemode/test/callbacks.test.ts @@ -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 () => { diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 4b467dee7b0..cd8eff62ce4 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -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) diff --git a/packages/codemode/test/test262/skipped.txt b/packages/codemode/test/test262/skipped.txt index 1d2f633c9df..8da1051ea2f 100644 --- a/packages/codemode/test/test262/skipped.txt +++ b/packages/codemode/test/test262/skipped.txt @@ -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.