mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-06 19:04:55 +00:00
Replace record-keyed tools and the separate namespace-description map with an OpenAI-style array of named tools and namespaces. Namespace descriptions are optional, namespaces may nest, and duplicate or dotted names are rejected.
1271 lines
45 KiB
TypeScript
1271 lines
45 KiB
TypeScript
/*
|
|
* Portable portions adapted from Test262 at revision
|
|
* 250f204f23a9249ff204be2baec29600faae7b75. Exact source paths are cited
|
|
* beside the corresponding tests below.
|
|
*
|
|
* Copyright (C) 2013-2017 the V8 project authors. All rights reserved.
|
|
* Copyright (C) 2018 Valerie Young. All rights reserved.
|
|
* Copyright (C) 2020 Alexey Shvayka. All rights reserved.
|
|
* Copyright (C) 2022 Kevin Gibbons. All rights reserved.
|
|
* Copyright Ecma International. All rights reserved.
|
|
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
|
*/
|
|
import { describe, expect, test } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import { CodeMode } from "../src/index.js"
|
|
|
|
const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code }))
|
|
|
|
const value = async (code: string) => {
|
|
const result = await execute(code)
|
|
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
|
return result.value
|
|
}
|
|
|
|
describe("confined generators", () => {
|
|
// test/built-ins/GeneratorPrototype/next/return-yield-expr.js
|
|
test("is lazy and preserves next(value), nested suspension, return, and exhaustion", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* generate() {
|
|
events.push("start")
|
|
const received = yield 1 + (yield 2)
|
|
return received
|
|
}
|
|
const iterator = generate()
|
|
const before = events.slice()
|
|
const first = iterator.next(99)
|
|
const second = iterator.next(3)
|
|
const third = iterator.next(7)
|
|
const fourth = iterator.next(8)
|
|
return [before, events, first, second, third, fourth]
|
|
`),
|
|
).toEqual([
|
|
[],
|
|
["start"],
|
|
{ value: 2, done: false },
|
|
{ value: 4, done: false },
|
|
{ value: 7, done: true },
|
|
{ value: null, done: true },
|
|
])
|
|
})
|
|
|
|
test("routes throw and return through catch and finally", async () => {
|
|
expect(
|
|
await value(`
|
|
function* generate() {
|
|
try {
|
|
try { yield "try" } catch (error) { yield "caught " + error }
|
|
} finally {
|
|
yield "finally"
|
|
}
|
|
}
|
|
const iterator = generate()
|
|
return [iterator.next(), iterator.throw("boom"), iterator.return("done"), iterator.next()]
|
|
`),
|
|
).toEqual([
|
|
{ value: "try", done: false },
|
|
{ value: "caught boom", done: false },
|
|
{ value: "finally", done: false },
|
|
{ value: "done", done: true },
|
|
])
|
|
})
|
|
|
|
test("throws into a suspended generator and after exhaustion", async () => {
|
|
expect(
|
|
await value(`
|
|
function* generate() { yield 1 }
|
|
const iterator = generate()
|
|
iterator.next()
|
|
let suspended
|
|
let exhausted
|
|
try { iterator.throw("first") } catch (error) { suspended = error }
|
|
try { iterator.throw("second") } catch (error) { exhausted = error }
|
|
return [suspended, exhausted, iterator.next()]
|
|
`),
|
|
).toEqual(["first", "second", { value: null, done: true }])
|
|
})
|
|
|
|
test("rejects synchronous generator reentry", async () => {
|
|
expect(
|
|
await value(`
|
|
let iterator
|
|
function* generate() {
|
|
try { iterator.next() } catch (error) { return error.name }
|
|
}
|
|
iterator = generate()
|
|
return iterator.next()
|
|
`),
|
|
).toEqual({ value: "TypeError", done: true })
|
|
})
|
|
|
|
test("delegates yield*, forwards next values, and receives the delegate return value", async () => {
|
|
expect(
|
|
await value(`
|
|
function* inner() {
|
|
const input = yield 1
|
|
return input * 2
|
|
}
|
|
function* outer() {
|
|
const result = yield* inner()
|
|
return result + 1
|
|
}
|
|
const iterator = outer()
|
|
return [iterator.next(), iterator.next(4)]
|
|
`),
|
|
).toEqual([
|
|
{ value: 1, done: false },
|
|
{ value: 9, done: true },
|
|
])
|
|
})
|
|
|
|
test("delegates throw and return to custom iterators", async () => {
|
|
expect(
|
|
await value(`
|
|
const calls = []
|
|
let step = 0
|
|
const delegate = {
|
|
[Symbol.iterator]: () => delegate,
|
|
next(...args) {
|
|
calls.push(["next", args.length, args[0]])
|
|
step += 1
|
|
return step === 1 ? { value: "one", done: false } : { value: "end", done: true }
|
|
},
|
|
throw(value) {
|
|
calls.push(["throw", value])
|
|
return { value: "recovered", done: false }
|
|
},
|
|
return(value) {
|
|
calls.push(["return", value])
|
|
return { value: value + "!", done: true }
|
|
},
|
|
}
|
|
function* generate() { return yield* delegate }
|
|
const iterator = generate()
|
|
const first = iterator.next()
|
|
const second = iterator.throw("x")
|
|
const third = iterator.return("stop")
|
|
return [first, second, third, calls]
|
|
`),
|
|
).toEqual([
|
|
{ value: "one", done: false },
|
|
{ value: "recovered", done: false },
|
|
{ value: "stop!", done: true },
|
|
[
|
|
["next", 1, null],
|
|
["throw", "x"],
|
|
["return", "stop"],
|
|
],
|
|
])
|
|
})
|
|
|
|
test("uses the missing-throw delegation path for built-in iterables", async () => {
|
|
expect(
|
|
await value(`
|
|
function* generate() { yield* [1, 2] }
|
|
const iterator = generate()
|
|
iterator.next()
|
|
try { iterator.throw("boom") } catch (error) { return error.name }
|
|
`),
|
|
).toBe("TypeError")
|
|
})
|
|
|
|
test("exposes only the appropriate iterator symbol and works in for...of", async () => {
|
|
expect(
|
|
await value(`
|
|
function* generate() { yield 1; yield 2 }
|
|
const iterator = generate()
|
|
const symbols = [iterator[Symbol.iterator]() === iterator, iterator[Symbol.asyncIterator]]
|
|
const values = []
|
|
for (const item of iterator) values.push(item)
|
|
return [symbols, values]
|
|
`),
|
|
).toEqual([
|
|
[true, null],
|
|
[1, 2],
|
|
])
|
|
})
|
|
|
|
test("accepts generator methods as iterator acquisition results", async () => {
|
|
expect(
|
|
await value(`
|
|
const sync = {
|
|
*[Symbol.iterator]() { yield 1; yield 2 },
|
|
}
|
|
const asynchronous = {
|
|
async *[Symbol.asyncIterator]() { yield 3; yield 4 },
|
|
}
|
|
const values = []
|
|
for (const item of sync) values.push(item)
|
|
for await (const item of asynchronous) values.push(item)
|
|
return values
|
|
`),
|
|
).toEqual([1, 2, 3, 4])
|
|
})
|
|
|
|
test("closes a generator when for...of exits abruptly", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* generate() {
|
|
try { yield 1; yield 2 } finally { events.push("closed") }
|
|
}
|
|
for (const item of generate()) break
|
|
return events
|
|
`),
|
|
).toEqual(["closed"])
|
|
})
|
|
|
|
test("async generator requests are promises and execute in request order", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
async function* generate() {
|
|
events.push("start")
|
|
const input = yield Promise.resolve(1)
|
|
events.push("received " + input)
|
|
return Promise.resolve(3)
|
|
}
|
|
const iterator = generate()
|
|
const first = iterator.next()
|
|
const second = iterator.next(2)
|
|
const third = iterator.next(4)
|
|
const promiseFlags = [first instanceof Promise, second instanceof Promise, third instanceof Promise]
|
|
return [promiseFlags, await Promise.all([first, second, third]), events]
|
|
`),
|
|
).toEqual([
|
|
[true, true, true],
|
|
[
|
|
{ value: 1, done: false },
|
|
{ value: 3, done: true },
|
|
{ value: null, done: true },
|
|
],
|
|
["start", "received 2"],
|
|
])
|
|
})
|
|
|
|
test("keeps requests queued while a completed generator adopts return values", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
let resolve
|
|
const pending = new Promise((done) => { resolve = done })
|
|
async function* generate() { return 1 }
|
|
const iterator = generate()
|
|
const first = iterator.next()
|
|
const returned = iterator.return(pending)
|
|
const later = first.then(() => iterator.next()).then(() => events.push("later"))
|
|
returned.then(() => events.push("returned"))
|
|
await first
|
|
await Promise.resolve()
|
|
const before = events.slice()
|
|
resolve(9)
|
|
await Promise.all([returned, later])
|
|
return [before, events]
|
|
`),
|
|
).toEqual([[], ["returned", "later"]])
|
|
})
|
|
|
|
test("serializes requests made after async generator exhaustion", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
let resolve
|
|
const pending = new Promise((done) => { resolve = done })
|
|
async function* generate() { return 1 }
|
|
const iterator = generate()
|
|
await iterator.next()
|
|
const returned = iterator.return(pending).then(() => events.push("returned"))
|
|
const later = iterator.next().then(() => events.push("later"))
|
|
await Promise.resolve()
|
|
const before = events.slice()
|
|
resolve(9)
|
|
await Promise.all([returned, later])
|
|
return [before, events]
|
|
`),
|
|
).toEqual([[], ["returned", "later"]])
|
|
})
|
|
|
|
test("async generators adopt yielded, returned, and return-request promises", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* yielded() { yield Promise.resolve(1) }
|
|
async function* returned() { return Promise.resolve(2) }
|
|
async function* pending() { yield 0 }
|
|
const first = yielded()
|
|
const second = returned()
|
|
const third = pending()
|
|
const exhausted = returned()
|
|
await third.next()
|
|
await exhausted.next()
|
|
return await Promise.all([
|
|
first.next(),
|
|
second.next(),
|
|
third.return(Promise.resolve(3)),
|
|
exhausted.return(Promise.resolve(4)),
|
|
])
|
|
`),
|
|
).toEqual([
|
|
{ value: 1, done: false },
|
|
{ value: 2, done: true },
|
|
{ value: 3, done: true },
|
|
{ value: 4, done: true },
|
|
])
|
|
})
|
|
|
|
test("awaits return-request promises before injecting completion", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
async function* generate() {
|
|
try {
|
|
yield 1
|
|
} catch (error) {
|
|
events.push("caught " + error)
|
|
yield "recovered"
|
|
} finally {
|
|
events.push("finally")
|
|
}
|
|
}
|
|
const iterator = generate()
|
|
const first = await iterator.next()
|
|
const returned = await iterator.return(Promise.reject("bad"))
|
|
const beforeNext = events.slice()
|
|
const last = await iterator.next()
|
|
return [first, returned, beforeNext, last, events]
|
|
`),
|
|
).toEqual([
|
|
{ value: 1, done: false },
|
|
{ value: "recovered", done: false },
|
|
["caught bad"],
|
|
{ value: null, done: true },
|
|
["caught bad", "finally"],
|
|
])
|
|
})
|
|
|
|
test("loop consumers call iterator next with no arguments", async () => {
|
|
expect(
|
|
await value(`
|
|
const calls = []
|
|
let syncStep = 0
|
|
const sync = {
|
|
[Symbol.iterator]: () => sync,
|
|
next(...args) {
|
|
calls.push(["sync", args.length])
|
|
syncStep += 1
|
|
return { value: syncStep, done: syncStep > 1 }
|
|
},
|
|
}
|
|
let asyncStep = 0
|
|
const asynchronous = {
|
|
[Symbol.asyncIterator]: () => asynchronous,
|
|
async next(...args) {
|
|
calls.push(["async", args.length])
|
|
asyncStep += 1
|
|
return { value: asyncStep, done: asyncStep > 1 }
|
|
},
|
|
}
|
|
for (const item of sync) {}
|
|
for await (const item of asynchronous) {}
|
|
return calls
|
|
`),
|
|
).toEqual([
|
|
["sync", 0],
|
|
["sync", 0],
|
|
["async", 0],
|
|
["async", 0],
|
|
])
|
|
})
|
|
|
|
test("supports async generators in for await...of and keeps them out of for...of", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* generate() { yield 1; yield await Promise.resolve(2) }
|
|
const values = []
|
|
for await (const item of generate()) values.push(item)
|
|
let name
|
|
try { for (const item of generate()) {} } catch (error) { name = error.name }
|
|
return [values, name]
|
|
`),
|
|
).toEqual([[1, 2], "TypeError"])
|
|
})
|
|
|
|
test("keeps generator references opaque at the data boundary", async () => {
|
|
const result = await execute(`function* generate() { yield 1 } return generate()`)
|
|
expect(result.ok).toBe(false)
|
|
if (result.ok) return
|
|
expect(result.error.kind).toBe("InvalidDataValue")
|
|
})
|
|
|
|
// test/built-ins/GeneratorPrototype/return/from-state-suspended-start.js
|
|
// test/built-ins/GeneratorPrototype/throw/from-state-suspended-start.js
|
|
// test/built-ins/GeneratorPrototype/return/from-state-completed.js
|
|
// test/built-ins/GeneratorPrototype/throw/from-state-completed.js
|
|
test("honors sync return and throw in suspended-start and completed states", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* generate() { events.push("body"); yield 1 }
|
|
|
|
const returned = generate()
|
|
const startReturn = returned.return(7)
|
|
const afterReturn = returned.next()
|
|
|
|
const thrown = generate()
|
|
let startThrow
|
|
try { thrown.throw("start") } catch (error) { startThrow = error }
|
|
const afterThrow = thrown.next()
|
|
let completedThrow
|
|
try { thrown.throw("completed") } catch (error) { completedThrow = error }
|
|
|
|
return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events]
|
|
`),
|
|
).toEqual([
|
|
{ value: 7, done: true },
|
|
{ value: null, done: true },
|
|
"start",
|
|
{ value: null, done: true },
|
|
"completed",
|
|
[],
|
|
])
|
|
})
|
|
|
|
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-promise.js
|
|
// test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedStart.js
|
|
// test/built-ins/AsyncGeneratorPrototype/return/return-state-completed.js
|
|
// test/built-ins/AsyncGeneratorPrototype/throw/throw-state-completed.js
|
|
test("honors async return and throw in suspended-start and completed states", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
async function* generate() { events.push("body"); yield 1 }
|
|
|
|
const returned = generate()
|
|
const startReturn = await returned.return(Promise.resolve(7))
|
|
const afterReturn = await returned.next()
|
|
|
|
const thrown = generate()
|
|
let startThrow
|
|
try { await thrown.throw("start") } catch (error) { startThrow = error }
|
|
const afterThrow = await thrown.next()
|
|
let completedThrow
|
|
try { await thrown.throw("completed") } catch (error) { completedThrow = error }
|
|
|
|
return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events]
|
|
`),
|
|
).toEqual([
|
|
{ value: 7, done: true },
|
|
{ value: null, done: true },
|
|
"start",
|
|
{ value: null, done: true },
|
|
"completed",
|
|
[],
|
|
])
|
|
})
|
|
|
|
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally.js
|
|
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally-return.js
|
|
// test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedYield-try-finally-throw.js
|
|
test("runs finally yields and lets finally completions override requests", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* yielding() {
|
|
try { yield 1 } finally { yield 2 }
|
|
}
|
|
async function* returning() {
|
|
try { yield 1 } finally { return "override" }
|
|
}
|
|
async function* throwing() {
|
|
try { yield 1 } finally { throw "override" }
|
|
}
|
|
|
|
const first = yielding()
|
|
await first.next()
|
|
const finallyYield = await first.return("sent")
|
|
const preservedReturn = await first.next()
|
|
|
|
const second = returning()
|
|
await second.next()
|
|
const overriddenReturn = await second.return("sent")
|
|
|
|
const third = throwing()
|
|
await third.next()
|
|
let overriddenThrow
|
|
try { await third.throw("sent") } catch (error) { overriddenThrow = error }
|
|
return [finallyYield, preservedReturn, overriddenReturn, overriddenThrow]
|
|
`),
|
|
).toEqual([{ value: 2, done: false }, { value: "sent", done: true }, { value: "override", done: true }, "override"])
|
|
})
|
|
|
|
// test/language/statements/async-generator/yield-promise-reject-next-catch.js
|
|
// test/language/statements/async-generator/yield-promise-reject-next-yield-star-sync-iterator.js
|
|
test("rejects yielded promises and closes direct and sync-delegating async generators", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* direct() { yield Promise.reject("direct") }
|
|
async function* delegated() { yield* [Promise.reject("delegated"), "unreachable"] }
|
|
const results = []
|
|
for (const iterator of [direct(), delegated()]) {
|
|
try { await iterator.next() } catch (error) { results.push(error) }
|
|
results.push(await iterator.next())
|
|
}
|
|
return results
|
|
`),
|
|
).toEqual(["direct", { value: null, done: true }, "delegated", { value: null, done: true }])
|
|
})
|
|
|
|
// test/built-ins/AsyncFromSyncIteratorPrototype/next/for-await-iterator-next-rejected-promise-close.js
|
|
// test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-iterator-next-rejected-promise-close.js
|
|
// test/built-ins/AsyncFromSyncIteratorPrototype/throw/iterator-result-rejected-promise-close.js
|
|
test("closes sync iterators when async-from-sync values reject", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* loopSource() {
|
|
try { yield Promise.reject("loop") } finally { events.push("loop close") }
|
|
}
|
|
let loopError
|
|
try { for await (const item of loopSource()) {} } catch (error) { loopError = error }
|
|
|
|
function* yieldSource() {
|
|
try { yield Promise.reject("yield") } finally { events.push("yield close") }
|
|
}
|
|
async function* delegate() { yield* yieldSource() }
|
|
let yieldError
|
|
try { await delegate().next() } catch (error) { yieldError = error }
|
|
|
|
const throwing = {
|
|
[Symbol.iterator]: () => throwing,
|
|
next: () => ({ value: 1, done: false }),
|
|
throw: () => ({ value: Promise.reject("throw"), done: false }),
|
|
return: () => { events.push("throw close"); return {} },
|
|
}
|
|
async function* throwDelegate() { yield* throwing }
|
|
const iterator = throwDelegate()
|
|
await iterator.next()
|
|
let throwError
|
|
try { await iterator.throw("sent") } catch (error) { throwError = error }
|
|
return [loopError, yieldError, throwError, events]
|
|
`),
|
|
).toEqual(["loop", "yield", "throw", ["loop close", "yield close", "throw close"]])
|
|
})
|
|
|
|
test("does not close rejected terminal or delegated return values", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
const terminal = {
|
|
[Symbol.iterator]: () => terminal,
|
|
next: () => ({ value: Promise.reject("terminal"), done: true }),
|
|
return: () => { events.push("terminal close"); return {} },
|
|
}
|
|
let terminalError
|
|
try { for await (const item of terminal) {} } catch (error) { terminalError = error }
|
|
|
|
let returnCount = 0
|
|
const returned = {
|
|
[Symbol.iterator]: () => returned,
|
|
next: () => ({ value: 1, done: false }),
|
|
return: () => {
|
|
returnCount += 1
|
|
return { value: Promise.reject("return"), done: false }
|
|
},
|
|
}
|
|
async function* delegate() { yield* returned }
|
|
const iterator = delegate()
|
|
await iterator.next()
|
|
let returnError
|
|
try { await iterator.return("sent") } catch (error) { returnError = error }
|
|
return [terminalError, returnError, returnCount, events]
|
|
`),
|
|
).toEqual(["terminal", "return", 1, []])
|
|
})
|
|
|
|
test("serializes a mixed async next, throw, return, and next request queue", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* generate() {
|
|
try {
|
|
try { yield 1; yield 2 } catch (error) { yield "caught " + error }
|
|
} finally {
|
|
yield "finally"
|
|
}
|
|
}
|
|
const iterator = generate()
|
|
const first = iterator.next()
|
|
const thrown = iterator.throw("x")
|
|
const returned = iterator.return("done")
|
|
const last = iterator.next()
|
|
return await Promise.all([first, thrown, returned, last])
|
|
`),
|
|
).toEqual([
|
|
{ value: 1, done: false },
|
|
{ value: "caught x", done: false },
|
|
{ value: "finally", done: false },
|
|
{ value: "done", done: true },
|
|
])
|
|
})
|
|
|
|
// test/built-ins/AsyncGeneratorPrototype/next/request-queue-order.js
|
|
// test/built-ins/AsyncGeneratorPrototype/throw/request-queue-order-state-executing.js
|
|
// test/built-ins/AsyncGeneratorPrototype/return/request-queue-order-state-executing.js
|
|
test("orders async requests enqueued while generators are executing", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
let returned, returnRequest
|
|
async function* returnWhileExecuting() {
|
|
returnRequest = returned.return(42).then((result) => events.push(["return", result]))
|
|
yield 1
|
|
}
|
|
returned = returnWhileExecuting()
|
|
const firstReturn = returned.next().then((result) => events.push(["first return", result]))
|
|
await Promise.all([firstReturn, returnRequest])
|
|
|
|
let thrown, throwRequest
|
|
async function* throwWhileExecuting() {
|
|
throwRequest = thrown.throw("boom").catch((error) => events.push(["throw", error]))
|
|
yield 2
|
|
}
|
|
thrown = throwWhileExecuting()
|
|
const firstThrow = thrown.next().then((result) => events.push(["first throw", result]))
|
|
await Promise.all([firstThrow, throwRequest])
|
|
|
|
async function* queued() { yield "first"; yield "second" }
|
|
const iterator = queued()
|
|
const first = iterator.next()
|
|
const second = iterator.next()
|
|
const third = iterator.next()
|
|
await Promise.all([
|
|
third.then(() => events.push("third")),
|
|
second.then(() => events.push("second")),
|
|
first.then(() => events.push("first")),
|
|
])
|
|
return events
|
|
`),
|
|
).toEqual([
|
|
["first return", { value: 1, done: false }],
|
|
["return", { value: 42, done: true }],
|
|
["first throw", { value: 2, done: false }],
|
|
["throw", "boom"],
|
|
"first",
|
|
"second",
|
|
"third",
|
|
])
|
|
})
|
|
|
|
// test/language/statements/async-generator/yield-star-promise-not-unwrapped.js
|
|
// test/language/statements/async-generator/yield-star-sync-next.js
|
|
test("preserves async iterator promise values but unwraps async-from-sync values", async () => {
|
|
expect(
|
|
await value(`
|
|
const asyncValue = Promise.resolve("async")
|
|
const asynchronous = {
|
|
[Symbol.asyncIterator]: () => asynchronous,
|
|
next: () => ({ value: asyncValue, done: false }),
|
|
}
|
|
const syncValue = Promise.resolve("sync")
|
|
const synchronous = {
|
|
[Symbol.iterator]: () => synchronous,
|
|
next: () => ({ value: syncValue, done: false }),
|
|
}
|
|
async function* delegate(value) { yield* value }
|
|
const asyncResult = await delegate(asynchronous).next()
|
|
const syncResult = await delegate(synchronous).next()
|
|
return [asyncResult.value === asyncValue, await asyncResult.value, syncResult]
|
|
`),
|
|
).toEqual([true, "async", { value: "sync", done: false }])
|
|
})
|
|
|
|
test("captures delegated result done before awaiting async-from-sync values", async () => {
|
|
expect(
|
|
await value(`
|
|
const result = { value: Promise.resolve(1), done: false }
|
|
result.value.then(() => { result.done = true })
|
|
const source = {
|
|
[Symbol.iterator]: () => source,
|
|
next: () => result,
|
|
}
|
|
async function* delegate() { yield* source }
|
|
return await delegate().next()
|
|
`),
|
|
).toEqual({ value: 1, done: false })
|
|
})
|
|
|
|
// test/language/statements/async-generator/yield-star-async-next.js
|
|
// test/language/statements/async-generator/yield-star-sync-return.js
|
|
// test/language/statements/async-generator/yield-star-async-throw.js
|
|
test("forwards next, return, and throw through sync and async yield delegates", async () => {
|
|
expect(
|
|
await value(`
|
|
const calls = []
|
|
const make = (symbol, label) => {
|
|
let step = 0
|
|
const iterator = {
|
|
[symbol]: () => iterator,
|
|
next(...args) {
|
|
calls.push([label, "next", args.length, args[0]])
|
|
step += 1
|
|
return { value: step, done: false }
|
|
},
|
|
return(value) {
|
|
calls.push([label, "return", value])
|
|
return { value: value + "!", done: true }
|
|
},
|
|
throw(value) {
|
|
calls.push([label, "throw", value])
|
|
return { value: "caught " + value, done: false }
|
|
},
|
|
}
|
|
return iterator
|
|
}
|
|
async function* delegate(iterator) { return yield* iterator }
|
|
const sync = delegate(make(Symbol.iterator, "sync"))
|
|
const asynchronous = delegate(make(Symbol.asyncIterator, "async"))
|
|
const results = [await sync.next(9), await sync.next(2), await sync.throw("x"), await sync.return("stop")]
|
|
results.push(await asynchronous.next(9), await asynchronous.next(3), await asynchronous.throw("y"))
|
|
return [results, calls]
|
|
`),
|
|
).toEqual([
|
|
[
|
|
{ value: 1, done: false },
|
|
{ value: 2, done: false },
|
|
{ value: "caught x", done: false },
|
|
{ value: "stop!", done: true },
|
|
{ value: 1, done: false },
|
|
{ value: 2, done: false },
|
|
{ value: "caught y", done: false },
|
|
],
|
|
[
|
|
["sync", "next", 1, null],
|
|
["sync", "next", 1, 2],
|
|
["sync", "throw", "x"],
|
|
["sync", "return", "stop"],
|
|
["async", "next", 1, null],
|
|
["async", "next", 1, 3],
|
|
["async", "throw", "y"],
|
|
],
|
|
])
|
|
})
|
|
|
|
// test/language/statements/async-generator/yield-star-sync-return.js
|
|
// test/language/statements/async-generator/yield-star-async-throw.js
|
|
test("continues delegation when return and throw report done false", async () => {
|
|
expect(
|
|
await value(`
|
|
let returns = 0
|
|
const synchronous = {
|
|
[Symbol.iterator]: () => synchronous,
|
|
next: () => ({ value: "next", done: false }),
|
|
return(value) {
|
|
returns += 1
|
|
return { value: returns === 1 ? "return pending" : value, done: returns > 1 }
|
|
},
|
|
}
|
|
let throws = 0
|
|
const asynchronous = {
|
|
[Symbol.asyncIterator]: () => asynchronous,
|
|
next: () => ({ value: "next", done: false }),
|
|
throw(value) {
|
|
throws += 1
|
|
return { value: throws === 1 ? "throw pending" : value, done: throws > 1 }
|
|
},
|
|
}
|
|
async function* delegate(iterator) { return yield* iterator }
|
|
const returned = delegate(synchronous)
|
|
const thrown = delegate(asynchronous)
|
|
return [
|
|
await returned.next(),
|
|
await returned.return("first"),
|
|
await returned.return("second"),
|
|
await thrown.next(),
|
|
await thrown.throw("first"),
|
|
await thrown.throw("second"),
|
|
]
|
|
`),
|
|
).toEqual([
|
|
{ value: "next", done: false },
|
|
{ value: "return pending", done: false },
|
|
{ value: "second", done: true },
|
|
{ value: "next", done: false },
|
|
{ value: "throw pending", done: false },
|
|
{ value: "second", done: true },
|
|
])
|
|
})
|
|
|
|
// test/language/expressions/yield/star-rhs-iter-nrml-next-call-non-obj.js
|
|
// test/language/expressions/yield/star-rhs-iter-thrw-thrw-call-non-obj.js
|
|
// test/language/expressions/yield/star-rhs-iter-rtrn-rtrn-call-non-obj.js
|
|
// test/language/statements/async-generator/yield-star-next-not-callable-number-throw.js
|
|
test("rejects malformed yield delegate methods and iterator results", async () => {
|
|
expect(
|
|
await value(`
|
|
const run = (iterator, operation) => {
|
|
function* generate() {
|
|
try { yield* iterator } catch (error) { return error.name }
|
|
}
|
|
const value = generate()
|
|
const first = value.next()
|
|
return operation === "next" ? first : value[operation]()
|
|
}
|
|
const iterable = (fields) => ({ [Symbol.iterator]: () => fields })
|
|
const nextResult = run(iterable({ next: () => 1 }), "next")
|
|
const throwResult = run(iterable({ next: () => ({ done: false }), throw: () => 1 }), "throw")
|
|
const returnResult = run(iterable({ next: () => ({ done: false }), return: () => 1 }), "return")
|
|
|
|
const badAsync = {
|
|
[Symbol.asyncIterator]: () => ({ next: 1 }),
|
|
}
|
|
async function* asynchronous() {
|
|
try { yield* badAsync } catch (error) { return error.name }
|
|
}
|
|
return [nextResult, throwResult, returnResult, await asynchronous().next()]
|
|
`),
|
|
).toEqual([
|
|
{ value: "TypeError", done: true },
|
|
{ value: "TypeError", done: true },
|
|
{ value: "TypeError", done: true },
|
|
{ value: "TypeError", done: true },
|
|
])
|
|
})
|
|
|
|
// test/language/expressions/yield/captured-free-vars.js
|
|
// test/language/statements/generators/dflt-params-ref-prior.js
|
|
// test/language/expressions/generators/dflt-params-ref-prior.js
|
|
// test/language/expressions/object/method-definition/generator-no-yield.js
|
|
test("supports declaration, expression, and method forms with closures and parameters", async () => {
|
|
expect(
|
|
await value(`
|
|
const captured = 4
|
|
function* declaration(x, y = x, ...rest) { yield captured + y + rest[0] }
|
|
const expression = function* (x, y = x) { yield captured + y }
|
|
const object = { *method({ value }, extra = 1) { return captured + value + extra } }
|
|
return [declaration(2, undefined, 3).next(), expression(5).next(), object.method({ value: 6 }).next()]
|
|
`),
|
|
).toEqual([
|
|
{ value: 9, done: false },
|
|
{ value: 9, done: false },
|
|
{ value: 11, done: true },
|
|
])
|
|
})
|
|
|
|
// test/language/expressions/assignment/dstr/array-elem-iter-nrml-close.js
|
|
test("steps holes and rest and closes array binding and assignment patterns early", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* binding() {
|
|
try { events.push("b1"); yield 1; events.push("b2"); yield 2; events.push("b3"); yield 3; yield 4 }
|
|
finally { events.push("binding close") }
|
|
}
|
|
const [first, , third] = binding()
|
|
function* assignment() {
|
|
try { yield 5; yield 6; yield 7 }
|
|
finally { events.push("assignment close") }
|
|
}
|
|
let head, rest
|
|
;[head, ...rest] = assignment()
|
|
return [first, third, head, rest, events]
|
|
`),
|
|
).toEqual([1, 3, 5, [6, 7], ["b1", "b2", "b3", "binding close", "assignment close"]])
|
|
})
|
|
|
|
// test/language/statements/variable/dstr/ary-ptrn-elem-id-init-throws.js
|
|
// test/language/expressions/assignment/dstr/array-elem-iter-thrw-close-err.js
|
|
test("closes on destructuring defaults and preserves the binding error over return failure", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
const iterator = {
|
|
[Symbol.iterator]: () => iterator,
|
|
next: () => ({ value: undefined, done: false }),
|
|
return: () => { events.push("close"); throw "close error" },
|
|
}
|
|
let caught
|
|
try {
|
|
const [value = (() => { throw "binding error" })()] = iterator
|
|
} catch (error) { caught = error }
|
|
return [caught, events]
|
|
`),
|
|
).toEqual(["binding error", ["close"]])
|
|
})
|
|
|
|
test("does not close an exhausted iterator when a destructuring default fails", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
const iterator = {
|
|
[Symbol.iterator]: () => iterator,
|
|
next: () => ({ done: true }),
|
|
return: () => { events.push("close"); return {} },
|
|
}
|
|
try { const [item = (() => { throw "default" })()] = iterator } catch {}
|
|
return events
|
|
`),
|
|
).toEqual([])
|
|
})
|
|
|
|
test("consumes generators in array and argument spread without awaiting yielded promises", async () => {
|
|
expect(
|
|
await value(`
|
|
function* values() { yield 1; yield Promise.resolve(2); yield 3 }
|
|
const array = [...values()]
|
|
const args = ((...items) => items)(...values())
|
|
return [array[0], array[1] instanceof Promise, await array[1], args[2]]
|
|
`),
|
|
).toEqual([1, true, 2, 3])
|
|
})
|
|
|
|
test("constructs Map, Set, and URLSearchParams from generators lazily", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* pairs() { events.push(1); yield ["a", 1]; events.push(2); yield ["b", 2] }
|
|
function* values() { yield 1; yield 1; yield 2 }
|
|
const map = new Map(pairs())
|
|
const set = new Set(values())
|
|
const params = new URLSearchParams(pairs())
|
|
return [map.get("b"), [...set], params.toString(), events]
|
|
`),
|
|
).toEqual([2, [1, 2], "a=1&b=2", [1, 2, 1, 2]])
|
|
})
|
|
|
|
test("keeps built-in collection iteration live during callbacks", async () => {
|
|
expect(
|
|
await value(`
|
|
const map = new Map([[1, 1], [2, 2]])
|
|
const mapped = Array.from(map, (entry, index) => {
|
|
if (index === 0) map.set(3, 3)
|
|
return entry[0]
|
|
})
|
|
const set = new Set([1, 2])
|
|
const grouped = Map.groupBy(set, (item, index) => {
|
|
if (index === 0) set.add(3)
|
|
return "items"
|
|
})
|
|
return [mapped, grouped.get("items")]
|
|
`),
|
|
).toEqual([
|
|
[1, 2, 3],
|
|
[1, 2, 3],
|
|
])
|
|
})
|
|
|
|
test("keeps built-in collection iteration live in loops and yield delegation", async () => {
|
|
expect(
|
|
await value(`
|
|
const map = new Map([[1, 1], [2, 2]])
|
|
const mapValues = []
|
|
for (const [key] of map) {
|
|
mapValues.push(key)
|
|
if (key === 1) map.set(3, 3)
|
|
}
|
|
const set = new Set([1, 2])
|
|
const setValues = []
|
|
for await (const item of set) {
|
|
setValues.push(item)
|
|
if (item === 1) set.add(3)
|
|
}
|
|
const params = new URLSearchParams("a=1&b=2")
|
|
function* delegate() { yield* params }
|
|
const iterator = delegate()
|
|
const first = iterator.next()
|
|
params.append("c", "3")
|
|
return [mapValues, setValues, first, iterator.next(), iterator.next()]
|
|
`),
|
|
).toEqual([
|
|
[1, 2, 3],
|
|
[1, 2, 3],
|
|
{ value: ["a", "1"], done: false },
|
|
{ value: ["b", "2"], done: false },
|
|
{ value: ["c", "3"], done: false },
|
|
])
|
|
})
|
|
|
|
test("preserves async-from-sync turns for built-in loop completion and close", async () => {
|
|
expect(
|
|
await value(`
|
|
const completed = []
|
|
Promise.resolve().then(() => completed.push("reaction"))
|
|
for await (const item of []) {}
|
|
completed.push("after")
|
|
|
|
const closed = []
|
|
for await (const item of [1]) {
|
|
Promise.resolve().then(() => closed.push("reaction"))
|
|
break
|
|
}
|
|
closed.push("after")
|
|
return [completed, closed]
|
|
`),
|
|
).toEqual([
|
|
["reaction", "after"],
|
|
["reaction", "after"],
|
|
])
|
|
})
|
|
|
|
test("reads iterator return only when closing", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
const iterator = {
|
|
[Symbol.iterator]: () => iterator,
|
|
next() {
|
|
iterator.return = () => { events.push("new"); return {} }
|
|
return { value: 1, done: false }
|
|
},
|
|
return: () => { events.push("old"); return {} },
|
|
}
|
|
const [item] = iterator
|
|
return [item, events]
|
|
`),
|
|
).toEqual([1, ["new"]])
|
|
})
|
|
|
|
test("accepts iterable URLSearchParams entry pairs", async () => {
|
|
expect(
|
|
await value(`
|
|
function* pair() { yield "a"; yield 1 }
|
|
function* entries() { yield pair() }
|
|
return new URLSearchParams(entries()).toString()
|
|
`),
|
|
).toBe("a=1")
|
|
})
|
|
|
|
test("converts URLSearchParams pair elements before requesting the next", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* pair() {
|
|
try {
|
|
events.push("first")
|
|
yield (function* () {})()
|
|
events.push("second")
|
|
yield 2
|
|
} finally { events.push("pair close") }
|
|
}
|
|
function* entries() {
|
|
try { yield pair() } finally { events.push("outer close") }
|
|
}
|
|
let name
|
|
try { new URLSearchParams(entries()) } catch (error) { name = error.name }
|
|
return [events, name]
|
|
`),
|
|
).toEqual([["first", "pair close", "outer close"], "Error"])
|
|
})
|
|
|
|
test("validates URLSearchParams pair lengths after converting the outer sequence", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* entries() {
|
|
try {
|
|
events.push("one")
|
|
yield ["a"]
|
|
events.push("two")
|
|
yield ["b", 2]
|
|
} finally { events.push("outer close") }
|
|
}
|
|
let name
|
|
try { new URLSearchParams(entries()) } catch (error) { name = error.name }
|
|
return [events, name]
|
|
`),
|
|
).toEqual([["one", "two", "outer close"], "TypeError"])
|
|
})
|
|
|
|
test("closes entry constructors when a generator yields a malformed entry", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* mapEntries() { try { yield ["a", 1]; yield null } finally { events.push("map close") } }
|
|
function* parameterEntries() { try { yield ["a"]; yield ["b", 2] } finally { events.push("params close") } }
|
|
const names = []
|
|
try { new Map(mapEntries()) } catch (error) { names.push(error.name) }
|
|
try { new URLSearchParams(parameterEntries()) } catch (error) { names.push(error.name) }
|
|
return [names, events]
|
|
`),
|
|
).toEqual([
|
|
["TypeError", "TypeError"],
|
|
["map close", "params close"],
|
|
])
|
|
})
|
|
|
|
// test/built-ins/Array/from/iter-map-fn-args.js
|
|
// test/built-ins/Array/from/iter-map-fn-err.js
|
|
test("interleaves Array.from mapping and closes when its mapper fails", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* source() {
|
|
try { events.push("next 1"); yield 1; events.push("next 2"); yield 2 }
|
|
finally { events.push("close") }
|
|
}
|
|
const mapped = Array.from(source(), (item) => { events.push("map " + item); return item * 2 })
|
|
let caught
|
|
try { Array.from(source(), (item) => { throw "mapper " + item }) } catch (error) { caught = error }
|
|
return [mapped, caught, events]
|
|
`),
|
|
).toEqual([[2, 4], "mapper 1", ["next 1", "map 1", "next 2", "map 2", "close", "next 1", "close"]])
|
|
})
|
|
|
|
test("reads array-like Array.from values immediately before mapping", async () => {
|
|
expect(
|
|
await value(`
|
|
const source = { 0: 1, 1: 2, length: 2 }
|
|
return Array.from(source, (item, index) => {
|
|
if (index === 0) source[1] = 9
|
|
return item
|
|
})
|
|
`),
|
|
).toEqual([1, 9])
|
|
})
|
|
|
|
test("interleaves Object.groupBy and Map.groupBy callbacks and closes on callback failure", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* source() { try { events.push("next"); yield 1; events.push("next"); yield 2 } finally { events.push("close") } }
|
|
const object = Object.groupBy(source(), (item) => { events.push("object " + item); return item % 2 })
|
|
const map = Map.groupBy(source(), (item) => { events.push("map " + item); return item % 2 })
|
|
let caught
|
|
try { Object.groupBy(source(), () => { throw "callback" }) } catch (error) { caught = error }
|
|
return [object, Object.fromEntries(map), caught, events]
|
|
`),
|
|
).toEqual([
|
|
{ 0: [2], 1: [1] },
|
|
{ 0: [2], 1: [1] },
|
|
"callback",
|
|
["next", "object 1", "next", "object 2", "close", "next", "map 1", "next", "map 2", "close", "next", "close"],
|
|
])
|
|
})
|
|
|
|
test("consumes Promise combinator generators in order and observes rejections", async () => {
|
|
expect(
|
|
await value(`
|
|
function* items() { yield Promise.resolve(1); yield Promise.reject("bad"); yield 3 }
|
|
const settled = await Promise.allSettled(items())
|
|
let all, any
|
|
try { await Promise.all(items()) } catch (error) { all = error }
|
|
try { await Promise.any((function* () { yield Promise.reject("a"); yield Promise.reject("b") })()) }
|
|
catch (error) { any = error.errors }
|
|
const race = await Promise.race((function* () { yield 4; yield Promise.resolve(5) })())
|
|
return [settled, all, any, race]
|
|
`),
|
|
).toEqual([
|
|
[
|
|
{ status: "fulfilled", value: 1 },
|
|
{ status: "rejected", reason: "bad" },
|
|
{ status: "fulfilled", value: 3 },
|
|
],
|
|
"bad",
|
|
["a", "b"],
|
|
4,
|
|
])
|
|
})
|
|
|
|
test("finishes Promise combinator iterator consumption before returning the promise", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* items() { events.push("first"); yield 1; events.push("second"); yield 2 }
|
|
const promise = Promise.all(items())
|
|
events.push("after call")
|
|
await promise
|
|
return events
|
|
`),
|
|
).toEqual(["first", "second", "after call"])
|
|
})
|
|
|
|
// test/built-ins/Object/fromEntries/iterator-closed-for-null-entry.js
|
|
test("closes Object.fromEntries on malformed entries and consumes valid generators", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* valid() { yield ["a", 1]; yield ["b", 2] }
|
|
function* invalid() { try { yield ["a", 1]; yield null; yield ["c", 3] } finally { events.push("close") } }
|
|
let name
|
|
try { Object.fromEntries(invalid()) } catch (error) { name = error.name }
|
|
return [Object.fromEntries(valid()), name, events]
|
|
`),
|
|
).toEqual([{ a: 1, b: 2 }, "TypeError", ["close"]])
|
|
})
|
|
|
|
test("consumes AggregateError and Math.sumPrecise generators and closes on invalid numbers", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
function* errors() { yield "a"; yield "b" }
|
|
function* numbers() { yield 1e30; yield 0.1; yield -1e30 }
|
|
function* invalid() { try { yield 1; yield "bad"; yield 2 } finally { events.push("close") } }
|
|
const aggregate = new AggregateError(errors(), "message")
|
|
let name
|
|
try { Math.sumPrecise(invalid()) } catch (error) { name = error.name }
|
|
return [aggregate.errors, aggregate.message, Math.sumPrecise(numbers()), name, events]
|
|
`),
|
|
).toEqual([["a", "b"], "message", 0.1, "TypeError", ["close"]])
|
|
})
|
|
|
|
test("rejects async generators in every synchronous iterable consumer", async () => {
|
|
expect(
|
|
await value(`
|
|
async function* source() { yield ["a", 1] }
|
|
const checks = [
|
|
() => [...source()],
|
|
() => ((...items) => items)(...source()),
|
|
() => { const [item] = source(); return item },
|
|
() => Array.from(source()),
|
|
() => new Map(source()),
|
|
() => new Set(source()),
|
|
() => new URLSearchParams(source()),
|
|
() => Object.fromEntries(source()),
|
|
() => Object.groupBy(source(), (item) => item),
|
|
() => Math.sumPrecise(source()),
|
|
() => new AggregateError(source()),
|
|
]
|
|
const names = []
|
|
for (const check of checks) {
|
|
try { check() } catch (error) { names.push(error.name) }
|
|
}
|
|
try { await Promise.all(source()) } catch (error) { names.push(error.name) }
|
|
return names
|
|
`),
|
|
).toEqual(Array(12).fill("TypeError"))
|
|
})
|
|
|
|
// test/built-ins/Array/from/iter-get-iter-err.js
|
|
// test/built-ins/Array/from/iter-adv-err.js
|
|
test("does not close when iterator acquisition or next-result validation fails", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
const acquisition = { [Symbol.iterator]: () => { events.push("acquire"); throw "acquisition" } }
|
|
const malformed = {
|
|
[Symbol.iterator]: () => malformed,
|
|
next: () => { events.push("next"); return 1 },
|
|
return: () => { events.push("close"); return {} },
|
|
}
|
|
for (const source of [acquisition, malformed]) {
|
|
try { Array.from(source) } catch {}
|
|
}
|
|
return events
|
|
`),
|
|
).toEqual(["acquire", "next"])
|
|
})
|
|
|
|
test("reports synchronous iterator failures before queued promise reactions", async () => {
|
|
expect(
|
|
await value(`
|
|
const events = []
|
|
Promise.resolve().then(() => events.push("reaction"))
|
|
const iterator = {
|
|
[Symbol.iterator]: () => iterator,
|
|
next: () => { throw "next" },
|
|
}
|
|
try { Array.from(iterator) } catch { events.push("catch") }
|
|
await Promise.resolve()
|
|
return events
|
|
`),
|
|
).toEqual(["catch", "reaction"])
|
|
})
|
|
})
|