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}