///|
/// Returns true when a FunctionDeclaration's name is already present as a
/// `VarBinding` in the current environment — meaning the instantiation/hoisting
/// pass already bound the function value, so the FunctionDeclaration's
/// *execution* step is a no-op. Module environments are also var scopes for
/// module-scoped `var` bindings, and module declaration instantiation
/// pre-initializes top-level function declarations this way.
fn function_decl_is_hoisted_noop(env : Environment, name : String) -> Bool {
  env.is_var_scope &&
  (match env.bindings.get(name) {
    Some(binding) => binding.kind == VarBinding
    None => false
  })
}

///|
/// Return the current value of a HoistableDeclaration binding created by
/// BlockDeclarationInstantiation. Block environments are not var scopes, and
/// their mutable lexical binding may have been assigned a non-function value
/// before execution reaches the declaration's no-op evaluation step.
fn instantiated_block_function_value(
  env : Environment,
  name : String,
) -> Value? {
  guard !env.is_var_scope else { return None }
  match env.bindings.get(name) {
    Some(binding) if binding.kind == LetBinding && binding.initialized =>
      Some(binding.value)
    _ => None
  }
}

///|
/// Bind a FunctionDeclaration's value into `env` per §B.3.3.3 step 2.b.i and
/// the runtime side of §10.2 FunctionDeclarationInstantiation, then propagate
/// the value to the outer var scope when Annex B hoisting tagged it.
///
/// The kind-gated match enforces the §B.3.3.3 conflict-skip invariant: a
/// block-level FunctionDeclaration whose name already binds a `let`, `const`,
/// or `class` in the surrounding scope must NOT overwrite that binding. The
/// hoist pass suppresses the Annex B var-promotion in that case; the runtime
/// must remain a no-op so `let f = 1; if (true) function f(){}` keeps
/// `f === 1`. Shared by FuncDecl and FuncDeclExt — keep it that way so the
/// next bug fix lands once, not twice (see PR #119 Codex review).
fn Interpreter::bind_function_decl_value(
  self : Interpreter,
  ctx : ExecContext,
  env : Environment,
  name : String,
  func_val : Value,
) -> Unit raise Error {
  let existing_kind = match env.bindings.get(name) {
    Some(b) => Some(b.kind)
    None => None
  }
  match existing_kind {
    Some(VarBinding) => env.assign(name, func_val)
    Some(_) => () // §B.3.3.3 conflict: leave outer lex binding intact
    None =>
      if env.has_marker_in_chain(eval_deletable_var_marker(name)) &&
        !env.has_var(name) &&
        env.parent is Some(p) &&
        physical_equal(p, env.find_var_env()) {
        // Top-level eval function whose binding was deleted — skip to
        // avoid recreating it. Block-level functions have a different
        // env.parent (block_env) and proceed normally to create a new
        // block-scoped binding.
        ()
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, func_val)
      } else {
        env.def(name, func_val, VarBinding)
      }
  }
  // Annex B §B.3.3.3: propagate block-level function to outer var scope when
  // the hoist pass tagged the binding `annex_b_hoisted`. The tag is only set
  // on reusable slots (param/var bindings), so genuine lex bindings in the
  // outer scope are never touched here.
  if self.annex_b && !ctx.strict && !env.is_var_scope {
    let var_env = env.find_var_env()
    match var_env.bindings.get(name) {
      Some(binding) => if binding.annex_b_hoisted { binding.value = func_val }
      None => ()
    }
  }
}

///|
const FOR_OF_RESUME_VALUE_BINDING = "[[ForOfResumeValue]]"

///|
/// Collect all binding names from a pattern (IdentPat leaves only).
/// Used to build the TDZ environment for let/const for-of ForDeclaration
/// per spec §14.7.5.12 step 2 (ForIn/OfHeadEvaluation TDZnames).
fn collect_pattern_tdz_names(
  pattern : @ast.Pattern,
  names : Array[String],
) -> Unit {
  for name in @static_semantics.bound_names(pattern) {
    names.push(name)
  }
}

///|
fn save_for_of_resume_state(
  gen_opt : GeneratorObject?,
  iterator : Value,
  next_method : Value,
  loop_env : Environment,
  yield_base : Int,
) -> Unit {
  match gen_opt {
    Some(g) => {
      g.for_of_iterator = iterator
      g.for_of_next = next_method
      g.for_of_iterator_stack.push(iterator)
      g.for_of_next_stack.push(next_method)
      g.for_of_resume = true
      g.loop_env_stack.push(loop_env)
      g.loop_yield_base_stack.push(yield_base)
    }
    None => ()
  }
}

///|
fn restore_for_of_resume_state(gen_opt : GeneratorObject?) -> (Value, Value) {
  match gen_opt {
    Some(g) => {
      let iterator = g.for_of_iterator_stack.pop().unwrap()
      let next_method = g.for_of_next_stack.pop().unwrap()
      if g.for_of_iterator_stack.length() > 0 {
        let idx = g.for_of_iterator_stack.length() - 1
        g.for_of_iterator = g.for_of_iterator_stack[idx]
        g.for_of_next = g.for_of_next_stack[idx]
        g.for_of_resume = true
      } else {
        g.for_of_iterator = Undefined
        g.for_of_next = Undefined
        g.for_of_resume = false
      }
      (iterator, next_method)
    }
    None => abort("unreachable: for-of resume without generator")
  }
}

///|
fn save_for_of_resume_value(loop_env : Environment, value : Value) -> Unit {
  loop_env.bindings[FOR_OF_RESUME_VALUE_BINDING] = {
    value,
    kind: VarBinding,
    initialized: true,
    annex_b_hoisted: false,
    is_parameter: false,
  }
}

///|
fn take_for_of_resume_value(loop_env : Environment) -> Value? {
  match loop_env.bindings.get(FOR_OF_RESUME_VALUE_BINDING) {
    Some(binding) => {
      let value = binding.value
      let _ = loop_env.bindings.remove(FOR_OF_RESUME_VALUE_BINDING)
      Some(value)
    }
    None => None
  }
}

///|
fn restore_for_of_loop_env(
  gen_opt : GeneratorObject?,
  fallback : Environment,
) -> Environment {
  match gen_opt {
    Some(g) =>
      if g.loop_env_stack.length() > 0 {
        let saved_env = g.loop_env_stack.pop().unwrap()
        match g.loop_yield_base_stack.pop() {
          Some(base) => g.yield_index = base
          None => ()
        }
        saved_env
      } else {
        fallback
      }
    None => fallback
  }
}

///|
fn save_stmt_resume_frame(
  gen_opt : GeneratorObject?,
  index : Int,
  env : Environment,
  last : Value,
) -> Unit {
  match gen_opt {
    Some(g) => {
      g.stmt_resume_index_stack.push(index)
      g.stmt_resume_env_stack.push(env)
      g.stmt_resume_value_stack.push(last)
    }
    None => ()
  }
}

///|
fn restore_stmt_resume_frame(
  gen_opt : GeneratorObject?,
  fallback_env : Environment,
) -> (Int, Environment, Value) {
  match gen_opt {
    Some(g) =>
      if g.resuming && g.stmt_resume_index_stack.length() > 0 {
        let index = g.stmt_resume_index_stack.pop().unwrap()
        let saved_env = g.stmt_resume_env_stack.pop().unwrap()
        let last = g.stmt_resume_value_stack.pop().unwrap()
        (index, saved_env, last)
      } else {
        (0, fallback_env, Undefined)
      }
    None => (0, fallback_env, Undefined)
  }
}

