#pragma once
#include "ast_node.hpp"
#include "module_resolver.hpp"
#include <nlohmann/json.hpp>
#include <sstream>
#include <stdexcept>
#include <string>
#include <map>
#include <set>
#include <vector>
#include <cstdlib>
#include <cstring>

// CEmitter — walks an ASTNode tree (or Module list) and produces C++17 source.
//
// Multi-module (CommonJS) design
// ────────────────────────────────
// Each dependency module is emitted as four parts:
//
//   1. static JsValue mod_X_exports;
//      The module's default export slot — set in _init().
//
//   2. static JsValue mod_X_<key>;   (one per value export not backed by a fn)
//      For exports like `module.exports.answer = 42` where there's no
//      FunctionDeclaration named "answer".
//
//   3. static JsValue mod_X_<localVar>;  (module-level JS variables)
//      Forward declarations; initialised inside _init().
//
//   4. static JsValue mod_X_<funcName>(...) { … }
//      One C function per top-level FunctionDeclaration, prefixed.
//
//   5. static void mod_X_init(void) { once-guard; assignments; side-effects }
//      Executes module top-level code exactly once. module.exports assignments
//      are EMITTED here (not suppressed) so that runtime values are correct.
//
// Entry module emits ordinary int main(void).
// At each `const { a } = require('./X')` site, mod_X_init() is called so the
// dep module's side effects run in the correct order.
// For `const x = require('./X')` (non-destructured), emits:
//   mod_X_init(); JsValue x = mod_X_exports;
// Built-in requires → js_require_builtin(JS_STR("name")).
class CEmitter {
public:
    // ── Single-file mode (backward-compatible) ───────────────────────────────
    std::string emit(const ASTNode& root) {
        if (root.type != "Program")
            throw std::runtime_error(
                "CEmitter: root must be Program, got " + root.type);
        clearModuleState();
        currentFilePath_ = ".";
        prePassRequires(root, currentFilePath_);
        emitAsyncFeatureWarnings(root, "<entry>");
        scanProgramForLowering(root);
        collectHttpCreateServerHandlerIds(root);
        scanFeUids(root);

        // Detect top-level FunctionDeclarations whose names are assigned anywhere
        // in the program (e.g. Babel helpers like _setPrototypeOf that memoize
        // themselves). These must be emitted as JsOwned variables + native FEs,
        // not as direct C++ functions, so the assignment can actually update them.
        std::set<std::string> assignedNames;
        scanAssignedNames(root, assignedNames);
        std::set<std::string> reassignableFds;
        for (const auto& ch : root.children)
            if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                std::string n = identName(ch.children[0]);
                if (assignedNames.count(n)) {
                    reassignableFds.insert(n);
                    // Queue as native FE so emitPendingFeBodies emits the body
                    if (!fe_uid_by_node_.count(&ch)) {
                        ++max_fe_uid_;
                        fe_uid_by_node_[&ch] = max_fe_uid_;
                        fe_emit_order_.push_back(&ch);
                    }
                }
            }

        std::ostringstream out;
        emitPrologueDefines(out);
        out << "/* Generated by Blue — JS-to-C++ compiler */\n";
        out << "#include \"js_value.hpp\"\n";
        out << "#include \"js_node.h\"\n";
        out << "#include \"js_globals.h\"\n";
        out << "#include <stdexcept>\n\n";
        injectJscGlobalSymbols();
        emitFeForwardDecls(out);
        emitJscGlobalsDecl(out);
        emitPendingHttpThunkDecls(out);

        // Emit reassignable FDs as file-scope JsOwned statics (like entry vars)
        for (const auto& n : reassignableFds)
            out << "static JsOwned " << sanitizeName(n) << ";\n";
        if (!reassignableFds.empty()) out << "\n";
        for (const auto& n : reassignableFds) {
            ownedLocals_.insert(n);
            fileScopeOwnedLocals_.insert(n);
        }

        for (const auto& child : root.children)
            if (child.type == "FunctionDeclaration") {
                std::string n = identName(child.children[0]);
                if (reassignableFds.count(n)) continue; // emitted via native FE
                emitFunction(child, out, 0, ""); out << "\n";
            }
        out << "int main(int argc, char** argv) {\n";
        out << "    js_process_init(argc, argv);\n";
        emitJscGlobalsBootstrap(out, 1);
        // Initialize reassignable FD variables from their native FE pointers
        for (const auto& ch : root.children)
            if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                std::string n = identName(ch.children[0]);
                if (!reassignableFds.count(n)) continue;
                auto it = fe_uid_by_node_.find(&ch);
                if (it != fe_uid_by_node_.end())
                    out << "    " << sanitizeName(n)
                        << " = JsOwned::from_value(js_native_fn(__jsc_fe_"
                        << it->second << "));\n";
            }
        ownedLocals_.clear();
        for (const auto& n : reassignableFds) ownedLocals_.insert(n);
        for (const auto& child : root.children)
            if (child.type != "FunctionDeclaration")
                emitStmt(child, out, 1);
        emitJscGlobalsEpilogue(out, 1);
        out << "    return 0;\n}\n";
        emitPendingHttpThunkBodies(out);
        emitPendingFeBodies(out);
        return out.str();
    }

    // ── Multi-module mode (CommonJS) ─────────────────────────────────────────
    std::string emitModules(const std::vector<Module>& modules,
                            bool hybridBuild = false) {
        allModules_ = &modules;
        clearModuleState();
        hybrid_build_ = hybridBuild;
        if (hybrid_build_) {
            needs_uv_      = true;
            needs_node_io_ = true;
            needs_quickjs_ = true;
        }
        for (const auto& mod : modules) {
            emitAsyncFeatureWarnings(mod.ast, mod.filePath);
            scanProgramForLowering(mod.ast);
        }

        for (const auto& mod : modules)
            collectBareRequireRuntimeFlags(mod.ast);
        for (const auto& mod : modules)
            collectHttpCreateServerHandlerIds(mod.ast);
        for (const auto& mod : modules)
            scanFeUids(mod.ast);

        // Pre-queue reassignable FunctionDeclarations in the entry module as
        // native FEs so their forward declarations are emitted correctly.
        {
            const ASTNode& entryAst = modules.back().ast;
            std::set<std::string> preAsgn;
            scanAssignedNames(entryAst, preAsgn);
            for (const auto& ch : entryAst.children)
                if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                    std::string n = identName(ch.children[0]);
                    if (preAsgn.count(n) && !fe_uid_by_node_.count(&ch)) {
                        ++max_fe_uid_;
                        fe_uid_by_node_[&ch] = max_fe_uid_;
                        fe_emit_order_.push_back(&ch);
                    }
                }
        }

        std::ostringstream out;
        emitPrologueDefines(out);
        out << "/* Generated by Blue — JS-to-C++ compiler */\n";
        out << "#include \"js_value.hpp\"\n";
        out << "#include \"js_node.h\"\n";
        out << "#include \"js_globals.h\"\n";
        if (hybrid_build_)
            out << "#include \"runtime/jsc_runtime.h\"\n";
        if (hybrid_build_)
            out << "#include <cstring>\n";
        out << "#include <stdexcept>\n\n";
        injectJscGlobalSymbols();
        emitFeForwardDecls(out);
        emitJscGlobalsDecl(out);

        if (modules.size() == 1) {
            clearModuleScopes();
            currentFilePath_ = modules[0].filePath;
            prePassRequires(modules[0].ast, modules[0].filePath);
            emitPendingHttpThunkDecls(out);

            /* Detect FunctionDeclarations whose names are assigned anywhere in
             * the program (e.g. Babel helpers like _setPrototypeOf). These must
             * be emitted as JsOwned variables + native FEs so reassignment works. */
            std::set<std::string> assignedNames;
            scanAssignedNames(modules[0].ast, assignedNames);
            std::set<std::string> reassignableFds;
            for (const auto& ch : modules[0].ast.children)
                if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                    std::string n = identName(ch.children[0]);
                    if (assignedNames.count(n) && fe_uid_by_node_.count(&ch))
                        reassignableFds.insert(n);
                }

            /* Collect entry-module variable names that top-level functions
             * reference.  These must be emitted as file-scope statics so
             * they are visible from the functions emitted before main(). */
            std::set<std::string> entryVarNames;
            for (const auto& child : modules[0].ast.children)
                if (child.type == "VariableDeclaration")
                    for (const auto& d : child.children)
                        if (d.type == "VariableDeclarator" && !d.children.empty() &&
                            d.children[0].type == "Identifier")
                            entryVarNames.insert(identName(d.children[0]));
            /* Also include reassignable FDs as file-scope JsOwned statics */
            for (const auto& n : reassignableFds)
                entryVarNames.insert(n);

            for (const auto& vn : entryVarNames)
                out << "static JsOwned " << sanitizeName(vn) << ";\n";
            if (!entryVarNames.empty()) out << "\n";
            for (const auto& vn : entryVarNames) {
                ownedLocals_.insert(vn);
                fileScopeOwnedLocals_.insert(vn);
            }
            for (const auto& child : modules[0].ast.children)
                if (child.type == "FunctionDeclaration") {
                    std::string n = identName(child.children[0]);
                    if (reassignableFds.count(n)) continue; // via native FE
                    emitFunction(child, out, 0, ""); out << "\n";
                }
            emitHybridAotDispatch(entryLikeModule(modules[0]), out);
            out << "int main(int argc, char** argv) {\n";
            out << "    js_process_init(argc, argv);\n";
            emitJscGlobalsBootstrap(out, 1);
            out << "    jsc_hybrid_set_aot_dispatch(__blue_hybrid_call_aot);\n";
            if (hybrid_build_) {
                out << "    jsc_hybrid_boot_island(jsc_hybrid_island_bundle, "
                       "jsc_hybrid_island_bundle_len);\n";
            }
            /* Initialize reassignable FD variables to their native FE functions */
            for (const auto& ch : modules[0].ast.children)
                if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                    std::string n = identName(ch.children[0]);
                    if (!reassignableFds.count(n)) continue;
                    auto it = fe_uid_by_node_.find(&ch);
                    if (it != fe_uid_by_node_.end())
                        out << "    " << sanitizeName(n)
                            << " = JsOwned::from_value(js_native_fn(__jsc_fe_"
                            << it->second << "));\n";
                }
            ownedLocals_.clear();
            for (const auto& vn : entryVarNames)
                ownedLocals_.insert(vn);
            for (const auto& child : modules[0].ast.children)
                if (child.type != "FunctionDeclaration")
                    emitStmt(child, out, 1);
            emitJscGlobalsEpilogue(out, 1);
            out << "    return 0;\n}\n";
            emitPendingHttpThunkBodies(out);
            emitPendingFeBodies(out);
            hybrid_build_ = false;
            allModules_ = nullptr;
            return out.str();
        }

        for (size_t i = 0; i + 1 < modules.size(); ++i)
            emitDepModule(modules[i], out);

        const Module& entry = modules.back();
        clearModuleScopes();
        currentFilePath_ = entry.filePath;
        prePassRequires(entry.ast, entry.filePath);
        injectJscGlobalSymbols();

        emitPendingHttpThunkDecls(out);

        // Reassignable FDs were pre-queued before forward decls; just identify them
        std::set<std::string> assignedNamesE;
        scanAssignedNames(entry.ast, assignedNamesE);
        std::set<std::string> reassignableFdsE;
        for (const auto& ch : entry.ast.children)
            if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                std::string n = identName(ch.children[0]);
                if (assignedNamesE.count(n) && fe_uid_by_node_.count(&ch))
                    reassignableFdsE.insert(n);
            }

        std::set<std::string> entryVarNames;
        for (const auto& child : entry.ast.children)
            if (child.type == "VariableDeclaration")
                for (const auto& d : child.children)
                    if (d.type == "VariableDeclarator" && !d.children.empty() &&
                        d.children[0].type == "Identifier")
                        entryVarNames.insert(identName(d.children[0]));
        for (const auto& n : reassignableFdsE) entryVarNames.insert(n);
        for (const auto& vn : entryVarNames)
            out << "static JsOwned " << sanitizeName(vn) << ";\n";
        if (!entryVarNames.empty()) out << "\n";
        for (const auto& vn : entryVarNames) {
            ownedLocals_.insert(vn);
            fileScopeOwnedLocals_.insert(vn);
        }
        for (const auto& child : entry.ast.children)
            if (child.type == "FunctionDeclaration") {
                std::string n = identName(child.children[0]);
                if (reassignableFdsE.count(n)) continue;
                emitFunction(child, out, 0, ""); out << "\n";
            }

        emitHybridAotDispatch(entry, out);
        out << "int main(int argc, char** argv) {\n";
        out << "    js_process_init(argc, argv);\n";
        emitJscGlobalsBootstrap(out, 1);
        out << "    jsc_hybrid_set_aot_dispatch(__blue_hybrid_call_aot);\n";
        if (hybrid_build_) {
            out << "    jsc_hybrid_boot_island(jsc_hybrid_island_bundle, "
                   "jsc_hybrid_island_bundle_len);\n";
        }
        for (const auto& ch : entry.ast.children)
            if (ch.type == "FunctionDeclaration" && !ch.children.empty()) {
                std::string n = identName(ch.children[0]);
                if (!reassignableFdsE.count(n)) continue;
                auto it = fe_uid_by_node_.find(&ch);
                if (it != fe_uid_by_node_.end())
                    out << "    " << sanitizeName(n)
                        << " = JsOwned::from_value(js_native_fn(__jsc_fe_"
                        << it->second << "));\n";
            }
        ownedLocals_.clear();
        for (const auto& vn : entryVarNames)
            ownedLocals_.insert(vn);
        for (const auto& child : entry.ast.children)
            if (child.type != "FunctionDeclaration")
                emitStmt(child, out, 1);
        emitJscGlobalsEpilogue(out, 1);
        out << "    return 0;\n}\n";
        emitPendingHttpThunkBodies(out);
        emitPendingFeBodies(out);
        hybrid_build_ = false;
        allModules_ = nullptr;
        return out.str();
    }

