use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::mangle::MangledName;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct EnumMember<V> {
pub name: String,
pub value: V,
pub member_count: usize,
}
impl<V> EnumMember<V> {
pub fn new(name: &str, value: V, member_count: usize) -> Self {
EnumMember {
name: name.to_string(),
value,
member_count,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum EnumValue {
Number(LiteralF64),
String(String),
}
impl EnumValue {
pub fn literal_type(&self) -> Type {
match self {
EnumValue::Number(value) => Type::NumberLiteral(*value),
EnumValue::String(value) => Type::StringLiteral(value.clone()),
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct LiteralF64(#[serde(with = "crate::artifact_f64")] pub f64);
impl PartialEq for LiteralF64 {
fn eq(&self, other: &Self) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for LiteralF64 {}
impl PartialOrd for LiteralF64 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LiteralF64 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
impl std::hash::Hash for LiteralF64 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Package(pub String);
impl Package {
pub fn prelude() -> Self {
Self(crate::mangle::PRELUDE_PACKAGE.to_string())
}
pub fn user() -> Self {
Self(crate::mangle::USER_PACKAGE.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl PartialEq for Package {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for Package {}
impl PartialOrd for Package {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Package {
fn cmp(&self, _: &Self) -> std::cmp::Ordering {
std::cmp::Ordering::Equal
}
}
impl std::hash::Hash for Package {
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ObjectField {
pub ty: Type,
pub optional: bool,
pub readonly: bool,
#[serde(default)]
pub method: bool,
}
impl ObjectField {
pub fn required(ty: Type) -> Self {
Self {
ty,
optional: false,
readonly: false,
method: false,
}
}
pub fn optional(ty: Type) -> Self {
Self {
ty,
optional: true,
readonly: false,
method: false,
}
}
pub fn read_ty(&self) -> Type {
Self::widen_optional(self.optional, self.ty.clone())
}
pub fn widen_optional(optional: bool, ty: Type) -> Type {
if optional {
Type::union(vec![ty, Type::Undefined])
} else {
ty
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IndexSignature {
pub value: Box<Type>,
pub readonly: bool,
}
impl IndexSignature {
pub fn map_value(&self, transform: impl FnOnce(&Type) -> Type) -> Self {
Self {
value: Box::new(transform(&self.value)),
readonly: self.readonly,
}
}
pub fn try_map_value<E>(
&self,
transform: impl FnOnce(&Type) -> Result<Type, E>,
) -> Result<Self, E> {
Ok(Self {
value: Box::new(transform(&self.value)?),
readonly: self.readonly,
})
}
pub fn read_ty(&self) -> Type {
Type::union(vec![(*self.value).clone(), Type::Undefined])
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TypePredicate {
pub parameter_index: u32,
pub asserted_type: Type,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TupleType {
pub elements: Vec<Type>,
pub optional: usize,
}
impl TupleType {
pub fn required_len(&self) -> usize {
self.elements.len().saturating_sub(self.optional)
}
pub fn map(&self, transform: impl FnMut(&Type) -> Type) -> Self {
Self {
elements: self.elements.iter().map(transform).collect(),
optional: self.optional,
}
}
pub fn try_map<E>(&self, transform: impl FnMut(&Type) -> Result<Type, E>) -> Result<Self, E> {
Ok(Self {
elements: self
.elements
.iter()
.map(transform)
.collect::<Result<_, _>>()?,
optional: self.optional,
})
}
}
impl From<Vec<Type>> for TupleType {
fn from(elements: Vec<Type>) -> Self {
Self {
elements,
optional: 0,
}
}
}
impl std::ops::Deref for TupleType {
type Target = [Type];
fn deref(&self) -> &Self::Target {
&self.elements
}
}
impl IntoIterator for TupleType {
type Item = Type;
type IntoIter = std::vec::IntoIter<Type>;
fn into_iter(self) -> Self::IntoIter {
self.elements.into_iter()
}
}
impl<'a> IntoIterator for &'a TupleType {
type Item = &'a Type;
type IntoIter = std::slice::Iter<'a, Type>;
fn into_iter(self) -> Self::IntoIter {
self.elements.iter()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Type {
Number,
NumberLiteral(LiteralF64),
BigInt,
BigIntLiteral(String),
String,
StringLiteral(String),
Uint8Array,
Boolean,
BooleanLiteral(bool),
Null,
Undefined,
Void,
Unknown,
Function {
params: Vec<Type>,
optional: usize,
ret: Box<Type>,
predicate: Option<Box<TypePredicate>>,
has_rest: bool,
},
Object {
fields: BTreeMap<String, ObjectField>,
index: Option<IndexSignature>,
},
Array(Box<Type>),
Tuple(TupleType),
Readonly(Box<Type>),
Error,
Never,
TypeVar(String),
GenericParam {
id: u32,
name: String,
},
Refined {
original: Box<Type>,
ty: Box<Type>,
},
InterfaceRef {
mangled: MangledName,
package: Package,
name: String,
args: Vec<Type>,
},
ClassRef {
mangled: MangledName,
package: Package,
name: String,
args: Vec<Type>,
},
NumberEnum {
mangled: MangledName,
package: Package,
name: String,
member: Option<EnumMember<LiteralF64>>,
},
StringEnum {
mangled: MangledName,
package: Package,
name: String,
member: Option<EnumMember<String>>,
},
Union(Vec<Type>),
Alias {
mangled: MangledName,
package: Package,
name: String,
args: Vec<Type>,
ty: Box<Type>,
},
AliasRef {
mangled: MangledName,
package: Package,
name: String,
args: Vec<Type>,
},
}
impl Type {
pub fn tuple(elements: Vec<Type>) -> Self {
Self::Tuple(elements.into())
}
pub fn is_structural_object(&self) -> bool {
match self.peel() {
Self::Object { .. } | Self::InterfaceRef { .. } => true,
Self::Union(members) => members.iter().all(Self::is_structural_object),
_ => false,
}
}
pub fn interface_ref(
package: Package,
name: impl Into<String>,
mangled: MangledName,
args: Vec<Type>,
) -> Type {
Type::InterfaceRef {
mangled,
package,
name: name.into(),
args,
}
}
pub fn class_ref(
package: Package,
name: impl Into<String>,
mangled: MangledName,
args: Vec<Type>,
) -> Type {
Type::ClassRef {
mangled,
package,
name: name.into(),
args,
}
}
pub fn number_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
Type::NumberEnum {
mangled,
package,
name: name.into(),
member: None,
}
}
pub fn string_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
Type::StringEnum {
mangled,
package,
name: name.into(),
member: None,
}
}
pub fn with_enum_member(
&self,
member_name: &str,
value: EnumValue,
member_count: usize,
) -> Type {
if member_count < 2 {
return self.without_enum_member();
}
match (self.clone(), value) {
(
Type::NumberEnum {
mangled,
package,
name,
..
},
EnumValue::Number(value),
) => Type::NumberEnum {
mangled,
package,
name,
member: Some(EnumMember::new(member_name, value, member_count)),
},
(
Type::StringEnum {
mangled,
package,
name,
..
},
EnumValue::String(value),
) => Type::StringEnum {
mangled,
package,
name,
member: Some(EnumMember::new(member_name, value, member_count)),
},
(ty, _) => ty,
}
}
pub fn without_enum_member(&self) -> Type {
match self {
Type::NumberEnum {
mangled,
package,
name,
member: Some(_),
} => Type::number_enum(package.clone(), name.clone(), mangled.clone()),
Type::StringEnum {
mangled,
package,
name,
member: Some(_),
} => Type::string_enum(package.clone(), name.clone(), mangled.clone()),
_ => self.clone(),
}
}
pub fn is_literal_type(&self) -> bool {
matches!(
self,
Type::NumberLiteral(_)
| Type::StringLiteral(_)
| Type::BooleanLiteral(_)
| Type::BigIntLiteral(_)
) || self.is_enum_member()
}
pub fn is_enum_member(&self) -> bool {
self.enum_member_name().is_some()
}
pub fn enum_member_name(&self) -> Option<(&MangledName, &str, usize)> {
match self {
Type::NumberEnum {
mangled,
member: Some(member),
..
} => Some((mangled, &member.name, member.member_count)),
Type::StringEnum {
mangled,
member: Some(member),
..
} => Some((mangled, &member.name, member.member_count)),
_ => None,
}
}
pub fn enum_member_value(&self) -> Option<EnumValue> {
match self.peel() {
Type::NumberEnum {
member: Some(member),
..
} => Some(EnumValue::Number(member.value)),
Type::StringEnum {
member: Some(member),
..
} => Some(EnumValue::String(member.value.clone())),
_ => None,
}
}
pub fn alias_ref(
package: Package,
name: impl Into<String>,
mangled: MangledName,
args: Vec<Type>,
) -> Type {
Type::AliasRef {
mangled,
package,
name: name.into(),
args,
}
}
pub fn alias_ty(
package: Package,
name: impl Into<String>,
mangled: MangledName,
args: Vec<Type>,
ty: Box<Type>,
) -> Type {
Type::Alias {
mangled,
package,
name: name.into(),
args,
ty,
}
}
pub fn prelude_interface(name: impl Into<String>, args: Vec<Type>) -> Type {
let name = name.into();
let mangled = crate::mangle::prelude(&name);
Type::interface_ref(Package::prelude(), name, mangled, args)
}
pub fn prelude_error_class() -> Type {
Type::class_ref(
Package::prelude(),
"Error",
crate::mangle::prelude("Error"),
Vec::new(),
)
}
pub fn prelude_range_error_class() -> Type {
Type::class_ref(
Package::prelude(),
"RangeError",
crate::mangle::prelude("RangeError"),
Vec::new(),
)
}
pub fn without_aliases(&self) -> &Type {
match self {
Type::Alias { ty, .. } => ty.without_aliases(),
_ => self,
}
}
pub fn holds_every_value(&self) -> bool {
let members: &[Type] = match self.without_aliases() {
Type::Unknown => return true,
Type::Union(members) => members,
_ => return false,
};
members.iter().any(Type::is_empty_object)
&& members
.iter()
.any(|m| matches!(m.without_aliases(), Type::Null))
&& members
.iter()
.any(|m| matches!(m.without_aliases(), Type::Undefined))
}
pub fn refinement_pieces(&self) -> Option<Vec<Type>> {
let Type::Refined { original, ty } = self.without_aliases() else {
return None;
};
let Type::Union(parts) = ty.peel() else {
return None;
};
Some(
parts
.iter()
.map(|part| match part.peel() {
Type::Null | Type::Undefined | Type::Void => part.clone(),
_ => Type::Refined {
original: original.clone(),
ty: Box::new(part.clone()),
},
})
.collect(),
)
}
pub fn refines_type_parameter(&self) -> bool {
matches!(self.without_aliases(), Type::Refined { original, .. }
if matches!(original.peel(), Type::GenericParam { .. }))
}
pub fn peel(&self) -> &Type {
let mut t = self;
while let Type::Alias { ty, .. } | Type::Refined { ty, .. } | Type::Readonly(ty) = t {
t = ty;
}
t
}
pub fn any_object() -> Type {
Type::Object {
index: Some(IndexSignature {
value: Box::new(Type::Unknown),
readonly: false,
}),
fields: BTreeMap::new(),
}
}
pub fn empty_object() -> Type {
Type::Object {
fields: BTreeMap::new(),
index: None,
}
}
pub fn is_empty_object(&self) -> bool {
let mut t = self;
while let Type::Alias { ty, .. } | Type::Readonly(ty) = t {
t = ty;
}
matches!(t, Type::Object { fields, index: None } if fields.is_empty())
}
pub fn lowered(&self) -> &Type {
let peeled = self.peel();
if peeled.is_empty_object() {
return &Type::Unknown;
}
peeled
}
pub fn peel_preserving_readonly(&self) -> &Type {
let mut t = self;
while let Type::Alias { ty, .. } | Type::Refined { ty, .. } = t {
t = ty;
}
t
}
pub fn is_readonly_array(&self) -> bool {
matches!(self.peel_preserving_readonly(), Type::Readonly(_))
}
pub fn rest_element(&self) -> Option<&Type> {
match self {
Type::Array(element) => Some(element),
Type::Readonly(inner) => match inner.as_ref() {
Type::Array(element) => Some(element),
_ => None,
},
_ => None,
}
}
pub fn rest_array_ignoring_readonly(&self) -> &Type {
match self {
Type::Readonly(inner) if matches!(inner.as_ref(), Type::Array(_)) => inner,
_ => self,
}
}
pub fn is_array_like_union(&self) -> bool {
match self.peel() {
Type::Union(members) => members
.iter()
.all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_))),
_ => false,
}
}
pub fn array_like_union_element(&self) -> Option<Type> {
let Type::Union(members) = self.peel() else {
return None;
};
let elements = members
.iter()
.map(|member| match member.peel() {
Type::Array(element) => Some((**element).clone()),
Type::Tuple(positions) => Some(Type::union(positions.elements.clone())),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
Some(Type::union(elements))
}
pub fn string_or_array_union_arrays(&self) -> Option<Type> {
let Type::Union(members) = self.peel() else {
return None;
};
let (strings, arrays): (Vec<Type>, Vec<Type>) = members
.iter()
.cloned()
.partition(|member| member.peel().is_string_shaped());
let all_arrays = arrays
.iter()
.all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_)));
(!strings.is_empty() && !arrays.is_empty() && all_arrays).then(|| Type::union(arrays))
}
pub fn array_like_union_view(&self) -> Option<Type> {
self.array_like_union_element()
.map(|element| Type::Array(Box::new(element)))
}
pub fn is_void(&self) -> bool {
matches!(self.peel(), Type::Void)
}
pub fn function_arity_fits(actual: usize, expected: usize, has_rest: bool) -> bool {
actual == expected || (!has_rest && actual < expected)
}
pub fn carries_void(&self) -> bool {
match self.peel() {
Type::Void => true,
Type::Union(members) => members.iter().any(|m| matches!(m.peel(), Type::Void)),
_ => false,
}
}
pub fn primitive_behavior(&self) -> &Type {
match self.peel() {
Type::NumberEnum { .. } => &Type::Number,
Type::StringEnum { .. } => &Type::String,
Type::BooleanLiteral(_) => &Type::Boolean,
Type::BigIntLiteral(_) => &Type::BigInt,
Type::Union(members)
if !members.is_empty()
&& members.iter().all(|member| {
matches!(
member.primitive_behavior(),
Type::Number | Type::NumberLiteral(_)
)
}) =>
{
&Type::Number
}
Type::Union(members)
if !members.is_empty()
&& members
.iter()
.all(|member| matches!(member.primitive_behavior(), Type::BigInt)) =>
{
&Type::BigInt
}
Type::Union(members)
if !members.is_empty() && members.iter().all(Type::is_string_shaped) =>
{
&Type::String
}
ty => ty,
}
}
pub fn is_bigint(&self) -> bool {
matches!(self.primitive_behavior(), Type::BigInt)
}
pub fn widen_literal(&self) -> Type {
match self {
Type::NumberLiteral(_) => Type::Number,
Type::StringLiteral(_) => Type::String,
Type::BooleanLiteral(_) => Type::Boolean,
Type::BigIntLiteral(_) => Type::BigInt,
_ if self.is_enum_member() => self.without_enum_member(),
Type::Union(members) => Type::union(without_members_of_empty_object(
members.iter().map(Type::widen_literal).collect(),
)),
_ => self.clone(),
}
}
pub fn literal_base(&self) -> Option<Type> {
let widened = self.widen_literal();
(widened != *self && matches!(widened, Type::Number | Type::String | Type::Boolean))
.then_some(widened)
}
pub fn is_string_shaped(&self) -> bool {
match self.peel() {
Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
Type::Union(members) => members.iter().all(Type::is_string_shaped),
_ => false,
}
}
pub fn contains_string(&self) -> bool {
match self.peel() {
Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
Type::Union(members) => members.iter().any(Type::contains_string),
_ => false,
}
}
pub fn any_member(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
match self.peel() {
Type::Union(members) => members.iter().any(|member| member.any_member(leaf)),
member => leaf(member),
}
}
pub fn all_members(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
!self.any_member(&|member| !leaf(member))
}
pub fn spells_null(&self) -> bool {
self.any_member(&|member| matches!(member, Type::Null))
}
pub fn spells_undefined(&self) -> bool {
self.any_member(&|member| matches!(member, Type::Undefined | Type::Void))
}
pub fn stringifies_to_undefined(leaf: &Type) -> bool {
matches!(
leaf.lowered(),
Type::Undefined
| Type::Void
| Type::Unknown
| Type::GenericParam { .. }
| Type::TypeVar(_)
| Type::Function { .. }
)
}
pub fn union(members: Vec<Type>) -> Type {
if members.iter().any(|m| matches!(m.peel(), Type::Error)) {
return Type::Error;
}
if members.iter().any(|m| matches!(m.peel(), Type::Unknown)) {
return Type::Unknown;
}
let members: Vec<Type> = members
.into_iter()
.filter(|m| !matches!(m.peel(), Type::Never))
.collect();
let mut flat: Vec<Type> = Vec::new();
for m in members {
match m.without_aliases() {
Type::Union(inner) => flat.extend(inner.iter().cloned()),
_ => flat.push(m),
}
}
let alias_rank = |t: &Type| u8::from(!matches!(t, Type::Alias { .. }));
flat.sort_by(|a, b| {
a.without_aliases()
.cmp(b.without_aliases())
.then_with(|| alias_rank(a).cmp(&alias_rank(b)))
});
flat.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
fold_boolean_literals(&mut flat);
fold_enum_members(&mut flat);
let refines_a_member = |member: &Type| {
matches!(member, Type::Refined { original, .. }
if flat.iter().any(|other| other.without_aliases() == original.without_aliases()))
};
let kept: Vec<Type> = flat
.iter()
.filter(|member| !refines_a_member(member))
.cloned()
.collect();
flat = kept;
if flat.len() > 1 {
return Type::Union(flat);
}
match flat.into_iter().next() {
Some(member) => member,
None => Type::Never,
}
}
pub fn interface_routing(&self) -> Option<(MangledName, &str, &str, Vec<Type>)> {
let prelude = crate::mangle::PRELUDE_PACKAGE;
match self.primitive_behavior() {
Type::Number | Type::NumberLiteral(_) => Some((
crate::mangle::prelude("Number"),
prelude,
"Number",
Vec::new(),
)),
Type::BigInt => Some((
crate::mangle::prelude("BigInt"),
prelude,
"BigInt",
Vec::new(),
)),
Type::Boolean => Some((
crate::mangle::prelude("Boolean"),
prelude,
"Boolean",
Vec::new(),
)),
Type::String | Type::StringLiteral(_) => Some((
crate::mangle::prelude("String"),
prelude,
"String",
Vec::new(),
)),
Type::Uint8Array => Some((
crate::mangle::prelude("Uint8Array"),
prelude,
"Uint8Array",
Vec::new(),
)),
Type::Array(elem) => Some((
crate::mangle::prelude("Array"),
prelude,
"Array",
vec![(**elem).clone()],
)),
Type::Tuple(elements) => Some((
crate::mangle::prelude("Array"),
prelude,
"Array",
vec![Type::union(elements.elements.clone())],
)),
Type::Object { .. } | Type::TypeVar(_) | Type::GenericParam { .. } => Some((
crate::mangle::prelude("Object"),
prelude,
"Object",
Vec::new(),
)),
Type::InterfaceRef {
mangled,
package,
name,
args,
..
}
| Type::ClassRef {
mangled,
package,
name,
args,
..
} => Some((
mangled.clone(),
package.as_str(),
name.as_str(),
args.clone(),
)),
Type::NumberEnum { .. } | Type::StringEnum { .. } => Some((
crate::mangle::prelude("Object"),
prelude,
"Object",
Vec::new(),
)),
Type::Unknown => Some((
crate::mangle::prelude("Object"),
prelude,
"Object",
Vec::new(),
)),
_ => None,
}
}
}
pub(crate) fn shown_beside_optional_marker(ty: &Type) -> Type {
match ty.peel() {
Type::Union(members) => Type::union(
members
.iter()
.filter(|member| !matches!(member.peel(), Type::Undefined))
.cloned()
.collect(),
),
_ => ty.clone(),
}
}
pub(crate) fn escape_string_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = crate::literal_units::literal_chars(s).peekable();
while let Some(c) = chars.next() {
let c = match c {
Ok(c) => c,
Err(lone) => {
out.push_str(&format!("\\u{lone:04X}"));
continue;
}
};
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{b}' => out.push_str("\\v"),
'\u{c}' => out.push_str("\\f"),
'\0' if chars
.peek()
.is_some_and(|next| next.is_ok_and(|d| d.is_ascii_digit())) =>
{
out.push_str("\\x00");
}
'\0' => out.push_str("\\0"),
'\u{0}'..='\u{1f}' | '\u{85}' | '\u{2028}' | '\u{2029}' => {
out.push_str(&format!("\\u{:04X}", u32::from(c)));
}
c => out.push(c),
}
}
out
}
pub fn bigint_literal_type(digits: &str) -> Type {
Type::BigIntLiteral(canonical_bigint_digits(digits))
}
pub fn negate_bigint_digits(digits: &str) -> String {
let canonical = canonical_bigint_digits(digits);
if canonical == "0" {
return canonical;
}
match canonical.strip_prefix('-') {
Some(magnitude) => magnitude.to_string(),
None => format!("-{canonical}"),
}
}
fn canonical_bigint_digits(digits: &str) -> String {
let (sign, magnitude) = match digits.strip_prefix('-') {
Some(magnitude) => ("-", magnitude),
None => ("", digits),
};
match magnitude.trim_start_matches('0') {
"" => "0".to_string(),
trimmed => format!("{sign}{trimmed}"),
}
}
pub fn without_members_of_empty_object(members: Vec<Type>) -> Vec<Type> {
if !members.iter().any(Type::is_empty_object) {
return members;
}
members
.into_iter()
.filter(|member| member.is_empty_object() || may_hold_values_outside_empty_object(member))
.collect()
}
fn may_hold_values_outside_empty_object(ty: &Type) -> bool {
match ty.peel() {
Type::Null
| Type::Undefined
| Type::Void
| Type::Unknown
| Type::GenericParam { .. }
| Type::TypeVar(_) => true,
Type::Union(members) => members.iter().any(may_hold_values_outside_empty_object),
_ => false,
}
}
fn fold_boolean_literals(members: &mut Vec<Type>) {
let is_boolean = |m: &Type| matches!(m.without_aliases(), Type::Boolean);
let is_literal = |m: &Type| matches!(m.without_aliases(), Type::BooleanLiteral(_));
let literals = members.iter().filter(|m| is_literal(m)).count();
let has_boolean = members.iter().any(is_boolean);
if literals == 0 || (literals == 1 && !has_boolean) {
return;
}
members.retain(|m| !is_literal(m));
if !has_boolean {
let at = members.partition_point(|m| m.without_aliases() < &Type::Boolean);
members.insert(at, Type::Boolean);
}
}
fn fold_enum_members(members: &mut Vec<Type>) {
absorb_enums_into_primitives(members);
let complete = complete_enums(members);
if complete.is_empty() {
return;
}
for member in members.iter_mut() {
if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } =
member.without_aliases()
&& complete.contains(mangled)
{
*member = member.without_aliases().without_enum_member();
}
}
members.sort_by(|a, b| a.without_aliases().cmp(b.without_aliases()));
members.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
}
fn absorb_enums_into_primitives(members: &mut Vec<Type>) {
let has = |base: &Type| members.iter().any(|m| m.without_aliases() == base);
let (has_number, has_string) = (has(&Type::Number), has(&Type::String));
members.retain(|m| match m.without_aliases() {
Type::NumberEnum { .. } => !has_number,
Type::StringEnum { .. } => !has_string,
_ => true,
});
}
fn complete_enums(members: &[Type]) -> BTreeSet<MangledName> {
struct NamedMembers<'a> {
member_count: usize,
names: BTreeSet<&'a str>,
}
let mut named: BTreeMap<&MangledName, NamedMembers> = BTreeMap::new();
let mut whole: BTreeSet<&MangledName> = BTreeSet::new();
for member in members {
let ty = member.without_aliases();
if let Some((mangled, name, member_count)) = ty.enum_member_name() {
named
.entry(mangled)
.or_insert_with(|| NamedMembers {
member_count,
names: BTreeSet::new(),
})
.names
.insert(name);
} else if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } = ty {
whole.insert(mangled);
}
}
named
.into_iter()
.filter(|(mangled, named)| {
whole.contains(mangled) || named.names.len() >= named.member_count
})
.map(|(mangled, _)| mangled.clone())
.collect()
}
impl Type {
pub fn render_checked(
&self,
limits: crate::rendering::RenderLimits,
) -> Result<crate::rendering::RenderedText, crate::rendering::RenderError> {
crate::type_rendering::render(self, limits)
}
}
pub fn union_display_order(members: &[Type]) -> Vec<&Type> {
let mut ordered: Vec<&Type> = members.iter().collect();
ordered.sort_by_key(|m| match m {
Type::Null => 1,
Type::Undefined => 2,
_ => 0,
});
ordered
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.render_checked(crate::rendering::RenderLimits::default()) {
Ok(rendered) => f.write_str(&rendered.text),
Err(_) => f.write_str("[diagnostic type unavailable]"),
}
}
}
#[cfg(test)]
mod tests {
use super::{Package, Type};
#[test]
fn tuple_mapping_preserves_optional_positions() {
let tuple = super::TupleType {
elements: vec![Type::NumberLiteral(super::LiteralF64(1.0)), Type::Undefined],
optional: 1,
};
let mapped = tuple.map(Type::widen_literal);
assert_eq!(mapped.optional, 1);
assert_eq!(mapped.required_len(), 1);
assert_eq!(mapped.elements, vec![Type::Number, Type::Undefined]);
}
#[test]
fn optional_fields_read_as_undefined() {
let field = super::ObjectField::optional(Type::String);
assert_eq!(
field.read_ty(),
Type::union(vec![Type::String, Type::Undefined])
);
}
#[test]
fn optional_function_and_tuple_display_preserve_omission() {
let ty = Type::Function {
params: vec![Type::union(vec![Type::String, Type::Undefined])],
optional: 1,
ret: Box::new(Type::Void),
predicate: None,
has_rest: false,
};
assert_eq!(ty.to_string(), "(arg0?: string) => void");
let tuple = Type::Tuple(super::TupleType {
elements: vec![
Type::Number,
Type::union(vec![Type::String, Type::Undefined]),
],
optional: 1,
});
assert_eq!(tuple.to_string(), "[number, (string | undefined)?]");
}
#[test]
fn atomic_display() {
assert_eq!(Type::Number.to_string(), "number");
assert_eq!(Type::String.to_string(), "string");
assert_eq!(Type::Boolean.to_string(), "boolean");
assert_eq!(Type::Null.to_string(), "null");
assert_eq!(Type::Void.to_string(), "void");
assert_eq!(Type::Error.to_string(), "<error>");
}
#[test]
fn readonly_display_matches_typescript() {
let numbers = Type::Array(Box::new(Type::Number));
let readonly = Type::Readonly(Box::new(numbers.clone()));
assert_eq!(readonly.to_string(), "readonly number[]");
assert_eq!(
Type::Array(Box::new(readonly.clone())).to_string(),
"(readonly number[])[]"
);
assert_eq!(
Type::Readonly(Box::new(Type::Array(Box::new(numbers)))).to_string(),
"readonly number[][]"
);
assert_eq!(
Type::Readonly(Box::new(Type::Tuple(
vec![Type::Number, Type::String].into()
)))
.to_string(),
"readonly [number, string]"
);
assert!(readonly.is_readonly_array());
assert_eq!(readonly.peel(), &Type::Array(Box::new(Type::Number)));
}
#[test]
fn string_literal_display_escapes_like_typescript() {
let cases = [
("G\"HI\\", r#""G\"HI\\""#),
("a\nb\r\t", r#""a\nb\r\t""#),
("\u{8}\u{b}\u{c}", r#""\b\v\f""#),
("\0x", r#""\0x""#),
("\u{0}1", r#""\x001""#),
("\u{7}\u{1b}", r#""\u0007\u001B""#),
("\u{85}\u{2028}\u{2029}", r#""\u0085\u2028\u2029""#),
("\u{7f}é", "\"\u{7f}é\""),
];
for (value, printed) in cases {
assert_eq!(Type::StringLiteral(value.into()).to_string(), printed);
}
}
#[test]
fn structural_equality() {
assert_eq!(Type::Number, Type::Number);
assert_ne!(Type::Number, Type::String);
let a = Type::Function {
params: vec![Type::Number, Type::String],
ret: Box::new(Type::Boolean),
predicate: None,
has_rest: false,
optional: 0,
};
let b = Type::Function {
params: vec![Type::Number, Type::String],
ret: Box::new(Type::Boolean),
predicate: None,
has_rest: false,
optional: 0,
};
let c = Type::Function {
params: vec![Type::Number],
ret: Box::new(Type::Boolean),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn function_display_no_params() {
let t = Type::Function {
params: vec![],
ret: Box::new(Type::Number),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(t.to_string(), "() => number");
}
#[test]
fn function_display_one_param() {
let t = Type::Function {
params: vec![Type::Number],
ret: Box::new(Type::Boolean),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(t.to_string(), "(arg0: number) => boolean");
}
#[test]
fn function_display_multi_param() {
let t = Type::Function {
params: vec![Type::Number, Type::String],
ret: Box::new(Type::Void),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(t.to_string(), "(arg0: number, arg1: string) => void");
}
#[test]
fn function_display_nested() {
let inner = Type::Function {
params: vec![],
ret: Box::new(Type::Number),
predicate: None,
has_rest: false,
optional: 0,
};
let outer = Type::Function {
params: vec![],
ret: Box::new(inner),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(outer.to_string(), "() => () => number");
}
#[test]
fn clone_roundtrip() {
let t = Type::Function {
params: vec![Type::Number, Type::String],
ret: Box::new(Type::Boolean),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(t.clone(), t);
}
fn point_type() -> Type {
let mut fields = std::collections::BTreeMap::new();
fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
fields.insert("y".to_string(), crate::ObjectField::required(Type::Number));
Type::Object {
index: None,
fields,
}
}
#[test]
fn object_structural_equality_ignores_field_insertion_order() {
let mut a = std::collections::BTreeMap::new();
a.insert("y".to_string(), crate::ObjectField::required(Type::Number));
a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
let mut b = std::collections::BTreeMap::new();
b.insert("x".to_string(), crate::ObjectField::required(Type::Number));
b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
assert_eq!(
Type::Object {
index: None,
fields: a
},
Type::Object {
index: None,
fields: b
}
);
}
#[test]
fn object_inequality_on_different_field_set() {
let mut a = std::collections::BTreeMap::new();
a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
let mut b = std::collections::BTreeMap::new();
b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
assert_ne!(
Type::Object {
index: None,
fields: a
},
Type::Object {
index: None,
fields: b
}
);
}
#[test]
fn object_display() {
assert_eq!(point_type().to_string(), "{ x: number; y: number }");
let empty = Type::Object {
index: None,
fields: std::collections::BTreeMap::new(),
};
assert_eq!(empty.to_string(), "{}");
}
#[test]
fn object_display_with_optional_field() {
let mut fields = std::collections::BTreeMap::new();
fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
fields.insert("y".to_string(), crate::ObjectField::optional(Type::String));
let t = Type::Object {
index: None,
fields,
};
assert_eq!(t.to_string(), "{ x: number; y?: string }");
}
#[test]
fn object_inequality_required_vs_optional() {
let mut a = std::collections::BTreeMap::new();
a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
let mut b = std::collections::BTreeMap::new();
b.insert("x".to_string(), crate::ObjectField::optional(Type::Number));
assert_ne!(
Type::Object {
index: None,
fields: a
},
Type::Object {
index: None,
fields: b
}
);
}
#[test]
fn array_equality() {
assert_eq!(
Type::Array(Box::new(Type::Number)),
Type::Array(Box::new(Type::Number))
);
assert_ne!(
Type::Array(Box::new(Type::Number)),
Type::Array(Box::new(Type::String))
);
}
#[test]
fn array_display() {
assert_eq!(Type::Array(Box::new(Type::Number)).to_string(), "number[]");
assert_eq!(
Type::Array(Box::new(Type::Array(Box::new(Type::Number)))).to_string(),
"number[][]"
);
}
#[test]
fn nested_object_in_array_display() {
let arr = Type::Array(Box::new(point_type()));
assert_eq!(arr.to_string(), "{ x: number; y: number }[]");
}
#[test]
fn type_var_display_uses_source_name() {
assert_eq!(Type::TypeVar("T".to_string()).to_string(), "T");
assert_eq!(Type::TypeVar("Key".to_string()).to_string(), "Key");
}
#[test]
fn type_var_structural_equality_by_name() {
let t = Type::TypeVar("T".to_string());
let t2 = Type::TypeVar("T".to_string());
let u = Type::TypeVar("U".to_string());
assert_eq!(t, t2);
assert_ne!(t, u);
assert_ne!(t, Type::Number);
}
#[test]
fn type_var_inside_array_and_function_displays_through() {
let arr = Type::Array(Box::new(Type::TypeVar("T".to_string())));
assert_eq!(arr.to_string(), "T[]");
let func = Type::Function {
params: vec![Type::TypeVar("T".to_string())],
ret: Box::new(Type::TypeVar("U".to_string())),
predicate: None,
has_rest: false,
optional: 0,
};
assert_eq!(func.to_string(), "(arg0: T) => U");
}
#[test]
fn generic_param_display_uses_name_not_id() {
let gp = Type::GenericParam {
id: 42,
name: "T".to_string(),
};
assert_eq!(gp.to_string(), "T");
}
#[test]
fn generic_param_equality_by_id() {
let a = Type::GenericParam {
id: 5,
name: "T".to_string(),
};
let b = Type::GenericParam {
id: 5,
name: "T".to_string(),
};
assert_eq!(a, b);
let c = Type::GenericParam {
id: 6,
name: "T".to_string(),
};
assert_ne!(a, c);
}
#[test]
fn union_canonical_dedup() {
let t = Type::union(vec![Type::Number, Type::Null, Type::Number]);
let Type::Union(members) = t else {
panic!("expected Union, got {t:?}");
};
assert_eq!(members, vec![Type::Number, Type::Null]);
}
#[test]
fn union_canonical_order_independent() {
let a = Type::union(vec![Type::Number, Type::Null]);
let b = Type::union(vec![Type::Null, Type::Number]);
assert_eq!(a, b);
}
#[test]
fn union_collapses_to_single_member() {
let t = Type::union(vec![Type::Number, Type::Number]);
assert_eq!(t, Type::Number);
}
#[test]
fn union_flattens_nested() {
let inner = Type::union(vec![Type::String, Type::Null]);
let outer = Type::union(vec![Type::Number, inner]);
let expected = Type::union(vec![Type::Number, Type::String, Type::Null]);
assert_eq!(outer, expected);
}
#[test]
fn union_error_member_collapses_to_error() {
let t = Type::union(vec![Type::Number, Type::Error]);
assert_eq!(t, Type::Error);
}
#[test]
fn union_display_basic() {
let t = Type::union(vec![Type::Number, Type::String]);
assert_eq!(t.to_string(), "number | string");
}
#[test]
fn union_display_with_null() {
let t = Type::union(vec![Type::Number, Type::Null]);
assert_eq!(t.to_string(), "number | null");
let t2 = Type::union(vec![Type::Null, Type::Number]);
assert_eq!(t2.to_string(), "number | null");
}
#[test]
fn union_display_wraps_function_member() {
let fn_ty = Type::Function {
params: vec![],
ret: Box::new(Type::String),
predicate: None,
has_rest: false,
optional: 0,
};
let t = Type::union(vec![fn_ty, Type::Boolean]);
assert_eq!(t.to_string(), "boolean | (() => string)");
}
use super::LiteralF64;
#[test]
fn string_literal_display_is_quoted() {
let t = Type::StringLiteral("north".to_string());
assert_eq!(t.to_string(), "\"north\"");
}
#[test]
fn number_literal_display_is_unquoted_with_whole_number_shape() {
let whole = Type::NumberLiteral(LiteralF64(42.0));
assert_eq!(whole.to_string(), "42");
let frac = Type::NumberLiteral(LiteralF64(4.5));
assert_eq!(frac.to_string(), "4.5");
}
#[test]
fn string_literal_equality_by_value() {
let a = Type::StringLiteral("hi".to_string());
let b = Type::StringLiteral("hi".to_string());
let c = Type::StringLiteral("bye".to_string());
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn number_literal_equality_by_value() {
let a = Type::NumberLiteral(LiteralF64(42.0));
let b = Type::NumberLiteral(LiteralF64(42.0));
let c = Type::NumberLiteral(LiteralF64(43.0));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn union_sorts_literals_next_to_base_types() {
let t = Type::union(vec![
Type::String,
Type::StringLiteral("hi".to_string()),
Type::Number,
Type::NumberLiteral(LiteralF64(42.0)),
]);
assert_eq!(t.to_string(), "number | 42 | string | \"hi\"");
}
#[test]
fn widen_literal_replaces_a_literal_with_its_base() {
assert_eq!(
Type::NumberLiteral(LiteralF64(1.0)).widen_literal(),
Type::Number
);
assert_eq!(
Type::StringLiteral("hi".to_string()).widen_literal(),
Type::String
);
}
#[test]
fn widen_literal_collapses_a_union_of_literals() {
let t = Type::union(vec![
Type::NumberLiteral(LiteralF64(1.0)),
Type::NumberLiteral(LiteralF64(2.0)),
]);
assert_eq!(t.widen_literal(), Type::Number);
}
#[test]
fn widen_literal_leaves_everything_else_alone() {
assert_eq!(Type::Number.widen_literal(), Type::Number);
assert_eq!(Type::Boolean.widen_literal(), Type::Boolean);
let arr = Type::Array(Box::new(Type::NumberLiteral(LiteralF64(1.0))));
assert_eq!(arr.widen_literal(), arr);
}
#[test]
fn union_of_two_string_literals_sorts_by_value() {
let t = Type::union(vec![
Type::StringLiteral("south".to_string()),
Type::StringLiteral("north".to_string()),
]);
assert_eq!(t.to_string(), "\"north\" | \"south\"");
}
#[test]
fn literal_f64_equality_distinguishes_negative_zero_at_bit_level() {
let pos = LiteralF64(0.0);
let neg = LiteralF64(-0.0);
assert_ne!(pos, neg);
}
#[test]
fn type_var_and_generic_param_are_distinct_types() {
let tv = Type::TypeVar("T".to_string());
let gp = Type::GenericParam {
id: 0,
name: "T".to_string(),
};
assert_ne!(tv, gp);
}
#[test]
fn alias_display_renders_name_not_body() {
let ty = user_alias("Circle", point_type());
assert_eq!(ty.to_string(), "Circle");
}
#[test]
fn peel_walks_through_alias_to_underlying() {
let inner = Type::Number;
let aliased = user_alias("ID", inner.clone());
assert_eq!(aliased.peel(), &inner);
}
#[test]
fn peel_is_idempotent_through_nested_aliases() {
let nested = user_alias("A", user_alias("B", Type::Number));
assert_eq!(nested.peel(), &Type::Number);
assert_eq!(nested.peel().peel(), &Type::Number);
}
#[test]
fn alias_is_not_structurally_equal_to_underlying() {
let aliased = user_alias("ID", Type::Number);
assert_ne!(aliased, Type::Number);
assert_eq!(aliased.peel(), &Type::Number);
}
#[test]
fn alias_routes_to_underlying_interface() {
let aliased = user_alias("ID", Type::Number);
let (_mangled, _pkg, iface, _args) = aliased.interface_routing().expect("number routes");
assert_eq!(iface, "Number");
}
#[test]
fn union_of_error_alias_collapses_to_error() {
let alias_err = user_alias("Bad", Type::Error);
let t = Type::union(vec![Type::Number, alias_err]);
assert_eq!(t, Type::Error);
}
#[test]
fn union_preserves_distinct_alias_members_for_display() {
let circle = user_alias("Circle", point_type());
let rect = user_alias("Rectangle", Type::Number);
let t = Type::union(vec![circle, rect]);
assert_eq!(t.to_string(), "Rectangle | Circle");
}
#[test]
fn union_dedups_an_alias_against_its_own_body_keeping_the_label() {
let n = user_alias("N", Type::Number);
assert_eq!(Type::union(vec![n.clone(), Type::Number]), n);
assert_eq!(Type::union(vec![Type::Number, n.clone()]), n);
}
#[test]
fn union_dedups_aliases_that_share_one_body() {
let circle = user_alias("Circle", point_type());
let rect = user_alias("Rectangle", point_type());
assert_eq!(Type::union(vec![circle.clone(), rect]), circle);
}
#[test]
fn union_flattens_an_alias_whose_body_is_a_union() {
let nullable = user_alias("MN", Type::union(vec![Type::Number, Type::Null]));
assert_eq!(
Type::union(vec![nullable, Type::Number]),
Type::union(vec![Type::Number, Type::Null])
);
}
#[test]
fn generic_alias_display_includes_args() {
let ty = user_alias_args("Box", vec![Type::Number], Type::Number);
assert_eq!(ty.to_string(), "Box<number>");
}
fn iface(package: &str, name: &str) -> Type {
Type::interface_ref(
Package(package.to_string()),
name,
crate::mangle::package_symbol(package, name),
Vec::new(),
)
}
fn user_alias(name: &str, ty: Type) -> Type {
user_alias_args(name, Vec::new(), ty)
}
fn user_alias_args(name: &str, args: Vec<Type>, ty: Type) -> Type {
Type::alias_ty(
Package::user(),
name,
crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, name),
args,
Box::new(ty),
)
}
#[test]
fn mangled_name_keys_type_identity() {
assert_eq!(iface("main", "Response"), iface("main", "Response"));
let http = iface("submilli:http", "Response");
let main = iface("main", "Response");
assert_ne!(
http, main,
"different package ⇒ different mangled ⇒ distinct"
);
assert_ne!(
http.cmp(&main),
std::cmp::Ordering::Equal,
"Ord must distinguish distinct mangled names"
);
assert_ne!(
iface("main", "Response"),
iface("main", "Other"),
"different names stay distinct"
);
}
#[test]
fn same_name_different_module_not_equal_nor_assignable() {
let root = iface("main", "Logger");
let sibling = Type::InterfaceRef {
mangled: crate::mangle::package_module_symbol("main", "util", "Logger"),
package: Package::user(),
name: "Logger".to_string(),
args: Vec::new(),
};
assert_ne!(root, sibling);
let root2 = iface("main", "Logger");
assert_eq!(root, root2);
}
#[test]
fn union_dedups_interface_refs_by_mangled() {
let same = Type::union(vec![iface("main", "Response"), iface("main", "Response")]);
assert!(
!matches!(same, Type::Union(_)),
"identical refs collapse to one member, got {same:?}"
);
let distinct = Type::union(vec![
iface("submilli:http", "Response"),
iface("main", "Response"),
]);
assert!(
matches!(distinct, Type::Union(ref ms) if ms.len() == 2),
"distinct-package refs stay a 2-member union, got {distinct:?}"
);
}
#[test]
fn generic_alias_display_multiple_args() {
let ty = user_alias_args("Pair", vec![Type::String, Type::Number], Type::Number);
assert_eq!(ty.to_string(), "Pair<string, number>");
}
}