///|
fn Interpreter::exec_stmt(
  self : Interpreter,
  ctx : ExecContext,
  stmt : @ast.Stmt,
  env : Environment,
  label? : String? = None,
) -> Signal raise Error {
  self.observe_execution_step()
  match stmt {
    ExprStmt(expr, _) => Normal(self.eval_expr(ctx, expr, env))
    VarDecl(kind, name, init, _) => {
      // Strict mode: cannot use eval/arguments as variable name
      if ctx.strict {
        @static_semantics.validate_strict_binding_name(name)
      }
      let bk : BindingKind = match kind {
        LetKind => LetBinding
        ConstKind => ConstBinding
        VarKind => VarBinding
      }
      let value : Value = match init {
        Some(expr) => self.eval_named_expr(ctx, expr, env, name)
        None => Undefined
      }
      // For var declarations that were hoisted, use assign instead of def.
      // Check parent scope too — var may have been hoisted to a parent env
      // (e.g., eval hoists var to caller scope, block-scoped var goes to function scope).
      // Use has_var to only match VarBinding, not let/const in parent scopes.
      if bk == VarBinding && (env.bindings.contains(name) || env.has_var(name)) {
        if init is Some(_) {
          // Use assign_var to skip any intervening let/const that shadow the name.
          // This ensures eval("var x = 1") targets the function scope's var x,
          // not a block-scoped let x in between.
          env.assign_var(name, value)
          // Mirror var assignment to global object when in global scope
          if physical_equal(env.find_var_env(), self.global) {
            self.mirror_to_global(name, value)
          }
        }
      } else if bk == VarBinding &&
        env.has_marker_in_chain(eval_deletable_var_marker(name)) {
        // Marker exists — check if the binding was deleted by a prior
        // `delete` in the same eval.
        if env.has_var(name) {
          // Binding still exists: marker is still valid. The hoisted
          // value takes precedence — skip the initializer.
          ()
        } else if init is Some(_) {
          // Binding was deleted but there's an initializer (e.g.
          // `eval("var x; delete x; var x = 1;")`). The initializer was
          // already evaluated. Since the env binding is gone, PutValue
          // creates an implicit global in sloppy mode.
          // In strict mode, throw ReferenceError per §13.3.2.2.
          match env.resolve_binding_env(name) {
            Some(target_env) =>
              target_env.assign_resolved(name, value, ctx.strict) catch {
                e => raise e
              }
            None => {
              if ctx.strict {
                raise @errors.ReferenceError(message="\{name} is not defined")
              }
              self.global.def(name, value, VarBinding)
              self.mirror_to_global(name, value, configurable=true)
            }
          }
        }
        // No init and binding was deleted: bare `var x` after `delete x`
        // does nothing — the declaration was already hoisted and deleted.
      } else if (bk == LetBinding || bk == ConstBinding) &&
        env.bindings.contains(name) {
        // let/const hoisted with TDZ - initialize it
        env.initialize(name, value)
      } else {
        env.def(name, value, bk)
        // Mirror new var declaration to global object when in global scope
        if bk == VarBinding && physical_equal(env.find_var_env(), self.global) {
          self.mirror_to_global(name, value)
        }
      }
      Normal(Undefined)
    }
    Block(stmts, _) => {
      let block_env = Environment::new(parent=Some(env))
      self.instantiate_block_declarations(stmts, block_env, ctx.strict)
      self.exec_stmts(ctx, stmts, block_env)
    }
    StmtList(stmts, _) =>
      // Execute statements in same scope (no new environment)
      // Used for comma-separated declarations like: var a, b, c;
      self.exec_stmts(ctx, stmts, env)
    IfStmt(cond, then_branch, else_branch, _) => {
      let cond_val = self.eval_expr(ctx, cond, env)
      if is_truthy(cond_val) {
        self.exec_stmt(ctx, then_branch, env)
      } else {
        match else_branch {
          Some(eb) => self.exec_stmt(ctx, eb, env)
          None => Normal(Undefined)
        }
      }
    }
    WhileStmt(cond, body, _) => {
      let gen_opt = ctx.current_generator
      let resuming_loop = match gen_opt {
        Some(g) => g.loop_env_stack.length() > 0
        _ => false
      }
      let mut first_iteration = resuming_loop
      // If resuming, pop the env marker (we push env as a marker for while loops)
      if resuming_loop {
        match gen_opt {
          Some(g) => {
            let _ = g.loop_env_stack.pop()
            match g.loop_yield_base_stack.pop() {
              Some(base) => g.yield_index = base
              None => ()
            }
          }
          _ => ()
        }
      }
      // Track last completion value for eval completion semantics
      let mut completion : Value = Undefined
      while true {
        if first_iteration {
          first_iteration = false
        } else if !is_truthy(self.eval_expr(ctx, cond, env)) {
          break
        }
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          _ => 0
        }
        let body_signal = self.exec_stmt(ctx, body, env) catch {
          e => {
            if e is YieldSignal(_) {
              match gen_opt {
                Some(g) => {
                  g.loop_env_stack.push(env)
                  g.loop_yield_base_stack.push(body_yield_base)
                }
                _ => ()
              }
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForStmt(init, cond, update, body, _) => {
      // Check if we're resuming inside a generator loop
      let gen_opt = ctx.current_generator
      let resuming_loop = match gen_opt {
        Some(g) => g.loop_env_stack.length() > 0
        _ => false
      }
      // Collect let/const variable names from init for per-iteration scoping
      let loop_vars : Array[String] = []
      fn collect_loop_vars(stmt : @ast.Stmt) {
        match stmt {
          VarDecl(kind, name, _, _) =>
            if kind == LetKind || kind == ConstKind {
              loop_vars.push(name)
            }
          StmtList(stmts, _) =>
            for s in stmts {
              collect_loop_vars(s)
            }
          _ => ()
        }
      }
      match init {
        Some(init_stmt) => collect_loop_vars(init_stmt)
        None => ()
      }
      let has_lexical_vars = loop_vars.length() > 0
      let for_env = if resuming_loop {
        // Use saved loop env from stack (pop from end = outermost first)
        match gen_opt {
          Some(g) => {
            let saved_env = g.loop_env_stack.pop().unwrap()
            match g.loop_yield_base_stack.pop() {
              Some(base) => g.yield_index = base
              None => ()
            }
            saved_env
          }
          _ => Environment::new(parent=Some(env))
        }
      } else {
        let e = Environment::new(parent=Some(env))
        match init {
          Some(init_stmt) => {
            let _ = self.exec_stmt(ctx, init_stmt, e)
          }
          None => ()
        }
        e
      }
      let mut first_iteration = resuming_loop // skip condition on first resumed iteration
      let mut completion : Value = Undefined
      while true {
        if first_iteration {
          first_iteration = false
        } else {
          match cond {
            Some(c) => if !is_truthy(self.eval_expr(ctx, c, for_env)) { break }
            None => ()
          }
        }
        // Per-iteration scoping: create a new scope with copies of loop vars
        let iter_env = if has_lexical_vars {
          let ie = Environment::new(parent=Some(env))
          for v in loop_vars {
            ie.def(v, for_env.get(v), LetBinding)
          }
          ie
        } else {
          for_env
        }
        let mut do_continue = false
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          _ => 0
        }
        let body_signal = self.exec_stmt(ctx, body, iter_env) catch {
          e => {
            if e is YieldSignal(_) {
              // Yield inside for loop body — save for_env for resume
              match gen_opt {
                Some(g) => {
                  g.loop_env_stack.push(for_env)
                  g.loop_yield_base_stack.push(body_yield_base)
                }
                _ => ()
              }
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            do_continue = true
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              do_continue = true
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
        // Copy iteration values back to for_env for update expression
        if has_lexical_vars {
          for v in loop_vars {
            for_env.assign(v, iter_env.get(v))
          }
        }
        if do_continue {
          match update {
            Some(u) => {
              let _ = self.eval_expr(ctx, u, for_env)
            }
            None => ()
          }
          continue
        }
        match update {
          Some(u) => {
            let _ = self.eval_expr(ctx, u, for_env)
          }
          None => ()
        }
      }
      Normal(completion)
    }
    FuncDecl(name, params, body, _, source_text) => {
      validate_function_signature(ctx.strict, Some(name), params, body)
      match instantiated_block_function_value(env, name) {
        Some(func_val) => {
          // Standard block instantiation already initialized the lexical
          // binding. The execution step only performs Annex B propagation.
          self.bind_function_decl_value(ctx, env, name, func_val)
          return Normal(Undefined)
        }
        None => ()
      }
      // FunctionDeclaration at the top of a var scope is fully handled during
      // hoisting; its execution is a no-op.
      guard !function_decl_is_hoisted_noop(env, name) else {
        return Normal(Undefined)
      }
      let func_data : FuncData = {
        name: Some(name),
        params,
        body,
        closure: env,
        strict: is_function_strict(ctx.strict, body),
        // FunctionDeclaration execution path — name binds in enclosing
        // VarEnvironment, no dedicated funcEnv.
        has_name_binding: false,
        is_method: false,
        source_text,
      }
      let func_val = make_func(func_data)
      self.bind_function_decl_value(ctx, env, name, func_val)
      Normal(Undefined)
    }
    ReturnStmt(expr, _) => {
      let value : Value = match expr {
        Some(e) => self.eval_expr(ctx, e, env)
        None => Undefined
      }
      ReturnSignal(value)
    }
    BreakStmt(label, _) => BreakSignal(None, label)
    ContinueStmt(label, _) => ContinueSignal(None, label)
    ThrowStmt(expr, _) => {
      let value = self.eval_expr(ctx, expr, env)
      raise JsException(value)
    }
    TryCatchStmt(try_body, catch_param, catch_body, finally_body, _) =>
      self.exec_try_catch(
        ctx, try_body, catch_param, catch_body, finally_body, env,
      )
    SwitchStmt(discriminant, cases, _) => {
      let disc_val = self.eval_expr(ctx, discriminant, env)
      let switch_env = Environment::new(parent=Some(env))
      let block_stmts = flatten_case_block_statements(cases)
      self.instantiate_block_declarations(block_stmts, switch_env, ctx.strict)
      let mut match_idx = -1
      let mut default_idx = -1
      for i = 0; i < cases.length(); i = i + 1 {
        match cases[i].condition {
          Some(test_expr) =>
            if match_idx < 0 {
              let test_val = self.eval_expr(ctx, test_expr, switch_env)
              if strict_equal(disc_val, test_val) {
                match_idx = i
              }
            }
          None => default_idx = i
        }
      }
      let start = if match_idx >= 0 {
        match_idx
      } else if default_idx >= 0 {
        default_idx
      } else {
        cases.length()
      }
      let mut completion : Value = Undefined
      for i = start; i < cases.length(); i = i + 1 {
        for stmt in cases[i].body {
          match self.exec_stmt(ctx, stmt, switch_env) {
            Normal(v) => completion = v
            BreakSignal(v, None) => {
              // UpdateEmpty: use break's carried value if non-empty, otherwise accumulated
              let cv = match v {
                None => completion
                Some(val) => val
              }
              return Normal(cv)
            }
            BreakSignal(v, Some(l)) => {
              let cv = match v {
                None => Some(completion)
                some => some
              }
              return BreakSignal(cv, Some(l))
            }
            ContinueSignal(v, label) => {
              let cv = match v {
                None => Some(completion)
                some => some
              }
              return ContinueSignal(cv, label)
            }
            other => return other
          }
        }
      }
      Normal(completion)
    }
    DoWhileStmt(body, cond, _) => {
      let gen_opt = ctx.current_generator
      let resuming_loop = match gen_opt {
        Some(g) => g.loop_env_stack.length() > 0
        _ => false
      }
      if resuming_loop {
        match gen_opt {
          Some(g) => {
            let _ = g.loop_env_stack.pop()
            match g.loop_yield_base_stack.pop() {
              Some(base) => g.yield_index = base
              None => ()
            }
          }
          _ => ()
        }
      }
      let mut completion : Value = Undefined
      while true {
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          _ => 0
        }
        let body_signal = self.exec_stmt(ctx, body, env) catch {
          e => {
            if e is YieldSignal(_) {
              match gen_opt {
                Some(g) => {
                  g.loop_env_stack.push(env)
                  g.loop_yield_base_stack.push(body_yield_base)
                }
                _ => ()
              }
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => completion = v.unwrap_or(completion)
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
        if !is_truthy(self.eval_expr(ctx, cond, env)) {
          break
        }
      }
      Normal(completion)
    }
    ForInStmt(var_kind, name, obj_expr, body, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let keys = collect_for_in_keys(obj, self)
      let mut completion : Value = Undefined
      for key in keys {
        let body_env = Environment::new(parent=Some(env))
        match var_kind {
          Some(kind) => {
            let bk : BindingKind = match kind {
              LetKind => LetBinding
              ConstKind => ConstBinding
              VarKind => VarBinding
            }
            body_env.def(name, String_(key), bk)
          }
          None =>
            env.assign_with_strict(name, String_(key), ctx.strict) catch {
              @errors.ReferenceError(_) =>
                if !ctx.strict {
                  self.global.def(name, String_(key), VarBinding)
                  self.mirror_to_global(name, String_(key), configurable=true)
                } else {
                  raise @errors.ReferenceError(message="\{name} is not defined")
                }
              e => raise e
            }
        }
        match self.exec_stmt(ctx, body, body_env) {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForOfStmt(var_kind, name, iterable_expr, body, loc) => {
      let mut loop_binding_env : Environment? = None
      // Check if we're resuming inside a generator for-of
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        _ => false
      }
      let (iterator, next_method) = if resuming_for_of {
        // Restore saved iterator state
        restore_for_of_resume_state(gen_opt)
      } else {
        if var_kind is Some(LetKind) || var_kind is Some(ConstKind) {
          let loop_env = Environment::new(parent=Some(env))
          let loop_binding_kind : BindingKind = match var_kind {
            Some(LetKind) => LetBinding
            _ => ConstBinding
          }
          loop_env.def_tdz(name, loop_binding_kind)
          loop_binding_env = Some(loop_env)
        }
        let iterable_env = match loop_binding_env {
          Some(e) => e
          None => env
        }
        let iterable = self.eval_expr(ctx, iterable_expr, iterable_env)

        // Try to get the iterator via Symbol.iterator protocol
        let iterator_sym = self.realm_state.well_known_symbols.iterator
        let iterator_method = self.get_computed_property(
          iterable,
          Symbol(iterator_sym),
          loc,
        )

        // Get the iterator object by calling the iterator method
        let it = match iterator_method {
          Object(data) =>
            match data.callable {
              Some(_) => self.call_value(iterator_method, iterable, [], loc)
              None =>
                raise @errors.TypeError(
                  message="Result of the Symbol.iterator method is not an object",
                )
            }
          Undefined =>
            // Fall back to built-in iteration for arrays and strings
            match iterable {
              Array(arr) => self.realm_state.make_array_iterator_value(arr)
              String_(s) => self.realm_state.make_string_iterator_value(s)
              _ =>
                raise @errors.TypeError(
                  message="\{type_of(iterable)} is not iterable (cannot read property Symbol(Symbol.iterator))",
                )
            }
          _ =>
            raise @errors.TypeError(
              message="Result of the Symbol.iterator method is not an object",
            )
        }

        let nm = self.get_iterator_next_method(it, loc)
        (it, nm)
      }
      // If resuming, first resume the body without calling next()
      let mut first_resume = resuming_for_of
      let mut completion : Value = Undefined
      // Iterate using the iterator protocol
      while true {
        if first_resume {
          // On resume, the body was interrupted by yield — replay it
          // The loop variable is already bound from before the yield
          first_resume = false
          let body_env_parent = match loop_binding_env {
            Some(e) => e
            None => env
          }
          let body_env = Environment::new(parent=Some(body_env_parent))
          // We need the loop variable bound. Since we're replaying the body,
          // gen.resuming will handle the yield. But we need the loop variable.
          // It's in the saved loop env stack.
          let resuming_loop = match gen_opt {
            Some(g) => g.loop_env_stack.length() > 0
            _ => false
          }
          let actual_body_env = if resuming_loop {
            match gen_opt {
              Some(g) => {
                let saved_env = g.loop_env_stack.pop().unwrap()
                match g.loop_yield_base_stack.pop() {
                  Some(base) => g.yield_index = base
                  None => ()
                }
                saved_env
              }
              _ => body_env
            }
          } else {
            body_env
          }
          let body_yield_base = match gen_opt {
            Some(g) => g.yield_index
            _ => 0
          }
          let body_signal = self.exec_stmt(ctx, body, actual_body_env) catch {
            e => {
              if e is YieldSignal(_) {
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, actual_body_env, body_yield_base,
                )
              }
              raise e
            }
          }
          match body_signal {
            Normal(v) => completion = v
            ReturnSignal(v) => return ReturnSignal(v)
            BreakSignal(v, None) => {
              completion = v.unwrap_or(completion)
              break
            }
            BreakSignal(bv, Some(l)) =>
              if label == Some(l) {
                completion = bv.unwrap_or(completion)
                break
              } else {
                return BreakSignal(bv, Some(l))
              }
            ContinueSignal(v, None) => {
              completion = v.unwrap_or(completion)
              continue
            }
            ContinueSignal(cv, Some(l)) =>
              if label == Some(l) {
                completion = cv.unwrap_or(completion)
                continue
              } else {
                return ContinueSignal(cv, Some(l))
              }
          }
          // After resume body completes, continue the loop normally below
          continue
        }
        let value = match self.iterator_step_value(iterator, next_method, loc) {
          None => break
          Some(value) => value
        }

        // Bind the value to the loop variable
        let body_env_parent = match loop_binding_env {
          Some(e) => e
          None => env
        }
        let body_env = Environment::new(parent=Some(body_env_parent))
        match var_kind {
          Some(VarKind) =>
            if env.bindings.contains(name) {
              env.assign(name, value)
            } else {
              env.def(name, value, VarBinding)
            }
          Some(kind) => {
            let bk : BindingKind = match kind {
              LetKind => LetBinding
              ConstKind => ConstBinding
              _ => fail("unreachable")
            }
            body_env.def(name, value, bk)
          }
          None =>
            env.assign_with_strict(name, value, ctx.strict) catch {
              e => {
                close_iterator_throw(self, iterator, loc)
                raise e
              }
            }
        }

        // Execute the loop body
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          _ => 0
        }
        let body_signal = self.exec_stmt(ctx, body, body_env) catch {
          e => {
            if e is YieldSignal(_) {
              // Save for-of state for resume
              save_for_of_resume_state(
                gen_opt, iterator, next_method, body_env, body_yield_base,
              )
            } else {
              // Close iterator on exception (IteratorClose)
              close_iterator_throw(self, iterator, loc)
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => {
            close_iterator(self, iterator, loc)
            return ReturnSignal(v)
          }
          BreakSignal(v, None) => {
            close_iterator(self, iterator, loc)
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              close_iterator(self, iterator, loc)
              completion = bv.unwrap_or(completion)
              break
            } else {
              close_iterator(self, iterator, loc)
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              close_iterator(self, iterator, loc)
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForInStmtPat(var_kind, pattern, obj_expr, body, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let keys = collect_for_in_keys(obj, self)
      let mut completion : Value = Undefined
      for key in keys {
        let body_env = Environment::new(parent=Some(env))
        match var_kind {
          Some(kind) => {
            let bk : BindingKind = match kind {
              LetKind => LetBinding
              ConstKind => ConstBinding
              VarKind => VarBinding
            }
            let bind_env = match kind {
              VarKind => env
              _ => body_env
            }
            self.bind_pattern(pattern, String_(key), bind_env, bk, ctx~)
          }
          None => self.assign_pattern(ctx, pattern, String_(key), env)
        }
        match self.exec_stmt(ctx, body, body_env) {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForOfStmtPat(var_kind, pattern, iterable_expr, body, loc) => {
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        None => false
      }
      let (iterator, next_method) : (Value, Value) = if resuming_for_of {
        restore_for_of_resume_state(gen_opt)
      } else {
        // Per spec §14.7.5.12 ForIn/OfHeadEvaluation step 2: for let/const
        // ForDeclaration, create a TDZ env with the binding names before
        // evaluating the iterable expression. Functions created in the iterable
        // expression that reference these names will see them as uninitialized
        // (ReferenceError on access) rather than seeing the outer binding.
        let iterable_eval_env = match var_kind {
          Some(LetKind | ConstKind) => {
            let tdz_env = Environment::new(parent=Some(env))
            let bk : BindingKind = match var_kind {
              Some(LetKind) => LetBinding
              _ => ConstBinding
            }
            let tdz_names : Array[String] = []
            collect_pattern_tdz_names(pattern, tdz_names)
            for tdz_name in tdz_names {
              tdz_env.def_tdz(tdz_name, bk)
            }
            tdz_env
          }
          _ => env
        }
        let iterable = self.eval_expr(ctx, iterable_expr, iterable_eval_env)

        // Try to get the iterator via Symbol.iterator protocol
        let iterator_sym = self.realm_state.well_known_symbols.iterator
        let iterator_method = self.get_computed_property(
          iterable,
          Symbol(iterator_sym),
          loc,
        )

        // Get the iterator object
        let iterator = match iterator_method {
          Object(data) =>
            match data.callable {
              Some(_) => self.call_value(iterator_method, iterable, [], loc)
              None =>
                raise @errors.TypeError(
                  message="Result of the Symbol.iterator method is not an object",
                )
            }
          Undefined =>
            match iterable {
              Array(arr) => self.realm_state.make_array_iterator_value(arr)
              String_(s) => self.realm_state.make_string_iterator_value(s)
              _ =>
                raise @errors.TypeError(
                  message="\{type_of(iterable)} is not iterable",
                )
            }
          _ =>
            raise @errors.TypeError(
              message="Result of the Symbol.iterator method is not an object",
            )
        }

        (iterator, self.get_iterator_next_method(iterator, loc))
      }

      // Iterate using the iterator protocol
      let mut completion : Value = Undefined
      let mut first_resume = resuming_for_of
      while true {
        let (value, body_env, should_bind) : (Value, Environment, Bool) = if first_resume {
          first_resume = false
          let saved_env = restore_for_of_loop_env(
            gen_opt,
            Environment::new(parent=Some(env)),
          )
          match take_for_of_resume_value(saved_env) {
            Some(value) => (value, saved_env, true)
            None => (Undefined, saved_env, false)
          }
        } else {
          let value = match
            self.iterator_step_value(iterator, next_method, loc) {
            None => break
            Some(value) => value
          }
          (value, Environment::new(parent=Some(env)), true)
        }
        if should_bind {
          let binding_yield_base = match gen_opt {
            Some(g) => g.yield_index
            None => 0
          }
          match var_kind {
            Some(kind) => {
              let bk : BindingKind = match kind {
                LetKind => LetBinding
                ConstKind => ConstBinding
                VarKind => VarBinding
              }
              let bind_env = match kind {
                VarKind => env
                _ => body_env
              }
              self.bind_pattern(pattern, value, bind_env, bk, ctx~) catch {
                e => {
                  if e is YieldSignal(_) {
                    save_for_of_resume_value(body_env, value)
                    save_for_of_resume_state(
                      gen_opt, iterator, next_method, body_env, binding_yield_base,
                    )
                  } else if e is GeneratorReturnSignal(_) {
                    // §7.4.9: return completion — outer loop close error replaces it
                    close_iterator(self, iterator, loc) catch {
                      close_err => raise close_err
                    }
                  } else {
                    // Throw completion — outer close error discarded (§7.4.9 step 4)
                    close_iterator_throw(self, iterator, loc)
                  }
                  raise e
                }
              }
            }
            None =>
              // No exec_stmt-level guard here: the guard lives inside
              // assign_pattern's ArrayPat branch (destructuring.mbt).  On abrupt
              // resume with a stacked dstr iterator, assign_pattern raises from
              // within, which propagates here and triggers the catch block below —
              // closing the outer for-of iterator in the right order (inner first,
              // then outer), mirroring how the Some(kind) → bind_pattern arm works.
              self.assign_pattern(ctx, pattern, value, env) catch {
                e => {
                  if e is YieldSignal(_) {
                    save_for_of_resume_value(body_env, value)
                    save_for_of_resume_state(
                      gen_opt, iterator, next_method, body_env, binding_yield_base,
                    )
                  } else if e is GeneratorReturnSignal(_) {
                    // §7.4.9: return completion — outer loop close error replaces it
                    close_iterator(self, iterator, loc) catch {
                      close_err => raise close_err
                    }
                  } else {
                    // Throw completion — outer close error discarded (§7.4.9 step 4)
                    close_iterator_throw(self, iterator, loc)
                  }
                  raise e
                }
              }
          }
        }
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          None => 0
        }
        let body_signal = self.exec_stmt(ctx, body, body_env) catch {
          e => {
            if e is YieldSignal(_) {
              save_for_of_resume_state(
                gen_opt, iterator, next_method, body_env, body_yield_base,
              )
            } else {
              close_iterator_throw(self, iterator, loc)
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => {
            close_iterator(self, iterator, loc)
            return ReturnSignal(v)
          }
          BreakSignal(v, None) => {
            close_iterator(self, iterator, loc)
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              close_iterator(self, iterator, loc)
              completion = bv.unwrap_or(completion)
              break
            } else {
              close_iterator(self, iterator, loc)
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              close_iterator(self, iterator, loc)
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForInExpr(lhs_expr, obj_expr, body, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let keys = collect_for_in_keys(obj, self)
      let mut completion : Value = Undefined
      for key in keys {
        let body_env = Environment::new(parent=Some(env))
        self.assign_to_expr(ctx, lhs_expr, String_(key), env)
        match self.exec_stmt(ctx, body, body_env) {
          Normal(v) => completion = v
          ReturnSignal(v) => return ReturnSignal(v)
          BreakSignal(v, None) => {
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              completion = bv.unwrap_or(completion)
              break
            } else {
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    ForOfExpr(lhs_expr, iterable_expr, body, loc) => {
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        None => false
      }
      let (iterator, next_method) : (Value, Value) = if resuming_for_of {
        restore_for_of_resume_state(gen_opt)
      } else {
        let iterable = self.eval_expr(ctx, iterable_expr, env)
        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(_) => self.call_value(iterator_method, iterable, [], loc)
              None =>
                raise @errors.TypeError(
                  message="Result of the Symbol.iterator method is not an object",
                )
            }
          Undefined =>
            match iterable {
              Array(arr) => self.realm_state.make_array_iterator_value(arr)
              String_(s) => self.realm_state.make_string_iterator_value(s)
              _ =>
                raise @errors.TypeError(
                  message="\{type_of(iterable)} is not iterable",
                )
            }
          _ =>
            raise @errors.TypeError(
              message="Result of the Symbol.iterator method is not an object",
            )
        }
        (iterator, self.get_iterator_next_method(iterator, loc))
      }
      let mut completion : Value = Undefined
      let mut first_resume = resuming_for_of
      while true {
        let (value, body_env, should_assign) : (Value, Environment, Bool) = if first_resume {
          first_resume = false
          let saved_env = restore_for_of_loop_env(
            gen_opt,
            Environment::new(parent=Some(env)),
          )
          match take_for_of_resume_value(saved_env) {
            Some(value) => (value, saved_env, true)
            None => (Undefined, saved_env, false)
          }
        } else {
          let value = match
            self.iterator_step_value(iterator, next_method, loc) {
            None => break
            Some(value) => value
          }
          (value, Environment::new(parent=Some(env)), true)
        }
        if should_assign {
          let assignment_yield_base = match gen_opt {
            Some(g) => g.yield_index
            None => 0
          }
          self.assign_to_expr(ctx, lhs_expr, value, env) catch {
            e => {
              if e is YieldSignal(_) {
                save_for_of_resume_value(body_env, value)
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, body_env, assignment_yield_base,
                )
              } else {
                close_iterator_throw(self, iterator, loc)
              }
              raise e
            }
          }
        }
        let body_yield_base = match gen_opt {
          Some(g) => g.yield_index
          None => 0
        }
        let body_signal = self.exec_stmt(ctx, body, body_env) catch {
          e => {
            if e is YieldSignal(_) {
              save_for_of_resume_state(
                gen_opt, iterator, next_method, body_env, body_yield_base,
              )
            } else {
              close_iterator_throw(self, iterator, loc)
            }
            raise e
          }
        }
        match body_signal {
          Normal(v) => completion = v
          ReturnSignal(v) => {
            close_iterator(self, iterator, loc)
            return ReturnSignal(v)
          }
          BreakSignal(v, None) => {
            close_iterator(self, iterator, loc)
            completion = v.unwrap_or(completion)
            break
          }
          BreakSignal(bv, Some(l)) =>
            if label == Some(l) {
              close_iterator(self, iterator, loc)
              completion = bv.unwrap_or(completion)
              break
            } else {
              close_iterator(self, iterator, loc)
              return BreakSignal(bv, Some(l))
            }
          ContinueSignal(v, None) => {
            completion = v.unwrap_or(completion)
            continue
          }
          ContinueSignal(cv, Some(l)) =>
            if label == Some(l) {
              completion = cv.unwrap_or(completion)
              continue
            } else {
              close_iterator(self, iterator, loc)
              return ContinueSignal(cv, Some(l))
            }
        }
      }
      Normal(completion)
    }
    DestructureDecl(kind, pattern, init_expr, _) => {
      let bk : BindingKind = match kind {
        LetKind => LetBinding
        ConstKind => ConstBinding
        VarKind => VarBinding
      }
      let value = self.eval_expr(ctx, init_expr, env)
      self.bind_pattern(pattern, value, env, bk, ctx~)
      Normal(Undefined)
    }
    FuncDeclExt(name, params, rest_param, body, _, source_text) => {
      validate_function_signature_ext(
        ctx.strict,
        Some(name),
        params,
        rest_param,
        body,
      )
      match instantiated_block_function_value(env, name) {
        Some(func_val) => {
          self.bind_function_decl_value(ctx, env, name, func_val)
          return Normal(Undefined)
        }
        None => ()
      }
      guard !function_decl_is_hoisted_noop(env, name) else {
        return Normal(Undefined)
      }
      let func_data : FuncDataExt = {
        name: Some(name),
        params,
        rest_param,
        body,
        closure: env,
        strict: is_function_strict(ctx.strict, body),
        has_name_binding: false,
        is_method: false,
        source_text,
      }
      let func_val = make_func_ext(func_data)
      self.bind_function_decl_value(ctx, env, name, func_val)
      Normal(Undefined)
    }
    GeneratorDecl(name, params, body, _, source_text) => {
      validate_function_signature(ctx.strict, Some(name), params, body)
      guard instantiated_block_function_value(env, name) is None &&
        !function_decl_is_hoisted_noop(env, name) else {
        return Normal(Undefined)
      }
      let gen_func = self.make_generator_function(
        Some(name),
        params,
        None,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, gen_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, gen_func)
      } else {
        env.def(name, gen_func, VarBinding)
      }
      Normal(Undefined)
    }
    GeneratorDeclExt(name, params, rest_param, body, _, source_text) => {
      validate_function_signature_ext(
        ctx.strict,
        Some(name),
        params,
        rest_param,
        body,
      )
      guard instantiated_block_function_value(env, name) is None &&
        !function_decl_is_hoisted_noop(env, name) else {
        return Normal(Undefined)
      }
      let gen_func = self.make_generator_function_ext(
        Some(name),
        params,
        rest_param,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, gen_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, gen_func)
      } else {
        env.def(name, gen_func, VarBinding)
      }
      Normal(Undefined)
    }
    AsyncFuncDecl(name, params, body, _, source_text) => {
      validate_function_signature(ctx.strict, Some(name), params, body)
      guard instantiated_block_function_value(env, name) is None else {
        return Normal(Undefined)
      }
      let async_func = self.make_async_function(
        Some(name),
        params,
        None,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, async_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, async_func)
      } else {
        env.def(name, async_func, VarBinding)
      }
      Normal(Undefined)
    }
    AsyncFuncDeclExt(name, params, rest_param, body, _, source_text) => {
      validate_function_signature_ext(
        ctx.strict,
        Some(name),
        params,
        rest_param,
        body,
      )
      guard instantiated_block_function_value(env, name) is None else {
        return Normal(Undefined)
      }
      let async_func = self.make_async_function_ext(
        Some(name),
        params,
        rest_param,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, async_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, async_func)
      } else {
        env.def(name, async_func, VarBinding)
      }
      Normal(Undefined)
    }
    AsyncGeneratorDecl(name, params, body, _, source_text) => {
      validate_function_signature(ctx.strict, Some(name), params, body)
      guard instantiated_block_function_value(env, name) is None else {
        return Normal(Undefined)
      }
      let gen_func = self.make_async_generator_function(
        Some(name),
        params,
        None,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, gen_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, gen_func)
      } else {
        env.def(name, gen_func, VarBinding)
      }
      Normal(Undefined)
    }
    AsyncGeneratorDeclExt(name, params, rest_param, body, _, source_text) => {
      validate_function_signature_ext(
        ctx.strict,
        Some(name),
        params,
        rest_param,
        body,
      )
      guard instantiated_block_function_value(env, name) is None else {
        return Normal(Undefined)
      }
      let gen_func = self.make_async_generator_function_ext(
        Some(name),
        params,
        rest_param,
        body,
        is_function_strict(ctx.strict, body),
        env,
        source_text~,
      )
      if env.bindings.contains(name) {
        env.assign(name, gen_func)
      } else if should_reconcile_eval_function_decl(env, name) {
        env.assign_var(name, gen_func)
      } else {
        env.def(name, gen_func, VarBinding)
      }
      Normal(Undefined)
    }
    LabeledStmt(lbl, body, _) =>
      match self.exec_stmt(ctx, body, env, label=Some(lbl)) {
        BreakSignal(v, Some(l)) =>
          if l == lbl {
            Normal(v.unwrap_or(Undefined))
          } else {
            BreakSignal(v, Some(l))
          }
        other => other
      }
    ClassDecl(name, superclass, methods, _, source_text) => {
      let class_val = self.create_class(
        ctx,
        name,
        superclass,
        methods,
        env,
        source_text,
        class_binding=Some(name),
      )
      // Class was hoisted with TDZ, initialize it now
      if env.bindings.contains(name) {
        env.initialize(name, class_val)
      } else {
        env.def(name, class_val, LetBinding)
      }
      Normal(Undefined)
    }
    // ES Module declarations
    ImportDecl(default_import, specifiers, namespace_import, source, _, loc) =>
      self.exec_import(
        default_import, specifiers, namespace_import, source, env, loc,
      )
    ExportNamedDecl(decl, specifiers, source, _, loc) =>
      self.exec_export_named(ctx, decl, specifiers, source, env, loc)
    ExportDefaultDecl(expr, _) => {
      if is_hoisted_default_export_expr(expr) &&
        self.module_exports.contains("default") {
        return Normal(Undefined)
      }
      let value = self.eval_named_expr(ctx, expr, env, "default")
      // Named declarations were predeclared as live cells backed by their
      // local binding. Preserve that cell and initialize only the local name;
      // synthetic defaults own their value directly in the exports map.
      match module_default_export_binding(expr) {
        LocalDefaultBinding(name) =>
          match expr {
            ClassExpr(_, _, _, _, _) =>
              if env.bindings.contains(name) {
                env.initialize(name, value)
              } else {
                env.def(name, value, LetBinding)
              }
            _ =>
              if !env.bindings.contains(name) {
                env.def(name, value, LetBinding)
              }
          }
        SyntheticDefaultBinding => self.module_exports["default"] = value
      }
      Normal(Undefined)
    }
    ExportAllDecl(ns_alias, source, _, loc) =>
      self.exec_export_all(ns_alias, source, loc)
    WithStmt(expr, body, _) => {
      // In strict mode, 'with' is always a SyntaxError
      if ctx.strict {
        raise @errors.SyntaxError(
          message="Strict mode code may not include a with statement",
        )
      }
      let raw_obj = self.eval_expr(ctx, expr, env)
      // Per spec: ToObject() the expression result. Throw TypeError for null/undefined.
      let obj = match raw_obj {
        Null => raise @errors.TypeError(message="Cannot convert null to object")
        Undefined =>
          raise @errors.TypeError(message="Cannot convert undefined to object")
        String_(s) => {
          // Box string primitive to String wrapper object
          let str_proto = env.get("[[StringPrototype]]") catch { _ => Null }
          let props : Map[String, Value] = Map([])
          let utf16_units = string_to_utf16(s)
          props["length"] = Number(utf16_units.length().to_double())
          // Add character indices as properties (UTF-16 code units)
          for i = 0; i < utf16_units.length(); i = i + 1 {
            let unit = utf16_units[i]
            let ch_str = if unit <= 0xFFFF {
              String::make(1, unit.unsafe_to_char())
            } else {
              String::make(1, unit.unsafe_to_char())
            }
            props[i.to_string()] = String_(ch_str)
          }
          Object({
            bag: {
              properties: props,
              symbol_properties: Map([]),
              descriptors: Map([]),
              symbol_descriptors: Map([]),
              internal_slots: Map::from_array([(PrimitiveValue, String_(s))]),
              host_slots: Map([]),
            },
            prototype: str_proto,
            callable: None,
            class_name: "String",
            extensible: true,
            arraybuffer_state: None,
          })
        }
        Number(n) => {
          // Box number primitive to Number wrapper object
          let num_proto = env.get("[[NumberPrototype]]") catch { _ => Null }
          Object({
            bag: {
              properties: Map([]),
              symbol_properties: Map([]),
              descriptors: Map([]),
              symbol_descriptors: Map([]),
              internal_slots: Map::from_array([(PrimitiveValue, Number(n))]),
              host_slots: Map([]),
            },
            prototype: num_proto,
            callable: None,
            class_name: "Number",
            extensible: true,
            arraybuffer_state: None,
          })
        }
        Bool(b) => {
          // Box boolean primitive to Boolean wrapper object
          let bool_proto = env.get("[[BooleanPrototype]]") catch { _ => Null }
          Object({
            bag: {
              properties: Map([]),
              symbol_properties: Map([]),
              descriptors: Map([]),
              symbol_descriptors: Map([]),
              internal_slots: Map::from_array([(PrimitiveValue, Bool(b))]),
              host_slots: Map([]),
            },
            prototype: bool_proto,
            callable: None,
            class_name: "Boolean",
            extensible: true,
            arraybuffer_state: None,
          })
        }
        Object(_) => raw_obj
        _ => raw_obj
      }
      // Create an object environment record with live object reference
      // Property lookups/assignments go directly through the object
      let with_env = Environment::new(parent=Some(env))
      with_env.with_object = Some(obj)
      self.exec_stmt(ctx, body, with_env)
    }
    AsyncForOfStmt(var_kind, name, iterable_expr, body, loc) => {
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        _ => false
      }
      let awaiting_next = match gen_opt {
        Some(g) => g.for_of_awaiting_next
        _ => false
      }
      let mut loop_binding_env : Environment? = None
      let (iterator, next_method) : (Value, Value) = if awaiting_next {
        match gen_opt {
          Some(g) => {
            g.for_of_awaiting_next = false
            g.resuming = false
            // Pop the loop env stacks pushed by save_for_of_resume_state
            // and restore the yield index so subsequent yields resume correctly.
            let _ = g.loop_env_stack.pop()
            match g.loop_yield_base_stack.pop() {
              Some(base) => g.yield_index = base
              None => ()
            }
          }
          _ => ()
        }
        let (it, nm) = restore_for_of_resume_state(gen_opt)
        (it, nm)
      } else if resuming_for_of {
        restore_for_of_resume_state(gen_opt)
      } else {
        if var_kind is Some(LetKind) || var_kind is Some(ConstKind) {
          let loop_env = Environment::new(parent=Some(env))
          let bk : BindingKind = match var_kind {
            Some(LetKind) => LetBinding
            _ => ConstBinding
          }
          loop_env.def_tdz(name, bk)
          loop_binding_env = Some(loop_env)
        }
        let iterable_env = match loop_binding_env {
          Some(e) => e
          None => env
        }
        let iterable = self.eval_expr(ctx, iterable_expr, iterable_env)
        let async_iter = get_async_iterator_for_of(self, iterable, loc)
        let nm = self.get_iterator_next_method(async_iter, loc)
        match gen_opt {
          Some(g) => {
            g.for_of_iterator = async_iter
            g.for_of_next = nm
          }
          None => ()
        }
        (async_iter, nm)
      }
      let mut first_resume = resuming_for_of && !awaiting_next
      let mut is_awaiting = awaiting_next
      let mut completion : Value = Undefined
      while true {
        if first_resume {
          first_resume = false
          let body_env_parent = match loop_binding_env {
            Some(e) => e
            None => env
          }
          let body_env = Environment::new(parent=Some(body_env_parent))
          let actual_body_env = match gen_opt {
            Some(g) =>
              if g.loop_env_stack.length() > 0 {
                let saved = g.loop_env_stack.pop().unwrap()
                match g.loop_yield_base_stack.pop() {
                  Some(base) => g.yield_index = base
                  None => ()
                }
                saved
              } else {
                body_env
              }
            None => body_env
          }
          let body_yield_base = match gen_opt {
            Some(g) => g.yield_index
            _ => 0
          }
          let body_signal = self.exec_stmt(ctx, body, actual_body_env) catch {
            e => {
              if e is YieldSignal(_) {
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, actual_body_env, body_yield_base,
                )
              }
              raise e
            }
          }
          match body_signal {
            Normal(v) => completion = v
            ReturnSignal(v) => return ReturnSignal(v)
            BreakSignal(v, None) => {
              completion = v.unwrap_or(completion)
              close_iterator_throw(self, iterator, loc)
              break
            }
            BreakSignal(bv, Some(l)) =>
              if label == Some(l) {
                completion = bv.unwrap_or(completion)
                close_iterator_throw(self, iterator, loc)
                break
              } else {
                return BreakSignal(bv, Some(l))
              }
            ContinueSignal(v, None) => {
              completion = v.unwrap_or(completion)
              continue
            }
            ContinueSignal(cv, Some(l)) =>
              if label == Some(l) {
                completion = cv.unwrap_or(completion)
                continue
              } else {
                return ContinueSignal(cv, Some(l))
              }
          }
          continue
        }
        // Await next iteration value
        let yielded_value = if is_awaiting {
          is_awaiting = false
          match gen_opt {
            Some(g) => {
              // Check if the resume was a throw (promise was rejected).
              // When a throw resumes the generator, g.yield_value is Undefined
              // (set by generator_resume's Throw handler), not a proper IteratorResult.
              // TODO(closeOnRejection): resume_action coupling - this reads
              // gen.resume_action directly, which is normally consumed by
              // eval_yield. If for-await-of is refactored to use eval_yield's
              // resume mechanism, this check would break because resume_action
              // would be consumed before reaching here.
              match g.resume_action {
                ThrowAction(e) => {
                  g.resume_action = NextAction
                  raise JsException(e)
                }
                _ => ()
              }
              g.yield_value
            }
            None => fail("for-await-of: no generator")
          }
        } else {
          let next_promise = self.call_value(next_method, iterator, [], loc)
          match gen_opt {
            Some(g) => {
              g.for_of_awaiting_next = true
              g.yield_value = next_promise
              let save_base = g.yield_index
              g.yield_index += 1
              g.resume_at_yield = save_base
              let body_env = Environment::new(parent=Some(env))
              save_for_of_resume_state(
                gen_opt, iterator, next_method, body_env, save_base,
              )
            }
            None => ()
          }
          raise YieldSignal(next_promise)
        }
        // Check done
        let done_val = self.get_property(yielded_value, "done", loc)
        if is_truthy(done_val) {
          completion = self.get_property(yielded_value, "value", loc)
          break
        }
        let value = self.get_property(yielded_value, "value", loc)
        // Bind value to loop variable
        let body_env_parent = match loop_binding_env {
          Some(e) => e
          None => env
        }
        let body_env = Environment::new(parent=Some(body_env_parent))
        match var_kind {
          Some(VarKind) =>
            if env.bindings.contains(name) {
              env.assign(name, value)
            } else {
              env.def(name, value, VarBinding)
            }
          Some(kind) => {
            let bk : BindingKind = match kind {
              LetKind => LetBinding
              ConstKind => ConstBinding
              _ => fail("unreachable")
            }
            body_env.def(name, value, bk)
          }
          None =>
            env.assign_with_strict(name, value, ctx.strict) catch {
              e => {
                close_iterator_throw(self, iterator, loc)
                raise e
              }
            }
        }
        // Execute loop body
        let (new_c, should_continue, propagate) = exec_for_body_signal(
          self, ctx, body, body_env, gen_opt, iterator, next_method, loc, label,
          completion,
        )
        completion = new_c
        if should_continue {
          continue
        }
        match propagate {
          Some(s) => return s
          None => ()
        }
      }
      Normal(completion)
    }
    AsyncForOfStmtPat(var_kind, pattern, iterable_expr, body, loc) => {
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        _ => false
      }
      let awaiting_next = match gen_opt {
        Some(g) => g.for_of_awaiting_next
        _ => false
      }
      let (iterator, next_method) : (Value, Value) = if awaiting_next {
        match gen_opt {
          Some(g) => {
            g.for_of_awaiting_next = false
            g.resuming = false
            let _ = g.loop_env_stack.pop()
            let _ = g.loop_yield_base_stack.pop()
          }
          _ => ()
        }
        let (it, nm) = restore_for_of_resume_state(gen_opt)
        (it, nm)
      } else if resuming_for_of {
        restore_for_of_resume_state(gen_opt)
      } else {
        let iterable_eval_env = match var_kind {
          Some(LetKind | ConstKind) => {
            let tdz_env = Environment::new(parent=Some(env))
            let bk : BindingKind = match var_kind {
              Some(LetKind) => LetBinding
              _ => ConstBinding
            }
            let tdz_names : Array[String] = []
            collect_pattern_tdz_names(pattern, tdz_names)
            for n in tdz_names {
              tdz_env.def_tdz(n, bk)
            }
            tdz_env
          }
          _ => env
        }
        let iterable = self.eval_expr(ctx, iterable_expr, iterable_eval_env)
        let async_iter = get_async_iterator_for_of(self, iterable, loc)
        let nm = self.get_iterator_next_method(async_iter, loc)
        match gen_opt {
          Some(g) => {
            g.for_of_iterator = async_iter
            g.for_of_next = nm
          }
          None => ()
        }
        (async_iter, nm)
      }
      let mut first_resume = resuming_for_of && !awaiting_next
      let mut is_awaiting = awaiting_next
      let mut completion : Value = Undefined
      while true {
        let (value, body_env, should_bind) : (Value, Environment, Bool) = if first_resume {
          first_resume = false
          let saved_env = restore_for_of_loop_env(
            gen_opt,
            Environment::new(parent=Some(env)),
          )
          match take_for_of_resume_value(saved_env) {
            Some(v) => (v, saved_env, true)
            None => (Undefined, saved_env, false)
          }
        } else {
          let yielded_value = if is_awaiting {
            is_awaiting = false
            match gen_opt {
              Some(g) => g.yield_value
              None => fail("for-await-of: no generator")
            }
          } else {
            let next_promise = self.call_value(next_method, iterator, [], loc)
            match gen_opt {
              Some(g) => {
                g.for_of_awaiting_next = true
                g.yield_value = next_promise
                let save_base = g.yield_index
                g.yield_index += 1
                g.resume_at_yield = save_base
                let body_env = Environment::new(parent=Some(env))
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, body_env, save_base,
                )
              }
              None => ()
            }
            raise YieldSignal(next_promise)
          }
          let done_val = self.get_property(yielded_value, "done", loc)
          if is_truthy(done_val) {
            completion = self.get_property(yielded_value, "value", loc)
            break
          }
          let v = self.get_property(yielded_value, "value", loc)
          (v, Environment::new(parent=Some(env)), true)
        }
        if should_bind {
          let binding_yield_base = match gen_opt {
            Some(g) => g.yield_index
            _ => 0
          }
          match var_kind {
            Some(kind) => {
              let bk : BindingKind = match kind {
                LetKind => LetBinding
                ConstKind => ConstBinding
                VarKind => VarBinding
              }
              let bind_env = match kind {
                VarKind => env
                _ => body_env
              }
              self.bind_pattern(pattern, value, bind_env, bk, ctx~) catch {
                e => {
                  if e is YieldSignal(_) {
                    save_for_of_resume_value(body_env, value)
                    save_for_of_resume_state(
                      gen_opt, iterator, next_method, body_env, binding_yield_base,
                    )
                  } else {
                    close_iterator_throw(self, iterator, loc)
                  }
                  raise e
                }
              }
            }
            None =>
              self.assign_pattern(ctx, pattern, value, env) catch {
                e => {
                  if e is YieldSignal(_) {
                    save_for_of_resume_value(body_env, value)
                    save_for_of_resume_state(
                      gen_opt, iterator, next_method, body_env, binding_yield_base,
                    )
                  } else {
                    close_iterator_throw(self, iterator, loc)
                  }
                  raise e
                }
              }
          }
        }
        let (new_c, should_continue, propagate) = exec_for_body_signal(
          self, ctx, body, body_env, gen_opt, iterator, next_method, loc, label,
          completion,
        )
        completion = new_c
        if should_continue {
          continue
        }
        match propagate {
          Some(s) => return s
          None => ()
        }
      }
      Normal(completion)
    }
    AsyncForOfExpr(lhs_expr, iterable_expr, body, loc) => {
      let gen_opt = ctx.current_generator
      let resuming_for_of = match gen_opt {
        Some(g) => g.for_of_resume
        _ => false
      }
      let awaiting_next = match gen_opt {
        Some(g) => g.for_of_awaiting_next
        _ => false
      }
      let (iterator, next_method) : (Value, Value) = if awaiting_next {
        match gen_opt {
          Some(g) => {
            g.for_of_awaiting_next = false
            g.resuming = false
            let _ = g.loop_env_stack.pop()
            let _ = g.loop_yield_base_stack.pop()
          }
          _ => ()
        }
        let (it, nm) = restore_for_of_resume_state(gen_opt)
        (it, nm)
      } else if resuming_for_of {
        restore_for_of_resume_state(gen_opt)
      } else {
        let iterable = self.eval_expr(ctx, iterable_expr, env)
        let async_iter = get_async_iterator_for_of(self, iterable, loc)
        let nm = self.get_iterator_next_method(async_iter, loc)
        match gen_opt {
          Some(g) => {
            g.for_of_iterator = async_iter
            g.for_of_next = nm
          }
          None => ()
        }
        (async_iter, nm)
      }
      let mut first_resume = resuming_for_of && !awaiting_next
      let mut is_awaiting = awaiting_next
      let mut completion : Value = Undefined
      while true {
        let (value, body_env, should_assign) : (Value, Environment, Bool) = if first_resume {
          first_resume = false
          let saved_env = restore_for_of_loop_env(
            gen_opt,
            Environment::new(parent=Some(env)),
          )
          match take_for_of_resume_value(saved_env) {
            Some(v) => (v, saved_env, true)
            None => (Undefined, saved_env, false)
          }
        } else {
          let yielded_value = if is_awaiting {
            is_awaiting = false
            match gen_opt {
              Some(g) => g.yield_value
              None => fail("for-await-of: no generator")
            }
          } else {
            let next_promise = self.call_value(next_method, iterator, [], loc)
            match gen_opt {
              Some(g) => {
                g.for_of_awaiting_next = true
                g.yield_value = next_promise
                let save_base = g.yield_index
                g.yield_index += 1
                g.resume_at_yield = save_base
                let body_env = Environment::new(parent=Some(env))
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, body_env, save_base,
                )
              }
              None => ()
            }
            raise YieldSignal(next_promise)
          }
          let done_val = self.get_property(yielded_value, "done", loc)
          if is_truthy(done_val) {
            completion = self.get_property(yielded_value, "value", loc)
            break
          }
          let v = self.get_property(yielded_value, "value", loc)
          (v, Environment::new(parent=Some(env)), true)
        }
        if should_assign {
          let assignment_yield_base = match gen_opt {
            Some(g) => g.yield_index
            _ => 0
          }
          self.assign_to_expr(ctx, lhs_expr, value, env) catch {
            e => {
              if e is YieldSignal(_) {
                save_for_of_resume_value(body_env, value)
                save_for_of_resume_state(
                  gen_opt, iterator, next_method, body_env, assignment_yield_base,
                )
              } else {
                close_iterator_throw(self, iterator, loc)
              }
              raise e
            }
          }
        }
        let (new_c, should_continue, propagate) = exec_for_body_signal(
          self, ctx, body, body_env, gen_opt, iterator, next_method, loc, label,
          completion,
        )
        completion = new_c
        if should_continue {
          continue
        }
        match propagate {
          Some(s) => return s
          None => ()
        }
      }
      Normal(completion)
    }
  }
}

///|
pub fn collect_for_in_keys(
  obj : Value,
  interp : Interpreter,
) -> Array[String] raise Error {
  let keys : Array[String] = []
  let seen = @set.Set::default()
  let mut current = obj
  while true {
    match current {
      Proxy(proxy_data) => {
        // Invoke [[OwnPropertyKeys]] trap, then filter for enumerable string keys
        let own_keys_val = proxy_own_property_keys(interp, proxy_data)
        let own_keys = match own_keys_val {
          Array(data) => data.elements
          _ => []
        }
        for k in own_keys {
          match k {
            String_(s) =>
              if !seen.contains(s) {
                // Use [[GetOwnPropertyDescriptor]] trap to check enumerability.
                // If the trap returns None the key is not an own property —
                // don't mark it seen so the prototype chain can still surface it.
                let desc_opt = proxy_get_own_property(
                  interp,
                  proxy_data,
                  String_(s),
                )
                match desc_opt {
                  Some((d, _)) => {
                    seen.add(s)
                    if d.enumerable {
                      keys.push(s)
                    }
                  }
                  None => ()
                }
              }
            // Symbol keys are never enumerated in for-in
            _ => ()
          }
        }
        // Walk prototype chain via [[GetPrototypeOf]] trap
        current = proxy_get_prototype_of(interp, proxy_data)
        continue
      }
      Object(data) => {
        // TypedArray: enumerate numeric indices as keys, but only while
        // IntegerIndexedElementGet would report them present. A detached
        // buffer makes every index invalid for [[GetOwnProperty]], so for-in
        // must skip them instead of yielding stale 0..length-1 keys.
        if is_typedarray_class(data.class_name) {
          let ta_len = match data.bag.internal_slots.get(ArrayLength) {
            Some(Number(n)) => n.to_int()
            _ => 0
          }
          for i = 0; i < ta_len; i = i + 1 {
            let k = i.to_string()
            if !seen.contains(k) &&
              (interp.stdlib_hooks.typedarray_is_valid_index)(
                data,
                i,
                interp.realm_state,
              ) {
              seen.add(k)
              keys.push(k)
            }
          }
        }
        let sorted = if data.class_name == "Module" {
          module_namespace_own_string_keys(data)
        } else {
          sort_property_keys(data.bag.properties)
        }
        for k in sorted {
          if !seen.contains(k) {
            seen.add(k)
            let enumerable = if data.class_name == "Module" {
              match interp.get_own_property(Object(data), String_(k)) {
                Some((d, _)) => d.enumerable
                None => false
              }
            } else {
              match data.bag.descriptors.get(k) {
                Some(d) => d.enumerable
                None => true
              }
            }
            if enumerable {
              keys.push(k)
            }
          }
        }
        current = data.prototype
      }
      Array(data) => {
        for i = 0; i < data.elements.length(); i = i + 1 {
          if data.holes.contains(i) {
            continue
          }
          let k = i.to_string()
          if !seen.contains(k) {
            let enumerable = match data.bag.descriptors.get(k) {
              Some(d) => d.enumerable
              None => true
            }
            seen.add(k)
            if enumerable {
              keys.push(k)
            }
          }
        }
        let sorted = sort_property_keys(data.bag.properties)
        for k in sorted {
          if !seen.contains(k) {
            let enumerable = match data.bag.descriptors.get(k) {
              Some(d) => d.enumerable
              None => true
            }
            seen.add(k)
            if enumerable {
              keys.push(k)
            }
          }
        }
        break
      }
      _ => break
    }
  }
  keys
}

///|
fn is_declaration_stmt(stmt : @ast.Stmt) -> Bool {
  match stmt {
    VarDecl(_, _, _, _)
    | DestructureDecl(_, _, _, _)
    | FuncDecl(_, _, _, _, _)
    | FuncDeclExt(_, _, _, _, _, _)
    | GeneratorDecl(_, _, _, _, _)
    | GeneratorDeclExt(_, _, _, _, _, _)
    | AsyncFuncDecl(_, _, _, _, _)
    | AsyncFuncDeclExt(_, _, _, _, _, _)
    | AsyncGeneratorDecl(_, _, _, _, _)
    | AsyncGeneratorDeclExt(_, _, _, _, _, _)
    | ClassDecl(_, _, _, _, _)
    | ImportDecl(_, _, _, _, _, _)
    | ExportNamedDecl(_, _, _, _, _)
    | ExportDefaultDecl(_, _)
    | ExportAllDecl(_, _, _, _) => true
    _ => false
  }
}

///|
fn Interpreter::exec_stmts(
  self : Interpreter,
  ctx : ExecContext,
  stmts : Array[@ast.Stmt],
  env : Environment,
) -> Signal raise Error {
  let (start, exec_env, initial_last) = restore_stmt_resume_frame(
    ctx.current_generator,
    env,
  )
  let mut last = initial_last
  for i = start; i < stmts.length(); i = i + 1 {
    let stmt = stmts[i]
    let signal = self.exec_stmt(ctx, stmt, exec_env) catch {
      e => {
        if e is YieldSignal(_) {
          save_stmt_resume_frame(ctx.current_generator, i, exec_env, last)
        }
        raise e
      }
    }
    match signal {
      Normal(v) =>
        // Per spec, declarations have empty completion values that don't update
        // the statement list's value. Only non-declaration statements update it.
        if !is_declaration_stmt(stmt) {
          last = v
        }
      BreakSignal(v, label) => {
        // UpdateEmpty: if break's value is None (empty), use accumulated value
        let cv = match v {
          None => Some(last)
          some => some
        }
        return BreakSignal(cv, label)
      }
      ContinueSignal(v, label) => {
        let cv = match v {
          None => Some(last)
          some => some
        }
        return ContinueSignal(cv, label)
      }
      other => return other
    }
  }
  Normal(last)
}

///|
/// §7.4.11 IteratorClose for a non-throw completion.
///
/// Resolves "return" via the [[Get]] dispatcher (`Interpreter::get_property`)
/// rather than walking `bag.properties` directly, so:
/// - Proxy `get` traps fire on Proxy-wrapped iterators
/// - Accessor `return` properties invoke their getter
/// - Map/Set iterator prototypes (and any non-ObjectData iterator) resolve
///   "return" through the same dispatcher path used by ordinary property access
///
/// Per §7.3.10 GetMethod: undefined/null → skip; non-callable → TypeError.
/// Per §7.4.11 steps 6–7: a non-Object result from `return()` → TypeError.
/// Use `close_iterator_throw` when the surrounding completion is a throw.
fn close_iterator(
  interp : Interpreter,
  iterator : Value,
  loc : @token.Loc,
) -> Unit raise Error {
  interp.iterator_close(iterator, loc)
}

///|
/// §7.4.11 IteratorClose for a ThrowCompletion — close errors are discarded.
fn close_iterator_throw(
  interp : Interpreter,
  iterator : Value,
  loc : @token.Loc,
) -> Unit {
  interp.iterator_close_throw(iterator, loc)
}

///|
fn Interpreter::exec_try_catch(
  self : Interpreter,
  ctx : ExecContext,
  try_body : Array[@ast.Stmt],
  catch_param : @ast.Pattern?,
  catch_body : Array[@ast.Stmt]?,
  finally_body : Array[@ast.Stmt]?,
  env : Environment,
) -> Signal raise Error {
  // Check if we're resuming inside a generator at a specific phase
  let gen_opt = ctx.current_generator
  let start_phase = match gen_opt {
    Some(g) =>
      if g.try_resume_phase >= 0 {
        let p = g.try_resume_phase
        g.try_resume_phase = -1 // consume it
        g.yield_index = g.try_resume_yield_base
        p
      } else {
        0
      }
    _ => 0
  }
  let mut result : Signal = Normal(Undefined)
  let mut pending_error : Error? = match gen_opt {
    Some(g) =>
      if start_phase == 2 {
        // Resuming in finally — restore pending error and result
        result = g.try_resume_result
        g.try_resume_result = Normal(Undefined)
        let pe = g.try_resume_pending_error
        g.try_resume_pending_error = None
        pe
      } else {
        None
      }
    _ => None
  }
  // Phase 0: Try body (skip if resuming at catch or finally)
  if start_phase == 0 {
    let try_env = Environment::new(parent=Some(env))
    let try_yield_base = match gen_opt {
      Some(g) => g.yield_index
      _ => 0
    }
    result = self.exec_stmts(ctx, try_body, try_env) catch {
      e =>
        if e is YieldSignal(_) {
          // Yield inside try body — save phase for resume
          match gen_opt {
            Some(g) => {
              g.try_resume_phase = 0
              g.try_resume_yield_base = try_yield_base
            }
            _ => ()
          }
          raise e
        } else if e is GeneratorReturnSignal(_) {
          // .return(v) injected at yield — save for finally
          pending_error = Some(e)
          Normal(Undefined)
        } else if is_js_catchable_error(e) {
          // JS exception caught — run catch body (phase 1)
          match (catch_param, catch_body) {
            (Some(param), Some(body)) => {
              let catch_env = Environment::new(parent=Some(env))
              let error_value = js_error_to_value_with_env(e, Some(env))
              // Bind the catch parameter - supports simple identifier and destructuring
              self.bind_pattern(param, error_value, catch_env, LetBinding, ctx~)
              let catch_yield_base = match gen_opt {
                Some(g) => g.yield_index
                _ => 0
              }
              self.exec_stmts(ctx, body, catch_env) catch {
                ce =>
                  if ce is YieldSignal(_) {
                    // Yield inside catch body — save phase and error for resume
                    match gen_opt {
                      Some(g) => {
                        g.try_resume_phase = 1
                        g.try_resume_yield_base = catch_yield_base
                        g.try_resume_error = error_value
                      }
                      _ => ()
                    }
                    raise ce
                  } else {
                    raise ce
                  }
              }
            }
            _ => Normal(Undefined)
          }
        } else {
          raise e
        }
    }
  } else if start_phase == 1 {
    // Resuming in catch body — skip try, go directly to catch
    match (catch_param, catch_body) {
      (Some(param), Some(body)) => {
        let catch_env = Environment::new(parent=Some(env))
        // Restore saved error value
        let error_value = match gen_opt {
          Some(g) => {
            let ev = g.try_resume_error
            g.try_resume_error = Undefined
            ev
          }
          _ => Undefined
        }
        // Bind the catch parameter - supports simple identifier and destructuring
        self.bind_pattern(param, error_value, catch_env, LetBinding, ctx~)
        let catch_yield_base = match gen_opt {
          Some(g) => g.yield_index
          _ => 0
        }
        result = self.exec_stmts(ctx, body, catch_env) catch {
          ce =>
            if ce is YieldSignal(_) {
              // Yield inside catch body again — save phase
              match gen_opt {
                Some(g) => {
                  g.try_resume_phase = 1
                  g.try_resume_yield_base = catch_yield_base
                  g.try_resume_error = error_value
                }
                _ => ()
              }
              raise ce
            } else {
              raise ce
            }
        }
      }
      _ => result = Normal(Undefined)
    }
  }
  // else start_phase == 2: skip try and catch, go to finally
  // Phase 2: Finally body (always runs if present)
  match finally_body {
    Some(body) => {
      let finally_yield_base = match gen_opt {
        Some(g) => g.yield_index
        _ => 0
      }
      let finally_result = self.exec_stmts(
        ctx,
        body,
        Environment::new(parent=Some(env)),
      ) catch {
        fe =>
          if fe is YieldSignal(_) {
            // Yield inside finally body — save phase and pending state for resume
            match gen_opt {
              Some(g) => {
                g.try_resume_phase = 2
                g.try_resume_yield_base = finally_yield_base
                g.try_resume_result = result
                g.try_resume_pending_error = pending_error
              }
              _ => ()
            }
            raise fe
          } else {
            raise fe
          }
      }
      match pending_error {
        Some(e) =>
          match finally_result {
            Normal(_) => raise e
            ReturnSignal(_) => finally_result
            _ => raise e
          }
        None =>
          match finally_result {
            Normal(_) => result
            other => other
          }
      }
    }
    None =>
      match pending_error {
        Some(e) => raise e
        None => result
      }
  }
}

///|
/// Execute a for-of/for-await-of loop body and dispatch its completion signal.
/// Shared by both sync and async for-of variants to avoid code duplication.
/// Returns (updated_completion, should_continue, optional_signal_to_propagate).
fn exec_for_body_signal(
  interp : Interpreter,
  ctx : ExecContext,
  body : @ast.Stmt,
  body_env : Environment,
  gen_opt : GeneratorObject?,
  iterator : Value,
  next_method : Value,
  loc : @token.Loc,
  label : String?,
  completion : Value,
) -> (Value, Bool, Signal?) raise Error {
  let body_yield_base = match gen_opt {
    Some(g) => g.yield_index
    _ => 0
  }
  let body_signal = interp.exec_stmt(ctx, body, body_env) catch {
    e => {
      if e is YieldSignal(_) {
        save_for_of_resume_state(
          gen_opt, iterator, next_method, body_env, body_yield_base,
        )
      } else {
        close_iterator_throw(interp, iterator, loc)
      }
      raise e
    }
  }
  match body_signal {
    Normal(v) => (v, false, None)
    ReturnSignal(v) => {
      close_iterator(interp, iterator, loc)
      (completion, false, Some(ReturnSignal(v)))
    }
    BreakSignal(v, None) => {
      close_iterator(interp, iterator, loc)
      (v.unwrap_or(completion), false, Some(BreakSignal(None, None)))
    }
    BreakSignal(bv, Some(l)) =>
      if label == Some(l) {
        close_iterator(interp, iterator, loc)
        (bv.unwrap_or(completion), false, Some(BreakSignal(None, None)))
      } else {
        close_iterator(interp, iterator, loc)
        (completion, false, Some(BreakSignal(bv, Some(l))))
      }
    ContinueSignal(v, None) => {
      let c = v.unwrap_or(completion)
      (c, true, None)
    }
    ContinueSignal(cv, Some(l)) =>
      if label == Some(l) {
        let c = cv.unwrap_or(completion)
        (c, true, None)
      } else {
        close_iterator(interp, iterator, loc)
        (completion, false, Some(ContinueSignal(cv, Some(l))))
      }
  }
}