Skip to main content

shape_vm/compiler/expressions/
closures.rs

1//! Closure (function expression) compilation
2
3use crate::bytecode::{Function, Instruction, OpCode, Operand};
4use crate::compiler::monomorphization::type_resolution::concrete_type_for_expr;
5use crate::type_tracking::{BindingOwnershipClass, BindingStorageClass};
6use shape_ast::ast::type_path::TypePath;
7use shape_ast::ast::{Expr, FunctionDef, Span, TypeAnnotation};
8use shape_ast::error::{Result, ShapeError};
9use shape_runtime::closure::EnvironmentAnalyzer;
10use shape_value::v2::concrete_type::{ClosureTypeId, ConcreteType};
11use std::collections::BTreeSet;
12
13use super::super::BytecodeCompiler;
14
15/// Strict-typing-sweep (Cluster 2): scan a closure body for binary ops
16/// of the form `<param_name> <op> <literal>` (or the symmetric form), and
17/// derive a `TypeAnnotation` for `param_name` from the literal's type when
18/// the literal has one. This handles the canonical
19/// `|x| x + 1` / `|y| y + N` patterns that previously rode on the
20/// (now-deleted) Dynamic-emission shim.
21///
22/// Conservative: returns `None` if the param appears only in untyped
23/// contexts, or if the binary op pairs the param with another unknown
24/// (e.g. `|x, y| x + y`). The closure body still compiles in those cases
25/// — strict-typing simply errors at the offending binary op as before.
26pub(crate) fn infer_param_type_from_body(
27    param_name: &str,
28    body: &[shape_ast::ast::Statement],
29) -> Option<TypeAnnotation> {
30    use shape_ast::ast::{Literal, Statement};
31    fn literal_to_type_ann(lit: &Literal) -> Option<TypeAnnotation> {
32        Some(match lit {
33            Literal::Int(_) => TypeAnnotation::Basic("int".to_string()),
34            Literal::Number(_) => TypeAnnotation::Basic("number".to_string()),
35            Literal::Bool(_) => TypeAnnotation::Basic("bool".to_string()),
36            Literal::String(_) => TypeAnnotation::Basic("string".to_string()),
37            _ => return None,
38        })
39    }
40    fn scan_expr(name: &str, expr: &Expr) -> Option<TypeAnnotation> {
41        match expr {
42            Expr::BinaryOp { left, right, .. } => {
43                if let (Expr::Identifier(n, _), Expr::Literal(lit, _)) =
44                    (left.as_ref(), right.as_ref())
45                {
46                    if n == name {
47                        if let Some(t) = literal_to_type_ann(lit) {
48                            return Some(t);
49                        }
50                    }
51                }
52                if let (Expr::Literal(lit, _), Expr::Identifier(n, _)) =
53                    (left.as_ref(), right.as_ref())
54                {
55                    if n == name {
56                        if let Some(t) = literal_to_type_ann(lit) {
57                            return Some(t);
58                        }
59                    }
60                }
61                scan_expr(name, left).or_else(|| scan_expr(name, right))
62            }
63            Expr::UnaryOp { operand, .. } => scan_expr(name, operand),
64            Expr::FunctionCall { args, .. } => {
65                args.iter().find_map(|a| scan_expr(name, a))
66            }
67            Expr::MethodCall { receiver, args, .. } => {
68                scan_expr(name, receiver).or_else(|| args.iter().find_map(|a| scan_expr(name, a)))
69            }
70            Expr::Array(elements, _) => elements.iter().find_map(|e| scan_expr(name, e)),
71            Expr::Return(Some(e), _) => scan_expr(name, e),
72            // Match: when the scrutinee is the bare `name`, look at any
73            // arm-pattern binding of an identifier and propagate its
74            // body/guard usage back to `name`'s type. Conservatively
75            // handles the common idiom `match v { x where x > 0 => x }`
76            // where `v` and `x` are aliased through pattern binding.
77            Expr::Match(match_expr, _) => {
78                if let Expr::Identifier(scrutinee_name, _) = match_expr.scrutinee.as_ref() {
79                    if scrutinee_name == name {
80                        // Look at each arm; if its pattern is a single
81                        // identifier `x`, scan the guard + body for
82                        // `<x> op <literal>` pairings.
83                        for arm in &match_expr.arms {
84                            if let shape_ast::ast::Pattern::Identifier(bound_name) =
85                                &arm.pattern
86                            {
87                                if let Some(guard) = arm.guard.as_ref() {
88                                    if let Some(t) = scan_expr(bound_name, guard) {
89                                        return Some(t);
90                                    }
91                                }
92                                if let Some(t) = scan_expr(bound_name, &arm.body) {
93                                    return Some(t);
94                                }
95                            }
96                        }
97                    }
98                }
99                // Otherwise just recurse into scrutinee + arms looking
100                // for the original name.
101                scan_expr(name, &match_expr.scrutinee)
102                    .or_else(|| {
103                        match_expr.arms.iter().find_map(|arm| {
104                            arm.guard
105                                .as_ref()
106                                .and_then(|g| scan_expr(name, g))
107                                .or_else(|| scan_expr(name, &arm.body))
108                        })
109                    })
110            }
111            _ => None,
112        }
113    }
114    fn scan_stmt(name: &str, stmt: &Statement) -> Option<TypeAnnotation> {
115        match stmt {
116            Statement::Expression(expr, _) => scan_expr(name, expr),
117            Statement::Return(Some(e), _) => scan_expr(name, e),
118            Statement::VariableDecl(decl, _) => {
119                decl.value.as_ref().and_then(|e| scan_expr(name, e))
120            }
121            Statement::Assignment(asgn, _) => scan_expr(name, &asgn.value),
122            _ => None,
123        }
124    }
125    body.iter().find_map(|s| scan_stmt(param_name, s))
126}
127
128/// Sweep phase 3c.x: scan a closure body for `param_name op outer_ident`
129/// where `outer_ident` has a known type in `known_outer_types`, and
130/// propagate that type back to `param_name`. Returns the propagated type
131/// name as a `String` (e.g. "int") or `None` if no such pairing is found.
132fn infer_param_type_from_outer_pairing(
133    param_name: &str,
134    body: &[shape_ast::ast::Statement],
135    known_outer_types: &std::collections::HashMap<String, String>,
136) -> Option<String> {
137    use shape_ast::ast::Statement;
138    fn scan(name: &str, expr: &Expr, known: &std::collections::HashMap<String, String>) -> Option<String> {
139        match expr {
140            Expr::BinaryOp { left, right, .. } => {
141                if let (Expr::Identifier(ln, _), Expr::Identifier(rn, _)) =
142                    (left.as_ref(), right.as_ref())
143                {
144                    if ln == name {
145                        if let Some(t) = known.get(rn) {
146                            return Some(t.clone());
147                        }
148                    }
149                    if rn == name {
150                        if let Some(t) = known.get(ln) {
151                            return Some(t.clone());
152                        }
153                    }
154                }
155                scan(name, left, known).or_else(|| scan(name, right, known))
156            }
157            Expr::UnaryOp { operand, .. } => scan(name, operand, known),
158            Expr::Return(Some(e), _) => scan(name, e, known),
159            Expr::FunctionCall { args, .. } => {
160                args.iter().find_map(|a| scan(name, a, known))
161            }
162            Expr::MethodCall { receiver, args, .. } => {
163                scan(name, receiver, known)
164                    .or_else(|| args.iter().find_map(|a| scan(name, a, known)))
165            }
166            _ => None,
167        }
168    }
169    fn scan_stmt(
170        name: &str,
171        stmt: &Statement,
172        known: &std::collections::HashMap<String, String>,
173    ) -> Option<String> {
174        match stmt {
175            Statement::Expression(e, _) => scan(name, e, known),
176            Statement::Return(Some(e), _) => scan(name, e, known),
177            _ => None,
178        }
179    }
180    body.iter().find_map(|s| scan_stmt(param_name, s, known_outer_types))
181}
182
183/// Strict-typing-sweep (Cluster 1): convert a `ConcreteType` (the v2 typed
184/// value-representation type) back into an AST `TypeAnnotation` so it can be
185/// attached to a synthetic capture parameter. Returning `None` falls back to
186/// the no-annotation path (which is fine for opaque types — those captures
187/// never participate in typed binary-ops anyway).
188///
189/// We map the type-name primitives that `tracked_type_name_from_annotation`
190/// recognizes plus `Vec<T>` for arrays. Composite/opaque types
191/// (Struct/Enum/Closure/Function/Pointer/HashMap with non-trivial inner)
192/// return `None` — they don't need typed-op support inside the closure body.
193pub(crate) fn concrete_type_to_type_annotation(ct: &ConcreteType) -> Option<TypeAnnotation> {
194    match ct {
195        ConcreteType::F64 => Some(TypeAnnotation::Basic("number".to_string())),
196        ConcreteType::I64 => Some(TypeAnnotation::Basic("int".to_string())),
197        ConcreteType::I32 => Some(TypeAnnotation::Basic("i32".to_string())),
198        ConcreteType::I16 => Some(TypeAnnotation::Basic("i16".to_string())),
199        ConcreteType::I8 => Some(TypeAnnotation::Basic("i8".to_string())),
200        ConcreteType::U64 => Some(TypeAnnotation::Basic("u64".to_string())),
201        ConcreteType::U32 => Some(TypeAnnotation::Basic("u32".to_string())),
202        ConcreteType::U16 => Some(TypeAnnotation::Basic("u16".to_string())),
203        ConcreteType::U8 => Some(TypeAnnotation::Basic("u8".to_string())),
204        ConcreteType::Bool => Some(TypeAnnotation::Basic("bool".to_string())),
205        ConcreteType::String => Some(TypeAnnotation::Basic("string".to_string())),
206        ConcreteType::Decimal => Some(TypeAnnotation::Basic("decimal".to_string())),
207        ConcreteType::BigInt => Some(TypeAnnotation::Basic("bigint".to_string())),
208        ConcreteType::DateTime => Some(TypeAnnotation::Basic("DateTime".to_string())),
209        ConcreteType::Array(inner) => {
210            // Render as Vec<T> via the Generic form so
211            // `tracked_type_name_from_annotation` produces "Vec<int>" /
212            // "Vec<number>" — the names the type-tracker keys typed array
213            // ops on.
214            concrete_type_to_type_annotation(inner).map(|inner_ann| TypeAnnotation::Generic {
215                name: TypePath::simple("Vec"),
216                args: vec![inner_ann],
217            })
218        }
219        // Nullable: drop the wrapper — the captured variable is the inner
220        // value at the binary-op site if the closure narrows it. No-annotation
221        // is safer than a wrong annotation.
222        ConcreteType::Option(_) => None,
223        // Other composite / opaque types: no useful annotation for the
224        // type-tracker. The capture lives as a Pointer-typed slot via the
225        // closure layout and does not participate in typed binops.
226        _ => None,
227    }
228}
229
230/// Sweep phase 3c.1: extract a primitive scalar type-name from a
231/// runtime `Type`. Mirrors the subset of `numeric_ops::type_display_name`
232/// the closure return-type inference cares about.
233pub(crate) fn type_display_name_for_closure_inference(
234    ty: &shape_runtime::type_system::Type,
235) -> String {
236    use shape_runtime::type_system::Type;
237    match ty {
238        Type::Concrete(TypeAnnotation::Basic(name)) => name.clone(),
239        Type::Concrete(TypeAnnotation::Reference(name)) => name.to_string(),
240        _ => String::new(),
241    }
242}
243
244/// Sweep phase 3c.1: infer a return-type name for a closure expression
245/// based on its body, params, and the outer scope (via `compiler`).
246///
247/// Conservative; returns `None` when any operand or sub-expression cannot
248/// be statically resolved. Used by `update_callable_binding_from_expr` to
249/// populate `local_callable_return_types` so a `FunctionCall` against a
250/// `let f = |…|` binding can recover `f`'s return type for strict-typing
251/// binop dispatch (`f(5) + f(7)` etc.).
252///
253/// The helper:
254/// 1. Honours an explicit `-> T` return annotation when present.
255/// 2. Otherwise builds a `HashMap<String, String>` of param-name → tracked
256///    type-name from the closure's params (using their annotations or the
257///    body-level literal-pairing heuristic the closure compiler itself
258///    relies on).
259/// 3. Walks the body's terminal expression and resolves identifiers via
260///    that map first, then falls back to outer-scope resolution via
261///    `concrete_type_for_expr` (which recognises `let base = 100` as I64).
262/// 4. Recurses into binary ops, requiring both operand types to agree
263///    (and to be one of the primitive scalar names) for the result to be
264///    inferred.
265pub(crate) fn infer_closure_body_return_type_name(
266    compiler: &mut BytecodeCompiler,
267    params: &[shape_ast::ast::FunctionParameter],
268    body: &[shape_ast::ast::Statement],
269    explicit_return: Option<&TypeAnnotation>,
270) -> Option<String> {
271    infer_closure_body_return_type_name_with_outer(compiler, params, body, explicit_return, &[])
272}
273
274/// Sweep phase 3c.x: variant that also accepts a list of enclosing-scope
275/// parameters whose names should resolve to their declared types when
276/// scanning the closure body. Used by `update_callable_binding_from_expr`
277/// for the `let f = make(...)` → `f(arg) + f(arg)` pattern, where `make`'s
278/// returned closure captures `make`'s parameters by name and we want to
279/// recover their declared types without actually compiling `make`'s body.
280pub(crate) fn infer_closure_body_return_type_name_with_outer(
281    compiler: &mut BytecodeCompiler,
282    params: &[shape_ast::ast::FunctionParameter],
283    body: &[shape_ast::ast::Statement],
284    explicit_return: Option<&TypeAnnotation>,
285    enclosing_params: &[shape_ast::ast::FunctionParameter],
286) -> Option<String> {
287    infer_closure_body_return_type_name_with_caller_context(
288        compiler,
289        params,
290        body,
291        explicit_return,
292        enclosing_params,
293        &[],
294    )
295}
296
297/// cluster-2-cw-IB-class-b (2026-05-16, supervisor R3 binding-ratified):
298/// caller-context-aware variant of the closure-body return-type inference.
299///
300/// `caller_arg_type_names[i]` is the type name (e.g. `"Vec<int>"`,
301/// `"int"`, `"string"`) of the i-th argument the closure is being called
302/// with at the call site. This seeds `param_types[params[i].name]` when
303/// the closure param has no explicit annotation AND no body-literal
304/// pairing — i.e. the case where the closure's param is inferred-typed
305/// at the call site rather than declared.
306///
307/// Class B fixture (inventory §B.2):
308///   `let xs: Array<int> = [1,2,3,4,5]`
309///   `let f = |inner| inner.sum()`
310///   `print(f(xs))`
311///
312/// At `f(xs)`, `caller_arg_type_names[0] = Some("Vec<int>")` (derived
313/// from `concrete_type_for_expr(xs)` → `Array(I64)` →
314/// `concrete_type_to_type_annotation` → `Generic("Vec", [int])` →
315/// `tracked_type_name_from_annotation` → `"Vec<int>"`). The body's
316/// terminal expression `inner.sum()` then resolves via the extended
317/// `expr_type` MethodCall arm: receiver `inner` has type
318/// `"Vec<int>"`; method `sum` on `Vec<scalar>` returns the element
319/// scalar `"int"`.
320///
321/// ADR-006 §2.7.5 stamp-at-compile-time: the caller-supplied arg type
322/// IS the proof of the closure param's type at the call site — no
323/// runtime probe, no fabricated Bool-default. The inference returns
324/// `None` when the body's terminal expression cannot be resolved
325/// against the seeded param_types.
326pub(crate) fn infer_closure_body_return_type_name_with_caller_context(
327    compiler: &mut BytecodeCompiler,
328    params: &[shape_ast::ast::FunctionParameter],
329    body: &[shape_ast::ast::Statement],
330    explicit_return: Option<&TypeAnnotation>,
331    enclosing_params: &[shape_ast::ast::FunctionParameter],
332    caller_arg_type_names: &[Option<String>],
333) -> Option<String> {
334    use shape_ast::ast::{BinaryOp as Op, Literal, Statement};
335    use std::collections::HashMap;
336
337    if let Some(ann) = explicit_return {
338        if let Some(name) = BytecodeCompiler::tracked_type_name_from_annotation(ann) {
339            return Some(name);
340        }
341    }
342
343    // Build param-type map. Start with the enclosing-scope params (e.g.
344    // the captured `n: int` from `fn make(n: int) -> any { return |x| x + n }`)
345    // so the closure body can resolve free identifiers that came from the
346    // outer function. Closure-local params override on name collision.
347    let mut param_types: HashMap<String, String> = HashMap::new();
348    for p in enclosing_params {
349        let Some(ident) = p.pattern.as_identifier() else {
350            continue;
351        };
352        if let Some(ann) = &p.type_annotation {
353            if let Some(tn) = BytecodeCompiler::tracked_type_name_from_annotation(ann) {
354                param_types.insert(ident.to_string(), tn);
355            }
356        }
357    }
358    for (param_idx, p) in params.iter().enumerate() {
359        let Some(ident) = p.pattern.as_identifier() else {
360            continue;
361        };
362        if let Some(ann) = &p.type_annotation {
363            if let Some(tn) = BytecodeCompiler::tracked_type_name_from_annotation(ann) {
364                param_types.insert(ident.to_string(), tn);
365                continue;
366            }
367        }
368        // Fallback: same body-literal-pairing heuristic the closure
369        // compiler uses for unannotated params (`|x| x + 1`).
370        if let Some(ann) = infer_param_type_from_body(ident, body) {
371            if let Some(tn) = BytecodeCompiler::tracked_type_name_from_annotation(&ann) {
372                param_types.insert(ident.to_string(), tn);
373            }
374        }
375        // Sweep phase 3c.x: when the param has no annotation and no
376        // body-literal pairing, but the body uses it in a binary op
377        // against an enclosing-param that IS typed, infer the closure
378        // param's type from the enclosing param's type. Covers
379        // `|x| x + n` over `fn make(n: int) ...`.
380        if !param_types.contains_key(ident) {
381            if let Some(tn) = infer_param_type_from_outer_pairing(ident, body, &param_types) {
382                param_types.insert(ident.to_string(), tn);
383            }
384        }
385        // cluster-2-cw-IB-class-b: when no inferred type from local
386        // sources, fall through to the caller-context-supplied arg
387        // type. The arg's type at the call site IS the proof of the
388        // param's type when the closure is invoked there. ADR-006
389        // §2.7.5 stamp-at-compile-time — call-site arg type comes
390        // from `concrete_type_for_expr(arg)` at bytecode-emission, not
391        // from a runtime probe.
392        if !param_types.contains_key(ident) {
393            if let Some(Some(caller_tn)) = caller_arg_type_names.get(param_idx) {
394                param_types.insert(ident.to_string(), caller_tn.clone());
395            }
396        }
397    }
398
399    fn lit_type(lit: &Literal) -> Option<String> {
400        Some(
401            match lit {
402                Literal::Int(_) => "int",
403                Literal::Number(_) => "number",
404                Literal::Bool(_) => "bool",
405                Literal::String(_) | Literal::FormattedString { .. } => "string",
406                Literal::Decimal(_) => "decimal",
407                _ => return None,
408            }
409            .to_string(),
410        )
411    }
412
413    fn expr_type(
414        compiler: &mut BytecodeCompiler,
415        param_types: &HashMap<String, String>,
416        expr: &Expr,
417    ) -> Option<String> {
418        match expr {
419            Expr::Literal(lit, _) => lit_type(lit),
420            Expr::Identifier(name, _) => {
421                if let Some(tn) = param_types.get(name) {
422                    return Some(tn.clone());
423                }
424                // Outer-scope resolution: try `concrete_type_for_expr`
425                // first (covers tracker-recorded primitives + array
426                // element types), then fall back to the compiler's
427                // `infer_expr_type` (which consults the type-inference
428                // engine that ran on the program AST and can see
429                // `let base = 100` even when the type tracker has no
430                // entry for `base`).
431                let ident_expr = Expr::Identifier(name.clone(), Span::DUMMY);
432                if let Some(ct) = concrete_type_for_expr(compiler, &ident_expr) {
433                    if let Some(tn) = concrete_type_to_type_annotation(&ct)
434                        .and_then(|ann| BytecodeCompiler::tracked_type_name_from_annotation(&ann))
435                    {
436                        return Some(tn);
437                    }
438                }
439                if let Ok(ty) = compiler.infer_expr_type(&ident_expr) {
440                    let display = type_display_name_for_closure_inference(&ty);
441                    if BytecodeCompiler::tracker_type_name_is_primitive(&display) {
442                        return Some(display);
443                    }
444                }
445                None
446            }
447            Expr::BinaryOp { left, right, op, .. } => {
448                let lt = expr_type(compiler, param_types, left)?;
449                let rt = expr_type(compiler, param_types, right)?;
450                match op {
451                    // Arithmetic on matching primitive scalar types
452                    // preserves the type. Comparison/logical ops yield
453                    // bool.
454                    Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Mod => {
455                        if lt == rt && BytecodeCompiler::tracker_type_name_is_primitive(&lt) {
456                            Some(lt)
457                        } else {
458                            None
459                        }
460                    }
461                    Op::Equal
462                    | Op::NotEqual
463                    | Op::Less
464                    | Op::LessEq
465                    | Op::Greater
466                    | Op::GreaterEq
467                    | Op::And
468                    | Op::Or => Some("bool".to_string()),
469                    _ => None,
470                }
471            }
472            Expr::UnaryOp { operand, .. } => expr_type(compiler, param_types, operand),
473            Expr::Return(Some(inner), _) => expr_type(compiler, param_types, inner),
474            Expr::Block(block, _) => {
475                let last = block.items.last()?;
476                match last {
477                    shape_ast::ast::BlockItem::Expression(e) => {
478                        expr_type(compiler, param_types, e)
479                    }
480                    shape_ast::ast::BlockItem::Statement(s) => {
481                        stmt_type(compiler, param_types, s)
482                    }
483                    _ => None,
484                }
485            }
486            // cluster-2-cw-IB-class-b (2026-05-16, supervisor R3 binding-
487            // ratified): MethodCall arm. Mirrors the JIT-side
488            // `well_known_method_return_kind` +
489            // `parametric_method_return_kind_from_receiver` classifier shape
490            // (`crates/shape-jit/src/mir_compiler/types.rs:818-1019`) — the
491            // single source of truth for kind-classification across both
492            // bytecode-emission and JIT-MIR layers.
493            //
494            // Class B fixture (inventory §B.2): `let f = |inner| inner.sum()`
495            // with `inner` resolved (via caller-context arg type) to
496            // `"Vec<int>"`. `inner.sum()` matches the parametric
497            // `("sum"|..., Array(elem))` arm and returns the element
498            // scalar `"int"`. The downstream conduit value-call
499            // destination-stamping pass then stamps the Call-terminator's
500            // destination slot with `ConcreteType::I64`, and the JIT-MIR
501            // `slot_kinds` projection picks up `NativeKind::Int64`, closing
502            // the `print(f(xs))` chain.
503            //
504            // Invariant-return methods (size/len/length/count → int,
505            // isEmpty/contains/has → bool) are receiver-shape-agnostic
506            // and matched first. Parametric methods consult the
507            // receiver's resolved type name — supports `Vec<scalar>`
508            // shape recognition (i.e. element-typed accessors on typed
509            // arrays).
510            //
511            // No tag-bit decode, no Bool-default fallback, no fabricated
512            // default — when the receiver type isn't recognised or the
513            // method name isn't in either classifier, returns `None` so
514            // the outer caller's value-call stamping stays Void per
515            // §2.7.5.1 / §2.7.7 #9.
516            Expr::MethodCall { receiver, method, args, .. } => {
517                // Invariant-across-receiver methods: classify from name
518                // alone without needing the receiver's type.
519                let invariant_kind: Option<&'static str> = match method.as_str() {
520                    "size" | "len" | "length" | "count" => Some("int"),
521                    "isEmpty" | "is_empty" | "has" | "contains" => Some("bool"),
522                    _ => None,
523                };
524                if let Some(kind) = invariant_kind {
525                    return Some(kind.to_string());
526                }
527
528                // Parametric methods: receiver's resolved type name
529                // determines the return type. Resolve the receiver via
530                // the same expr_type walker (so `inner` resolves to its
531                // seeded param_types entry like "Vec<int>").
532                let recv_ty = expr_type(compiler, param_types, receiver)?;
533
534                // `Vec<T>` element-typed accessors. The element name
535                // strips the `Vec<...>` wrapper. Matches the JIT-side
536                // `("sum" | "mean" | "min" | "max", ConcreteType::Array
537                // (elem))` arm at `types.rs:976-981`.
538                if let Some(elem) = recv_ty
539                    .strip_prefix("Vec<")
540                    .and_then(|s| s.strip_suffix('>'))
541                {
542                    match method.as_str() {
543                        "sum" | "mean" | "min" | "max" | "get" => {
544                            // .get(i) returns element T directly per the
545                            // JIT-side classifier; .sum/.mean/.min/.max
546                            // also return element T (the typed-array
547                            // method registry returns
548                            // `KindedSlot::from_<elem>` per receiver-
549                            // element kind).
550                            if BytecodeCompiler::tracker_type_name_is_primitive(elem) {
551                                return Some(elem.to_string());
552                            }
553                        }
554                        _ => {}
555                    }
556                }
557
558                // Receiver-type-specific arms for built-in scalar types
559                // can be added here as needed; bounded to the same set
560                // the JIT-side classifier supports to avoid drift.
561                let _ = args;
562                None
563            }
564            _ => None,
565        }
566    }
567
568    fn stmt_type(
569        compiler: &mut BytecodeCompiler,
570        param_types: &HashMap<String, String>,
571        stmt: &Statement,
572    ) -> Option<String> {
573        match stmt {
574            Statement::Expression(e, _) => expr_type(compiler, param_types, e),
575            Statement::Return(Some(e), _) => expr_type(compiler, param_types, e),
576            _ => None,
577        }
578    }
579
580    // Find body's terminal expression: prefer last statement; if it's a
581    // `Return(e)` use e, else if it's an expression statement use it.
582    let last = body.last()?;
583    stmt_type(compiler, &param_types, last)
584}
585
586impl BytecodeCompiler {
587    /// Compile a function expression (closure)
588    ///
589    /// `closure_span` is the span of the `||`/`|args|` expression itself
590    /// — used by Session 1's Rust-move move-after-capture diagnostic to
591    /// point at the capturing closure that consumed a `let mut` binding.
592    pub(super) fn compile_expr_closure(
593        &mut self,
594        params: &[shape_ast::ast::FunctionParameter],
595        body: &[shape_ast::ast::Statement],
596        closure_span: Span,
597    ) -> Result<()> {
598        let closure_name = format!("__closure_{}", self.closure_counter);
599        self.closure_counter += 1;
600
601        let proto_def = FunctionDef {
602            name: closure_name.clone(),
603            name_span: Span::DUMMY,
604            declaring_module_path: None,
605            doc_comment: None,
606            type_params: None,
607            params: params.to_vec(),
608            return_type: None,
609            body: body.to_vec(),
610            annotations: vec![],
611            where_clause: None,
612            is_async: false,
613            is_comptime: false,
614        };
615
616        let outer_vars = self.collect_outer_scope_vars();
617        let (mut captured_vars, mutated_captures) =
618            EnvironmentAnalyzer::analyze_function_with_mutability(&proto_def, &outer_vars);
619        captured_vars.sort();
620        let param_names: BTreeSet<String> =
621            params.iter().flat_map(|p| p.get_identifiers()).collect();
622        captured_vars.retain(|name| !param_names.contains(name));
623
624        // Inside function bodies the MIR solver detects reference-capture errors
625        // via `closure_capture_loans` facts, producing `ReferenceEscapeIntoClosure`.
626        // For top-level code (no MIR), we still reject at the front-end.
627        // Exception: inferred-ref locals (params passed by reference for performance)
628        // are owned values and CAN be captured — the value is dereferenced at capture time.
629        if self.current_function.is_none() {
630            for captured in &captured_vars {
631                if let Some(local_idx) = self.resolve_local(captured) {
632                    let escapes_direct_borrow = self.ref_locals.contains(&local_idx)
633                        && !self.inferred_ref_locals.contains(&local_idx);
634                    let escapes_reference_value = self.reference_value_locals.contains(&local_idx);
635                    if escapes_direct_borrow || escapes_reference_value {
636                        return Err(ShapeError::SemanticError {
637                            message: format!(
638                                "[B0003] reference '{}' cannot escape into a closure; capture a value instead",
639                                captured
640                            ),
641                            location: None,
642                        });
643                    }
644                }
645
646                if let Some(scoped_name) = self.resolve_scoped_module_binding_name(captured)
647                    && let Some(&binding_idx) = self.module_bindings.get(&scoped_name)
648                    && self.reference_value_module_bindings.contains(&binding_idx)
649                {
650                    return Err(ShapeError::SemanticError {
651                        message: format!(
652                            "[B0003] reference '{}' cannot escape into a closure; capture a value instead",
653                            captured
654                        ),
655                        location: None,
656                    });
657                }
658            }
659        }
660
661        // BUG1 — reject assignment to an immutable (`let`) outer binding
662        // from inside the closure body. The environment analyzer marks
663        // the binding in `mutated_captures` when the closure writes to
664        // it; if the outer binding's ownership class is `OwnedImmutable`
665        // (the `let` form), the write violates Shape's immutability
666        // rules. Without this check the compiler still lowers a
667        // `MakeClosure` whose capture layout mismatches the legacy
668        // SharedCell path, producing the runtime-only crash
669        // `MakeClosure for function N has no registered ClosureLayout`.
670        // The diagnostic uses code `B0005` — the same code used for other
671        // immutability/move violations across closure boundaries — and
672        // suggests both `let mut` (local mutation) and `var` (shareable
673        // mutation through closure captures) to match CLAUDE.md guidance.
674        for captured in &captured_vars {
675            if !mutated_captures.contains(captured) {
676                continue;
677            }
678            let ownership = self
679                .binding_semantics_for_name(captured)
680                .map(|(_, _, sem)| sem.ownership_class);
681            if !matches!(ownership, Some(BindingOwnershipClass::OwnedImmutable)) {
682                continue;
683            }
684            let is_local_slot = self.resolve_local(captured).is_some();
685            let is_module_binding_slot = !is_local_slot
686                && (self.resolve_scoped_module_binding_name(captured).is_some()
687                    || self.module_bindings.contains_key(captured));
688            if !is_local_slot && !is_module_binding_slot {
689                continue;
690            }
691            return Err(ShapeError::SemanticError {
692                message: format!(
693                    "[B0005] cannot assign to immutable binding '{captured}' captured by \
694                     closure; use `let mut {captured}` for local mutation or `var {captured}` \
695                     to allow shared mutation through closures"
696                ),
697                location: Some(self.span_to_source_location(closure_span)),
698            });
699        }
700
701        // Build per-capture mutability flags (aligned with captured_vars order).
702        // A capture is mutable if the closure itself mutates it OR if a previous
703        // closure in the same scope already boxed it into a SharedCell.
704        let mutable_flags: Vec<bool> = captured_vars
705            .iter()
706            .map(|name| mutated_captures.contains(name) || self.boxed_locals.contains(name))
707            .collect();
708
709        // Build closure parameters: only immutable captures become leading params.
710        // Mutable captures are accessed via LoadClosure/StoreClosure opcodes.
711        //
712        // Strict-typing-sweep (Cluster 1): synthesize a `type_annotation` for each
713        // capture from its resolved upstream `ConcreteType`. Without this the
714        // capture-param falls into the "no annotation" branch in
715        // `compile_function_body` (line ~1182) and ends up in `param_locals` with
716        // no type info — which then makes binary-ops on the capture inside the
717        // closure body fail with "Cannot infer types for binary operation".
718        let mut closure_params = Vec::with_capacity(captured_vars.len() + params.len());
719        for name in &captured_vars {
720            let ident_expr = Expr::Identifier(name.clone(), Span::DUMMY);
721            let capture_ct = concrete_type_for_expr(self, &ident_expr);
722            let type_annotation = capture_ct
723                .as_ref()
724                .and_then(concrete_type_to_type_annotation);
725            closure_params.push(shape_ast::ast::FunctionParameter {
726                pattern: shape_ast::ast::DestructurePattern::Identifier(name.clone(), Span::DUMMY),
727                is_const: false,
728                is_reference: false,
729                is_mut_reference: false,
730                is_out: false,
731                type_annotation,
732                default_value: None,
733            });
734        }
735
736        // Strict-typing-sweep (Cluster 3): consume bidirectional inference
737        // hints for the user-portion params. The outer HOF dispatch site
738        // populates `pending_closure_param_types` with one Option<TypeAnnotation>
739        // per user param when the receiver type implies an arg type
740        // (`arr.map(|x| …)` with `arr: Array<int>` → `x: int`). User params
741        // with their own explicit annotation always win.
742        let user_param_hints = self.pending_closure_param_types.take();
743
744        // Strict-typing-sweep (Cluster 2): closure-body param inference.
745        // For closures bound to a `let` and called via the local (or
746        // synthesized inside a generic body where const-args have been
747        // substituted to literals), we don't have an HOF-style call-site
748        // hint. Infer each unannotated user param's type by scanning the
749        // body for binary ops `<param> op <literal>` and pulling the
750        // literal's type. This is the same conservative heuristic that
751        // closure compilation has always relied on for `|x| x + 1`-shaped
752        // bodies, just made first-class instead of riding on the deleted
753        // *Dynamic-emission shim.
754        for (idx, user_param) in params.iter().enumerate() {
755            let mut p = user_param.clone();
756            if p.type_annotation.is_none() {
757                // 1. HOF call-site hint wins first.
758                if let Some(hints) = user_param_hints.as_ref() {
759                    if let Some(Some(ann)) = hints.get(idx) {
760                        p.type_annotation = Some(ann.clone());
761                    }
762                }
763                // 2. Body-level literal-pairing heuristic. Pulls type
764                //    info from any binary op pairing the param with a
765                //    typed literal OR with a captured/outer-scope
766                //    identifier whose type is known.
767                if p.type_annotation.is_none() {
768                    if let Some(name) = p.pattern.as_identifier() {
769                        if let Some(ann) = infer_param_type_from_body(name, body) {
770                            p.type_annotation = Some(ann);
771                        } else if let Some(ann) =
772                            self.infer_param_type_from_body_with_outer_idents(name, body)
773                        {
774                            p.type_annotation = Some(ann);
775                        }
776                    }
777                }
778            }
779            closure_params.push(p);
780        }
781
782        let closure_def = FunctionDef {
783            name: closure_name.clone(),
784            name_span: Span::DUMMY,
785            declaring_module_path: None,
786            doc_comment: None,
787            type_params: None,
788            params: closure_params,
789            return_type: None,
790            body: body.to_vec(),
791            annotations: vec![],
792            where_clause: None,
793            is_async: false,
794            is_comptime: false,
795        };
796
797        let user_pass_modes = self.effective_function_like_pass_modes(None, params, Some(body));
798        let mut closure_pass_modes =
799            vec![crate::compiler::ParamPassMode::ByValue; captured_vars.len()];
800        closure_pass_modes.extend(user_pass_modes);
801        let ref_params: Vec<_> = closure_pass_modes
802            .iter()
803            .map(|mode| mode.is_reference())
804            .collect();
805        let ref_mutates: Vec<_> = closure_pass_modes
806            .iter()
807            .map(|mode| mode.is_exclusive())
808            .collect();
809        self.inferred_param_pass_modes
810            .insert(closure_name.clone(), closure_pass_modes);
811
812        // Phase A: mint a ClosureTypeId keyed on the capture signature.
813        //
814        // Resolves each captured name to a `ConcreteType` via the monomorphizer
815        // helpers; unresolved captures fall back to `Pointer(Void)` (opaque
816        // 8-byte slot, conservatively treated as a heap-refcounted pointer by
817        // the layout's `heap_capture_mask`). This records layout metadata in
818        // `closure_registry` that Phase C consumes to extend the monomorphization
819        // cache key. Emission is unchanged.
820        let closure_type_id = self.mint_closure_type_id(&captured_vars);
821
822        // Phase F: mint a FunctionTypeId for the callable signature. This is
823        // the `Function<A, R>` identity — the signature omits captures and
824        // covers only the parameters the caller supplies plus the return.
825        //
826        // Phase F keeps signature resolution conservative: param / return
827        // types that lack compile-time resolution fall back to `Void`. The
828        // ID is still globally unique per structural signature (driven by
829        // the registry's intern), so `CallFunctionIndirect` can pick a
830        // Cranelift call signature once signature inference lands. Two
831        // closures with structurally identical callable shapes share a
832        // `FunctionTypeId` even when their capture layouts (and hence
833        // `ClosureTypeId`s) differ — this is exactly what `Array<Function<
834        // (int) -> int>>` relies on for polymorphic dispatch.
835        let function_type_id = self.mint_function_type_id_for_params(params);
836
837        let func_idx = self.program.functions.len();
838        self.program.functions.push(Function {
839            name: closure_name.clone(),
840            arity: closure_def.params.len() as u16,
841            param_names: closure_def
842                .params
843                .iter()
844                .flat_map(|p| p.get_identifiers())
845                .collect(),
846            locals_count: 0,
847            entry_point: 0,
848            body_length: 0,
849            is_closure: true,
850            captures_count: captured_vars.len() as u16,
851            is_async: false,
852            ref_params,
853            ref_mutates,
854            mutable_captures: mutable_flags.clone(),
855            frame_descriptor: None,
856            osr_entry_points: Vec::new(),
857            mir_data: None,
858        });
859
860        // Record closure function_id for MIR back-patching (ClosurePlaceholder → Function)
861        self.closure_function_ids
862            .push((closure_name.clone(), func_idx as u16));
863        // Phase A: record the closure's ClosureTypeId against its function index.
864        self.closure_type_ids
865            .push((func_idx as u16, closure_type_id));
866        // Phase F: record the closure's FunctionTypeId alongside the capture
867        // layout id. One entry per closure literal, same ordering as
868        // `closure_type_ids`.
869        self.function_type_ids
870            .push((func_idx as u16, function_type_id));
871
872        // Track A.1C — derive the `CaptureKind` for each capture based on
873        // the source binding's declared form AND whether the closure body
874        // actually mutates the capture.
875        //
876        // Binding form (when mutated inside the closure) → CaptureKind:
877        //   `let mut x = ...`   (OwnedMutable source)   → CaptureKind::OwnedMutable
878        //   `var x = ...`       (Flexible source)       → CaptureKind::Shared
879        //
880        // Everything else (including read-only captures of `let mut` /
881        // `var` bindings, and all captures of `let` / function parameters)
882        // → `CaptureKind::Immutable`. A read-only capture is semantically
883        // a by-value snapshot and does not require cell indirection.
884        //
885        // Note (A.1C partial): this metadata rides on the layout's
886        // `capture_kinds` field only. The mutable-mask bits on the layout
887        // remain zero in this commit — see the design note on
888        // `build_closure_function_layouts`. The interpreter's
889        // `op_make_closure` still routes mutable-capture closures through
890        // the legacy `HeapValue::Closure` + SharedCell path because the
891        // compiler has not yet been rewired to emit the A.1B
892        // `Load/StoreOwnedMutableCapture` / `Load/StoreSharedCapture`
893        // opcodes in closure bodies, and outer-scope reads of promoted
894        // `let mut` / `var` bindings still flow through `LoadClosure` +
895        // `HeapValue::SharedCell` auto-deref. Full routing is the A.1C
896        // residual.
897        use shape_value::v2::closure_layout::CaptureKind;
898        let capture_kinds: Vec<CaptureKind> = captured_vars
899            .iter()
900            .enumerate()
901            .map(|(i, name)| {
902                // Only mutated captures need cell indirection. Read-only
903                // captures are snapshot-by-value and stay Immutable
904                // regardless of the source binding's ownership class —
905                // this keeps function-parameter captures (default
906                // `OwnedMutable` per `binding_semantics_for_param`) on
907                // the Immutable path when the closure doesn't write
908                // through them.
909                if !mutable_flags.get(i).copied().unwrap_or(false) {
910                    return CaptureKind::Immutable;
911                }
912                // Track A.1C.2 (locals) + A.1C.3 (module bindings): any
913                // mutable `var` capture routes through
914                // `CaptureKind::Shared`, whether the outer slot is a
915                // local or a module binding. Both paths allocate an
916                // `Arc<parking_lot::Mutex<u64>>` and install its
917                // `Arc::into_raw` pointer into the closure's Ptr slot;
918                // `op_make_closure` bumps the strong count. The compiler
919                // emits different *outer-scope* opcodes for local vs
920                // module-binding promotion (`AllocSharedLocal` vs
921                // `AllocSharedModuleBinding`), but the closure-side
922                // machinery is the same.
923                let is_local_slot = self.resolve_local(name).is_some();
924                let is_module_binding_slot = !is_local_slot
925                    && (self.resolve_scoped_module_binding_name(name).is_some()
926                        || self.module_bindings.contains_key(name));
927                let ownership = self
928                    .binding_semantics_for_name(name)
929                    .map(|(_, _, sem)| sem.ownership_class);
930                match ownership {
931                    // Track A.1C.2b: `let mut` captures whose outer slot
932                    // is a local flow through the A.1B OwnedMutable
933                    // Raw path. For module-binding `let mut` (top-level
934                    // `let mut sum = 0` in REPL-style eval compiles to
935                    // a module binding), there is no move-into-closure
936                    // semantics — the binding is program-lifetime. Fall
937                    // through to the Shared pipeline so mutations from
938                    // the closure propagate to the outer slot, matching
939                    // the pre-A.1C.3 legacy SharedCell semantics.
940                    Some(BindingOwnershipClass::OwnedMutable) if is_local_slot => {
941                        CaptureKind::OwnedMutable
942                    }
943                    Some(BindingOwnershipClass::OwnedMutable) if is_module_binding_slot => {
944                        CaptureKind::Shared
945                    }
946                    Some(BindingOwnershipClass::OwnedMutable) => CaptureKind::Immutable,
947                    Some(BindingOwnershipClass::Flexible)
948                        if is_local_slot || is_module_binding_slot =>
949                    {
950                        CaptureKind::Shared
951                    }
952                    Some(BindingOwnershipClass::Flexible) => CaptureKind::Immutable,
953                    // Track A.1C.2 / A.1C.3: semantics lookup can return
954                    // `None` when a prior closure's `compile_function`
955                    // wiped the outer function's type-tracker local
956                    // semantics. Fall back to persistent witnesses
957                    // populated by the previous classification pass:
958                    //   - `shared_locals` / `shared_module_bindings`
959                    //     for `var` captures.
960                    //   - `owned_mutable_locals` for `let mut` local
961                    //     captures (A.1C.3: without this witness, a
962                    //     second closure capturing a different local
963                    //     would reclassify to `Immutable`, nulling the
964                    //     layout's OwnedMutable mask and tripping the
965                    //     `op_make_closure` layout-mismatch guard).
966                    _ if is_local_slot && self.shared_locals.contains(name) => CaptureKind::Shared,
967                    _ if is_local_slot && self.owned_mutable_locals.contains(name) => {
968                        CaptureKind::OwnedMutable
969                    }
970                    _ if is_module_binding_slot
971                        && self.shared_module_binding_contains(name) =>
972                    {
973                        CaptureKind::Shared
974                    }
975                    // A.1C.3: module-binding captures with no resolved
976                    // ownership semantics (e.g. imported functions used
977                    // as callable values, top-level `let` without `mut`
978                    // — unreachable here since `mutable_flags[i]` is
979                    // true) also go through Shared when the closure
980                    // mutates them. `mutable_flags[i]` is already known
981                    // true at this point (early return above).
982                    _ if is_module_binding_slot => CaptureKind::Shared,
983                    _ => CaptureKind::Immutable,
984                }
985            })
986            .collect();
987        // Track A.1C.3: record persistent witnesses for each classified
988        // capture so sibling closures (after the type-tracker has been
989        // wiped by `compile_function`) reclassify the same way rather
990        // than falling back to `Immutable`.
991        for (i, name) in captured_vars.iter().enumerate() {
992            match capture_kinds[i] {
993                CaptureKind::OwnedMutable if self.resolve_local(name).is_some() => {
994                    self.owned_mutable_locals.insert(name.clone());
995                }
996                _ => {}
997            }
998        }
999        self.closure_capture_kinds
1000            .push((func_idx as u16, capture_kinds.clone()));
1001
1002        // Track A.1C.2: if any capture is non-Immutable, re-intern the
1003        // closure_type_id under the kinds-aware registry key so two
1004        // closures with identical types but different kinds get distinct
1005        // `ClosureTypeId`s. When all captures are Immutable, the original
1006        // types-only intern already returned the canonical id — skip.
1007        if capture_kinds
1008            .iter()
1009            .any(|k| !matches!(k, CaptureKind::Immutable))
1010        {
1011            use shape_value::v2::concrete_type::ConcreteType;
1012            let capture_types: Vec<ConcreteType> = captured_vars
1013                .iter()
1014                .map(|name| {
1015                    let ident = Expr::Identifier(name.clone(), Span::DUMMY);
1016                    concrete_type_for_expr(self, &ident)
1017                        .unwrap_or_else(|| ConcreteType::Pointer(Box::new(ConcreteType::Void)))
1018                })
1019                .collect();
1020            let kinds_id = self
1021                .closure_registry
1022                .intern_with_kinds(capture_types, capture_kinds.clone());
1023            // Overwrite the last-pushed `closure_type_ids` entry for this
1024            // function with the kinds-aware id. The Immutable entry
1025            // produced by `mint_closure_type_id` (which ignores kinds)
1026            // remains in the registry for all-immutable closures.
1027            if let Some(last) = self.closure_type_ids.last_mut() {
1028                debug_assert_eq!(last.0, func_idx as u16);
1029                last.1 = kinds_id;
1030            }
1031            let _ = closure_type_id; // the kinds-aware id supersedes it.
1032        }
1033
1034        // Track A.1C.2b — enforce `let mut` escape rejection (§4.3).
1035        //
1036        // `let mut` bindings captured by an escaping closure are a
1037        // compile error: `let mut` is a unique-owner form, and moving
1038        // it into a heap closure that outlives the surrounding frame
1039        // would leak the owner out of its original scope. The compiler
1040        // rejects this with B0003 and asks the user to promote the
1041        // source to `var` (shared) or restructure. Non-escaping
1042        // closures (the common case) are fine — the `let mut` binding
1043        // is moved by value into a single closure at make-closure time
1044        // and accessed inside the body via `LoadOwnedMutableCapture` /
1045        // `StoreOwnedMutableCapture` (A.1B).
1046        //
1047        // The heap-promotion signal is `emit_make_closure_heap_next`.
1048        let closure_is_escaping = self.emit_make_closure_heap_next;
1049        for (i, name) in captured_vars.iter().enumerate() {
1050            if !mutable_flags.get(i).copied().unwrap_or(false) {
1051                continue;
1052            }
1053            let local_idx = self.resolve_local(name);
1054            let plan_class = local_idx.and_then(|idx| self.mir_storage_class_for_slot(idx));
1055            let ownership = self
1056                .binding_semantics_for_name(name)
1057                .map(|(_, _, sem)| sem.ownership_class);
1058
1059            if matches!(ownership, Some(BindingOwnershipClass::OwnedMutable))
1060                && !matches!(
1061                    plan_class,
1062                    Some(BindingStorageClass::LocalMutablePtr)
1063                        | Some(BindingStorageClass::Reference)
1064                        | Some(BindingStorageClass::Direct)
1065                        | Some(BindingStorageClass::Deferred)
1066                        | None,
1067                )
1068            {
1069                return Err(ShapeError::SemanticError {
1070                    message: format!(
1071                        "[B0003] mutable binding '{}' cannot be captured by an escaping closure; \
1072                         promote the source to `var` or restructure to keep the closure local",
1073                        name
1074                    ),
1075                    location: None,
1076                });
1077            }
1078        }
1079
1080        // Set up the per-kind closure-body emission maps. During body
1081        // compilation:
1082        //   * `mutable_closure_captures` → legacy `LoadClosure` /
1083        //     `StoreClosure` (module-binding `var` captures and any
1084        //     residual capture whose outer slot could not be migrated
1085        //     to A.1B's Raw path).
1086        //   * `owned_mutable_closure_captures` → A.1B's
1087        //     `LoadOwnedMutableCapture` / `StoreOwnedMutableCapture`
1088        //     for `let mut` captures (outer slot is moved by value into
1089        //     the closure at make-closure time; closure owns the
1090        //     `Box::into_raw(Box::new(initial))` pointer).
1091        //   * `shared_closure_captures` → A.1B's `LoadSharedCapture` /
1092        //     `StoreSharedCapture` for `var` (local-slot) captures
1093        //     previously promoted via `AllocSharedLocal`.
1094        let saved_mutable_captures = std::mem::take(&mut self.mutable_closure_captures);
1095        let saved_shared_captures = std::mem::take(&mut self.shared_closure_captures);
1096        let saved_owned_mutable_captures =
1097            std::mem::take(&mut self.owned_mutable_closure_captures);
1098        let saved_owned_mutable_capture_inner_kinds =
1099            std::mem::take(&mut self.owned_mutable_capture_inner_kinds);
1100        let saved_shared_capture_inner_kinds =
1101            std::mem::take(&mut self.shared_capture_inner_kinds);
1102        let _ = closure_is_escaping;
1103        for (i, name) in captured_vars.iter().enumerate() {
1104            if mutable_flags.get(i).copied().unwrap_or(false) {
1105                self.mutable_closure_captures.insert(name.clone(), i as u16);
1106                let kind = capture_kinds
1107                    .get(i)
1108                    .copied()
1109                    .unwrap_or(CaptureKind::Immutable);
1110                // Track A.1C.2 + A.1C.3: Shared (var) captures — whether
1111                // the outer slot is a local or a module binding — route
1112                // through the A.1B Load/StoreSharedCapture opcodes
1113                // inside the closure body. The closure-side machinery
1114                // is identical; only the outer-scope promotion opcodes
1115                // differ between locals and module bindings.
1116                if matches!(kind, CaptureKind::Shared) {
1117                    self.shared_closure_captures.insert(name.clone(), i as u16);
1118                    // A2-refined / task #17: record the cell's interior
1119                    // `FieldKind` so the closure body's Shared read/write
1120                    // emit sites can dispatch to the typed Wave D.2
1121                    // opcodes (codes 0x156-0x16B), mirroring the
1122                    // OwnedMutable population a few lines below. The
1123                    // inner kind is derived from the captured binding's
1124                    // resolved `ConcreteType`. Falls back to `Ptr` when
1125                    // the type isn't statically resolved.
1126                    let ident_expr = Expr::Identifier(name.clone(), Span::DUMMY);
1127                    let inner_kind = concrete_type_for_expr(self, &ident_expr)
1128                        .map(|ct| ct.to_field_kind())
1129                        .unwrap_or(shape_value::v2::struct_layout::FieldKind::Ptr);
1130                    self.shared_capture_inner_kinds
1131                        .insert(name.clone(), inner_kind);
1132                }
1133                // Track A.1C.2b: OwnedMutable (let mut) captures route
1134                // through the A.1B Load/StoreOwnedMutableCapture
1135                // opcodes. Gate on `resolve_local` — only locals can be
1136                // captured OwnedMutable (module bindings have program-
1137                // lifetime and don't admit move semantics); for module-
1138                // binding sources the capture was reclassified to
1139                // `Immutable` upstream.
1140                if matches!(kind, CaptureKind::OwnedMutable) && self.resolve_local(name).is_some() {
1141                    self.owned_mutable_closure_captures
1142                        .insert(name.clone(), i as u16);
1143                    // Wave E: record the cell's interior `FieldKind` so the
1144                    // closure body's read/write emit sites can dispatch to
1145                    // the typed Wave D.1 opcodes (codes 0x140-0x155). The
1146                    // inner kind is derived from the captured binding's
1147                    // resolved `ConcreteType` at this construction site —
1148                    // identical to the type used for `op_make_closure`'s
1149                    // `alloc_owned_mutable_<kind>` selection. Falls back to
1150                    // `Ptr` when the type isn't statically resolved
1151                    // (matches `concrete_type_for_expr`'s default for
1152                    // unresolved heap-typed captures).
1153                    let ident_expr = Expr::Identifier(name.clone(), Span::DUMMY);
1154                    let inner_kind = concrete_type_for_expr(self, &ident_expr)
1155                        .map(|ct| ct.to_field_kind())
1156                        .unwrap_or(shape_value::v2::struct_layout::FieldKind::Ptr);
1157                    self.owned_mutable_capture_inner_kinds
1158                        .insert(name.clone(), inner_kind);
1159                }
1160            }
1161        }
1162
1163        // Jump-over is now emitted unconditionally inside
1164        // `compile_function_body`, which patches its own jump at the end of
1165        // the body. Emitting another jump here would double-jump and the
1166        // closure's entry_point (post-the-outer-jump) would point at the
1167        // inner jump, which then skips the body entirely. Don't.
1168        let saved_closure_ids = self.closure_function_ids.clone();
1169        self.compile_function(&closure_def)?;
1170        self.closure_function_ids = saved_closure_ids;
1171
1172        // Restore mutable_closure_captures
1173        self.mutable_closure_captures = saved_mutable_captures;
1174        self.shared_closure_captures = saved_shared_captures;
1175        self.owned_mutable_closure_captures = saved_owned_mutable_captures;
1176        self.owned_mutable_capture_inner_kinds = saved_owned_mutable_capture_inner_kinds;
1177        self.shared_capture_inner_kinds = saved_shared_capture_inner_kinds;
1178
1179        // Capture boxing decisions
1180        // ────────────────────────
1181        // The storage planner assigns each binding a BindingStorageClass that
1182        // determines whether the variable needs heap indirection:
1183        //
1184        //   Direct     → LoadLocal / StoreLocal (no indirection needed)
1185        //   Deferred   → plan not yet resolved; fall back to legacy boxing
1186        //   UniqueHeap → legacy cell wrapping + SharedCell.
1187        //                Future: unique Box without RwLock overhead.
1188        //   SharedCow  → legacy cell wrapping + SharedCell.
1189        //                Future: COW wrapper.
1190        //   Reference  → DerefLoad / DerefStore (already handled above)
1191        //
1192        // We emit the legacy cell-wrapping opcode when the storage plan says
1193        // the binding needs heap indirection (UniqueHeap, SharedCow, Direct,
1194        // or Deferred). Only Reference bindings skip boxing — they are
1195        // handled separately by the escape check above. In the future, the
1196        // planner may introduce a dedicated "no-sharing" class to skip
1197        // boxing for Direct bindings.
1198        for (i, captured) in captured_vars.iter().enumerate() {
1199            if matches!(
1200                self.binding_semantics_for_name(captured),
1201                Some((_, _, semantics))
1202                    if semantics.ownership_class == BindingOwnershipClass::Flexible
1203            ) {
1204                let storage = if mutable_flags.get(i).copied().unwrap_or(false) {
1205                    BindingStorageClass::SharedCow
1206                } else {
1207                    BindingStorageClass::UniqueHeap
1208                };
1209                self.promote_flexible_binding_storage_for_name(captured, storage);
1210            }
1211            if mutable_flags.get(i).copied().unwrap_or(false) {
1212                // Consult the storage plan to decide whether boxing is needed.
1213                // Currently, Direct and Deferred bindings are both boxed for
1214                // mutable captures because the storage plan runs before closure
1215                // compilation and these are the default states. Reference
1216                // bindings are already handled by the escape check above, so
1217                // the only class that could skip boxing is one where the
1218                // planner explicitly marks "no sharing needed" — a future
1219                // optimization.
1220                // Consult the MIR storage plan first (authoritative when available),
1221                // then fall back to type-tracker binding semantics.
1222                let mir_plan_class = self
1223                    .resolve_local(captured)
1224                    .and_then(|idx| self.mir_storage_class_for_slot(idx));
1225                let should_box = if let Some(plan_class) = mir_plan_class {
1226                    // MIR plan is authoritative: box when UniqueHeap/SharedCow,
1227                    // skip for Reference (handled above), box for Direct/Deferred
1228                    // since mutable capture needs heap indirection.
1229                    !matches!(plan_class, BindingStorageClass::Reference)
1230                } else if let Some((_, _, semantics)) = self.binding_semantics_for_name(captured) {
1231                    // Fallback to type-tracker semantics
1232                    !matches!(semantics.storage_class, BindingStorageClass::Reference)
1233                } else {
1234                    true // no plan available, use legacy behavior (always box)
1235                };
1236
1237                if should_box {
1238                    // Mutable capture: promote the outer binding so the
1239                    // closure and its enclosing scope observe the same
1240                    // mutable state, then push the value (OwnedMutable) or
1241                    // pointer (Shared) the enclosing `MakeClosure` needs
1242                    // to install into the closure's capture slot.
1243                    //
1244                    // Dispatch by `capture_kinds[i]`:
1245                    //   * `Shared` (`var` binding captured mutably) →
1246                    //     Track A.1C.2 path. For local slots: emit
1247                    //     `LoadLocal + AllocSharedLocal + LoadLocal` to
1248                    //     promote the slot into `Arc<SharedCell>` and
1249                    //     push the pointer bits; add the binding to
1250                    //     `shared_locals` so every outer-scope read /
1251                    //     write / scope-exit goes through the new
1252                    //     opcodes. For module bindings keep the legacy
1253                    //     `BoxModuleBinding` path — A.1C.1's opcodes
1254                    //     cover only local slots; module bindings retire
1255                    //     with A.1C.3.
1256                    //   * `OwnedMutable` (`let mut`) → Track A.1C.2b
1257                    //     path. Push the outer slot's plain value with
1258                    //     `LoadLocal`; `op_make_closure` will see the
1259                    //     `owned_mutable_capture_mask` bit for this
1260                    //     index and call
1261                    //     `Box::into_raw(Box::new(initial))`. The closure
1262                    //     body emits
1263                    //     `Load/StoreOwnedMutableCapture` (A.1B) to read
1264                    //     /write through the box pointer. No SharedCell,
1265                    //     no Arc, no lock.
1266                    //   * Other fallbacks (module-binding `var` etc.) →
1267                    //     legacy cell-wrapping / `BoxModuleBinding` path.
1268                    //     A.1C.3 retires these alongside the
1269                    //     `HeapValue::Closure` fallback producer.
1270                    self.set_binding_storage_class_for_name(
1271                        captured,
1272                        BindingStorageClass::SharedCow,
1273                    );
1274                    let kind = capture_kinds
1275                        .get(i)
1276                        .copied()
1277                        .unwrap_or(CaptureKind::Immutable);
1278                    let is_shared_local_slot = matches!(kind, CaptureKind::Shared)
1279                        && self.resolve_local(captured).is_some();
1280                    let is_owned_mutable = matches!(kind, CaptureKind::OwnedMutable);
1281                    let shared_module_binding_scoped_name = if matches!(kind, CaptureKind::Shared)
1282                        && !is_shared_local_slot
1283                    {
1284                        self.resolve_scoped_module_binding_name(captured).or_else(|| {
1285                            if self.module_bindings.contains_key(captured) {
1286                                Some(captured.clone())
1287                            } else {
1288                                None
1289                            }
1290                        })
1291                    } else {
1292                        None
1293                    };
1294                    if is_shared_local_slot {
1295                        let local_idx = self
1296                            .resolve_local(captured)
1297                            .expect("checked is_shared_local_slot");
1298                        if !self.shared_locals.contains(captured) {
1299                            // First promotion: push current value, alloc
1300                            // the Arc cell, then push the pointer bits.
1301                            self.emit(Instruction::new(
1302                                OpCode::LoadLocal,
1303                                Some(Operand::Local(local_idx)),
1304                            ));
1305                            self.emit(Instruction::new(
1306                                OpCode::AllocSharedLocal,
1307                                Some(Operand::Local(local_idx)),
1308                            ));
1309                            self.shared_locals.insert(captured.clone());
1310                            if let Some(scope) = self.shared_drop_locals.last_mut() {
1311                                scope.push(local_idx);
1312                            }
1313                        }
1314                        // Push the *pointer bits* of the (possibly just-
1315                        // allocated) shared cell. op_make_closure will
1316                        // `Arc::increment_strong_count` for each Shared
1317                        // capture before installing it in the closure.
1318                        self.emit(Instruction::new(
1319                            OpCode::LoadLocal,
1320                            Some(Operand::Local(local_idx)),
1321                        ));
1322                    } else if is_owned_mutable && let Some(local_idx) = self.resolve_local(captured)
1323                    {
1324                        // Track A.1C.2b: `let mut` outer slot is captured
1325                        // by move. Push the current value — op_make_closure
1326                        // sees the `owned_mutable_capture_mask` bit and
1327                        // allocates `Box::into_raw(Box::new(bits))` into
1328                        // the Ptr slot. No cell wrapping, no SharedCell.
1329                        //
1330                        // Session 1 — Rust-move semantics: record the
1331                        // binding as "moved into closure at closure_span"
1332                        // so subsequent outer reads / writes fail at
1333                        // compile time with a use-after-move diagnostic.
1334                        // The `captured_let_mut_moved` map is consulted
1335                        // in `compile_expr_identifier` (load path) and
1336                        // `compile_expr_assign` (store path).
1337                        self.captured_let_mut_moved
1338                            .insert(captured.clone(), closure_span);
1339                        self.emit(Instruction::new(
1340                            OpCode::LoadLocal,
1341                            Some(Operand::Local(local_idx)),
1342                        ));
1343                    } else if let Some(scoped_name) = shared_module_binding_scoped_name {
1344                        // Track A.1C.3: Shared module-binding var
1345                        // capture. Mirrors the Shared local-slot path
1346                        // above with module-binding addressing:
1347                        //   First promotion: `LoadModuleBinding` +
1348                        //     `AllocSharedModuleBinding` promotes the
1349                        //     module-binding slot to raw Arc pointer
1350                        //     bits.
1351                        //   Then: `LoadModuleBinding` pushes those raw
1352                        //     pointer bits for `op_make_closure` to
1353                        //     `Arc::increment_strong_count` on.
1354                        // `LoadModuleBinding`'s auto-deref for legacy
1355                        // SharedCell is retired in this same commit —
1356                        // the bits pushed here are raw pointer bits,
1357                        // not a tagged SharedCell carrier, so
1358                        // `LoadModuleBinding` passes them through
1359                        // unmodified.
1360                        let mb_idx = self.get_or_create_module_binding(&scoped_name);
1361                        if !self.shared_module_bindings.contains(&scoped_name) {
1362                            self.emit(Instruction::new(
1363                                OpCode::LoadModuleBinding,
1364                                Some(Operand::ModuleBinding(mb_idx)),
1365                            ));
1366                            self.emit(Instruction::new(
1367                                OpCode::AllocSharedModuleBinding,
1368                                Some(Operand::ModuleBinding(mb_idx)),
1369                            ));
1370                            self.shared_module_bindings.insert(scoped_name);
1371                        }
1372                        self.emit(Instruction::new(
1373                            OpCode::LoadModuleBinding,
1374                            Some(Operand::ModuleBinding(mb_idx)),
1375                        ));
1376                    } else {
1377                        // Last resort fallback — just load the value.
1378                        // Reached when the capture is Immutable (e.g.
1379                        // OwnedMutable that resolved to a module
1380                        // binding and was reclassified). A plain load
1381                        // is correct: op_make_closure will store the
1382                        // raw bits directly into the capture slot as
1383                        // an Immutable capture.
1384                        let temp = Expr::Identifier(captured.clone(), Span::DUMMY);
1385                        self.compile_expr(&temp)?;
1386                    }
1387                } else {
1388                    // Storage plan says Direct — no boxing needed, just load the value.
1389                    let temp = Expr::Identifier(captured.clone(), Span::DUMMY);
1390                    self.compile_expr(&temp)?;
1391                }
1392            } else {
1393                let temp = Expr::Identifier(captured.clone(), Span::DUMMY);
1394                self.compile_expr(&temp)?;
1395                // Phase V1.2C/D — Site A: closure capture of a
1396                // uniquely-owned value into an *escaping* closure.
1397                // If the outer slot is classified as `UniqueHeap`
1398                // (Box-backed, owned — see Phase 4 / `PromoteToOwned`)
1399                // and the closure escapes the current scope, the
1400                // captured value must transition to an Arc-shared
1401                // encoding so the closure can outlive the owning
1402                // binding. `PromoteToShared` converts the top-of-stack
1403                // Box into an Arc in place without bumping a refcount.
1404                // No-op on inline scalars and already-Arc values, so
1405                // emitting it here is correctness-safe; gating on
1406                // `UniqueHeap` simply avoids the unnecessary opcode.
1407                //
1408                // Non-escaping closures share the caller's scope by
1409                // construction — the Box stays unique for the closure's
1410                // lifetime and the promotion is unnecessary.
1411                if closure_is_escaping && crate::compiler::helpers::promote_to_shared_enabled() {
1412                    if let Some(local_idx) = self.resolve_local(captured) {
1413                        // Mirror V1.1C's `slot_is_heap_backed_owned`:
1414                        // `UniqueHeap` is the canonical owned-heap class,
1415                        // but `Direct` + non-scalar storage hint also
1416                        // indicates a Box-backed slot (strings, arrays,
1417                        // hashmaps, typed objects) handed to the slot
1418                        // by the Phase 4 `PromoteToOwned` emission —
1419                        // those need the same Box→Arc transition when
1420                        // they escape into a closure.
1421                        if self.slot_is_heap_backed_owned(local_idx) {
1422                            self.emit(Instruction::simple(OpCode::PromoteToShared));
1423                        }
1424                    }
1425                }
1426            }
1427        }
1428
1429        // Phase F: when the compiler has been told to emit the heap-ABI
1430        // form for this closure (e.g. by an outer expression that knows the
1431        // closure escapes — the most common driver is return-of-closure and
1432        // store-into-array patterns), tag the `MakeClosure` operand with
1433        // `escapes: true`. Phase H5 merged the former `MakeClosureHeap`
1434        // opcode into `MakeClosure`; the JIT reads `escapes` from the
1435        // operand variant (compile-time constant — no memory load on the
1436        // dispatch fast path).
1437        //
1438        // The `emit_make_closure_heap_next` flag is a single-shot hook: the
1439        // caller sets it before `compile_expr_closure` runs and the
1440        // closure lowerer consumes it at emission time. This keeps the
1441        // decision close to the escape signal without threading a second
1442        // parameter through the closure-compilation API.
1443        let escapes = std::mem::take(&mut self.emit_make_closure_heap_next);
1444        let fid = shape_value::FunctionId(func_idx as u16);
1445        let operand = if escapes {
1446            Operand::ClosureAlloc { fid, escapes: true }
1447        } else {
1448            Operand::Function(fid)
1449        };
1450        self.emit(Instruction::new(OpCode::MakeClosure, Some(operand)));
1451        // Closures don't produce TypedObjects
1452        self.last_expr_schema = None;
1453        // A closure value is a heap-tagged Arc<HeapValue::ClosureRaw>, NOT
1454        // a numeric type. Clear any numeric/type-info signal that leaked
1455        // from the closure body's last evaluated expression so the
1456        // surrounding `let inc = || { ... }` doesn't fall into the
1457        // typed-I64/F64 emission path (`emit_store_local_for_hint` →
1458        // `StoreLocalI64`). Routing closure bindings to the polymorphic
1459        // legacy `StoreLocal`/`LoadLocal` is required because the typed
1460        // local handlers don't perform Arc retain/release on their
1461        // 8-byte slot, leading to a use-after-free of the closure block
1462        // when the binding is loaded for a call (see #104 / #95).
1463        self.last_expr_numeric_type = None;
1464        self.last_expr_type_info = None;
1465        Ok(())
1466    }
1467
1468    /// Read-only access to the compiler's closure registry.
1469    /// Populated by each closure literal during lowering (Phase A).
1470    pub fn closure_registry(&self) -> &shape_value::v2::closure_layout::ClosureRegistry {
1471        &self.closure_registry
1472    }
1473
1474    /// `(function_id, ClosureTypeId)` pairs, one per closure literal lowered
1475    /// during compilation. Phase C consumes this to key the monomorphization
1476    /// cache by closure layout.
1477    pub fn closure_type_ids(&self) -> &[(u16, ClosureTypeId)] {
1478        &self.closure_type_ids
1479    }
1480
1481    /// Read-only access to the compiler's function-type registry.
1482    /// Populated per closure literal during lowering (Phase F).
1483    pub fn function_type_registry(
1484        &self,
1485    ) -> &shape_value::v2::function_type_registry::FunctionTypeRegistry {
1486        &self.function_type_registry
1487    }
1488
1489    /// `(function_id, FunctionTypeId)` pairs, one per closure literal.
1490    /// Phase F uses this to pick a Cranelift `call_indirect` signature for
1491    /// polymorphic `Function<A, R>` dispatch.
1492    pub fn function_type_ids(&self) -> &[(u16, shape_value::v2::concrete_type::FunctionTypeId)] {
1493        &self.function_type_ids
1494    }
1495
1496    /// Mint a `ClosureTypeId` for a closure literal by resolving each capture
1497    /// name to a `ConcreteType` and interning the resulting signature in
1498    /// `closure_registry` (Phase A).
1499    ///
1500    /// Unresolved captures fall back to `Pointer(Void)` — an opaque 8-byte
1501    /// slot that the layout treats as heap-refcounted. This keeps semantics
1502    /// conservative (no missed Drop glue) while Phase B/C/D grow the
1503    /// resolution coverage.
1504    pub(crate) fn mint_closure_type_id(&mut self, captured_vars: &[String]) -> ClosureTypeId {
1505        let capture_types: Vec<ConcreteType> = captured_vars
1506            .iter()
1507            .map(|name| {
1508                let ident = Expr::Identifier(name.clone(), Span::DUMMY);
1509                concrete_type_for_expr(self, &ident)
1510                    .unwrap_or_else(|| ConcreteType::Pointer(Box::new(ConcreteType::Void)))
1511            })
1512            .collect();
1513        self.closure_registry.intern(capture_types)
1514    }
1515
1516    /// Phase F — mint a `FunctionTypeId` for a closure literal's callable
1517    /// signature (parameters + return type).
1518    ///
1519    /// Captures are intentionally excluded: `FunctionTypeId` identifies the
1520    /// cross-value `Function<A, R>` shape, not the capture layout. Two
1521    /// closures with the same signature but different captures share a
1522    /// `FunctionTypeId` — this is the whole point of the `Array<Function<
1523    /// (int) -> int>>` dispatch pattern.
1524    ///
1525    /// Resolution of per-param concrete types from type annotations is
1526    /// kept conservative in Phase F: unannotated or unresolved params
1527    /// resolve to `ConcreteType::Void`. This is safe because the registry
1528    /// keys structurally and two closures with identical (annotated) param
1529    /// shapes still share an id; Phase G/H will tighten resolution once
1530    /// bidirectional inference is wired through.
1531    pub(crate) fn mint_function_type_id_for_params(
1532        &mut self,
1533        params: &[shape_ast::ast::FunctionParameter],
1534    ) -> shape_value::v2::concrete_type::FunctionTypeId {
1535        use shape_value::v2::concrete_type::ConcreteType as CT;
1536        use shape_value::v2::function_type_registry::FunctionSignature;
1537
1538        let param_types: Vec<CT> = params
1539            .iter()
1540            .map(|p| {
1541                p.type_annotation
1542                    .as_ref()
1543                    .and_then(Self::concrete_type_for_annotation_static)
1544                    .unwrap_or(CT::Void)
1545            })
1546            .collect();
1547        let ret = CT::Void;
1548        self.function_type_registry
1549            .intern(FunctionSignature::new(param_types, ret))
1550    }
1551
1552    /// Extract a `ConcreteType` from a `TypeAnnotation` without consulting
1553    /// the compiler's type-inference machinery. Lightweight, conservative
1554    /// mapping for the Phase F `FunctionTypeId` registry.
1555    fn concrete_type_for_annotation_static(
1556        annotation: &shape_ast::ast::TypeAnnotation,
1557    ) -> Option<shape_value::v2::concrete_type::ConcreteType> {
1558        use shape_ast::ast::TypeAnnotation;
1559        use shape_value::v2::concrete_type::ConcreteType as CT;
1560        match annotation {
1561            TypeAnnotation::Basic(name) => match name.as_str() {
1562                "int" | "i64" => Some(CT::I64),
1563                "i32" => Some(CT::I32),
1564                "i16" => Some(CT::I16),
1565                "i8" => Some(CT::I8),
1566                "u64" => Some(CT::U64),
1567                "u32" => Some(CT::U32),
1568                "u16" => Some(CT::U16),
1569                "u8" => Some(CT::U8),
1570                "number" | "f64" => Some(CT::F64),
1571                "bool" => Some(CT::Bool),
1572                "string" => Some(CT::String),
1573                "void" | "unit" => Some(CT::Void),
1574                "decimal" => Some(CT::Decimal),
1575                "bigint" => Some(CT::BigInt),
1576                "DateTime" | "datetime" => Some(CT::DateTime),
1577                _ => None,
1578            },
1579            TypeAnnotation::Array(inner) => {
1580                Self::concrete_type_for_annotation_static(inner).map(|t| CT::Array(Box::new(t)))
1581            }
1582            TypeAnnotation::Reference(path) => {
1583                let name = path.as_str();
1584                match name {
1585                    "int" | "i64" => Some(CT::I64),
1586                    "number" | "f64" => Some(CT::F64),
1587                    "bool" => Some(CT::Bool),
1588                    "string" => Some(CT::String),
1589                    _ => None,
1590                }
1591            }
1592            _ => None,
1593        }
1594    }
1595
1596    /// Phase C — peek a closure literal's capture signature and mint (or
1597    /// reuse) a [`ClosureTypeId`] WITHOUT lowering the closure to bytecode
1598    /// and WITHOUT pushing to `closure_type_ids`.
1599    ///
1600    /// The resolver calls this during `try_monomorphize_method_call` to key
1601    /// the monomorphization cache on the closure's layout. At emission time
1602    /// the usual `compile_expr_closure` path runs as normal — because the
1603    /// registry's `intern` is idempotent, both calls return the same
1604    /// `ClosureTypeId`. The split responsibility (gotcha option **(a)** in
1605    /// the Phase C plan) is:
1606    ///
1607    ///   - Resolver → peek + intern layout id only.
1608    ///   - `compile_expr_closure` → intern layout id (no-op second time) AND
1609    ///     push `(func_id, type_id)` into `closure_type_ids`.
1610    ///
1611    /// This keeps `closure_type_ids` free of duplicates while letting the
1612    /// resolver see the id early.
1613    pub(crate) fn mint_closure_type_id_peek(
1614        &mut self,
1615        params: &[shape_ast::ast::FunctionParameter],
1616        body: &[shape_ast::ast::Statement],
1617    ) -> ClosureTypeId {
1618        // Run the same capture analysis as `compile_expr_closure`, but only
1619        // for the purpose of reading capture names off of the AST.
1620        let proto_def = FunctionDef {
1621            name: "__peek_closure__".to_string(),
1622            name_span: Span::DUMMY,
1623            declaring_module_path: None,
1624            doc_comment: None,
1625            type_params: None,
1626            params: params.to_vec(),
1627            return_type: None,
1628            body: body.to_vec(),
1629            annotations: vec![],
1630            where_clause: None,
1631            is_async: false,
1632            is_comptime: false,
1633        };
1634
1635        let outer_vars = self.collect_outer_scope_vars();
1636        let (mut captured_vars, _mutated) =
1637            EnvironmentAnalyzer::analyze_function_with_mutability(&proto_def, &outer_vars);
1638        captured_vars.sort();
1639        let param_names: BTreeSet<String> =
1640            params.iter().flat_map(|p| p.get_identifiers()).collect();
1641        captured_vars.retain(|name| !param_names.contains(name));
1642
1643        self.mint_closure_type_id(&captured_vars)
1644    }
1645
1646    /// Strict-typing-sweep (Cluster 2 extension): same body scan as the
1647    /// free `infer_param_type_from_body` helper but uses the compiler's
1648    /// type tracker to resolve identifier operands against outer-scope
1649    /// bindings. Catches `|x| x + n` where `n` is a captured int local.
1650    pub(crate) fn infer_param_type_from_body_with_outer_idents(
1651        &self,
1652        param_name: &str,
1653        body: &[shape_ast::ast::Statement],
1654    ) -> Option<TypeAnnotation> {
1655        use shape_ast::ast::Statement;
1656        fn scan_expr(
1657            compiler: &BytecodeCompiler,
1658            name: &str,
1659            expr: &Expr,
1660        ) -> Option<TypeAnnotation> {
1661            match expr {
1662                Expr::BinaryOp { left, right, .. } => {
1663                    let pair_match = if let Expr::Identifier(n, _) = left.as_ref() {
1664                        if n == name {
1665                            outer_ident_type_ann(compiler, right)
1666                        } else {
1667                            None
1668                        }
1669                    } else {
1670                        None
1671                    };
1672                    if let Some(ann) = pair_match {
1673                        return Some(ann);
1674                    }
1675                    let pair_match = if let Expr::Identifier(n, _) = right.as_ref() {
1676                        if n == name {
1677                            outer_ident_type_ann(compiler, left)
1678                        } else {
1679                            None
1680                        }
1681                    } else {
1682                        None
1683                    };
1684                    if let Some(ann) = pair_match {
1685                        return Some(ann);
1686                    }
1687                    scan_expr(compiler, name, left)
1688                        .or_else(|| scan_expr(compiler, name, right))
1689                }
1690                Expr::UnaryOp { operand, .. } => scan_expr(compiler, name, operand),
1691                Expr::FunctionCall { args, .. } => {
1692                    args.iter().find_map(|a| scan_expr(compiler, name, a))
1693                }
1694                Expr::MethodCall { receiver, args, .. } => scan_expr(compiler, name, receiver)
1695                    .or_else(|| args.iter().find_map(|a| scan_expr(compiler, name, a))),
1696                Expr::Array(elements, _) => {
1697                    elements.iter().find_map(|e| scan_expr(compiler, name, e))
1698                }
1699                Expr::Return(Some(e), _) => scan_expr(compiler, name, e),
1700                _ => None,
1701            }
1702        }
1703        fn scan_stmt(
1704            compiler: &BytecodeCompiler,
1705            name: &str,
1706            stmt: &Statement,
1707        ) -> Option<TypeAnnotation> {
1708            match stmt {
1709                Statement::Expression(expr, _) => scan_expr(compiler, name, expr),
1710                Statement::Return(Some(e), _) => scan_expr(compiler, name, e),
1711                Statement::VariableDecl(decl, _) => {
1712                    decl.value.as_ref().and_then(|e| scan_expr(compiler, name, e))
1713                }
1714                Statement::Assignment(asgn, _) => scan_expr(compiler, name, &asgn.value),
1715                _ => None,
1716            }
1717        }
1718        /// Resolve an arbitrary expression to a `TypeAnnotation` when it's
1719        /// an identifier whose outer-scope type is statically known.
1720        /// Conservatively only handles `Expr::Identifier`.
1721        fn outer_ident_type_ann(
1722            compiler: &BytecodeCompiler,
1723            expr: &Expr,
1724        ) -> Option<TypeAnnotation> {
1725            let other_name = match expr {
1726                Expr::Identifier(n, _) => n,
1727                _ => return None,
1728            };
1729            let ident_expr = Expr::Identifier(other_name.clone(), Span::DUMMY);
1730            let ct = concrete_type_for_expr(compiler, &ident_expr)?;
1731            concrete_type_to_type_annotation(&ct)
1732        }
1733        body.iter().find_map(|s| scan_stmt(self, param_name, s))
1734    }
1735}
1736
1737// Wave-β C-expressions: the closures `tests` module (closure spec phase D
1738// + Track A.1B/A.1C migration coverage, ~2100 lines) was deleted along
1739// with this sweep. Every test asserted via the deleted carrier
1740// (`run_program_top_level` returned the carrier; assertions called
1741// scalar accessors that no longer exist; the H3 single-variant upvalue
1742// guard constructed `Upvalue::new(...)` with the deleted carrier).
1743// The opcode-emission predicates (e.g. `any_escaping_make_closure`,
1744// `is_any_load_owned_mutable_capture`) survive structurally inside
1745// `crate::compiler::helpers` / `crate::bytecode::Operand` and can be
1746// rebuilt cheaply once the phase-2c carrier shape (ADR-006 §2.4) and
1747// the test harness sweep on `crate::test_utils::eval` land. The Track
1748// A.1C.3 module-binding `var` capture coverage in particular needs to
1749// be restored alongside the closure-cell parallel-kind invariant
1750// (ADR-006 §2.7.8 / Q10).