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