use std::{
cmp::{max, min},
collections::{HashMap, HashSet},
};
use id_arena::Arena;
use indexmap::{IndexMap, IndexSet};
use log::{debug, trace};
use crate::{
AtomRestriction, AtomSemantic, BuiltinTypes, Type, TypeId, stringify_impl, substitute,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Comparison {
Assign,
Cast,
Invalid,
}
#[derive(Debug, Default)]
pub(crate) struct ComparisonContext<'a> {
infer: Option<&'a mut HashMap<TypeId, Vec<TypeId>>>,
stack: IndexSet<(TypeId, TypeId)>,
}
pub fn compare_with_inference(
arena: &mut Arena<Type>,
builtins: &BuiltinTypes,
lhs: TypeId,
rhs: TypeId,
infer: Option<&mut HashMap<TypeId, Vec<TypeId>>>,
) -> Comparison {
let lhs = substitute(arena, lhs);
let rhs = substitute(arena, rhs);
let lhs_name = stringify_impl(arena, lhs, &mut IndexMap::new());
let rhs_name = stringify_impl(arena, rhs, &mut IndexMap::new());
trace!("Comparing {lhs_name} to {rhs_name}");
let result = compare_impl(
arena,
builtins,
&mut ComparisonContext {
infer,
..Default::default()
},
lhs,
rhs,
None,
None,
);
trace!("Comparison from {lhs_name} to {rhs_name} yielded {result:?}");
result
}
pub fn compare(
arena: &mut Arena<Type>,
builtins: &BuiltinTypes,
lhs: TypeId,
rhs: TypeId,
) -> Comparison {
compare_with_inference(arena, builtins, lhs, rhs, None)
}
pub(crate) fn compare_impl(
arena: &Arena<Type>,
builtins: &BuiltinTypes,
ctx: &mut ComparisonContext<'_>,
lhs: TypeId,
rhs: TypeId,
lhs_semantic: Option<TypeId>,
rhs_semantic: Option<TypeId>,
) -> Comparison {
if !ctx.stack.insert((lhs, rhs)) {
return Comparison::Assign;
}
let result = match (arena[lhs].clone(), arena[rhs].clone()) {
(Type::Apply(_), _) | (_, Type::Apply(_)) => unreachable!(),
(Type::Ref(lhs), _) => {
compare_impl(arena, builtins, ctx, lhs, rhs, lhs_semantic, rhs_semantic)
}
(_, Type::Ref(rhs)) => {
compare_impl(arena, builtins, ctx, lhs, rhs, lhs_semantic, rhs_semantic)
}
(Type::Unresolved, _) | (_, Type::Unresolved) => Comparison::Assign,
(Type::Never, _) => Comparison::Assign,
(_, Type::Any) => Comparison::Assign,
(Type::Atom(lhs), Type::Atom(rhs)) => {
let semantic = if lhs.semantic == rhs.semantic
|| rhs.semantic == AtomSemantic::Any
|| (lhs.semantic == AtomSemantic::String && rhs.semantic == AtomSemantic::Bytes)
|| (lhs.semantic == AtomSemantic::Bytes && rhs.semantic == AtomSemantic::String)
{
Comparison::Assign
} else {
Comparison::Cast
};
let restriction = match (lhs.restriction, rhs.restriction) {
(_, None) => Comparison::Assign,
(None, _) => Comparison::Invalid,
(Some(AtomRestriction::Length(lhs)), Some(AtomRestriction::Length(rhs))) => {
if lhs == rhs {
Comparison::Assign
} else {
Comparison::Invalid
}
}
(Some(AtomRestriction::Value(lhs)), Some(AtomRestriction::Value(rhs))) => {
if lhs == rhs {
Comparison::Assign
} else {
Comparison::Invalid
}
}
(Some(AtomRestriction::Length(_)), Some(AtomRestriction::Value(_))) => {
Comparison::Invalid
}
(Some(AtomRestriction::Value(lhs)), Some(AtomRestriction::Length(rhs))) => {
if lhs.len() == rhs {
Comparison::Assign
} else {
Comparison::Invalid
}
}
};
max(semantic, restriction)
}
(Type::Pair(lhs), Type::Pair(rhs)) => {
let first = compare_impl(arena, builtins, ctx, lhs.first, rhs.first, None, None);
let rest = compare_impl(arena, builtins, ctx, lhs.rest, rhs.rest, None, None);
max(first, rest)
}
(Type::Atom(_), Type::Pair(_)) => Comparison::Invalid,
(Type::Pair(_), Type::Atom(_)) => Comparison::Invalid,
(Type::Function(lhs), Type::Function(rhs)) => {
if lhs.nil_terminated != rhs.nil_terminated || lhs.params.len() != rhs.params.len() {
Comparison::Invalid
} else {
let mut result = compare_impl(arena, builtins, ctx, lhs.ret, rhs.ret, None, None);
for (i, param) in lhs.params.iter().enumerate() {
result = max(
result,
compare_impl(arena, builtins, ctx, *param.1, rhs.params[i], None, None),
);
}
result
}
}
(_, Type::Generic(_)) => {
if lhs == rhs {
Comparison::Assign
} else if let Some(infer) = &mut ctx.infer {
debug!(
"Inferring {} could include {}",
stringify_impl(arena, rhs, &mut IndexMap::new()),
stringify_impl(arena, lhs, &mut IndexMap::new())
);
infer.entry(rhs).or_default().push(lhs);
Comparison::Assign
} else if let Type::Union(lhs) = arena[lhs].clone() {
let mut result = Comparison::Assign;
for &id in &lhs.types {
result = max(
result,
compare_impl(arena, builtins, ctx, id, rhs, lhs_semantic, rhs_semantic),
);
}
result
} else {
Comparison::Invalid
}
}
(Type::Struct(lhs), Type::Struct(rhs)) => max(
compare_impl(arena, builtins, ctx, lhs.inner, rhs.inner, None, None),
if lhs.semantic == rhs.semantic {
Comparison::Assign
} else {
Comparison::Cast
},
),
(Type::Struct(lhs), _) => {
let inner = compare_impl(
arena,
builtins,
ctx,
lhs.inner,
rhs,
Some(lhs.semantic),
rhs_semantic,
);
let rhs_semantics = semantics_of(arena, rhs);
if rhs_semantic == Some(lhs.semantic)
|| rhs_semantics.contains(&Some(Semantic::Id(lhs.semantic)))
|| rhs_semantics.contains(&Some(Semantic::All))
{
inner
} else {
max(inner, Comparison::Cast)
}
}
(_, Type::Struct(rhs)) => {
let inner = compare_impl(
arena,
builtins,
ctx,
lhs,
rhs.inner,
lhs_semantic,
Some(rhs.semantic),
);
let semantics = semantics_of(arena, lhs);
if (semantics.len() != 1
|| (!semantics.contains(&Some(Semantic::Id(rhs.semantic)))
&& !semantics.contains(&Some(Semantic::All))))
&& lhs_semantic != Some(rhs.semantic)
{
max(inner, Comparison::Cast)
} else {
inner
}
}
(Type::Alias(lhs), _) => compare_impl(
arena,
builtins,
ctx,
lhs.inner,
rhs,
lhs_semantic,
rhs_semantic,
),
(_, Type::Alias(rhs)) => compare_impl(
arena,
builtins,
ctx,
lhs,
rhs.inner,
lhs_semantic,
rhs_semantic,
),
(Type::Generic(_) | Type::Any, _) => compare_impl(
arena,
builtins,
ctx,
builtins.recursive_any,
rhs,
lhs_semantic,
rhs_semantic,
),
(Type::Function(_), _) => {
let result = if let Type::Union(rhs) = arena[rhs].clone() {
let mut result = Comparison::Invalid;
for &id in &rhs.types {
result = min(
result,
compare_impl(arena, builtins, ctx, lhs, id, lhs_semantic, rhs_semantic),
);
}
result
} else {
Comparison::Invalid
};
min(
result,
compare_impl(
arena,
builtins,
ctx,
builtins.recursive_any,
rhs,
lhs_semantic,
rhs_semantic,
),
)
}
(Type::Union(lhs), _) => {
let mut result = Comparison::Assign;
for &id in &lhs.types {
result = max(
result,
compare_impl(arena, builtins, ctx, id, rhs, lhs_semantic, rhs_semantic),
);
}
result
}
(_, Type::Union(rhs)) => {
let mut result = Comparison::Invalid;
for &id in &rhs.types {
result = min(
result,
compare_impl(arena, builtins, ctx, lhs, id, lhs_semantic, rhs_semantic),
);
}
result
}
(_, Type::Never) => Comparison::Invalid,
(_, Type::Function(_)) => Comparison::Invalid,
};
ctx.stack.pop().unwrap();
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Semantic {
All,
Id(TypeId),
}
fn semantics_of(arena: &Arena<Type>, id: TypeId) -> HashSet<Option<Semantic>> {
match arena[id].clone() {
Type::Apply(_) => unreachable!(),
Type::Ref(id) => semantics_of(arena, id),
Type::Alias(alias) => semantics_of(arena, alias.inner),
Type::Never => HashSet::new(),
Type::Any => HashSet::from_iter([Some(Semantic::All)]),
Type::Unresolved | Type::Generic(_) | Type::Atom(_) | Type::Pair(_) | Type::Function(_) => {
HashSet::from_iter([None])
}
Type::Struct(ty) => HashSet::from_iter([Some(Semantic::Id(ty.semantic))]),
Type::Union(ty) => {
let mut semantics = HashSet::new();
for &id in &ty.types {
semantics.extend(semantics_of(arena, id));
}
semantics
}
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use id_arena::Arena;
use rstest::rstest;
use crate::{Atom, Type, compare};
use super::*;
#[rstest]
#[case(Atom::NIL, Atom::NIL, Comparison::Assign)]
#[case(Atom::NIL, Atom::FALSE, Comparison::Cast)]
#[case(Atom::NIL, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::NIL, Atom::BYTES, Comparison::Assign)]
#[case(Atom::NIL, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::NIL, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::NIL, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::NIL, Atom::INT, Comparison::Cast)]
#[case(Atom::FALSE, Atom::NIL, Comparison::Cast)]
#[case(Atom::FALSE, Atom::FALSE, Comparison::Assign)]
#[case(Atom::FALSE, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::FALSE, Atom::BYTES, Comparison::Cast)]
#[case(Atom::FALSE, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::FALSE, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::FALSE, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::FALSE, Atom::INT, Comparison::Cast)]
#[case(Atom::TRUE, Atom::NIL, Comparison::Invalid)]
#[case(Atom::TRUE, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::TRUE, Atom::TRUE, Comparison::Assign)]
#[case(Atom::TRUE, Atom::BYTES, Comparison::Cast)]
#[case(Atom::TRUE, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::TRUE, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::TRUE, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::TRUE, Atom::INT, Comparison::Cast)]
#[case(Atom::BYTES, Atom::NIL, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::BYTES, Comparison::Assign)]
#[case(Atom::BYTES, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::BYTES, Atom::INT, Comparison::Cast)]
#[case(Atom::BYTES_32, Atom::NIL, Comparison::Invalid)]
#[case(Atom::BYTES_32, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::BYTES_32, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::BYTES_32, Atom::BYTES, Comparison::Assign)]
#[case(Atom::BYTES_32, Atom::BYTES_32, Comparison::Assign)]
#[case(Atom::BYTES_32, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::BYTES_32, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::BYTES_32, Atom::INT, Comparison::Cast)]
#[case(Atom::PUBLIC_KEY, Atom::NIL, Comparison::Invalid)]
#[case(Atom::PUBLIC_KEY, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::PUBLIC_KEY, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::PUBLIC_KEY, Atom::BYTES, Comparison::Cast)]
#[case(Atom::PUBLIC_KEY, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::PUBLIC_KEY, Atom::PUBLIC_KEY, Comparison::Assign)]
#[case(Atom::PUBLIC_KEY, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::PUBLIC_KEY, Atom::INT, Comparison::Cast)]
#[case(Atom::SIGNATURE, Atom::NIL, Comparison::Invalid)]
#[case(Atom::SIGNATURE, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::SIGNATURE, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::SIGNATURE, Atom::BYTES, Comparison::Cast)]
#[case(Atom::SIGNATURE, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::SIGNATURE, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::SIGNATURE, Atom::SIGNATURE, Comparison::Assign)]
#[case(Atom::SIGNATURE, Atom::INT, Comparison::Cast)]
#[case(Atom::INT, Atom::NIL, Comparison::Invalid)]
#[case(Atom::INT, Atom::FALSE, Comparison::Invalid)]
#[case(Atom::INT, Atom::TRUE, Comparison::Invalid)]
#[case(Atom::INT, Atom::BYTES, Comparison::Cast)]
#[case(Atom::INT, Atom::BYTES_32, Comparison::Invalid)]
#[case(Atom::INT, Atom::PUBLIC_KEY, Comparison::Invalid)]
#[case(Atom::INT, Atom::SIGNATURE, Comparison::Invalid)]
#[case(Atom::INT, Atom::INT, Comparison::Assign)]
#[case(Atom::new(AtomSemantic::Int, Some(AtomRestriction::Value(Cow::Borrowed(&[1])))), Atom::INT, Comparison::Assign)]
#[case(Atom::new(AtomSemantic::Int, Some(AtomRestriction::Value(Cow::Borrowed(&[1])))), Atom::BYTES, Comparison::Cast)]
fn test_atoms(#[case] lhs: Atom, #[case] rhs: Atom, #[case] expected: Comparison) {
let mut arena = Arena::new();
let builtins = BuiltinTypes::new(&mut arena);
let lhs_id = arena.alloc(Type::Atom(lhs.clone()));
let rhs_id = arena.alloc(Type::Atom(rhs.clone()));
assert_eq!(
compare(&mut arena, &builtins, lhs_id, rhs_id),
expected,
"{lhs} -> {rhs}"
);
}
}