| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- /*
- * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
- * - test/language/statements/for-await-of/ticks-with-sync-iter-resolved-promise-and-constructor-lookup.js
- * - test/language/statements/for-await-of/ticks-with-async-iter-resolved-promise-and-constructor-lookup.js
- * - test/language/statements/for-await-of/async-func-dstr-let-ary-ptrn-elem-id-iter-val.js
- * - test/language/statements/for-await-of/async-func-decl-dstr-array-rest-after-element.js
- * - test/language/statements/for-await-of/iterator-close-non-throw-get-method-is-null.js
- * - test/language/statements/for-await-of/iterator-close-non-throw-get-method-non-callable.js
- * - test/language/statements/for-await-of/iterator-close-throw-get-method-non-callable.js
- *
- * Copyright (C) 2019 André Bargull. All rights reserved.
- * Copyright (C) 2020 Alexey Shvayka. 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, tools: {} }))
- 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("Test262 for-await-of adaptations", () => {
- test("awaits promise and plain values from a synchronous array", async () => {
- expect(
- await value(`
- const values = []
- for await (const item of [Promise.resolve(1), 2, new Promise((resolve) => resolve(3))]) {
- values.push(item)
- }
- return values
- `),
- ).toEqual([1, 2, 3])
- })
- test("defers the body even for an already-resolved or plain value", async () => {
- expect(
- await value(`
- const events = []
- const before = Promise.resolve().then(() => events.push("before"))
- for await (const item of [Promise.resolve(1), 2]) events.push("body " + item)
- await before
- return events
- `),
- ).toEqual(["before", "body 1", "body 2"])
- })
- test("an awaited rejection exits through normal try/catch", async () => {
- const result = await execute(`
- const values = []
- try {
- for await (const item of [Promise.resolve(1), Promise.reject("stop"), Promise.resolve(3)]) {
- values.push(item)
- }
- } catch (error) {
- return [values, error]
- }
- return "missed"
- `)
- expect(result.ok).toBe(true)
- if (!result.ok) return
- expect(result.value).toEqual([[1], "stop"])
- expect(result.warnings ?? []).toEqual([])
- })
- test("destructures after resolving each yielded value", async () => {
- expect(
- await value(`
- const values = []
- for await (const [first, ...rest] of [Promise.resolve([1, 2, 3])]) {
- values.push(first, rest)
- }
- return values
- `),
- ).toEqual([1, [2, 3]])
- })
- test("supports assignment targets", async () => {
- expect(
- await value(`
- let first
- let rest
- for await ([first, ...rest] of [Promise.resolve([1, 2, 3])]) {}
- return [first, rest]
- `),
- ).toEqual([1, [2, 3]])
- })
- test("preserves fresh lexical bindings per iteration", async () => {
- expect(
- await value(`
- const reads = []
- for await (const item of [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]) {
- reads.push(() => item)
- }
- return reads.map((read) => read())
- `),
- ).toEqual([1, 2, 3])
- })
- test("supports every existing collection iterable", async () => {
- expect(
- await value(`
- const string = []
- for await (const item of "ab") string.push(item)
- const set = []
- for await (const item of new Set([Promise.resolve(1), 2])) set.push(item)
- const map = []
- for await (const [key, item] of new Map([["a", 1], ["b", 2]])) map.push(key, item)
- const params = []
- for await (const [key, item] of new URLSearchParams("a=1&b=2")) params.push(key, item)
- return { string, set, map, params }
- `),
- ).toEqual({ string: ["a", "b"], set: [1, 2], map: ["a", 1, "b", 2], params: ["a", "1", "b", "2"] })
- })
- test("preserves labeled break and continue behavior", async () => {
- expect(
- await value(`
- const values = []
- outer: for await (const item of [1, 2, 3, 4]) {
- if (item === 2) continue outer
- if (item === 4) break outer
- values.push(item)
- }
- return values
- `),
- ).toEqual([1, 3])
- })
- test("drives a custom async iterator sequentially", async () => {
- expect(
- await value(`
- let index = 0
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- async next() {
- index += 1
- if (index > 3) return { done: true }
- return { done: false, value: index }
- },
- }
- const values = []
- for await (const item of iterator) values.push(item)
- return values
- `),
- ).toEqual([1, 2, 3])
- })
- test("leaves async iterator values under the iterator's control", async () => {
- expect(
- await value(`
- let done = false
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => done ? { done: true } : (done = true, { done: false, value: Promise.resolve(1) }),
- }
- for await (const item of iterator) return [item instanceof Promise, await item]
- `),
- ).toEqual([true, 1])
- })
- test("awaits synchronous results from an async iterator before the body", async () => {
- expect(
- await value(`
- const events = ["pre"]
- const ticks = Promise.resolve()
- .then(() => events.push("tick 1"))
- .then(() => events.push("tick 2"))
- let done = false
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: () => done ? { done: true } : (done = true, { done: false, value: Promise.resolve(1) }),
- }
- for await (const item of iterator) events.push(item instanceof Promise ? "loop" : "adopted")
- events.push("post")
- await ticks
- return events
- `),
- ).toEqual(["pre", "tick 1", "loop", "tick 2", "post"])
- })
- test("falls back to a custom synchronous iterator", async () => {
- expect(
- await value(`
- let index = 0
- const iterator = {
- [Symbol.iterator]: () => iterator,
- next() {
- index += 1
- return index > 2 ? { done: true } : { done: false, value: Promise.resolve(index) }
- },
- }
- const values = []
- for await (const item of iterator) values.push(item)
- return values
- `),
- ).toEqual([1, 2])
- })
- test("adopts terminal and close values from synchronous iterators", async () => {
- expect(
- await value(`
- const terminal = {
- [Symbol.iterator]: () => terminal,
- next: () => ({ done: true, value: Promise.reject("terminal") }),
- }
- let terminalError
- try {
- for await (const item of terminal) {}
- } catch (error) {
- terminalError = error
- }
- const closing = {
- [Symbol.iterator]: () => closing,
- next: () => ({ done: false, value: 1 }),
- return: () => ({ done: true, value: Promise.reject("close") }),
- }
- let closeError
- try {
- for await (const item of closing) break
- } catch (error) {
- closeError = error
- }
- return [terminalError, closeError]
- `),
- ).toEqual(["terminal", "close"])
- })
- test("captures the next method when acquiring the iterator", async () => {
- expect(
- await value(`
- let count = 0
- const next = () => {
- count += 1
- if (count === 1) iterator.next = () => ({ done: true })
- return count > 2 ? { done: true } : { done: false, value: count }
- }
- const iterator = { [Symbol.iterator]: () => iterator, next }
- const values = []
- for await (const item of iterator) values.push(item)
- return values
- `),
- ).toEqual([1, 2])
- })
- test("captures synchronous iterator result fields before suspending", async () => {
- expect(
- await value(`
- let count = 0
- const result = { done: false, value: 1 }
- const iterator = {
- [Symbol.iterator]: () => iterator,
- next() {
- count += 1
- if (count > 1) return { done: true }
- Promise.resolve().then(() => {
- result.done = true
- result.value = 2
- })
- return result
- },
- }
- const values = []
- for await (const item of iterator) values.push(item)
- return values
- `),
- ).toEqual([1])
- })
- test("preserves the async close turn for a sync iterator without return", async () => {
- expect(
- await value(`
- const events = []
- const iterator = {
- [Symbol.iterator]: () => iterator,
- next: () => ({ done: false, value: 1 }),
- }
- for await (const item of iterator) {
- Promise.resolve().then(() => events.push("reaction"))
- break
- }
- events.push("after")
- return events
- `),
- ).toEqual(["reaction", "after"])
- })
- test("defers synchronous iterator protocol errors", async () => {
- expect(
- await value(`
- const events = []
- const throwing = {
- [Symbol.iterator]: () => throwing,
- next() {
- Promise.resolve().then(() => events.push("next reaction"))
- throw "next"
- },
- }
- try {
- for await (const item of throwing) {}
- } catch (error) {
- events.push("next catch")
- }
- const malformed = {
- [Symbol.iterator]: () => malformed,
- next() {
- Promise.resolve().then(() => events.push("result reaction"))
- return 1
- },
- }
- try {
- for await (const item of malformed) {}
- } catch (error) {
- events.push("result catch")
- }
- const closing = {
- [Symbol.iterator]: () => closing,
- next: () => ({ done: false, value: 1 }),
- return() {
- Promise.resolve().then(() => events.push("return reaction"))
- throw "return"
- },
- }
- try {
- for await (const item of closing) break
- } catch (error) {
- events.push("return catch")
- }
- return events
- `),
- ).toEqual(["next reaction", "next catch", "result reaction", "result catch", "return reaction", "return catch"])
- })
- test("prefers Symbol.asyncIterator over Symbol.iterator", async () => {
- expect(
- await value(`
- let done = false
- const asyncIterator = {
- next: async () => done ? { done: true } : (done = true, { done: false, value: "async" }),
- }
- const syncIterator = { next: () => ({ done: true }) }
- const iterable = {
- [Symbol.asyncIterator]: () => asyncIterator,
- [Symbol.iterator]: () => syncIterator,
- }
- const values = []
- for await (const item of iterable) values.push(item)
- return values
- `),
- ).toEqual(["async"])
- })
- test("closes a custom iterator on abrupt loop completion", async () => {
- expect(
- await value(`
- let closed = 0
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => ({ done: false, value: 1 }),
- return: async () => (closed += 1, { done: true }),
- }
- for await (const item of iterator) break
- try {
- for await (const item of iterator) throw "stop"
- } catch (error) {}
- return closed
- `),
- ).toBe(2)
- })
- test("treats a null iterator return method as absent", async () => {
- expect(
- await value(`
- let count = 0
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => ({ done: false, value: 1 }),
- return: null,
- }
- for await (const item of iterator) {
- count += 1
- break
- }
- return count
- `),
- ).toBe(1)
- })
- test("a non-callable return method replaces break with TypeError", async () => {
- expect(
- await value(`
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => ({ done: false, value: 1 }),
- return: true,
- }
- try {
- for await (const item of iterator) break
- } catch (error) {
- return error.name
- }
- return "missed"
- `),
- ).toBe("TypeError")
- })
- test("a body throw wins over a non-callable return method", async () => {
- expect(
- await value(`
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => ({ done: false, value: 1 }),
- return: true,
- }
- try {
- for await (const item of iterator) throw "body"
- } catch (error) {
- return error
- }
- return "missed"
- `),
- ).toBe("body")
- })
- test("rejects a primitive iterator return result", async () => {
- expect(
- await value(`
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => ({ done: false, value: 1 }),
- return: async () => null,
- }
- try {
- for await (const item of iterator) break
- } catch (error) {
- return error.name
- }
- return "missed"
- `),
- ).toBe("TypeError")
- })
- test("propagates iterator acquisition failures", async () => {
- expect(
- await value(`
- const iterator = {
- [Symbol.asyncIterator]: () => { throw "acquire" },
- }
- try {
- for await (const item of iterator) {}
- } catch (error) {
- return error
- }
- return "missed"
- `),
- ).toBe("acquire")
- })
- test("rejects malformed iterator acquisition methods and results", async () => {
- expect(
- await value(`
- const names = []
- const invalid = [
- { [Symbol.asyncIterator]: true },
- { [Symbol.asyncIterator]: () => 1 },
- { [Symbol.asyncIterator]: () => ({ next: true }) },
- ]
- for (const iterator of invalid) {
- try {
- for await (const item of iterator) {}
- } catch (error) {
- names.push(error.name)
- }
- }
- return names
- `),
- ).toEqual(["TypeError", "TypeError", "TypeError"])
- })
- test("preserves iterator protocol keys through object copies", async () => {
- expect(
- await value(`
- let done = false
- const iterable = {
- plain: true,
- [Symbol.asyncIterator]: () => iterable,
- next: async () => done ? { done: true } : (done = true, { done: false, value: 1 }),
- }
- const spread = { ...iterable }
- const { plain, ...rest } = iterable
- const assigned = Object.assign({}, iterable)
- const values = []
- for await (const item of spread) values.push(item)
- return [
- values,
- Object.hasOwn(spread, Symbol.asyncIterator),
- Object.hasOwn(rest, Symbol.asyncIterator),
- Object.hasOwn(assigned, Symbol.asyncIterator),
- ]
- `),
- ).toEqual([[1], true, true, true])
- })
- test("rejects malformed iterator protocol results", async () => {
- const result = await execute(`
- const iterator = {
- [Symbol.asyncIterator]: () => iterator,
- next: async () => 1,
- }
- for await (const item of iterator) {}
- `)
- expect(result.ok).toBe(false)
- if (result.ok) return
- expect(result.error.message).toContain("Iterator next() result must be an object")
- })
- test("rejects objects without an iterator protocol method", async () => {
- const result = await execute(`for await (const item of { values: [1, 2] }) {}`)
- expect(result.ok).toBe(false)
- if (result.ok) return
- expect(result.error.message).toContain("or custom iterator value")
- })
- })
|