crisp-typeck 1.7.2

HM-style type inference and constraint solving (spec §3.4)
Documentation
//! User-facing generics + parametric shapes (#70 / #71).

use crisp_typeck::{TypeChecker, TypeError, format_sig};
use std::path::PathBuf;

fn generics_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/generics")
}

fn shapes_generic_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/shapes_generic")
}

fn shapes_user_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/shapes_user")
}

fn generics_implicit_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/generics_implicit")
}

fn generics_pub_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/generics_pub")
}

fn fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name)
}

fn sig_named<'a>(typed: &'a crisp_typeck::TypedCrate, name: &str) -> &'a crisp_typeck::InferredSig {
    typed
        .signatures
        .values()
        .find(|s| s.name == name)
        .unwrap_or_else(|| panic!("missing signature {name}"))
}

#[test]
fn generics_example_typechecks() {
    let typed = TypeChecker::check_crate(&generics_root()).expect("typecheck generics");
    let id = format_sig(sig_named(&typed, "id"));
    let first = format_sig(sig_named(&typed, "first"));
    let second = format_sig(sig_named(&typed, "second"));
    let unwrap_int = format_sig(sig_named(&typed, "unwrap_int"));
    let unwrap_str = format_sig(sig_named(&typed, "unwrap_str"));
    eprintln!("id: {id}");
    eprintln!("first: {first}");
    eprintln!("second: {second}");
    eprintln!("unwrap_int: {unwrap_int}");
    eprintln!("unwrap_str: {unwrap_str}");
    assert_eq!(id, "id<T: Clone>(x: T) -> T");
    assert_eq!(first, "first<A: Clone, B: Clone>(p: Pair<A, B>) -> A");
    assert_eq!(second, "second<A: Clone, B: Clone>(p: Pair<A, B>) -> B");
    assert_eq!(unwrap_int, "unwrap_int(b: Boxy<int>) -> int");
    assert_eq!(unwrap_str, "unwrap_str(b: Boxy<str>) -> str");
    let mut trait_unwraps: Vec<_> = typed
        .signatures
        .values()
        .filter(|s| s.name == "unwrap")
        .map(|s| (s.impl_ty.clone(), format_sig(s)))
        .collect();
    trait_unwraps.sort();
    assert!(
        trait_unwraps.iter().any(
            |(ty, sig)| ty.as_deref() == Some("IntBox") && sig == "unwrap(self: IntBox) -> int"
        ),
        "expected IntBox unwrap, got {trait_unwraps:?}"
    );
    assert!(
        trait_unwraps.iter().any(
            |(ty, sig)| ty.as_deref() == Some("StrBox") && sig == "unwrap(self: StrBox) -> str"
        ),
        "expected StrBox unwrap, got {trait_unwraps:?}"
    );
}

#[test]
fn shapes_generic_example_typechecks() {
    let typed = TypeChecker::check_crate(&shapes_generic_root()).expect("typecheck shapes_generic");
    assert_eq!(
        format_sig(sig_named(&typed, "unwrap_int")),
        "unwrap_int(b: Boxy<int>) -> int"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "unwrap_str")),
        "unwrap_str(b: Boxy<str>) -> str"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "distance")),
        "distance<T: Clone + Copy + Add + Mul + Sub>(a: HasPosition<T>, b: HasPosition<T>) -> T"
    );
}

#[test]
fn shapes_user_example_typechecks() {
    let typed = TypeChecker::check_crate(&shapes_user_root()).expect("typecheck shapes_user");
    assert_eq!(
        format_sig(sig_named(&typed, "distance")),
        "distance<T: Clone + Measure>(a: HasPosition<T>, b: HasPosition<T>) -> T"
    );
}

#[test]
fn measure_instantiation_rejects_missing_impl() {
    let err = TypeChecker::check_crate(&fixture("measure_bound_mismatch"))
        .expect_err("distance on Label coordinates must fail in typeck");
    let msg = err.to_string();
    eprintln!("measure bound: {msg}");
    assert!(
        matches!(err, TypeError::UnsatisfiedBound { .. }) || msg.contains("E0084"),
        "{msg}"
    );
    assert!(msg.contains("Measure"), "{msg}");
}

