///|
/// Generator state machine
pub(all) enum GenState {
  SuspendedStart // Created but body not yet started
  Executing // Currently running (re-entrancy guard)
  SuspendedYield // Suspended at a yield point
  Completed // Body finished or abruptly completed
}

///|
/// How the generator was resumed
pub(all) enum ResumeKind {
  Next(Value) // .next(v)
  Throw(Value) // .throw(e)
  Return(Value) // .return(v)
}

///|
/// Resume action for eval_yield to inspect when replaying
pub(all) enum ResumeAction {
  NextAction // deliver yield_value normally
  ThrowAction(Value) // inject throw at yield point
  ReturnAction(Value) // inject return at yield point
}

///|
/// Generator object stored as internal data on an ObjectData
pub(all) struct GeneratorObject {
  id : Int // generator object ID for cleanup
  mut state : GenState
  // The interpreter, body, params, closure, etc. needed to execute
  body : Array[@ast.Stmt]
  strict : Bool // strict mode flag captured from the defining function
  params : Array[String] // simple params
  params_ext : Array[@ast.Param]? // extended params (with defaults)
  rest_param : String?
  closure : Environment
  is_async : Bool
  name : String?
  interpreter : Interpreter
  args : Array[Value] // arguments passed when generator function was called
  this_val : Value // this value at call time
  // Step engine state
  mut env : Environment? // execution environment (created on first .next())
  mut pc : Int // program counter: index into body
  mut yield_value : Value // value to be returned by yield expression on resume
  // Resume replay state
  mut resuming : Bool // true when replaying a statement to deliver resume value
  mut resume_action : ResumeAction // what to do when eval_yield is hit during replay
  // Try/catch/finally resume tracking
  // -1 = no saved phase, 0 = try body, 1 = catch body, 2 = finally body
  mut try_resume_phase : Int
  mut try_resume_yield_base : Int // yield index at the start of the resumed try/catch/finally phase
  mut try_resume_error : Value // saved error value when resuming in catch
  mut try_resume_result : Signal // saved try/catch result when resuming in finally
  mut try_resume_pending_error : Error? // saved non-catchable error for finally
  // Loop resume tracking — stack of saved loop envs (innermost first, outermost last)
  loop_env_stack : Array[Environment]
  loop_yield_base_stack : Array[Int]
  // Statement-list resume tracking. Frames are pushed while YieldSignal unwinds,
  // so the outermost frame is restored first and nested frames follow in LIFO order.
  stmt_resume_index_stack : Array[Int]
  stmt_resume_env_stack : Array[Environment]
  stmt_resume_value_stack : Array[Value]
  // For-of resume state
  mut for_of_iterator : Value // last saved iterator object (compat/debug)
  mut for_of_next : Value // last saved next method (compat/debug)
  for_of_iterator_stack : Array[Value]
  for_of_next_stack : Array[Value]
  mut for_of_resume : Bool // true if resuming inside for-of
  // For-await-of: flag to distinguish next-await from body-yield on resume.
  // The for_of_iterator/next stacks above are shared with sync for-of.
  mut for_of_awaiting_next : Bool // true if resuming from an awaited next() call
  // Inner iterators saved when YieldSignal escapes array destructuring inside for-of.
  // On abrupt resume (ReturnAction/ThrowAction), these are closed directly without
  // re-running assign_pattern (which would call iterator.next() again).
  dstr_iterator_stack : Array[Value]
  // yield* delegation state
  mut delegate_iterator : Value // the delegate iterator (or Undefined if not delegating)
  mut delegate_next : Value // the delegate's next method
  mut delegating : Bool // true if currently in yield* delegation
  // Yield index tracking for nested yields (e.g. yield yield 1)
  mut yield_index : Int // counter incremented each time eval_yield is called
  mut resume_at_yield : Int // index of the yield that was suspended
  mut yield_resume_values : Map[Int, Value] // yield_index -> resume value for replay
}

///|
/// Mark generator as completed and remove from global map to free resources
fn complete_generator(gen : GeneratorObject) -> Unit {
  gen.state = Completed
  // Clear references to reduce memory pressure while keeping
  // the object in the map so subsequent .next/.throw/.return calls
  // correctly return {done: true} instead of "incompatible receiver".
  gen.env = None
  gen.delegate_iterator = Undefined
  gen.delegate_next = Undefined
  gen.delegating = false
  gen.for_of_iterator = Undefined
  gen.for_of_next = Undefined
  gen.for_of_iterator_stack.clear()
  gen.for_of_next_stack.clear()
  gen.for_of_resume = false
  gen.for_of_awaiting_next = false
  gen.dstr_iterator_stack.clear()
  gen.loop_env_stack.clear()
  gen.loop_yield_base_stack.clear()
  gen.stmt_resume_index_stack.clear()
  gen.stmt_resume_env_stack.clear()
  gen.stmt_resume_value_stack.clear()
  gen.yield_resume_values.clear()
  // Clear mutable fields to allow GC
  // Note: body/args/closure/this_val are immutable and may be shared
  // across generator instances from the same function, so leave them.
  gen.yield_value = Undefined
  gen.try_resume_yield_base = 0
  gen.try_resume_error = Undefined
  gen.try_resume_result = Normal(Undefined)
  gen.try_resume_pending_error = None
}

///|
/// §20.2.1.1.1 step 17a: Returns true if expr contains a YieldExpression at
/// any level, stopping at nested function boundaries (which have their own
/// generator context and may use yield freely).
priv enum GeneratorParamsYieldWork {
  Param(@ast.Param)
  Pattern(@ast.Pattern)
  Expr(@ast.Expr)
}

