mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(codemode): add Uint8Array callback methods and coerce indexOf start indexes (#50829)
This commit is contained in:
@@ -310,7 +310,8 @@ reject }` object.
|
||||
`(value, index)` arguments and stepwise synchronous iterator consumption.
|
||||
- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`.
|
||||
- [x] Searching/tests: `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`, `includes`, `indexOf`, and
|
||||
`lastIndexOf`.
|
||||
`lastIndexOf`. An explicit `undefined` fromIndex counts as present: `[1, 2, 1].lastIndexOf(1, undefined)` is `0`
|
||||
while `lastIndexOf(1)` is `2`.
|
||||
- [x] Aggregation: `reduce` and `reduceRight`.
|
||||
- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, `join`, and `toLocaleString` (each element's
|
||||
@@ -495,11 +496,16 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
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.
|
||||
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.
|
||||
- [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) [...]`.
|
||||
- [ ] Callback methods (`forEach`, `map`, `filter`, `find`, `reduce`, ...); use `Array.from(bytes, fn)` meanwhile.
|
||||
- [x] Callback methods `forEach`, `map`, `filter`, `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`,
|
||||
`reduce`, and `reduceRight`, sharing the Array implementations; the callback receives `(byte, index, bytes)`.
|
||||
`map` and `filter` return new Uint8Arrays with results clamped like index writes (`map((b) => b * 100)` on
|
||||
`[1, 2, 3]` is `[100, 200, 44]`); `reduce` on an empty Uint8Array without an initial value is a `TypeError`.
|
||||
- [x] `sort` in place, numeric ascending by default (`[10, 9, 1]` sorts to `[1, 9, 10]`) or by comparator.
|
||||
- [ ] `ArrayBuffer`, `DataView`, and other typed arrays.
|
||||
|
||||
## Web platform helpers
|
||||
|
||||
@@ -125,24 +125,6 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
|
||||
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
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,
|
||||
length: number,
|
||||
body: (
|
||||
target: Array<Value>,
|
||||
receiver: Arr,
|
||||
apply: (args: Array<Value>) => Effect.Effect<Value, unknown, R>,
|
||||
args: Array<Value>,
|
||||
) => Effect.Effect<Value, unknown, R>,
|
||||
): Method => [
|
||||
name,
|
||||
length,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, name)
|
||||
return body(target.items, target, applyCollectionCallback(ctx, args[0], `Array.${name}`), args)
|
||||
},
|
||||
]
|
||||
|
||||
methods(builtins, proto, [
|
||||
[
|
||||
@@ -177,7 +159,8 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "lastIndexOf").items
|
||||
return args[1] === undefined ? target.lastIndexOf(args[0]) : target.lastIndexOf(args[0], optNumber(args[1]))
|
||||
// An explicit undefined is a fromIndex of 0, unlike omitting it.
|
||||
return args.length < 2 ? target.lastIndexOf(args[0]) : target.lastIndexOf(args[0], optNumber(args[1]))
|
||||
},
|
||||
],
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber(args[0]) ?? 0)],
|
||||
@@ -349,6 +332,60 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
.map(([index, item]) => wrap([index, item])),
|
||||
),
|
||||
],
|
||||
[
|
||||
"flatMap",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "flatMap")
|
||||
const apply = applyCollectionCallback(ctx, args[0], "Array.flatMap")
|
||||
return Effect.gen(function* () {
|
||||
const length = target.items.length
|
||||
const values: Array<Value> = []
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target.items)) continue
|
||||
const mapped = yield* apply([target.items[index], index, target])
|
||||
if (mapped instanceof Arr) values.push(...mapped.items)
|
||||
else values.push(mapped)
|
||||
}
|
||||
return wrap(values)
|
||||
})
|
||||
},
|
||||
],
|
||||
...callbackMethods(ctx, "Array", self, (target) => target.items, wrap),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return array
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback methods Array and Uint8Array share. They fix the iteration length while reading existing elements
|
||||
* live; `wrap` builds the collection `map` and `filter` return.
|
||||
*/
|
||||
export const callbackMethods = <R, T extends Obj>(
|
||||
ctx: Interpreter<R>,
|
||||
label: string,
|
||||
self: (thisValue: Value, name: string) => T,
|
||||
elements: (target: T) => ArrayLike<Value>,
|
||||
wrap: (values: Array<Value>) => Value,
|
||||
): Array<Method> => {
|
||||
const iterate = (
|
||||
name: string,
|
||||
length: number,
|
||||
body: (
|
||||
target: ArrayLike<Value>,
|
||||
receiver: T,
|
||||
apply: (args: Array<Value>) => Effect.Effect<Value, unknown, R>,
|
||||
args: Array<Value>,
|
||||
) => Effect.Effect<Value, unknown, R>,
|
||||
): Method => [
|
||||
name,
|
||||
length,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, name)
|
||||
return body(elements(target), target, applyCollectionCallback(ctx, args[0], `${label}.${name}`), args)
|
||||
},
|
||||
]
|
||||
return [
|
||||
iterate("map", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
@@ -361,19 +398,6 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return wrap(values)
|
||||
}),
|
||||
),
|
||||
iterate("flatMap", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
const values: Array<Value> = []
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const mapped = yield* apply([target[index], index, receiver])
|
||||
if (mapped instanceof Arr) values.push(...mapped.items)
|
||||
else values.push(mapped)
|
||||
}
|
||||
return wrap(values)
|
||||
}),
|
||||
),
|
||||
iterate("filter", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
const length = target.length
|
||||
@@ -459,7 +483,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
if (args.length < 2) {
|
||||
while (start < length && !(start in target)) start += 1
|
||||
if (start === length) {
|
||||
throw typeError("Array.reduce of an empty array with no initial value.")
|
||||
throw typeError(`${label}.reduce of an empty array with no initial value.`)
|
||||
}
|
||||
accumulator = target[start]
|
||||
start += 1
|
||||
@@ -478,7 +502,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
if (args.length < 2) {
|
||||
while (start >= 0 && !(start in target)) start -= 1
|
||||
if (start < 0) {
|
||||
throw typeError("Array.reduceRight of an empty array with no initial value.")
|
||||
throw typeError(`${label}.reduceRight of an empty array with no initial value.`)
|
||||
}
|
||||
accumulator = target[start]
|
||||
start -= 1
|
||||
@@ -490,7 +514,5 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return accumulator
|
||||
}),
|
||||
),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return array
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.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> => {
|
||||
@@ -125,6 +126,22 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"sort",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "sort")
|
||||
// Without a comparator, typed arrays sort numerically rather than by string.
|
||||
if (args[0] === undefined) {
|
||||
target.bytes.sort()
|
||||
return target
|
||||
}
|
||||
return Effect.map(sortArray(ctx, [...target.bytes], args[0], "Uint8Array.sort"), (sorted) => {
|
||||
target.bytes.set(Uint8Array.from(sorted, coerceToNumber))
|
||||
return target
|
||||
})
|
||||
},
|
||||
],
|
||||
[
|
||||
"indexOf",
|
||||
1,
|
||||
@@ -135,7 +152,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "lastIndexOf").bytes
|
||||
return args[1] === undefined
|
||||
return args.length < 2
|
||||
? target.lastIndexOf(coerceToNumber(args[0]))
|
||||
: target.lastIndexOf(coerceToNumber(args[0]), optNumber(args[1]))
|
||||
},
|
||||
@@ -170,6 +187,13 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
.map(([index, byte]) => wrapAll([index, byte])),
|
||||
),
|
||||
],
|
||||
...callbackMethods(
|
||||
ctx,
|
||||
"Uint8Array",
|
||||
self,
|
||||
(target) => target.bytes,
|
||||
(values) => wrap(Uint8Array.from(values, coerceToNumber)),
|
||||
),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return uint8Array
|
||||
|
||||
@@ -1173,7 +1173,7 @@ describe("TextEncoder and TextDecoder", () => {
|
||||
test("crypto.getRandomValues fills the given bytes in place", async () => {
|
||||
expect(
|
||||
await value(
|
||||
`const b = new Uint8Array(16); const same = crypto.getRandomValues(b) === b; return [same, b.length, b.some ? 0 : Array.from(b).some((n) => n !== 0)]`,
|
||||
`const b = new Uint8Array(16); const same = crypto.getRandomValues(b) === b; return [same, b.length, b.some((n) => n !== 0)]`,
|
||||
),
|
||||
).toEqual([true, 16, true])
|
||||
expect((await error(`crypto.getRandomValues([1])`)).message).toContain("expects a Uint8Array, received an array")
|
||||
@@ -1923,3 +1923,97 @@ describe("CodeMode values at intra-CodeMode checkpoints", () => {
|
||||
expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Uint8Array callback methods", () => {
|
||||
test("map and filter return new Uint8Arrays with clamped bytes", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([1, 2, 3])
|
||||
const mapped = b.map((byte) => byte * 100)
|
||||
const filtered = b.filter((byte) => byte > 1)
|
||||
mapped[0] = 9
|
||||
return [
|
||||
[...mapped], mapped instanceof Uint8Array, Array.isArray(mapped), [...b], [...filtered],
|
||||
[...b.map(() => "7")], b.map((byte) => byte, {}).length, [...new Uint8Array().map((byte) => byte)],
|
||||
]
|
||||
`),
|
||||
).toEqual([[9, 200, 44], true, false, [1, 2, 3], [2, 3], [7, 7, 7], 3, []])
|
||||
})
|
||||
|
||||
test("find, findIndex, findLast, findLastIndex, some, every, and forEach", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([1, 2, 3])
|
||||
const seen = []
|
||||
b.forEach((byte, index, array) => seen.push([byte, index, array === b]))
|
||||
return [
|
||||
b.find((byte) => byte > 1), b.find((byte) => byte > 5) === undefined, b.findIndex((byte) => byte > 1),
|
||||
b.findIndex((byte) => byte > 5), b.findLast((byte) => byte < 3), b.findLastIndex((byte) => byte < 3),
|
||||
b.findLastIndex((byte) => byte > 9), b.some((byte) => byte > 2), b.every((byte) => byte > 2),
|
||||
new Uint8Array().some(() => true), new Uint8Array().every(() => false),
|
||||
b.every((byte, index, array) => array === b), seen,
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
2,
|
||||
true,
|
||||
1,
|
||||
-1,
|
||||
2,
|
||||
1,
|
||||
-1,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
[
|
||||
[1, 0, true],
|
||||
[2, 1, true],
|
||||
[3, 2, true],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test("reduce and reduceRight", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([1, 2, 3])
|
||||
return [
|
||||
b.reduce((sum, byte) => sum + byte), b.reduce((sum, byte) => sum + byte, 10),
|
||||
b.reduceRight((text, byte) => text + byte, ""), new Uint8Array().reduce((sum, byte) => sum + byte, 5),
|
||||
b.reduce((_, byte, index, array) => array === b && index, 0),
|
||||
]
|
||||
`),
|
||||
).toEqual([6, 16, "321", 5, 2])
|
||||
expect((await error(`new Uint8Array().reduce((sum, byte) => sum + byte)`)).message).toContain(
|
||||
"Uint8Array.reduce of an empty array with no initial value",
|
||||
)
|
||||
expect((await error(`new Uint8Array().reduceRight((sum, byte) => sum + byte)`)).message).toContain(
|
||||
"Uint8Array.reduceRight of an empty array with no initial value",
|
||||
)
|
||||
expect((await error(`new Uint8Array([1]).map(null)`)).message).toContain("Uint8Array.map expects a function")
|
||||
})
|
||||
|
||||
test("sort is numeric by default and in place, with an optional comparator", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([10, 9, 1])
|
||||
const same = b.sort() === b
|
||||
const desc = new Uint8Array([3, 1, 2]).sort((x, y) => y - x)
|
||||
return [same, [...b], [...desc], desc instanceof Uint8Array, [...new Uint8Array([2, 1]).sort(() => NaN)]]
|
||||
`),
|
||||
).toEqual([true, [1, 9, 10], [3, 2, 1], true, [2, 1]])
|
||||
expect((await error(`new Uint8Array([2, 1]).sort(null)`)).message).toContain("Uint8Array.sort expects a function")
|
||||
})
|
||||
|
||||
test("lastIndexOf treats an explicit undefined fromIndex as 0", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const a = [1, 2, 1]
|
||||
const b = new Uint8Array([1, 2, 1])
|
||||
return [a.lastIndexOf(1, undefined), a.lastIndexOf(1), a.lastIndexOf(2, undefined), b.lastIndexOf(1, undefined), b.lastIndexOf(1)]
|
||||
`),
|
||||
).toEqual([0, 2, -1, 0, 2])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,7 +64,6 @@ built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-22.js # Array.lastIndexOf exp
|
||||
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-4.js # a.lastIndexOf(2,undefined) Expected SameValue(«1», «-1») to be true
|
||||
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-b-i-7.js # [, , , ].lastIndexOf(true) Expected SameValue(«-1», «0») to be true
|
||||
|
||||
Reference in New Issue
Block a user