use crate::compiler_error::CompilerFailure;
use std::collections::BTreeMap;
use crate::{
ExprId, ExprKind, Ident, MethodSig, Span, StmtKind, Type, TypeAnnotation, TypedExprKind,
TypedParam, TypedStmt, TypedStmtKind, ValueKind,
};
use super::{Inferer, type_limit_at};
use crate::type_size::{TypeBudget, TypeLimits, TypeTooLarge, map_children};
use crate::typechecker::type_param_substitution::{CloseMatch, TypeParamSubstitution, UnifyError};
#[derive(Clone, Copy, PartialEq)]
pub(super) enum GenericCallee {
Function,
Constructor,
}
impl GenericCallee {
fn subject(self, name: &str, generics: &[String]) -> String {
match self {
Self::Function => format!("`{name}`"),
Self::Constructor if generics.is_empty() => format!("constructor of `{name}`"),
Self::Constructor => {
format!("constructor of `{name}<{}>`", generics.join(", "))
}
}
}
fn explicit_arg_hint(
self,
name: &str,
generics: &[String],
sub: &crate::typechecker::type_param_substitution::TypeParamSubstitution,
limits: &TypeLimits,
) -> String {
let args = generics
.iter()
.map(|g| {
match sub.apply(&Type::TypeVar(g.clone()), limits) {
Ok(Type::TypeVar(n)) if n == g[..] => g.clone(),
Ok(resolved) => resolved.to_string(),
Err(exceeded) => {
limits.record(exceeded);
g.clone()
}
}
})
.collect::<Vec<_>>()
.join(", ");
match self {
Self::Function => {
format!("add an explicit type argument: `{name}<{args}>(…)`")
}
Self::Constructor => {
format!("add an explicit type argument: `new {name}<{args}>(…)`")
}
}
}
}
use super::format_signature::SignatureKind;
use crate::Param;
pub(super) fn substitute_typevars(
ty: &Type,
bindings: &BTreeMap<String, Type>,
limits: &TypeLimits,
) -> Result<Type, TypeTooLarge> {
substitute_at(ty, bindings, 1, &mut limits.budget())
}
pub(super) fn substitute_or_record(
ty: &Type,
bindings: &BTreeMap<String, Type>,
limits: &TypeLimits,
) -> Type {
limits.type_or_error(substitute_typevars(ty, bindings, limits))
}
fn substitute_at(
ty: &Type,
bindings: &BTreeMap<String, Type>,
depth: u32,
budget: &mut TypeBudget<'_>,
) -> Result<Type, TypeTooLarge> {
if let Type::TypeVar(name) = ty
&& let Some(bound) = bindings.get(name)
{
budget.charge_copy(bound, depth)?;
return Ok(bound.clone());
}
budget.charge(depth)?;
let child = depth.saturating_add(1);
map_children(ty, |inner| substitute_at(inner, bindings, child, budget))
}
pub(super) fn erase_generic_params(ty: &Type) -> Type {
match ty {
Type::Refined { original, ty } => Type::Refined {
original: Box::new(erase_generic_params(original)),
ty: Box::new(erase_generic_params(ty)),
},
Type::GenericParam { name, .. } => Type::TypeVar(name.clone()),
Type::Object { fields, index } => Type::Object {
index: index.as_ref().map(|i| i.map_value(erase_generic_params)),
fields: fields
.iter()
.map(|(k, v)| {
(
k.clone(),
crate::ObjectField {
ty: erase_generic_params(&v.ty),
optional: v.optional,
readonly: v.readonly,
method: v.method,
},
)
})
.collect(),
},
Type::Array(elem) => Type::Array(Box::new(erase_generic_params(elem))),
Type::Readonly(inner) => Type::Readonly(Box::new(erase_generic_params(inner))),
Type::Tuple(elements) => Type::Tuple(elements.map(erase_generic_params)),
Type::Function {
params,
ret,
predicate,
has_rest,
optional,
} => Type::Function {
params: params.iter().map(erase_generic_params).collect(),
ret: Box::new(erase_generic_params(ret)),
predicate: predicate.as_ref().map(|p| {
Box::new(crate::TypePredicate {
parameter_index: p.parameter_index,
asserted_type: erase_generic_params(&p.asserted_type),
})
}),
has_rest: *has_rest,
optional: *optional,
},
Type::InterfaceRef {
mangled,
package,
name,
args,
} => Type::InterfaceRef {
mangled: mangled.clone(),
package: package.clone(),
name: name.clone(),
args: args.iter().map(erase_generic_params).collect(),
},
Type::ClassRef {
mangled,
package,
name,
args,
} => Type::ClassRef {
mangled: mangled.clone(),
package: package.clone(),
name: name.clone(),
args: args.iter().map(erase_generic_params).collect(),
},
Type::Union(members) => Type::union(members.iter().map(erase_generic_params).collect()),
Type::Alias {
mangled,
package,
name,
args,
ty: inner,
} => Type::Alias {
mangled: mangled.clone(),
package: package.clone(),
name: name.clone(),
args: args.iter().map(erase_generic_params).collect(),
ty: Box::new(erase_generic_params(inner)),
},
Type::AliasRef {
mangled,
package,
name,
args,
} => Type::AliasRef {
mangled: mangled.clone(),
package: package.clone(),
name: name.clone(),
args: args.iter().map(erase_generic_params).collect(),
},
Type::TypeVar(_)
| Type::Number
| Type::BigInt
| Type::BigIntLiteral(_)
| Type::NumberLiteral(_)
| Type::String
| Type::StringLiteral(_)
| Type::Uint8Array
| Type::Boolean
| Type::BooleanLiteral(_)
| Type::Null
| Type::Undefined
| Type::Void
| Type::Never
| Type::Unknown
| Type::NonPrimitive
| Type::Error
| Type::NumberEnum { .. }
| Type::StringEnum { .. } => ty.clone(),
}
}
pub(super) fn erase_generic_params_in_expr(expr: &mut crate::TypedExpr) {
expr.ty = erase_generic_params(&expr.ty);
match &mut expr.kind {
TypedExprKind::ObjectLiteral { fields, .. } => {
for f in fields {
f.ty = erase_generic_params(&f.ty);
}
}
TypedExprKind::ArrayLiteral { element_ty, .. } => {
*element_ty = erase_generic_params(element_ty);
}
TypedExprKind::TupleLiteral { element_types, .. } => {
for t in element_types {
*t = erase_generic_params(t);
}
}
TypedExprKind::Closure {
params,
return_type,
captured,
..
} => {
for p in params {
p.ty = erase_generic_params(&p.ty);
}
*return_type = erase_generic_params(return_type);
for c in captured {
c.ty = erase_generic_params(&c.ty);
}
}
TypedExprKind::GenericCall {
type_args,
return_cast,
..
} => {
for arg in type_args {
*arg = erase_generic_params(arg);
}
if let Some(t) = return_cast {
*t = erase_generic_params(t);
}
}
TypedExprKind::GenericMethodCall {
return_cast: Some(t),
..
} => {
*t = erase_generic_params(t);
}
TypedExprKind::InstanceOf { class, .. } => {
*class = erase_generic_params(class);
}
TypedExprKind::Cast { target_ty, .. } => {
*target_ty = erase_generic_params(target_ty);
}
_ => {}
}
}
pub(super) fn erase_generic_params_in_stmt(stmt: &mut TypedStmt) {
match &mut stmt.kind {
TypedStmtKind::Let { ty, .. } | TypedStmtKind::Const { ty, .. } => {
*ty = erase_generic_params(ty);
}
_ => {}
}
}
fn return_erases_to_object_slot(ret: &Type) -> bool {
matches!(
ret.peel(),
Type::TypeVar(_) | Type::Unknown | Type::Union(_)
)
}
impl Inferer<'_> {
fn resolve_call_type_argument(
&mut self,
annot: &TypeAnnotation,
subject: &str,
) -> Result<Type, CompilerFailure> {
let resolved = self.resolve_type(annot)?;
if let Some(offender) = super::value_operand::valueless_within_type_argument(&resolved) {
let offender = offender.clone();
self.error(
annot.span,
format!(
"`{offender}` cannot be used as a type argument to {subject} — use a value type"
),
);
return Ok(Type::Error);
}
Ok(resolved)
}
fn structural_member_unify(
&mut self,
sub: &mut crate::typechecker::type_param_substitution::TypeParamSubstitution,
param_ty: &Type,
arg_ty: &Type,
) -> bool {
let members: Vec<Type> = match param_ty.peel() {
Type::Union(ms) => ms.clone(),
other => vec![other.clone()],
};
let Some((ma, _pa, na, aa)) = arg_ty.interface_routing() else {
return false;
};
let Some(actual_form) = self.structural_form(&ma, na, &aa) else {
return false;
};
for member in &members {
if matches!(member.peel(), Type::Object { .. } | Type::TypeVar(_)) {
continue;
}
let Some((me, _pe, ne, ae)) = member.interface_routing() else {
continue;
};
if me == ma {
continue;
}
let Some(expected_form) = self.structural_form(&me, ne, &ae) else {
continue;
};
if expected_form.is_empty() {
continue;
}
let snapshot = sub.clone();
let resolver = self.resolver();
let all = expected_form.iter().all(|(name, exp)| {
actual_form
.get(name)
.is_some_and(|act| sub.unify(&exp.ty, &act.ty, resolver).is_ok())
});
if all {
return true;
}
*sub = snapshot;
}
false
}
pub(super) fn infer_functions(&mut self) -> Result<(), CompilerFailure> {
let top_level: Vec<_> = self.ast.top_level.clone();
for stmt_id in top_level {
let stmt = self
.ast
.try_stmt(stmt_id)
.map_err(super::arena_failure)?
.clone();
let span = stmt.span;
let StmtKind::Function {
name,
params,
body,
doc,
..
} = stmt.kind
else {
continue;
};
let (generic_names, sig_param_types, sig_ret_type, sig_param_defaults): (
Vec<String>,
Vec<Type>,
Type,
Vec<Option<crate::DefaultValue>>,
) = match self.top_symbols.get(&name.name) {
Some(entry) => match &entry.kind {
ValueKind::Function {
generics,
params,
ret,
..
} => (
generics.clone(),
params.iter().map(|p| p.ty.clone()).collect(),
ret.clone(),
params.iter().map(|p| p.default.clone()).collect(),
),
_ => (
Vec::new(),
vec![Type::Error; params.len()],
Type::Error,
vec![None; params.len()],
),
},
None => (
Vec::new(),
vec![Type::Error; params.len()],
Type::Error,
vec![None; params.len()],
),
};
let resolved_predicate = match self.top_symbols.get(&name.name) {
Some(entry) => match &entry.kind {
ValueKind::Function { type_predicate, .. } => type_predicate.clone(),
_ => None,
},
None => None,
};
let body_instantiation = self.push_body_generics(generic_names.clone())?;
let body_param_types: Vec<Type> = sig_param_types
.iter()
.map(|t| substitute_typevars(t, &body_instantiation, &self.type_limits))
.collect::<Result<_, _>>()
.map_err(type_limit_at(name.span))?;
let body_ret_type =
substitute_typevars(&sig_ret_type, &body_instantiation, &self.type_limits)
.map_err(type_limit_at(name.span))?;
let typed_params: Vec<TypedParam> = params
.iter()
.zip(sig_param_types.iter())
.zip(sig_param_defaults.iter())
.map(|((p, ty), default)| TypedParam {
name: p.name.clone(),
ty: ty.clone(),
boxed: false,
rest: p.rest,
optional: p.is_omittable(),
default: default.clone(),
})
.collect();
let stored_return = sig_ret_type.clone();
self.scopes.push();
let prev_return = self.current_return.replace(body_ret_type.clone());
let prev_reachable = self.enter_body_reachability();
let prev_predicate = self.current_type_predicate.take();
if let Some(pred) = &resolved_predicate {
let idx = pred.parameter_index as usize;
if let Some(param) = params.get(idx) {
self.current_type_predicate = Some((pred.clone(), param.name.name.clone()));
}
}
let exprs_before = self.typed_ast.exprs_len();
let stmts_before = self.typed_ast.stmts_len();
let prologue = self.infer_parameter_defaults(¶ms, &body_param_types)?;
let body_id = self
.infer_body_with_narrowing_boundary(body)?
.ok_or_else(|| {
super::inference_failure("function body is a Block, never a type-only decl")
})?;
let body_id = self.prepend_parameter_defaults(prologue, body_id)?;
self.current_return = prev_return;
self.current_type_predicate = prev_predicate;
self.restore_reachability(prev_reachable);
self.scopes.pop();
self.pop_body_generics();
for id in self
.typed_ast
.expr_ids()
.map_err(crate::typechecker::arena_failure)?
.skip(exprs_before)
{
erase_generic_params_in_expr(
self.typed_ast
.try_expr_mut(id)
.map_err(crate::typechecker::arena_failure)?,
);
}
for id in self
.typed_ast
.stmt_ids()
.map_err(crate::typechecker::arena_failure)?
.skip(stmts_before)
{
erase_generic_params_in_stmt(
self.typed_ast
.try_stmt_mut(id)
.map_err(crate::typechecker::arena_failure)?,
);
}
let ret_type = stored_return;
let mangled_name = self.mangle_top_symbol(&name.name)?;
self.add_typed_function(crate::TypedFunction {
name,
mangled_name,
generics: generic_names,
params: typed_params,
return_type: ret_type,
type_predicate: resolved_predicate,
body: body_id,
doc,
span,
})?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn infer_generic_method_call(
&mut self,
typed_receiver: ExprId,
iface_mangled: crate::MangledName,
name: Ident,
sig: MethodSig,
interface_bindings: BTreeMap<String, Type>,
type_args: Option<Vec<TypeAnnotation>>,
args: Vec<ExprId>,
expected: Option<&Type>,
span: Span,
) -> Result<(TypedExprKind, Type), CompilerFailure> {
let mut sig = sig;
if iface_mangled == crate::mangle::prelude("ArrayConstructor")
&& name.name == "from"
&& args.len() == 1
{
sig.generics = vec!["T".into()];
sig.ret = Type::Array(Box::new(Type::TypeVar("T".into())));
sig.params
.get_mut(1)
.ok_or_else(|| super::inference_failure("missing Array.from map parameter"))?
.ty = Type::Undefined;
}
let receiver_ty = self
.typed_ast
.try_expr(typed_receiver)
.map_err(crate::typechecker::arena_failure)?
.ty
.clone();
let mut sub = TypeParamSubstitution::new();
for (k, v) in interface_bindings {
sub.insert(k, v);
}
let receiver_params: Vec<crate::Param> = sig
.params
.iter()
.map(|param| crate::Param {
ty: sub.apply_or_record(¶m.ty, &self.type_limits),
..param.clone()
})
.collect();
if let Some(targs) = &type_args {
if targs.len() != sig.generics.len() {
let help = self.format_signature(SignatureKind::Method {
receiver_ty: &receiver_ty,
name: &name.name,
sig: &sig,
});
self.error_with_help(
span,
format!(
"method `{}` expects {} type argument(s), got {}",
name.name,
sig.generics.len(),
targs.len(),
),
vec![help],
);
}
let subject = format!("method `{}`", name.name);
for (gname, annot) in sig.generics.iter().zip(targs.iter()) {
let resolved = self.resolve_call_type_argument(annot, &subject)?;
sub.insert(gname.clone(), resolved);
}
}
if let Some(want) = expected.filter(|want| pins_type_parameters(want)) {
self.bind_from_expected_result(&mut sub, &sig.ret, want);
}
let has_rest = sig.params.last().is_some_and(|p| p.rest);
let fixed_count = sig.params.iter().take_while(|p| !p.rest).count();
let max_args = if has_rest {
usize::MAX
} else {
sig.params.len()
};
let min_args = super::expr::required_parameter_count(&receiver_params);
let arity_ok = args.len() >= min_args && args.len() <= max_args;
if !arity_ok {
let help = self.format_signature(SignatureKind::Method {
receiver_ty: &receiver_ty,
name: &name.name,
sig: &sig,
});
let msg = if has_rest {
format!(
"method `{}` expects {}+ argument(s), got {}",
name.name,
min_args,
args.len(),
)
} else if min_args == max_args {
format!(
"method `{}` expects {} argument(s), got {}",
name.name,
max_args,
args.len(),
)
} else {
format!(
"method `{}` expects {}-{} argument(s), got {}",
name.name,
min_args,
max_args,
args.len(),
)
};
self.error_with_help(span, msg, vec![help]);
}
let rest_elem_ty: Type = if has_rest {
match &sig
.params
.last()
.ok_or_else(|| super::inference_failure("rest signature has no parameters"))?
.ty
.rest_element()
{
Some(elem) => Type::clone(elem),
None => Type::Error,
}
} else {
Type::Error
};
let signature_help = |this: &mut Self| {
this.format_signature(SignatureKind::Method {
receiver_ty: &receiver_ty,
name: &name.name,
sig: &sig,
})
};
let errors_before_args = self.error_count();
let inferred_generics: &[String] = if type_args.is_some() {
&[]
} else {
&sig.generics
};
let (mut typed_args, argument_checks) = self.infer_generic_arguments(
&args,
&sig.params,
&sig.ret,
inferred_generics,
&rest_elem_ty,
&mut sub,
signature_help,
)?;
let object_assign =
iface_mangled == crate::mangle::prelude("ObjectConstructor") && name.name == "assign";
let assign_result = if object_assign {
Some(self.object_assign_result(&typed_args)?)
} else {
None
};
if has_rest || typed_args.len() < sig.params.len() {
self.typed_ast
.record_authored_arguments(span, typed_args.clone());
}
if arity_ok {
self.fill_omitted_defaults(&receiver_params, args.len(), span, &mut typed_args)?;
}
if arity_ok && has_rest {
self.pack_rest_tail(fixed_count, rest_elem_ty.clone(), span, &mut typed_args)?;
}
let array_from =
iface_mangled == crate::mangle::prelude("ArrayConstructor") && name.name == "from";
let array_from_mapper = array_from && sig.generics.len() == 2;
let mapper_type = typed_args
.get(1)
.map(|id| {
Ok::<_, crate::compiler_error::CompilerFailure>(
self.typed_ast
.try_expr(*id)
.map_err(crate::typechecker::arena_failure)?
.ty
.clone(),
)
})
.transpose()?;
if array_from
&& let Some(element) = sig.generics.first()
&& sub.get(element).is_none()
{
sub.insert(element.clone(), Type::Unknown);
}
let array_like = if array_from {
self.array_like_source(&typed_args)?
} else {
None
};
if array_from_mapper && array_like.is_some() {
self.reject_array_like_element_annotation(&typed_args, mapper_type.as_ref())?;
}
if array_from_mapper
&& mapper_type
.as_ref()
.is_some_and(|ty| ty.peel() == &Type::Undefined)
{
let element = sub
.apply(&Type::TypeVar("T".into()), &self.type_limits)
.map_err(type_limit_at(span))?;
sub.insert("U".into(), element);
}
sub.bind_whole_union_fallbacks();
let close_match_spans = self.check_deferred_close_matches(&mut sub, span);
self.bind_leftover_type_parameters(
&mut sub,
&sig.generics,
&sig.ret,
expected,
errors_before_args,
);
self.bind_uninferred_to_unknown(&mut sub, &sig.generics, &iface_mangled, span)?;
if let Err(unbound) = sub
.resolve_all(&sig.generics, &self.type_limits)
.map_err(type_limit_at(span))?
{
self.error_with_help(
span,
format!(
"cannot infer type parameter(s) {} for method `{}`; consider adding an explicit type argument",
unbound
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", "),
name.name,
),
vec![format!(
"add an explicit type argument: `receiver.{}<T>(…)`",
name.name
)],
);
}
self.check_arguments_after_inference(&sub, argument_checks, &close_match_spans)?;
if array_from && !array_from_mapper && array_like.is_some() {
let element = self.instantiate(&sub, &Type::TypeVar("T".into()), span)?;
self.reject_array_like_element_type(&element, span);
}
let mut result_ty = match assign_result {
Some(ty) => ty,
None => self.instantiate(&sub, &sig.ret, span)?,
};
if array_from_mapper && mapper_type.as_ref().is_some_and(|ty| {
matches!(ty.peel(), Type::Union(members) if members.iter().any(|member| member.peel() == &Type::Undefined))
}) {
result_ty = Type::Array(Box::new(Type::union(vec![
self.instantiate(&sub, &Type::TypeVar("T".into()), span)?,
self.instantiate(&sub, &Type::TypeVar("U".into()), span)?,
])));
}
let type_predicate = match &sig.predicate {
Some(p) => Some(Box::new(crate::TypePredicate {
parameter_index: p.parameter_index,
asserted_type: self.instantiate(&sub, &p.asserted_type, span)?,
})),
None => None,
};
self.record_generic_call_arguments(&typed_args, &sig.params, &sig.ret, type_args.is_some());
let any_generic_arg = sig.params.iter().any(|p| matches!(p.ty, Type::TypeVar(_)));
let return_needs_cast = return_erases_to_object_slot(&sig.ret);
if any_generic_arg || return_needs_cast {
let generic_args: Vec<crate::GenericArgument> = typed_args
.into_iter()
.zip(sig.params.iter())
.map(|(expr, p)| crate::GenericArgument {
expr,
is_generic: matches!(p.ty, Type::TypeVar(_)),
})
.collect();
let return_cast = if object_assign && result_ty.is_structural_object() {
Some(Type::any_object())
} else if return_needs_cast {
Some(result_ty.clone())
} else {
None
};
return Ok((
TypedExprKind::GenericMethodCall {
receiver: typed_receiver,
iface: iface_mangled,
name,
args: generic_args,
return_cast,
type_predicate,
},
result_ty,
));
}
Ok((
TypedExprKind::MethodCall {
receiver: typed_receiver,
iface: iface_mangled,
name,
args: typed_args,
type_predicate,
},
result_ty,
))
}
fn generic_callee_lift(
&self,
callee: GenericCallee,
callee_ident: &Ident,
generics: &[String],
params: &[Param],
ret: &Type,
) -> String {
match callee {
GenericCallee::Function => {
let doc = self.lookup_function_doc(&callee_ident.name);
self.format_signature(SignatureKind::Function {
name: &callee_ident.name,
predicate: None,
generics,
params,
ret,
doc: doc.as_ref(),
})
}
GenericCallee::Constructor => {
let header = if generics.is_empty() {
callee_ident.name.clone()
} else {
format!("{}<{}>", callee_ident.name, generics.join(", "))
};
let rendered: Vec<String> = params
.iter()
.map(|p| format!("{}: {}", p.name, p.ty))
.collect();
format!("class {header} {{ constructor({}); }}", rendered.join(", "))
}
}
}
fn bind_leftover_type_parameters(
&self,
sub: &mut TypeParamSubstitution,
generics: &[String],
ret: &Type,
expected: Option<&Type>,
errors_before_args: usize,
) {
if let Some(want) = expected.filter(|want| !pins_type_parameters(want)) {
let _ = sub.unify(ret, &writable_view_for(ret, want), self.resolver());
}
if self.error_count() > errors_before_args
&& let Err(exceeded) = bind_remaining(sub, generics, Type::Error, &self.type_limits)
{
self.type_limits.record(exceeded);
}
}
fn bind_from_expected_result(&self, sub: &mut TypeParamSubstitution, ret: &Type, want: &Type) {
let before = sub.clone();
self.bind_from_expected_type(sub, ret, want);
sub.mark_from_expected_result(&before);
sub.forget_close_matches_after(before.close_match_count());
}
fn bind_from_expected_type(&self, sub: &mut TypeParamSubstitution, ret: &Type, want: &Type) {
let want = &writable_view_for(ret, want);
if let Type::Union(members) = ret.peel() {
let bare: Vec<&String> = members
.iter()
.filter_map(|member| match member.peel() {
Type::TypeVar(name) if sub.is_unbound(name) => Some(name),
_ => None,
})
.collect();
if bare.len() > 1 {
for name in bare {
sub.insert(name.clone(), want.clone());
}
return;
}
}
let unifies_with_whole_union = matches!(ret.peel(), Type::TypeVar(_) | Type::Union(_));
let members = match want.peel() {
Type::Union(members) if !unifies_with_whole_union => members,
_ => {
let _ = sub.unify(ret, want, self.resolver());
return;
}
};
let ret_kind = ShapeKind::of(ret);
let mut agreed: Option<TypeParamSubstitution> = None;
for member in members {
if !ret_kind.could_be(ShapeKind::of(member)) {
continue;
}
let mut trial = sub.clone();
if trial.unify(ret, member, self.resolver()).is_err() {
return;
}
match &agreed {
Some(bindings) if *bindings != trial => return,
_ => agreed = Some(trial),
}
}
if let Some(bindings) = agreed {
*sub = bindings;
}
}
fn bind_uninferred_to_unknown(
&self,
sub: &mut TypeParamSubstitution,
generics: &[String],
declared_by: &crate::MangledName,
span: Span,
) -> Result<(), CompilerFailure> {
if crate::mangle::is_builtin(declared_by) {
return Ok(());
}
bind_remaining(sub, generics, Type::Unknown, &self.type_limits).map_err(type_limit_at(span))
}
fn check_deferred_close_matches(
&mut self,
sub: &mut TypeParamSubstitution,
span: Span,
) -> Vec<Span> {
let mut reported = Vec::new();
for close_match in sub.take_close_matches() {
if sub
.clone()
.unify(&close_match.param, &close_match.arg, self.resolver())
.is_ok()
{
continue;
}
let error_span = close_match.argument_span.unwrap_or(span);
let (expected, got) = self.close_match_mismatch(sub, close_match);
self.error_with_help(
error_span,
format!("expected `{expected}`, got `{got}`"),
self.render_help_list(super::type_diff::type_mismatch_help(&expected, &got)),
);
reported.push(error_span);
}
reported
}
fn close_match_mismatch(
&self,
sub: &TypeParamSubstitution,
close_match: CloseMatch,
) -> (Type, Type) {
match sub.clone().unify(
&close_match.matched_member,
&close_match.arg,
self.resolver(),
) {
Err(UnifyError::Mismatch { expected, got }) => {
(sub.apply_or_record(&expected, &self.type_limits), got)
}
_ => (
sub.apply_or_record(&close_match.matched_member, &self.type_limits),
close_match.arg,
),
}
}
fn instantiate(
&self,
sub: &TypeParamSubstitution,
ty: &Type,
span: Span,
) -> Result<Type, CompilerFailure> {
sub.apply(ty, &self.type_limits)
.map_err(type_limit_at(span))
}
fn bind_from_annotated_params(
&mut self,
literal: ExprId,
param_ty: &Type,
sub: &mut TypeParamSubstitution,
) -> Result<(), CompilerFailure> {
let Some(Type::Function { params, .. }) = function_part(param_ty) else {
return Ok(());
};
let Some(declared_params) = self.function_literal_params(literal)? else {
return Ok(());
};
let diagnostics_before = self.diagnostics.len();
let close_matches_before = sub.close_match_count();
for (declared, param) in declared_params.iter().zip(params) {
if let Some(annotation) = &declared.ty {
let annotated = self.resolve_type(annotation)?;
let _ = sub.unify_argument(param, &annotated, self.resolver());
}
}
sub.forget_close_matches_after(close_matches_before);
self.diagnostics.truncate(diagnostics_before);
Ok(())
}
fn start_literal_argument_inference(
&mut self,
arg: ExprId,
param_ty: &Type,
sub: &TypeParamSubstitution,
) -> Result<Option<LiteralArgumentInference>, CompilerFailure> {
let enclosing = self.literal_argument_inference.take();
let slots = match self
.ast
.try_expr(arg)
.map_err(super::arena_failure)?
.kind
.clone()
{
ExprKind::ObjectLiteral { members } => {
self.object_argument_fields(&members, param_ty)?
}
ExprKind::ArrayLiteral { elements } => {
self.tuple_argument_slots(&elements, param_ty)?
}
_ => None,
};
if let Some(slots) = slots {
self.literal_argument_inference = Some(LiteralArgumentInference {
literal: arg,
slots,
sub: sub.clone(),
fallback_echoes: Vec::new(),
});
}
Ok(enclosing)
}
fn object_argument_fields(
&self,
members: &[crate::ObjectLiteralMember],
param_ty: &Type,
) -> Result<Option<BTreeMap<String, crate::ObjectField>>, CompilerFailure> {
let fields = match param_ty.peel() {
Type::Object { fields, .. } => fields.clone(),
interface @ Type::InterfaceRef { .. } => {
match super::assignable::expand_interface_data_shape(interface, self.resolver()) {
Some(Type::Object { fields, .. }) => fields,
_ => return Ok(None),
}
}
_ => return Ok(None),
};
for member in members {
let crate::ObjectLiteralMember::Field(field) = member else {
continue;
};
if self.is_context_sensitive_function(field.value)? {
return Ok(Some(fields));
}
let field_builds_from_type_parameters = fields
.get(&field.name.name)
.is_some_and(|declared| builds_from_type_parameters(&declared.ty));
if field_builds_from_type_parameters
&& self.may_infer_type_arguments_from_context(field.value)?
{
return Ok(Some(fields));
}
}
Ok(None)
}
fn may_infer_type_arguments_from_context(&self, expr: ExprId) -> Result<bool, CompilerFailure> {
let expr = super::expr::peel_parens(self.ast, expr)?;
let ExprKind::New {
callee,
type_args: None,
args,
} = &self.ast.try_expr(expr).map_err(super::arena_failure)?.kind
else {
return Ok(false);
};
if !args.is_empty() {
return Ok(false);
}
let ExprKind::Identifier(ident) = &self
.ast
.try_expr(*callee)
.map_err(super::arena_failure)?
.kind
else {
return Ok(false);
};
match self
.lookup_named_type(&ident.name)
.map(|symbol| &symbol.kind)
{
Some(crate::TypeKind::Class { generics, .. }) => Ok(!generics.is_empty()),
_ => Ok(true),
}
}
pub(super) fn type_params_of_one_field(
&self,
arg: ExprId,
param_ty: &Type,
inferred: &[String],
) -> Result<Vec<String>, CompilerFailure> {
let ExprKind::ObjectLiteral { members } =
&self.ast.try_expr(arg).map_err(super::arena_failure)?.kind
else {
return Ok(inferred.to_vec());
};
let shapes = self.object_shapes(param_ty);
if shapes.is_empty() {
return Ok(inferred.to_vec());
}
if members
.iter()
.any(|member| matches!(member, crate::ObjectLiteralMember::Spread { .. }))
{
return Ok(Vec::new());
}
let mut given_fields = Vec::new();
for member in members {
let crate::ObjectLiteralMember::Field(field) = member else {
continue;
};
if !self.is_plain_value(field.value)? {
given_fields.push(field.name.name.as_str());
}
}
let most_fields_typed_by = |name: &str| {
shapes
.iter()
.map(|fields| {
given_fields
.iter()
.filter_map(|field| fields.get(*field))
.filter(|field| is_or_has_type_var(&field.ty, name))
.count()
})
.max()
.unwrap_or(0)
};
Ok(inferred
.iter()
.filter(|name| most_fields_typed_by(name) <= 1)
.cloned()
.collect())
}
fn is_plain_value(&self, expr: ExprId) -> Result<bool, CompilerFailure> {
let kind = &self.ast.try_expr(expr).map_err(super::arena_failure)?.kind;
Ok(match kind {
ExprKind::Paren(inner)
| ExprKind::PostfixUnary {
op: crate::PostfixOp::NonNullAssert,
operand: inner,
} => self.is_plain_value(*inner)?,
ExprKind::Binary {
op: crate::BinOp::And | crate::BinOp::Or | crate::BinOp::NullishCoalesce,
lhs,
rhs,
} => self.is_plain_value(*lhs)? && self.is_plain_value(*rhs)?,
ExprKind::Ternary { then_, else_, .. } => {
self.is_plain_value(*then_)? && self.is_plain_value(*else_)?
}
ExprKind::Number(_)
| ExprKind::BigInt(_)
| ExprKind::String(_)
| ExprKind::Boolean(_)
| ExprKind::Null
| ExprKind::ObjectLiteral { .. }
| ExprKind::ArrayLiteral { .. }
| ExprKind::TemplateLiteral { .. }
| ExprKind::Regex { .. }
| ExprKind::Binary { .. }
| ExprKind::Unary { .. }
| ExprKind::PostfixUnary { .. }
| ExprKind::Typeof { .. }
| ExprKind::Delete { .. }
| ExprKind::InstanceOf { .. } => true,
_ => false,
})
}
pub(super) fn object_shapes(&self, ty: &Type) -> Vec<BTreeMap<String, crate::ObjectField>> {
match ty.peel() {
Type::Object { fields, .. } => vec![fields.clone()],
interface @ Type::InterfaceRef { .. } => {
match super::assignable::expand_interface_data_shape(interface, self.resolver()) {
Some(Type::Object { fields, .. }) => vec![fields],
_ => Vec::new(),
}
}
Type::Union(members) => members
.iter()
.filter(|member| !matches!(member.peel(), Type::Union(_)))
.flat_map(|member| self.object_shapes(member))
.collect(),
_ => Vec::new(),
}
}
fn tuple_argument_slots(
&self,
elements: &[crate::ArrayLiteralElement],
param_ty: &Type,
) -> Result<Option<BTreeMap<String, crate::ObjectField>>, CompilerFailure> {
let Type::Tuple(slots) = super::expr::array_literal_hint_shape(param_ty) else {
return Ok(None);
};
if slots.len() != elements.len() {
return Ok(None);
}
let mut has_context_sensitive_element = false;
for element in elements {
let crate::ArrayLiteralElement::Value(value) = element else {
return Ok(None);
};
has_context_sensitive_element |= self.is_context_sensitive_function(*value)?;
}
Ok(has_context_sensitive_element.then(|| {
slots
.iter()
.enumerate()
.map(|(index, slot)| {
(
index.to_string(),
crate::ObjectField::required(slot.clone()),
)
})
.collect()
}))
}
fn finish_literal_argument_inference(
&mut self,
enclosing: Option<LiteralArgumentInference>,
sub: &mut TypeParamSubstitution,
) {
if let Some(mut finished) =
std::mem::replace(&mut self.literal_argument_inference, enclosing)
{
finished
.sub
.bind_whole_union_fallbacks_named(&finished.fallback_echoes);
*sub = finished.sub;
}
}
pub(super) fn infers_one_at_a_time(&self, literal: ExprId) -> bool {
self.literal_argument_inference
.as_ref()
.is_some_and(|inference| inference.literal == literal)
}
pub(super) fn argument_slot_hint(&self, literal: ExprId, name: &str) -> Option<Type> {
let inference = self
.literal_argument_inference
.as_ref()
.filter(|inference| inference.literal == literal)?;
let slot = inference.slots.get(name)?;
let mut sub = inference.sub.clone();
if let Some(Type::Function { params, .. }) = function_part(&slot.ty) {
sub.bind_whole_union_fallbacks_in(params);
}
Some(sub.apply_or_record(&slot.ty, &self.type_limits))
}
pub(super) fn infer_argument_slot_value<R>(
&mut self,
literal: ExprId,
name: &str,
value: ExprId,
infer: impl FnOnce(&mut Self) -> Result<R, CompilerFailure>,
) -> Result<R, CompilerFailure> {
if !self.slot_widens_candidate(literal, name) {
return infer(self);
}
if !self.values_widening_candidates.insert(value) {
return infer(self);
}
let inferred = infer(self);
self.values_widening_candidates.remove(&value);
inferred
}
fn slot_widens_candidate(&self, literal: ExprId, name: &str) -> bool {
let Some(inference) = self
.literal_argument_inference
.as_ref()
.filter(|inference| inference.literal == literal)
else {
return false;
};
let Some(Type::TypeVar(var)) = inference.slots.get(name).map(|slot| slot.ty.peel()) else {
return false;
};
inference.sub.is_widenable(var)
}
pub(super) fn field_keeps_returned_literals(
&self,
literal: ExprId,
field_ty: Option<&Type>,
) -> bool {
let Some((argument, inferred)) = &self.fields_keeping_returned_literals else {
return false;
};
*argument == literal
&& matches!(field_ty.map(Type::peel), Some(Type::TypeVar(name)) if inferred.contains(name))
}
pub(super) fn infer_from_argument_slot(
&mut self,
literal: ExprId,
name: &str,
value_ty: &Type,
) {
let Some(mut inference) = self
.literal_argument_inference
.take_if(|inference| inference.literal == literal)
else {
return;
};
if let Some(slot) = inference.slots.get(name) {
let before = inference.sub.clone();
let close_matches_before = inference.sub.close_match_count();
let _ = inference
.sub
.unify_argument(&slot.ty, value_ty, self.resolver());
inference
.sub
.nest_close_matches_after(close_matches_before, name);
if function_part(&slot.ty).is_some() {
let echoes = inference.sub.unbind_fallback_echoes(&before);
inference.fallback_echoes.extend(echoes);
}
}
self.literal_argument_inference = Some(inference);
}
pub(super) fn object_field_hint(
&self,
literal: ExprId,
name: &str,
expected_fields: Option<&BTreeMap<String, crate::ObjectField>>,
expected_index: Option<&crate::IndexSignature>,
) -> Option<Type> {
super::reserved::override_field_signature(name)
.or_else(|| self.argument_slot_hint(literal, name))
.or_else(|| expected_field_hint(name, expected_fields, expected_index))
}
pub(super) fn infer_fields_ahead_of_callbacks(
&mut self,
literal: ExprId,
members: &[crate::ObjectLiteralMember],
expected_fields: Option<&BTreeMap<String, crate::ObjectField>>,
expected_index: Option<&crate::IndexSignature>,
inferred: &mut super::expr::InferredObjectFields,
) -> Result<std::collections::BTreeSet<ExprId>, CompilerFailure> {
let mut ahead = std::collections::BTreeSet::new();
if !self.infers_one_at_a_time(literal) {
return Ok(ahead);
}
let mut fields = Vec::new();
for member in members {
let crate::ObjectLiteralMember::Field(field) = member else {
return Ok(ahead);
};
if matches!(
self.ast
.try_expr(field.value)
.map_err(super::arena_failure)?
.kind,
ExprKind::FunctionExpression { .. }
) {
return Ok(ahead);
}
fields.push(field);
}
for field in fields {
if self.is_context_sensitive_function(field.value)?
|| inferred.contains_key(&field.value)
|| super::reserved::is_reserved_object_field(&field.name.name)
{
continue;
}
let name = &field.name.name;
let hint = self.object_field_hint(literal, name, expected_fields, expected_index);
let expected_field_ty = expected_fields
.and_then(|fields| fields.get(name))
.map(|field| &field.ty);
self.next_function_keeps_returned_literals =
self.field_keeps_returned_literals(literal, expected_field_ty);
let errors_before = self.error_count();
let (typed_value, value_ty) =
self.infer_argument_slot_value(literal, name, field.value, |this| {
this.infer_expr(field.value, hint.as_ref())
})?;
self.next_function_keeps_returned_literals = false;
let errored = self.error_count() > errors_before;
let unified_ty =
if expected_field_ty.is_some_and(super::expr::is_type_parameter_position) {
self.widen_fresh_literals(typed_value, &value_ty)?
} else {
value_ty.clone()
};
self.infer_from_argument_slot(literal, name, &unified_ty);
inferred.insert(field.value, (typed_value, value_ty, errored));
ahead.insert(field.value);
}
Ok(ahead)
}
pub(super) fn function_literal_params(
&self,
expr: ExprId,
) -> Result<Option<Vec<crate::ParamDecl>>, CompilerFailure> {
Ok(
match &self.ast.try_expr(expr).map_err(super::arena_failure)?.kind {
ExprKind::Paren(inner) => self.function_literal_params(*inner)?,
ExprKind::FunctionExpression { function, .. } => {
self.function_literal_params(*function)?
}
ExprKind::Arrow { params, .. } => Some(params.clone()),
_ => None,
},
)
}
pub(super) fn is_context_sensitive_function(
&self,
expr: ExprId,
) -> Result<bool, CompilerFailure> {
Ok(self
.function_literal_params(expr)?
.is_some_and(|params| params.iter().any(|p| p.ty.is_none())))
}
pub(super) fn is_fully_annotated_function(
&self,
expr: ExprId,
) -> Result<bool, CompilerFailure> {
Ok(self
.function_literal_params(expr)?
.is_some_and(|params| params.iter().all(|p| p.ty.is_some())))
}
fn type_params_fixed_by_callbacks<'p>(
&mut self,
args_with_param_types: &'p [(ExprId, Type)],
inferred_generics: &[String],
) -> Result<(std::collections::BTreeSet<String>, Vec<&'p Type>), CompilerFailure> {
let mut callback_params = Vec::new();
for (arg_id, param_ty) in args_with_param_types {
if self.is_context_sensitive_function(*arg_id)?
|| self.literal_holds_context_sensitive_function(*arg_id)?
{
callback_params.push(param_ty);
}
}
let names =
self.type_params_bindable_by_later_arguments(args_with_param_types, inferred_generics)?;
Ok((names, callback_params))
}
#[allow(clippy::too_many_arguments)]
fn infer_generic_arguments(
&mut self,
args: &[ExprId],
params: &[Param],
ret: &Type,
inferred_generics: &[String],
rest_elem_ty: &Type,
sub: &mut TypeParamSubstitution,
signature_help: impl Fn(&mut Self) -> String,
) -> Result<(Vec<ExprId>, Vec<ArgumentCheck>), CompilerFailure> {
let has_rest = params.last().is_some_and(|p| p.rest);
let fixed_count = params.iter().take_while(|p| !p.rest).count();
let mut checks = Vec::new();
let args_with_param_types: Vec<(ExprId, Type)> = args
.iter()
.enumerate()
.map(|(i, &arg_id)| {
let param_ty = match params.get(i) {
Some(param) if i < fixed_count => super::expr::callable_parameter_type(param),
_ if has_rest => rest_elem_ty.clone(),
_ => Type::Error,
};
(arg_id, param_ty)
})
.collect();
let (fixed_by_callbacks, callback_params) =
self.type_params_fixed_by_callbacks(&args_with_param_types, inferred_generics)?;
let arguments = GenericArguments {
inferred_generics,
sourced_by_literals: self
.literal_inferred_type_params(&args_with_param_types, inferred_generics)?,
literal_types: LiteralTypeArguments::new(
&args_with_param_types,
ret,
inferred_generics,
FixedByCallbacks {
names: &fixed_by_callbacks,
callback_params: &callback_params,
},
),
};
let mut typed_slots: Vec<Option<ExprId>> = vec![None; args.len()];
for deferred_pass in [false, true] {
for (i, (arg_id, param_ty)) in args_with_param_types.iter().enumerate() {
let (arg_id, param_ty) = (*arg_id, param_ty.clone());
if typed_slots[i].is_some() {
continue;
}
let deferred = function_part(¶m_ty).is_some()
&& self.is_context_sensitive_function(arg_id)?;
if deferred && !deferred_pass {
self.bind_from_annotated_params(arg_id, ¶m_ty, sub)?;
}
if deferred != deferred_pass {
continue;
}
if deferred {
fix_callback_parameters(sub, ¶m_ty, inferred_generics);
} else {
self.prebind_from_literal_argument(i, &args_with_param_types, &arguments, sub)?;
}
let errors_before = self.error_count();
let diagnostics_before = self.diagnostics.len();
let close_matches_before = sub.close_match_count();
let (typed_id, arg_ty) =
self.infer_generic_argument(arg_id, ¶m_ty, &arguments, sub)?;
typed_slots[i] = Some(typed_id);
let missing_slot = i >= fixed_count && !has_rest;
if missing_slot || matches!(arg_ty, Type::Error) {
continue;
}
let already_reported = self.error_count() > errors_before;
if already_reported {
sub.keep_replaceable_bindings(¶m_ty);
}
let unified = if self.builds_literal(arg_id)? {
sub.unify_literal_argument(¶m_ty, &arg_ty, self.resolver())
} else {
sub.unify_argument(¶m_ty, &arg_ty, self.resolver())
};
if unified.is_err() {
sub.forget_close_matches_after(close_matches_before);
} else {
self.locate_argument_close_matches(
sub,
arg_id,
typed_id,
close_matches_before,
diagnostics_before,
)?;
}
match unified {
Err(error) => self.unify_argument_error(
error,
sub,
(¶m_ty, &arg_ty),
typed_id,
already_reported,
&signature_help,
)?,
Ok(()) if !already_reported => checks.push(ArgumentCheck {
arg: typed_id,
param_ty,
arg_ty,
}),
Ok(()) => {}
}
}
}
Ok((typed_slots.into_iter().flatten().collect(), checks))
}
fn check_arguments_after_inference(
&mut self,
sub: &TypeParamSubstitution,
checks: Vec<ArgumentCheck>,
already_reported: &[Span],
) -> Result<(), CompilerFailure> {
for check in checks {
let param = sub.apply_or_record(&check.param_ty, &self.type_limits);
if super::expr::type_contains_type_var(¶m) {
continue;
}
if super::assignable(&check.arg_ty, ¶m, self.resolver()) {
if super::expr::type_contains_type_var(&check.param_ty) {
self.retype_argument_literals(check.arg, ¶m)?;
}
continue;
}
let span = self.argument_span(check.arg)?;
if already_reported
.iter()
.any(|reported| span_contains(span, *reported))
{
continue;
}
let help =
self.render_help_list(super::type_diff::type_mismatch_help(¶m, &check.arg_ty));
self.error_with_help(
span,
format!("expected `{param}`, got `{}`", check.arg_ty),
help,
);
}
Ok(())
}
pub(super) fn infer_generic_argument(
&mut self,
arg_id: ExprId,
param_ty: &Type,
arguments: &GenericArguments,
sub: &mut TypeParamSubstitution,
) -> Result<(ExprId, Type), CompilerFailure> {
let hint = sub.apply_or_record(param_ty, &self.type_limits);
let diagnostics_before = self.diagnostics.len();
let errors_before = self.error_count();
let first = self.infer_generic_argument_with(arg_id, param_ty, hint, arguments, sub)?;
if self.error_count() == errors_before
|| !sub.mentions_candidate_binding(param_ty)
|| (self.builds_literal(arg_id)?
&& sub.mentions_non_literal_candidate_binding(param_ty))
{
return Ok(first);
}
let first_diagnostics = self.diagnostics.split_off(diagnostics_before);
let mut loose = sub.without_candidate_bindings();
for name in arguments.inferred_generics {
if let Some(bound) = sub.get(name).filter(|_| takes_in_callback(param_ty, name)) {
loose.insert(name.clone(), bound.clone());
}
}
let loose_hint = loose.apply_or_record(param_ty, &self.type_limits);
let second =
self.infer_generic_argument_with(arg_id, param_ty, loose_hint, arguments, sub)?;
if self.error_count() == errors_before {
return Ok(second);
}
self.diagnostics.truncate(diagnostics_before);
self.diagnostics.extend(first_diagnostics);
Ok(first)
}
fn infer_generic_argument_with(
&mut self,
arg_id: ExprId,
param_ty: &Type,
hint: Type,
arguments: &GenericArguments,
sub: &mut TypeParamSubstitution,
) -> Result<(ExprId, Type), CompilerFailure> {
let hint = self.method_interface_literal_hint(arg_id, hint, param_ty, sub)?;
if let Type::InterfaceRef { name, .. } = hint.peel()
&& self.builds_literal(arg_id)?
{
let literal = super::expr::peel_parens(self.ast, arg_id)?;
self.literal_hint_interfaces.insert(literal, name.clone());
}
let hint = self.literal_argument_hint(arg_id, hint, arguments.inferred_generics)?;
let hinted_by_replaceable_binding = sub.mentions_replaceable_binding(param_ty);
if hinted_by_replaceable_binding {
self.arguments_with_replaceable_hints.insert(arg_id);
}
let enclosing = self.start_literal_argument_inference(arg_id, param_ty, sub)?;
let params_keeping_returned_literals =
self.type_params_of_one_field(arg_id, param_ty, arguments.inferred_generics)?;
let enclosing_fields = self
.fields_keeping_returned_literals
.replace((arg_id, params_keeping_returned_literals));
let keeps_literal = arguments.literal_types.keeps(param_ty);
let inferred = self.with_inferred_positions(
arg_id,
param_ty,
&arguments.sourced_by_literals,
|this| {
this.keeps_literal_types = keeps_literal;
this.next_function_keeps_returned_literals =
arguments.literal_types.keeps_returned(param_ty);
this.infer_expr(arg_id, Some(&hint))
},
);
self.finish_literal_argument_inference(enclosing, sub);
self.fields_keeping_returned_literals = enclosing_fields;
self.arguments_with_replaceable_hints.remove(&arg_id);
let (typed_id, arg_ty) = inferred?;
if keeps_literal {
self.record_kept_literal_argument(typed_id);
}
let hint_is_known = !super::expr::mentions_type_var(&hint, &|var| {
arguments.inferred_generics.iter().any(|name| name == var)
});
let fits_binding = (hinted_by_replaceable_binding || hint_is_known)
&& super::assignable(&arg_ty, &hint, self.resolver());
if arguments.literal_types.widens(param_ty)
&& !fits_binding
&& !is_literal_member_of(&arg_ty, param_ty)
&& !self.fits_without_fallback_type_params(sub, param_ty, &arg_ty)
{
let widened = self.widen_fresh_literals(typed_id, &arg_ty)?;
return Ok((typed_id, widened));
}
Ok((typed_id, arg_ty))
}
fn fits_without_fallback_type_params(
&self,
sub: &TypeParamSubstitution,
param_ty: &Type,
arg_ty: &Type,
) -> bool {
sub.without_fallback_type_params(param_ty, &self.type_limits)
.is_some_and(|concrete| super::assignable(arg_ty, &concrete, self.resolver()))
}
fn method_interface_literal_hint(
&mut self,
arg_id: ExprId,
hint: Type,
param_ty: &Type,
sub: &TypeParamSubstitution,
) -> Result<Type, CompilerFailure> {
let Type::InterfaceRef {
mangled,
name,
args,
..
} = hint.peel()
else {
return Ok(hint);
};
if !super::expr::type_contains_type_var(&hint)
|| !matches!(
self.ast
.try_expr(arg_id)
.map_err(super::arena_failure)?
.kind,
ExprKind::ObjectLiteral { .. }
)
|| !self.resolver().interface_has_methods(mangled, name)
{
return Ok(hint);
}
let Some(fields) = self.structural_form(mangled, name, args) else {
return Ok(hint);
};
let shape = Type::Object {
fields,
index: None,
};
let diagnostics_before = self.diagnostics.len();
let scratch = self.infer_expr(arg_id, Some(&shape));
self.diagnostics.truncate(diagnostics_before);
let (_, scratch_ty) = scratch?;
let mut bound = sub.clone();
let _ = bound.unify_argument(&shape, &scratch_ty, self.resolver());
Ok(bound.apply_or_record(param_ty, &self.type_limits))
}
fn literal_argument_hint(
&self,
arg_id: ExprId,
hint: Type,
inferred_generics: &[String],
) -> Result<Type, CompilerFailure> {
if self.builds_literal(arg_id)? {
return Ok(expand_hint_interfaces(
&hint,
inferred_generics,
self.resolver(),
));
}
if self.function_literal_params(arg_id)?.is_none() {
return Ok(hint);
}
let Type::Function {
params,
optional,
ret,
predicate,
has_rest,
} = hint.peel()
else {
return Ok(hint);
};
Ok(Type::Function {
params: params.clone(),
optional: *optional,
ret: Box::new(expand_hint_interfaces(
ret,
inferred_generics,
self.resolver(),
)),
predicate: predicate.clone(),
has_rest: *has_rest,
})
}
pub(super) fn builds_literal(&self, expr: ExprId) -> Result<bool, CompilerFailure> {
Ok(
match &self.ast.try_expr(expr).map_err(super::arena_failure)?.kind {
ExprKind::ObjectLiteral { .. } | ExprKind::ArrayLiteral { .. } => true,
ExprKind::Paren(inner) => self.builds_literal(*inner)?,
ExprKind::Ternary { then_, else_, .. } => {
self.builds_literal(*then_)? && self.builds_literal(*else_)?
}
_ => false,
},
)
}
fn array_like_source(
&mut self,
typed_args: &[ExprId],
) -> Result<Option<Type>, crate::compiler_error::CompilerFailure> {
let Some(source) = typed_args.first() else {
return Ok(None);
};
let source = self
.typed_ast
.try_expr(*source)
.map_err(crate::typechecker::arena_failure)?;
let (source_ty, source_span) = (source.ty.clone(), source.span);
if matches!(source_ty.peel(), Type::Error | Type::Unknown) {
return Ok(None);
}
let iterable = Type::union(vec![
Type::Readonly(Box::new(Type::Array(Box::new(Type::Unknown)))),
Type::prelude_interface("Iterator".to_string(), vec![Type::Unknown]),
Type::prelude_interface("Iterable".to_string(), vec![Type::Unknown]),
]);
let has_length = crate::runtime::prelude::array::array_like_type();
if super::assignable(&source_ty, &iterable, self.resolver())
|| !super::assignable(&source_ty, &has_length, self.resolver())
{
return Ok(None);
}
if self.has_keyed_entries(&source_ty) {
self.error_with_help(
source_span,
format!(
"`Array.from` doesn't read the keyed entries of `{source_ty}`: Submilli \
treats an array-like `{{ length }}` as having no elements"
),
vec!["convert the entries to an array first, or pass an array".to_string()],
);
return Ok(None);
}
if !matches!(source_ty.peel(), Type::Object { .. }) {
self.error_with_help(
source_span,
format!(
"`Array.from` takes an array-like only as an object type: `{source_ty}` \
has no number index signature, so it isn't an `ArrayLike`"
),
vec![
"annotate the value with an object type such as `{ length: number }`, or \
pass an array"
.to_string(),
],
);
return Ok(None);
}
Ok(Some(source_ty))
}
fn has_keyed_entries(&self, source: &Type) -> bool {
if self.resolver().index_signature(source).is_some() {
return true;
}
match source.peel() {
Type::Object { fields, .. } => fields.keys().any(|name| is_array_index(name)),
Type::Union(members) => members.iter().any(|member| self.has_keyed_entries(member)),
_ => false,
}
}
fn reject_array_like_element_type(&mut self, element: &Type, span: Span) {
if admits_undefined(element) {
return;
}
self.error_with_help(
span,
format!(
"`Array.from` of an array-like `{{ length }}` has no elements, so it makes an \
array of `undefined`, not of `{element}`"
),
vec![
"give the array an element type that admits `undefined`, such as \
`(number | undefined)[]`, or pass a callback that builds each element from its \
index"
.to_string(),
],
);
}
fn reject_array_like_element_annotation(
&mut self,
typed_args: &[ExprId],
mapper_type: Option<&Type>,
) -> Result<(), crate::compiler_error::CompilerFailure> {
let Some(mapper) = typed_args.get(1) else {
return Ok(());
};
let Some(Type::Function { params, .. }) = mapper_type.map(Type::peel) else {
return Ok(());
};
let Some(element) = params.first() else {
return Ok(());
};
if admits_undefined(element) {
return Ok(());
}
let span = self
.typed_ast
.try_expr(*mapper)
.map_err(crate::typechecker::arena_failure)?
.span;
self.error_with_help(
span,
format!(
"`Array.from` of an array-like `{{ length }}` has no elements, so the callback's \
element parameter receives `undefined`, not a `{element}`"
),
vec![
"declare the element parameter as `unknown` or a type that admits `undefined`, or \
leave it unannotated, and build the value from the index"
.to_string(),
],
);
Ok(())
}
fn unify_argument_error(
&mut self,
error: UnifyError,
sub: &mut TypeParamSubstitution,
(param_ty, arg_ty): (&Type, &Type),
arg: ExprId,
already_reported: bool,
signature_help: &impl Fn(&mut Self) -> String,
) -> Result<(), crate::compiler_error::CompilerFailure> {
let arg_span = self.argument_span(arg)?;
let _: () = match error {
UnifyError::Conflict { .. } if already_reported => {}
UnifyError::Conflict { name, prev, new } => self.error(
arg_span,
format!(
"type parameter `{name}` already bound to `{prev}`, cannot bind to `{new}`"
),
),
UnifyError::Mismatch { expected, got } => {
if self.structural_member_unify(sub, param_ty, arg_ty) || already_reported {
return Ok(());
}
let expected = sub.apply_or_record(&expected, &self.type_limits);
let mut help = vec![signature_help(self)];
help.extend(
self.render_help_list(super::type_diff::type_mismatch_help(&expected, &got)),
);
self.error_with_help(
arg_span,
super::diagnostics::mismatch_message(&expected, &got),
help,
);
}
};
Ok(())
}
fn locate_argument_close_matches(
&self,
sub: &mut TypeParamSubstitution,
arg: ExprId,
typed_arg: ExprId,
close_matches_before: usize,
diagnostics_before: usize,
) -> Result<(), CompilerFailure> {
let arg_span = self.argument_span(typed_arg)?;
sub.locate_close_matches_after(close_matches_before, |path| {
self.argument_field_span(arg, path).unwrap_or(arg_span)
});
let reported: Vec<Span> = self
.diagnostics
.get(diagnostics_before..)
.unwrap_or_default()
.iter()
.filter(|diagnostic| diagnostic.severity == crate::Severity::Error)
.map(|diagnostic| diagnostic.span)
.collect();
sub.retain_close_matches_after(close_matches_before, |close_match| {
close_match
.argument_span
.is_some_and(|span| !reported.iter().any(|error| span_contains(span, *error)))
});
Ok(())
}
fn argument_field_span(&self, arg: ExprId, path: &[String]) -> Option<Span> {
let mut expr = self.ast.try_expr(arg).ok()?;
let mut found = None;
for name in path {
while let ExprKind::Paren(inner) = expr.kind {
expr = self.ast.try_expr(inner).ok()?;
}
let ExprKind::ObjectLiteral { members } = &expr.kind else {
break;
};
let value = members.iter().rev().find_map(|member| match member {
crate::ObjectLiteralMember::Field(field) if field.name.name == *name => {
Some(field.value)
}
_ => None,
});
let Some(value) = value else {
break;
};
expr = self.ast.try_expr(value).ok()?;
found = Some(expr.span);
}
found
}
fn argument_span(&self, arg: ExprId) -> Result<Span, crate::compiler_error::CompilerFailure> {
Ok(self
.typed_ast
.try_expr(arg)
.map_err(crate::typechecker::arena_failure)?
.span)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn infer_generic_call(
&mut self,
callee_ident: Ident,
generics: Vec<String>,
params: Vec<Param>,
ret: Type,
mangled: crate::MangledName,
predicate: Option<crate::TypePredicate>,
type_args: Option<Vec<crate::TypeAnnotation>>,
args: Vec<ExprId>,
expected: Option<&Type>,
span: Span,
callee: GenericCallee,
) -> Result<(TypedExprKind, Type), CompilerFailure> {
let mut sub = TypeParamSubstitution::new();
let type_args_written = type_args.is_some();
let checked_get = crate::stdlib::session::declaration::is_checked_get(&mangled);
let schema_checked = crate::stdlib::schema_checked_call(&mangled);
if let Some(targs) = &type_args {
if targs.len() != generics.len() {
let help =
self.generic_callee_lift(callee, &callee_ident, &generics, ¶ms, &ret);
self.error_with_help(
span,
format!(
"{} expects {} type argument(s), got {}",
callee.subject(&callee_ident.name, &generics),
generics.len(),
targs.len(),
),
vec![help],
);
}
let subject = callee.subject(&callee_ident.name, &generics);
for (name, annot) in generics.iter().zip(targs.iter()) {
let resolved = self.resolve_call_type_argument(annot, &subject)?;
sub.insert(name.clone(), resolved);
}
}
if let Some(want) = expected.filter(|want| pins_type_parameters(want)) {
self.bind_from_expected_result(&mut sub, &ret, want);
}
let has_rest = params.last().is_some_and(|p| p.rest);
let fixed_count = params.iter().take_while(|p| !p.rest).count();
let max_args = if has_rest { usize::MAX } else { params.len() };
let min_args = super::expr::required_parameter_count(¶ms);
let arity_ok = args.len() >= min_args && args.len() <= max_args;
if !arity_ok {
let help = self.generic_callee_lift(callee, &callee_ident, &generics, ¶ms, &ret);
let subject = callee.subject(&callee_ident.name, &generics);
let msg = if has_rest {
format!(
"{subject} expects {}+ argument(s), got {}",
min_args,
args.len(),
)
} else if min_args == max_args {
format!(
"{subject} expects {} argument(s), got {}",
max_args,
args.len()
)
} else {
format!(
"expected {}-{} argument(s), got {}",
min_args,
max_args,
args.len(),
)
};
self.error_with_help(span, msg, vec![help]);
}
let rest_elem_ty: Type = if has_rest {
match ¶ms
.last()
.ok_or_else(|| super::inference_failure("rest signature has no parameters"))?
.ty
.rest_element()
{
Some(elem) => Type::clone(elem),
None => Type::Error,
}
} else {
Type::Error
};
let signature_help = |this: &mut Self| {
this.generic_callee_lift(callee, &callee_ident, &generics, ¶ms, &ret)
};
let errors_before_args = self.error_count();
let inferred_generics: &[String] = if type_args_written { &[] } else { &generics };
let (mut typed_args, argument_checks) = self.infer_generic_arguments(
&args,
¶ms,
&ret,
inferred_generics,
&rest_elem_ty,
&mut sub,
signature_help,
)?;
if has_rest || typed_args.len() < params.len() {
self.typed_ast
.record_authored_arguments(span, typed_args.clone());
}
if arity_ok {
self.fill_omitted_defaults(¶ms, args.len(), span, &mut typed_args)?;
}
if arity_ok && has_rest {
self.pack_rest_tail(fixed_count, rest_elem_ty.clone(), span, &mut typed_args)?;
}
if checked_get && !type_args_written {
bind_remaining(&mut sub, &generics, Type::Unknown, &self.type_limits)
.map_err(type_limit_at(span))?;
}
if let Some(checked) = &schema_checked
&& !type_args_written
{
bind_remaining(
&mut sub,
&generics,
checked.untyped_result.clone(),
&self.type_limits,
)
.map_err(type_limit_at(span))?;
}
sub.bind_whole_union_fallbacks();
let close_match_spans = self.check_deferred_close_matches(&mut sub, span);
self.bind_leftover_type_parameters(&mut sub, &generics, &ret, expected, errors_before_args);
self.bind_uninferred_to_unknown(&mut sub, &generics, &mangled, span)?;
if let Err(unbound) = sub
.resolve_all(&generics, &self.type_limits)
.map_err(type_limit_at(span))?
{
self.error_with_help(
span,
format!(
"cannot infer type parameter(s) {}; consider adding an explicit type argument",
unbound
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", "),
),
vec![callee.explicit_arg_hint(
&callee_ident.name,
&generics,
&sub,
&self.type_limits,
)],
);
}
self.check_arguments_after_inference(&sub, argument_checks, &close_match_spans)?;
let result_ty = self.instantiate(&sub, &ret, span)?;
if let Some(checked) = &schema_checked
&& args.len() > checked.written_param_count
{
let name = &callee_ident.name;
let written = checked.written_params;
let extra = checked.schema_argument_ordinal();
self.error_with_help(
span,
format!(
"`{name}` takes {written}; the schema argument is filled by \
the compiler from the type argument"
),
vec![format!(
"drop the {extra} argument — write `{name}<T>(...)` to send a schema for `T`, \
or `{name}(...)` for an untyped call"
)],
);
return Ok((TypedExprKind::Null, Type::Error));
}
let mut emitted_schema = None;
if let Some(checked) = &schema_checked {
match self.schema_checked_call_schema(checked, &result_ty, type_args_written, span) {
Ok(schema) => emitted_schema = schema,
Err(()) => return Ok((TypedExprKind::Null, Type::Error)),
}
}
if let Some(schema) = &emitted_schema {
self.substitute_schema_argument(¶ms, &mut typed_args, schema, span)?;
}
self.record_generic_call_arguments(&typed_args, ¶ms, &ret, type_args_written);
let generic_args: Vec<crate::GenericArgument> = typed_args
.into_iter()
.zip(params.iter())
.map(|(expr, p)| crate::GenericArgument {
expr,
is_generic: matches!(p.ty, Type::TypeVar(_)),
})
.collect();
let return_cast = if return_erases_to_object_slot(&ret) {
Some(result_ty.clone())
} else {
None
};
let type_predicate = match predicate {
Some(p) => Some(Box::new(crate::TypePredicate {
parameter_index: p.parameter_index,
asserted_type: self.instantiate(&sub, &p.asserted_type, span)?,
})),
None => None,
};
let runtime_args: Vec<_> = generics
.iter()
.map(|name| self.instantiate(&sub, &Type::TypeVar(name.clone()), span))
.collect::<Result<_, _>>()?;
for arg in &runtime_args {
self.record_runtime_type_test(arg)?;
}
let call = TypedExprKind::GenericCall {
mangled,
type_args: runtime_args,
args: generic_args,
return_cast: if checked_get || schema_checked.is_some() {
None
} else {
return_cast
},
type_predicate,
};
if checked_get {
return self.checked_session_get(call, &result_ty, type_args_written, span);
}
if schema_checked.is_some() && emitted_schema.is_some() {
return self.schema_checked_cast(call, &result_ty, span);
}
Ok((call, result_ty))
}
}
struct ArgumentCheck {
arg: ExprId,
param_ty: Type,
arg_ty: Type,
}
pub(super) struct GenericArguments<'a> {
pub(super) inferred_generics: &'a [String],
sourced_by_literals: Vec<String>,
literal_types: LiteralTypeArguments,
}
struct LiteralTypeArguments {
kept: Vec<String>,
sole: Vec<String>,
widened: Vec<String>,
}
impl LiteralTypeArguments {
fn new(
args: &[(ExprId, Type)],
ret: &Type,
inferred_generics: &[String],
fixed: FixedByCallbacks<'_>,
) -> Self {
let in_result = top_level_type_params(ret);
let mut kept = Vec::new();
let mut sole = Vec::new();
let mut widened = Vec::new();
for name in inferred_generics {
let mentioning: Vec<&Type> = args
.iter()
.map(|(_, param)| param)
.filter(|param| super::expr::mentions_type_var(param, &|var| var == name))
.collect();
if fixed.widens(name, &mentioning) {
widened.push(name.clone());
continue;
}
let only_top_level = mentioning
.iter()
.all(|param| top_level_type_params(param).contains(&name.as_str()));
if mentioning.is_empty() || !only_top_level {
continue;
}
if in_result.contains(&name.as_str()) {
if mentioning.len() == 1 {
sole.push(name.clone());
}
kept.push(name.clone());
} else {
widened.push(name.clone());
}
}
Self {
kept,
sole,
widened,
}
}
fn keeps(&self, param: &Type) -> bool {
top_level_type_params(param)
.iter()
.any(|name| self.kept.iter().any(|kept| kept == name))
}
fn keeps_returned(&self, param: &Type) -> bool {
top_level_type_params(param)
.iter()
.any(|name| self.sole.iter().any(|sole| sole == name))
}
fn widens(&self, param: &Type) -> bool {
top_level_type_params(param)
.iter()
.any(|name| self.widened.iter().any(|widened| widened == name))
}
}
struct FixedByCallbacks<'a> {
names: &'a std::collections::BTreeSet<String>,
callback_params: &'a [&'a Type],
}
impl FixedByCallbacks<'_> {
fn widens(&self, name: &str, mentioning: &[&Type]) -> bool {
self.names.contains(name)
&& mentioning.iter().all(|param| {
top_level_type_params(param).contains(&name)
|| (self.callback_params.contains(param)
&& mentions_only_in_functions(param, name))
})
}
}
fn mentions_only_in_functions(ty: &Type, name: &str) -> bool {
match ty.peel() {
Type::Function { .. } => true,
Type::TypeVar(var) => var != name,
Type::Union(members) => members
.iter()
.all(|member| mentions_only_in_functions(member, name)),
Type::Object { fields, index } => {
index.is_none()
&& fields
.values()
.all(|field| mentions_only_in_functions(&field.ty, name))
}
other => !super::expr::mentions_type_var(other, &|var| var == name),
}
}
fn is_literal_member_of(arg: &Type, param: &Type) -> bool {
arg.literal_base().is_some()
&& matches!(param.peel(), Type::Union(members) if members.iter().any(|member| member.peel() == arg.peel()))
}
fn expected_field_hint(
name: &str,
expected_fields: Option<&BTreeMap<String, crate::ObjectField>>,
expected_index: Option<&crate::IndexSignature>,
) -> Option<Type> {
expected_fields
.and_then(|fields| fields.get(name))
.map(crate::ObjectField::read_ty)
.or_else(|| expected_index.map(|index| (*index.value).clone()))
}
fn span_contains(outer: Span, inner: Span) -> bool {
outer.file == inner.file && outer.start <= inner.start && inner.end <= outer.end
}
fn top_level_type_params(ty: &Type) -> Vec<&str> {
match ty.peel() {
Type::Union(members) => members.iter().filter_map(type_param_name).collect(),
other => type_param_name(other).into_iter().collect(),
}
}
fn type_param_name(ty: &Type) -> Option<&str> {
match ty.peel() {
Type::TypeVar(name) => Some(name),
_ => None,
}
}
const MAX_HINT_INTERFACE_EXPANSIONS: usize = 64;
const MAX_HINT_UNION_DEPTH: usize = 8;
struct InterfaceExpansion {
expanding: Vec<crate::MangledName>,
remaining_at_max_depth: usize,
remaining_shallower: usize,
expansion_count: usize,
union_depth: usize,
max_union_depth: usize,
starved: bool,
cut_at_union_depth: bool,
}
impl InterfaceExpansion {
fn new(max_union_depth: usize, remaining_at_max_depth: usize) -> Self {
Self {
expanding: Vec::new(),
remaining_at_max_depth,
remaining_shallower: MAX_HINT_INTERFACE_EXPANSIONS,
expansion_count: 0,
union_depth: 0,
max_union_depth,
starved: false,
cut_at_union_depth: false,
}
}
fn budget(&mut self) -> &mut usize {
if self.union_depth < self.max_union_depth {
&mut self.remaining_shallower
} else {
&mut self.remaining_at_max_depth
}
}
fn can_expand(&mut self) -> bool {
let available = *self.budget() > 0;
self.starved |= !available;
available
}
fn spend(&mut self) {
*self.budget() -= 1;
self.expansion_count += 1;
}
}
fn expand_hint_interfaces(
ty: &Type,
inferred_generics: &[String],
types: super::assignable::TypeResolver,
) -> Type {
let mut expansion = InterfaceExpansion::new(1, MAX_HINT_INTERFACE_EXPANSIONS);
let mut deepest = expand_inferred_interfaces(ty, inferred_generics, types, &mut expansion);
for max_union_depth in 2..=MAX_HINT_UNION_DEPTH {
let remaining = MAX_HINT_INTERFACE_EXPANSIONS.saturating_sub(expansion.expansion_count);
if expansion.starved || !expansion.cut_at_union_depth || remaining == 0 {
break;
}
expansion = InterfaceExpansion::new(max_union_depth, remaining);
deepest = expand_inferred_interfaces(ty, inferred_generics, types, &mut expansion);
}
deepest
}
fn expand_inferred_interfaces(
ty: &Type,
inferred_generics: &[String],
types: super::assignable::TypeResolver,
expansion: &mut InterfaceExpansion,
) -> Type {
let expand = |inner: &Type, expansion: &mut InterfaceExpansion| {
expand_inferred_interfaces(inner, inferred_generics, types, expansion)
};
match ty.peel() {
interface @ Type::InterfaceRef { mangled, .. }
if !expansion.expanding.contains(mangled)
&& super::expr::mentions_type_var(interface, &|var| {
inferred_generics.iter().any(|name| name == var)
}) =>
{
if expansion.union_depth > expansion.max_union_depth {
expansion.cut_at_union_depth = true;
return ty.clone();
}
if !expansion.can_expand() {
return ty.clone();
}
let Some(shape) = super::assignable::expand_interface_data_shape(interface, types)
else {
return ty.clone();
};
expansion.spend();
expansion.expanding.push(mangled.clone());
let expanded = expand(&shape, expansion);
expansion.expanding.pop();
expanded
}
Type::Object { fields, index } => Type::Object {
fields: fields
.iter()
.map(|(name, field)| {
let ty = expand(&field.ty, expansion);
(
name.clone(),
crate::types::ObjectField {
ty,
..field.clone()
},
)
})
.collect(),
index: index
.as_ref()
.map(|index| index.map_value(|value| expand(value, expansion))),
},
Type::Array(element) => Type::Array(Box::new(expand(element, expansion))),
Type::Tuple(elements) => Type::Tuple(elements.map(|element| expand(element, expansion))),
Type::Union(members) => {
expansion.union_depth += 1;
let expanded = members
.iter()
.map(|member| expand(member, expansion))
.collect();
expansion.union_depth -= 1;
Type::union(expanded)
}
_ => ty.clone(),
}
}
fn function_part(ty: &Type) -> Option<&Type> {
match ty.peel() {
function @ Type::Function { .. } => Some(function),
Type::Union(members) => {
let mut functions = members
.iter()
.map(Type::peel)
.filter(|member| matches!(member, Type::Function { .. }));
let function = functions.next()?;
functions.next().is_none().then_some(function)
}
_ => None,
}
}
fn fix_callback_parameters(sub: &mut TypeParamSubstitution, param_ty: &Type, inferred: &[String]) {
for name in inferred {
if takes_in_callback(param_ty, name) && sub.get(name).is_none() {
let fixed = sub.whole_union_fallback(name).cloned();
sub.insert(name.clone(), fixed.unwrap_or(Type::Unknown));
}
}
}
fn takes_in_callback(param_ty: &Type, name: &str) -> bool {
let Some(Type::Function { params, .. }) = function_part(param_ty) else {
return false;
};
params
.iter()
.any(|param| super::expr::mentions_type_var(param, &|var| var == name))
}
pub(crate) struct LiteralArgumentInference {
literal: ExprId,
slots: std::collections::BTreeMap<String, crate::ObjectField>,
sub: TypeParamSubstitution,
fallback_echoes: Vec<String>,
}
#[derive(Clone, Copy, PartialEq)]
enum ShapeKind {
Function,
Structured,
Primitive,
Any,
}
impl ShapeKind {
fn of(ty: &Type) -> Self {
match ty.peel() {
Type::Function { .. } => Self::Function,
Type::Object { .. }
| Type::InterfaceRef { .. }
| Type::ClassRef { .. }
| Type::Array(_)
| Type::Tuple(_) => Self::Structured,
Type::Number
| Type::NumberLiteral(_)
| Type::BigInt
| Type::BigIntLiteral(_)
| Type::String
| Type::StringLiteral(_)
| Type::Uint8Array
| Type::Boolean
| Type::BooleanLiteral(_)
| Type::NumberEnum { .. }
| Type::StringEnum { .. }
| Type::Null
| Type::Undefined
| Type::Void
| Type::Never => Self::Primitive,
_ => Self::Any,
}
}
fn could_be(self, other: Self) -> bool {
self == other || self == Self::Any || other == Self::Any
}
}
fn pins_type_parameters(want: &Type) -> bool {
match want.peel() {
Type::Error | Type::Unknown | Type::TypeVar(_) => false,
Type::Union(members) => !members
.iter()
.any(|member| matches!(member.peel(), Type::TypeVar(_))),
_ => true,
}
}
fn bind_remaining(
sub: &mut crate::typechecker::type_param_substitution::TypeParamSubstitution,
generics: &[String],
fallback: Type,
limits: &TypeLimits,
) -> Result<(), TypeTooLarge> {
if let Err(unbound) = sub.resolve_all(generics, limits)? {
for name in unbound {
sub.insert(name, fallback.clone());
}
}
Ok(())
}
fn is_or_has_type_var(ty: &Type, name: &str) -> bool {
let is_var = |ty: &Type| matches!(ty.peel(), Type::TypeVar(var) if var == name);
match ty.peel() {
Type::Union(members) => members.iter().any(is_var),
_ => is_var(ty),
}
}
fn builds_from_type_parameters(ty: &Type) -> bool {
!matches!(ty.peel(), Type::TypeVar(_)) && super::expr::type_contains_type_var(ty)
}
fn is_array_index(name: &str) -> bool {
name.parse::<u32>()
.is_ok_and(|index| index != u32::MAX && index.to_string() == name)
}
fn admits_undefined(ty: &Type) -> bool {
let holds_undefined =
|ty: &Type| matches!(ty.peel(), Type::Undefined | Type::Void | Type::Unknown);
match ty.peel() {
Type::Error => true,
Type::Union(members) => members.iter().any(holds_undefined),
other => holds_undefined(other),
}
}
fn writable_view_for(ret: &Type, want: &Type) -> Type {
match want.peel_preserving_readonly() {
Type::Readonly(inner)
if !ret.is_readonly_array() && !matches!(ret.peel(), Type::TypeVar(_)) =>
{
(**inner).clone()
}
_ => want.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substitute_typevars_replaces_top_level_typevar() {
let mut bindings: BTreeMap<String, Type> = BTreeMap::new();
bindings.insert("T".into(), Type::Number);
let out = substitute_typevars(
&Type::TypeVar("T".into()),
&bindings,
&crate::type_size::TypeLimits::default(),
)
.unwrap();
assert_eq!(out, Type::Number);
}
#[test]
fn substitute_typevars_recurses_into_nested_constructors() {
let mut bindings: BTreeMap<String, Type> = BTreeMap::new();
bindings.insert("T".into(), Type::String);
let arr = Type::Array(Box::new(Type::TypeVar("T".into())));
let out =
substitute_typevars(&arr, &bindings, &crate::type_size::TypeLimits::default()).unwrap();
assert_eq!(out, Type::Array(Box::new(Type::String)));
let func = Type::Function {
params: vec![Type::TypeVar("T".into())],
ret: Box::new(Type::TypeVar("T".into())),
predicate: None,
has_rest: false,
optional: 0,
};
let out = substitute_typevars(&func, &bindings, &crate::type_size::TypeLimits::default())
.unwrap();
assert_eq!(
out,
Type::Function {
params: vec![Type::String],
ret: Box::new(Type::String),
predicate: None,
has_rest: false,
optional: 0,
}
);
}
#[test]
fn substitute_typevars_leaves_unbound_names_alone() {
let mut bindings: BTreeMap<String, Type> = BTreeMap::new();
bindings.insert("T".into(), Type::Number);
let out = substitute_typevars(
&Type::TypeVar("U".into()),
&bindings,
&crate::type_size::TypeLimits::default(),
)
.unwrap();
assert_eq!(out, Type::TypeVar("U".into()));
}
#[test]
fn substitute_typevars_charges_each_copy_of_a_binding() {
crate::type_size::tests::on_compiler_stack(
substitute_typevars_charges_each_copy_of_a_binding_inner,
);
}
fn substitute_typevars_charges_each_copy_of_a_binding_inner() {
use crate::compiler_limits::{MAX_TYPE_DEPTH, MAX_TYPE_NODES};
use crate::type_size::{TypeLimits, TypeTooLarge};
let pair = Type::Tuple(vec![Type::TypeVar("T".into()), Type::TypeVar("T".into())].into());
let limits = TypeLimits::default();
let half = Type::Tuple(vec![Type::Number; (MAX_TYPE_NODES / 2 - 2) as usize].into());
let bindings = BTreeMap::from([("T".to_string(), half)]);
assert!(substitute_typevars(&pair, &bindings, &limits).is_ok());
let over = Type::Tuple(vec![Type::Number; (MAX_TYPE_NODES / 2 - 1) as usize].into());
let bindings = BTreeMap::from([("T".to_string(), over)]);
assert_eq!(
substitute_typevars(&pair, &bindings, &limits),
Err(TypeTooLarge::Nodes)
);
let deep = (1..MAX_TYPE_DEPTH).fold(Type::Number, |inner, _| Type::Array(Box::new(inner)));
let bindings = BTreeMap::from([("T".to_string(), deep)]);
assert_eq!(
substitute_typevars(&pair, &bindings, &limits),
Err(TypeTooLarge::Depth)
);
}
use super::super::test_support::{run, run_clean};
use crate::{ExprId, PackageDeclaration, Param, StmtId, TypedAst, TypedStmtKind, ValueKind};
fn last_call_resolved_ty(ta: &TypedAst) -> Type {
for &id in ta.top_level_statements.iter().rev() {
if let TypedStmtKind::AssignGlobal { value, .. } = &ta.try_stmt(id).unwrap().kind {
return ta.try_expr(*value).unwrap().ty.clone();
}
}
panic!("no top-level let/const init found");
}
#[test]
fn hof_map_infers_return_with_defaulted_callback_parameter() {
let (ta, diags) = run(r#"
function addDefault(value: number, index: number = 100): number {
return value + index;
}
let result = [1, 2, 3].map(addDefault);
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
assert_eq!(
last_call_resolved_ty(&ta),
Type::Array(Box::new(Type::Number))
);
}
#[test]
fn hof_map_infers_return_with_wider_callback_parameter() {
let (ta, diags) = run(r#"
function describe(value: number | null): string { return "value"; }
let result = [1, 2, 3].map(describe);
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
assert_eq!(
last_call_resolved_ty(&ta),
Type::Array(Box::new(Type::String))
);
}
#[test]
fn hof_map_rejects_narrower_callback_parameter() {
let (_, diags) = run(r#"
function describe(value: number): string { return "value"; }
let values: (number | null)[] = [1, null];
let result = values.map(describe);
"#);
assert!(
!diags.is_empty(),
"narrow callback must reject nullable inputs"
);
}
#[test]
fn hof_map_typechecks_closure_arg() {
let (ta, diags) = run(r#"
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
return [];
}
let result = map([1, 2, 3], (x: number): string => x.toString());
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let ty = last_call_resolved_ty(&ta);
assert_eq!(
ty,
Type::Array(Box::new(Type::String)),
"expected string[], got {ty:?}",
);
}
#[test]
fn hof_filter_typechecks_predicate() {
let (ta, diags) = run(r#"
function filter<T>(arr: T[], pred: (x: T) => boolean): T[] {
return [];
}
let result = filter(["a", "b"], (x: string): boolean => x === "a");
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let ty = last_call_resolved_ty(&ta);
assert_eq!(ty, Type::Array(Box::new(Type::String)));
}
#[test]
fn hof_reduce_typechecks_with_accumulator() {
let (ta, diags) = run(r#"
function reduce<T, U>(arr: T[], fn: (acc: U, x: T) => U, init: U): U {
return init;
}
let result = reduce([1, 2, 3], (acc: number, x: number): number => acc + x, 0);
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let ty = last_call_resolved_ty(&ta);
assert_eq!(ty, Type::Number);
}
#[test]
fn hof_bidirectional_return_seeds_t_from_lhs_annotation() {
let (ta, diags) = run(r#"
function none<T>(): T[] {
return [];
}
let result: number[] = none();
"#);
assert!(diags.is_empty(), "unexpected diags: {diags:?}");
let ty = last_call_resolved_ty(&ta);
assert_eq!(ty, Type::Array(Box::new(Type::Number)));
}
fn collect_gps(ty: &Type, out: &mut Vec<u32>) {
match ty {
Type::GenericParam { id, .. } => out.push(*id),
Type::Array(elem) => collect_gps(elem, out),
Type::Function { params, ret, .. } => {
for p in params {
collect_gps(p, out);
}
collect_gps(ret, out);
}
Type::Object { fields, .. } => {
for v in fields.values() {
collect_gps(&v.ty, out);
}
}
_ => {}
}
}
fn collect_body_expr_types(ta: &TypedAst, stmt_id: StmtId, out: &mut Vec<Type>) {
let stmt = ta.try_stmt(stmt_id).unwrap();
match &stmt.kind {
TypedStmtKind::Block(children) => {
for &c in children {
collect_body_expr_types(ta, c, out);
}
}
TypedStmtKind::Let { value, .. }
| TypedStmtKind::Const { value, .. }
| TypedStmtKind::AssignLocal { value, .. }
| TypedStmtKind::AssignGlobal { value, .. } => {
collect_expr_types(ta, *value, out);
}
TypedStmtKind::AssignField {
receiver, value, ..
} => {
collect_expr_types(ta, *receiver, out);
collect_expr_types(ta, *value, out);
}
TypedStmtKind::AssignIndex {
receiver,
index,
value,
..
} => {
collect_expr_types(ta, *receiver, out);
collect_expr_types(ta, *index, out);
collect_expr_types(ta, *value, out);
}
TypedStmtKind::Return(Some(e)) | TypedStmtKind::Expr(e) => {
collect_expr_types(ta, *e, out);
}
TypedStmtKind::Return(None) => {}
TypedStmtKind::If {
condition,
then_block,
else_block,
} => {
collect_expr_types(ta, *condition, out);
collect_body_expr_types(ta, *then_block, out);
if let Some(eb) = else_block {
collect_body_expr_types(ta, *eb, out);
}
}
TypedStmtKind::While { condition, body } => {
collect_expr_types(ta, *condition, out);
collect_body_expr_types(ta, *body, out);
}
TypedStmtKind::For {
init,
condition,
update,
body,
} => {
if let Some(i) = init {
collect_body_expr_types(ta, *i, out);
}
if let Some(c) = condition {
collect_expr_types(ta, *c, out);
}
if let Some(u) = update {
collect_body_expr_types(ta, *u, out);
}
collect_body_expr_types(ta, *body, out);
}
TypedStmtKind::ForOf { iter, body, .. } => {
collect_expr_types(ta, *iter, out);
collect_body_expr_types(ta, *body, out);
}
TypedStmtKind::DoWhile { body, condition } => {
collect_body_expr_types(ta, *body, out);
collect_expr_types(ta, *condition, out);
}
TypedStmtKind::Switch {
discriminant,
cases,
default,
..
} => {
collect_expr_types(ta, *discriminant, out);
for comparison in cases
.iter()
.flat_map(crate::TypedSwitchCase::label_comparisons)
{
collect_expr_types(ta, comparison, out);
}
for case in cases {
collect_body_expr_types(ta, case.body, out);
}
if let Some(d) = default {
collect_body_expr_types(ta, *d, out);
}
}
TypedStmtKind::Break | TypedStmtKind::Continue | TypedStmtKind::ReboxLocal { .. } => {}
TypedStmtKind::NarrowRegion { source, body, .. } => {
collect_expr_types(ta, *source, out);
collect_body_expr_types(ta, *body, out);
}
TypedStmtKind::Throw { value } => collect_expr_types(ta, *value, out),
TypedStmtKind::Try {
body,
catches,
finally,
} => {
collect_body_expr_types(ta, *body, out);
for c in catches {
collect_body_expr_types(ta, c.body, out);
}
if let Some(f) = finally {
collect_body_expr_types(ta, *f, out);
}
}
}
}
fn collect_expr_types(ta: &TypedAst, expr_id: ExprId, out: &mut Vec<Type>) {
let expr = ta.try_expr(expr_id).unwrap();
out.push(expr.ty.clone());
match &expr.kind {
crate::TypedExprKind::Binary { lhs, rhs, .. } => {
collect_expr_types(ta, *lhs, out);
collect_expr_types(ta, *rhs, out);
}
crate::TypedExprKind::Unary { operand, .. } => {
collect_expr_types(ta, *operand, out);
}
crate::TypedExprKind::Call { args, .. } | TypedExprKind::VirtualCall { args, .. } => {
for a in args {
collect_expr_types(ta, *a, out);
}
}
crate::TypedExprKind::CallClosure { callee, args } => {
collect_expr_types(ta, *callee, out);
for a in args {
collect_expr_types(ta, *a, out);
}
}
crate::TypedExprKind::IntrinsicCall { args, .. } => {
for a in args {
collect_expr_types(ta, *a, out);
}
}
crate::TypedExprKind::FieldAccess { receiver, .. } => {
collect_expr_types(ta, *receiver, out);
}
crate::TypedExprKind::IndexAccess { receiver, index } => {
collect_expr_types(ta, *receiver, out);
collect_expr_types(ta, *index, out);
}
crate::TypedExprKind::ObjectLiteral { members, .. } => {
for member in members {
for expression in member.expressions() {
collect_expr_types(ta, expression, out);
}
}
}
crate::TypedExprKind::ArrayLiteral { elements, .. } => {
for e in elements {
collect_expr_types(ta, e.expr_id(), out);
}
}
_ => {}
}
}
fn nth_function(ta: &TypedAst, n: usize) -> &crate::TypedFunction {
&ta.functions[n]
}
#[test]
fn generic_params_erased_to_typevar_after_inference() {
let ta = run_clean(
r#"
function f<T>(x: T): T {
let y: T = x;
return y;
}
"#,
);
let defs = PackageDeclaration::from_typed_ast(&ta);
let f = defs.values.get("f").expect("f registered");
match &f.kind {
ValueKind::Function {
generics,
params,
ret,
..
} => {
assert_eq!(generics, &vec!["T".to_string()]);
assert_eq!(
params,
&vec![Param::new("x", Type::TypeVar("T".to_string()))]
);
assert_eq!(*ret, Type::TypeVar("T".to_string()));
}
other => panic!("expected ValueKind::Function, got {other:?}"),
}
let f = nth_function(&ta, 0);
let typed_params = &f.params;
let typed_ret = f.return_type.clone();
let body = f.body;
for p in typed_params {
assert!(
matches!(p.ty, Type::TypeVar(_)),
"param `{}`: expected TypeVar, got {:?}",
p.name.name,
p.ty,
);
}
assert!(
matches!(typed_ret, Type::TypeVar(_)),
"return: expected TypeVar, got {typed_ret:?}",
);
let mut body_tys = Vec::new();
collect_body_expr_types(&ta, body, &mut body_tys);
let saw_typevar = body_tys.iter().any(|t| matches!(t, Type::TypeVar(_)));
assert!(
saw_typevar,
"body should contain TypeVar after erasure, got {body_tys:?}"
);
let mut gp_ids = Vec::new();
for t in &body_tys {
collect_gps(t, &mut gp_ids);
}
assert!(
gp_ids.is_empty(),
"body should have no GenericParam after erasure, got ids {gp_ids:?}"
);
}
#[test]
fn cross_scope_generic_bodies_have_no_gp_after_erasure() {
let ta = run_clean(
r#"
function inner<U>(y: U): U { return y; }
function outer<T>(x: T): T { return inner(x); }
"#,
);
let defs = PackageDeclaration::from_typed_ast(&ta);
for fn_name in ["inner", "outer"] {
match &defs.values.get(fn_name).unwrap().kind {
ValueKind::Function { params, ret, .. } => {
for p in params {
assert!(
matches!(p.ty, Type::TypeVar(_)),
"{fn_name}'s param should be TypeVar, got {:?}",
p.ty,
);
}
assert!(
matches!(ret, Type::TypeVar(_)),
"{fn_name}'s ret should be TypeVar, got {ret:?}"
);
}
_ => panic!(),
}
}
for n in 0..2 {
let body = nth_function(&ta, n).body;
let mut body_tys = Vec::new();
collect_body_expr_types(&ta, body, &mut body_tys);
let mut gp_ids = Vec::new();
for t in &body_tys {
collect_gps(t, &mut gp_ids);
}
assert!(
gp_ids.is_empty(),
"function #{n}'s body has GenericParam ids {gp_ids:?} after erasure",
);
}
}
#[test]
fn conflict_diagnostic_uses_real_names_not_garbled() {
let (_, diags) = run(r#"
function pair<T>(a: T, b: T): T { return a; }
function main(): void {
pair(1, "x");
}
"#);
assert!(!diags.is_empty(), "expected at least one diagnostic");
let saw_clean = diags.iter().any(|d| {
d.message.contains("`1`") && d.message.contains("`\"x\"`") && !d.message.contains("`T`")
});
assert!(
saw_clean,
"expected clean conflict diagnostic, got: {diags:#?}",
);
}
#[test]
fn cross_scope_gp_assignment_rejects_with_clean_message() {
let (_, diags) = run(r#"
function identity<T>(x: T): T { return x; }
function bar<T, U>(y: U): void {
const x: T = identity(y);
}
"#);
assert!(
!diags.is_empty(),
"expected rejection of cross-GP assignment"
);
let saw_clean = diags.iter().any(|d| {
d.message.contains('T')
&& d.message.contains('U')
&& !d.message.contains("`T`, cannot bind to `T`")
&& !d.message.contains("`T` already bound to `T`")
});
assert!(
saw_clean,
"expected clean cross-GP diagnostic mentioning both T and U, got: {diags:#?}",
);
}
#[test]
fn resolve_all_treats_self_binding_as_unbound() {
use crate::typechecker::type_param_substitution::TypeParamSubstitution;
let mut sub = TypeParamSubstitution::new();
sub.insert("T".to_string(), Type::TypeVar("T".to_string()));
let result = sub
.resolve_all(&["T".to_string()], &crate::type_size::TypeLimits::default())
.unwrap();
assert!(
result.is_err(),
"self-binding T → TypeVar(T) should be reported as unbound; got {result:?}",
);
}
#[test]
fn array_map_infers_method_level_generic_from_closure() {
let (ta, diags) = run(r#"
function main(): void {
const xs = [1, 2, 3].map((x: number): number => x);
}
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
let body = ta.functions[0].body;
let TypedStmtKind::Block(stmts) = &ta.try_stmt(body).unwrap().kind else {
panic!("expected block body");
};
let TypedStmtKind::Const { ty, .. } = &ta.try_stmt(stmts[0]).unwrap().kind else {
panic!("expected const");
};
assert_eq!(*ty, Type::Array(Box::new(Type::Number)));
}
#[test]
fn array_map_with_explicit_method_type_arg() {
let (ta, diags) = run(r#"
function main(): void {
const xs = [1, 2, 3].map<number>((x: number): number => x);
}
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
let body = ta.functions[0].body;
let TypedStmtKind::Block(stmts) = &ta.try_stmt(body).unwrap().kind else {
panic!("expected block body");
};
let TypedStmtKind::Const { ty, .. } = &ta.try_stmt(stmts[0]).unwrap().kind else {
panic!("expected const");
};
assert_eq!(*ty, Type::Array(Box::new(Type::Number)));
}
#[test]
fn array_map_explicit_type_arg_arity_mismatch_diagnoses() {
let (_, diags) = run(r#"
function main(): void {
const xs = [1].map<number, string>((x: number): number => x);
}
"#);
assert!(
diags.iter().any(|d| d
.message
.contains("method `map` expects 1 type argument(s), got 2")),
"expected type-arg arity diagnostic, got: {diags:?}",
);
}
#[test]
fn array_map_with_user_typed_closure_returns_inferred_array() {
let (ta, diags) = run(r#"
function main(): void {
const xs = [{x: 1}, {x: 2}].map(
(o: { x: number }): number => o.x
);
}
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
let body = ta.functions[0].body;
let TypedStmtKind::Block(stmts) = &ta.try_stmt(body).unwrap().kind else {
panic!("expected block body");
};
let TypedStmtKind::Const { ty, .. } = &ta.try_stmt(stmts[0]).unwrap().kind else {
panic!("expected const");
};
assert_eq!(*ty, Type::Array(Box::new(Type::Number)));
}
#[test]
fn user_interface_method_generic_typechecks() {
let (_, diags) = run(r#"
interface Box<T> {
map<U>(fn: (x: T) => U): U;
}
function main(): void {}
"#);
assert!(
diags.is_empty(),
"expected clean typecheck of user method-generic interface, got: {diags:?}",
);
}
#[test]
fn method_generic_shadowing_interface_generic_diagnoses() {
let (_, diags) = run(r#"
interface Foo<T> {
f<T>(): T;
}
function main(): void {}
"#);
assert!(
diags.iter().any(|d| d
.message
.contains("method generic `T` shadows interface generic `T`")),
"expected shadowing diagnostic, got: {diags:?}",
);
}
#[test]
fn array_reduce_infers_u_from_init_arg() {
let (ta, diags) = run(r#"
function main(): void {
let nums: number[] = [1, 2, 3];
let sum = nums.reduce((acc, x) => acc + x, 0);
}
"#);
assert!(
diags.is_empty(),
"expected reduce<U> to infer from init arg, got: {diags:?}",
);
let body = ta.functions[0].body;
let TypedStmtKind::Block(stmts) = &ta.try_stmt(body).unwrap().kind else {
panic!("expected block body");
};
let TypedStmtKind::Let { ty, .. } = &ta.try_stmt(stmts[1]).unwrap().kind else {
panic!("expected let sum");
};
assert_eq!(*ty, Type::Number, "sum should infer as number");
}
#[test]
fn array_reduce_infers_u_from_string_init() {
let (_, diags) = run(r#"
function main(): void {
let words = ["a", "b"].reduce((acc, s) => acc + s, "");
}
"#);
assert!(
diags.is_empty(),
"expected reduce<U> to infer U := string, got: {diags:?}",
);
}
#[test]
fn under_constrained_method_generic_is_unknown() {
let (_, diags) = run(r#"
interface Box<T> {
empty<U>(): U[];
}
function go(b: Box<number>): void {
const xs: unknown[] = b.empty();
}
"#);
assert!(
diags.is_empty(),
"expected `U` to infer as `unknown`, got: {diags:?}"
);
}
#[test]
fn rest_param_accepts_zero_trailing_args() {
let (_, diags) = run(r#"
function sum(...n: number[]): number { return 0; }
function main(): void { let x = sum(); }
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
}
#[test]
fn rest_param_accepts_many_trailing_args() {
let (_, diags) = run(r#"
function sum(...n: number[]): number { return 0; }
function main(): void { let x = sum(1, 2, 3); }
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
}
#[test]
fn rest_param_rejects_mismatched_trailing_arg() {
let (_, diags) = run(r#"
function sum(...n: number[]): number { return 0; }
function main(): void { let x = sum(1, "x"); }
"#);
assert!(
diags
.iter()
.any(|d| d.message.contains("number") && d.message.contains("string")),
"expected number-vs-string mismatch, got: {diags:?}",
);
}
#[test]
fn generic_rest_param_infers_element_type() {
let (_, diags) = run(r#"
function pack<T>(...xs: T[]): T[] { return xs; }
function main(): void { let n = pack(1, 2, 3); let s = pack("a", "b"); }
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
}
#[test]
fn generic_rest_param_rejects_mixed_element_types() {
let (_, diags) = run(r#"
function pack<T>(...xs: T[]): T[] { return xs; }
function main(): void { let x = pack(1, "a"); }
"#);
assert!(
!diags.is_empty(),
"expected conflict diagnostic for mixed types"
);
}
#[test]
fn fixed_plus_rest_accepts_mixed_args() {
let (_, diags) = run(r#"
function tag(label: string, ...vals: number[]): string { return label; }
function main(): void {
let a = tag("x");
let b = tag("x", 1, 2);
}
"#);
assert!(diags.is_empty(), "expected clean typecheck, got: {diags:?}");
}
#[test]
fn fixed_plus_rest_requires_prefix_arg() {
let (_, diags) = run(r#"
function tag(label: string, ...vals: number[]): string { return label; }
function main(): void { let x = tag(); }
"#);
assert!(
diags
.iter()
.any(|d| d.message.contains("expected 1+ argument(s), got 0")),
"expected arity diagnostic with '+' form, got: {diags:?}",
);
}
}