///|
fn generator_params_contain_yield(
  work : Array[GeneratorParamsYieldWork],
) -> Bool {
  while work.pop() is Some(item) {
    let scheduled : Array[GeneratorParamsYieldWork] = []
    match item {
      Param(param) => {
        match param.default_val {
          Some(expr) => scheduled.push(Expr(expr))
          None => ()
        }
        match param.pattern {
          Some(pattern) => scheduled.push(Pattern(pattern))
          None => ()
        }
      }
      Pattern(pattern) =>
        ignore(
          @ast.pattern_immediate_children_any(
            pattern,
            child => {
              scheduled.push(Pattern(child))
              false
            },
            child => {
              scheduled.push(Expr(child))
              false
            },
          ),
        )
      Expr(YieldExpr(_, _, _)) => return true
      Expr(
        ArrowFuncExt(params, _, _, _, _)
        | AsyncArrowFuncExt(params, _, _, _, _)
      ) =>
        for param in params {
          scheduled.push(Param(param))
        }
      // Nested function boundaries — yield inside is not the outer generator's.
      Expr(
        FuncExpr(_, _, _, _, _)
        | FuncExprExt(_, _, _, _, _, _)
        | GeneratorExpr(_, _, _, _, _)
        | GeneratorExprExt(_, _, _, _, _, _)
        | AsyncFuncExpr(_, _, _, _, _)
        | AsyncFuncExprExt(_, _, _, _, _, _)
        | AsyncArrowFunc(_, _, _, _)
        | AsyncGeneratorExpr(_, _, _, _, _)
        | AsyncGeneratorExprExt(_, _, _, _, _, _)
        | ArrowFunc(_, _, _, _)
      ) => ()
      Expr(ClassExpr(_, superclass, members, _, _)) => {
        match superclass {
          Some(expr) => scheduled.push(Expr(expr))
          None => ()
        }
        for class_member in members {
          match class_member {
            Method(class_method) =>
              if class_method.computed {
                scheduled.push(Expr(class_method.key))
              }
            Field(field) =>
              if field.computed {
                scheduled.push(Expr(field.key))
              }
            StaticBlock(_) => ()
          }
        }
      }
      Expr(DestructureAssign(pattern, expr, _)) => {
        scheduled.push(Pattern(pattern))
        scheduled.push(Expr(expr))
      }
      Expr(expr) =>
        ignore(
          @ast.expr_immediate_children_any(expr, child => {
            scheduled.push(Expr(child))
            false
          }),
        )
    }
    for i in (scheduled.length() - 1)>=..0 {
      work.push(scheduled[i])
    }
  }
  false
}

///|
fn expr_contains_yield(expr : @ast.Expr) -> Bool {
  generator_params_contain_yield([Expr(expr)])
}

///|
// Walk a binding pattern for yield expressions in default values and computed
// property keys. Property name strings (e.g. the "yield" in {yield: x}) are
// not expressions and are intentionally not flagged here.
fn pattern_contains_yield(pat : @ast.Pattern) -> Bool {
  generator_params_contain_yield([Pattern(pat)])
}

///|
fn params_contain_yield(params : Array[@ast.Param]) -> Bool {
  for param in params {
    match param.default_val {
      Some(expr) => if expr_contains_yield(expr) { return true }
      None => ()
    }
    match param.pattern {
      Some(pattern) => if pattern_contains_yield(pattern) { return true }
      None => ()
    }
  }
  false
}

