//! AST to HIR lowering — extracted from `lower/mod.rs` (issue #1101).
//!
//! Pure mechanical split: no logic changes. Helpers keep their original
//! visibility and are re-exported from `lower/mod.rs` so the existing
//! `expr_*` submodules and the rest of the crate keep compiling unchanged.

#![allow(unused_imports)]

use anyhow::{anyhow, Result};
use perry_types::{FuncId, FunctionType, GlobalId, LocalId, Type, TypeParam};
use std::collections::{HashMap, HashSet};
use swc_ecma_ast as ast;

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

/// Recursively walk a destructuring pattern collecting every leaf identifier
/// (and pre-defining each as a local). Used by the for-of binding pre-pass so
/// the loop body can reference variables introduced by *nested* patterns like
/// `for (const { foo, bar: [a, b] } of arr)` — the outer per-prop loop only
/// handled `Ident` leaves, so leaves buried in nested array/object patterns
/// were silently skipped and read as zero in the body. Issue #554.
pub(crate) fn collect_for_of_pattern_leaves(
    ctx: &mut LoweringContext,
    pat: &ast::Pat,
    out: &mut Vec<(String, LocalId)>,
) {
    match pat {
        ast::Pat::Ident(ident) => {
            let name = ident.id.sym.to_string();
            let id = ctx.define_local(name.clone(), Type::Any);
            out.push((name, id));
        }
        ast::Pat::Array(arr_pat) => {
            for elem in &arr_pat.elems {
                if let Some(ep) = elem {
                    if let ast::Pat::Rest(rest) = ep {
                        collect_for_of_pattern_leaves(ctx, &rest.arg, out);
                    } else {
                        collect_for_of_pattern_leaves(ctx, ep, out);
                    }
                }
            }
        }
        ast::Pat::Object(obj_pat) => {
            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);
                        out.push((name, id));
                    }
                    ast::ObjectPatProp::KeyValue(kv) => {
                        collect_for_of_pattern_leaves(ctx, &kv.value, out);
                    }
                    ast::ObjectPatProp::Rest(rest) => {
                        collect_for_of_pattern_leaves(ctx, &rest.arg, out);
                    }
                }
            }
        }
        ast::Pat::Assign(assign_pat) => {
            collect_for_of_pattern_leaves(ctx, &assign_pat.left, out);
        }
        ast::Pat::Rest(rest) => {
            collect_for_of_pattern_leaves(ctx, &rest.arg, out);
        }
        _ => {}
    }
}

/// Emit `Stmt::Let` bindings for a destructuring pattern given a `source`
/// expression that produces the value being destructured. Reuses the
/// pre-allocated leaf ids in `var_ids` (in source order) so the loop body — which
/// was already lowered against those ids — sees the correct bindings. Mirrors
/// `destructuring::lower_pattern_binding_into` but takes pre-allocated ids
/// instead of calling `define_local` itself. Issue #554.
pub(crate) fn emit_for_of_pattern_binding(
    ctx: &mut LoweringContext,
    pat: &ast::Pat,
    source: Expr,
    var_ids: &[(String, LocalId)],
    var_idx: &mut usize,
    out: &mut Vec<Stmt>,
) -> Result<()> {
    match pat {
        ast::Pat::Ident(_) => {
            let (name, id) = var_ids[*var_idx].clone();
            *var_idx += 1;
            out.push(Stmt::Let {
                id,
                name,
                ty: Type::Any,
                mutable: false,
                init: Some(source),
            });
            Ok(())
        }
        ast::Pat::Array(arr_pat) => {
            // Default to Array(Any) so OOB reads return undefined/NaN as the
            // existing `destructuring::lower_pattern_binding_into` helper does.
            // (Typing the temp as Any pulls reads through the typed-element
            // fast path which returns 0 for OOB and breaks default-value
            // handling.)
            let arr_ty = arr_pat
                .type_ann
                .as_ref()
                .map(|ann| crate::lower_types::extract_ts_type(&ann.type_ann))
                .unwrap_or(Type::Array(Box::new(Type::Any)));
            let tmp_id = ctx.fresh_local();
            let tmp_name = format!("__destruct_{}", tmp_id);
            ctx.locals.push((tmp_name.clone(), tmp_id, arr_ty.clone()));
            out.push(Stmt::Let {
                id: tmp_id,
                name: tmp_name,
                ty: arr_ty,
                mutable: false,
                init: Some(source),
            });
            for (idx, elem) in arr_pat.elems.iter().enumerate() {
                let Some(elem_pat) = elem else { continue };
                if let ast::Pat::Rest(rest) = elem_pat {
                    let slice = Expr::ArraySlice {
                        array: Box::new(Expr::LocalGet(tmp_id)),
                        start: Box::new(Expr::Number(idx as f64)),
                        end: None,
                    };
                    emit_for_of_pattern_binding(ctx, &rest.arg, slice, var_ids, var_idx, out)?;
                    break;
                }
                let elem_src = Expr::IndexGet {
                    object: Box::new(Expr::LocalGet(tmp_id)),
                    index: Box::new(Expr::Number(idx as f64)),
                };
                emit_for_of_pattern_binding(ctx, elem_pat, elem_src, var_ids, var_idx, out)?;
            }
            Ok(())
        }
        ast::Pat::Object(obj_pat) => {
            let tmp_id = ctx.fresh_local();
            let tmp_name = format!("__destruct_{}", tmp_id);
            ctx.locals.push((tmp_name.clone(), tmp_id, Type::Any));
            out.push(Stmt::Let {
                id: tmp_id,
                name: tmp_name,
                ty: Type::Any,
                mutable: false,
                init: Some(source),
            });
            for prop in &obj_pat.props {
                match prop {
                    ast::ObjectPatProp::Assign(assign) => {
                        let key = assign.key.sym.to_string();
                        let prop_access = Expr::PropertyGet {
                            object: Box::new(Expr::LocalGet(tmp_id)),
                            property: key,
                        };
                        let init = if let Some(default_expr) = &assign.value {
                            let default_val = lower_expr(ctx, default_expr)?;
                            Expr::Conditional {
                                condition: Box::new(Expr::Compare {
                                    op: CompareOp::Ne,
                                    left: Box::new(prop_access.clone()),
                                    right: Box::new(Expr::Undefined),
                                }),
                                then_expr: Box::new(prop_access),
                                else_expr: Box::new(default_val),
                            }
                        } else {
                            prop_access
                        };
                        let (name, id) = var_ids[*var_idx].clone();
                        *var_idx += 1;
                        out.push(Stmt::Let {
                            id,
                            name,
                            ty: Type::Any,
                            mutable: false,
                            init: Some(init),
                        });
                    }
                    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 elem_src = Expr::PropertyGet {
                            object: Box::new(Expr::LocalGet(tmp_id)),
                            property: key,
                        };
                        emit_for_of_pattern_binding(
                            ctx, &kv.value, elem_src, var_ids, var_idx, out,
                        )?;
                    }
                    _ => {}
                }
            }
            Ok(())
        }
        ast::Pat::Assign(assign_pat) => {
            let tmp_id = ctx.fresh_local();
            let tmp_name = format!("__destruct_{}", tmp_id);
            ctx.locals.push((tmp_name.clone(), tmp_id, Type::Any));
            out.push(Stmt::Let {
                id: tmp_id,
                name: tmp_name,
                ty: Type::Any,
                mutable: false,
                init: Some(source),
            });
            let default_val = lower_expr(ctx, &assign_pat.right)?;
            let with_default = Expr::Conditional {
                condition: Box::new(Expr::IsUndefinedOrBareNan(Box::new(Expr::LocalGet(tmp_id)))),
                then_expr: Box::new(default_val),
                else_expr: Box::new(Expr::LocalGet(tmp_id)),
            };
            emit_for_of_pattern_binding(ctx, &assign_pat.left, with_default, var_ids, var_idx, out)
        }
        _ => Ok(()),
    }
}

