Parcourir la source

feat(codemode): support generator functions (#38172)

Aiden Cline il y a 2 semaines
Parent
commit
c8a40450e5

+ 43 - 16
packages/codemode/interpreter-support.md

@@ -29,7 +29,8 @@ ultimate source of truth.
 ## Values and literals
 
 - [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings.
-- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams.
+- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, custom synchronous
+      iterators, and synchronous generators.
 - [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and
       `undefined` are no-ops, while arrays are rejected.
 - [x] Template literals with interpolation.
@@ -57,7 +58,9 @@ ultimate source of truth.
 - [ ] Hoist function declarations accepted directly in switch cases.
 - [x] Computed object destructuring keys such as `const { [field]: value } = record`.
 - [x] Object destructuring from arrays, such as `const { length } = values`.
-- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams.
+- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
+      iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
+      or binding/default failure.
 
 ## Statements and control flow
 
@@ -65,7 +68,8 @@ ultimate source of truth.
 - [x] `if`/`else` and conditional expressions.
 - [x] `switch`, including default clauses and fallthrough.
 - [x] `for`, `while`, and `do...while`.
-- [x] `for...of` over arrays, strings, Maps, Sets, and URLSearchParams.
+- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and confined
+      synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
 - [x] `for...in` over own keys of plain objects, arrays, and tool references.
 - [x] Unlabeled `break` and `continue`.
 - [x] `try`, `catch`, optional catch bindings, and `finally`.
@@ -75,7 +79,7 @@ ultimate source of truth.
       `Symbol.asyncIterator` or the `Symbol.iterator` fallback. Each iterator step is sequential, yielded promises and
       plain values from synchronous collections and sync iterators are awaited before binding, and abrupt loop
       completion invokes the iterator's optional `return()`. Custom async iterators control their yielded values, as in
-      JavaScript; only their `next()` results are awaited. Async generators remain outside the supported subset.
+      JavaScript; only their `next()` results are awaited. Confined sync and async generators are iterable here.
 
 ## Functions and callbacks
 
@@ -104,7 +108,27 @@ ultimate source of truth.
 - [ ] User-defined constructor calls.
 - [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
 - [ ] Classes and private fields.
-- [ ] Generator functions and `yield`.
+- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
+      `next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
+      `try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
+      iterator but preserves values supplied by a manually implemented async iterator. Generator values are opaque
+      runtime references.
+- [x] Synchronous generators and custom synchronous iterators are consumed stepwise by array/argument spread, array
+      destructuring, `Array.from`, Map/Set/URLSearchParams construction, `Object.fromEntries`, Object/Map `groupBy`,
+      Promise combinators, `AggregateError`, and `Math.sumPrecise`. Mapper/grouping callbacks interleave with iterator
+      steps; synchronous consumers preserve yielded promise objects rather than awaiting them. Async generators are
+      rejected by every synchronous consumer.
+- [x] Synchronous iterator acquisition and result validation follow `IteratorClose` boundaries: consumer errors and
+      intentional early stops invoke `return()`, acquisition/`next()` failures do not, and an original consumer error
+      wins over a cleanup failure. Async iterator consumption remains limited to `for await...of` and async `yield*`.
+- [x] Portable generator protocol coverage is adapted from pinned Test262 cases for suspended-start, suspended-yield,
+      and completed states; sync and async `next`/`return`/`throw`; finally yields and completion overrides; rejected
+      yielded promises; mixed async request queues; sync and async `yield*` forwarding; malformed methods/results;
+      and declaration, expression, and object-method forms with closure and parameter behavior. The adapted suite
+      deliberately skips Test262 variants whose observation mechanism requires unsupported getter definitions,
+      proxies, prototype inspection or mutation, non-arrow `this`, classes, or arbitrary symbols. It also skips tests
+      asserting exact promise reaction-turn counts beyond the observable ordering guarantee documented below. These
+      are interpreter-surface boundaries, not claims that the corresponding full Test262 families pass unchanged.
 
 ## Expressions and operators
 
@@ -130,8 +154,8 @@ ultimate source of truth.
 - [x] Tool calls start eagerly and return supervised, run-once CodeMode promises.
 - [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
 - [x] `Promise.resolve` and `Promise.reject`.
-- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing
-      promises and plain values.
+- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over finite collections, custom synchronous
+      iterators, and synchronous generators containing promises and plain values.
 - [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings.
 - [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records.
 - [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
@@ -175,14 +199,16 @@ ultimate source of truth.
 - [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged;
       primitive wrapper objects (`Object(1)`) are rejected explicitly.
 - [x] Computed property names and object spread.
-- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`.
+- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with
+      synchronous iterator support for `fromEntries`.
 - [x] `Object.keys` over arrays and tool references.
 - [x] Object identity is preserved by in-CodeMode Object helpers.
 - [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
 - [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
       cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
 - [x] `Object.is` for supported data values.
-- [x] `Object.groupBy` over supported collection iterables, with string-key coercion and null-prototype results.
+- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
+      and null-prototype results.
 
 ## Arrays
 
@@ -190,7 +216,7 @@ ultimate source of truth.
       array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, 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.
+      `(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`.
@@ -245,7 +271,8 @@ ultimate source of truth.
       for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
       example `Math.sum is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw,
       and unknown `Promise` statics keep their descriptive error.
-- [x] `Math.sumPrecise` over supported collection iterables, rejecting non-number elements without coercion.
+- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number
+      elements without coercion.
 - [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
 
 ## JSON and console
@@ -297,10 +324,10 @@ ultimate source of truth.
 
 ## Map and Set
 
-- [x] Static `Map.groupBy` over supported collection iterables, preserving key identity.
-- [x] `new Map()` from entry arrays or another Map.
+- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
+- [x] `new Map()` from synchronous iterables of entries.
 - [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`.
-- [x] `new Set()` from arrays, strings, or another Set.
+- [x] `new Set()` from synchronous iterables.
 - [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
 - [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set.
 - [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
@@ -316,7 +343,7 @@ ultimate source of truth.
 - [x] Readable URL fields: `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`,
       `pathname`, `search`, and `hash`.
 - [x] Writable URL fields except `origin`.
-- [x] `new URLSearchParams()` from query strings, data objects, pairs, Maps, and URLSearchParams.
+- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams.
 - [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`,
       `entries`, `toString`, and `size`.
 - [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
@@ -326,7 +353,7 @@ ultimate source of truth.
 - [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
       or without `new`.
 - [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
-      an all-rejected `Promise.any`.
+      an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
 - [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
 - [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
 - [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited

+ 26 - 13
packages/codemode/src/interpreter/errors.ts

@@ -1,9 +1,10 @@
+import { Effect } from "effect"
 import type { Diagnostic } from "../codemode.js"
 import { ToolError } from "../tool-error.js"
 import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
 import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js"
 import { containsRuntimeReference } from "./references.js"
-import { spreadItems } from "../stdlib/collections.js"
+import { type SyncIteratorRunner } from "./iterator.js"
 import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js"
 
 export const normalizeError = (error: unknown): Diagnostic => {
@@ -79,15 +80,27 @@ export const caughtErrorValue = (thrown: unknown): unknown => {
   return createErrorValue(name, normalizeError(thrown).message)
 }
 
-export const constructErrorValue = (name: string, args: Array<unknown>, node: AstNode): SafeObject => {
-  if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
-  const errors = spreadItems(args[0])
-  if (errors === undefined) {
-    throw new InterpreterRuntimeError(
-      "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).",
-      node,
-    ).as("TypeError")
-  }
-  // Error values must not alias caller-owned arrays.
-  return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1]))
-}
+export const constructErrorValue = (name: string, args: Array<unknown>): SafeObject =>
+  createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
+
+export const constructAggregateErrorValue = <R>(
+  runner: SyncIteratorRunner<R>,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<SafeObject, unknown, R> =>
+  Effect.gen(function* () {
+    const cursor = yield* runner.syncIterator(args[0], node)
+    if (cursor === undefined) {
+      throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node).as(
+        "TypeError",
+      )
+    }
+    const errors: Array<unknown> = []
+    while (true) {
+      const step = yield* cursor.next
+      if (step.done) {
+        return createAggregateErrorValue(errors, args[1] === undefined ? "" : coerceToString(args[1]))
+      }
+      errors.push(step.value)
+    }
+  })

+ 21 - 0
packages/codemode/src/interpreter/iterator.ts

@@ -0,0 +1,21 @@
+import { Effect, Exit } from "effect"
+import type { AstNode } from "./model.js"
+
+export type IteratorCursor<R> = {
+  readonly next: Effect.Effect<{ readonly done: boolean; readonly value: unknown }, unknown, R>
+  readonly close: Effect.Effect<void, unknown, R>
+}
+
+export type SyncIteratorRunner<R> = {
+  readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>
+}
+
+export const preserveConsumerError = <A, R>(
+  cursor: IteratorCursor<R>,
+  effect: Effect.Effect<A, unknown, R>,
+): Effect.Effect<A, unknown, R> =>
+  Effect.flatMap(Effect.exit(effect), (exit) =>
+    Exit.isSuccess(exit)
+      ? Effect.succeed(exit.value)
+      : Effect.andThen(Effect.exit(cursor.close), Effect.failCause(exit.cause)),
+  )

+ 54 - 37
packages/codemode/src/interpreter/methods.ts

@@ -2,6 +2,7 @@ import { Effect } from "effect"
 import {
   type AstNode,
   CodeModeFunction,
+  CodeModeGenerator,
   CoercionFunction,
   ErrorConstructorReference,
   GlobalMethodReference,
@@ -33,6 +34,7 @@ import { invokeRegExpMethod, invokeRegExpStatic, matchToValue, toHostRegex } fro
 import { invokeStringStatic } from "../stdlib/string.js"
 import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
 import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js"
+import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js"
 
 export type CallbackRunner<R> = {
   readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
@@ -360,19 +362,12 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
       return Array.isArray(args[0])
     case "of":
       return [...args]
-    case "from":
-      return arrayFromItems(args[0], node)
     default:
       throw new InterpreterRuntimeError(`Array.${name} is not available.`, node)
   }
 }
 
-const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
-  if (source instanceof CodeModeMap) return Array.from(source.map.entries(), ([key, item]) => [key, item])
-  if (source instanceof CodeModeSet) return Array.from(source.set.values())
-  if (source instanceof CodeModeURLSearchParams) {
-    return Array.from(source.params.entries(), ([key, value]) => [key, value])
-  }
+const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
   if (source instanceof CodeModePromise) {
     throw new InterpreterRuntimeError(
       "Array.from received an un-awaited Promise; await it before creating the array.",
@@ -380,15 +375,16 @@ const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
       "InvalidDataValue",
     )
   }
-  if (typeof source === "string") return Array.from(source)
-  if (Array.isArray(source)) return [...source]
   if (
     source !== null &&
     typeof source === "object" &&
     (Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
     typeof (source as { length?: unknown }).length === "number"
   ) {
-    return Array.from(source as ArrayLike<unknown>)
+    const length = (source as { length: number }).length
+    const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
+    if (normalized > 4_294_967_295) throw new RangeError("Invalid array length")
+    return { length: normalized, source }
   }
   throw new InterpreterRuntimeError(
     "Array.from expects an array, string, Map, Set, or array-like value.",
@@ -398,24 +394,42 @@ const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
 }
 
 export const invokeArrayFrom = <R>(
-  runner: CallbackRunner<R>,
+  runner: CallbackRunner<R> & SyncIteratorRunner<R>,
   args: Array<unknown>,
   node: AstNode,
 ): Effect.Effect<unknown, unknown, R> => {
-  const items = arrayFromItems(args[0], node)
-  if (args.length < 2 || args[1] === undefined) return Effect.succeed(items)
-  const apply = applyCollectionCallback(runner, args[1], "Array.from", node)
+  const source = args[0]
+  const apply =
+    args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from", node)
   return Effect.gen(function* () {
+    const cursor = yield* runner.syncIterator(source, node)
+    if (cursor === undefined) {
+      if (source instanceof CodeModeGenerator) {
+        throw new InterpreterRuntimeError("Array.from expects a synchronous iterable or array-like value.", node).as(
+          "TypeError",
+        )
+      }
+      const arrayLike = arrayLikeSource(source, node)
+      const values: Array<unknown> = []
+      for (let index = 0; index < arrayLike.length; index += 1) {
+        const item = Reflect.get(arrayLike.source, index)
+        values.push(apply === undefined ? item : yield* apply([item, index]))
+      }
+      return values
+    }
     const values: Array<unknown> = []
-    for (let index = 0; index < items.length; index += 1) {
-      values.push(yield* apply([items[index], index]))
+    let index = 0
+    while (true) {
+      const step = yield* cursor.next
+      if (step.done) return values
+      values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
+      index += 1
     }
-    return values
   })
 }
 
 export const invokeGroupBy = <R>(
-  runner: CallbackRunner<R>,
+  runner: CallbackRunner<R> & SyncIteratorRunner<R>,
   namespace: "Map" | "Object",
   args: Array<unknown>,
   node: AstNode,
@@ -425,47 +439,50 @@ export const invokeGroupBy = <R>(
     throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
   }
   const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node)
-  const items = supportedIterableItems(source)
-  if (items === undefined) {
-    throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
-  }
   return Effect.gen(function* () {
+    const cursor = yield* runner.syncIterator(source, node)
+    if (cursor === undefined) {
+      throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
+    }
     if (namespace === "Map") {
       const result = new CodeModeMap()
       let index = 0
-      for (const item of items) {
-        const key = yield* apply([item, index])
+      while (true) {
+        const step = yield* cursor.next
+        if (step.done) return result
+        const item = step.value
+        const key = yield* preserveConsumerError(cursor, apply([item, index]))
         const group = result.map.get(key)
         if (group === undefined) result.map.set(key, [item])
         else (group as Array<unknown>).push(item)
         index += 1
       }
-      return result
     }
 
     const result: SafeObject = Object.create(null) as SafeObject
     let index = 0
-    for (const item of items) {
-      const key = yield* coerceGroupByPropertyKey(runner, yield* apply([item, index]), node)
+    while (true) {
+      const step = yield* cursor.next
+      if (step.done) return result
+      const item = step.value
+      const key = yield* preserveConsumerError(
+        cursor,
+        Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)),
+      )
       if (isBlockedMember(key)) {
-        throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
+        return yield* preserveConsumerError(
+          cursor,
+          Effect.fail(new InterpreterRuntimeError(`Property '${key}' is not available.`, node)),
+        )
       }
       const group = result[key]
       if (group === undefined) result[key] = [item]
       else (group as Array<unknown>).push(item)
       index += 1
     }
-    return result
   })
 }
 
-const supportedIterableItems = (source: unknown): Iterable<unknown> | undefined => {
-  if (Array.isArray(source) || typeof source === "string") return source
-  if (source instanceof CodeModeMap) return source.map.entries()
-  if (source instanceof CodeModeSet) return source.set.values()
-  if (source instanceof CodeModeURLSearchParams) return source.params.entries()
-}
-
 const coerceGroupByPropertyKey = <R>(
   runner: CallbackRunner<R>,
   value: unknown,

+ 26 - 0
packages/codemode/src/interpreter/model.ts

@@ -1,3 +1,4 @@
+import type { Effect } from "effect"
 import type { SafeObject } from "../tool-runtime.js"
 import type { CodeModePromise, CodeModeRegExp, CodeModeURL } from "../values.js"
 
@@ -45,6 +46,27 @@ export class CodeModeFunction {
     readonly body: AstNode,
     readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
     readonly async: boolean,
+    readonly generator: boolean,
+  ) {}
+}
+
+export type GeneratorRequestKind = "next" | "return" | "throw"
+
+export class CodeModeGenerator {
+  constructor(
+    readonly asynchronous: boolean,
+    readonly request: (
+      kind: GeneratorRequestKind,
+      value: unknown,
+      node: AstNode,
+    ) => Effect.Effect<unknown, unknown, unknown>,
+  ) {}
+}
+
+export class GeneratorMethodReference {
+  constructor(
+    readonly generator: CodeModeGenerator,
+    readonly kind: GeneratorRequestKind | "iterator",
   ) {}
 }
 
@@ -128,6 +150,10 @@ export class ProgramThrow {
   constructor(readonly value: unknown) {}
 }
 
+export class GeneratorReturn {
+  constructor(readonly value: unknown) {}
+}
+
 export class ErrorConstructorReference {
   constructor(readonly name: string) {}
 }

+ 73 - 78
packages/codemode/src/interpreter/promises.ts

@@ -13,9 +13,9 @@ import {
 import { caughtErrorValue, normalizeError } from "./errors.js"
 import { applyCollectionCallback, isSupportedCallback, type CallbackRunner, type SupportedCallback } from "./methods.js"
 import { typeofValue } from "./references.js"
-import { spreadItems } from "../stdlib/collections.js"
 import { createAggregateErrorValue } from "../stdlib/value.js"
 import { CodeModePromise } from "../values.js"
+import type { SyncIteratorRunner } from "./iterator.js"
 
 // Observation only controls rejection reporting; program completion interrupts all promise work.
 export class PromiseRuntime<R> {
@@ -64,6 +64,10 @@ export class PromiseRuntime<R> {
     return Fiber.await(promise.fiber)
   }
 
+  fork(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<void, never, R> {
+    return Effect.asVoid(Effect.forkIn(effect, this.scope, { startImmediately: true }))
+  }
+
   diagnostics(): Array<Diagnostic> {
     return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure)
   }
@@ -84,7 +88,7 @@ export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
   new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
 
 export const invokePromiseMethod = <R>(
-  runner: CallbackRunner<R>,
+  runner: CallbackRunner<R> & SyncIteratorRunner<R>,
   promises: PromiseRuntime<R>,
   ref: PromiseMethodReference,
   args: Array<unknown>,
@@ -98,79 +102,69 @@ export const invokePromiseMethod = <R>(
     return promises.create(Effect.fail(new ProgramThrow(args[0])))
   }
 
-  const spread = spreadItems(args[0])
-  if (spread === undefined) {
-    return promises.create(
-      Effect.fail(
-        new InterpreterRuntimeError(
-          `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
+  return promises.create(
+    Effect.gen(function* () {
+      const cursor = yield* runner.syncIterator(args[0], node)
+      if (cursor === undefined) {
+        throw new InterpreterRuntimeError(
+          `Promise.${ref.name} expects an array or other synchronous iterable.`,
           node,
-        ).as("TypeError"),
-      ),
-    )
-  }
-  const items = Array.from(spread)
-
-  for (const item of items) {
-    if (item instanceof CodeModePromise) promises.markObserved(item)
-  }
+        ).as("TypeError")
+      }
+      const items: Array<unknown> = []
+      while (true) {
+        const step = yield* cursor.next
+        if (step.done) break
+        items.push(step.value)
+        if (step.value instanceof CodeModePromise) promises.markObserved(step.value)
+      }
 
-  switch (ref.name) {
-    case "all": {
-      const observations = items.map((item) =>
-        item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
-      )
-      return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" })))
-    }
-    case "allSettled": {
-      const observations = items.map((item) =>
-        item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
-      )
-      return promises.create(
-        settleAfterTurn(
-          Effect.gen(function* () {
-            const outcomes: Array<unknown> = []
-            for (const observation of observations) {
-              const exit = yield* observation
-              if (Exit.isSuccess(exit)) {
-                outcomes.push(
-                  Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
-                )
-                continue
-              }
-              if (Cause.hasInterruptsOnly(exit.cause)) {
-                // Teardown interruption is not a program-level rejection.
-                return yield* Effect.failCause(exit.cause)
-              }
-              outcomes.push(
-                Object.assign(Object.create(null) as SafeObject, {
-                  status: "rejected",
-                  reason: caughtErrorValue(Cause.squash(exit.cause)),
-                }),
-              )
-            }
-            return outcomes
-          }),
-        ),
-      )
-    }
-    case "race": {
-      if (items.length === 0) {
-        return promises.create(
-          Effect.fail(
-            new InterpreterRuntimeError(
-              "Promise.race([]) would never settle; provide at least one promise or value.",
-              node,
+      if (ref.name === "all") {
+        return yield* settleAfterTurn(
+          Effect.all(
+            items.map((item) =>
+              item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
+            ),
+            { concurrency: "unbounded" },
+          ),
+        )
+      }
+      if (ref.name === "allSettled") {
+        const outcomes: Array<unknown> = []
+        for (const item of items) {
+          const exit = item instanceof CodeModePromise ? yield* promises.await(item) : Exit.succeed(item)
+          if (Exit.isSuccess(exit)) {
+            outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }))
+            continue
+          }
+          if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
+          outcomes.push(
+            Object.assign(Object.create(null) as SafeObject, {
+              status: "rejected",
+              reason: caughtErrorValue(Cause.squash(exit.cause)),
+            }),
+          )
+        }
+        yield* Effect.yieldNow
+        return outcomes
+      }
+      if (ref.name === "race") {
+        if (items.length === 0) {
+          throw new InterpreterRuntimeError(
+            "Promise.race([]) would never settle; provide at least one promise or value.",
+            node,
+          )
+        }
+        return yield* settleAfterTurn(
+          Effect.flatten(
+            Effect.raceAll(
+              items.map((item) =>
+                item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
+              ),
             ),
           ),
         )
       }
-      const observations = items.map((item) =>
-        item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
-      )
-      return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
-    }
-    case "any": {
       const flipped = items.map((item) =>
         item instanceof CodeModePromise
           ? Effect.flatMap(promises.await(item), (exit) => {
@@ -180,17 +174,18 @@ export const invokePromiseMethod = <R>(
             })
           : Effect.fail(new PromiseAnyFulfilled(item)),
       )
-      const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe(
-        Effect.flatMap((reasons) =>
-          Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
-        ),
-        Effect.catch((error) =>
-          error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
+      return yield* settleAfterTurn(
+        Effect.all(flipped, { concurrency: "unbounded" }).pipe(
+          Effect.flatMap((reasons) =>
+            Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
+          ),
+          Effect.catch((error) =>
+            error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
+          ),
         ),
       )
-      return promises.create(settleAfterTurn(body))
-    }
-  }
+    }),
+  )
 }
 
 export const invokePromiseInstanceMethod = <R>(

+ 5 - 0
packages/codemode/src/interpreter/references.ts

@@ -1,10 +1,12 @@
 import {
   type AstNode,
   CodeModeFunction,
+  CodeModeGenerator,
   CoercionFunction,
   ErrorConstructorReference,
   GlobalMethodReference,
   GlobalNamespace,
+  GeneratorMethodReference,
   InterpreterRuntimeError,
   IntrinsicReference,
   JsonMethodReference,
@@ -21,6 +23,8 @@ import { isCodeModeValue, CodeModePromise } from "../values.js"
 
 export const isRuntimeReference = (value: unknown): boolean =>
   value instanceof CodeModeFunction ||
+  value instanceof CodeModeGenerator ||
+  value instanceof GeneratorMethodReference ||
   value instanceof ToolReference ||
   value instanceof IntrinsicReference ||
   value instanceof GlobalNamespace ||
@@ -107,6 +111,7 @@ export const rejectCircularInsertion = (container: object, value: unknown, label
 export const typeofValue = (value: unknown): string => {
   if (
     value instanceof CodeModeFunction ||
+    value instanceof GeneratorMethodReference ||
     value instanceof CoercionFunction ||
     value instanceof IntrinsicReference ||
     value instanceof GlobalMethodReference ||

Fichier diff supprimé car celui-ci est trop grand
+ 609 - 170
packages/codemode/src/interpreter/runtime.ts


+ 28 - 12
packages/codemode/src/stdlib/math.ts

@@ -1,5 +1,6 @@
+import { Effect } from "effect"
+import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js"
 import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
-import { spreadItems } from "./collections.js"
 
 // Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types.
 declare global {
@@ -53,17 +54,6 @@ export const mathMethods = new Set([
 export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
   if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available.`, node)
   if (name === "random") return Math.random()
-  if (name === "sumPrecise") {
-    const items = spreadItems(args[0])
-    if (items === undefined) {
-      throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable collection.", node).as("TypeError")
-    }
-    const numbers = Array.from(items)
-    if (!numbers.every((item): item is number => typeof item === "number")) {
-      throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError")
-    }
-    return Math.sumPrecise(numbers)
-  }
   // Validate only the arguments the method consumes; like JS, extras are ignored
   // (so built-ins work as callbacks receiving (element, index, array)).
   const num = (index: number): number => {
@@ -153,3 +143,29 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
   }
   throw new InterpreterRuntimeError(`Math.${name} is not available.`, node)
 }
+
+export const invokeMathSumPrecise = <R>(
+  runner: SyncIteratorRunner<R>,
+  source: unknown,
+  node: AstNode,
+): Effect.Effect<number, unknown, R> =>
+  Effect.gen(function* () {
+    const cursor = yield* runner.syncIterator(source, node)
+    if (cursor === undefined) {
+      throw new InterpreterRuntimeError("Math.sumPrecise expects a synchronous iterable.", node).as("TypeError")
+    }
+    const numbers: Array<number> = []
+    while (true) {
+      const step = yield* cursor.next
+      if (step.done) return Math.sumPrecise(numbers)
+      yield* preserveConsumerError(
+        cursor,
+        Effect.sync(() => {
+          if (typeof step.value !== "number") {
+            throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError")
+          }
+          numbers.push(step.value)
+        }),
+      )
+    }
+  })

+ 44 - 32
packages/codemode/src/stdlib/object.ts

@@ -1,3 +1,4 @@
+import { Effect } from "effect"
 import {
   type AstNode,
   AsyncIteratorSymbol,
@@ -7,8 +8,9 @@ import {
 } from "../interpreter/model.js"
 import { containsOpaqueReference } from "../interpreter/references.js"
 import { isBlockedMember } from "../tool-runtime.js"
-import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js"
+import { isCodeModeValue, CodeModePromise } from "../values.js"
 import { boundedData, coerceToString } from "./value.js"
+import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js"
 
 export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"])
 
@@ -39,11 +41,6 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
     if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
     out[key] = item
   }
-  const addEntry = (out: Record<string, unknown>, key: unknown, item: unknown): void => {
-    boundedData(key, "Object.fromEntries key")
-    boundedData(item, "Object.fromEntries value")
-    guardedSet(out, coerceToString(key), item)
-  }
   switch (name) {
     case "keys":
       return Object.keys(requireObject())
@@ -79,32 +76,47 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
       }
       return out
     }
-    case "fromEntries": {
-      if (args[0] instanceof CodeModeMap) {
-        const out: Record<string, unknown> = Object.create(null)
-        for (const [key, item] of args[0].map.entries()) addEntry(out, key, item)
-        return out
-      }
-      if (args[0] instanceof CodeModeURLSearchParams) {
-        const out: Record<string, unknown> = Object.create(null)
-        for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
-        return out
-      }
-      const pairs = args[0] instanceof CodeModeSet ? Array.from(args[0].set.values()) : args[0]
-      if (!Array.isArray(pairs)) {
-        boundedData(args[0], "Object.fromEntries input")
-        throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
-      }
-      const out: Record<string, unknown> = Object.create(null)
-      for (const pair of pairs) {
-        const validated = boundedData(pair, "Object.fromEntries entry")
-        if (validated === null || typeof validated !== "object" || isCodeModeValue(validated))
-          throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node)
-        const entry = pair as Record<string, unknown>
-        addEntry(out, entry[0], entry[1])
-      }
-      return out
-    }
   }
   throw new InterpreterRuntimeError(`Object.${name} is not available.`, node)
 }
+
+export const invokeObjectFromEntries = <R>(
+  runner: SyncIteratorRunner<R>,
+  source: unknown,
+  node: AstNode,
+): Effect.Effect<Record<string, unknown>, unknown, R> => {
+  const out: Record<string, unknown> = Object.create(null)
+  return Effect.gen(function* () {
+    const cursor = yield* runner.syncIterator(source, node)
+    if (cursor === undefined) {
+      throw new InterpreterRuntimeError("Object.fromEntries expects a synchronous iterable of entries.", node).as(
+        "TypeError",
+      )
+    }
+    while (true) {
+      const step = yield* cursor.next
+      if (step.done) return out
+      yield* preserveConsumerError(
+        cursor,
+        Effect.sync(() => {
+          if (
+            step.value === null ||
+            typeof step.value !== "object" ||
+            isCodeModeValue(step.value) ||
+            containsOpaqueReference(step.value)
+          ) {
+            throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as(
+              "TypeError",
+            )
+          }
+          const entry = step.value as Record<string, unknown>
+          boundedData(entry[0], "Object.fromEntries key")
+          boundedData(entry[1], "Object.fromEntries value")
+          const key = coerceToString(entry[0])
+          if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
+          out[key] = entry[1]
+        }),
+      )
+    }
+  })
+}

+ 1 - 1
packages/codemode/src/tool-runtime.ts

@@ -553,7 +553,7 @@ export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget
     "## Language",
     "",
     "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
-    "Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.",
+    "Modules/imports, classes, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.",
     "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
     "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
   ]

+ 2 - 1
packages/codemode/test/codemode.test.ts

@@ -732,9 +732,10 @@ describe("CodeMode public contract", () => {
     expect(instructions).toContain("not a general-purpose runtime")
     expect(instructions).not.toContain("Standard modern JavaScript works")
     expect(instructions).not.toContain("TypeScript type annotations")
-    for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
+    for (const missing of ["Modules/imports", "classes", "fetch"]) {
       expect(instructions).toContain(missing)
     }
+    expect(instructions).not.toContain("generators")
     expect(instructions).not.toContain("new Promise(...) are unavailable")
     expect(instructions).not.toContain("promise chaining")
     expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")

+ 1271 - 0
packages/codemode/test/generator-test262.test.ts

@@ -0,0 +1,1271 @@
+/*
+ * 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, 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("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"])
+  })
+})

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff