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}