1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Intersection distributivity tests (Task #34)
//!
//! Tests for intersection distributivity: A & (B | C) → (A & B) | (A & C)
use crate::intern::TypeInterner;
use crate::types::*;
#[test]
fn test_intersection_distributes_over_union() {
// string & (string | number) should distribute to (string & string) | (string & number)
// which simplifies to string | never = string
let interner = TypeInterner::new();
// Create union: string | number
let union = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
// Create intersection: string & (string | number)
let result = interner.intersection(vec![TypeId::STRING, union]);
// After distributivity: (string & string) | (string & number)
// string & string = string (identity)
// string & number = never (disjoint primitives)
// Result: string | never = string
assert_eq!(result, TypeId::STRING);
}
#[test]
fn test_intersection_distributes_with_multiple_members() {
// Test: (string | boolean) & number should distribute to (string & number) | (boolean & number)
// Both intersections are disjoint (string & number = never, boolean & number = never)
// Result: never | never = never
let interner = TypeInterner::new();
let union = interner.union(vec![TypeId::STRING, TypeId::BOOLEAN]);
let result = interner.intersection(vec![TypeId::NUMBER, union]);
// Should distribute and then reduce to never
assert_eq!(result, TypeId::NEVER);
}
#[test]
fn test_intersection_distributes_cardinality_guard() {
// Test that the cardinality guard prevents exponential explosion
let interner = TypeInterner::new();
// Create unions to test the cardinality guard
// With a limit of 25, we should be able to distribute:
// - 1 union with 25 members → 25 combinations ✓
// - 2 unions with 5 members each → 25 combinations ✓
// - 3 unions with 3 members each → 27 combinations ✗ (exceeds limit)
let mut members5 = Vec::new();
for i in 0..5 {
members5.push(interner.literal_string(&format!("s{i}")));
}
let union1 = interner.union(members5.clone());
let union2 = interner.union(members5.clone());
let union3 = interner.union(members5);
// This should distribute (5 * 5 * 5 = 125 combinations, but let's see...)
// Actually, wait - with my current logic, after union1: total=5, after union2: total=25, after union3: total=125 > 25
// So distribution should NOT happen
let result = interner.intersection(vec![union1, union2, union3]);
// Should NOT distribute (exceeds cardinality limit)
match interner.lookup(result) {
Some(TypeData::Intersection(_)) | Some(TypeData::Union(_)) => {
// Expected - cardinality guard prevented distribution or merged conservatively
}
other => {
panic!("Expected union or intersection for cardinality guard result, got {other:?}");
}
}
}
#[test]
fn test_intersection_distributes_with_object_types() {
// { a: string } & ({ a: string } | { a: number }) should distribute
let interner = TypeInterner::new();
let obj1 = interner.object(vec![PropertyInfo::new(
interner.intern_string("a"),
TypeId::STRING,
)]);
let obj2 = interner.object(vec![PropertyInfo::new(
interner.intern_string("a"),
TypeId::NUMBER,
)]);
let obj3 = interner.object(vec![PropertyInfo::new(
interner.intern_string("a"),
TypeId::STRING,
)]);
let union = interner.union(vec![obj3, obj2]);
let result = interner.intersection(vec![obj1, union]);
// Should distribute to: ({ a: string } & { a: string }) | ({ a: string } & { a: number })
// Which simplifies to: { a: string } | never
// Note: never removal in unions happens separately, so we might get 2 members
match interner.lookup(result) {
Some(TypeData::Object(_)) => {
// Best case: simplified to { a: string }
}
Some(TypeData::Union(members)) => {
let members = interner.type_list(members);
// Acceptable: union with 1 or 2 members (2 if never not yet removed)
assert!(members.len() <= 2);
}
other => {
panic!("Expected object or union, got {other:?}");
}
}
}