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, ¶m_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(<) {
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, ¶m_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).