Skip to main content

shape_vm/compiler/monomorphization/
cache.rs

1//! Specialization cache for monomorphized generic functions.
2//!
3//! Each generic function (`fn map<T, U>(arr: Array<T>, f: (T) -> U) -> Array<U>`)
4//! is compiled lazily, once per concrete type-argument tuple it is called with.
5//! The cache maps a stable [`mono_key`](#mono_key-format) string to the index
6//! of the compiled specialization in
7//! [`crate::bytecode::BytecodeProgram::functions`].
8//!
9//! # mono_key format
10//!
11//! The key is `"<base_fn_name>::<type1>_<type2>_..."`, where each `typeN` is
12//! the result of [`shape_value::v2::ConcreteType::mono_key`]. Examples:
13//!
14//! - `"identity::i64"`
15//! - `"map::i64_string"`
16//! - `"reduce::array_f64_i64"`
17//!
18//! Use [`build_mono_key`] to construct keys consistently across the compiler.
19
20use shape_ast::ast::TypeAnnotation;
21use shape_ast::error::{Result, ShapeError};
22use shape_value::v2::ConcreteType;
23use std::collections::HashMap;
24
25use crate::compiler::BytecodeCompiler;
26use crate::compiler::monomorphization::substitution;
27use crate::compiler::monomorphization::substitution::concrete_to_annotation;
28use crate::compiler::monomorphization::type_resolution::{
29    ClosureSpec, ComptimeConstValue, build_mono_key_full, build_mono_key_with_consts,
30    comptime_const_value_from_literal_expr, split_type_and_const_param_names,
31};
32
33/// Phase C — per-module specialization budget.
34///
35/// Once the per-module closure-specialization count exceeds this threshold
36/// the compiler falls back to the existing (non-inlined) generic dispatch
37/// path. Prevents unbounded code-size growth for programs that generate
38/// hundreds of distinct closure types. See §3.4 of
39/// `docs/v2-closure-specialization.md` for the rationale.
40pub const DEFAULT_CLOSURE_SPECIALIZATION_BUDGET: u32 = 64;
41
42/// Cache mapping a monomorphization key to the compiled function index.
43///
44/// The cache is owned by [`crate::compiler::BytecodeCompiler`] and lives for
45/// the duration of one compilation session. It is parallel to (not a
46/// replacement for) `const_specializations`, which keys on compile-time const
47/// argument values.
48#[derive(Debug, Default, Clone)]
49pub struct MonomorphizationCache {
50    entries: HashMap<String, u16>,
51}
52
53impl MonomorphizationCache {
54    /// Create an empty cache.
55    pub fn new() -> Self {
56        Self {
57            entries: HashMap::new(),
58        }
59    }
60
61    /// Look up a previously specialized function by its mono key.
62    ///
63    /// Returns the function index in `BytecodeProgram::functions`, or `None`
64    /// if no specialization has been recorded for this key yet.
65    pub fn lookup(&self, mono_key: &str) -> Option<u16> {
66        self.entries.get(mono_key).copied()
67    }
68
69    /// Record that the function compiled at `function_idx` is the
70    /// specialization for `mono_key`.
71    ///
72    /// If the key already exists, the existing entry is overwritten — callers
73    /// should `lookup` first if they need to detect duplicates.
74    pub fn insert(&mut self, mono_key: String, function_idx: u16) {
75        self.entries.insert(mono_key, function_idx);
76    }
77
78    /// Number of distinct specializations currently cached.
79    pub fn len(&self) -> usize {
80        self.entries.len()
81    }
82
83    /// Whether the cache is empty.
84    pub fn is_empty(&self) -> bool {
85        self.entries.is_empty()
86    }
87
88    /// Iterate over `(mono_key, function_idx)` pairs.
89    ///
90    /// Useful for diagnostics and incremental-compilation snapshots.
91    pub fn iter(&self) -> impl Iterator<Item = (&String, &u16)> {
92        self.entries.iter()
93    }
94
95    /// Iterate over the cached `mono_key` strings.
96    ///
97    /// Used by diagnostics and by Agent 4's integration tests, which assert
98    /// that specific keys land in the cache after compiling generic call
99    /// sites.
100    pub fn keys(&self) -> impl Iterator<Item = &String> {
101        self.entries.keys()
102    }
103}
104
105/// Construct a monomorphization key from a base function name and a tuple of
106/// concrete type arguments.
107///
108/// The key shape is `"<base_fn_name>::<ct1>_<ct2>_..."`. With no type
109/// arguments the key is just the base name.
110///
111/// This is a thin wrapper over
112/// [`crate::compiler::monomorphization::type_resolution::build_mono_key_with_consts`]
113/// passing an empty const-args slice. The two functions are guaranteed
114/// byte-for-byte identical for type-only inputs.
115pub fn build_mono_key(base_fn_name: &str, type_args: &[ConcreteType]) -> String {
116    build_mono_key_with_consts(base_fn_name, type_args, &[])
117}
118
119/// Phase C — construct a mono key that includes per-closure-arg
120/// specialization segments. Thin re-export of
121/// [`build_mono_key_full`] so external callers can go through the cache
122/// module uniformly.
123pub fn build_mono_key_with_closures(
124    base_fn_name: &str,
125    type_args: &[ConcreteType],
126    closure_specs: &[ClosureSpec],
127) -> String {
128    build_mono_key_full(base_fn_name, type_args, &[], closure_specs)
129}
130
131/// Phase 3a (Option β): map a `ConcreteType` to the type-name string used by
132/// `type_implements_trait`. The mapping mirrors what
133/// [`concrete_to_annotation`] produces for the primitive cases — `I64 →
134/// "int"`, `F64 → "number"`, `String → "string"`, etc. — so trait-impl
135/// lookups against the registered primitive impls (registered in
136/// `register_operator_traits`) hit.
137///
138/// Composite types (`Array<int>`, `HashMap<…>`) collapse to their head name
139/// for now (e.g. `"Array"`). The trait registry's keys are head-name based,
140/// so e.g. `impl Iterable for Array` succeeds for any element type.
141fn concrete_type_to_type_name(ct: &ConcreteType) -> String {
142    match concrete_to_annotation(ct) {
143        TypeAnnotation::Basic(name) => name,
144        TypeAnnotation::Reference(path) => path.name().to_string(),
145        TypeAnnotation::Generic { name, .. } => name.name().to_string(),
146        // Tuple/Function/Closure/etc. — fall back to the mono-key form, which
147        // is at least stable. Trait impls for these are not in scope for
148        // Phase 3a (no `<T: Tup3>` bounds exist).
149        _ => ct.mono_key(),
150    }
151}
152
153impl BytecodeCompiler {
154    /// Phase 3a (Option β foundation): enforce trait bounds on a generic
155    /// function's type parameters at the specialization site.
156    ///
157    /// Each `<T: Bound>` declaration on a generic function constrains the
158    /// concrete type a call site may bind. If the resolved `ConcreteType`
159    /// for a bounded param does not implement every declared bound, this
160    /// returns a precise diagnostic — Rust-style monomorphizing dispatch
161    /// (no `dyn Trait`).
162    ///
163    /// Bounds are checked against the per-process trait registry on
164    /// `type_inference.env`. Primitive impls of `Eq`/`Ord` (and any
165    /// user-declared `impl Trait for Type` blocks already processed) are
166    /// visible there.
167    ///
168    /// Const-kind params have empty bounds (per
169    /// `TypeParam::trait_bounds`), so they are skipped.
170    pub(crate) fn check_trait_bounds_at_specialization(
171        &self,
172        base_fn_name: &str,
173        original_def: &shape_ast::ast::FunctionDef,
174        subs: &HashMap<String, ConcreteType>,
175    ) -> Result<()> {
176        let Some(type_params) = original_def.type_params.as_ref() else {
177            return Ok(());
178        };
179        for tp in type_params {
180            if tp.is_const() {
181                continue;
182            }
183            let bounds = tp.trait_bounds();
184            if bounds.is_empty() {
185                continue;
186            }
187            let Some(bound_ct) = subs.get(tp.name()) else {
188                continue;
189            };
190            let target_type_name = concrete_type_to_type_name(bound_ct);
191            for bound in bounds {
192                let trait_name = bound.name();
193                if !self
194                    .type_inference
195                    .env
196                    .type_implements_trait(&target_type_name, trait_name)
197                {
198                    return Err(ShapeError::SemanticError {
199                        message: format!(
200                            "trait bound not satisfied: type `{}` does not implement trait `{}` \
201                             (required by `{}<{}: {}>`)",
202                            target_type_name,
203                            trait_name,
204                            base_fn_name,
205                            tp.name(),
206                            trait_name,
207                        ),
208                        location: None,
209                    });
210                }
211            }
212        }
213        Ok(())
214    }
215
216    /// Ensure a monomorphized specialization of `base_fn_name` for the given
217    /// concrete type arguments exists in the bytecode program. Returns the
218    /// function index of the specialized function.
219    ///
220    /// On a cache hit, this is a constant-time lookup.
221    ///
222    /// On a cache miss, the original `FunctionDef` is fetched from
223    /// `function_defs`, cloned, and handed to Agent 2's substitution helpers
224    /// to produce a type-specialized clone. The clone is then registered and
225    /// compiled via the normal pipeline, and its index is recorded in the
226    /// cache before being returned.
227    ///
228    /// `type_args` is a positional list aligned to the callee's declared
229    /// `type_params`: `type_args[i]` binds `def.type_params[i]`. If the
230    /// callee declares no type parameters or the arity does not match, an
231    /// error is returned.
232    ///
233    /// # Errors
234    ///
235    /// - `Err(...)` if `base_fn_name` is not a known function in the current
236    ///   compiler state.
237    /// - `Err(...)` if the callee declares no type parameters but type
238    ///   arguments were supplied.
239    /// - `Err(...)` if `type_args.len()` does not match the number of declared
240    ///   type parameters.
241    /// - Any compile error returned by `compile_function` for the
242    ///   substituted body.
243    pub fn ensure_monomorphic_function(
244        &mut self,
245        base_fn_name: &str,
246        type_args: &[ConcreteType],
247    ) -> Result<u16> {
248        // B.3 — if the callee declares any const generic parameters, auto-bind
249        // them from their declared default expressions (literals only today —
250        // no call-site `::<4>` turbofish syntax exists yet) and route through
251        // the const-aware entry point. This keeps every caller of the type-only
252        // API working unchanged while still producing distinct mono keys per
253        // distinct const value.
254        //
255        // The resolution rule, per the Track-B B.3 plan:
256        //   - literal default on the const param  → bind immediately
257        //   - missing default / non-literal       → compile error
258        //     ("const generic arg must be a compile-time constant")
259        //
260        // Comptime-evaluation of arbitrary default expressions is intentionally
261        // out of scope here and deferred to a follow-up.
262        if let Some(type_params) = self
263            .function_defs
264            .get(base_fn_name)
265            .and_then(|d| d.type_params.clone())
266        {
267            if type_params.iter().any(|tp| tp.is_const()) {
268                let const_args = resolve_const_defaults_or_error(base_fn_name, &type_params)?;
269                return self.ensure_monomorphic_function_with_consts(
270                    base_fn_name,
271                    type_args,
272                    &const_args,
273                );
274            }
275        }
276
277        let mono_key = build_mono_key(base_fn_name, type_args);
278
279        if let Some(existing) = self.monomorphization_cache.lookup(&mono_key) {
280            return Ok(existing);
281        }
282
283        // BUG3 — cycle detector. If this exact `(base_fn_name, type_args)`
284        // specialization is already being compiled further down the call
285        // stack, its cache entry has not been written yet (that happens after
286        // `register_function` / `find_function` below). A transitive attempt
287        // to resolve the same specialization — e.g. a generic body whose
288        // `x.type()` dispatch path re-enters monomorphization for the same
289        // `(type, method)` pair — would recurse forever. Refuse to descend a
290        // second time and let the caller fall back to the generic path.
291        if self.monomorphization_in_progress.contains(&mono_key) {
292            return Err(ShapeError::SemanticError {
293                message: format!(
294                    "ensure_monomorphic_function: cycle detected while specializing '{}' — the generic body transitively resolves to itself",
295                    mono_key
296                ),
297                location: None,
298            });
299        }
300
301        // Look up the original FunctionDef AST. The bytecode compiler always
302        // populates `function_defs` during `register_function`, so this is
303        // the canonical store for substitution input.
304        let original_def = self.function_defs.get(base_fn_name).cloned().ok_or_else(|| {
305            ShapeError::SemanticError {
306                message: format!(
307                    "ensure_monomorphic_function: no FunctionDef AST recorded for '{}'",
308                    base_fn_name
309                ),
310                location: None,
311            }
312        })?;
313
314        // Build the {type-param-name -> ConcreteType} substitution map that
315        // Agent 2's `substitute_function_def` consumes. This requires the
316        // callee's declared `type_params` to align positionally with the
317        // supplied `type_args`. Const-kind params are filtered out here (they
318        // contribute nothing to type substitution) — the short-circuit above
319        // already redirected any callee-with-const-params to the const-aware
320        // entry point, so this path only sees type-kind params.
321        let declared_type_params: Vec<String> = original_def
322            .type_params
323            .as_ref()
324            .map(|tps| {
325                tps.iter()
326                    .filter(|tp| !tp.is_const())
327                    .map(|tp| tp.name().to_string())
328                    .collect()
329            })
330            .unwrap_or_default();
331
332        if declared_type_params.is_empty() {
333            return Err(ShapeError::SemanticError {
334                message: format!(
335                    "ensure_monomorphic_function: '{}' declares no type parameters but {} type arguments were supplied",
336                    base_fn_name,
337                    type_args.len()
338                ),
339                location: None,
340            });
341        }
342        if declared_type_params.len() != type_args.len() {
343            return Err(ShapeError::SemanticError {
344                message: format!(
345                    "ensure_monomorphic_function: '{}' declares {} type parameters but {} type arguments were supplied",
346                    base_fn_name,
347                    declared_type_params.len(),
348                    type_args.len()
349                ),
350                location: None,
351            });
352        }
353
354        let subs: HashMap<String, ConcreteType> = declared_type_params
355            .iter()
356            .cloned()
357            .zip(type_args.iter().cloned())
358            .collect();
359
360        // Phase 3a (Option β foundation): trait-bound checking at the
361        // specialization site. See `check_trait_bounds_at_specialization`.
362        self.check_trait_bounds_at_specialization(base_fn_name, &original_def, &subs)?;
363
364        // Substitute type parameters throughout the cloned AST. Agent 2's
365        // helper renames the function deterministically using
366        // `mono_key_from_subs`, so the new name is unique per (base, subs).
367        let specialized_def = substitution::substitute_function_def(&original_def, &subs);
368        let specialized_name = specialized_def.name.clone();
369
370        // Register the new function definition in the program. This populates
371        // `function_defs`, `function_arity_bounds`, etc.
372        self.register_function(&specialized_def)?;
373        let specialization_idx_usize =
374            self.find_function(&specialized_name).ok_or_else(|| {
375                ShapeError::SemanticError {
376                    message: format!(
377                        "ensure_monomorphic_function: failed to register specialization '{}'",
378                        specialized_name
379                    ),
380                    location: None,
381                }
382            })?;
383        let specialization_idx: u16 =
384            specialization_idx_usize.try_into().map_err(|_| ShapeError::SemanticError {
385                message: format!(
386                    "ensure_monomorphic_function: function index {} for '{}' overflows u16",
387                    specialization_idx_usize, specialized_name
388                ),
389                location: None,
390            })?;
391
392        // Cache the index BEFORE compiling the body so recursive calls inside
393        // the specialized function self-reference the same cache entry instead
394        // of recursively re-monomorphizing.
395        self.monomorphization_cache
396            .insert(mono_key.clone(), specialization_idx);
397        self.next_monomorphization_id = self.next_monomorphization_id.saturating_add(1);
398
399        // BUG3 — mark this key as in-progress before compiling the body. The
400        // cache insert above handles direct self-recursive calls to the same
401        // specialization, but a transitive resolver path (e.g. a dispatch
402        // helper that re-triggers monomorphization for the same `(name,
403        // type_args)` pair via a different entry point) would otherwise
404        // recurse forever. The entry is removed below regardless of success.
405        self.monomorphization_in_progress.insert(mono_key.clone());
406
407        // F7: save/restore `closure_function_ids` across the recursive
408        // `compile_function` call. The specialized body's own
409        // `compile_function` ends with a `.clear()` that would otherwise
410        // wipe the OUTER function's accumulated `("__closure_N", fid)`
411        // list — for example, when `__main__`'s body is still being walked
412        // between two closure literals that sandwich an `arr.map(|x| ...)`
413        // call. Without this guard the outer back-patcher pairs the wrong
414        // `ClosureCapture` with the wrong `function_id`, tripping the
415        // MIR-to-IR `capture-count mismatch` assertion.
416        let saved_closure_function_ids =
417            std::mem::take(&mut self.closure_function_ids);
418        // v0.3 WS-6: save/restore the per-function local ConcreteType table
419        // across the nested specialized-body `compile_function`. That call
420        // clears + repopulates `current_function_local_concrete_types` for
421        // the specialization's own local slots; without the save/restore the
422        // OUTER (caller) function's compilation would resume with the
423        // specialization's slot entries still installed, mis-resolving a
424        // later monomorphization call site's argument types against a stale
425        // foreign-function slot index.
426        let saved_local_concrete_types =
427            std::mem::take(&mut self.current_function_local_concrete_types);
428        // Compile the specialized body. On failure, surface the error — the
429        // caller is responsible for falling through to the generic path on
430        // any failure mode it wants to tolerate.
431        let result = self.compile_function(&specialized_def);
432        self.closure_function_ids = saved_closure_function_ids;
433        self.current_function_local_concrete_types = saved_local_concrete_types;
434        self.monomorphization_in_progress.remove(&mono_key);
435        result?;
436
437        Ok(specialization_idx)
438    }
439
440    /// Ensure a monomorphized specialization of `base_fn_name` for the given
441    /// type AND const generic arguments exists. Returns the function index of
442    /// the specialized function.
443    ///
444    /// This is the const-generic-aware sibling of
445    /// [`Self::ensure_monomorphic_function`]. The cache key incorporates both
446    /// the type args and the const args (see [`build_mono_key_with_consts`]),
447    /// so the same callee specialised twice with different `N` values
448    /// (`repeat<3>` vs `repeat<5>`) produces two cache entries; specialised
449    /// twice with the same `N` produces one. The same applies to mixed
450    /// type+const generic functions (`fn matrix<T, const ROWS: int>(...)`).
451    ///
452    /// **Grammar gap**: as of Phase 5 the grammar does not yet allow declaring
453    /// const generic params, so the `const_args` slice is empty for every
454    /// real call site. This entry point exists so that the cache + naming +
455    /// substitution path is exercised by tests today and is ready to wire up
456    /// the moment the grammar adds `<const N: int>`.
457    ///
458    /// On a cache hit, this is a constant-time lookup.
459    ///
460    /// On a cache miss, the original `FunctionDef` is fetched from
461    /// `function_defs`, cloned, and substituted by
462    /// [`substitution::substitute_function_def_with_consts`]. The clone is
463    /// then registered, compiled, and recorded in the cache.
464    ///
465    /// `type_args` is positional against `def.type_params` (the type-kind
466    /// generics). `const_args` is positional against the const-kind generics
467    /// in declaration order. When the grammar exposes a way to mark a generic
468    /// as `const`, the alignment logic here will need to interleave them
469    /// correctly — see the TODO inside the body.
470    ///
471    /// # Errors
472    ///
473    /// Same as [`Self::ensure_monomorphic_function`], plus:
474    ///
475    /// - `Err(...)` if the type args don't satisfy the same arity / presence
476    ///   constraints as the type-only path.
477    pub fn ensure_monomorphic_function_with_consts(
478        &mut self,
479        base_fn_name: &str,
480        type_args: &[ConcreteType],
481        const_args: &[ComptimeConstValue],
482    ) -> Result<u16> {
483        // Fast path: no const args at all → reuse the existing entry point so
484        // type-only callers stay byte-for-byte identical.
485        if const_args.is_empty() {
486            return self.ensure_monomorphic_function(base_fn_name, type_args);
487        }
488
489        let mono_key = build_mono_key_with_consts(base_fn_name, type_args, const_args);
490
491        if let Some(existing) = self.monomorphization_cache.lookup(&mono_key) {
492            return Ok(existing);
493        }
494
495        // BUG3 — cycle detector (see `ensure_monomorphic_function`).
496        if self.monomorphization_in_progress.contains(&mono_key) {
497            return Err(ShapeError::SemanticError {
498                message: format!(
499                    "ensure_monomorphic_function_with_consts: cycle detected while specializing '{}' — the generic body transitively resolves to itself",
500                    mono_key
501                ),
502                location: None,
503            });
504        }
505
506        let original_def = self.function_defs.get(base_fn_name).cloned().ok_or_else(|| {
507            ShapeError::SemanticError {
508                message: format!(
509                    "ensure_monomorphic_function_with_consts: no FunctionDef AST recorded for '{}'",
510                    base_fn_name
511                ),
512                location: None,
513            }
514        })?;
515
516        // Partition the declared generic params into type-kind names and
517        // const-kind names (positional against `type_args` / `const_args`
518        // respectively). This became tractable in B.2 when `TypeParam` grew
519        // a `Const` variant — prior to B.3 the wiring here fell back to
520        // synthetic `__const_<i>` names because there was no split.
521        let (type_param_names, const_param_names) = original_def
522            .type_params
523            .as_ref()
524            .map(|tps| split_type_and_const_param_names(tps))
525            .unwrap_or_default();
526
527        if type_param_names.len() != type_args.len() {
528            return Err(ShapeError::SemanticError {
529                message: format!(
530                    "ensure_monomorphic_function_with_consts: '{}' declares {} type parameters but {} type arguments were supplied",
531                    base_fn_name,
532                    type_param_names.len(),
533                    type_args.len()
534                ),
535                location: None,
536            });
537        }
538
539        if const_param_names.len() != const_args.len() {
540            return Err(ShapeError::SemanticError {
541                message: format!(
542                    "ensure_monomorphic_function_with_consts: '{}' declares {} const generic parameters but {} const arguments were supplied",
543                    base_fn_name,
544                    const_param_names.len(),
545                    const_args.len()
546                ),
547                location: None,
548            });
549        }
550
551        let type_subs: HashMap<String, ConcreteType> = type_param_names
552            .iter()
553            .cloned()
554            .zip(type_args.iter().cloned())
555            .collect();
556
557        // Phase 3a — bound check (same as type-only path) before substitution.
558        self.check_trait_bounds_at_specialization(base_fn_name, &original_def, &type_subs)?;
559
560        // Now that const generic params carry real names (B.2 `TypeParam::Const`),
561        // key `const_subs` by the declared name. The substitution pass in
562        // `substitution::substitute_function_def_with_consts` rewrites any
563        // body-position `Identifier(N)` that matches this name to the bound
564        // literal value. If the callee never references the name in its body
565        // (common for the B.3 integration tests), the substitution pass is a
566        // no-op — the mono-key differentiation is still the load-bearing bit.
567        let const_subs: HashMap<String, ComptimeConstValue> = const_param_names
568            .iter()
569            .cloned()
570            .zip(const_args.iter().cloned())
571            .collect();
572
573        let specialized_def = substitution::substitute_function_def_with_consts(
574            &original_def,
575            &type_subs,
576            &const_subs,
577            &mono_key,
578        );
579        let specialized_name = specialized_def.name.clone();
580
581        self.register_function(&specialized_def)?;
582        let specialization_idx_usize =
583            self.find_function(&specialized_name).ok_or_else(|| {
584                ShapeError::SemanticError {
585                    message: format!(
586                        "ensure_monomorphic_function_with_consts: failed to register specialization '{}'",
587                        specialized_name
588                    ),
589                    location: None,
590                }
591            })?;
592        let specialization_idx: u16 =
593            specialization_idx_usize.try_into().map_err(|_| ShapeError::SemanticError {
594                message: format!(
595                    "ensure_monomorphic_function_with_consts: function index {} for '{}' overflows u16",
596                    specialization_idx_usize, specialized_name
597                ),
598                location: None,
599            })?;
600
601        // Cache BEFORE compiling so a recursive const-generic call inside
602        // the body resolves through the cache instead of recursively
603        // re-monomorphizing.
604        self.monomorphization_cache
605            .insert(mono_key.clone(), specialization_idx);
606        self.next_monomorphization_id = self.next_monomorphization_id.saturating_add(1);
607
608        // BUG3 — mark this key as in-progress while its body is compiled.
609        self.monomorphization_in_progress.insert(mono_key.clone());
610
611        // F7: see note in `ensure_monomorphic_function` above.
612        let saved_closure_function_ids =
613            std::mem::take(&mut self.closure_function_ids);
614        // v0.3 WS-6: see `ensure_monomorphic_function` — save/restore the
615        // per-function local ConcreteType table across the nested compile.
616        let saved_local_concrete_types =
617            std::mem::take(&mut self.current_function_local_concrete_types);
618        let result = self.compile_function(&specialized_def);
619        self.closure_function_ids = saved_closure_function_ids;
620        self.current_function_local_concrete_types = saved_local_concrete_types;
621        self.monomorphization_in_progress.remove(&mono_key);
622        result?;
623
624        Ok(specialization_idx)
625    }
626
627    /// Phase C — closure-aware specialization entry point.
628    ///
629    /// Like [`Self::ensure_monomorphic_function`] but additionally keys the
630    /// specialization on per-closure-arg [`ClosureSpec`]s and inlines each
631    /// closure literal's body into the specialized stdlib template (replacing
632    /// calls to the formal closure parameter with the closure body).
633    ///
634    /// Flow:
635    ///   1. Build the full mono key (`base::T1_..._closure_N_ret_...`).
636    ///   2. Cache hit → return existing index.
637    ///   3. Budget exhausted → bail out (`Ok(None)`) so the caller falls
638    ///      back to the generic path.
639    ///   4. Substitute type params through the function def (as in the
640    ///      type-only path).
641    ///   5. For each closure spec, call
642    ///      [`super::substitution::inline_closure_body_into_specialization`]
643    ///      to rewrite the specialized body.
644    ///   6. Register + compile the specialized function; record cache entry;
645    ///      bump the closure-specialization count.
646    ///
647    /// The `closure_defs` parallel to `closure_specs` carries the peeked
648    /// closure literals (params, body, captures) that the inliner needs.
649    /// `callee_closure_param_names[i]` is the name of the callee's formal
650    /// parameter that holds the i-th closure; it's the identifier the inliner
651    /// rewrites. When empty or mismatched, specialization bails and returns
652    /// `Ok(None)`.
653    #[allow(clippy::too_many_arguments)]
654    pub fn ensure_monomorphic_function_with_closures(
655        &mut self,
656        base_fn_name: &str,
657        type_args: &[ConcreteType],
658        closure_specs: &[ClosureSpec],
659        closure_defs: &[ClosureDefPeek],
660        callee_closure_param_names: &[String],
661    ) -> Result<Option<u16>> {
662        let mono_key = build_mono_key_with_closures(base_fn_name, type_args, closure_specs);
663
664        // Cache hit — reuse.
665        if let Some(existing) = self.monomorphization_cache.lookup(&mono_key) {
666            return Ok(Some(existing));
667        }
668
669        // BUG3 — cycle detector (see `ensure_monomorphic_function`).
670        // Closure-aware specialization is the soft-fail variant: bail to the
671        // generic path when a cycle is hit rather than surfacing the error.
672        if self.monomorphization_in_progress.contains(&mono_key) {
673            return Ok(None);
674        }
675
676        // Per-module specialization budget (§3.4). When we've already produced
677        // DEFAULT_CLOSURE_SPECIALIZATION_BUDGET closure-aware specializations,
678        // bail and let the caller emit the generic (non-inlined) path.
679        if self.closure_specialization_count >= DEFAULT_CLOSURE_SPECIALIZATION_BUDGET {
680            return Ok(None);
681        }
682
683        // Closure-spec length sanity check: we must have peek info and a
684        // param name for every recorded spec. If not, the caller messed up.
685        if closure_defs.len() != closure_specs.len()
686            || callee_closure_param_names.len() != closure_specs.len()
687        {
688            return Ok(None);
689        }
690
691        // Look up the base def; fail soft if missing.
692        let original_def = match self.function_defs.get(base_fn_name).cloned() {
693            Some(d) => d,
694            None => return Ok(None),
695        };
696
697        let declared_type_params: Vec<String> = original_def
698            .type_params
699            .as_ref()
700            .map(|tps| tps.iter().map(|tp| tp.name().to_string()).collect())
701            .unwrap_or_default();
702
703        // If type args and declared params disagree, bail (falls back to
704        // generic path) rather than raise — the caller may still want to
705        // compile the call with an unspecialized body.
706        if declared_type_params.len() != type_args.len() {
707            return Ok(None);
708        }
709
710        let subs: HashMap<String, ConcreteType> = declared_type_params
711            .iter()
712            .cloned()
713            .zip(type_args.iter().cloned())
714            .collect();
715
716        // Phase 3a — bound check on the (type-kind) generic params before
717        // proceeding. Closure-aware path is the soft-fail variant — bounds
718        // violations would also be caught by the type-only fallback path,
719        // but rejecting here avoids needless work and yields the same hard
720        // error from the eventual `ensure_monomorphic_function` call.
721        if let Err(e) =
722            self.check_trait_bounds_at_specialization(base_fn_name, &original_def, &subs)
723        {
724            return Err(e);
725        }
726
727        // Substitute type params first.
728        let mut specialized_def = substitution::substitute_function_def(&original_def, &subs);
729        // Overwrite the name with the full closure-aware key so the cache key
730        // and the registered function name agree.
731        specialized_def.name = mono_key.clone();
732
733        // Inline each closure body in turn.
734        for (i, spec_info) in closure_defs.iter().enumerate() {
735            let closure_param_name = &callee_closure_param_names[i];
736            // D-α.1 close (2026-05-22, KC #6(f)): extract per-closure-param
737            // type annotations from the callee's already-substituted spec.
738            // `specialized_def` has had its declared type params (e.g. `T`)
739            // substituted to ConcreteType (e.g. `int`) by
740            // `substitute_function_def` above, so the param at
741            // `closure_param_name` carries `(int, int) => int` (rather
742            // than `(T, T) => int`) here. The annotations are then
743            // attached as type hints to the inlined block's
744            // `let a = arg0; let b = arg1` prelude, closing the
745            // strict-typing gap in `Vec.sort`'s `let mut src = []`
746            // promotion-deferred body. See
747            // `v0.3-d-alpha-audit.md` §4 KC #6(f).
748            let closure_param_annotations: Vec<Option<shape_ast::ast::TypeAnnotation>> =
749                specialized_def
750                    .params
751                    .iter()
752                    .find(|p| {
753                        p.get_identifiers()
754                            .first()
755                            .map(|s| s == closure_param_name)
756                            .unwrap_or(false)
757                    })
758                    .and_then(|p| p.type_annotation.as_ref())
759                    .and_then(|ann| {
760                        if let shape_ast::ast::TypeAnnotation::Function {
761                            params: fps,
762                            ..
763                        } = ann
764                        {
765                            Some(
766                                fps.iter()
767                                    .map(|fp| Some(fp.type_annotation.clone()))
768                                    .collect(),
769                            )
770                        } else {
771                            None
772                        }
773                    })
774                    .unwrap_or_default();
775            // cluster-2 V3-S6f empirical-verification trace (2026-05-16);
776            // cluster-2 closure-wave-F tracing-crate migration (2026-05-16):
777            // ADR-006 §2.7.5 amendment — `tracing::debug!` is compile-out
778            // when the `jit-trace` Cargo feature is OFF (default), so this
779            // site costs zero in release builds. Replaces the legacy
780            // `SHAPE_JIT_DEBUG` env-var gating; CLI selector is
781            // `--trace-jit=shape_jit=debug`.
782            tracing::debug!(
783                target: "shape_jit",
784                mono_key = %mono_key,
785                closure_param = %closure_param_name,
786                closure_param_count = spec_info.param_names.len(),
787                closure_body_stmts = spec_info.body.len(),
788                "mono-phaseC inline_closure_body_into_specialization",
789            );
790            if substitution::inline_closure_body_into_specialization(
791                &mut specialized_def,
792                closure_param_name,
793                &spec_info.param_names,
794                &spec_info.body,
795                &spec_info.capture_names,
796                &closure_param_annotations,
797            )
798            .is_err()
799            {
800                tracing::debug!(
801                    target: "shape_jit",
802                    mono_key = %mono_key,
803                    "mono-phaseC inline FAILED",
804                );
805                // Inlining bailed — fall back to generic path.
806                return Ok(None);
807            }
808        }
809
810        // Register + compile.
811        if self.register_function(&specialized_def).is_err() {
812            return Ok(None);
813        }
814        let specialization_idx_usize = match self.find_function(&specialized_def.name) {
815            Some(idx) => idx,
816            None => return Ok(None),
817        };
818        let specialization_idx: u16 = match specialization_idx_usize.try_into() {
819            Ok(x) => x,
820            Err(_) => return Ok(None),
821        };
822
823        // Cache BEFORE compile_function so any recursive call inside the
824        // specialized body resolves through the cache.
825        self.monomorphization_cache
826            .insert(mono_key.clone(), specialization_idx);
827        self.next_monomorphization_id = self.next_monomorphization_id.saturating_add(1);
828        self.closure_specialization_count =
829            self.closure_specialization_count.saturating_add(1);
830
831        // BUG3 — mark this key as in-progress while its body is compiled.
832        self.monomorphization_in_progress.insert(mono_key.clone());
833
834        // F7: see note in `ensure_monomorphic_function`.
835        let saved_closure_function_ids =
836            std::mem::take(&mut self.closure_function_ids);
837        // v0.3 WS-6: see `ensure_monomorphic_function` — save/restore the
838        // per-function local ConcreteType table across the nested compile.
839        let saved_local_concrete_types =
840            std::mem::take(&mut self.current_function_local_concrete_types);
841        let compile_result = self.compile_function(&specialized_def);
842        self.closure_function_ids = saved_closure_function_ids;
843        self.current_function_local_concrete_types = saved_local_concrete_types;
844        self.monomorphization_in_progress.remove(&mono_key);
845        if compile_result.is_err() {
846            // Compilation failed — we already inserted the cache entry; the
847            // caller will fall back to the generic path anyway. Returning
848            // Ok(None) keeps the error surface clean.
849            return Ok(None);
850        }
851
852        Ok(Some(specialization_idx))
853    }
854}
855
856/// B.3 — resolve a callee's const generic parameters from their declared
857/// default expressions.
858///
859/// The grammar does not yet accept call-site turbofish (`::<4>`) for binding
860/// const generic args, so the only source we have for a const value today is
861/// the optional `default` on each `TypeParam::Const`. The rule:
862///
863///   - `TypeParam::Const { default: Some(literal_expr), .. }` → bind that value.
864///   - `TypeParam::Const { default: None, .. }`              → compile error.
865///   - non-literal default expression                        → compile error.
866///
867/// `TypeParam::Type` entries are skipped — they are handled by the type-arg
868/// resolution path elsewhere.
869///
870/// Returns the `const_args` vector in declaration order (positional against
871/// the const-kind entries in `type_params`).
872fn resolve_const_defaults_or_error(
873    base_fn_name: &str,
874    type_params: &[shape_ast::ast::TypeParam],
875) -> Result<Vec<ComptimeConstValue>> {
876    let mut const_args: Vec<ComptimeConstValue> = Vec::new();
877    for tp in type_params {
878        match tp {
879            shape_ast::ast::TypeParam::Type { .. } => continue,
880            shape_ast::ast::TypeParam::Const { name, default, .. } => {
881                let Some(default_expr) = default else {
882                    return Err(ShapeError::SemanticError {
883                        message: format!(
884                            "const generic arg must be a compile-time constant: '{}' declares const generic parameter '{}' with no default value, and call-site const argument syntax is not yet supported",
885                            base_fn_name, name
886                        ),
887                        location: None,
888                    });
889                };
890                let Some(value) = comptime_const_value_from_literal_expr(default_expr) else {
891                    return Err(ShapeError::SemanticError {
892                        message: format!(
893                            "const generic arg must be a compile-time constant: '{}' const generic parameter '{}' has a non-literal default expression (only literals are supported in B.3 — comptime-evaluated defaults are a follow-up)",
894                            base_fn_name, name
895                        ),
896                        location: None,
897                    });
898                };
899                const_args.push(value);
900            }
901        }
902    }
903    Ok(const_args)
904}
905
906/// Peeked closure-literal info handed to the inliner. The resolver fills
907/// this from the `Expr::FunctionExpr` args before lowering.
908#[derive(Debug, Clone)]
909pub struct ClosureDefPeek {
910    /// Formal parameter names of the closure literal (`x` in `|x| x + n`).
911    pub param_names: Vec<String>,
912    /// The closure literal's body statements.
913    pub body: Vec<shape_ast::ast::Statement>,
914    /// Names of the closure's captures, in order, as leading params for the
915    /// specialized body.
916    pub capture_names: Vec<String>,
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    #[test]
924    fn empty_cache_lookup_returns_none() {
925        let cache = MonomorphizationCache::new();
926        assert_eq!(cache.lookup("map::i64_string"), None);
927        assert_eq!(cache.len(), 0);
928        assert!(cache.is_empty());
929    }
930
931    #[test]
932    fn insert_then_lookup_returns_index() {
933        let mut cache = MonomorphizationCache::new();
934        cache.insert("map::i64_string".to_string(), 42);
935        assert_eq!(cache.lookup("map::i64_string"), Some(42));
936        assert_eq!(cache.len(), 1);
937        assert!(!cache.is_empty());
938    }
939
940    #[test]
941    fn multiple_instantiations_produce_distinct_keys() {
942        let mut cache = MonomorphizationCache::new();
943
944        let key_int_string = build_mono_key(
945            "map",
946            &[ConcreteType::I64, ConcreteType::String],
947        );
948        let key_f64_bool = build_mono_key("map", &[ConcreteType::F64, ConcreteType::Bool]);
949        let key_array_f64 = build_mono_key(
950            "map",
951            &[
952                ConcreteType::Array(Box::new(ConcreteType::F64)),
953                ConcreteType::I64,
954            ],
955        );
956
957        // Sanity-check the key shapes match the design doc.
958        assert_eq!(key_int_string, "map::i64_string");
959        assert_eq!(key_f64_bool, "map::f64_bool");
960        assert_eq!(key_array_f64, "map::array_f64_i64");
961
962        cache.insert(key_int_string.clone(), 1);
963        cache.insert(key_f64_bool.clone(), 2);
964        cache.insert(key_array_f64.clone(), 3);
965
966        assert_eq!(cache.lookup(&key_int_string), Some(1));
967        assert_eq!(cache.lookup(&key_f64_bool), Some(2));
968        assert_eq!(cache.lookup(&key_array_f64), Some(3));
969        assert_eq!(cache.len(), 3);
970    }
971
972    #[test]
973    fn build_mono_key_no_type_args() {
974        // A "monomorphization" of a non-generic function: just the base name.
975        assert_eq!(build_mono_key("foo", &[]), "foo");
976    }
977
978    #[test]
979    fn build_mono_key_single_type_arg() {
980        assert_eq!(
981            build_mono_key("identity", &[ConcreteType::I64]),
982            "identity::i64"
983        );
984    }
985
986    #[test]
987    fn insert_overwrites_existing_key() {
988        let mut cache = MonomorphizationCache::new();
989        cache.insert("identity::i64".to_string(), 5);
990        cache.insert("identity::i64".to_string(), 9);
991        assert_eq!(cache.lookup("identity::i64"), Some(9));
992        assert_eq!(cache.len(), 1);
993    }
994
995    #[test]
996    fn iter_yields_inserted_pairs() {
997        let mut cache = MonomorphizationCache::new();
998        cache.insert("a::i64".to_string(), 1);
999        cache.insert("b::f64".to_string(), 2);
1000
1001        let mut collected: Vec<(String, u16)> =
1002            cache.iter().map(|(k, v)| (k.clone(), *v)).collect();
1003        collected.sort();
1004        assert_eq!(
1005            collected,
1006            vec![("a::i64".to_string(), 1), ("b::f64".to_string(), 2)]
1007        );
1008    }
1009
1010    #[test]
1011    fn ensure_monomorphic_function_unknown_name_errors() {
1012        let mut compiler = BytecodeCompiler::new();
1013        let result = compiler.ensure_monomorphic_function(
1014            "definitely_not_a_function",
1015            &[ConcreteType::I64],
1016        );
1017        assert!(result.is_err(), "expected error for unknown function name");
1018        let msg = format!("{:?}", result.err().unwrap());
1019        assert!(
1020            msg.contains("no FunctionDef AST recorded"),
1021            "expected unknown-function error, got: {}",
1022            msg
1023        );
1024    }
1025
1026    // ---- Const generic cache tests ---------------------------------------
1027    //
1028    // These tests verify the de-duplication / distinctness behaviour of the
1029    // monomorphization cache when const generic args are involved. They use
1030    // raw cache.insert/lookup so they don't depend on the (still missing)
1031    // grammar surface for `<const N: int>`.
1032
1033    #[test]
1034    fn const_generic_repeat_n_3_caches_one_entry() {
1035        // Simulate "repeat<3>(...)": the call site builds a mono_key via
1036        // build_mono_key_with_consts and inserts a specialization index.
1037        let mut cache = MonomorphizationCache::new();
1038        let key = build_mono_key_with_consts(
1039            "repeat",
1040            &[],
1041            &[ComptimeConstValue::Int(3)],
1042        );
1043        assert_eq!(key, "repeat::int_3");
1044        cache.insert(key.clone(), 11);
1045        assert_eq!(cache.lookup(&key), Some(11));
1046        assert_eq!(cache.len(), 1);
1047    }
1048
1049    #[test]
1050    fn const_generic_repeat_n_3_and_n_5_produce_two_entries() {
1051        let mut cache = MonomorphizationCache::new();
1052        let k3 = build_mono_key_with_consts(
1053            "repeat",
1054            &[],
1055            &[ComptimeConstValue::Int(3)],
1056        );
1057        let k5 = build_mono_key_with_consts(
1058            "repeat",
1059            &[],
1060            &[ComptimeConstValue::Int(5)],
1061        );
1062        assert_ne!(k3, k5);
1063        cache.insert(k3.clone(), 11);
1064        cache.insert(k5.clone(), 12);
1065        assert_eq!(cache.len(), 2);
1066        assert_eq!(cache.lookup(&k3), Some(11));
1067        assert_eq!(cache.lookup(&k5), Some(12));
1068    }
1069
1070    #[test]
1071    fn const_generic_repeat_n_3_twice_collapses_to_one_entry() {
1072        // Two calls to repeat<3> should hit the SAME cache entry. We model
1073        // that by inserting twice with the same key and verifying the cache
1074        // length never grows past 1 (and the second insert overwrites).
1075        let mut cache = MonomorphizationCache::new();
1076        let key = build_mono_key_with_consts(
1077            "repeat",
1078            &[],
1079            &[ComptimeConstValue::Int(3)],
1080        );
1081        cache.insert(key.clone(), 11);
1082        cache.insert(key.clone(), 11);
1083        assert_eq!(cache.len(), 1);
1084        assert_eq!(cache.lookup(&key), Some(11));
1085    }
1086
1087    #[test]
1088    fn ensure_monomorphic_function_with_consts_unknown_name_errors() {
1089        let mut compiler = BytecodeCompiler::new();
1090        let result = compiler.ensure_monomorphic_function_with_consts(
1091            "definitely_not_a_function",
1092            &[],
1093            &[ComptimeConstValue::Int(3)],
1094        );
1095        assert!(
1096            result.is_err(),
1097            "expected error for unknown function name even on the const-aware path"
1098        );
1099        let msg = format!("{:?}", result.err().unwrap());
1100        assert!(
1101            msg.contains("no FunctionDef AST recorded"),
1102            "expected unknown-function error, got: {}",
1103            msg
1104        );
1105    }
1106
1107    // =====================================================================
1108    // Phase C — closure-aware specialization tests.
1109    // =====================================================================
1110
1111    #[test]
1112    fn build_mono_key_with_closures_matches_design_doc_format() {
1113        // Single closure arg, i64 return: `map::array_i64_closure_7_i64`.
1114        let type_args = [ConcreteType::Array(Box::new(ConcreteType::I64))];
1115        let closure_specs = [ClosureSpec {
1116            closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(7),
1117            return_type: Some(ConcreteType::I64),
1118            body_hash: 0,
1119        }];
1120        let key = build_mono_key_with_closures("map", &type_args, &closure_specs);
1121        assert_eq!(key, "map::array_i64_closure_7_i64");
1122    }
1123
1124    #[test]
1125    fn build_mono_key_filter_with_closure_bool_return() {
1126        // `filter(|x| x > 0)` over `Array<number>`:
1127        // `"filter::array_f64_closure_N_bool"`.
1128        let type_args = [ConcreteType::Array(Box::new(ConcreteType::F64))];
1129        let closure_specs = [ClosureSpec {
1130            closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(3),
1131            return_type: Some(ConcreteType::Bool),
1132            body_hash: 0,
1133        }];
1134        let key = build_mono_key_with_closures("filter", &type_args, &closure_specs);
1135        assert_eq!(key, "filter::array_f64_closure_3_bool");
1136    }
1137
1138    #[test]
1139    fn build_mono_key_reduce_with_two_closure_args() {
1140        // `reduce` with two closures: both peeked, both contribute to the key.
1141        let type_args = [ConcreteType::I64];
1142        let closure_specs = [
1143            ClosureSpec {
1144                closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(4),
1145                return_type: Some(ConcreteType::I64),
1146                body_hash: 0,
1147            },
1148            ClosureSpec {
1149                closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(5),
1150                return_type: Some(ConcreteType::Bool),
1151                body_hash: 0,
1152            },
1153        ];
1154        let key = build_mono_key_with_closures("reduce", &type_args, &closure_specs);
1155        assert_eq!(key, "reduce::i64_closure_4_i64_closure_5_bool");
1156    }
1157
1158    #[test]
1159    fn build_mono_key_with_closures_unknown_return_type() {
1160        // When the return type is unknown (couldn't be inferred), the key
1161        // encodes `unknown` so different captures still produce distinct
1162        // keys.
1163        let closure_specs = [ClosureSpec {
1164            closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(0),
1165            return_type: None,
1166            body_hash: 0,
1167        }];
1168        let key = build_mono_key_with_closures("map", &[ConcreteType::I64], &closure_specs);
1169        assert_eq!(key, "map::i64_closure_0_unknown");
1170    }
1171
1172    #[test]
1173    fn budget_fallback_returns_none_when_exhausted() {
1174        // Budget is the per-module cap on closure specializations. When
1175        // exhausted, ensure_monomorphic_function_with_closures returns
1176        // Ok(None) so the caller falls back to the direct-call path.
1177        let mut compiler = BytecodeCompiler::new();
1178        compiler.closure_specialization_count = DEFAULT_CLOSURE_SPECIALIZATION_BUDGET;
1179
1180        // No function registered — even so, the budget check runs first and
1181        // returns Ok(None) for budget exhaustion. (If we got past the budget
1182        // check, we'd fall into the "function_defs lookup" path and still
1183        // return Ok(None), which is what we want.)
1184        let result = compiler.ensure_monomorphic_function_with_closures(
1185            "map",
1186            &[ConcreteType::I64],
1187            &[ClosureSpec {
1188                closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(0),
1189                return_type: Some(ConcreteType::I64),
1190                body_hash: 0,
1191            }],
1192            &[ClosureDefPeek {
1193                param_names: vec!["x".into()],
1194                body: vec![],
1195                capture_names: vec![],
1196            }],
1197            &["f".into()],
1198        );
1199        // Budget is at cap → Ok(None) (fallback path).
1200        assert_eq!(result.unwrap(), None);
1201    }
1202
1203    #[test]
1204    fn budget_counter_starts_at_zero() {
1205        let compiler = BytecodeCompiler::new();
1206        assert_eq!(compiler.closure_specialization_count, 0);
1207    }
1208
1209    #[test]
1210    fn cache_hit_returns_same_index_on_second_call() {
1211        // Two lookups with the same closure-aware mono key must hit the
1212        // cache on the second try. Exercises the fast path at the top of
1213        // ensure_monomorphic_function_with_closures.
1214        let mut cache = MonomorphizationCache::new();
1215        let key = build_mono_key_with_closures(
1216            "map",
1217            &[ConcreteType::I64],
1218            &[ClosureSpec {
1219                closure_type_id: shape_value::v2::concrete_type::ClosureTypeId(0),
1220                return_type: Some(ConcreteType::I64),
1221                body_hash: 0,
1222            }],
1223        );
1224        cache.insert(key.clone(), 7);
1225        assert_eq!(cache.lookup(&key), Some(7));
1226        // Re-lookup — cache still hits.
1227        assert_eq!(cache.lookup(&key), Some(7));
1228        assert_eq!(cache.len(), 1);
1229    }
1230
1231    // =====================================================================
1232    // B.3 — bind const generic args at monomorphization.
1233    //
1234    // These tests drive real `FunctionDef`s with `TypeParam::Const` members
1235    // through the cache entry points and assert:
1236    //   - distinct const values produce distinct mono-key cache entries,
1237    //   - identical const values collapse to one cache entry,
1238    //   - wrong arity / wrong type / missing-default cases surface clear
1239    //     compile errors,
1240    //   - functions without any const params are unaffected.
1241    // =====================================================================
1242
1243    use shape_ast::ast::{
1244        DestructurePattern, FunctionDef, FunctionParameter, Literal, Span, TypeAnnotation,
1245        TypeParam,
1246    };
1247
1248    /// Build a minimal FunctionDef with:
1249    ///   - `type_params` — the generic list (mix of `Type` and `Const` OK),
1250    ///   - a single `x: int` param,
1251    ///   - `int` return type,
1252    ///   - body `return x` so compile_function succeeds.
1253    fn b3_identity_n_def(type_params: Vec<TypeParam>) -> FunctionDef {
1254        FunctionDef {
1255            name: "identity_n".into(),
1256            name_span: Span::default(),
1257            declaring_module_path: None,
1258            doc_comment: None,
1259            type_params: if type_params.is_empty() {
1260                None
1261            } else {
1262                Some(type_params)
1263            },
1264            params: vec![FunctionParameter {
1265                pattern: DestructurePattern::Identifier("x".into(), Span::default()),
1266                is_const: false,
1267                is_reference: false,
1268                is_mut_reference: false,
1269                is_out: false,
1270                type_annotation: Some(TypeAnnotation::Basic("int".into())),
1271                default_value: None,
1272            }],
1273            return_type: Some(TypeAnnotation::Basic("int".into())),
1274            where_clause: None,
1275            body: vec![shape_ast::ast::Statement::Return(
1276                Some(shape_ast::ast::Expr::Identifier("x".into(), Span::default())),
1277                Span::default(),
1278            )],
1279            annotations: Vec::new(),
1280            is_async: false,
1281            is_comptime: false,
1282        }
1283    }
1284
1285    fn const_param(name: &str, default: Option<i64>) -> TypeParam {
1286        TypeParam::Const {
1287            name: name.into(),
1288            span: Span::default(),
1289            doc_comment: None,
1290            ty: TypeAnnotation::Basic("int".into()),
1291            default: default.map(|v| shape_ast::ast::Expr::Literal(Literal::Int(v), Span::default())),
1292        }
1293    }
1294
1295    #[test]
1296    fn b3_const_generic_distinct_values_produce_distinct_monomorphizations() {
1297        // identity_n<const N: int = 4> compiled with N=4 then N=8 must produce
1298        // TWO distinct cache entries (keys `identity_n::int_4` and
1299        // `identity_n::int_8`) — the load-bearing deliverable of B.3.
1300        let mut compiler = BytecodeCompiler::new();
1301        let def = b3_identity_n_def(vec![const_param("N", Some(4))]);
1302        compiler.function_defs.insert("identity_n".into(), def);
1303
1304        // First monomorphization: N=4.
1305        let idx4 = compiler
1306            .ensure_monomorphic_function_with_consts(
1307                "identity_n",
1308                &[],
1309                &[ComptimeConstValue::Int(4)],
1310            )
1311            .expect("N=4 monomorphization should succeed");
1312        assert_eq!(
1313            compiler.monomorphization_cache.lookup("identity_n::int_4"),
1314            Some(idx4)
1315        );
1316
1317        // Second monomorphization: N=8.
1318        let idx8 = compiler
1319            .ensure_monomorphic_function_with_consts(
1320                "identity_n",
1321                &[],
1322                &[ComptimeConstValue::Int(8)],
1323            )
1324            .expect("N=8 monomorphization should succeed");
1325        assert_eq!(
1326            compiler.monomorphization_cache.lookup("identity_n::int_8"),
1327            Some(idx8)
1328        );
1329
1330        assert_ne!(idx4, idx8, "distinct const values must produce distinct specializations");
1331        assert_eq!(compiler.monomorphization_cache.len(), 2);
1332    }
1333
1334    #[test]
1335    fn b3_const_generic_same_value_collapses_to_one_entry() {
1336        let mut compiler = BytecodeCompiler::new();
1337        let def = b3_identity_n_def(vec![const_param("N", Some(4))]);
1338        compiler.function_defs.insert("identity_n".into(), def);
1339
1340        let a = compiler
1341            .ensure_monomorphic_function_with_consts(
1342                "identity_n",
1343                &[],
1344                &[ComptimeConstValue::Int(4)],
1345            )
1346            .unwrap();
1347        let b = compiler
1348            .ensure_monomorphic_function_with_consts(
1349                "identity_n",
1350                &[],
1351                &[ComptimeConstValue::Int(4)],
1352            )
1353            .unwrap();
1354        assert_eq!(a, b, "identical const args must collapse to one cache entry");
1355        assert_eq!(compiler.monomorphization_cache.len(), 1);
1356    }
1357
1358    #[test]
1359    fn b3_const_generic_runtime_body_without_substitution_references() {
1360        // Exercises the full compile pipeline: register a const-generic
1361        // function whose body does NOT reference N, monomorphize, then
1362        // look up the specialized function. This proves that B.3 wiring
1363        // works end-to-end even before B.4 substitutes body references.
1364        let mut compiler = BytecodeCompiler::new();
1365        let def = b3_identity_n_def(vec![const_param("N", Some(4))]);
1366        compiler.function_defs.insert("identity_n".into(), def);
1367
1368        let specialized_idx = compiler
1369            .ensure_monomorphic_function_with_consts(
1370                "identity_n",
1371                &[],
1372                &[ComptimeConstValue::Int(4)],
1373            )
1374            .expect("const-generic compile should succeed without body references");
1375
1376        // The specialized function is registered in the program under a name
1377        // derived from the mono key. We can at least verify the cache records
1378        // the same index for the same key.
1379        assert_eq!(
1380            compiler.monomorphization_cache.lookup("identity_n::int_4"),
1381            Some(specialized_idx)
1382        );
1383    }
1384
1385    #[test]
1386    fn b3_wrong_const_arity_errors() {
1387        let mut compiler = BytecodeCompiler::new();
1388        // Callee declares ONE const generic (N) but we pass TWO const args.
1389        let def = b3_identity_n_def(vec![const_param("N", Some(4))]);
1390        compiler.function_defs.insert("identity_n".into(), def);
1391
1392        let result = compiler.ensure_monomorphic_function_with_consts(
1393            "identity_n",
1394            &[],
1395            &[ComptimeConstValue::Int(4), ComptimeConstValue::Int(5)],
1396        );
1397        assert!(result.is_err(), "wrong const arity must error");
1398        let msg = format!("{:?}", result.err().unwrap());
1399        assert!(
1400            msg.contains("const generic parameters"),
1401            "error should mention const generic arity, got: {}",
1402            msg
1403        );
1404    }
1405
1406    #[test]
1407    fn b3_type_only_entry_routes_const_params_through_with_consts_path() {
1408        // When the type-only `ensure_monomorphic_function` is called against a
1409        // callee with const params, it must auto-bind them from defaults and
1410        // delegate to `ensure_monomorphic_function_with_consts`. The cache
1411        // entry must have the const-aware key, not the bare `identity_n` key.
1412        let mut compiler = BytecodeCompiler::new();
1413        let def = b3_identity_n_def(vec![const_param("N", Some(4))]);
1414        compiler.function_defs.insert("identity_n".into(), def);
1415
1416        let idx = compiler
1417            .ensure_monomorphic_function("identity_n", &[])
1418            .expect("delegation to const-aware path should succeed");
1419        assert_eq!(
1420            compiler.monomorphization_cache.lookup("identity_n::int_4"),
1421            Some(idx),
1422            "type-only entry must route through the const-aware mono key"
1423        );
1424        assert!(
1425            compiler.monomorphization_cache.lookup("identity_n").is_none(),
1426            "bare `identity_n` key must NOT appear — const params always differentiate"
1427        );
1428    }
1429
1430    #[test]
1431    fn b3_missing_const_default_errors_with_specific_message() {
1432        // `identity_n<const N: int>` (no default) must error at the type-only
1433        // entry point since there's no call-site turbofish syntax yet.
1434        let mut compiler = BytecodeCompiler::new();
1435        let def = b3_identity_n_def(vec![const_param("N", None)]);
1436        compiler.function_defs.insert("identity_n".into(), def);
1437
1438        let result = compiler.ensure_monomorphic_function("identity_n", &[]);
1439        assert!(result.is_err());
1440        let msg = format!("{:?}", result.err().unwrap());
1441        assert!(
1442            msg.contains("const generic arg must be a compile-time constant"),
1443            "expected B.3 diagnostic, got: {}",
1444            msg
1445        );
1446    }
1447
1448    #[test]
1449    fn ensure_monomorphic_function_with_consts_empty_consts_delegates_to_legacy_path() {
1450        // With no const args supplied, the const-aware entry point must
1451        // delegate to ensure_monomorphic_function (the type-only path) so
1452        // every existing caller stays byte-for-byte identical.
1453        //
1454        // We verify this by passing an unknown function name and checking
1455        // the error message — the legacy path's error string is distinct
1456        // from the const-aware one.
1457        let mut compiler = BytecodeCompiler::new();
1458        let result = compiler.ensure_monomorphic_function_with_consts(
1459            "definitely_not_a_function",
1460            &[ConcreteType::I64],
1461            &[], // empty const args → must delegate
1462        );
1463        assert!(result.is_err());
1464        let msg = format!("{:?}", result.err().unwrap());
1465        // Legacy error string ("ensure_monomorphic_function: ...") not the
1466        // const-aware one ("ensure_monomorphic_function_with_consts: ...").
1467        assert!(
1468            msg.contains("ensure_monomorphic_function:"),
1469            "expected delegation to legacy path, got: {}",
1470            msg
1471        );
1472        assert!(
1473            !msg.contains("ensure_monomorphic_function_with_consts:"),
1474            "should NOT have used the const-aware error path: {}",
1475            msg
1476        );
1477    }
1478
1479    // =====================================================================
1480    // B.5 — Track B close-out: end-to-end coverage for const generics via
1481    // the default-value grammar route.
1482    //
1483    // Turbofish call-site syntax (`fn_name::<3>(...)`) is a separate grammar
1484    // extension outside Track B's scope; the `const_generic_repeat_n_3_end_to_end`
1485    // placeholder in `type_resolution.rs` tracks that follow-up work.
1486    //
1487    // These tests drive the full pipeline parser → AST → cache key →
1488    // substituted body, using real Shape source text through
1489    // `parse_program` and the `BytecodeCompiler::ensure_monomorphic_function`
1490    // entry point. They do NOT reach runtime value assertions (top-level
1491    // generic function calls are not yet wired into monomorphization — see
1492    // the `test_user_defined_generic_function` ignore in
1493    // `integration_tests.rs`). Instead, each test asserts:
1494    //
1495    //   (a) the parser accepts `fn f<const N: int = V>(...) { body }`,
1496    //   (b) the function registers and the compiler caches the expected
1497    //       specialization under `f::int_V`,
1498    //   (c) the substituted function body has every `Identifier(N)`
1499    //       position rewritten to the bound literal.
1500    // =====================================================================
1501
1502    /// Extract the `FunctionDef` for `name` from a freshly-parsed program.
1503    fn b5_function_def_from_source(source: &str, name: &str) -> FunctionDef {
1504        let program = shape_ast::parser::parse_program(source)
1505            .unwrap_or_else(|e| panic!("parse failed for source `{}`: {:?}", source, e));
1506        for item in &program.items {
1507            if let shape_ast::ast::Item::Function(def, _) = item {
1508                if def.name == name {
1509                    return def.clone();
1510                }
1511            }
1512        }
1513        panic!("function `{}` not found in parsed program", name);
1514    }
1515
1516    /// Register the parsed function in a fresh compiler and drive it through
1517    /// the type-only `ensure_monomorphic_function` entry point. Returns the
1518    /// produced function index.
1519    fn b5_register_and_monomorphize(
1520        compiler: &mut BytecodeCompiler,
1521        def: FunctionDef,
1522    ) -> Result<u16> {
1523        let fn_name = def.name.clone();
1524        compiler.register_function(&def)?;
1525        compiler.ensure_monomorphic_function(&fn_name, &[])
1526    }
1527
1528    /// Count how many `Identifier("<name>", ...)` nodes survive anywhere in
1529    /// the function def (body, params, type annotations). Uses the debug
1530    /// representation so it doesn't have to keep up with AST variant changes —
1531    /// B.4's exhaustive-match substitution is already tested directly in
1532    /// `substitution.rs`; here we just want a cheap "no N leaked through"
1533    /// smoke check on the post-substitution FunctionDef.
1534    fn b5_count_surviving_identifier(def: &FunctionDef, name: &str) -> usize {
1535        let dbg = format!("{:?}", def);
1536        let needle = format!("Identifier(\"{}\"", name);
1537        dbg.matches(&needle).count()
1538    }
1539
1540    #[test]
1541    fn b5_parser_to_cache_single_const_default() {
1542        // Flow: parse `fn add_n<const N: int = 4>(x: int) -> int { x + N }`,
1543        // register, then call `ensure_monomorphic_function("add_n", &[])`.
1544        // The const default auto-binds N = 4 and the specialization lands in
1545        // the cache under `add_n::int_4`.
1546        let src = r#"
1547            fn add_n<const N: int = 4>(x: int) -> int {
1548                return x + N
1549            }
1550        "#;
1551        let def = b5_function_def_from_source(src, "add_n");
1552        let mut compiler = BytecodeCompiler::new();
1553        let idx = b5_register_and_monomorphize(&mut compiler, def)
1554            .expect("add_n<const N = 4> should monomorphize");
1555        assert_eq!(
1556            compiler.monomorphization_cache.lookup("add_n::int_4"),
1557            Some(idx),
1558            "expected cache entry keyed on N's bound value"
1559        );
1560    }
1561
1562    #[test]
1563    fn b5_two_defaults_produce_distinct_specializations() {
1564        // Two wrapper functions with different const defaults → TWO cache
1565        // entries with distinct keys. This exercises the load-bearing B.3
1566        // distinctness guarantee end-to-end through the parser.
1567        let src = r#"
1568            fn add_4<const N: int = 4>(x: int) -> int { return x + N }
1569            fn add_8<const N: int = 8>(x: int) -> int { return x + N }
1570        "#;
1571        let def_4 = b5_function_def_from_source(src, "add_4");
1572        let def_8 = b5_function_def_from_source(src, "add_8");
1573        let mut compiler = BytecodeCompiler::new();
1574
1575        let idx_4 = b5_register_and_monomorphize(&mut compiler, def_4).unwrap();
1576        let idx_8 = b5_register_and_monomorphize(&mut compiler, def_8).unwrap();
1577
1578        assert_eq!(compiler.monomorphization_cache.lookup("add_4::int_4"), Some(idx_4));
1579        assert_eq!(compiler.monomorphization_cache.lookup("add_8::int_8"), Some(idx_8));
1580        assert_ne!(idx_4, idx_8, "distinct defaults must produce distinct specializations");
1581    }
1582
1583    #[test]
1584    fn b5_multi_const_param_mono_key_carries_both_values() {
1585        // `fn rect<const R: int = 3, const C: int = 5>() -> int { R * C }`
1586        // should monomorphize to `rect::int_3_int_5`.
1587        let src = r#"
1588            fn rect<const R: int = 3, const C: int = 5>(x: int) -> int {
1589                return x + R * C
1590            }
1591        "#;
1592        let def = b5_function_def_from_source(src, "rect");
1593        let mut compiler = BytecodeCompiler::new();
1594        let idx = b5_register_and_monomorphize(&mut compiler, def).unwrap();
1595        assert_eq!(
1596            compiler.monomorphization_cache.lookup("rect::int_3_int_5"),
1597            Some(idx),
1598            "multi-const mono key must interleave in declaration order"
1599        );
1600    }
1601
1602    #[test]
1603    fn b5_const_in_if_branch_is_substituted() {
1604        // Verify B.4's body substitution walks through `if` branches: no
1605        // `Identifier("N")` should survive in the specialized body.
1606        let src = r#"
1607            fn clamp_n<const N: int = 10>(x: int) -> int {
1608                if x > N {
1609                    return N
1610                }
1611                return x
1612            }
1613        "#;
1614        let def = b5_function_def_from_source(src, "clamp_n");
1615        let mut compiler = BytecodeCompiler::new();
1616        b5_register_and_monomorphize(&mut compiler, def).unwrap();
1617        let specialized = compiler
1618            .function_defs
1619            .get("clamp_n::int_10")
1620            .expect("specialization should be recorded by name");
1621        assert_eq!(
1622            b5_count_surviving_identifier(specialized, "N"),
1623            0,
1624            "bound const param N must not survive substitution inside `if`",
1625        );
1626    }
1627
1628    #[test]
1629    fn b5_const_in_while_and_for_is_substituted() {
1630        // Verify substitution walks through `while` and `for in` loop bodies.
1631        let src = r#"
1632            fn sum_upto<const N: int = 5>(x: int) -> int {
1633                let mut acc = x
1634                let mut i = 0
1635                while i < N {
1636                    acc = acc + i
1637                    i = i + 1
1638                }
1639                for j in 0..N {
1640                    acc = acc + j
1641                }
1642                return acc
1643            }
1644        "#;
1645        let def = b5_function_def_from_source(src, "sum_upto");
1646        let mut compiler = BytecodeCompiler::new();
1647        b5_register_and_monomorphize(&mut compiler, def).unwrap();
1648        let specialized = compiler
1649            .function_defs
1650            .get("sum_upto::int_5")
1651            .expect("specialization should be recorded by name");
1652        assert_eq!(
1653            b5_count_surviving_identifier(specialized, "N"),
1654            0,
1655            "bound const param N must not survive in while/for",
1656        );
1657    }
1658
1659    #[test]
1660    fn b5_const_in_closure_body_is_substituted() {
1661        // A closure literal inside the specialized body must also have its
1662        // `Identifier(N)` references rewritten — B.4 recurses into
1663        // `FunctionExpr` bodies.
1664        let src = r#"
1665            fn offset_by<const N: int = 7>(x: int) -> int {
1666                let adder = |y| y + N
1667                return adder(x)
1668            }
1669        "#;
1670        let def = b5_function_def_from_source(src, "offset_by");
1671        let mut compiler = BytecodeCompiler::new();
1672        b5_register_and_monomorphize(&mut compiler, def).unwrap();
1673        let specialized = compiler
1674            .function_defs
1675            .get("offset_by::int_7")
1676            .expect("specialization should be recorded by name");
1677        assert_eq!(
1678            b5_count_surviving_identifier(specialized, "N"),
1679            0,
1680            "bound const param N must not survive inside closure body",
1681        );
1682    }
1683
1684    #[test]
1685    fn b5_const_in_match_arm_body_is_substituted() {
1686        // Match arm bodies are walked by B.4's substitution — no `N` should
1687        // survive.
1688        let src = r#"
1689            fn tagged<const N: int = 2>(flag: int) -> int {
1690                let result = match flag {
1691                    0 => N,
1692                    _ => N + 1,
1693                }
1694                return result
1695            }
1696        "#;
1697        let def = b5_function_def_from_source(src, "tagged");
1698        let mut compiler = BytecodeCompiler::new();
1699        b5_register_and_monomorphize(&mut compiler, def).unwrap();
1700        let specialized = compiler
1701            .function_defs
1702            .get("tagged::int_2")
1703            .expect("specialization should be recorded by name");
1704        assert_eq!(
1705            b5_count_surviving_identifier(specialized, "N"),
1706            0,
1707            "bound const param N must not survive inside match arm",
1708        );
1709    }
1710
1711    #[test]
1712    fn b5_missing_const_default_surfaces_b3_diagnostic_through_parser() {
1713        // End-to-end: parse a const-generic function with NO default, then
1714        // trigger the type-only entry point. Since turbofish syntax isn't
1715        // wired yet, the only way to bind `N` is a default — so this must
1716        // produce the B.3 "must be a compile-time constant" diagnostic.
1717        let src = r#"
1718            fn id_n<const N: int>(x: int) -> int { return x + N }
1719        "#;
1720        let def = b5_function_def_from_source(src, "id_n");
1721        let mut compiler = BytecodeCompiler::new();
1722        compiler.register_function(&def).unwrap();
1723        let result = compiler.ensure_monomorphic_function("id_n", &[]);
1724        assert!(
1725            result.is_err(),
1726            "const-generic fn with no default must not auto-bind successfully"
1727        );
1728        let msg = format!("{:?}", result.err().unwrap());
1729        assert!(
1730            msg.contains("const generic arg must be a compile-time constant"),
1731            "expected B.3 diagnostic, got: {}",
1732            msg
1733        );
1734    }
1735
1736    #[test]
1737    fn b5_same_default_twice_collapses_to_single_cache_entry() {
1738        // Calling the const-aware entry point twice with the same resolved
1739        // default must hit the cache on the second try — proof that the
1740        // monomorphization pipeline is idempotent per (fn_name, const values).
1741        let src = r#"
1742            fn pin_n<const N: int = 11>(x: int) -> int { return x + N }
1743        "#;
1744        let def = b5_function_def_from_source(src, "pin_n");
1745        let mut compiler = BytecodeCompiler::new();
1746        compiler.register_function(&def).unwrap();
1747        let a = compiler.ensure_monomorphic_function("pin_n", &[]).unwrap();
1748        let b = compiler.ensure_monomorphic_function("pin_n", &[]).unwrap();
1749        assert_eq!(a, b, "second call must hit the cache");
1750        assert_eq!(
1751            compiler.monomorphization_cache.lookup("pin_n::int_11"),
1752            Some(a)
1753        );
1754    }
1755}