use std::collections::BTreeMap;
use std::ops::ControlFlow;
use crate::{MangledName, MethodSig, ObjectField, Type, TypeKind};
use super::generic::substitute_or_record;
use super::narrowing;
use super::type_namespace::TypeNamespace;
use super::type_registry::TypeRegistry;
use super::variance::Variance;
#[derive(Clone, Copy)]
pub(crate) struct TypeResolver<'a> {
pub(in crate::typechecker) types: &'a TypeNamespace<'a>,
pub(in crate::typechecker) registry: &'a TypeRegistry<'a>,
pub(crate) limits: &'a crate::type_size::TypeLimits,
pub(crate) strict_subtype: bool,
pub(in crate::typechecker) generic_constraints: &'a BTreeMap<u32, Type>,
}
pub(super) enum ImplementsFailure {
Missing(String),
Incompatible {
member: String,
expected: String,
actual: String,
},
NotWritable {
member: String,
ty: String,
},
NotReadable {
member: String,
ty: String,
},
OptionalityMismatch {
member: String,
ty: String,
},
}
impl<'a> TypeResolver<'a> {
pub(super) fn lookup(
&self,
mangled: &MangledName,
name: &str,
) -> Option<&'a crate::TypeSymbol> {
self.registry
.lookup(mangled)
.or_else(|| self.types.lookup(name))
}
pub(super) fn interface_full_form(
&self,
mangled: &MangledName,
name: &str,
args: &[Type],
) -> Option<BTreeMap<String, ObjectField>> {
let TypeKind::Interface {
generics,
methods,
properties,
..
} = &self.lookup(mangled, name)?.kind
else {
return None;
};
let bindings: BTreeMap<String, Type> =
generics.iter().cloned().zip(args.iter().cloned()).collect();
let mut out: BTreeMap<String, ObjectField> = BTreeMap::new();
for (member, sig) in methods {
if !sig.generics.is_empty() {
continue;
}
out.insert(
member.clone(),
ObjectField {
ty: self.method_type(sig, &bindings),
optional: sig.optional,
readonly: true,
method: true,
},
);
}
for (member, sig) in properties {
out.insert(
member.clone(),
ObjectField {
ty: substitute_or_record(&sig.ty, &bindings, self.limits),
optional: sig.optional,
readonly: sig.readonly,
method: false,
},
);
}
Some(out)
}
pub(super) fn sym_by_mangled(&self, mangled: &MangledName) -> Option<&'a crate::TypeSymbol> {
self.registry
.lookup(mangled)
.or_else(|| self.types.lookup_by_mangled(mangled))
}
pub(super) fn is_subclass(&self, actual: &MangledName, expected: &MangledName) -> bool {
let mut cur = Some(actual.clone());
let mut seen: Vec<MangledName> = Vec::new();
while let Some(m) = cur {
if &m == expected {
return true;
}
if seen.contains(&m) {
break;
}
seen.push(m.clone());
cur = match self.sym_by_mangled(&m).map(|s| &s.kind) {
Some(TypeKind::Class { extends, .. }) => extends.as_ref().map(|e| e.parent.clone()),
_ => None,
};
}
false
}
pub(super) fn class_args_at_ancestor(
&self,
actual: &MangledName,
actual_args: &[Type],
expected: &MangledName,
) -> Option<Vec<Type>> {
super::classes::walk_class_chain_with(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
actual,
actual_args,
|sym, bindings| {
if &sym.mangled_name != expected {
return ControlFlow::Continue(());
}
let TypeKind::Class { generics, .. } = &sym.kind else {
return ControlFlow::Break(None);
};
ControlFlow::Break(Some(
generics
.iter()
.map(|g| bindings.get(g).cloned().unwrap_or(Type::Unknown))
.collect(),
))
},
)
}
pub(super) fn class_full_form(
&self,
mangled: &MangledName,
args: &[Type],
) -> Option<BTreeMap<String, ObjectField>> {
let mut out: BTreeMap<String, ObjectField> = BTreeMap::new();
super::classes::for_each_class_in_chain(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
mangled,
args,
|sym, bindings| {
let TypeKind::Class {
fields,
methods,
method_visibility,
..
} = &sym.kind
else {
return;
};
for (name, sig) in methods {
if !sig.generics.is_empty()
|| method_visibility.get(name).copied() == Some(crate::Visibility::Private)
{
continue;
}
out.entry(name.clone()).or_insert(ObjectField {
ty: self.method_type(sig, bindings),
optional: sig.optional,
readonly: true,
method: true,
});
}
for (name, f) in fields {
if f.visibility == crate::Visibility::Private {
continue;
}
out.entry(name.clone()).or_insert(ObjectField {
ty: substitute_or_record(&f.ty, bindings, self.limits),
optional: f.optional,
readonly: f.readonly,
method: false,
});
}
},
)?;
Some(out)
}
fn method_type(&self, sig: &MethodSig, bindings: &BTreeMap<String, Type>) -> Type {
Type::Function {
params: sig
.params
.iter()
.map(|p| {
crate::ObjectField::widen_optional(
p.is_omittable(),
substitute_or_record(&p.ty, bindings, self.limits),
)
})
.collect(),
ret: Box::new(substitute_or_record(&sig.ret, bindings, self.limits)),
predicate: None,
has_rest: sig.params.last().is_some_and(|p| p.rest),
optional: crate::package_declaration::optional_parameter_count(&sig.params),
}
}
pub(super) fn class_private_members(
&self,
mangled: &MangledName,
args: &[Type],
) -> Option<PrivateMembers> {
let mut private = BTreeMap::new();
super::classes::for_each_class_in_chain(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
mangled,
args,
|sym, bindings| {
let TypeKind::Class {
fields,
methods,
method_visibility,
..
} = &sym.kind
else {
return;
};
let mut insert = |name: &String, ty: Type| {
private.insert((sym.mangled_name.clone(), name.clone()), ty);
};
for (name, field) in fields {
if field.visibility == crate::Visibility::Private {
insert(name, substitute_or_record(&field.ty, bindings, self.limits));
}
}
for (name, sig) in methods {
if method_visibility.get(name).copied() == Some(crate::Visibility::Private) {
insert(name, self.method_type(sig, bindings));
}
}
},
)?;
Some(private)
}
pub(super) fn implements_failures(
&self,
class_mangled: &MangledName,
class_args: &[Type],
iface_mangled: &MangledName,
iface_name: &str,
iface_args: &[Type],
) -> Vec<ImplementsFailure> {
let Some(iface_form) = self.interface_full_form(iface_mangled, iface_name, iface_args)
else {
return Vec::new();
};
self.implements_form_failures(class_mangled, class_args, &iface_form)
}
pub(super) fn implements_form_failures(
&self,
class_mangled: &MangledName,
class_args: &[Type],
iface_form: &BTreeMap<String, ObjectField>,
) -> Vec<ImplementsFailure> {
let Some(class_form) = self.class_full_form(class_mangled, class_args) else {
return Vec::new();
};
let mut failures = Vec::new();
for (member, exp) in iface_form {
match class_form.get(member) {
Some(act) => {
let mut seen = Vec::new();
if self.class_property_is_write_only(class_mangled, class_args, member) {
failures.push(ImplementsFailure::NotReadable {
member: member.clone(),
ty: exp.ty.to_string(),
});
} else if !exp.optional && act.optional {
failures.push(ImplementsFailure::OptionalityMismatch {
member: member.clone(),
ty: exp.ty.to_string(),
});
} else if !self.class_member_assignable(
class_mangled,
class_args,
member,
&act.read_ty(),
&exp.read_ty(),
&mut seen,
) {
failures.push(ImplementsFailure::Incompatible {
member: member.clone(),
expected: exp.ty.to_string(),
actual: act.ty.to_string(),
});
} else if !exp.readonly
&& act.readonly
&& !self.class_member_is_method(class_mangled, class_args, member)
{
failures.push(ImplementsFailure::NotWritable {
member: member.clone(),
ty: exp.ty.to_string(),
});
}
}
None if !exp.optional => failures.push(ImplementsFailure::Missing(member.clone())),
None => {}
}
}
failures
}
fn class_member_assignable(
self,
class: &MangledName,
args: &[Type],
member: &str,
actual: &Type,
expected: &Type,
seen: &mut Vec<(Type, Type)>,
) -> bool {
if assignable_rec(actual, expected, self, seen) {
return true;
}
if let Type::Union(members) = expected.peel() {
return members.iter().any(|expected| {
self.class_member_assignable(class, args, member, actual, expected, seen)
});
}
let (
Type::Function {
params,
ret,
predicate,
optional: _,
has_rest: false,
},
Type::Function {
params: expected_params,
has_rest: false,
..
},
) = (actual.peel(), expected.peel())
else {
return false;
};
if expected_params.len() >= params.len() {
return false;
}
let omittable = super::classes::walk_class_chain_with(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
class,
args,
|sym, _| {
let TypeKind::Class {
methods, fields, ..
} = &sym.kind
else {
return ControlFlow::Continue(());
};
if fields.contains_key(member) {
return ControlFlow::Break(Some(false));
}
let Some(method) = methods.get(member) else {
return ControlFlow::Continue(());
};
ControlFlow::Break(Some(
method
.params
.iter()
.skip(expected_params.len())
.all(|param| param.default.is_some()),
))
},
)
.unwrap_or(false);
omittable
&& assignable_rec(
&Type::Function {
params: params.iter().take(expected_params.len()).cloned().collect(),
ret: ret.clone(),
predicate: predicate.clone(),
has_rest: false,
optional: 0,
},
expected,
self,
seen,
)
}
fn class_member_is_method(&self, mangled: &MangledName, args: &[Type], member: &str) -> bool {
super::classes::walk_class_chain_with(
|name| self.sym_by_mangled(name).cloned(),
self.limits,
mangled,
args,
|symbol, _| {
let TypeKind::Class {
methods, fields, ..
} = &symbol.kind
else {
return ControlFlow::Continue(());
};
if fields.contains_key(member) {
return ControlFlow::Break(Some(false));
}
if methods.contains_key(member) {
return ControlFlow::Break(Some(true));
}
ControlFlow::Continue(())
},
)
.unwrap_or(false)
}
fn class_property_is_write_only(
&self,
mangled: &MangledName,
args: &[Type],
member: &str,
) -> bool {
let mut has_setter = false;
let mut has_getter = false;
super::classes::for_each_class_in_chain(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
mangled,
args,
|sym, _| {
if let TypeKind::Class { accessors, .. } = &sym.kind {
for accessor in accessors.iter().filter(|a| a.name() == member) {
match accessor {
crate::AccessorSig::Getter { .. } => has_getter = true,
crate::AccessorSig::Setter { .. } => has_setter = true,
}
}
}
},
);
has_setter && !has_getter
}
pub(super) fn interface_data_shape(
&self,
mangled: &MangledName,
name: &str,
args: &[Type],
) -> Option<BTreeMap<String, ObjectField>> {
let TypeKind::Interface {
generics,
properties,
methods,
..
} = &self.lookup(mangled, name)?.kind
else {
return None;
};
if !methods.is_empty() {
return None;
}
let bindings: BTreeMap<String, Type> =
generics.iter().cloned().zip(args.iter().cloned()).collect();
Some(
properties
.iter()
.map(|(field, sig)| {
(
field.clone(),
ObjectField {
ty: substitute_or_record(&sig.ty, &bindings, self.limits),
optional: sig.optional,
readonly: sig.readonly,
method: false,
},
)
})
.collect(),
)
}
pub(super) fn interface_method_names(&self, mangled: &MangledName, name: &str) -> Vec<String> {
match self.lookup(mangled, name).map(|symbol| &symbol.kind) {
Some(TypeKind::Interface { methods, .. }) => methods.keys().cloned().collect(),
_ => Vec::new(),
}
}
pub(super) fn class_method_names(&self, mangled: &MangledName, args: &[Type]) -> Vec<String> {
let mut names = Vec::new();
super::classes::for_each_class_in_chain(
|m| self.sym_by_mangled(m).cloned(),
self.limits,
mangled,
args,
|sym, _| {
if let TypeKind::Class { methods, .. } = &sym.kind {
names.extend(methods.keys().cloned());
}
},
);
names
}
pub(super) fn interface_has_methods(&self, mangled: &MangledName, name: &str) -> bool {
match self.lookup(mangled, name).map(|symbol| &symbol.kind) {
Some(TypeKind::Interface { methods, .. }) => !methods.is_empty(),
_ => false,
}
}
}
pub(crate) fn rest_function_accepts(
actual: &[Type],
expected: &[Type],
mut accepts: impl FnMut(&Type, &Type) -> bool,
) -> bool {
let Some((rest, fixed)) = actual.split_last() else {
return false;
};
let Type::Array(element) = rest.peel() else {
return false;
};
let Some(past_fixed) = expected.get(fixed.len()..) else {
return false;
};
actual.len() != expected.len()
&& fixed
.iter()
.zip(expected)
.all(|(declared, passed)| accepts(passed, declared))
&& past_fixed.iter().all(|passed| accepts(passed, element))
}
fn enum_member_fits<V>(
actual: Option<&crate::types::EnumMember<V>>,
expected: Option<&crate::types::EnumMember<V>>,
) -> bool {
match (actual, expected) {
(_, None) => true,
(Some(actual), Some(expected)) => actual.name == expected.name,
(None, Some(_)) => false,
}
}
fn enum_as_member_union(
actual: &Type,
expected: &[Type],
types: TypeResolver,
) -> Option<Vec<Type>> {
let (Type::NumberEnum {
mangled: enum_mangled,
member: None,
..
}
| Type::StringEnum {
mangled: enum_mangled,
member: None,
..
}) = actual.peel()
else {
return None;
};
let names_member = expected.iter().any(|m| match m.peel() {
Type::NumberEnum {
mangled,
member: Some(_),
..
}
| Type::StringEnum {
mangled,
member: Some(_),
..
} => mangled == enum_mangled,
_ => false,
});
if !names_member {
return None;
}
super::comparable::enum_member_types(actual.peel(), types)
}
pub(crate) fn assignable(actual: &Type, expected: &Type, types: TypeResolver) -> bool {
let mut seen: Vec<(Type, Type)> = Vec::new();
assignable_rec(actual, expected, types, &mut seen)
}
pub(crate) fn strict_subtype(actual: &Type, expected: &Type, types: TypeResolver) -> bool {
let types = TypeResolver {
strict_subtype: true,
..types
};
assignable(actual, expected, types)
}
pub(crate) fn strict_subtype_union(types: Vec<Type>, resolver: TypeResolver) -> Type {
let mut members = match Type::union(types) {
Type::Union(members) => members,
single => return single,
};
let mut index = members.len();
while index > 0 {
index -= 1;
let absorbed = members.iter().enumerate().any(|(other, member)| {
other != index && strict_subtype(&members[index], member, resolver)
});
if absorbed {
members.remove(index);
}
}
Type::union(members)
}
fn assignable_rec(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
if !types.limits.spend_work(1) {
return false;
}
if let Some(actual) = super::utility_types::evaluate_concrete_operator(actual, types) {
return assignable_rec(&actual, expected, types, seen);
}
if let Some(expected) = super::utility_types::evaluate_concrete_operator(expected, types) {
return assignable_rec(actual, &expected, types, seen);
}
if let Some(expected) = rewrite_to_operand_form(expected) {
return assignable_rec(actual, &expected, types, seen);
}
if let Some(actual) = rewrite_to_operand_form(actual) {
return assignable_rec(&actual, expected, types, seen);
}
if matches!(actual.peel(), Type::Unknown | Type::GenericParam { .. })
&& expected.holds_every_value()
{
return true;
}
if matches!(actual.without_aliases(), Type::Refined { .. })
&& refinement_fits_union(actual, expected, types, seen)
{
return true;
}
if let Type::Refined { original, ty } = expected.without_aliases() {
return assignable_rec(actual, original, types, seen)
&& assignable_rec(actual, ty, types, seen);
}
if let Type::Refined { original, ty } = actual.without_aliases() {
if let Some(MappedProperty::Optional(property)) = mapped_property(original)
&& !ty.holds_undefined()
&& assignable_rec(&property, expected, types, seen)
{
return true;
}
let bounded_parameter = matches!(original.without_aliases(), Type::GenericParam { id, .. }
if types.generic_constraints.contains_key(id));
if (original.is_type_operator() || bounded_parameter) && !mentions_signature_var(original) {
let bound = upper_bound(original, types, 0);
let admitted: Vec<Type> = super::utility_types::members(&bound)
.into_iter()
.filter(|member| assignable_rec(member, ty, types, seen))
.collect();
if !admitted.is_empty() && assignable_rec(&Type::union(admitted), expected, types, seen)
{
return true;
}
}
return assignable_rec(original, expected, types, seen)
|| assignable_rec(ty, expected, types, seen);
}
if let Type::GenericParam { id, .. } = actual.peel() {
if matches!(expected.peel(), Type::GenericParam { id: other, .. } if id == other) {
return true;
}
if let Some(constraint) = types.generic_constraints.get(id) {
let pair = (actual.clone(), expected.clone());
if seen.contains(&pair) {
return false;
}
if seen.len() >= crate::compiler_limits::MAX_TYPE_RESOLUTION_DEPTH as usize {
types.limits.record(crate::type_size::TypeTooLarge::Depth);
return false;
}
seen.push(pair);
let result = assignable_rec(constraint, expected, types, seen);
seen.pop();
if result {
return true;
}
}
}
if same_alias_instance(actual, expected) {
return true;
}
if let Some(related) = relate_type_operators(actual, expected, types, seen) {
return related;
}
if seen
.iter()
.any(|(a, e)| same_alias_instance(actual, a) && same_alias_instance(expected, e))
{
return true;
}
if let Type::Union(ms) = actual.peel() {
return ms.iter().all(|m| assignable_rec(m, expected, types, seen));
}
if let Type::Union(ms) = expected.peel() {
if let Some(members) = enum_as_member_union(actual, ms, types) {
return members
.iter()
.all(|member| ms.iter().any(|m| assignable_rec(member, m, types, seen)));
}
return ms.iter().any(|m| assignable_rec(actual, m, types, seen));
}
if drops_readonly(actual, expected) || readonly_meets_mutator(actual, expected, types) {
return false;
}
if let Some(chain) = super::utility_types::symbolic_chain(expected) {
return fits_symbolic_mapping(&chain, actual);
}
if matches!(actual, Type::Alias { .. }) || matches!(expected, Type::Alias { .. }) {
let pair = (actual.clone(), expected.clone());
seen.push(pair);
let result = assignable_rec(actual.peel(), expected.peel(), types, seen);
seen.pop();
return result;
}
let actual = actual.peel();
let expected = expected.peel();
if actual.is_regexp_match_array() && matches!(expected, Type::Array(_)) {
return assignable_rec(
&Type::Array(Box::new(Type::regexp_element())),
expected,
types,
seen,
);
}
if matches!(actual, Type::Error) || matches!(expected, Type::Error) {
return true;
}
if matches!(actual, Type::Never) {
return true;
}
if matches!(
(actual, expected),
(Type::Undefined | Type::Void, Type::Void)
) {
return true;
}
if matches!(expected, Type::TypeVar(_)) || matches!(actual, Type::TypeVar(_)) {
return true;
}
if matches!(expected, Type::Unknown) {
return true;
}
if matches!(actual, Type::Unknown) {
return false;
}
if matches!(actual, Type::AliasRef { .. }) || matches!(expected, Type::AliasRef { .. }) {
if let (
Type::AliasRef {
mangled: ma,
args: aa,
..
},
Type::AliasRef {
mangled: me,
args: ae,
..
},
) = (actual, expected)
&& ma == me
&& aa.len() == ae.len()
&& aa
.iter()
.zip(ae.iter())
.all(|(a, e)| assignable_rec(a, e, types, seen))
{
return true;
}
let key = (actual.clone(), expected.clone());
if seen.iter().any(|(a, e)| a == &key.0 && e == &key.1) {
return true;
}
let recursive_expansions = seen
.iter()
.filter(|(actual, expected)| {
matches!(actual, Type::AliasRef { .. }) || matches!(expected, Type::AliasRef { .. })
})
.count();
if recursive_expansions >= 32 {
return false;
}
seen.push(key);
let a = expand_alias_ref(actual, types);
let e = expand_alias_ref(expected, types);
let result = assignable_rec(&a, &e, types, seen);
seen.pop();
return result;
}
match (actual, expected) {
(Type::Null | Type::Undefined | Type::Void, _) if expected.is_empty_object() => false,
(Type::GenericParam { .. }, _) if expected.is_empty_object() => false,
(_, _) if expected.is_empty_object() => true,
(_, Type::NonPrimitive) => matches!(
actual,
Type::NonPrimitive
| Type::Object { .. }
| Type::Array(_)
| Type::Tuple(_)
| Type::Readonly(_)
| Type::InterfaceRef { .. }
| Type::ClassRef { .. }
| Type::Function { .. }
| Type::Uint8Array
),
(Type::NonPrimitive, _) => false,
(_, Type::Object { .. } | Type::InterfaceRef { .. } | Type::ClassRef { .. })
if types.strict_subtype && is_primitive(actual) =>
{
false
}
(Type::GenericParam { id: a, .. }, Type::GenericParam { id: b, .. }) => a == b,
(Type::GenericParam { .. }, _) | (_, Type::GenericParam { .. }) => false,
(
Type::TemplateLiteral {
parts: actual_parts,
substitutions: actual_holes,
},
Type::TemplateLiteral {
parts: expected_parts,
substitutions: expected_holes,
},
) => {
if actual_parts == expected_parts && actual_holes.len() == expected_holes.len() {
actual_holes
.iter()
.zip(expected_holes)
.all(|(actual, expected)| {
matches!(expected.peel(), Type::String)
|| assignable_rec(actual, expected, types, seen)
})
} else if expected_holes.len() == 1
&& matches!(expected_holes.first().map(Type::peel), Some(Type::String))
{
actual_parts
.first()
.zip(expected_parts.first())
.is_some_and(|(a, b)| a.starts_with(b))
&& actual_parts
.last()
.zip(expected_parts.last())
.is_some_and(|(a, b)| a.ends_with(b))
} else {
false
}
}
(Type::StringLiteral(value), Type::TemplateLiteral { .. }) => {
crate::property_keys::matches_name(expected, value, types.limits)
}
(Type::StringLiteral(a), Type::StringLiteral(b)) => a == b,
(
Type::TemplateLiteral { .. } | Type::StringLiteral(_) | Type::StringEnum { .. },
Type::String,
) => true,
(Type::String, Type::StringLiteral(_)) => false,
(Type::NumberLiteral(a), Type::NumberLiteral(b)) => a == b,
(Type::NumberLiteral(_) | Type::NumberEnum { .. }, Type::Number) => true,
(Type::Number, Type::NumberLiteral(_)) => false,
(Type::BigIntLiteral(a), Type::BigIntLiteral(b)) => a == b,
(Type::BigIntLiteral(_), Type::BigInt) => true,
(Type::BigInt, Type::BigIntLiteral(_)) => false,
(Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => a == b,
(Type::BooleanLiteral(_), Type::Boolean) => true,
(Type::Boolean, Type::BooleanLiteral(_)) => false,
(
Type::NumberEnum {
mangled: ma,
member: am,
..
},
Type::NumberEnum {
mangled: me,
member: em,
..
},
) => ma == me && enum_member_fits(am.as_ref(), em.as_ref()),
(
Type::StringEnum {
mangled: ma,
member: am,
..
},
Type::StringEnum {
mangled: me,
member: em,
..
},
) => ma == me && enum_member_fits(am.as_ref(), em.as_ref()),
(
Type::NumberEnum {
member: Some(member),
..
},
Type::NumberLiteral(value),
) => member.value == *value,
(
Type::StringEnum {
member: Some(member),
..
},
Type::StringLiteral(value),
) => member.value == *value,
(
Type::NumberLiteral(value),
Type::NumberEnum {
member: Some(member),
..
},
) => member.value == *value,
(Type::Array(ae), Type::Array(ee)) => assignable_rec(ae, ee, types, seen),
(Type::Tuple(aa), Type::Tuple(ae)) => {
aa.required_len() >= ae.required_len()
&& aa.len() <= ae.len()
&& aa
.iter()
.zip(ae.iter())
.all(|(a, e)| assignable_rec(a, e, types, seen))
}
(Type::Tuple(aa), Type::Array(ee)) => aa.iter().all(|a| assignable_rec(a, ee, types, seen)),
(
Type::InterfaceRef {
mangled: ma,
args: aa,
name: na,
..
},
Type::InterfaceRef {
mangled: me,
args: ae,
..
},
) => {
if ma == me {
match args_relate_at_variances(ma, na, aa, ae, types, seen) {
Some(ArgsRelation::Related) => return true,
Some(ArgsRelation::Unrelated) => return false,
Some(ArgsRelation::MembersDecide) | None => {}
}
if args_relate_covariantly(aa, ae, types, seen) {
return true;
}
}
satisfies_structurally(actual, expected, types, seen)
}
(
Type::ClassRef {
mangled: ma,
args: aa,
..
},
Type::ClassRef {
mangled: me,
name: ne,
args: ae,
..
},
) => {
let actual_at_expected = if ma == me {
Some(aa.clone())
} else {
types.class_args_at_ancestor(ma, aa, me)
};
match actual_at_expected {
Some(at) => match args_relate_at_variances(me, ne, &at, ae, types, seen) {
Some(relation) => relation == ArgsRelation::Related,
None => args_relate_covariantly(&at, ae, types, seen),
},
None => false,
}
}
(
Type::ClassRef {
mangled: ma,
args: aa,
..
},
Type::InterfaceRef {
mangled: me,
name: ne,
args: ae,
..
},
) => {
if is_host_backed_interface(expected) {
return false;
}
let (Some(class_form), Some(iface_form)) = (
types.class_full_form(ma, aa),
types.interface_full_form(me, ne, ae),
) else {
return false;
};
if weak_type_rejects(&class_form, &iface_form) {
return false;
}
iface_form
.iter()
.all(|(member, exp)| match class_form.get(member) {
Some(act) => {
!types.class_property_is_write_only(ma, aa, member)
&& (exp.optional || !act.optional)
&& types.class_member_assignable(
ma,
aa,
member,
&act.read_ty(),
&exp.read_ty(),
seen,
)
}
None => exp.optional,
})
}
(Type::ClassRef { mangled, args, .. }, Type::Object { fields, index }) => {
let Some(class_form) = types.class_full_form(mangled, args) else {
return false;
};
if index.as_ref().is_some_and(|i| {
class_form
.values()
.any(|f| !assignable_rec(&f.ty, &i.value, types, seen))
}) {
return false;
}
if weak_type_rejects(&class_form, fields) {
return false;
}
fields
.iter()
.all(|(member, expected)| match class_form.get(member) {
Some(actual) => {
!types.class_property_is_write_only(mangled, args, member)
&& (expected.optional || !actual.optional)
&& types.class_member_assignable(
mangled,
args,
member,
&actual.read_ty(),
&expected.read_ty(),
seen,
)
}
None => expected.optional,
})
}
(Type::InterfaceRef { .. }, Type::ClassRef { .. }) => false,
(_, Type::InterfaceRef { .. }) if satisfies_structurally(actual, expected, types, seen) => {
true
}
(
Type::Function {
params: pa,
ret: ra,
predicate: predicate_a,
has_rest: rest_a,
optional: optional_a,
},
Type::Function {
params: pe,
ret: re,
predicate: predicate_e,
has_rest: rest_e,
optional: optional_e,
},
) => {
if types.strict_subtype
&& !strict_arity_fits(
Arity {
params: pa,
optional: *optional_a,
rest: *rest_a,
},
Arity {
params: pe,
optional: *optional_e,
rest: *rest_e,
},
|a, e| assignable_rec(a, e, types, seen),
)
{
return false;
}
if *rest_e && !rest_a {
return false;
}
let predicate_ok = match (predicate_a, predicate_e) {
(Some(a), Some(e)) => {
a.parameter_index == e.parameter_index
&& assignable_rec(&a.asserted_type, &e.asserted_type, types, seen)
}
(Some(_), None) => true,
(None, Some(_)) => false,
(None, None) => true,
};
let params_ok = if *rest_a && !rest_e {
rest_function_accepts(pa, pe, |e, a| assignable_rec(e, a, types, seen))
} else {
let required = if *rest_a {
pa.len()
} else {
pa.len().saturating_sub(*optional_a)
};
Type::function_arity_fits(required, pe.len(), *rest_a)
&& pa.iter().zip(pe.iter()).enumerate().all(|(i, (a, e))| {
let both_rest = *rest_a && i + 1 == pa.len() && i + 1 == pe.len();
if both_rest {
assignable_rec(
e.rest_array_ignoring_readonly(),
a.rest_array_ignoring_readonly(),
types,
seen,
)
} else {
assignable_rec(e, a, types, seen)
}
})
};
predicate_ok
&& params_ok
&& (re.is_void()
|| (ra.is_void() && matches!(re.peel(), Type::TypeVar(_) | Type::Unknown))
|| assignable_rec(ra, re, types, seen))
}
(
Type::Object {
fields: a_fields,
index: a_index,
},
Type::Object {
fields: e_fields,
index: e_index,
},
) => {
if let Some(expected_index) = e_index {
if !a_fields
.iter()
.filter(|(name, _)| {
crate::property_keys::matches_name(&expected_index.key, name, types.limits)
})
.all(|(_, f)| assignable_rec(&f.ty, &expected_index.value, types, seen))
{
return false;
}
if let Some(actual_index) = a_index
&& index_domains_related(&actual_index.key, &expected_index.key, types)
&& !assignable_rec(&actual_index.value, &expected_index.value, types, seen)
{
return false;
}
}
if e_index.is_none() && weak_type_rejects(a_fields, e_fields) {
return false;
}
e_fields.iter().all(|(k, e_field)| match a_fields.get(k) {
Some(a_field) => {
if a_field.optional && !e_field.optional {
return false;
}
assignable_rec(&a_field.read_ty(), &e_field.read_ty(), types, seen)
}
None => e_field.optional && !types.strict_subtype,
})
}
(Type::Object { .. }, Type::InterfaceRef { .. })
| (Type::InterfaceRef { .. }, Type::Object { .. }) => {
if is_host_backed_interface(expected) {
return false;
}
let expanded_pair = if matches!(expected, Type::InterfaceRef { .. }) {
expand_interface_data_shape(expected, types).map(|o| (actual.clone(), o))
} else {
expand_interface_data_shape(actual, types).map(|o| (o, expected.clone()))
};
let Some((a, e)) = expanded_pair else {
return false;
};
let key = (actual.clone(), expected.clone());
if seen.iter().any(|(sa, se)| sa == &key.0 && se == &key.1) {
return true;
}
seen.push(key);
let result = assignable_rec(&a, &e, types, seen);
seen.pop();
result
}
_ => actual == expected,
}
}
fn is_primitive(ty: &Type) -> bool {
matches!(
ty,
Type::String
| Type::StringLiteral(_)
| Type::StringEnum { .. }
| Type::Number
| Type::NumberLiteral(_)
| Type::NumberEnum { .. }
| Type::BigInt
| Type::BigIntLiteral(_)
| Type::Boolean
| Type::BooleanLiteral(_)
| Type::Null
| Type::Undefined
| Type::Void
)
}
#[derive(Clone, Copy)]
struct Arity<'a> {
params: &'a [Type],
optional: usize,
rest: bool,
}
impl Arity<'_> {
fn required(self) -> usize {
super::expr::required_count_of_function_type(self.params, self.optional, self.rest)
}
}
fn strict_arity_fits(
actual: Arity<'_>,
expected: Arity<'_>,
mut related: impl FnMut(&Type, &Type) -> bool,
) -> bool {
if !expected.rest && (actual.rest || actual.params.len() > expected.params.len()) {
return false;
}
let omittable_only_in_actual = actual.required()..expected.required();
omittable_only_in_actual.into_iter().all(|position| {
match (actual.params.get(position), expected.params.get(position)) {
(Some(actual), Some(expected)) => !related(actual, expected),
_ => true,
}
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ArgsRelation {
Related,
Unrelated,
MembersDecide,
}
fn args_relate_at_variances(
mangled: &MangledName,
name: &str,
actual_args: &[Type],
expected_args: &[Type],
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> Option<ArgsRelation> {
if actual_args.len() != expected_args.len() {
return Some(ArgsRelation::Unrelated);
}
if actual_args == expected_args {
return Some(ArgsRelation::Related);
}
let variances = types.settled_variances(mangled, name, actual_args.len())?;
let mut relation = ArgsRelation::Related;
for ((actual, expected), variance) in actual_args.iter().zip(expected_args).zip(variances) {
if variance.relates(actual, expected, |a, e| assignable_rec(a, e, types, seen)) {
continue;
}
let members_may_accept = (variance == Variance::Covariant
&& matches!(expected, Type::Void))
|| super::expr::type_contains_type_var(expected);
if !members_may_accept {
return Some(ArgsRelation::Unrelated);
}
relation = ArgsRelation::MembersDecide;
}
Some(relation)
}
fn args_relate_covariantly(
actual_args: &[Type],
expected_args: &[Type],
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
actual_args.len() == expected_args.len()
&& actual_args
.iter()
.zip(expected_args)
.all(|(actual, expected)| assignable_rec(actual, expected, types, seen))
}
pub(crate) fn drops_readonly(actual: &Type, expected: &Type) -> bool {
actual.is_readonly_array()
&& !expected.is_readonly_array()
&& match expected.peel() {
Type::Array(_) | Type::Tuple(_) => true,
Type::InterfaceRef { mangled, .. } => {
*mangled == crate::mangle::prelude("Map")
|| *mangled == crate::mangle::prelude("Set")
}
_ => false,
}
}
fn same_alias_instance(left: &Type, right: &Type) -> bool {
if left == right {
return true;
}
let (Some((left_name, left_args)), Some((right_name, right_args))) =
(alias_identity(left), alias_identity(right))
else {
return false;
};
left_name == right_name
&& left_args.len() == right_args.len()
&& left_args
.iter()
.zip(right_args)
.all(|(left, right)| same_alias_instance(left, right))
}
pub(super) fn alias_identity(ty: &Type) -> Option<(&MangledName, &[Type])> {
match ty {
Type::Alias { mangled, args, .. } | Type::AliasRef { mangled, args, .. } => {
Some((mangled, args))
}
_ => None,
}
}
pub(super) type PrivateMembers = BTreeMap<(MangledName, String), Type>;
pub(crate) fn expand_alias_ref(ty: &Type, types: TypeResolver) -> Type {
let Type::AliasRef {
mangled,
name,
args,
..
} = ty
else {
return ty.clone();
};
if super::utility_types::utility(mangled).is_some() {
return super::type_aliases::alias_ref_body(mangled, name, args, types);
}
let Some(sym) = types.lookup(mangled, name) else {
return ty.clone();
};
let TypeKind::Alias {
generics, ty: body, ..
} = &sym.kind
else {
return ty.clone();
};
if generics.is_empty() {
body.clone()
} else {
let sub = crate::typechecker::type_param_substitution::TypeParamSubstitution::from_pairs(
generics, args,
);
sub.apply_or_record(body, types.limits)
}
}
fn readonly_meets_mutator(actual: &Type, expected: &Type, types: TypeResolver) -> bool {
if !actual.is_readonly_array() || expected.is_readonly_array() {
return false;
}
let Some((mangled, _package, name, args)) = expected.interface_routing() else {
return false;
};
types
.interface_full_form(&mangled, name, &args)
.is_some_and(|form| {
form.iter()
.any(|(member, field)| !field.optional && is_mutating_member(member))
})
}
fn is_mutating_member(name: &str) -> bool {
super::expr::is_mutating_array_method(name)
|| matches!(name, "set" | "add" | "delete" | "clear")
}
fn satisfies_structurally(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
if is_host_backed_interface(expected) || is_host_backed_interface(actual) {
return false;
}
let key = (actual.clone(), expected.clone());
if seen.contains(&key) {
return true;
}
if expanding_interface_pair(actual, expected, seen) {
return true;
}
let Some((me, _pe, ne, ae)) = expected.interface_routing() else {
return false;
};
let Some((ma, _pa, na, aa)) = actual.interface_routing() else {
return false;
};
let Some(expected_form) = types.interface_full_form(&me, ne, &ae) else {
return false;
};
let Some(actual_form) = types.interface_full_form(&ma, na, &aa) else {
return false;
};
if weak_type_rejects(&actual_form, &expected_form) {
return false;
}
if let Some(index) = types.index_signature(expected) {
if !matches!(
actual.peel(),
Type::Object { .. } | Type::ClassRef { .. } | Type::InterfaceRef { .. }
) {
return false;
}
seen.push(key.clone());
let compatible = actual_form
.iter()
.filter(|(name, _)| crate::property_keys::matches_name(&index.key, name, types.limits))
.all(|(_, field)| assignable_rec(&field.ty, &index.value, types, seen))
&& types.index_signature(actual).is_none_or(|actual| {
!index_domains_related(&actual.key, &index.key, types)
|| assignable_rec(&actual.value, &index.value, types, seen)
});
seen.pop();
if !compatible {
return false;
}
}
seen.push(key);
let ok = expected_form
.iter()
.all(|(member, exp)| match actual_form.get(member) {
Some(act) => {
(exp.optional || !act.optional)
&& assignable_rec(&act.read_ty(), &exp.read_ty(), types, seen)
}
None => exp.optional,
});
seen.pop();
ok
}
fn expanding_interface_pair(actual: &Type, expected: &Type, seen: &[(Type, Type)]) -> bool {
let (
Type::InterfaceRef {
mangled: actual_name,
args: actual_args,
..
},
Type::InterfaceRef {
mangled: expected_name,
args: expected_args,
..
},
) = (actual, expected)
else {
return false;
};
let mut previous = (
type_argument_depth(actual_args),
type_argument_depth(expected_args),
);
let mut expansions = 0;
for (actual, expected) in seen.iter().rev() {
let (
Type::InterfaceRef {
mangled: ancestor_actual_name,
args: ancestor_actual_args,
..
},
Type::InterfaceRef {
mangled: ancestor_expected_name,
args: ancestor_expected_args,
..
},
) = (actual, expected)
else {
continue;
};
if ancestor_actual_name != actual_name || ancestor_expected_name != expected_name {
continue;
}
let depth = (
type_argument_depth(ancestor_actual_args),
type_argument_depth(ancestor_expected_args),
);
if depth.0 > previous.0 || depth.1 > previous.1 || depth == previous {
continue;
}
expansions += 1;
if expansions == 3 {
return true;
}
previous = depth;
}
false
}
fn type_argument_depth(args: &[Type]) -> usize {
args.iter().map(type_nesting_depth).max().unwrap_or(0)
}
fn type_nesting_depth(ty: &Type) -> usize {
1 + match ty {
Type::Array(element) | Type::Readonly(element) => type_nesting_depth(element),
Type::InterfaceRef { args, .. }
| Type::ClassRef { args, .. }
| Type::AliasRef { args, .. }
| Type::Alias { args, .. } => type_argument_depth(args),
Type::Tuple(elements) => type_argument_depth(elements),
Type::Union(elements) => type_argument_depth(elements),
Type::Function { params, ret, .. } => {
type_argument_depth(params).max(type_nesting_depth(ret))
}
Type::Refined { original, ty } => type_nesting_depth(original).max(type_nesting_depth(ty)),
Type::Object { fields, index } => fields
.values()
.map(|field| type_nesting_depth(&field.ty))
.chain(index.iter().map(|i| type_nesting_depth(&i.value)))
.max()
.unwrap_or(0),
_ => 0,
}
}
pub(super) fn weak_type_rejects(
actual_form: &BTreeMap<String, ObjectField>,
expected_form: &BTreeMap<String, ObjectField>,
) -> bool {
!expected_form.is_empty()
&& expected_form.values().all(|f| f.optional)
&& !actual_form.is_empty()
&& !expected_form.keys().any(|k| actual_form.contains_key(k))
}
pub(crate) fn expand_interface_data_shape(ty: &Type, types: TypeResolver) -> Option<Type> {
let Type::InterfaceRef {
mangled,
name,
args,
..
} = ty
else {
return None;
};
Some(Type::Object {
index: types.index_signature(ty),
fields: types.interface_data_shape(mangled, name, args)?,
})
}
fn relate_type_operators(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> Option<bool> {
let (actual, expected) = (actual.without_aliases(), expected.without_aliases());
let actual_form = actual.is_type_operator();
let expected_form = expected.is_type_operator();
if !actual_form && !expected_form {
return None;
}
if (actual_form && mentions_signature_var(actual))
|| (expected_form && mentions_signature_var(expected))
{
return Some(true);
}
if matches!(actual.peel(), Type::Error | Type::Never)
|| matches!(expected.peel(), Type::Error | Type::Unknown)
|| expected.holds_every_value()
|| actual == expected
{
return Some(true);
}
if actual_form
&& let Type::Union(members) = expected.peel()
&& members
.iter()
.any(|member| assignable_rec(actual, member, types, seen))
{
return Some(true);
}
if let Some(keys) = mapped_keys(expected) {
let fits = assignable_rec(actual, &keys, types, seen);
if fits || actual_form {
return Some(fits);
}
}
if let Some(keys) = mapped_keys(actual) {
return Some(assignable_rec(&keys, expected, types, seen));
}
if let Some(related) = relate_same_shape(actual, expected, types, seen) {
return Some(related);
}
if relate_subset_of_operand(actual, expected, types, seen) {
return Some(true);
}
if let Some(property) = mapped_property(actual) {
let property = match property {
MappedProperty::Same(property) => property,
MappedProperty::Optional(property) => Type::union(vec![property, Type::Undefined]),
};
return Some(assignable_rec(&property, expected, types, seen));
}
if expected_form {
return Some(relate_into_form(actual, expected, types, seen));
}
Some(relate_out_of_form(actual, expected, types, seen))
}
fn relate_same_shape(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> Option<bool> {
use super::utility_types::{Utility, deferred_application};
if let (Type::KeyOf(actual_operand), Type::KeyOf(expected_operand)) = (actual, expected) {
let (actual_base, expected_base) = (
homomorphic_base(actual_operand),
homomorphic_base(expected_operand),
);
return Some(
actual_base == expected_base || assignable_rec(expected_base, actual_base, types, seen),
);
}
if let (
Type::IndexedAccess {
object: actual_object,
index: actual_index,
},
Type::IndexedAccess {
object: expected_object,
index: expected_index,
},
) = (actual, expected)
&& assignable_rec(actual_object, expected_object, types, seen)
&& assignable_rec(actual_index, expected_index, types, seen)
{
return Some(true);
}
if let (Some((actual_base, actual_optional)), Some((expected_base, expected_optional))) =
(homomorphic_wrapping(actual), homomorphic_wrapping(expected))
&& actual_base == expected_base
{
return Some(match (actual_optional, expected_optional) {
(_, Optionality::Optional) => true,
(Optionality::Optional, _) => false,
(_, Optionality::Required) => actual_optional == Optionality::Required,
(_, Optionality::Unchanged) => true,
});
}
let (Some((left, left_args)), Some((right, right_args))) =
(deferred_application(actual), deferred_application(expected))
else {
return None;
};
if left != right || left_args.len() != right_args.len() {
return None;
}
let covariant = match (left, left_args, right_args) {
(Utility::Exclude | Utility::Extract, [left, left_filter], [right, right_filter]) => {
left_filter == right_filter && assignable_rec(left, right, types, seen)
}
(
Utility::Exclude
| Utility::Extract
| Utility::Pick
| Utility::Omit
| Utility::Parameters,
_,
_,
) => false,
_ => left_args
.iter()
.zip(right_args)
.all(|(left, right)| assignable_rec(left, right, types, seen)),
};
covariant.then_some(true)
}
fn relate_subset_of_operand(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
use super::utility_types::{Utility, deferred_application};
matches!(
deferred_application(actual),
Some((
Utility::Readonly | Utility::Required | Utility::Exclude | Utility::Extract,
[operand, ..],
)) if assignable_rec(operand, expected, types, seen)
)
}
fn relate_into_form(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
use super::utility_types::{Utility, deferred_application};
if let Some((utility, args)) = deferred_application(expected) {
return match (utility, args) {
(Utility::Partial, _) if actual.is_empty_object() => true,
(
Utility::Partial | Utility::Readonly | Utility::Pick | Utility::Omit,
[operand, ..],
) => operand == actual,
_ => false,
};
}
if let Type::KeyOf(operand) = expected
&& !actual.is_type_operator()
{
let bound = concrete_upper_bound(operand, types);
if matches!(bound.peel(), Type::Error) {
return false;
}
return super::utility_types::keyof_type(&bound, types)
.is_some_and(|keys| assignable_rec(actual, &keys, types, seen));
}
if let Type::IndexedAccess { object, index } = expected
&& !actual.is_type_operator()
{
if matches!(actual.peel(), Type::Undefined)
&& matches!(deferred_application(object), Some((Utility::Partial, _)))
{
return true;
}
return bound_properties(object, index, types).is_some_and(|properties| {
properties.iter().all(|property| {
let unit = property.peel();
(unit.is_literal_type() || matches!(unit, Type::Null | Type::Undefined))
&& assignable_rec(actual, unit, types, seen)
})
});
}
false
}
pub(super) fn fits_bound_property_write(actual: &Type, target: &Type, types: TypeResolver) -> bool {
indexed_bound_properties(target, types).is_some_and(|properties| {
properties
.iter()
.all(|property| assignable(actual, property, types))
})
}
pub(super) fn write_context(target: &Type, types: TypeResolver) -> Option<Type> {
let Some(properties) = indexed_bound_properties(target, types) else {
return Some(target.clone());
};
match properties.split_first() {
Some((first, rest)) if rest.iter().all(|property| property == first) => Some(first.clone()),
_ => None,
}
}
fn indexed_bound_properties(target: &Type, types: TypeResolver) -> Option<Vec<Type>> {
if let Some(property) = written_property(target) {
return indexed_bound_properties(&property, types);
}
let Type::IndexedAccess { object, index } = target.without_aliases() else {
return None;
};
bound_properties(object, index, types)
}
fn bound_properties(object: &Type, index: &Type, types: TypeResolver) -> Option<Vec<Type>> {
use super::utility_types::{index_access_type, members};
let object_bound = declared_bound(object, types)?;
let index_bound = concrete_upper_bound(index, types);
if has_no_concrete_bound(&object_bound) || has_no_concrete_bound(&index_bound) {
return None;
}
let keys = members(&index_bound);
if keys.is_empty() || !keys.iter().all(Type::is_literal_type) {
return None;
}
let object_bound = if object_bound == *object {
object_bound
} else {
without_index_signature(object_bound, types)
};
keys.iter()
.map(|key| index_access_type(&object_bound, key, types).ok())
.collect()
}
fn has_no_concrete_bound(ty: &Type) -> bool {
let open = ty.without_aliases();
open.lowers_to_erased_slot() || matches!(open, Type::Error | Type::Unknown)
}
fn declared_bound(ty: &Type, types: TypeResolver) -> Option<Type> {
let open = ty.without_aliases();
if let Type::GenericParam { id, .. } = open {
let mut bound = types.generic_constraints.get(id)?.clone();
for _ in 0..crate::compiler_limits::MAX_TYPE_RESOLUTION_DEPTH {
let Type::GenericParam { id, .. } = bound.without_aliases() else {
return declared_bound(&bound, types);
};
bound = types.generic_constraints.get(id)?.clone();
}
return None;
}
if open.is_type_operator() {
return closed_bounds(open, types).then(|| base_constraint(open, types));
}
Some(ty.clone())
}
fn closed_bounds(ty: &Type, types: TypeResolver) -> bool {
let mut closed = true;
let mut pending = vec![ty];
while let Some(ty) = pending.pop() {
if let Type::GenericParam { id, .. } = ty
&& types
.generic_constraints
.get(id)
.is_some_and(super::utility_types::holds_type_parameter)
{
closed = false;
}
crate::type_size::for_each_child(ty, |child| pending.push(child));
}
closed
}
fn without_index_signature(ty: Type, types: TypeResolver) -> Type {
match ty.peel() {
Type::Object { fields, .. } => Type::Object {
fields: fields.clone(),
index: None,
},
Type::InterfaceRef { .. } if types.index_signature(&ty).is_some() => {
match expand_interface_data_shape(&ty, types) {
Some(Type::Object { fields, .. }) => Type::Object {
fields,
index: None,
},
_ => ty,
}
}
_ => ty,
}
}
fn concrete_upper_bound(ty: &Type, types: TypeResolver) -> Type {
let bound = upper_bound(ty, types, 0);
if matches!(bound.without_aliases(), Type::GenericParam { .. }) && bound != *ty {
return upper_bound(&bound, types, 0);
}
bound
}
fn relate_out_of_form(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
let constraint = base_constraint(actual, types);
if constraint.is_type_operator() || matches!(constraint.peel(), Type::Never | Type::Error) {
return false;
}
assignable_rec(&constraint, expected, types, seen)
}
fn mapped_keys(form: &Type) -> Option<Type> {
use super::utility_types::{Utility, deferred, deferred_application};
let Type::KeyOf(operand) = form.without_aliases() else {
return None;
};
match deferred_application(operand)? {
(Utility::Pick, [_, keys]) => Some(keys.clone()),
(Utility::Omit, [object, keys]) => Some(deferred(
Utility::Exclude,
vec![Type::KeyOf(Box::new(object.clone())), keys.clone()],
)),
(Utility::Partial | Utility::Readonly | Utility::Required, [object]) => {
Some(Type::KeyOf(Box::new(object.clone())))
}
_ => None,
}
}
fn written_property(form: &Type) -> Option<Type> {
use super::utility_types::{Utility, deferred_application};
let Type::IndexedAccess { object, index } = form.without_aliases() else {
return None;
};
let mut object = object.as_ref();
let mut mapped = false;
while let Some((Utility::Readonly | Utility::Pick, [operand, ..])) =
deferred_application(object)
{
object = operand;
mapped = true;
}
mapped.then(|| Type::indexed_access(object.clone(), index.as_ref().clone()))
}
enum MappedProperty {
Same(Type),
Optional(Type),
}
fn mapped_property(form: &Type) -> Option<MappedProperty> {
use super::utility_types::{Utility, deferred_application};
let Type::IndexedAccess { object, index } = form.without_aliases() else {
return None;
};
let mut object = object.as_ref();
let mut mapped = false;
loop {
match deferred_application(object) {
Some((Utility::Readonly | Utility::Required | Utility::Pick, [operand, ..])) => {
object = operand;
mapped = true;
}
Some((Utility::Partial, [operand])) => {
return Some(MappedProperty::Optional(Type::indexed_access(
operand.clone(),
index.as_ref().clone(),
)));
}
_ => break,
}
}
mapped
.then(|| MappedProperty::Same(Type::indexed_access(object.clone(), index.as_ref().clone())))
}
fn rewrite_to_operand_form(ty: &Type) -> Option<Type> {
use super::utility_types::{Utility, deferred_application};
match deferred_application(ty) {
Some((Utility::NoInfer, [operand])) => Some(operand.clone()),
Some((Utility::NonNullable, [operand])) => Some(Type::Refined {
original: Box::new(operand.clone()),
ty: Box::new(Type::empty_object()),
}),
_ => None,
}
}
fn homomorphic_base(ty: &Type) -> &Type {
homomorphic_wrapping(ty).map_or(ty, |(base, _)| base)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Optionality {
Unchanged,
Optional,
Required,
}
fn homomorphic_wrapping(ty: &Type) -> Option<(&Type, Optionality)> {
use super::utility_types::{Utility, deferred_application};
let (base, optionality) = match deferred_application(ty) {
Some((Utility::Partial, [operand])) => {
(homomorphic_wrapping(operand)?.0, Optionality::Optional)
}
Some((Utility::Required, [operand])) => {
(homomorphic_wrapping(operand)?.0, Optionality::Required)
}
Some((Utility::Readonly, [operand])) => homomorphic_wrapping(operand)?,
Some(_) => return None,
None => (ty, Optionality::Unchanged),
};
super::utility_types::holds_type_parameter(base).then_some((base, optionality))
}
fn mentions_signature_var(ty: &Type) -> bool {
let mut pending = vec![ty];
while let Some(ty) = pending.pop() {
if matches!(ty, Type::TypeVar(_)) {
return true;
}
crate::type_size::for_each_child(ty, |child| pending.push(child));
}
false
}
pub(super) fn base_constraint(ty: &Type, types: TypeResolver) -> Type {
use super::utility_types::{Utility, deferred_application, index_access_type};
let upper = |operand: &Type| concrete_upper_bound(operand, types);
let evaluate = |ty: Type| super::utility_types::normalize(&ty, types).unwrap_or(Type::Error);
if let Some(property) = mapped_property(ty) {
return match property {
MappedProperty::Same(property) => base_constraint(&property, types),
MappedProperty::Optional(property) => {
Type::union(vec![base_constraint(&property, types), Type::Undefined])
}
};
}
match ty.without_aliases() {
Type::KeyOf(_) => Type::union(vec![Type::String, Type::Number]),
Type::IndexedAccess { object, index } => {
index_access_type(&upper(object), &upper(index), types).unwrap_or(Type::Unknown)
}
operator => match deferred_application(operator) {
Some((Utility::Exclude, [operand, filter])) => {
let operand = upper(operand);
if super::utility_types::holds_type_parameter(filter) {
return operand;
}
evaluate(super::utility_types::deferred(
Utility::Exclude,
vec![operand, filter.clone()],
))
}
Some((Utility::Extract, [operand, filter])) => {
let operand = upper(operand);
if matches!(operand.peel(), Type::Unknown)
|| super::utility_types::holds_type_parameter(filter)
{
return upper(filter);
}
let filter_members = super::utility_types::members(filter);
let bounded: Vec<Type> = super::utility_types::members(&operand)
.into_iter()
.flat_map(|member| {
if assignable(&member, filter, types) {
vec![member]
} else {
filter_members
.iter()
.filter(|part| assignable(part, &member, types))
.cloned()
.collect()
}
})
.collect();
if bounded.is_empty() {
return upper(filter);
}
Type::union(bounded)
}
Some((Utility::Pick, [object, Type::KeyOf(keys_of)])) if keys_of.as_ref() == object => {
upper(object)
}
Some((Utility::Pick, [object, keys])) => evaluate(super::utility_types::deferred(
Utility::Pick,
vec![upper(object), lower_keys(keys, types)],
)),
Some((Utility::NonNullable, [operand])) => {
let kept = Type::union(
super::utility_types::members(&upper(operand))
.into_iter()
.filter(|member| !matches!(member.peel(), Type::Null | Type::Undefined))
.collect(),
);
upper(&kept)
}
Some((Utility::Parameters, _)) => Type::Unknown,
Some((utility, args)) => evaluate(super::utility_types::deferred(
utility,
args.iter().map(&upper).collect(),
)),
None => substitute_bounds(operator, types),
},
}
}
fn lower_keys(keys: &Type, types: TypeResolver) -> Type {
use super::utility_types::{
Utility, deferred, deferred_application, holds_type_parameter, keyof_type, normalize,
};
let evaluate = |ty: Type| normalize(&ty, types).unwrap_or(Type::Never);
let open = keys.without_aliases();
if !holds_type_parameter(open) {
return keys.clone();
}
match open {
Type::KeyOf(operand) => {
keyof_type(&concrete_upper_bound(operand, types), types).unwrap_or(Type::Never)
}
Type::Union(members) => Type::union(
members
.iter()
.map(|member| lower_keys(member, types))
.collect(),
),
other => match deferred_application(other) {
Some((Utility::Extract, [operand, filter])) if !holds_type_parameter(filter) => {
evaluate(deferred(
Utility::Extract,
vec![lower_keys(operand, types), filter.clone()],
))
}
Some((Utility::Exclude, [operand, filter])) => evaluate(deferred(
Utility::Exclude,
vec![
lower_keys(operand, types),
concrete_upper_bound(filter, types),
],
)),
_ => Type::Never,
},
}
}
fn upper_bound(ty: &Type, types: TypeResolver, depth: u32) -> Type {
if depth > crate::compiler_limits::MAX_TYPE_RESOLUTION_DEPTH {
return Type::Error;
}
if ty.without_aliases().is_type_operator() {
return base_constraint(ty, types);
}
if let Type::GenericParam { id, .. } = ty.without_aliases()
&& let Some(bound) = types.generic_constraints.get(id)
&& (bound.without_aliases().is_type_operator()
|| matches!(bound.without_aliases(), Type::GenericParam { .. }))
{
return upper_bound(bound, types, depth + 1);
}
substitute_bounds(ty, types)
}
pub(super) fn substitute_bounds(ty: &Type, types: TypeResolver) -> Type {
fn replace(
ty: &Type,
types: TypeResolver,
open: &mut Vec<u32>,
budget: &mut crate::type_size::TypeBudget<'_>,
depth: u32,
) -> Result<Type, crate::type_size::TypeTooLarge> {
budget.charge(depth)?;
match ty {
Type::GenericParam { id, .. } if open.contains(id) => Ok(ty.clone()),
Type::GenericParam { id, .. } => match types.generic_constraints.get(id) {
Some(bound) => {
open.push(*id);
let replaced = replace(bound, types, open, budget, depth);
open.pop();
replaced
}
None => Ok(Type::Unknown),
},
_ => crate::type_size::map_children(ty, |child| {
replace(child, types, open, budget, depth.saturating_add(1))
}),
}
}
let mut budget = types.limits.budget();
match replace(ty, types, &mut Vec::new(), &mut budget, 1) {
Ok(ty) => ty,
Err(exceeded) => {
types.limits.record(exceeded);
Type::Error
}
}
}
fn fits_symbolic_mapping(chain: &[super::utility_types::Utility], actual: &Type) -> bool {
use super::utility_types::{Utility, chain_preserves, symbolic_chain};
if let Some(actual_chain) = symbolic_chain(actual) {
return actual_chain == chain || (chain.len() == 1 && actual_chain.last() == chain.last());
}
match actual.without_aliases() {
Type::Never | Type::Error => true,
Type::StringLiteral(text) => chain_preserves(chain, text),
Type::StringEnum {
member: Some(member),
..
} => chain_preserves(chain, &member.value),
Type::TemplateLiteral {
parts,
substitutions,
} => {
if let [mapping @ (Utility::Capitalize | Utility::Uncapitalize)] = chain {
return match parts.first() {
Some(head) if !head.is_empty() => chain_preserves(&[*mapping], head),
_ => substitutions
.first()
.is_none_or(|hole| fits_symbolic_mapping(chain, hole)),
};
}
parts.iter().all(|part| chain_preserves(chain, part))
&& substitutions
.iter()
.all(|hole| fits_symbolic_mapping(chain, hole))
}
_ => false,
}
}
pub(crate) fn is_host_backed_interface(ty: &Type) -> bool {
let Type::InterfaceRef { mangled, .. } = ty.peel() else {
return false;
};
crate::stdlib::host_views().any(|view| {
mangled.as_str().split_once(crate::mangle::SEP) == Some((view.module, view.interface))
})
}
pub(super) fn literal_value_of(kind: &crate::TypedExprKind) -> Option<narrowing::LiteralValue> {
use crate::TypedExprKind;
match kind {
TypedExprKind::Number(n) => Some(narrowing::LiteralValue::Number(
crate::types::LiteralF64(*n),
)),
TypedExprKind::String(s) => Some(narrowing::LiteralValue::String(s.clone())),
TypedExprKind::Boolean(b) => Some(narrowing::LiteralValue::Boolean(*b)),
TypedExprKind::BigInt(digits) => match crate::types::bigint_literal_type(digits) {
Type::BigIntLiteral(canonical) => Some(narrowing::LiteralValue::BigInt(canonical)),
_ => None,
},
_ => None,
}
}
pub(super) fn literal_to_type(lit: &narrowing::LiteralValue) -> Type {
match lit {
narrowing::LiteralValue::Number(n) => Type::NumberLiteral(*n),
narrowing::LiteralValue::String(s) => Type::StringLiteral(s.clone()),
narrowing::LiteralValue::Boolean(b) => Type::BooleanLiteral(*b),
narrowing::LiteralValue::BigInt(digits) => Type::BigIntLiteral(digits.clone()),
narrowing::LiteralValue::Null => Type::Null,
narrowing::LiteralValue::Undefined => Type::Undefined,
}
}
fn refinement_fits_union(
actual: &Type,
expected: &Type,
types: TypeResolver,
seen: &mut Vec<(Type, Type)>,
) -> bool {
let Type::Union(members) = expected.without_aliases() else {
return false;
};
if members
.iter()
.any(|member| assignable_rec(actual, member, types, seen))
{
return true;
}
let Some(pieces) = actual.refinement_pieces() else {
return false;
};
pieces
.iter()
.all(|piece| assignable_rec(piece, expected, types, seen))
}
pub(super) fn index_domains_related(actual: &Type, expected: &Type, types: TypeResolver) -> bool {
matches!(actual.peel(), Type::String)
|| matches!(expected.peel(), Type::String)
|| assignable(actual, expected, types)
|| assignable(expected, actual, types)
}
#[cfg(test)]
mod tests {
use super::*;
fn assignable(actual: &Type, expected: &Type) -> bool {
let types = TypeNamespace::new();
let registry = TypeRegistry::new();
let limits = crate::type_size::TypeLimits::default();
super::assignable(
actual,
expected,
TypeResolver {
types: &types,
registry: ®istry,
limits: &limits,
strict_subtype: false,
generic_constraints: &BTreeMap::new(),
},
)
}
fn fn_ty(params: Vec<Type>, ret: Type) -> Type {
Type::Function {
params,
ret: Box::new(ret),
predicate: None,
has_rest: false,
optional: 0,
}
}
#[test]
fn undefined_is_distinct_from_null_and_assignable_to_void() {
assert!(assignable(&Type::Undefined, &Type::Void));
assert!(assignable(&Type::Void, &Type::Unknown));
assert!(!assignable(&Type::Void, &Type::Undefined));
assert!(!assignable(&Type::Null, &Type::Undefined));
assert!(!assignable(&Type::Undefined, &Type::Null));
}
#[test]
fn optional_tuple_accepts_omission_but_cannot_satisfy_required_position() {
let optional = Type::Tuple(crate::types::TupleType {
elements: vec![
Type::Number,
Type::union(vec![Type::String, Type::Undefined]),
],
optional: 1,
});
let short = Type::Tuple(vec![Type::Number].into());
let full = Type::Tuple(vec![Type::Number, Type::String].into());
assert!(assignable(&short, &optional));
assert!(assignable(&full, &optional));
assert!(!assignable(&optional, &short));
assert!(!assignable(&optional, &full));
}
#[test]
fn optional_function_parameter_can_be_omitted_by_target_callers() {
let optional = Type::Function {
params: vec![Type::union(vec![Type::String, Type::Undefined])],
ret: Box::new(Type::Void),
predicate: None,
has_rest: false,
optional: 1,
};
let no_args = fn_ty(vec![], Type::Void);
let required = fn_ty(vec![Type::String], Type::Void);
assert!(assignable(&optional, &no_args));
assert!(assignable(&optional, &required));
assert!(!assignable(&required, &optional));
let required_union = fn_ty(
vec![Type::union(vec![Type::String, Type::Undefined])],
Type::Void,
);
assert!(assignable(&required_union, &optional));
assert!(!assignable(&required_union, &no_args));
}
#[test]
fn optional_object_field_accepts_explicit_undefined() {
let object = |field| Type::Object {
fields: [("value".to_string(), field)].into_iter().collect(),
index: None,
};
let optional = object(ObjectField::optional(Type::String));
assert!(assignable(
&object(ObjectField::required(Type::Undefined)),
&optional
));
assert!(!assignable(
&object(ObjectField::required(Type::Null)),
&optional
));
}
#[test]
fn assignable_function_exact_match() {
let a = fn_ty(vec![Type::Number], Type::String);
let b = fn_ty(vec![Type::Number], Type::String);
assert!(assignable(&a, &b));
assert!(assignable(&b, &a));
}
#[test]
fn assignable_function_may_take_fewer_params_but_not_more() {
let one = fn_ty(vec![Type::Number], Type::Void);
let two = fn_ty(vec![Type::Number, Type::Number], Type::Void);
assert!(assignable(&one, &two));
assert!(!assignable(&two, &one));
}
#[test]
fn assignable_function_return_mismatch() {
let a = fn_ty(vec![Type::Number], Type::Number);
let b = fn_ty(vec![Type::Number], Type::String);
assert!(!assignable(&a, &b));
assert!(!assignable(&b, &a));
}
#[test]
fn assignable_function_param_mismatch() {
let a = fn_ty(vec![Type::Number], Type::Void);
let b = fn_ty(vec![Type::String], Type::Void);
assert!(!assignable(&a, &b));
assert!(!assignable(&b, &a));
}
#[test]
fn assignable_function_error_param_is_bidirectional() {
let a = fn_ty(vec![Type::Error], Type::Void);
let b = fn_ty(vec![Type::Number], Type::Void);
assert!(assignable(&a, &b));
assert!(assignable(&b, &a));
}
#[test]
fn assignable_function_error_return_is_bidirectional() {
let a = fn_ty(vec![Type::Number], Type::Error);
let b = fn_ty(vec![Type::Number], Type::String);
assert!(assignable(&a, &b));
assert!(assignable(&b, &a));
}
#[test]
fn assignable_function_param_contravariance_uses_assignable() {
let actual = fn_ty(vec![Type::Number], Type::Void);
let expected = fn_ty(vec![Type::Error], Type::Void);
assert!(assignable(&actual, &expected));
assert!(assignable(&expected, &actual));
}
#[test]
fn assignable_function_return_covariance_uses_assignable() {
let actual = fn_ty(vec![Type::Number], Type::Error);
let expected = fn_ty(vec![Type::Number], Type::String);
assert!(assignable(&actual, &expected));
assert!(assignable(&expected, &actual));
}
#[test]
fn assignable_nested_function_recurses() {
let inner = fn_ty(vec![Type::Number], Type::Number);
let outer = fn_ty(vec![Type::Number], inner.clone());
assert!(assignable(&outer, &outer));
let inner_bad = fn_ty(vec![Type::Number], Type::String);
let outer_bad = fn_ty(vec![Type::Number], inner_bad);
assert!(!assignable(&outer, &outer_bad));
assert!(!assignable(&outer_bad, &outer));
}
#[test]
fn assignable_zero_arity_function() {
let a = fn_ty(vec![], Type::Number);
let b = fn_ty(vec![], Type::Number);
assert!(assignable(&a, &b));
}
#[test]
fn assignable_function_vs_non_function_rejected() {
let a = fn_ty(vec![Type::Number], Type::Void);
let b = Type::Number;
assert!(!assignable(&a, &b));
assert!(!assignable(&b, &a));
}
#[test]
fn assignable_non_function_still_structural_eq() {
assert!(assignable(&Type::Number, &Type::Number));
assert!(!assignable(&Type::Number, &Type::String));
assert!(assignable(&Type::Error, &Type::String));
assert!(assignable(&Type::String, &Type::Error));
}
#[test]
fn assignable_member_to_union() {
let u = Type::union(vec![Type::Number, Type::String]);
assert!(assignable(&Type::Number, &u));
assert!(assignable(&Type::String, &u));
}
#[test]
fn assignable_non_member_to_union_fails() {
let u = Type::union(vec![Type::Number, Type::String]);
assert!(!assignable(&Type::Boolean, &u));
}
#[test]
fn assignable_union_to_superset_union() {
let actual = Type::union(vec![Type::Number, Type::String]);
let expected = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
assert!(assignable(&actual, &expected));
}
#[test]
fn assignable_union_to_subset_union_fails() {
let actual = Type::union(vec![Type::Number, Type::String]);
let expected = Type::union(vec![Type::Number, Type::Boolean]);
assert!(!assignable(&actual, &expected));
}
#[test]
fn assignable_union_to_member_fails() {
let actual = Type::union(vec![Type::Number, Type::String]);
assert!(!assignable(&actual, &Type::Number));
}
#[test]
fn assignable_union_with_error_member_is_bidirectional() {
let with_error = Type::union(vec![Type::Number, Type::Error]);
assert_eq!(with_error, Type::Error);
assert!(assignable(&with_error, &Type::Boolean));
assert!(assignable(&Type::Boolean, &with_error));
}
#[test]
fn assignable_array_of_union_recurses() {
let union_elem = Type::union(vec![Type::Number, Type::String]);
let union_arr = Type::Array(Box::new(union_elem));
assert!(assignable(&union_arr, &union_arr));
let narrow_arr = Type::Array(Box::new(Type::Number));
assert!(assignable(&narrow_arr, &union_arr));
assert!(!assignable(&union_arr, &narrow_arr));
}
#[test]
fn readonly_accepts_mutable_but_not_the_reverse() {
let array = |elem: Type| Type::Array(Box::new(elem));
let readonly = |ty: Type| Type::Readonly(Box::new(ty));
let numbers = array(Type::Number);
let either = array(Type::union(vec![Type::Number, Type::String]));
assert!(assignable(&numbers, &readonly(numbers.clone())));
assert!(!assignable(&readonly(numbers.clone()), &numbers));
assert!(assignable(
&readonly(numbers.clone()),
&readonly(either.clone())
));
assert!(!assignable(&readonly(either), &readonly(numbers.clone())));
let pair = Type::Tuple(vec![Type::Number, Type::Number].into());
assert!(assignable(&pair, &readonly(pair.clone())));
assert!(!assignable(&readonly(pair.clone()), &pair));
assert!(assignable(
&readonly(pair.clone()),
&readonly(numbers.clone())
));
assert!(!assignable(&readonly(pair), &numbers));
assert!(assignable(&readonly(numbers), &Type::Unknown));
}
#[test]
fn assignable_anything_to_unknown() {
for t in [
Type::Number,
Type::String,
Type::Boolean,
Type::Null,
Type::Array(Box::new(Type::Number)),
Type::Object {
index: None,
fields: std::collections::BTreeMap::new(),
},
Type::union(vec![Type::Number, Type::String]),
] {
assert!(
assignable(&t, &Type::Unknown),
"{t} should be assignable to unknown",
);
}
}
#[test]
fn assignable_unknown_to_concrete_fails() {
for t in [
Type::Number,
Type::String,
Type::Boolean,
Type::Null,
Type::Array(Box::new(Type::Number)),
] {
assert!(
!assignable(&Type::Unknown, &t),
"unknown should NOT flow into {t}",
);
}
}
#[test]
fn assignable_unknown_to_unknown_identity() {
assert!(assignable(&Type::Unknown, &Type::Unknown));
}
#[test]
fn union_with_unknown_collapses() {
assert_eq!(
Type::union(vec![Type::Number, Type::Unknown]),
Type::Unknown,
);
assert_eq!(
Type::union(vec![Type::Unknown, Type::String, Type::Null]),
Type::Unknown,
);
}
fn obj(fields: Vec<(&str, Type, bool)>) -> Type {
Type::Object {
index: None,
fields: fields
.into_iter()
.map(|(k, ty, optional)| {
(
k.to_string(),
crate::ObjectField {
ty,
optional,
readonly: false,
method: false,
},
)
})
.collect(),
}
}
fn iref(name: &str) -> Type {
Type::interface_ref(
crate::types::Package::user(),
name,
crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, name),
Vec::new(),
)
}
fn ns_with_interface(
name: &'static str,
properties: Vec<(&'static str, Type, bool)>,
has_method: bool,
) -> TypeNamespace<'static> {
use crate::package_declaration::{Dispatch, MethodSig, PropertySig};
use std::collections::BTreeMap;
let properties: BTreeMap<String, PropertySig> = properties
.into_iter()
.map(|(field, ty, optional)| {
(
field.to_string(),
PropertySig {
ty,
readonly: false,
intrinsic: false,
optional,
doc: None,
},
)
})
.collect();
let mut methods = BTreeMap::new();
if has_method {
methods.insert(
"m".to_string(),
MethodSig {
optional: false,
generics: Default::default(),
params: Vec::new(),
ret: Type::Void,
predicate: None,
doc: None,
},
);
}
let mut ns = TypeNamespace::new();
ns.insert(
name.to_string(),
crate::mangle::USER_PACKAGE.to_string(),
crate::TypeSymbol {
name: name.to_string(),
mangled_name: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, name),
declaration_span: crate::Span::at(crate::FileId(0)),
kind: TypeKind::Interface {
index: None,
generics: Default::default(),
methods,
properties,
dispatch: Dispatch::VTable,
doc: None,
},
},
);
ns
}
fn assignable_with(actual: &Type, expected: &Type, ns: &TypeNamespace) -> bool {
let registry = TypeRegistry::new();
let limits = crate::type_size::TypeLimits::default();
super::assignable(
actual,
expected,
TypeResolver {
types: ns,
registry: ®istry,
limits: &limits,
strict_subtype: false,
generic_constraints: &BTreeMap::new(),
},
)
}
#[test]
fn object_value_assignable_to_data_only_interface_both_ways() {
let ns = ns_with_interface("Issue", vec![("id", Type::String, false)], false);
let object = obj(vec![("id", Type::String, false)]);
let iface = iref("Issue");
assert!(assignable_with(&object, &iface, &ns), "object -> interface");
assert!(assignable_with(&iface, &object, &ns), "interface -> object");
}
#[test]
fn object_width_subtyping_into_interface() {
let ns = ns_with_interface("Issue", vec![("id", Type::String, false)], false);
let wider = obj(vec![
("id", Type::String, false),
("extra", Type::Number, false),
]);
assert!(assignable_with(&wider, &iref("Issue"), &ns));
}
#[test]
fn object_missing_required_interface_field_rejected() {
let ns = ns_with_interface(
"Issue",
vec![("id", Type::String, false), ("title", Type::String, false)],
false,
);
let only_id = obj(vec![("id", Type::String, false)]);
assert!(!assignable_with(&only_id, &iref("Issue"), &ns));
}
#[test]
fn method_bearing_interface_not_structurally_assignable() {
let ns = ns_with_interface("Logger", vec![("id", Type::String, false)], true);
let object = obj(vec![("id", Type::String, false)]);
assert!(
!assignable_with(&object, &iref("Logger"), &ns),
"object -> method iface"
);
assert!(
!assignable_with(&iref("Logger"), &object, &ns),
"method iface -> object"
);
}
}