//! Array method lowering for array-typed receivers.
//!
//! Contains `lower_array_method` which dispatches `.pop()`, `.join()`,
//! `.some()`, `.every()`, `.toString()`, `.concat()`, `.sort()`,
//! `.reverse()`, `.flat()`, `.flatMap()`, plus safety-net handlers for
//! methods that normally arrive as HIR variants but may reach here as
//! generic MethodCall when the HIR lowering doesn't recognize the pattern.

use anyhow::{bail, Result};
use perry_hir::Expr;

use crate::expr::{
    lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_str_handle, unbox_to_i64, FnCtx,
};
use crate::nanbox::double_literal;
use crate::types::{DOUBLE, I32, I64, PTR};

/// Lower `arr.method(args…)` for an array-typed receiver. Currently
/// supported: `pop`, `join`. `push` is handled separately by the HIR
/// `Expr::ArrayPush` variant (Phase B.7).
pub(crate) fn lower_array_method(
    ctx: &mut FnCtx<'_>,
    object: &Expr,
    property: &str,
    args: &[Expr],
) -> Result<String> {
    let recv_box = lower_expr(ctx, object)?;

    match property {
        "pop" => {
            if !args.is_empty() {
                bail!("perry-codegen: Array.pop takes no args, got {}", args.len());
            }
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            // Returns f64 directly (the popped element, NaN if empty).
            Ok(blk.call(DOUBLE, "js_array_pop_f64", &[(I64, &recv_handle)]))
        }
        "join" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.join expects 1 arg (separator), got {}",
                    args.len()
                );
            }
            let sep_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            // Separator is a JS string. `unbox_str_handle` materializes SSO
            // values to a real heap StringHeader; plain `unbox_to_i64`
            // returns the inline-payload bits, which `js_array_join` then
            // dereferences as a `*StringHeader` and reads `byte_len` from
            // — producing garbage / silent empty join. Same SSO bug class
            // as #214.
            let sep_handle = unbox_str_handle(blk, &sep_box);
            let result_handle = blk.call(
                I64,
                "js_array_join",
                &[(I64, &recv_handle), (I64, &sep_handle)],
            );
            Ok(nanbox_string_inline(blk, &result_handle))
        }
        "some" | "every" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.{} expects 1 arg, got {}",
                    property,
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let runtime_fn = if property == "some" {
                "js_array_some"
            } else {
                "js_array_every"
            };
            Ok(blk.call(
                DOUBLE,
                runtime_fn,
                &[(I64, &recv_handle), (I64, &cb_handle)],
            ))
        }
        "toString" => {
            // arr.toString() == arr.join(",")
            let key_idx = ctx.strings.intern(",");
            let handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global);
            let blk = ctx.block();
            let sep_box = blk.load(DOUBLE, &handle_global);
            let recv_handle = unbox_to_i64(blk, &recv_box);
            // Interned literal "," — heap allocated at module init, so
            // `unbox_to_i64` would technically work, but routing through
            // `unbox_str_handle` keeps the path uniform with the `join`
            // arm and is robust if interning ever changes to SSO-eligible.
            let sep_handle = unbox_str_handle(blk, &sep_box);
            let result_handle = blk.call(
                I64,
                "js_array_join",
                &[(I64, &recv_handle), (I64, &sep_handle)],
            );
            Ok(nanbox_string_inline(blk, &result_handle))
        }
        "concat" => {
            // arr.concat(other) — call js_array_concat_new (non-mutating).
            // Issue #637: pre-fix this called `js_array_concat` (mutating
            // — used internally by spread-into-array desugar) which wrote
            // `other`'s elements into `recv`'s storage. When `recv` was the
            // result of `Object.keys(privateField)` (which fast-paths to
            // returning `(*obj).keys_array` directly — see
            // `js_object_keys`), the user-visible `k.concat(k2)` call
            // mutated the source object's keys_array, corrupting Object.keys
            // output for that object thereafter and aliasing it with newly-
            // allocated keys_arrays of OTHER objects via GC reuse. The
            // user-visible `.concat()` is spec-non-mutating; route to the
            // dedicated non-mutating helper.
            // For simplicity we only handle single-argument concat.
            if args.len() != 1 {
                return Ok(recv_box);
            }
            let other_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let other_handle = unbox_to_i64(blk, &other_box);
            let result = blk.call(
                I64,
                "js_array_concat_new",
                &[(I64, &recv_handle), (I64, &other_handle)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "sort" => {
            // arr.sort() — default comparator (stringwise compare).
            // arr.sort(cb) — custom comparator path.
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = if args.is_empty() {
                blk.call(I64, "js_array_sort_default", &[(I64, &recv_handle)])
            } else {
                let cb_box = lower_expr(ctx, &args[0])?;
                let blk = ctx.block();
                let recv_handle = unbox_to_i64(blk, &recv_box);
                let cb_handle = unbox_to_i64(blk, &cb_box);
                blk.call(
                    I64,
                    "js_array_sort_with_comparator",
                    &[(I64, &recv_handle), (I64, &cb_handle)],
                )
            };
            Ok(nanbox_pointer_inline(ctx.block(), &result))
        }
        "reverse" => {
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(I64, "js_array_reverse", &[(I64, &recv_handle)]);
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "copyWithin" => {
            // ECMA-262 §23.1.3.5 `arr.copyWithin(target, start?, end?)`.
            // The HIR `Expr::ArrayCopyWithin` lowering in `expr_call.rs:3461`
            // only fires when the receiver is a local — literal-receiver
            // calls (`[1,2,3,4,5].copyWithin(0, 1)`) fall through to here.
            // Without this arm the call returned the receiver unchanged,
            // because the general method-dispatch fallback doesn't know
            // about copyWithin and silently no-op'd.
            if args.is_empty() {
                bail!("perry-codegen: Array.copyWithin expects 1-3 args, got 0",);
            }
            let target_d = lower_expr(ctx, &args[0])?;
            let start_d = if args.len() >= 2 {
                lower_expr(ctx, &args[1])?
            } else {
                double_literal(0.0)
            };
            let (has_end_str, end_d) = if args.len() >= 3 {
                let v = lower_expr(ctx, &args[2])?;
                ("1".to_string(), v)
            } else {
                ("0".to_string(), "0.0".to_string())
            };
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(
                I64,
                "js_array_copy_within",
                &[
                    (I64, &recv_handle),
                    (DOUBLE, &target_d),
                    (DOUBLE, &start_d),
                    (I32, &has_end_str),
                    (DOUBLE, &end_d),
                ],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "flat" => {
            // ECMA-262 §23.1.3.10 `arr.flat(depth?)`. Default depth = 1.
            // The depth-aware path routes to `js_array_flat_depth` (handles
            // 0 = shallow copy, Infinity = full recursion); 0-arg keeps
            // the legacy `js_array_flat` fast path.
            if args.is_empty() {
                let blk = ctx.block();
                let recv_handle = unbox_to_i64(blk, &recv_box);
                let result = blk.call(I64, "js_array_flat", &[(I64, &recv_handle)]);
                Ok(nanbox_pointer_inline(blk, &result))
            } else {
                let depth_d = lower_expr(ctx, &args[0])?;
                let blk = ctx.block();
                let recv_handle = unbox_to_i64(blk, &recv_box);
                let result = blk.call(
                    I64,
                    "js_array_flat_depth",
                    &[(I64, &recv_handle), (DOUBLE, &depth_d)],
                );
                Ok(nanbox_pointer_inline(blk, &result))
            }
        }
        "flatMap" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.flatMap expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let result = blk.call(
                I64,
                "js_array_flatMap",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        // -------- Safety-net handlers for methods that normally arrive --------
        // as HIR variants but may reach here as generic MethodCall when
        // the HIR lowering doesn't recognize the pattern.
        "find" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.find expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            Ok(blk.call(
                DOUBLE,
                "js_array_find",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            ))
        }
        "findIndex" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.findIndex expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let i32_v = blk.call(
                I32,
                "js_array_findIndex",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            Ok(blk.sitofp(I32, &i32_v, DOUBLE))
        }
        "findLast" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.findLast expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            Ok(blk.call(
                DOUBLE,
                "js_array_find_last",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            ))
        }
        "findLastIndex" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.findLastIndex expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let i32_v = blk.call(
                I32,
                "js_array_find_last_index",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            Ok(blk.sitofp(I32, &i32_v, DOUBLE))
        }
        "reduce" => {
            if args.is_empty() || args.len() > 2 {
                bail!(
                    "perry-codegen: Array.reduce expects 1-2 args, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let (has_initial, initial_box) = if args.len() == 2 {
                let init = lower_expr(ctx, &args[1])?;
                (1i32, init)
            } else {
                (0i32, "0.0".to_string())
            };
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let has_init_str = format!("{}", has_initial);
            Ok(blk.call(
                DOUBLE,
                "js_array_reduce",
                &[
                    (I64, &recv_handle),
                    (I64, &cb_handle),
                    (I32, &has_init_str),
                    (DOUBLE, &initial_box),
                ],
            ))
        }
        "reduceRight" => {
            if args.is_empty() || args.len() > 2 {
                bail!(
                    "perry-codegen: Array.reduceRight expects 1-2 args, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let (has_initial, initial_box) = if args.len() == 2 {
                let init = lower_expr(ctx, &args[1])?;
                (1i32, init)
            } else {
                (0i32, "0.0".to_string())
            };
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let has_init_str = format!("{}", has_initial);
            Ok(blk.call(
                DOUBLE,
                "js_array_reduce_right",
                &[
                    (I64, &recv_handle),
                    (I64, &cb_handle),
                    (I32, &has_init_str),
                    (DOUBLE, &initial_box),
                ],
            ))
        }
        "map" => {
            if args.len() != 1 {
                bail!("perry-codegen: Array.map expects 1 arg, got {}", args.len());
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let result = blk.call(
                I64,
                "js_array_map",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "filter" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.filter expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            let result = blk.call(
                I64,
                "js_array_filter",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "forEach" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.forEach expects 1 arg, got {}",
                    args.len()
                );
            }
            let cb_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let cb_handle = unbox_to_i64(blk, &cb_box);
            blk.call_void(
                "js_array_forEach",
                &[(I64, &recv_handle), (I64, &cb_handle)],
            );
            // forEach returns undefined
            Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)))
        }
        "includes" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.includes expects 1 arg, got {}",
                    args.len()
                );
            }
            let val_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            // Use `js_array_includes_jsvalue` for deep equality so
            // string values stored in arrays (from e.g. `Object.keys()`
            // or `Object.getOwnPropertyNames()`) match by content, not
            // pointer identity. The `*_f64` variant compares raw bits
            // which fails for strings allocated at different sites.
            let i32_v = blk.call(
                I32,
                "js_array_includes_jsvalue",
                &[(I64, &recv_handle), (DOUBLE, &val_box)],
            );
            // Convert i32 boolean to NaN-boxed true/false
            let bit = blk.icmp_ne(I32, &i32_v, "0");
            let tagged = blk.select(
                "i1",
                &bit,
                I64,
                crate::nanbox::TAG_TRUE_I64,
                crate::nanbox::TAG_FALSE_I64,
            );
            Ok(blk.bitcast_i64_to_double(&tagged))
        }
        "indexOf" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.indexOf expects 1 arg, got {}",
                    args.len()
                );
            }
            let val_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            // Issue #214: route through `_jsvalue` so string elements
            // match by content (handles SSO + heap-string mixed arrays
            // — `arr.indexOf("hello")` on a `JSON.parse(...)`-derived
            // string array returned -1 because the SSO element bits
            // never bit-equal the heap-string needle bits). Mirrors
            // the existing `includes` arm.
            let i32_v = blk.call(
                I32,
                "js_array_indexOf_jsvalue",
                &[(I64, &recv_handle), (DOUBLE, &val_box)],
            );
            Ok(blk.sitofp(I32, &i32_v, DOUBLE))
        }
        "at" => {
            if args.len() != 1 {
                bail!("perry-codegen: Array.at expects 1 arg, got {}", args.len());
            }
            let idx_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            Ok(blk.call(
                DOUBLE,
                "js_array_at",
                &[(I64, &recv_handle), (DOUBLE, &idx_box)],
            ))
        }
        "slice" => {
            if args.len() > 2 {
                bail!(
                    "perry-codegen: Array.slice expects 0-2 args, got {}",
                    args.len()
                );
            }
            // Zero-arg `.slice()` is the JS shallow-copy idiom: same as
            // `.slice(0)`. Lower it to start=0, end=i32::MAX.
            let start_i32 = if args.is_empty() {
                "0".to_string()
            } else {
                let start_box = lower_expr(ctx, &args[0])?;
                let blk = ctx.block();
                blk.fptosi(DOUBLE, &start_box, I32)
            };
            let end_i32 = if args.len() == 2 {
                let end_box = lower_expr(ctx, &args[1])?;
                let blk = ctx.block();
                blk.fptosi(DOUBLE, &end_box, I32)
            } else {
                "2147483647".to_string()
            };
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(
                I64,
                "js_array_slice",
                &[(I64, &recv_handle), (I32, &start_i32), (I32, &end_i32)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "shift" => {
            if !args.is_empty() {
                bail!(
                    "perry-codegen: Array.shift takes no args, got {}",
                    args.len()
                );
            }
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            Ok(blk.call(DOUBLE, "js_array_shift_f64", &[(I64, &recv_handle)]))
        }
        "fill" => {
            // ECMA-262 Array.prototype.fill(value, start?, end?). The
            // 2-/3-arg forms route through `js_array_fill_range` which
            // applies the spec's negative-index + clamp rules and fills
            // `[start, end)`. The 1-arg form keeps the existing
            // whole-array fast path.
            match args.len() {
                1 => {
                    let val_box = lower_expr(ctx, &args[0])?;
                    let blk = ctx.block();
                    let recv_handle = unbox_to_i64(blk, &recv_box);
                    let result = blk.call(
                        I64,
                        "js_array_fill",
                        &[(I64, &recv_handle), (DOUBLE, &val_box)],
                    );
                    Ok(nanbox_pointer_inline(blk, &result))
                }
                2 | 3 => {
                    let val_box = lower_expr(ctx, &args[0])?;
                    let start_d = lower_expr(ctx, &args[1])?;
                    let end_d = if args.len() == 3 {
                        lower_expr(ctx, &args[2])?
                    } else {
                        // Default end = +Infinity → clamps to length in the runtime.
                        crate::nanbox::double_literal(f64::INFINITY)
                    };
                    let blk = ctx.block();
                    let recv_handle = unbox_to_i64(blk, &recv_box);
                    let result = blk.call(
                        I64,
                        "js_array_fill_range",
                        &[
                            (I64, &recv_handle),
                            (DOUBLE, &val_box),
                            (DOUBLE, &start_d),
                            (DOUBLE, &end_d),
                        ],
                    );
                    Ok(nanbox_pointer_inline(blk, &result))
                }
                _ => bail!(
                    "perry-codegen: Array.fill expects 1-3 args, got {}",
                    args.len()
                ),
            }
        }
        "unshift" => {
            if args.len() != 1 {
                bail!(
                    "perry-codegen: Array.unshift expects 1 arg, got {}",
                    args.len()
                );
            }
            // Issue #656: returns the new array length per ECMA-262, not the
            // (possibly reallocated) array pointer. The generic dispatch path
            // does not have a local-id to write back to; we just compute the
            // length on the returned new header. Callers using the bare
            // form `arr.unshift(x)` for side-effect-only semantics still see
            // correct mutation; callers using the return value get the spec
            // value instead of the array.
            let val_box = lower_expr(ctx, &args[0])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let new_handle = blk.call(
                I64,
                "js_array_unshift_f64",
                &[(I64, &recv_handle), (DOUBLE, &val_box)],
            );
            let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]);
            Ok(blk.sitofp(I32, &len_i32, DOUBLE))
        }
        // Issue #655 (chained-receiver path): without this arm, a
        // chained `obj.field.splice(...)` resolved through `is_array_expr`
        // (now that interface property types are recognized) but fell
        // off the end of `lower_array_method` into the silent fallback,
        // which returned the receiver unchanged and never invoked
        // `js_array_splice`. The HIR-level `Expr::ArraySplice` variant
        // covers single-identifier receivers; this arm handles the
        // generic property-chained case (`m.get(k)!.field.splice(...)`).
        // Writeback to the source storage is best-effort for the local
        // / module-global cases — when the array's parent is a heap
        // property we do not re-emit a PropertySet here, matching the
        // existing `Expr::ArraySplice` lowering's tolerance for
        // missing local IDs. In-place mutation is correct regardless;
        // only growth-induced reallocation could leave the parent
        // pointing at the old header (queue-deletion patterns never
        // grow the array, so the issue's hot path is unaffected).
        "splice" => {
            let start_d = if args.is_empty() {
                "0.0".to_string()
            } else {
                lower_expr(ctx, &args[0])?
            };
            let count_d = if args.len() >= 2 {
                lower_expr(ctx, &args[1])?
            } else {
                "2147483647.0".to_string()
            };
            let mut item_vals: Vec<String> = Vec::new();
            for it in args.iter().skip(2) {
                item_vals.push(lower_expr(ctx, it)?);
            }
            let blk = ctx.block();
            let out_slot = blk.alloca(I64);
            blk.store(I64, "0", &out_slot);
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let start_i32 = blk.fptosi(DOUBLE, &start_d, I32);
            let count_i32 = blk.fptosi(DOUBLE, &count_d, I32);
            let (items_ptr, items_count_str) = if item_vals.is_empty() {
                ("null".to_string(), "0".to_string())
            } else {
                let n = item_vals.len();
                let buf_reg = blk.next_reg();
                blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n));
                for (i, val) in item_vals.iter().enumerate() {
                    let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]);
                    blk.store(DOUBLE, val, &slot);
                }
                (buf_reg, format!("{}", n))
            };
            let deleted_handle = blk.call(
                I64,
                "js_array_splice",
                &[
                    (I64, &recv_handle),
                    (I32, &start_i32),
                    (I32, &count_i32),
                    (PTR, &items_ptr),
                    (I32, &items_count_str),
                    (PTR, &out_slot),
                ],
            );
            // Best-effort writeback when the receiver is a single local
            // — mirrors the `Expr::ArraySplice` lowering. For
            // property-chained receivers we leave the parent pointing
            // at the original header; growth-induced reallocation is
            // a corner case that doesn't fire on queue-shrink usage.
            if let Expr::LocalGet(array_id) = object {
                let modified_handle = ctx.block().load(I64, &out_slot);
                let modified_box = nanbox_pointer_inline(ctx.block(), &modified_handle);
                if let Some(slot) = ctx.locals.get(array_id).cloned() {
                    ctx.block().store(DOUBLE, &modified_box, &slot);
                } else if let Some(global_name) = ctx.module_globals.get(array_id).cloned() {
                    let g_ref = format!("@{}", global_name);
                    ctx.block().store(DOUBLE, &modified_box, &g_ref);
                }
            }
            Ok(nanbox_pointer_inline(ctx.block(), &deleted_handle))
        }
        "entries" => {
            for a in args {
                let _ = lower_expr(ctx, a)?;
            }
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(I64, "js_array_entries", &[(I64, &recv_handle)]);
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "keys" => {
            for a in args {
                let _ = lower_expr(ctx, a)?;
            }
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(I64, "js_array_keys", &[(I64, &recv_handle)]);
            Ok(nanbox_pointer_inline(blk, &result))
        }
        "values" => {
            for a in args {
                let _ = lower_expr(ctx, a)?;
            }
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(I64, "js_array_values", &[(I64, &recv_handle)]);
            Ok(nanbox_pointer_inline(blk, &result))
        }
        // Issue #515 followup: `arr.with(idx, val)` reaches here when the
        // receiver passes `is_array_expr` but the HIR fold bailed (e.g. the
        // receiver is an `any`-typed local whose initializer is an Array
        // literal — `is_array_expr` recognizes this even though the binding's
        // declared type is `Type::Any`). Without this arm the catch-all below
        // silently returned the receiver unchanged.
        "with" if args.len() >= 2 => {
            let idx_d = lower_expr(ctx, &args[0])?;
            let val_d = lower_expr(ctx, &args[1])?;
            let blk = ctx.block();
            let recv_handle = unbox_to_i64(blk, &recv_box);
            let result = blk.call(
                I64,
                "js_array_with",
                &[(I64, &recv_handle), (DOUBLE, &idx_d), (DOUBLE, &val_d)],
            );
            Ok(nanbox_pointer_inline(blk, &result))
        }
        // Best-effort fallback: lower args for side effects, return
        // the receiver.
        _ => {
            for a in args {
                let _ = lower_expr(ctx, a)?;
            }
            Ok(recv_box)
        }
    }
}
