1use crate::Type;
3use crate::type_size::{TypeLimits, TypeTooLarge};
4
5pub fn matches_name(domain: &Type, name: &str, limits: &TypeLimits) -> bool {
6 if !limits.spend_work(name.len() as u64) {
7 return false;
8 }
9 let Ok(units) = crate::literal_units::try_literal_units(name) else {
10 limits.record(TypeTooLarge::Allocation);
11 return false;
12 };
13 let result = matches_units(domain, &units, false, &mut |work| {
14 limits
15 .spend_work(work)
16 .then_some(())
17 .ok_or(TypeTooLarge::Work)
18 });
19 match result {
20 Ok(value) => value,
21 Err(MatchError::Work(error)) => {
22 limits.record(error);
23 false
24 }
25 Err(MatchError::Allocation) => {
26 limits.record(TypeTooLarge::Allocation);
27 false
28 }
29 }
30}
31
32pub fn canonical_number(name: &str) -> bool {
33 name.parse::<f64>()
34 .is_ok_and(|number| crate::runtime::number::format_number_js(number) == name)
35}
36
37#[derive(Debug)]
38pub enum MatchError<E> {
39 Work(E),
40 Allocation,
41}
42
43pub fn matches_units<E>(
47 domain: &Type,
48 units: &[u16],
49 substitution: bool,
50 charge: &mut impl FnMut(u64) -> Result<(), E>,
51) -> Result<bool, MatchError<E>> {
52 charge(1).map_err(MatchError::Work)?;
53 Ok(match domain.peel() {
54 Type::String => true,
55 Type::StringLiteral(value) => {
56 charge(value.len() as u64).map_err(MatchError::Work)?;
57 literal_units(value)?.as_slice() == units
58 }
59 Type::Number | Type::NumberLiteral(_) => {
60 charge(units.len() as u64).map_err(MatchError::Work)?;
61 let Some(text) = decode_units(units)? else {
62 return Ok(false);
63 };
64 match domain.peel() {
65 Type::NumberLiteral(value) => {
66 crate::runtime::number::format_number_js(value.0) == text
67 }
68 _ if substitution => {
69 !text.is_empty()
70 && crate::runtime::number::string_to_number_js(&text).is_finite()
71 }
72 _ => canonical_number(&text),
73 }
74 }
75 Type::BigInt | Type::BigIntLiteral(_) if substitution => {
76 charge(units.len() as u64).map_err(MatchError::Work)?;
77 let Some(text) = decode_units(units)? else {
78 return Ok(false);
79 };
80 match domain.peel() {
81 Type::BigIntLiteral(value) => value == &text,
82 _ => bigint_text(&text),
83 }
84 }
85 Type::Boolean if substitution => {
86 units == [116, 114, 117, 101] || units == [102, 97, 108, 115, 101]
87 }
88 Type::BooleanLiteral(value) if substitution => {
89 value.to_string().encode_utf16().eq(units.iter().copied())
90 }
91 Type::Null if substitution => units == [110, 117, 108, 108],
92 Type::Undefined if substitution => units == [117, 110, 100, 101, 102, 105, 110, 101, 100],
93 Type::Union(members) => {
94 for member in members {
95 if matches_units(member, units, substitution, charge)? {
96 return Ok(true);
97 }
98 }
99 false
100 }
101 Type::TemplateLiteral {
102 parts,
103 substitutions,
104 } => matches_template(parts, substitutions, units, charge)?,
105 _ => false,
106 })
107}
108
109fn matches_template<E>(
110 parts: &[String],
111 substitutions: &[Type],
112 units: &[u16],
113 charge: &mut impl FnMut(u64) -> Result<(), E>,
114) -> Result<bool, MatchError<E>> {
115 let Some(first) = parts.first() else {
116 return Ok(false);
117 };
118 if substitutions.is_empty() {
119 return matches_units(&Type::StringLiteral(first.clone()), units, false, charge);
120 }
121 let Some(last) = parts.last() else {
122 return Ok(false);
123 };
124 charge(first.len().saturating_add(last.len()) as u64).map_err(MatchError::Work)?;
125 let prefix = literal_units(first)?;
126 let suffix = literal_units(last)?;
127 if units.len() < prefix.len().saturating_add(suffix.len())
128 || !units.starts_with(&prefix)
129 || !units.ends_with(&suffix)
130 {
131 return Ok(false);
132 }
133 let remaining_end = units.len() - suffix.len();
134 let mut position = prefix.len();
135 for (index, hole) in substitutions.iter().enumerate() {
136 let last_hole = index.saturating_add(1) == substitutions.len();
137 let (end, delimiter_length) = if last_hole {
138 (remaining_end, 0)
139 } else {
140 let Some(delimiter) = parts.get(index.saturating_add(1)) else {
141 return Ok(false);
142 };
143 charge(delimiter.len() as u64).map_err(MatchError::Work)?;
144 let delimiter = literal_units(delimiter)?;
145 if delimiter.is_empty() {
146 if position >= remaining_end {
147 return Ok(false);
148 }
149 (position + 1, 0)
150 } else {
151 let mut found = None;
152 for candidate in position..=remaining_end {
153 charge(delimiter.len() as u64).map_err(MatchError::Work)?;
154 if units
155 .get(candidate..remaining_end)
156 .is_some_and(|tail| tail.starts_with(&delimiter))
157 {
158 found = Some(candidate);
159 break;
160 }
161 }
162 let Some(end) = found else {
163 return Ok(false);
164 };
165 (end, delimiter.len())
166 }
167 };
168 let Some(value) = units.get(position..end) else {
169 return Ok(false);
170 };
171 if !matches_units(hole, value, true, charge)? {
172 return Ok(false);
173 }
174 position = end.saturating_add(delimiter_length);
175 }
176 Ok(true)
177}
178
179fn literal_units<E>(text: &str) -> Result<Vec<u16>, MatchError<E>> {
180 crate::literal_units::try_literal_units(text).map_err(|_| MatchError::Allocation)
181}
182
183fn decode_units<E>(units: &[u16]) -> Result<Option<String>, MatchError<E>> {
184 let capacity = units.len().checked_mul(3).ok_or(MatchError::Allocation)?;
185 let mut text = String::new();
186 text.try_reserve(capacity)
187 .map_err(|_| MatchError::Allocation)?;
188 for scalar in char::decode_utf16(units.iter().copied()) {
189 let Ok(scalar) = scalar else {
190 return Ok(None);
191 };
192 text.push(scalar);
193 }
194 Ok(Some(text))
195}
196
197fn bigint_text(text: &str) -> bool {
198 let text = text.strip_prefix('-').unwrap_or(text);
199 let (digits, radix) =
200 if let Some(digits) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
201 (digits, 16)
202 } else if let Some(digits) = text.strip_prefix("0o").or_else(|| text.strip_prefix("0O")) {
203 (digits, 8)
204 } else if let Some(digits) = text.strip_prefix("0b").or_else(|| text.strip_prefix("0B")) {
205 (digits, 2)
206 } else {
207 (text, 10)
208 };
209 !digits.is_empty()
210 && (radix != 10 || digits.len() == 1 || !digits.starts_with('0'))
211 && digits
212 .chars()
213 .all(|digit| digit.is_ascii() && digit.is_digit(radix))
214}
215
216pub fn pattern(ty: &Type) -> Option<String> {
217 match ty.peel() {
218 Type::String => Some("[\\s\\S]*".into()),
219 Type::Number => Some(r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?".into()),
220 Type::BigInt => Some(r"-?(?:[0-9]+|0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][01]+)".into()),
221 Type::BigIntLiteral(value) => Some(regex::escape(value)),
222 Type::Null => Some("null".into()),
223 Type::Undefined => Some("undefined".into()),
224 Type::StringLiteral(value) => Some(literal_pattern(value)),
225 Type::NumberLiteral(value) => Some(regex::escape(
226 &crate::runtime::number::format_number_js(value.0),
227 )),
228 Type::BooleanLiteral(value) => Some(value.to_string()),
229 Type::Boolean => Some("(?:true|false)".into()),
230 Type::Union(members) => Some(format!(
231 "(?:{})",
232 members
233 .iter()
234 .map(pattern)
235 .collect::<Option<Vec<_>>>()?
236 .join("|")
237 )),
238 Type::TemplateLiteral {
239 parts,
240 substitutions,
241 } => {
242 if parts.len() != substitutions.len().checked_add(1)? {
243 return None;
244 }
245 let mut result = String::new();
246 for (position, part) in parts.iter().enumerate() {
247 result.push_str(&literal_pattern(part));
248 if let Some(substitution) = substitutions.get(position) {
249 if position + 1 < substitutions.len() && parts.get(position + 1)?.is_empty() {
250 if !matches!(substitution.peel(), Type::String) {
251 return None;
252 }
253 result.push_str("[\\s\\S]");
254 } else {
255 result.push_str(&pattern(substitution)?);
256 }
257 }
258 }
259 Some(result)
260 }
261 _ => None,
262 }
263}
264
265fn literal_pattern(text: &str) -> String {
266 let mut result = String::new();
267 for unit in crate::literal_units::literal_units(text) {
268 result.push_str(&format!("\\u{unit:04x}"));
269 }
270 result
271}
272
273pub fn runtime_domain(ty: &Type) -> Result<Type, TypeTooLarge> {
276 fn simplify(
277 ty: &Type,
278 depth: u32,
279 budget: &mut crate::type_size::TypeBudget<'_>,
280 ) -> Result<Type, TypeTooLarge> {
281 budget.charge(depth)?;
282 let simplified = crate::type_size::map_children(ty.peel(), |child| {
283 simplify(child, depth.saturating_add(1), budget)
284 })?;
285 let simplified = match simplified {
286 Type::Union(members) => Type::union(members),
287 other => other,
288 };
289 normalize_template(simplified, budget)
290 }
291 let limits = TypeLimits::default();
292 simplify(ty, 1, &mut limits.budget())
293}
294
295pub fn normalize_template(
299 ty: Type,
300 budget: &mut crate::type_size::TypeBudget<'_>,
301) -> Result<Type, TypeTooLarge> {
302 let Type::TemplateLiteral {
303 parts,
304 substitutions,
305 } = ty
306 else {
307 return Ok(ty);
308 };
309 if parts.len() != substitutions.len().saturating_add(1) {
310 return Err(TypeTooLarge::InvalidTemplate);
311 }
312 let first = parts.first().ok_or(TypeTooLarge::InvalidTemplate)?;
313 budget.charge_work(first.len() as u64)?;
314 let mut builders = vec![TemplateBuilder {
315 first: first.clone(),
316 holes: Vec::new(),
317 }];
318 for (hole, suffix) in substitutions.into_iter().zip(parts.into_iter().skip(1)) {
319 let alternatives = match hole.peel() {
320 Type::Union(members) => members.clone(),
321 Type::Boolean => vec![Type::BooleanLiteral(false), Type::BooleanLiteral(true)],
322 Type::Never => return Ok(Type::Never),
323 _ => vec![hole],
324 };
325 let mut expanded = Vec::new();
326 for builder in builders {
327 for alternative in &alternatives {
328 budget.charge(1)?;
329 budget.charge_work(
330 (builder.first.len() as u64)
331 .saturating_add(
332 builder
333 .holes
334 .iter()
335 .map(|hole| hole.suffix.len() as u64)
336 .sum::<u64>(),
337 )
338 .saturating_add(suffix.len() as u64),
339 )?;
340 for hole in &builder.holes {
341 budget.charge_copy(&hole.ty, 1)?;
342 }
343 budget.charge_copy(alternative, 1)?;
344 let mut next = builder.clone();
345 next.append(alternative, &suffix)?;
346 expanded.push(next);
347 }
348 }
349 builders = expanded;
350 }
351 Ok(Type::union(
352 builders.into_iter().map(TemplateBuilder::finish).collect(),
353 ))
354}
355
356#[derive(Clone)]
357struct TemplateBuilder {
358 first: String,
359 holes: Vec<TemplateHole>,
360}
361
362#[derive(Clone)]
363struct TemplateHole {
364 ty: Type,
365 suffix: String,
366}
367
368impl TemplateBuilder {
369 fn tail(&mut self) -> &mut String {
370 match self.holes.last_mut() {
371 Some(hole) => &mut hole.suffix,
372 None => &mut self.first,
373 }
374 }
375
376 fn append(&mut self, hole: &Type, suffix: &str) -> Result<(), TypeTooLarge> {
377 let literal = match hole.peel() {
378 Type::StringLiteral(text) | Type::BigIntLiteral(text) => Some(text.clone()),
379 Type::NumberLiteral(value) => Some(crate::runtime::number::format_number_js(value.0)),
380 Type::BooleanLiteral(value) => Some(value.to_string()),
381 Type::Null => Some("null".into()),
382 Type::Undefined => Some("undefined".into()),
383 _ => None,
384 };
385 if let Some(literal) = literal {
386 crate::literal_units::push_literal_text(self.tail(), &literal);
387 crate::literal_units::push_literal_text(self.tail(), suffix);
388 } else if let Type::TemplateLiteral {
389 parts,
390 substitutions,
391 } = hole.peel()
392 {
393 if parts.len() != substitutions.len().saturating_add(1) {
394 return Err(TypeTooLarge::InvalidTemplate);
395 }
396 let first = parts.first().ok_or(TypeTooLarge::InvalidTemplate)?;
397 crate::literal_units::push_literal_text(self.tail(), first);
398 self.holes
399 .extend(
400 substitutions
401 .iter()
402 .zip(parts.iter().skip(1))
403 .map(|(ty, suffix)| TemplateHole {
404 ty: ty.clone(),
405 suffix: suffix.clone(),
406 }),
407 );
408 crate::literal_units::push_literal_text(self.tail(), suffix);
409 } else {
410 self.holes.push(TemplateHole {
411 ty: hole.clone(),
412 suffix: suffix.into(),
413 });
414 }
415 Ok(())
416 }
417
418 fn finish(self) -> Type {
419 if self.holes.is_empty() {
420 return Type::StringLiteral(self.first);
421 }
422 if self.first.is_empty()
423 && self
424 .holes
425 .iter()
426 .all(|hole| hole.suffix.is_empty() && matches!(hole.ty.peel(), Type::String))
427 {
428 return Type::String;
429 }
430 let (substitutions, suffixes): (Vec<_>, Vec<_>) = self
431 .holes
432 .into_iter()
433 .map(|hole| (hole.ty, hole.suffix))
434 .unzip();
435 let parts = std::iter::once(self.first).chain(suffixes).collect();
436 Type::TemplateLiteral {
437 parts,
438 substitutions,
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 fn template(parts: &[&str], substitutions: Vec<Type>) -> Type {
448 Type::TemplateLiteral {
449 parts: parts.iter().map(ToString::to_string).collect(),
450 substitutions,
451 }
452 }
453
454 #[test]
455 fn numeric_names_use_canonical_javascript_spelling() {
456 for name in ["0", "1.5", "1e+21", "NaN", "Infinity", "-Infinity"] {
457 assert!(canonical_number(name), "{name}");
458 }
459 for name in ["-0", "01", "1e21", " 1", "0x10", ""] {
460 assert!(!canonical_number(name), "{name}");
461 }
462 }
463
464 #[test]
465 fn template_matching_uses_typescript_splits_and_numeric_syntax() {
466 let limits = TypeLimits::default();
467 let numeric = template(&["n_", ""], vec![Type::Number]);
468 for name in ["n_ ", "n_0x10", "n_-0", "n_1.5"] {
469 assert!(matches_name(&numeric, name, &limits), "{name}");
470 }
471 for name in ["n_", "n_NaN", "n_Infinity", "n_1e999"] {
472 assert!(!matches_name(&numeric, name, &limits), "{name}");
473 }
474 assert!(!matches_name(
475 &template(&["", "", ""], vec![Type::String, Type::Number]),
476 "xx42",
477 &limits
478 ));
479 assert!(!matches_name(
480 &template(&["", "_", ""], vec![Type::String, Type::Number]),
481 "a_b_2",
482 &limits
483 ));
484 let bigint = template(&["", ""], vec![Type::BigInt]);
485 assert!(matches_name(&bigint, "-0x10", &limits));
486 assert!(!matches_name(&bigint, "01", &limits));
487 assert!(!matches_name(&bigint, "+1", &limits));
488 }
489
490 #[test]
491 fn matching_preserves_lone_surrogates_and_stops_when_work_is_refused() {
492 let domain = template(&["id_", ""], vec![Type::String]);
493 let units = [105, 100, 95, 0xd800];
494 assert!(matches_units(&domain, &units, false, &mut |_| Ok::<_, ()>(())).unwrap());
495 assert!(matches!(
496 matches_units(&domain, &units, false, &mut |_| Err(())),
497 Err(MatchError::Work(()))
498 ));
499 }
500
501 #[test]
502 fn normalization_joins_literal_holes_and_checks_shape_and_work() {
503 let limits = TypeLimits::default();
504 let result = normalize_template(
505 template(
506 &["id_", "", ""],
507 vec![Type::StringLiteral("abc".into()), Type::Number],
508 ),
509 &mut limits.budget(),
510 )
511 .unwrap();
512 assert!(matches_name(&result, "id_abc12", &limits));
513 assert!(
514 normalize_template(
515 template(&["missing suffix"], vec![Type::Number]),
516 &mut limits.budget()
517 )
518 .is_err()
519 );
520 let exhausted = TypeLimits::with_work_allowance(0);
521 assert!(
522 normalize_template(
523 template(&["prefix", ""], vec![Type::String]),
524 &mut exhausted.budget()
525 )
526 .is_err()
527 );
528 }
529}