#[test]
fn arith_instantiation_rejects_non_numeric_t() {
    let err = TypeChecker::check_crate(&fixture("arith_bound_mismatch"))
        .expect_err("distance on str coordinates must fail in typeck");
    let msg = err.to_string();
    eprintln!("arith bound: {msg}");
    assert!(
        matches!(err, TypeError::UnsatisfiedBound { .. }) || msg.contains("E0084"),
        "{msg}"
    );
    assert!(msg.contains("distance"), "{msg}");
    assert!(msg.contains("str"), "{msg}");
}

#[test]
fn unannotated_arith_infers_add_bound() {
    let math = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/math");
    let typed = TypeChecker::check_crate(&math).expect("typecheck math");
    assert_eq!(
        format_sig(sig_named(&typed, "double")),
        "double<T: Clone + Copy + Add>(n: T) -> T"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "sum")),
        "sum<T: Clone + Copy + Add>(a: T, b: T) -> T"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "product")),
        "product<T: Clone + Copy + Mul>(a: T, b: T) -> T"
    );
}

#[test]
fn add_instantiation_rejects_str() {
    let err = TypeChecker::check_crate(&fixture("arith_add_str"))
        .expect_err("add(\"a\", \"b\") must fail in typeck");
    let msg = err.to_string();
    eprintln!("add str: {msg}");
    assert!(
        matches!(err, TypeError::UnsatisfiedBound { .. }) || msg.contains("E0084"),
        "{msg}"
    );
}

#[test]
fn trait_instantiation_rejects_missing_impl() {
    let err = TypeChecker::check_crate(&fixture("trait_bound_mismatch"))
        .expect_err("label(Item) must fail without impl Show");
    let msg = err.to_string();
    eprintln!("trait bound: {msg}");
    assert!(
        matches!(err, TypeError::UnsatisfiedBound { .. }) || msg.contains("E0084"),
        "{msg}"
    );
    assert!(msg.contains("Show"), "{msg}");
}

#[test]
fn parametric_shape_rejects_wrong_field_type() {
    let err = TypeChecker::check_crate(&fixture("shape_generic_mismatch"))
        .expect_err("StrBox must not satisfy Boxy<int>");
    let msg = err.to_string();
    eprintln!("mismatch: {msg}");
    assert!(
        matches!(err, TypeError::Unify(_)) || msg.contains("shape") || msg.contains("mismatch"),
        "{msg}"
    );
}

#[test]
fn free_type_names_typecheck_like_explicit_binders() {
    let typed =
        TypeChecker::check_crate(&generics_implicit_root()).expect("typecheck generics_implicit");
    assert_eq!(
        format_sig(sig_named(&typed, "id")),
        "id<T: Clone>(x: T) -> T"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "first")),
        "first<A: Clone, B: Clone>(p: Pair<A, B>) -> A"
    );
    assert_eq!(
        format_sig(sig_named(&typed, "unwrap_int")),
        "unwrap_int(b: Boxy<int>) -> int"
    );
    assert!(
        typed
            .impl_trait_args
            .values()
            .any(|args| matches!(args.first(), Some(crisp_typeck::Ty::Int))),
        "expected inferred Wrapper<int>, got {:?}",
        typed.impl_trait_args
    );
    assert!(
        typed
            .impl_trait_args
            .values()
            .any(|args| matches!(args.first(), Some(crisp_typeck::Ty::Str))),
        "expected inferred Wrapper<str>, got {:?}",
        typed.impl_trait_args
    );
}

#[test]
fn explicit_binder_shadowing_type_is_error() {
    let err = TypeChecker::check_crate(&fixture("generic_shadows_type"))
        .expect_err("<T> must not shadow type T");
    let msg = err.to_string();
    eprintln!("shadow: {msg}");
    assert!(msg.contains("E0049") || msg.contains("shadow"), "{msg}");
}

