//! `for...of` and `for...in` statement lowering.
//!
//! Extracted from `lower/stmt.rs` so that file stays under the 2,000-LOC
//! soft cap. Both arms produce significant generated HIR — the for-of arm
//! covers the generator iterator-protocol path, the
//! `*[Symbol.iterator]()`-based class path, the `for await (... of ...)`
//! async path, and the regular indexed-array path. The for-in arm
//! desugars to a for-of over `Object.keys(...)`.
//!
//! The match arms inside `lower_stmt` collapse to one-line delegations
//! to `lower_stmt_for_of` / `lower_stmt_for_in`.

use anyhow::{anyhow, Result};
use perry_types::{LocalId, Type};
use swc_ecma_ast as ast;

use super::*;
use crate::ir::*;

pub(crate) fn lower_stmt_for_of(
    ctx: &mut LoweringContext,
    module: &mut Module,
    for_of_stmt: &ast::ForOfStmt,
) -> Result<()> {
    // --- Iterator protocol path for generators ---
    // Detect: for (const x of genFunc(...)) where genFunc is function*
    let is_generator_call = if let ast::Expr::Call(call) = &*for_of_stmt.right {
        if let ast::Callee::Expr(callee_expr) = &call.callee {
            if let ast::Expr::Ident(ident) = &**callee_expr {
                ctx.generator_func_names.contains(ident.sym.as_ref())
            } else {
                false
            }
        } else {
            false
        }
    } else {
        false
    };

    // Detect whether the called generator was an `async function*`.
    // Async generators always return `Promise<{value, done}>` from
    // `.next()`, so the iterator-protocol loop must `await` each
    // call before reading `.value` / `.done`. Either the user
    // wrote `for await (...)` (SWC `is_await`) or the callee was
    // declared async — both must trigger awaiting.
    let callee_is_async_gen = if let ast::Expr::Call(call) = &*for_of_stmt.right {
        if let ast::Callee::Expr(callee_expr) = &call.callee {
            if let ast::Expr::Ident(ident) = &**callee_expr {
                ctx.async_generator_func_names.contains(ident.sym.as_ref())
            } else {
                false
            }
        } else {
            false
        }
    } else {
        false
    };
    let needs_await = for_of_stmt.is_await || callee_is_async_gen;

    // Also detect: for (const x of new Range(...)) where Range
    // defines `*[Symbol.iterator]()`. We lowered that method as
    // a synthesized top-level generator function taking `this`
    // as its first parameter; the for-of here dispatches by
    // calling that function with the lowered receiver.
    let iter_from_class: Option<perry_types::FuncId> =
        if let ast::Expr::New(new_expr) = &*for_of_stmt.right {
            if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() {
                let class_name = ident.sym.to_string();
                ctx.iterator_func_for_class.get(&class_name).copied()
            } else {
                None
            }
        } else {
            None
        };

    if is_generator_call || iter_from_class.is_some() {
        // Lower to iterator protocol:
        //   let __iter = genFunc(...);                     // generator-fn path
        //   let __iter = __perry_iter_Range(new Range(...));  // class path
        //   let __result = __iter.next();
        //   while (!__result.done) { const x = __result.value; body; __result = __iter.next(); }
        let for_scope_mark = ctx.push_block_scope();
        let iter_expr = lower_expr(ctx, &for_of_stmt.right)?;
        // For the class path we wrap the lowered `new Range(..)`
        // in a direct FuncRef call to the synthesized iterator
        // function (which has `this` as its first parameter).
        let iter_expr = if let Some(iter_fn_id) = iter_from_class {
            Expr::Call {
                callee: Box::new(Expr::FuncRef(iter_fn_id)),
                args: vec![iter_expr],
                type_args: vec![],
            }
        } else {
            iter_expr
        };
        let iter_id = ctx.fresh_local();
        ctx.locals
            .push((format!("__iter_{}", iter_id), iter_id, Type::Any));
        module.init.push(Stmt::Let {
            id: iter_id,
            name: format!("__iter_{}", iter_id),
            ty: Type::Any,
            mutable: false,
            init: Some(iter_expr),
        });

        let result_id = ctx.fresh_local();
        ctx.locals
            .push((format!("__result_{}", result_id), result_id, Type::Any));
        // __result = __iter.next()
        // For async generators / `for await ... of`, wrap the
        // call in `Expr::Await` so the resolved iter-result
        // (`{value, done}`) is what's stored, not the Promise.
        let raw_next_call = Expr::Call {
            callee: Box::new(Expr::PropertyGet {
                object: Box::new(Expr::LocalGet(iter_id)),
                property: "next".to_string(),
            }),
            args: vec![],
            type_args: vec![],
        };
        let next_call = if needs_await {
            Expr::Await(Box::new(raw_next_call))
        } else {
            raw_next_call
        };
        module.init.push(Stmt::Let {
            id: result_id,
            name: format!("__result_{}", result_id),
            ty: Type::Any,
            mutable: true,
            init: Some(next_call.clone()),
        });

        // Extract the loop variable binding
        let item_name = if let ast::ForHead::VarDecl(var_decl) = &for_of_stmt.left {
            if let Some(decl) = var_decl.decls.first() {
                if let ast::Pat::Ident(ident) = &decl.name {
                    ident.id.sym.to_string()
                } else {
                    "__gen_item".to_string()
                }
            } else {
                "__gen_item".to_string()
            }
        } else {
            "__gen_item".to_string()
        };
        let item_id = ctx.define_local(item_name.clone(), Type::Any);

        // Lower loop body
        let mut body_stmts = Vec::new();
        // const x = __result.value
        body_stmts.push(Stmt::Let {
            id: item_id,
            name: item_name,
            ty: Type::Any,
            mutable: false,
            init: Some(Expr::PropertyGet {
                object: Box::new(Expr::LocalGet(result_id)),
                property: "value".to_string(),
            }),
        });
        // Lower user body statements. lower_stmt appends to module.init,
        // so we snapshot and drain to capture the body stmts.
        // Handle both Block bodies (`for (...) { ... }`) AND single-statement
        // bodies (`for (...) console.log(v);`). Pre-fix the brace-less
        // form was silently dropped — `for (const v of gen()) doThing(v);`
        // produced no output at all.
        let init_before = module.init.len();
        if let ast::Stmt::Block(block) = &*for_of_stmt.body {
            for s in &block.stmts {
                lower_stmt(ctx, module, s)?;
            }
        } else {
            lower_stmt(ctx, module, &for_of_stmt.body)?;
        }
        let mut user_body: Vec<Stmt> = module.init.drain(init_before..).collect();
        body_stmts.append(&mut user_body);
        // __result = __iter.next()
        body_stmts.push(Stmt::Expr(Expr::LocalSet(result_id, Box::new(next_call))));

        // while (!__result.done) { body }
        module.init.push(Stmt::While {
            condition: Expr::Unary {
                op: UnaryOp::Not,
                operand: Box::new(Expr::PropertyGet {
                    object: Box::new(Expr::LocalGet(result_id)),
                    property: "done".to_string(),
                }),
            },
            body: body_stmts,
        });

        ctx.pop_block_scope(for_scope_mark);
        return Ok(());
    }

    // --- Standard array-based for-of path ---
    // Desugar for-of to a regular for loop:
    // for (const x of arr) { body }
    // becomes:
    // { let __arr = arr; for (let __i = 0; __i < __arr.length; __i++) { const x = __arr[__i]; body } }
    // Push a block scope so loop variables and internal temporaries don't leak.
    let for_scope_mark = ctx.push_block_scope();

    // Detect string iteration BEFORE lowering (so we can use the AST-level type info).
    // for (const ch of "hello") — each iteration yields a 1-char string via str[i].
    let is_string_iter = is_ast_string_expr(ctx, &for_of_stmt.right);

    // `for (const [k, v] of h)` where h is a Headers handle: WHATWG
    // Fetch spec says iteration of a Headers object yields `[key,
    // value]` pairs sorted by key. Without this rewrite, for-of falls
    // through to the generic array path and reads `.length` on the
    // raw handle (returns 0 → silent empty loop). Refs #576.
    let is_headers_iter = match &*for_of_stmt.right {
        ast::Expr::Ident(ident) => matches!(
            ctx.lookup_native_instance(ident.sym.as_ref()),
            Some((_, "Headers"))
        ),
        _ => false,
    };

    // `for (const [k, v] of params)` where `params` is a
    // URLSearchParams local. Same shape as the Headers case but
    // tracked via `lookup_local_type` (Type::Named) instead of the
    // native-instance registry. Refs #575.
    let is_urlsp_iter = match &*for_of_stmt.right {
        ast::Expr::Ident(ident) => matches!(
            ctx.lookup_local_type(ident.sym.as_ref()),
            Some(Type::Named(n)) if n == "URLSearchParams"
        ),
        ast::Expr::New(new_expr) => matches!(
            new_expr.callee.as_ref(),
            ast::Expr::Ident(c) if c.sym.as_ref() == "URLSearchParams"
        ),
        _ => false,
    };

    // Lower the iterable expression (the array)
    let arr_expr = lower_expr(ctx, &for_of_stmt.right)?;
    let arr_expr = if is_headers_iter {
        Expr::NativeMethodCall {
            module: "Headers".to_string(),
            class_name: Some("Headers".to_string()),
            object: Some(Box::new(arr_expr)),
            method: "entries".to_string(),
            args: vec![],
        }
    } else if is_urlsp_iter {
        Expr::UrlSearchParamsEntries(Box::new(arr_expr))
    } else {
        arr_expr
    };

    // Issue #302: resolve iterable type from either local var or
    // class instance field (`this.someMap`). Was limited to
    // `Ident` only. Issue #311 extends to plain object property
    // access (`obj.m` where `obj` is a local with an inferred
    // `Type::Object` shape) — without this arm `for (const x of
    // obj.m)` fell through to `None`, the loop read `.length` on
    // a raw Map handle (returns 0), and silently iterated zero
    // times.
    let iterable_type: Option<Type> = match &*for_of_stmt.right {
        ast::Expr::Ident(ident) => ctx.lookup_local_type(ident.sym.as_ref()).cloned(),
        ast::Expr::Member(m) => {
            if matches!(m.obj.as_ref(), ast::Expr::This(_)) {
                if let (Some(cls), ast::MemberProp::Ident(p)) = (ctx.current_class.clone(), &m.prop)
                {
                    ctx.lookup_class_field_type(&cls, p.sym.as_ref()).cloned()
                } else {
                    None
                }
            } else if let ast::MemberProp::Ident(p) = &m.prop {
                let obj_ty = crate::lower_types::infer_type_from_expr(&m.obj, ctx);
                match obj_ty {
                    Type::Object(ot) => ot.properties.get(p.sym.as_ref()).map(|pi| pi.ty.clone()),
                    // Class instance: receiver is `new Example()` or
                    // a local typed `Example`. Consult the same
                    // class_field_types registry the `this.<field>`
                    // arm uses (populated for #302).
                    Type::Named(cls) => ctx.lookup_class_field_type(&cls, p.sym.as_ref()).cloned(),
                    _ => None,
                }
            } else {
                None
            }
        }
        _ => None,
    };

    // If the iterable is a Map, wrap in MapEntries to convert to array
    // This handles: for (const [k, v] of myMap) { ... } AND
    // for (const [k, v] of this.classMap) { ... } per #302.
    let mut map_key_type: Option<Type> = None;
    let mut map_val_type: Option<Type> = None;
    // Issue #542/#543: also accept Type::Union containing Map (the
    // shape produced by `Map<K, V> | undefined` parameters/returns).
    let type_contains_map =
        |ty: &Type| -> bool { matches!(ty, Type::Generic { base, .. } if base == "Map") };
    let is_iterable_map = match &iterable_type {
        Some(Type::Generic { base, .. }) if base == "Map" => true,
        Some(Type::Union(variants)) => variants.iter().any(type_contains_map),
        _ => false,
    };
    // Fast path: `for (const [k, v] of mapExpr)` with an exact two-element
    // identifier destructure can iterate the Map's flat entries buffer
    // directly via `MapEntryKeyAt` / `MapEntryValueAt`, skipping the N+1
    // small Array allocations that `MapEntries` would do per iteration.
    // Detected here so we can keep the iterable expression unwrapped
    // and emit a different binding/bound shape below.
    // Map fast path also fires for the single-binding shapes
    //   for (const [k] of map)        — only key
    //   for (const [, v] of map)      — only value
    // Each non-empty slot must be a plain Ident (no nested patterns).
    // Anything else falls through to the MapEntries materialization
    // path so destructuring semantics for objects / nested arrays
    // / defaults stay correct.
    let map_kv_fastpath = is_iterable_map
        && match &for_of_stmt.left {
            ast::ForHead::VarDecl(var_decl) => match var_decl.decls.first() {
                Some(decl) => match &decl.name {
                    ast::Pat::Array(arr_pat) => {
                        let len = arr_pat.elems.len();
                        (len == 1 || len == 2)
                            && arr_pat
                                .elems
                                .iter()
                                .all(|e| e.is_none() || matches!(e, Some(ast::Pat::Ident(_))))
                    }
                    _ => false,
                },
                None => false,
            },
            _ => false,
        };
    // Fast path: `for (const x of setExpr)` with a single-Ident
    // binding. Reads elements directly via `SetValueAt` (→
    // `js_set_value_at`) instead of materializing the buffer with
    // `js_set_to_array`. ECS hot paths (changeset.removes, etc.)
    // iterate Sets repeatedly; this saves an Array alloc per loop.
    // Issue #542/#543: also accept Type::Union containing Set.
    let type_contains_set =
        |ty: &Type| -> bool { matches!(ty, Type::Generic { base, .. } if base == "Set") };
    let is_iterable_set = match &iterable_type {
        Some(Type::Generic { base, .. }) if base == "Set" => true,
        Some(Type::Union(variants)) => variants.iter().any(type_contains_set),
        _ => false,
    };
    let set_fastpath = is_iterable_set
        && match &for_of_stmt.left {
            ast::ForHead::VarDecl(var_decl) => match var_decl.decls.first() {
                Some(decl) => matches!(&decl.name, ast::Pat::Ident(_)),
                None => false,
            },
            _ => false,
        };
    // Issue #542/#543: dispatch on `is_iterable_map` / `is_iterable_set`
    // so the Union-with-Map / Union-with-Set shapes also wrap correctly
    // (matches the same fix applied to `lower_decl.rs`'s for-of arm).
    // Extract the Map's K/V type args from whichever variant carries
    // them (direct Generic or the Union's Map arm).
    let map_type_args: Option<Vec<Type>> = if is_iterable_map {
        match &iterable_type {
            Some(Type::Generic { base, type_args }) if base == "Map" => Some(type_args.clone()),
            Some(Type::Union(variants)) => variants.iter().find_map(|v| match v {
                Type::Generic { base, type_args } if base == "Map" => Some(type_args.clone()),
                _ => None,
            }),
            _ => None,
        }
    } else {
        None
    };
    // Issue #578: typed-array iterables. Wrap in `Expr::ArrayFrom`
    // so the holder is a regular Array of materialized element values.
    // Without this, the generated `for (let i=0; i<__arr.length; ++i)
    // __item = __arr[i]` loop reads f64s straight off the typed
    // array's byte-packed storage and yields raw bit reinterpretations.
    // `js_array_clone` (the runtime backing of `ArrayFrom`) detects the
    // typed-array tag and materializes through the per-kind accessor.
    let is_iterable_typed_array = matches!(
        &iterable_type,
        Some(Type::Named(name)) if matches!(name.as_str(),
            "Uint8Array" | "Int8Array" | "Uint8ClampedArray"
            | "Uint16Array" | "Int16Array"
            | "Uint32Array" | "Int32Array"
            | "Float32Array" | "Float64Array"
        )
    );
    let arr_expr = if is_iterable_map {
        if let Some(args) = map_type_args.as_ref() {
            if args.len() >= 2 {
                map_key_type = Some(args[0].clone());
                map_val_type = Some(args[1].clone());
            }
        }
        if map_kv_fastpath {
            arr_expr
        } else {
            Expr::MapEntries(Box::new(arr_expr))
        }
    } else if is_iterable_set {
        if set_fastpath {
            arr_expr
        } else {
            Expr::SetValues(Box::new(arr_expr))
        }
    } else if is_iterable_typed_array {
        Expr::ArrayFrom(Box::new(arr_expr))
    } else {
        arr_expr
    };

    // Determine the array element type: String for strings, Tuple(K, V) for Maps, Any otherwise.
    // For an identifier iterable like `for (const word of words)` where
    // `words: string[]`, extract the element type from the local's
    // declared Array<T> so the synthesized iteration variable gets
    // the right type (was always Any, breaking `word.length` etc.).
    // #302: also draws Set + class-field Array element types
    // from the resolved `iterable_type` above instead of
    // re-doing the Ident lookup here.
    let elem_type = if is_string_iter {
        Type::String
    } else if let (Some(ref k), Some(ref v)) = (&map_key_type, &map_val_type) {
        Type::Tuple(vec![k.clone(), v.clone()])
    } else if is_iterable_typed_array {
        // Issue #578: typed-array element values are always Number.
        Type::Number
    } else {
        match &iterable_type {
            Some(Type::Array(elem)) => (**elem).clone(),
            Some(Type::Generic { base, type_args }) if base == "Array" && type_args.len() == 1 => {
                type_args[0].clone()
            }
            Some(Type::Generic { base, type_args }) if base == "Set" && !type_args.is_empty() => {
                type_args[0].clone()
            }
            _ => Type::Any,
        }
    };
    // The __arr holder's type: String for string iteration, Map for
    // the Map-fast-path so `__m.size` resolves through `is_map_expr`,
    // Array otherwise.
    let arr_type = if is_string_iter {
        Type::String
    } else if map_kv_fastpath {
        Type::Generic {
            base: "Map".to_string(),
            type_args: vec![
                map_key_type.clone().unwrap_or(Type::Any),
                map_val_type.clone().unwrap_or(Type::Any),
            ],
        }
    } else if set_fastpath {
        Type::Generic {
            base: "Set".to_string(),
            type_args: vec![elem_type.clone()],
        }
    } else {
        Type::Array(Box::new(elem_type.clone()))
    };

    // Create internal variables for the array and index
    let arr_id = ctx.fresh_local();
    let idx_id = ctx.fresh_local();
    // Register these in the context so they can be looked up
    ctx.locals
        .push((format!("__arr_{}", arr_id), arr_id, arr_type.clone()));
    ctx.locals
        .push((format!("__idx_{}", idx_id), idx_id, Type::Number));

    // Store array reference: let __arr = arr
    module.init.push(Stmt::Let {
        id: arr_id,
        name: format!("__arr_{}", arr_id),
        ty: arr_type,
        mutable: false,
        init: Some(arr_expr),
    });

    // IMPORTANT: Define iteration variables BEFORE lowering the body
    // so the body can reference them
    let item_id = ctx.fresh_local();
    ctx.locals
        .push((format!("__item_{}", item_id), item_id, elem_type.clone()));

    // Pre-define all variables from the pattern so body can reference them
    let var_ids: Vec<(String, u32)> = match &for_of_stmt.left {
        ast::ForHead::VarDecl(var_decl) => {
            if let Some(decl) = var_decl.decls.first() {
                match &decl.name {
                    ast::Pat::Ident(ident) => {
                        let name = ident.id.sym.to_string();
                        let id = ctx.define_local(name.clone(), elem_type.clone());
                        vec![(name, id)]
                    }
                    ast::Pat::Array(arr_pat) => {
                        let mut ids = Vec::new();
                        for (idx, elem) in arr_pat.elems.iter().enumerate() {
                            if let Some(elem_pat) = elem {
                                if let ast::Pat::Ident(ident) = elem_pat {
                                    let name = ident.id.sym.to_string();
                                    // For Map destructuring [k, v], use key type for idx 0, value type for idx 1
                                    let var_type = if let Type::Tuple(ref types) = elem_type {
                                        types.get(idx).cloned().unwrap_or(Type::Any)
                                    } else {
                                        Type::Any
                                    };
                                    let id = ctx.define_local(name.clone(), var_type);
                                    ids.push((name, id));
                                }
                            }
                        }
                        ids
                    }
                    ast::Pat::Object(obj_pat) => {
                        let mut ids = Vec::new();
                        for prop in &obj_pat.props {
                            match prop {
                                ast::ObjectPatProp::Assign(assign) => {
                                    let name = assign.key.sym.to_string();
                                    let id = ctx.define_local(name.clone(), Type::Any);
                                    ids.push((name, id));
                                }
                                ast::ObjectPatProp::KeyValue(kv) => {
                                    if let ast::Pat::Ident(ident) = &*kv.value {
                                        let name = ident.id.sym.to_string();
                                        let id = ctx.define_local(name.clone(), Type::Any);
                                        ids.push((name, id));
                                    } else {
                                        // Nested pattern (e.g. `key: [a, b]`).
                                        // Recurse so leaves get pre-defined and
                                        // the body can reference them. Issue #554.
                                        collect_for_of_pattern_leaves(ctx, &kv.value, &mut ids);
                                    }
                                }
                                _ => {}
                            }
                        }
                        ids
                    }
                    _ => {
                        let name = get_binding_name(&decl.name)?;
                        let id = ctx.define_local(name.clone(), Type::Any);
                        vec![(name, id)]
                    }
                }
            } else {
                return Err(anyhow!("for-of requires a variable declaration"));
            }
        }
        ast::ForHead::Pat(pat) => {
            let name = get_pat_name(pat)?;
            let id = ctx.define_local(name.clone(), Type::Any);
            vec![(name, id)]
        }
        _ => return Err(anyhow!("Unsupported for-of left-hand side")),
    };

    // NOW lower the body - variables are defined so body can reference them
    let mut loop_body = lower_body_stmt(ctx, &for_of_stmt.body)?;

    // Build binding statements using the pre-defined variable IDs
    let binding_stmts = match &for_of_stmt.left {
        ast::ForHead::VarDecl(var_decl) => {
            if let Some(decl) = var_decl.decls.first() {
                // `for await (const x of arr)`: spec ECMA-262 §14.7.5.10
                // says each iteration must Await the value yielded by
                // the iterator. For a plain-array iterable that means
                // `await arr[i]` — unwraps a Promise element into its
                // resolved value before binding. Without this, `for
                // await (const x of [Promise.resolve(1), …])` would
                // bind `x = <Promise object>` and any numeric op would
                // see NaN. The iterator-protocol path above already
                // wraps the `__iter.next()` call in `Expr::Await` for
                // async generators; this brings the array-iteration
                // path to parity.
                let raw_item_expr = Expr::IndexGet {
                    object: Box::new(Expr::LocalGet(arr_id)),
                    index: Box::new(Expr::LocalGet(idx_id)),
                };
                let item_expr = if for_of_stmt.is_await {
                    Expr::Await(Box::new(raw_item_expr))
                } else {
                    raw_item_expr
                };

                match &decl.name {
                    ast::Pat::Ident(_) => {
                        // Simple binding: for (const x of arr)
                        let (name, id) = var_ids[0].clone();
                        let init = if set_fastpath {
                            Expr::SetValueAt {
                                set: Box::new(Expr::LocalGet(arr_id)),
                                idx: Box::new(Expr::LocalGet(idx_id)),
                            }
                        } else {
                            item_expr
                        };
                        vec![Stmt::Let {
                            id,
                            name,
                            ty: elem_type.clone(),
                            mutable: false,
                            init: Some(init),
                        }]
                    }
                    ast::Pat::Array(arr_pat) => {
                        if map_kv_fastpath {
                            // Map [k, v] / [k] / [, v] fast path: read
                            // each requested entry slot directly from
                            // the Map's flat buffer at the loop index.
                            // No `__item` Array materialization. Skipped
                            // slots ([,v] etc.) emit no binding.
                            let key_ty = map_key_type.clone().unwrap_or(Type::Any);
                            let val_ty = map_val_type.clone().unwrap_or(Type::Any);
                            let mut stmts: Vec<Stmt> = Vec::new();
                            let mut var_idx = 0;
                            for (slot, elem) in arr_pat.elems.iter().enumerate() {
                                let Some(ast::Pat::Ident(_)) = elem else {
                                    continue;
                                };
                                let (name, id) = var_ids[var_idx].clone();
                                var_idx += 1;
                                let (ty, init) = if slot == 0 {
                                    (
                                        key_ty.clone(),
                                        Expr::MapEntryKeyAt {
                                            map: Box::new(Expr::LocalGet(arr_id)),
                                            idx: Box::new(Expr::LocalGet(idx_id)),
                                        },
                                    )
                                } else {
                                    (
                                        val_ty.clone(),
                                        Expr::MapEntryValueAt {
                                            map: Box::new(Expr::LocalGet(arr_id)),
                                            idx: Box::new(Expr::LocalGet(idx_id)),
                                        },
                                    )
                                };
                                stmts.push(Stmt::Let {
                                    id,
                                    name,
                                    ty,
                                    mutable: false,
                                    init: Some(init),
                                });
                            }
                            stmts
                        } else {
                            // Array destructuring: for (const [a, b] of arr)
                            let mut stmts = vec![Stmt::Let {
                                id: item_id,
                                name: format!("__item_{}", item_id),
                                ty: elem_type.clone(),
                                mutable: false,
                                init: Some(item_expr),
                            }];

                            // Extract each element using pre-defined IDs
                            let mut var_idx = 0;
                            for (idx, elem) in arr_pat.elems.iter().enumerate() {
                                if let Some(elem_pat) = elem {
                                    if let ast::Pat::Ident(_) = elem_pat {
                                        let (name, id) = var_ids[var_idx].clone();
                                        var_idx += 1;
                                        // For Map destructuring, use the Tuple element type
                                        let var_type = if let Type::Tuple(ref types) = elem_type {
                                            types.get(idx).cloned().unwrap_or(Type::Any)
                                        } else {
                                            Type::Any
                                        };
                                        stmts.push(Stmt::Let {
                                            id,
                                            name,
                                            ty: var_type,
                                            mutable: false,
                                            init: Some(Expr::IndexGet {
                                                object: Box::new(Expr::LocalGet(item_id)),
                                                index: Box::new(Expr::Number(idx as f64)),
                                            }),
                                        });
                                    }
                                }
                            }
                            stmts
                        }
                    }
                    ast::Pat::Object(obj_pat) => {
                        // Object destructuring: for (const { a, b } of arr)
                        let mut stmts = vec![Stmt::Let {
                            id: item_id,
                            name: format!("__item_{}", item_id),
                            ty: Type::Any,
                            mutable: false,
                            init: Some(item_expr),
                        }];

                        // Extract each property using pre-defined IDs
                        let mut var_idx = 0;
                        for prop in &obj_pat.props {
                            match prop {
                                ast::ObjectPatProp::Assign(assign) => {
                                    let prop_name = assign.key.sym.to_string();
                                    let (name, id) = var_ids[var_idx].clone();
                                    var_idx += 1;
                                    let init_value = if let Some(default_expr) = &assign.value {
                                        let prop_access = Expr::PropertyGet {
                                            object: Box::new(Expr::LocalGet(item_id)),
                                            property: prop_name,
                                        };
                                        let default_val = lower_expr(ctx, default_expr)?;
                                        let condition = Expr::Compare {
                                            op: CompareOp::Ne,
                                            left: Box::new(prop_access.clone()),
                                            right: Box::new(Expr::Undefined),
                                        };
                                        Expr::Conditional {
                                            condition: Box::new(condition),
                                            then_expr: Box::new(prop_access),
                                            else_expr: Box::new(default_val),
                                        }
                                    } else {
                                        Expr::PropertyGet {
                                            object: Box::new(Expr::LocalGet(item_id)),
                                            property: prop_name,
                                        }
                                    };
                                    stmts.push(Stmt::Let {
                                        id,
                                        name,
                                        ty: Type::Any,
                                        mutable: false,
                                        init: Some(init_value),
                                    });
                                }
                                ast::ObjectPatProp::KeyValue(kv) => {
                                    let key = match &kv.key {
                                        ast::PropName::Ident(ident) => ident.sym.to_string(),
                                        ast::PropName::Str(s) => {
                                            s.value.as_str().unwrap_or("").to_string()
                                        }
                                        _ => continue,
                                    };
                                    let key_source = Expr::PropertyGet {
                                        object: Box::new(Expr::LocalGet(item_id)),
                                        property: key,
                                    };
                                    if let ast::Pat::Ident(_) = &*kv.value {
                                        let (name, id) = var_ids[var_idx].clone();
                                        var_idx += 1;
                                        stmts.push(Stmt::Let {
                                            id,
                                            name,
                                            ty: Type::Any,
                                            mutable: false,
                                            init: Some(key_source),
                                        });
                                    } else {
                                        // Nested pattern (e.g. `key: [a, b]`).
                                        // Issue #554.
                                        emit_for_of_pattern_binding(
                                            ctx,
                                            &kv.value,
                                            key_source,
                                            &var_ids,
                                            &mut var_idx,
                                            &mut stmts,
                                        )?;
                                    }
                                }
                                _ => {}
                            }
                        }
                        stmts
                    }
                    _ => {
                        let (name, id) = var_ids[0].clone();
                        vec![Stmt::Let {
                            id,
                            name,
                            ty: Type::Any,
                            mutable: false,
                            init: Some(Expr::IndexGet {
                                object: Box::new(Expr::LocalGet(arr_id)),
                                index: Box::new(Expr::LocalGet(idx_id)),
                            }),
                        }]
                    }
                }
            } else {
                return Err(anyhow!("for-of requires a variable declaration"));
            }
        }
        ast::ForHead::Pat(_) => {
            let (name, id) = var_ids[0].clone();
            vec![Stmt::Let {
                id,
                name,
                ty: Type::Any,
                mutable: false,
                init: Some(Expr::IndexGet {
                    object: Box::new(Expr::LocalGet(arr_id)),
                    index: Box::new(Expr::LocalGet(idx_id)),
                }),
            }]
        }
        _ => return Err(anyhow!("Unsupported for-of left-hand side")),
    };

    // Prepend the binding statements to the loop body
    for (i, stmt) in binding_stmts.into_iter().enumerate() {
        loop_body.insert(i, stmt);
    }

    // Loop bound. Map/Set fast paths read `.size` (lowered by
    // codegen to `js_map_size` / `js_set_size`); regular path uses
    // `__arr.length` against the materialized iterable.
    let bound_expr = if map_kv_fastpath || set_fastpath {
        Expr::PropertyGet {
            object: Box::new(Expr::LocalGet(arr_id)),
            property: "size".to_string(),
        }
    } else {
        Expr::PropertyGet {
            object: Box::new(Expr::LocalGet(arr_id)),
            property: "length".to_string(),
        }
    };
    // Create the for loop:
    // for (let __i = 0; __i < __arr.length; __i++) { ... }
    module.init.push(Stmt::For {
        init: Some(Box::new(Stmt::Let {
            id: idx_id,
            name: format!("__idx_{}", idx_id),
            ty: Type::Number,
            mutable: true,
            init: Some(Expr::Number(0.0)),
        })),
        condition: Some(Expr::Compare {
            op: CompareOp::Lt,
            left: Box::new(Expr::LocalGet(idx_id)),
            right: Box::new(bound_expr),
        }),
        update: Some(Expr::Update {
            id: idx_id,
            op: UpdateOp::Increment,
            prefix: true,
        }),
        body: loop_body,
    });
    ctx.pop_block_scope(for_scope_mark);
    Ok(())
}

