1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{ExprId, Ident, MangledName, Type, types::LiteralF64};
6
7#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct ReferencePath {
9 pub root: BindingId,
10 pub chain: Vec<PathElem>,
11}
12
13impl ReferencePath {
14 pub fn is_bare_local(&self) -> bool {
17 self.chain.is_empty() && matches!(self.root, BindingId::Local { .. })
18 }
19
20 pub fn root(root: BindingId) -> Self {
21 Self {
22 root,
23 chain: Vec::new(),
24 }
25 }
26
27 pub fn is_prefix_of(&self, other: &ReferencePath) -> bool {
32 self.root == other.root
33 && self.chain.len() <= other.chain.len()
34 && self
35 .chain
36 .iter()
37 .zip(&other.chain)
38 .all(|(written, read)| read.is_written_by(written))
39 }
40
41 pub fn render(&self) -> String {
42 let mut out = self.root.render();
43 for elem in &self.chain {
44 match elem {
45 PathElem::Field(name) => {
46 out.push('.');
47 out.push_str(name);
48 }
49 PathElem::Key(binding, _) => {
50 out.push('[');
51 out.push_str(&binding.render());
52 out.push(']');
53 }
54 PathElem::Index(lit) => {
55 use std::fmt::Write;
56 match lit {
57 LiteralValue::Number(n) => {
58 let _ = write!(out, "[{}]", n.0);
59 }
60 LiteralValue::String(s) => {
61 let _ = write!(out, "[{s:?}]");
62 }
63 LiteralValue::Boolean(b) => {
64 let _ = write!(out, "[{b}]");
65 }
66 LiteralValue::BigInt(digits) => {
67 let _ = write!(out, "[{digits}n]");
68 }
69 LiteralValue::Null => {
70 let _ = write!(out, "[null]");
71 }
72 LiteralValue::Undefined => {
73 let _ = write!(out, "[undefined]");
74 }
75 }
76 }
77 }
78 }
79 out
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum BindingId {
90 Local { name: String, decl_scope: ScopeId },
91 Global(MangledName),
92 This,
93}
94
95impl BindingId {
96 pub fn render(&self) -> String {
98 match self {
99 BindingId::Local { name, .. } => name.clone(),
100 BindingId::Global(mangled) => mangled
101 .as_str()
102 .rsplit('#')
103 .next()
104 .unwrap_or(mangled.as_str())
105 .to_string(),
106 BindingId::This => "this".to_string(),
107 }
108 }
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub struct ScopeId(pub u32);
113
114#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub enum PathElem {
120 Field(String),
121 Index(LiteralValue),
122 Key(BindingId, KeyKind),
123}
124
125impl PathElem {
126 fn is_written_by(&self, written: &PathElem) -> bool {
127 matches!(self, PathElem::Key(..)) || self == written
128 }
129}
130
131#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
133pub enum KeyKind {
134 Property,
135 Element,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub enum LiteralValue {
140 Number(LiteralF64),
141 String(String),
142 Boolean(bool),
143 BigInt(String),
145 Null,
148 Undefined,
149}
150
151impl LiteralValue {
152 pub fn of_enum_member(ty: &Type) -> Option<Self> {
154 Some(match ty.enum_member_value()? {
155 crate::types::EnumValue::Number(value) => LiteralValue::Number(value),
156 crate::types::EnumValue::String(value) => LiteralValue::String(value),
157 })
158 }
159}
160
161#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub struct TypeFacts(pub u16);
164
165impl TypeFacts {
166 pub const EMPTY: Self = Self(0);
167 pub const TRUTHY: Self = Self(1 << 0);
168 pub const FALSY: Self = Self(1 << 1);
169 pub const EQ_NULL: Self = Self(1 << 2);
170 pub const NE_NULL: Self = Self(1 << 3);
171 pub const IS_NUMBER: Self = Self(1 << 4);
172 pub const IS_STRING: Self = Self(1 << 5);
173 pub const IS_BOOLEAN: Self = Self(1 << 6);
174 pub const IS_OBJECT: Self = Self(1 << 7);
175 pub const IS_FUNCTION: Self = Self(1 << 8);
176 pub const IS_ARRAY: Self = Self(1 << 9);
177 pub const EQ_UNDEFINED: Self = Self(1 << 10);
178 pub const NE_UNDEFINED: Self = Self(1 << 11);
179 pub const IS_BIGINT: Self = Self(1 << 12);
180 pub const IS_UNDEFINED: Self = Self(1 << 13);
181
182 pub fn contains(self, other: Self) -> bool {
183 (self.0 & other.0) == other.0
184 }
185
186 pub fn is_empty(self) -> bool {
187 self.0 == 0
188 }
189}
190
191impl std::ops::BitOr for TypeFacts {
192 type Output = Self;
193 fn bitor(self, rhs: Self) -> Self {
194 Self(self.0 | rhs.0)
195 }
196}
197
198impl std::ops::BitOrAssign for TypeFacts {
199 fn bitor_assign(&mut self, rhs: Self) {
200 self.0 |= rhs.0;
201 }
202}
203
204impl std::ops::BitAnd for TypeFacts {
205 type Output = Self;
206 fn bitand(self, rhs: Self) -> Self {
207 Self(self.0 & rhs.0)
208 }
209}
210
211impl std::ops::Not for TypeFacts {
212 type Output = Self;
213 fn not(self) -> Self {
214 const ALL: u16 = (1 << 14) - 1;
215 Self((!self.0) & ALL)
216 }
217}
218
219#[derive(Debug)]
221pub struct PendingJoinFrame {
222 pub kind: PendingJoinKind,
223 pub narrow_depth: usize,
230 pub breaks: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
231 pub continues: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
232}
233
234#[derive(Copy, Clone, Debug, PartialEq, Eq)]
235pub enum PendingJoinKind {
236 Loop,
238 Switch,
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
243pub struct NarrowedView {
244 pub narrowed_ty: Type,
245 pub facts: TypeFacts,
246 pub excluded_literals: BTreeSet<LiteralValue>,
249 pub binding: Ident,
252 pub source: ExprId,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct CastInfo {
262 pub from_ty: Type,
263 pub to_ty: Type,
264 pub cast_kind: CastKind,
265}
266
267#[derive(Copy, Clone, Debug, PartialEq, Eq)]
270pub enum CastKind {
271 NonNull,
273 Unbox,
278 RefSubtype,
282}
283
284pub fn cast_info_for(from_ty: Type, narrowed_ty: Type) -> CastInfo {
286 let cast_kind = match &narrowed_ty {
287 Type::Number | Type::NumberLiteral(_) | Type::Boolean | Type::BooleanLiteral(_) => {
288 CastKind::Unbox
289 }
290 Type::Object { .. } | Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
291 CastKind::RefSubtype
292 }
293 Type::String
295 | Type::StringLiteral(_)
296 | Type::Array(_)
297 | Type::Tuple(_)
298 | Type::Uint8Array
299 if matches!(from_ty.peel(), Type::Unknown) =>
300 {
301 CastKind::RefSubtype
302 }
303 _ => CastKind::NonNull,
304 };
305 CastInfo {
306 from_ty,
307 to_ty: narrowed_ty,
308 cast_kind,
309 }
310}
311
312#[derive(Clone, Debug)]
315pub struct PendingPostIfMaterialization {
316 pub path: ReferencePath,
317 pub source: ExprId,
318 pub binding: Ident,
319 pub cast_info: CastInfo,
320 pub span: crate::Span,
321}
322
323#[derive(Clone, Debug)]
326pub enum InvalidationReason {
327 ShapeUnrebuildable { narrowed_ty: Type },
329 Write { span: crate::Span },
333 Reassignment { span: crate::Span },
336 CapturedMutator { closure_span: Option<crate::Span> },
339}
340
341impl InvalidationReason {
342 pub fn invalidates(&self) -> bool {
343 !matches!(self, Self::ShapeUnrebuildable { .. })
344 }
345
346 pub fn span(&self) -> Option<crate::Span> {
347 match self {
348 Self::ShapeUnrebuildable { .. } => None,
349 Self::Write { span } => Some(*span),
350 Self::Reassignment { span } => Some(*span),
351 Self::CapturedMutator { closure_span } => *closure_span,
352 }
353 }
354}
355
356pub type Discriminant = (String, DiscriminantTable);
359
360#[derive(Clone, Debug, Default, PartialEq)]
364pub struct DiscriminantTable {
365 entries: Vec<(LiteralValue, VariantIdx)>,
366}
367
368#[derive(Clone, Copy, PartialEq, Eq)]
370enum UndefinedNames {
371 Several,
373 One,
375}
376
377impl DiscriminantTable {
378 pub fn iter(&self) -> impl Iterator<Item = (&LiteralValue, &VariantIdx)> {
379 self.entries.iter().map(|(value, variant)| (value, variant))
380 }
381
382 pub fn get(&self, value: &LiteralValue) -> Option<VariantIdx> {
384 let mut variants = self.variants_for(value);
385 let variant = variants.next()?;
386 variants.next().is_none().then_some(variant)
387 }
388
389 pub fn variants_for<'a>(
391 &'a self,
392 value: &'a LiteralValue,
393 ) -> impl Iterator<Item = VariantIdx> + 'a {
394 self.entries
395 .iter()
396 .filter(move |(entry, _)| entry == value)
397 .map(|(_, variant)| *variant)
398 }
399
400 pub fn has_other_values(&self, variant: VariantIdx, value: &LiteralValue) -> bool {
403 self.entries
404 .iter()
405 .any(|(entry, named)| *named == variant && entry != value)
406 }
407
408 pub fn is_covered(&self, variant: VariantIdx, covered: &BTreeSet<LiteralValue>) -> bool {
411 self.entries
412 .iter()
413 .filter(|(_, named)| *named == variant)
414 .all(|(value, _)| covered.contains(value))
415 }
416
417 pub fn uncovered_members(
419 &self,
420 members: &[Type],
421 covered: &BTreeSet<LiteralValue>,
422 ) -> Vec<Type> {
423 members
424 .iter()
425 .enumerate()
426 .filter(|(index, _)| {
427 u32::try_from(*index)
428 .map_or(true, |index| !self.is_covered(VariantIdx(index), covered))
429 })
430 .map(|(_, member)| member.clone())
431 .collect()
432 }
433
434 pub fn uncovered(&self, covered: &BTreeSet<LiteralValue>) -> Vec<LiteralValue> {
436 let mut missing: Vec<LiteralValue> = Vec::new();
437 for (value, _) in &self.entries {
438 if !covered.contains(value) && !missing.contains(value) {
439 missing.push(value.clone());
440 }
441 }
442 missing
443 }
444
445 pub fn partition(
450 &self,
451 members: &[Type],
452 literal: &LiteralValue,
453 ) -> Option<(Vec<Type>, Vec<Type>)> {
454 let named: Vec<VariantIdx> = self.variants_for(literal).collect();
455 if named.is_empty() {
456 return None;
457 }
458 let mut matched = Vec::with_capacity(named.len());
459 let mut remaining = Vec::new();
460 for (index, member) in members.iter().enumerate() {
461 let variant = VariantIdx(u32::try_from(index).ok()?);
462 if !named.contains(&variant) {
463 remaining.push(member.clone());
464 continue;
465 }
466 matched.push(member.clone());
467 if self.has_other_values(variant, literal) {
468 remaining.push(member.clone());
469 }
470 }
471 Some((matched, remaining))
472 }
473
474 fn insert(
477 &mut self,
478 value: LiteralValue,
479 variant: VariantIdx,
480 undefined: UndefinedNames,
481 ) -> Option<()> {
482 let shareable = undefined == UndefinedNames::Several && value == LiteralValue::Undefined;
483 if !shareable && self.entries.iter().any(|(entry, _)| *entry == value) {
484 return None;
485 }
486 self.entries.push((value, variant));
487 Some(())
488 }
489}
490
491#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
493pub struct VariantIdx(pub u32);
494
495pub fn union_discriminant(members: &[Type]) -> Option<Discriminant> {
509 let shapes = inline_member_shapes(members)?;
515 discriminant_from_shapes(&shapes)
516}
517
518fn inline_member_shapes(
521 members: &[Type],
522) -> Option<Vec<BTreeMap<String, crate::types::ObjectField>>> {
523 members
524 .iter()
525 .map(|m| match m.peel() {
526 Type::Object { fields, .. } => Some(fields.clone()),
527 _ => None,
528 })
529 .collect()
530}
531
532pub fn inline_union_discriminant_for_key(members: &[Type], key: &str) -> Option<DiscriminantTable> {
536 let shapes = inline_member_shapes(members)?;
537 discriminant_table_for_key(&shapes, key)
538}
539
540pub fn discriminant_from_shapes(
545 shapes: &[BTreeMap<String, crate::types::ObjectField>],
546) -> Option<Discriminant> {
547 let first = shapes.first()?;
548 let may_be_omitted = |key: &String| {
549 shapes
550 .iter()
551 .any(|shape| shape.get(key).is_some_and(|f| f.optional))
552 };
553 let (required, omittable): (Vec<&String>, Vec<&String>) =
554 first.keys().partition(|key| !may_be_omitted(key));
555 required
556 .into_iter()
557 .chain(omittable)
558 .find_map(|key| Some((key.clone(), discriminant_table_for_key(shapes, key)?)))
559}
560
561pub fn discriminant_table_for_key(
564 shapes: &[BTreeMap<String, crate::types::ObjectField>],
565 key: &str,
566) -> Option<DiscriminantTable> {
567 if shapes.len() < 2 {
568 return None;
569 }
570 let mut table = DiscriminantTable::default();
571 for (idx, shape) in shapes.iter().enumerate() {
572 let field = shape.get(key)?;
573 let lit = unit_value(&field.ty)?;
574 let absent =
577 (field.optional && lit != LiteralValue::Undefined).then_some(LiteralValue::Undefined);
578 let variant = VariantIdx(u32::try_from(idx).ok()?);
579 for value in std::iter::once(lit).chain(absent) {
580 table.insert(value, variant, UndefinedNames::Several)?;
581 }
582 }
583 Some(table)
584}
585
586pub fn tuple_union_discriminant(members: &[Type]) -> Option<(usize, DiscriminantTable)> {
590 if members.len() < 2 {
591 return None;
592 }
593 let tuples: Vec<&crate::types::TupleType> = members
594 .iter()
595 .map(|m| match m.peel() {
596 Type::Tuple(elements) => Some(elements),
597 _ => None,
598 })
599 .collect::<Option<_>>()?;
600 let min_arity = tuples.iter().map(|t| t.required_len()).min().unwrap_or(0);
601 (0..min_arity).find_map(|position| {
602 let mut table = DiscriminantTable::default();
603 for (idx, tuple) in tuples.iter().enumerate() {
604 let lit = tuple.get(position).and_then(unit_value)?;
605 table.insert(
608 lit,
609 VariantIdx(u32::try_from(idx).ok()?),
610 UndefinedNames::One,
611 )?;
612 }
613 Some((position, table))
614 })
615}
616
617pub fn switch_value_literal(value: &crate::TypedSwitchValue) -> Option<LiteralValue> {
620 Some(match value {
621 crate::TypedSwitchValue::String { value, .. } => LiteralValue::String(value.clone()),
622 crate::TypedSwitchValue::Number { value, .. } => {
623 LiteralValue::Number(crate::types::LiteralF64(*value))
624 }
625 crate::TypedSwitchValue::Boolean { value, .. } => LiteralValue::Boolean(*value),
626 crate::TypedSwitchValue::Null { .. } => LiteralValue::Null,
627 crate::TypedSwitchValue::Undefined { .. } => LiteralValue::Undefined,
628 crate::TypedSwitchValue::Expr { literal, .. } => return literal.clone(),
629 crate::TypedSwitchValue::Enum { value, .. } => match value {
630 crate::EnumVariantPayload::Number(n) => {
631 LiteralValue::Number(crate::types::LiteralF64(*n))
632 }
633 crate::EnumVariantPayload::String(s) => LiteralValue::String(s.clone()),
634 },
635 })
636}
637
638pub fn literal_type(literal: &LiteralValue) -> Type {
640 match literal {
641 LiteralValue::String(s) => Type::StringLiteral(s.clone()),
642 LiteralValue::Number(n) => Type::NumberLiteral(*n),
643 LiteralValue::Boolean(b) => Type::BooleanLiteral(*b),
644 LiteralValue::BigInt(digits) => Type::BigIntLiteral(digits.clone()),
645 LiteralValue::Null => Type::Null,
646 LiteralValue::Undefined => Type::Undefined,
647 }
648}
649
650pub fn has_unit_member(ty: &Type) -> bool {
654 match ty.peel() {
655 Type::Union(members) => members.iter().any(has_unit_member),
656 Type::Null | Type::Undefined | Type::NumberEnum { .. } | Type::StringEnum { .. } => true,
657 other => unit_literal_value(other).is_some(),
658 }
659}
660
661pub fn is_covered_by_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> bool {
664 match ty.peel() {
665 Type::Union(members) => members
666 .iter()
667 .all(|member| is_covered_by_literals(member, covered)),
668 Type::Boolean => [true, false]
670 .into_iter()
671 .all(|value| covered.contains(&LiteralValue::Boolean(value))),
672 other => unit_value(other).is_some_and(|value| covered.contains(&value)),
673 }
674}
675
676pub fn has_type_parameter_member(ty: &Type) -> bool {
678 match ty.peel() {
679 Type::Union(members) => members.iter().any(has_type_parameter_member),
680 other => matches!(other, Type::TypeVar(_) | Type::GenericParam { .. }),
681 }
682}
683
684pub(super) fn unit_literal_value(ty: &Type) -> Option<LiteralValue> {
685 match ty.peel() {
686 Type::StringLiteral(s) => Some(LiteralValue::String(s.clone())),
687 Type::NumberLiteral(n) => Some(LiteralValue::Number(*n)),
688 Type::BooleanLiteral(b) => Some(LiteralValue::Boolean(*b)),
689 Type::BigIntLiteral(digits) => Some(LiteralValue::BigInt(digits.clone())),
690 other => LiteralValue::of_enum_member(other),
691 }
692}
693
694pub(super) fn unit_value(ty: &Type) -> Option<LiteralValue> {
697 match ty.peel() {
698 Type::Null => Some(LiteralValue::Null),
699 Type::Undefined | Type::Void => Some(LiteralValue::Undefined),
700 other => unit_literal_value(other),
701 }
702}
703
704pub fn strip_null(ty: &Type) -> Type {
707 if let Type::Refined {
708 original,
709 ty: shape,
710 } = ty.without_aliases()
711 {
712 return preserve_refinement(original, strip_null(shape));
713 }
714 match ty.peel() {
715 Type::Null => Type::Error,
716 Type::Union(members) => without_nullish_refinements(Type::union(
717 members
718 .iter()
719 .filter(|m| !matches!(m.peel(), Type::Null))
720 .map(|m| {
721 if is_type_parameter_or_its_narrowing(m) {
722 strip_null(m)
723 } else {
724 m.clone()
725 }
726 })
727 .collect(),
728 )),
729 Type::Unknown => Type::union(vec![Type::empty_object(), Type::Undefined]),
731 Type::GenericParam { .. } => preserve_refinement(ty, strip_null(&Type::Unknown)),
734 _ => ty.clone(),
736 }
737}
738
739pub fn strip_undefined(ty: &Type) -> Type {
743 if let Type::Refined {
744 original,
745 ty: shape,
746 } = ty.without_aliases()
747 {
748 return preserve_refinement(original, strip_undefined(shape));
749 }
750 match ty.peel() {
751 Type::Undefined => Type::Error,
752 Type::Union(members) => without_nullish_refinements(Type::union(
753 members
754 .iter()
755 .filter(|member| !matches!(member.peel(), Type::Undefined))
756 .map(|member| {
757 if is_type_parameter_or_its_narrowing(member) {
758 strip_undefined(member)
759 } else {
760 member.clone()
761 }
762 })
763 .collect(),
764 )),
765 Type::Unknown => Type::union(vec![Type::empty_object(), Type::Null]),
766 Type::GenericParam { .. } => preserve_refinement(ty, strip_undefined(&Type::Unknown)),
767 _ => ty.clone(),
768 }
769}
770
771fn is_type_parameter_or_its_narrowing(member: &Type) -> bool {
774 matches!(member.peel(), Type::GenericParam { .. }) || member.refines_type_parameter()
775}
776
777pub fn strip_nullish(ty: &Type) -> Type {
779 strip_undefined(&strip_null(ty))
780}
781
782pub fn narrow_field_presence(
786 receiver_ty: &Type,
787 field: &str,
788 lookup: &dyn Fn(&Type, &str) -> Option<crate::ObjectField>,
789) -> (Type, Type) {
790 if let Type::Refined {
791 original,
792 ty: shape,
793 } = receiver_ty.without_aliases()
794 {
795 let (present, absent) = narrow_field_presence(shape, field, lookup);
796 return (
797 preserve_refinement(original, present),
798 preserve_refinement(original, absent),
799 );
800 }
801 match receiver_ty.peel() {
802 Type::Unknown => {
803 let fields = BTreeMap::from([(
804 field.to_string(),
805 crate::ObjectField::required(Type::Unknown),
806 )]);
807 (
808 Type::Object {
809 index: None,
810 fields,
811 },
812 Type::Unknown,
813 )
814 }
815 Type::Object { fields, .. } => {
816 let mut present = fields.clone();
817 present
818 .entry(field.to_string())
819 .or_insert_with(|| crate::ObjectField::required(Type::Unknown));
820 let absent = match fields.get(field) {
821 Some(f) if !f.optional => Type::Error,
822 _ => receiver_ty.clone(),
823 };
824 (
825 Type::Object {
826 index: None,
827 fields: present,
828 },
829 absent,
830 )
831 }
832 Type::Union(members) => {
833 let has = members
834 .iter()
835 .filter(|m| lookup(m, field).is_some())
836 .cloned()
837 .collect();
838 let lacks = members
839 .iter()
840 .filter(|m| lookup(m, field).is_none_or(|f| f.optional))
841 .cloned()
842 .collect();
843 (Type::union(has), Type::union(lacks))
844 }
845 Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
846 let absent = if lookup(receiver_ty, field).is_some_and(|f| !f.optional) {
847 Type::Error
848 } else {
849 receiver_ty.clone()
850 };
851 (receiver_ty.clone(), absent)
852 }
853 _ => (Type::Error, Type::Error),
854 }
855}
856
857#[derive(Clone, Debug, Default)]
864pub struct NarrowEnv {
865 views: BTreeMap<ReferencePath, NarrowedView>,
866 pub dropped: BTreeMap<ReferencePath, Type>,
869}
870
871impl NarrowEnv {
872 pub fn new() -> Self {
873 Self::default()
874 }
875
876 pub fn insert(&mut self, path: ReferencePath, view: NarrowedView) -> Option<NarrowedView> {
877 self.dropped.remove(&path);
878 self.views.insert(path, view)
879 }
880
881 pub fn extend_env(&mut self, other: Self) {
882 self.dropped.extend(other.dropped);
883 self.extend(other.views);
884 }
885
886 pub fn clear(&mut self) {
887 self.views.clear();
888 self.dropped.clear();
889 }
890}
891
892impl std::ops::Deref for NarrowEnv {
893 type Target = BTreeMap<ReferencePath, NarrowedView>;
894 fn deref(&self) -> &Self::Target {
895 &self.views
896 }
897}
898
899impl std::ops::DerefMut for NarrowEnv {
900 fn deref_mut(&mut self) -> &mut Self::Target {
901 &mut self.views
902 }
903}
904
905impl IntoIterator for NarrowEnv {
906 type Item = (ReferencePath, NarrowedView);
907 type IntoIter = std::collections::btree_map::IntoIter<ReferencePath, NarrowedView>;
908 fn into_iter(self) -> Self::IntoIter {
909 self.views.into_iter()
910 }
911}
912
913impl<'a> IntoIterator for &'a NarrowEnv {
914 type Item = (&'a ReferencePath, &'a NarrowedView);
915 type IntoIter = std::collections::btree_map::Iter<'a, ReferencePath, NarrowedView>;
916 fn into_iter(self) -> Self::IntoIter {
917 self.views.iter()
918 }
919}
920
921impl Extend<(ReferencePath, NarrowedView)> for NarrowEnv {
922 fn extend<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(&mut self, iter: I) {
923 for (path, view) in iter {
924 self.insert(path, view);
925 }
926 }
927}
928
929impl FromIterator<(ReferencePath, NarrowedView)> for NarrowEnv {
930 fn from_iter<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(iter: I) -> Self {
931 let mut env = Self::new();
932 env.extend(iter);
933 env
934 }
935}
936
937pub fn wrap_order(a: &ReferencePath, b: &ReferencePath) -> std::cmp::Ordering {
942 b.chain.len().cmp(&a.chain.len()).then_with(|| a.cmp(b))
943}
944
945pub fn envs_per_path_narrowed_ty_equal(a: &NarrowEnv, b: &NarrowEnv) -> bool {
948 if a.len() != b.len() {
949 return false;
950 }
951 for (path, a_view) in a {
952 match b.get(path) {
953 Some(b_view) if a_view.narrowed_ty == b_view.narrowed_ty => continue,
954 _ => return false,
955 }
956 }
957 true
958}
959
960#[derive(Clone, Copy, Debug, PartialEq, Eq)]
963pub enum TruthinessClass {
964 AlwaysTruthy,
965 AlwaysFalsy,
966 NumberLike,
968 StringLike,
970 BooleanLike,
972 BigIntLike,
974 Dynamic,
976}
977
978pub fn truthiness_class(member: &Type) -> TruthinessClass {
979 use TruthinessClass::*;
980 match member.peel() {
981 Type::Null | Type::Undefined => AlwaysFalsy,
982 Type::Boolean => BooleanLike,
983 Type::BooleanLiteral(true) => AlwaysTruthy,
984 Type::BooleanLiteral(false) => AlwaysFalsy,
985 Type::Number => NumberLike,
986 Type::NumberLiteral(n) => {
987 if n.0 == 0.0 || n.0.is_nan() {
988 AlwaysFalsy
989 } else {
990 AlwaysTruthy
991 }
992 }
993 Type::String => StringLike,
994 Type::StringLiteral(s) => {
995 if s.is_empty() {
996 AlwaysFalsy
997 } else {
998 AlwaysTruthy
999 }
1000 }
1001 Type::BigInt => BigIntLike,
1002 Type::BigIntLiteral(digits) => {
1003 if digits == "0" {
1004 AlwaysFalsy
1005 } else {
1006 AlwaysTruthy
1007 }
1008 }
1009 Type::NumberEnum {
1010 member: Some(member),
1011 ..
1012 } => truthiness_class(&Type::NumberLiteral(member.value)),
1013 Type::StringEnum {
1014 member: Some(member),
1015 ..
1016 } => truthiness_class(&Type::StringLiteral(member.value.clone())),
1017 Type::NumberEnum { .. } => NumberLike,
1018 Type::StringEnum { .. } => StringLike,
1019 empty if empty.is_empty_object() => Dynamic,
1022 Type::Object { .. }
1023 | Type::Array(_)
1024 | Type::Tuple(_)
1025 | Type::Function { .. }
1026 | Type::Uint8Array
1027 | Type::InterfaceRef { .. }
1028 | Type::ClassRef { .. } => AlwaysTruthy,
1029 _ => Dynamic,
1030 }
1031}
1032
1033pub fn has_known_truthiness(ty: &Type) -> bool {
1036 union_members(ty).into_iter().all(|member| {
1037 matches!(
1038 truthiness_class(member),
1039 TruthinessClass::AlwaysTruthy | TruthinessClass::AlwaysFalsy
1040 )
1041 })
1042}
1043
1044pub fn negation_type(ty: &Type) -> Type {
1048 let classes: Vec<TruthinessClass> = union_members(ty)
1049 .into_iter()
1050 .map(truthiness_class)
1051 .collect();
1052 if classes
1053 .iter()
1054 .all(|class| *class == TruthinessClass::AlwaysTruthy)
1055 {
1056 return Type::BooleanLiteral(false);
1057 }
1058 if classes
1059 .iter()
1060 .all(|class| *class == TruthinessClass::AlwaysFalsy)
1061 {
1062 return Type::BooleanLiteral(true);
1063 }
1064 Type::Boolean
1065}
1066
1067pub(super) fn union_members(ty: &Type) -> Vec<&Type> {
1068 match ty.peel() {
1069 Type::Union(members) => members.iter().collect(),
1070 _ => vec![ty],
1072 }
1073}
1074
1075pub fn truthy_part(ty: &Type) -> Type {
1079 if let Type::Refined {
1080 original,
1081 ty: shape,
1082 } = ty.without_aliases()
1083 {
1084 return preserve_refinement(original, truthy_part(shape));
1085 }
1086 let kept: Vec<Type> = union_members(ty)
1088 .into_iter()
1089 .filter(|m| truthiness_class(m) != TruthinessClass::AlwaysFalsy)
1090 .map(|m| match m.without_aliases() {
1091 Type::Boolean => Type::BooleanLiteral(true),
1092 Type::GenericParam { .. } => intersect_with(m, TypeFacts::TRUTHY),
1094 _ => m.clone(),
1095 })
1096 .collect();
1097 Type::union(kept)
1098}
1099
1100pub fn is_never_falsy(ty: &Type) -> bool {
1102 union_members(ty)
1103 .into_iter()
1104 .all(|m| truthiness_class(m) == TruthinessClass::AlwaysTruthy)
1105}
1106
1107pub fn falsy_part(ty: &Type) -> Type {
1113 if let Type::Refined {
1114 original,
1115 ty: shape,
1116 } = ty.without_aliases()
1117 {
1118 return preserve_refinement(original, falsy_part(shape));
1119 }
1120 let kept: Vec<Type> = union_members(ty)
1121 .into_iter()
1122 .filter_map(|m| match truthiness_class(m) {
1123 TruthinessClass::AlwaysTruthy => None,
1124 TruthinessClass::StringLike => Some(match m.peel() {
1125 Type::String => Type::StringLiteral(String::new()),
1126 _ => m.clone(),
1127 }),
1128 _ => Some(match m.without_aliases() {
1129 Type::Boolean => Type::BooleanLiteral(false),
1130 _ => m.clone(),
1131 }),
1132 })
1133 .collect();
1134 Type::union(kept)
1135}
1136
1137pub fn falsy_values_are_only_null(ty: &Type) -> bool {
1140 union_members(ty).into_iter().all(|m| {
1141 matches!(m.peel(), Type::Null) || truthiness_class(m) == TruthinessClass::AlwaysTruthy
1142 })
1143}
1144
1145pub fn type_matches_facts(ty: &Type, facts: TypeFacts) -> bool {
1146 let ty = ty.peel();
1150 if facts.contains(TypeFacts::TRUTHY) {
1154 return truthiness_class(ty) != TruthinessClass::AlwaysFalsy;
1155 }
1156 if facts.contains(TypeFacts::FALSY) {
1157 return truthiness_class(ty) != TruthinessClass::AlwaysTruthy;
1158 }
1159 if facts.contains(TypeFacts::NE_NULL) || facts.contains(TypeFacts::NE_UNDEFINED) {
1160 return !(facts.contains(TypeFacts::NE_NULL) && matches!(ty, Type::Null)
1161 || facts.contains(TypeFacts::NE_UNDEFINED) && matches!(ty, Type::Undefined));
1162 }
1163 if facts.contains(TypeFacts::IS_UNDEFINED) {
1166 return matches!(ty, Type::Undefined);
1167 }
1168 if facts.contains(TypeFacts::EQ_UNDEFINED) {
1169 return matches!(ty, Type::Undefined);
1170 }
1171 if facts.contains(TypeFacts::EQ_NULL) {
1172 return matches!(ty, Type::Null);
1173 }
1174 if facts.contains(TypeFacts::IS_BIGINT) {
1175 return matches!(ty, Type::BigInt);
1176 }
1177 if facts.contains(TypeFacts::IS_NUMBER) {
1178 return matches!(
1180 ty,
1181 Type::Number | Type::NumberLiteral(_) | Type::NumberEnum { .. }
1182 );
1183 }
1184 if facts.contains(TypeFacts::IS_STRING) {
1185 return matches!(
1186 ty,
1187 Type::String | Type::StringLiteral(_) | Type::StringEnum { .. }
1188 );
1189 }
1190 if facts.contains(TypeFacts::IS_BOOLEAN) {
1191 return matches!(ty, Type::Boolean | Type::BooleanLiteral(_));
1192 }
1193 if facts.contains(TypeFacts::IS_ARRAY) {
1194 return matches!(ty, Type::Array(_));
1195 }
1196 if facts.contains(TypeFacts::IS_FUNCTION) {
1197 return matches!(ty, Type::Function { .. });
1198 }
1199 if facts.contains(TypeFacts::IS_OBJECT) {
1200 return is_typeof_object(ty);
1201 }
1202 false
1203}
1204
1205fn is_typeof_object(ty: &Type) -> bool {
1213 match ty {
1214 Type::Union(members) => members.iter().all(is_typeof_object),
1217
1218 Type::Object { .. }
1219 | Type::Array(_)
1220 | Type::Tuple(_)
1221 | Type::InterfaceRef { .. }
1222 | Type::ClassRef { .. }
1223 | Type::Uint8Array
1224 | Type::Null => true,
1226
1227 Type::Undefined
1228 | Type::Number
1229 | Type::NumberLiteral(_)
1230 | Type::NumberEnum { .. }
1231 | Type::String
1232 | Type::StringLiteral(_)
1233 | Type::StringEnum { .. }
1234 | Type::Boolean
1235 | Type::BooleanLiteral(_)
1236 | Type::BigInt
1237 | Type::BigIntLiteral(_)
1238 | Type::Function { .. } => false,
1239
1240 Type::Void
1247 | Type::Never
1248 | Type::Error
1249 | Type::Unknown
1250 | Type::TypeVar(_)
1251 | Type::GenericParam { .. }
1252 | Type::AliasRef { .. } => false,
1253
1254 Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => false,
1256 }
1257}
1258
1259pub fn intersect_with(ty: &Type, facts: TypeFacts) -> Type {
1262 if let Type::Refined {
1263 original,
1264 ty: shape,
1265 } = ty.without_aliases()
1266 {
1267 return preserve_refinement(original, intersect_with(shape, facts));
1268 }
1269 let peeled = ty.peel();
1270 if let Some(asserted) = asserted_type_for_facts(facts) {
1271 match peeled {
1272 Type::Unknown => return asserted,
1273 empty
1277 if empty.is_empty_object() && !matches!(asserted, Type::Null | Type::Undefined) =>
1278 {
1279 return asserted;
1280 }
1281 Type::GenericParam { .. } => {
1282 return Type::Refined {
1283 original: Box::new(ty.clone()),
1284 ty: Box::new(asserted),
1285 };
1286 }
1287 _ => {}
1288 }
1289 }
1290 if matches!(peeled, Type::Unknown) && tests_only_presence(facts) {
1291 return recombine_unknown(intersect_with(&unknown_as_union(), facts));
1292 }
1293 if matches!(peeled, Type::GenericParam { .. }) && tests_only_presence(facts) {
1294 return preserve_refinement(ty, intersect_with(&Type::Unknown, facts));
1295 }
1296 match peeled {
1297 Type::Union(members) => without_nullish_refinements(Type::union(
1298 members
1299 .iter()
1300 .map(|m| intersect_with(m, facts))
1301 .filter(|ty| !matches!(ty, Type::Error))
1302 .collect(),
1303 )),
1304 Type::Void if facts.contains(TypeFacts::IS_UNDEFINED) => Type::Undefined,
1307 Type::Boolean if facts == TypeFacts::TRUTHY => Type::BooleanLiteral(true),
1309 Type::Boolean if facts == TypeFacts::FALSY => Type::BooleanLiteral(false),
1310 _ if type_matches_facts(peeled, facts) => ty.clone(),
1311 _ if matches!(peeled, Type::Unknown) => Type::Unknown,
1312 _ => Type::Error,
1313 }
1314}
1315
1316pub fn subtract(ty: &Type, facts: TypeFacts) -> Type {
1317 if let Type::Refined {
1318 original,
1319 ty: shape,
1320 } = ty.without_aliases()
1321 {
1322 return preserve_refinement(original, subtract(shape, facts));
1323 }
1324 let peeled = ty.peel();
1325 if matches!(peeled, Type::Unknown) {
1328 if tests_only_nullishness(facts) {
1329 return recombine_unknown(subtract(&unknown_as_union(), facts));
1330 }
1331 return Type::Unknown;
1332 }
1333 if matches!(peeled, Type::GenericParam { .. }) {
1334 return preserve_refinement(ty, subtract(&Type::Unknown, facts));
1335 }
1336 let ruled_out =
1340 |member: &Type| !matches!(member.peel(), Type::Void) && type_matches_facts(member, facts);
1341 match peeled {
1342 Type::Union(members) => {
1343 let kept: Vec<Type> = members.iter().filter(|m| !ruled_out(m)).cloned().collect();
1344 Type::union(kept)
1345 }
1346 _ if !ruled_out(peeled) => ty.clone(),
1347 _ => Type::Error,
1348 }
1349}
1350
1351pub(super) fn with_source_refinement(source: &Type, shape: Type) -> Type {
1352 match source.without_aliases() {
1353 Type::Refined { original, .. } => preserve_refinement(original, shape),
1354 _ => shape,
1355 }
1356}
1357
1358pub(super) fn preserve_refinement(original: &Type, shape: Type) -> Type {
1359 if matches!(shape, Type::Error | Type::Never) {
1360 return shape;
1361 }
1362 if matches!(shape, Type::Unknown) {
1364 return original.clone();
1365 }
1366 Type::Refined {
1367 original: Box::new(original.clone()),
1368 ty: Box::new(shape),
1369 }
1370}
1371
1372pub fn subtract_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> Type {
1380 if let Type::Refined {
1381 original,
1382 ty: shape,
1383 } = ty.without_aliases()
1384 {
1385 return preserve_refinement(original, subtract_literals(shape, covered));
1386 }
1387 match ty.peel() {
1388 Type::Union(members) => {
1389 let kept: Vec<Type> = members
1390 .iter()
1391 .map(|m| subtract_literals(m, covered))
1392 .collect();
1393 Type::union(kept)
1394 }
1395 Type::Boolean => {
1397 let remaining: Vec<Type> = [false, true]
1398 .into_iter()
1399 .filter(|b| !covered.contains(&LiteralValue::Boolean(*b)))
1400 .map(Type::BooleanLiteral)
1401 .collect();
1402 Type::union(remaining)
1403 }
1404 single @ (Type::StringLiteral(_)
1405 | Type::NumberLiteral(_)
1406 | Type::BooleanLiteral(_)
1407 | Type::BigIntLiteral(_)
1408 | Type::Null
1409 | Type::Undefined) => match unit_value(single) {
1410 Some(lit) if covered.contains(&lit) => Type::Never,
1411 _ => ty.clone(),
1412 },
1413 _ => ty.clone(),
1414 }
1415}
1416
1417fn tests_only_presence(facts: TypeFacts) -> bool {
1421 let presence = TypeFacts::TRUTHY
1422 | TypeFacts::FALSY
1423 | TypeFacts::EQ_NULL
1424 | TypeFacts::NE_NULL
1425 | TypeFacts::EQ_UNDEFINED
1426 | TypeFacts::NE_UNDEFINED
1427 | TypeFacts::IS_UNDEFINED;
1428 facts != TypeFacts::EMPTY && presence.contains(facts)
1429}
1430
1431fn tests_only_nullishness(facts: TypeFacts) -> bool {
1433 tests_only_presence(facts)
1434 && !facts.contains(TypeFacts::TRUTHY)
1435 && !facts.contains(TypeFacts::FALSY)
1436}
1437
1438fn without_nullish_refinements(union: Type) -> Type {
1442 let Type::Union(members) = &union else {
1443 return union;
1444 };
1445 let holds = |value: &Type| members.iter().any(|member| member.peel() == value);
1446 let (holds_null, holds_undefined) = (holds(&Type::Null), holds(&Type::Undefined));
1447 if !holds_null && !holds_undefined {
1448 return union;
1449 }
1450 Type::union(
1451 members
1452 .iter()
1453 .filter_map(|member| match member {
1454 Type::Refined { original, ty } => {
1455 let mut shape = (**ty).clone();
1456 if holds_null {
1457 shape = strip_null(&shape);
1458 }
1459 if holds_undefined {
1460 shape = strip_undefined(&shape);
1461 }
1462 (!is_ruled_out(&shape)).then(|| preserve_refinement(original, shape))
1464 }
1465 other => Some(other.clone()),
1466 })
1467 .collect(),
1468 )
1469}
1470
1471fn unknown_as_union() -> Type {
1473 Type::Union(vec![Type::empty_object(), Type::Null, Type::Undefined])
1474}
1475
1476fn recombine_unknown(ty: Type) -> Type {
1479 if ty.holds_every_value() {
1480 return Type::Unknown;
1481 }
1482 ty
1483}
1484
1485fn asserted_type_for_facts(facts: TypeFacts) -> Option<Type> {
1489 if facts.contains(TypeFacts::IS_BIGINT) {
1490 return Some(Type::BigInt);
1491 }
1492 if facts.contains(TypeFacts::IS_NUMBER) {
1493 return Some(Type::Number);
1494 }
1495 if facts.contains(TypeFacts::IS_STRING) {
1496 return Some(Type::String);
1497 }
1498 if facts.contains(TypeFacts::IS_BOOLEAN) {
1499 return Some(Type::Boolean);
1500 }
1501 if facts.contains(TypeFacts::IS_ARRAY) {
1502 return Some(Type::Array(Box::new(Type::Unknown)));
1503 }
1504 if facts.contains(TypeFacts::EQ_NULL) {
1505 return Some(Type::Null);
1506 }
1507 if facts.contains(TypeFacts::EQ_UNDEFINED) || facts.contains(TypeFacts::IS_UNDEFINED) {
1508 return Some(Type::Undefined);
1509 }
1510 None
1513}
1514
1515pub fn union_envs(
1530 a_narrowings: NarrowEnv,
1531 a_assigned: BTreeSet<ReferencePath>,
1532 b_narrowings: NarrowEnv,
1533 b_assigned: BTreeSet<ReferencePath>,
1534) -> (NarrowEnv, BTreeSet<ReferencePath>) {
1535 let mut joined_narrowings = NarrowEnv::new();
1536 for (path, a_view) in &a_narrowings {
1537 if let Some(b_view) = b_narrowings.get(path) {
1538 let Some(joined_ty) = join_narrowed_types(&a_view.narrowed_ty, &b_view.narrowed_ty)
1539 else {
1540 continue;
1541 };
1542 let a_view = if is_ruled_out(&a_view.narrowed_ty) {
1544 b_view
1545 } else {
1546 a_view
1547 };
1548 joined_narrowings.insert(
1549 path.clone(),
1550 NarrowedView {
1551 narrowed_ty: joined_ty,
1552 facts: TypeFacts::EMPTY,
1557 excluded_literals: BTreeSet::new(),
1558 binding: a_view.binding.clone(),
1564 source: a_view.source,
1565 },
1566 );
1567 }
1568 }
1569
1570 for (path, ty) in a_narrowings.dropped.iter().chain(&b_narrowings.dropped) {
1571 let left_ty = a_narrowings
1572 .dropped
1573 .get(path)
1574 .or_else(|| a_narrowings.get(path).map(|view| &view.narrowed_ty));
1575 let right_ty = b_narrowings
1576 .dropped
1577 .get(path)
1578 .or_else(|| b_narrowings.get(path).map(|view| &view.narrowed_ty));
1579 if left_ty == Some(ty) && right_ty == Some(ty) {
1580 joined_narrowings.dropped.insert(path.clone(), ty.clone());
1581 }
1582 }
1583
1584 let mut joined_assigned = a_assigned;
1585 joined_assigned.extend(b_assigned);
1586
1587 joined_narrowings.dropped.retain(|key, _| {
1588 !joined_assigned
1589 .iter()
1590 .any(|assigned| assigned.is_prefix_of(key))
1591 });
1592 joined_narrowings.retain(|key, _| {
1593 !joined_assigned
1594 .iter()
1595 .any(|assigned| assigned != key && assigned.is_prefix_of(key))
1596 });
1597
1598 (joined_narrowings, joined_assigned)
1599}
1600
1601pub(super) fn tuple_position(index: f64) -> Option<usize> {
1603 (index >= 0.0 && index.fract() == 0.0 && index <= u32::MAX as f64).then_some(index as usize)
1604}
1605
1606pub const RULED_OUT: Type = Type::Error;
1611
1612pub fn is_ruled_out(ty: &Type) -> bool {
1614 matches!(ty, Type::Error)
1615}
1616
1617pub(super) fn join_narrowed_types(left: &Type, right: &Type) -> Option<Type> {
1621 let joined = join_flow_types(left, right);
1622 (!joined.holds_every_value()).then_some(joined)
1623}
1624
1625fn join_flow_types(left: &Type, right: &Type) -> Type {
1628 if is_ruled_out(left) {
1630 return right.clone();
1631 }
1632 if is_ruled_out(right) {
1633 return left.clone();
1634 }
1635 let joined = Type::union(vec![left.clone(), right.clone()]);
1636 let Type::Union(members) = joined else {
1637 return joined;
1638 };
1639 let mut members = crate::types::without_members_of_empty_object(members);
1640 let has_number = members
1641 .iter()
1642 .any(|ty| matches!(ty.without_aliases(), Type::Number));
1643 let has_string = members
1644 .iter()
1645 .any(|ty| matches!(ty.without_aliases(), Type::String));
1646 members.retain(|ty| match ty.without_aliases() {
1647 Type::NumberLiteral(_) => !has_number,
1648 Type::StringLiteral(_) => !has_string,
1649 _ => true,
1650 });
1651 Type::union(members)
1652}
1653
1654pub fn has_erased_member(ty: &Type) -> bool {
1662 match ty.peel() {
1663 Type::TypeVar(_) | Type::GenericParam { .. } => true,
1664 Type::Union(members) => members.iter().any(has_erased_member),
1665 _ => false,
1666 }
1667}
1668
1669pub fn condition_compatible(ty: &Type) -> bool {
1677 !matches!(ty.peel(), Type::Unknown | Type::Void)
1678}
1679
1680pub fn facts_for_target_type(target: &Type) -> TypeFacts {
1681 match target.peel() {
1682 Type::Number | Type::NumberLiteral(_) => TypeFacts::IS_NUMBER,
1683 Type::String | Type::StringLiteral(_) => TypeFacts::IS_STRING,
1684 Type::Boolean | Type::BooleanLiteral(_) => TypeFacts::IS_BOOLEAN,
1685 Type::Null => TypeFacts::EQ_NULL,
1686 Type::Undefined => TypeFacts::EQ_UNDEFINED,
1687 Type::BigInt => TypeFacts::IS_BIGINT,
1688 Type::Array(_) => TypeFacts::IS_ARRAY,
1689 _ => TypeFacts::EMPTY,
1690 }
1691}
1692
1693#[cfg(test)]
1694mod analyzer_tests {
1695 use super::*;
1696
1697 #[test]
1698 fn typeof_undefined_splits_void_without_assuming_its_truthiness() {
1699 let ty = Type::union(vec![Type::Boolean, Type::Void]);
1700 assert_eq!(
1701 intersect_with(&ty, TypeFacts::IS_UNDEFINED),
1702 Type::Undefined
1703 );
1704 assert_eq!(subtract(&ty, TypeFacts::IS_UNDEFINED), ty);
1705 assert_eq!(truthiness_class(&Type::Void), TruthinessClass::Dynamic);
1706 assert!(!type_matches_facts(&Type::Void, TypeFacts::EQ_UNDEFINED));
1707 }
1708
1709 #[test]
1710 fn undefined_facts_are_distinct_from_null() {
1711 let ty = Type::union(vec![Type::String, Type::Null, Type::Undefined]);
1712 assert_eq!(
1713 strip_null(&ty),
1714 Type::union(vec![Type::String, Type::Undefined])
1715 );
1716 assert_eq!(
1717 strip_undefined(&ty),
1718 Type::union(vec![Type::String, Type::Null])
1719 );
1720 assert_eq!(strip_nullish(&ty), Type::String);
1721 assert_eq!(
1722 intersect_with(&ty, TypeFacts::EQ_UNDEFINED),
1723 Type::Undefined
1724 );
1725 assert_eq!(
1726 intersect_with(&Type::Unknown, TypeFacts::EQ_UNDEFINED),
1727 Type::Undefined
1728 );
1729 assert_eq!(
1730 intersect_with(&ty, TypeFacts::NE_NULL | TypeFacts::NE_UNDEFINED),
1731 Type::String
1732 );
1733 assert_eq!(truthy_part(&ty), Type::String);
1734 assert_eq!(
1735 truthiness_class(&Type::Undefined),
1736 TruthinessClass::AlwaysFalsy
1737 );
1738 assert!(!type_matches_facts(&Type::Undefined, TypeFacts::IS_OBJECT));
1739 }
1740
1741 #[test]
1742 fn presence_keeps_optional_read_undefined() {
1743 let ty = Type::Object {
1744 fields: BTreeMap::from([("value".into(), crate::ObjectField::optional(Type::Number))]),
1745 index: None,
1746 };
1747 let (present, absent) = narrow_field_presence(&ty, "value", &|_, _| None);
1748 assert_eq!(present, ty);
1749 assert_eq!(absent, ty);
1750 }
1751
1752 #[test]
1753 fn refined_literal_residual_retains_generic_identity() {
1754 let original = Type::GenericParam {
1755 id: 1,
1756 name: "T".into(),
1757 };
1758 let shape = Type::union(vec![
1759 Type::StringLiteral("a".into()),
1760 Type::StringLiteral("b".into()),
1761 ]);
1762 let refined = preserve_refinement(&original, shape);
1763 let covered = BTreeSet::from([LiteralValue::String("a".into())]);
1764 assert_eq!(
1765 subtract_literals(&refined, &covered),
1766 preserve_refinement(&original, Type::StringLiteral("b".into()))
1767 );
1768 }
1769
1770 #[test]
1771 fn strip_null_from_nullable_string() {
1772 let ty = Type::union(vec![Type::String, Type::Null]);
1773 assert_eq!(strip_null(&ty), Type::String);
1774 }
1775
1776 #[test]
1777 fn strip_null_from_non_nullable_is_identity() {
1778 assert_eq!(strip_null(&Type::String), Type::String);
1779 assert_eq!(strip_null(&Type::Number), Type::Number);
1780 }
1781
1782 #[test]
1783 fn strip_null_from_null_is_error() {
1784 assert_eq!(strip_null(&Type::Null), Type::Error);
1785 }
1786
1787 #[test]
1788 fn strip_null_from_multi_member_union() {
1789 let ty = Type::union(vec![Type::String, Type::Number, Type::Null]);
1790 let stripped = strip_null(&ty);
1791 match stripped {
1793 Type::Union(members) => assert_eq!(members.len(), 2),
1794 _ => panic!("expected Union, got {stripped:?}"),
1795 }
1796 }
1797
1798 #[test]
1799 fn intersect_with_is_number_filters_union() {
1800 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1801 let kept = intersect_with(&ty, TypeFacts::IS_NUMBER);
1802 assert_eq!(kept, Type::Number);
1803 }
1804
1805 #[test]
1806 fn subtract_is_number_keeps_others() {
1807 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1808 let kept = subtract(&ty, TypeFacts::IS_NUMBER);
1809 match kept {
1810 Type::Union(members) => {
1811 assert_eq!(members.len(), 2);
1812 assert!(members.contains(&Type::String));
1813 assert!(members.contains(&Type::Boolean));
1814 }
1815 _ => panic!("expected Union, got {kept:?}"),
1816 }
1817 }
1818
1819 #[test]
1820 fn intersect_with_truthy_strips_null() {
1821 let ty = Type::union(vec![Type::String, Type::Null]);
1822 let kept = intersect_with(&ty, TypeFacts::TRUTHY);
1823 assert_eq!(kept, Type::String);
1824 }
1825
1826 #[test]
1827 fn subtract_truthy_keeps_only_null() {
1828 let ty = Type::union(vec![Type::String, Type::Null]);
1829 let kept = subtract(&ty, TypeFacts::TRUTHY);
1830 assert_eq!(kept, Type::Null);
1831 }
1832
1833 #[test]
1834 fn intersect_with_no_match_yields_error() {
1835 let kept = intersect_with(&Type::String, TypeFacts::IS_NUMBER);
1836 assert_eq!(kept, Type::Error);
1837 }
1838
1839 #[test]
1840 fn facts_for_target_type_covers_primitives() {
1841 assert_eq!(facts_for_target_type(&Type::Number), TypeFacts::IS_NUMBER);
1842 assert_eq!(facts_for_target_type(&Type::String), TypeFacts::IS_STRING);
1843 assert_eq!(facts_for_target_type(&Type::Boolean), TypeFacts::IS_BOOLEAN);
1844 assert_eq!(facts_for_target_type(&Type::Null), TypeFacts::EQ_NULL);
1845 }
1846}
1847
1848#[cfg(test)]
1849mod tests {
1850 use super::*;
1851 use crate::Span;
1852
1853 #[test]
1854 fn reference_path_equality_is_structural() {
1855 let a = ReferencePath::root(BindingId::Local {
1856 name: "x".to_string(),
1857 decl_scope: ScopeId(1),
1858 });
1859 let b = ReferencePath::root(BindingId::Local {
1860 name: "x".to_string(),
1861 decl_scope: ScopeId(1),
1862 });
1863 assert_eq!(a, b);
1864 }
1865
1866 #[test]
1867 fn reference_path_distinguishes_scopes() {
1868 let a = ReferencePath::root(BindingId::Local {
1869 name: "x".to_string(),
1870 decl_scope: ScopeId(1),
1871 });
1872 let b = ReferencePath::root(BindingId::Local {
1873 name: "x".to_string(),
1874 decl_scope: ScopeId(2),
1875 });
1876 assert_ne!(a, b);
1877 }
1878
1879 #[test]
1880 fn reference_path_distinguishes_chain() {
1881 let a = ReferencePath {
1882 root: BindingId::Local {
1883 name: "x".to_string(),
1884 decl_scope: ScopeId(1),
1885 },
1886 chain: vec![PathElem::Field("foo".to_string())],
1887 };
1888 let b = ReferencePath::root(BindingId::Local {
1889 name: "x".to_string(),
1890 decl_scope: ScopeId(1),
1891 });
1892 assert_ne!(a, b);
1893 }
1894
1895 #[test]
1896 fn type_facts_or_combines() {
1897 let f = TypeFacts::TRUTHY | TypeFacts::NE_NULL;
1898 assert!(f.contains(TypeFacts::TRUTHY));
1899 assert!(f.contains(TypeFacts::NE_NULL));
1900 assert!(!f.contains(TypeFacts::EQ_NULL));
1901 }
1902
1903 #[test]
1904 fn type_facts_not_complements() {
1905 let f = TypeFacts::EQ_NULL;
1906 let flipped = !f;
1907 assert!(flipped.contains(TypeFacts::NE_NULL));
1908 assert!(!flipped.contains(TypeFacts::EQ_NULL));
1909 }
1910
1911 #[test]
1912 fn binding_id_global_uses_mangled_name() {
1913 let g = BindingId::Global(crate::mangle::package_symbol("main", "x"));
1914 if let BindingId::Global(name) = &g {
1915 assert_eq!(name.as_str(), "main#x");
1916 } else {
1917 panic!("expected Global");
1918 }
1919 }
1920
1921 #[test]
1922 fn narrowed_view_construction() {
1923 let view = NarrowedView {
1924 narrowed_ty: Type::String,
1925 facts: TypeFacts::NE_NULL,
1926 excluded_literals: BTreeSet::new(),
1927 binding: Ident {
1928 name: "#narrow_0".to_string(),
1929 span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1930 },
1931 source: crate::ExprId(0),
1932 };
1933 assert_eq!(view.narrowed_ty, Type::String);
1934 assert!(view.facts.contains(TypeFacts::NE_NULL));
1935 }
1936
1937 #[test]
1938 fn cast_info_holds_kind_and_types() {
1939 let info = CastInfo {
1940 from_ty: Type::Union(vec![Type::String, Type::Null]),
1941 to_ty: Type::String,
1942 cast_kind: CastKind::NonNull,
1943 };
1944 assert!(matches!(info.cast_kind, CastKind::NonNull));
1945 }
1946
1947 #[test]
1948 fn union_discriminant_stub_returns_none() {
1949 assert!(union_discriminant(&[Type::String, Type::Null]).is_none());
1950 }
1951
1952 #[test]
1953 fn intersect_unknown_with_is_string() {
1954 assert_eq!(
1955 intersect_with(&Type::Unknown, TypeFacts::IS_STRING),
1956 Type::String
1957 );
1958 }
1959
1960 #[test]
1961 fn intersect_unknown_with_is_number() {
1962 assert_eq!(
1963 intersect_with(&Type::Unknown, TypeFacts::IS_NUMBER),
1964 Type::Number
1965 );
1966 }
1967
1968 #[test]
1969 fn intersect_unknown_with_is_boolean() {
1970 assert_eq!(
1971 intersect_with(&Type::Unknown, TypeFacts::IS_BOOLEAN),
1972 Type::Boolean
1973 );
1974 }
1975
1976 #[test]
1977 fn intersect_unknown_with_is_array_carries_unknown_element() {
1978 assert_eq!(
1981 intersect_with(&Type::Unknown, TypeFacts::IS_ARRAY),
1982 Type::Array(Box::new(Type::Unknown)),
1983 );
1984 }
1985
1986 #[test]
1987 fn intersect_unknown_with_eq_null() {
1988 assert_eq!(
1989 intersect_with(&Type::Unknown, TypeFacts::EQ_NULL),
1990 Type::Null
1991 );
1992 }
1993
1994 #[test]
1995 fn intersect_unknown_with_truthy_is_empty_object() {
1996 assert_eq!(
1998 intersect_with(&Type::Unknown, TypeFacts::TRUTHY),
1999 Type::empty_object()
2000 );
2001 }
2002
2003 #[test]
2004 fn subtract_unknown_removes_only_nullish_values() {
2005 assert_eq!(
2008 subtract(&Type::Unknown, TypeFacts::IS_STRING),
2009 Type::Unknown
2010 );
2011 assert_eq!(
2012 subtract(&Type::Unknown, TypeFacts::EQ_NULL),
2013 Type::union(vec![Type::empty_object(), Type::Undefined])
2014 );
2015 assert_eq!(subtract(&Type::Unknown, TypeFacts::TRUTHY), Type::Unknown);
2016 }
2017
2018 #[test]
2019 fn strip_null_from_unknown_leaves_empty_object_or_undefined() {
2020 assert_eq!(
2021 strip_null(&Type::Unknown),
2022 Type::union(vec![Type::empty_object(), Type::Undefined])
2023 );
2024 assert_eq!(strip_nullish(&Type::Unknown), Type::empty_object());
2025 }
2026
2027 #[test]
2028 fn condition_compatible_rejects_unknown() {
2029 assert!(!condition_compatible(&Type::Unknown));
2030 }
2031
2032 #[test]
2033 fn condition_compatible_accepts_js_truthiness_types() {
2034 for ty in [
2035 Type::Boolean,
2036 Type::String,
2037 Type::Number,
2038 Type::BigInt,
2039 Type::Null,
2040 Type::Array(Box::new(Type::Number)),
2041 Type::union(vec![Type::String, Type::Null]),
2042 Type::union(vec![Type::Number, Type::Void]),
2043 ] {
2044 assert!(condition_compatible(&ty), "{ty:?}");
2045 }
2046 assert!(condition_compatible(&Type::Never));
2047 assert!(!condition_compatible(&Type::Void));
2048 }
2049
2050 #[test]
2051 fn truthiness_class_table() {
2052 use TruthinessClass::*;
2053 let cases = [
2054 (Type::Null, AlwaysFalsy),
2055 (Type::Boolean, BooleanLike),
2056 (Type::Number, NumberLike),
2057 (
2058 Type::NumberLiteral(crate::types::LiteralF64(0.0)),
2059 AlwaysFalsy,
2060 ),
2061 (
2062 Type::NumberLiteral(crate::types::LiteralF64(-0.0)),
2063 AlwaysFalsy,
2064 ),
2065 (
2066 Type::NumberLiteral(crate::types::LiteralF64(3.0)),
2067 AlwaysTruthy,
2068 ),
2069 (Type::String, StringLike),
2070 (Type::StringLiteral(String::new()), AlwaysFalsy),
2071 (Type::StringLiteral("a".to_string()), AlwaysTruthy),
2072 (Type::BigInt, BigIntLike),
2073 (Type::Uint8Array, AlwaysTruthy),
2074 (Type::Array(Box::new(Type::Number)), AlwaysTruthy),
2075 (Type::Unknown, Dynamic),
2076 ];
2077 for (ty, expected) in cases {
2078 assert_eq!(truthiness_class(&ty), expected, "{ty:?}");
2079 }
2080 }
2081
2082 #[test]
2083 fn truthy_part_strips_null_and_falsy_literals() {
2084 assert_eq!(
2085 truthy_part(&Type::union(vec![Type::String, Type::Null])),
2086 Type::String
2087 );
2088 assert_eq!(
2089 truthy_part(&Type::union(vec![
2090 Type::StringLiteral(String::new()),
2091 Type::StringLiteral("a".to_string()),
2092 Type::Null,
2093 ])),
2094 Type::StringLiteral("a".to_string())
2095 );
2096 assert_eq!(truthy_part(&Type::Null), Type::Never);
2097 assert_eq!(truthy_part(&Type::Boolean), Type::BooleanLiteral(true));
2098 assert_eq!(truthy_part(&Type::Number), Type::Number);
2099 }
2100
2101 #[test]
2102 fn falsy_part_keeps_falsy_capable_members() {
2103 assert_eq!(
2104 falsy_part(&Type::union(vec![Type::String, Type::Null])),
2105 Type::union(vec![Type::StringLiteral(String::new()), Type::Null])
2106 );
2107 assert_eq!(falsy_part(&Type::Number), Type::Number);
2108 assert_eq!(falsy_part(&Type::Boolean), Type::BooleanLiteral(false));
2109 assert_eq!(
2111 falsy_part(&Type::union(vec![
2112 Type::Array(Box::new(Type::Number)),
2113 Type::Null
2114 ])),
2115 Type::Null
2116 );
2117 assert_eq!(
2118 falsy_part(&Type::Array(Box::new(Type::Number))),
2119 Type::Never
2120 );
2121 assert_eq!(falsy_part(&Type::empty_object()), Type::empty_object());
2122 }
2123
2124 #[test]
2125 fn falsy_only_null_distinguishes_reference_unions() {
2126 assert!(falsy_values_are_only_null(&Type::union(vec![
2127 Type::Array(Box::new(Type::Number)),
2128 Type::Null
2129 ])));
2130 assert!(!falsy_values_are_only_null(&Type::union(vec![
2131 Type::String,
2132 Type::Null
2133 ])));
2134 assert!(falsy_values_are_only_null(&Type::Uint8Array));
2135 assert!(!falsy_values_are_only_null(&Type::Number));
2136 }
2137
2138 #[test]
2139 fn intersect_truthy_and_falsy_respect_js_truthiness() {
2140 let string_or_null = Type::union(vec![Type::String, Type::Null]);
2141 assert_eq!(
2142 intersect_with(&string_or_null, TypeFacts::TRUTHY | TypeFacts::NE_NULL),
2143 Type::String
2144 );
2145 assert_eq!(
2147 intersect_with(&string_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2148 string_or_null
2149 );
2150 let arr_or_null = Type::union(vec![Type::Array(Box::new(Type::Number)), Type::Null]);
2151 assert_eq!(
2152 intersect_with(&arr_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2153 Type::Null
2154 );
2155 assert_eq!(
2157 intersect_with(&string_or_null, TypeFacts::NE_NULL),
2158 Type::String
2159 );
2160 }
2161}