mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
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:
committed by
GitHub
Unverified
parent
40cdf4fbdd
commit
db45c950a9
@@ -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) [...]`.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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(`
|
||||
|
||||
Reference in New Issue
Block a user