Skip to main content

tsz_solver/intern/
template.rs

1//! Template literal type interning and normalization.
2//!
3//! This module handles:
4//! - Template literal expansion to union types
5//! - Template span cardinality computation
6//! - Template literal normalization (merging adjacent text spans)
7//! - Template literal introspection (interpolation positions, span access)
8
9use super::{TEMPLATE_LITERAL_EXPANSION_LIMIT, TypeInterner};
10use crate::types::{LiteralValue, TemplateSpan, TypeData, TypeId};
11
12impl TypeInterner {
13    fn template_span_cardinality(&self, type_id: TypeId) -> Option<usize> {
14        // Handle BOOLEAN intrinsic (expands to 2 values: true | false)
15        if type_id == TypeId::BOOLEAN {
16            return Some(2);
17        }
18
19        // Handle intrinsic types that expand to string literals
20        if type_id == TypeId::BOOLEAN_TRUE
21            || type_id == TypeId::BOOLEAN_FALSE
22            || type_id == TypeId::NULL
23            || type_id == TypeId::UNDEFINED
24            || type_id == TypeId::VOID
25        {
26            return Some(1);
27        }
28
29        match self.lookup(type_id) {
30            // Accept all literal types (String, Number, Boolean, BigInt) - they all stringify
31            Some(TypeData::Literal(_)) => Some(1),
32            Some(TypeData::Union(list_id)) => {
33                let members = self.type_list(list_id);
34                let mut count = 0usize;
35                for member in members.iter() {
36                    // Recurse to handle all cases uniformly (literals, intrinsics, nested unions)
37                    let member_count = self.template_span_cardinality(*member)?;
38                    count = count.checked_add(member_count)?;
39                }
40                Some(count)
41            }
42            // Task #47: Handle nested template literals
43            Some(TypeData::TemplateLiteral(list_id)) => {
44                let spans = self.template_list(list_id);
45                let mut total = 1usize;
46                for span in spans.iter() {
47                    let span_count = match span {
48                        TemplateSpan::Text(_) => 1,
49                        TemplateSpan::Type(t) => self.template_span_cardinality(*t)?,
50                    };
51                    total = total.saturating_mul(span_count);
52                }
53                Some(total)
54            }
55            _ => None,
56        }
57    }
58
59    fn template_literal_exceeds_limit(&self, spans: &[TemplateSpan]) -> bool {
60        let mut total = 1usize;
61        for span in spans {
62            let span_count = match span {
63                TemplateSpan::Text(_) => Some(1),
64                TemplateSpan::Type(type_id) => self.template_span_cardinality(*type_id),
65            };
66            let Some(span_count) = span_count else {
67                return false;
68            };
69            total = total.saturating_mul(span_count);
70            if total > TEMPLATE_LITERAL_EXPANSION_LIMIT {
71                return true;
72            }
73        }
74        false
75    }
76
77    /// Check if a template literal can be expanded to a union of string literals.
78    /// Returns true if all type interpolations are string literals or unions of string literals.
79    fn can_expand_template_literal(&self, spans: &[TemplateSpan]) -> bool {
80        for span in spans {
81            if let TemplateSpan::Type(type_id) = span
82                && self.template_span_cardinality(*type_id).is_none()
83            {
84                return false;
85            }
86        }
87        true
88    }
89
90    /// Get the string literal values from a type (single literal or union of literals).
91    /// Returns None if the type is not a string literal or union of string literals.
92    fn get_string_literal_values(&self, type_id: TypeId) -> Option<Vec<String>> {
93        // Handle BOOLEAN intrinsic (expands to two string literals)
94        if type_id == TypeId::BOOLEAN {
95            return Some(vec!["true".to_string(), "false".to_string()]);
96        }
97
98        // Helper to convert a single type to a string value if possible
99        let to_string_val = |id: TypeId| -> Option<String> {
100            // Handle intrinsics that stringify to text
101            if id == TypeId::NULL {
102                return Some("null".to_string());
103            }
104            if id == TypeId::UNDEFINED || id == TypeId::VOID {
105                return Some("undefined".to_string());
106            }
107            if id == TypeId::BOOLEAN_TRUE {
108                return Some("true".to_string());
109            }
110            if id == TypeId::BOOLEAN_FALSE {
111                return Some("false".to_string());
112            }
113
114            // Handle literal types
115            match self.lookup(id) {
116                Some(TypeData::Literal(LiteralValue::String(atom))) => {
117                    Some(self.resolve_atom_ref(atom).to_string())
118                }
119                Some(TypeData::Literal(LiteralValue::Boolean(b))) => Some(b.to_string()),
120                Some(TypeData::Literal(LiteralValue::Number(n))) => {
121                    // TypeScript stringifies numbers in templates (e.g., 1 -> "1", 1.5 -> "1.5")
122                    Some(format!("{}", n.0))
123                }
124                Some(TypeData::Literal(LiteralValue::BigInt(atom))) => {
125                    // BigInts in templates are stringified (e.g., 100n -> "100")
126                    Some(self.resolve_atom_ref(atom).to_string())
127                }
128                _ => None,
129            }
130        };
131
132        // Handle the top-level type (either a single value or a union)
133        if let Some(val) = to_string_val(type_id) {
134            return Some(vec![val]);
135        }
136
137        match self.lookup(type_id) {
138            Some(TypeData::Union(list_id)) => {
139                let members = self.type_list(list_id);
140                let mut values = Vec::new();
141                for member in members.iter() {
142                    // RECURSIVE CALL: Handle boolean-in-union and nested unions correctly
143                    let member_values = self.get_string_literal_values(*member)?;
144                    values.extend(member_values);
145                }
146                Some(values)
147            }
148            // Task #47: Handle nested template literals by expanding them recursively
149            Some(TypeData::TemplateLiteral(list_id)) => {
150                let spans = self.template_list(list_id);
151                // Check if all spans are text-only (can return a single string)
152                if spans.iter().all(|s| matches!(s, TemplateSpan::Text(_))) {
153                    let mut combined = String::new();
154                    for span in spans.iter() {
155                        if let TemplateSpan::Text(atom) = span {
156                            combined.push_str(&self.resolve_atom_ref(*atom));
157                        }
158                    }
159                    return Some(vec![combined]);
160                }
161                // Otherwise, try to expand via Cartesian product (recursively call expand_template_literal_to_union)
162                // But we need to be careful not to cause infinite recursion
163                // For now, return None to indicate this template cannot be expanded as simple string literals
164                None
165            }
166            _ => None,
167        }
168    }
169
170    /// Expand a template literal with union interpolations into a union of string literals.
171    /// For example: `prefix-${"a" | "b"}-suffix` -> "prefix-a-suffix" | "prefix-b-suffix"
172    fn expand_template_literal_to_union(&self, spans: &[TemplateSpan]) -> TypeId {
173        // Collect text parts and interpolation alternatives
174        let mut parts: Vec<Vec<String>> = Vec::new();
175
176        for span in spans {
177            match span {
178                TemplateSpan::Text(atom) => {
179                    let text = self.resolve_atom_ref(*atom).to_string();
180                    parts.push(vec![text]);
181                }
182                TemplateSpan::Type(type_id) => {
183                    if let Some(values) = self.get_string_literal_values(*type_id) {
184                        parts.push(values);
185                    } else {
186                        // Should not happen if can_expand_template_literal returned true
187                        return TypeId::STRING;
188                    }
189                }
190            }
191        }
192
193        // Generate all combinations using Cartesian product
194        let mut combinations: Vec<String> = vec![String::new()];
195
196        for part in &parts {
197            let mut new_combinations = Vec::with_capacity(combinations.len() * part.len());
198            for prefix in &combinations {
199                for suffix in part {
200                    let mut combined = prefix.clone();
201                    combined.push_str(suffix);
202                    new_combinations.push(combined);
203                }
204            }
205            combinations = new_combinations;
206
207            // Safety check: should not exceed limit at this point, but verify
208            if combinations.len() > TEMPLATE_LITERAL_EXPANSION_LIMIT {
209                return TypeId::STRING;
210            }
211        }
212
213        // Create union of string literals
214        if combinations.is_empty() {
215            return TypeId::NEVER;
216        }
217
218        if combinations.len() == 1 {
219            return self.literal_string(&combinations[0]);
220        }
221
222        let members: Vec<TypeId> = combinations
223            .iter()
224            .map(|s| self.literal_string(s))
225            .collect();
226
227        self.union(members)
228    }
229
230    /// Normalize template literal spans by merging consecutive text spans
231    fn normalize_template_spans(&self, spans: Vec<TemplateSpan>) -> Vec<TemplateSpan> {
232        if spans.len() <= 1 {
233            return spans;
234        }
235
236        let mut normalized = Vec::with_capacity(spans.len());
237        let mut pending_text: Option<String> = None;
238        let mut has_consecutive_texts = false;
239
240        for span in &spans {
241            match span {
242                TemplateSpan::Text(atom) => {
243                    let text = self.resolve_atom_ref(*atom).to_string();
244                    if let Some(ref mut pt) = pending_text {
245                        pt.push_str(&text);
246                        has_consecutive_texts = true;
247                    } else {
248                        pending_text = Some(text);
249                    }
250                }
251                TemplateSpan::Type(type_id) => {
252                    // Task #47: Flatten nested template literals
253                    // If a Type(type_id) refers to another TemplateLiteral, splice its spans into the parent
254                    if let Some(TypeData::TemplateLiteral(nested_list_id)) = self.lookup(*type_id) {
255                        let nested_spans = self.template_list(nested_list_id);
256                        // Process each nested span as if it were part of the parent template
257                        for nested_span in nested_spans.iter() {
258                            match nested_span {
259                                TemplateSpan::Text(atom) => {
260                                    let text = self.resolve_atom_ref(*atom).to_string();
261                                    if let Some(ref mut pt) = pending_text {
262                                        pt.push_str(&text);
263                                        has_consecutive_texts = true;
264                                    } else {
265                                        pending_text = Some(text);
266                                    }
267                                }
268                                TemplateSpan::Type(nested_type_id) => {
269                                    // Flush pending text before adding the nested type
270                                    if let Some(text) = pending_text.take()
271                                        && !text.is_empty()
272                                    {
273                                        normalized
274                                            .push(TemplateSpan::Text(self.intern_string(&text)));
275                                    }
276                                    normalized.push(TemplateSpan::Type(*nested_type_id));
277                                }
278                            }
279                        }
280                        // Continue to the next span in the parent template
281                        continue;
282                    }
283
284                    // Task #47: Intrinsic stringification/expansion rules
285                    match *type_id {
286                        TypeId::NULL => {
287                            // null becomes text "null"
288                            let text = "null";
289                            if let Some(ref mut pt) = pending_text {
290                                pt.push_str(text);
291                                has_consecutive_texts = true;
292                            } else {
293                                pending_text = Some(text.to_string());
294                            }
295                            continue;
296                        }
297                        TypeId::UNDEFINED | TypeId::VOID => {
298                            // undefined/void becomes text "undefined"
299                            let text = "undefined";
300                            if let Some(ref mut pt) = pending_text {
301                                pt.push_str(text);
302                                has_consecutive_texts = true;
303                            } else {
304                                pending_text = Some(text.to_string());
305                            }
306                            continue;
307                        }
308                        // number, bigint, string intrinsics do NOT widen - they're kept as-is for pattern matching
309                        // BOOLEAN is also kept as-is for pattern matching - the general expansion logic handles it
310                        _ => {}
311                    }
312
313                    // Task #47: Remove empty string literals from interpolations
314                    // An empty string literal contributes nothing to the template
315                    if let Some(TypeData::Literal(LiteralValue::String(s))) = self.lookup(*type_id)
316                    {
317                        let s = self.resolve_atom_ref(s);
318                        if s.is_empty() {
319                            // Skip this empty string literal
320                            // Flush pending text first
321                            if let Some(text) = pending_text.take()
322                                && !text.is_empty()
323                            {
324                                normalized.push(TemplateSpan::Text(self.intern_string(&text)));
325                            }
326                            // Don't add the empty type span - continue to next span
327                            continue;
328                        }
329                    }
330
331                    // Flush any pending text before adding a type span
332                    if let Some(text) = pending_text.take()
333                        && !text.is_empty()
334                    {
335                        normalized.push(TemplateSpan::Text(self.intern_string(&text)));
336                    }
337                    normalized.push(TemplateSpan::Type(*type_id));
338                }
339            }
340        }
341
342        // Flush any remaining pending text
343        if let Some(text) = pending_text
344            && !text.is_empty()
345        {
346            normalized.push(TemplateSpan::Text(self.intern_string(&text)));
347        }
348
349        // If no normalization occurred, return original to avoid unnecessary allocation
350        if !has_consecutive_texts && normalized.len() == spans.len() {
351            return spans;
352        }
353
354        normalized
355    }
356
357    /// Intern a template literal type
358    pub fn template_literal(&self, spans: Vec<TemplateSpan>) -> TypeId {
359        // Task #47: High-level absorption and widening (Pass 1)
360        // These checks must happen BEFORE structural normalization
361
362        // Never absorption: if any part is never, the whole type is never
363        for span in &spans {
364            if let TemplateSpan::Type(type_id) = span
365                && *type_id == TypeId::NEVER
366            {
367                return TypeId::NEVER;
368            }
369        }
370
371        // Unknown and Any widening: if any part is unknown or any, the whole type is string
372        // Note: string intrinsic does NOT widen (it's used for pattern matching)
373        for span in &spans {
374            if let TemplateSpan::Type(type_id) = span
375                && (*type_id == TypeId::UNKNOWN || *type_id == TypeId::ANY)
376            {
377                return TypeId::STRING;
378            }
379        }
380
381        // Normalize spans by merging consecutive text spans (Pass 2)
382        let normalized = self.normalize_template_spans(spans);
383
384        // Check if expansion would exceed the limit
385        if self.template_literal_exceeds_limit(&normalized) {
386            return TypeId::STRING;
387        }
388
389        // Try to expand to union of string literals if all interpolations are expandable
390        if self.can_expand_template_literal(&normalized) {
391            // Check if there are any type interpolations
392            let has_type_interpolations = normalized
393                .iter()
394                .any(|s| matches!(s, TemplateSpan::Type(_)));
395
396            if has_type_interpolations {
397                return self.expand_template_literal_to_union(&normalized);
398            }
399
400            // If only text spans, combine them into a single string literal
401            if normalized
402                .iter()
403                .all(|s| matches!(s, TemplateSpan::Text(_)))
404            {
405                let mut combined = String::new();
406                for span in &normalized {
407                    if let TemplateSpan::Text(atom) = span {
408                        combined.push_str(&self.resolve_atom_ref(*atom));
409                    }
410                }
411                return self.literal_string(&combined);
412            }
413        }
414
415        let list_id = self.intern_template_list(normalized);
416        self.intern(TypeData::TemplateLiteral(list_id))
417    }
418
419    /// Get the interpolation positions from a template literal type
420    /// Returns indices of type interpolation spans
421    pub fn template_literal_interpolation_positions(&self, type_id: TypeId) -> Vec<usize> {
422        match self.lookup(type_id) {
423            Some(TypeData::TemplateLiteral(spans_id)) => {
424                let spans = self.template_list(spans_id);
425                spans
426                    .iter()
427                    .enumerate()
428                    .filter_map(|(idx, span)| match span {
429                        TemplateSpan::Type(_) => Some(idx),
430                        _ => None,
431                    })
432                    .collect()
433            }
434            _ => Vec::new(),
435        }
436    }
437
438    /// Get the span at a given position from a template literal type
439    pub fn template_literal_get_span(&self, type_id: TypeId, index: usize) -> Option<TemplateSpan> {
440        match self.lookup(type_id) {
441            Some(TypeData::TemplateLiteral(spans_id)) => {
442                let spans = self.template_list(spans_id);
443                spans.get(index).cloned()
444            }
445            _ => None,
446        }
447    }
448
449    /// Get the number of spans in a template literal type
450    pub fn template_literal_span_count(&self, type_id: TypeId) -> usize {
451        match self.lookup(type_id) {
452            Some(TypeData::TemplateLiteral(spans_id)) => {
453                let spans = self.template_list(spans_id);
454                spans.len()
455            }
456            _ => 0,
457        }
458    }
459
460    /// Check if a template literal contains only text (no interpolations)
461    /// Also returns true for string literals (which are the result of text-only template expansion)
462    pub fn template_literal_is_text_only(&self, type_id: TypeId) -> bool {
463        match self.lookup(type_id) {
464            Some(TypeData::TemplateLiteral(spans_id)) => {
465                let spans = self.template_list(spans_id);
466                spans.iter().all(TemplateSpan::is_text)
467            }
468            // String literals are the result of text-only template expansion
469            Some(TypeData::Literal(LiteralValue::String(_))) => true,
470            _ => false,
471        }
472    }
473}