///|
/// Set up the GeneratorFunction constructor on the given interpreter.
/// GeneratorFunction("a", "yield a") creates a generator function,
/// analogous to Function("a", "return a") for regular functions.
pub fn setup_generator_function_constructor(
  env : Environment,
  global_this : Value,
  well_known_symbols~ : WellKnownSymbols,
) -> Unit {
  // %GeneratorFunction.prototype% inherits from Function.prototype
  let func_proto : Value = match env.bindings.get("[[FunctionPrototype]]") {
    Some(binding) => binding.value
    None => Null
  }
  let iter_proto = match env.realm_state {
    Some(rs) => rs.get_iterator_proto()
    None => Null
  }
  let tostringtag_sym = well_known_symbols.to_string_tag
  let non_enum : PropDescriptor = {
    writable: true,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let non_enum_non_writable : PropDescriptor = {
    writable: false,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let frozen : PropDescriptor = {
    writable: false,
    enumerable: false,
    configurable: false,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  // %GeneratorFunction%.prototype — build shell first so it can serve as
  // the constructor value in %GeneratorPrototype%'s property map, ensuring
  // insertion order follows §27.3.3: constructor, prototype (string keys).
  let gf_sym_props : Map[Int, Value] = Map([])
  let gf_sym_descs : Map[Int, PropDescriptor] = Map([])
  gf_sym_props[tostringtag_sym.id] = String_("GeneratorFunction")
  gf_sym_descs[tostringtag_sym.id] = non_enum_non_writable
  let gen_func_proto : Value = Object({
    bag: {
      properties: Map([]),
      symbol_properties: gf_sym_props,
      descriptors: Map([]),
      symbol_descriptors: gf_sym_descs,
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: func_proto,
    callable: None,
    class_name: "GeneratorFunction",
    extensible: true,
    arraybuffer_state: None,
  })
  // %GeneratorPrototype% — shared singleton for all sync generator instances.
  // Own string keys follow §27.3.1: constructor, next, return, throw.
  let gp_props : Map[String, Value] = Map([])
  let gp_sym_props : Map[Int, Value] = Map([])
  let gp_descs : Map[String, PropDescriptor] = Map([])
  let gp_sym_descs : Map[Int, PropDescriptor] = Map([])
  gp_props["constructor"] = gen_func_proto
  gp_descs["constructor"] = non_enum_non_writable
  gp_props["next"] = make_interp_method_func(
    name="next",
    length=1,
    realm_state=env.realm_state,
    fn(ip, this_val, args) raise {
      let arg = match args {
        [first, ..] => first
        [] => Undefined
      }
      generator_resume(ip, this_val, Next(arg))
    },
  )
  gp_descs["next"] = non_enum
  gp_props["return"] = make_interp_method_func(
    name="return",
    length=1,
    realm_state=env.realm_state,
    fn(ip, this_val, args) raise {
      let arg = match args {
        [first, ..] => first
        [] => Undefined
      }
      generator_resume(ip, this_val, Return(arg))
    },
  )
  gp_descs["return"] = non_enum
  gp_props["throw"] = make_interp_method_func(
    name="throw",
    length=1,
    realm_state=env.realm_state,
    fn(ip, this_val, args) raise {
      let arg = match args {
        [first, ..] => first
        [] => Undefined
      }
      generator_resume(ip, this_val, Throw(arg))
    },
  )
  gp_descs["throw"] = non_enum
  gp_sym_props[tostringtag_sym.id] = String_("Generator")
  gp_sym_descs[tostringtag_sym.id] = non_enum_non_writable
  // [[Prototype]] = %IteratorPrototype% per §27.3.1 — Symbol.iterator is
  // inherited from there, not an own property of %GeneratorPrototype%.
  let gen_proto : Value = Object({
    bag: {
      properties: gp_props,
      symbol_properties: gp_sym_props,
      descriptors: gp_descs,
      symbol_descriptors: gp_sym_descs,
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: iter_proto,
    callable: None,
    class_name: "Generator",
    extensible: true,
    arraybuffer_state: None,
  })
  // GeneratorFunction constructor: GeneratorFunction(p1, p2, ..., body)
  let gen_func_ctor : Value = make_interp_method_func(
    name="GeneratorFunction",
    length=1,
    realm_state=env.realm_state,
    fn(interp, _this, args) raise {
      // §20.2.1.1.1: coerce params first (step 5a), body last (step 6).
      // Last arg is body, previous args are parameter names.
      let param_count = if args.length() == 0 { 0 } else { args.length() - 1 }
      let param_parts : Array[String] = []
      for i = 0; i < param_count; i = i + 1 {
        match args[i] {
          String_(s) => param_parts.push(s)
          _ => param_parts.push(interp.to_js_string(args[i]))
        }
      }
      let body_str = if args.length() == 0 {
        ""
      } else {
        match args[args.length() - 1] {
          String_(s) => s
          _ => interp.to_js_string(args[args.length() - 1])
        }
      }
      let params_str = param_parts.join(",")
      let source = "function* anonymous(" +
        params_str +
        "\n) {\n" +
        body_str +
        "\n}"
      let prog = @parser.parse(source)
      if prog.stmts.length() > 0 {
        match prog.stmts[0] {
          GeneratorDecl(_, params, body, _, source_text) => {
            let body_strict = @static_semantics.has_use_strict(body)
            validate_function_constructor_params(true, params, None, body)
            interp.validate_block_early_errors(body, body_strict)
            return interp.make_generator_function(
              Some("anonymous"),
              params,
              None,
              body,
              body_strict,
              interp.global,
              source_text~,
            )
          }
          GeneratorDeclExt(_, params, rest_param, body, _, source_text) => {
            // §20.2.1.1.1 step 17a: YieldExpression in generator params → SyntaxError.
            // The parser now enters generator context for params, so `yield` in
            // default values is parsed as YieldExpr and caught here via AST walk.
            if params_contain_yield(params) {
              raise @errors.SyntaxError(
                message="Generator function parameters may not contain yield expressions",
              )
            }
            let body_strict = @static_semantics.has_use_strict(body)
            validate_function_constructor_params_ext(params, rest_param, body)
            interp.validate_block_early_errors(body, body_strict)
            return interp.make_generator_function_ext(
              Some("anonymous"),
              params,
              rest_param,
              body,
              body_strict,
              interp.global,
              source_text~,
            )
          }
          _ => ()
        }
      }
      raise @errors.SyntaxError(
        message="Invalid GeneratorFunction constructor source",
      )
    },
  )
  // §27.3.3: gen_func_proto string keys: constructor (§27.3.3.1) then prototype (§27.3.3.2).
  match gen_func_proto {
    Object(data) => {
      data.bag.properties["constructor"] = gen_func_ctor
      data.bag.descriptors["constructor"] = non_enum_non_writable
      data.bag.properties["prototype"] = gen_proto
      data.bag.descriptors["prototype"] = non_enum_non_writable
    }
    _ => ()
  }
  // Mirror onto globalThis
  match global_this {
    Object(data) => data.bag.properties["GeneratorFunction"] = gen_func_ctor
    _ => ()
  }
  // Add prototype property to GeneratorFunction constructor
  match gen_func_ctor {
    Object(data) => {
      data.bag.properties["prototype"] = gen_func_proto
      data.bag.descriptors["prototype"] = frozen
    }
    _ => ()
  }
  env.def_builtin("GeneratorFunction", gen_func_ctor)
  env.def_builtin("[[GeneratorFunctionPrototype]]", gen_func_proto)
  env.def_builtin("[[GeneratorPrototype]]", gen_proto)
}

///|
/// Yield signal — used to suspend generator execution
pub suberror YieldSignal {
  YieldSignal(Value) // the yielded value
}

///|
/// Generator return signal — used to inject return at yield point
/// This is NOT JS-catchable, but must trigger finally blocks.
pub suberror GeneratorReturnSignal {
  GeneratorReturnSignal(Value)
}

///|
/// Create a generator function value (simple params)
fn Interpreter::make_generator_function(
  self : Interpreter,
  name : String?,
  params : Array[String],
  rest_param : String?,
  body : Array[@ast.Stmt],
  strict : Bool,
  closure : Environment,
  has_name_binding? : Bool = false,
  is_method? : Bool = false,
  source_text? : String? = None,
) -> Value {
  let interp = self
  let gen_proto : Value = match
    interp.global.bindings.get("[[GeneratorPrototype]]") {
    Some(binding) => binding.value
    None => Null
  }
  // Create the generator function's own .prototype (instances inherit from it)
  let func_prototype = Object({
    bag: PropertyBag(),
    prototype: gen_proto,
    callable: None,
    class_name: "Generator",
    extensible: true,
    arraybuffer_state: None,
  })
  // Generator functions inherit from %GeneratorFunction.prototype%
  let gen_func_proto : Value = match
    interp.global.bindings.get("[[GeneratorFunctionPrototype]]") {
    Some(binding) => binding.value
    None => Null
  }
  let func_name = match name {
    Some(n) => n
    None => ""
  }
  let fn_props : Map[String, Value] = Map([])
  fn_props["prototype"] = func_prototype
  fn_props["name"] = String_(func_name)
  fn_props["length"] = Number(params.length().to_double())
  let internal_slots : Map[InternalSlotKey, Value] = Map([])
  match source_text {
    Some(text) => internal_slots[SourceText] = String_(text)
    None => ()
  }
  let nf_desc : PropDescriptor = {
    writable: false,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let effective_closure : Environment = if has_name_binding {
    match name {
      Some(n) => {
        let ne = Environment::new(parent=Some(closure))
        ne.bindings[n] = {
          value: Undefined,
          kind: FunctionNameBinding,
          initialized: true,
          annex_b_hoisted: false,
          is_parameter: false,
        }
        ne
      }
      None => closure
    }
  } else {
    closure
  }
  let gen_func = stamp_function_realm(
    Object({
      bag: {
        properties: fn_props,
        symbol_properties: Map([]),
        descriptors: {
          "name": nf_desc,
          "length": nf_desc,
          "prototype": {
            writable: true,
            enumerable: false,
            configurable: false,
            getter: None,
            setter: None,
            is_accessor: false,
          },
        },
        symbol_descriptors: Map([]),
        internal_slots,
        host_slots: Map([]),
      },
      prototype: gen_func_proto,
      callable: Some(
        InterpreterCallable(func_name, fn(ip, this_val, args) raise {
          create_generator_instance(
            ip,
            this_val,
            args,
            name,
            params,
            None,
            rest_param,
            body,
            strict,
            effective_closure,
            func_prototype,
            is_method~,
          )
        }),
      ),
      class_name: "GeneratorFunction",
      extensible: true,
      arraybuffer_state: None,
    }),
    realm_state=Some(self.realm_state),
  )
  if has_name_binding {
    match name {
      Some(n) =>
        match effective_closure.bindings.get(n) {
          Some(b) => b.value = gen_func
          None => ()
        }
      None => ()
    }
  }
  gen_func
}

///|
/// Create a generator function value (extended params)
fn Interpreter::make_generator_function_ext(
  self : Interpreter,
  name : String?,
  params : Array[@ast.Param],
  rest_param : String?,
  body : Array[@ast.Stmt],
  strict : Bool,
  closure : Environment,
  has_name_binding? : Bool = false,
  is_method? : Bool = false,
  source_text? : String? = None,
) -> Value {
  let interp = self
  let gen_proto : Value = match
    interp.global.bindings.get("[[GeneratorPrototype]]") {
    Some(binding) => binding.value
    None => Null
  }
  let func_prototype = Object({
    bag: PropertyBag(),
    prototype: gen_proto,
    callable: None,
    class_name: "Generator",
    extensible: true,
    arraybuffer_state: None,
  })
  // Generator functions inherit from %GeneratorFunction.prototype%
  let gen_func_proto : Value = match
    interp.global.bindings.get("[[GeneratorFunctionPrototype]]") {
    Some(binding) => binding.value
    None => Null
  }
  let func_name = match name {
    Some(n) => n
    None => ""
  }
  let expected_arg_count = expected_argument_count_ext(params)
  let fn_props : Map[String, Value] = Map([])
  fn_props["prototype"] = func_prototype
  fn_props["name"] = String_(func_name)
  fn_props["length"] = Number(expected_arg_count.to_double())
  let internal_slots : Map[InternalSlotKey, Value] = Map([])
  match source_text {
    Some(text) => internal_slots[SourceText] = String_(text)
    None => ()
  }
  let nf_desc : PropDescriptor = {
    writable: false,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let effective_closure : Environment = if has_name_binding {
    match name {
      Some(n) => {
        let ne = Environment::new(parent=Some(closure))
        ne.bindings[n] = {
          value: Undefined,
          kind: FunctionNameBinding,
          initialized: true,
          annex_b_hoisted: false,
          is_parameter: false,
        }
        ne
      }
      None => closure
    }
  } else {
    closure
  }
  let gen_func = stamp_function_realm(
    Object({
      bag: {
        properties: fn_props,
        symbol_properties: Map([]),
        descriptors: {
          "name": nf_desc,
          "length": nf_desc,
          "prototype": {
            writable: true,
            enumerable: false,
            configurable: false,
            getter: None,
            setter: None,
            is_accessor: false,
          },
        },
        symbol_descriptors: Map([]),
        internal_slots,
        host_slots: Map([]),
      },
      prototype: gen_func_proto,
      callable: Some(
        InterpreterCallable(func_name, fn(ip, this_val, args) raise {
          create_generator_instance(
            ip,
            this_val,
            args,
            name,
            [],
            Some(params),
            rest_param,
            body,
            strict,
            effective_closure,
            func_prototype,
            is_method~,
          )
        }),
      ),
      class_name: "GeneratorFunction",
      extensible: true,
      arraybuffer_state: None,
    }),
    realm_state=Some(self.realm_state),
  )
  if has_name_binding {
    match name {
      Some(n) =>
        match effective_closure.bindings.get(n) {
          Some(b) => b.value = gen_func
          None => ()
        }
      None => ()
    }
  }
  gen_func
}

///|
/// Create a generator instance when a generator function is called
fn create_generator_instance(
  interp : Interpreter,
  this_val : Value,
  args : Array[Value],
  name : String?,
  params : Array[String],
  params_ext : Array[@ast.Param]?,
  rest_param : String?,
  body : Array[@ast.Stmt],
  strict : Bool,
  closure : Environment,
  func_prototype : Value,
  is_method? : Bool = false,
  is_arrow? : Bool = false,
  callee? : Value = Undefined,
  is_async? : Bool = false,
) -> Value raise Error {
  // Per spec, FunctionDeclarationInstantiation runs eagerly when generator is called,
  // NOT when .next() is called. This means parameter binding errors throw immediately.
  // Use the stored strict flag instead of re-scanning the body.
  let gen_strict = strict
  let setup_ctx : ExecContext = { strict: gen_strict, current_generator: None }
  // §10.2.11 split: Ext-shape generator params (params_ext=Some) get a
  // separate body_env for body var/let/const. Simple params (params_ext=None)
  // stay single-env. `func_env` below is the param env in the Ext case and
  // the single env in the simple case; `body_exec_env` is the env the
  // generator body executes in (body_env when split, func_env otherwise).
  let func_env = Environment::new(parent=Some(closure))
  func_env.is_var_scope = true
  // Arrow functions don't rebind `this` (use closure's lexical this).
  if !is_arrow {
    // Every non-arrow generator-family call is a PerformEval boundary.
    // Methods authorize their own lexical super; ordinary functions stop lookup.
    func_env.def_builtin("[[EvalMethodContext]]", Bool(is_method))
    let effective_this = if !strict {
      match this_val {
        Undefined | Null => interp.global_this
        _ => this_val
      }
    } else {
      this_val
    }
    func_env.def("this", effective_this, LetBinding)
  }
  func_env.def("", Undefined, LetBinding)
  let has_arguments_param = match params_ext {
    Some(pext) => ext_params_include_arguments(pext, rest_param)
    None => params_include_arguments(params, rest_param)
  }
  // Arrow functions don't create own `arguments` (fall through to closure).
  if !is_arrow && !has_arguments_param {
    // Create arguments object before binding params so default value
    // evaluation can reference it (ES2026 §10.2.11 step 20).
    let args_obj = match (params_ext, strict) {
      (None, false) =>
        // Sloppy mode + simple params: mapped arguments with live param bindings.
        make_arguments_object(
          interp.realm_state,
          interp.realm_state.well_known_symbols,
          args,
          callee,
          false,
          mapped_names=params,
          mapped_env=Some(func_env),
        )
      _ => {
        // Strict mode or complex params: unmapped arguments object.
        let tte_val : Value? = if func_env.has("[[ThrowTypeError]]") {
          Some(func_env.get("[[ThrowTypeError]]"))
        } else {
          None
        }
        make_arguments_object(
          interp.realm_state,
          interp.realm_state.well_known_symbols,
          args,
          callee,
          strict,
          throw_type_error=tte_val,
        )
      }
    }
    func_env.def("arguments", args_obj, VarBinding)
  }
  // Bind parameters eagerly
  // §19.2.1.3 gate (#A.6): generator / async / async-gen functions are
  // all non-arrow, so default-expression evaluation activates the same
  // eval-declares-arguments gate as `bind_ext_params_and_exec_body`.
  // Save on entry / reset to false / restore on exit so nested invocations
  // in defaults don't inherit outer state.
  let saved_in_default = interp.in_nonarrow_param_default_eval
  let saved_param_default_conflicts = interp.param_default_eval_var_conflicts
  interp.in_nonarrow_param_default_eval = false
  interp.param_default_eval_var_conflicts = None
  let gen_result : Value = try {
    match params_ext {
      Some(pext) => {
        let param_default_conflicts = collect_param_default_eval_var_conflicts(
          pext,
          rest_param,
          implicit_arguments=true,
        )
        // §10.2.11 step 21: pre-declare all param BoundNames as TDZ so
        // that self- and forward-referencing defaults throw ReferenceError.
        let mut has_rest_pattern_param = false
        for p in pext {
          if p.is_rest_pattern {
            has_rest_pattern_param = true
            // Declare bound names from the rest destructuring pattern (...[a] → a),
            // but NOT the synthetic "$rest" name stored in rest_param.
            match p.pattern {
              Some(pat) =>
                for name in @static_semantics.bound_names(pat) {
                  func_env.def_param_tdz(name)
                }
              None => ()
            }
            continue
          }
          match p.pattern {
            Some(pat) =>
              // Destructuring param: pre-declare all bound names (e.g. {y} → y).
              for name in @static_semantics.bound_names(pat) {
                func_env.def_param_tdz(name)
              }
            None => func_env.def_param_tdz(p.name)
          }
        }
        // Simple named rest (e.g. ...rest): pre-declare the user name.
        // Destructuring rest (...[a]): bound names declared above via BoundNames.
        if !has_rest_pattern_param {
          match rest_param {
            Some(rp) => func_env.def_param_tdz(rp)
            None => ()
          }
        }
        let mut effective_count = 0
        for i = 0; i < pext.length(); i = i + 1 {
          let param = pext[i]
          if param.is_rest_pattern {
            continue
          }
          let val : Value = if effective_count < args.length() &&
            !(args[effective_count] is Undefined) {
            args[effective_count]
          } else {
            match param.default_val {
              Some(d) => {
                interp.in_nonarrow_param_default_eval = true
                interp.param_default_eval_var_conflicts = Some(
                  param_default_conflicts,
                )
                let v = interp.eval_expr(setup_ctx, d, func_env)
                interp.in_nonarrow_param_default_eval = false
                interp.param_default_eval_var_conflicts = None
                v
              }
              None =>
                if effective_count < args.length() {
                  args[effective_count]
                } else {
                  Undefined
                }
            }
          }
          match param.pattern {
            Some(pat) =>
              interp.bind_pattern(pat, val, func_env, LetBinding, ctx=setup_ctx)
            None => func_env.initialize(param.name, val)
          }
          effective_count = effective_count + 1
        }
        match rest_param {
          Some(rp) => {
            let rest_elems : Array[Value] = []
            for i = effective_count; i < args.length(); i = i + 1 {
              rest_elems.push(args[i])
            }
            let rest_val = make_array(rest_elems)
            let mut bound_rest_pattern = false
            for p in pext {
              if p.is_rest_pattern {
                match p.pattern {
                  Some(pat) =>
                    interp.bind_pattern(
                      pat,
                      rest_val,
                      func_env,
                      LetBinding,
                      ctx=setup_ctx,
                    )
                  None => ()
                }
                bound_rest_pattern = true
                break
              }
            }
            if !bound_rest_pattern {
              func_env.initialize(rp, rest_val)
            }
          }
          None => ()
        }
      }
      None => {
        for i = 0; i < params.length(); i = i + 1 {
          let val = if i < args.length() { args[i] } else { Undefined }
          func_env.def_parameter(params[i], val)
        }
        match rest_param {
          Some(rp) => {
            let rest_elems : Array[Value] = []
            for i = params.length(); i < args.length(); i = i + 1 {
              rest_elems.push(args[i])
            }
            func_env.def_parameter(rp, make_array(rest_elems))
          }
          None => ()
        }
      }
    }
    // Split body env iff Ext shape AND HasParameterExpressions; simple
    // params + plain-rest Ext both stay single-env per §10.2.11 step 26.
    let split_scope = match params_ext {
      Some(pext) => has_parameter_expressions(pext)
      None => false
    }
    let body_exec_env = if split_scope {
      let be = Environment::new(parent=Some(func_env))
      be.is_var_scope = true
      be
    } else {
      func_env
    }
    let param_source : Environment? = if split_scope {
      Some(func_env)
    } else {
      None
    }
    // Hoist declarations eagerly too
    interp.hoist_declarations(
      body,
      body_exec_env,
      strict=gen_strict,
      param_source~,
    )
    hoist_block_tdz(body, body_exec_env)
    let gen_id = interp.gen_id_counter.val
    interp.gen_id_counter.val = gen_id + 1
    let gen_obj : GeneratorObject = {
      id: gen_id,
      state: SuspendedStart,
      body,
      strict: gen_strict,
      params,
      params_ext,
      rest_param,
      closure,
      is_async,
      name,
      interpreter: interp,
      args,
      this_val,
      env: Some(body_exec_env),
      pc: 0,
      yield_value: Undefined,
      yield_resume_values: Map([]),
      resuming: false,
      resume_action: NextAction,
      try_resume_phase: -1,
      try_resume_yield_base: 0,
      try_resume_error: Undefined,
      try_resume_result: Normal(Undefined),
      try_resume_pending_error: None,
      loop_env_stack: [],
      loop_yield_base_stack: [],
      stmt_resume_index_stack: [],
      stmt_resume_env_stack: [],
      stmt_resume_value_stack: [],
      for_of_iterator: Undefined,
      for_of_next: Undefined,
      for_of_iterator_stack: [],
      for_of_next_stack: [],
      for_of_resume: false,
      for_of_awaiting_next: false,
      dstr_iterator_stack: [],
      delegate_iterator: Undefined,
      delegate_next: Undefined,
      delegating: false,
      yield_index: 0,
      resume_at_yield: 0,
    }
    interp.generator_objects[gen_id] = gen_obj
    // Create the generator instance object with gen_id stored in a property
    let props : Map[String, Value] = Map([])
    props[""] = Number(gen_id.to_double())
    Object({
      bag: {
        properties: props,
        symbol_properties: Map([]),
        descriptors: Map([]),
        symbol_descriptors: Map([]),
        internal_slots: Map([]),
        host_slots: Map([]),
      },
      prototype: func_prototype,
      callable: None,
      class_name: if is_async {
        "AsyncGenerator"
      } else {
        "Generator"
      },
      extensible: true,
      arraybuffer_state: None,
    })
  } catch {
    e => {
      interp.in_nonarrow_param_default_eval = saved_in_default
      interp.param_default_eval_var_conflicts = saved_param_default_conflicts
      raise e
    }
  }
  interp.in_nonarrow_param_default_eval = saved_in_default
  interp.param_default_eval_var_conflicts = saved_param_default_conflicts
  gen_result
}

///|
/// Get GeneratorObject from a generator instance Value
fn get_generator_object(
  interp : Interpreter,
  this_val : Value,
) -> GeneratorObject? {
  match this_val {
    Object(data) =>
      match data.bag.properties.get("") {
        Some(Number(id)) => interp.generator_objects.get(id.to_int())
        _ => None
      }
    _ => None
  }
}

///|
/// Resume generator execution
fn generator_resume(
  interp : Interpreter,
  this_val : Value,
  resume_kind : ResumeKind,
) -> Value raise Error {
  let gen = match get_generator_object(interp, this_val) {
    Some(g) => g
    None =>
      raise @errors.TypeError(
        message="Method next/throw/return called on incompatible receiver",
      )
  }
  match gen.state {
    Executing => raise @errors.TypeError(message="Generator is already running")
    Completed =>
      match resume_kind {
        Next(_) => create_iter_result(Undefined, true)
        Return(v) => create_iter_result(v, true)
        Throw(e) => raise JsException(e)
      }
    SuspendedStart =>
      match resume_kind {
        Next(_) => {
          // Start execution of generator body
          gen.state = Executing
          generator_start(interp, gen)
        }
        Throw(e) => {
          // .throw(e) on SuspendedStart: complete generator and throw
          complete_generator(gen)
          raise JsException(e)
        }
        Return(v) => {
          // .return(v) on SuspendedStart: complete generator and return done
          complete_generator(gen)
          create_iter_result(v, true)
        }
      }
    SuspendedYield =>
      if gen.delegating {
        // Forward to delegate iterator
        gen.state = Executing
        delegate_resume(interp, gen, resume_kind)
      } else {
        match resume_kind {
          Next(v) => {
            gen.state = Executing
            gen.yield_value = v
            gen.yield_resume_values[gen.resume_at_yield] = v
            gen.resume_action = NextAction
            generator_continue(interp, gen)
          }
          Throw(e) => {
            gen.state = Executing
            gen.yield_value = Undefined
            gen.resume_action = ThrowAction(e)
            generator_continue(interp, gen)
          }
          Return(v) => {
            gen.state = Executing
            gen.yield_value = v
            gen.resume_action = ReturnAction(v)
            generator_continue(interp, gen)
          }
        }
      }
  }
}

///|
/// Start generator body execution (first .next() call)
fn generator_start(
  interp : Interpreter,
  gen : GeneratorObject,
) -> Value raise Error {
  // Environment was already created and parameters bound in create_generator_instance
  let func_env = match gen.env {
    Some(e) => e
    None => abort("generator_start: no environment")
  }
  // Execute generator body, catching yields
  run_generator_body(interp, gen, func_env)
}

///|
/// Run generator body, handling yields.
/// On initial execution, statements run from gen.pc forward.
/// When a YieldSignal is caught, the generator suspends.
/// On resume, the SAME statement is re-executed with gen.resuming=true,
/// so that eval_yield returns the resume value instead of yielding again.
fn run_generator_body(
  interp : Interpreter,
  gen : GeneratorObject,
  env : Environment,
) -> Value raise Error {
  // Build per-execution context with this generator as the current generator.
  // Use the stored strict flag rather than re-scanning the body on every resume.
  let gen_ctx : ExecContext = {
    strict: gen.strict,
    current_generator: Some(gen),
  }
  let result = try {
    while gen.pc < gen.body.length() {
      let i = gen.pc
      let stmt = gen.body[i]
      if !gen.resuming {
        gen.yield_resume_values.clear()
        gen.yield_index = 0 // Reset yield counter for each non-resuming statement
      } else {
        // Resumes replay the suspended statement from its beginning, including
        // loop statements after their saved environments are restored. Keep
        // yield indexes stable so earlier yields in the same statement can be
        // replayed and abrupt resumes target the suspended yield.
        gen.yield_index = 0
      }
      let signal = interp.exec_stmt(gen_ctx, stmt, env) catch {
        YieldSignal(yielded_value) => {
          // Statement yielded. DON'T advance pc — on resume we'll replay this stmt.
          gen.state = SuspendedYield
          return create_iter_result(yielded_value, false)
        }
        GeneratorReturnSignal(v) => {
          // .return(v) was injected at yield point and propagated up
          // (possibly through finally blocks). Complete the generator.
          complete_generator(gen)
          return create_iter_result(v, true)
        }
        e => raise e
      }
      // Statement completed normally — advance to next
      gen.pc = i + 1
      match signal {
        Normal(_) => ()
        ReturnSignal(v) => {
          complete_generator(gen)
          return create_iter_result(v, true)
        }
        BreakSignal(_, _) | ContinueSignal(_, _) => ()
      }
    }
    // Body completed normally — per spec, generators return undefined if no explicit return
    complete_generator(gen)
    create_iter_result(Undefined, true)
  } catch {
    e => {
      complete_generator(gen)
      raise e
    }
  }
  result
}

///|
/// Continue generator after yield (resume with next value)
fn generator_continue(
  interp : Interpreter,
  gen : GeneratorObject,
) -> Value raise Error {
  match gen.env {
    Some(env) => {
      // Set resuming flag — the statement will be replayed, and eval_yield
      // will return the resume value instead of throwing YieldSignal
      gen.resuming = true
      run_generator_body(interp, gen, env)
    }
    None => {
      complete_generator(gen)
      create_iter_result(Undefined, true)
    }
  }
}

// generator_continue_throw and generator_continue_return are now handled
// by the unified generator_continue path via resume_action field.

///|
/// Evaluate a yield expression during generator body execution.
/// When resuming (gen.resuming == true), inspects resume_action:
/// - NextAction: returns the resume value
/// - ThrowAction(e): raises JsException(e) at the yield point
/// - ReturnAction(v): raises GeneratorReturnSignal(v) at the yield point
/// When delegate is true (yield*), iterates the delegate and yields each value.
fn Interpreter::eval_yield(
  self : Interpreter,
  ctx : ExecContext,
  argument : @ast.Expr?,
  delegate : Bool,
  env : Environment,
) -> Value raise Error {
  match ctx.current_generator {
    Some(gen) => {
      let current_yield_index = gen.yield_index
      gen.yield_index = gen.yield_index + 1
      if gen.resuming && current_yield_index == gen.resume_at_yield {
        // This is the exact yield that was suspended — apply resume action
        gen.resuming = false
        match gen.resume_action {
          NextAction => {
            gen.yield_resume_values[current_yield_index] = gen.yield_value
            return gen.yield_value
          }
          ThrowAction(e) => {
            gen.resume_action = NextAction
            raise JsException(e)
          }
          ReturnAction(v) => {
            gen.resume_action = NextAction
            raise GeneratorReturnSignal(v)
          }
        }
      }
      if gen.resuming && gen.loop_env_stack.length() == 0 {
        // If this yield already completed on a prior pass, replay its value.
        // Missing entries mean this is the active resume point or a yield nested
        // above it, so we must continue evaluating it normally.
        match gen.yield_resume_values.get(current_yield_index) {
          Some(v) => return v
          None => ()
        }
      }
      if delegate {
        // yield* expr — delegate to another iterable
        let iterable = match argument {
          Some(expr) => self.eval_expr(ctx, expr, env)
          None => Undefined
        }
        let loc = @token.Loc::default()
        if gen.is_async {
          // Async yield*: try @@asyncIterator first, fall back to wrapping sync @@iterator
          let async_iter_sym = self.realm_state.well_known_symbols.async_iterator
          let async_iter_method = self.get_computed_property(
            iterable,
            Symbol(async_iter_sym),
            loc,
          )
          let async_iterator = match async_iter_method {
            Object(data) =>
              match data.callable {
                Some(_) => self.call_value(async_iter_method, iterable, [], loc)
                None =>
                  raise @errors.TypeError(
                    message="Result of the Symbol.asyncIterator method is not callable",
                  )
              }
            Undefined | Null => {
              // Fall back to wrapping sync iterator via CreateAsyncFromSyncIterator
              let sync_iter_sym = self.realm_state.well_known_symbols.iterator
              let sync_iter_method = self.get_computed_property(
                iterable,
                Symbol(sync_iter_sym),
                loc,
              )
              let sync_iterator = match sync_iter_method {
                Object(data) =>
                  match data.callable {
                    Some(_) =>
                      self.call_value(sync_iter_method, iterable, [], loc)
                    None =>
                      raise @errors.TypeError(
                        message="Symbol.iterator is not a function",
                      )
                  }
                _ =>
                  raise @errors.TypeError(
                    message="yield* delegate is not async iterable",
                  )
              }
              let sync_next = get_iterator_next(self, sync_iterator)
              create_async_from_sync_iterator(
                self, sync_iterator, sync_next, loc,
              )
            }
            _ =>
              raise @errors.TypeError(
                message="Result of the Symbol.asyncIterator method is not an object",
              )
          }
          // Save async iterator delegation state on generator BEFORE calling first
          // .next() — the Promise from .next() is yielded and resolved asynchronously
          // by async_generator_step, which needs the delegate state to handle it.
          // (Sync yield* calls .next() first and only saves state if not immediately
          // done — async yield* always yields because .next() returns a Promise.)
          let async_next = get_iterator_next(self, async_iterator)
          gen.delegate_iterator = async_iterator
          gen.delegate_next = async_next
          gen.delegating = true
          gen.resume_at_yield = current_yield_index
          gen.yield_resume_values[current_yield_index] = Undefined
          // Start delegation by calling async .next() — returns a Promise
          let first_result = self.call_value(
            async_next,
            async_iterator,
            [],
            loc,
          )
          // Yield the Promise for async_generator_step to await and handle the result
          raise YieldSignal(first_result)
        } else {
          // Sync yield*: get iterator via Symbol.iterator
          let iterator_sym = self.realm_state.well_known_symbols.iterator
          let iterator_method = self.get_computed_property(
            iterable,
            Symbol(iterator_sym),
            loc,
          )
          let iterator = match iterator_method {
            Object(data) =>
              match data.callable {
                Some(_) => {
                  let result = self.call_value(
                    iterator_method,
                    iterable,
                    [],
                    loc,
                  )
                  match result {
                    Object(_) => result
                    _ =>
                      raise @errors.TypeError(
                        message="Result of the Symbol.iterator method is not an object",
                      )
                  }
                }
                None =>
                  raise @errors.TypeError(
                    message="Symbol.iterator is not a function",
                  )
              }
            _ =>
              raise @errors.TypeError(message="yield* delegate is not iterable")
          }
          // Get next method
          let next_method = get_iterator_next(self, iterator)
          // Start delegation: call next() and yield first value
          let first_result = self.call_value(
            next_method,
            iterator,
            [Undefined],
            loc,
          )
          guard is_object_value(first_result) else {
            raise @errors.TypeError(message="Iterator result is not an object")
          }
          // Extract done via proper Get (§7.4.2 IteratorComplete) so getter
          // side-effects fire correctly.
          let done = is_truthy(self.get_property(first_result, "done", loc))
          if done {
            // Delegate is immediately done — extract value via proper Get
            // (§7.4.3 IteratorValue) and yield* evaluates to the return value.
            self.get_property(first_result, "value", loc)
          } else {
            // Save delegation state on generator and yield the value.
            // Read value from bag.properties directly — must NOT trigger
            // value getter when done is false (test262 check).
            let value = match first_result {
              Object(data) =>
                match data.bag.properties.get("value") {
                  Some(v) => v
                  None => Undefined
                }
              _ => Undefined
            }
            gen.delegate_iterator = iterator
            gen.delegate_next = next_method
            gen.delegating = true
            gen.resume_at_yield = current_yield_index
            raise YieldSignal(value)
          }
        }
      } else {
        // Normal yield: evaluate argument and suspend
        let yielded_value = match argument {
          Some(expr) => self.eval_expr(ctx, expr, env)
          None => Undefined
        }
        // Track the value to use when re-running this yield during replay.
        gen.yield_resume_values[current_yield_index] = Undefined
        gen.resume_at_yield = current_yield_index
        raise YieldSignal(yielded_value)
      }
    }
    None =>
      // Parser prevents YieldExpr outside generator context, so this is unreachable
      raise @errors.TypeError(
        message="Internal error: yield outside generator context",
      )
  }
}

///|
/// Get iterator's next method (walk prototype chain)
fn get_iterator_next(
  interp : Interpreter,
  iterator : Value,
) -> Value raise Error {
  match get_optional_method(iterator, "next", interp) {
    Some(next_fn) => next_fn
    None => raise @errors.TypeError(message="iterator.next is not a function")
  }
}

///|
/// Inject an error into the generator body via ThrowAction.
/// Clears delegation state and resumes the generator with the error.
fn inject_error(
  interp : Interpreter,
  gen : GeneratorObject,
  error : Error,
) -> Value raise Error {
  gen.delegating = false
  gen.delegate_iterator = Undefined
  gen.delegate_next = Undefined
  let v = match error {
    JsException(js_val) => js_val
    _ => js_error_to_value_with_env(error, Some(interp.global))
  }
  gen.resume_action = ThrowAction(v)
  generator_continue(interp, gen)
}

///|

///|
/// Resume a delegated yield* — forward the resume to the delegate iterator

///|
/// Process a sync delegation result: check IsObject, extract done/value,
/// then either complete delegation (done:true, calling `make_resume_action`)
/// or yield the value (done:false).
fn process_delegate_result(
  interp : Interpreter,
  gen : GeneratorObject,
  result : Value,
  make_resume_action : (Value) -> ResumeAction,
) -> Value raise Error {
  let loc = @token.Loc::default()
  guard is_object_value(result) else {
    raise @errors.TypeError(message="Iterator result is not an object")
  }
  let done = is_truthy(interp.get_property(result, "done", loc))
  if done {
    let value = interp.get_property(result, "value", loc)
    gen.delegating = false
    gen.delegate_iterator = Undefined
    gen.delegate_next = Undefined
    gen.yield_value = value
    gen.resume_action = make_resume_action(value)
    generator_continue(interp, gen)
  } else {
    let value = match result {
      Object(data) =>
        match data.bag.properties.get("value") {
          Some(v) => v
          None => Undefined
        }
      _ => Undefined
    }
    gen.state = SuspendedYield
    create_iter_result(value, false)
  }
}

///|
fn delegate_resume(
  interp : Interpreter,
  gen : GeneratorObject,
  resume_kind : ResumeKind,
) -> Value raise Error {
  let loc = @token.Loc::default()
  let iterator = gen.delegate_iterator
  let next_method = gen.delegate_next
  // Forward the resume to the delegate
  match resume_kind {
    Next(v) =>
      try {
        let result = interp.call_value(next_method, iterator, [v], loc)
        if gen.is_async {
          gen.state = SuspendedYield
          create_iter_result(result, false)
        } else {
          process_delegate_result(interp, gen, result, fn(_) { NextAction })
        }
      } catch {
        e => inject_error(interp, gen, e)
      }
    Throw(e) =>
      try {
        let throw_method = get_optional_method(iterator, "throw", interp)
        match throw_method {
          Some(tm) => {
            let result = interp.call_value(tm, iterator, [e], loc)
            if gen.is_async {
              gen.state = SuspendedYield
              create_iter_result(result, false)
            } else {
              process_delegate_result(interp, gen, result, fn(_) { NextAction })
            }
          }
          None => {
            // No .throw: close delegate via .return() then throw TypeError
            let return_method = get_optional_method(iterator, "return", interp)
            match return_method {
              Some(rm) => {
                let close_result = interp.call_value(rm, iterator, [], loc)
                guard is_object_value(close_result) else {
                  raise @errors.TypeError(
                    message="Iterator close result is not an object",
                  )
                }
              }
              None => ()
            }
            gen.delegating = false
            gen.delegate_iterator = Undefined
            gen.delegate_next = Undefined
            raise @errors.TypeError(
              message="Iterator does not have a 'throw' method",
            )
          }
        }
      } catch {
        err => inject_error(interp, gen, err)
      }
    Return(v) =>
      try {
        let return_method = get_optional_method(iterator, "return", interp)
        match return_method {
          Some(rm) => {
            let result = interp.call_value(rm, iterator, [v], loc)
            if gen.is_async {
              gen.state = SuspendedYield
              create_iter_result(result, false)
            } else {
              process_delegate_result(interp, gen, result, fn(v) {
                ReturnAction(v)
              })
            }
          }
          None => {
            // No .return: propagate return through outer body for finally blocks
            gen.delegating = false
            gen.delegate_iterator = Undefined
            gen.delegate_next = Undefined
            gen.yield_value = v
            gen.resume_action = ReturnAction(v)
            generator_continue(interp, gen)
          }
        }
      } catch {
        err => inject_error(interp, gen, err)
      }
  }
}

///|
/// `None` if the property is absent, undefined, or null, or `Some(method)`
/// if it exists and is callable.  Raises `TypeError` if the property exists
/// but is not callable.  Delegates to `interp.get_property` for the full
/// `[[Get]]` chain (own data, own accessor getters, prototype walk, proxies).
fn get_optional_method(
  obj : Value,
  name : String,
  interp : Interpreter,
) -> Value? raise Error {
  let loc = @token.Loc::default()
  let func = interp.get_property(obj, name, loc)
  match func {
    Undefined | Null => None
    Object(fd) =>
      match fd.callable {
        Some(_) => Some(func)
        None => raise @errors.TypeError(message="\{name} is not a function")
      }
    _ => raise @errors.TypeError(message="\{name} is not a function")
  }
}

///|
/// On abrupt generator resume (ReturnAction/ThrowAction) while a destructuring
/// iterator is stacked, close iterators per §7.4.9 and propagate the
/// appropriate completion signal.  The for-of caller additionally closes its
/// own outer iterator *before* calling this helper.
///
/// Close order: innermost-first (index 0 = first pushed = innermost level),
/// matching the non-generator spec flow where each nested pattern closes its
/// own iterator before the containing pattern does.
fn Interpreter::close_dstr_stack_on_abrupt_resume(
  self : Interpreter,
  g : GeneratorObject,
  loc : @token.Loc,
) -> Unit raise Error {
  guard g.dstr_iterator_stack.length() > 0 else { return }
  // Only abrupt completions need to close iterators.
  let action = g.resume_action
  guard action is (ReturnAction(_) | ThrowAction(_)) else { return }
  g.resume_action = NextAction
  // Snapshot in push order (index 0 = innermost); drain the stack so stale
  // refs don't linger if a close raises.
  let n = g.dstr_iterator_stack.length()
  let iters : Array[Value] = []
  for i = 0; i < n; i = i + 1 {
    iters.push(g.dstr_iterator_stack[i])
  }
  while g.dstr_iterator_stack.length() > 0 {
    let _ = g.dstr_iterator_stack.pop()
  }
  match action {
    ReturnAction(v) => {
      // §7.4.9 step 8: a close error replaces the return completion.
      // Continue closing remaining iterators even after an error.
      // First error wins: once a completion is abrupt, later close errors are
      // discarded (§7.4.9 step 4 in subsequent levels).
      let mut close_err : Error? = None
      for iter in iters {
        self.iterator_close(iter, loc) catch {
          e => if close_err is None { close_err = Some(e) }
        }
      }
      match close_err {
        Some(e) => raise e
        None => raise GeneratorReturnSignal(v)
      }
    }
    ThrowAction(e) => {
      for iter in iters {
        // ThrowCompletion: §7.4.11 step 5 — close errors discarded.
        self.iterator_close_throw(iter, loc)
      }
      raise JsException(e)
    }
    NextAction => () // unreachable: guard above excludes NextAction
  }
}