#[test]
fn in_scope_type_is_not_a_parameter() {
    let typed = TypeChecker::check_crate(&fixture("in_scope_type_not_param"))
        .expect("T in scope is the struct");
    assert_eq!(format_sig(sig_named(&typed, "id")), "id(x: T) -> T");
    let err = TypeChecker::check_crate(&fixture("in_scope_type_rejects_int"))
        .expect_err("id(1) must not instantiate in-scope type T");
    let msg = err.to_string();
    eprintln!("in-scope T vs int: {msg}");
    assert!(
        matches!(err, TypeError::Unify(_)) || msg.contains("mismatch"),
        "{msg}"
    );
}

#[test]
fn rigid_param_rejects_concrete_return() {
    let err = TypeChecker::check_crate(&fixture("generic_return_mismatch"))
        .expect_err("id<T>(x: T) -> int must not typecheck");
    let msg = err.to_string();
    eprintln!("rigid return: {msg}");
    assert!(
        matches!(err, TypeError::Unify(_)) || msg.contains("mismatch") || msg.contains("T"),
        "{msg}"
    );
}

#[test]
fn publication_generalizes_unannotated_and_specializes_internal() {
    let typed = TypeChecker::check_crate(&generics_pub_root()).expect("typecheck generics_pub");
    let id = sig_named(&typed, "id");
    let once = sig_named(&typed, "once");
    let identity = sig_named(&typed, "identity");
    eprintln!("id: {}", format_sig(id));
    eprintln!("once: {}", format_sig(once));
    eprintln!("identity: {}", format_sig(identity));
    assert_eq!(format_sig(id), "id<T: Clone>(x: T) -> T");
    assert!(
        id.instantiations.iter().any(|s| s.contains("int"))
            && id.instantiations.iter().any(|s| s.contains("str")),
        "id instantiations: {:?}",
        id.instantiations
    );
    assert!(id.mono_args.is_none(), "id used at two types stays generic");
    assert_eq!(format_sig(once), "once<T: Clone>(x: T) -> T");
    assert!(
        once.mono_args.is_some(),
        "once used only at int is marked for mono emit"
    );
    assert_eq!(format_sig(identity), "identity<T: Clone>(x: T) -> T");
    assert!(identity.is_pub);
    assert!(
        identity.mono_args.is_none(),
        "pub identity is never specialized"
    );
}

#[test]
fn mut_binding_is_not_generalized() {
    let err = TypeChecker::check_crate(&fixture("mut_not_generalized"))
        .expect_err("mut f := id must pin after first use");
    let msg = err.to_string();
    eprintln!("value restriction: {msg}");
    assert!(
        matches!(err, TypeError::Unify(_)) || msg.contains("mismatch"),
        "{msg}"
    );
}

#[test]
fn closures_example_typechecks() {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/closures");
    let typed = TypeChecker::check_crate(&root).expect("typecheck closures");
    eprintln!("apply: {}", format_sig(sig_named(&typed, "apply")));
    eprintln!("run: {}", format_sig(sig_named(&typed, "run")));
}

#[test]
fn hole_arity_is_e0085() {
    let err = TypeChecker::check_crate(&fixture("hole_arity")).expect_err("two holes vs one param");
    let msg = err.to_string();
    eprintln!("hole arity: {msg}");
    assert!(
        matches!(err, TypeError::HoleArity { .. }) || msg.contains("E0085"),
        "{msg}"
    );
}

#[test]
fn misplaced_hole_is_e0086() {
    let err = TypeChecker::check_crate(&fixture("hole_misplaced")).expect_err("hole in print arg");
    let msg = err.to_string();
    eprintln!("hole misplaced: {msg}");
    assert!(
        matches!(err, TypeError::HoleMisplaced { .. }) || msg.contains("E0086"),
        "{msg}"
    );
}