private:
    // ── State ─────────────────────────────────────────────────────────────────
    std::map<std::string, std::string> symbolTable_;
    // Maps local JS name → builtin module name (e.g. "fs" → "fs") for
    // variables obtained via `const fs = require('fs')`.  Used for
    // compile-time method-call dispatch so fs.readFileSync(…) → js_fs_readFileSync(…).
    std::map<std::string, std::string> builtinBindings_;
    // Local JS names that hold JsValue JS_FUNCTION values obtained from a
    // destructured builtin require (`const { readFileSync } = require('fs')`).
    // These must be called via js_call() rather than as plain C functions.
    std::set<std::string> jsValueBindings_;
    const std::vector<Module>* allModules_ = nullptr;
    std::string currentFilePath_;
    // Locals / params / destructured builtins use JsOwned in generated C++.
    std::set<std::string> ownedLocals_;
    std::set<std::string> fileScopeOwnedLocals_;
    bool hybrid_build_    = false;
    bool needs_quickjs_   = false;
    bool needs_uv_        = false;
    bool needs_node_io_   = false;
    bool needs_webview_   = false;
    int  jsc_method_uid_ = 0;
    std::set<std::string> pending_http_thunks_;
    /* FunctionExpression → stable __jsc_fe_<n> (pre-scan for forward decl order) */
    int                              max_fe_uid_ = 0;
    std::map<const ASTNode*, int>    fe_uid_by_node_;
    std::vector<const ASTNode*>      fe_emit_order_;
    int native_return_mode_ = 0; /* 0: return JsOwned; 1: return js_clone_value(...) */
    std::set<std::string> warnedUnsupported_;
    /* Maps nested-FD C++ name → list of captured JS variable names (free vars). */
    std::map<std::string, std::vector<std::string>> nestedFdCaptures_;

    void clearModuleScopes() {
        symbolTable_.clear();
        builtinBindings_.clear();
        jsValueBindings_.clear();
        ownedLocals_.clear();
        jsc_method_uid_ = 0;
    }

    void clearModuleState() {
        clearModuleScopes();
        fileScopeOwnedLocals_.clear();
        hybrid_build_    = false;
        needs_quickjs_ = false;
        needs_uv_      = false;
        needs_node_io_ = false;
        needs_webview_ = false;
        pending_http_thunks_.clear();
        max_fe_uid_      = 0;
        fe_uid_by_node_.clear();
        fe_emit_order_.clear();
        warnedUnsupported_.clear();
        nestedFdCaptures_.clear();
    }

    bool strictUnsupportedEnabled() const {
        const char* s = std::getenv("BLUE_STRICT_UNSUPPORTED");
        return s && s[0] != '\0' && std::strcmp(s, "0") != 0;
    }

    void diagUnsupported(const std::string& feature,
                         const std::string& detail) {
        std::string key = feature + "|" + detail;
        if (warnedUnsupported_.count(key))
            return;
        warnedUnsupported_.insert(key);
        if (strictUnsupportedEnabled()) {
            throw std::runtime_error(
                "❌ Blue Build Error: Unimplemented JavaScript Feature\n"
                "Feature: " + feature + "\n"
                "Detail: " + detail + "\n"
                "The Fix: Move this logic to src/island.js, or rewrite using "
                "supported static JavaScript.");
        }
        std::fprintf(stderr,
                     "⚠️ Blue Build Warning: Unimplemented JavaScript Feature\n"
                     "Feature: %s\n"
                     "Detail: %s\n"
                     "The Fix: Move this logic to src/island.js, or rewrite "
                     "using supported static JavaScript.\n",
                     feature.c_str(), detail.c_str());
    }

    const Module& entryLikeModule(const Module& only) const {
        return only;
    }

    /* Count the number of parameters a FunctionDeclaration has. */
    static int countFunctionParams(const ASTNode& fnNode) {
        int count = 0;
        size_t i = 0;
        if (functionAstHasBindingId(fnNode)) ++i; // skip name
        for (; i < fnNode.children.size(); ++i) {
            if (fnNode.children[i].type == "BlockStatement") break;
            ++count;
        }
        return count;
    }

    void emitHybridAotDispatch(const Module& entry, std::ostringstream& out) {
        struct FnInfo { std::string name; int paramCount; bool isVar; };
        std::vector<FnInfo> fnInfos;
        // Pre-scan to know which FDs are reassignable (Babel helpers, etc.)
        std::set<std::string> asgn;
        scanAssignedNames(entry.ast, asgn);

        for (const auto& child : entry.ast.children) {
            if (child.type == "FunctionDeclaration" && !child.children.empty()) {
                std::string name = child.children[0].data.value("name", "");
                // Skip underscore-prefixed helpers and reassignable Babel helpers
                if (name.empty() || (!name.empty() && name[0] == '_') || asgn.count(name))
                    continue;
                fnInfos.push_back({name, countFunctionParams(child), false});
            }
            if (child.type == "VariableDeclaration") {
                for (const auto& decl : child.children) {
                    if (decl.type != "VariableDeclarator" || decl.children.size() < 2)
                        continue;
                    const ASTNode& lhs = decl.children[0];
                    const ASTNode& rhs = decl.children[1];
                    if (lhs.type != "Identifier")
                        continue;
                    if (rhs.type == "FunctionExpression" ||
                        rhs.type == "ArrowFunctionExpression") {
                        std::string name = lhs.data.value("name", "");
                        if (!name.empty()) {
                            int pc = 0;
                            size_t ri = 0;
                            if (functionAstHasBindingId(rhs)) ++ri;
                            for (; ri < rhs.children.size(); ++ri) {
                                if (rhs.children[ri].type == "BlockStatement") break;
                                ++pc;
                            }
                            fnInfos.push_back({name, pc, true}); // stored in JsOwned var
                        }
                    }
                }
            }
        }
        bool anyHasParams = false;
        for (const auto& fi : fnInfos)
            if (fi.paramCount > 0) { anyHasParams = true; break; }

        out << "static JsValue __blue_hybrid_call_aot(const char* name, const char* json) {\n";
        out << "    if (!name || !*name) return JS_UNDEFINED();\n";
        if (fnInfos.empty() || !anyHasParams)
            out << "    (void)json;\n";
        if (anyHasParams)
            out << "    JsOwned __payload = JsOwned::from_value(JS_STR(json ? json : \"\"));\n";
        for (const auto& fi : fnInfos) {
            out << "    if (strcmp(name, \"" << fi.name << "\") == 0) {\n";
            std::string cn = sanitizeName(fi.name);
            if (fi.isVar) {
                // Variable holding a JS_FUNCTION — call via js_call()
                if (fi.paramCount > 0) {
                    out << "        JsOwned __ret = JsOwned::from_value(js_call("
                        << cn << ".borrow(), 1, __payload.borrow()));\n";
                } else {
                    out << "        JsOwned __ret = JsOwned::from_value(js_call("
                        << cn << ".borrow(), 0));\n";
                }
            } else {
                // Direct C++ function
                if (fi.paramCount > 0) {
                    out << "        JsOwned __ret = " << cn << "(__payload.clone());\n";
                } else {
                    out << "        JsOwned __ret = " << cn << "();\n";
                }
            }
            out << "        return js_clone_value(__ret.borrow());\n";
            out << "    }\n";
        }
        out << "    return JS_UNDEFINED();\n";
        out << "}\n\n";
    }

    void collectHttpCreateServerHandlerIds(const ASTNode& n) {
        if (n.type == "CallExpression" && !n.children.empty()) {
            const ASTNode& callee = n.children[0];
            if (callee.type == "MemberExpression" && callee.children.size() >= 2) {
                const ASTNode& obj  = callee.children[0];
                const ASTNode& prop = callee.children[1];
                if (obj.type == "Identifier" && identName(obj) == "http" &&
                    prop.type == "Identifier" && identName(prop) == "createServer" &&
                    n.children.size() >= 2) {
                    const ASTNode& arg0 = n.children[1];
                    if (arg0.type == "Identifier")
                        pending_http_thunks_.insert(identName(arg0));
                }
            }
        }
        for (const auto& ch : n.children)
            collectHttpCreateServerHandlerIds(ch);
    }

    void emitPendingHttpThunkDecls(std::ostringstream& out) {
        for (const auto& id : pending_http_thunks_) {
            const std::string th = "__jsc_http_th_" + sanitizeName(id);
            out << "static JsValue " << th << "(int argc, JsValue* argv);\n";
        }
        if (!pending_http_thunks_.empty())
            out << "\n";
    }

    void emitPendingHttpThunkBodies(std::ostringstream& out) {
        for (const auto& id : pending_http_thunks_) {
            const std::string th = "__jsc_http_th_" + sanitizeName(id);
            out << "static JsValue " << th << "(int argc, JsValue* argv) {\n"
                << "    JsOwned a = argc > 0 ? JsOwned::from_value(js_clone_value(argv[0])) : JsOwned();\n"
                << "    JsOwned b = argc > 1 ? JsOwned::from_value(js_clone_value(argv[1])) : JsOwned();\n"
                << "    (void)" << id << "(std::move(a), std::move(b));\n"
                << "    return js_undefined();\n"
                << "}\n\n";
        }
        pending_http_thunks_.clear();
    }

    void scanFeUids(const ASTNode& root) { scanFeUidsSubtree(root); }

    /* Collect identifiers in `node` that appear in `outerScope` but not `localScope`. */
    static void collectFreeVars(const ASTNode& node,
                                const std::set<std::string>& localScope,
                                const std::set<std::string>& outerScope,
                                std::vector<std::string>& freeVars,
                                std::set<std::string>& seen) {
        if (node.type == "Identifier") {
            std::string n = identName(node);
            if (!n.empty() && n != "<anon>" &&
                !localScope.count(n) && outerScope.count(n) && !seen.count(n)) {
                freeVars.push_back(n);
                seen.insert(n);
            }
        }
        for (const auto& ch : node.children)
            collectFreeVars(ch, localScope, outerScope, freeVars, seen);
    }

    /* Collect all Identifier names used as the direct LHS of an AssignmentExpression. */
    static void scanAssignedNames(const ASTNode& node, std::set<std::string>& out) {
        if (node.type == "AssignmentExpression" && node.children.size() >= 2) {
            const ASTNode& lhs = node.children[0];
            if (lhs.type == "Identifier") {
                std::string n = identName(lhs);
                if (!n.empty() && n != "<anon>") out.insert(n);
            }
        }
        for (const auto& ch : node.children) scanAssignedNames(ch, out);
    }

    void scanFeUidsSubtree(const ASTNode& n) {
        if (n.type == "FunctionExpression" ||
            n.type == "ArrowFunctionExpression") {
            if (!fe_uid_by_node_.count(&n)) {
                ++max_fe_uid_;
                fe_uid_by_node_[&n] = max_fe_uid_;
                fe_emit_order_.push_back(&n);
            }
        }
        for (const auto& ch : n.children) scanFeUidsSubtree(ch);
    }

    void emitFeForwardDecls(std::ostringstream& out) {
        for (int u = 1; u <= max_fe_uid_; ++u)
            out << "static JsValue __jsc_fe_" << u
                << "(int argc, JsValue* argv);\n";
        if (max_fe_uid_ > 0)
            out << "\n";
    }

    void emitPendingFeBodies(std::ostringstream& out) {
        for (const ASTNode* p : fe_emit_order_) {
            auto it = fe_uid_by_node_.find(p);
            if (it == fe_uid_by_node_.end())
                continue;
            emitNativeFeFunction(*p, "__jsc_fe_" + std::to_string(it->second), out);
        }
        fe_emit_order_.clear();
        fe_uid_by_node_.clear();
        max_fe_uid_ = 0;
    }

    static bool functionAstHasBindingId(const ASTNode& node) {
        if (node.type == "FunctionDeclaration")
            return true;
        return node.type == "FunctionExpression" &&
               node.data.contains("id") && node.data["id"].is_object();
    }

    static void extractFunctionSignature(const ASTNode& node, std::string* nameOut,
                                         std::vector<std::string>* params,
                                         const ASTNode** bodyOut) {
        nameOut->clear();
        params->clear();
        *bodyOut = nullptr;
        size_t i = 0;
        if (functionAstHasBindingId(node)) {
            if (i < node.children.size() && node.children[i].type == "Identifier")
                *nameOut = identName(node.children[i]);
            ++i;
        }
        for (; i < node.children.size(); ++i) {
            const ASTNode& p = node.children[i];
            if (p.type == "BlockStatement") {
                *bodyOut = &p;
                break;
            }
            // AssignmentPattern: `x = default` → extract name from left
            if (p.type == "AssignmentPattern" && !p.children.empty())
                params->push_back(identName(p.children[0]));
            // RestElement: `...rest` → extract name from argument
            else if (p.type == "RestElement" && !p.children.empty())
                params->push_back(identName(p.children[0]));
            // ObjectPattern / ArrayPattern param: name stays <anon> (handled at runtime)
            else
                params->push_back(identName(p));
        }
        // For ArrowFunctionExpression the body may be a plain expression (not BlockStatement)
        if (!*bodyOut && node.type == "ArrowFunctionExpression") {
            for (; i < node.children.size(); ++i) {
                if (node.children[i].type != "Identifier")
                    *bodyOut = &node.children[i];
            }
        }
    }

    void emitNativeFeFunction(const ASTNode& feNode, const std::string& qname,
                              std::ostringstream& out) {
        (void)feNode;  // accessed via extractFunctionSignature
        std::string bindName;
        std::vector<std::string> params;
        const ASTNode* body = nullptr;
        extractFunctionSignature(feNode, &bindName, &params, &body);
        (void)bindName;

        // Find the raw param nodes (past the optional id node) so we can detect
        // RestElement / AssignmentPattern.
        size_t paramStart = functionAstHasBindingId(feNode) ? 1 : 0;
        // For ArrowFunctionExpression there's no id node.
        std::vector<const ASTNode*> rawParams;
        for (size_t i = paramStart; i < feNode.children.size(); ++i) {
            const ASTNode& p = feNode.children[i];
            if (p.type == "BlockStatement") break;
            // Arrow: body may be an expression node — stop at non-param types
            if (feNode.type == "ArrowFunctionExpression" &&
                p.type != "Identifier" && p.type != "AssignmentPattern" &&
                p.type != "RestElement" && p.type != "ObjectPattern" &&
                p.type != "ArrayPattern")
                break;
            rawParams.push_back(&p);
        }

        // ── Hoist nested FunctionDeclarations BEFORE the outer function header ─
        // (nested fn-defs are illegal in C++; emit them as preceding siblings)
        // Pre-add params to ownedLocals_ so free-var collection sees them.
        for (size_t i = 0; i < rawParams.size(); ++i) {
            const ASTNode& rp = *rawParams[i];
            std::string n;
            if (rp.type == "RestElement" && !rp.children.empty())       n = identName(rp.children[0]);
            else if (rp.type == "AssignmentPattern" && !rp.children.empty()) n = identName(rp.children[0]);
            else if (rp.type == "Identifier") n = identName(rp);
            if (!n.empty()) ownedLocals_.insert(n);
        }

        struct NestedFn { std::string jsName; std::string cName; };
        std::vector<NestedFn> nestedFns;
        if (body && body->type == "BlockStatement") {
            int nestIdx = 0;
            for (const auto& s : body->children) {
                if (s.type != "FunctionDeclaration" || s.children.empty()) continue;
                std::string innerJs = identName(s.children[0]);
                std::string innerC  = qname + "_nd" + std::to_string(nestIdx++)
                                    + "_" + sanitizeName(innerJs);

                // Collect free variables: identifiers in the nested FD body that
                // belong to the outer FE's scope but are not the nested FD's own params.
                std::set<std::string> innerParams;
                for (size_t pi = 0; pi < s.children.size(); ++pi) {
                    const ASTNode& p = s.children[pi];
                    if (p.type == "BlockStatement") break;
                    if (p.type == "Identifier") innerParams.insert(identName(p));
                    else if (p.type == "AssignmentPattern" && !p.children.empty())
                        innerParams.insert(identName(p.children[0]));
                    else if (p.type == "RestElement" && !p.children.empty())
                        innerParams.insert(identName(p.children[0]));
                }
                // Combine inner params + already-declared hoisted inner fns so far
                for (const auto& nf2 : nestedFns) innerParams.insert(nf2.jsName);

                std::vector<std::string> freeVars;
                std::set<std::string> seen;
                for (size_t ci = 0; ci < s.children.size(); ++ci) {
                    if (s.children[ci].type == "BlockStatement") {
                        collectFreeVars(s.children[ci], innerParams, ownedLocals_,
                                        freeVars, seen);
                        break;
                    }
                }
                if (!freeVars.empty())
                    nestedFdCaptures_[innerC] = freeVars;

                emitNativeFeFunction(s, innerC, out); // emitted before outer header
                nestedFns.push_back({innerJs, innerC});
            }
        }

        out << "static JsValue " << qname << "(int argc, JsValue* argv) {\n";
        if (params.empty())
            out << "    (void)argc;\n"
                   "    (void)argv;\n";

        std::set<std::string> outerOwned = std::move(ownedLocals_);
        ownedLocals_.clear();

        for (size_t i = 0; i < rawParams.size(); ++i) {
            const ASTNode& rp = *rawParams[i];
            if (rp.type == "RestElement" && !rp.children.empty()) {
                // ...rest → collect remaining argv into an array object
                std::string rname = identName(rp.children[0]);
                out << "    JsOwned " << rname << " = JsOwned::from_value(js_obj_new());\n"
                    << "    { int __ri = 0; char __rk[24];\n"
                    << "      for (int __rj = " << i << "; __rj < argc; ++__rj) {\n"
                    << "        snprintf(__rk, 24, \"%d\", __ri++);\n"
                    << "        js_obj_set(" << rname << ".ptr(), __rk, js_clone_value(argv[__rj])); }\n"
                    << "      snprintf(__rk, 24, \"%d\", __ri);\n"
                    << "      js_obj_set(" << rname << ".ptr(), \"length\", JS_NUM(__ri)); }\n";
                ownedLocals_.insert(rname);
                break; // rest must be last
            }
            std::string pname = params[i];
            if (rp.type == "AssignmentPattern" && rp.children.size() >= 2) {
                // default param: use argv[i] unless undefined
                out << "    JsOwned " << pname << " = (argc > " << i
                    << " && argv[" << i << "].tag != JS_UNDEFINED) "
                    << "? JsOwned::from_value(js_clone_value(argv[" << i << "])) : "
                    << "JsOwned::from_value(";
                emitOwned(rp.children[1], out);
                out << ");\n";
            } else {
                out << "    JsOwned " << pname << " = argc > " << i
                    << " ? JsOwned::from_value(js_clone_value(argv[" << i << "])) : "
                       "JsOwned();\n";
            }
            ownedLocals_.insert(pname);
        }

        int oldNr = native_return_mode_;
        native_return_mode_ = 1;

        // Declare 'arguments' array so Babel helpers that reference it work
        out << "    JsOwned arguments = JsOwned::from_value(js_obj_new());\n"
               "    { char __ak[24]; for (int __ai = 0; __ai < argc; ++__ai) {\n"
               "        snprintf(__ak, 24, \"%d\", __ai);\n"
               "        js_obj_set(arguments.ptr(), __ak, js_clone_value(argv[__ai])); }\n"
               "      js_obj_set(arguments.ptr(), \"length\", JS_NUM((double)argc)); }\n";
        ownedLocals_.insert("arguments");

        // Emit closure captures and declare hoisted inner functions as JsOwned locals
        for (const auto& nf : nestedFns) {
            // Store captured outer-scope variables into the closure store
            auto capIt = nestedFdCaptures_.find(nf.cName);
            if (capIt != nestedFdCaptures_.end() && !capIt->second.empty()) {
                int nc = (int)capIt->second.size();
                out << "    { JsValue __caps[] = {";
                for (int ci = 0; ci < nc; ++ci) {
                    if (ci) out << ", ";
                    std::string cn = sanitizeName(capIt->second[(size_t)ci]);
                    if (isOwnedBinding(capIt->second[(size_t)ci]))
                        out << cn << ".borrow()";
                    else
                        out << resolve(capIt->second[(size_t)ci]);
                }
                out << "};\n      js_closure_set(" << nf.cName << ", " << nc
                    << ", __caps); }\n";
            }
            out << "    JsOwned " << sanitizeName(nf.jsName)
                << " = JsOwned::from_value(js_native_fn(" << nf.cName << "));\n";
            ownedLocals_.insert(nf.jsName);
        }

        // Inject reads for any captured variables this function itself needs
        {
            auto capIt = nestedFdCaptures_.find(qname);
            if (capIt != nestedFdCaptures_.end()) {
                for (int ci = 0; ci < (int)capIt->second.size(); ++ci) {
                    const std::string& capJsName = capIt->second[(size_t)ci];
                    std::string capCName = sanitizeName(capJsName);
                    out << "    JsOwned " << capCName
                        << " = JsOwned::from_value(js_closure_get("
                        << qname << ", " << ci << "));\n";
                    ownedLocals_.insert(capJsName);
                }
            }
        }

        if (body) {
            if (body->type == "BlockStatement") {
                for (const auto& s : body->children) {
                    if (s.type == "FunctionDeclaration") continue; // already hoisted
                    emitStmt(s, out, 1);
                }
            } else {
                // Arrow expression body: `x => x + 1` → return x + 1
                out << "    return js_clone_value(";
                emitValue(*body, out);
                out << ");\n";
            }
        }
        native_return_mode_ = oldNr;
        ownedLocals_ = std::move(outerOwned);

        bool endsRet = body && body->type == "BlockStatement" &&
                       !body->children.empty() &&
                       body->children.back().type == "ReturnStatement";
        if (!endsRet && !(body && body->type != "BlockStatement"))
            out << "    return js_clone_value(JS_UNDEFINED());\n";
        out << "}\n\n";
    }

    std::string queueFeNative(const ASTNode& feNode) {
        auto it = fe_uid_by_node_.find(&feNode);
        if (it == fe_uid_by_node_.end())
            throw std::runtime_error(
                "CEmitter: FunctionExpression not covered by scanFeUids (internal)");
        return "__jsc_fe_" + std::to_string(it->second);
    }

    void emitPrologueDefines(std::ostringstream& out) const {
        if (hybrid_build_)
            out << "#define JSC_HYBRID 1\n";
        if (!needs_quickjs_ && !needs_uv_ && !needs_node_io_ && !needs_webview_)
            return;
        if (needs_quickjs_)
            out << "#ifndef JSC_RUNTIME_QUICKJS\n#define JSC_RUNTIME_QUICKJS 1\n#endif\n";
        if (needs_uv_)
            out << "#ifndef JSC_RUNTIME_UV\n#define JSC_RUNTIME_UV 1\n#endif\n";
        if (needs_node_io_)
            out << "#ifndef JSC_RUNTIME_NODE_IO\n#define JSC_RUNTIME_NODE_IO 1\n#endif\n";
        if (needs_webview_)
            out << "#ifndef JSC_RUNTIME_WEBVIEW\n#define JSC_RUNTIME_WEBVIEW 1\n#endif\n";
        out << "\n";
    }

    void emitJscGlobalsDecl(std::ostringstream& out) {
        out << "\nstatic JsValue jsc_g_console;\n"
               "static JsValue jsc_g_process;\n"
               "static JsValue jsc_g_Buffer;\n"
               "static JsValue jsc_g_globalThis;\n"
               "static JsValue jsc_g_Blue;\n"
               "static JsValue jsc_g_Math;\n"
               "static JsValue jsc_g_JSON;\n"
               "static JsValue jsc_g_Object;\n"
               "static JsValue __jsc_this __attribute__((unused));\n";
    }

    void emitJscGlobalsBootstrap(std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');
        out << ind << "jsc_g_console = js_builtin_console();\n"
            << ind << "jsc_g_process = js_node_process();\n"
            << ind << "jsc_g_Buffer = js_builtin_buffer_namespace();\n"
            << ind
            << "jsc_g_globalThis = js_jsc_init_global_this(jsc_g_console, "
               "jsc_g_process, jsc_g_Buffer);\n"
            << ind << "{\n"
            << ind << "    JsValue mkey = JS_STR(\"Math\");\n"
            << ind << "    jsc_g_Math = js_obj_get(jsc_g_globalThis, mkey);\n"
            << ind << "    JsValue jkey = JS_STR(\"JSON\");\n"
            << ind << "    jsc_g_JSON = js_obj_get(jsc_g_globalThis, jkey);\n"
            << ind << "    JsValue okey = JS_STR(\"Object\");\n"
            << ind << "    jsc_g_Object = js_obj_get(jsc_g_globalThis, okey);\n"
            << ind << "    js_dispose_value(&mkey);\n"
            << ind << "    js_dispose_value(&jkey);\n"
            << ind << "    js_dispose_value(&okey);\n"
            << ind << "}\n";
        out << ind << "{\n"
            << ind << "    JsValue akey = JS_STR(\"Blue\");\n"
            << ind << "    jsc_g_Blue = js_obj_get(jsc_g_globalThis, akey);\n"
            << ind << "    js_dispose_value(&akey);\n"
            << ind << "}\n";
        (void)ind;
    }

    void emitJscGlobalsEpilogue(std::ostringstream& out, int depth) const {
        (void)depth;
        if (!needs_uv_) return;
        std::string ind(depth * 4, ' ');
        if (needs_node_io_)
            out << ind << "jsc_uv_run_loop_forever();\n";
        else
            out << ind << "jsc_uv_run_loop_until_idle();\n";
    }

    void injectJscGlobalSymbols() {
        /* Resolved via CEmitter::resolve() so dependency modules inherit names. */
    }

    void emitAsyncFeatureWarnings(const ASTNode& prog, const std::string& path) {
        for (const auto& ch : prog.children)
            warnAsyncSubtree(ch, path);
    }

    void warnAsyncSubtree(const ASTNode& node, const std::string& path) {
        if (node.type == "AwaitExpression") {
            std::fprintf(stderr,
                         "blue: warning (%s): async code (await / Promise chaining) "
                         "is only supported via QuickJS runtime islands in this toolchain; "
                         "static compilation skips async semantics.\n",
                         path.c_str());
            return;
        }
        /* Single-name scan for AsyncFunction variants */
        std::vector<std::string> bad = {"AsyncFunctionExpression",
                                         "AsyncFunctionDeclaration",
                                         "AsyncArrowFunctionExpression"};
        for (const auto& b : bad) {
            if (node.type == b) {
                std::fprintf(stderr,
                             "blue: warning (%s): `%s` is not statically compiled "
                             "(use QuickJS-supported patterns or refactor).\n",
                             path.c_str(), node.type.c_str());
                return;
            }
        }
        if (node.type == "Identifier") {
            std::string id = node.data.value("name", "");
            if (id == "Promise") {
                std::fprintf(stderr,
                             "blue: warning (%s): Promise/global async requires the "
                             "embedded QuickJS + libuv path; static codegen has no Promises.\n",
                             path.c_str());
                return;
            }
        }
        if (node.type == "UnaryExpression") {
            std::string op = node.data.value("operator", "");
            if (op == "typeof" && !node.children.empty() &&
                node.children[0].type == "Identifier") {
                std::string sub = node.children[0].data.value("name", "");
                if (sub == "Promise") {
                    std::fprintf(stderr,
                                 "blue: warning (%s): Promise/async detection at runtime "
                                 "is limited to QuickJS-hosted code.\n",
                                 path.c_str());
                    return;
                }
            }
        }
        for (const auto& c : node.children)
            warnAsyncSubtree(c, path);
    }

    /* Scan lowering sites (currently: eval / timers / constructors). */
    void scanProgramForLowering(const ASTNode& prog) { scanLoweringSubtree(prog); }

    void scanLoweringSubtree(const ASTNode& node) {
        if (node.type == "Identifier") {
            std::string n = node.data.value("name", "");
            if (n == "setTimeout" || n == "setInterval") needs_uv_ = true;
            if (n == "eval") needs_quickjs_ = true;
        }
        if (node.type == "CallExpression" && !node.children.empty()) {
            const ASTNode& c0 = node.children[0];
            if (identName(c0) == "eval") needs_quickjs_ = true;
            if (c0.type == "MemberExpression" && c0.children.size() >= 2) {
                std::string o = identName(c0.children[0]);
                std::string m = identName(c0.children[1]);
                if (m == "eval" || o == "eval") needs_quickjs_ = true;
                if (m == "open" &&
                    (o == "window" || o == "globalThis" || o == "global"))
                    needs_webview_ = true;
                if ((o == "globalThis" || o == "global") &&
                    (m == "setTimeout" || m == "setInterval"))
                    needs_uv_ = true;
            }
            if (c0.type == "Identifier" &&
                (identName(c0) == "setTimeout" || identName(c0) == "setInterval"))
                needs_uv_ = true;
        }
        /* `new Function` */
        if (node.type == "NewExpression" && node.children.empty() == false &&
            node.children[0].type == "Identifier" &&
            identName(node.children[0]) == "Function")
            needs_quickjs_ = true;
        for (const auto& c : node.children) scanLoweringSubtree(c);
    }

    bool nodeReturnsJsOwned(const ASTNode& node) const {
        if (node.type != "CallExpression" || node.children.empty())
            return false;
        const ASTNode& callee = node.children[0];
        if (callee.type != "Identifier")
            return true;
        std::string n = identName(callee);
        if (n == "String" || n == "Number" || n == "Boolean" || n == "eval" ||
            n == "Function" || n == "setTimeout" || n == "setInterval" ||
            n == "clearTimeout" || n == "clearInterval")
            return false;
        if (jsValueBindings_.count(n))
            return false;
        return true;
    }

    bool producesOwnedValue(const ASTNode& node) const {
        if (node.type == "Literal" ||
            node.type == "BinaryExpression" ||
            node.type == "UnaryExpression" ||
            node.type == "LogicalExpression" ||
            node.type == "ObjectExpression" ||
            node.type == "ArrayExpression" ||
            node.type == "TemplateLiteral" ||
            node.type == "MemberExpression" ||
            node.type == "NewExpression")
            return true;
        return false;
    }

    bool isOwnedBinding(const std::string& name) const {
        return ownedLocals_.count(name) || fileScopeOwnedLocals_.count(name) || jsValueBindings_.count(name);
    }

    // emitValue: Prints a JsValue (borrow). Managed by RAII temporaries if needed.
    void emitValue(const ASTNode& node, std::ostringstream& out) {
        if (node.type == "Identifier") {
            std::string n = identName(node);
            if (isOwnedBinding(n))
                out << sanitizeName(n) << ".borrow()";
            else
                out << "JsOwned::from_value(js_clone_value(" << resolve(n) << ")).borrow()";
        } else if (node.type == "AssignmentExpression") {
            // Assignment used as a value: (_ref = expr) → assign then borrow result
            if (node.children.size() >= 2) {
                const ASTNode& lhs = node.children[0];
                out << "([&]()->JsValue{ ";
                emitExpr(node, out);
                out << "; return ";
                if (lhs.type == "Identifier") {
                    std::string ln = identName(lhs);
                    out << (isOwnedBinding(ln) ? sanitizeName(ln) + ".borrow()"
                                               : resolve(ln));
                } else {
                    out << "JS_UNDEFINED()";
                }
                out << "; })()";
            } else {
                emitExpr(node, out);
            }
        } else if (node.type == "CallExpression") {
            if (nodeReturnsJsOwned(node)) {
                emitExpr(node, out);
            } else {
                out << "JsOwned::from_value(";
                emitExpr(node, out);
                out << ").borrow()";
            }
        } else if (producesOwnedValue(node)) {
            out << "JsOwned::from_value(";
            emitExpr(node, out);
            out << ").borrow()";
        } else {
            emitExpr(node, out);
        }
    }

    // emitOwned: Prints an OWNED JsValue. Suitable for storage (obj_set, assignment).
    void emitOwned(const ASTNode& node, std::ostringstream& out) {
        if (node.type == "Identifier") {
            std::string n = identName(node);
            if (isOwnedBinding(n))
                out << "js_clone_value(" << sanitizeName(n) << ".borrow())";
            else
                out << "js_clone_value(" << resolve(n) << ")";
        } else if (node.type == "CallExpression") {
            if (nodeReturnsJsOwned(node)) {
                out << "js_clone_value(";
                emitExpr(node, out);
                out << ")";
            } else {
                emitExpr(node, out);
            }
        } else if (producesOwnedValue(node)) {
            emitExpr(node, out);
        } else {
            out << "js_clone_value(";
            emitExpr(node, out);
            out << ")";
        }
    }

    void emitLvaluePtr(const ASTNode& node, std::ostringstream& out) {
        if (node.type == "Identifier") {
            std::string n = identName(node);
            if (isOwnedBinding(n)) {
                out << sanitizeName(n) << ".ptr()";
                return;
            }
            out << "&" << resolve(n);
            return;
        }
        emitExpr(node, out);
    }

    void emitCallArg(const ASTNode& node, std::ostringstream& out) {
        if (node.type == "Identifier") {
            std::string n = identName(node);
            if (isOwnedBinding(n)) {
                out << "JsOwned::from_borrowed(" << sanitizeName(n) << ".borrow())";
                return;
            }
            out << "JsOwned::from_borrowed(" << resolve(n) << ")";
            return;
        }
        if (node.type == "CallExpression") {
            if (nodeReturnsJsOwned(node)) {
                emitCallImpl(node, out, false); // No .borrow()
            } else {
                out << "JsOwned::from_value(";
                emitExpr(node, out);
                out << ")";
            }
            return;
        }
        out << "JsOwned::from_value(";
        emitExpr(node, out);
        out << ")";
    }

    // ── Dep-module emission ───────────────────────────────────────────────────

    void emitDepModule(const Module& mod, std::ostringstream& out) {
        out << "/* === module: " << mod.filePath << " === */\n\n";
        clearModuleScopes();
        currentFilePath_ = mod.filePath;

        // Register own functions so intra-module calls resolve correctly
        for (const auto& child : mod.ast.children)
            if (child.type == "FunctionDeclaration" && !child.children.empty()) {
                std::string n = child.children[0].data.value("name", "");
                if (!n.empty()) symbolTable_[n] = mod.cPrefix + "_" + n;
            }
        // Register module-level JS variables (non-require) so they resolve
        for (const auto& child : mod.ast.children)
            if (child.type == "VariableDeclaration")
                for (const auto& decl : child.children)
                    if (decl.type == "VariableDeclarator" && !decl.children.empty() &&
                        decl.children[0].type == "Identifier" &&
                        (decl.children.size() < 2 || !isRequireCall(decl.children[1])))
                    {
                        std::string n = identName(decl.children[0]);
                        symbolTable_[n] = mod.cPrefix + "_" + n;
                    }
        // Resolve any require() calls this module makes to other modules
        prePassRequires(mod.ast, mod.filePath);

        // ── Part 1: static JsValue declarations ─────────────────────────────
        // Always emit the module's default exports slot
        out << "static JsValue " << mod.cPrefix << "_exports;\n";

        // Emit a static JsValue for each value export (not backed by a fn)
        // Collect already-declared local-var cNames to avoid duplicates
        std::set<std::string> localVarCNames;
        for (const auto& child : mod.ast.children)
            if (child.type == "VariableDeclaration")
                for (const auto& decl : child.children)
                    if (decl.type == "VariableDeclarator" && !decl.children.empty() &&
                        decl.children[0].type == "Identifier" &&
                        (decl.children.size() < 2 || !isRequireCall(decl.children[1])))
                    {
                        std::string cName = resolve(identName(decl.children[0]));
                        out << "static JsValue " << cName << ";\n";
                        localVarCNames.insert(cName);
                    }

        // Named exports that are value exports (not C functions, not local vars)
        for (const auto& kv : mod.exports) {
            if (kv.first == "default") continue;
            const std::string& cName = kv.second;
            if (mod.functionCNames.count(cName)) continue;  // C function
            if (localVarCNames.count(cName))      continue;  // already declared
            out << "static JsValue " << cName << ";\n";
        }
        out << "\n";

        // ── Part 2: static function implementations ──────────────────────────
        for (const auto& child : mod.ast.children)
            if (child.type == "FunctionDeclaration") {
                emitFunction(child, out, 0, mod.cPrefix); out << "\n";
            }

        // ── Part 3: init function ────────────────────────────────────────────
        out << "static void " << mod.cPrefix << "_init(void) {\n";
        out << "    static int _done = 0; if (_done) return; _done = 1;\n";

        for (const auto& child : mod.ast.children) {
            if (child.type == "FunctionDeclaration") {
                // already emitted above as a static C function
            } else if (child.type == "VariableDeclaration") {
                // Emit initializer assignments (static vars declared above)
                for (const auto& decl : child.children)
                    emitModuleVarInit(decl, out);
            } else if (child.type == "ExpressionStatement" &&
                       !child.children.empty()) {
                const ASTNode& expr = child.children[0];
                if (expr.type == "AssignmentExpression" &&
                    expr.children.size() >= 2) {
                    const ASTNode& lhs = expr.children[0];
                    const ASTNode& rhs = expr.children[1];
                    if (isModuleExprProp(lhs, "module", "exports")) {
                        // module.exports = <expr>
                        // If RHS is a function identifier, skip the JsValue
                        // assignment — C functions can't be stored in JsValue,
                        // and the symbol table already routes calls correctly.
                        bool skipFnAssign = false;
                        if (rhs.type == "Identifier") {
                            std::string cName = mod.cPrefix + "_" +
                                                rhs.data.value("name", "");
                            skipFnAssign = mod.functionCNames.count(cName) > 0;
                        }
                        if (!skipFnAssign) {
                            out << "    " << mod.cPrefix << "_exports = ";
                            emitExpr(rhs, out);
                            out << ";\n";
                        }
                    } else if (isModuleExportsDotProp(lhs)) {
                        // module.exports.key = <expr>
                        std::string key = memberPropName(lhs);
                        auto it = mod.exports.find(key);
                        if (it != mod.exports.end()) {
                            if (!mod.functionCNames.count(it->second)) {
                                // Value export — assign the static JsValue var
                                out << "    " << it->second << " = ";
                                emitExpr(rhs, out);
                                out << ";\n";
                            }
                            // Function export — no assignment needed (C fn already defined)
                        }
                    } else {
                        emitStmt(child, out, 1);
                    }
                } else {
                    emitStmt(child, out, 1);
                }
            } else {
                emitStmt(child, out, 1);
            }
        }
        out << "}\n\n";
    }

    // Emit a variable initializer inside a module's _init() function.
    // Emits:  <prefixed_cname> = <expr>;  (no JsValue prefix — already declared)
    void emitModuleVarInit(const ASTNode& decl, std::ostringstream& out) {
        if (decl.type != "VariableDeclarator" || decl.children.empty()) return;
        const ASTNode& id = decl.children[0];
        if (id.type != "Identifier") return;

        std::string jsName = identName(id);
        std::string cName  = resolve(jsName);

        if (decl.children.size() >= 2 && isRequireCall(decl.children[1])) {
            std::string req = requireArg(decl.children[1]);
            if (isRelativeRequire(req)) {
                std::string resolved = ModuleResolver::resolvePath(currentFilePath_, req);
                const Module* dep = findModule(resolved);
                if (dep) out << "    " << dep->cPrefix << "_init();\n";
                out << "    /* " << jsName << " = require('" << escapeString(req)
                    << "') resolved */\n";
            } else {
                out << "    " << cName << " = ";
                emitBareRequireExpr(req, out);
                out << ";\n";
            }
            return;
        }
        out << "    " << cName << " = ";
        if (decl.children.size() >= 2) emitExpr(decl.children[1], out);
        else                            out << "JS_UNDEFINED()";
        out << ";\n";
    }

    // ── Pre-pass: build symbol table from require() calls ─────────────────────

    void prePassRequires(const ASTNode& ast, const std::string& fromFile) {
        for (const auto& stmt : ast.children) {
            if (stmt.type != "VariableDeclaration") continue;
            for (const auto& decl : stmt.children)
                resolveRequireDecl(decl, fromFile);
        }
    }

    void collectBareRequireRuntimeFlags(const ASTNode& ast) {
        for (const auto& stmt : ast.children) {
            if (stmt.type != "VariableDeclaration")
                continue;
            for (const auto& decl : stmt.children) {
                if (decl.type != "VariableDeclarator" || decl.children.size() < 2)
                    continue;
                const ASTNode& init = decl.children[1];
                if (!isRequireCall(init))
                    continue;
                std::string reqStr = requireArg(init);
                if (!isRelativeRequire(reqStr) &&
                    (reqStr == "http" || reqStr == "https" || reqStr == "net" ||
                     reqStr == "stream" || reqStr == "events" || reqStr == "crypto")) {
                    needs_uv_      = true;
                    needs_node_io_ = true;
                }
            }
        }
    }

    void resolveRequireDecl(const ASTNode& decl, const std::string& fromFile) {
        if (decl.type != "VariableDeclarator" || decl.children.size() < 2) return;
        const ASTNode& id   = decl.children[0];
        const ASTNode& init = decl.children[1];
        if (!isRequireCall(init)) return;
        std::string reqStr = requireArg(init);

        // ── Bare (non-relative) require ──────────────────────────────────────
        if (!isRelativeRequire(reqStr)) {
            if (reqStr == "http" || reqStr == "https" || reqStr == "net" ||
                reqStr == "stream" || reqStr == "events" || reqStr == "crypto") {
                needs_uv_      = true;
                needs_node_io_ = true;
            }
            if (id.type == "Identifier") {
                // `const fs = require('fs')` — track so member calls can be
                // dispatched at compile time (e.g. fs.readFileSync → js_fs_readFileSync).
                builtinBindings_[identName(id)] = reqStr;
            } else if (id.type == "ObjectPattern") {
                // `const { readFileSync } = require('fs')` — each extracted
                // name holds a JsValue JS_FUNCTION; must be called via js_call().
                for (const auto& prop : id.children) {
                    if (prop.children.empty()) continue;
                    std::string localName = identName(
                        prop.children.size() >= 2 ? prop.children[1] : prop.children[0]);
                    jsValueBindings_.insert(localName);
                }
            }
            return;
        }

        // ── Relative require ─────────────────────────────────────────────────
        std::string resolved = ModuleResolver::resolvePath(fromFile, reqStr);
        const Module* mod    = findModule(resolved);
        if (!mod) return;

        if (id.type == "ObjectPattern") {
            for (const auto& prop : id.children) {
                if (prop.type != "Property" || prop.children.empty()) continue;
                std::string key = identName(prop.children[0]);
                std::string localName = (prop.children.size() >= 2)
                    ? identName(prop.children[1]) : key;
                auto it = mod->exports.find(key);
                if (it != mod->exports.end())
                    symbolTable_[localName] = it->second;
            }
        } else if (id.type == "Identifier") {
            // For non-destructured require, pre-pass populates symbol table
            // only for single-identifier default exports (module.exports = fn).
            // Literal / object exports are handled at emit time via _exports slot.
            std::string localName = identName(id);
            auto def = mod->exports.find("default");
            if (def != mod->exports.end() &&
                mod->functionCNames.count(def->second))
            {
                // It's a function — map the local name directly to the C fn
                symbolTable_[localName] = def->second;
            }
            // Otherwise leave unresolved; emitVarDecl will use _exports slot.
        }
    }

    // ── Identifier resolution ─────────────────────────────────────────────────
    std::string resolve(const std::string& name) const {
        auto it = symbolTable_.find(name);
        if (it != symbolTable_.end()) return it->second;
        if (name == "console") return "jsc_g_console";
        if (name == "process") return "jsc_g_process";
        if (name == "Buffer") return "jsc_g_Buffer";
        if (name == "Blue") return "jsc_g_Blue";
        if (name == "Math")   return "jsc_g_Math";
        if (name == "JSON")   return "jsc_g_JSON";
        if (name == "Object") return "jsc_g_Object";
        if (name == "global" || name == "globalThis") return "jsc_g_globalThis";
        return sanitizeName(name);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    // Emit the value expression for a bare (non-relative) require(name).
    // Known Node.js builtins → js_node_require(JS_STR("name")) which returns
    // a real JsValue property-bag object.
    // Unknown packages → undeclared-function reference so the C compiler catches it.
    void emitBareRequireExpr(const std::string& req, std::ostringstream& out) {
        if (ModuleResolver::isBuiltin(req)) {
            out << "js_node_require(JS_STR(\"" << escapeString(req) << "\"))";
        } else {
            // Warn at code-gen time; emit undefined so the C compiler also
            // sees the error via an undeclared-function reference.
            std::fprintf(stderr,
                "blue: warning: require('%s') is not a known Node.js builtin "
                "and was not resolved to a file — emitting undefined.\n",
                req.c_str());
            out << "js_require_unsupported_" << sanitizeName(req)
                << "() /* unsupported package: " << escapeString(req) << " */";
        }
    }

    // Sanitize an arbitrary string into a valid C identifier fragment.
    static bool isCxxKeyword(const std::string& s) {
        static const char* kw[] = {
            "alignas","alignof","and","and_eq","asm","auto","bitand","bitor",
            "bool","break","case","catch","char","char8_t","char16_t","char32_t",
            "class","compl","concept","const","consteval","constexpr","constinit",
            "const_cast","continue","co_await","co_return","co_yield","decltype",
            "default","delete","do","double","dynamic_cast","else","enum","explicit",
            "export","extern","false","float","for","friend","goto","if","inline",
            "int","long","mutable","namespace","new","noexcept","not","not_eq",
            "nullptr","operator","or","or_eq","private","protected","public",
            "register","reinterpret_cast","requires","return","short","signed",
            "sizeof","static","static_assert","static_cast","struct","switch",
            "template","this","thread_local","throw","true","try","typedef",
            "typeid","typename","union","unsigned","using","virtual","void",
            "volatile","wchar_t","while","xor","xor_eq", nullptr
        };
        for (int i = 0; kw[i]; ++i)
            if (s == kw[i]) return true;
        return false;
    }

    static std::string sanitizeName(const std::string& s) {
        std::string r;
        r.reserve(s.size());
        for (char c : s)
            r += (std::isalnum(static_cast<unsigned char>(c)) ? c : '_');
        if (isCxxKeyword(r)) r += "_js";
        return r;
    }

    const Module* findModule(const std::string& path) const {
        if (!allModules_) return nullptr;
        for (const auto& m : *allModules_)
            if (m.filePath == path) return &m;
        return nullptr;
    }

    static bool isRequireCall(const ASTNode& node) {
        if (node.type != "CallExpression" || node.children.empty()) return false;
        const ASTNode& callee = node.children[0];
        return callee.type == "Identifier" &&
               callee.data.value("name", "") == "require";
    }

    static std::string requireArg(const ASTNode& call) {
        if (call.children.size() < 2) return "";
        const ASTNode& arg = call.children[1];
        if (arg.type == "Literal" && arg.data.contains("value")) {
            if (arg.data["value"].is_string())
                return arg.data["value"].get<std::string>();
            if (arg.data["value"].is_number()) {
                double d = arg.data["value"].get<double>();
                if (d == std::floor(d) && d >= 0 && d < 1e15)
                    return std::to_string(static_cast<long long>(d));
                return arg.data["value"].dump();
            }
            if (arg.data["value"].is_boolean())
                return arg.data["value"].get<bool>() ? "true" : "false";
        }
        return "";
    }

    static bool isRelativeRequire(const std::string& s) {
        return s.size() >= 2 &&
               (s.substr(0, 2) == "./" || s.substr(0, 3) == "../");
    }

    // True if node is `obj.prop` (MemberExpression with Identifier children)
    static bool isModuleExprProp(const ASTNode& node,
                                  const std::string& obj,
                                  const std::string& prop) {
        if (node.type != "MemberExpression" || node.children.size() < 2) return false;
        return node.children[0].data.value("name", "") == obj &&
               node.children[1].data.value("name", "") == prop;
    }

    // True if node is `module.exports.key`
    static bool isModuleExportsDotProp(const ASTNode& node) {
        if (node.type != "MemberExpression" || node.children.size() < 2) return false;
        return isModuleExprProp(node.children[0], "module", "exports");
    }

    // Extract the property name from `module.exports.key`
    static std::string memberPropName(const ASTNode& node) {
        if (node.children.size() < 2) return "";
        const ASTNode& prop = node.children[1];
        if (prop.type == "Identifier") return prop.data.value("name", "");
        if (prop.type == "Literal" && prop.data.contains("value") &&
            prop.data["value"].is_string())
            return prop.data["value"].get<std::string>();
        return "";
    }

    static bool isModuleExportsStmt(const ASTNode& node) {
        if (node.type != "ExpressionStatement" || node.children.empty()) return false;
        const ASTNode& expr = node.children[0];
        if (expr.type != "AssignmentExpression" || expr.children.empty()) return false;
        const ASTNode& lhs = expr.children[0];
        if (isModuleExprProp(lhs, "module", "exports")) return true;
        if (isModuleExportsDotProp(lhs)) return true;
        return false;
    }

    // ── Statements ─────────────────────────────────────────────────────────────

    static bool switchCaseIsDefault(const ASTNode& sc) {
        if (sc.type != "SwitchCase")
            return false;
        return !sc.data.contains("test") || sc.data["test"].is_null();
    }

    void emitStmt(const ASTNode& node, std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');

        // In the entry module, suppress module.exports assignments
        if (isModuleExportsStmt(node)) {
            out << ind << "/* module.exports — handled by compiler */\n";
            return;
        }

        if (node.type == "VariableDeclaration") {
            for (const auto& decl : node.children)
                emitVarDecl(decl, out, depth);
        } else if (node.type == "ExpressionStatement") {
            if (!node.children.empty()) {
                const ASTNode& expr = node.children[0];
                if (producesOwnedValue(expr)) {
                    out << ind << "{ JsValue __v = ";
                    emitExpr(expr, out);
                    out << "; js_dispose_value(&__v); }\n";
                } else {
                    out << ind;
                    emitExpr(expr, out);
                    out << ";\n";
                }
            }
        } else if (node.type == "IfStatement") {
            out << ind << "if (js_truthy(";
            emitValue(node.children[0], out);
            out << ")) ";
            emitBlock(node.children[1], out, depth);
            if (node.children.size() >= 3) {
                out << ind << "else ";
                emitBlock(node.children[2], out, depth);
            }
        } else if (node.type == "WhileStatement") {
            out << ind << "while (js_truthy(";
            emitValue(node.children[0], out);
            out << ")) ";
            emitBlock(node.children[1], out, depth);
        } else if (node.type == "DoWhileStatement") {
            out << ind << "do ";
            emitBlock(node.children[0], out, depth);
            out << ind << "while (js_truthy(";
            emitValue(node.children[1], out);
            out << "));\n";
        } else if (node.type == "ForStatement") {
            emitFor(node, out, depth);
        } else if (node.type == "ReturnStatement") {
            out << ind << "return";
            if (native_return_mode_) {
                out << " js_clone_value(";
                if (!node.children.empty())
                    emitValue(node.children[0], out);
                else
                    out << "JS_UNDEFINED()";
                out << ")";
            } else if (!node.children.empty()) {
                const ASTNode& rv = node.children[0];
                if (producesOwnedValue(rv)) {
                    out << " JsOwned::from_value(";
                    emitExpr(rv, out);
                    out << ")";
                } else if (rv.type == "Identifier") {
                    out << " " << sanitizeName(identName(rv)) << ".clone()";
                } else {
                    out << " JsOwned::from_value(js_clone_value(";
                    emitExpr(rv, out);
                    out << "))";
                }
            } else {
                out << " JsOwned()";
            }
            out << ";\n";
        } else if (node.type == "ThrowStatement") {
            out << ind << "{ char* __jt = js_to_cstr(";
            if (!node.children.empty())
                emitValue(node.children[0], out);
            else
                out << "JS_UNDEFINED()";
            out << "); std::runtime_error __jr(__jt ? __jt : \"\"); "
                   "if (__jt) free(__jt); throw __jr; }\n";
        } else if (node.type == "TryStatement") {
            if (node.children.empty()) {
                out << ind << "/* malformed TryStatement */\n";
                return;
            }
            const ASTNode& tryBlk = node.children[0];
            size_t ix = 1;
            const ASTNode* catchClause  = nullptr;
            const ASTNode* finallyBlock = nullptr;
            if (ix < node.children.size()) {
                if (node.children[ix].type == "CatchClause") {
                    catchClause = &node.children[ix++];
                } else if (node.children[ix].type == "BlockStatement") {
                    finallyBlock = &node.children[ix++]; // try/finally (no catch)
                }
            }
            if (ix < node.children.size() &&
                node.children[ix].type == "BlockStatement")
                finallyBlock = &node.children[ix];

            auto emitTryBody = [&](int d) {
                if (tryBlk.type == "BlockStatement")
                    for (const auto& s : tryBlk.children) emitStmt(s, out, d);
                else emitStmt(tryBlk, out, d);
            };
            auto emitCatch = [&](int d) {
                if (!catchClause) return;
                std::string ci(d * 4, ' ');
                out << ci << "catch (const std::exception& __jsc_ex) {\n";
                size_t bi = 0;
                if (!catchClause->children.empty() &&
                    catchClause->children[0].type == "Identifier") {
                    std::string pn = identName(catchClause->children[0]);
                    out << ci << "    const char* __jw = __jsc_ex.what();\n";
                    out << ci << "    JsOwned " << sanitizeName(pn)
                        << " = JsOwned::from_value(js_str(__jw ? __jw : \"\"));\n";
                    ownedLocals_.insert(pn);
                    bi = 1;
                }
                if (catchClause->children.size() > bi &&
                    catchClause->children[bi].type == "BlockStatement")
                    for (const auto& s : catchClause->children[bi].children)
                        emitStmt(s, out, d + 1);
                out << ci << "}\n";
            };

            if (!finallyBlock) {
                out << ind << "try {\n";
                emitTryBody(depth + 1);
                out << ind << "}\n";
                emitCatch(depth);
            } else {
                // Wrap the whole construct in a block so the RAII guard destructs
                // immediately after the try/catch completes (not at outer scope end).
                int uid = jsc_method_uid_++;
                std::string finLambda = "__fin_" + std::to_string(uid);
                out << ind << "{\n";
                out << ind << "    auto " << finLambda << " = [&]() noexcept {\n";
                for (const auto& s : finallyBlock->children)
                    emitStmt(s, out, depth + 2);
                out << ind << "    };\n";
                out << ind << "    struct __FinGuard_" << uid << " {\n";
                out << ind << "        decltype(" << finLambda << ")& fn;\n";
                out << ind << "        ~__FinGuard_" << uid << "() { fn(); }\n";
                out << ind << "    } __fg_" << uid << "{" << finLambda << "};\n";
                out << ind << "    try {\n";
                if (catchClause) {
                    out << ind << "        try {\n";
                    emitTryBody(depth + 3);
                    out << ind << "        }\n";
                    emitCatch(depth + 2);
                } else {
                    emitTryBody(depth + 2);
                }
                out << ind << "    } catch (...) { throw; }\n";
                out << ind << "}\n";
            }
        } else if (node.type == "SwitchStatement") {
            if (node.children.empty()) {
                out << ind << "/* malformed SwitchStatement */\n";
                return;
            }
            out << ind << "{\n";
            out << ind << "    JsOwned __sw = JsOwned::from_value(js_clone_value(";
            emitValue(node.children[0], out);
            out << "));\n";
            bool firstBranch = true;
            for (size_t ci = 1; ci < node.children.size(); ++ci) {
                const ASTNode& sc = node.children[ci];
                if (sc.type != "SwitchCase")
                    continue;
                if (switchCaseIsDefault(sc)) {
                    out << ind << "    "
                        << (firstBranch ? "if (js_truthy(JS_BOOL(1))) " : "else ")
                        << "{\n";
                    firstBranch = false;
                    for (const auto& s : sc.children) emitStmt(s, out, depth + 2);
                    out << ind << "    }\n";
                    continue;
                }
                if (sc.children.empty())
                    continue;
                const ASTNode& tst = sc.children[0];
                out << ind << "    " << (firstBranch ? "if" : "else if")
                    << " (js_truthy(js_eq(__sw.borrow(), ";
                emitValue(tst, out);
                out << "))) {\n";
                firstBranch = false;
                for (size_t si = 1; si < sc.children.size(); ++si)
                    emitStmt(sc.children[si], out, depth + 2);
                out << ind << "    }\n";
            }
            out << ind << "}\n";
        } else if (node.type == "ForOfStatement") {
            emitForOf(node, out, depth);
        } else if (node.type == "ForInStatement") {
            emitForIn(node, out, depth);
        } else if (node.type == "BreakStatement") {
            if (!node.children.empty()) {
                std::string lbl = identName(node.children[0]);
                out << ind << "goto __break_" << sanitizeName(lbl) << ";\n";
            } else {
                out << ind << "break;\n";
            }
        } else if (node.type == "ContinueStatement") {
            if (!node.children.empty()) {
                std::string lbl = identName(node.children[0]);
                out << ind << "goto __continue_" << sanitizeName(lbl) << ";\n";
            } else {
                out << ind << "continue;\n";
            }
        } else if (node.type == "LabeledStatement") {
            // JS labels map to C++ goto targets placed after the labelled loop.
            // Emit the body, then place the break/continue targets.
            if (node.children.size() >= 2) {
                std::string lbl = identName(node.children[0]);
                std::string clbl = sanitizeName(lbl);
                emitStmt(node.children[1], out, depth);
                out << ind << "__continue_" << clbl << ": ;\n";
                out << ind << "__break_" << clbl << ": ;\n";
            }
        } else if (node.type == "EmptyStatement") {
            (void)ind;
        } else if (node.type == "FunctionDeclaration") {
            diagUnsupported(
                "nested FunctionDeclaration",
                "Nested FunctionDeclaration outside a function body is not hoisted.");
            out << ind << "/* unsupported stmt: nested FunctionDeclaration outside "
                   "a function body (not hoisted here) */\n";
        } else if (node.type == "BlockStatement") {
            for (const auto& s : node.children) emitStmt(s, out, depth);
        } else {
            diagUnsupported(node.type, "Statement is emitted as no-op stub.");
            out << ind << "/* unsupported stmt: " << node.type << " */\n";
        }
    }

    void emitBlock(const ASTNode& node, std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');
        out << "{\n";
        if (node.type == "BlockStatement")
            for (const auto& s : node.children) emitStmt(s, out, depth + 1);
        else
            emitStmt(node, out, depth + 1);
        out << ind << "}\n";
    }

    // ── Variable declarator ─────────────────────────────────────────────────

    void emitVarDecl(const ASTNode& decl, std::ostringstream& out, int depth,
                     bool withType = true, bool withSemi = true) {
        std::string ind(depth * 4, ' ');
        if (decl.type != "VariableDeclarator") return;
        if (decl.children.empty()) return;

        const ASTNode& id = decl.children[0];

        // ── ObjectPattern destructuring ──────────────────────────────────────
        if (id.type == "ObjectPattern") {
            if (decl.children.size() >= 2 && isRequireCall(decl.children[1])) {
                std::string req = requireArg(decl.children[1]);
                if (isRelativeRequire(req)) {
                    std::string resolved =
                        ModuleResolver::resolvePath(currentFilePath_, req);
                    const Module* dep = findModule(resolved);
                    if (dep)
                        out << ind << dep->cPrefix << "_init();"
                            << " /* execute module: " << escapeString(req) << " */\n";
                    out << ind << "/* require('" << escapeString(req)
                        << "') resolved at compile time */\n";
                    return;
                }
                for (const auto& prop : id.children) {
                    if (prop.children.empty()) continue;
                    std::string key = identName(prop.children[0]);
                    std::string localName = identName(
                        prop.children.size() >= 2 ? prop.children[1] : prop.children[0]);
                    if (withType) out << ind << "JsOwned ";
                    out << localName << " = JsOwned::from_value(js_obj_get(";
                    emitBareRequireExpr(req, out);
                    out << ", JS_STR(\"" << escapeString(key) << "\")))";
                    if (withSemi) out << ";\n";
                }
                return;
            }
            // General ObjectPattern: const { a, b: c } = expr
            if (decl.children.size() >= 2) {
                int uid = jsc_method_uid_++;
                std::string src = "__dp_" + std::to_string(uid);
                out << ind << "JsOwned " << src << " = JsOwned::from_value(";
                emitOwned(decl.children[1], out);
                out << ");\n";
                for (const auto& prop : id.children) {
                    if (prop.type == "RestElement" && !prop.children.empty()) {
                        // { ...rest } — collect remaining keys into a new object
                        // (simplified: emit all props as rest; full exclusion tracking omitted)
                        std::string rname = identName(prop.children[0]);
                        if (withType) out << ind << "JsOwned ";
                        out << rname << " = JsOwned::from_value(js_clone_value(" << src << ".borrow()))";
                        if (withSemi) out << ";\n";
                        ownedLocals_.insert(rname);
                        continue;
                    }
                    if (prop.children.empty()) continue;
                    std::string key = identName(prop.children[0]);
                    std::string lname, defaultSuffix;
                    const ASTNode* valNode = prop.children.size() >= 2 ? &prop.children[1] : &prop.children[0];
                    if (valNode->type == "AssignmentPattern" && valNode->children.size() >= 2) {
                        lname = identName(valNode->children[0]);
                        // default value handled below
                    } else {
                        lname = identName(*valNode);
                    }
                    if (withType) out << ind << "JsOwned ";
                    out << lname << " = JsOwned::from_value(js_obj_get_q(" << src << ".borrow(), \""
                        << escapeString(key) << "\"))";
                    if (withSemi) out << ";\n";
                    ownedLocals_.insert(lname);
                    // Apply default if the extracted value is undefined
                    if (valNode->type == "AssignmentPattern" && valNode->children.size() >= 2) {
                        out << ind << "if (" << lname << ".borrow().tag == JS_UNDEFINED) { "
                            << lname << " = JsOwned::from_value(";
                        emitOwned(valNode->children[1], out);
                        out << "); }\n";
                    }
                }
            } else {
                // No initializer — all bindings are undefined
                for (const auto& prop : id.children) {
                    if (prop.children.empty()) continue;
                    std::string lname = identName(
                        prop.children.size() >= 2 ? prop.children[1] : prop.children[0]);
                    if (withType) out << ind << "JsOwned ";
                    out << lname << " = JsOwned::from_value(JS_UNDEFINED())";
                    if (withSemi) out << ";\n";
                    ownedLocals_.insert(lname);
                }
            }
            return;
        }

        // ── ArrayPattern destructuring ───────────────────────────────────────
        if (id.type == "ArrayPattern") {
            if (decl.children.size() >= 2) {
                int uid = jsc_method_uid_++;
                std::string src = "__ap_" + std::to_string(uid);
                out << ind << "JsOwned " << src << " = JsOwned::from_value(";
                emitOwned(decl.children[1], out);
                out << ");\n";
                for (size_t pi = 0; pi < id.children.size(); ++pi) {
                    const ASTNode& elem = id.children[pi];
                    if (elem.type == "RestElement" && !elem.children.empty()) {
                        std::string rname = identName(elem.children[0]);
                        out << ind << "JsOwned " << rname << " = JsOwned::from_value(js_obj_new());\n";
                        out << ind << "{ int __rn = (int)js_to_num(js_obj_get_q("
                            << src << ".borrow(), \"length\")), __ri = 0; char __rk[24];\n";
                        out << ind << "  for (int __rj = " << pi << "; __rj < __rn; ++__rj) {\n";
                        out << ind << "    snprintf(__rk, 24, \"%d\", __ri++);\n";
                        out << ind << "    js_obj_set(" << rname << ".ptr(), __rk, "
                            << "js_obj_get(" << src << ".borrow(), JS_NUM(__rj))); }\n";
                        out << ind << "  snprintf(__rk, 24, \"%d\", __ri);\n";
                        out << ind << "  js_obj_set(" << rname << ".ptr(), \"length\", JS_NUM(__ri)); }\n";
                        ownedLocals_.insert(rname);
                        break;
                    }
                    if (elem.type == "Identifier") {
                        std::string ename = identName(elem);
                        if (withType) out << ind << "JsOwned ";
                        out << ename << " = JsOwned::from_value(js_obj_get("
                            << src << ".borrow(), JS_NUM(" << pi << ")))";
                        if (withSemi) out << ";\n";
                        ownedLocals_.insert(ename);
                    } else if (elem.type == "AssignmentPattern" && elem.children.size() >= 2) {
                        std::string ename = identName(elem.children[0]);
                        if (withType) out << ind << "JsOwned ";
                        out << ename << " = JsOwned::from_value(js_obj_get("
                            << src << ".borrow(), JS_NUM(" << pi << ")));\n";
                        ownedLocals_.insert(ename);
                        out << ind << "if (" << ename << ".borrow().tag == JS_UNDEFINED) { "
                            << ename << " = JsOwned::from_value(";
                        emitOwned(elem.children[1], out);
                        out << "); }\n";
                    }
                    // null/elision elements are skipped
                }
            } else {
                for (const auto& elem : id.children) {
                    if (elem.type == "Identifier") {
                        std::string ename = identName(elem);
                        if (withType) out << ind << "JsOwned ";
                        out << ename << " = JsOwned::from_value(JS_UNDEFINED())";
                        if (withSemi) out << ";\n";
                        ownedLocals_.insert(ename);
                    }
                }
            }
            return;
        }

        std::string name = identName(id);

        if (decl.children.size() >= 2 && isRequireCall(decl.children[1])) {
            std::string req = requireArg(decl.children[1]);
            if (isRelativeRequire(req)) {
                std::string resolved =
                    ModuleResolver::resolvePath(currentFilePath_, req);
                const Module* dep = findModule(resolved);
                if (dep)
                    out << ind << dep->cPrefix << "_init();"
                        << " /* execute module: " << escapeString(req) << " */\n";

                std::string cName = resolve(name);
                bool resolvedToFunc =
                    (cName != name) && dep && dep->functionCNames.count(cName);
                if (!resolvedToFunc) {
                    std::string exportsSlot =
                        dep ? (dep->cPrefix + "_exports") : "JS_UNDEFINED()";
                    if (withType) out << ind << "JsOwned ";
                    out << sanitizeName(name) << " = JsOwned::from_borrowed(" << exportsSlot << ")";
                    if (withSemi) out << ";\n";
                    ownedLocals_.insert(name);
                } else {
                    out << ind << "/* require('" << escapeString(req)
                        << "') resolved at compile time */\n";
                }
                return;
            }
            if (withType) out << ind << "JsOwned ";
            out << sanitizeName(name) << " = JsOwned::from_borrowed(";
            emitBareRequireExpr(req, out);
            out << ")";
            if (withSemi) out << ";\n";
            ownedLocals_.insert(name);
            return;
        }

        /* If the variable is already in ownedLocals_ it was pre-declared at
         * file scope (entry-module hoisting).  Skip the type prefix to avoid
         * shadowing the file-scope static with a local declaration. */
        bool alreadyDeclared = ownedLocals_.count(name) > 0;
        if (withType && !alreadyDeclared) out << ind << "JsOwned ";
        else out << ind;
        out << sanitizeName(name) << " = ";
        if (decl.children.size() >= 2) {
            emitOwned(decl.children[1], out);
        } else {
            out << "js_undefined()";
        }
        if (withSemi) out << ";\n";
        ownedLocals_.insert(name);
    }

    // ── For statement ───────────────────────────────────────────────────────

    void emitFor(const ASTNode& node, std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');
        out << ind << "for (";
        size_t ci = 0;
        const size_t n = node.children.size();
        if (ci < n && node.children[ci].type == "VariableDeclaration") {
            const auto& vd = node.children[ci];
            // Emit all declarators: JsOwned a = v1, b = v2
            bool firstDecl = true;
            for (const auto& d : vd.children) {
                if (!firstDecl) out << ", ";
                emitVarDecl(d, out, 0, firstDecl, false);
                firstDecl = false;
            }
            ci++;
        }
        out << "; ";
        if (ci < n && node.children[ci].type != "BlockStatement") {
            out << "js_truthy("; emitValue(node.children[ci], out); out << ")"; ci++;
        }
        out << "; ";
        if (ci < n && node.children[ci].type != "BlockStatement") {
            emitExpr(node.children[ci], out); ci++;
        }
        out << ") ";
        if (ci < n) emitBlock(node.children[ci], out, depth);
        else        out << "{}\n";
    }

    // ── Function declaration ────────────────────────────────────────────────

    void emitFunction(const ASTNode& node, std::ostringstream& out,
                      int depth, const std::string& prefix) {
        std::string bindName;
        std::vector<std::string> params;
        const ASTNode* body = nullptr;
        extractFunctionSignature(node, &bindName, &params, &body);
        if (bindName.empty() && node.type == "FunctionDeclaration")
            return;
        std::string cname = prefix.empty() ? sanitizeName(bindName)
                                           : (prefix + "_" + sanitizeName(bindName));
        std::string qual  = prefix.empty() ? "" : "static ";

        /* Hoisted nested function declarations inside this block (C++ has no nested
         * functions): emit nested static JsOwned definitions before the parent. */
        struct SymSave {
            std::string key;
            bool        had;
            std::string old;
        };
        std::vector<SymSave> symRestore;
        int                    nestIdx = 0;
        if (body && body->type == "BlockStatement") {
            for (const auto& s : body->children) {
                if (s.type != "FunctionDeclaration")
                    continue;
                std::string inner = identName(s.children[0]);
                std::string nestPfx =
                    cname + "__n" + std::to_string(static_cast<long long>(nestIdx++));
                std::string innerC = nestPfx + "_" + inner;
                SymSave sv;
                sv.key = inner;
                auto it = symbolTable_.find(inner);
                sv.had = (it != symbolTable_.end());
                if (sv.had)
                    sv.old = it->second;
                symRestore.push_back(std::move(sv));
                symbolTable_[inner] = innerC;
                emitFunction(s, out, depth, nestPfx);
            }
        }

        out << qual << "JsOwned " << cname << "(";
        for (size_t i = 0; i < params.size(); ++i) {
            if (i) out << ", ";
            out << "JsOwned " << sanitizeName(params[i]);
        }
        out << ") {\n";
        /* Silence -Wunused-parameter without JS shims (e.g. hybrid bridge passes a payload
           the body may ignore). */
        {
            std::string pid(static_cast<size_t>(depth + 1) * 4, ' ');
            for (const auto& p : params)
                out << pid << "(void)" << sanitizeName(p) << ";\n";
        }
        std::set<std::string> outerOwned = std::move(ownedLocals_);
        ownedLocals_.clear();
        for (const auto& p : params) ownedLocals_.insert(p);
        if (body) {
            for (const auto& s : body->children) {
                if (s.type == "FunctionDeclaration")
                    continue;
                emitStmt(s, out, depth + 1);
            }
        }
        ownedLocals_ = std::move(outerOwned);
        // Implicit return undefined if function body doesn't end with return
        bool endsRet = body && !body->children.empty() &&
                       body->children.back().type == "ReturnStatement";
        if (!endsRet) out << std::string(static_cast<size_t>(depth + 1) * 4, ' ')
                          << "return JsOwned();\n";
        out << "}\n";

        for (auto it = symRestore.rbegin(); it != symRestore.rend(); ++it) {
            if (it->had)
                symbolTable_[it->key] = it->old;
            else
                symbolTable_.erase(it->key);
        }
    }

    // ── Expressions ─────────────────────────────────────────────────────────

    void emitExpr(const ASTNode& node, std::ostringstream& out) {
        if      (node.type == "Literal")            emitLiteral(node, out);
        else if (node.type == "Identifier") {
            std::string n = identName(node);
            if (isOwnedBinding(n))
                out << sanitizeName(n) << ".borrow()";
            else
                out << resolve(n);
        }
        else if (node.type == "BinaryExpression")   emitBinary(node, out);
        else if (node.type == "LogicalExpression")  emitLogical(node, out);
        else if (node.type == "AssignmentExpression") emitAssignment(node, out);
        else if (node.type == "UnaryExpression")    emitUnary(node, out);
        else if (node.type == "UpdateExpression")   emitUpdate(node, out);
        else if (node.type == "CallExpression")     emitCall(node, out);
        else if (node.type == "NewExpression") {
            if (node.children.empty()) {
                out << "JS_UNDEFINED()";
                return;
            }
            const ASTNode& nc = node.children[0];
            if (identName(nc) == "Function" && node.children.size() > 1) {
                needs_quickjs_ = true;
                int na = (int)node.children.size() - 1;
                if (na > 24) na = 24;
                out << "jsc_quickjs_function_construct((int)" << na
                    << ", (const JsValue[]) { ";
                for (int i = 0; i < na; ++i) {
                    if (i) out << ", ";
                    emitValue(node.children[(size_t)i + 1], out);
                }
                out << " })";
                return;
            }
            // User-defined constructor: create object, thread __jsc_this, call ctor
            {
                std::string ctorName = identName(nc);
                std::string cname    = resolve(ctorName);
                bool ctorIsOwned     = isOwnedBinding(ctorName);
                int uid = jsc_method_uid_++;
                out << "([&](){ "
                    << "JsValue __new_" << uid << " = js_obj_new(); ";
                // Set __proto__ from the constructor's Function.prototype
                if (ctorIsOwned) {
                    std::string cn = sanitizeName(ctorName);
                    out << "{ JsValue __ct = " << cn << ".borrow(); "
                        << "if (__ct.tag == JS_FUNCTION && __ct.as.func) { "
                        << "JsValue* __pp = js_fn_get_or_create_proto(__ct.as.func); "
                        << "if (__pp) js_obj_set(&__new_" << uid
                        << ", \"__proto__\", js_clone_value(*__pp)); } } ";
                }
                // Pre-evaluate constructor args before changing __jsc_this,
                // so expressions like `this.x + other.x` use the caller's this.
                int na = (int)node.children.size() - 1;
                for (int ai = 0; ai < na; ++ai) {
                    out << "JsOwned __na_" << uid << "_" << ai
                        << " = JsOwned::from_value(";
                    emitOwned(node.children[(size_t)ai + 1], out);
                    out << "); ";
                }
                out << "JsValue __old_this_" << uid << " = __jsc_this; "
                    << "__jsc_this = __new_" << uid << "; ";
                if (ctorIsOwned) {
                    std::string cn = sanitizeName(ctorName);
                    out << "js_call(" << cn << ".borrow(), " << na;
                    for (int ai = 0; ai < na; ++ai)
                        out << ", __na_" << uid << "_" << ai << ".borrow()";
                    out << "); ";
                } else {
                    out << cname << "(";
                    for (int ai = 0; ai < na; ++ai) {
                        if (ai) out << ", ";
                        out << "__na_" << uid << "_" << ai;
                    }
                    out << "); ";
                }
                out << "__jsc_this = __old_this_" << uid << "; "
                    << "return __new_" << uid << "; })()";
            }
        } else if (node.type == "MemberExpression") {
            emitMemberExpr(node, out);
        } else if (node.type == "ObjectExpression") {
            emitObjectExpr(node, out);
        }
        else if (node.type == "FunctionExpression" ||
                 node.type == "ArrowFunctionExpression") {
            std::string qn = queueFeNative(node);
            out << "js_native_fn(" << qn << ")";
        } else if (node.type == "ArrayExpression") {
            emitArrayExpr(node, out);
        } else if (node.type == "TemplateLiteral") {
            emitTemplateLiteral(node, out);
        } else if (node.type == "ConditionalExpression") {
            if (node.children.size() < 3) {
                out << "JS_UNDEFINED()";
                return;
            }
            out << "(js_truthy(";
            emitValue(node.children[0], out);
            out << ") ? ";
            emitExpr(node.children[1], out);
            out << " : ";
            emitExpr(node.children[2], out);
            out << ")";
        } else if (node.type == "SequenceExpression") {
            if (node.children.empty()) {
                out << "JS_UNDEFINED()";
                return;
            }
            out << "(";
            for (size_t i = 0; i < node.children.size(); ++i) {
                if (i) out << ", ";
                emitExpr(node.children[i], out);
            }
            out << ")";
        } else if (node.type == "ThisExpression") {
            out << "__jsc_this";
        } else {
            diagUnsupported(node.type, "Expression lowers to JS_UNDEFINED() stub.");
            out << "JS_UNDEFINED() /* unsupported: " << node.type << " */";
        }
    }

    void emitObjectExpr(const ASTNode& node, std::ostringstream& out) {
        out << "([&](){ JsValue __obj = js_obj_new(); ";
        for (size_t i = 0; i < node.children.size(); ++i) {
            const ASTNode& p = node.children[i];
            if (p.type != "Property" || p.children.size() < 2)
                continue;
            const ASTNode& keyNode = p.children[0];
            out << "js_obj_set(&__obj, ";
            if (keyNode.type == "Identifier") {
                out << "\"" << escapeString(identName(keyNode)) << "\"";
            } else if (keyNode.type == "Literal" && keyNode.data.contains("value")) {
                const auto& kv = keyNode.data["value"];
                if (kv.is_string()) {
                    out << "\"" << escapeString(kv.get<std::string>()) << "\"";
                } else if (kv.is_number()) {
                    out << "\"" << kv.dump() << "\"";
                } else {
                    out << "\"\"";
                }
            } else {
                out << "\"\"";
            }
            out << ", ";
            emitOwned(p.children[1], out);
            out << "); ";
        }
        out << "return __obj; })()";
    }

    // ── Literal ─────────────────────────────────────────────────────────────

    void emitLiteral(const ASTNode& node, std::ostringstream& out) {
        const auto& d = node.data;
        if (!d.contains("value")) { out << "JS_UNDEFINED()"; return; }
        const auto& v = d["value"];
        if (v.is_null())    { out << "JS_NULL()"; }
        else if (v.is_boolean()) { out << "JS_BOOL(" << (v.get<bool>() ? 1 : 0) << ")"; }
        else if (v.is_number()) {
            std::string raw = (d.contains("raw") && d["raw"].is_string())
                              ? d["raw"].get<std::string>() : v.dump();
            out << "JS_NUM(" << raw << ")";
        } else if (v.is_string()) {
            out << "JS_STR(\"" << escapeString(v.get<std::string>()) << "\")";
        } else {
            out << "JS_UNDEFINED()";
        }
    }

    // ── Binary expression ────────────────────────────────────────────────────

    void emitBinary(const ASTNode& node, std::ostringstream& out) {
        if (node.children.size() < 2) { out << "JS_UNDEFINED()"; return; }
        std::string op = node.data.value("operator", "+");
        static const struct { const char* jsop; const char* cfn; } opmap[] = {
            {"+","js_add"},{"-","js_sub"},{"*","js_mul"},{"/","js_div"},
            {"%","js_mod"},{"**","js_pow"},
            {"==","js_eq"},{"!=","js_neq"},{"===","js_eq"},{"!==","js_neq"},
            {"<","js_lt"},{"<=","js_lte"},{">","js_gt"},{">=","js_gte"},
            {nullptr,nullptr}
        };
        const char* fn = nullptr;
        for (int i = 0; opmap[i].jsop; ++i)
            if (op == opmap[i].jsop) { fn = opmap[i].cfn; break; }
        if (fn) {
            out << fn << "("; emitValue(node.children[0], out);
            out << ", ";      emitValue(node.children[1], out); out << ")";
        } else {
            out << "JS_NUM((long long)js_to_num(";
            emitValue(node.children[0], out);
            out << ") " << op << " (long long)js_to_num(";
            emitValue(node.children[1], out); out << "))";
        }
    }

    // ── Logical expression ───────────────────────────────────────────────────

    void emitLogical(const ASTNode& node, std::ostringstream& out) {
        if (node.children.size() < 2) { out << "JS_UNDEFINED()"; return; }
        std::string op = node.data.value("operator", "&&");
        if (op == "??") {
            out << "js_nullish_coalesce("; emitValue(node.children[0], out);
            out << ", ";                    emitValue(node.children[1], out); out << ")";
            return;
        }
        const char* fn = (op == "&&") ? "js_and" : "js_or";
        out << fn << "("; emitValue(node.children[0], out);
        out << ", ";      emitValue(node.children[1], out); out << ")";
    }

    // ── Assignment expression ────────────────────────────────────────────────

    void emitAssignment(const ASTNode& node, std::ostringstream& out) {
        if (node.children.size() < 2) return;
        std::string op = node.data.value("operator", "=");
        const ASTNode& lhs = node.children[0];
        const ASTNode& rhs = node.children[1];

        if (lhs.type == "MemberExpression" && op == "=") {
            const ASTNode& obj = lhs.children[0];
            const ASTNode& prop = lhs.children[1];
            bool computed = lhs.data.value("computed", false);
            if (!computed) {
                out << "([&](){ JsValue __o = ";
                emitValue(obj, out);
                out << "; js_obj_set(&__o, \"" << escapeString(identName(prop)) << "\", ";
                emitOwned(rhs, out);
                out << "); return js_clone_value(__o); })()";
            } else {
                // Computed: obj[key] = val
                out << "([&](){ JsValue __o = ";
                emitValue(obj, out);
                out << "; char __ck[48]; ";
                out << "JsOwned __kv = JsOwned::from_value(";
                emitOwned(prop, out);
                out << "); char* __ks = js_to_cstr(__kv.borrow()); ";
                out << "snprintf(__ck, 48, \"%s\", __ks ? __ks : \"\"); free(__ks); ";
                out << "js_obj_set(&__o, __ck, ";
                emitOwned(rhs, out);
                out << "); return js_clone_value(__o); })()";
            }
            return;
        }

        if (lhs.type == "Identifier") {
            std::string ln = identName(lhs);
            std::string cn = sanitizeName(ln);
            if (isOwnedBinding(ln)) {
                if (op == "=") {
                    out << cn << " = ";
                    emitOwned(rhs, out);
                    return;
                }
                static const struct { const char* op; const char* fn; } cmap[] = {
                    {"+=","js_add"},{"-=","js_sub"},{"*=","js_mul"},
                    {"/=","js_div"},{"%=","js_mod"},{"**=","js_pow"},{nullptr,nullptr}
                };
                const char* fn = nullptr;
                for (int i = 0; cmap[i].op; ++i)
                    if (op == cmap[i].op) { fn = cmap[i].fn; break; }
                if (fn) {
                    out << cn << " = JsOwned::from_value(" << fn << "("
                        << cn << ".borrow(), ";
                    emitExpr(rhs, out);
                    out << "))";
                } else {
                    out << cn << " = ";
                    emitExpr(rhs, out);
                }
                return;
            }
        }
        if (op == "=") {
            emitExpr(lhs, out); out << " = "; emitExpr(rhs, out);
        } else {
            static const struct { const char* op; const char* fn; } cmap[] = {
                {"+=","js_add"},{"-=","js_sub"},{"*=","js_mul"},
                {"/=","js_div"},{"%=","js_mod"},{"**=","js_pow"},{nullptr,nullptr}
            };
            const char* fn = nullptr;
            for (int i = 0; cmap[i].op; ++i)
                if (op == cmap[i].op) { fn = cmap[i].fn; break; }
            if (fn) {
                emitExpr(lhs, out);
                out << " = " << fn << "("; emitValue(lhs, out);
                out << ", "; emitExpr(rhs, out); out << ")";
            } else {
                emitExpr(lhs, out); out << " = "; emitExpr(rhs, out);
            }
        }
    }

    // ── Unary / Update ───────────────────────────────────────────────────────

    void emitUnary(const ASTNode& node, std::ostringstream& out) {
        if (node.children.empty()) { out << "JS_UNDEFINED()"; return; }
        std::string op = node.data.value("operator", "!");
        if (op == "!")        { out << "js_not(";    emitValue(node.children[0], out); out << ")"; }
        else if (op == "-")   { out << "js_neg(";    emitValue(node.children[0], out); out << ")"; }
        else if (op == "typeof") { out << "js_typeof("; emitValue(node.children[0], out); out << ")"; }
        else { out << op; emitExpr(node.children[0], out); }
    }

    void emitUpdate(const ASTNode& node, std::ostringstream& out) {
        if (node.children.empty()) { out << "JS_UNDEFINED()"; return; }
        std::string op  = node.data.value("operator", "++");
        bool prefix     = node.data.value("prefix", true);
        const char* fn  = (op == "++")
            ? (prefix ? "js_preinc" : "js_postinc")
            : (prefix ? "js_predec" : "js_postdec");
        out << fn << "("; emitLvaluePtr(node.children[0], out); out << ")";
    }

    void emitJscMemberCall(const ASTNode& obj, const std::string& meth,
                           const std::vector<const ASTNode*>& args,
                           std::ostringstream& out) {
        int na = (int)args.size();
        if (na > 8) na = 8;
        int uid = jsc_method_uid_++;

        // Pre-evaluate receiver and all args BEFORE changing __jsc_this,
        // so expressions like `this.x` in args use the caller's context.
        // Use JsOwned (not raw JsValue) so heap strings / objects live until
        // the lambda returns — emitValue borrows from temporaries that die at ';'.
        out << "([&](){ JsOwned __mcr_" << uid << " = ";
        emitOwned(obj, out);
        out << "; ";
        for (int i = 0; i < na; ++i) {
            out << "JsOwned __mca_" << uid << "_" << i << " = ";
            emitOwned(*args[(size_t)i], out);
            out << "; ";
        }
        out << "JsValue __mco_" << uid << " = __jsc_this; ";

        // Function.prototype.apply/call: set __jsc_this = thisArg (first arg),
        // not the receiver.  This makes _Ctor.apply(this, args) work correctly.
        if ((meth == "apply" || meth == "call") && na >= 1) {
            out << "__jsc_this = __mca_" << uid << "_0.borrow(); ";
        } else {
            out << "__jsc_this = __mcr_" << uid << ".borrow(); ";
        }

        out << "JsValue __mcv_" << uid << " = js_jsc_cm" << na
            << "(__mcr_" << uid << ".borrow(), JS_STR(\"" << escapeString(meth) << "\")";
        for (int i = 0; i < na; ++i)
            out << ", __mca_" << uid << "_" << i << ".borrow()";
        out << "); __jsc_this = __mco_" << uid
            << "; return __mcv_" << uid << "; })()";
    }

    // ── Call expression ──────────────────────────────────────────────────────
    // forValue: append .borrow() when a user function returns JsOwned but a
    // JsValue is needed (avoids double-free vs JsOwned::from_value(borrow())).

    void emitCallImpl(const ASTNode& node, std::ostringstream& out, bool forValue) {
        if (node.children.empty()) { out << "JS_UNDEFINED()"; return; }
        const ASTNode& callee = node.children[0];
        std::vector<const ASTNode*> args;
        for (size_t i = 1; i < node.children.size(); ++i)
            args.push_back(&node.children[i]);

        // ── console.log / console.error ──────────────────────────────────────
        if (isConsoleMember(callee, "log")) {
            out << "js_console_log(" << args.size();
            for (const auto* a : args) { out << ", "; emitValue(*a, out); }
            out << ")"; return;
        }
        if (isConsoleMember(callee, "error")) {
            out << "js_console_error(" << args.size();
            for (const auto* a : args) { out << ", "; emitValue(*a, out); }
            out << ")"; return;
        }
        // ── window.open ──────────────────────────────────────────────────────
        if (isWindowMember(callee, "open") || isGlobalOpenMember(callee)) {
            out << "js_window_open(";
            for (size_t i = 0; i < 4; ++i) {
                if (i) out << ", ";
                if (i < args.size()) emitValue(*args[i], out);
                else                 out << "JS_UNDEFINED()";
            }
            out << ")"; return;
        }

        // ── Member call (obj.method(...)) ────────────────────────────────────
        if (callee.type == "MemberExpression" && callee.children.size() >= 2) {
            const ASTNode& obj    = callee.children[0];
            const ASTNode& method = callee.children[1];
            std::string objName   = identName(obj);
            std::string meth      = identName(method);

            // Check if obj is a tracked builtin module binding, or is the
            // literal global "process" identifier (no require() needed).
            auto bit = builtinBindings_.find(objName);
            std::string resolvedMod = (bit != builtinBindings_.end())
                                        ? bit->second
                                        : (objName == "process"   ? "process"
                                           : objName == "console" ? "console"
                                                                  : "");
            if (!resolvedMod.empty()) {
                if (resolvedMod != "console") {
                    emitBuiltinMethodCall(resolvedMod, meth, args, out);
                    return;
                }
                /* Route console.* through JsValue-aware dispatch (.toString etc). */
                emitJscMemberCall(obj, meth, args, out);
                return;
            }

            emitJscMemberCall(obj, meth, args, out);
            return;
        }

        // ── IIFE: (function(){...})() or (()=>{...})() ───────────────────────
        if (callee.type == "FunctionExpression" ||
            callee.type == "ArrowFunctionExpression") {
            std::string qn = queueFeNative(callee);
            // __jsc_fe_N returns JsValue; wrap in JsOwned so .borrow() works below
            if (args.empty()) {
                out << "JsOwned::from_value(" << qn << "(0, nullptr))";
            } else {
                int na = (int)args.size();
                out << "([&](){ JsValue __iife_a[" << na << "] = {";
                for (int i = 0; i < na; ++i) {
                    if (i) out << ", ";
                    emitValue(*args[(size_t)i], out);
                }
                out << "}; return JsOwned::from_value(" << qn << "("
                    << na << ", __iife_a)); })()";
            }
            if (forValue) out << ".borrow()";
            return;
        }

        // ── Plain identifier call ────────────────────────────────────────────
        std::string calleeName = identName(callee);

        /* JS String(x) built-in → coerce to string via js_to_cstr */
        if (calleeName == "String" && callee.type == "Identifier") {
            out << "([&](){ char* __s = js_to_cstr(";
            if (!args.empty())
                emitValue(*args[0], out);
            else
                out << "JS_UNDEFINED()";
            out << "); JsValue __sv = js_str(__s ? __s : \"\"); free(__s); return __sv; }())";
            return;
        }

        /* JS Number(x) built-in → coerce to number */
        if (calleeName == "Number" && callee.type == "Identifier") {
            out << "JS_NUM(js_to_num(";
            if (!args.empty())
                emitValue(*args[0], out);
            else
                out << "JS_UNDEFINED()";
            out << "))";
            return;
        }

        /* JS Boolean(x) built-in → coerce to boolean */
        if (calleeName == "Boolean" && callee.type == "Identifier") {
            out << "JS_BOOL(js_truthy(";
            if (!args.empty())
                emitValue(*args[0], out);
            else
                out << "JS_UNDEFINED()";
            out << "))";
            return;
        }

        if (calleeName == "eval" && callee.type == "Identifier") {
            needs_quickjs_ = true;
            out << "jsc_quickjs_eval_from_jsvalue(";
            if (!args.empty())
                emitValue(*args[0], out);
            else
                out << "JS_UNDEFINED()";
            out << ", \"eval\")";
            return;
        }

        if (calleeName == "Function" && callee.type == "Identifier") {
            needs_quickjs_ = true;
            int na = (int)args.size();
            if (na > 24) na = 24;
            out << "jsc_quickjs_function_construct((int)" << na
                << ", (const JsValue[]){ ";
            for (int i = 0; i < na; ++i) {
                if (i) out << ", ";
                emitValue(*args[(size_t)i], out);
            }
            out << " })";
            return;
        }

        /* Timers → libuv shim (callbacks must be JsValue JS_FUNCTION) */
        if (calleeName == "setTimeout" && callee.type == "Identifier") {
            needs_uv_ = true;
            out << "jsc_uv_set_timeout_ms(";
            if (args.size() >= 1) emitValue(*args[0], out);
            else                  out << "JS_UNDEFINED()";
            out << ", ";
            if (args.size() >= 2) emitValue(*args[1], out);
            else                  out << "JS_NUM(0)";
            out << ")";
            return;
        }
        if (calleeName == "setInterval" && callee.type == "Identifier") {
            needs_uv_ = true;
            out << "jsc_uv_set_interval_ms(";
            if (args.size() >= 1) emitValue(*args[0], out);
            else                  out << "JS_UNDEFINED()";
            out << ", ";
            if (args.size() >= 2) emitValue(*args[1], out);
            else                  out << "JS_NUM(0)";
            out << ")";
            return;
        }
        if (calleeName == "clearTimeout" && callee.type == "Identifier") {
            needs_uv_ = true;
            out << "jsc_uv_clear_timer(";
            if (!args.empty()) emitValue(*args[0], out);
            else                out << "JS_UNDEFINED()";
            out << ")";
            return;
        }
        if (calleeName == "clearInterval" && callee.type == "Identifier") {
            needs_uv_ = true;
            out << "jsc_uv_clear_timer(";
            if (!args.empty()) emitValue(*args[0], out);
            else                out << "JS_UNDEFINED()";
            out << ")";
            return;
        }

        // If the callee is a JsValue function binding (from destructured builtin),
        // or a JsOwned local variable holding a JS_FUNCTION value, call via js_call().
        if (jsValueBindings_.count(calleeName) || isOwnedBinding(calleeName)) {
            out << "js_call(" << sanitizeName(calleeName) << ".borrow(), " << args.size();
            for (const auto* a : args) { out << ", "; emitValue(*a, out); }
            out << ")";
            return;
        }

        std::string cname = resolve(calleeName);
        out << cname << "(";
        for (size_t i = 0; i < args.size(); ++i) {
            if (i) out << ", ";
            emitCallArg(*args[i], out);
        }
        out << ")";
        if (forValue)
            out << ".borrow()";
    }

    void emitCall(const ASTNode& node, std::ostringstream& out) {
        emitCallImpl(node, out, true);
    }

    // ── Compile-time dispatch for known builtin module method calls ──────────
    // Emit a direct C function call for a known module.method pattern.
    // For variadic functions (path.join), emits the count as the first arg.
    // For fixed-arity functions, pads missing args with JS_UNDEFINED().
    void emitBuiltinMethodCall(const std::string& mod, const std::string& meth,
                                const std::vector<const ASTNode*>& args,
                                std::ostringstream& out)
    {
        // Helper: emit exactly `total` args, padding with JS_UNDEFINED()
        auto emitPadded = [&](int total) {
            for (int i = 0; i < total; ++i) {
                if (i) out << ", ";
                if (i < (int)args.size()) emitValue(*args[i], out);
                else                      out << "JS_UNDEFINED()";
            }
        };

        if (mod == "fs") {
            if (meth == "readFileSync")  { out << "js_fs_readFileSync(";  emitPadded(2); out << ")"; return; }
            if (meth == "writeFileSync") { out << "js_fs_writeFileSync("; emitPadded(3); out << ")"; return; }
            if (meth == "existsSync")    { out << "js_fs_existsSync(";    emitPadded(1); out << ")"; return; }
            if (meth == "readdirSync")   { out << "js_fs_readdirSync(";   emitPadded(1); out << ")"; return; }
        }
        if (mod == "path") {
            if (meth == "join") {
                // Variadic: first arg is the count
                out << "js_path_join(" << args.size();
                for (const auto* a : args) { out << ", "; emitValue(*a, out); }
                out << ")"; return;
            }
            if (meth == "resolve")  { out << "js_path_resolve(";  emitPadded(1); out << ")"; return; }
            if (meth == "dirname")  { out << "js_path_dirname(";  emitPadded(1); out << ")"; return; }
            if (meth == "basename") { out << "js_path_basename("; emitPadded(2); out << ")"; return; }
            if (meth == "extname")  { out << "js_path_extname(";  emitPadded(1); out << ")"; return; }
        }
        if (mod == "os") {
            if (meth == "platform") { out << "js_os_platform()"; return; }
            if (meth == "homedir")  { out << "js_os_homedir()";  return; }
            if (meth == "tmpdir")   { out << "js_os_tmpdir()";   return; }
        }
        if (mod == "process") {
            if (meth == "cwd")  { out << "js_process_cwd()";             return; }
            if (meth == "exit") { out << "js_process_exit("; emitPadded(1); out << ")"; return; }
        }
        if (mod == "http") {
            if (meth == "createServer") {
                if (!args.empty() && args[0]->type == "Identifier") {
                    std::string id = identName(*args[0]);
                    const std::string th = "__jsc_http_th_" + sanitizeName(id);
                    out << "js_http_createServer(JsOwned::from_value(js_native_fn("
                        << th << ")).borrow())";
                    return;
                }
                if (!args.empty() && args[0]->type == "FunctionExpression") {
                    std::string qn = queueFeNative(*args[0]);
                    out << "js_http_createServer(JsOwned::from_value(js_native_fn("
                        << qn << ")).borrow())";
                    return;
                }
                out << "js_http_createServer(";
                if (!args.empty()) emitValue(*args[0], out);
                else                  out << "JS_UNDEFINED()";
                out << ")";
                return;
            }
        }
        if (mod == "net") {
            if (meth == "createServer") {
                if (!args.empty() && args[0]->type == "FunctionExpression") {
                    std::string qn = queueFeNative(*args[0]);
                    out << "js_net_createServer(JsOwned::from_value(js_native_fn("
                        << qn << ")).borrow())";
                    return;
                }
                out << "js_net_createServer(";
                if (!args.empty()) emitValue(*args[0], out);
                else
                    out << "JS_UNDEFINED()";
                out << ")";
                return;
            }
        }
        if (mod == "crypto") {
            if (meth == "randomBytes") {
                out << "js_crypto_randomBytes("; emitPadded(2); out << ")";
                return;
            }
            if (meth == "createHash") {
                out << "js_crypto_createHash("; emitPadded(1); out << ")";
                return;
            }
        }
        // Unknown method on a known builtin — fall back to runtime dispatch
        out << "js_call(js_obj_get(js_node_require(JS_STR(\""
            << escapeString(mod) << "\")), JS_STR(\""
            << escapeString(meth) << "\")), " << args.size();
        for (const auto* a : args) { out << ", "; emitValue(*a, out); }
        out << ")";
    }

    // ── Member expression (as a value, not as a callee) ─────────────────────
    //
    // Handles:
    //   1. process.env.KEY  → js_process_env_get(JS_STR("KEY"))
    //   2. process.argv     → js_process_get_argv()
    //   3. <builtin>.prop   → js_obj_get(<builtin>, JS_STR("prop"))
    //   4. other            → obj.prop  (old literal emit, safe for module.exports etc.)
    void emitMemberExpr(const ASTNode& node, std::ostringstream& out) {
        if (node.children.size() < 2) { out << "JS_UNDEFINED()"; return; }
        const ASTNode& obj  = node.children[0];
        const ASTNode& prop = node.children[1];
        std::string propName = identName(prop);

        if (node.data.contains("computed") && node.data["computed"].is_boolean()
                && node.data["computed"].get<bool>()) {
            out << "js_obj_get(";
            emitValue(obj, out);
            out << ", ";
            emitValue(prop, out);
            out << ")";
            return;
        }

        // ── process.env.KEY (two-level MemberExpression) ─────────────────────
        if (obj.type == "MemberExpression" && obj.children.size() >= 2) {
            const ASTNode& innerObj  = obj.children[0];
            const ASTNode& innerProp = obj.children[1];
            std::string innerObjName = identName(innerObj);
            auto bit = builtinBindings_.find(innerObjName);
            bool isProcess = (bit != builtinBindings_.end() && bit->second == "process")
                             || (innerObjName == "process");
            if (isProcess && identName(innerProp) == "env" &&
                !propName.empty() && propName != "<anon>")
            {
                out << "js_process_env_get(JS_STR(\""
                    << escapeString(propName) << "\"))";
                return;
            }
        }

        // ── Single-level builtin property access ─────────────────────────────
        std::string objStr = identName(obj);
        auto bit = builtinBindings_.find(objStr);
        std::string mod = (bit != builtinBindings_.end())
                            ? bit->second
                            : (objStr == "process" ? "process" : "");
        if (!mod.empty()) {
            // Known process properties
            if (mod == "process") {
                if (propName == "argv") { out << "js_process_get_argv()"; return; }
                if (propName == "cwd")  { out << "js_process_cwd()"; return; }
            }
            // Generic builtin property — runtime lookup via js_obj_get_q
            out << "js_obj_get_q(";
            emitValue(obj, out);
            out << ", \"" << escapeString(propName) << "\")";
            return;
        }

        // ── Fallback ──────────────────────────────────────────────────────────
        // C struct member access (obj.prop) is only valid for the internal
        // `module` pseudo-object used for module.exports patterns.  All other
        // identifiers (user JS variables) hold JsValue and must use js_obj_get.
        bool isCStruct = (obj.type == "Identifier" &&
                          identName(obj) == "module");
        if (isCStruct) {
            emitExpr(obj, out);
            out << ".";
            emitExpr(prop, out);
        } else {
            out << "js_obj_get_q(";
            emitValue(obj, out);
            out << ", \"" << escapeString(propName) << "\")";
        }
    }

    // ── Utilities ────────────────────────────────────────────────────────────

    static std::string identName(const ASTNode& node) {
        if (node.data.contains("name") && node.data["name"].is_string())
            return node.data["name"].get<std::string>();
        return "<anon>";
    }

    static bool isConsoleMember(const ASTNode& callee, const std::string& method) {
        if (callee.type != "MemberExpression" || callee.children.size() < 2) return false;
        return identName(callee.children[0]) == "console" &&
               identName(callee.children[1]) == method;
    }

    static bool isWindowMember(const ASTNode& callee, const std::string& method) {
        if (callee.type != "MemberExpression" || callee.children.size() < 2) return false;
        return identName(callee.children[0]) == "window" &&
               identName(callee.children[1]) == method;
    }

    /* window.open / globalThis.open / global.open (global maps to blue global) */
    static bool isGlobalOpenMember(const ASTNode& callee) {
        if (callee.type != "MemberExpression" || callee.children.size() < 2) return false;
        std::string o = identName(callee.children[0]);
        std::string m = identName(callee.children[1]);
        return m == "open" &&
               (o == "globalThis" || o == "global");
    }

    static std::string escapeString(const std::string& s) {
        std::string out;
        out.reserve(s.size());
        for (char c : s) {
            switch (c) {
                case '"':  out += "\\\""; break;
                case '\\': out += "\\\\"; break;
                case '\n': out += "\\n";  break;
                case '\r': out += "\\r";  break;
                case '\t': out += "\\t";  break;
                default:   out += c;
            }
        }
        return out;
    }

    // ── Array expression ─────────────────────────────────────────────────────

    void emitArrayExpr(const ASTNode& node, std::ostringstream& out) {
        bool hasSpread = false;
        for (const auto& e : node.children)
            if (e.type == "SpreadElement") { hasSpread = true; break; }

        if (!hasSpread) {
            out << "([&](){ JsValue __arr = js_obj_new(); ";
            for (size_t i = 0; i < node.children.size(); ++i) {
                out << "js_obj_set(&__arr, \"" << i << "\", ";
                emitOwned(node.children[i], out);
                out << "); ";
            }
            out << "js_obj_set(&__arr, \"length\", JS_NUM("
                << node.children.size() << ")); return __arr; })()";
        } else {
            out << "([&](){ JsValue __arr = js_obj_new(); int __ai = 0; char __ak[24]; ";
            for (const auto& elem : node.children) {
                if (elem.type == "SpreadElement" && !elem.children.empty()) {
                    int uid = jsc_method_uid_++;
                    std::string sv = "__sp" + std::to_string(uid);
                    out << "{ JsOwned " << sv << " = JsOwned::from_value(";
                    emitOwned(elem.children[0], out);
                    out << "); int " << sv << "_n = (int)js_to_num(js_obj_get_q("
                        << sv << ".borrow(), \"length\")); "
                        << "for (int " << sv << "_j = 0; " << sv << "_j < " << sv << "_n; ++"
                        << sv << "_j) { snprintf(__ak, 24, \"%d\", __ai++); "
                        << "js_obj_set(&__arr, __ak, js_obj_get(" << sv
                        << ".borrow(), JS_NUM(" << sv << "_j))); } } ";
                } else {
                    out << "snprintf(__ak, 24, \"%d\", __ai); js_obj_set(&__arr, __ak, ";
                    emitOwned(elem, out);
                    out << "); ++__ai; ";
                }
            }
            out << "snprintf(__ak, 24, \"%d\", __ai); "
                   "js_obj_set(&__arr, \"length\", JS_NUM(__ai)); return __arr; })()";
        }
    }

    // ── Template literal ────────────────────────────────────────────────────

    void emitTemplateLiteral(const ASTNode& node, std::ostringstream& out) {
        if (node.children.empty()) { out << "JS_STR(\"\")"; return; }
        out << "([&](){ JsOwned __tl = JsOwned::from_value(JS_STR(\"\")); ";
        for (const auto& ch : node.children) {
            if (ch.type == "TemplateElement") {
                std::string cooked;
                if (ch.data.contains("value") && ch.data["value"].is_object() &&
                    ch.data["value"].contains("cooked") &&
                    ch.data["value"]["cooked"].is_string())
                    cooked = ch.data["value"]["cooked"].get<std::string>();
                if (!cooked.empty())
                    out << "__tl = JsOwned::from_value(js_add(__tl.borrow(), JS_STR(\""
                        << escapeString(cooked) << "\"))); ";
            } else {
                out << "__tl = JsOwned::from_value(js_add(__tl.borrow(), ";
                emitValue(ch, out);
                out << ")); ";
            }
        }
        out << "return __tl.release(); })()";
    }

    // ── For-of statement ────────────────────────────────────────────────────

    void emitForOf(const ASTNode& node, std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');
        if (node.children.size() < 3) {
            out << ind << "/* malformed ForOfStatement */\n";
            return;
        }
        const ASTNode& left  = node.children[0];
        const ASTNode& right = node.children[1];
        const ASTNode& body  = node.children[2];

        int uid = jsc_method_uid_++;
        std::string arrVar = "__foa" + std::to_string(uid);
        std::string idxVar = "__foi" + std::to_string(uid);

        out << ind << "{\n";
        out << ind << "    JsOwned " << arrVar << " = JsOwned::from_value(";
        emitOwned(right, out);
        out << ");\n";
        out << ind << "    int " << idxVar << "_n = (int)js_to_num(js_obj_get_q("
            << arrVar << ".borrow(), \"length\"));\n";
        out << ind << "    for (int " << idxVar << " = 0; "
            << idxVar << " < " << idxVar << "_n; ++" << idxVar << ") {\n";

        if (left.type == "VariableDeclaration" && !left.children.empty()) {
            const ASTNode& decl = left.children[0];
            if (decl.type == "VariableDeclarator" && !decl.children.empty()) {
                std::string varName = identName(decl.children[0]);
                out << ind << "        JsOwned " << varName
                    << " = JsOwned::from_value(js_obj_get("
                    << arrVar << ".borrow(), JS_NUM(" << idxVar << ")));\n";
                ownedLocals_.insert(varName);
            }
        } else if (left.type == "Identifier") {
            std::string varName = identName(left);
            out << ind << "        " << varName
                << " = JsOwned::from_value(js_obj_get("
                << arrVar << ".borrow(), JS_NUM(" << idxVar << ")));\n";
        }

        if (body.type == "BlockStatement")
            for (const auto& s : body.children) emitStmt(s, out, depth + 2);
        else
            emitStmt(body, out, depth + 2);

        out << ind << "    }\n";
        out << ind << "}\n";
    }

    // ── For-in statement ────────────────────────────────────────────────────

    void emitForIn(const ASTNode& node, std::ostringstream& out, int depth) {
        std::string ind(depth * 4, ' ');
        if (node.children.size() < 3) {
            out << ind << "/* malformed ForInStatement */\n";
            return;
        }
        const ASTNode& left  = node.children[0];
        const ASTNode& right = node.children[1];
        const ASTNode& body  = node.children[2];

        int uid = jsc_method_uid_++;
        std::string objVar = "__fio" + std::to_string(uid);
        std::string idxVar = "__fii" + std::to_string(uid);

        out << ind << "{\n";
        out << ind << "    JsOwned " << objVar << " = JsOwned::from_value(";
        emitOwned(right, out);
        out << ");\n";
        out << ind << "    int " << idxVar << "_n = js_obj_key_count(" << objVar << ".borrow());\n";
        out << ind << "    for (int " << idxVar << " = 0; "
            << idxVar << " < " << idxVar << "_n; ++" << idxVar << ") {\n";

        if (left.type == "VariableDeclaration" && !left.children.empty()) {
            const ASTNode& decl = left.children[0];
            if (decl.type == "VariableDeclarator" && !decl.children.empty()) {
                std::string varName = identName(decl.children[0]);
                out << ind << "        JsOwned " << varName
                    << " = JsOwned::from_value(js_obj_key_at("
                    << objVar << ".borrow(), " << idxVar << "));\n";
                ownedLocals_.insert(varName);
            }
        } else if (left.type == "Identifier") {
            std::string varName = identName(left);
            out << ind << "        " << varName
                << " = JsOwned::from_value(js_obj_key_at("
                << objVar << ".borrow(), " << idxVar << "));\n";
        }

        if (body.type == "BlockStatement")
            for (const auto& s : body.children) emitStmt(s, out, depth + 2);
        else
            emitStmt(body, out, depth + 2);

        out << ind << "    }\n";
        out << ind << "}\n";
    }
};
