Skip to main content

tsz_solver/inference/
infer_matching.rs

1//! Structural type matching for inference.
2//!
3//! This module implements the structural type-walking algorithm that collects
4//! inference candidates by recursing into type shapes (objects, functions,
5//! tuples, unions, intersections, template literals, etc.).
6//!
7//! It is the core of `infer_from_types`: given a source type and a target type
8//! containing type parameters, it walks both structures in parallel and records
9//! lower/upper bound candidates for each inference variable.
10
11use crate::types::{
12    CallableShapeId, FunctionShapeId, InferencePriority, IntrinsicKind, LiteralValue, MappedTypeId,
13    ObjectShapeId, TemplateLiteralId, TemplateSpan, TupleElement, TupleListId, TypeApplicationId,
14    TypeData, TypeId, TypeListId,
15};
16use tsz_common::interner::Atom;
17
18use super::infer::{InferenceContext, InferenceError, InferenceVar};
19
20impl<'a> InferenceContext<'a> {
21    /// Perform structural type inference from a source type to a target type.
22    ///
23    /// This is the core algorithm for inferring type parameters from function arguments.
24    /// It walks the structure of both types, collecting constraints for type parameters.
25    ///
26    /// # Arguments
27    /// * `source` - The type from the value argument (e.g., `string` from `identity("hello")`)
28    /// * `target` - The type from the parameter (e.g., `T` from `function identity<T>(x: T)`)
29    /// * `priority` - The inference priority (e.g., `NakedTypeVariable` for direct arguments)
30    ///
31    /// # Type Inference Algorithm
32    ///
33    /// TypeScript uses structural type inference with the following rules:
34    ///
35    /// 1. **Direct Parameter Match**: If target is a type parameter `T` we're inferring,
36    ///    add source as a lower bound candidate for `T`.
37    ///
38    /// 2. **Structural Recursion**: For complex types, recurse into the structure:
39    ///    - Objects: Match properties recursively
40    ///    - Arrays: Match element types
41    ///    - Functions: Match parameters (contravariant) and return types (covariant)
42    ///
43    /// 3. **Variance Handling**:
44    ///    - Covariant positions (properties, arrays, return types): `infer(source, target)`
45    ///    - Contravariant positions (function parameters): `infer(target, source)` (swapped!)
46    ///
47    /// # Example
48    /// ```ignore
49    /// let mut ctx = InferenceContext::new(&interner);
50    /// let t_var = ctx.fresh_type_param(interner.intern_string("T"), false);
51    ///
52    /// // Inference: identity("hello") should infer T = string
53    /// ctx.infer_from_types(string_type, t_type, InferencePriority::NakedTypeVariable)?;
54    /// ```
55    pub fn infer_from_types(
56        &mut self,
57        source: TypeId,
58        target: TypeId,
59        priority: InferencePriority,
60    ) -> Result<(), InferenceError> {
61        // Resolve the types to their actual TypeDatas
62        let source_key = self.interner.lookup(source);
63        let target_key = self.interner.lookup(target);
64
65        // Block inference if target is NoInfer<T> (TypeScript 5.4+)
66        // NoInfer prevents inference from flowing through this type position
67        if let Some(TypeData::NoInfer(_)) = target_key {
68            return Ok(()); // Stop inference - don't descend into NoInfer
69        }
70
71        // Unwrap NoInfer from source if present (rare but possible)
72        let source_key = if let Some(TypeData::NoInfer(inner)) = source_key {
73            self.interner.lookup(inner)
74        } else {
75            source_key
76        };
77
78        // Case 1: Target is a TypeParameter we're inferring (Lower Bound: source <: T)
79        if let Some(TypeData::TypeParameter(ref param_info)) = target_key
80            && let Some(var) = self.find_type_param(param_info.name)
81        {
82            // Add source as a lower bound candidate for this type parameter
83            self.add_candidate(var, source, priority);
84            return Ok(());
85        }
86
87        // Case 2: Source is a TypeParameter we're inferring (Upper Bound: T <: target)
88        // CRITICAL: This handles contravariance! When function parameters are swapped,
89        // the TypeParameter moves to source position and becomes an upper bound.
90        if let Some(TypeData::TypeParameter(ref param_info)) = source_key
91            && let Some(var) = self.find_type_param(param_info.name)
92        {
93            // T <: target, so target is an UPPER bound
94            self.add_upper_bound(var, target);
95            return Ok(());
96        }
97
98        // Case 3: Structural recursion - match based on type structure
99        match (source_key, target_key) {
100            // Object types: recurse into properties
101            (Some(TypeData::Object(source_shape)), Some(TypeData::Object(target_shape))) => {
102                self.infer_objects(source_shape, target_shape, priority)?;
103            }
104
105            // Function types: handle variance (parameters are contravariant, return is covariant)
106            (Some(TypeData::Function(source_func)), Some(TypeData::Function(target_func))) => {
107                self.infer_functions(source_func, target_func, priority)?;
108            }
109
110            // Callable types: infer across signatures and properties
111            (Some(TypeData::Callable(source_call)), Some(TypeData::Callable(target_call))) => {
112                self.infer_callables(source_call, target_call, priority)?;
113            }
114
115            // Array types: recurse into element types
116            (Some(TypeData::Array(source_elem)), Some(TypeData::Array(target_elem))) => {
117                self.infer_from_types(source_elem, target_elem, priority)?;
118            }
119
120            // Tuple types: recurse into elements
121            (Some(TypeData::Tuple(source_elems)), Some(TypeData::Tuple(target_elems))) => {
122                self.infer_tuples(source_elems, target_elems, priority)?;
123            }
124
125            // Union types: try to infer against each member
126            (Some(TypeData::Union(source_members)), Some(TypeData::Union(target_members))) => {
127                self.infer_unions(source_members, target_members, priority)?;
128            }
129
130            // Intersection types
131            (
132                Some(TypeData::Intersection(source_members)),
133                Some(TypeData::Intersection(target_members)),
134            ) => {
135                self.infer_intersections(source_members, target_members, priority)?;
136            }
137
138            // TypeApplication: recurse into instantiated type
139            (Some(TypeData::Application(source_app)), Some(TypeData::Application(target_app))) => {
140                self.infer_applications(source_app, target_app, priority)?;
141            }
142
143            // Index access types: infer both object and index types
144            (
145                Some(TypeData::IndexAccess(source_obj, source_idx)),
146                Some(TypeData::IndexAccess(target_obj, target_idx)),
147            ) => {
148                self.infer_from_types(source_obj, target_obj, priority)?;
149                self.infer_from_types(source_idx, target_idx, priority)?;
150            }
151
152            // ReadonlyType: unwrap if both are readonly (e.g. readonly [T] vs readonly [number])
153            (
154                Some(TypeData::ReadonlyType(source_inner)),
155                Some(TypeData::ReadonlyType(target_inner)),
156            ) => {
157                self.infer_from_types(source_inner, target_inner, priority)?;
158            }
159
160            // Unwrap ReadonlyType when only target is readonly (mutable source is compatible)
161            (_, Some(TypeData::ReadonlyType(target_inner))) => {
162                self.infer_from_types(source, target_inner, priority)?;
163            }
164
165            // Task #40: Template literal deconstruction for infer patterns
166            // Handles: source extends `prefix${infer T}suffix` ? true : false
167            (Some(source_key), Some(TypeData::TemplateLiteral(target_id))) => {
168                self.infer_from_template_literal(source, Some(&source_key), target_id, priority)?;
169            }
170
171            // Mapped type inference: infer from object properties against mapped type
172            // Handles: source { a: string, b: number } against target { [P in K]: T }
173            // Infers K from property names and T from property value types
174            (Some(TypeData::Object(source_shape)), Some(TypeData::Mapped(mapped_id))) => {
175                self.infer_from_mapped_type(source_shape, mapped_id, priority)?;
176            }
177
178            // If we can't match structurally, that's okay - it might mean the types are incompatible
179            // The Checker will handle this with proper error reporting
180            _ => {
181                // No structural match possible
182                // This is not an error - the Checker will verify assignability separately
183            }
184        }
185
186        Ok(())
187    }
188
189    /// Infer from object types by matching properties
190    fn infer_objects(
191        &mut self,
192        source_shape: ObjectShapeId,
193        target_shape: ObjectShapeId,
194        priority: InferencePriority,
195    ) -> Result<(), InferenceError> {
196        let source_shape = self.interner.object_shape(source_shape);
197        let target_shape = self.interner.object_shape(target_shape);
198
199        // For each property in the target, try to find a matching property in the source
200        for target_prop in &target_shape.properties {
201            if let Some(source_prop) = source_shape
202                .properties
203                .iter()
204                .find(|p| p.name == target_prop.name)
205            {
206                self.infer_from_types(source_prop.type_id, target_prop.type_id, priority)?;
207            }
208        }
209
210        // Also check index signatures for inference
211        // If target has a string index signature, infer from source's string index
212        if let (Some(target_string_idx), Some(source_string_idx)) =
213            (&target_shape.string_index, &source_shape.string_index)
214        {
215            self.infer_from_types(
216                source_string_idx.value_type,
217                target_string_idx.value_type,
218                priority,
219            )?;
220        }
221
222        // If target has a number index signature, infer from source's number index
223        if let (Some(target_number_idx), Some(source_number_idx)) =
224            (&target_shape.number_index, &source_shape.number_index)
225        {
226            self.infer_from_types(
227                source_number_idx.value_type,
228                target_number_idx.value_type,
229                priority,
230            )?;
231        }
232
233        Ok(())
234    }
235
236    /// Infer type arguments from an object type matched against a mapped type.
237    ///
238    /// When source is `{ a: string, b: number }` and target is `{ [P in K]: T }`:
239    /// - Infer K from the union of source property name literals ("a" | "b")
240    /// - Infer T from each source property value type against the mapped template
241    fn infer_from_mapped_type(
242        &mut self,
243        source_shape: ObjectShapeId,
244        mapped_id: MappedTypeId,
245        priority: InferencePriority,
246    ) -> Result<(), InferenceError> {
247        let mapped = self.interner.mapped_type(mapped_id);
248        let source = self.interner.object_shape(source_shape);
249
250        if source.properties.is_empty() {
251            return Ok(());
252        }
253
254        // Infer the constraint type (K) from the union of source property names
255        // e.g., for { foo: string, bar: number }, K = "foo" | "bar"
256        let name_literals: Vec<TypeId> = source
257            .properties
258            .iter()
259            .map(|p| self.interner.literal_string_atom(p.name))
260            .collect();
261        let names_union = if name_literals.len() == 1 {
262            name_literals[0]
263        } else {
264            self.interner.union(name_literals)
265        };
266        self.infer_from_types(names_union, mapped.constraint, priority)?;
267
268        // Infer the template type (T) from each source property value type
269        for prop in &source.properties {
270            self.infer_from_types(prop.type_id, mapped.template, priority)?;
271        }
272
273        Ok(())
274    }
275
276    /// Infer from function types, handling variance correctly
277    fn infer_functions(
278        &mut self,
279        source_func: FunctionShapeId,
280        target_func: FunctionShapeId,
281        priority: InferencePriority,
282    ) -> Result<(), InferenceError> {
283        let source_sig = self.interner.function_shape(source_func);
284        let target_sig = self.interner.function_shape(target_func);
285
286        tracing::trace!(
287            source_params = source_sig.params.len(),
288            target_params = target_sig.params.len(),
289            "infer_functions called"
290        );
291
292        // Parameters are contravariant: swap source and target
293        let mut source_params = source_sig.params.iter().peekable();
294        let mut target_params = target_sig.params.iter().peekable();
295
296        loop {
297            let source_rest = source_params.peek().is_some_and(|p| p.rest);
298            let target_rest = target_params.peek().is_some_and(|p| p.rest);
299
300            tracing::trace!(
301                source_rest,
302                target_rest,
303                "Checking rest params in loop iteration"
304            );
305
306            // If both have rest params, infer the rest element types
307            if source_rest && target_rest {
308                let source_param = source_params.next().unwrap();
309                let target_param = target_params.next().unwrap();
310                self.infer_from_types(target_param.type_id, source_param.type_id, priority)?;
311                break;
312            }
313
314            // If source has rest param, infer all remaining target params into it
315            if source_rest {
316                let source_param = source_params.next().unwrap();
317                for target_param in target_params.by_ref() {
318                    self.infer_from_types(target_param.type_id, source_param.type_id, priority)?;
319                }
320                break;
321            }
322
323            // If target has rest param, infer all remaining source params into it
324            if target_rest {
325                let target_param = target_params.next().unwrap();
326
327                // CRITICAL: Check if target rest param is a type parameter (like A extends any[])
328                // If so, we need to infer it as a TUPLE of all remaining source params,
329                // not as individual param types.
330                //
331                // Example: wrap<A extends any[], R>(fn: (...args: A) => R)
332                //          with add(a: number, b: number): number
333                //          should infer A = [number, number], not A = number
334                let target_is_type_param = matches!(
335                    self.interner.lookup(target_param.type_id),
336                    Some(TypeData::TypeParameter(_) | TypeData::Infer(_))
337                );
338
339                tracing::trace!(
340                    target_is_type_param,
341                    target_param_type = ?target_param.type_id,
342                    "Rest parameter inference - target is type param check"
343                );
344
345                if target_is_type_param {
346                    // Collect all remaining source params into a tuple
347                    let mut tuple_elements = Vec::new();
348                    for source_param in source_params.by_ref() {
349                        tuple_elements.push(TupleElement {
350                            type_id: source_param.type_id,
351                            name: source_param.name,
352                            optional: source_param.optional,
353                            rest: source_param.rest,
354                        });
355                    }
356
357                    tracing::trace!(
358                        num_elements = tuple_elements.len(),
359                        "Collected source params into tuple"
360                    );
361
362                    // Infer the tuple type against the type parameter
363                    // Note: Parameters are contravariant, so target comes first
364                    if !tuple_elements.is_empty() {
365                        let tuple_type = self.interner.tuple(tuple_elements);
366                        tracing::trace!(
367                            tuple_type = ?tuple_type,
368                            target_param = ?target_param.type_id,
369                            "Inferring tuple against type parameter"
370                        );
371                        self.infer_from_types(target_param.type_id, tuple_type, priority)?;
372                    }
373                } else {
374                    // Target rest param is not a type parameter (e.g., number[] or Array<string>)
375                    // Infer each source param individually against the rest element type
376                    for source_param in source_params.by_ref() {
377                        self.infer_from_types(
378                            target_param.type_id,
379                            source_param.type_id,
380                            priority,
381                        )?;
382                    }
383                }
384                break;
385            }
386
387            // Neither has rest param, do normal pairwise comparison
388            match (source_params.next(), target_params.next()) {
389                (Some(source_param), Some(target_param)) => {
390                    // Note the swapped arguments! This is the key to handling contravariance.
391                    self.infer_from_types(target_param.type_id, source_param.type_id, priority)?;
392                }
393                _ => break, // Mismatch in arity - stop here
394            }
395        }
396
397        // Return type is covariant: normal order
398        self.infer_from_types(source_sig.return_type, target_sig.return_type, priority)?;
399
400        // This type is contravariant
401        if let (Some(source_this), Some(target_this)) = (source_sig.this_type, target_sig.this_type)
402        {
403            self.infer_from_types(target_this, source_this, priority)?;
404        }
405
406        // Type predicates are covariant
407        if let (Some(source_pred), Some(target_pred)) =
408            (&source_sig.type_predicate, &target_sig.type_predicate)
409        {
410            // Compare targets by index if possible
411            let targets_match = match (source_pred.parameter_index, target_pred.parameter_index) {
412                (Some(s_idx), Some(t_idx)) => s_idx == t_idx,
413                _ => source_pred.target == target_pred.target,
414            };
415
416            tracing::trace!(
417                targets_match,
418                ?source_pred.parameter_index,
419                ?target_pred.parameter_index,
420                "Inferring from type predicates"
421            );
422
423            if targets_match
424                && source_pred.asserts == target_pred.asserts
425                && let (Some(source_ty), Some(target_ty)) =
426                    (source_pred.type_id, target_pred.type_id)
427            {
428                self.infer_from_types(source_ty, target_ty, priority)?;
429            }
430        }
431
432        Ok(())
433    }
434
435    /// Infer from tuple types
436    fn infer_tuples(
437        &mut self,
438        source_elems: TupleListId,
439        target_elems: TupleListId,
440        priority: InferencePriority,
441    ) -> Result<(), InferenceError> {
442        let source_list = self.interner.tuple_list(source_elems);
443        let target_list = self.interner.tuple_list(target_elems);
444
445        for (source_elem, target_elem) in source_list.iter().zip(target_list.iter()) {
446            self.infer_from_types(source_elem.type_id, target_elem.type_id, priority)?;
447        }
448
449        Ok(())
450    }
451
452    /// Infer from callable types, handling signatures and properties
453    fn infer_callables(
454        &mut self,
455        source_id: CallableShapeId,
456        target_id: CallableShapeId,
457        priority: InferencePriority,
458    ) -> Result<(), InferenceError> {
459        let source = self.interner.callable_shape(source_id);
460        let target = self.interner.callable_shape(target_id);
461
462        // For each call signature in the target, try to find a compatible one in the source
463        for target_sig in &target.call_signatures {
464            for source_sig in &source.call_signatures {
465                if source_sig.params.len() == target_sig.params.len() {
466                    for (s_param, t_param) in source_sig.params.iter().zip(target_sig.params.iter())
467                    {
468                        self.infer_from_types(t_param.type_id, s_param.type_id, priority)?;
469                    }
470                    self.infer_from_types(
471                        source_sig.return_type,
472                        target_sig.return_type,
473                        priority,
474                    )?;
475                    break;
476                }
477            }
478        }
479
480        // For each construct signature
481        for target_sig in &target.construct_signatures {
482            for source_sig in &source.construct_signatures {
483                if source_sig.params.len() == target_sig.params.len() {
484                    for (s_param, t_param) in source_sig.params.iter().zip(target_sig.params.iter())
485                    {
486                        self.infer_from_types(t_param.type_id, s_param.type_id, priority)?;
487                    }
488                    self.infer_from_types(
489                        source_sig.return_type,
490                        target_sig.return_type,
491                        priority,
492                    )?;
493                    break;
494                }
495            }
496        }
497
498        // Properties
499        for target_prop in &target.properties {
500            if let Some(source_prop) = source
501                .properties
502                .iter()
503                .find(|p| p.name == target_prop.name)
504            {
505                self.infer_from_types(source_prop.type_id, target_prop.type_id, priority)?;
506            }
507        }
508
509        // String index
510        if let (Some(target_idx), Some(source_idx)) = (&target.string_index, &source.string_index) {
511            self.infer_from_types(source_idx.value_type, target_idx.value_type, priority)?;
512        }
513
514        // Number index
515        if let (Some(target_idx), Some(source_idx)) = (&target.number_index, &source.number_index) {
516            self.infer_from_types(source_idx.value_type, target_idx.value_type, priority)?;
517        }
518
519        Ok(())
520    }
521
522    /// Infer from union types
523    fn infer_unions(
524        &mut self,
525        source_members: TypeListId,
526        target_members: TypeListId,
527        priority: InferencePriority,
528    ) -> Result<(), InferenceError> {
529        let source_list = self.interner.type_list(source_members);
530        let target_list = self.interner.type_list(target_members);
531
532        // TypeScript inference filtering: when the target union contains both
533        // type parameters and fixed types (e.g., `T | undefined`), strip source
534        // members that match fixed target members before inferring against the
535        // parameterized members. This prevents `undefined` in `number | undefined`
536        // from being inferred as a candidate for `T` in `T | undefined`.
537        let (parameterized, fixed): (Vec<TypeId>, Vec<TypeId>) = target_list
538            .iter()
539            .partition(|&&t| self.target_contains_inference_param(t));
540
541        if !parameterized.is_empty() && !fixed.is_empty() {
542            // Filter source: only infer members not already covered by fixed targets
543            for &source_ty in source_list.iter() {
544                let matches_fixed = fixed.contains(&source_ty);
545                if !matches_fixed {
546                    for &target_ty in &parameterized {
547                        self.infer_from_types(source_ty, target_ty, priority)?;
548                    }
549                }
550            }
551        } else {
552            // No filtering needed — fall back to exhaustive inference
553            for source_ty in source_list.iter() {
554                for target_ty in target_list.iter() {
555                    self.infer_from_types(*source_ty, *target_ty, priority)?;
556                }
557            }
558        }
559
560        Ok(())
561    }
562
563    /// Check if a target type directly is or contains an inference type parameter.
564    fn target_contains_inference_param(&self, target: TypeId) -> bool {
565        let Some(key) = self.interner.lookup(target) else {
566            return false;
567        };
568        match key {
569            TypeData::TypeParameter(ref info) => self.find_type_param(info.name).is_some(),
570            _ => false,
571        }
572    }
573
574    /// Infer from intersection types
575    fn infer_intersections(
576        &mut self,
577        source_members: TypeListId,
578        target_members: TypeListId,
579        priority: InferencePriority,
580    ) -> Result<(), InferenceError> {
581        let source_list = self.interner.type_list(source_members);
582        let target_list = self.interner.type_list(target_members);
583
584        // For intersections, we can pick any member that matches
585        for source_ty in source_list.iter() {
586            for target_ty in target_list.iter() {
587                // Don't fail if one member doesn't match
588                let _ = self.infer_from_types(*source_ty, *target_ty, priority);
589            }
590        }
591
592        Ok(())
593    }
594
595    /// Infer from `TypeApplication` (generic type instantiations)
596    fn infer_applications(
597        &mut self,
598        source_app: TypeApplicationId,
599        target_app: TypeApplicationId,
600        priority: InferencePriority,
601    ) -> Result<(), InferenceError> {
602        let source_info = self.interner.type_application(source_app);
603        let target_info = self.interner.type_application(target_app);
604
605        // The base types must match for inference to work
606        if source_info.base != target_info.base {
607            return Ok(());
608        }
609
610        // Recurse into the type arguments
611        for (source_arg, target_arg) in source_info.args.iter().zip(target_info.args.iter()) {
612            self.infer_from_types(*source_arg, *target_arg, priority)?;
613        }
614
615        Ok(())
616    }
617
618    // =========================================================================
619    // Task #40: Template Literal Deconstruction
620    // =========================================================================
621
622    /// Infer from template literal patterns with `infer` placeholders.
623    ///
624    /// This implements the "Reverse String Matcher" for extracting type information
625    /// from string literals that match template patterns like `user_${infer ID}`.
626    ///
627    /// # Example
628    ///
629    /// ```typescript
630    /// type GetID<T> = T extends `user_${infer ID}` ? ID : never;
631    /// // GetID<"user_123"> should infer ID = "123"
632    /// ```
633    ///
634    /// # Algorithm
635    ///
636    /// The matching is **non-greedy** for all segments except the last:
637    /// 1. Scan through template spans sequentially
638    /// 2. For text spans: match literal text at current position
639    /// 3. For infer type spans: capture text until next literal anchor (non-greedy)
640    /// 4. For the last span: capture all remaining text (greedy)
641    ///
642    /// # Arguments
643    ///
644    /// * `source` - The source type being checked (e.g., `"user_123"`)
645    /// * `source_key` - The `TypeData` of the source (cached for efficiency)
646    /// * `target_template` - The template literal pattern to match against
647    /// * `priority` - Inference priority for the extracted candidates
648    fn infer_from_template_literal(
649        &mut self,
650        source: TypeId,
651        source_key: Option<&TypeData>,
652        target_template: TemplateLiteralId,
653        priority: InferencePriority,
654    ) -> Result<(), InferenceError> {
655        let spans = self.interner.template_list(target_template);
656
657        // Special case: if source is `any` or the intrinsic `string` type, all infer vars get that type
658        if source == TypeId::ANY
659            || matches!(source_key, Some(TypeData::Intrinsic(IntrinsicKind::String)))
660        {
661            for span in spans.iter() {
662                if let TemplateSpan::Type(type_id) = span
663                    && let Some(TypeData::Infer(param_info)) = self.interner.lookup(*type_id)
664                    && let Some(var) = self.find_type_param(param_info.name)
665                {
666                    // Source is `any` or `string`, so infer that for all variables
667                    self.add_candidate(var, source, priority);
668                }
669            }
670            return Ok(());
671        }
672
673        // If source is a union, try to match each member against the template
674        if let Some(TypeData::Union(source_members)) = source_key {
675            let members = self.interner.type_list(*source_members);
676            for &member in members.iter() {
677                let member_key = self.interner.lookup(member);
678                self.infer_from_template_literal(
679                    member,
680                    member_key.as_ref(),
681                    target_template,
682                    priority,
683                )?;
684            }
685            return Ok(());
686        }
687
688        // For literal string types, perform the actual pattern matching
689        if let Some(source_str) = self.extract_string_literal(source)
690            && let Some(captures) = self.match_template_pattern(&source_str, &spans)
691        {
692            // Convert captured strings to literal types and add as candidates
693            for (infer_var, captured_string) in captures {
694                let literal_type = self.interner.literal_string(&captured_string);
695                self.add_candidate(infer_var, literal_type, priority);
696            }
697        }
698
699        Ok(())
700    }
701
702    /// Extract a string literal value from a `TypeId`.
703    ///
704    /// Returns None if the type is not a literal string.
705    fn extract_string_literal(&self, type_id: TypeId) -> Option<String> {
706        match self.interner.lookup(type_id) {
707            Some(TypeData::Literal(LiteralValue::String(s))) => Some(self.interner.resolve_atom(s)),
708            _ => None,
709        }
710    }
711
712    /// Match a source string against a template pattern, extracting infer variable bindings.
713    ///
714    /// # Arguments
715    ///
716    /// * `source` - The source string to match (e.g., `"user_123"`)
717    /// * `spans` - The template spans (e.g., `[Text("user_"), Type(ID), Text("_")]`)
718    ///
719    /// # Returns
720    ///
721    /// * `Some(bindings)` - Mapping from inference variables to captured strings
722    /// * `None` - The source doesn't match the pattern
723    fn match_template_pattern(
724        &self,
725        source: &str,
726        spans: &[TemplateSpan],
727    ) -> Option<Vec<(InferenceVar, String)>> {
728        let mut bindings = Vec::new();
729        let mut pos = 0;
730
731        for (i, span) in spans.iter().enumerate() {
732            let is_last = i == spans.len() - 1;
733
734            match span {
735                TemplateSpan::Text(text_atom) => {
736                    // Match literal text at current position
737                    let text = self.interner.resolve_atom(*text_atom).to_string();
738                    if !source.get(pos..)?.starts_with(&text) {
739                        return None; // Text doesn't match
740                    }
741                    pos += text.len();
742                }
743
744                TemplateSpan::Type(type_id) => {
745                    // Check if this is an infer variable
746                    if let Some(TypeData::Infer(param_info)) = self.interner.lookup(*type_id)
747                        && let Some(var) = self.find_type_param(param_info.name)
748                    {
749                        if is_last {
750                            // Last span: capture all remaining text (greedy)
751                            let captured = source[pos..].to_string();
752                            bindings.push((var, captured));
753                            pos = source.len();
754                        } else {
755                            // Non-last span: capture until next literal anchor (non-greedy)
756                            // Find the next text span to use as an anchor
757                            if let Some(anchor_text) = self.find_next_text_anchor(spans, i) {
758                                let anchor = self.interner.resolve_atom(anchor_text).to_string();
759                                // Find the first occurrence of the anchor (non-greedy)
760                                let capture_end = source[pos..].find(&anchor)? + pos;
761                                let captured = source[pos..capture_end].to_string();
762                                bindings.push((var, captured));
763                                pos = capture_end;
764                            } else {
765                                // No text anchor found (e.g., `${infer A}${infer B}`)
766                                // Capture empty string for non-greedy match and continue
767                                bindings.push((var, String::new()));
768                                // pos remains unchanged - next infer var starts here
769                            }
770                        }
771                    }
772                }
773            }
774        }
775
776        // Must have consumed the entire source string
777        (pos == source.len()).then_some(bindings)
778    }
779
780    /// Find the next text span after a given index to use as a matching anchor.
781    fn find_next_text_anchor(&self, spans: &[TemplateSpan], start_idx: usize) -> Option<Atom> {
782        spans.iter().skip(start_idx + 1).find_map(|span| {
783            if let TemplateSpan::Text(text) = span {
784                Some(*text)
785            } else {
786                None
787            }
788        })
789    }
790}