//! String pool emission. Split out of `codegen.rs` (now `codegen/mod.rs`).

use std::collections::HashMap;

use crate::module::LlModule;
use crate::strings::StringPool;
use crate::types::{DOUBLE, I32, I64, PTR, VOID};

use super::helpers::sanitize;

/// Emit the string pool into the module: byte-array constants, handle
/// globals, and the `__perry_init_strings_<prefix>` function that
/// allocates + NaN-boxes + GC-roots each handle exactly once at startup.
///
/// The string pool was constructed with a `module_prefix`, so every
/// `entry.bytes_global` / `entry.handle_global` is already prefixed.
/// Emission uses those names directly — no extra prefixing here.
pub(super) fn emit_string_pool(
    llmod: &mut LlModule,
    strings: &StringPool,
    module_prefix: &str,
    class_keys_init_data: &[(String, String, u32, Vec<u64>)],
    class_ids: &HashMap<String, u32>,
    classes: &HashMap<String, &perry_hir::Class>,
    closure_rest_params: &HashMap<u32, usize>,
    closure_arities: &HashMap<u32, u32>,
    // Issue #653: wrappers (`__perry_wrap_<name>`) for top-level user functions
    // that declare a rest param. Each entry is `(wrapper_symbol, fixed_arity)`
    // — the runtime side-table is keyed on the wrapper's func_ptr, NOT the
    // underlying user function, because that's what `js_closure_alloc_singleton`
    // stores in the ClosureHeader. Without this registration, calling a user
    // function as a value through `js_closure_call_apply_with_spread` fed the
    // raw spread elements into the wrapper's flat `(this, a0, a1)` signature
    // instead of bundling args[fixed_arity..] into a real array — the rest
    // param then read a single element's bits as if it were the rest array.
    user_fn_wrapper_rest: &[(String, usize)],
    // Refs #915 (gap 1 from #899): subset of `closure_rest_params` whose
    // rest param is the HIR-synthesized `arguments` array. These need
    // `js_register_closure_synthetic_arguments` so the runtime bundles
    // ALL passed args (not just the trailing tail) into the rest slot —
    // matching JS spec semantics for `arguments.length`.
    closure_synthetic_arguments: &std::collections::HashSet<u32>,
    // Mirror of `closure_synthetic_arguments` for the top-level user-fn
    // wrapper path: each entry is `wrapper_symbol` whose underlying
    // function has its synthesized `arguments` rest param.
    user_fn_wrapper_synthetic_arguments: &std::collections::HashSet<String>,
    // Declared param count for every top-level user-function wrapper
    // (`__perry_wrap_<original_name>`) — used to register the wrapper's
    // arity in the runtime's `CLOSURE_ARITY_REGISTRY` so reads of
    // `fn.length` on a closure value return the spec-correct
    // declared-param count. Entries for wrappers also present in
    // `user_fn_wrapper_rest` are skipped (those go through the rest
    // registry which already pins arity).
    user_fn_wrapper_arity: &[(String, u32)],
    // `(wrapper_symbol, display_name)` for every top-level user function
    // we want `console.log` / `util.inspect` to label with the original
    // JS name. Each entry produces one `js_register_function_name` call
    // in `__perry_init_strings_<prefix>` so the registry is populated
    // before user code runs. See #1202.
    user_fn_display_names: &[(String, String)],
) {
    for entry in strings.iter() {
        // .rodata bytes — `[N+1 x i8]` because we include the null terminator.
        llmod.add_named_string_constant(&entry.bytes_global, entry.byte_len + 1, &entry.escaped_ir);
        llmod.add_internal_global(&entry.handle_global, DOUBLE, "0.0");
    }

    // Per-class packed-keys constants (rodata) — referenced by the
    // js_build_class_keys_array call below at module init.
    // Naming: `@perry_class_keys_packed_<modprefix>__<idx>` so we
    // don't collide with anything else.
    let mut packed_global_names: Vec<String> = Vec::with_capacity(class_keys_init_data.len());
    for (idx, (_global_name, packed, _fc, _mask_words)) in class_keys_init_data.iter().enumerate() {
        if packed.is_empty() {
            packed_global_names.push(String::new());
            continue;
        }
        let bytes = packed.as_bytes();
        let mut lit = String::with_capacity(bytes.len() + 8);
        lit.push_str("c\"");
        for &b in bytes {
            if (32..127).contains(&b) && b != b'"' && b != b'\\' {
                lit.push(b as char);
            } else {
                lit.push('\\');
                lit.push_str(&format!("{:02X}", b));
            }
        }
        lit.push_str("\\00\"");
        let name = format!("perry_class_keys_packed_{}__{}", module_prefix, idx);
        llmod.add_named_string_constant(&name, bytes.len() + 1, &lit);
        packed_global_names.push(name);
    }

    // Pre-allocate string constants for function-name registration. Same
    // borrow-ordering constraint as the class-name constants below: we
    // must mint the rodata globals BEFORE `init_fn` claims `&mut llmod`.
    // Each entry becomes one `js_register_function_name(<sym>, <str>,
    // <len>)` call inside the init function. See #1202.
    let mut user_fn_name_constants: Vec<(String, String, usize)> = Vec::new();
    for (wrapper_sym, display_name) in user_fn_display_names {
        if wrapper_sym.is_empty() || display_name.is_empty() {
            continue;
        }
        let (const_name, byte_len) = llmod.add_string_constant(display_name);
        user_fn_name_constants.push((wrapper_sym.clone(), const_name, byte_len));
    }

    // Pre-allocate string constants for class-name registration. We need
    // these BEFORE `init_fn` is created, because once `init_fn` borrows
    // `llmod` we can no longer mutate the module's constant pool. (#1021.)
    let mut named_class_name_constants: Vec<(u32, String, usize)> = Vec::new();
    {
        let mut named: Vec<(u32, String)> = Vec::new();
        for (class_name, class) in classes.iter() {
            if *class_name != class.name {
                continue;
            }
            let cid = match class_ids.get(class_name).copied() {
                Some(c) if c != 0 => c,
                _ => continue,
            };
            if !class_name.starts_with("__AnonShape_") {
                named.push((cid, class_name.clone()));
            }
        }
        named.sort_by(|a, b| a.0.cmp(&b.0));
        named.dedup_by_key(|(cid, _)| *cid);
        for (cid, name) in named {
            let (const_name, byte_len) = llmod.add_string_constant(&name);
            named_class_name_constants.push((cid, const_name, byte_len));
        }
    }

    // Emit per-class typed-shape pointer-mask globals. Empty mask = nothing
    // to emit (no typed slots in this class shape). Must run BEFORE
    // `init_fn = llmod.define_function(...)` because that call holds a
    // mutable borrow of `llmod` for the lifetime of the function/block
    // used by everything below.
    for (global_name, _packed, _field_count, mask_words) in class_keys_init_data.iter() {
        if mask_words.is_empty() {
            continue;
        }
        let mask_global = crate::typed_shape::mask_global_name_from_keys_global(global_name);
        let words = mask_words
            .iter()
            .map(|word| format!("i64 {}", word))
            .collect::<Vec<_>>()
            .join(", ");
        llmod.add_raw_global(format!(
            "@{} = private unnamed_addr constant [{} x i64] [{}]",
            mask_global,
            mask_words.len(),
            words
        ));
    }

    let init_name = format!("__perry_init_strings_{}", module_prefix);
    let init_fn = llmod.define_function(&init_name, VOID, vec![]);
    let _ = init_fn.create_block("entry");
    let blk = init_fn.block_mut(0).unwrap();

    for entry in strings.iter() {
        let bytes_ref = format!("@{}", entry.bytes_global);
        let handle_ref = format!("@{}", entry.handle_global);
        let len_str = entry.byte_len.to_string();

        let init_fn = if entry.is_wtf8 {
            "js_string_from_wtf8_bytes"
        } else {
            "js_string_from_bytes"
        };
        let handle = blk.call(I64, init_fn, &[(PTR, &bytes_ref), (I32, &len_str)]);
        let nanboxed = blk.call(DOUBLE, "js_nanbox_string", &[(I64, &handle)]);
        blk.store(DOUBLE, &nanboxed, &handle_ref);
        let addr_i64 = blk.ptrtoint(&handle_ref, I64);
        blk.call_void("js_gc_register_global_root", &[(I64, &addr_i64)]);
    }

    // Register display names for top-level user functions so
    // `console.log(myFn)` prints `[Function: myFn]` instead of
    // `[Function (anonymous)]`. The runtime registry is keyed on the
    // wrapper's compiled address (`__perry_wrap_<name>`), which is
    // what `js_closure_alloc_singleton` stamps into ClosureHeader.
    // See #1202.
    for (wrapper_sym, name_const, name_len) in &user_fn_name_constants {
        let wrapper_ref = format!("@{}", wrapper_sym);
        let name_ref = format!("@{}", name_const);
        let len_str = name_len.to_string();
        blk.call_void(
            "js_register_function_name",
            &[(PTR, &wrapper_ref), (PTR, &name_ref), (I32, &len_str)],
        );
    }

    // Build per-class keys arrays via js_build_class_keys_array,
    // store the result in the per-class keys global. Done ONCE at
    // module init; every `new ClassName()` call from then on does a
    // single global load + inline allocator call (no SHAPE_CACHE
    // lookup, no js_build_class_keys_array overhead).
    for (idx, (global_name, packed, field_count, _mask_words)) in
        class_keys_init_data.iter().enumerate()
    {
        // Resolve class id from the global name. The global name is
        // `perry_class_keys_<modprefix>__<class>` so we strip the
        // prefix to recover the sanitized class name and look up
        // the id by walking class_ids. Since multiple classes might
        // have the same sanitized name (rare but possible), we just
        // pick the first matching one — class_ids is keyed by the
        // pre-sanitized name so a direct lookup works for ASCII.
        let prefix = format!("perry_class_keys_{}__", module_prefix);
        let sanitized_class = global_name.strip_prefix(&prefix).unwrap_or("");
        let class_id = class_ids
            .iter()
            .find(|(k, _)| sanitize(k) == sanitized_class)
            .map(|(_, &v)| v)
            .unwrap_or(0);

        let cid_str = class_id.to_string();
        let fc_str = field_count.to_string();
        let packed_ref = if packed.is_empty() {
            "null".to_string()
        } else {
            format!("@{}", packed_global_names[idx])
        };
        let len_str = packed.len().to_string();
        let arr = blk.call(
            I64,
            "js_build_class_keys_array",
            &[
                (I32, &cid_str),
                (I32, &fc_str),
                (PTR, &packed_ref),
                (I32, &len_str),
            ],
        );
        blk.store(I64, &arr, &format!("@{}", global_name));
    }

    // Register the parent-class chain for every class with a parent.
    // The runtime allocators do this on every alloc; the inline
    // bump allocator skips it. Without this one-time call, the
    // CLASS_REGISTRY misses the `child → parent` edge and walks of
    // the inheritance chain (e.g. `instanceof Shape` on a `Square`
    // where `Square extends Rectangle extends Shape`) terminate
    // prematurely. We emit one call per inheriting class, sorted by
    // class id for deterministic ordering.
    let mut parent_pairs: Vec<(u32, u32)> = Vec::new();
    for (name, &cid) in class_ids.iter() {
        if let Some(class) = classes.get(name) {
            if let Some(parent_name) = &class.extends_name {
                if let Some(&parent_cid) = class_ids.get(parent_name) {
                    if parent_cid != 0 {
                        parent_pairs.push((cid, parent_cid));
                    }
                }
            }
        }
    }
    parent_pairs.sort_unstable();
    for (cid, parent_cid) in parent_pairs {
        blk.call_void(
            "js_register_class_parent",
            &[(I32, &cid.to_string()), (I32, &parent_cid.to_string())],
        );
    }

    // Issue #392: register every user class method in the runtime
    // VTABLE_REGISTRY so cross-module callers can dispatch via
    // `js_native_call_method` even when the codegen of the calling
    // module can't see the class definition. Same-module calls
    // already resolve through the static idispatch tower in
    // `lower_call.rs` (which iterates `ctx.classes` to find
    // implementors); cross-module calls fall through to
    // `js_native_call_method`, which reads the receiver's class_id
    // and looks up the vtable.
    //
    // Only register classes DEFINED in this module — `class_ids` may
    // include imported classes (Changeset imported from `shared.ts`
    // into `main.ts` for `new Changeset()`), but the `perry_method_*`
    // symbols for those live in the defining module's object file.
    // Each module's init registers its own classes; the linker
    // ensures all init functions run before main.
    let mut method_triples: Vec<(u32, String, String, u32)> = Vec::new();
    for (class_name, class) in classes.iter() {
        // Refs #486: skip alias keys (class_table now contains both the
        // canonical name and self-binding aliases like `_X` from
        // `var X = class _X`); the symbol emission iterates by canonical
        // class.name. Without this skip the alias key generates bogus
        // symbol names like `perry_method_<mod>___X__method` (extra
        // leading underscore from sanitize("_X")) that don't resolve at
        // link time.
        if *class_name != class.name {
            continue;
        }
        // Imported class stubs carry id == 0 (they're typed-name
        // placeholders for cross-module dispatch; the defining module's init
        // registers their methods). Skip them here so we don't re-emit the
        // registration. Previously this filter was `method.body.is_empty()`;
        // the id check is equivalent for stubs and also catches getter/setter
        // and property-decorator init that legitimately has an empty body.
        if class.id == 0 {
            continue;
        }
        let cid = match class_ids.get(class_name) {
            Some(&c) if c != 0 => c,
            _ => continue,
        };
        for method in &class.methods {
            let llvm_name = format!(
                "perry_method_{}__{}__{}",
                module_prefix,
                sanitize(class_name),
                sanitize(&method.name),
            );
            method_triples.push((
                cid,
                method.name.clone(),
                llvm_name,
                method.params.len() as u32,
            ));
        }
    }
    method_triples.sort_unstable();
    for (cid, method_name, llvm_name, param_count) in method_triples {
        // The pre-intern pass before `emit_string_pool` ensured every
        // method name has a string pool entry; look it up here without
        // mutating the pool.
        let entry = match strings.iter().find(|e| e.value == method_name) {
            Some(e) => e,
            None => continue,
        };
        let bytes_global = format!("@{}", entry.bytes_global);
        let len_str = entry.byte_len.to_string();
        // Cast the method function pointer to i64 via ptrtoint so the
        // runtime can store it as a `usize` in the VTABLE_REGISTRY
        // entry. The `inttoptr` round-trip in `call_vtable_method`
        // restores it for the indirect call.
        let func_ref = format!("@{}", llvm_name);
        let func_i64 = blk.ptrtoint(&func_ref, I64);
        let bytes_i64 = blk.ptrtoint(&bytes_global, I64);
        blk.call_void(
            "js_register_class_method",
            &[
                (I64, &cid.to_string()),
                (I64, &bytes_i64),
                (I64, &len_str),
                (I64, &func_i64),
                (I64, &param_count.to_string()),
            ],
        );
    }

    // Refs #618 / #420: register every class id with the runtime so
    // `js_value_typeof` can distinguish a class ref (NaN-boxed INT32 with
    // class_id payload) from a real int32 numeric value. Without this,
    // `typeof <class>` returns "number" for classes that don't define any
    // methods (the existing `js_register_class_method` loop only fires
    // for classes with at least one method body). drizzle's `class
    // FakePrimitiveParam { static [entityKind] = "FakePrimitiveParam" }`
    // and similar method-less marker classes hit this.
    {
        let mut all_class_ids: Vec<u32> = Vec::new();
        let mut anon_shape_ids: Vec<u32> = Vec::new();
        // Also collect `(cid, name)` pairs so we can mirror Perry's
        // user-visible class name into the runtime — V8 reads it back as
        // `metatype.name` (#1021 NestJS module token factory).
        let mut named_classes: Vec<(u32, String)> = Vec::new();
        for (class_name, class) in classes.iter() {
            if *class_name != class.name {
                continue;
            }
            let cid = match class_ids.get(class_name).copied() {
                Some(c) if c != 0 => c,
                _ => continue,
            };
            all_class_ids.push(cid);
            // date-fns / drizzle / lodash plain-object duck-checks need
            // `({ x: 1 }).constructor === Object` to hold. The HIR
            // synthesizes an `__AnonShape_<hash>` class per literal
            // shape; mark each such class id so the runtime
            // `js_object_get_field_by_name` resolves `.constructor` to
            // the global `Object` constructor instead of the synthetic
            // class ref.
            if class_name.starts_with("__AnonShape_") {
                anon_shape_ids.push(cid);
            } else {
                named_classes.push((cid, class_name.clone()));
            }
        }
        all_class_ids.sort_unstable();
        all_class_ids.dedup();
        for cid in all_class_ids {
            blk.call_void(
                "js_register_class_id",
                &[(crate::types::I32, &cid.to_string())],
            );
        }
        anon_shape_ids.sort_unstable();
        anon_shape_ids.dedup();
        for cid in anon_shape_ids {
            blk.call_void(
                "js_register_anon_shape_class_id",
                &[(crate::types::I32, &cid.to_string())],
            );
        }
        // (Class-name registration uses pre-allocated string constants — see
        // `named_class_name_constants` below.) Drop the named_classes
        // collection here since the pre-computed list is what we'll emit.
        let _ = named_classes;
    }
    // Mirror class names into the runtime so the V8 bridge can surface them
    // as `metatype.name`. Strings were pre-allocated above before `init_fn`
    // borrowed `llmod`. (#1021 NestJS.)
    for (cid, const_name, byte_len) in &named_class_name_constants {
        let const_ref = format!("@{}", const_name);
        blk.call_void(
            "js_register_class_name",
            &[
                (crate::types::I32, &cid.to_string()),
                (crate::types::PTR, &const_ref),
                (crate::types::I32, &byte_len.to_string()),
            ],
        );
    }

    // Refs #486 (hono logger middleware): also register every class
    // getter in the runtime VTABLE_REGISTRY. Without this, cross-module
    // `obj.prop` reads (where `obj` is statically typed `any` so the
    // codegen has no static dispatch info) fall through `js_get_object_field_by_name`
    // past the `vtable.getters.get(prop)` lookup at value.rs:2268 — the
    // map is always empty — and into the field-by-name dispatcher,
    // which returns `undefined` for properties that exist only as
    // getters. Hono's `Context.get req()` is the canonical breakage:
    // the logger middleware reads `c.req.url` from a JS-bundled hono
    // dist via `compilePackages`, and pre-fix `c.req` always returned
    // `undefined`.
    let mut getter_pairs: Vec<(u32, String, String)> = Vec::new();
    for (class_name, class) in classes.iter() {
        // Refs #486: skip alias keys (see method-emission loop above).
        if *class_name != class.name {
            continue;
        }
        // Imported class stubs carry id == 0 (they're typed-name
        // placeholders for cross-module dispatch; the defining module's init
        // registers their methods). Skip them here so we don't re-emit the
        // registration. Previously this filter was `method.body.is_empty()`;
        // the id check is equivalent for stubs and also catches getter/setter
        // and property-decorator init that legitimately has an empty body.
        if class.id == 0 {
            continue;
        }
        let cid = match class_ids.get(class_name).copied() {
            Some(c) if c != 0 => c,
            _ => continue,
        };
        for (prop, getter_fn) in &class.getters {
            // The local-emit path at codegen.rs:1858 prepends `__get_`
            // to the HIR-assigned getter name (`get_<prop>`), giving
            // the LLVM symbol `perry_method_<modprefix>__<class>__<sanitize(__get_get_<prop>)>`.
            // Use the same mangling here so the registered func_ptr
            // matches the actual emitted body.
            let inner = format!("__get_{}", getter_fn.name);
            let llvm_name = format!(
                "perry_method_{}__{}__{}",
                module_prefix,
                sanitize(class_name),
                sanitize(&inner),
            );
            getter_pairs.push((cid, prop.clone(), llvm_name));
        }
    }
    getter_pairs.sort_unstable();
    for (cid, prop_name, llvm_name) in getter_pairs {
        let entry = match strings.iter().find(|e| e.value == prop_name) {
            Some(e) => e,
            None => continue,
        };
        let bytes_global = format!("@{}", entry.bytes_global);
        let len_str = entry.byte_len.to_string();
        let func_ref = format!("@{}", llvm_name);
        let func_i64 = blk.ptrtoint(&func_ref, I64);
        let bytes_i64 = blk.ptrtoint(&bytes_global, I64);
        blk.call_void(
            "js_register_class_getter",
            &[
                (I64, &cid.to_string()),
                (I64, &bytes_i64),
                (I64, &len_str),
                (I64, &func_i64),
            ],
        );
    }

    // Refs #486 (hono): parallel registration for class setters. Without
    // this, `c.res = response` (where `c` is `any`-typed) bypasses hono
    // Context's `set res(_res) { …; this.finalized = true; }` and writes
    // directly to a regular field slot. `this.finalized = true` never
    // executes, hono-base sees `c.finalized = false` and throws "Context
    // is not finalized" on every request through compose. Mirror's the
    // getter-pairs loop above; emission mangling matches the
    // setter-method-emission path at codegen.rs:2041 (renamed.name =
    // "__set_<prop>" → LLVM symbol perry_method_<mp>__<class>____set_<f.name>).
    let mut setter_pairs: Vec<(u32, String, String)> = Vec::new();
    for (class_name, class) in classes.iter() {
        if *class_name != class.name {
            continue;
        }
        // Imported class stubs carry id == 0 (they're typed-name
        // placeholders for cross-module dispatch; the defining module's init
        // registers their methods). Skip them here so we don't re-emit the
        // registration. Previously this filter was `method.body.is_empty()`;
        // the id check is equivalent for stubs and also catches getter/setter
        // and property-decorator init that legitimately has an empty body.
        if class.id == 0 {
            continue;
        }
        let cid = match class_ids.get(class_name).copied() {
            Some(c) if c != 0 => c,
            _ => continue,
        };
        for (prop, setter_fn) in &class.setters {
            let inner = format!("__set_{}", setter_fn.name);
            let llvm_name = format!(
                "perry_method_{}__{}__{}",
                module_prefix,
                sanitize(class_name),
                sanitize(&inner),
            );
            setter_pairs.push((cid, prop.clone(), llvm_name));
        }
    }
    setter_pairs.sort_unstable();
    for (cid, prop_name, llvm_name) in setter_pairs {
        let entry = match strings.iter().find(|e| e.value == prop_name) {
            Some(e) => e,
            None => continue,
        };
        let bytes_global = format!("@{}", entry.bytes_global);
        let len_str = entry.byte_len.to_string();
        let func_ref = format!("@{}", llvm_name);
        let func_i64 = blk.ptrtoint(&func_ref, I64);
        let bytes_i64 = blk.ptrtoint(&bytes_global, I64);
        blk.call_void(
            "js_register_class_setter",
            &[
                (I64, &cid.to_string()),
                (I64, &bytes_i64),
                (I64, &len_str),
                (I64, &func_i64),
            ],
        );
    }

    // Issue #493: register each rest-bearing closure body's func_ptr ->
    // fixed_arity in the runtime's closure-rest side table. `js_closure_callN`
    // consults it to bundle trailing args at call sites where codegen
    // doesn't know the closure's arity statically (e.g. `obj.cb(a, b, c)`
    // where `cb` is a class field holding `(...args) => …`). Without this
    // entry the closure body sees the first arg as the rest param itself,
    // not the bundled array — `args.length` reads `1` against the string
    // value, and trailing args are dropped. Static call sites (named fns,
    // `Expr::FuncRef`, `let f = (...args)=>…; f(a,b,c)`) keep their
    // existing call-site bundling and never enter this dispatch path.
    let mut sorted_rest: Vec<(u32, usize)> = closure_rest_params
        .iter()
        .map(|(fid, ri)| (*fid, *ri))
        .collect();
    sorted_rest.sort_unstable();
    for (fid, fixed_arity) in sorted_rest {
        let closure_sym = format!("perry_closure_{}__{}", module_prefix, fid);
        let func_ref = format!("@{}", closure_sym);
        // Refs #915 (gap 1 from #899): closures whose rest param is the
        // synthesized `arguments` use the synthetic-arguments registration
        // so the runtime bundles ALL args into the rest slot.
        let runtime_fn = if closure_synthetic_arguments.contains(&fid) {
            "js_register_closure_synthetic_arguments"
        } else {
            "js_register_closure_rest"
        };
        blk.call_void(
            runtime_fn,
            &[(PTR, &func_ref), (I32, &fixed_arity.to_string())],
        );
    }

    // Refs #421: register every non-rest closure's declared param count so
    // `js_native_call_value` can pad missing trailing args with TAG_UNDEFINED
    // when a closure stored as a class field is invoked method-style on an
    // any-typed receiver with fewer args than declared. Rest-bearing closures
    // are already handled by the closure-rest registry above (which pads
    // internally via `dispatch_rest_bundled`).
    let mut sorted_arities: Vec<(u32, u32)> = closure_arities
        .iter()
        .map(|(fid, arity)| (*fid, *arity))
        .collect();
    sorted_arities.sort_unstable();
    for (fid, arity) in sorted_arities {
        let closure_sym = format!("perry_closure_{}__{}", module_prefix, fid);
        let func_ref = format!("@{}", closure_sym);
        blk.call_void(
            "js_register_closure_arity",
            &[(PTR, &func_ref), (I32, &arity.to_string())],
        );
    }

    // Issue #653: register `__perry_wrap_<name>` wrappers for top-level user
    // functions whose source signature includes a rest param. Mirrors the
    // closure-rest loop above but keyed on the wrapper's symbol rather than
    // the closure body. See `user_fn_wrapper_rest` doc on this fn's signature.
    let mut sorted_wrappers: Vec<(String, usize)> = user_fn_wrapper_rest.to_vec();
    sorted_wrappers.sort();
    let rest_wrapper_names: std::collections::HashSet<String> =
        sorted_wrappers.iter().map(|(s, _)| s.clone()).collect();
    for (wrap_sym, fixed_arity) in sorted_wrappers {
        let func_ref = format!("@{}", wrap_sym);
        // Refs #915 (gap 1 from #899): wrappers whose underlying function
        // declared a synthesized `arguments` rest param need the
        // synthetic-arguments registration.
        let runtime_fn = if user_fn_wrapper_synthetic_arguments.contains(&wrap_sym) {
            "js_register_closure_synthetic_arguments"
        } else {
            "js_register_closure_rest"
        };
        blk.call_void(
            runtime_fn,
            &[(PTR, &func_ref), (I32, &fixed_arity.to_string())],
        );
    }

    // Register declared param count for `__perry_wrap_<name>` wrappers of
    // every non-rest top-level user function. Mirrors the closure-arity
    // loop above (which registered inline closures) and the rest-wrapper
    // loop just above. The runtime's `.length` property accessor on a
    // closure value reads from this registry — ramda's
    // `converge(<fn>, [filter, reject])` IIFE feeds
    // `pluck('length', fns)` → `reduce(max, 0, …)` → `curryN(N, …)` →
    // `_arity(N, …)` at module init; without the wrappers registering
    // their arity, `pluck('length', [filter, reject])` came back as
    // `[undefined, undefined]`, `reduce(max, 0, …)` evaluated to `NaN`,
    // and `_arity(NaN, …)` threw
    // `First argument to _arity must be a non-negative integer no greater
    // than ten` before R.add / R.sum was ever called.
    let mut sorted_wrapper_arities: Vec<(String, u32)> = user_fn_wrapper_arity
        .iter()
        .filter(|(name, _)| !rest_wrapper_names.contains(name))
        .cloned()
        .collect();
    sorted_wrapper_arities.sort();
    for (wrap_sym, arity) in sorted_wrapper_arities {
        let func_ref = format!("@{}", wrap_sym);
        blk.call_void(
            "js_register_closure_arity",
            &[(PTR, &func_ref), (I32, &arity.to_string())],
        );
    }

    blk.ret_void();
}
