fix(codemode): honor Uint8Array.from mappers, compare byte searches without coercion, skip flatMap holes, and truncate fractional radixes (#57285)

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
Co-authored-by: sdivyanshu90 <divyanshu74.80@gmail.com>
Co-authored-by: icearia0219 <133481415+icearia0219@users.noreply.github.com>
This commit is contained in:
Aiden Cline
2026-10-20 00:16:55 -05:00
committed by GitHub
Unverified
parent 40cdf4fbdd
commit db45c950a9
6 changed files with 131 additions and 29 deletions
+8 -5
View File
@@ -359,8 +359,8 @@ reject }` object.
## Arrays
- [x] The `Array` constructor with or without `new`: `Array(a, b)` collects arguments and `Array(n)` creates a sparse
array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, and JSON handle holes like
JavaScript, and host results normalize holes to `null`.
array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, `flatMap`, and JSON handle
holes like JavaScript, and host results normalize holes to `null`.
- [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`, including the `Array.from` mapper form with
`(value, index)` arguments and stepwise synchronous iterator consumption.
- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`.
@@ -426,6 +426,8 @@ reject }` object.
`Number.parseInt`, and `Number.parseFloat`.
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, `valueOf`, and `toLocaleString`, which
always formats as `en-US` (`"1,234.5"`) so output does not depend on the host.
- [x] `Number.prototype.toString` truncates a fractional radix before validating the 2–36 range; invalid radixes
throw a catchable `RangeError`.
- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`,
`POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`.
- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`.
@@ -552,13 +554,14 @@ The only binary type. Bytes stay inside the program or cross to extensions as co
with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
- [x] `new Uint8Array(length | array | iterable | Uint8Array)`, `Uint8Array.from`, `Uint8Array.of`, `fromBase64`,
and `fromHex`. Lengths are capped like arrays.
and `fromHex`. Lengths are capped like arrays. The `Uint8Array.from` mapper and `thisArg` see the original
values, so `Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))` decodes base64.
- [x] Index reads and writes with JS byte semantics: values wrap modulo 256, out-of-range writes are ignored, indexes
cannot be deleted. `length` is a prototype accessor, so `Object.keys` lists only indexes.
- [x] `at`, `slice`, `subarray` (a view on the same bytes), `set`, `fill`, `reverse`, `indexOf`, `lastIndexOf`,
`includes`, `join`, `toString`, `toBase64`, `toHex`, and live `keys`, `values`, `entries`, and `[Symbol.iterator]`
iterators. Start indexes coerce as for arrays, and `lastIndexOf(x, undefined)` searches from index 0 while
`lastIndexOf(x)` searches from the end, as in JS.
iterators. Start indexes coerce as for arrays, search values do not (`bytes.includes("1")` is `false`), and
`lastIndexOf(x, undefined)` searches from index 0 while `lastIndexOf(x)` searches from the end, as in JS.
- [x] Spread, destructuring, `for...of`, `yield*`, `Array.from`, and `new Set(bytes)`. `Array.isArray` is false.
- [x] String coercion joins with commas; `JSON.stringify` gives `{"0":1,...}`; `console.log` prints
`Uint8Array(n) [...]`.
+6 -2
View File
@@ -375,8 +375,12 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
for (let index = 0; index < length; index += 1) {
if (!(index in target.items)) continue
const mapped = yield* apply([target.items[index], index, target], args[1])
if (mapped instanceof Arr) values.push(...mapped.items)
else values.push(mapped)
if (mapped instanceof Arr) {
// forEach skips holes, as flattening does; spreading would turn them into undefined.
mapped.items.forEach((item) => values.push(item))
continue
}
values.push(mapped)
}
return wrap(values)
})
+27 -15
View File
@@ -17,12 +17,23 @@ import {
type Value,
} from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
import { applyCollectionCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { callbackMethods, sortArray } from "./array.js"
/** The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. */
const collectBytes = <R>(ctx: Interpreter<R>, source: Value, name: string): Effect.Effect<Uint8Array, unknown, R> => {
if (source instanceof Bytes) return Effect.succeed(new Uint8Array(source.bytes))
/**
* The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. As in
* TypedArray.from, a mapper sees the original values, after the source is consumed in full.
*/
const collectBytes = <R>(
ctx: Interpreter<R>,
source: Value,
name: string,
mapper?: Value,
thisArg?: Value,
): Effect.Effect<Uint8Array, unknown, R> => {
const apply = mapper === undefined ? undefined : applyCollectionCallback(ctx, mapper, name)
if (source instanceof Bytes && apply === undefined) return Effect.succeed(new Uint8Array(source.bytes))
return Effect.gen(function* () {
const cursor = yield* ctx.iterate(source)
if (cursor === undefined) {
@@ -30,13 +41,16 @@ const collectBytes = <R>(ctx: Interpreter<R>, source: Value, name: string): Effe
`${name} expects a Uint8Array, an array, or an iterable of numbers, received ${describeValue(source)}.`,
)
}
const values: Array<number> = []
const values: Array<Value> = []
while (true) {
const step = yield* cursor.next
if (step.done) return Uint8Array.from(values)
values.push(coerceToNumber(step.value))
if (step.done) break
values.push(step.value)
checkArrayLength(values.length)
}
const mapped =
apply === undefined ? values : yield* Effect.forEach(values, (value, index) => apply([value, index], thisArg))
return Uint8Array.from(mapped, coerceToNumber)
})
}
@@ -70,7 +84,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
}
}
methods(builtins, uint8Array, [
["from", 1, (_, args) => Effect.map(collectBytes(ctx, args[0], "Uint8Array.from"), wrap)],
["from", 1, (_, args) => Effect.map(collectBytes(ctx, args[0], "Uint8Array.from", args[1], args[2]), wrap)],
["of", 0, (_, args) => wrap(Uint8Array.from(args, coerceToNumber))],
["fromBase64", 1, (_, args) => decode("fromBase64", args, (text) => Uint8Array.fromBase64(text))],
["fromHex", 1, (_, args) => decode("fromHex", args, (text) => Uint8Array.fromHex(text))],
@@ -78,6 +92,8 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
// Uint8Array cannot contain NaN, so non-number search values never match a byte.
const search = (value: Value) => (typeof value === "number" ? value : NaN)
const wrapAll = (items: Array<Value>) => new Arr(builtins.Array, items)
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
methods(builtins, proto, [
@@ -142,25 +158,21 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
})
},
],
[
"indexOf",
1,
(thisValue, args) => self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber(args[1])),
],
["indexOf", 1, (thisValue, args) => self(thisValue, "indexOf").bytes.indexOf(search(args[0]), optNumber(args[1]))],
[
"lastIndexOf",
1,
(thisValue, args) => {
const target = self(thisValue, "lastIndexOf").bytes
return args.length < 2
? target.lastIndexOf(coerceToNumber(args[0]))
: target.lastIndexOf(coerceToNumber(args[0]), optNumber(args[1]))
? target.lastIndexOf(search(args[0]))
: target.lastIndexOf(search(args[0]), optNumber(args[1]))
},
],
[
"includes",
1,
(thisValue, args) => self(thisValue, "includes").bytes.includes(coerceToNumber(args[0]), optNumber(args[1])),
(thisValue, args) => self(thisValue, "includes").bytes.includes(search(args[0]), optNumber(args[1])),
],
[
"join",
+2 -7
View File
@@ -1,6 +1,6 @@
import { constructor, constants, type Method, methods } from "../interpreter/native.js"
import { coerceToNumber, type Value } from "../interpreter/objects.js"
import { rangeError, typeError } from "../interpreter/model.js"
import { typeError } from "../interpreter/model.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { withPrimitives } from "../interpreter/callback.js"
import { coerce, coercion } from "./value.js"
@@ -51,12 +51,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
formatting("toExponential", (value, digits) => value.toExponential(digits)),
formatting("toPrecision", (value, digits) => (digits === undefined ? value.toString() : value.toPrecision(digits))),
formatting("toString", (value, radix) => {
if (radix !== undefined && (radix < 2 || radix > 36)) {
throw rangeError("Number.toString radix must be between 2 and 36.")
}
return value.toString(radix)
}),
formatting("toString", (value, radix) => value.toString(radix)),
["valueOf", 0, (thisValue) => self(thisValue, "valueOf")],
])
return number
@@ -337,6 +337,20 @@ describe("Test262 Array callback adaptations", () => {
})
describe("Array callback regressions", () => {
test("flatMap skips holes in callback results while retaining explicit undefined", async () => {
expect(
await value(`
return [
[1].flatMap(() => [, 2]),
[1, 2].flatMap(() => Array(2)),
[1].flatMap(() => [undefined, 2]),
[1].flatMap(() => [[, 2]])[0].length,
0 in [1].flatMap(() => [[, 2]])[0]
]
`),
).toEqual([[2], [], [null, 2], 2, false])
})
test("reduce and reduceRight find the first present element", async () => {
expect(
await value(`
+74
View File
@@ -36,6 +36,26 @@ const error = async (code: string) => {
}
describe("Number and Math", () => {
test("Number.toString truncates the radix before checking its range", async () => {
expect(
await value(`return [36.1, 36.9, "36.9", { valueOf() { return 36.9 } }].map(radix => (35).toString(radix))`),
).toEqual(["z", "z", "z", "z"])
})
test("Number.toString keeps invalid radixes catchable as RangeError", async () => {
expect(
await value(`return [0, 1.9, 37, NaN, Infinity, -Infinity].map(radix => {
try { return (35).toString(radix) }
catch (error) { return error instanceof RangeError }
})`),
).toEqual([true, true, true, true, true, true])
expect(await value(`return [(35).toString(), (35).toString(undefined), (35).toString(2.9)]`)).toEqual([
"35",
"35",
"100011",
])
})
test("Math.random returns a number in [0, 1)", async () => {
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
})
@@ -1003,6 +1023,36 @@ describe("Set", () => {
})
describe("Uint8Array", () => {
test("from maps the original values with thisArg, after consuming the source", async () => {
expect(
await value(`
const events = []
function* source() {
events.push("start")
yield 300
events.push("end")
}
return [
[...Uint8Array.from([300, 2], (value, index) => value / 2 + index)],
[...Uint8Array.from(new Uint8Array([2, 3]), (value, index) => value + 255 + index)],
[...Uint8Array.from([1, 2], function (value, index) { return value + index + this.offset }, { offset: 5 })],
[...Uint8Array.from(atob("aGk="), (c) => c.charCodeAt(0))],
[...Uint8Array.from([300, 2], undefined)],
[...Uint8Array.from(source(), (value) => { events.push("map"); return value })],
events,
]
`),
).toEqual([[150, 2], [1, 3], [6, 8], [104, 105], [44, 2], [44], ["start", "end", "map"]])
})
test("from rejects a non-callable mapper even for an empty source", async () => {
for (const mapper of ["42", "null", "{}"]) {
expect((await error(`return Uint8Array.from([], ${mapper}).length`)).message).toContain(
"TypeError: Uint8Array.from expects a function callback",
)
}
})
test("constructs from a length, an array, an iterable, or another Uint8Array", async () => {
expect(
await value(`
@@ -1927,6 +1977,30 @@ describe("CodeMode values at intra-CodeMode checkpoints", () => {
})
describe("Uint8Array callback methods", () => {
test.each(["'1'", "true", "false", "null", "{}", "undefined", "NaN"])(
"typed-array searches do not coerce %s into a byte",
async (search) => {
expect(
await value(`
const bytes = new Uint8Array([0, 1, 2, 1])
return [bytes.indexOf(${search}), bytes.lastIndexOf(${search}), bytes.includes(${search})]
`),
).toEqual([-1, -1, false])
},
)
test("typed-array searches keep numeric matching and start-index coercion", async () => {
expect(
await value(`
const bytes = new Uint8Array([0, 1, 2, 1])
return [
bytes.indexOf(1, "2"), bytes.lastIndexOf(1, "2"), bytes.includes(2, "3"),
bytes.indexOf(-0), bytes.includes(257), bytes.indexOf(1, -1)
]
`),
).toEqual([3, 1, false, 0, false, 3])
})
test("map and filter return new Uint8Arrays with clamped bytes", async () => {
expect(
await value(`