Compare commits

...
1 Commits
5 changed files with 188 additions and 43 deletions
+9 -3
View File
@@ -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
+59 -37
View File
@@ -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
]
}
+25 -1
View File
@@ -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
+95 -1
View File
@@ -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