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 ¶meterized {
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}