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) => {
717 let non_null: Vec<Type> = members
718 .iter()
719 .filter(|m| !matches!(m.peel(), Type::Null))
720 .cloned()
721 .collect();
722 Type::union(non_null)
723 }
724 Type::Unknown => Type::Unknown,
729 _ => ty.clone(),
731 }
732}
733
734pub fn strip_undefined(ty: &Type) -> Type {
738 if let Type::Refined {
739 original,
740 ty: shape,
741 } = ty.without_aliases()
742 {
743 return preserve_refinement(original, strip_undefined(shape));
744 }
745 match ty.peel() {
746 Type::Undefined => Type::Error,
747 Type::Union(members) => Type::union(
748 members
749 .iter()
750 .filter(|member| !matches!(member.peel(), Type::Undefined))
751 .cloned()
752 .collect(),
753 ),
754 _ => ty.clone(),
755 }
756}
757
758pub fn strip_nullish(ty: &Type) -> Type {
760 strip_undefined(&strip_null(ty))
761}
762
763pub fn narrow_field_presence(
767 receiver_ty: &Type,
768 field: &str,
769 lookup: &dyn Fn(&Type, &str) -> Option<crate::ObjectField>,
770) -> (Type, Type) {
771 if let Type::Refined {
772 original,
773 ty: shape,
774 } = receiver_ty.without_aliases()
775 {
776 let (present, absent) = narrow_field_presence(shape, field, lookup);
777 return (
778 preserve_refinement(original, present),
779 preserve_refinement(original, absent),
780 );
781 }
782 match receiver_ty.peel() {
783 Type::Unknown => {
784 let fields = BTreeMap::from([(
785 field.to_string(),
786 crate::ObjectField::required(Type::Unknown),
787 )]);
788 (
789 Type::Object {
790 index: None,
791 fields,
792 },
793 Type::Unknown,
794 )
795 }
796 Type::Object { fields, .. } => {
797 let mut present = fields.clone();
798 present
799 .entry(field.to_string())
800 .or_insert_with(|| crate::ObjectField::required(Type::Unknown));
801 let absent = match fields.get(field) {
802 Some(f) if !f.optional => Type::Error,
803 _ => receiver_ty.clone(),
804 };
805 (
806 Type::Object {
807 index: None,
808 fields: present,
809 },
810 absent,
811 )
812 }
813 Type::Union(members) => {
814 let has = members
815 .iter()
816 .filter(|m| lookup(m, field).is_some())
817 .cloned()
818 .collect();
819 let lacks = members
820 .iter()
821 .filter(|m| lookup(m, field).is_none_or(|f| f.optional))
822 .cloned()
823 .collect();
824 (Type::union(has), Type::union(lacks))
825 }
826 Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
827 let absent = if lookup(receiver_ty, field).is_some_and(|f| !f.optional) {
828 Type::Error
829 } else {
830 receiver_ty.clone()
831 };
832 (receiver_ty.clone(), absent)
833 }
834 _ => (Type::Error, Type::Error),
835 }
836}
837
838#[derive(Clone, Debug, Default)]
845pub struct NarrowEnv {
846 views: BTreeMap<ReferencePath, NarrowedView>,
847 pub dropped: BTreeMap<ReferencePath, Type>,
850}
851
852impl NarrowEnv {
853 pub fn new() -> Self {
854 Self::default()
855 }
856
857 pub fn insert(&mut self, path: ReferencePath, view: NarrowedView) -> Option<NarrowedView> {
858 self.dropped.remove(&path);
859 self.views.insert(path, view)
860 }
861
862 pub fn extend_env(&mut self, other: Self) {
863 self.dropped.extend(other.dropped);
864 self.extend(other.views);
865 }
866
867 pub fn clear(&mut self) {
868 self.views.clear();
869 self.dropped.clear();
870 }
871}
872
873impl std::ops::Deref for NarrowEnv {
874 type Target = BTreeMap<ReferencePath, NarrowedView>;
875 fn deref(&self) -> &Self::Target {
876 &self.views
877 }
878}
879
880impl std::ops::DerefMut for NarrowEnv {
881 fn deref_mut(&mut self) -> &mut Self::Target {
882 &mut self.views
883 }
884}
885
886impl IntoIterator for NarrowEnv {
887 type Item = (ReferencePath, NarrowedView);
888 type IntoIter = std::collections::btree_map::IntoIter<ReferencePath, NarrowedView>;
889 fn into_iter(self) -> Self::IntoIter {
890 self.views.into_iter()
891 }
892}
893
894impl<'a> IntoIterator for &'a NarrowEnv {
895 type Item = (&'a ReferencePath, &'a NarrowedView);
896 type IntoIter = std::collections::btree_map::Iter<'a, ReferencePath, NarrowedView>;
897 fn into_iter(self) -> Self::IntoIter {
898 self.views.iter()
899 }
900}
901
902impl Extend<(ReferencePath, NarrowedView)> for NarrowEnv {
903 fn extend<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(&mut self, iter: I) {
904 for (path, view) in iter {
905 self.insert(path, view);
906 }
907 }
908}
909
910impl FromIterator<(ReferencePath, NarrowedView)> for NarrowEnv {
911 fn from_iter<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(iter: I) -> Self {
912 let mut env = Self::new();
913 env.extend(iter);
914 env
915 }
916}
917
918pub fn wrap_order(a: &ReferencePath, b: &ReferencePath) -> std::cmp::Ordering {
923 b.chain.len().cmp(&a.chain.len()).then_with(|| a.cmp(b))
924}
925
926pub fn envs_per_path_narrowed_ty_equal(a: &NarrowEnv, b: &NarrowEnv) -> bool {
929 if a.len() != b.len() {
930 return false;
931 }
932 for (path, a_view) in a {
933 match b.get(path) {
934 Some(b_view) if a_view.narrowed_ty == b_view.narrowed_ty => continue,
935 _ => return false,
936 }
937 }
938 true
939}
940
941#[derive(Clone, Copy, Debug, PartialEq, Eq)]
944pub enum TruthinessClass {
945 AlwaysTruthy,
946 AlwaysFalsy,
947 NumberLike,
949 StringLike,
951 BooleanLike,
953 BigIntLike,
955 Dynamic,
957}
958
959pub fn truthiness_class(member: &Type) -> TruthinessClass {
960 use TruthinessClass::*;
961 match member.peel() {
962 Type::Null | Type::Undefined => AlwaysFalsy,
963 Type::Boolean => BooleanLike,
964 Type::BooleanLiteral(true) => AlwaysTruthy,
965 Type::BooleanLiteral(false) => AlwaysFalsy,
966 Type::Number => NumberLike,
967 Type::NumberLiteral(n) => {
968 if n.0 == 0.0 || n.0.is_nan() {
969 AlwaysFalsy
970 } else {
971 AlwaysTruthy
972 }
973 }
974 Type::String => StringLike,
975 Type::StringLiteral(s) => {
976 if s.is_empty() {
977 AlwaysFalsy
978 } else {
979 AlwaysTruthy
980 }
981 }
982 Type::BigInt => BigIntLike,
983 Type::BigIntLiteral(digits) => {
984 if digits == "0" {
985 AlwaysFalsy
986 } else {
987 AlwaysTruthy
988 }
989 }
990 Type::NumberEnum {
991 member: Some(member),
992 ..
993 } => truthiness_class(&Type::NumberLiteral(member.value)),
994 Type::StringEnum {
995 member: Some(member),
996 ..
997 } => truthiness_class(&Type::StringLiteral(member.value.clone())),
998 Type::NumberEnum { .. } => NumberLike,
999 Type::StringEnum { .. } => StringLike,
1000 empty if is_empty_object(empty) => Dynamic,
1003 Type::Object { .. }
1004 | Type::Array(_)
1005 | Type::Tuple(_)
1006 | Type::Function { .. }
1007 | Type::Uint8Array
1008 | Type::InterfaceRef { .. }
1009 | Type::ClassRef { .. } => AlwaysTruthy,
1010 _ => Dynamic,
1011 }
1012}
1013
1014fn is_empty_object(ty: &Type) -> bool {
1016 matches!(ty.peel(), Type::Object { fields, index } if fields.is_empty() && index.is_none())
1017}
1018
1019pub fn has_known_truthiness(ty: &Type) -> bool {
1022 union_members(ty).into_iter().all(|member| {
1023 matches!(
1024 truthiness_class(member),
1025 TruthinessClass::AlwaysTruthy | TruthinessClass::AlwaysFalsy
1026 )
1027 })
1028}
1029
1030pub fn negation_type(ty: &Type) -> Type {
1034 let classes: Vec<TruthinessClass> = union_members(ty)
1035 .into_iter()
1036 .map(truthiness_class)
1037 .collect();
1038 if classes
1039 .iter()
1040 .all(|class| *class == TruthinessClass::AlwaysTruthy)
1041 {
1042 return Type::BooleanLiteral(false);
1043 }
1044 if classes
1045 .iter()
1046 .all(|class| *class == TruthinessClass::AlwaysFalsy)
1047 {
1048 return Type::BooleanLiteral(true);
1049 }
1050 Type::Boolean
1051}
1052
1053pub(super) fn union_members(ty: &Type) -> Vec<&Type> {
1054 match ty.peel() {
1055 Type::Union(members) => members.iter().collect(),
1056 _ => vec![ty],
1058 }
1059}
1060
1061pub fn truthy_part(ty: &Type) -> Type {
1065 if let Type::Refined {
1066 original,
1067 ty: shape,
1068 } = ty.without_aliases()
1069 {
1070 return preserve_refinement(original, truthy_part(shape));
1071 }
1072 let kept: Vec<Type> = union_members(ty)
1074 .into_iter()
1075 .filter(|m| truthiness_class(m) != TruthinessClass::AlwaysFalsy)
1076 .map(|m| match m.without_aliases() {
1077 Type::Boolean => Type::BooleanLiteral(true),
1078 _ => m.clone(),
1079 })
1080 .collect();
1081 Type::union(kept)
1082}
1083
1084pub fn is_never_falsy(ty: &Type) -> bool {
1086 union_members(ty)
1087 .into_iter()
1088 .all(|m| truthiness_class(m) == TruthinessClass::AlwaysTruthy)
1089}
1090
1091pub fn falsy_part(ty: &Type) -> Type {
1096 if let Type::Refined {
1097 original,
1098 ty: shape,
1099 } = ty.without_aliases()
1100 {
1101 return preserve_refinement(original, falsy_part(shape));
1102 }
1103 let kept: Vec<Type> = union_members(ty)
1104 .into_iter()
1105 .filter_map(|m| match truthiness_class(m) {
1106 TruthinessClass::AlwaysTruthy => None,
1107 TruthinessClass::Dynamic if is_empty_object(m) => None,
1110 TruthinessClass::StringLike => Some(match m.peel() {
1111 Type::String => Type::StringLiteral(String::new()),
1112 _ => m.clone(),
1113 }),
1114 _ => Some(match m.without_aliases() {
1115 Type::Boolean => Type::BooleanLiteral(false),
1116 _ => m.clone(),
1117 }),
1118 })
1119 .collect();
1120 Type::union(kept)
1121}
1122
1123pub fn falsy_values_are_only_null(ty: &Type) -> bool {
1126 union_members(ty).into_iter().all(|m| {
1127 matches!(m.peel(), Type::Null) || truthiness_class(m) == TruthinessClass::AlwaysTruthy
1128 })
1129}
1130
1131pub fn type_matches_facts(ty: &Type, facts: TypeFacts) -> bool {
1132 let ty = ty.peel();
1136 if facts.contains(TypeFacts::TRUTHY) {
1140 return truthiness_class(ty) != TruthinessClass::AlwaysFalsy;
1141 }
1142 if facts.contains(TypeFacts::FALSY) {
1143 return truthiness_class(ty) != TruthinessClass::AlwaysTruthy;
1144 }
1145 if facts.contains(TypeFacts::NE_NULL) || facts.contains(TypeFacts::NE_UNDEFINED) {
1146 return !(facts.contains(TypeFacts::NE_NULL) && matches!(ty, Type::Null)
1147 || facts.contains(TypeFacts::NE_UNDEFINED) && matches!(ty, Type::Undefined));
1148 }
1149 if facts.contains(TypeFacts::IS_UNDEFINED) {
1152 return matches!(ty, Type::Undefined);
1153 }
1154 if facts.contains(TypeFacts::EQ_UNDEFINED) {
1155 return matches!(ty, Type::Undefined);
1156 }
1157 if facts.contains(TypeFacts::EQ_NULL) {
1158 return matches!(ty, Type::Null);
1159 }
1160 if facts.contains(TypeFacts::IS_BIGINT) {
1161 return matches!(ty, Type::BigInt);
1162 }
1163 if facts.contains(TypeFacts::IS_NUMBER) {
1164 return matches!(
1166 ty,
1167 Type::Number | Type::NumberLiteral(_) | Type::NumberEnum { .. }
1168 );
1169 }
1170 if facts.contains(TypeFacts::IS_STRING) {
1171 return matches!(
1172 ty,
1173 Type::String | Type::StringLiteral(_) | Type::StringEnum { .. }
1174 );
1175 }
1176 if facts.contains(TypeFacts::IS_BOOLEAN) {
1177 return matches!(ty, Type::Boolean | Type::BooleanLiteral(_));
1178 }
1179 if facts.contains(TypeFacts::IS_ARRAY) {
1180 return matches!(ty, Type::Array(_));
1181 }
1182 if facts.contains(TypeFacts::IS_FUNCTION) {
1183 return matches!(ty, Type::Function { .. });
1184 }
1185 if facts.contains(TypeFacts::IS_OBJECT) {
1186 return is_typeof_object(ty);
1187 }
1188 false
1189}
1190
1191fn is_typeof_object(ty: &Type) -> bool {
1199 match ty {
1200 Type::Union(members) => members.iter().all(is_typeof_object),
1203
1204 Type::Object { .. }
1205 | Type::Array(_)
1206 | Type::Tuple(_)
1207 | Type::InterfaceRef { .. }
1208 | Type::ClassRef { .. }
1209 | Type::Uint8Array
1210 | Type::Null => true,
1212
1213 Type::Undefined
1214 | Type::Number
1215 | Type::NumberLiteral(_)
1216 | Type::NumberEnum { .. }
1217 | Type::String
1218 | Type::StringLiteral(_)
1219 | Type::StringEnum { .. }
1220 | Type::Boolean
1221 | Type::BooleanLiteral(_)
1222 | Type::BigInt
1223 | Type::BigIntLiteral(_)
1224 | Type::Function { .. } => false,
1225
1226 Type::Void
1233 | Type::Never
1234 | Type::Error
1235 | Type::Unknown
1236 | Type::TypeVar(_)
1237 | Type::GenericParam { .. }
1238 | Type::AliasRef { .. } => false,
1239
1240 Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => false,
1242 }
1243}
1244
1245pub fn intersect_with(ty: &Type, facts: TypeFacts) -> Type {
1248 if let Type::Refined {
1249 original,
1250 ty: shape,
1251 } = ty.without_aliases()
1252 {
1253 return preserve_refinement(original, intersect_with(shape, facts));
1254 }
1255 let peeled = ty.peel();
1256 if let Some(asserted) = asserted_type_for_facts(facts) {
1257 match peeled {
1258 Type::Unknown => return asserted,
1259 Type::GenericParam { .. } => {
1260 return Type::Refined {
1261 original: Box::new(ty.clone()),
1262 ty: Box::new(asserted),
1263 };
1264 }
1265 _ => {}
1266 }
1267 }
1268 match peeled {
1269 Type::Union(members) => Type::union(
1270 members
1271 .iter()
1272 .map(|m| intersect_with(m, facts))
1273 .filter(|ty| !matches!(ty, Type::Error))
1274 .collect(),
1275 ),
1276 Type::Void if facts.contains(TypeFacts::IS_UNDEFINED) => Type::Undefined,
1279 Type::Boolean if facts == TypeFacts::TRUTHY => Type::BooleanLiteral(true),
1281 Type::Boolean if facts == TypeFacts::FALSY => Type::BooleanLiteral(false),
1282 _ if type_matches_facts(peeled, facts) => ty.clone(),
1283 _ if matches!(peeled, Type::Unknown) => Type::Unknown,
1284 _ => Type::Error,
1285 }
1286}
1287
1288pub fn subtract(ty: &Type, facts: TypeFacts) -> Type {
1289 if let Type::Refined {
1290 original,
1291 ty: shape,
1292 } = ty.without_aliases()
1293 {
1294 return preserve_refinement(original, subtract(shape, facts));
1295 }
1296 let peeled = ty.peel();
1297 if matches!(peeled, Type::Unknown) {
1300 return Type::Unknown;
1301 }
1302 let ruled_out =
1306 |member: &Type| !matches!(member.peel(), Type::Void) && type_matches_facts(member, facts);
1307 match peeled {
1308 Type::Union(members) => {
1309 let kept: Vec<Type> = members.iter().filter(|m| !ruled_out(m)).cloned().collect();
1310 Type::union(kept)
1311 }
1312 _ if !ruled_out(peeled) => ty.clone(),
1313 _ => Type::Error,
1314 }
1315}
1316
1317pub(super) fn with_source_refinement(source: &Type, shape: Type) -> Type {
1318 match source.without_aliases() {
1319 Type::Refined { original, .. } => preserve_refinement(original, shape),
1320 _ => shape,
1321 }
1322}
1323
1324pub(super) fn preserve_refinement(original: &Type, shape: Type) -> Type {
1325 if matches!(shape, Type::Error | Type::Never) {
1326 return shape;
1327 }
1328 Type::Refined {
1329 original: Box::new(original.clone()),
1330 ty: Box::new(shape),
1331 }
1332}
1333
1334pub fn subtract_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> Type {
1342 if let Type::Refined {
1343 original,
1344 ty: shape,
1345 } = ty.without_aliases()
1346 {
1347 return preserve_refinement(original, subtract_literals(shape, covered));
1348 }
1349 match ty.peel() {
1350 Type::Union(members) => {
1351 let kept: Vec<Type> = members
1352 .iter()
1353 .map(|m| subtract_literals(m, covered))
1354 .collect();
1355 Type::union(kept)
1356 }
1357 Type::Boolean => {
1359 let remaining: Vec<Type> = [false, true]
1360 .into_iter()
1361 .filter(|b| !covered.contains(&LiteralValue::Boolean(*b)))
1362 .map(Type::BooleanLiteral)
1363 .collect();
1364 Type::union(remaining)
1365 }
1366 single @ (Type::StringLiteral(_)
1367 | Type::NumberLiteral(_)
1368 | Type::BooleanLiteral(_)
1369 | Type::BigIntLiteral(_)
1370 | Type::Null
1371 | Type::Undefined) => match unit_value(single) {
1372 Some(lit) if covered.contains(&lit) => Type::Never,
1373 _ => ty.clone(),
1374 },
1375 _ => ty.clone(),
1376 }
1377}
1378
1379fn asserted_type_for_facts(facts: TypeFacts) -> Option<Type> {
1383 if facts.contains(TypeFacts::IS_BIGINT) {
1384 return Some(Type::BigInt);
1385 }
1386 if facts.contains(TypeFacts::IS_NUMBER) {
1387 return Some(Type::Number);
1388 }
1389 if facts.contains(TypeFacts::IS_STRING) {
1390 return Some(Type::String);
1391 }
1392 if facts.contains(TypeFacts::IS_BOOLEAN) {
1393 return Some(Type::Boolean);
1394 }
1395 if facts.contains(TypeFacts::IS_ARRAY) {
1396 return Some(Type::Array(Box::new(Type::Unknown)));
1397 }
1398 if facts.contains(TypeFacts::EQ_NULL) {
1399 return Some(Type::Null);
1400 }
1401 if facts.contains(TypeFacts::EQ_UNDEFINED) || facts.contains(TypeFacts::IS_UNDEFINED) {
1402 return Some(Type::Undefined);
1403 }
1404 None
1407}
1408
1409pub fn union_envs(
1424 a_narrowings: NarrowEnv,
1425 a_assigned: BTreeSet<ReferencePath>,
1426 b_narrowings: NarrowEnv,
1427 b_assigned: BTreeSet<ReferencePath>,
1428) -> (NarrowEnv, BTreeSet<ReferencePath>) {
1429 let mut joined_narrowings = NarrowEnv::new();
1430 for (path, a_view) in &a_narrowings {
1431 if let Some(b_view) = b_narrowings.get(path) {
1432 let joined_ty = join_flow_types(&a_view.narrowed_ty, &b_view.narrowed_ty);
1433 let a_view = if is_ruled_out(&a_view.narrowed_ty) {
1435 b_view
1436 } else {
1437 a_view
1438 };
1439 joined_narrowings.insert(
1440 path.clone(),
1441 NarrowedView {
1442 narrowed_ty: joined_ty,
1443 facts: TypeFacts::EMPTY,
1448 excluded_literals: BTreeSet::new(),
1449 binding: a_view.binding.clone(),
1455 source: a_view.source,
1456 },
1457 );
1458 }
1459 }
1460
1461 for (path, ty) in a_narrowings.dropped.iter().chain(&b_narrowings.dropped) {
1462 let left_ty = a_narrowings
1463 .dropped
1464 .get(path)
1465 .or_else(|| a_narrowings.get(path).map(|view| &view.narrowed_ty));
1466 let right_ty = b_narrowings
1467 .dropped
1468 .get(path)
1469 .or_else(|| b_narrowings.get(path).map(|view| &view.narrowed_ty));
1470 if left_ty == Some(ty) && right_ty == Some(ty) {
1471 joined_narrowings.dropped.insert(path.clone(), ty.clone());
1472 }
1473 }
1474
1475 let mut joined_assigned = a_assigned;
1476 joined_assigned.extend(b_assigned);
1477
1478 joined_narrowings.dropped.retain(|key, _| {
1479 !joined_assigned
1480 .iter()
1481 .any(|assigned| assigned.is_prefix_of(key))
1482 });
1483 joined_narrowings.retain(|key, _| {
1484 !joined_assigned
1485 .iter()
1486 .any(|assigned| assigned != key && assigned.is_prefix_of(key))
1487 });
1488
1489 (joined_narrowings, joined_assigned)
1490}
1491
1492pub(super) fn tuple_position(index: f64) -> Option<usize> {
1494 (index >= 0.0 && index.fract() == 0.0 && index <= u32::MAX as f64).then_some(index as usize)
1495}
1496
1497pub const RULED_OUT: Type = Type::Error;
1502
1503pub fn is_ruled_out(ty: &Type) -> bool {
1505 matches!(ty, Type::Error)
1506}
1507
1508pub(super) fn join_flow_types(left: &Type, right: &Type) -> Type {
1511 if is_ruled_out(left) {
1513 return right.clone();
1514 }
1515 if is_ruled_out(right) {
1516 return left.clone();
1517 }
1518 let joined = Type::union(vec![left.clone(), right.clone()]);
1519 let Type::Union(mut members) = joined else {
1520 return joined;
1521 };
1522 let has_number = members
1523 .iter()
1524 .any(|ty| matches!(ty.without_aliases(), Type::Number));
1525 let has_string = members
1526 .iter()
1527 .any(|ty| matches!(ty.without_aliases(), Type::String));
1528 members.retain(|ty| match ty.without_aliases() {
1529 Type::NumberLiteral(_) => !has_number,
1530 Type::StringLiteral(_) => !has_string,
1531 _ => true,
1532 });
1533 Type::union(members)
1534}
1535
1536pub fn has_erased_member(ty: &Type) -> bool {
1544 match ty.peel() {
1545 Type::TypeVar(_) | Type::GenericParam { .. } => true,
1546 Type::Union(members) => members.iter().any(has_erased_member),
1547 _ => false,
1548 }
1549}
1550
1551pub fn condition_compatible(ty: &Type) -> bool {
1559 !matches!(ty.peel(), Type::Unknown | Type::Void)
1560}
1561
1562pub fn facts_for_target_type(target: &Type) -> TypeFacts {
1563 match target.peel() {
1564 Type::Number | Type::NumberLiteral(_) => TypeFacts::IS_NUMBER,
1565 Type::String | Type::StringLiteral(_) => TypeFacts::IS_STRING,
1566 Type::Boolean | Type::BooleanLiteral(_) => TypeFacts::IS_BOOLEAN,
1567 Type::Null => TypeFacts::EQ_NULL,
1568 Type::Undefined => TypeFacts::EQ_UNDEFINED,
1569 Type::BigInt => TypeFacts::IS_BIGINT,
1570 Type::Array(_) => TypeFacts::IS_ARRAY,
1571 _ => TypeFacts::EMPTY,
1572 }
1573}
1574
1575#[cfg(test)]
1576mod analyzer_tests {
1577 use super::*;
1578
1579 #[test]
1580 fn typeof_undefined_splits_void_without_assuming_its_truthiness() {
1581 let ty = Type::union(vec![Type::Boolean, Type::Void]);
1582 assert_eq!(
1583 intersect_with(&ty, TypeFacts::IS_UNDEFINED),
1584 Type::Undefined
1585 );
1586 assert_eq!(subtract(&ty, TypeFacts::IS_UNDEFINED), ty);
1587 assert_eq!(truthiness_class(&Type::Void), TruthinessClass::Dynamic);
1588 assert!(!type_matches_facts(&Type::Void, TypeFacts::EQ_UNDEFINED));
1589 }
1590
1591 #[test]
1592 fn undefined_facts_are_distinct_from_null() {
1593 let ty = Type::union(vec![Type::String, Type::Null, Type::Undefined]);
1594 assert_eq!(
1595 strip_null(&ty),
1596 Type::union(vec![Type::String, Type::Undefined])
1597 );
1598 assert_eq!(
1599 strip_undefined(&ty),
1600 Type::union(vec![Type::String, Type::Null])
1601 );
1602 assert_eq!(strip_nullish(&ty), Type::String);
1603 assert_eq!(
1604 intersect_with(&ty, TypeFacts::EQ_UNDEFINED),
1605 Type::Undefined
1606 );
1607 assert_eq!(
1608 intersect_with(&Type::Unknown, TypeFacts::EQ_UNDEFINED),
1609 Type::Undefined
1610 );
1611 assert_eq!(
1612 intersect_with(&ty, TypeFacts::NE_NULL | TypeFacts::NE_UNDEFINED),
1613 Type::String
1614 );
1615 assert_eq!(truthy_part(&ty), Type::String);
1616 assert_eq!(
1617 truthiness_class(&Type::Undefined),
1618 TruthinessClass::AlwaysFalsy
1619 );
1620 assert!(!type_matches_facts(&Type::Undefined, TypeFacts::IS_OBJECT));
1621 }
1622
1623 #[test]
1624 fn presence_keeps_optional_read_undefined() {
1625 let ty = Type::Object {
1626 fields: BTreeMap::from([("value".into(), crate::ObjectField::optional(Type::Number))]),
1627 index: None,
1628 };
1629 let (present, absent) = narrow_field_presence(&ty, "value", &|_, _| None);
1630 assert_eq!(present, ty);
1631 assert_eq!(absent, ty);
1632 }
1633
1634 #[test]
1635 fn refined_literal_residual_retains_generic_identity() {
1636 let original = Type::GenericParam {
1637 id: 1,
1638 name: "T".into(),
1639 };
1640 let shape = Type::union(vec![
1641 Type::StringLiteral("a".into()),
1642 Type::StringLiteral("b".into()),
1643 ]);
1644 let refined = preserve_refinement(&original, shape);
1645 let covered = BTreeSet::from([LiteralValue::String("a".into())]);
1646 assert_eq!(
1647 subtract_literals(&refined, &covered),
1648 preserve_refinement(&original, Type::StringLiteral("b".into()))
1649 );
1650 }
1651
1652 #[test]
1653 fn strip_null_from_nullable_string() {
1654 let ty = Type::union(vec![Type::String, Type::Null]);
1655 assert_eq!(strip_null(&ty), Type::String);
1656 }
1657
1658 #[test]
1659 fn strip_null_from_non_nullable_is_identity() {
1660 assert_eq!(strip_null(&Type::String), Type::String);
1661 assert_eq!(strip_null(&Type::Number), Type::Number);
1662 }
1663
1664 #[test]
1665 fn strip_null_from_null_is_error() {
1666 assert_eq!(strip_null(&Type::Null), Type::Error);
1667 }
1668
1669 #[test]
1670 fn strip_null_from_multi_member_union() {
1671 let ty = Type::union(vec![Type::String, Type::Number, Type::Null]);
1672 let stripped = strip_null(&ty);
1673 match stripped {
1675 Type::Union(members) => assert_eq!(members.len(), 2),
1676 _ => panic!("expected Union, got {stripped:?}"),
1677 }
1678 }
1679
1680 #[test]
1681 fn intersect_with_is_number_filters_union() {
1682 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1683 let kept = intersect_with(&ty, TypeFacts::IS_NUMBER);
1684 assert_eq!(kept, Type::Number);
1685 }
1686
1687 #[test]
1688 fn subtract_is_number_keeps_others() {
1689 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1690 let kept = subtract(&ty, TypeFacts::IS_NUMBER);
1691 match kept {
1692 Type::Union(members) => {
1693 assert_eq!(members.len(), 2);
1694 assert!(members.contains(&Type::String));
1695 assert!(members.contains(&Type::Boolean));
1696 }
1697 _ => panic!("expected Union, got {kept:?}"),
1698 }
1699 }
1700
1701 #[test]
1702 fn intersect_with_truthy_strips_null() {
1703 let ty = Type::union(vec![Type::String, Type::Null]);
1704 let kept = intersect_with(&ty, TypeFacts::TRUTHY);
1705 assert_eq!(kept, Type::String);
1706 }
1707
1708 #[test]
1709 fn subtract_truthy_keeps_only_null() {
1710 let ty = Type::union(vec![Type::String, Type::Null]);
1711 let kept = subtract(&ty, TypeFacts::TRUTHY);
1712 assert_eq!(kept, Type::Null);
1713 }
1714
1715 #[test]
1716 fn intersect_with_no_match_yields_error() {
1717 let kept = intersect_with(&Type::String, TypeFacts::IS_NUMBER);
1718 assert_eq!(kept, Type::Error);
1719 }
1720
1721 #[test]
1722 fn facts_for_target_type_covers_primitives() {
1723 assert_eq!(facts_for_target_type(&Type::Number), TypeFacts::IS_NUMBER);
1724 assert_eq!(facts_for_target_type(&Type::String), TypeFacts::IS_STRING);
1725 assert_eq!(facts_for_target_type(&Type::Boolean), TypeFacts::IS_BOOLEAN);
1726 assert_eq!(facts_for_target_type(&Type::Null), TypeFacts::EQ_NULL);
1727 }
1728}
1729
1730#[cfg(test)]
1731mod tests {
1732 use super::*;
1733 use crate::Span;
1734
1735 #[test]
1736 fn reference_path_equality_is_structural() {
1737 let a = ReferencePath::root(BindingId::Local {
1738 name: "x".to_string(),
1739 decl_scope: ScopeId(1),
1740 });
1741 let b = ReferencePath::root(BindingId::Local {
1742 name: "x".to_string(),
1743 decl_scope: ScopeId(1),
1744 });
1745 assert_eq!(a, b);
1746 }
1747
1748 #[test]
1749 fn reference_path_distinguishes_scopes() {
1750 let a = ReferencePath::root(BindingId::Local {
1751 name: "x".to_string(),
1752 decl_scope: ScopeId(1),
1753 });
1754 let b = ReferencePath::root(BindingId::Local {
1755 name: "x".to_string(),
1756 decl_scope: ScopeId(2),
1757 });
1758 assert_ne!(a, b);
1759 }
1760
1761 #[test]
1762 fn reference_path_distinguishes_chain() {
1763 let a = ReferencePath {
1764 root: BindingId::Local {
1765 name: "x".to_string(),
1766 decl_scope: ScopeId(1),
1767 },
1768 chain: vec![PathElem::Field("foo".to_string())],
1769 };
1770 let b = ReferencePath::root(BindingId::Local {
1771 name: "x".to_string(),
1772 decl_scope: ScopeId(1),
1773 });
1774 assert_ne!(a, b);
1775 }
1776
1777 #[test]
1778 fn type_facts_or_combines() {
1779 let f = TypeFacts::TRUTHY | TypeFacts::NE_NULL;
1780 assert!(f.contains(TypeFacts::TRUTHY));
1781 assert!(f.contains(TypeFacts::NE_NULL));
1782 assert!(!f.contains(TypeFacts::EQ_NULL));
1783 }
1784
1785 #[test]
1786 fn type_facts_not_complements() {
1787 let f = TypeFacts::EQ_NULL;
1788 let flipped = !f;
1789 assert!(flipped.contains(TypeFacts::NE_NULL));
1790 assert!(!flipped.contains(TypeFacts::EQ_NULL));
1791 }
1792
1793 #[test]
1794 fn binding_id_global_uses_mangled_name() {
1795 let g = BindingId::Global(crate::mangle::package_symbol("main", "x"));
1796 if let BindingId::Global(name) = &g {
1797 assert_eq!(name.as_str(), "main#x");
1798 } else {
1799 panic!("expected Global");
1800 }
1801 }
1802
1803 #[test]
1804 fn narrowed_view_construction() {
1805 let view = NarrowedView {
1806 narrowed_ty: Type::String,
1807 facts: TypeFacts::NE_NULL,
1808 excluded_literals: BTreeSet::new(),
1809 binding: Ident {
1810 name: "#narrow_0".to_string(),
1811 span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1812 },
1813 source: crate::ExprId(0),
1814 };
1815 assert_eq!(view.narrowed_ty, Type::String);
1816 assert!(view.facts.contains(TypeFacts::NE_NULL));
1817 }
1818
1819 #[test]
1820 fn cast_info_holds_kind_and_types() {
1821 let info = CastInfo {
1822 from_ty: Type::Union(vec![Type::String, Type::Null]),
1823 to_ty: Type::String,
1824 cast_kind: CastKind::NonNull,
1825 };
1826 assert!(matches!(info.cast_kind, CastKind::NonNull));
1827 }
1828
1829 #[test]
1830 fn union_discriminant_stub_returns_none() {
1831 assert!(union_discriminant(&[Type::String, Type::Null]).is_none());
1832 }
1833
1834 #[test]
1835 fn intersect_unknown_with_is_string() {
1836 assert_eq!(
1837 intersect_with(&Type::Unknown, TypeFacts::IS_STRING),
1838 Type::String
1839 );
1840 }
1841
1842 #[test]
1843 fn intersect_unknown_with_is_number() {
1844 assert_eq!(
1845 intersect_with(&Type::Unknown, TypeFacts::IS_NUMBER),
1846 Type::Number
1847 );
1848 }
1849
1850 #[test]
1851 fn intersect_unknown_with_is_boolean() {
1852 assert_eq!(
1853 intersect_with(&Type::Unknown, TypeFacts::IS_BOOLEAN),
1854 Type::Boolean
1855 );
1856 }
1857
1858 #[test]
1859 fn intersect_unknown_with_is_array_carries_unknown_element() {
1860 assert_eq!(
1863 intersect_with(&Type::Unknown, TypeFacts::IS_ARRAY),
1864 Type::Array(Box::new(Type::Unknown)),
1865 );
1866 }
1867
1868 #[test]
1869 fn intersect_unknown_with_eq_null() {
1870 assert_eq!(
1871 intersect_with(&Type::Unknown, TypeFacts::EQ_NULL),
1872 Type::Null
1873 );
1874 }
1875
1876 #[test]
1877 fn intersect_unknown_with_truthy_stays_unknown() {
1878 assert_eq!(
1881 intersect_with(&Type::Unknown, TypeFacts::TRUTHY),
1882 Type::Unknown
1883 );
1884 }
1885
1886 #[test]
1887 fn subtract_unknown_stays_unknown() {
1888 assert_eq!(
1891 subtract(&Type::Unknown, TypeFacts::IS_STRING),
1892 Type::Unknown
1893 );
1894 assert_eq!(subtract(&Type::Unknown, TypeFacts::EQ_NULL), Type::Unknown);
1895 assert_eq!(subtract(&Type::Unknown, TypeFacts::TRUTHY), Type::Unknown);
1896 }
1897
1898 #[test]
1899 fn strip_null_unknown_preserves_unknown() {
1900 assert_eq!(strip_null(&Type::Unknown), Type::Unknown);
1901 }
1902
1903 #[test]
1904 fn condition_compatible_rejects_unknown() {
1905 assert!(!condition_compatible(&Type::Unknown));
1906 }
1907
1908 #[test]
1909 fn condition_compatible_accepts_js_truthiness_types() {
1910 for ty in [
1911 Type::Boolean,
1912 Type::String,
1913 Type::Number,
1914 Type::BigInt,
1915 Type::Null,
1916 Type::Array(Box::new(Type::Number)),
1917 Type::union(vec![Type::String, Type::Null]),
1918 Type::union(vec![Type::Number, Type::Void]),
1919 ] {
1920 assert!(condition_compatible(&ty), "{ty:?}");
1921 }
1922 assert!(condition_compatible(&Type::Never));
1923 assert!(!condition_compatible(&Type::Void));
1924 }
1925
1926 #[test]
1927 fn truthiness_class_table() {
1928 use TruthinessClass::*;
1929 let cases = [
1930 (Type::Null, AlwaysFalsy),
1931 (Type::Boolean, BooleanLike),
1932 (Type::Number, NumberLike),
1933 (
1934 Type::NumberLiteral(crate::types::LiteralF64(0.0)),
1935 AlwaysFalsy,
1936 ),
1937 (
1938 Type::NumberLiteral(crate::types::LiteralF64(-0.0)),
1939 AlwaysFalsy,
1940 ),
1941 (
1942 Type::NumberLiteral(crate::types::LiteralF64(3.0)),
1943 AlwaysTruthy,
1944 ),
1945 (Type::String, StringLike),
1946 (Type::StringLiteral(String::new()), AlwaysFalsy),
1947 (Type::StringLiteral("a".to_string()), AlwaysTruthy),
1948 (Type::BigInt, BigIntLike),
1949 (Type::Uint8Array, AlwaysTruthy),
1950 (Type::Array(Box::new(Type::Number)), AlwaysTruthy),
1951 (Type::Unknown, Dynamic),
1952 ];
1953 for (ty, expected) in cases {
1954 assert_eq!(truthiness_class(&ty), expected, "{ty:?}");
1955 }
1956 }
1957
1958 #[test]
1959 fn truthy_part_strips_null_and_falsy_literals() {
1960 assert_eq!(
1961 truthy_part(&Type::union(vec![Type::String, Type::Null])),
1962 Type::String
1963 );
1964 assert_eq!(
1965 truthy_part(&Type::union(vec![
1966 Type::StringLiteral(String::new()),
1967 Type::StringLiteral("a".to_string()),
1968 Type::Null,
1969 ])),
1970 Type::StringLiteral("a".to_string())
1971 );
1972 assert_eq!(truthy_part(&Type::Null), Type::Never);
1973 assert_eq!(truthy_part(&Type::Boolean), Type::BooleanLiteral(true));
1974 assert_eq!(truthy_part(&Type::Number), Type::Number);
1975 }
1976
1977 #[test]
1978 fn falsy_part_keeps_falsy_capable_members() {
1979 assert_eq!(
1980 falsy_part(&Type::union(vec![Type::String, Type::Null])),
1981 Type::union(vec![Type::StringLiteral(String::new()), Type::Null])
1982 );
1983 assert_eq!(falsy_part(&Type::Number), Type::Number);
1984 assert_eq!(falsy_part(&Type::Boolean), Type::BooleanLiteral(false));
1985 assert_eq!(
1987 falsy_part(&Type::union(vec![
1988 Type::Array(Box::new(Type::Number)),
1989 Type::Null
1990 ])),
1991 Type::Null
1992 );
1993 assert_eq!(
1994 falsy_part(&Type::Array(Box::new(Type::Number))),
1995 Type::Never
1996 );
1997 }
1998
1999 #[test]
2000 fn falsy_only_null_distinguishes_reference_unions() {
2001 assert!(falsy_values_are_only_null(&Type::union(vec![
2002 Type::Array(Box::new(Type::Number)),
2003 Type::Null
2004 ])));
2005 assert!(!falsy_values_are_only_null(&Type::union(vec![
2006 Type::String,
2007 Type::Null
2008 ])));
2009 assert!(falsy_values_are_only_null(&Type::Uint8Array));
2010 assert!(!falsy_values_are_only_null(&Type::Number));
2011 }
2012
2013 #[test]
2014 fn intersect_truthy_and_falsy_respect_js_truthiness() {
2015 let string_or_null = Type::union(vec![Type::String, Type::Null]);
2016 assert_eq!(
2017 intersect_with(&string_or_null, TypeFacts::TRUTHY | TypeFacts::NE_NULL),
2018 Type::String
2019 );
2020 assert_eq!(
2022 intersect_with(&string_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2023 string_or_null
2024 );
2025 let arr_or_null = Type::union(vec![Type::Array(Box::new(Type::Number)), Type::Null]);
2026 assert_eq!(
2027 intersect_with(&arr_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2028 Type::Null
2029 );
2030 assert_eq!(
2032 intersect_with(&string_or_null, TypeFacts::NE_NULL),
2033 Type::String
2034 );
2035 }
2036}