use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use bock_air::stubs::{TypeInfo, Value};
use bock_air::{AIRNode, EnumVariantPayload, NodeId, NodeKind};
use bock_ast::{BinOp, GenericParam, Literal, TypeConstraint, TypeExpr, TypePath, UnaryOp};
use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
use crate::traits::{resolve_impl, ImplTable, TraitRef};
use crate::{
unify, EffectRef, FnType, GenericType, PrimitiveType, Substitution, Type, TypeError, TypeVarId,
};
const E_TYPE_MISMATCH: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4001,
};
const E_UNDEFINED_VAR: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4002,
};
const E_ARITY_MISMATCH: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4003,
};
const E_NOT_CALLABLE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4004,
};
const E_WHERE_CLAUSE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4005,
};
const E_NOT_EQUATABLE: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4015,
};
const E_NO_CONVERSION: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4012,
};
const E_NO_SUCH_METHOD: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4013,
};
const E_BARE_MODULE_IMPORT: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 4014,
};
const E_RESERVED_LAMBDA_HANDLER: DiagnosticCode = DiagnosticCode {
prefix: 'E',
number: 6006,
};
pub const RECV_KIND_META_KEY: &str = "recv_kind";
pub const LIST_CONCAT_META_KEY: &str = "list_concat";
pub const STRING_CONCAT_META_KEY: &str = "string_concat";
pub const INT_ARITH_META_KEY: &str = "int_arith";
pub const BOOL_STRINGIFY_META_KEY: &str = "bool_stringify";
pub const USER_COMPARE_META_KEY: &str = "user_compare";
pub const USER_EQ_META_KEY: &str = "user_eq";
pub const DERIVE_EQ_META_KEY: &str = "derive_structural_eq";
pub const CUSTOM_EQ_META_KEY: &str = "custom_eq_impl";
const SYNTH_ID_BASE: NodeId = 0x4000_0000;
type EnumVariantPayloadTypes = (String, Vec<(String, Type)>);
#[must_use]
pub fn recv_kind_tag(ty: &Type) -> Option<String> {
match ty {
Type::Primitive(p) => Some(format!("Primitive:{p:?}")),
Type::Optional(_) => Some("Optional".to_string()),
Type::Result(_, _) => Some("Result".to_string()),
Type::Generic(g) => match g.constructor.as_str() {
"List" => Some("List".to_string()),
"Set" => Some("Set".to_string()),
"Map" => Some("Map".to_string()),
other => Some(format!("Generic:{other}")),
},
Type::Named(n) => Some(format!("User:{}", n.name)),
_ => None,
}
}
struct TypeVarGen {
counter: AtomicU32,
}
impl TypeVarGen {
fn new() -> Self {
Self {
counter: AtomicU32::new(0),
}
}
fn next(&self) -> TypeVarId {
self.counter.fetch_add(1, Ordering::SeqCst)
}
}
pub struct TypeEnv {
scopes: Vec<HashMap<String, Type>>,
}
impl TypeEnv {
#[must_use]
pub fn new() -> Self {
Self {
scopes: vec![HashMap::new()],
}
}
pub fn push_scope(&mut self) {
self.scopes.push(HashMap::new());
}
pub fn pop_scope(&mut self) {
debug_assert!(self.scopes.len() > 1, "cannot pop the global scope");
self.scopes.pop();
}
pub fn define(&mut self, name: impl Into<String>, ty: Type) {
let scope = self.scopes.last_mut().expect("at least one scope");
scope.insert(name.into(), ty);
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<&Type> {
self.scopes.iter().rev().find_map(|s| s.get(name))
}
}
impl Default for TypeEnv {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
struct FnSig {
generic_params: Vec<String>,
generic_var_ids: Vec<TypeVarId>,
param_types: Vec<Type>,
return_type: Type,
where_clause: Vec<TypeConstraint>,
}
pub struct TypeChecker {
pub env: TypeEnv,
pub subst: Substitution,
pub diags: DiagnosticBag,
var_gen: TypeVarGen,
types: HashMap<NodeId, Type>,
fn_sigs: HashMap<String, FnSig>,
return_ty_stack: Vec<Type>,
pub impl_table: Option<ImplTable>,
imported_trait_impls: Vec<(String, Vec<Type>, Type)>,
method_types: HashMap<String, HashMap<String, Type>>,
method_generic_params: HashMap<String, HashMap<String, Vec<String>>>,
effect_op_types: HashMap<String, Vec<(String, Type)>>,
effect_components: HashMap<String, Vec<String>>,
record_field_types: HashMap<String, Vec<(String, Type)>>,
record_generic_params: HashMap<String, Vec<String>>,
type_aliases: HashMap<String, Type>,
trait_method_types: HashMap<String, HashMap<String, Type>>,
type_var_bounds: HashMap<TypeVarId, Vec<String>>,
current_fn_param_bounds: HashMap<String, Vec<String>>,
class_names: HashSet<String>,
enum_variant_payloads: HashMap<String, Vec<EnumVariantPayloadTypes>>,
synth_id: std::cell::Cell<NodeId>,
synth_iter_var: std::cell::Cell<u32>,
reported_undefined: HashSet<(String, Span)>,
}
impl TypeChecker {
#[must_use]
pub fn new() -> Self {
Self {
env: TypeEnv::new(),
subst: Substitution::new(),
diags: DiagnosticBag::new(),
var_gen: TypeVarGen::new(),
types: HashMap::new(),
fn_sigs: HashMap::new(),
return_ty_stack: Vec::new(),
impl_table: None,
imported_trait_impls: Vec::new(),
method_types: HashMap::new(),
method_generic_params: HashMap::new(),
effect_op_types: HashMap::new(),
effect_components: HashMap::new(),
record_field_types: HashMap::new(),
record_generic_params: HashMap::new(),
type_aliases: HashMap::new(),
trait_method_types: HashMap::new(),
type_var_bounds: HashMap::new(),
current_fn_param_bounds: HashMap::new(),
class_names: HashSet::new(),
enum_variant_payloads: HashMap::new(),
synth_id: std::cell::Cell::new(SYNTH_ID_BASE),
synth_iter_var: std::cell::Cell::new(0),
reported_undefined: HashSet::new(),
}
}
fn fresh_var(&self) -> Type {
Type::TypeVar(self.var_gen.next())
}
fn next_synth_id(&self) -> NodeId {
let id = self.synth_id.get();
self.synth_id.set(id.wrapping_add(1));
id
}
fn synth_node(&self, span: Span, scope_id: i64, kind: NodeKind) -> AIRNode {
let mut node = AIRNode::new(self.next_synth_id(), span, kind);
node.metadata
.insert("scope_id".to_string(), Value::Int(scope_id));
node
}
fn synth_ident(&self, name: &str, span: Span, scope_id: i64) -> AIRNode {
self.synth_node(
span,
scope_id,
NodeKind::Identifier {
name: bock_ast::Ident {
name: name.to_string(),
span,
},
},
)
}
fn synth_method_call(
&self,
receiver: AIRNode,
method: &str,
span: Span,
scope_id: i64,
) -> AIRNode {
let field_access = self.synth_node(
span,
scope_id,
NodeKind::FieldAccess {
object: Box::new(receiver.clone()),
field: bock_ast::Ident {
name: method.to_string(),
span,
},
},
);
let self_arg = bock_air::AirArg {
label: None,
value: receiver,
};
self.synth_node(
span,
scope_id,
NodeKind::Call {
callee: Box::new(field_access),
args: vec![self_arg],
type_args: vec![],
},
)
}
fn synth_ctor_pat(
&self,
ctor: &str,
fields: Vec<AIRNode>,
span: Span,
scope_id: i64,
) -> AIRNode {
self.synth_node(
span,
scope_id,
NodeKind::ConstructorPat {
path: TypePath {
segments: vec![bock_ast::Ident {
name: ctor.to_string(),
span,
}],
span,
},
fields,
},
)
}
fn desugar_for_iterable(
&self,
node: &mut AIRNode,
pattern: AIRNode,
iterable: AIRNode,
body: AIRNode,
iter_var: &str,
) {
let span = node.span;
let scope_id = node
.metadata
.get("scope_id")
.and_then(|v| match v {
Value::Int(i) => Some(*i),
_ => None,
})
.unwrap_or(0);
let iter_call = self.synth_method_call(iterable, "iter", span, scope_id);
let let_pat = self.synth_node(
span,
scope_id,
NodeKind::BindPat {
name: bock_ast::Ident {
name: iter_var.to_string(),
span,
},
is_mut: true,
},
);
let let_binding = self.synth_node(
span,
scope_id,
NodeKind::LetBinding {
is_mut: true,
pattern: Box::new(let_pat),
ty: None,
value: Box::new(iter_call),
},
);
let next_recv = self.synth_ident(iter_var, span, scope_id);
let next_call = self.synth_method_call(next_recv, "next", span, scope_id);
let some_pat = self.synth_ctor_pat("Some", vec![pattern], span, scope_id);
let some_arm = self.synth_node(
span,
scope_id,
NodeKind::MatchArm {
pattern: Box::new(some_pat),
guard: None,
body: Box::new(body),
},
);
let none_pat = self.synth_ctor_pat("None", vec![], span, scope_id);
let break_node = self.synth_node(span, scope_id, NodeKind::Break { value: None });
let none_arm = self.synth_node(
span,
scope_id,
NodeKind::MatchArm {
pattern: Box::new(none_pat),
guard: None,
body: Box::new(break_node),
},
);
let match_node = self.synth_node(
span,
scope_id,
NodeKind::Match {
scrutinee: Box::new(next_call),
arms: vec![some_arm, none_arm],
},
);
let loop_body = self.synth_node(
span,
scope_id,
NodeKind::Block {
stmts: vec![match_node],
tail: None,
},
);
let loop_node = self.synth_node(
span,
scope_id,
NodeKind::Loop {
body: Box::new(loop_body),
},
);
node.kind = NodeKind::Block {
stmts: vec![let_binding, loop_node],
tail: None,
};
}
fn record(&mut self, node: &mut AIRNode, ty: Type) -> Type {
let resolved = self.subst.apply(&ty);
self.types.insert(node.id, resolved.clone());
node.type_info = Some(TypeInfo {
resolved_type: None,
});
if matches!(resolved, Type::Primitive(_)) {
node.metadata.insert("copy_type".into(), Value::Bool(true));
}
resolved
}
#[must_use]
pub fn type_of(&self, id: NodeId) -> Option<&Type> {
self.types.get(&id)
}
fn stamp_recv_kind(&self, node: &mut AIRNode, receiver_ty: &Type) {
let resolved = self.subst.apply(receiver_ty);
let tag = match &resolved {
Type::TypeVar(id) => self
.type_var_bounds
.get(id)
.and_then(|bounds| bounds.first())
.map(|trait_name| format!("TraitBound:{trait_name}")),
_ => recv_kind_tag(&resolved),
};
if let Some(tag) = tag {
node.metadata
.insert(RECV_KIND_META_KEY.to_string(), Value::String(tag));
}
}
#[must_use]
pub fn record_field_types(&self) -> &HashMap<String, Vec<(String, Type)>> {
&self.record_field_types
}
#[must_use]
pub fn record_generic_params(&self) -> &HashMap<String, Vec<String>> {
&self.record_generic_params
}
#[must_use]
pub fn effect_op_types(&self) -> &HashMap<String, Vec<(String, Type)>> {
&self.effect_op_types
}
#[must_use]
pub fn effect_components(&self) -> &HashMap<String, Vec<String>> {
&self.effect_components
}
#[must_use]
pub fn method_types(&self) -> &HashMap<String, HashMap<String, Type>> {
&self.method_types
}
#[must_use]
pub fn trait_method_types(&self) -> &HashMap<String, HashMap<String, Type>> {
&self.trait_method_types
}
#[must_use]
pub fn type_aliases(&self) -> &HashMap<String, Type> {
&self.type_aliases
}
#[must_use]
pub fn fn_where_bounds(&self, name: &str) -> Vec<(TypeVarId, Vec<String>)> {
let Some(sig) = self.fn_sigs.get(name) else {
return vec![];
};
let name_to_id: HashMap<&str, TypeVarId> = sig
.generic_params
.iter()
.zip(sig.generic_var_ids.iter())
.map(|(n, id)| (n.as_str(), *id))
.collect();
let mut out: Vec<(TypeVarId, Vec<String>)> = Vec::new();
for clause in &sig.where_clause {
let Some(&var_id) = name_to_id.get(clause.param.name.as_str()) else {
continue; };
let traits: Vec<String> = clause.bounds.iter().map(type_path_to_name).collect();
if traits.is_empty() {
continue;
}
if let Some(existing) = out.iter_mut().find(|(id, _)| *id == var_id) {
for t in traits {
if !existing.1.contains(&t) {
existing.1.push(t);
}
}
} else {
out.push((var_id, traits));
}
}
out
}
pub fn insert_record_field_types(&mut self, name: String, fields: Vec<(String, Type)>) {
self.record_field_types.insert(name, fields);
}
pub fn insert_record_generic_params(&mut self, name: String, params: Vec<String>) {
self.record_generic_params.insert(name, params);
}
pub fn insert_trait_method_types(&mut self, name: String, methods: HashMap<String, Type>) {
self.trait_method_types.insert(name, methods);
}
pub fn insert_effect_op_types(&mut self, name: String, ops: Vec<(String, Type)>) {
self.effect_op_types.insert(name, ops);
}
pub fn insert_effect_components(&mut self, name: String, components: Vec<String>) {
self.effect_components.insert(name, components);
}
pub fn register_imported_trait_impl(
&mut self,
trait_name: String,
trait_args: Vec<Type>,
target: Type,
) {
self.imported_trait_impls
.push((trait_name, trait_args, target));
}
pub fn insert_type_alias(&mut self, name: String, underlying: Type) {
self.type_aliases.insert(name, underlying);
}
pub fn insert_method_types(&mut self, type_name: String, methods: HashMap<String, Type>) {
self.method_types.insert(type_name, methods);
}
pub fn seed_imported_generic_fn(&mut self, name: &str, fn_ty: &FnType) -> Type {
self.seed_imported_generic_fn_with_bounds(name, fn_ty, &[])
}
pub fn seed_imported_generic_fn_with_bounds(
&mut self,
name: &str,
fn_ty: &FnType,
bounds: &[(TypeVarId, Vec<String>)],
) -> Type {
let mut original_ids = Vec::new();
collect_type_var_ids_fn(fn_ty, &mut original_ids);
if original_ids.is_empty() {
let ty = Type::Function(fn_ty.clone());
self.env.define(name, ty.clone());
return ty;
}
let remap: HashMap<TypeVarId, Type> = original_ids
.iter()
.map(|&id| (id, self.fresh_var()))
.collect();
let fresh_ids: Vec<TypeVarId> = original_ids
.iter()
.map(|id| match &remap[id] {
Type::TypeVar(fresh) => *fresh,
_ => unreachable!(),
})
.collect();
let remapped = Type::Function(FnType {
params: fn_ty
.params
.iter()
.map(|t| self.replace_type_vars(t, &remap))
.collect(),
ret: Box::new(self.replace_type_vars(&fn_ty.ret, &remap)),
effects: fn_ty.effects.clone(),
});
self.env.define(name, remapped.clone());
let generic_params: Vec<String> =
(0..original_ids.len()).map(|i| format!("T{i}")).collect();
let where_clause: Vec<TypeConstraint> = bounds
.iter()
.filter_map(|(orig_id, traits)| {
let pos = original_ids.iter().position(|id| id == orig_id)?;
if traits.is_empty() {
return None;
}
Some(TypeConstraint {
id: 0,
span: Span::dummy(),
param: bock_ast::Ident {
name: generic_params[pos].clone(),
span: Span::dummy(),
},
bounds: traits
.iter()
.map(|t| TypePath {
segments: t
.split('.')
.map(|seg| bock_ast::Ident {
name: seg.to_string(),
span: Span::dummy(),
})
.collect(),
span: Span::dummy(),
})
.collect(),
})
})
.collect();
if let Type::Function(ref f) = remapped {
self.fn_sigs.insert(
name.to_string(),
FnSig {
generic_params,
generic_var_ids: fresh_ids,
param_types: f.params.clone(),
return_type: (*f.ret).clone(),
where_clause,
},
);
}
remapped
}
fn unify_or_error(&mut self, found: &Type, expected: &Type, span: Span, context: &str) -> Type {
let found = self.resolve_alias(&self.subst.apply(found));
let expected = self.resolve_alias(&self.subst.apply(expected));
match unify(&expected, &found, &mut self.subst) {
Ok(()) => self.subst.apply(&found),
Err(e) => {
let msg = match &e {
TypeError::Mismatch { .. } => {
format!(
"type mismatch in {context}: expected `{expected}`, found `{found}`"
)
}
other => format!("type mismatch in {context}: {other}"),
};
let diag = self.diags.error(E_TYPE_MISMATCH, msg, span);
if let Some(hint) = conversion_hint(&found, &expected) {
diag.note(hint);
}
Type::Error
}
}
}
pub fn check_module(&mut self, module: &mut AIRNode) {
let (items, imports) = match &module.kind {
NodeKind::Module { items, imports, .. } => (items.clone(), imports.clone()),
_ => return,
};
self.reject_bare_module_imports(&imports);
let mut impl_table = ImplTable::build_from(module);
crate::traits::register_canonical_conformances(&mut impl_table);
crate::traits::register_canonical_conversions(&mut impl_table);
if !self.imported_trait_impls.is_empty() {
impl_table.fold_imported_impls(&self.imported_trait_impls);
}
self.diags.absorb(&impl_table.diags);
self.impl_table = Some(impl_table);
for item in &items {
self.collect_sig(item);
}
{
let mut visited = std::collections::HashSet::new();
for item in &items {
if let NodeKind::ModuleHandle { effect, .. } = &item.kind {
let ename = type_path_to_name(effect);
self.inject_effect_ops_into_env(&ename, &mut visited);
}
}
}
if let NodeKind::Module { items, .. } = &mut module.kind {
for item in items.iter_mut() {
self.check_item(item);
}
}
self.record(module, Type::Primitive(PrimitiveType::Void));
}
fn reject_bare_module_imports(&mut self, imports: &[AIRNode]) {
for import in imports {
let NodeKind::ImportDecl { path, items } = &import.kind else {
continue;
};
if !matches!(items, bock_ast::ImportItems::Module) {
continue;
}
let path_str = path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
self.diags
.error(
E_BARE_MODULE_IMPORT,
format!(
"`use {path_str}` is not a v1 import form: a `use` must \
name what it imports with a brace-list or a wildcard"
),
import.span,
)
.note(format!(
"import the names you need with the braced form, e.g. \
`use {path_str}.{{ /* names */ }}`"
))
.note(
"module-qualified access (referring to symbols as \
`module.Symbol`) is deferred to v1.x",
);
}
}
fn collect_sig(&mut self, node: &AIRNode) {
match &node.kind {
NodeKind::FnDecl {
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
..
} => {
let gp_names: Vec<String> =
generic_params.iter().map(|g| g.name.name.clone()).collect();
let gp_map: HashMap<String, Type> = gp_names
.iter()
.map(|n| (n.clone(), self.fresh_var()))
.collect();
let gp_var_ids: Vec<TypeVarId> = gp_names
.iter()
.map(|n| match &gp_map[n] {
Type::TypeVar(id) => *id,
_ => unreachable!(),
})
.collect();
let param_types: Vec<Type> = params
.iter()
.map(|p| self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map))
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
let effects: Vec<EffectRef> = effect_clause
.iter()
.map(|tp| {
let name = tp
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
EffectRef::new(name)
})
.collect();
let fn_ty = Type::Function(FnType {
params: param_types.clone(),
ret: Box::new(ret_ty.clone()),
effects: effects.clone(),
});
self.env.define(name.name.clone(), fn_ty);
let bracket_bounds = generic_params.iter().filter_map(|gp| {
if gp.bounds.is_empty() {
None
} else {
Some(TypeConstraint {
id: gp.id,
span: gp.span,
param: gp.name.clone(),
bounds: gp.bounds.clone(),
})
}
});
let merged_where_clause: Vec<TypeConstraint> =
bracket_bounds.chain(where_clause.iter().cloned()).collect();
self.fn_sigs.insert(
name.name.clone(),
FnSig {
generic_params: gp_names,
generic_var_ids: gp_var_ids,
param_types,
return_type: ret_ty,
where_clause: merged_where_clause,
},
);
}
NodeKind::ConstDecl { name, ty, .. } => {
let const_ty = self.air_type_node_to_type(ty, &HashMap::new());
self.env.define(name.name.clone(), const_ty);
}
NodeKind::EnumDecl {
name,
variants,
generic_params,
..
} => {
let enum_name = name.name.clone();
let gp_names: Vec<String> =
generic_params.iter().map(|g| g.name.name.clone()).collect();
let named_ty = Type::Named(crate::NamedType {
name: enum_name.clone(),
});
let (gp_map, gp_var_ids, generic_ret_ty) = if gp_names.is_empty() {
(HashMap::new(), vec![], named_ty.clone())
} else {
let gp_map: HashMap<String, Type> = gp_names
.iter()
.map(|n| (n.clone(), self.fresh_var()))
.collect();
let gp_var_ids: Vec<TypeVarId> = gp_names
.iter()
.map(|n| match &gp_map[n] {
Type::TypeVar(id) => *id,
_ => unreachable!(),
})
.collect();
let type_args: Vec<Type> = gp_names.iter().map(|n| gp_map[n].clone()).collect();
let generic_ret_ty = Type::Generic(GenericType {
constructor: enum_name.clone(),
args: type_args,
});
(gp_map, gp_var_ids, generic_ret_ty)
};
self.env.define(enum_name.clone(), named_ty.clone());
if !gp_names.is_empty() {
self.record_generic_params
.insert(enum_name.clone(), gp_names.clone());
}
let symbolic_gp_map: HashMap<String, Type> = gp_names
.iter()
.map(|n| (n.clone(), Type::Named(crate::NamedType { name: n.clone() })))
.collect();
let mut payloads: Vec<EnumVariantPayloadTypes> = Vec::new();
for variant in variants {
if let NodeKind::EnumVariant {
name: vname,
payload,
} = &variant.kind
{
let components: Vec<(String, Type)> = match payload {
EnumVariantPayload::Unit => vec![],
EnumVariantPayload::Tuple(param_nodes) => param_nodes
.iter()
.enumerate()
.map(|(i, p)| {
(
format!("_{i}"),
self.air_type_node_to_type(p, &symbolic_gp_map),
)
})
.collect(),
EnumVariantPayload::Struct(fields) => fields
.iter()
.map(|f| {
(
f.name.name.clone(),
self.type_expr_to_type(&f.ty, &symbolic_gp_map),
)
})
.collect(),
};
payloads.push((vname.name.clone(), components));
}
}
self.enum_variant_payloads
.insert(enum_name.clone(), payloads);
for variant in variants {
if let NodeKind::EnumVariant {
name: vname,
payload,
} = &variant.kind
{
match payload {
EnumVariantPayload::Unit => {
self.env.define(vname.name.clone(), named_ty.clone());
}
EnumVariantPayload::Tuple(param_nodes) => {
let param_tys: Vec<Type> = param_nodes
.iter()
.map(|p| self.air_type_node_to_type(p, &gp_map))
.collect();
let fn_ty = Type::Function(FnType {
params: param_tys.clone(),
ret: Box::new(generic_ret_ty.clone()),
effects: vec![],
});
self.env.define(vname.name.clone(), fn_ty);
if !gp_names.is_empty() {
self.fn_sigs.insert(
vname.name.clone(),
FnSig {
generic_params: gp_names.clone(),
generic_var_ids: gp_var_ids.clone(),
param_types: param_tys,
return_type: generic_ret_ty.clone(),
where_clause: vec![],
},
);
}
}
EnumVariantPayload::Struct(fields) => {
self.env.define(vname.name.clone(), named_ty.clone());
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|f| {
let ty = self.type_expr_to_type(&f.ty, &gp_map);
(f.name.name.clone(), ty)
})
.collect();
self.record_field_types
.insert(vname.name.clone(), field_types);
if !gp_names.is_empty() {
self.record_generic_params
.insert(vname.name.clone(), gp_names.clone());
}
}
}
}
}
}
NodeKind::ImplBlock {
target, methods, ..
} => {
let target_name = match &target.kind {
NodeKind::TypeNamed { path, .. } => type_path_to_name(path),
_ => return,
};
let target_ty = Type::Named(crate::NamedType {
name: target_name.clone(),
});
for method in methods {
if let NodeKind::FnDecl {
name,
params,
return_type,
generic_params: method_gps,
..
} = &method.kind
{
let gp_map: HashMap<String, Type> = HashMap::new();
let method_gp_names: Vec<String> =
method_gps.iter().map(|g| g.name.name.clone()).collect();
if !method_gp_names.is_empty() {
self.method_generic_params
.entry(target_name.clone())
.or_default()
.insert(name.name.clone(), method_gp_names);
}
let param_types: Vec<Type> = params
.iter()
.map(|p| {
if let NodeKind::Param {
pattern, ty: None, ..
} = &p.kind
{
if let NodeKind::BindPat { name, .. } = &pattern.kind {
if name.name == "self" {
return target_ty.clone();
}
}
}
self.air_type_node_to_type(p, &gp_map)
})
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
let fn_ty = Type::Function(FnType {
params: param_types,
ret: Box::new(ret_ty),
effects: vec![],
});
let self_params = ["Self".to_string()];
let self_args = [target_ty.clone()];
let fn_ty = substitute_type_params(&fn_ty, &self_params, &self_args);
self.method_types
.entry(target_name.clone())
.or_default()
.insert(name.name.clone(), fn_ty);
}
}
}
NodeKind::EffectDecl {
name,
operations,
components,
..
} => {
let mut ops = Vec::new();
for op in operations {
if let NodeKind::FnDecl {
name: op_name,
params,
return_type,
..
} = &op.kind
{
let param_types: Vec<Type> = params
.iter()
.map(|p| {
self.air_type_node_to_type(p.kind.param_ty_node(), &HashMap::new())
})
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &HashMap::new()))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
let fn_ty = Type::Function(FnType {
params: param_types,
ret: Box::new(ret_ty),
effects: vec![],
});
ops.push((op_name.name.clone(), fn_ty));
}
}
self.effect_op_types.insert(name.name.clone(), ops);
let comp_names: Vec<String> = components.iter().map(type_path_to_name).collect();
if !comp_names.is_empty() {
self.effect_components.insert(name.name.clone(), comp_names);
}
}
NodeKind::RecordDecl {
name,
fields,
generic_params,
..
} => {
let record_name = name.name.clone();
let gp_names: Vec<String> =
generic_params.iter().map(|g| g.name.name.clone()).collect();
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|f| {
let ty = self.type_expr_to_type(&f.ty, &HashMap::new());
(f.name.name.clone(), ty)
})
.collect();
self.record_field_types
.insert(record_name.clone(), field_types);
if !gp_names.is_empty() {
self.record_generic_params
.insert(record_name.clone(), gp_names);
}
self.env.define(
record_name.clone(),
Type::Named(crate::NamedType { name: record_name }),
);
}
NodeKind::TypeAlias { name, ty, .. } => {
let underlying = self.air_type_node_to_type(ty, &HashMap::new());
self.type_aliases.insert(name.name.clone(), underlying);
}
NodeKind::ClassDecl {
name,
fields,
methods,
base,
generic_params,
..
} => {
let class_name = name.name.clone();
self.class_names.insert(class_name.clone());
let gp_names: Vec<String> =
generic_params.iter().map(|g| g.name.name.clone()).collect();
if !gp_names.is_empty() {
self.record_generic_params
.insert(class_name.clone(), gp_names);
}
let field_types: Vec<(String, Type)> = fields
.iter()
.map(|f| {
let ty = self.type_expr_to_type(&f.ty, &HashMap::new());
(f.name.name.clone(), ty)
})
.collect();
self.record_field_types
.insert(class_name.clone(), field_types);
let class_ty = Type::Named(crate::NamedType {
name: class_name.clone(),
});
self.env.define(class_name.clone(), class_ty.clone());
if let Some(base_path) = base {
let base_name = type_path_to_name(base_path);
if let Some(base_methods) = self.method_types.get(&base_name).cloned() {
self.method_types
.entry(class_name.clone())
.or_default()
.extend(base_methods);
}
}
for method in methods {
if let NodeKind::FnDecl {
name: method_name,
params,
return_type,
generic_params: method_gps,
..
} = &method.kind
{
let gp_map: HashMap<String, Type> = HashMap::new();
let method_gp_names: Vec<String> =
method_gps.iter().map(|g| g.name.name.clone()).collect();
if !method_gp_names.is_empty() {
self.method_generic_params
.entry(class_name.clone())
.or_default()
.insert(method_name.name.clone(), method_gp_names);
}
let param_types: Vec<Type> = params
.iter()
.map(|p| {
if let NodeKind::Param {
pattern, ty: None, ..
} = &p.kind
{
if let NodeKind::BindPat { name, .. } = &pattern.kind {
if name.name == "self" {
return class_ty.clone();
}
}
}
self.air_type_node_to_type(p, &gp_map)
})
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
let fn_ty = Type::Function(FnType {
params: param_types,
ret: Box::new(ret_ty),
effects: vec![],
});
self.method_types
.entry(class_name.clone())
.or_default()
.insert(method_name.name.clone(), fn_ty);
}
}
}
NodeKind::TraitDecl { name, methods, .. } => {
let trait_name = name.name.clone();
let self_ty = Type::Named(crate::NamedType {
name: "Self".to_string(),
});
let mut trait_methods = HashMap::new();
for method in methods {
if let NodeKind::FnDecl {
name: method_name,
params,
return_type,
..
} = &method.kind
{
let gp_map: HashMap<String, Type> = HashMap::new();
let param_types: Vec<Type> = params
.iter()
.map(|p| {
if let NodeKind::Param {
pattern, ty: None, ..
} = &p.kind
{
if let NodeKind::BindPat { name, .. } = &pattern.kind {
if name.name == "self" {
return self_ty.clone();
}
}
}
self.air_type_node_to_type(p, &gp_map)
})
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
let fn_ty = Type::Function(FnType {
params: param_types,
ret: Box::new(ret_ty),
effects: vec![],
});
trait_methods.insert(method_name.name.clone(), fn_ty);
}
}
if !trait_methods.is_empty() {
self.trait_method_types.insert(trait_name, trait_methods);
}
}
_ => {}
}
}
fn resolve_alias(&self, ty: &Type) -> Type {
match ty {
Type::Named(nt) => {
if let Some(underlying) = self.type_aliases.get(&nt.name) {
underlying.clone()
} else {
ty.clone()
}
}
_ => ty.clone(),
}
}
fn check_item(&mut self, node: &mut AIRNode) {
match &node.kind {
NodeKind::FnDecl { .. } => {
self.check_fn_decl(node);
}
NodeKind::ConstDecl { .. } => {
self.check_const_decl(node);
}
NodeKind::ImplBlock { .. } => {
self.check_impl_block(node);
}
NodeKind::ClassDecl { .. } => {
self.check_class_decl(node);
self.stamp_derive_structural_eq(node);
}
NodeKind::RecordDecl { .. } | NodeKind::EnumDecl { .. } => {
self.stamp_derive_structural_eq(node);
self.record(node, Type::Primitive(PrimitiveType::Void));
}
_ => {
self.record(node, Type::Primitive(PrimitiveType::Void));
}
}
}
fn stamp_derive_structural_eq(&mut self, node: &mut AIRNode) {
let is_class = matches!(&node.kind, NodeKind::ClassDecl { .. });
let name = match &node.kind {
NodeKind::RecordDecl { name, .. }
| NodeKind::EnumDecl { name, .. }
| NodeKind::ClassDecl { name, .. } => name.name.clone(),
_ => return,
};
let named = Type::Named(crate::NamedType { name });
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &named, table).is_some() {
node.metadata
.insert(CUSTOM_EQ_META_KEY.to_string(), Value::Bool(true));
return;
}
}
if is_class {
return;
}
let mut in_progress = HashSet::new();
let mut path = Vec::new();
if self
.structural_equatable_witness(&named, &mut in_progress, &mut path)
.is_none()
{
node.metadata
.insert(DERIVE_EQ_META_KEY.to_string(), Value::Bool(true));
}
}
fn check_impl_block(&mut self, node: &mut AIRNode) {
let (generic_params, target) = match &node.kind {
NodeKind::ImplBlock {
generic_params,
target,
..
} => (generic_params.clone(), target.clone()),
_ => return,
};
let target_name = match &target.kind {
NodeKind::TypeNamed { path, .. } => Some(type_path_to_name(path)),
_ => None,
};
let Some(target_name) = target_name else {
self.record(node, Type::Primitive(PrimitiveType::Void));
return;
};
let (impl_gp_map, target_ty) = self.build_impl_context(&generic_params, &target_name);
if let NodeKind::ImplBlock { methods, .. } = &mut node.kind {
let mut methods = std::mem::take(methods);
for method in methods.iter_mut() {
self.check_method_body(method, &target_ty, &impl_gp_map);
}
if let NodeKind::ImplBlock { methods: slot, .. } = &mut node.kind {
*slot = methods;
}
}
self.record(node, Type::Primitive(PrimitiveType::Void));
}
fn check_class_decl(&mut self, node: &mut AIRNode) {
let (generic_params, class_name) = match &node.kind {
NodeKind::ClassDecl {
generic_params,
name,
..
} => (generic_params.clone(), name.name.clone()),
_ => return,
};
let (impl_gp_map, target_ty) = self.build_impl_context(&generic_params, &class_name);
if let NodeKind::ClassDecl { methods, .. } = &mut node.kind {
let mut methods = std::mem::take(methods);
for method in methods.iter_mut() {
self.check_method_body(method, &target_ty, &impl_gp_map);
}
if let NodeKind::ClassDecl { methods: slot, .. } = &mut node.kind {
*slot = methods;
}
}
self.record(node, Type::Primitive(PrimitiveType::Void));
}
fn build_impl_context(
&mut self,
generic_params: &[GenericParam],
target_name: &str,
) -> (HashMap<String, Type>, Type) {
let mut gp_map: HashMap<String, Type> = generic_params
.iter()
.map(|g| (g.name.name.clone(), self.fresh_var()))
.collect();
for gp in generic_params {
if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.type_var_bounds
.entry(*id)
.or_default()
.extend(bound_names);
}
}
}
let target_ty = if generic_params.is_empty() {
Type::Named(crate::NamedType {
name: target_name.to_string(),
})
} else {
let args: Vec<Type> = generic_params
.iter()
.map(|g| gp_map[&g.name.name].clone())
.collect();
Type::Generic(GenericType {
constructor: target_name.to_string(),
args,
})
};
gp_map.insert("Self".to_string(), target_ty.clone());
(gp_map, target_ty)
}
fn check_method_body(
&mut self,
node: &mut AIRNode,
target_ty: &Type,
impl_gp_map: &HashMap<String, Type>,
) {
let (generic_params, params, return_type, effect_clause, where_clause) =
match node.kind.clone() {
NodeKind::FnDecl {
generic_params,
params,
return_type,
effect_clause,
where_clause,
..
} => (
generic_params,
params,
return_type,
effect_clause,
where_clause,
),
_ => return,
};
self.env.push_scope();
let mut gp_map = impl_gp_map.clone();
for gp in &generic_params {
gp_map.insert(gp.name.name.clone(), self.fresh_var());
}
for gp in &generic_params {
if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.type_var_bounds
.entry(*id)
.or_default()
.extend(bound_names);
}
}
}
for clause in &where_clause {
if let Some(Type::TypeVar(id)) = gp_map.get(&clause.param.name) {
let bound_names: Vec<String> =
clause.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.type_var_bounds
.entry(*id)
.or_default()
.extend(bound_names);
}
}
}
for p in ¶ms {
if let NodeKind::Param { pattern, ty, .. } = &p.kind {
if let NodeKind::BindPat { name, .. } = &pattern.kind {
if name.name == "self" && ty.is_none() {
self.env.define("self".to_string(), target_ty.clone());
continue;
}
let pty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
self.env.define(name.name.clone(), pty);
} else if let Some(pat_name) = p.kind.param_pat_name() {
let pty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
self.env.define(pat_name, pty);
}
}
}
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
{
let mut visited = std::collections::HashSet::new();
for effect_tp in &effect_clause {
let ename = type_path_to_name(effect_tp);
self.inject_effect_ops_into_env(&ename, &mut visited);
}
}
self.check_where_clause(&where_clause, &gp_map, node.span);
self.return_ty_stack.push(ret_ty.clone());
if let NodeKind::FnDecl { body, .. } = &mut node.kind {
self.check_node(body, &ret_ty);
}
self.return_ty_stack.pop();
self.env.pop_scope();
self.record(node, Type::Primitive(PrimitiveType::Void));
}
fn check_fn_decl(&mut self, node: &mut AIRNode) {
let (_name, generic_params, params, return_type, effect_clause, where_clause, _body) =
match node.kind.clone() {
NodeKind::FnDecl {
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
body,
..
} => (
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
body,
),
_ => return,
};
self.env.push_scope();
let gp_map: HashMap<String, Type> = generic_params
.iter()
.map(|g| (g.name.name.clone(), self.fresh_var()))
.collect();
for gp in &generic_params {
if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.type_var_bounds
.entry(*id)
.or_default()
.extend(bound_names);
}
}
}
for clause in &where_clause {
if let Some(Type::TypeVar(id)) = gp_map.get(&clause.param.name) {
let bound_names: Vec<String> =
clause.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.type_var_bounds
.entry(*id)
.or_default()
.extend(bound_names);
}
}
}
let saved_fn_param_bounds = std::mem::take(&mut self.current_fn_param_bounds);
for gp in &generic_params {
let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.current_fn_param_bounds
.entry(gp.name.name.clone())
.or_default()
.extend(bound_names);
}
}
for clause in &where_clause {
let bound_names: Vec<String> = clause.bounds.iter().map(type_path_to_name).collect();
if !bound_names.is_empty() {
self.current_fn_param_bounds
.entry(clause.param.name.clone())
.or_default()
.extend(bound_names);
}
}
let param_types: Vec<Type> = params
.iter()
.map(|p| {
let ty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
let pat_name = p.kind.param_pat_name();
if let Some(n) = pat_name {
self.env.define(n, ty.clone());
}
ty
})
.collect();
let ret_ty = return_type
.as_deref()
.map(|n| self.air_type_node_to_type(n, &gp_map))
.unwrap_or(Type::Primitive(PrimitiveType::Void));
{
let mut visited = std::collections::HashSet::new();
for effect_tp in &effect_clause {
let ename = type_path_to_name(effect_tp);
self.inject_effect_ops_into_env(&ename, &mut visited);
}
}
self.check_where_clause(&where_clause, &gp_map, node.span);
self.return_ty_stack.push(ret_ty.clone());
if let NodeKind::FnDecl { body, .. } = &mut node.kind {
self.check_node(body, &ret_ty);
}
self.return_ty_stack.pop();
self.env.pop_scope();
self.current_fn_param_bounds = saved_fn_param_bounds;
let effects: Vec<EffectRef> = effect_clause
.iter()
.map(|tp| {
let name = tp
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
EffectRef::new(name)
})
.collect();
let fn_ty = Type::Function(FnType {
params: param_types,
ret: Box::new(ret_ty),
effects,
});
self.record(node, fn_ty);
}
fn check_const_decl(&mut self, node: &mut AIRNode) {
let (name, ty_node, _value_node) = match node.kind.clone() {
NodeKind::ConstDecl {
name, ty, value, ..
} => (name, ty, value),
_ => return,
};
let expected_ty = self.air_type_node_to_type(&ty_node, &HashMap::new());
if let NodeKind::ConstDecl { value, .. } = &mut node.kind {
self.check_node(value, &expected_ty);
}
self.env.define(name.name, expected_ty.clone());
self.record(node, expected_ty);
}
fn check_where_clause(
&mut self,
clauses: &[TypeConstraint],
gp_map: &HashMap<String, Type>,
span: Span,
) {
for clause in clauses {
if !gp_map.contains_key(&clause.param.name) {
self.diags.error(
E_WHERE_CLAUSE,
format!(
"where-clause references unknown type parameter `{}`",
clause.param.name
),
span,
);
}
}
}
fn inject_effect_ops_into_env(
&mut self,
effect_name: &str,
visited: &mut std::collections::HashSet<String>,
) {
if !visited.insert(effect_name.to_string()) {
return;
}
if let Some(ops) = self.effect_op_types.get(effect_name).cloned() {
for (op_name, fn_ty) in ops {
self.env.define(op_name, fn_ty);
}
}
if let Some(components) = self.effect_components.get(effect_name).cloned() {
for comp in &components {
self.inject_effect_ops_into_env(comp, visited);
}
}
}
fn check_trait_bounds_at_call(
&mut self,
fn_name: &str,
sig: &FnSig,
fresh_map: &HashMap<TypeVarId, Type>,
span: Span,
) {
let impl_table = match &self.impl_table {
Some(t) => t,
None => return, };
let name_to_fresh: HashMap<&str, &Type> = sig
.generic_params
.iter()
.zip(sig.generic_var_ids.iter())
.filter_map(|(name, orig_id)| {
fresh_map
.get(orig_id)
.map(|fresh_ty| (name.as_str(), fresh_ty))
})
.collect();
for clause in &sig.where_clause {
let param_name = &clause.param.name;
let concrete_ty = match name_to_fresh.get(param_name.as_str()) {
Some(fresh) => self.subst.apply(fresh),
None => continue, };
for bound_path in &clause.bounds {
let trait_name = bound_path
.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
let trait_ref = TraitRef::new(&trait_name);
let concrete_key = crate::traits::type_key(&concrete_ty);
let satisfied = resolve_impl(&trait_ref, &concrete_ty, impl_table).is_some()
|| impl_table.has_any_param_trait_impl(&trait_name, &concrete_key)
|| self.abstract_param_satisfies_bound(&concrete_ty, &trait_name);
if !satisfied {
if trait_name == "Equatable" {
let mut in_progress = HashSet::new();
let mut path = Vec::new();
match self.structural_equatable_witness(
&concrete_ty,
&mut in_progress,
&mut path,
) {
None => continue,
Some(witness) => {
let (detail, suggestion) =
equatable_failure_wording(&concrete_key, &witness);
self.diags
.error(
E_NOT_EQUATABLE,
format!(
"type `{concrete_ty}` does not satisfy bound \
`Equatable` required by function `{fn_name}` \
— {detail}"
),
span,
)
.note(suggestion);
continue;
}
}
}
self.diags
.error(
E_WHERE_CLAUSE,
format!(
"type `{concrete_ty}` does not satisfy bound `{trait_name}` \
required by function `{fn_name}`",
),
span,
)
.note(format!(
"implement the trait for the type, e.g. `impl {trait_name} for \
{concrete_ty}`, or call `{fn_name}` with a conforming type"
));
}
}
}
}
fn abstract_param_satisfies_bound(&self, concrete_ty: &Type, trait_name: &str) -> bool {
match concrete_ty {
Type::Named(nt) => self
.current_fn_param_bounds
.get(&nt.name)
.is_some_and(|bounds| bounds.iter().any(|b| b == trait_name)),
Type::TypeVar(_) => true,
_ => false,
}
}
fn infer_node(&mut self, node: &mut AIRNode) -> Type {
let span = node.span;
let ty = match &node.kind {
NodeKind::Literal { lit } => self.infer_literal(lit),
NodeKind::Identifier { name } => {
let name = name.name.clone();
match self.env.lookup(&name) {
Some(ty) => {
let ty = ty.clone();
self.subst.apply(&ty)
}
None => {
if self.reported_undefined.insert((name.clone(), span)) {
self.diags.error(
E_UNDEFINED_VAR,
format!("undefined variable `{name}`"),
span,
);
}
Type::Error
}
}
}
NodeKind::BinaryOp { op, .. } => {
let op = *op;
let (lt, rt) = if let NodeKind::BinaryOp { left, right, .. } = &mut node.kind {
let lt = self.infer_node(left);
let rt = self.infer_node(right);
(lt, rt)
} else {
unreachable!()
};
let result = self.infer_binop(op, <, &rt, span);
if matches!(op, BinOp::Add) {
let is_list = |t: &Type| matches!(self.subst.apply(t), Type::Generic(g) if g.constructor == "List");
if is_list(&result) || is_list(<) || is_list(&rt) {
node.metadata
.insert(LIST_CONCAT_META_KEY.to_string(), Value::Bool(true));
}
let is_string = |t: &Type| {
matches!(self.subst.apply(t), Type::Primitive(PrimitiveType::String))
};
if is_string(&result) || is_string(<) || is_string(&rt) {
node.metadata
.insert(STRING_CONCAT_META_KEY.to_string(), Value::Bool(true));
}
}
if matches!(op, BinOp::Div | BinOp::Rem) {
let is_int = |t: &Type| {
matches!(
self.subst.apply(t),
Type::Primitive(
PrimitiveType::Int
| PrimitiveType::Int8
| PrimitiveType::Int16
| PrimitiveType::Int32
| PrimitiveType::Int64
| PrimitiveType::Int128
| PrimitiveType::UInt8
| PrimitiveType::UInt16
| PrimitiveType::UInt32
| PrimitiveType::UInt64
)
)
};
if is_int(<) && is_int(&rt) {
node.metadata
.insert(INT_ARITH_META_KEY.to_string(), Value::Bool(true));
}
}
if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge) {
let probe = match self.subst.apply(<) {
Type::TypeVar(_) => &rt,
_ => <,
};
if self.is_user_comparable(probe) {
node.metadata
.insert(USER_COMPARE_META_KEY.to_string(), Value::Bool(true));
}
}
if matches!(op, BinOp::Eq | BinOp::Ne) {
let probe = match self.subst.apply(<) {
Type::TypeVar(_) => &rt,
_ => <,
};
if let Some(kind) = self.user_eq_kind(probe) {
node.metadata.insert(
USER_EQ_META_KEY.to_string(),
Value::String(kind.to_string()),
);
}
}
result
}
NodeKind::UnaryOp { op, .. } => {
let op = *op;
let operand_ty = if let NodeKind::UnaryOp { operand, .. } = &mut node.kind {
self.infer_node(operand)
} else {
unreachable!()
};
self.infer_unop(op, &operand_ty, span)
}
NodeKind::FieldAccess { field, .. } => {
let field_name = field.name.clone();
if field_name == "handler" {
if let NodeKind::FieldAccess { object, .. } = &node.kind {
if let NodeKind::Identifier { name } = &object.kind {
let effect_name = name.name.clone();
if self.env.lookup(&effect_name).is_none()
&& (self.effect_op_types.contains_key(&effect_name)
|| self.effect_components.contains_key(&effect_name))
{
self.reported_undefined
.insert((effect_name.clone(), object.span));
self.diags
.error(
E_RESERVED_LAMBDA_HANDLER,
format!(
"the lambda-handler form `{effect_name}.handler(...)` is reserved until v1.x"
),
span,
)
.note(format!(
"v1 supports one handler form: declare a record, `impl {effect_name} for <Record>`, then install it with `handle {effect_name} with <record>` (module level) or `handling ({effect_name} with <record>) {{ ... }}` (block level)"
));
return self.record(node, Type::Error);
}
}
}
}
let obj_ty = if let NodeKind::FieldAccess { object, .. } = &mut node.kind {
self.infer_node(object)
} else {
unreachable!()
};
let obj_ty = self.subst.apply(&obj_ty);
match &obj_ty {
Type::Error => Type::Error,
Type::Named(nt) => {
if let Some(fields) = self.record_field_types.get(&nt.name) {
if let Some((_, field_ty)) =
fields.iter().find(|(n, _)| n == &field_name)
{
return self.record(node, field_ty.clone());
}
}
if let Some(fn_ty) = self
.method_types
.get(&nt.name)
.and_then(|methods| methods.get(&field_name))
.cloned()
{
let resolved =
self.freshen_method_type_params(&nt.name, &field_name, fn_ty);
return self.record(node, resolved);
}
self.fresh_var()
}
Type::Generic(g) => {
if let Some(fields) = self.record_field_types.get(&g.constructor) {
if let Some((_, field_ty)) =
fields.iter().find(|(n, _)| n == &field_name)
{
let resolved = if let Some(params) =
self.record_generic_params.get(&g.constructor)
{
substitute_type_params(field_ty, params, &g.args)
} else {
field_ty.clone()
};
return self.record(node, resolved);
}
}
if let Some(fn_ty) = self
.method_types
.get(&g.constructor)
.and_then(|methods| methods.get(&field_name))
.cloned()
{
let resolved = if let Some(params) =
self.record_generic_params.get(&g.constructor)
{
substitute_type_params(&fn_ty, params, &g.args)
} else {
fn_ty
};
let resolved = self.freshen_method_type_params(
&g.constructor,
&field_name,
resolved,
);
return self.record(node, resolved);
}
if let Some(fn_ty) =
self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
{
fn_ty
} else {
self.fresh_var()
}
}
Type::TypeVar(id) => {
if let Some(bounds) = self.type_var_bounds.get(id).cloned() {
let self_params = vec!["Self".to_string()];
let self_args = vec![obj_ty.clone()];
for trait_name in &bounds {
if let Some(methods) =
self.trait_method_types.get(trait_name).cloned()
{
if let Some(fn_ty) = methods.get(&field_name) {
let resolved =
substitute_type_params(fn_ty, &self_params, &self_args);
return self.record(node, resolved);
}
}
}
}
if let Some(fn_ty) =
self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
{
fn_ty
} else {
self.fresh_var()
}
}
Type::Primitive(_) => {
if let Some(fn_ty) =
self.resolve_primitive_canonical_method_fn_type(&obj_ty, &field_name)
{
fn_ty
} else if let Some(fn_ty) =
self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
{
fn_ty
} else {
self.fresh_var()
}
}
_ => {
if let Some(fn_ty) =
self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
{
fn_ty
} else {
self.fresh_var()
}
}
}
}
NodeKind::Index { .. } => {
let (obj_ty, idx_ty) = if let NodeKind::Index { object, index } = &mut node.kind {
let o = self.infer_node(object);
let i = self.infer_node(index);
(o, i)
} else {
unreachable!()
};
self.unify_or_error(&idx_ty, &Type::Primitive(PrimitiveType::Int), span, "index");
match &obj_ty {
Type::Error => Type::Error,
Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
g.args[0].clone()
}
_ => self.fresh_var(),
}
}
NodeKind::Call { .. } => {
if let Some(result_ty) = self.try_resolve_primitive_conversion_call(node) {
return self.record(node, result_ty);
}
let (callee_clone, args_clone, _type_args_clone) = if let NodeKind::Call {
callee,
args,
type_args,
} = &node.kind
{
(*callee.clone(), args.clone(), type_args.clone())
} else {
unreachable!()
};
let callee_name = if let NodeKind::Identifier { name } = &callee_clone.kind {
Some(name.name.clone())
} else {
None
};
let mut callee_ty = if let NodeKind::Call { callee, .. } = &mut node.kind {
self.infer_node(callee)
} else {
unreachable!()
};
if let NodeKind::FieldAccess { object, field, .. } = &callee_clone.kind {
if let Some(recv_ty) = self.types.get(&object.id).cloned() {
self.stamp_recv_kind(node, &recv_ty);
let recv_ty = self.subst.apply(&recv_ty);
let map_contains = field.name == "contains"
&& matches!(&recv_ty, Type::Generic(g)
if g.constructor == "Map" && g.args.len() == 2);
if map_contains {
self.diags
.error(
E_NO_SUCH_METHOD,
"`contains` is not a method on `Map`; \
did you mean `contains_key`?",
field.span,
)
.note(
"use `contains_key(k)` to test for a key \
or `contains_value(v)` for a value; bare \
`contains` is a `Set` method",
);
} else {
self.check_unknown_method_on_concrete(
&recv_ty,
&field.name,
field.span,
);
}
if !matches!(callee_ty, Type::Function(_)) {
if let Some(fn_ty) =
self.resolve_user_method_fn_type(&recv_ty, &field.name)
{
callee_ty = fn_ty;
}
}
}
}
let mut call_site_info: Option<(String, FnSig, HashMap<TypeVarId, Type>)> = None;
let effective_ty = match (&callee_name, &callee_ty) {
(Some(name), Type::Function(f)) => {
if let Some(sig) = self.fn_sigs.get(name).cloned() {
if !sig.generic_params.is_empty() {
let fresh_map: HashMap<TypeVarId, Type> = sig
.generic_var_ids
.iter()
.map(|&id| (id, self.fresh_var()))
.collect();
let ety = Type::Function(FnType {
params: f
.params
.iter()
.map(|t| self.replace_type_vars(t, &fresh_map))
.collect(),
ret: Box::new(self.replace_type_vars(&f.ret, &fresh_map)),
effects: f.effects.clone(),
});
call_site_info = Some((name.clone(), sig, fresh_map));
ety
} else {
callee_ty.clone()
}
} else {
callee_ty.clone()
}
}
_ => callee_ty.clone(),
};
let ret_ty = self.check_call(callee_clone.span, &effective_ty, &args_clone, span);
if let NodeKind::Call { args, .. } = &mut node.kind {
match &effective_ty {
Type::Function(f) => {
for (arg, param_ty) in args.iter_mut().zip(f.params.iter()) {
let pt = self.subst.apply(param_ty);
self.check_node(&mut arg.value, &pt);
}
}
_ => {
for arg in args.iter_mut() {
self.infer_node(&mut arg.value);
}
}
}
}
if let Some((fn_name, sig, fresh_map)) = &call_site_info {
self.check_trait_bounds_at_call(fn_name, sig, fresh_map, span);
}
ret_ty
}
NodeKind::MethodCall { method, .. } => {
let method_name = method.name.clone();
let method_span = method.span;
let receiver_ty =
if let NodeKind::MethodCall { receiver, args, .. } = &mut node.kind {
let rt = self.infer_node(receiver);
for arg in args.iter_mut() {
self.infer_node(&mut arg.value);
}
rt
} else {
unreachable!()
};
self.stamp_recv_kind(node, &receiver_ty);
self.check_unknown_method_on_concrete(&receiver_ty, &method_name, method_span);
self.resolve_method_return_type(&receiver_ty, &method_name)
}
NodeKind::Lambda { .. } => {
let (param_tys, body_ty) = self.infer_lambda(node);
Type::Function(FnType {
params: param_tys,
ret: Box::new(body_ty),
effects: vec![],
})
}
NodeKind::Pipe { .. } => {
let (lty, rty) = if let NodeKind::Pipe { left, right } = &mut node.kind {
let l = self.infer_node(left);
let r = self.infer_node(right);
(l, r)
} else {
unreachable!()
};
match &rty {
Type::Function(f) if f.params.len() == 1 => {
let param_ty = self.subst.apply(&f.params[0]);
self.unify_or_error(<y, ¶m_ty, span, "pipe");
self.subst.apply(&f.ret)
}
Type::Error => Type::Error,
_ => self.fresh_var(),
}
}
NodeKind::If { .. } => self.infer_if(node),
NodeKind::Match { .. } => self.infer_match(node),
NodeKind::Block { .. } => self.infer_block(node),
NodeKind::LetBinding { .. } => {
self.check_let_binding(node);
Type::Primitive(PrimitiveType::Void)
}
NodeKind::Return { .. } => {
let expected = self.return_ty_stack.last().cloned();
if let NodeKind::Return { value } = &mut node.kind {
match (value, &expected) {
(Some(v), Some(e)) => {
let et = e.clone();
self.check_node(v, &et);
}
(Some(v), None) => {
self.infer_node(v);
}
_ => {}
}
}
Type::Primitive(PrimitiveType::Never)
}
NodeKind::ListLiteral { .. } => {
let elem_ty = self.fresh_var();
if let NodeKind::ListLiteral { elems } = &mut node.kind {
for elem in elems.iter_mut() {
let et = elem_ty.clone();
self.check_node(elem, &et);
}
}
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![self.subst.apply(&elem_ty)],
})
}
NodeKind::TupleLiteral { .. } => {
let elem_tys: Vec<Type> = if let NodeKind::TupleLiteral { elems } = &mut node.kind {
elems.iter_mut().map(|e| self.infer_node(e)).collect()
} else {
vec![]
};
Type::Tuple(elem_tys)
}
NodeKind::MapLiteral { .. } => {
let k_ty = self.fresh_var();
let v_ty = self.fresh_var();
if let NodeKind::MapLiteral { entries } = &mut node.kind {
for entry in entries.iter_mut() {
let kt = k_ty.clone();
let vt = v_ty.clone();
self.check_node(&mut entry.key, &kt);
self.check_node(&mut entry.value, &vt);
}
}
Type::Generic(GenericType {
constructor: "Map".into(),
args: vec![self.subst.apply(&k_ty), self.subst.apply(&v_ty)],
})
}
NodeKind::SetLiteral { .. } => {
let elem_ty = self.fresh_var();
if let NodeKind::SetLiteral { elems } = &mut node.kind {
for elem in elems.iter_mut() {
let et = elem_ty.clone();
self.check_node(elem, &et);
}
}
Type::Generic(GenericType {
constructor: "Set".into(),
args: vec![self.subst.apply(&elem_ty)],
})
}
NodeKind::Interpolation { .. } => {
if let NodeKind::Interpolation { parts } = &mut node.kind {
for part in parts.iter_mut() {
if let bock_air::AirInterpolationPart::Expr(e) = part {
let part_ty = self.infer_node(e);
if matches!(
self.subst.apply(&part_ty),
Type::Primitive(PrimitiveType::Bool)
) {
e.metadata
.insert(BOOL_STRINGIFY_META_KEY.to_string(), Value::Bool(true));
}
}
}
}
Type::Primitive(PrimitiveType::String)
}
NodeKind::ResultConstruct { variant, .. } => {
let variant = *variant;
let has_value =
matches!(&node.kind, NodeKind::ResultConstruct { value: Some(_), .. });
let inner_ty = if has_value {
if let NodeKind::ResultConstruct { value: Some(v), .. } = &mut node.kind {
self.infer_node(v)
} else {
unreachable!()
}
} else {
Type::Primitive(PrimitiveType::Void)
};
let err_ty = self.fresh_var();
let ok_ty = self.fresh_var();
match variant {
bock_air::ResultVariant::Ok => {
self.unify_or_error(&inner_ty, &ok_ty, span, "Ok construct");
Type::Result(Box::new(ok_ty), Box::new(err_ty))
}
bock_air::ResultVariant::Err => {
self.unify_or_error(&inner_ty, &err_ty, span, "Err construct");
Type::Result(Box::new(ok_ty), Box::new(err_ty))
}
}
}
NodeKind::Propagate { .. } => {
let inner_ty = if let NodeKind::Propagate { expr } = &mut node.kind {
self.infer_node(expr)
} else {
unreachable!()
};
match &inner_ty {
Type::Result(ok, _) => *ok.clone(),
Type::Optional(inner) => *inner.clone(),
Type::Error => Type::Error,
_ => self.fresh_var(),
}
}
NodeKind::Await { .. } => {
if let NodeKind::Await { expr } = &mut node.kind {
self.infer_node(expr);
}
self.fresh_var()
}
NodeKind::Borrow { .. } | NodeKind::MutableBorrow { .. } => {
match &mut node.kind {
NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } => {
self.infer_node(expr)
}
_ => unreachable!(),
}
}
NodeKind::Move { .. } => {
if let NodeKind::Move { expr } = &mut node.kind {
self.infer_node(expr)
} else {
unreachable!()
}
}
NodeKind::Assign { .. } => {
let (tty, vty) = if let NodeKind::Assign { target, value, .. } = &mut node.kind {
let t = self.infer_node(target);
let v = self.infer_node(value);
(t, v)
} else {
unreachable!()
};
self.unify_or_error(&vty, &tty, span, "assignment");
Type::Primitive(PrimitiveType::Void)
}
NodeKind::Range { .. } => {
let (lty, hty) = if let NodeKind::Range { lo, hi, .. } = &mut node.kind {
let l = self.infer_node(lo);
let h = self.infer_node(hi);
(l, h)
} else {
unreachable!()
};
self.unify_or_error(&hty, <y, span, "range bounds");
Type::Generic(GenericType {
constructor: "Range".into(),
args: vec![lty],
})
}
NodeKind::For { .. } => {
let node_span = node.span;
let iter_ty = if let NodeKind::For { iterable, .. } = &mut node.kind {
self.infer_node(iterable)
} else {
unreachable!()
};
let resolved_iter_ty = self.subst.apply(&iter_ty);
let is_builtin_iterable = matches!(
&resolved_iter_ty,
Type::Generic(g)
if matches!(g.constructor.as_str(), "List" | "Range" | "Map" | "Set")
);
if !is_builtin_iterable {
let implements_iterable = self
.impl_table
.as_ref()
.map(|table| {
let key = crate::traits::type_key(&resolved_iter_ty);
resolve_impl(&TraitRef::new("Iterable"), &resolved_iter_ty, table)
.is_some()
|| table.has_any_param_trait_impl("Iterable", &key)
})
.unwrap_or(false);
if implements_iterable {
let (pattern, iterable, body) = if let NodeKind::For {
pattern,
iterable,
body,
} = &mut node.kind
{
(
std::mem::replace(
pattern,
Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
),
std::mem::replace(
iterable,
Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
),
std::mem::replace(
body,
Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
),
)
} else {
unreachable!()
};
let n = self.synth_iter_var.get();
self.synth_iter_var.set(n.wrapping_add(1));
let iter_var = format!("__bock_iter_{n}");
self.desugar_for_iterable(node, *pattern, *iterable, *body, &iter_var);
return self.infer_block(node);
}
}
self.env.push_scope();
if let NodeKind::For {
pattern,
iterable: _,
body,
} = &mut node.kind
{
let elem_ty = match &resolved_iter_ty {
Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
g.args[0].clone()
}
Type::Generic(g) if g.constructor == "Range" && g.args.len() == 1 => {
g.args[0].clone()
}
_ => self.fresh_var(),
};
self.bind_pattern_type(pattern, &elem_ty);
self.infer_node(body);
}
self.env.pop_scope();
Type::Primitive(PrimitiveType::Void)
}
NodeKind::While { .. } => {
if let NodeKind::While { condition, body } = &mut node.kind {
let bool_ty = Type::Primitive(PrimitiveType::Bool);
self.check_node(condition, &bool_ty);
self.infer_node(body);
}
Type::Primitive(PrimitiveType::Void)
}
NodeKind::Loop { .. } => {
if let NodeKind::Loop { body } = &mut node.kind {
self.infer_node(body);
}
self.fresh_var()
}
NodeKind::Break { .. } => {
if let NodeKind::Break { value: Some(v) } = &mut node.kind {
self.infer_node(v);
}
Type::Primitive(PrimitiveType::Never)
}
NodeKind::Continue => Type::Primitive(PrimitiveType::Never),
NodeKind::Guard { .. } => {
if let NodeKind::Guard {
let_pattern,
condition,
else_block,
} = &mut node.kind
{
if let_pattern.is_some() {
let cond_ty = self.infer_node(condition);
if let Some(pat) = let_pattern {
self.bind_pattern_type(pat, &cond_ty);
}
} else {
let bool_ty = Type::Primitive(PrimitiveType::Bool);
self.check_node(condition, &bool_ty);
}
self.infer_node(else_block);
}
Type::Primitive(PrimitiveType::Void)
}
NodeKind::Compose { .. } => {
if let NodeKind::Compose { left, right } = &mut node.kind {
self.infer_node(left);
self.infer_node(right);
}
self.fresh_var() }
NodeKind::Placeholder => self.fresh_var(),
NodeKind::Unreachable => Type::Primitive(PrimitiveType::Never),
NodeKind::HandlingBlock { .. } => {
if let NodeKind::HandlingBlock { handlers, body } = &mut node.kind {
for hp in handlers.iter_mut() {
self.infer_node(&mut hp.handler);
}
let effect_names: Vec<String> = handlers
.iter()
.map(|hp| type_path_to_name(&hp.effect))
.collect();
self.env.push_scope();
let mut visited = std::collections::HashSet::new();
for ename in &effect_names {
self.inject_effect_ops_into_env(ename, &mut visited);
}
let ty = self.infer_node(body);
self.env.pop_scope();
ty
} else {
unreachable!()
}
}
NodeKind::RecordConstruct { path, .. } => {
let name = path
.segments
.last()
.map(|s| s.name.clone())
.unwrap_or_default();
let generic_params = self.record_generic_params.get(&name).cloned();
let fresh_type_args: Option<Vec<Type>> = generic_params
.as_ref()
.map(|params| params.iter().map(|_| self.fresh_var()).collect());
if let NodeKind::RecordConstruct { fields, spread, .. } = &mut node.kind {
let declared_fields = self.record_field_types.get(&name).cloned();
for f in fields.iter_mut() {
if let Some(v) = &mut f.value {
if let Some(ref decl) = declared_fields {
if let Some((_, expected_ty)) =
decl.iter().find(|(n, _)| n == &f.name.name)
{
let et = if let (Some(ref params), Some(ref args)) =
(&generic_params, &fresh_type_args)
{
substitute_type_params(expected_ty, params, args)
} else {
expected_ty.clone()
};
self.check_node(v, &et);
} else {
self.infer_node(v);
}
} else {
self.infer_node(v);
}
}
}
if let Some(s) = spread {
self.infer_node(s);
}
}
if let Some(type_args) = fresh_type_args {
Type::Generic(GenericType {
constructor: name,
args: type_args,
})
} else {
self.env
.lookup(&name)
.cloned()
.unwrap_or(Type::Named(crate::NamedType { name }))
}
}
NodeKind::Error => Type::Error,
_ => self.fresh_var(),
};
self.record(node, ty)
}
fn check_node(&mut self, node: &mut AIRNode, expected: &Type) {
let span = node.span;
match &node.kind {
NodeKind::ListLiteral { .. } => {
if let Type::Generic(g) = expected {
if g.constructor == "List" && g.args.len() == 1 {
let elem_ty = g.args[0].clone();
if let NodeKind::ListLiteral { elems } = &mut node.kind {
for elem in elems.iter_mut() {
let et = elem_ty.clone();
self.check_node(elem, &et);
}
}
self.record(node, expected.clone());
return;
}
}
let inferred = self.infer_node(node);
self.unify_or_error(&inferred, expected, span, "list literal");
}
NodeKind::Lambda { .. } => {
if let Type::Function(f_expected) = expected {
let param_types = f_expected.params.clone();
let ret_ty = *f_expected.ret.clone();
self.env.push_scope();
if let NodeKind::Lambda { params, body } = &mut node.kind {
for (param, pty) in params.iter_mut().zip(param_types.iter()) {
if let Some(name) = param.kind.param_pat_name() {
self.env.define(name, pty.clone());
}
self.record(param, pty.clone());
}
self.check_node(body, &ret_ty);
}
self.env.pop_scope();
self.record(node, expected.clone());
} else {
let inferred = self.infer_node(node);
self.unify_or_error(&inferred, expected, span, "lambda");
}
}
NodeKind::Match { .. } => {
let scrutinee_ty = if let NodeKind::Match { scrutinee, .. } = &mut node.kind {
self.infer_node(scrutinee)
} else {
unreachable!()
};
if let NodeKind::Match { arms, .. } = &mut node.kind {
for arm in arms.iter_mut() {
self.env.push_scope();
if let NodeKind::MatchArm {
pattern,
guard,
body,
} = &mut arm.kind
{
self.bind_pattern_type(pattern, &scrutinee_ty.clone());
if let Some(g) = guard {
let bt = Type::Primitive(PrimitiveType::Bool);
self.check_node(g, &bt);
}
let et = expected.clone();
self.check_node(body, &et);
}
self.env.pop_scope();
self.record(arm, expected.clone());
}
}
self.record(node, expected.clone());
}
NodeKind::If { .. } => {
if let NodeKind::If {
condition,
then_block,
else_block,
..
} = &mut node.kind
{
let bt = Type::Primitive(PrimitiveType::Bool);
self.check_node(condition, &bt);
let et = expected.clone();
self.check_node(then_block, &et);
if let Some(eb) = else_block {
let et2 = expected.clone();
self.check_node(eb, &et2);
}
}
self.record(node, expected.clone());
}
NodeKind::Block { .. } => {
if let NodeKind::Block { stmts, tail } = &mut node.kind {
self.env.push_scope();
for stmt in stmts.iter_mut() {
self.infer_node(stmt);
}
if let Some(tail_expr) = tail {
let et = expected.clone();
self.check_node(tail_expr, &et);
} else {
let void_ty = Type::Primitive(PrimitiveType::Void);
self.unify_or_error(&void_ty, expected, node.span, "block");
}
self.env.pop_scope();
}
self.record(node, expected.clone());
}
NodeKind::Call { callee, args, .. }
if args.len() == 1
&& matches!(
&callee.kind,
NodeKind::FieldAccess { field, .. } if field.name == "into"
) =>
{
let target = self.subst.apply(expected);
let receiver_ty = if let NodeKind::Call { args, .. } = &mut node.kind {
self.infer_node(&mut args[0].value)
} else {
unreachable!()
};
let receiver_ty = self.subst.apply(&receiver_ty);
let resolvable = !matches!(target, Type::TypeVar(_) | Type::Error)
&& !matches!(receiver_ty, Type::TypeVar(_) | Type::Error);
if resolvable {
if let Some(table) = self.impl_table.as_ref() {
let trait_ref = TraitRef::parameterized("Into", vec![target.clone()]);
if resolve_impl(&trait_ref, &receiver_ty, table).is_some() {
self.record(node, target.clone());
return;
}
self.diags.error(
E_NO_CONVERSION,
format!(
"cannot convert `{}` into `{}` via `.into()`: no `From`/`Into` impl relates these types",
crate::traits::type_key(&receiver_ty),
crate::traits::type_key(&target),
),
span,
);
self.record(node, target.clone());
return;
}
}
let inferred = self.infer_node(node);
let expected = self.subst.apply(expected);
self.unify_or_error(&inferred, &expected, span, "expression");
}
_ => {
let inferred = self.infer_node(node);
let expected = self.subst.apply(expected);
self.unify_or_error(&inferred, &expected, span, "expression");
}
}
}
fn infer_if(&mut self, node: &mut AIRNode) -> Type {
let span = node.span;
if let NodeKind::If {
condition,
then_block,
else_block,
..
} = &mut node.kind
{
let bool_ty = Type::Primitive(PrimitiveType::Bool);
self.check_node(condition, &bool_ty);
let then_ty = self.infer_node(then_block);
if let Some(eb) = else_block {
let else_ty = self.infer_node(eb);
let never = Type::Primitive(PrimitiveType::Never);
let (a, b) = if then_ty == never {
(&else_ty, &then_ty)
} else {
(&then_ty, &else_ty)
};
self.unify_or_error(b, a, span, "if-else branches")
} else {
Type::Primitive(PrimitiveType::Void)
}
} else {
unreachable!()
}
}
fn infer_match(&mut self, node: &mut AIRNode) -> Type {
let span = node.span;
let never = Type::Primitive(PrimitiveType::Never);
let scrutinee_ty = if let NodeKind::Match { scrutinee, .. } = &mut node.kind {
self.infer_node(scrutinee)
} else {
unreachable!()
};
let mut arm_types: Vec<Type> = Vec::new();
if let NodeKind::Match { arms, .. } = &mut node.kind {
for arm in arms.iter_mut() {
self.env.push_scope();
let arm_ty = if let NodeKind::MatchArm {
pattern,
guard,
body,
} = &mut arm.kind
{
self.bind_pattern_type(pattern, &scrutinee_ty.clone());
if let Some(g) = guard {
let bt = Type::Primitive(PrimitiveType::Bool);
self.check_node(g, &bt);
}
self.infer_node(body)
} else {
self.fresh_var()
};
self.env.pop_scope();
self.record(arm, arm_ty.clone());
arm_types.push(arm_ty);
}
}
let non_never: Vec<&Type> = arm_types.iter().filter(|t| **t != never).collect();
if non_never.is_empty() {
never
} else {
let result_ty = self.fresh_var();
for t in &non_never {
let rt = result_ty.clone();
self.unify_or_error(t, &rt, span, "match arm");
}
self.subst.apply(&result_ty)
}
}
fn infer_block(&mut self, node: &mut AIRNode) -> Type {
self.env.push_scope();
let ty = if let NodeKind::Block { stmts, tail } = &mut node.kind {
for stmt in stmts.iter_mut() {
self.infer_node(stmt);
}
if let Some(tail_expr) = tail {
self.infer_node(tail_expr)
} else {
Type::Primitive(PrimitiveType::Void)
}
} else {
unreachable!()
};
self.env.pop_scope();
ty
}
fn check_let_binding(&mut self, node: &mut AIRNode) {
let (ty_node, _value_clone) = match &node.kind {
NodeKind::LetBinding { ty, value, .. } => (ty.clone(), *value.clone()),
_ => return,
};
if let Some(ty_ann) = &ty_node {
let expected = self.air_type_node_to_type(ty_ann, &HashMap::new());
if let NodeKind::LetBinding { value, pattern, .. } = &mut node.kind {
self.check_node(value, &expected);
self.bind_pattern_type(pattern, &expected);
}
} else {
let inferred = if let NodeKind::LetBinding { value, .. } = &mut node.kind {
self.infer_node(value)
} else {
unreachable!()
};
let resolved = self.subst.apply(&inferred);
if let NodeKind::LetBinding { pattern, .. } = &mut node.kind {
self.bind_pattern_type(pattern, &resolved);
}
}
}
fn infer_lambda(&mut self, node: &mut AIRNode) -> (Vec<Type>, Type) {
self.env.push_scope();
let (param_tys, body_ty) = if let NodeKind::Lambda { params, body } = &mut node.kind {
let param_tys: Vec<Type> = params
.iter_mut()
.map(|p| {
let ty = self.fresh_var();
if let Some(name) = p.kind.param_pat_name() {
self.env.define(name, ty.clone());
}
ty
})
.collect();
let body_ty = self.infer_node(body);
(param_tys, body_ty)
} else {
unreachable!()
};
self.env.pop_scope();
(param_tys, body_ty)
}
fn check_call(
&mut self,
callee_span: Span,
callee_ty: &Type,
args: &[bock_air::AirArg],
call_span: Span,
) -> Type {
match callee_ty {
Type::Error => Type::Error,
Type::Function(f) => {
if f.params.len() != args.len() {
self.diags.error(
E_ARITY_MISMATCH,
format!(
"function expects {} argument(s), got {}",
f.params.len(),
args.len()
),
call_span,
);
return Type::Error;
}
self.subst.apply(&f.ret)
}
_ => {
if let Type::Named(nt) = callee_ty {
if let Some(sig) = self.fn_sigs.get(&nt.name).cloned() {
return self.instantiate_and_check(&nt.name, &sig, args, call_span);
}
}
if matches!(callee_ty, Type::TypeVar(_)) {
return self.fresh_var();
}
self.diags.error(
E_NOT_CALLABLE,
format!("expected a function type, got {callee_ty:?}"),
callee_span,
);
Type::Error
}
}
}
fn instantiate_and_check(
&mut self,
fn_name: &str,
sig: &FnSig,
args: &[bock_air::AirArg],
span: Span,
) -> Type {
if sig.param_types.len() != args.len() {
self.diags.error(
E_ARITY_MISMATCH,
format!(
"function expects {} argument(s), got {}",
sig.param_types.len(),
args.len()
),
span,
);
return Type::Error;
}
let fresh_map: HashMap<TypeVarId, Type> = sig
.generic_var_ids
.iter()
.map(|&id| (id, self.fresh_var()))
.collect();
let _param_tys: Vec<Type> = sig
.param_types
.iter()
.map(|t| self.replace_type_vars(t, &fresh_map))
.collect();
self.check_trait_bounds_at_call(fn_name, sig, &fresh_map, span);
self.replace_type_vars(&sig.return_type, &fresh_map)
}
fn resolve_primitive_canonical_method_return(
&self,
receiver_ty: &Type,
method: &str,
) -> Option<Type> {
let impl_table = self.impl_table.as_ref()?;
for (trait_name, methods) in &self.trait_method_types {
let Some(Type::Function(fn_ty)) = methods.get(method) else {
continue;
};
let trait_ref = TraitRef::new(trait_name);
if resolve_impl(&trait_ref, receiver_ty, impl_table).is_none() {
continue;
}
let self_params = ["Self".to_string()];
let self_args = [receiver_ty.clone()];
return Some(substitute_type_params(&fn_ty.ret, &self_params, &self_args));
}
None
}
fn resolve_primitive_canonical_method_fn_type(
&self,
receiver_ty: &Type,
method: &str,
) -> Option<Type> {
let impl_table = self.impl_table.as_ref()?;
for (trait_name, methods) in &self.trait_method_types {
let Some(fn_ty @ Type::Function(_)) = methods.get(method) else {
continue;
};
let trait_ref = TraitRef::new(trait_name);
if resolve_impl(&trait_ref, receiver_ty, impl_table).is_none() {
continue;
}
let self_params = ["Self".to_string()];
let self_args = [receiver_ty.clone()];
return Some(substitute_type_params(fn_ty, &self_params, &self_args));
}
None
}
fn resolve_user_method_fn_type(&self, receiver_ty: &Type, method: &str) -> Option<Type> {
let receiver_ty = self.subst.apply(receiver_ty);
let (type_name, fn_ty) = match &receiver_ty {
Type::Named(nt) => {
let fn_ty = self
.method_types
.get(&nt.name)
.and_then(|m| m.get(method))
.cloned()?;
(nt.name.clone(), fn_ty)
}
Type::Generic(g) => {
let fn_ty = self
.method_types
.get(&g.constructor)
.and_then(|m| m.get(method))
.cloned()?;
let fn_ty = if let Some(params) = self.record_generic_params.get(&g.constructor) {
substitute_type_params(&fn_ty, params, &g.args)
} else {
fn_ty
};
(g.constructor.clone(), fn_ty)
}
_ => return None,
};
Some(self.freshen_method_type_params(&type_name, method, fn_ty))
}
fn try_resolve_primitive_conversion_call(&mut self, node: &mut AIRNode) -> Option<Type> {
if !is_associated_call_node(node) {
return None;
}
let (target_prim, method, method_span) = {
let NodeKind::Call { callee, .. } = &node.kind else {
return None;
};
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return None;
};
let NodeKind::Identifier { name } = &object.kind else {
return None;
};
let prim = name_to_primitive(&name.name)?;
let method = field.name.clone();
if method != "from" && method != "try_from" {
return None;
}
(prim, method, field.span)
};
let target_ty = Type::Primitive(target_prim);
let arg_ty = {
let NodeKind::Call { args, .. } = &mut node.kind else {
return None;
};
if args.len() != 1 {
return None;
}
self.infer_node(&mut args[0].value)
};
let arg_ty = self.subst.apply(&arg_ty);
if matches!(arg_ty, Type::TypeVar(_) | Type::Error) {
return None;
}
let trait_name = if method == "from" { "From" } else { "TryFrom" };
let resolves = self
.impl_table
.as_ref()
.map(|table| {
let trait_ref = TraitRef::parameterized(trait_name, vec![arg_ty.clone()]);
resolve_impl(&trait_ref, &target_ty, table).is_some()
})
.unwrap_or(false);
if resolves {
let result_ty = if method == "from" {
target_ty
} else {
Type::Result(
Box::new(target_ty),
Box::new(Type::Named(crate::NamedType {
name: "ConvertError".to_string(),
})),
)
};
return Some(result_ty);
}
self.diags.error(
E_NO_CONVERSION,
format!(
"cannot convert `{}` to `{}` via `{}.{}()`: no canonical `{}` \
conversion relates these types",
crate::traits::type_key(&arg_ty),
crate::traits::type_key(&target_ty),
crate::traits::type_key(&target_ty),
method,
trait_name,
),
method_span,
);
Some(Type::Error)
}
fn freshen_method_type_params(&self, type_name: &str, method: &str, fn_ty: Type) -> Type {
let Some(names) = self
.method_generic_params
.get(type_name)
.and_then(|m| m.get(method))
else {
return fn_ty;
};
if names.is_empty() {
return fn_ty;
}
let fresh: Vec<Type> = names.iter().map(|_| self.fresh_var()).collect();
substitute_type_params(&fn_ty, names, &fresh)
}
const CONVERSION_METHODS: &'static [&'static str] = &["into", "from", "try_from"];
fn method_is_resolvable(&self, receiver_ty: &Type, method: &str) -> bool {
let receiver_ty = self.subst.apply(receiver_ty);
if Self::CONVERSION_METHODS.contains(&method) {
return true;
}
if self
.resolve_builtin_method_fn_type(&receiver_ty, method)
.is_some()
{
return true;
}
if matches!(receiver_ty, Type::Primitive(_))
&& self
.resolve_primitive_canonical_method_fn_type(&receiver_ty, method)
.is_some()
{
return true;
}
let user_name = match &receiver_ty {
Type::Named(nt) => Some(&nt.name),
Type::Generic(g) => Some(&g.constructor),
_ => None,
};
if let Some(name) = user_name {
if self
.method_types
.get(name)
.is_some_and(|m| m.contains_key(method))
{
return true;
}
if self
.record_field_types
.get(name)
.is_some_and(|fs| fs.iter().any(|(n, _)| n == method))
{
return true;
}
}
if self.type_implements_trait_method(&receiver_ty, method) {
return true;
}
false
}
fn type_implements_trait_method(&self, receiver_ty: &Type, method: &str) -> bool {
let Some(table) = self.impl_table.as_ref() else {
return false;
};
let key = crate::traits::type_key(receiver_ty);
for entry in table.entries() {
if entry.type_key != key {
continue;
}
let Some(trait_ref) = &entry.trait_ref else {
continue;
};
if self
.trait_method_types
.get(&trait_ref.name)
.is_some_and(|m| m.contains_key(method))
{
return true;
}
for supertrait in table.all_supertraits(&trait_ref.name) {
if self
.trait_method_types
.get(&supertrait)
.is_some_and(|m| m.contains_key(method))
{
return true;
}
}
}
false
}
fn concrete_closed_method_names(&self, receiver_ty: &Type) -> Option<Vec<String>> {
let receiver_ty = self.subst.apply(receiver_ty);
match &receiver_ty {
Type::Primitive(p) if !matches!(p, PrimitiveType::Void | PrimitiveType::Never) => {
let mut names = self.builtin_method_names(&receiver_ty);
for methods in self.trait_method_types.values() {
for m in methods.keys() {
if self
.resolve_primitive_canonical_method_fn_type(&receiver_ty, m)
.is_some()
{
names.push(m.clone());
}
}
}
Some(names)
}
Type::Optional(_) | Type::Result(_, _) => Some(self.builtin_method_names(&receiver_ty)),
Type::Generic(g) if matches!(g.constructor.as_str(), "List" | "Map" | "Set") => {
let mut names = self.builtin_method_names(&receiver_ty);
if let Some(m) = self.method_types.get(&g.constructor) {
names.extend(m.keys().cloned());
}
Some(names)
}
Type::Named(nt) => {
if !self.record_field_types.contains_key(&nt.name)
&& !self.method_types.contains_key(&nt.name)
{
return None;
}
let mut names: Vec<String> = self
.method_types
.get(&nt.name)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
if let Some(fs) = self.record_field_types.get(&nt.name) {
names.extend(fs.iter().map(|(n, _)| n.clone()));
}
Some(names)
}
Type::Generic(g) => {
if !self.record_field_types.contains_key(&g.constructor)
&& !self.method_types.contains_key(&g.constructor)
{
return None;
}
let mut names: Vec<String> = self
.method_types
.get(&g.constructor)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
if let Some(fs) = self.record_field_types.get(&g.constructor) {
names.extend(fs.iter().map(|(n, _)| n.clone()));
}
Some(names)
}
_ => None,
}
}
fn builtin_method_names(&self, receiver_ty: &Type) -> Vec<String> {
const ALL_BUILTIN_METHODS: &[&str] = &[
"len",
"length",
"count",
"byte_len",
"is_empty",
"contains",
"contains_key",
"first",
"last",
"find",
"get",
"index_of",
"push",
"append",
"pop",
"insert",
"remove",
"remove_at",
"concat",
"clear",
"reverse",
"sort",
"dedup",
"flatten",
"take",
"skip",
"slice",
"filter",
"map",
"map_values",
"flat_map",
"fold",
"reduce",
"for_each",
"any",
"all",
"enumerate",
"zip",
"join",
"to_set",
"to_list",
"keys",
"values",
"entries",
"set",
"delete",
"merge",
"add",
"union",
"intersection",
"difference",
"symmetric_difference",
"is_subset",
"is_superset",
"is_disjoint",
"starts_with",
"ends_with",
"regex_match",
"to_upper",
"to_lower",
"trim",
"trim_start",
"trim_end",
"substring",
"replace",
"repeat",
"pad_start",
"pad_end",
"format",
"regex_replace",
"regex_find",
"split",
"chars",
"bytes",
"char_at",
"abs",
"min",
"max",
"clamp",
"shift_left",
"shift_right",
"to_float",
"to_int",
"floor",
"ceil",
"round",
"sqrt",
"is_nan",
"is_infinite",
"negate",
"is_alpha",
"is_digit",
"is_whitespace",
"compare",
"hash_code",
"equals",
"to_string",
"display",
"is_some",
"is_none",
"unwrap",
"unwrap_or",
"is_ok",
"is_err",
"map_err",
];
ALL_BUILTIN_METHODS
.iter()
.filter(|m| self.method_is_resolvable(receiver_ty, m))
.map(|m| (*m).to_string())
.collect()
}
fn check_unknown_method_on_concrete(&mut self, receiver_ty: &Type, method: &str, span: Span) {
if Self::CONVERSION_METHODS.contains(&method) {
return;
}
if self.method_is_resolvable(receiver_ty, method) {
return;
}
let Some(candidates) = self.concrete_closed_method_names(receiver_ty) else {
return;
};
let receiver_ty = self.subst.apply(receiver_ty);
let recv_desc = describe_receiver_type(&receiver_ty);
let diag = self.diags.error(
E_NO_SUCH_METHOD,
format!("no method `{method}` on `{recv_desc}`"),
span,
);
if let Some(suggestion) = nearest_method_name(method, &candidates) {
diag.note(format!("did you mean `{suggestion}`?"));
}
}
fn resolve_method_return_type(&self, receiver_ty: &Type, method: &str) -> Type {
let receiver_ty = self.subst.apply(receiver_ty);
if matches!(receiver_ty, Type::Primitive(_)) {
if let Some(ty) = self.resolve_primitive_canonical_method_return(&receiver_ty, method) {
return ty;
}
}
match &receiver_ty {
Type::Error => Type::Error,
Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
let elem_ty = &g.args[0];
match method {
"len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
"first" | "last" | "find" | "get" => Type::Optional(Box::new(elem_ty.clone())),
"index_of" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
"contains" | "is_empty" | "any" | "all" => Type::Primitive(PrimitiveType::Bool),
"push" | "append" | "insert" | "reverse" | "set" => {
Type::Primitive(PrimitiveType::Void)
}
"pop" => Type::Optional(Box::new(elem_ty.clone())),
"remove_at" => elem_ty.clone(),
"concat" | "sort" | "filter" | "dedup" | "take" | "skip" | "flat_map"
| "slice" | "flatten" => receiver_ty.clone(),
"clear" | "for_each" => Type::Primitive(PrimitiveType::Void),
"join" | "display" => Type::Primitive(PrimitiveType::String),
"enumerate" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Tuple(vec![
Type::Primitive(PrimitiveType::Int),
elem_ty.clone(),
])],
}),
"to_set" => Type::Generic(GenericType {
constructor: "Set".into(),
args: vec![elem_ty.clone()],
}),
_ => self.fresh_var(),
}
}
Type::Generic(g) if g.constructor == "Map" && g.args.len() == 2 => {
let key_ty = &g.args[0];
let val_ty = &g.args[1];
match method {
"len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
"contains_key" | "is_empty" => Type::Primitive(PrimitiveType::Bool),
"get" => Type::Optional(Box::new(val_ty.clone())),
"set" | "delete" | "merge" | "filter" => receiver_ty.clone(),
"for_each" => Type::Primitive(PrimitiveType::Void),
"keys" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![key_ty.clone()],
}),
"values" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![val_ty.clone()],
}),
"entries" | "to_list" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Tuple(vec![key_ty.clone(), val_ty.clone()])],
}),
_ => self.fresh_var(),
}
}
Type::Primitive(PrimitiveType::String) => match method {
"len" | "length" | "count" | "byte_len" => Type::Primitive(PrimitiveType::Int),
"contains" | "starts_with" | "ends_with" | "is_empty" | "regex_match" => {
Type::Primitive(PrimitiveType::Bool)
}
"to_upper" | "to_lower" | "trim" | "trim_start" | "trim_end" | "reverse"
| "slice" | "substring" | "replace" | "to_string" | "display" | "repeat"
| "pad_start" | "pad_end" | "format" | "regex_replace" | "join" => {
Type::Primitive(PrimitiveType::String)
}
"split" | "regex_find" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::String)],
}),
"chars" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Char)],
}),
"bytes" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
}),
"index_of" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
"char_at" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Char))),
_ => self.fresh_var(),
},
Type::Primitive(PrimitiveType::Int) => match method {
"abs" | "min" | "max" | "clamp" | "shift_left" | "shift_right" | "compare"
| "hash_code" => Type::Primitive(PrimitiveType::Int),
"to_float" => Type::Primitive(PrimitiveType::Float),
"to_string" | "display" => Type::Primitive(PrimitiveType::String),
"equals" => Type::Primitive(PrimitiveType::Bool),
_ => self.fresh_var(),
},
Type::Primitive(PrimitiveType::Float) => match method {
"abs" | "floor" | "ceil" | "round" | "sqrt" | "min" | "max" | "clamp" => {
Type::Primitive(PrimitiveType::Float)
}
"to_int" => Type::Primitive(PrimitiveType::Int),
"to_string" | "display" => Type::Primitive(PrimitiveType::String),
"is_nan" | "is_infinite" | "equals" => Type::Primitive(PrimitiveType::Bool),
"compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
_ => self.fresh_var(),
},
Type::Primitive(PrimitiveType::Bool) => match method {
"negate" => Type::Primitive(PrimitiveType::Bool),
"to_int" => Type::Primitive(PrimitiveType::Int),
"to_string" | "display" => Type::Primitive(PrimitiveType::String),
"compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
"equals" => Type::Primitive(PrimitiveType::Bool),
_ => self.fresh_var(),
},
Type::Primitive(PrimitiveType::Char) => match method {
"to_upper" | "to_lower" => Type::Primitive(PrimitiveType::Char),
"is_alpha" | "is_digit" | "is_whitespace" | "equals" => {
Type::Primitive(PrimitiveType::Bool)
}
"to_int" | "compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
"to_string" | "display" => Type::Primitive(PrimitiveType::String),
_ => self.fresh_var(),
},
Type::Generic(g) if g.constructor == "Set" && g.args.len() == 1 => {
let elem_ty = &g.args[0];
match method {
"len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
"contains" | "is_empty" | "is_subset" | "is_superset" | "is_disjoint" => {
Type::Primitive(PrimitiveType::Bool)
}
"add"
| "remove"
| "union"
| "intersection"
| "difference"
| "symmetric_difference"
| "filter"
| "map" => receiver_ty.clone(),
"for_each" => Type::Primitive(PrimitiveType::Void),
"to_list" => Type::Generic(GenericType {
constructor: "List".into(),
args: vec![elem_ty.clone()],
}),
_ => self.fresh_var(),
}
}
Type::Optional(inner_ty) => match method {
"is_some" | "is_none" => Type::Primitive(PrimitiveType::Bool),
"unwrap" | "unwrap_or" => *inner_ty.clone(),
_ => self.fresh_var(),
},
Type::Result(ok_ty, _err_ty) => match method {
"is_ok" | "is_err" => Type::Primitive(PrimitiveType::Bool),
"unwrap" | "unwrap_or" => *ok_ty.clone(),
_ => self.fresh_var(),
},
Type::Named(nt) => {
if let Some(methods) = self.method_types.get(&nt.name) {
if let Some(Type::Function(f)) = methods.get(method) {
return self.subst.apply(&f.ret);
}
}
self.fresh_var()
}
Type::Generic(g) => {
if let Some(methods) = self.method_types.get(&g.constructor) {
if let Some(Type::Function(f)) = methods.get(method) {
let ret_ty = self.subst.apply(&f.ret);
if let Some(params) = self.record_generic_params.get(&g.constructor) {
return substitute_type_params(&ret_ty, params, &g.args);
}
return ret_ty;
}
}
self.fresh_var()
}
_ => self.fresh_var(),
}
}
fn resolve_builtin_method_fn_type(&self, receiver_ty: &Type, method: &str) -> Option<Type> {
let receiver_ty = self.subst.apply(receiver_ty);
let mk = |recv: &Type, params: Vec<Type>, ret: Type| -> Option<Type> {
let mut all_params = vec![recv.clone()];
all_params.extend(params);
Some(Type::Function(FnType {
params: all_params,
ret: Box::new(ret),
effects: vec![],
}))
};
match &receiver_ty {
Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
let elem = &g.args[0];
let r = &receiver_ty;
match method {
"len" | "length" | "count" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Int))
}
"is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"contains" => mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Bool)),
"first" | "last" => mk(r, vec![], Type::Optional(Box::new(elem.clone()))),
"find" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
});
mk(r, vec![cb], Type::Optional(Box::new(elem.clone())))
}
"get" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int)],
Type::Optional(Box::new(elem.clone())),
),
"index_of" => mk(
r,
vec![elem.clone()],
Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
),
"push" | "append" => {
mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Void))
}
"pop" => mk(r, vec![], Type::Optional(Box::new(elem.clone()))),
"remove_at" => mk(r, vec![Type::Primitive(PrimitiveType::Int)], elem.clone()),
"insert" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int), elem.clone()],
Type::Primitive(PrimitiveType::Void),
),
"reverse" => mk(r, vec![], Type::Primitive(PrimitiveType::Void)),
"set" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int), elem.clone()],
Type::Primitive(PrimitiveType::Void),
),
"concat" => mk(r, vec![receiver_ty.clone()], receiver_ty.clone()),
"clear" => mk(r, vec![], Type::Primitive(PrimitiveType::Void)),
"sort" | "dedup" | "flatten" => mk(r, vec![], receiver_ty.clone()),
"take" | "skip" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int)],
receiver_ty.clone(),
),
"slice" => mk(
r,
vec![
Type::Primitive(PrimitiveType::Int),
Type::Primitive(PrimitiveType::Int),
],
receiver_ty.clone(),
),
"filter" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
});
mk(r, vec![cb], receiver_ty.clone())
}
"map" => {
let u = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(u.clone()),
effects: vec![],
});
let ret = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![u],
});
mk(r, vec![cb], ret)
}
"flat_map" => {
let u = self.fresh_var();
let inner_list = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![u.clone()],
});
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(inner_list),
effects: vec![],
});
let ret = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![u],
});
mk(r, vec![cb], ret)
}
"fold" => {
let acc = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![acc.clone(), elem.clone()],
ret: Box::new(acc.clone()),
effects: vec![],
});
mk(r, vec![acc.clone(), cb], acc)
}
"reduce" => {
let cb = Type::Function(FnType {
params: vec![elem.clone(), elem.clone()],
ret: Box::new(elem.clone()),
effects: vec![],
});
mk(r, vec![cb], elem.clone())
}
"for_each" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
});
mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
}
"any" | "all" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
});
mk(r, vec![cb], Type::Primitive(PrimitiveType::Bool))
}
"enumerate" => {
let pair =
Type::Tuple(vec![Type::Primitive(PrimitiveType::Int), elem.clone()]);
mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![pair],
}),
)
}
"zip" => {
let f = self.fresh_var();
let other_list = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![f.clone()],
});
let pair = Type::Tuple(vec![elem.clone(), f]);
mk(
r,
vec![other_list],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![pair],
}),
)
}
"join" => mk(
r,
vec![Type::Primitive(PrimitiveType::String)],
Type::Primitive(PrimitiveType::String),
),
"to_set" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "Set".into(),
args: vec![elem.clone()],
}),
),
_ => None,
}
}
Type::Generic(g) if g.constructor == "Map" && g.args.len() == 2 => {
let key = &g.args[0];
let val = &g.args[1];
let r = &receiver_ty;
match method {
"len" | "length" | "count" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Int))
}
"is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"contains_key" => {
mk(r, vec![key.clone()], Type::Primitive(PrimitiveType::Bool))
}
"get" => mk(r, vec![key.clone()], Type::Optional(Box::new(val.clone()))),
"set" => mk(r, vec![key.clone(), val.clone()], receiver_ty.clone()),
"delete" => mk(r, vec![key.clone()], receiver_ty.clone()),
"merge" => mk(r, vec![receiver_ty.clone()], receiver_ty.clone()),
"keys" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![key.clone()],
}),
),
"values" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![val.clone()],
}),
),
"entries" | "to_list" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Tuple(vec![key.clone(), val.clone()])],
}),
),
"map_values" => {
let u = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![val.clone()],
ret: Box::new(u.clone()),
effects: vec![],
});
mk(
r,
vec![cb],
Type::Generic(GenericType {
constructor: "Map".into(),
args: vec![key.clone(), u],
}),
)
}
"filter" => {
let cb = Type::Function(FnType {
params: vec![key.clone(), val.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
});
mk(r, vec![cb], receiver_ty.clone())
}
"for_each" => {
let cb = Type::Function(FnType {
params: vec![key.clone(), val.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
});
mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
}
_ => None,
}
}
Type::Generic(g) if g.constructor == "Set" && g.args.len() == 1 => {
let elem = &g.args[0];
let r = &receiver_ty;
match method {
"len" | "length" | "count" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Int))
}
"is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"contains" => mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Bool)),
"add" | "remove" => mk(r, vec![elem.clone()], receiver_ty.clone()),
"union" | "intersection" | "difference" | "symmetric_difference" => {
mk(r, vec![receiver_ty.clone()], receiver_ty.clone())
}
"is_subset" | "is_superset" | "is_disjoint" => mk(
r,
vec![receiver_ty.clone()],
Type::Primitive(PrimitiveType::Bool),
),
"filter" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
});
mk(r, vec![cb], receiver_ty.clone())
}
"map" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(elem.clone()),
effects: vec![],
});
mk(r, vec![cb], receiver_ty.clone())
}
"for_each" => {
let cb = Type::Function(FnType {
params: vec![elem.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
});
mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
}
"to_list" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![elem.clone()],
}),
),
_ => None,
}
}
Type::Primitive(PrimitiveType::String) => {
let r = &receiver_ty;
let str_ty = Type::Primitive(PrimitiveType::String);
let int_ty = Type::Primitive(PrimitiveType::Int);
match method {
"len" | "length" | "count" | "byte_len" => mk(r, vec![], int_ty),
"is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"contains" | "starts_with" | "ends_with" => mk(
r,
vec![str_ty.clone()],
Type::Primitive(PrimitiveType::Bool),
),
"regex_match" => mk(
r,
vec![str_ty.clone()],
Type::Primitive(PrimitiveType::Bool),
),
"to_upper" | "to_lower" | "trim" | "trim_start" | "trim_end" | "reverse"
| "to_string" | "display" => mk(r, vec![], str_ty),
"repeat" => mk(r, vec![Type::Primitive(PrimitiveType::Int)], str_ty),
"slice" | "substring" => mk(
r,
vec![
Type::Primitive(PrimitiveType::Int),
Type::Primitive(PrimitiveType::Int),
],
str_ty,
),
"replace" | "regex_replace" => {
mk(r, vec![str_ty.clone(), str_ty.clone()], str_ty)
}
"pad_start" | "pad_end" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int), str_ty.clone()],
str_ty,
),
"format" => mk(r, vec![], str_ty),
"join" => mk(
r,
vec![Type::Generic(GenericType {
constructor: "List".into(),
args: vec![str_ty.clone()],
})],
str_ty,
),
"split" => mk(
r,
vec![str_ty],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::String)],
}),
),
"regex_find" => mk(
r,
vec![Type::Primitive(PrimitiveType::String)],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::String)],
}),
),
"chars" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Char)],
}),
),
"bytes" => mk(
r,
vec![],
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
}),
),
"index_of" => mk(
r,
vec![Type::Primitive(PrimitiveType::String)],
Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
),
"char_at" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int)],
Type::Optional(Box::new(Type::Primitive(PrimitiveType::Char))),
),
_ => None,
}
}
Type::Primitive(PrimitiveType::Int) => {
let r = &receiver_ty;
let int_ty = Type::Primitive(PrimitiveType::Int);
match method {
"abs" => mk(r, vec![], int_ty),
"min" | "max" | "shift_left" | "shift_right" | "compare" => {
mk(r, vec![int_ty.clone()], int_ty)
}
"clamp" => mk(r, vec![int_ty.clone(), int_ty.clone()], int_ty),
"equals" => mk(
r,
vec![Type::Primitive(PrimitiveType::Int)],
Type::Primitive(PrimitiveType::Bool),
),
"hash_code" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
"to_float" => mk(r, vec![], Type::Primitive(PrimitiveType::Float)),
"to_string" | "display" => {
mk(r, vec![], Type::Primitive(PrimitiveType::String))
}
_ => None,
}
}
Type::Primitive(PrimitiveType::Float) => {
let r = &receiver_ty;
let float_ty = Type::Primitive(PrimitiveType::Float);
match method {
"abs" | "floor" | "ceil" | "round" | "sqrt" => mk(r, vec![], float_ty),
"min" | "max" => mk(r, vec![float_ty.clone()], float_ty),
"clamp" => mk(r, vec![float_ty.clone(), float_ty.clone()], float_ty),
"to_int" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
"to_string" | "display" => {
mk(r, vec![], Type::Primitive(PrimitiveType::String))
}
"is_nan" | "is_infinite" | "equals" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Bool))
}
"compare" | "hash_code" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
_ => None,
}
}
Type::Primitive(PrimitiveType::Bool) => {
let r = &receiver_ty;
match method {
"negate" | "equals" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"to_int" | "compare" | "hash_code" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Int))
}
"to_string" | "display" => {
mk(r, vec![], Type::Primitive(PrimitiveType::String))
}
_ => None,
}
}
Type::Primitive(PrimitiveType::Char) => {
let r = &receiver_ty;
match method {
"to_upper" | "to_lower" => mk(r, vec![], Type::Primitive(PrimitiveType::Char)),
"is_alpha" | "is_digit" | "is_whitespace" | "equals" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Bool))
}
"to_int" | "compare" | "hash_code" => {
mk(r, vec![], Type::Primitive(PrimitiveType::Int))
}
"to_string" | "display" => {
mk(r, vec![], Type::Primitive(PrimitiveType::String))
}
_ => None,
}
}
Type::Optional(inner_ty) => {
let r = &receiver_ty;
let inner = *inner_ty.clone();
match method {
"is_some" | "is_none" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"unwrap" => mk(r, vec![], inner),
"unwrap_or" => mk(r, vec![inner.clone()], inner),
"map" => {
let u = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![inner],
ret: Box::new(u.clone()),
effects: vec![],
});
mk(r, vec![cb], Type::Optional(Box::new(u)))
}
"flat_map" => {
let u = self.fresh_var();
let opt_u = Type::Optional(Box::new(u));
let cb = Type::Function(FnType {
params: vec![inner],
ret: Box::new(opt_u.clone()),
effects: vec![],
});
mk(r, vec![cb], opt_u)
}
_ => None,
}
}
Type::Result(ok_ty, err_ty) => {
let r = &receiver_ty;
let ok = *ok_ty.clone();
let err = *err_ty.clone();
match method {
"is_ok" | "is_err" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
"unwrap" => mk(r, vec![], ok),
"unwrap_or" => mk(r, vec![ok.clone()], ok),
"map" => {
let u = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![ok],
ret: Box::new(u.clone()),
effects: vec![],
});
mk(r, vec![cb], Type::Result(Box::new(u), Box::new(err)))
}
"map_err" => {
let e2 = self.fresh_var();
let cb = Type::Function(FnType {
params: vec![err],
ret: Box::new(e2.clone()),
effects: vec![],
});
mk(r, vec![cb], Type::Result(Box::new(ok), Box::new(e2)))
}
_ => None,
}
}
_ => None,
}
}
fn replace_type_vars(&self, ty: &Type, map: &HashMap<TypeVarId, Type>) -> Type {
match ty {
Type::TypeVar(id) => map.get(id).cloned().unwrap_or_else(|| ty.clone()),
Type::Function(f) => Type::Function(FnType {
params: f
.params
.iter()
.map(|t| self.replace_type_vars(t, map))
.collect(),
ret: Box::new(self.replace_type_vars(&f.ret, map)),
effects: f.effects.clone(),
}),
Type::Generic(g) => Type::Generic(GenericType {
constructor: g.constructor.clone(),
args: g
.args
.iter()
.map(|t| self.replace_type_vars(t, map))
.collect(),
}),
Type::Tuple(elems) => Type::Tuple(
elems
.iter()
.map(|t| self.replace_type_vars(t, map))
.collect(),
),
Type::Optional(inner) => Type::Optional(Box::new(self.replace_type_vars(inner, map))),
Type::Result(ok, err) => Type::Result(
Box::new(self.replace_type_vars(ok, map)),
Box::new(self.replace_type_vars(err, map)),
),
_ => ty.clone(),
}
}
fn require_comparable_operand(&mut self, operand: &Type, span: Span) {
let resolved = self.subst.apply(operand);
match &resolved {
Type::TypeVar(_) | Type::Flexible(_) | Type::Error => return,
_ => {}
}
let impl_table = match self.impl_table.as_ref() {
Some(t) => t,
None => return, };
let trait_ref = TraitRef::new("Comparable");
if resolve_impl(&trait_ref, &resolved, impl_table).is_none() {
let key = crate::traits::type_key(&resolved);
let suggestion = if matches!(resolved, Type::Primitive(_)) {
format!(
"`{key}` is not `Comparable` (the `(core trait, primitive)` \
conformances are sealed); wrap it in a newtype with its own \
`impl Comparable`"
)
} else {
format!("implement `Comparable` for `{key}`")
};
self.diags.error(
E_WHERE_CLAUSE,
format!(
"type `{key}` does not implement `Comparable`; the \
`<`/`>`/`<=`/`>=` operators require it — {suggestion}"
),
span,
);
}
}
fn is_user_comparable(&self, operand: &Type) -> bool {
let resolved = self.subst.apply(operand);
let Type::Named(_) = &resolved else {
return false;
};
let Some(impl_table) = self.impl_table.as_ref() else {
return false;
};
let trait_ref = TraitRef::new("Comparable");
resolve_impl(&trait_ref, &resolved, impl_table).is_some()
}
fn structural_equatable_witness(
&self,
ty: &Type,
in_progress: &mut HashSet<String>,
path: &mut Vec<String>,
) -> Option<NonEquatableWitness> {
let resolved = self.subst.apply(ty);
let witness_here = |path: &[String], class_name: Option<String>| {
Some(NonEquatableWitness {
path: path.to_vec(),
leaf: resolved.clone(),
class_name,
})
};
match &resolved {
Type::TypeVar(_) | Type::Flexible(_) | Type::Error => None,
Type::Function(_) => witness_here(path, None),
Type::Primitive(p) => {
if matches!(p, PrimitiveType::Void) {
return None;
}
let table = self.impl_table.as_ref()?;
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
None
} else {
witness_here(path, None)
}
}
Type::Named(n) => {
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
return None;
}
}
if !in_progress.insert(n.name.clone()) {
return None;
}
let result = if self.class_names.contains(&n.name) {
witness_here(path, Some(n.name.clone()))
} else if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
self.enum_payloads_witness(
&variants.clone(),
&HashMap::new(),
in_progress,
path,
)
} else if let Some(fields) = self.record_field_types.get(&n.name) {
self.record_fields_witness(&fields.clone(), &HashMap::new(), in_progress, path)
} else {
None
};
in_progress.remove(&n.name);
result
}
Type::Generic(g) => {
match (g.constructor.as_str(), g.args.as_slice()) {
("List" | "Set", [elem]) => {
path.push("[..]".to_string());
let w = self.structural_equatable_witness(elem, in_progress, path);
path.pop();
w
}
("Map", [key, value]) => {
path.push("[key]".to_string());
if let Some(w) = self.structural_equatable_witness(key, in_progress, path) {
path.pop();
return Some(w);
}
path.pop();
path.push("[value]".to_string());
let w = self.structural_equatable_witness(value, in_progress, path);
path.pop();
w
}
_ => {
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some()
{
return None;
}
}
let key = crate::traits::type_key(&resolved);
if !in_progress.insert(key.clone()) {
return None;
}
let subst_map: HashMap<String, Type> = self
.record_generic_params
.get(&g.constructor)
.map(|params| {
params.iter().cloned().zip(g.args.iter().cloned()).collect()
})
.unwrap_or_default();
let result = if let Some(variants) =
self.enum_variant_payloads.get(&g.constructor)
{
self.enum_payloads_witness(
&variants.clone(),
&subst_map,
in_progress,
path,
)
} else if let Some(fields) = self.record_field_types.get(&g.constructor) {
if self.class_names.contains(&g.constructor) {
witness_here(path, Some(g.constructor.clone()))
} else {
self.record_fields_witness(
&fields.clone(),
&subst_map,
in_progress,
path,
)
}
} else {
None
};
in_progress.remove(&key);
result
}
}
}
Type::Tuple(elems) => {
for (i, elem) in elems.iter().enumerate() {
path.push(i.to_string());
if let Some(w) = self.structural_equatable_witness(elem, in_progress, path) {
path.pop();
return Some(w);
}
path.pop();
}
None
}
Type::Optional(inner) => {
path.push("[..]".to_string());
let w = self.structural_equatable_witness(inner, in_progress, path);
path.pop();
w
}
Type::Result(ok, err) => {
path.push("[ok]".to_string());
if let Some(w) = self.structural_equatable_witness(ok, in_progress, path) {
path.pop();
return Some(w);
}
path.pop();
path.push("[err]".to_string());
let w = self.structural_equatable_witness(err, in_progress, path);
path.pop();
w
}
Type::Refined(base, _) => self.structural_equatable_witness(base, in_progress, path),
}
}
fn record_fields_witness(
&self,
fields: &[(String, Type)],
subst_map: &HashMap<String, Type>,
in_progress: &mut HashSet<String>,
path: &mut Vec<String>,
) -> Option<NonEquatableWitness> {
for (fname, fty) in fields {
let fty = substitute_named_params(fty, subst_map);
path.push(fname.clone());
if let Some(w) = self.structural_equatable_witness(&fty, in_progress, path) {
path.pop();
return Some(w);
}
path.pop();
}
None
}
fn enum_payloads_witness(
&self,
variants: &[EnumVariantPayloadTypes],
subst_map: &HashMap<String, Type>,
in_progress: &mut HashSet<String>,
path: &mut Vec<String>,
) -> Option<NonEquatableWitness> {
for (vname, components) in variants {
for (label, cty) in components {
let cty = substitute_named_params(cty, subst_map);
path.push(format!("{vname}.{label}"));
if let Some(w) = self.structural_equatable_witness(&cty, in_progress, path) {
path.pop();
return Some(w);
}
path.pop();
}
}
None
}
pub(crate) fn equatable_provenance(
&self,
ty: &Type,
in_progress: &mut HashSet<String>,
) -> EqProvenance {
let resolved = self.subst.apply(ty);
match &resolved {
Type::TypeVar(_) | Type::Flexible(_) | Type::Error => EqProvenance::StructuralDefault,
Type::Function(_) => EqProvenance::NotEquatable,
Type::Primitive(p) => {
if matches!(p, PrimitiveType::Void) {
return EqProvenance::StructuralDefault;
}
match self.impl_table.as_ref() {
Some(table)
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table)
.is_some() =>
{
EqProvenance::StructuralDefault
}
_ => EqProvenance::NotEquatable,
}
}
Type::Named(n) => {
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
return EqProvenance::CustomImpl;
}
}
if !in_progress.insert(n.name.clone()) {
return EqProvenance::StructuralDefault;
}
let result = if self.class_names.contains(&n.name) {
EqProvenance::NotEquatable
} else if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
self.enum_payloads_provenance(&variants.clone(), &HashMap::new(), in_progress)
} else if let Some(fields) = self.record_field_types.get(&n.name) {
self.record_fields_provenance(&fields.clone(), &HashMap::new(), in_progress)
} else {
EqProvenance::StructuralDefault
};
in_progress.remove(&n.name);
result
}
Type::Generic(g) => match (g.constructor.as_str(), g.args.as_slice()) {
("List" | "Set", [elem]) => self.equatable_provenance(elem, in_progress),
("Map", [key, value]) => self
.equatable_provenance(key, in_progress)
.join(self.equatable_provenance(value, in_progress)),
_ => {
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
return EqProvenance::CustomImpl;
}
}
let key = crate::traits::type_key(&resolved);
if !in_progress.insert(key.clone()) {
return EqProvenance::StructuralDefault;
}
let subst_map: HashMap<String, Type> = self
.record_generic_params
.get(&g.constructor)
.map(|params| params.iter().cloned().zip(g.args.iter().cloned()).collect())
.unwrap_or_default();
let result = if let Some(variants) =
self.enum_variant_payloads.get(&g.constructor)
{
self.enum_payloads_provenance(&variants.clone(), &subst_map, in_progress)
} else if let Some(fields) = self.record_field_types.get(&g.constructor) {
if self.class_names.contains(&g.constructor) {
EqProvenance::NotEquatable
} else {
self.record_fields_provenance(&fields.clone(), &subst_map, in_progress)
}
} else {
EqProvenance::StructuralDefault
};
in_progress.remove(&key);
result
}
},
Type::Tuple(elems) => elems
.iter()
.fold(EqProvenance::StructuralDefault, |acc, e| {
acc.join(self.equatable_provenance(e, in_progress))
}),
Type::Optional(inner) => self.equatable_provenance(inner, in_progress),
Type::Result(ok, err) => self
.equatable_provenance(ok, in_progress)
.join(self.equatable_provenance(err, in_progress)),
Type::Refined(base, _) => self.equatable_provenance(base, in_progress),
}
}
fn record_fields_provenance(
&self,
fields: &[(String, Type)],
subst_map: &HashMap<String, Type>,
in_progress: &mut HashSet<String>,
) -> EqProvenance {
fields
.iter()
.fold(EqProvenance::StructuralDefault, |acc, (_, fty)| {
let fty = substitute_named_params(fty, subst_map);
acc.join(self.equatable_provenance(&fty, in_progress))
})
}
fn enum_payloads_provenance(
&self,
variants: &[EnumVariantPayloadTypes],
subst_map: &HashMap<String, Type>,
in_progress: &mut HashSet<String>,
) -> EqProvenance {
variants
.iter()
.flat_map(|(_, components)| components.iter())
.fold(EqProvenance::StructuralDefault, |acc, (_, cty)| {
let cty = substitute_named_params(cty, subst_map);
acc.join(self.equatable_provenance(&cty, in_progress))
})
}
fn require_equatable_operand(&mut self, operand: &Type, span: Span) {
let resolved = self.subst.apply(operand);
match &resolved {
Type::TypeVar(_) | Type::Flexible(_) | Type::Error => return,
_ => {}
}
if self.impl_table.is_none() {
return; }
let mut in_progress = HashSet::new();
let mut path = Vec::new();
if let Some(witness) =
self.structural_equatable_witness(&resolved, &mut in_progress, &mut path)
{
let key = crate::traits::type_key(&resolved);
let (detail, suggestion) = equatable_failure_wording(&key, &witness);
self.diags
.error(
E_NOT_EQUATABLE,
format!(
"type `{resolved}` does not implement `Equatable`; the `==`/`!=` \
operators require it — {detail}"
),
span,
)
.note(suggestion);
}
}
fn user_eq_kind(&self, operand: &Type) -> Option<&'static str> {
let resolved = self.subst.apply(operand);
match &resolved {
Type::TypeVar(id) => {
let bounds = self.type_var_bounds.get(id)?;
if bounds.iter().any(|b| b == "Equatable" || b == "Comparable") {
Some("generic")
} else {
None
}
}
Type::Flexible(_) | Type::Error | Type::Primitive(_) | Type::Function(_) => None,
Type::Named(_) => {
if let Some(table) = self.impl_table.as_ref() {
if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
return Some("impl");
}
}
self.container_eq_kind(&resolved)
}
Type::Generic(_) | Type::Tuple(_) | Type::Optional(_) | Type::Result(_, _) => {
self.container_eq_kind(&resolved)
}
Type::Refined(base, _) => self.user_eq_kind(base),
}
}
fn container_eq_kind(&self, resolved: &Type) -> Option<&'static str> {
let native = if self.type_needs_deep_eq(resolved, &mut HashSet::new()) {
"deep"
} else {
"structural"
};
match self.equatable_provenance(resolved, &mut HashSet::new()) {
EqProvenance::CustomImpl => Some("deep_custom"),
EqProvenance::StructuralDefault | EqProvenance::NotEquatable => Some(native),
}
}
fn type_needs_deep_eq(&self, ty: &Type, in_progress: &mut HashSet<String>) -> bool {
let resolved = self.subst.apply(ty);
match &resolved {
Type::Generic(g) => match (g.constructor.as_str(), g.args.as_slice()) {
("List" | "Set" | "Map", _) => true,
_ => {
let key = crate::traits::type_key(&resolved);
if !in_progress.insert(key.clone()) {
return false;
}
let subst_map: HashMap<String, Type> = self
.record_generic_params
.get(&g.constructor)
.map(|params| params.iter().cloned().zip(g.args.iter().cloned()).collect())
.unwrap_or_default();
let deep =
if let Some(variants) = self.enum_variant_payloads.get(&g.constructor) {
variants.clone().iter().any(|(_, components)| {
components.iter().any(|(_, cty)| {
self.type_needs_deep_eq(
&substitute_named_params(cty, &subst_map),
in_progress,
)
})
})
} else if let Some(fields) = self.record_field_types.get(&g.constructor) {
fields.clone().iter().any(|(_, fty)| {
self.type_needs_deep_eq(
&substitute_named_params(fty, &subst_map),
in_progress,
)
})
} else {
false
};
in_progress.remove(&key);
deep
}
},
Type::Optional(_) | Type::Result(_, _) => true,
Type::Named(n) => {
if !in_progress.insert(n.name.clone()) {
return false;
}
let deep = if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
variants.clone().iter().any(|(_, components)| {
components
.iter()
.any(|(_, cty)| self.type_needs_deep_eq(cty, in_progress))
})
} else if let Some(fields) = self.record_field_types.get(&n.name) {
fields
.clone()
.iter()
.any(|(_, fty)| self.type_needs_deep_eq(fty, in_progress))
} else {
false
};
in_progress.remove(&n.name);
deep
}
Type::Tuple(elems) => elems
.iter()
.any(|e| self.type_needs_deep_eq(e, in_progress)),
Type::Refined(base, _) => self.type_needs_deep_eq(base, in_progress),
_ => false,
}
}
fn infer_binop(&mut self, op: BinOp, lt: &Type, rt: &Type, span: Span) -> Type {
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem | BinOp::Pow => {
self.unify_or_error(rt, lt, span, "arithmetic operands");
self.subst.apply(lt)
}
BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
self.unify_or_error(rt, lt, span, "comparison operands");
let probe = match self.subst.apply(lt) {
Type::TypeVar(_) => rt,
_ => lt,
};
if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge) {
self.require_comparable_operand(probe, span);
} else if matches!(op, BinOp::Eq | BinOp::Ne) {
self.require_equatable_operand(probe, span);
}
Type::Primitive(PrimitiveType::Bool)
}
BinOp::And | BinOp::Or => {
let bool_ty = Type::Primitive(PrimitiveType::Bool);
self.unify_or_error(lt, &bool_ty, span, "logical operand");
self.unify_or_error(rt, &bool_ty, span, "logical operand");
bool_ty
}
BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => {
self.unify_or_error(rt, lt, span, "bitwise operands");
self.subst.apply(lt)
}
BinOp::Compose => self.fresh_var(),
BinOp::Is => Type::Primitive(PrimitiveType::Bool),
}
}
fn infer_unop(&mut self, op: UnaryOp, operand_ty: &Type, span: Span) -> Type {
match op {
UnaryOp::Neg => {
self.subst.apply(operand_ty)
}
UnaryOp::Not => {
let bool_ty = Type::Primitive(PrimitiveType::Bool);
self.unify_or_error(operand_ty, &bool_ty, span, "logical not operand");
bool_ty
}
UnaryOp::BitNot => {
self.subst.apply(operand_ty)
}
}
}
fn infer_literal(&self, lit: &Literal) -> Type {
match lit {
Literal::Int(s) => {
let (_, suffix) = bock_ast::strip_type_suffix(s);
match suffix {
Some("i8") => Type::Primitive(PrimitiveType::Int8),
Some("i16") => Type::Primitive(PrimitiveType::Int16),
Some("i32") => Type::Primitive(PrimitiveType::Int32),
Some("i64") => Type::Primitive(PrimitiveType::Int64),
Some("i128") => Type::Primitive(PrimitiveType::Int128),
Some("u8") => Type::Primitive(PrimitiveType::UInt8),
Some("u16") => Type::Primitive(PrimitiveType::UInt16),
Some("u32") => Type::Primitive(PrimitiveType::UInt32),
Some("u64") => Type::Primitive(PrimitiveType::UInt64),
_ => Type::Primitive(PrimitiveType::Int),
}
}
Literal::Float(s) => {
let (_, suffix) = bock_ast::strip_type_suffix(s);
match suffix {
Some("f32") => Type::Primitive(PrimitiveType::Float32),
Some("f64") => Type::Primitive(PrimitiveType::Float64),
_ => Type::Primitive(PrimitiveType::Float),
}
}
Literal::Bool(_) => Type::Primitive(PrimitiveType::Bool),
Literal::Char(_) => Type::Primitive(PrimitiveType::Char),
Literal::String(_) => Type::Primitive(PrimitiveType::String),
Literal::Unit => Type::Primitive(PrimitiveType::Void),
}
}
fn bind_pattern_type(&mut self, pattern: &mut AIRNode, ty: &Type) {
match &pattern.kind {
NodeKind::WildcardPat | NodeKind::RestPat => {
self.record(pattern, ty.clone());
}
NodeKind::BindPat { name, .. } => {
let name = name.name.clone();
self.env.define(name, ty.clone());
self.record(pattern, ty.clone());
}
NodeKind::LiteralPat { lit } => {
let lit_ty = self.infer_literal(lit);
self.unify_or_error(&lit_ty, ty, pattern.span, "literal pattern");
self.record(pattern, lit_ty);
}
NodeKind::TuplePat { .. } => {
if let NodeKind::TuplePat { elems } = &mut pattern.kind {
if let Type::Tuple(elem_tys) = ty {
for (e, et) in elems.iter_mut().zip(elem_tys.iter()) {
let et = et.clone();
self.bind_pattern_type(e, &et);
}
} else {
for e in elems.iter_mut() {
let fv = self.fresh_var();
self.bind_pattern_type(e, &fv);
}
}
}
self.record(pattern, ty.clone());
}
NodeKind::ConstructorPat { .. } => {
let ctor_name = if let NodeKind::ConstructorPat { path, .. } = &pattern.kind {
type_path_to_name(path)
} else {
String::new()
};
let resolved_ty = self.subst.apply(ty);
if let NodeKind::ConstructorPat { fields, .. } = &mut pattern.kind {
match (ctor_name.as_str(), &resolved_ty) {
("Some", Type::Optional(inner)) if fields.len() == 1 => {
let inner_ty = self.subst.apply(inner);
self.bind_pattern_type(&mut fields[0], &inner_ty);
}
("Ok", Type::Result(ok, _)) if fields.len() == 1 => {
let ok_ty = self.subst.apply(ok);
self.bind_pattern_type(&mut fields[0], &ok_ty);
}
("Err", Type::Result(_, err)) if fields.len() == 1 => {
let err_ty = self.subst.apply(err);
self.bind_pattern_type(&mut fields[0], &err_ty);
}
_ => {
for f in fields.iter_mut() {
let fv = self.fresh_var();
self.bind_pattern_type(f, &fv);
}
}
}
}
self.record(pattern, ty.clone());
}
NodeKind::OrPat { .. } => {
if let NodeKind::OrPat { alternatives } = &mut pattern.kind {
for alt in alternatives.iter_mut() {
let t = ty.clone();
self.bind_pattern_type(alt, &t);
}
}
self.record(pattern, ty.clone());
}
NodeKind::ListPat { .. } => {
let elem_ty = match ty {
Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
g.args[0].clone()
}
_ => self.fresh_var(),
};
if let NodeKind::ListPat { elems, rest } = &mut pattern.kind {
for e in elems.iter_mut() {
let et = elem_ty.clone();
self.bind_pattern_type(e, &et);
}
if let Some(r) = rest {
let list_ty = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![elem_ty],
});
self.bind_pattern_type(r, &list_ty);
}
}
self.record(pattern, ty.clone());
}
NodeKind::RecordPat { .. } => {
if let NodeKind::RecordPat { fields, .. } = &mut pattern.kind {
for f in fields.iter_mut() {
let fv = self.fresh_var();
if let Some(sub_pat) = &mut f.pattern {
self.bind_pattern_type(sub_pat, &fv);
} else {
self.env.define(f.name.name.clone(), fv);
}
}
}
self.record(pattern, ty.clone());
}
_ => {
self.record(pattern, ty.clone());
}
}
}
fn air_type_node_to_type(&mut self, node: &AIRNode, gp_map: &HashMap<String, Type>) -> Type {
match &node.kind {
NodeKind::TypeNamed { path, args } => {
let name = type_path_to_name(path);
if let Some(ty) = gp_map.get(&name) {
return ty.clone();
}
if let Some(prim) = name_to_primitive(&name) {
return Type::Primitive(prim);
}
if args.is_empty() {
if let Some(underlying) = self.type_aliases.get(&name) {
return underlying.clone();
}
Type::Named(crate::NamedType { name })
} else {
let converted_args: Vec<Type> = args
.iter()
.map(|a| self.air_type_node_to_type(a, gp_map))
.collect();
match (name.as_str(), converted_args.len()) {
("Result", 2) => Type::Result(
Box::new(converted_args[0].clone()),
Box::new(converted_args[1].clone()),
),
("Optional", 1) => Type::Optional(Box::new(converted_args[0].clone())),
_ => Type::Generic(GenericType {
constructor: name,
args: converted_args,
}),
}
}
}
NodeKind::TypeTuple { elems } => {
let elem_tys: Vec<Type> = elems
.iter()
.map(|e| self.air_type_node_to_type(e, gp_map))
.collect();
Type::Tuple(elem_tys)
}
NodeKind::TypeFunction { params, ret, .. } => {
let param_tys: Vec<Type> = params
.iter()
.map(|p| self.air_type_node_to_type(p, gp_map))
.collect();
let ret_ty = self.air_type_node_to_type(ret, gp_map);
Type::Function(FnType {
params: param_tys,
ret: Box::new(ret_ty),
effects: vec![],
})
}
NodeKind::TypeOptional { inner } => {
Type::Optional(Box::new(self.air_type_node_to_type(inner, gp_map)))
}
NodeKind::TypeSelf => {
if let Some(ty) = gp_map.get("Self") {
ty.clone()
} else {
Type::Named(crate::NamedType {
name: "Self".into(),
})
}
}
NodeKind::Param { ty, .. } => {
if let Some(ty_node) = ty {
self.air_type_node_to_type(ty_node, gp_map)
} else {
self.fresh_var()
}
}
_ => self.fresh_var(),
}
}
fn type_expr_to_type(&self, ty: &TypeExpr, gp_map: &HashMap<String, Type>) -> Type {
match ty {
TypeExpr::Named { path, args, .. } => {
let name = type_path_to_name(path);
if let Some(t) = gp_map.get(&name) {
return t.clone();
}
if let Some(prim) = name_to_primitive(&name) {
return Type::Primitive(prim);
}
if args.is_empty() {
if let Some(underlying) = self.type_aliases.get(&name) {
return underlying.clone();
}
Type::Named(crate::NamedType { name })
} else {
let converted_args: Vec<Type> = args
.iter()
.map(|a| self.type_expr_to_type(a, gp_map))
.collect();
match (name.as_str(), converted_args.len()) {
("Result", 2) => Type::Result(
Box::new(converted_args[0].clone()),
Box::new(converted_args[1].clone()),
),
("Optional", 1) => Type::Optional(Box::new(converted_args[0].clone())),
_ => Type::Generic(GenericType {
constructor: name,
args: converted_args,
}),
}
}
}
TypeExpr::Tuple { elems, .. } => Type::Tuple(
elems
.iter()
.map(|e| self.type_expr_to_type(e, gp_map))
.collect(),
),
TypeExpr::Function { params, ret, .. } => {
let param_tys: Vec<Type> = params
.iter()
.map(|p| self.type_expr_to_type(p, gp_map))
.collect();
let ret_ty = self.type_expr_to_type(ret, gp_map);
Type::Function(FnType {
params: param_tys,
ret: Box::new(ret_ty),
effects: vec![],
})
}
TypeExpr::Optional { inner, .. } => {
Type::Optional(Box::new(self.type_expr_to_type(inner, gp_map)))
}
TypeExpr::SelfType { .. } => Type::Named(crate::NamedType {
name: "Self".into(),
}),
}
}
pub fn infer_expr(&mut self, expr: &AIRNode) -> Type {
if let Some(ty) = self.types.get(&expr.id) {
return ty.clone();
}
let mut cloned = expr.clone();
self.infer_node(&mut cloned)
}
pub fn check_expr(&mut self, expr: &AIRNode, expected: &Type) {
if let Some(ty) = self.types.get(&expr.id) {
let ty = ty.clone();
self.unify_or_error(&ty, expected, expr.span, "expression");
return;
}
let mut cloned = expr.clone();
self.check_node(&mut cloned, expected);
}
}
impl Default for TypeChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EqProvenance {
StructuralDefault,
CustomImpl,
NotEquatable,
}
impl EqProvenance {
#[must_use]
fn join(self, other: EqProvenance) -> EqProvenance {
use EqProvenance::{CustomImpl, NotEquatable, StructuralDefault};
match (self, other) {
(NotEquatable, _) | (_, NotEquatable) => NotEquatable,
(CustomImpl, _) | (_, CustomImpl) => CustomImpl,
(StructuralDefault, StructuralDefault) => StructuralDefault,
}
}
}
struct NonEquatableWitness {
path: Vec<String>,
leaf: Type,
class_name: Option<String>,
}
fn substitute_named_params(ty: &Type, subst_map: &HashMap<String, Type>) -> Type {
if subst_map.is_empty() {
return ty.clone();
}
match ty {
Type::Named(n) => subst_map
.get(&n.name)
.cloned()
.unwrap_or_else(|| ty.clone()),
Type::Generic(g) => Type::Generic(GenericType {
constructor: g.constructor.clone(),
args: g
.args
.iter()
.map(|a| substitute_named_params(a, subst_map))
.collect(),
}),
Type::Tuple(elems) => Type::Tuple(
elems
.iter()
.map(|e| substitute_named_params(e, subst_map))
.collect(),
),
Type::Function(f) => Type::Function(FnType {
params: f
.params
.iter()
.map(|p| substitute_named_params(p, subst_map))
.collect(),
ret: Box::new(substitute_named_params(&f.ret, subst_map)),
effects: f.effects.clone(),
}),
Type::Optional(inner) => {
Type::Optional(Box::new(substitute_named_params(inner, subst_map)))
}
Type::Result(ok, err) => Type::Result(
Box::new(substitute_named_params(ok, subst_map)),
Box::new(substitute_named_params(err, subst_map)),
),
Type::Refined(base, pred) => Type::Refined(
Box::new(substitute_named_params(base, subst_map)),
pred.clone(),
),
_ => ty.clone(),
}
}
fn equatable_failure_wording(key: &str, witness: &NonEquatableWitness) -> (String, String) {
if let Some(class_name) = &witness.class_name {
let detail = if witness.path.is_empty() {
format!(
"`{class_name}` is a class, and classes are excluded from structural \
equality (data/identity line)"
)
} else {
format!(
"field `{}` is the class `{class_name}`, and classes are excluded from \
structural equality (data/identity line)",
witness.path.join(".")
)
};
return (
detail,
format!("implement `Equatable` for `{class_name}` or remove the comparison"),
);
}
if witness.path.is_empty() {
let detail = match &witness.leaf {
Type::Function(_) => "function types have no equality".to_string(),
Type::Primitive(_) => format!(
"`{key}` has no canonical equality (the `(core trait, primitive)` \
conformances are sealed)"
),
other => format!("`{other}` is not Equatable"),
};
let suggestion = match &witness.leaf {
Type::Primitive(_) => format!(
"wrap `{key}` in a newtype with its own `impl Equatable`, or remove the \
comparison"
),
_ => "remove the comparison".to_string(),
};
return (detail, suggestion);
}
(
format!(
"field `{}` of type `{}` is not Equatable",
witness.path.join("."),
witness.leaf
),
format!("implement `Equatable` for `{key}` or remove the comparison"),
)
}
trait NodeKindExt {
fn param_ty_node(&self) -> &AIRNode;
fn param_pat_name(&self) -> Option<String>;
}
impl NodeKindExt for NodeKind {
fn param_ty_node(&self) -> &AIRNode {
match self {
NodeKind::Param { ty, pattern, .. } => ty.as_deref().unwrap_or(pattern),
_ => unreachable!("param_ty_node called on non-Param node"),
}
}
fn param_pat_name(&self) -> Option<String> {
match self {
NodeKind::Param { pattern, .. } => match &pattern.kind {
NodeKind::BindPat { name, .. } => Some(name.name.clone()),
NodeKind::WildcardPat => None,
_ => None,
},
_ => None,
}
}
}
fn collect_type_var_ids_fn(fn_ty: &FnType, out: &mut Vec<TypeVarId>) {
for param in &fn_ty.params {
collect_type_var_ids(param, out);
}
collect_type_var_ids(&fn_ty.ret, out);
}
fn collect_type_var_ids(ty: &Type, out: &mut Vec<TypeVarId>) {
match ty {
Type::TypeVar(id) if !out.contains(id) => {
out.push(*id);
}
Type::Function(f) => {
for p in &f.params {
collect_type_var_ids(p, out);
}
collect_type_var_ids(&f.ret, out);
}
Type::Generic(g) => {
for a in &g.args {
collect_type_var_ids(a, out);
}
}
Type::Tuple(elems) => {
for e in elems {
collect_type_var_ids(e, out);
}
}
Type::Optional(inner) => collect_type_var_ids(inner, out),
Type::Result(ok, err) => {
collect_type_var_ids(ok, out);
collect_type_var_ids(err, out);
}
_ => {}
}
}
fn substitute_type_params(ty: &Type, param_names: &[String], args: &[Type]) -> Type {
match ty {
Type::Named(nt) => {
if let Some(idx) = param_names.iter().position(|n| n == &nt.name) {
if idx < args.len() {
return args[idx].clone();
}
}
ty.clone()
}
Type::Generic(g) => Type::Generic(GenericType {
constructor: g.constructor.clone(),
args: g
.args
.iter()
.map(|a| substitute_type_params(a, param_names, args))
.collect(),
}),
Type::Optional(inner) => {
Type::Optional(Box::new(substitute_type_params(inner, param_names, args)))
}
Type::Result(ok, err) => Type::Result(
Box::new(substitute_type_params(ok, param_names, args)),
Box::new(substitute_type_params(err, param_names, args)),
),
Type::Tuple(elems) => Type::Tuple(
elems
.iter()
.map(|e| substitute_type_params(e, param_names, args))
.collect(),
),
Type::Function(f) => Type::Function(FnType {
params: f
.params
.iter()
.map(|p| substitute_type_params(p, param_names, args))
.collect(),
ret: Box::new(substitute_type_params(&f.ret, param_names, args)),
effects: f.effects.clone(),
}),
_ => ty.clone(),
}
}
fn type_path_to_name(path: &TypePath) -> String {
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".")
}
fn describe_receiver_type(ty: &Type) -> String {
crate::traits::type_key(ty)
}
fn nearest_method_name(target: &str, candidates: &[String]) -> Option<String> {
let mut best: Option<(usize, &String)> = None;
for cand in candidates {
if cand == target {
continue;
}
let dist = levenshtein(target, cand);
if best.is_none_or(|(d, _)| dist < d) {
best = Some((dist, cand));
}
}
let (dist, cand) = best?;
let threshold = (target.len().max(cand.len()) / 3).clamp(1, 3);
if dist <= threshold {
Some(cand.clone())
} else {
None
}
}
fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr: Vec<usize> = vec![0; b.len() + 1];
for (i, &ca) in a.iter().enumerate() {
curr[0] = i + 1;
for (j, &cb) in b.iter().enumerate() {
let cost = usize::from(ca != cb);
curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}
fn is_associated_call_node(node: &AIRNode) -> bool {
matches!(
node.metadata.get(bock_air::lower::ASSOC_CALL_META_KEY),
Some(Value::Bool(true))
)
}
fn name_to_primitive(name: &str) -> Option<PrimitiveType> {
match name {
"Int" => Some(PrimitiveType::Int),
"Float" => Some(PrimitiveType::Float),
"Bool" => Some(PrimitiveType::Bool),
"String" => Some(PrimitiveType::String),
"Char" => Some(PrimitiveType::Char),
"Void" => Some(PrimitiveType::Void),
"Never" => Some(PrimitiveType::Never),
"Byte" => Some(PrimitiveType::Byte),
"Bytes" => Some(PrimitiveType::Bytes),
"Int8" => Some(PrimitiveType::Int8),
"Int16" => Some(PrimitiveType::Int16),
"Int32" => Some(PrimitiveType::Int32),
"Int64" => Some(PrimitiveType::Int64),
"Int128" => Some(PrimitiveType::Int128),
"UInt8" => Some(PrimitiveType::UInt8),
"UInt16" => Some(PrimitiveType::UInt16),
"UInt32" => Some(PrimitiveType::UInt32),
"UInt64" => Some(PrimitiveType::UInt64),
"Float32" => Some(PrimitiveType::Float32),
"Float64" => Some(PrimitiveType::Float64),
"BigInt" => Some(PrimitiveType::BigInt),
"BigFloat" => Some(PrimitiveType::BigFloat),
"Decimal" => Some(PrimitiveType::Decimal),
_ => None,
}
}
fn conversion_hint(found: &Type, expected: &Type) -> Option<String> {
let f = as_primitive(found)?;
let e = as_primitive(expected)?;
use PrimitiveType as P;
let is_int = |p: &P| {
matches!(
p,
P::Int
| P::Int8
| P::Int16
| P::Int32
| P::Int64
| P::Int128
| P::UInt8
| P::UInt16
| P::UInt32
| P::UInt64
| P::BigInt
)
};
let is_float = |p: &P| matches!(p, P::Float | P::Float32 | P::Float64 | P::BigFloat);
if is_int(&f) && e == P::Float {
return Some(format!(
"call `.to_float()` on the `{f}` value to produce the expected `Float`"
));
}
if is_float(&f) && e == P::Int {
return Some(format!(
"call `.to_int()` on the `{f}` value (truncates toward zero) to produce the expected `Int`"
));
}
if f == P::String && matches!(e, P::Int | P::Float) {
return Some(format!(
"a `String` is not implicitly converted; parse it with `{e}.try_from(...)` (returns a `Result` — handle the failure case)"
));
}
if e == P::String && f != P::String {
return Some(format!(
"call `.to_string()` on the `{f}` value to produce the expected `String`"
));
}
None
}
fn as_primitive(ty: &Type) -> Option<PrimitiveType> {
match ty {
Type::Primitive(p) => Some(p.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bock_air::{AIRNode, NodeIdGen, NodeKind};
use bock_ast::{BinOp, Ident, Literal, TypePath};
use bock_errors::{FileId, Span};
fn span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.into(),
span: span(),
}
}
fn make_node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
AIRNode::new(gen.next(), span(), kind)
}
fn int_lit(gen: &NodeIdGen) -> AIRNode {
make_node(
gen,
NodeKind::Literal {
lit: Literal::Int("42".into()),
},
)
}
fn bool_lit(gen: &NodeIdGen, v: bool) -> AIRNode {
make_node(
gen,
NodeKind::Literal {
lit: Literal::Bool(v),
},
)
}
fn str_lit(gen: &NodeIdGen) -> AIRNode {
make_node(
gen,
NodeKind::Literal {
lit: Literal::String("hello".into()),
},
)
}
fn float_lit(gen: &NodeIdGen) -> AIRNode {
make_node(
gen,
NodeKind::Literal {
lit: Literal::Float("3.14".into()),
},
)
}
fn type_named_node(gen: &NodeIdGen, name: &str) -> AIRNode {
make_node(
gen,
NodeKind::TypeNamed {
path: TypePath {
segments: vec![ident(name)],
span: span(),
},
args: vec![],
},
)
}
#[test]
fn type_mismatch_message_reads_expected_then_found() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let lit = str_lit(&gen);
checker.check_expr(&lit, &Type::Primitive(PrimitiveType::Int));
let diag = checker.diags.iter().next().expect("a diagnostic");
assert_eq!(diag.code.to_string(), "E4001");
assert!(
diag.message.contains("expected `Int`, found `String`"),
"message: {}",
diag.message
);
assert!(
!diag.message.contains("Primitive("),
"message leaks Debug representation: {}",
diag.message
);
}
#[test]
fn type_mismatch_hint_for_expected_int_found_string_is_parse() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let lit = str_lit(&gen);
checker.check_expr(&lit, &Type::Primitive(PrimitiveType::Int));
let diag = checker.diags.iter().next().expect("a diagnostic");
assert!(
diag.notes.iter().any(|n| n.contains("Int.try_from")),
"notes: {:?}",
diag.notes
);
assert!(
!diag.notes.iter().any(|n| n.contains(".to_string()")),
"misleading wrong-direction hint: {:?}",
diag.notes
);
}
#[test]
fn conversion_hint_is_direction_aware() {
let int = Type::Primitive(PrimitiveType::Int);
let float = Type::Primitive(PrimitiveType::Float);
let string = Type::Primitive(PrimitiveType::String);
let bool_t = Type::Primitive(PrimitiveType::Bool);
let hint = conversion_hint(&int, &float).expect("hint");
assert!(hint.contains(".to_float()"), "{hint}");
let hint = conversion_hint(&float, &int).expect("hint");
assert!(hint.contains(".to_int()"), "{hint}");
let hint = conversion_hint(&int, &string).expect("hint");
assert!(hint.contains(".to_string()"), "{hint}");
let hint = conversion_hint(&string, &int).expect("hint");
assert!(hint.contains("Int.try_from"), "{hint}");
let hint = conversion_hint(&string, &float).expect("hint");
assert!(hint.contains("Float.try_from"), "{hint}");
assert_eq!(conversion_hint(&bool_t, &int), None);
assert_eq!(conversion_hint(&string, &bool_t), None);
assert_eq!(
conversion_hint(&int, &Type::Primitive(PrimitiveType::Float32)),
None
);
}
#[test]
fn undefined_variable_reported_once_per_name_and_span() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let first = make_node(
&gen,
NodeKind::Identifier {
name: ident("ghost"),
},
);
let second = make_node(
&gen,
NodeKind::Identifier {
name: ident("ghost"),
},
);
checker.infer_expr(&first);
checker.infer_expr(&second);
assert_eq!(
checker.diags.error_count(),
1,
"expected exactly one E4002 for one root cause"
);
}
#[test]
fn reserved_lambda_handler_reports_e6006_once() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.insert_effect_op_types(
"Log".into(),
vec![(
"log".into(),
Type::Function(FnType {
params: vec![Type::Primitive(PrimitiveType::String)],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
}),
)],
);
let object = make_node(&gen, NodeKind::Identifier { name: ident("Log") });
let field_access = make_node(
&gen,
NodeKind::FieldAccess {
object: Box::new(object),
field: ident("handler"),
},
);
let ty = checker.infer_expr(&field_access);
assert_eq!(ty, Type::Error);
assert_eq!(checker.diags.error_count(), 1, "exactly one diagnostic");
let diag = checker.diags.iter().next().expect("a diagnostic");
assert_eq!(diag.code.to_string(), "E6006");
assert!(
diag.message.contains("`Log.handler(...)`")
&& diag.message.contains("reserved until v1.x"),
"message: {}",
diag.message
);
assert!(
diag.notes.iter().any(|n| n.contains("impl Log for")),
"note must state the supported v1 handler form: {:?}",
diag.notes
);
}
#[test]
fn handler_field_on_non_effect_still_undefined_variable() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let object = make_node(
&gen,
NodeKind::Identifier {
name: ident("NotAnEffect"),
},
);
let field_access = make_node(
&gen,
NodeKind::FieldAccess {
object: Box::new(object),
field: ident("handler"),
},
);
checker.infer_expr(&field_access);
let diag = checker.diags.iter().next().expect("a diagnostic");
assert_eq!(diag.code.to_string(), "E4002");
}
#[test]
fn infer_int_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = int_lit(&gen);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn infer_float_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = float_lit(&gen);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Float));
}
#[test]
fn infer_bool_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = bool_lit(&gen, true);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn infer_string_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = str_lit(&gen);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::String));
}
#[test]
fn infer_defined_variable() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.env.define("x", Type::Primitive(PrimitiveType::Int));
let node = make_node(&gen, NodeKind::Identifier { name: ident("x") });
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn infer_undefined_variable_emits_error() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::Identifier {
name: ident("unknown"),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Error);
assert!(checker.diags.has_errors());
}
#[test]
fn infer_int_addition() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let left = int_lit(&gen);
let right = int_lit(&gen);
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(left),
right: Box::new(right),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn infer_comparison_returns_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let left = int_lit(&gen);
let right = int_lit(&gen);
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Lt,
left: Box::new(left),
right: Box::new(right),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn comparison_on_user_type_without_comparable_errors() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
checker.env.define(
"a",
Type::Named(crate::NamedType {
name: "Point".into(),
}),
);
checker.env.define(
"b",
Type::Named(crate::NamedType {
name: "Point".into(),
}),
);
let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Lt,
left: Box::new(left),
right: Box::new(right),
},
);
checker.infer_expr(&node);
assert!(
checker.diags.has_errors(),
"expected error: Point does not implement Comparable"
);
}
#[test]
fn comparison_on_user_type_with_comparable_ok() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let point = Type::Named(crate::NamedType {
name: "Point".into(),
});
checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
checker.env.define("a", point.clone());
checker.env.define("b", point);
let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Gt,
left: Box::new(left),
right: Box::new(right),
},
);
let ty = checker.infer_expr(&node);
assert!(
!checker.diags.has_errors(),
"expected no errors: Point implements Comparable"
);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn all_ordering_operators_gated_on_user_types() {
for op in [BinOp::Lt, BinOp::Le, BinOp::Gt, BinOp::Ge] {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
checker.env.define(
"a",
Type::Named(crate::NamedType {
name: "Widget".into(),
}),
);
checker.env.define(
"b",
Type::Named(crate::NamedType {
name: "Widget".into(),
}),
);
let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
let node = make_node(
&gen,
NodeKind::BinaryOp {
op,
left: Box::new(left),
right: Box::new(right),
},
);
checker.infer_expr(&node);
assert!(
checker.diags.has_errors(),
"expected error for {op:?} on a non-Comparable user type"
);
}
}
#[test]
fn comparison_on_primitive_not_gated_when_conformant() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut table = ImplTable::new();
crate::traits::register_canonical_conformances(&mut table);
checker.impl_table = Some(table);
let left = int_lit(&gen);
let right = int_lit(&gen);
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Lt,
left: Box::new(left),
right: Box::new(right),
},
);
let ty = checker.infer_expr(&node);
assert!(
!checker.diags.has_errors(),
"Int is Comparable; `<` must be accepted"
);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn comparison_on_bounded_generic_param_not_gated() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
let tv = checker.fresh_var();
checker.env.define("a", tv.clone());
checker.env.define("b", tv);
let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Lt,
left: Box::new(left),
right: Box::new(right),
},
);
checker.infer_expr(&node);
assert!(
!checker.diags.has_errors(),
"comparison on an inference variable must not trigger the user-type gate"
);
}
fn infer_binop_node(checker: &mut TypeChecker, op: BinOp, operand_ty: Type) -> AIRNode {
let gen = NodeIdGen::new();
checker.env.define("a", operand_ty.clone());
checker.env.define("b", operand_ty);
let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
let mut node = make_node(
&gen,
NodeKind::BinaryOp {
op,
left: Box::new(left),
right: Box::new(right),
},
);
checker.infer_node(&mut node);
node
}
#[test]
fn user_comparison_stamps_ordering_ops() {
let point = Type::Named(crate::NamedType {
name: "Point".into(),
});
for op in [BinOp::Lt, BinOp::Le, BinOp::Gt, BinOp::Ge] {
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
let node = infer_binop_node(&mut checker, op, point.clone());
assert_eq!(
node.metadata.get(USER_COMPARE_META_KEY),
Some(&bock_air::Value::Bool(true)),
"{op:?} on a user Comparable type must be stamped"
);
}
}
#[test]
fn primitive_comparison_not_stamped() {
let mut checker = TypeChecker::new();
let mut table = ImplTable::new();
crate::traits::register_canonical_conformances(&mut table);
checker.impl_table = Some(table);
let node = infer_binop_node(&mut checker, BinOp::Lt, Type::Primitive(PrimitiveType::Int));
assert!(
!node.metadata.contains_key(USER_COMPARE_META_KEY),
"primitive `<` must not carry the user-compare stamp"
);
}
#[test]
fn user_equality_not_stamped_by_comparison_arm() {
let point = Type::Named(crate::NamedType {
name: "Point".into(),
});
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
let node = infer_binop_node(&mut checker, BinOp::Eq, point);
assert!(
!node.metadata.contains_key(USER_COMPARE_META_KEY),
"`==` is the Equatable lane and must not carry the user-compare stamp"
);
}
#[test]
fn non_comparable_user_type_not_stamped() {
let point = Type::Named(crate::NamedType {
name: "Point".into(),
});
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
let node = infer_binop_node(&mut checker, BinOp::Lt, point);
assert!(
!node.metadata.contains_key(USER_COMPARE_META_KEY),
"a non-Comparable user type must not be stamped"
);
}
#[test]
fn infer_logical_and_requires_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let left = bool_lit(&gen, true);
let right = bool_lit(&gen, false);
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::And,
left: Box::new(left),
right: Box::new(right),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn type_mismatch_in_binop_emits_error() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let left = int_lit(&gen);
let right = bool_lit(&gen, true);
let node = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(left),
right: Box::new(right),
},
);
checker.infer_expr(&node);
assert!(checker.diags.has_errors());
}
#[test]
fn infer_neg_int() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let operand = int_lit(&gen);
let node = make_node(
&gen,
NodeKind::UnaryOp {
op: UnaryOp::Neg,
operand: Box::new(operand),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn infer_not_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let operand = bool_lit(&gen, true);
let node = make_node(
&gen,
NodeKind::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(operand),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn check_list_literal_against_list_int() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let expected = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
});
let node = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), int_lit(&gen)],
},
);
checker.check_expr(&node, &expected);
assert!(!checker.diags.has_errors());
}
#[test]
fn list_element_mismatch_emits_error() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let expected = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
});
let node = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), bool_lit(&gen, true)],
},
);
checker.check_expr(&node, &expected);
assert!(checker.diags.has_errors());
}
#[test]
fn infer_list_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), int_lit(&gen)],
},
);
let ty = checker.infer_expr(&node);
assert!(matches!(&ty, Type::Generic(g) if g.constructor == "List"
&& g.args.len() == 1
&& g.args[0] == Type::Primitive(PrimitiveType::Int)));
}
#[test]
fn infer_tuple_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::TupleLiteral {
elems: vec![int_lit(&gen), bool_lit(&gen, false)],
},
);
let ty = checker.infer_expr(&node);
assert_eq!(
ty,
Type::Tuple(vec![
Type::Primitive(PrimitiveType::Int),
Type::Primitive(PrimitiveType::Bool),
])
);
}
#[test]
fn infer_block_tail_expression() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let tail = int_lit(&gen);
let node = make_node(
&gen,
NodeKind::Block {
stmts: vec![],
tail: Some(Box::new(tail)),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn infer_block_no_tail_is_void() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
}
#[test]
fn let_binding_infers_and_binds() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let pat = make_node(
&gen,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
);
let val = int_lit(&gen);
let let_node = make_node(
&gen,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(pat),
ty: None,
value: Box::new(val),
},
);
let ident_x = make_node(&gen, NodeKind::Identifier { name: ident("x") });
let block = make_node(
&gen,
NodeKind::Block {
stmts: vec![let_node],
tail: Some(Box::new(ident_x)),
},
);
let ty = checker.infer_expr(&block);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
assert!(!checker.diags.has_errors());
}
#[test]
fn fresh_var_for_generic_params() {
let mut checker = TypeChecker::new();
let t_var = checker.fresh_var(); let t_id = match &t_var {
Type::TypeVar(id) => *id,
_ => unreachable!(),
};
let sig = FnSig {
generic_params: vec!["T".into()],
generic_var_ids: vec![t_id],
param_types: vec![Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t_var.clone()],
})],
return_type: Type::Optional(Box::new(t_var)),
where_clause: vec![],
};
let gen = NodeIdGen::new();
let arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen)],
},
);
let args: Vec<bock_air::AirArg> = vec![bock_air::AirArg {
label: None,
value: arg,
}];
let ret = checker.instantiate_and_check("first", &sig, &args, span());
assert!(!checker.diags.has_errors());
assert!(matches!(ret, Type::Optional(_)));
}
fn register_generic_fn(
checker: &mut TypeChecker,
name: &str,
generic_names: &[&str],
build_sig: impl FnOnce(&[Type]) -> (Vec<Type>, Type),
) {
let vars: Vec<Type> = generic_names.iter().map(|_| checker.fresh_var()).collect();
let var_ids: Vec<TypeVarId> = vars
.iter()
.map(|t| match t {
Type::TypeVar(id) => *id,
_ => unreachable!(),
})
.collect();
let (param_types, return_type) = build_sig(&vars);
let fn_ty = Type::Function(FnType {
params: param_types.clone(),
ret: Box::new(return_type.clone()),
effects: vec![],
});
checker.env.define(name, fn_ty);
checker.fn_sigs.insert(
name.into(),
FnSig {
generic_params: generic_names.iter().map(|s| (*s).into()).collect(),
generic_var_ids: var_ids,
param_types,
return_type,
where_clause: vec![],
},
);
}
#[test]
fn generic_first_infers_int() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
register_generic_fn(&mut checker, "first", &["T"], |vars| {
let t = vars[0].clone();
let params = vec![Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t.clone()],
})];
(params, t)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("first"),
},
);
let list_arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), int_lit(&gen), int_lit(&gen)],
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: list_arg,
}],
},
);
let ty = checker.infer_expr(&call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
assert!(!checker.diags.has_errors());
}
#[test]
fn generic_identity_infers_string() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
register_generic_fn(&mut checker, "identity", &["T"], |vars| {
let t = vars[0].clone();
(vec![t.clone()], t)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("identity"),
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: str_lit(&gen),
}],
},
);
let ty = checker.infer_expr(&call);
assert_eq!(ty, Type::Primitive(PrimitiveType::String));
assert!(!checker.diags.has_errors());
}
#[test]
fn generic_two_params_swap() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
register_generic_fn(&mut checker, "swap", &["A", "B"], |vars| {
let a = vars[0].clone();
let b = vars[1].clone();
let params = vec![a.clone(), b.clone()];
let ret = Type::Tuple(vec![b, a]);
(params, ret)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("swap"),
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![
bock_air::AirArg {
label: None,
value: int_lit(&gen),
},
bock_air::AirArg {
label: None,
value: str_lit(&gen),
},
],
},
);
let ty = checker.infer_expr(&call);
assert_eq!(
ty,
Type::Tuple(vec![
Type::Primitive(PrimitiveType::String),
Type::Primitive(PrimitiveType::Int),
])
);
assert!(!checker.diags.has_errors());
}
#[test]
fn method_call_on_known_type_returns_correct_type() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let list = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), int_lit(&gen), int_lit(&gen)],
},
);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(list),
method: ident("len"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
assert!(!checker.diags.has_errors());
}
#[test]
fn method_call_string_contains_returns_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = str_lit(&gen);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("contains"),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: str_lit(&gen),
}],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
assert!(!checker.diags.has_errors());
}
#[test]
fn method_call_list_push_returns_void() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let list = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen)],
},
);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(list),
method: ident("push"),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: int_lit(&gen),
}],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
}
#[test]
fn method_call_list_append_returns_void() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let list = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen)],
},
);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(list),
method: ident("append"),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: int_lit(&gen),
}],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
}
fn desugared_map_method_call(
gen: &NodeIdGen,
method: &str,
key: AIRNode,
val: AIRNode,
arg: AIRNode,
) -> AIRNode {
let map = make_node(
gen,
NodeKind::MapLiteral {
entries: vec![bock_air::AirMapEntry { key, value: val }],
},
);
let map_self = map.clone();
let callee = make_node(
gen,
NodeKind::FieldAccess {
object: Box::new(map),
field: ident(method),
},
);
make_node(
gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![
bock_air::AirArg {
label: None,
value: map_self,
},
bock_air::AirArg {
label: None,
value: arg,
},
],
},
)
}
#[test]
fn map_contains_is_rejected_with_suggestion() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let call = desugared_map_method_call(
&gen,
"contains",
str_lit(&gen),
int_lit(&gen),
str_lit(&gen),
);
let _ = checker.infer_expr(&call);
assert!(checker.diags.has_errors());
let err = checker
.diags
.iter()
.find(|d| d.code == E_NO_SUCH_METHOD)
.expect("expected an E4013 Map-contains rejection");
assert!(err.message.contains("contains_key"));
assert!(!err.notes.is_empty(), "expected a suggestion note");
}
#[test]
fn map_contains_key_still_resolves() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let call = desugared_map_method_call(
&gen,
"contains_key",
str_lit(&gen),
int_lit(&gen),
str_lit(&gen),
);
let _ = checker.infer_expr(&call);
assert!(
!checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
"contains_key must not be rejected"
);
}
fn desugared_list_method_call(gen: &NodeIdGen, method: &str, arg: Option<AIRNode>) -> AIRNode {
let list = make_node(
gen,
NodeKind::ListLiteral {
elems: vec![int_lit(gen)],
},
);
let list_self = list.clone();
let callee = make_node(
gen,
NodeKind::FieldAccess {
object: Box::new(list),
field: ident(method),
},
);
let mut args = vec![bock_air::AirArg {
label: None,
value: list_self,
}];
if let Some(a) = arg {
args.push(bock_air::AirArg {
label: None,
value: a,
});
}
make_node(
gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args,
},
)
}
#[test]
fn list_unknown_method_is_rejected() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let call = desugared_list_method_call(&gen, "frobnicate", None);
let _ = checker.infer_expr(&call);
let err = checker
.diags
.iter()
.find(|d| d.code == E_NO_SUCH_METHOD)
.expect("expected an E4013 unknown-method rejection");
assert!(err.message.contains("frobnicate"));
assert!(err.message.contains("List[Int]"));
}
#[test]
fn list_unknown_method_suggests_nearest() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let call = desugared_list_method_call(&gen, "lenght", None);
let _ = checker.infer_expr(&call);
let err = checker
.diags
.iter()
.find(|d| d.code == E_NO_SUCH_METHOD)
.expect("expected an E4013 unknown-method rejection");
assert!(
err.notes.iter().any(|n| n.contains("length")),
"expected a `did you mean `length`?` suggestion, got: {:?}",
err.notes
);
}
#[test]
fn list_known_method_not_rejected() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let lambda = make_node(
&gen,
NodeKind::Lambda {
params: vec![make_node(
&gen,
NodeKind::Param {
pattern: Box::new(make_node(
&gen,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
)),
ty: None,
default: None,
},
)],
body: Box::new(make_node(&gen, NodeKind::Identifier { name: ident("x") })),
},
);
let call = desugared_list_method_call(&gen, "map", Some(lambda));
let _ = checker.infer_expr(&call);
assert!(
!checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
"a known List method (`map`) must not be rejected"
);
}
#[test]
fn nearest_method_name_thresholds() {
let cands = vec!["length".to_string(), "len".to_string(), "push".to_string()];
assert_eq!(
nearest_method_name("lenght", &cands).as_deref(),
Some("length")
);
assert_eq!(nearest_method_name("frobnicate", &cands), None);
}
fn module_with_import(
gen: &NodeIdGen,
segments: &[&str],
items: bock_ast::ImportItems,
) -> AIRNode {
let dummy = bock_errors::Span {
file: bock_errors::FileId(0),
start: 0,
end: 0,
};
let import = make_node(
gen,
NodeKind::ImportDecl {
path: bock_ast::ModulePath {
segments: segments
.iter()
.map(|s| bock_ast::Ident {
name: (*s).to_string(),
span: dummy,
})
.collect(),
span: dummy,
},
items,
},
);
make_node(
gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![import],
items: vec![],
},
)
}
#[test]
fn bare_module_import_is_rejected() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut module =
module_with_import(&gen, &["core", "error"], bock_ast::ImportItems::Module);
checker.check_module(&mut module);
let err = checker
.diags
.iter()
.find(|d| d.code == E_BARE_MODULE_IMPORT)
.expect("expected an E4014 bare-module-import rejection");
assert!(err.message.contains("core.error"));
assert!(
err.notes.iter().any(|n| n.contains("{")),
"expected a braced-form suggestion note, got: {:?}",
err.notes
);
}
#[test]
fn braced_import_not_rejected() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let named = bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
name: bock_ast::Ident {
name: "Error".to_string(),
span: bock_errors::Span {
file: bock_errors::FileId(0),
start: 0,
end: 0,
},
},
alias: None,
span: bock_errors::Span {
file: bock_errors::FileId(0),
start: 0,
end: 0,
},
}]);
let mut module = module_with_import(&gen, &["core", "error"], named);
checker.check_module(&mut module);
assert!(
!checker.diags.iter().any(|d| d.code == E_BARE_MODULE_IMPORT),
"a braced import must not be rejected"
);
}
#[test]
fn wildcard_import_not_rejected() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut module = module_with_import(&gen, &["core", "error"], bock_ast::ImportItems::Glob);
checker.check_module(&mut module);
assert!(
!checker.diags.iter().any(|d| d.code == E_BARE_MODULE_IMPORT),
"a wildcard import must not be rejected"
);
}
#[test]
fn infer_interpolation_is_string() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::Interpolation {
parts: vec![
bock_air::AirInterpolationPart::Literal("hello ".into()),
bock_air::AirInterpolationPart::Expr(Box::new(int_lit(&gen))),
],
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::String));
}
#[test]
fn infer_unreachable_is_never() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(&gen, NodeKind::Unreachable);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Never));
}
#[test]
fn check_module_simple_fn() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let x_pat = make_node(
&gen,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
);
let y_pat = make_node(
&gen,
NodeKind::BindPat {
name: ident("y"),
is_mut: false,
},
);
let int_ty = type_named_node(&gen, "Int");
let x_param = make_node(
&gen,
NodeKind::Param {
pattern: Box::new(x_pat),
ty: Some(Box::new(int_ty.clone())),
default: None,
},
);
let y_param = make_node(
&gen,
NodeKind::Param {
pattern: Box::new(y_pat),
ty: Some(Box::new(int_ty.clone())),
default: None,
},
);
let x_ref = make_node(&gen, NodeKind::Identifier { name: ident("x") });
let y_ref = make_node(&gen, NodeKind::Identifier { name: ident("y") });
let add_expr = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(x_ref),
right: Box::new(y_ref),
},
);
let body = make_node(
&gen,
NodeKind::Block {
stmts: vec![],
tail: Some(Box::new(add_expr)),
},
);
let ret_ty = type_named_node(&gen, "Int");
let fn_node = make_node(
&gen,
NodeKind::FnDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident("add"),
generic_params: vec![],
params: vec![x_param, y_param],
return_type: Some(Box::new(ret_ty)),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
let mut module = make_node(
&gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![fn_node],
},
);
checker.check_module(&mut module);
assert!(
!checker.diags.has_errors(),
"errors: {:?}",
checker.diags.iter().collect::<Vec<_>>()
);
}
fn impl_with_method(
gen: &NodeIdGen,
target: &str,
method: &str,
extra_params: Vec<(&str, AIRNode)>,
ret: AIRNode,
) -> AIRNode {
let self_pat = make_node(
gen,
NodeKind::BindPat {
name: ident("self"),
is_mut: false,
},
);
let self_param = make_node(
gen,
NodeKind::Param {
pattern: Box::new(self_pat),
ty: None,
default: None,
},
);
let mut params = vec![self_param];
for (pname, pty) in extra_params {
let pat = make_node(
gen,
NodeKind::BindPat {
name: ident(pname),
is_mut: false,
},
);
params.push(make_node(
gen,
NodeKind::Param {
pattern: Box::new(pat),
ty: Some(Box::new(pty)),
default: None,
},
));
}
let body = make_node(
gen,
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
let method_node = make_node(
gen,
NodeKind::FnDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident(method),
generic_params: vec![],
params,
return_type: Some(Box::new(ret)),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
make_node(
gen,
NodeKind::ImplBlock {
annotations: vec![],
generic_params: vec![],
trait_path: None,
trait_args: vec![],
target: Box::new(type_named_node(gen, target)),
where_clause: vec![],
methods: vec![method_node],
},
)
}
#[test]
fn impl_method_self_in_return_is_substituted() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let self_ret = make_node(&gen, NodeKind::TypeSelf);
let impl_node = impl_with_method(&gen, "Doubler", "double", vec![], self_ret);
checker.collect_sig(&impl_node);
let method_ty = checker
.method_types
.get("Doubler")
.and_then(|m| m.get("double"))
.expect("double should be registered on Doubler");
let Type::Function(fn_ty) = method_ty else {
panic!("expected a function type, got {method_ty:?}");
};
let doubler = Type::Named(crate::NamedType {
name: "Doubler".into(),
});
assert_eq!(*fn_ty.ret, doubler, "return `Self` should become Doubler");
assert_eq!(fn_ty.params, vec![doubler]);
}
#[test]
fn impl_method_self_in_param_is_substituted() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let other_ty = make_node(&gen, NodeKind::TypeSelf);
let int_ret = type_named_node(&gen, "Int");
let impl_node = impl_with_method(
&gen,
"Counter",
"combine",
vec![("other", other_ty)],
int_ret,
);
checker.collect_sig(&impl_node);
let method_ty = checker
.method_types
.get("Counter")
.and_then(|m| m.get("combine"))
.expect("combine should be registered on Counter");
let Type::Function(fn_ty) = method_ty else {
panic!("expected a function type, got {method_ty:?}");
};
let counter = Type::Named(crate::NamedType {
name: "Counter".into(),
});
assert_eq!(fn_ty.params, vec![counter.clone(), counter]);
assert_eq!(*fn_ty.ret, Type::Primitive(PrimitiveType::Int));
}
fn impl_with_bodied_method(
gen: &NodeIdGen,
target: &str,
method: &str,
ret: AIRNode,
tail: AIRNode,
) -> AIRNode {
let self_pat = make_node(
gen,
NodeKind::BindPat {
name: ident("self"),
is_mut: false,
},
);
let self_param = make_node(
gen,
NodeKind::Param {
pattern: Box::new(self_pat),
ty: None,
default: None,
},
);
let body = make_node(
gen,
NodeKind::Block {
stmts: vec![],
tail: Some(Box::new(tail)),
},
);
let method_node = make_node(
gen,
NodeKind::FnDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident(method),
generic_params: vec![],
params: vec![self_param],
return_type: Some(Box::new(ret)),
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
make_node(
gen,
NodeKind::ImplBlock {
annotations: vec![],
generic_params: vec![],
trait_path: None,
trait_args: vec![],
target: Box::new(type_named_node(gen, target)),
where_clause: vec![],
methods: vec![method_node],
},
)
}
#[test]
fn impl_method_body_type_error_is_reported() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let ret = type_named_node(&gen, "Int");
let tail = str_lit(&gen);
let impl_node = impl_with_bodied_method(&gen, "Widget", "id", ret, tail);
let mut module = make_node(
&gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![impl_node],
},
);
checker.check_module(&mut module);
assert!(
checker.diags.has_errors(),
"expected a method-body type error, got none"
);
}
#[test]
fn impl_method_body_well_typed_is_clean() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let ret = type_named_node(&gen, "String");
let tail = str_lit(&gen);
let impl_node = impl_with_bodied_method(&gen, "Widget", "name", ret, tail);
let mut module = make_node(
&gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![impl_node],
},
);
checker.check_module(&mut module);
assert!(
!checker.diags.has_errors(),
"well-typed method body should not error: {:?}",
checker.diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn impl_getter_named_like_field_reads_the_field() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let field = bock_ast::RecordDeclField {
id: gen.next(),
span: span(),
name: ident("message"),
ty: TypeExpr::Named {
id: gen.next(),
span: span(),
path: TypePath {
segments: vec![ident("String")],
span: span(),
},
args: vec![],
},
default: None,
};
let record_node = make_node(
&gen,
NodeKind::RecordDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
name: ident("Err"),
generic_params: vec![],
fields: vec![field],
},
);
let self_ref = make_node(
&gen,
NodeKind::Identifier {
name: ident("self"),
},
);
let field_access = make_node(
&gen,
NodeKind::FieldAccess {
object: Box::new(self_ref),
field: ident("message"),
},
);
let ret = type_named_node(&gen, "String");
let impl_node = impl_with_bodied_method(&gen, "Err", "message", ret, field_access);
let mut module = make_node(
&gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![record_node, impl_node],
},
);
checker.check_module(&mut module);
assert!(
!checker.diags.has_errors(),
"field-named getter should read the field, not the method: {:?}",
checker.diags.iter().collect::<Vec<_>>()
);
}
#[test]
fn check_lambda_from_context() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let x_pat = make_node(
&gen,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
);
let x_param = make_node(
&gen,
NodeKind::Param {
pattern: Box::new(x_pat),
ty: None,
default: None,
},
);
let x_ref = make_node(&gen, NodeKind::Identifier { name: ident("x") });
let one = make_node(
&gen,
NodeKind::Literal {
lit: Literal::Int("1".into()),
},
);
let body = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(x_ref),
right: Box::new(one),
},
);
let lambda = make_node(
&gen,
NodeKind::Lambda {
params: vec![x_param],
body: Box::new(body),
},
);
let expected = Type::Function(FnType {
params: vec![Type::Primitive(PrimitiveType::Int)],
ret: Box::new(Type::Primitive(PrimitiveType::Int)),
effects: vec![],
});
checker.check_expr(&lambda, &expected);
assert!(!checker.diags.has_errors());
}
#[test]
fn error_type_prevents_cascade() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let undef = make_node(
&gen,
NodeKind::Identifier {
name: ident("undefined_var"),
},
);
let one = int_lit(&gen);
let add = make_node(
&gen,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(undef),
right: Box::new(one),
},
);
let ty = checker.infer_expr(&add);
assert_eq!(checker.diags.error_count(), 1);
assert_eq!(ty, Type::Error);
}
#[test]
fn where_clause_unknown_param_emits_error() {
let mut checker = TypeChecker::new();
let clauses = vec![TypeConstraint {
id: 0,
span: span(),
param: ident("X"), bounds: vec![TypePath {
segments: vec![ident("Equatable")],
span: span(),
}],
}];
checker.check_where_clause(&clauses, &HashMap::new(), span());
assert!(checker.diags.has_errors());
}
fn type_named_node_with_args(gen: &NodeIdGen, name: &str, args: Vec<AIRNode>) -> AIRNode {
make_node(
gen,
NodeKind::TypeNamed {
path: TypePath {
segments: vec![ident(name)],
span: span(),
},
args,
},
)
}
#[test]
fn result_annotation_produces_type_result() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let int_node = type_named_node(&gen, "Int");
let string_node = type_named_node(&gen, "String");
let result_node = type_named_node_with_args(&gen, "Result", vec![int_node, string_node]);
let ty = checker.air_type_node_to_type(&result_node, &HashMap::new());
assert_eq!(
ty,
Type::Result(
Box::new(Type::Primitive(PrimitiveType::Int)),
Box::new(Type::Primitive(PrimitiveType::String)),
)
);
}
#[test]
fn optional_annotation_produces_type_optional() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let int_node = type_named_node(&gen, "Int");
let optional_node = type_named_node_with_args(&gen, "Optional", vec![int_node]);
let ty = checker.air_type_node_to_type(&optional_node, &HashMap::new());
assert_eq!(
ty,
Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)))
);
}
#[test]
fn result_annotation_unifies_with_ok_construction() {
let annotated = Type::Result(
Box::new(Type::Primitive(PrimitiveType::Int)),
Box::new(Type::Primitive(PrimitiveType::String)),
);
let constructed = Type::Result(
Box::new(Type::Primitive(PrimitiveType::Int)),
Box::new(Type::TypeVar(99)),
);
let mut subst = crate::Substitution::new();
assert!(crate::unify(&annotated, &constructed, &mut subst).is_ok());
assert_eq!(subst.lookup(99), Type::Primitive(PrimitiveType::String));
}
#[test]
fn optional_annotation_unifies_with_some_construction() {
let annotated = Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)));
let constructed = Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)));
let mut subst = crate::Substitution::new();
assert!(crate::unify(&annotated, &constructed, &mut subst).is_ok());
}
fn register_generic_fn_with_bounds(
checker: &mut TypeChecker,
name: &str,
generic_names: &[&str],
bounds: Vec<TypeConstraint>,
build_sig: impl FnOnce(&[Type]) -> (Vec<Type>, Type),
) {
let vars: Vec<Type> = generic_names.iter().map(|_| checker.fresh_var()).collect();
let var_ids: Vec<TypeVarId> = vars
.iter()
.map(|t| match t {
Type::TypeVar(id) => *id,
_ => unreachable!(),
})
.collect();
let (param_types, return_type) = build_sig(&vars);
let fn_ty = Type::Function(FnType {
params: param_types.clone(),
ret: Box::new(return_type.clone()),
effects: vec![],
});
checker.env.define(name, fn_ty);
checker.fn_sigs.insert(
name.into(),
FnSig {
generic_params: generic_names.iter().map(|s| (*s).into()).collect(),
generic_var_ids: var_ids,
param_types,
return_type,
where_clause: bounds,
},
);
}
fn make_constraint(param: &str, bound_names: &[&str]) -> TypeConstraint {
use bock_ast::TypeConstraint;
TypeConstraint {
id: 0,
span: span(),
param: ident(param),
bounds: bound_names
.iter()
.map(|b| TypePath {
segments: vec![ident(b)],
span: span(),
})
.collect(),
}
}
fn make_impl_table(impls: &[(&str, Type)]) -> ImplTable {
let mut table = ImplTable::new();
for (trait_name, ty) in impls {
table.register_trait_impl(*trait_name, ty);
}
table
}
#[test]
fn trait_bound_satisfied_no_error() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
let bounds = vec![make_constraint("T", &["Comparable"])];
register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
let t = vars[0].clone();
let list_t = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t.clone()],
});
(vec![list_t.clone()], list_t)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("sort"),
},
);
let list_arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen), int_lit(&gen)],
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: list_arg,
}],
},
);
checker.infer_expr(&call);
assert!(
!checker.diags.has_errors(),
"expected no errors for Int: Comparable"
);
}
#[test]
fn trait_bound_violated_emits_diagnostic() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
let bounds = vec![make_constraint("T", &["Comparable"])];
register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
let t = vars[0].clone();
let list_t = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t.clone()],
});
(vec![list_t.clone()], list_t)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("sort"),
},
);
let list_arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![bool_lit(&gen, true), bool_lit(&gen, false)],
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: list_arg,
}],
},
);
checker.infer_expr(&call);
assert!(
checker.diags.has_errors(),
"expected error: Bool does not implement Comparable"
);
assert_eq!(checker.diags.error_count(), 1);
}
#[test]
fn multiple_trait_bounds_both_satisfied() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[
("Comparable", Type::Primitive(PrimitiveType::Int)),
("Displayable", Type::Primitive(PrimitiveType::Int)),
]));
let bounds = vec![make_constraint("T", &["Comparable", "Displayable"])];
register_generic_fn_with_bounds(&mut checker, "display_sorted", &["T"], bounds, |vars| {
let t = vars[0].clone();
let list_t = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t],
});
(vec![list_t], Type::Primitive(PrimitiveType::Void))
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("display_sorted"),
},
);
let list_arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen)],
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: list_arg,
}],
},
);
checker.infer_expr(&call);
assert!(
!checker.diags.has_errors(),
"expected no errors: Int satisfies both bounds"
);
}
#[test]
fn multiple_trait_bounds_one_missing() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.impl_table = Some(make_impl_table(&[(
"Comparable",
Type::Primitive(PrimitiveType::Int),
)]));
let bounds = vec![make_constraint("T", &["Comparable", "Displayable"])];
register_generic_fn_with_bounds(&mut checker, "display_sorted", &["T"], bounds, |vars| {
let t = vars[0].clone();
let list_t = Type::Generic(GenericType {
constructor: "List".into(),
args: vec![t],
});
(vec![list_t], Type::Primitive(PrimitiveType::Void))
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("display_sorted"),
},
);
let list_arg = make_node(
&gen,
NodeKind::ListLiteral {
elems: vec![int_lit(&gen)],
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: list_arg,
}],
},
);
checker.infer_expr(&call);
assert!(
checker.diags.has_errors(),
"expected error: Int missing Displayable"
);
assert_eq!(checker.diags.error_count(), 1);
}
#[test]
fn no_impl_table_skips_bound_checking() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let bounds = vec![make_constraint("T", &["Comparable"])];
register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
let t = vars[0].clone();
(vec![t.clone()], t)
});
let callee = make_node(
&gen,
NodeKind::Identifier {
name: ident("sort"),
},
);
let call = make_node(
&gen,
NodeKind::Call {
callee: Box::new(callee),
type_args: vec![],
args: vec![bock_air::AirArg {
label: None,
value: int_lit(&gen),
}],
},
);
checker.infer_expr(&call);
assert!(!checker.diags.has_errors());
}
#[test]
fn infer_char_literal() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let node = make_node(
&gen,
NodeKind::Literal {
lit: Literal::Char("a".into()),
},
);
let ty = checker.infer_expr(&node);
assert_eq!(ty, Type::Primitive(PrimitiveType::Char));
}
#[test]
fn fn_type_carries_effects() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let body = make_node(
&gen,
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
let fn_decl = make_node(
&gen,
NodeKind::FnDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
is_async: false,
name: ident("greet"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![
TypePath {
segments: vec![ident("Log")],
span: span(),
},
TypePath {
segments: vec![ident("Clock")],
span: span(),
},
],
where_clause: vec![],
body: Box::new(body),
},
);
let module = make_node(
&gen,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![fn_decl],
},
);
let mut module = module;
checker.check_module(&mut module);
let fn_ty = checker
.env
.lookup("greet")
.expect("greet should be defined");
match fn_ty {
Type::Function(f) => {
assert_eq!(f.effects.len(), 2);
assert_eq!(f.effects[0].name, "Log");
assert_eq!(f.effects[1].name, "Clock");
}
other => panic!("expected Function type, got {other:?}"),
}
}
#[test]
fn method_call_float_abs_returns_float() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = float_lit(&gen);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("abs"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Float));
assert!(!checker.diags.has_errors());
}
#[test]
fn method_call_float_to_int_returns_int() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = float_lit(&gen);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("to_int"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
}
#[test]
fn method_call_bool_negate_returns_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = bool_lit(&gen, true);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("negate"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn method_call_char_is_alpha_returns_bool() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = make_node(
&gen,
NodeKind::Literal {
lit: Literal::Char("a".into()),
},
);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("is_alpha"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
}
#[test]
fn method_call_char_to_upper_returns_char() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = make_node(
&gen,
NodeKind::Literal {
lit: Literal::Char("a".into()),
},
);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("to_upper"),
type_args: vec![],
args: vec![],
},
);
let ty = checker.infer_expr(&method_call);
assert_eq!(ty, Type::Primitive(PrimitiveType::Char));
}
#[test]
fn method_call_unknown_method_on_concrete_errors() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let receiver = int_lit(&gen);
let method_call = make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(receiver),
method: ident("nonexistent"),
type_args: vec![],
args: vec![],
},
);
let _ = checker.infer_expr(&method_call);
assert!(
checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD
&& d.message.contains("nonexistent")
&& d.message.contains("Int")),
"unknown method on a concrete `Int` receiver must raise E4013"
);
}
#[test]
fn method_call_unknown_method_on_typevar_does_not_error() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let lambda = make_node(
&gen,
NodeKind::Lambda {
params: vec![make_node(
&gen,
NodeKind::Param {
pattern: Box::new(make_node(
&gen,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
)),
ty: None,
default: None,
},
)],
body: Box::new(make_node(
&gen,
NodeKind::MethodCall {
receiver: Box::new(make_node(
&gen,
NodeKind::Identifier { name: ident("x") },
)),
method: ident("whatever"),
type_args: vec![],
args: vec![],
},
)),
},
);
let _ = checker.infer_expr(&lambda);
assert!(
!checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
"an unknown method on an unresolved type-var receiver must NOT error"
);
}
#[test]
fn recv_kind_tag_maps_each_category() {
use crate::NamedType;
assert_eq!(
recv_kind_tag(&Type::Primitive(PrimitiveType::Int)).as_deref(),
Some("Primitive:Int")
);
assert_eq!(
recv_kind_tag(&Type::Primitive(PrimitiveType::Float)).as_deref(),
Some("Primitive:Float")
);
assert_eq!(
recv_kind_tag(&Type::Primitive(PrimitiveType::String)).as_deref(),
Some("Primitive:String")
);
assert_eq!(
recv_kind_tag(&Type::Optional(Box::new(Type::Primitive(
PrimitiveType::Int
))))
.as_deref(),
Some("Optional")
);
assert_eq!(
recv_kind_tag(&Type::Result(
Box::new(Type::Primitive(PrimitiveType::Int)),
Box::new(Type::Primitive(PrimitiveType::String)),
))
.as_deref(),
Some("Result")
);
assert_eq!(
recv_kind_tag(&Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
}))
.as_deref(),
Some("List")
);
assert_eq!(
recv_kind_tag(&Type::Named(NamedType {
name: "Point".into(),
}))
.as_deref(),
Some("User:Point")
);
assert_eq!(recv_kind_tag(&Type::TypeVar(0)), None);
}
fn desugared_method_call(
gen: &NodeIdGen,
receiver: AIRNode,
method: &str,
extra_args: Vec<AIRNode>,
) -> AIRNode {
let field_access = make_node(
gen,
NodeKind::FieldAccess {
object: Box::new(receiver.clone()),
field: ident(method),
},
);
let mut args = vec![bock_air::AirArg {
label: None,
value: receiver,
}];
for a in extra_args {
args.push(bock_air::AirArg {
label: None,
value: a,
});
}
make_node(
gen,
NodeKind::Call {
callee: Box::new(field_access),
type_args: vec![],
args,
},
)
}
fn with_primitive_comparable(checker: &mut TypeChecker, prim: PrimitiveType) {
let self_ty = Type::Named(crate::NamedType {
name: "Self".into(),
});
let mut comparable = HashMap::new();
comparable.insert(
"compare".to_string(),
Type::Function(FnType {
params: vec![self_ty.clone(), self_ty.clone()],
ret: Box::new(Type::Named(crate::NamedType {
name: "Ordering".into(),
})),
effects: vec![],
}),
);
checker.insert_trait_method_types("Comparable".to_string(), comparable);
let mut equatable = HashMap::new();
equatable.insert(
"eq".to_string(),
Type::Function(FnType {
params: vec![self_ty.clone(), self_ty.clone()],
ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
effects: vec![],
}),
);
checker.insert_trait_method_types("Equatable".to_string(), equatable);
checker.impl_table = Some(make_impl_table(&[
("Comparable", Type::Primitive(prim.clone())),
("Equatable", Type::Primitive(prim)),
]));
}
#[test]
fn stamps_recv_kind_on_primitive_compare() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
with_primitive_comparable(&mut checker, PrimitiveType::Int);
let mut call = desugared_method_call(&gen, int_lit(&gen), "compare", vec![int_lit(&gen)]);
let ty = checker.infer_node(&mut call);
assert_eq!(
ty,
Type::Named(crate::NamedType {
name: "Ordering".into()
})
);
assert_eq!(
call.metadata.get(RECV_KIND_META_KEY),
Some(&Value::String("Primitive:Int".to_string())),
"expected recv_kind stamped on the compare call node"
);
}
#[test]
fn stamps_recv_kind_on_primitive_eq_and_to_string() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
with_primitive_comparable(&mut checker, PrimitiveType::Int);
let mut eq_call = desugared_method_call(&gen, int_lit(&gen), "eq", vec![int_lit(&gen)]);
checker.infer_node(&mut eq_call);
assert_eq!(
eq_call.metadata.get(RECV_KIND_META_KEY),
Some(&Value::String("Primitive:Int".to_string())),
);
let mut ts_call = desugared_method_call(&gen, int_lit(&gen), "to_string", vec![]);
checker.infer_node(&mut ts_call);
assert_eq!(
ts_call.metadata.get(RECV_KIND_META_KEY),
Some(&Value::String("Primitive:Int".to_string())),
);
}
#[test]
fn stamps_recv_kind_optional_and_list() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
checker.env.define(
"o",
Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
);
let o_ref = make_node(&gen, NodeKind::Identifier { name: ident("o") });
let mut opt_call = desugared_method_call(&gen, o_ref, "unwrap_or", vec![int_lit(&gen)]);
checker.infer_node(&mut opt_call);
assert_eq!(
opt_call.metadata.get(RECV_KIND_META_KEY),
Some(&Value::String("Optional".to_string())),
);
checker.env.define(
"xs",
Type::Generic(GenericType {
constructor: "List".into(),
args: vec![Type::Primitive(PrimitiveType::Int)],
}),
);
let xs_ref = make_node(&gen, NodeKind::Identifier { name: ident("xs") });
let mut list_call = desugared_method_call(&gen, xs_ref, "len", vec![]);
checker.infer_node(&mut list_call);
assert_eq!(
list_call.metadata.get(RECV_KIND_META_KEY),
Some(&Value::String("List".to_string())),
);
}
fn binop_node(gen: &NodeIdGen, op: BinOp, left: AIRNode, right: AIRNode) -> AIRNode {
make_node(
gen,
NodeKind::BinaryOp {
op,
left: Box::new(left),
right: Box::new(right),
},
)
}
fn sized_int_lit(gen: &NodeIdGen, suffix: &str) -> AIRNode {
make_node(
gen,
NodeKind::Literal {
lit: Literal::Int(format!("42_{suffix}")),
},
)
}
#[test]
fn stamps_int_arith_on_integer_div_and_rem() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut div = binop_node(&gen, BinOp::Div, int_lit(&gen), int_lit(&gen));
checker.infer_node(&mut div);
assert_eq!(
div.metadata.get(INT_ARITH_META_KEY),
Some(&Value::Bool(true)),
"expected int_arith stamped on Int / Int",
);
let mut rem = binop_node(&gen, BinOp::Rem, int_lit(&gen), int_lit(&gen));
checker.infer_node(&mut rem);
assert_eq!(
rem.metadata.get(INT_ARITH_META_KEY),
Some(&Value::Bool(true)),
"expected int_arith stamped on Int % Int",
);
}
#[test]
fn stamps_int_arith_on_sized_integer_div() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut div = binop_node(
&gen,
BinOp::Div,
sized_int_lit(&gen, "i32"),
sized_int_lit(&gen, "i32"),
);
checker.infer_node(&mut div);
assert_eq!(
div.metadata.get(INT_ARITH_META_KEY),
Some(&Value::Bool(true)),
"expected int_arith stamped on Int32 / Int32",
);
let mut udiv = binop_node(
&gen,
BinOp::Div,
sized_int_lit(&gen, "u64"),
sized_int_lit(&gen, "u64"),
);
checker.infer_node(&mut udiv);
assert_eq!(
udiv.metadata.get(INT_ARITH_META_KEY),
Some(&Value::Bool(true)),
"expected int_arith stamped on UInt64 / UInt64",
);
}
#[test]
fn no_int_arith_stamp_on_float_div_or_addition() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut fdiv = binop_node(&gen, BinOp::Div, float_lit(&gen), float_lit(&gen));
checker.infer_node(&mut fdiv);
assert!(
!fdiv.metadata.contains_key(INT_ARITH_META_KEY),
"Float / Float must not be stamped int_arith",
);
let mut add = binop_node(&gen, BinOp::Add, int_lit(&gen), int_lit(&gen));
checker.infer_node(&mut add);
assert!(
!add.metadata.contains_key(INT_ARITH_META_KEY),
"Int + Int is not integer division",
);
}
#[test]
fn stamps_bool_stringify_on_bool_interpolation_part() {
let gen = NodeIdGen::new();
let mut checker = TypeChecker::new();
let mut interp = make_node(
&gen,
NodeKind::Interpolation {
parts: vec![
bock_air::AirInterpolationPart::Expr(Box::new(bool_lit(&gen, true))),
bock_air::AirInterpolationPart::Expr(Box::new(int_lit(&gen))),
],
},
);
checker.infer_node(&mut interp);
let NodeKind::Interpolation { parts } = &interp.kind else {
panic!("expected interpolation");
};
let bock_air::AirInterpolationPart::Expr(bool_part) = &parts[0] else {
panic!("expected expr part 0");
};
assert_eq!(
bool_part.metadata.get(BOOL_STRINGIFY_META_KEY),
Some(&Value::Bool(true)),
"expected bool_stringify stamped on the Bool interpolation part",
);
let bock_air::AirInterpolationPart::Expr(int_part) = &parts[1] else {
panic!("expected expr part 1");
};
assert!(
!int_part.metadata.contains_key(BOOL_STRINGIFY_META_KEY),
"Int interpolation part must not be stamped bool_stringify",
);
}
}