use std::cmp::Ordering;
use std::collections::BTreeSet;
use brink_format::DefinitionId;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Ty {
Int,
Float,
Bool,
String,
Content,
Divert,
List(String),
Array(Box<Ty>),
Map(Box<Ty>, Box<Ty>),
Struct(String),
Fn(Vec<Ty>, Box<Ty>, FnRow),
Handle(String),
Option(Box<Ty>),
Range {
non_empty: bool,
},
Weighted(Box<Ty>),
Tower(TowerTy),
Unknown,
Conflicted,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[expect(
clippy::box_collection,
reason = "the box is the point: it keeps `Ty::Fn` one pointer wider \
instead of three words wider, and the unknown top element — \
which is what almost every `Ty::Fn` carries — costs nothing \
at all through the `Option` niche"
)]
pub struct FnRow(Option<Box<BTreeSet<DefinitionId>>>);
impl FnRow {
#[must_use]
pub fn unknown() -> Self {
FnRow(None)
}
#[must_use]
pub fn of_target(target: DefinitionId) -> Self {
FnRow(Some(Box::new(BTreeSet::from([target]))))
}
#[must_use]
pub fn empty() -> Self {
FnRow(Some(Box::new(BTreeSet::new())))
}
#[must_use]
pub fn is_unknown(&self) -> bool {
self.0.is_none()
}
#[must_use]
pub fn targets(&self) -> Option<&BTreeSet<DefinitionId>> {
self.0.as_deref()
}
#[must_use]
pub fn join(&self, other: &FnRow) -> FnRow {
match (&self.0, &other.0) {
(Some(a), Some(b)) => FnRow(Some(Box::new(a.union(b).copied().collect()))),
_ => FnRow::unknown(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TowerTy {
Vec2,
Vec3,
Vec4,
Quat,
Mat2,
Mat3,
Mat4,
}
impl TowerTy {
#[must_use]
pub fn name(self) -> &'static str {
match self {
TowerTy::Vec2 => "vec2",
TowerTy::Vec3 => "vec3",
TowerTy::Vec4 => "vec4",
TowerTy::Quat => "quat",
TowerTy::Mat2 => "mat2",
TowerTy::Mat3 => "mat3",
TowerTy::Mat4 => "mat4",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"vec2" => Some(TowerTy::Vec2),
"vec3" => Some(TowerTy::Vec3),
"vec4" => Some(TowerTy::Vec4),
"quat" => Some(TowerTy::Quat),
"mat2" => Some(TowerTy::Mat2),
"mat3" => Some(TowerTy::Mat3),
"mat4" => Some(TowerTy::Mat4),
_ => None,
}
}
}
impl Default for Ty {
fn default() -> Self {
Ty::Unknown
}
}
impl Ty {
#[must_use]
pub fn display(&self) -> String {
match self {
Ty::Int => "int".to_string(),
Ty::Float => "float".to_string(),
Ty::Bool => "bool".to_string(),
Ty::String => "string".to_string(),
Ty::Content => "content".to_string(),
Ty::Divert => "divert".to_string(),
Ty::List(name) => format!("List<{name}>"),
Ty::Array(elem) => format!("Array<{}>", elem.display()),
Ty::Map(k, v) => format!("Map<{}, {}>", k.display(), v.display()),
Ty::Struct(name) => name.clone(),
Ty::Fn(params, ret, _) => {
let row = params
.iter()
.map(Ty::display)
.collect::<Vec<_>>()
.join(", ");
format!("fn({row}): {}", ret.display())
}
Ty::Handle(kind) => format!("Handle<{kind}>"),
Ty::Option(elem) => format!("Option<{}>", elem.display()),
Ty::Range { non_empty: false } => "range".to_string(),
Ty::Range { non_empty: true } => "NonEmptyRange".to_string(),
Ty::Tower(kind) => kind.name().to_string(),
Ty::Weighted(elem) => format!("Weighted<{}>", elem.display()),
Ty::Unknown => "Unknown".to_string(),
Ty::Conflicted => "Conflicted".to_string(),
}
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, Ty::Unknown)
}
#[must_use]
pub fn is_conflicted(&self) -> bool {
matches!(self, Ty::Conflicted)
}
#[must_use]
pub fn is_unresolved(&self) -> bool {
matches!(self, Ty::Unknown | Ty::Conflicted)
}
#[must_use]
pub fn is_numeric(&self) -> bool {
matches!(self, Ty::Int | Ty::Float)
}
}
impl PartialOrd for Ty {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Ty {
fn cmp(&self, other: &Self) -> Ordering {
fn rank(t: &Ty) -> u8 {
match t {
Ty::Int => 0,
Ty::Float => 1,
Ty::Bool => 2,
Ty::String => 3,
Ty::Divert => 4,
Ty::List(_) => 5,
Ty::Array(_) => 6,
Ty::Map(_, _) => 7,
Ty::Struct(_) => 8,
Ty::Fn(..) => 9,
Ty::Handle(_) => 10,
Ty::Option(_) => 11,
Ty::Range { .. } => 12,
Ty::Tower(_) => 13,
Ty::Weighted(_) => 14,
Ty::Content => 15,
Ty::Unknown => 16,
Ty::Conflicted => 17,
}
}
match (self, other) {
(Ty::List(a), Ty::List(b))
| (Ty::Struct(a), Ty::Struct(b))
| (Ty::Handle(a), Ty::Handle(b)) => a.cmp(b),
(Ty::Array(a), Ty::Array(b))
| (Ty::Option(a), Ty::Option(b))
| (Ty::Weighted(a), Ty::Weighted(b)) => a.cmp(b),
(Ty::Range { non_empty: a }, Ty::Range { non_empty: b }) => a.cmp(b),
(Ty::Tower(a), Ty::Tower(b)) => a.cmp(b),
(Ty::Map(k1, v1), Ty::Map(k2, v2)) => k1.cmp(k2).then_with(|| v1.cmp(v2)),
(Ty::Fn(p1, r1, e1), Ty::Fn(p2, r2, e2)) => {
p1.cmp(p2).then_with(|| r1.cmp(r2)).then_with(|| e1.cmp(e2))
}
_ => rank(self).cmp(&rank(other)),
}
}
}
#[must_use]
pub fn unify(a: &Ty, b: &Ty) -> Ty {
match (a, b) {
(Ty::Unknown, x) | (x, Ty::Unknown) => x.clone(),
(Ty::Conflicted, _) | (_, Ty::Conflicted) => Ty::Conflicted,
(x, y) if x == y => x.clone(),
(Ty::Int, Ty::Float) | (Ty::Float, Ty::Int) => Ty::Float,
(Ty::Array(x), Ty::Array(y)) => Ty::Array(Box::new(unify(x, y))),
(Ty::Option(x), Ty::Option(y)) => Ty::Option(Box::new(unify(x, y))),
(Ty::Weighted(x), Ty::Weighted(y)) => Ty::Weighted(Box::new(unify(x, y))),
(Ty::Range { non_empty: a }, Ty::Range { non_empty: b }) => Ty::Range {
non_empty: *a && *b,
},
(Ty::Map(k1, v1), Ty::Map(k2, v2)) => {
Ty::Map(Box::new(unify(k1, k2)), Box::new(unify(v1, v2)))
}
(Ty::Fn(p1, r1, e1), Ty::Fn(p2, r2, e2)) if p1.len() == p2.len() => Ty::Fn(
p1.iter().zip(p2).map(|(x, y)| unify(x, y)).collect(),
Box::new(unify(r1, r2)),
e1.join(e2),
),
_ => Ty::Conflicted,
}
}
#[must_use]
pub fn unify_all(tys: impl IntoIterator<Item = Ty>) -> Ty {
tys.into_iter().fold(Ty::Unknown, |acc, t| unify(&acc, &t))
}
#[must_use]
pub fn erase_fn_rows(ty: &Ty) -> Ty {
match ty {
Ty::Fn(params, ret, _) => Ty::Fn(
params.iter().map(erase_fn_rows).collect(),
Box::new(erase_fn_rows(ret)),
FnRow::unknown(),
),
Ty::Array(elem) => Ty::Array(Box::new(erase_fn_rows(elem))),
Ty::Option(elem) => Ty::Option(Box::new(erase_fn_rows(elem))),
Ty::Weighted(elem) => Ty::Weighted(Box::new(erase_fn_rows(elem))),
Ty::Map(k, v) => Ty::Map(Box::new(erase_fn_rows(k)), Box::new(erase_fn_rows(v))),
Ty::Int
| Ty::Float
| Ty::Bool
| Ty::String
| Ty::Content
| Ty::Divert
| Ty::List(_)
| Ty::Struct(_)
| Ty::Handle(_)
| Ty::Range { .. }
| Ty::Tower(_)
| Ty::Unknown
| Ty::Conflicted => ty.clone(),
}
}
#[must_use]
pub fn assignable(target: &Ty, source: &Ty) -> bool {
erase_fn_rows(&unify(target, source)) == erase_fn_rows(target)
}
#[must_use]
pub fn ref_assignable(target: &Ty, source: &Ty) -> bool {
invariant_eq(&erase_fn_rows(target), &erase_fn_rows(source))
}
fn invariant_eq(a: &Ty, b: &Ty) -> bool {
match (a, b) {
(Ty::Unknown, _) | (_, Ty::Unknown) => true,
(Ty::Array(x), Ty::Array(y))
| (Ty::Option(x), Ty::Option(y))
| (Ty::Weighted(x), Ty::Weighted(y)) => invariant_eq(x, y),
(Ty::Map(k1, v1), Ty::Map(k2, v2)) => invariant_eq(k1, k2) && invariant_eq(v1, v2),
(Ty::Fn(p1, r1, e1), Ty::Fn(p2, r2, e2)) => {
e1 == e2
&& p1.len() == p2.len()
&& p1.iter().zip(p2.iter()).all(|(x, y)| invariant_eq(x, y))
&& invariant_eq(r1, r2)
}
_ => a == b,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoalesceError {
LeftNotOption(Ty),
Mismatch { element: Ty, fallback: Ty },
}
pub fn coalesce(lhs: &Ty, rhs: &Ty) -> Result<Ty, CoalesceError> {
match (lhs, rhs) {
(Ty::Option(elem), Ty::Option(relem)) => Ok(Ty::Option(Box::new(unify(elem, relem)))),
(Ty::Option(elem), _) => {
let joined = unify(elem, rhs);
if joined.is_conflicted() && !elem.is_conflicted() && !rhs.is_conflicted() {
Err(CoalesceError::Mismatch {
element: (**elem).clone(),
fallback: rhs.clone(),
})
} else {
Ok(joined)
}
}
(Ty::Unknown, _) => Ok(Ty::Unknown),
(Ty::Conflicted, _) => Ok(Ty::Conflicted),
(other, _) => Err(CoalesceError::LeftNotOption(other.clone())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_is_identity() {
assert_eq!(unify(&Ty::Unknown, &Ty::Int), Ty::Int);
assert_eq!(unify(&Ty::Int, &Ty::Unknown), Ty::Int);
assert_eq!(unify(&Ty::Unknown, &Ty::Unknown), Ty::Unknown);
}
#[test]
fn int_float_join_is_directional_to_float() {
assert_eq!(unify(&Ty::Int, &Ty::Float), Ty::Float);
assert_eq!(unify(&Ty::Float, &Ty::Int), Ty::Float);
}
#[test]
fn equal_types_join_to_themselves() {
assert_eq!(unify(&Ty::String, &Ty::String), Ty::String);
assert_eq!(
unify(&Ty::List("Weathers".into()), &Ty::List("Weathers".into())),
Ty::List("Weathers".into())
);
}
#[test]
fn content_joins_with_itself() {
assert_eq!(unify(&Ty::Content, &Ty::Content), Ty::Content);
}
#[test]
fn content_never_coerces_to_or_from_string() {
assert_eq!(unify(&Ty::Content, &Ty::String), Ty::Conflicted);
assert_eq!(unify(&Ty::String, &Ty::Content), Ty::Conflicted);
}
#[test]
fn content_is_assignable_only_to_content() {
assert!(assignable(&Ty::Content, &Ty::Content));
assert!(!assignable(&Ty::Content, &Ty::String));
assert!(!assignable(&Ty::String, &Ty::Content));
}
#[test]
fn structural_mismatch_yields_conflicted_not_unknown() {
assert_eq!(unify(&Ty::Int, &Ty::String), Ty::Conflicted);
assert_eq!(unify(&Ty::Bool, &Ty::Divert), Ty::Conflicted);
}
#[test]
fn conflicted_absorbs_unknown_and_everything_else() {
assert_eq!(unify(&Ty::Conflicted, &Ty::Unknown), Ty::Conflicted);
assert_eq!(unify(&Ty::Unknown, &Ty::Conflicted), Ty::Conflicted);
assert_eq!(unify(&Ty::Conflicted, &Ty::Int), Ty::Conflicted);
assert_eq!(unify(&Ty::Int, &Ty::Conflicted), Ty::Conflicted);
assert_eq!(unify(&Ty::Conflicted, &Ty::Conflicted), Ty::Conflicted);
}
#[test]
fn conflict_detection_is_order_independent() {
let orderings: [&[Ty]; 6] = [
&[Ty::Int, Ty::String, Ty::Int],
&[Ty::String, Ty::Int, Ty::Int],
&[Ty::Int, Ty::Int, Ty::String],
&[Ty::String, Ty::Int, Ty::Int],
&[Ty::Int, Ty::String, Ty::Int],
&[Ty::String, Ty::String, Ty::Int],
];
for ordering in orderings {
assert_eq!(
unify_all(ordering.iter().cloned()),
Ty::Conflicted,
"order {ordering:?} must detect the conflict"
);
}
}
#[test]
fn conflict_detection_survives_unknown_interleaving_in_any_order() {
let orderings: [&[Ty]; 4] = [
&[Ty::Unknown, Ty::Int, Ty::Unknown, Ty::String],
&[Ty::Int, Ty::Unknown, Ty::String, Ty::Unknown],
&[Ty::String, Ty::Unknown, Ty::Unknown, Ty::Int],
&[Ty::Unknown, Ty::Unknown, Ty::String, Ty::Int],
];
for ordering in orderings {
assert_eq!(unify_all(ordering.iter().cloned()), Ty::Conflicted);
}
}
#[test]
fn array_and_map_join_recursively() {
assert_eq!(
unify(
&Ty::Array(Box::new(Ty::Int)),
&Ty::Array(Box::new(Ty::Float))
),
Ty::Array(Box::new(Ty::Float))
);
assert_eq!(
unify(
&Ty::Map(Box::new(Ty::Int), Box::new(Ty::String)),
&Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::String))
),
Ty::Map(Box::new(Ty::Int), Box::new(Ty::String))
);
}
#[test]
fn unify_all_folds_left_to_right_from_unknown() {
assert_eq!(unify_all([Ty::Int, Ty::Float]), Ty::Float);
assert_eq!(unify_all(Vec::<Ty>::new()), Ty::Unknown);
assert_eq!(unify_all([Ty::Bool]), Ty::Bool);
}
fn fn_ty(params: &[Ty], ret: Ty) -> Ty {
Ty::Fn(params.to_vec(), Box::new(ret), FnRow::unknown())
}
fn def(n: u64) -> DefinitionId {
DefinitionId::new(brink_format::DefinitionTag::Address, n)
}
fn fn_ty_from(params: &[Ty], ret: Ty, targets: &[u64]) -> Ty {
let row = targets.iter().fold(FnRow::empty(), |acc, &t| {
acc.join(&FnRow::of_target(def(t)))
});
Ty::Fn(params.to_vec(), Box::new(ret), row)
}
#[test]
fn fn_unifies_pointwise_including_the_directional_numeric_join() {
assert_eq!(
unify(&fn_ty(&[Ty::Int], Ty::Int), &fn_ty(&[Ty::Float], Ty::Int)),
fn_ty(&[Ty::Float], Ty::Int)
);
assert_eq!(
unify(
&fn_ty(&[Ty::String], Ty::Int),
&fn_ty(&[Ty::String], Ty::Float)
),
fn_ty(&[Ty::String], Ty::Float)
);
}
#[test]
fn fn_unknown_row_slots_absorb_concrete_ones() {
assert_eq!(
unify(
&fn_ty(&[Ty::Unknown], Ty::Unknown),
&fn_ty(&[Ty::Int], Ty::Bool)
),
fn_ty(&[Ty::Int], Ty::Bool)
);
}
#[test]
fn fn_arity_mismatch_is_conflicted() {
assert_eq!(
unify(&fn_ty(&[Ty::Int], Ty::Int), &fn_ty(&[], Ty::Int)),
Ty::Conflicted
);
}
#[test]
fn fn_vs_other_concrete_is_conflicted_per_the_627_lattice() {
assert_eq!(unify(&fn_ty(&[], Ty::Int), &Ty::Int), Ty::Conflicted);
assert_eq!(unify(&Ty::String, &fn_ty(&[], Ty::Int)), Ty::Conflicted);
assert_eq!(
unify(&fn_ty(&[], Ty::Int), &Ty::Array(Box::new(Ty::Int))),
Ty::Conflicted
);
}
#[test]
fn fn_conflict_inside_the_row_stays_inside_the_row() {
assert_eq!(
unify(&fn_ty(&[Ty::Int], Ty::Int), &fn_ty(&[Ty::String], Ty::Int)),
fn_ty(&[Ty::Conflicted], Ty::Int)
);
}
#[test]
fn fn_row_join_is_union_with_unknown_absorbing() {
let a = FnRow::of_target(def(1));
let b = FnRow::of_target(def(2));
let ab = a.join(&b);
assert_eq!(
ab.targets().map(BTreeSet::len),
Some(2),
"two creation sites union"
);
assert!(ab.targets().is_some_and(|t| t.contains(&def(1))));
assert!(ab.targets().is_some_and(|t| t.contains(&def(2))));
assert!(a.join(&FnRow::unknown()).is_unknown());
assert!(FnRow::unknown().join(&a).is_unknown());
assert!(FnRow::unknown().join(&FnRow::unknown()).is_unknown());
assert_eq!(FnRow::empty().join(&a), a);
}
#[test]
fn fn_row_join_is_commutative_and_idempotent() {
let a = FnRow::of_target(def(1));
let b = FnRow::of_target(def(2));
assert_eq!(a.join(&b), b.join(&a));
assert_eq!(a.join(&a), a);
assert_eq!(a.join(&b).join(&b), a.join(&b));
}
#[test]
fn unify_joins_the_effect_row_alongside_params_and_return() {
let joined = unify(
&fn_ty_from(&[Ty::Int], Ty::Int, &[1]),
&fn_ty_from(&[Ty::Int], Ty::Int, &[2]),
);
assert_eq!(joined, fn_ty_from(&[Ty::Int], Ty::Int, &[1, 2]));
assert_eq!(
unify(
&fn_ty_from(&[Ty::Int], Ty::Int, &[1]),
&fn_ty(&[Ty::Int], Ty::Int)
),
fn_ty(&[Ty::Int], Ty::Int)
);
let traced = fn_ty_from(&[Ty::Int], Ty::Int, &[1]);
assert_eq!(unify(&Ty::Unknown, &traced), traced);
assert_eq!(unify(&traced, &Ty::Unknown), traced);
}
#[test]
fn effect_rows_do_not_change_the_displayed_type() {
assert_eq!(
fn_ty_from(&[Ty::Int], Ty::Bool, &[1, 2]).display(),
fn_ty(&[Ty::Int], Ty::Bool).display()
);
}
#[test]
fn erase_fn_rows_reaches_nested_positions() {
let nested = Ty::Array(Box::new(Ty::Map(
Box::new(Ty::String),
Box::new(fn_ty_from(
&[fn_ty_from(&[], Ty::Int, &[3])],
Ty::Int,
&[1, 2],
)),
)));
let erased = Ty::Array(Box::new(Ty::Map(
Box::new(Ty::String),
Box::new(fn_ty(&[fn_ty(&[], Ty::Int)], Ty::Int)),
)));
assert_eq!(erase_fn_rows(&nested), erased);
assert_eq!(erase_fn_rows(&erased), erased, "erasure is idempotent");
}
enum ErasureShape {
NestsTy,
Leaf,
}
fn classify_erasure_shape(ty: &Ty) -> ErasureShape {
match ty {
Ty::Fn(..) | Ty::Array(_) | Ty::Map(_, _) | Ty::Option(_) | Ty::Weighted(_) => {
ErasureShape::NestsTy
}
Ty::Int
| Ty::Float
| Ty::Bool
| Ty::String
| Ty::Content
| Ty::Divert
| Ty::List(_)
| Ty::Struct(_)
| Ty::Handle(_)
| Ty::Range { .. }
| Ty::Tower(_)
| Ty::Unknown
| Ty::Conflicted => ErasureShape::Leaf,
}
}
#[test]
fn erase_fn_rows_leaves_every_nominal_leaf_untouched() {
let leaves = [
Ty::Int,
Ty::Float,
Ty::Bool,
Ty::String,
Ty::Divert,
Ty::List("Weathers".into()),
Ty::Struct("Vec2".into()),
Ty::Handle("AudioInstance".into()),
Ty::Range { non_empty: true },
Ty::Range { non_empty: false },
Ty::Tower(TowerTy::Vec2),
Ty::Unknown,
Ty::Conflicted,
];
for leaf in leaves {
assert!(
matches!(classify_erasure_shape(&leaf), ErasureShape::Leaf),
"expected {leaf:?} to classify as a nominal leaf"
);
assert_eq!(erase_fn_rows(&leaf), leaf, "leaf erasure must be a no-op");
}
}
#[test]
fn erase_fn_rows_reaches_option_and_weighted_nesting() {
let rowed = fn_ty_from(&[], Ty::Int, &[1]);
let erased = fn_ty(&[], Ty::Int);
let via_option = Ty::Option(Box::new(rowed.clone()));
assert!(matches!(
classify_erasure_shape(&via_option),
ErasureShape::NestsTy
));
assert_eq!(
erase_fn_rows(&via_option),
Ty::Option(Box::new(erased.clone()))
);
let via_weighted = Ty::Weighted(Box::new(rowed));
assert!(matches!(
classify_erasure_shape(&via_weighted),
ErasureShape::NestsTy
));
assert_eq!(erase_fn_rows(&via_weighted), Ty::Weighted(Box::new(erased)));
}
enum UnifyNestingShape {
NestsTy,
Leaf,
}
fn classify_unify_nesting(ty: &Ty) -> UnifyNestingShape {
match ty {
Ty::Fn(..) | Ty::Array(_) | Ty::Map(_, _) | Ty::Option(_) | Ty::Weighted(_) => {
UnifyNestingShape::NestsTy
}
Ty::Int
| Ty::Float
| Ty::Bool
| Ty::String
| Ty::Content
| Ty::Divert
| Ty::List(_)
| Ty::Struct(_)
| Ty::Handle(_)
| Ty::Range { .. }
| Ty::Tower(_)
| Ty::Unknown
| Ty::Conflicted => UnifyNestingShape::Leaf,
}
}
#[test]
fn unify_has_a_pointwise_arm_for_every_nesting_ty_variant() {
let nesting_pairs: [(Ty, Ty, Ty); 5] = [
(
Ty::Array(Box::new(Ty::Int)),
Ty::Array(Box::new(Ty::Float)),
Ty::Array(Box::new(Ty::Float)),
),
(
Ty::Map(Box::new(Ty::Int), Box::new(Ty::String)),
Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::String)),
Ty::Map(Box::new(Ty::Int), Box::new(Ty::String)),
),
(
Ty::Option(Box::new(Ty::Int)),
Ty::Option(Box::new(Ty::Float)),
Ty::Option(Box::new(Ty::Float)),
),
(
Ty::Weighted(Box::new(Ty::Int)),
Ty::Weighted(Box::new(Ty::Float)),
Ty::Weighted(Box::new(Ty::Float)),
),
(
fn_ty(&[Ty::Int], Ty::Int),
fn_ty(&[Ty::Float], Ty::Int),
fn_ty(&[Ty::Float], Ty::Int),
),
];
for (x, y, expected) in &nesting_pairs {
assert!(
matches!(classify_unify_nesting(x), UnifyNestingShape::NestsTy),
"{x:?} must classify as NestsTy to belong in this table"
);
let joined = unify(x, y);
assert_ne!(
joined,
Ty::Conflicted,
"unify({x:?}, {y:?}) fell through the pair-match wildcard \
to Conflicted — a same-variant nesting pair must unify \
pointwise instead (#1772)"
);
assert_eq!(
&joined, expected,
"unify({x:?}, {y:?}) did not join pointwise on the nested \
element"
);
}
}
#[test]
fn assignable_ignores_effect_rows_but_not_the_rest_of_the_type() {
let declared = fn_ty(&[Ty::Int], Ty::Int);
assert!(assignable(
&declared,
&fn_ty_from(&[Ty::Int], Ty::Int, &[1])
));
assert!(assignable(
&fn_ty_from(&[Ty::Int], Ty::Int, &[1]),
&fn_ty_from(&[Ty::Int], Ty::Int, &[2])
));
assert!(assignable(
&Ty::Array(Box::new(declared.clone())),
&Ty::Array(Box::new(fn_ty_from(&[Ty::Int], Ty::Int, &[7])))
));
assert!(!assignable(
&declared,
&fn_ty_from(&[Ty::String], Ty::Int, &[1])
));
assert!(!assignable(&declared, &fn_ty_from(&[], Ty::Int, &[1])));
assert!(!assignable(&declared, &Ty::Int));
assert!(!assignable(&Ty::Int, &Ty::Float));
assert!(assignable(&Ty::Float, &Ty::Int));
}
#[test]
fn ref_assignable_rejects_the_widening_assignable_permits() {
assert!(assignable(&Ty::Float, &Ty::Int));
assert!(!ref_assignable(&Ty::Float, &Ty::Int));
assert!(!ref_assignable(&Ty::Int, &Ty::Float));
assert!(ref_assignable(&Ty::Int, &Ty::Int));
assert!(ref_assignable(&Ty::Float, &Ty::Float));
assert!(ref_assignable(
&fn_ty(&[Ty::Int], Ty::Int),
&fn_ty_from(&[Ty::Int], Ty::Int, &[1])
));
assert!(!ref_assignable(
&fn_ty(&[Ty::Int], Ty::Int),
&fn_ty_from(&[Ty::String], Ty::Int, &[1])
));
}
#[test]
fn ref_assignable_treats_nested_unknown_as_a_wildcard() {
assert!(ref_assignable(
&Ty::Array(Box::new(Ty::Float)),
&Ty::Array(Box::new(Ty::Unknown))
));
assert!(ref_assignable(
&Ty::Option(Box::new(Ty::Float)),
&Ty::Option(Box::new(Ty::Unknown))
));
assert!(ref_assignable(
&Ty::Array(Box::new(Ty::Unknown)),
&Ty::Array(Box::new(Ty::Float))
));
assert!(ref_assignable(
&Ty::Map(Box::new(Ty::String), Box::new(Ty::Int)),
&Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::Unknown))
));
assert!(!ref_assignable(
&Ty::Array(Box::new(Ty::Float)),
&Ty::Array(Box::new(Ty::Int))
));
assert!(!ref_assignable(
&Ty::Option(Box::new(Ty::Float)),
&Ty::Option(Box::new(Ty::String))
));
}
#[test]
fn fn_unify_is_order_independent() {
let a = fn_ty(&[Ty::Int, Ty::String], Ty::Int);
let b = fn_ty(&[Ty::Float, Ty::String], Ty::Unknown);
let c = fn_ty(&[Ty::Unknown, Ty::String], Ty::Float);
let expected = fn_ty(&[Ty::Float, Ty::String], Ty::Float);
let orderings: [[&Ty; 3]; 6] = [
[&a, &b, &c],
[&a, &c, &b],
[&b, &a, &c],
[&b, &c, &a],
[&c, &a, &b],
[&c, &b, &a],
];
for ordering in orderings {
assert_eq!(
unify_all(ordering.iter().map(|t| (*t).clone())),
expected,
"order {ordering:?} must reach the same join"
);
}
}
#[test]
fn fn_conflict_detection_is_order_independent() {
let a = fn_ty(&[Ty::Int], Ty::Int);
let b = fn_ty(&[Ty::String], Ty::Int);
let u = fn_ty(&[Ty::Unknown], Ty::Unknown);
let expected = fn_ty(&[Ty::Conflicted], Ty::Int);
let orderings: [[&Ty; 3]; 6] = [
[&a, &b, &u],
[&a, &u, &b],
[&b, &a, &u],
[&b, &u, &a],
[&u, &a, &b],
[&u, &b, &a],
];
for ordering in orderings {
assert_eq!(
unify_all(ordering.iter().map(|t| (*t).clone())),
expected,
"order {ordering:?} must detect the row conflict"
);
}
}
#[test]
fn fn_display_is_the_reserved_written_form() {
assert_eq!(fn_ty(&[Ty::Int], Ty::Int).display(), "fn(int): int");
assert_eq!(
fn_ty(&[Ty::Int, Ty::String], Ty::Bool).display(),
"fn(int, string): bool"
);
assert_eq!(fn_ty(&[], Ty::Float).display(), "fn(): float");
}
#[test]
fn handle_same_kind_unifies_to_itself() {
let h = Ty::Handle("AudioInstance".to_string());
assert_eq!(unify(&h, &h), h);
assert_eq!(unify(&Ty::Unknown, &h), h);
assert_eq!(unify(&h, &Ty::Unknown), h);
}
#[test]
fn handle_cross_kind_is_conflicted_not_unknown() {
let a = Ty::Handle("AudioInstance".to_string());
let b = Ty::Handle("Timer".to_string());
assert_eq!(unify(&a, &b), Ty::Conflicted);
assert_eq!(unify(&b, &a), Ty::Conflicted);
}
#[test]
fn handle_vs_other_concrete_type_is_conflicted() {
let h = Ty::Handle("AudioInstance".to_string());
assert_eq!(unify(&h, &Ty::Int), Ty::Conflicted);
assert_eq!(unify(&Ty::String, &h), Ty::Conflicted);
assert_eq!(unify(&h, &Ty::Array(Box::new(Ty::Int))), Ty::Conflicted);
}
#[test]
fn handle_conflicted_absorbs_everything() {
let h = Ty::Handle("AudioInstance".to_string());
assert_eq!(unify(&Ty::Conflicted, &h), Ty::Conflicted);
assert_eq!(unify(&h, &Ty::Conflicted), Ty::Conflicted);
}
#[test]
fn handle_display_carries_the_kind_name() {
assert_eq!(
Ty::Handle("AudioInstance".to_string()).display(),
"Handle<AudioInstance>"
);
}
fn opt(t: Ty) -> Ty {
Ty::Option(Box::new(t))
}
#[test]
fn option_unifies_pointwise_like_array() {
assert_eq!(opt(Ty::Int).display(), "Option<int>");
assert_eq!(unify(&opt(Ty::Int), &opt(Ty::Int)), opt(Ty::Int));
assert_eq!(unify(&opt(Ty::Int), &opt(Ty::Float)), opt(Ty::Float));
assert_eq!(unify(&opt(Ty::Unknown), &opt(Ty::String)), opt(Ty::String));
assert_eq!(unify(&Ty::Unknown, &opt(Ty::Int)), opt(Ty::Int));
}
#[test]
fn option_vs_bare_type_is_conflicted_the_ruled_strictness() {
assert_eq!(unify(&opt(Ty::Int), &Ty::Int), Ty::Conflicted);
assert_eq!(unify(&Ty::Int, &opt(Ty::Int)), Ty::Conflicted);
assert_eq!(unify(&opt(Ty::String), &Ty::String), Ty::Conflicted);
assert_eq!(
unify(&opt(Ty::Int), &Ty::Array(Box::new(Ty::Int))),
Ty::Conflicted
);
}
#[test]
fn option_nests_like_any_parameterized_builtin() {
let nested = opt(opt(Ty::Int));
assert_eq!(nested.display(), "Option<Option<int>>");
assert_eq!(unify(&nested, &nested), nested);
assert_eq!(unify(&nested, &opt(Ty::Int)), opt(Ty::Conflicted));
}
#[test]
fn option_element_conflict_stays_inside_the_element() {
assert_eq!(unify(&opt(Ty::Int), &opt(Ty::String)), opt(Ty::Conflicted));
}
fn range(non_empty: bool) -> Ty {
Ty::Range { non_empty }
}
#[test]
fn range_display_names_the_refinement() {
assert_eq!(range(false).display(), "range");
assert_eq!(range(true).display(), "NonEmptyRange");
assert_eq!(opt(range(true)).display(), "Option<NonEmptyRange>");
}
#[test]
fn range_evidence_joins_with_and() {
assert_eq!(unify(&range(true), &range(true)), range(true));
assert_eq!(unify(&range(true), &range(false)), range(false));
assert_eq!(unify(&range(false), &range(true)), range(false));
assert_eq!(unify(&range(false), &range(false)), range(false));
assert_eq!(unify(&Ty::Unknown, &range(true)), range(true));
}
#[test]
fn range_vs_other_concrete_is_conflicted() {
assert_eq!(unify(&range(false), &Ty::Int), Ty::Conflicted);
assert_eq!(
unify(&range(true), &Ty::Array(Box::new(Ty::Int))),
Ty::Conflicted
);
assert_eq!(unify(&opt(range(true)), &range(true)), Ty::Conflicted);
}
#[test]
fn range_evidence_join_is_order_independent() {
let orderings: [&[Ty]; 3] = [
&[
Ty::Range { non_empty: true },
Ty::Range { non_empty: false },
],
&[
Ty::Range { non_empty: false },
Ty::Range { non_empty: true },
],
&[
Ty::Unknown,
Ty::Range { non_empty: true },
Ty::Range { non_empty: false },
],
];
for ordering in orderings {
assert_eq!(
unify_all(ordering.iter().cloned()),
Ty::Range { non_empty: false },
"order {ordering:?} must lose the evidence at the join"
);
}
}
#[test]
fn coalesce_option_then_value_collapses_to_the_value_type() {
assert_eq!(coalesce(&opt(Ty::Int), &Ty::Int), Ok(Ty::Int));
assert_eq!(coalesce(&opt(Ty::Int), &Ty::Float), Ok(Ty::Float));
assert_eq!(coalesce(&opt(Ty::String), &Ty::String), Ok(Ty::String));
}
#[test]
fn coalesce_two_options_keeps_optionality_for_chaining() {
assert_eq!(coalesce(&opt(Ty::Int), &opt(Ty::Int)), Ok(opt(Ty::Int)));
assert_eq!(coalesce(&opt(Ty::Int), &opt(Ty::Float)), Ok(opt(Ty::Float)));
}
#[test]
fn coalesce_chains_left_associatively() {
let step1 = coalesce(&opt(Ty::Int), &opt(Ty::Int)).expect("chain step");
assert_eq!(step1, opt(Ty::Int));
assert_eq!(coalesce(&step1, &Ty::Int), Ok(Ty::Int));
}
#[test]
fn coalesce_mismatched_fallback_is_an_error() {
assert_eq!(
coalesce(&opt(Ty::Int), &Ty::String),
Err(CoalesceError::Mismatch {
element: Ty::Int,
fallback: Ty::String,
})
);
}
#[test]
fn coalesce_non_option_left_is_an_error() {
assert_eq!(
coalesce(&Ty::Int, &Ty::Int),
Err(CoalesceError::LeftNotOption(Ty::Int))
);
assert_eq!(
coalesce(&Ty::Array(Box::new(Ty::Int)), &Ty::Int),
Err(CoalesceError::LeftNotOption(Ty::Array(Box::new(Ty::Int))))
);
}
#[test]
fn coalesce_gradual_escapes() {
assert_eq!(coalesce(&Ty::Unknown, &Ty::Int), Ok(Ty::Unknown));
assert_eq!(coalesce(&opt(Ty::Unknown), &Ty::Int), Ok(Ty::Int));
assert_eq!(coalesce(&opt(Ty::Int), &Ty::Unknown), Ok(Ty::Int));
assert_eq!(coalesce(&Ty::Conflicted, &Ty::Int), Ok(Ty::Conflicted));
assert_eq!(coalesce(&opt(Ty::Conflicted), &Ty::Int), Ok(Ty::Conflicted));
}
#[test]
fn fn_composes_with_handle_typed_params() {
let a = fn_ty(&[Ty::Handle("AudioInstance".to_string())], Ty::Bool);
let b = fn_ty(&[Ty::Handle("AudioInstance".to_string())], Ty::Bool);
assert_eq!(unify(&a, &b), a);
let mismatched = fn_ty(&[Ty::Handle("Timer".to_string())], Ty::Bool);
assert_eq!(unify(&a, &mismatched), fn_ty(&[Ty::Conflicted], Ty::Bool));
}
}