use std::collections::{BTreeMap, BTreeSet};
use crate::ast::*;
use crate::builtin_signatures;
use crate::builtin_signatures::TyExt;
use crate::diagnostic_codes::Code;
use harn_lexer::Span;
use super::super::format::format_type;
use super::super::schema_inference::schema_type_expr_from_node;
use super::super::scope::{is_builtin, EnumDeclInfo, FnSignature, StructDeclInfo, TypeScope};
use super::super::union::collapse_members_opt;
use super::super::union::without_nil;
use super::super::TypeChecker;
impl TypeChecker {
fn builtin_param_for_arg(
sig: &builtin_signatures::BuiltinSignature,
index: usize,
) -> Option<&builtin_signatures::Param> {
if sig.has_rest && index >= sig.params.len().saturating_sub(1) {
sig.params.last()
} else {
sig.params.get(index)
}
}
fn function_param_for_arg(
sig: &FnSignature,
index: usize,
) -> Option<(&str, Option<&TypeExpr>)> {
let entry = if sig.has_rest && index >= sig.params.len().saturating_sub(1) {
sig.params.last()
} else {
sig.params.get(index)
}?;
Some((entry.0.as_str(), entry.1.as_ref()))
}
fn union_of_shape_field_types(fields: &[ShapeField]) -> Option<TypeExpr> {
let mut members: Vec<TypeExpr> = Vec::new();
for field in fields {
if !members.contains(&field.type_expr) {
members.push(field.type_expr.clone());
}
}
collapse_members_opt(members, TypeExpr::Union)
}
fn builtin_uses_strict_llm_option_keys(name: &str, param_name: &str) -> bool {
param_name == "options"
&& matches!(
name,
"llm_call"
| "llm_call_safe"
| "llm_stream_call"
| "llm_call_structured"
| "llm_call_structured_safe"
| "llm_call_structured_result"
| "llm_completion"
)
}
fn check_strict_llm_option_keys(
&mut self,
builtin_name: &str,
param_name: &str,
expected: &TypeExpr,
arg: &SNode,
) {
if !Self::builtin_uses_strict_llm_option_keys(builtin_name, param_name) {
return;
}
let TypeExpr::Shape(fields) = expected else {
return;
};
let Node::DictLiteral(entries) = &arg.node else {
return;
};
let candidates: Vec<&str> = fields.iter().map(|field| field.name.as_str()).collect();
for entry in entries {
if matches!(entry.value.node, Node::Spread(_)) {
continue;
}
let key = match &entry.key.node {
Node::StringLiteral(key) | Node::Identifier(key) => key,
_ => continue,
};
if fields.iter().any(|field| field.name == *key) {
continue;
}
let message =
match crate::diagnostic::find_closest_match(key, candidates.iter().copied(), 3) {
Some(suggestion) => {
format!(
"unknown `{builtin_name}` option `{key}`; did you mean `{suggestion}`?"
)
}
None => format!("unknown `{builtin_name}` option `{key}`"),
};
self.warning_at(Code::UnknownLlmOption, message, entry.key.span);
}
}
fn option_bag_type_name(ty: &TypeExpr) -> Option<&str> {
match ty {
TypeExpr::Named(name) | TypeExpr::Applied { name, .. }
if name.ends_with("Options") || name.ends_with("Config") =>
{
Some(name)
}
TypeExpr::Union(members) => members.iter().find_map(Self::option_bag_type_name),
_ => None,
}
}
fn is_option_bag_param_name(param_name: &str) -> bool {
matches!(param_name, "opts" | "options" | "config")
|| param_name.ends_with("_opts")
|| param_name.ends_with("_options")
|| param_name.ends_with("_config")
}
fn option_bag_shape_fields(
&self,
param_name: &str,
expected: &TypeExpr,
scope: &TypeScope,
) -> Option<Vec<ShapeField>> {
let option_bag_name = Self::option_bag_type_name(expected);
if option_bag_name.is_none() && !Self::is_option_bag_param_name(param_name) {
return None;
}
let resolved = self.resolve_alias(expected, scope);
match resolved {
TypeExpr::Shape(fields) => Some(fields),
TypeExpr::Union(members) => {
let mut shapes = members.into_iter().filter_map(|member| match member {
TypeExpr::Named(name) if name == "nil" => None,
TypeExpr::Shape(fields) => Some(fields),
_ => None,
});
let fields = shapes.next()?;
if shapes.next().is_none() {
Some(fields)
} else {
None
}
}
_ => None,
}
}
fn check_unknown_option_bag_fields(
&mut self,
context: impl Into<String>,
param_name: &str,
expected: &TypeExpr,
arg: &SNode,
scope: &TypeScope,
) {
let Node::DictLiteral(entries) = &arg.node else {
return;
};
let Some(fields) = self.option_bag_shape_fields(param_name, expected, scope) else {
return;
};
let known: BTreeSet<&str> = fields.iter().map(|field| field.name.as_str()).collect();
if known.is_empty() {
return;
}
let context = context.into();
let expected_list = known
.iter()
.map(|field| format!("`{field}`"))
.collect::<Vec<_>>()
.join(", ");
for entry in entries {
if matches!(entry.value.node, Node::Spread(_)) {
continue;
}
let key = match &entry.key.node {
Node::StringLiteral(key) | Node::Identifier(key) => key,
_ => continue,
};
if known.contains(key.as_str()) {
continue;
}
let mut message =
format!("{context}: unknown option `{key}`; expected one of {expected_list}");
if let Some(candidate) =
crate::diagnostic::find_closest_match(key, known.iter().copied(), 3)
{
message.push_str(&format!(" — did you mean `{candidate}`?"));
}
self.error_at(Code::UnknownOption, message, entry.key.span);
}
}
fn check_builtin_signature_call(
&mut self,
name: &str,
sig: &builtin_signatures::BuiltinSignature,
type_args: &[TypeExpr],
args: &[SNode],
has_spread: bool,
scope: &mut TypeScope,
span: Span,
) {
if !type_args.is_empty() {
if !sig.is_generic() {
self.error_at(
Code::GenericTypeArgumentUnsupported,
format!("Builtin function '{name}' does not declare type parameters"),
span,
);
} else if type_args.len() != sig.type_params.len() {
self.error_at(
Code::GenericTypeArgumentArity,
format!(
"Builtin function '{}' expects {} type arguments, got {}",
name,
sig.type_params.len(),
type_args.len()
),
span,
);
}
}
if !has_spread {
let required = sig.required_params();
let total = sig.params.len();
let arity_ok = if sig.has_rest {
args.len() >= total.saturating_sub(1)
} else {
args.len() >= required && args.len() <= total
};
if !arity_ok {
let (expected, single_arg) = if sig.has_rest {
(
format!("at least {}", total.saturating_sub(1)),
total.saturating_sub(1) == 1,
)
} else if required == total {
(total.to_string(), total == 1)
} else {
(format!("{required}-{total}"), false)
};
let arg_word = if single_arg { "argument" } else { "arguments" };
self.warning_at(
Code::BuiltinArity,
format!(
"Builtin function '{}' expects {} {}, got {}",
name,
expected,
arg_word,
args.len()
),
span,
);
}
}
let type_param_names = sig.type_param_names();
let type_param_set: std::collections::BTreeSet<String> =
type_param_names.iter().cloned().collect();
let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
if type_args.len() == type_param_names.len() {
for (param_name, type_arg) in type_param_names.iter().zip(type_args.iter()) {
type_bindings.insert(param_name.clone(), type_arg.clone());
}
}
for (i, arg) in args.iter().enumerate() {
let Some(param) = Self::builtin_param_for_arg(sig, i) else {
continue;
};
if param.ty.is_any() {
continue;
}
let param_ty = param.ty.to_type_expr();
if let Err(message) =
self.bind_from_arg_node(¶m_ty, arg, &type_param_set, &mut type_bindings, scope)
{
self.error_at(Code::ArgumentTypeMismatch, message, arg.span);
}
}
let unbound_type_params: std::collections::BTreeSet<String> = type_param_set
.iter()
.filter(|name| !type_bindings.contains_key(*name))
.cloned()
.collect();
let mut expected_args: Vec<Option<TypeExpr>> = Vec::with_capacity(args.len());
let mut contextual_args = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
let Some(param) = Self::builtin_param_for_arg(sig, i) else {
self.check_node(arg, scope);
expected_args.push(None);
contextual_args.push(false);
continue;
};
if param.ty.is_any() || matches!(param.ty, builtin_signatures::Ty::SchemaOf(_)) {
self.check_node(arg, scope);
expected_args.push(None);
contextual_args.push(false);
continue;
}
let expected = Self::apply_type_bindings(¶m.ty.to_type_expr(), &type_bindings);
let contextual_expected =
(!Self::contains_type_param(&expected, &unbound_type_params)).then_some(&expected);
let context_checked = self.check_node_with_expected(arg, contextual_expected, scope);
expected_args.push(Some(expected));
contextual_args.push(context_checked);
}
let call_scope_owned;
let call_scope: &TypeScope = if sig.type_params.is_empty() {
scope
} else {
let mut s = scope.child();
for tp_name in sig.type_params {
s.generic_type_params.insert((*tp_name).to_string());
}
call_scope_owned = s;
&call_scope_owned
};
for (i, arg) in args.iter().enumerate() {
let Some(param) = Self::builtin_param_for_arg(sig, i) else {
continue;
};
let Some(expected) = expected_args.get(i).and_then(|ty| ty.as_ref()) else {
continue;
};
let actual = self.infer_type(arg, scope);
if let Some(actual) = &actual {
self.check_strict_llm_option_keys(name, param.name, expected, arg);
if !Self::builtin_uses_strict_llm_option_keys(name, param.name) {
self.check_unknown_option_bag_fields(
format!("argument {} `{}`", i + 1, param.name),
param.name,
expected,
arg,
call_scope,
);
}
let compatible = contextual_args.get(i).copied().unwrap_or(false)
|| self.types_compatible(expected, actual, call_scope)
|| (param.optional
&& without_nil(actual).is_none_or(|non_nil| {
self.types_compatible(expected, &non_nil, call_scope)
}));
if !compatible {
self.type_mismatch_at(
Code::ArgumentTypeMismatch,
format!("argument {} `{}`", i + 1, param.name),
expected,
actual,
arg.span,
(None, Some(arg.span)),
call_scope,
);
}
}
}
if !sig.where_clauses.is_empty() {
for (type_param, bound) in sig.where_clauses {
if let Some(concrete_type) = type_bindings.get(*type_param) {
let concrete_name = format_type(concrete_type);
let Some(base_type_name) = Self::base_type_name(concrete_type) else {
self.error_at(Code::WhereConstraintMismatch,
format!(
"Type '{concrete_name}' does not satisfy interface '{bound}': only named types can satisfy interfaces (required by constraint `where {type_param}: {bound}`)"
),
span,
);
continue;
};
if let Some(reason) = self.interface_mismatch_reason(
base_type_name,
bound,
&BTreeMap::new(),
scope,
) {
self.error_at(
Code::WhereConstraintMismatch,
format!(
"Type '{concrete_name}' does not satisfy interface '{bound}': {reason} \
(required by constraint `where {type_param}: {bound}`)"
),
span,
);
}
}
}
}
}
pub(in crate::typechecker) fn check_harness_method_call(
&mut self,
object: &SNode,
method: &str,
args: &[SNode],
scope: &mut TypeScope,
span: Span,
) -> bool {
let Some(raw_type) = self.infer_type(object, scope) else {
return false;
};
let TypeExpr::Named(type_name) = self.resolve_alias(&raw_type, scope) else {
return false;
};
let Some(sub_handle) = crate::harness_methods::harness_type_sub_handle(type_name.as_str())
else {
return false;
};
let Some(ambient) = crate::harness_methods::harness_sub_handle_ambient(sub_handle, method)
else {
return false;
};
let Some(sig) = builtin_signatures::lookup(ambient) else {
return false;
};
let display_name = format!("harness.{sub_handle}.{method}");
let has_spread = args.iter().any(|arg| matches!(&arg.node, Node::Spread(_)));
self.check_builtin_signature_call(&display_name, sig, &[], args, has_spread, scope, span);
true
}
pub(in crate::typechecker) fn bind_type_param(
param_name: &str,
concrete: &TypeExpr,
bindings: &mut BTreeMap<String, TypeExpr>,
) -> Result<(), String> {
if Self::is_wildcard_type(concrete) {
return Ok(());
}
if let Some(existing) = bindings.get(param_name) {
if Self::is_wildcard_type(existing) {
bindings.insert(param_name.to_string(), concrete.clone());
return Ok(());
}
if existing != concrete {
return Err(format!(
"type parameter '{}' was inferred as both {} and {}",
param_name,
format_type(existing),
format_type(concrete)
));
}
return Ok(());
}
bindings.insert(param_name.to_string(), concrete.clone());
Ok(())
}
pub(in crate::typechecker) fn extract_type_bindings(
param_type: &TypeExpr,
arg_type: &TypeExpr,
type_params: &std::collections::BTreeSet<String>,
bindings: &mut BTreeMap<String, TypeExpr>,
) -> Result<(), String> {
match (param_type, arg_type) {
(TypeExpr::Named(param_name), concrete) if type_params.contains(param_name) => {
Self::bind_type_param(param_name, concrete, bindings)
}
(TypeExpr::List(p_inner), TypeExpr::List(a_inner)) => {
Self::extract_type_bindings(p_inner, a_inner, type_params, bindings)
}
(TypeExpr::DictType(pk, pv), TypeExpr::DictType(ak, av)) => {
Self::extract_type_bindings(pk, ak, type_params, bindings)?;
Self::extract_type_bindings(pv, av, type_params, bindings)
}
(TypeExpr::DictType(pk, pv), TypeExpr::Shape(arg_fields)) => {
if matches!(pk.as_ref(), TypeExpr::Named(name) if name == "string") {
let value_union = Self::union_of_shape_field_types(arg_fields)
.unwrap_or_else(|| TypeExpr::Named("nil".into()));
Self::extract_type_bindings(pv, &value_union, type_params, bindings)?;
}
Ok(())
}
(
TypeExpr::Applied {
name: p_name,
args: p_args,
},
TypeExpr::Applied {
name: a_name,
args: a_args,
},
) if p_name == a_name && p_args.len() == a_args.len() => {
for (param, arg) in p_args.iter().zip(a_args.iter()) {
Self::extract_type_bindings(param, arg, type_params, bindings)?;
}
Ok(())
}
(TypeExpr::Shape(param_fields), TypeExpr::Shape(arg_fields)) => {
for param_field in param_fields {
if let Some(arg_field) = arg_fields
.iter()
.find(|field| field.name == param_field.name)
{
Self::extract_type_bindings(
¶m_field.type_expr,
&arg_field.type_expr,
type_params,
bindings,
)?;
}
}
Ok(())
}
(
TypeExpr::FnType {
params: p_params,
return_type: p_ret,
},
TypeExpr::FnType {
params: a_params,
return_type: a_ret,
},
) => {
for (param, arg) in p_params.iter().zip(a_params.iter()) {
Self::extract_type_bindings(param, arg, type_params, bindings)?;
}
Self::extract_type_bindings(p_ret, a_ret, type_params, bindings)
}
_ => Ok(()),
}
}
pub(in crate::typechecker) fn bind_from_arg_node(
&self,
param: &TypeExpr,
arg: &SNode,
type_params: &std::collections::BTreeSet<String>,
bindings: &mut BTreeMap<String, TypeExpr>,
scope: &TypeScope,
) -> Result<(), String> {
match param {
TypeExpr::Applied { name, args } if name == "Schema" && args.len() == 1 => {
if let TypeExpr::Named(tp) = &args[0] {
if type_params.contains(tp) {
if let Some(resolved) = schema_type_expr_from_node(arg, scope) {
Self::bind_type_param(tp, &resolved, bindings)?;
}
}
}
Ok(())
}
TypeExpr::Shape(fields) => {
if let Node::DictLiteral(entries) = &arg.node {
for field in fields {
let matching = entries.iter().find(|entry| match &entry.key.node {
Node::StringLiteral(key) | Node::Identifier(key) => key == &field.name,
_ => false,
});
if let Some(entry) = matching {
self.bind_from_arg_node(
&field.type_expr,
&entry.value,
type_params,
bindings,
scope,
)?;
}
}
return Ok(());
}
if let Some(arg_ty) = self.infer_type(arg, scope) {
Self::extract_type_bindings(param, &arg_ty, type_params, bindings)?;
}
Ok(())
}
_ => {
if let Some(arg_ty) = self.infer_type(arg, scope) {
Self::extract_type_bindings(param, &arg_ty, type_params, bindings)?;
}
Ok(())
}
}
}
pub(in crate::typechecker) fn apply_type_bindings(
ty: &TypeExpr,
bindings: &BTreeMap<String, TypeExpr>,
) -> TypeExpr {
match ty {
TypeExpr::Named(name) => bindings
.get(name)
.cloned()
.unwrap_or_else(|| TypeExpr::Named(name.clone())),
TypeExpr::Union(items) => TypeExpr::Union(
items
.iter()
.map(|item| Self::apply_type_bindings(item, bindings))
.collect(),
),
TypeExpr::Intersection(items) => TypeExpr::Intersection(
items
.iter()
.map(|item| Self::apply_type_bindings(item, bindings))
.collect(),
),
TypeExpr::Shape(fields) => TypeExpr::Shape(
fields
.iter()
.map(|field| ShapeField {
name: field.name.clone(),
type_expr: Self::apply_type_bindings(&field.type_expr, bindings),
optional: field.optional,
})
.collect(),
),
TypeExpr::List(inner) => {
TypeExpr::List(Box::new(Self::apply_type_bindings(inner, bindings)))
}
TypeExpr::Iter(inner) => {
TypeExpr::Iter(Box::new(Self::apply_type_bindings(inner, bindings)))
}
TypeExpr::Generator(inner) => {
TypeExpr::Generator(Box::new(Self::apply_type_bindings(inner, bindings)))
}
TypeExpr::Stream(inner) => {
TypeExpr::Stream(Box::new(Self::apply_type_bindings(inner, bindings)))
}
TypeExpr::DictType(key, value) => TypeExpr::DictType(
Box::new(Self::apply_type_bindings(key, bindings)),
Box::new(Self::apply_type_bindings(value, bindings)),
),
TypeExpr::Applied { name, args } => TypeExpr::Applied {
name: name.clone(),
args: args
.iter()
.map(|arg| Self::apply_type_bindings(arg, bindings))
.collect(),
},
TypeExpr::FnType {
params,
return_type,
} => TypeExpr::FnType {
params: params
.iter()
.map(|param| Self::apply_type_bindings(param, bindings))
.collect(),
return_type: Box::new(Self::apply_type_bindings(return_type, bindings)),
},
TypeExpr::Never => TypeExpr::Never,
TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
TypeExpr::Owned(inner) => {
TypeExpr::Owned(Box::new(Self::apply_type_bindings(inner, bindings)))
}
}
}
pub(in crate::typechecker) fn applied_type_or_name(
name: &str,
args: Vec<TypeExpr>,
) -> TypeExpr {
if args.is_empty() {
TypeExpr::Named(name.to_string())
} else {
TypeExpr::Applied {
name: name.to_string(),
args,
}
}
}
pub(in crate::typechecker) fn infer_struct_bindings(
&self,
struct_info: &StructDeclInfo,
fields: &[DictEntry],
scope: &TypeScope,
) -> BTreeMap<String, TypeExpr> {
let type_param_set: std::collections::BTreeSet<String> = struct_info
.type_params
.iter()
.map(|tp| tp.name.clone())
.collect();
let mut bindings = BTreeMap::new();
for field in &struct_info.fields {
let Some(expected_type) = &field.type_expr else {
continue;
};
let Some(entry) = fields.iter().find(|entry| {
matches!(&entry.key.node, Node::StringLiteral(key) | Node::Identifier(key) if key == &field.name)
}) else {
continue;
};
let Some(actual_type) = self.infer_type(&entry.value, scope) else {
continue;
};
let _ = Self::extract_type_bindings(
expected_type,
&actual_type,
&type_param_set,
&mut bindings,
);
}
bindings
}
pub(in crate::typechecker) fn infer_struct_type(
&self,
struct_name: &str,
struct_info: &StructDeclInfo,
fields: &[DictEntry],
scope: &TypeScope,
) -> TypeExpr {
let bindings = self.infer_struct_bindings(struct_info, fields, scope);
let args = struct_info
.type_params
.iter()
.map(|tp| {
bindings
.get(&tp.name)
.cloned()
.unwrap_or_else(Self::wildcard_type)
})
.collect();
Self::applied_type_or_name(struct_name, args)
}
pub(in crate::typechecker) fn infer_enum_type(
&self,
enum_name: &str,
enum_info: &EnumDeclInfo,
variant_name: &str,
args: &[SNode],
scope: &TypeScope,
) -> TypeExpr {
let type_param_set: std::collections::BTreeSet<String> = enum_info
.type_params
.iter()
.map(|tp| tp.name.clone())
.collect();
let mut bindings = BTreeMap::new();
if let Some(variant) = enum_info
.variants
.iter()
.find(|variant| variant.name == variant_name)
{
for (field, arg) in variant.fields.iter().zip(args.iter()) {
let Some(expected_type) = &field.type_expr else {
continue;
};
let Some(actual_type) = self.infer_type(arg, scope) else {
continue;
};
let _ = Self::extract_type_bindings(
expected_type,
&actual_type,
&type_param_set,
&mut bindings,
);
}
}
let args = enum_info
.type_params
.iter()
.map(|tp| {
bindings
.get(&tp.name)
.cloned()
.unwrap_or_else(Self::wildcard_type)
})
.collect();
Self::applied_type_or_name(enum_name, args)
}
pub(in crate::typechecker) fn visit_for_deprecation(&mut self, node: &SNode) {
match &node.node {
Node::FunctionCall { name, args, .. } => {
if let Some((since, use_hint)) = self.deprecated_fns.get(name).cloned() {
let mut msg = format!("`{name}` is deprecated");
if let Some(s) = since {
msg.push_str(&format!(" (since {s})"));
}
match use_hint {
Some(h) => self.warning_at_with_help(
Code::DeprecatedFunction,
msg,
node.span,
format!("use `{h}` instead"),
),
None => self.warning_at(Code::DeprecatedFunction, msg, node.span),
}
}
for a in args {
self.visit_for_deprecation(a);
}
}
Node::MethodCall { object, args, .. }
| Node::OptionalMethodCall { object, args, .. } => {
self.visit_for_deprecation(object);
for a in args {
self.visit_for_deprecation(a);
}
}
Node::AttributedDecl { inner, .. } => self.visit_for_deprecation(inner),
Node::CostRoute { options, body } => {
for (_, value) in options {
self.visit_for_deprecation(value);
}
for s in body {
self.visit_for_deprecation(s);
}
}
Node::HitlExpr { args, .. } => {
for arg in args {
self.visit_for_deprecation(&arg.value);
}
}
Node::Pipeline { body, .. }
| Node::OverrideDecl { body, .. }
| Node::FnDecl { body, .. }
| Node::ToolDecl { body, .. }
| Node::SpawnExpr { body }
| Node::TryExpr { body }
| Node::Block(body)
| Node::Closure { body, .. }
| Node::WhileLoop { body, .. }
| Node::Retry { body, .. }
| Node::DeferStmt { body }
| Node::MutexBlock { body, .. }
| Node::Parallel { body, .. } => {
for s in body {
self.visit_for_deprecation(s);
}
}
Node::SkillDecl { fields, .. } => {
for (_k, v) in fields {
self.visit_for_deprecation(v);
}
}
Node::EvalPackDecl {
fields,
body,
summarize,
..
} => {
for (_k, v) in fields {
self.visit_for_deprecation(v);
}
for s in body {
self.visit_for_deprecation(s);
}
if let Some(summary_body) = summarize {
for s in summary_body {
self.visit_for_deprecation(s);
}
}
}
Node::IfElse {
condition,
then_body,
else_body,
} => {
self.visit_for_deprecation(condition);
for s in then_body {
self.visit_for_deprecation(s);
}
if let Some(eb) = else_body {
for s in eb {
self.visit_for_deprecation(s);
}
}
}
Node::ForIn { iterable, body, .. } => {
self.visit_for_deprecation(iterable);
for s in body {
self.visit_for_deprecation(s);
}
}
Node::TryCatch {
has_catch: _,
body,
catch_body,
finally_body,
..
} => {
for s in body {
self.visit_for_deprecation(s);
}
for s in catch_body {
self.visit_for_deprecation(s);
}
if let Some(fb) = finally_body {
for s in fb {
self.visit_for_deprecation(s);
}
}
}
Node::DeadlineBlock { duration, body } => {
self.visit_for_deprecation(duration);
for s in body {
self.visit_for_deprecation(s);
}
}
Node::MatchExpr { value, arms } => {
self.visit_for_deprecation(value);
for arm in arms {
if let Some(g) = &arm.guard {
self.visit_for_deprecation(g);
}
for s in &arm.body {
self.visit_for_deprecation(s);
}
}
}
Node::LetBinding { value, .. } | Node::VarBinding { value, .. } => {
self.visit_for_deprecation(value);
}
Node::ConstBinding { value, .. } => {
self.visit_for_deprecation(value);
}
Node::Assignment { target, value, .. } => {
self.visit_for_deprecation(target);
self.visit_for_deprecation(value);
}
Node::ReturnStmt { value: Some(v) } | Node::YieldExpr { value: Some(v) } => {
self.visit_for_deprecation(v);
}
Node::EmitExpr { value: v } => {
self.visit_for_deprecation(v);
}
Node::ThrowStmt { value }
| Node::TryOperator { operand: value }
| Node::TryStar { operand: value }
| Node::Spread(value) => self.visit_for_deprecation(value),
Node::UnaryOp { operand, .. } => self.visit_for_deprecation(operand),
Node::BinaryOp { left, right, .. } => {
self.visit_for_deprecation(left);
self.visit_for_deprecation(right);
}
Node::Ternary {
condition,
true_expr,
false_expr,
} => {
self.visit_for_deprecation(condition);
self.visit_for_deprecation(true_expr);
self.visit_for_deprecation(false_expr);
}
Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
self.visit_for_deprecation(object);
}
Node::SubscriptAccess { object, index }
| Node::OptionalSubscriptAccess { object, index } => {
self.visit_for_deprecation(object);
self.visit_for_deprecation(index);
}
Node::SliceAccess { object, start, end } => {
self.visit_for_deprecation(object);
if let Some(s) = start {
self.visit_for_deprecation(s);
}
if let Some(e) = end {
self.visit_for_deprecation(e);
}
}
Node::EnumConstruct { args, .. } | Node::ListLiteral(args) => {
for a in args {
self.visit_for_deprecation(a);
}
}
Node::DictLiteral(entries)
| Node::StructConstruct {
fields: entries, ..
} => {
for e in entries {
self.visit_for_deprecation(&e.key);
self.visit_for_deprecation(&e.value);
}
}
Node::GuardStmt {
condition,
else_body,
} => {
self.visit_for_deprecation(condition);
for s in else_body {
self.visit_for_deprecation(s);
}
}
Node::RequireStmt {
condition, message, ..
} => {
self.visit_for_deprecation(condition);
if let Some(m) = message {
self.visit_for_deprecation(m);
}
}
Node::RangeExpr { start, end, .. } => {
self.visit_for_deprecation(start);
self.visit_for_deprecation(end);
}
Node::SelectExpr {
cases,
timeout,
default_body,
} => {
for c in cases {
self.visit_for_deprecation(&c.channel);
for s in &c.body {
self.visit_for_deprecation(s);
}
}
if let Some((d, b)) = timeout {
self.visit_for_deprecation(d);
for s in b {
self.visit_for_deprecation(s);
}
}
if let Some(b) = default_body {
for s in b {
self.visit_for_deprecation(s);
}
}
}
Node::ImplBlock { methods, .. } => {
for m in methods {
self.visit_for_deprecation(m);
}
}
_ => {}
}
}
pub(in crate::typechecker) fn check_call(
&mut self,
name: &str,
type_args: &[TypeExpr],
args: &[SNode],
scope: &mut TypeScope,
span: Span,
) {
if let Some(imported) = self.imported_names.as_ref() {
let resolvable = is_builtin(name)
|| scope.get_fn(name).is_some()
|| scope.get_struct(name).is_some()
|| scope.get_enum(name).is_some()
|| scope.get_var(name).is_some()
|| imported.contains(name)
|| scope.is_generic_type_param(name)
|| name.starts_with("__")
|| name.starts_with("hostlib_")
|| matches!(name, "Ok" | "Err" | "Some" | "None");
if !resolvable {
let candidates: Vec<String> = builtin_signatures::iter_builtin_names()
.map(|s| s.to_string())
.chain(scope.all_fn_names())
.chain(imported.iter().cloned())
.collect();
let suggestion = crate::diagnostic::renamed_stdlib_symbol(name)
.map(str::to_string)
.or_else(|| {
crate::diagnostic::find_closest_match(
name,
candidates.iter().map(|s| s.as_str()),
2,
)
.map(|c| c.to_string())
});
let message = match &suggestion {
Some(s) => format!(
"call target `{name}` is not defined or imported — did you mean `{s}`?"
),
None => format!("call target `{name}` is not defined or imported"),
};
match suggestion {
Some(s) => self.error_at_with_help(
Code::UndefinedFunction,
message,
span,
format!("did you mean `{s}`?"),
),
None => self.error_at(Code::UndefinedFunction, message, span),
}
}
}
if let Some((since, use_hint)) = self.deprecated_fns.get(name).cloned() {
let mut msg = format!("`{name}` is deprecated");
if let Some(s) = since {
msg.push_str(&format!(" (since {s})"));
}
let help = use_hint.map(|h| format!("use `{h}` instead"));
match help {
Some(h) => self.warning_at_with_help(Code::DeprecatedFunction, msg, span, h),
None => self.warning_at(Code::DeprecatedFunction, msg, span),
}
}
if name == "unreachable" {
if let Some(arg) = args.first() {
if matches!(&arg.node, Node::Identifier(_)) {
let arg_type = self.infer_type(arg, scope);
if let Some(ref ty) = arg_type {
if !matches!(ty, TypeExpr::Never) {
self.error_at(Code::NonExhaustiveMatch,
format!(
"unreachable() argument has type `{}` — not all cases are handled",
format_type(ty)
),
span,
);
}
}
}
}
self.check_unknown_exhaustiveness(scope, span, "unreachable()");
for arg in args {
self.check_node(arg, scope);
}
return;
}
if let Some(sig) = scope.get_fn(name).cloned() {
if matches!(sig.return_type, Some(TypeExpr::Never)) {
self.check_unknown_exhaustiveness(scope, span, &format!("{name}()"));
}
}
let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
if let Some(sig) = scope.get_fn(name).cloned() {
if !type_args.is_empty() {
if sig.type_param_names.is_empty() {
self.error_at(
Code::GenericTypeArgumentUnsupported,
format!("Function '{name}' does not declare type parameters"),
span,
);
} else if type_args.len() != sig.type_param_names.len() {
self.error_at(
Code::GenericTypeArgumentArity,
format!(
"Function '{}' expects {} type arguments, got {}",
name,
sig.type_param_names.len(),
type_args.len()
),
span,
);
}
}
if !has_spread && !is_builtin(name) {
let arity_ok = if sig.has_rest {
args.len() >= sig.params.len().saturating_sub(1)
} else {
args.len() >= sig.required_params && args.len() <= sig.params.len()
};
if !arity_ok {
let total = sig.params.len();
let (expected, single_arg) = if sig.has_rest {
(
format!("at least {}", total.saturating_sub(1)),
total.saturating_sub(1) == 1,
)
} else if sig.required_params == total {
(format!("{total}"), total == 1)
} else {
(format!("{}-{}", sig.required_params, total), false)
};
let arg_word = if single_arg { "argument" } else { "arguments" };
self.warning_at(
Code::OrchestrationArity,
format!(
"Function '{}' expects {} {}, got {}",
name,
expected,
arg_word,
args.len()
),
span,
);
}
}
let mut type_bindings: BTreeMap<String, TypeExpr> = BTreeMap::new();
let type_param_set: std::collections::BTreeSet<String> =
sig.type_param_names.iter().cloned().collect();
if type_args.len() == sig.type_param_names.len() {
for (param_name, type_arg) in sig.type_param_names.iter().zip(type_args.iter()) {
type_bindings.insert(param_name.clone(), type_arg.clone());
}
}
for (i, arg) in args.iter().enumerate() {
let Some((_param_name, param_type)) = Self::function_param_for_arg(&sig, i) else {
continue;
};
if let Some(param_ty) = param_type {
if let Err(message) = self.bind_from_arg_node(
param_ty,
arg,
&type_param_set,
&mut type_bindings,
scope,
) {
self.error_at(Code::ArgumentTypeMismatch, message, arg.span);
}
}
}
let unbound_type_params: std::collections::BTreeSet<String> = type_param_set
.iter()
.filter(|name| !type_bindings.contains_key(*name))
.cloned()
.collect();
let mut expected_args: Vec<Option<(String, TypeExpr)>> = Vec::with_capacity(args.len());
let mut contextual_args = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
let Some((param_name, param_type)) = Self::function_param_for_arg(&sig, i) else {
self.check_node(arg, scope);
expected_args.push(None);
contextual_args.push(false);
continue;
};
if let Some(expected) = param_type {
let expected = Self::apply_type_bindings(expected, &type_bindings);
let contextual_expected =
(!Self::contains_type_param(&expected, &unbound_type_params))
.then_some(&expected);
let context_checked =
self.check_node_with_expected(arg, contextual_expected, scope);
expected_args.push(Some((param_name.to_string(), expected)));
contextual_args.push(context_checked);
} else {
self.check_node(arg, scope);
expected_args.push(None);
contextual_args.push(false);
}
}
let call_scope_owned;
let call_scope: &TypeScope = if sig.type_param_names.is_empty() {
scope
} else {
let mut s = scope.child();
for tp_name in &sig.type_param_names {
s.generic_type_params.insert(tp_name.clone());
}
call_scope_owned = s;
&call_scope_owned
};
for (i, arg) in args.iter().enumerate() {
if let Some((param_name, expected)) =
expected_args.get(i).and_then(|entry| entry.as_ref())
{
let actual = self.infer_type(arg, scope);
if let Some(actual) = &actual {
self.check_unknown_option_bag_fields(
format!("argument {} `{}`", i + 1, param_name),
param_name,
expected,
arg,
call_scope,
);
if !contextual_args.get(i).copied().unwrap_or(false)
&& !self.types_compatible(expected, actual, call_scope)
{
self.type_mismatch_at(
Code::ArgumentTypeMismatch,
format!("argument {} `{}`", i + 1, param_name),
expected,
actual,
arg.span,
(
sig.definition_span.map(|span| {
(span, format!("parameter `{param_name}` declared here"))
}),
Some(arg.span),
),
call_scope,
);
}
}
}
}
if !sig.where_clauses.is_empty() {
for (type_param, bound) in &sig.where_clauses {
if let Some(concrete_type) = type_bindings.get(type_param) {
let concrete_name = format_type(concrete_type);
let Some(base_type_name) = Self::base_type_name(concrete_type) else {
self.error_at(Code::WhereConstraintMismatch,
format!(
"Type '{concrete_name}' does not satisfy interface '{bound}': only named types can satisfy interfaces (required by constraint `where {type_param}: {bound}`)"
),
span,
);
continue;
};
if let Some(reason) = self.interface_mismatch_reason(
base_type_name,
bound,
&BTreeMap::new(),
scope,
) {
self.error_at(
Code::WhereConstraintMismatch,
format!(
"Type '{concrete_name}' does not satisfy interface '{bound}': {reason} \
(required by constraint `where {type_param}: {bound}`)"
),
span,
);
}
}
}
}
} else if let Some(sig) = builtin_signatures::lookup(name) {
self.check_builtin_signature_call(name, sig, type_args, args, has_spread, scope, span);
} else {
for arg in args {
self.check_node(arg, scope);
}
}
}
}