pub(crate) fn lower_stmt_for_in(
    ctx: &mut LoweringContext,
    module: &mut Module,
    for_in_stmt: &ast::ForInStmt,
) -> Result<()> {
    // Desugar for-in to a for-of over Object.keys(obj):
    // for (const key in obj) { body }
    // becomes:
    // { let __keys = Object.keys(obj); for (let __i = 0; __i < __keys.length; __i++) { const key = __keys[__i]; body } }
    // Push a block scope so the loop key and internal temporaries don't leak.
    let for_scope_mark = ctx.push_block_scope();

    // Get the iteration variable name
    let key_name = match &for_in_stmt.left {
        ast::ForHead::VarDecl(var_decl) => {
            if let Some(decl) = var_decl.decls.first() {
                get_binding_name(&decl.name)?
            } else {
                return Err(anyhow!("for-in requires a variable declaration"));
            }
        }
        ast::ForHead::Pat(pat) => get_pat_name(pat)?,
        _ => return Err(anyhow!("Unsupported for-in left-hand side")),
    };

    // Lower the object expression
    let obj_expr = lower_expr(ctx, &for_in_stmt.right)?;

    // Create Object.keys(obj) expression to get the array of keys
    let keys_expr = Expr::ObjectKeys(Box::new(obj_expr));

    // Create internal variables for the keys array and index
    let keys_id = ctx.fresh_local();
    let idx_id = ctx.fresh_local();
    let key_id = ctx.define_local(key_name.clone(), Type::String);

    // Store keys array reference: let __keys = Object.keys(obj)
    module.init.push(Stmt::Let {
        id: keys_id,
        name: format!("__keys_{}", keys_id),
        ty: Type::Array(Box::new(Type::String)),
        mutable: false,
        init: Some(keys_expr),
    });

    // Lower the body
    let mut loop_body = lower_body_stmt(ctx, &for_in_stmt.body)?;

    // Prepend: const key = __keys[__i]
    loop_body.insert(
        0,
        Stmt::Let {
            id: key_id,
            name: key_name,
            ty: Type::String,
            mutable: false,
            init: Some(Expr::IndexGet {
                object: Box::new(Expr::LocalGet(keys_id)),
                index: Box::new(Expr::LocalGet(idx_id)),
            }),
        },
    );

    // Create the for loop:
    // for (let __i = 0; __i < __keys.length; __i++) { ... }
    module.init.push(Stmt::For {
        init: Some(Box::new(Stmt::Let {
            id: idx_id,
            name: format!("__idx_{}", idx_id),
            ty: Type::Number,
            mutable: true,
            init: Some(Expr::Number(0.0)),
        })),
        condition: Some(Expr::Compare {
            op: CompareOp::Lt,
            left: Box::new(Expr::LocalGet(idx_id)),
            right: Box::new(Expr::PropertyGet {
                object: Box::new(Expr::LocalGet(keys_id)),
                property: "length".to_string(),
            }),
        }),
        update: Some(Expr::Update {
            id: idx_id,
            op: UpdateOp::Increment,
            prefix: true,
        }),
        body: loop_body,
    });
    ctx.pop_block_scope(for_scope_mark);
    Ok(())
}
