Skip to main content

tsz_solver/evaluation/evaluate_rules/
template_literal.rs

1//! Template literal type evaluation.
2//!
3//! Handles TypeScript template literal types like "hello ${T}".
4
5use crate::relations::subtype::TypeResolver;
6use crate::types::{LiteralValue, TemplateLiteralId, TemplateSpan, TypeData, TypeId};
7
8use super::super::evaluate::TypeEvaluator;
9
10impl<'a, R: TypeResolver> TypeEvaluator<'a, R> {
11    /// Evaluate a template literal type: `hello${T}world`
12    ///
13    /// Template literals evaluate to a union of all possible literal string combinations.
14    /// For example: `get${K}` where K = "a" | "b" evaluates to "geta" | "getb"
15    /// Multiple unions compute a Cartesian product: `${"a"|"b"}-${"x"|"y"}` => "a-x"|"a-y"|"b-x"|"b-y"
16    pub fn evaluate_template_literal(&mut self, spans: TemplateLiteralId) -> TypeId {
17        use crate::intern::TEMPLATE_LITERAL_EXPANSION_LIMIT;
18
19        let span_list = self.interner().template_list(spans);
20
21        tracing::trace!(
22            span_count = span_list.len(),
23            "evaluate_template_literal: called with {} spans",
24            span_list.len()
25        );
26
27        // Check if all spans are just text (no interpolation)
28        let all_text = span_list
29            .iter()
30            .all(|span| matches!(span, TemplateSpan::Text(_)));
31
32        if all_text {
33            tracing::trace!("evaluate_template_literal: all text - concatenating");
34            // Concatenate all text spans into a single string literal
35            let mut result = String::new();
36            for span in span_list.iter() {
37                if let TemplateSpan::Text(atom) = span {
38                    result.push_str(self.interner().resolve_atom_ref(*atom).as_ref());
39                }
40            }
41            return self.interner().literal_string(&result);
42        }
43
44        // PERF: Pre-evaluate all type spans once and cache results.
45        // This avoids double evaluation in the size-check loop and expansion loop.
46        let mut evaluated_spans = Vec::with_capacity(span_list.len());
47        let mut total_combinations: usize = 1;
48
49        for span in span_list.iter() {
50            match span {
51                TemplateSpan::Text(_atom) => {
52                    evaluated_spans.push(None); // Marker for text span
53                }
54                TemplateSpan::Type(type_id) => {
55                    let evaluated = self.evaluate(*type_id);
56                    let strings = self.extract_literal_strings(evaluated);
57
58                    if strings.is_empty() {
59                        // Contains non-literal types, can't fully evaluate
60                        return self.interner().template_literal(span_list.to_vec());
61                    }
62
63                    total_combinations = total_combinations.saturating_mul(strings.len());
64                    if total_combinations > TEMPLATE_LITERAL_EXPANSION_LIMIT {
65                        // Would exceed limit - keep unexpanded
66                        return self.interner().template_literal(span_list.to_vec());
67                    }
68                    evaluated_spans.push(Some(strings));
69                }
70            }
71        }
72
73        // Check if we can fully evaluate to a union of literals
74        let mut combinations = vec![String::new()];
75
76        for (i, span) in span_list.iter().enumerate() {
77            match span {
78                TemplateSpan::Text(atom) => {
79                    let text = self.interner().resolve_atom_ref(*atom);
80                    for combo in &mut combinations {
81                        combo.push_str(text.as_ref());
82                    }
83                }
84                TemplateSpan::Type(_) => {
85                    // Safety: index i always matches evaluated_spans length
86                    let string_values = evaluated_spans[i].as_ref().unwrap();
87                    let new_size = combinations.len() * string_values.len();
88
89                    // Pre-allocate to minimize reallocations during Cartesian product
90                    let mut new_combinations = Vec::with_capacity(new_size);
91                    for combo in &combinations {
92                        for value in string_values {
93                            // OPTIMIZATION: Reserve exact capacity for the new string
94                            let mut new_combo = String::with_capacity(combo.len() + value.len());
95                            new_combo.push_str(combo);
96                            new_combo.push_str(value);
97                            new_combinations.push(new_combo);
98                        }
99                    }
100                    combinations = new_combinations;
101                }
102            }
103        }
104
105        // Convert combinations to union of literal strings
106        if combinations.is_empty() {
107            return TypeId::NEVER;
108        }
109
110        let literal_types: Vec<TypeId> = combinations
111            .iter()
112            .map(|s| self.interner().literal_string(s))
113            .collect();
114
115        if literal_types.len() == 1 {
116            literal_types[0]
117        } else {
118            self.interner().union(literal_types)
119        }
120    }
121
122    /// Maximum recursion depth for counting literal members to prevent stack overflow
123    const MAX_LITERAL_COUNT_DEPTH: u32 = 50;
124
125    /// Count the number of literal members that can be converted to strings.
126    /// Returns 0 if the type contains non-literal types that cannot be stringified.
127    pub fn count_literal_members(&self, type_id: TypeId) -> usize {
128        self.count_literal_members_impl(type_id, 0)
129    }
130
131    /// Internal implementation with depth tracking.
132    fn count_literal_members_impl(&self, type_id: TypeId, depth: u32) -> usize {
133        // Prevent infinite recursion in deeply nested union types
134        if depth > Self::MAX_LITERAL_COUNT_DEPTH {
135            return 0; // Abort - too deep
136        }
137
138        if let Some(TypeData::Union(members)) = self.interner().lookup(type_id) {
139            let members = self.interner().type_list(members);
140            let mut count = 0;
141            for &member in members.iter() {
142                let member_count = self.count_literal_members_impl(member, depth + 1);
143                if member_count == 0 {
144                    return 0;
145                }
146                count += member_count;
147            }
148            count
149        } else if let Some(TypeData::Literal(_)) = self.interner().lookup(type_id) {
150            1
151        } else if let Some(TypeData::Enum(_, structural_type)) = self.interner().lookup(type_id) {
152            // Enum member types wrap a literal - delegate to the structural type
153            self.count_literal_members_impl(structural_type, depth + 1)
154        } else if type_id == TypeId::STRING
155            || type_id == TypeId::NUMBER
156            || type_id == TypeId::BOOLEAN
157            || type_id == TypeId::BIGINT
158        {
159            // Primitive types can't be fully enumerated
160            0
161        } else {
162            0
163        }
164    }
165
166    /// Extract string representations from a type.
167    /// Handles string, number, boolean, and bigint literals, converting them to their string form.
168    /// For unions, extracts all members recursively.
169    pub fn extract_literal_strings(&self, type_id: TypeId) -> Vec<String> {
170        self.extract_literal_strings_impl(type_id, 0)
171    }
172
173    /// Internal implementation with depth tracking.
174    fn extract_literal_strings_impl(&self, type_id: TypeId, depth: u32) -> Vec<String> {
175        // Prevent infinite recursion in deeply nested union types
176        if depth > Self::MAX_LITERAL_COUNT_DEPTH {
177            return Vec::new(); // Abort - too deep
178        }
179
180        if let Some(TypeData::Union(members)) = self.interner().lookup(type_id) {
181            let members = self.interner().type_list(members);
182            let mut result = Vec::new();
183            for &member in members.iter() {
184                let strings = self.extract_literal_strings_impl(member, depth + 1);
185                if strings.is_empty() {
186                    // Union contains a non-stringifiable type
187                    return Vec::new();
188                }
189                result.extend(strings);
190            }
191            result
192        } else if let Some(TypeData::Literal(lit)) = self.interner().lookup(type_id) {
193            match lit {
194                LiteralValue::String(atom) => {
195                    vec![self.interner().resolve_atom_ref(atom).to_string()]
196                }
197                LiteralValue::Number(n) => {
198                    // Convert number to string matching JavaScript's Number::toString(10)
199                    // ECMAScript spec: use scientific notation if |x| < 10^-6 or |x| >= 10^21
200                    let n_val = n.0;
201                    let abs_val = n_val.abs();
202
203                    tracing::trace!(
204                        number = n_val,
205                        abs_val = abs_val,
206                        "extract_literal_strings: converting number to string"
207                    );
208
209                    if !(1e-6..1e21).contains(&abs_val) {
210                        // Use scientific notation (Rust adds sign for negative exponents, but not positive)
211                        let mut s = format!("{n_val:e}");
212                        // Rust outputs "1e-7" for 1e-7 (good) but "1e21" instead of "1e+21" for 1e21
213                        // We need to add "+" to positive exponents
214                        if s.contains("e") && !s.contains("e-") && !s.contains("e+") {
215                            let parts: Vec<&str> = s.split('e').collect();
216                            if parts.len() == 2 {
217                                s = format!("{}e+{}", parts[0], parts[1]);
218                            }
219                        }
220                        tracing::trace!(result = %s, "extract_literal_strings: scientific notation");
221                        vec![s]
222                    } else if n_val.fract() == 0.0 && abs_val < 1e15 {
223                        // Integer-like number - avoid scientific notation
224                        let s = format!("{}", n_val as i64);
225                        tracing::trace!(result = %s, "extract_literal_strings: integer-like");
226                        vec![s]
227                    } else {
228                        // Fixed-point notation
229                        let s = format!("{n_val}");
230                        tracing::trace!(result = %s, "extract_literal_strings: fixed-point");
231                        vec![s]
232                    }
233                }
234                LiteralValue::Boolean(b) => {
235                    vec![if b {
236                        "true".to_string()
237                    } else {
238                        "false".to_string()
239                    }]
240                }
241                LiteralValue::BigInt(atom) => {
242                    // BigInt literals are stored without the 'n' suffix
243                    vec![self.interner().resolve_atom_ref(atom).to_string()]
244                }
245            }
246        } else if let Some(TypeData::Enum(_, structural_type)) = self.interner().lookup(type_id) {
247            // Enum member types wrap a literal (e.g., AnimalType.cat wraps "cat").
248            // Delegate to the structural type to extract the underlying literal string.
249            self.extract_literal_strings_impl(structural_type, depth + 1)
250        } else {
251            // Not a literal type - can't extract string
252            Vec::new()
253        }
254    }
255}