pub(crate) fn lower_stmt(
    ctx: &mut LoweringContext,
    module: &mut Module,
    stmt: &ast::Stmt,
) -> Result<()> {
    match stmt {
        ast::Stmt::Decl(decl) => {
            match decl {
                ast::Decl::Fn(fn_decl) => {
                    // Skip declare functions (no body) - they are external FFI declarations
                    if fn_decl.function.body.is_none() {
                        return Ok(());
                    }
                    let func = lower_fn_decl(ctx, fn_decl)?;
                    // Register return type for call-site inference
                    if let Some((module, class)) =
                        native_instance_from_return_type(&func.return_type)
                    {
                        ctx.func_return_native_instances.push((
                            func.name.clone(),
                            module.to_string(),
                            class.to_string(),
                        ));
                    }
                    if !matches!(func.return_type, Type::Any) {
                        ctx.register_func_return_type(func.name.clone(), func.return_type.clone());
                    }
                    // Store parameter defaults for call-site resolution
                    let defaults: Vec<Option<Expr>> =
                        func.params.iter().map(|p| p.default.clone()).collect();
                    let param_ids: Vec<LocalId> = func.params.iter().map(|p| p.id).collect();
                    let rest_idx = func.params.iter().position(|p| p.is_rest);
                    let has_synth_args = func
                        .params
                        .last()
                        .is_some_and(|p| p.is_rest && p.name == "arguments");
                    ctx.func_defaults.push((
                        func.id,
                        defaults,
                        param_ids,
                        rest_idx,
                        has_synth_args,
                    ));
                    module.functions.push(func);
                }
                ast::Decl::Var(var_decl) => {
                    let mutable = var_decl.kind != ast::VarDeclKind::Const;
                    let is_var = var_decl.kind == ast::VarDeclKind::Var;
                    for decl in &var_decl.decls {
                        // Check if this is a Widget({...}) call from perry/widget
                        if let Some(init) = &decl.init {
                            if let ast::Expr::Call(call_expr) = init.as_ref() {
                                if let Some(widget_decl) = try_lower_widget_decl(ctx, call_expr) {
                                    module.widgets.push(widget_decl);
                                    continue;
                                }
                            }
                        }
                        // For array destructuring from generator calls, wrap init in
                        // IteratorToArray so the destructuring gets a real array.
                        // This converts: const [a, b, ...rest] = gen()
                        // to: const [a, b, ...rest] = IteratorToArray(gen())
                        // by inserting a temp variable.
                        if matches!(&decl.name, ast::Pat::Array(_)) {
                            if let Some(init) = &decl.init {
                                if let ast::Expr::Call(call) = init.as_ref() {
                                    if let ast::Callee::Expr(callee) = &call.callee {
                                        if let ast::Expr::Ident(ident) = callee.as_ref() {
                                            if ctx.generator_func_names.contains(ident.sym.as_ref())
                                            {
                                                // Lower the generator call, wrap in IteratorToArray, assign to temp
                                                let gen_expr = lower_expr(ctx, init)?;
                                                let arr_expr =
                                                    Expr::IteratorToArray(Box::new(gen_expr));
                                                let temp_id = ctx.fresh_local();
                                                ctx.locals.push((
                                                    format!("__gen_arr_{}", temp_id),
                                                    temp_id,
                                                    Type::Array(Box::new(Type::Any)),
                                                ));
                                                module.init.push(Stmt::Let {
                                                    id: temp_id,
                                                    name: format!("__gen_arr_{}", temp_id),
                                                    ty: Type::Array(Box::new(Type::Any)),
                                                    mutable: false,
                                                    init: Some(arr_expr),
                                                });
                                                // Now destructure from the temp array
                                                // Create a synthetic VarDeclarator with init = LocalGet(temp_id)
                                                // For simplicity, manually extract each element
                                                if let ast::Pat::Array(arr_pat) = &decl.name {
                                                    let mut idx = 0;
                                                    for elem in &arr_pat.elems {
                                                        if let Some(elem_pat) = elem {
                                                            match elem_pat {
                                                                ast::Pat::Ident(ident) => {
                                                                    let name =
                                                                        ident.id.sym.to_string();
                                                                    let id = ctx.define_local(
                                                                        name.clone(),
                                                                        Type::Any,
                                                                    );
                                                                    module.init.push(Stmt::Let {
                                                                        id,
                                                                        name,
                                                                        ty: Type::Any,
                                                                        mutable,
                                                                        init: Some(
                                                                            Expr::IndexGet {
                                                                                object: Box::new(
                                                                                    Expr::LocalGet(
                                                                                        temp_id,
                                                                                    ),
                                                                                ),
                                                                                index: Box::new(
                                                                                    Expr::Number(
                                                                                        idx as f64,
                                                                                    ),
                                                                                ),
                                                                            },
                                                                        ),
                                                                    });
                                                                    idx += 1;
                                                                }
                                                                ast::Pat::Rest(rest) => {
                                                                    if let ast::Pat::Ident(
                                                                        rest_ident,
                                                                    ) = &*rest.arg
                                                                    {
                                                                        let name = rest_ident
                                                                            .id
                                                                            .sym
                                                                            .to_string();
                                                                        let id = ctx.define_local(
                                                                            name.clone(),
                                                                            Type::Array(Box::new(
                                                                                Type::Any,
                                                                            )),
                                                                        );
                                                                        module.init.push(Stmt::Let {
                                                                            id,
                                                                            name,
                                                                            ty: Type::Array(Box::new(Type::Any)),
                                                                            mutable,
                                                                            init: Some(Expr::ArraySlice {
                                                                                array: Box::new(Expr::LocalGet(temp_id)),
                                                                                start: Box::new(Expr::Number(idx as f64)),
                                                                                end: None,
                                                                            }),
                                                                        });
                                                                    }
                                                                }
                                                                _ => {
                                                                    idx += 1;
                                                                }
                                                            }
                                                        } else {
                                                            idx += 1; // skip holes
                                                        }
                                                    }
                                                }
                                                continue; // skip the regular destructuring path
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        // Track locals assigned from `regex.exec(...)` so .index/.groups
                        // accesses route to the bare RegExpExecIndex/Groups variants.
                        if let (ast::Pat::Ident(ident), Some(init)) = (&decl.name, &decl.init) {
                            if is_regex_exec_init(ctx, init) {
                                ctx.regex_exec_locals.insert(ident.id.sym.to_string());
                            }
                        }
                        // `const { proxy: revProxy, revoke } = Proxy.revocable(t, h)`
                        // is rewritten to a ProxyNew binding + a dummy revoke binding.
                        if let (ast::Pat::Object(obj_pat), Some(init)) = (&decl.name, &decl.init) {
                            let inner = {
                                let mut e = init.as_ref();
                                loop {
                                    match e {
                                        ast::Expr::TsAs(ts_as) => e = &ts_as.expr,
                                        ast::Expr::TsNonNull(nn) => e = &nn.expr,
                                        ast::Expr::TsConstAssertion(ca) => e = &ca.expr,
                                        ast::Expr::TsTypeAssertion(ta) => e = &ta.expr,
                                        ast::Expr::Paren(p) => e = &p.expr,
                                        _ => break,
                                    }
                                }
                                e
                            };
                            let mut is_proxy_revocable = false;
                            if let ast::Expr::Call(call) = inner {
                                if let ast::Callee::Expr(callee) = &call.callee {
                                    if let ast::Expr::Member(m) = callee.as_ref() {
                                        if let ast::Expr::Ident(o) = m.obj.as_ref() {
                                            if o.sym.as_ref() == "Proxy" {
                                                if let ast::MemberProp::Ident(p) = &m.prop {
                                                    if p.sym.as_ref() == "revocable" {
                                                        is_proxy_revocable = true;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            if is_proxy_revocable {
                                if let ast::Expr::Call(call) = inner {
                                    let target_ast = call.args.first().map(|a| a.expr.clone());
                                    let handler_ast = call.args.get(1).map(|a| a.expr.clone());
                                    let target = if let Some(t) = target_ast {
                                        lower_expr(ctx, &t)?
                                    } else {
                                        Expr::Undefined
                                    };
                                    let handler = if let Some(h) = handler_ast {
                                        lower_expr(ctx, &h)?
                                    } else {
                                        Expr::Object(vec![])
                                    };
                                    let mut proxy_alias: Option<String> = None;
                                    let mut revoke_alias: Option<String> = None;
                                    for prop in &obj_pat.props {
                                        match prop {
                                            ast::ObjectPatProp::KeyValue(kv) => {
                                                let key_name = match &kv.key {
                                                    ast::PropName::Ident(i) => i.sym.to_string(),
                                                    ast::PropName::Str(s) => {
                                                        s.value.as_str().unwrap_or("").to_string()
                                                    }
                                                    _ => continue,
                                                };
                                                if let ast::Pat::Ident(alias) = &*kv.value {
                                                    let alias_name = alias.id.sym.to_string();
                                                    if key_name == "proxy" {
                                                        proxy_alias = Some(alias_name);
                                                    } else if key_name == "revoke" {
                                                        revoke_alias = Some(alias_name);
                                                    }
                                                }
                                            }
                                            ast::ObjectPatProp::Assign(a) => {
                                                let name = a.key.sym.to_string();
                                                if name == "proxy" {
                                                    proxy_alias = Some(name);
                                                } else if name == "revoke" {
                                                    revoke_alias = Some(name);
                                                }
                                            }
                                            _ => {}
                                        }
                                    }
                                    if let Some(p_name) = proxy_alias {
                                        let proxy_id = ctx.define_local(p_name.clone(), Type::Any);
                                        module.init.push(Stmt::Let {
                                            id: proxy_id,
                                            name: p_name.clone(),
                                            ty: Type::Any,
                                            mutable,
                                            init: Some(Expr::ProxyNew {
                                                target: Box::new(target),
                                                handler: Box::new(handler),
                                            }),
                                        });
                                        ctx.proxy_locals.insert(p_name.clone());
                                        if let Some(r_name) = revoke_alias {
                                            ctx.proxy_revoke_locals.insert(r_name.clone(), p_name);
                                            let rev_id =
                                                ctx.define_local(r_name.clone(), Type::Any);
                                            module.init.push(Stmt::Let {
                                                id: rev_id,
                                                name: r_name,
                                                ty: Type::Any,
                                                mutable,
                                                init: Some(Expr::Undefined),
                                            });
                                        }
                                    }
                                    continue;
                                }
                            }
                        }
                        // `const X = class { ... }` — lower the class expression
                        // inline using the binding name as the class name (so
                        // `new X(...)` later resolves without a dynamic dispatch
                        // shim). The let binding still stores a sentinel value
                        // (the new'd object) but the class is fully lowered.
                        if let (ast::Pat::Ident(ident), Some(init)) = (&decl.name, &decl.init) {
                            let inner_expr = {
                                let mut e = init.as_ref();
                                loop {
                                    match e {
                                        ast::Expr::Paren(p) => e = &p.expr,
                                        ast::Expr::TsAs(a) => e = &a.expr,
                                        ast::Expr::TsNonNull(n) => e = &n.expr,
                                        ast::Expr::TsTypeAssertion(a) => e = &a.expr,
                                        _ => break,
                                    }
                                }
                                e
                            };
                            if let ast::Expr::Class(class_expr) = inner_expr {
                                let bind_name = ident.id.sym.to_string();
                                // Only handle if there's no explicit type annotation
                                // that would conflict, and the binding name isn't
                                // already a class (no shadow).
                                if ctx.lookup_class(&bind_name).is_none() {
                                    // Refs #486: `var X = class _X { ... new _X() ... }` —
                                    // the inner self-binding name `_X` references the same
                                    // class as the outer binding `X`. Pre-register the inner
                                    // name as a class alias BEFORE lowering the class body
                                    // so any `Ident("_X")` inside method bodies (e.g.
                                    // `new _X()`) lowers to `Expr::ClassRef("_X")` instead
                                    // of falling through to ExternFuncRef. The HIR `new`
                                    // ident path keys off `lookup_class`. Hono's
                                    // `var Node = class _Node { ... }` and similar npm dist
                                    // shapes hit this.
                                    let inner_name_for_register = class_expr
                                        .ident
                                        .as_ref()
                                        .map(|i| i.sym.to_string())
                                        .filter(|n| n != &bind_name);
                                    if let Some(ref inner_name) = inner_name_for_register {
                                        // Allocate the class id eagerly so we can register
                                        // it under both names; lower_class_from_ast picks up
                                        // the same id via lookup_class(bind_name).
                                        let class_id = ctx.fresh_class();
                                        ctx.register_class(bind_name.clone(), class_id);
                                        ctx.register_class(inner_name.clone(), class_id);
                                        ctx.class_expr_aliases
                                            .insert(inner_name.clone(), bind_name.clone());
                                    }
                                    // Lower the class with the binding name so
                                    // `new BindName(...)` works unchanged.
                                    let mut lowered_class =
                                        crate::lower_decl::lower_class_from_ast(
                                            ctx,
                                            &class_expr.class,
                                            &bind_name,
                                            false,
                                        )?;
                                    if let Some(inner_name) = inner_name_for_register {
                                        lowered_class.aliases.push(inner_name);
                                    }
                                    push_class_dedup(module, lowered_class);
                                    // Register the alias so `new X()` → `new X()`
                                    // (no-op lookup, but marks the binding as a class).
                                    ctx.class_expr_aliases
                                        .insert(bind_name.clone(), bind_name.clone());
                                    // We intentionally DO NOT push a Stmt::Let for
                                    // this binding — the class itself takes the
                                    // role of a "static value" referenced by name.
                                    continue;
                                }
                            }
                            // `const Mixed = MixinFn(BaseClass)` — detect a call
                            // to a known mixin function and synthesize a real
                            // class extending the supplied base. The mixin's
                            // class AST is taken from the pre-scan map and
                            // copied verbatim with the `extends` clause rewritten
                            // to point at the concrete base class.
                            if let ast::Expr::Call(call) = inner_expr {
                                if let ast::Callee::Expr(callee_expr) = &call.callee {
                                    if let ast::Expr::Ident(fn_ident) = callee_expr.as_ref() {
                                        let fn_name = fn_ident.sym.to_string();
                                        if let Some((_param_name, mixin_class_box)) =
                                            ctx.mixin_funcs.get(&fn_name).cloned()
                                        {
                                            if call.args.len() == 1 {
                                                if let ast::Expr::Ident(base_ident) =
                                                    call.args[0].expr.as_ref()
                                                {
                                                    let base_class_name =
                                                        base_ident.sym.to_string();
                                                    if ctx.lookup_class(&base_class_name).is_some()
                                                    {
                                                        let bind_name = ident.id.sym.to_string();
                                                        if ctx.lookup_class(&bind_name).is_none() {
                                                            let mut new_class =
                                                                (*mixin_class_box).clone();
                                                            let base_id = ast::Ident::new(
                                                                base_class_name.clone().into(),
                                                                base_ident.span,
                                                                base_ident.ctxt,
                                                            );
                                                            new_class.super_class = Some(Box::new(
                                                                ast::Expr::Ident(base_id),
                                                            ));
                                                            let lowered_class = crate::lower_decl::lower_class_from_ast(
                                                                ctx,
                                                                &new_class,
                                                                &bind_name,
                                                                false,
                                                            )?;
                                                            push_class_dedup(module, lowered_class);
                                                            ctx.class_expr_aliases.insert(
                                                                bind_name.clone(),
                                                                bind_name.clone(),
                                                            );
                                                            continue;
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        let stmts = lower_var_decl_with_destructuring(ctx, decl, mutable)?;
                        // `var` is function-scoped: mark defined locals so
                        // `pop_block_scope` preserves them when leaving an inner block.
                        if is_var {
                            for s in &stmts {
                                if let Stmt::Let { id, .. } = s {
                                    ctx.var_hoisted_ids.insert(*id);
                                }
                            }
                        }
                        // Track awaited native module calls as native instances
                        // so property accesses (response.status, response.data) route
                        // through NativeMethodCall dispatch instead of generic PropertyGet.
                        for s in &stmts {
                            if let Stmt::Let {
                                name,
                                init: Some(Expr::Await(inner)),
                                ..
                            } = s
                            {
                                if let Expr::NativeMethodCall {
                                    module: mod_name,
                                    method,
                                    ..
                                } = inner.as_ref()
                                {
                                    let class_name = match (mod_name.as_str(), method.as_str()) {
                                        (
                                            "axios",
                                            "get" | "post" | "put" | "delete" | "patch" | "request",
                                        ) => Some("Response"),
                                        ("mongodb", "connect") => Some("MongoClient"),
                                        ("pg", "connect") => Some("Client"),
                                        _ => None,
                                    };
                                    if let Some(cn) = class_name {
                                        ctx.register_native_instance(
                                            name.clone(),
                                            mod_name.clone(),
                                            cn.to_string(),
                                        );
                                    }
                                }
                            }
                            // Track synchronous native module factories as native instances.
                            // Added for workstream A1.5 so `const sock = net.createConnection(...)`
                            // registers `sock` as a Socket instance; without this, subsequent
                            // `sock.write/on/end/destroy` miss the NATIVE_MODULE_TABLE dispatch
                            // and never reach the `js_net_socket_*` FFI in perry-stdlib.
                            if let Stmt::Let {
                                name,
                                init:
                                    Some(Expr::NativeMethodCall {
                                        module: mod_name,
                                        method,
                                        object: None,
                                        ..
                                    }),
                                ..
                            } = s
                            {
                                let class_name = match (mod_name.as_str(), method.as_str()) {
                                    ("net", "createConnection" | "connect") => Some("Socket"),
                                    // tls.connect returns the same Socket class — reuses
                                    // all the write/end/destroy/on/upgradeToTLS dispatch.
                                    ("tls", "connect") => Some("Socket"),
                                    // Issue #422: `new net.Socket()` lowers to a
                                    // receiver-less `Expr::NativeMethodCall` whose
                                    // method is the constructor name "Socket"; this
                                    // arm registers the binding so `sock.connect/...`
                                    // dispatches via the class-filtered entries.
                                    ("net", "Socket") => Some("Socket"),
                                    _ => None,
                                };
                                if let Some(cn) = class_name {
                                    // Register under `"net"` (the module the Socket class belongs to)
                                    // regardless of which module the factory lived in, so method
                                    // dispatch resolves correctly.
                                    ctx.register_native_instance(
                                        name.clone(),
                                        "net".to_string(),
                                        cn.to_string(),
                                    );
                                    let _ = mod_name; // suppress unused on tls branch
                                }
                                // Issue #577 — node:http / node:https / node:http2 server
                                // factories. `const s = createServer(...)` (named import)
                                // and `const s = http.createServer(...)` (namespace import)
                                // both lower to a receiver-less NativeMethodCall here, so
                                // this arm covers both shapes.
                                let http_class = match (mod_name.as_str(), method.as_str()) {
                                    ("http", "createServer") => Some("HttpServer"),
                                    ("https", "createServer") => Some("HttpsServer"),
                                    ("http2", "createSecureServer") => Some("Http2SecureServer"),
                                    ("async_hooks", "createHook") => Some("AsyncHook"),
                                    _ => None,
                                };
                                if let Some(cn) = http_class {
                                    let module_owned = mod_name.clone();
                                    ctx.register_native_instance(
                                        name.clone(),
                                        module_owned.clone(),
                                        cn.to_string(),
                                    );
                                    ctx.module_native_instances.push((
                                        name.clone(),
                                        module_owned,
                                        cn.to_string(),
                                    ));
                                }
                                // Issue #769 — node:http / node:https CLIENT factories.
                                // `const req = http.request(url, cb)` and `http.get` / `https.*`
                                // variants return a ClientRequest handle; register under
                                // module `"http"` so subsequent `req.on/.end/.write/...`
                                // resolve via the class-filtered entries in NATIVE_MODULE_TABLE.
                                let client_class = match (mod_name.as_str(), method.as_str()) {
                                    ("http", "request" | "get") => Some("ClientRequest"),
                                    ("https", "request" | "get") => Some("ClientRequest"),
                                    _ => None,
                                };
                                if let Some(cn) = client_class {
                                    ctx.register_native_instance(
                                        name.clone(),
                                        "http".to_string(),
                                        cn.to_string(),
                                    );
                                }
                            }
                            // Issue #1123 followup — `net.createServer(...)` /
                            // bare `createServer(...)` from `node:net` lower
                            // to `Expr::NetCreateServer { … }` (NOT the
                            // generic `NativeMethodCall` shape above), so
                            // they need their own registration arm. Tagging
                            // the binding as `("net", "Server")` makes
                            // subsequent `server.listen/.close/.on/.address`
                            // calls dispatch via the class_filter rows
                            // added in lower_call.rs.
                            if let Stmt::Let {
                                name,
                                init: Some(Expr::NetCreateServer { .. }),
                                ..
                            } = s
                            {
                                ctx.register_native_instance(
                                    name.clone(),
                                    "net".to_string(),
                                    "Server".to_string(),
                                );
                                ctx.module_native_instances.push((
                                    name.clone(),
                                    "net".to_string(),
                                    "Server".to_string(),
                                ));
                            }
                            // User-defined factory wrappers: when the init is a
                            // bare call to `userFunc(...)` and `userFunc` was
                            // registered as a native-instance factory (via
                            // its declared return type), inherit the class so
                            // downstream `local.method(...)` dispatches statically.
                            // Example: `function openSocket(): Socket { ... }`
                            // followed by `const sock = openSocket(...)` registers
                            // sock as ("net", "Socket").
                            if let Stmt::Let {
                                name,
                                init: Some(Expr::Call { callee, .. }),
                                ..
                            } = s
                            {
                                if let Expr::FuncRef(func_id) = callee.as_ref() {
                                    let func_name_owned =
                                        ctx.lookup_func_name(*func_id).map(|s| s.to_string());
                                    if let Some(func_name) = func_name_owned {
                                        let lookup = ctx
                                            .lookup_func_return_native_instance(&func_name)
                                            .map(|(m, c)| (m.to_string(), c.to_string()));
                                        if let Some((m, c)) = lookup {
                                            ctx.register_native_instance(name.clone(), m, c);
                                        }
                                    }
                                }
                            }
                        }
                        module.init.extend(stmts);
                    }
                }
                ast::Decl::Class(class_decl) => {
                    let class = lower_class_decl(ctx, class_decl, false)?;
                    // Issue #711: emit dynamic parent-class registration
                    // at the source-order position of the class declaration
                    // BEFORE the static-field-init stmts. Static field
                    // initializers may reference inherited static methods,
                    // and method dispatch reads the parent chain — wiring
                    // the parent edge first keeps the inherited lookup
                    // path consistent for those inits.
                    if let Some(extends_expr) = &class.extends_expr {
                        module
                            .init
                            .push(Stmt::Expr(Expr::RegisterClassParentDynamic {
                                class_name: class.name.clone(),
                                parent_expr: extends_expr.clone(),
                            }));
                    }
                    // Inject static-field-init statements at the source
                    // position of the class declaration. Per ES spec, a
                    // class declaration's static initializers run when the
                    // declaration evaluates — i.e., here in source order,
                    // not at the top of module init. This matters when a
                    // static field's initializer references a top-level
                    // const declared earlier in the module: the upfront
                    // `init_static_fields` pass at codegen.rs:3449 runs
                    // before any user `Let` bindings, so it captures
                    // unbound (undefined) values. The inline statements
                    // re-run with the correct values once we reach this
                    // point in source order.
                    for sf in &class.static_fields {
                        if let Some(init) = &sf.init {
                            if let Some(key) = sf.key_expr.as_ref() {
                                module.init.push(Stmt::Expr(Expr::ClassStaticSymbolSet {
                                    class_name: class.name.clone(),
                                    key: Box::new(key.clone()),
                                    value: Box::new(init.clone()),
                                }));
                            } else {
                                module.init.push(Stmt::Expr(Expr::StaticFieldSet {
                                    class_name: class.name.clone(),
                                    field_name: sf.name.clone(),
                                    value: Box::new(init.clone()),
                                }));
                            }
                        }
                    }
                    append_legacy_decorator_init_for_class(ctx, &mut module.init, &class);
                    push_class_dedup(module, class);
                }
                ast::Decl::TsEnum(enum_decl) => {
                    let en = lower_enum_decl(ctx, enum_decl, false)?;
                    module.enums.push(en);
                }
                ast::Decl::TsInterface(iface_decl) => {
                    let iface = lower_interface_decl(ctx, iface_decl, false)?;
                    module.interfaces.push(iface);
                }
                ast::Decl::TsTypeAlias(alias_decl) => {
                    let alias = lower_type_alias_decl(ctx, alias_decl, false)?;
                    module.type_aliases.push(alias);
                }
                ast::Decl::Using(using_decl) => {
                    // `using x = expr` / `await using x = expr` — TC39 Explicit
                    // Resource Management. Lower as const bindings. Disposal at
                    // block-scope exit is not yet automated — the variables are
                    // accessible but [Symbol.dispose/asyncDispose] isn't called.
                    // Treat as a const var declaration.
                    let fake_var = ast::VarDecl {
                        span: using_decl.span,
                        kind: ast::VarDeclKind::Const,
                        declare: false,
                        decls: using_decl.decls.clone(),
                        ctxt: Default::default(),
                    };
                    let mutable = false;
                    let _is_var = false;
                    for decl in &fake_var.decls {
                        if let Some(init) = &decl.init {
                            if let ast::Pat::Ident(bind_ident) = &decl.name {
                                let name = bind_ident.sym.to_string();
                                let init_expr = lower_expr(ctx, init)?;
                                let ty = Type::Any;
                                let id = ctx.fresh_local();
                                ctx.locals.push((name.clone(), id, ty.clone()));
                                module.init.push(Stmt::Let {
                                    id,
                                    name,
                                    ty,
                                    mutable,
                                    init: Some(init_expr),
                                });
                            }
                        }
                    }
                }
                ast::Decl::TsModule(ts_module) => {
                    // namespace X { ... } — lower as a synthetic class with static members
                    if !ts_module.declare {
                        if let Some(ref body) = ts_module.body {
                            let ns_name = match &ts_module.id {
                                ast::TsModuleName::Ident(ident) => ident.sym.to_string(),
                                ast::TsModuleName::Str(s) => {
                                    s.value.as_str().unwrap_or("").to_string()
                                }
                            };
                            let class =
                                lower_namespace_as_class(ctx, module, &ns_name, body, false)?;
                            push_class_dedup(module, class);
                        }
                    }
                }
                // #853: `ast::Decl` is `#[non_exhaustive]` upstream — keep
                // this catch-all so a future SWC variant is dropped silently
                // (the supported variants above each have explicit handling).
                #[allow(unreachable_patterns)]
                _ => {}
            }
        }
        ast::Stmt::Expr(expr_stmt) => {
            // Check if this is a destructuring assignment that needs special handling
            if let ast::Expr::Assign(assign) = expr_stmt.expr.as_ref() {
                if let ast::AssignTarget::Pat(pat) = &assign.left {
                    // This is a destructuring assignment at statement level
                    // We can emit proper Let statements for temporaries
                    let stmts = lower_destructuring_assignment_stmt(ctx, pat, &assign.right)?;
                    module.init.extend(stmts);
                    return Ok(());
                }
            }
            let expr = lower_expr(ctx, &expr_stmt.expr)?;
            module.init.push(Stmt::Expr(expr));
        }
        ast::Stmt::If(if_stmt) => {
            let condition = lower_expr(ctx, &if_stmt.test)?;
            // Each branch introduces its own lexical scope. Skip extra push if
            // branch is a BlockStmt (handled there) or an If (else-if chain).
            let then_branch = if matches!(*if_stmt.cons, ast::Stmt::Block(_)) {
                lower_body_stmt(ctx, &if_stmt.cons)?
            } else {
                let mark = ctx.push_block_scope();
                let stmts = lower_body_stmt(ctx, &if_stmt.cons)?;
                ctx.pop_block_scope(mark);
                stmts
            };
            let else_branch = if_stmt
                .alt
                .as_ref()
                .map(|s| {
                    if matches!(**s, ast::Stmt::Block(_)) || matches!(**s, ast::Stmt::If(_)) {
                        lower_body_stmt(ctx, s)
                    } else {
                        let mark = ctx.push_block_scope();
                        let stmts = lower_body_stmt(ctx, s);
                        ctx.pop_block_scope(mark);
                        stmts
                    }
                })
                .transpose()?;
            module.init.push(Stmt::If {
                condition,
                then_branch,
                else_branch,
            });
        }
        ast::Stmt::While(while_stmt) => {
            let condition = lower_expr(ctx, &while_stmt.test)?;
            let body = if matches!(*while_stmt.body, ast::Stmt::Block(_)) {
                lower_body_stmt(ctx, &while_stmt.body)?
            } else {
                let mark = ctx.push_block_scope();
                let stmts = lower_body_stmt(ctx, &while_stmt.body)?;
                ctx.pop_block_scope(mark);
                stmts
            };
            module.init.push(Stmt::While { condition, body });
        }
        ast::Stmt::DoWhile(do_while_stmt) => {
            let body = lower_body_stmt(ctx, &do_while_stmt.body)?;
            let condition = lower_expr(ctx, &do_while_stmt.test)?;
            module.init.push(Stmt::DoWhile { body, condition });
        }
        ast::Stmt::Labeled(labeled_stmt) => {
            let label = labeled_stmt.label.sym.to_string();
            let inner = lower_body_stmt(ctx, &labeled_stmt.body)?;
            if inner.len() == 1 {
                let body = inner.into_iter().next().unwrap();
                module.init.push(Stmt::Labeled {
                    label,
                    body: Box::new(body),
                });
            } else {
                let mut inner = inner;
                let last = inner.pop().unwrap();
                for s in inner {
                    module.init.push(s);
                }
                module.init.push(Stmt::Labeled {
                    label,
                    body: Box::new(last),
                });
            }
        }
        ast::Stmt::For(for_stmt) => {
            // Push a lexical scope covering init/test/update/body, so
            // `for (let i = 0; ...)` bindings don't leak to the outer scope.
            let for_scope_mark = ctx.push_block_scope();
            let init = if let Some(init) = &for_stmt.init {
                match init {
                    ast::VarDeclOrExpr::VarDecl(var_decl) => {
                        let is_var = var_decl.kind == ast::VarDeclKind::Var;
                        if is_var {
                            for decl in var_decl.decls.iter() {
                                let name = get_binding_name(&decl.name)?;
                                let init_expr =
                                    decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?;
                                let id = ctx.define_local(name.clone(), Type::Any);
                                ctx.var_hoisted_ids.insert(id);
                                module.init.push(Stmt::Let {
                                    id,
                                    name,
                                    ty: Type::Any,
                                    mutable: true,
                                    init: init_expr,
                                });
                            }
                            None
                        } else {
                            for decl in var_decl.decls.iter().skip(1) {
                                let name = get_binding_name(&decl.name)?;
                                let init_expr =
                                    decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?;
                                let id = ctx.define_local(name.clone(), Type::Any);
                                module.init.push(Stmt::Let {
                                    id,
                                    name,
                                    ty: Type::Any,
                                    mutable: true,
                                    init: init_expr,
                                });
                            }
                            if let Some(decl) = var_decl.decls.first() {
                                let name = get_binding_name(&decl.name)?;
                                let init_expr =
                                    decl.init.as_ref().map(|e| lower_expr(ctx, e)).transpose()?;
                                let id = ctx.define_local(name.clone(), Type::Any);
                                Some(Box::new(Stmt::Let {
                                    id,
                                    name,
                                    ty: Type::Any,
                                    mutable: true,
                                    init: init_expr,
                                }))
                            } else {
                                None
                            }
                        }
                    }
                    ast::VarDeclOrExpr::Expr(expr) => {
                        Some(Box::new(Stmt::Expr(lower_expr(ctx, expr)?)))
                    }
                }
            } else {
                None
            };
            let condition = for_stmt
                .test
                .as_ref()
                .map(|e| lower_expr(ctx, e))
                .transpose()?;
            let update = for_stmt
                .update
                .as_ref()
                .map(|e| lower_expr(ctx, e))
                .transpose()?;
            let body = lower_body_stmt(ctx, &for_stmt.body)?;
            ctx.pop_block_scope(for_scope_mark);
            module.init.push(Stmt::For {
                init,
                condition,
                update,
                body,
            });
        }
        ast::Stmt::Block(block) => {
            // Bare block: introduce a lexical scope so inner let/const shadow
            // without leaking into the enclosing module scope.
            let stmts = lower_block_stmt_scoped(ctx, block)?;
            for stmt in stmts {
                module.init.push(stmt);
            }
        }
        ast::Stmt::Try(try_stmt) => {
            // try body is its own lexical scope
            let body = lower_block_stmt_scoped(ctx, &try_stmt.block)?;

            // Lower catch clause (if present)
            let catch = if let Some(ref catch_clause) = try_stmt.handler {
                let scope_mark = ctx.enter_scope();

                let param = if let Some(ref pat) = catch_clause.param {
                    let param_name = get_pat_name(pat)?;
                    let param_id = ctx.define_local(param_name.clone(), Type::Any);
                    Some((param_id, param_name))
                } else {
                    None
                };

                let catch_body = lower_block_stmt(ctx, &catch_clause.body)?;
                ctx.exit_scope(scope_mark);

                Some(CatchClause {
                    param,
                    body: catch_body,
                })
            } else {
                None
            };

            // finally block is its own lexical scope
            let finally = if let Some(ref finally_block) = try_stmt.finalizer {
                Some(lower_block_stmt_scoped(ctx, finally_block)?)
            } else {
                None
            };

            module.init.push(Stmt::Try {
                body,
                catch,
                finally,
            });
        }
        ast::Stmt::Throw(throw_stmt) => {
            let expr = lower_expr(ctx, &throw_stmt.arg)?;
            module.init.push(Stmt::Throw(expr));
        }
        ast::Stmt::Switch(switch_stmt) => {
            let discriminant = lower_expr(ctx, &switch_stmt.discriminant)?;
            let mut cases = Vec::new();

            for case in &switch_stmt.cases {
                let test = case.test.as_ref().map(|e| lower_expr(ctx, e)).transpose()?;

                let mut body = Vec::new();
                for stmt in &case.cons {
                    body.extend(lower_body_stmt(ctx, stmt)?);
                }

                cases.push(SwitchCase { test, body });
            }

            module.init.push(Stmt::Switch {
                discriminant,
                cases,
            });
        }
        ast::Stmt::ForOf(for_of_stmt) => {
            lower_stmt_for_of(ctx, module, for_of_stmt)?;
        }
        ast::Stmt::ForIn(for_in_stmt) => {
            lower_stmt_for_in(ctx, module, for_in_stmt)?;
        }
        _ => {}
    }
    Ok(())
}
