tsz-solver 0.1.8

TypeScript type solver for the tsz compiler
Documentation
//! 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:?}");
        }
    }
}