tsz-solver 0.1.8

TypeScript type solver for the tsz compiler
Documentation
use super::*;
use crate::TypeInterner;
use crate::def::DefId;
use crate::visitor::application_id;

fn atom_names(interner: &TypeInterner, atoms: &[tsz_common::interner::Atom]) -> Vec<String> {
    let mut names: Vec<String> = atoms
        .iter()
        .map(|atom| interner.resolve_atom(*atom))
        .collect();
    names.sort();
    names
}

#[test]
fn test_try_evaluate_mapped_constraint_keyof_object() {
    let interner = TypeInterner::new();
    let checker = SubtypeChecker::new(&interner);

    let obj = interner.object(vec![
        PropertyInfo::new(interner.intern_string("a"), TypeId::NUMBER),
        PropertyInfo::new(interner.intern_string("b"), TypeId::STRING),
    ]);

    let constraint = interner.intern(TypeData::KeyOf(obj));
    let keys = checker
        .try_evaluate_mapped_constraint(constraint)
        .expect("expected concrete keys");

    assert_eq!(atom_names(&interner, &keys), vec!["a", "b"]);
}

#[test]
fn test_try_evaluate_mapped_constraint_string_literal() {
    let interner = TypeInterner::new();
    let checker = SubtypeChecker::new(&interner);

    let constraint = interner.literal_string("k");
    let keys = checker
        .try_evaluate_mapped_constraint(constraint)
        .expect("expected literal key");

    assert_eq!(atom_names(&interner, &keys), vec!["k"]);
}

#[test]
fn test_try_evaluate_mapped_constraint_union_with_non_literal_member() {
    let interner = TypeInterner::new();
    let checker = SubtypeChecker::new(&interner);

    let constraint = interner.union(vec![
        interner.literal_string("only"),
        interner.literal_number(1.0),
    ]);
    let keys = checker
        .try_evaluate_mapped_constraint(constraint)
        .expect("expected partial literal keys");

    assert_eq!(atom_names(&interner, &keys), vec!["only"]);
}

#[test]
fn test_try_get_keyof_keys_object_with_index_returns_properties() {
    let interner = TypeInterner::new();
    let checker = SubtypeChecker::new(&interner);

    let obj = interner.object_with_index(ObjectShape {
        symbol: None,
        flags: ObjectFlags::empty(),
        properties: vec![PropertyInfo::new(
            interner.intern_string("x"),
            TypeId::NUMBER,
        )],
        string_index: Some(IndexSignature {
            key_type: TypeId::STRING,
            value_type: TypeId::STRING,
            readonly: false,
        }),
        number_index: None,
    });

    let keys = checker
        .try_get_keyof_keys(obj)
        .expect("expected property keys");
    assert_eq!(atom_names(&interner, &keys), vec!["x"]);
}

#[test]
fn test_try_get_keyof_keys_empty_object_returns_none() {
    let interner = TypeInterner::new();
    let checker = SubtypeChecker::new(&interner);

    let obj = interner.object(vec![]);
    assert!(checker.try_get_keyof_keys(obj).is_none());
}

#[test]
fn test_try_get_keyof_keys_resolves_reference() {
    let interner = TypeInterner::new();

    let def_id = DefId(10);
    let resolved = interner.object(vec![PropertyInfo::new(
        interner.intern_string("value"),
        TypeId::STRING,
    )]);

    let mut env = TypeEnvironment::new();
    env.insert_def(def_id, resolved);
    let checker = SubtypeChecker::with_resolver(&interner, &env);

    let ref_type = interner.lazy(def_id);
    let keys = checker
        .try_get_keyof_keys(ref_type)
        .expect("expected resolved keys");

    assert_eq!(atom_names(&interner, &keys), vec!["value"]);
}

#[test]
fn test_try_expand_application_instantiates_type_params() {
    let interner = TypeInterner::new();

    let param_info = TypeParamInfo {
        name: interner.intern_string("T"),
        constraint: None,
        default: None,
        is_const: false,
    };
    let param_type = interner.intern(TypeData::TypeParameter(param_info.clone()));

    let def_id = DefId(20);
    let box_struct = interner.object(vec![PropertyInfo::new(
        interner.intern_string("value"),
        param_type,
    )]);

    let mut env = TypeEnvironment::new();
    env.insert_def_with_params(def_id, box_struct, vec![param_info]);
    let mut checker = SubtypeChecker::with_resolver(&interner, &env);

    let base_ref = interner.lazy(def_id);
    let app_type = interner.application(base_ref, vec![TypeId::STRING]);
    let app_id = application_id(&interner, app_type).expect("expected app id");

    let expanded = checker
        .try_expand_application(app_id)
        .expect("expected expanded application");

    let Some(TypeData::Object(shape_id)) = interner.lookup(expanded) else {
        panic!("expected expanded object type");
    };
    let shape = interner.object_shape(shape_id);
    let value_name = interner.intern_string("value");
    let value_prop = shape
        .properties
        .iter()
        .find(|prop| prop.name == value_name)
        .expect("expected value property");

    assert_eq!(value_prop.type_id, TypeId::STRING);
}

#[test]
fn test_try_expand_application_non_ref_base_returns_none() {
    let interner = TypeInterner::new();
    let mut checker = SubtypeChecker::new(&interner);

    let base = interner.object(vec![]);
    let app_type = interner.application(base, vec![TypeId::STRING]);
    let app_id = application_id(&interner, app_type).expect("expected app id");

    assert!(checker.try_expand_application(app_id).is_none());
}

#[test]
fn test_try_expand_application_self_reference_returns_none() {
    let interner = TypeInterner::new();

    let symbol = SymbolRef(30);
    let base_ref = interner.reference(symbol);
    let app_type = interner.application(base_ref, vec![TypeId::STRING]);
    let app_id = application_id(&interner, app_type).expect("expected app id");

    let mut env = TypeEnvironment::new();
    env.insert_with_params(
        symbol,
        app_type,
        vec![TypeParamInfo {
            name: interner.intern_string("T"),
            constraint: None,
            default: None,
            is_const: false,
        }],
    );

    let mut checker = SubtypeChecker::with_resolver(&interner, &env);
    assert!(checker.try_expand_application(app_id).is_none());
}