extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::ast::*;
use crate::token::Span;
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
I64,
F64,
Bool,
Unit,
Str,
Tuple(Vec<Type>),
Array(Box<Type>, i64),
Option(Box<Type>),
Struct(String, Vec<Type>),
Enum(String, Vec<Type>),
Opaque(String),
Var(u32),
Unknown,
}
impl Type {
fn from_expr(expr: &TypeExpr, defined_types: &BTreeMap<String, TypeKind>) -> Type {
Type::from_expr_with_params(expr, defined_types, &BTreeMap::new())
}
fn from_expr_with_params(
expr: &TypeExpr,
defined_types: &BTreeMap<String, TypeKind>,
type_params: &BTreeMap<String, Type>,
) -> Type {
match expr {
TypeExpr::Prim(p, _) => match p {
PrimType::I64 => Type::I64,
PrimType::F64 => Type::F64,
PrimType::Bool => Type::Bool,
PrimType::KString => Type::Str,
},
TypeExpr::Unit(_) => Type::Unit,
TypeExpr::Tuple(ts, _) => Type::Tuple(
ts.iter()
.map(|t| Type::from_expr_with_params(t, defined_types, type_params))
.collect(),
),
TypeExpr::Array(elem, len, _) => Type::Array(
Box::new(Type::from_expr_with_params(
elem,
defined_types,
type_params,
)),
*len,
),
TypeExpr::Option(inner, _) => Type::Option(Box::new(Type::from_expr_with_params(
inner,
defined_types,
type_params,
))),
TypeExpr::Named(name, args, _) => {
if let Some(t) = type_params.get(name) {
return t.clone();
}
let resolved_args: Vec<Type> = args
.iter()
.map(|a| Type::from_expr_with_params(a, defined_types, type_params))
.collect();
match defined_types.get(name) {
Some(TypeKind::Struct) => Type::Struct(name.clone(), resolved_args),
Some(TypeKind::Enum) => Type::Enum(name.clone(), resolved_args),
None => Type::Opaque(name.clone()),
}
}
}
}
pub fn display(&self) -> String {
match self {
Type::I64 => "i64".to_string(),
Type::F64 => "f64".to_string(),
Type::Bool => "bool".to_string(),
Type::Unit => "()".to_string(),
Type::Str => "String".to_string(),
Type::Tuple(ts) => {
let inner: Vec<String> = ts.iter().map(|t| t.display()).collect();
format!("({})", inner.join(", "))
}
Type::Array(elem, n) => format!("[{}; {}]", elem.display(), n),
Type::Option(inner) => format!("Option<{}>", inner.display()),
Type::Struct(name, args) | Type::Enum(name, args) => {
if args.is_empty() {
name.clone()
} else {
let inner: Vec<String> = args.iter().map(|t| t.display()).collect();
format!("{}<{}>", name, inner.join(", "))
}
}
Type::Opaque(name) => name.clone(),
Type::Var(n) => format!("?T{}", n),
Type::Unknown => "<unknown>".to_string(),
}
}
pub fn occurs(&self, var: u32) -> bool {
match self {
Type::Var(v) => *v == var,
Type::Tuple(items) => items.iter().any(|t| t.occurs(var)),
Type::Array(elem, _) => elem.occurs(var),
Type::Option(inner) => inner.occurs(var),
Type::Struct(_, args) | Type::Enum(_, args) => args.iter().any(|t| t.occurs(var)),
_ => false,
}
}
pub fn apply(&self, subst: &Subst) -> Type {
match self {
Type::Var(v) => match subst.get(*v) {
Some(t) => t.apply(subst),
None => self.clone(),
},
Type::Tuple(items) => Type::Tuple(items.iter().map(|t| t.apply(subst)).collect()),
Type::Array(elem, n) => Type::Array(Box::new(elem.apply(subst)), *n),
Type::Option(inner) => Type::Option(Box::new(inner.apply(subst))),
Type::Struct(name, args) => {
Type::Struct(name.clone(), args.iter().map(|t| t.apply(subst)).collect())
}
Type::Enum(name, args) => {
Type::Enum(name.clone(), args.iter().map(|t| t.apply(subst)).collect())
}
other => other.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Subst {
map: BTreeMap<u32, Type>,
}
impl Subst {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, var: u32) -> Option<&Type> {
self.map.get(&var)
}
pub fn insert(&mut self, var: u32, ty: Type) {
self.map.insert(var, ty);
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum UnifyError {
Mismatch { left: Type, right: Type },
OccursCheck { var: u32, ty: Type },
ArrayLengthMismatch { left: i64, right: i64 },
TupleArityMismatch { left: usize, right: usize },
}
pub fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<(), UnifyError> {
let a = a.apply(subst);
let b = b.apply(subst);
match (a, b) {
(Type::I64, Type::I64)
| (Type::F64, Type::F64)
| (Type::Bool, Type::Bool)
| (Type::Unit, Type::Unit)
| (Type::Str, Type::Str) => Ok(()),
(Type::Var(v), other) | (other, Type::Var(v)) => {
if let Type::Var(w) = other
&& v == w
{
return Ok(());
}
if other.occurs(v) {
return Err(UnifyError::OccursCheck { var: v, ty: other });
}
subst.insert(v, other);
Ok(())
}
(Type::Tuple(ls), Type::Tuple(rs)) => {
if ls.len() != rs.len() {
return Err(UnifyError::TupleArityMismatch {
left: ls.len(),
right: rs.len(),
});
}
for (l, r) in ls.iter().zip(rs.iter()) {
unify(l, r, subst)?;
}
Ok(())
}
(Type::Array(le, ln), Type::Array(re, rn)) => {
if ln != rn {
return Err(UnifyError::ArrayLengthMismatch {
left: ln,
right: rn,
});
}
unify(&le, &re, subst)
}
(Type::Option(li), Type::Option(ri)) => unify(&li, &ri, subst),
(Type::Struct(ln, la), Type::Struct(rn, ra)) | (Type::Enum(ln, la), Type::Enum(rn, ra))
if ln == rn && la.len() == ra.len() =>
{
for (l, r) in la.iter().zip(ra.iter()) {
unify(l, r, subst)?;
}
Ok(())
}
(Type::Opaque(ln), Type::Opaque(rn)) if ln == rn => Ok(()),
(l, r) => Err(UnifyError::Mismatch { left: l, right: r }),
}
}
#[derive(Debug, Default)]
pub struct VarGen {
next: u32,
}
impl VarGen {
pub fn fresh(&mut self) -> Type {
let v = self.next;
self.next += 1;
Type::Var(v)
}
pub fn count(&self) -> u32 {
self.next
}
}
#[derive(Debug, Clone, Copy)]
enum TypeKind {
Struct,
Enum,
}
fn type_head_name(t: &Type) -> Option<String> {
use alloc::string::ToString;
match t {
Type::I64 => Some("i64".to_string()),
Type::F64 => Some("f64".to_string()),
Type::Bool => Some("bool".to_string()),
Type::Unit => Some("()".to_string()),
Type::Str => Some("String".to_string()),
Type::Tuple(_) => Some("tuple".to_string()),
Type::Array(_, _) => Some("array".to_string()),
Type::Option(_) => Some("Option".to_string()),
Type::Struct(name, _) | Type::Enum(name, _) | Type::Opaque(name) => Some(name.clone()),
Type::Var(_) | Type::Unknown => None,
}
}
#[derive(Debug, Clone)]
struct FnSig {
type_params: Vec<String>,
type_param_vars: Vec<Type>,
type_param_bounds: Vec<Vec<String>>,
params: Vec<Type>,
return_type: Type,
}
#[derive(Debug, Clone)]
pub struct TypeError {
pub message: String,
pub span: Span,
}
impl TypeError {
fn new(message: String, span: Span) -> Self {
Self { message, span }
}
}
struct Ctx {
types: BTreeMap<String, TypeKind>,
structs: BTreeMap<String, BTreeMap<String, Type>>,
struct_type_param_vars: BTreeMap<String, Vec<Type>>,
enums: BTreeMap<String, BTreeMap<String, Vec<Type>>>,
enum_type_param_vars: BTreeMap<String, Vec<Type>>,
traits: BTreeMap<String, Vec<TraitMethodSig>>,
impls: BTreeMap<String, BTreeSet<String>>,
functions: BTreeMap<String, FnSig>,
natives: BTreeSet<String>,
data: BTreeMap<String, BTreeMap<String, Type>>,
locals: Vec<BTreeMap<String, Type>>,
current_return: Option<Type>,
vargen: VarGen,
subst: Subst,
}
impl Ctx {
fn new() -> Self {
Self {
types: BTreeMap::new(),
structs: BTreeMap::new(),
struct_type_param_vars: BTreeMap::new(),
enums: BTreeMap::new(),
enum_type_param_vars: BTreeMap::new(),
traits: BTreeMap::new(),
impls: BTreeMap::new(),
functions: BTreeMap::new(),
natives: BTreeSet::new(),
data: BTreeMap::new(),
locals: Vec::new(),
current_return: None,
vargen: VarGen::default(),
subst: Subst::new(),
}
}
fn push_scope(&mut self) {
self.locals.push(BTreeMap::new());
}
fn pop_scope(&mut self) {
self.locals.pop();
}
fn add_local(&mut self, name: String, ty: Type) {
if let Some(scope) = self.locals.last_mut() {
scope.insert(name, ty);
}
}
fn lookup_local(&self, name: &str) -> Option<&Type> {
for scope in self.locals.iter().rev() {
if let Some(ty) = scope.get(name) {
return Some(ty);
}
}
None
}
fn fresh(&mut self) -> Type {
self.vargen.fresh()
}
}
fn build_instance_subst(ctx: &mut Ctx, abstract_vars: &[Type]) -> (Subst, Vec<Type>) {
let mut inst = Subst::new();
let mut fresh_args: Vec<Type> = Vec::with_capacity(abstract_vars.len());
for var in abstract_vars {
if let Type::Var(v) = var {
let fresh = ctx.vargen.fresh();
inst.insert(*v, fresh.clone());
fresh_args.push(fresh);
}
}
(inst, fresh_args)
}
fn instantiate_sig(ctx: &mut Ctx, sig: &FnSig) -> (Vec<Type>, Type, Vec<Type>) {
if sig.type_params.is_empty() {
return (sig.params.clone(), sig.return_type.clone(), Vec::new());
}
let mut inst = Subst::new();
let mut fresh_vars: Vec<Type> = Vec::with_capacity(sig.type_param_vars.len());
for var in &sig.type_param_vars {
if let Type::Var(v) = var {
let fresh = ctx.vargen.fresh();
inst.insert(*v, fresh.clone());
fresh_vars.push(fresh);
}
}
let params: Vec<Type> = sig.params.iter().map(|t| t.apply(&inst)).collect();
let return_type = sig.return_type.apply(&inst);
(params, return_type, fresh_vars)
}
fn types_compatible(ctx: &mut Ctx, a: &Type, b: &Type) -> bool {
if matches!(a, Type::Unknown) || matches!(b, Type::Unknown) {
return true;
}
unify(a, b, &mut ctx.subst).is_ok()
}
pub fn check(program: &Program) -> Result<(), TypeError> {
let mut ctx = Ctx::new();
for type_def in &program.types {
match type_def {
TypeDef::Struct(s) => {
ctx.types.insert(s.name.clone(), TypeKind::Struct);
}
TypeDef::Enum(e) => {
ctx.types.insert(e.name.clone(), TypeKind::Enum);
}
}
}
for type_def in &program.types {
match type_def {
TypeDef::Struct(s) => {
let mut tp_map: BTreeMap<String, Type> = BTreeMap::new();
let mut tp_vars: Vec<Type> = Vec::new();
for tp in &s.type_params {
let v = ctx.fresh();
tp_map.insert(tp.name.clone(), v.clone());
tp_vars.push(v);
}
let mut fields = BTreeMap::new();
for f in &s.fields {
fields.insert(
f.name.clone(),
Type::from_expr_with_params(&f.type_expr, &ctx.types, &tp_map),
);
}
ctx.structs.insert(s.name.clone(), fields);
ctx.struct_type_param_vars.insert(s.name.clone(), tp_vars);
}
TypeDef::Enum(e) => {
let mut tp_map: BTreeMap<String, Type> = BTreeMap::new();
let mut tp_vars: Vec<Type> = Vec::new();
for tp in &e.type_params {
let v = ctx.fresh();
tp_map.insert(tp.name.clone(), v.clone());
tp_vars.push(v);
}
let mut variants = BTreeMap::new();
for v in &e.variants {
let payload: Vec<Type> = v
.fields
.iter()
.map(|t| Type::from_expr_with_params(t, &ctx.types, &tp_map))
.collect();
variants.insert(v.name.clone(), payload);
}
ctx.enums.insert(e.name.clone(), variants);
ctx.enum_type_param_vars.insert(e.name.clone(), tp_vars);
}
}
}
for data in &program.data_decls {
let mut fields = BTreeMap::new();
for f in &data.fields {
fields.insert(f.name.clone(), Type::from_expr(&f.type_expr, &ctx.types));
}
ctx.data.insert(data.name.clone(), fields);
}
for use_decl in &program.uses {
if let ImportItem::Name(name) = &use_decl.import {
let full = if use_decl.path.is_empty() {
name.clone()
} else {
let mut full = String::new();
for (i, seg) in use_decl.path.iter().enumerate() {
if i > 0 {
full.push_str("::");
}
full.push_str(seg);
}
full.push_str("::");
full.push_str(name);
full
};
ctx.natives.insert(full);
}
}
for func in &program.functions {
let mut tp_map: BTreeMap<String, Type> = BTreeMap::new();
let mut tp_vars: Vec<Type> = Vec::new();
let mut tp_names: Vec<String> = Vec::new();
let mut tp_bounds: Vec<Vec<String>> = Vec::new();
for tp in &func.type_params {
let v = ctx.fresh();
tp_map.insert(tp.name.clone(), v.clone());
tp_vars.push(v);
tp_names.push(tp.name.clone());
tp_bounds.push(tp.bounds.clone());
}
let params: Vec<Type> = func
.params
.iter()
.map(|p| match &p.type_expr {
Some(t) => Type::from_expr_with_params(t, &ctx.types, &tp_map),
None => ctx.fresh(),
})
.collect();
let return_type = Type::from_expr_with_params(&func.return_type, &ctx.types, &tp_map);
ctx.functions.insert(
func.name.clone(),
FnSig {
type_params: tp_names,
type_param_vars: tp_vars,
type_param_bounds: tp_bounds,
params,
return_type,
},
);
}
for trait_def in &program.traits {
ctx.traits
.insert(trait_def.name.clone(), trait_def.methods.clone());
}
for impl_block in &program.impls {
let head = match Type::from_expr(&impl_block.for_type, &ctx.types) {
Type::I64 => "i64".to_string(),
Type::F64 => "f64".to_string(),
Type::Bool => "bool".to_string(),
Type::Unit => "()".to_string(),
Type::Str => "String".to_string(),
Type::Tuple(_) => "tuple".to_string(),
Type::Array(_, _) => "array".to_string(),
Type::Option(_) => "Option".to_string(),
Type::Struct(name, _) | Type::Enum(name, _) | Type::Opaque(name) => name,
Type::Var(_) | Type::Unknown => continue,
};
ctx.impls
.entry(impl_block.trait_name.clone())
.or_default()
.insert(head.clone());
for method in &impl_block.methods {
let mangled = format!("{}::{}::{}", impl_block.trait_name, head, method.name);
let mut tp_map: BTreeMap<String, Type> = BTreeMap::new();
let mut tp_vars: Vec<Type> = Vec::new();
let mut tp_names: Vec<String> = Vec::new();
let mut tp_bounds: Vec<Vec<String>> = Vec::new();
for tp in &method.type_params {
let v = ctx.fresh();
tp_map.insert(tp.name.clone(), v.clone());
tp_vars.push(v);
tp_names.push(tp.name.clone());
tp_bounds.push(tp.bounds.clone());
}
let params: Vec<Type> = method
.params
.iter()
.map(|p| match &p.type_expr {
Some(t) => Type::from_expr_with_params(t, &ctx.types, &tp_map),
None => ctx.fresh(),
})
.collect();
let return_type = Type::from_expr_with_params(&method.return_type, &ctx.types, &tp_map);
ctx.functions.insert(
mangled,
FnSig {
type_params: tp_names,
type_param_vars: tp_vars,
type_param_bounds: tp_bounds,
params,
return_type,
},
);
}
}
for impl_block in &program.impls {
let trait_methods = match ctx.traits.get(&impl_block.trait_name) {
Some(m) => m.clone(),
None => {
return Err(TypeError::new(
format!("impl references unknown trait `{}`", impl_block.trait_name),
impl_block.span,
));
}
};
for impl_method in &impl_block.methods {
let trait_sig = trait_methods.iter().find(|m| m.name == impl_method.name);
let trait_sig = match trait_sig {
Some(s) => s,
None => {
return Err(TypeError::new(
format!(
"impl for trait `{}` provides method `{}` that is not in the trait",
impl_block.trait_name, impl_method.name
),
impl_method.span,
));
}
};
if trait_sig.params.len() != impl_method.params.len() {
return Err(TypeError::new(
format!(
"impl method `{}::{}` has {} parameter(s), trait declares {}",
impl_block.trait_name,
impl_method.name,
impl_method.params.len(),
trait_sig.params.len()
),
impl_method.span,
));
}
for (idx, (impl_param, trait_param)) in impl_method
.params
.iter()
.zip(trait_sig.params.iter())
.enumerate()
{
let impl_ty = match &impl_param.type_expr {
Some(t) => Type::from_expr(t, &ctx.types),
None => continue,
};
let trait_ty = match &trait_param.type_expr {
Some(t) => Type::from_expr(t, &ctx.types),
None => continue,
};
if impl_ty != trait_ty {
return Err(TypeError::new(
format!(
"impl method `{}::{}` parameter {} has type {} but trait declares {}",
impl_block.trait_name,
impl_method.name,
idx,
impl_ty.display(),
trait_ty.display()
),
impl_param.span,
));
}
}
let impl_ret = Type::from_expr(&impl_method.return_type, &ctx.types);
let trait_ret = Type::from_expr(&trait_sig.return_type, &ctx.types);
if impl_ret != trait_ret {
return Err(TypeError::new(
format!(
"impl method `{}::{}` returns {} but trait declares {}",
impl_block.trait_name,
impl_method.name,
impl_ret.display(),
trait_ret.display()
),
impl_method.span,
));
}
}
}
for func in &program.functions {
check_function(&mut ctx, func)?;
}
for impl_block in &program.impls {
let head = match Type::from_expr(&impl_block.for_type, &ctx.types) {
Type::I64 => "i64".to_string(),
Type::F64 => "f64".to_string(),
Type::Bool => "bool".to_string(),
Type::Unit => "()".to_string(),
Type::Str => "String".to_string(),
Type::Tuple(_) => "tuple".to_string(),
Type::Array(_, _) => "array".to_string(),
Type::Option(_) => "Option".to_string(),
Type::Struct(name, _) | Type::Enum(name, _) | Type::Opaque(name) => name,
Type::Var(_) | Type::Unknown => continue,
};
for method in &impl_block.methods {
let mut renamed = method.clone();
renamed.name = format!("{}::{}::{}", impl_block.trait_name, head, method.name);
check_function(&mut ctx, &renamed)?;
}
}
Ok(())
}
fn check_function(ctx: &mut Ctx, func: &FunctionDef) -> Result<(), TypeError> {
let subst_snapshot = ctx.subst.clone();
ctx.push_scope();
let return_type = ctx
.functions
.get(&func.name)
.map(|s| s.return_type.clone())
.unwrap_or_else(|| ctx.fresh());
ctx.current_return = Some(return_type.clone());
let sig_params = ctx
.functions
.get(&func.name)
.map(|s| s.params.clone())
.unwrap_or_default();
for (param, param_type) in func.params.iter().zip(sig_params.iter()) {
bind_pattern(ctx, ¶m.pattern, param_type.clone());
}
let body_type = type_of_block(ctx, &func.body)?;
if !types_compatible(ctx, &body_type, &return_type) {
ctx.pop_scope();
ctx.current_return = None;
let body_resolved = body_type.apply(&ctx.subst);
let return_resolved = return_type.apply(&ctx.subst);
return Err(TypeError::new(
format!(
"function `{}` returns {} but body produces {}",
func.name,
return_resolved.display(),
body_resolved.display()
),
func.body.span,
));
}
ctx.pop_scope();
ctx.current_return = None;
if let Some(sig) = ctx.functions.get_mut(&func.name) {
sig.return_type = sig.return_type.apply(&ctx.subst);
for p in sig.params.iter_mut() {
*p = p.apply(&ctx.subst);
}
}
ctx.subst = subst_snapshot;
Ok(())
}
fn bind_pattern(ctx: &mut Ctx, pattern: &Pattern, ty: Type) {
match pattern {
Pattern::Variable(name, _) => ctx.add_local(name.clone(), ty),
Pattern::Wildcard(_) => {}
Pattern::Tuple(parts, _) => {
if let Type::Tuple(part_tys) = ty {
for (pat, pty) in parts.iter().zip(part_tys) {
bind_pattern(ctx, pat, pty);
}
} else {
for pat in parts {
{
let fresh = ctx.fresh();
bind_pattern(ctx, pat, fresh);
}
}
}
}
Pattern::Enum(enum_name, variant, sub_pats, _) => {
let payload = ctx
.enums
.get(enum_name)
.and_then(|vs| vs.get(variant))
.cloned();
for (i, sub_pat) in sub_pats.iter().enumerate() {
let sub_ty = payload
.as_ref()
.and_then(|tys| tys.get(i).cloned())
.unwrap_or_else(|| ctx.fresh());
bind_pattern(ctx, sub_pat, sub_ty);
}
}
Pattern::Struct(struct_name, field_pats, _) => {
let struct_fields = ctx.structs.get(struct_name).cloned();
for field_pat in field_pats {
let field_ty = struct_fields
.as_ref()
.and_then(|fields| fields.get(&field_pat.name).cloned())
.unwrap_or_else(|| ctx.fresh());
if let Some(pat) = &field_pat.pattern {
bind_pattern(ctx, pat, field_ty);
} else {
ctx.add_local(field_pat.name.clone(), field_ty);
}
}
}
Pattern::Literal(_, _) => {}
}
}
fn check_pattern_against_type(
ctx: &mut Ctx,
pattern: &Pattern,
scrutinee_ty: &Type,
) -> Result<(), TypeError> {
match pattern {
Pattern::Wildcard(_) | Pattern::Variable(_, _) => Ok(()),
Pattern::Literal(lit, span) => {
let lit_ty = match lit {
Literal::Int(_) => Type::I64,
Literal::Float(_) => Type::F64,
Literal::String(_) => Type::Str,
Literal::Bool(_) => Type::Bool,
Literal::Unit => Type::Unit,
};
if !types_compatible(ctx, &lit_ty, scrutinee_ty) {
return Err(TypeError::new(
format!(
"literal pattern of type {} does not match scrutinee type {}",
lit_ty.display(),
scrutinee_ty.display()
),
*span,
));
}
Ok(())
}
Pattern::Tuple(parts, span) => match scrutinee_ty {
Type::Tuple(elem_types) => {
if parts.len() != elem_types.len() {
return Err(TypeError::new(
format!(
"tuple pattern of {} elements does not match scrutinee {} of {} elements",
parts.len(),
scrutinee_ty.display(),
elem_types.len()
),
*span,
));
}
for (pat, elem_ty) in parts.iter().zip(elem_types.iter()) {
check_pattern_against_type(ctx, pat, elem_ty)?;
}
Ok(())
}
Type::Unknown | Type::Var(_) => Ok(()),
_ => Err(TypeError::new(
format!(
"tuple pattern does not match scrutinee type {}",
scrutinee_ty.display()
),
*span,
)),
},
Pattern::Enum(enum_name, variant, sub_pats, span) => {
match scrutinee_ty {
Type::Enum(scrutinee_name, _) if scrutinee_name == enum_name => {}
Type::Unknown | Type::Var(_) => return Ok(()),
_ => {
return Err(TypeError::new(
format!(
"enum pattern `{}::{}` does not match scrutinee type {}",
enum_name,
variant,
scrutinee_ty.display()
),
*span,
));
}
}
let payload = ctx
.enums
.get(enum_name)
.and_then(|vs| vs.get(variant))
.cloned()
.ok_or_else(|| {
TypeError::new(
format!("enum `{}` has no variant `{}`", enum_name, variant),
*span,
)
})?;
if sub_pats.len() != payload.len() {
return Err(TypeError::new(
format!(
"variant `{}::{}` expects {} payload elements, pattern has {}",
enum_name,
variant,
payload.len(),
sub_pats.len()
),
*span,
));
}
for (sub_pat, payload_ty) in sub_pats.iter().zip(payload.iter()) {
check_pattern_against_type(ctx, sub_pat, payload_ty)?;
}
Ok(())
}
Pattern::Struct(name, field_pats, span) => {
match scrutinee_ty {
Type::Struct(scrutinee_name, _) if scrutinee_name == name => {}
Type::Unknown | Type::Var(_) => return Ok(()),
_ => {
return Err(TypeError::new(
format!(
"struct pattern `{}` does not match scrutinee type {}",
name,
scrutinee_ty.display()
),
*span,
));
}
}
let fields = ctx
.structs
.get(name)
.cloned()
.ok_or_else(|| TypeError::new(format!("unknown struct `{}`", name), *span))?;
for field_pat in field_pats {
let field_ty = fields.get(&field_pat.name).ok_or_else(|| {
TypeError::new(
format!("struct `{}` has no field `{}`", name, field_pat.name),
field_pat.span,
)
})?;
if let Some(pat) = &field_pat.pattern {
check_pattern_against_type(ctx, pat, field_ty)?;
}
}
Ok(())
}
}
}
fn check_exhaustiveness(
ctx: &Ctx,
arms: &[MatchArm],
scrutinee_ty: &Type,
span: Span,
) -> Result<(), TypeError> {
let has_catchall = arms
.iter()
.any(|arm| matches!(arm.pattern, Pattern::Wildcard(_) | Pattern::Variable(_, _)));
if has_catchall {
return Ok(());
}
match scrutinee_ty {
Type::Bool => {
let mut has_true = false;
let mut has_false = false;
for arm in arms {
if let Pattern::Literal(Literal::Bool(b), _) = &arm.pattern {
if *b {
has_true = true;
} else {
has_false = true;
}
}
}
if !has_true || !has_false {
return Err(TypeError::new(
String::from(
"non-exhaustive match on bool: needs both true and false arms or a wildcard",
),
span,
));
}
Ok(())
}
Type::Enum(enum_name, _) => {
let variants = ctx
.enums
.get(enum_name)
.ok_or_else(|| TypeError::new(format!("unknown enum `{}`", enum_name), span))?;
let mut covered: BTreeSet<String> = BTreeSet::new();
for arm in arms {
if let Pattern::Enum(en, variant, _, _) = &arm.pattern
&& en == enum_name
{
covered.insert(variant.clone());
}
}
let missing: Vec<&String> = variants.keys().filter(|k| !covered.contains(*k)).collect();
if !missing.is_empty() {
let names: Vec<String> = missing.iter().map(|s| (*s).clone()).collect();
return Err(TypeError::new(
format!(
"non-exhaustive match on enum `{}`: missing variant(s) {}",
enum_name,
names.join(", ")
),
span,
));
}
Ok(())
}
Type::Unit => {
let has_unit_lit = arms
.iter()
.any(|arm| matches!(arm.pattern, Pattern::Literal(Literal::Unit, _)));
if has_unit_lit {
Ok(())
} else {
Err(TypeError::new(
String::from("non-exhaustive match on (): requires `()` or wildcard arm"),
span,
))
}
}
Type::Unknown | Type::Var(_) => Ok(()),
other => Err(TypeError::new(
format!(
"non-exhaustive match on {}: requires a wildcard arm",
other.display()
),
span,
)),
}
}
fn type_of_block(ctx: &mut Ctx, block: &Block) -> Result<Type, TypeError> {
ctx.push_scope();
for stmt in &block.stmts {
check_stmt(ctx, stmt)?;
}
let ty = match &block.tail_expr {
Some(e) => type_of_expr(ctx, e)?,
None => Type::Unit,
};
ctx.pop_scope();
Ok(ty)
}
fn check_stmt(ctx: &mut Ctx, stmt: &Stmt) -> Result<(), TypeError> {
match stmt {
Stmt::Let(let_stmt) => {
if let Pattern::Variable(name, _) = &let_stmt.pattern
&& let Expr::Closure { .. } = &let_stmt.value
&& let Some(scope) = ctx.locals.last_mut()
{
let pre_ty = ctx.vargen.fresh();
scope.insert(name.clone(), pre_ty);
}
let value_ty = type_of_expr(ctx, &let_stmt.value)?;
let bound_ty = match &let_stmt.type_expr {
Some(t) => {
let declared = Type::from_expr(t, &ctx.types);
if !types_compatible(ctx, &declared, &value_ty) {
return Err(TypeError::new(
format!(
"let binding declared as {} but value has type {}",
declared.display(),
value_ty.display()
),
let_stmt.span,
));
}
declared
}
None => value_ty,
};
bind_pattern(ctx, &let_stmt.pattern, bound_ty);
Ok(())
}
Stmt::For(for_stmt) => {
let elem_ty = match &for_stmt.iterable {
Iterable::Range(start, end) => {
let s = type_of_expr(ctx, start)?;
let e = type_of_expr(ctx, end)?;
if !types_compatible(ctx, &s, &Type::I64)
|| !types_compatible(ctx, &e, &Type::I64)
{
return Err(TypeError::new(
format!(
"for-range bounds must be i64, got {} and {}",
s.display(),
e.display()
),
for_stmt.span,
));
}
Type::I64
}
Iterable::Expr(e) => match type_of_expr(ctx, e)? {
Type::Array(inner, _) => *inner,
other => {
return Err(TypeError::new(
format!("for-in expects an array, got {}", other.display()),
for_stmt.span,
));
}
},
};
ctx.push_scope();
ctx.add_local(for_stmt.var.clone(), elem_ty);
let _ = type_of_block(ctx, &for_stmt.body)?;
ctx.pop_scope();
Ok(())
}
Stmt::Break(_) => Ok(()),
Stmt::DataFieldAssign {
data_name,
field,
value,
span,
} => {
let data_fields = ctx.data.get(data_name).ok_or_else(|| {
TypeError::new(format!("unknown data block `{}`", data_name), *span)
})?;
let declared = data_fields.get(field).ok_or_else(|| {
TypeError::new(
format!("unknown field `{}` on data block `{}`", field, data_name),
*span,
)
})?;
let declared = declared.clone();
let value_ty = type_of_expr(ctx, value)?;
if !types_compatible(ctx, &declared, &value_ty) {
return Err(TypeError::new(
format!(
"assignment to `{}.{}` expects {}, got {}",
data_name,
field,
declared.display(),
value_ty.display()
),
*span,
));
}
Ok(())
}
Stmt::Expr(e) => {
let _ = type_of_expr(ctx, e)?;
Ok(())
}
}
}
fn type_of_expr(ctx: &mut Ctx, expr: &Expr) -> Result<Type, TypeError> {
match expr {
Expr::Literal { value, .. } => Ok(match value {
Literal::Int(_) => Type::I64,
Literal::Float(_) => Type::F64,
Literal::String(_) => Type::Str,
Literal::Bool(_) => Type::Bool,
Literal::Unit => Type::Unit,
}),
Expr::Ident { name, span } => {
if let Some(ty) = ctx.lookup_local(name) {
return Ok(ty.clone());
}
if ctx.data.contains_key(name) {
return Ok(Type::Struct(name.clone(), Vec::new()));
}
Err(TypeError::new(
format!("undefined identifier `{}`", name),
*span,
))
}
Expr::BinOp {
op,
left,
right,
span,
} => {
let lt = type_of_expr(ctx, left)?;
let rt = type_of_expr(ctx, right)?;
match op {
BinOp::Add => {
if matches!(lt, Type::I64) && matches!(rt, Type::I64) {
Ok(Type::I64)
} else if matches!(lt, Type::F64) && matches!(rt, Type::F64) {
Ok(Type::F64)
} else if matches!(lt, Type::Str) && matches!(rt, Type::Str) {
Ok(Type::Str)
} else if matches!(lt, Type::Unknown | Type::Var(_))
|| matches!(rt, Type::Unknown | Type::Var(_))
{
Ok(ctx.fresh())
} else {
Err(TypeError::new(
format!("cannot add {} and {}", lt.display(), rt.display()),
*span,
))
}
}
BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod => {
if matches!(lt, Type::I64) && matches!(rt, Type::I64) {
Ok(Type::I64)
} else if matches!(lt, Type::F64) && matches!(rt, Type::F64) {
Ok(Type::F64)
} else if matches!(lt, Type::Unknown | Type::Var(_))
|| matches!(rt, Type::Unknown | Type::Var(_))
{
Ok(ctx.fresh())
} else {
Err(TypeError::new(
format!(
"arithmetic on incompatible types {} and {}",
lt.display(),
rt.display()
),
*span,
))
}
}
BinOp::Eq | BinOp::NotEq => {
if !types_compatible(ctx, <, &rt) {
return Err(TypeError::new(
format!("cannot compare {} and {}", lt.display(), rt.display()),
*span,
));
}
Ok(Type::Bool)
}
BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq => {
if !types_compatible(ctx, <, &rt) {
return Err(TypeError::new(
format!("cannot order {} and {}", lt.display(), rt.display()),
*span,
));
}
Ok(Type::Bool)
}
BinOp::And | BinOp::Or => {
if !types_compatible(ctx, <, &Type::Bool)
|| !types_compatible(ctx, &rt, &Type::Bool)
{
return Err(TypeError::new(
format!(
"logical operator requires bool operands, got {} and {}",
lt.display(),
rt.display()
),
*span,
));
}
Ok(Type::Bool)
}
}
}
Expr::UnaryOp { op, operand, span } => {
let ty = type_of_expr(ctx, operand)?;
match op {
UnaryOp::Neg => match ty {
Type::I64 | Type::F64 | Type::Unknown | Type::Var(_) => Ok(ty),
other => Err(TypeError::new(
format!("cannot negate {}", other.display()),
*span,
)),
},
UnaryOp::Not => {
if !types_compatible(ctx, &ty, &Type::Bool) {
return Err(TypeError::new(
format!("`not` requires bool, got {}", ty.display()),
*span,
));
}
Ok(Type::Bool)
}
}
}
Expr::Call { name, args, span } => {
if ctx.lookup_local(name).is_some() {
for arg in args {
type_of_expr(ctx, arg)?;
}
return Ok(ctx.fresh());
}
let sig = match ctx.functions.get(name).cloned() {
Some(s) => s,
None => {
if ctx.natives.contains(name) || name.contains("::") {
for arg in args {
type_of_expr(ctx, arg)?;
}
return Ok(ctx.fresh());
}
return Err(TypeError::new(
format!("undefined function `{}`", name),
*span,
));
}
};
if args.len() != sig.params.len() {
return Err(TypeError::new(
format!(
"function `{}` expects {} arguments, got {}",
name,
sig.params.len(),
args.len()
),
*span,
));
}
let (inst_params, inst_return, fresh_vars) = instantiate_sig(ctx, &sig);
for (arg, param_ty) in args.iter().zip(inst_params.iter()) {
let arg_ty = type_of_expr(ctx, arg)?;
if !types_compatible(ctx, &arg_ty, param_ty) {
return Err(TypeError::new(
format!(
"argument to `{}` expects {}, got {}",
name,
param_ty.display(),
arg_ty.display()
),
arg.span(),
));
}
}
for (var, bounds) in fresh_vars.iter().zip(sig.type_param_bounds.iter()) {
if bounds.is_empty() {
continue;
}
let resolved = var.apply(&ctx.subst);
let head = match type_head_name(&resolved) {
Some(h) => h,
None => continue,
};
for bound in bounds {
let satisfies = ctx
.impls
.get(bound)
.map(|set| set.contains(&head))
.unwrap_or(false);
if !satisfies {
return Err(TypeError::new(
format!(
"type `{}` does not implement trait `{}` required by `{}`",
head, bound, name
),
*span,
));
}
}
}
Ok(inst_return)
}
Expr::Pipeline {
left,
func,
args,
span,
} => {
let left_ty = type_of_expr(ctx, left)?;
if let Some(sig) = ctx.functions.get(func).cloned() {
if sig.params.len() != args.len() + 1 {
return Err(TypeError::new(
format!(
"pipeline target `{}` expects {} arguments, got {}",
func,
sig.params.len(),
args.len() + 1
),
expr.span(),
));
}
if let Some(first_param) = sig.params.first()
&& !types_compatible(ctx, &left_ty, first_param)
{
return Err(TypeError::new(
format!(
"pipeline left side has type {} but `{}` expects {}",
left_ty.display(),
func,
first_param.display()
),
expr.span(),
));
}
for (arg, param_ty) in args.iter().zip(sig.params.iter().skip(1)) {
let arg_ty = type_of_expr(ctx, arg)?;
if !types_compatible(ctx, &arg_ty, param_ty) {
return Err(TypeError::new(
format!(
"argument to `{}` expects {}, got {}",
func,
param_ty.display(),
arg_ty.display()
),
arg.span(),
));
}
}
Ok(sig.return_type)
} else if ctx.natives.contains(func) || func.contains("::") {
for arg in args {
let _ = type_of_expr(ctx, arg)?;
}
Ok(ctx.fresh())
} else {
Err(TypeError::new(
format!("undefined function `{}`", func),
*span,
))
}
}
Expr::Yield { value, .. } => {
let _ = type_of_expr(ctx, value)?;
Ok(ctx.fresh())
}
Expr::If {
condition,
then_block,
else_block,
span,
} => {
let cond_ty = type_of_expr(ctx, condition)?;
if !types_compatible(ctx, &cond_ty, &Type::Bool) {
return Err(TypeError::new(
format!("if condition must be bool, got {}", cond_ty.display()),
*span,
));
}
let then_ty = type_of_block(ctx, then_block)?;
match else_block {
Some(b) => {
let else_ty = type_of_block(ctx, b)?;
if !types_compatible(ctx, &then_ty, &else_ty) {
return Err(TypeError::new(
format!(
"if branches have differing types {} and {}",
then_ty.display(),
else_ty.display()
),
*span,
));
}
Ok(then_ty)
}
None => Ok(Type::Unit),
}
}
Expr::Match {
scrutinee,
arms,
span,
} => {
let scrutinee_ty = type_of_expr(ctx, scrutinee)?;
let mut common: Option<Type> = None;
for arm in arms {
check_pattern_against_type(ctx, &arm.pattern, &scrutinee_ty)?;
ctx.push_scope();
bind_pattern(ctx, &arm.pattern, scrutinee_ty.clone());
let arm_ty = type_of_expr(ctx, &arm.expr)?;
ctx.pop_scope();
match &common {
None => common = Some(arm_ty),
Some(c) => {
if !types_compatible(ctx, c, &arm_ty) {
return Err(TypeError::new(
format!(
"match arms have differing types {} and {}",
c.display(),
arm_ty.display()
),
arm.span,
));
}
}
}
}
check_exhaustiveness(ctx, arms, &scrutinee_ty, *span)?;
Ok(common.unwrap_or(Type::Unit))
}
Expr::Loop { body, .. } => {
let _ = type_of_block(ctx, body)?;
Ok(Type::Unit)
}
Expr::FieldAccess {
object,
field,
span,
} => {
let obj_ty = type_of_expr(ctx, object)?;
match obj_ty {
Type::Struct(ref name, ref args) => {
let abstract_vars = ctx
.struct_type_param_vars
.get(name)
.cloned()
.unwrap_or_default();
let mut inst = Subst::new();
for (abstract_var, concrete) in abstract_vars.iter().zip(args.iter()) {
if let Type::Var(v) = abstract_var {
inst.insert(*v, concrete.clone());
}
}
if let Some(fields) = ctx.structs.get(name)
&& let Some(t) = fields.get(field)
{
return Ok(t.apply(&inst));
}
if let Some(fields) = ctx.data.get(name)
&& let Some(t) = fields.get(field)
{
return Ok(t.clone());
}
Err(TypeError::new(
format!("type {} has no field `{}`", obj_ty.display(), field),
*span,
))
}
Type::Unknown | Type::Var(_) => Ok(ctx.fresh()),
other => Err(TypeError::new(
format!("field access on non-struct type {}", other.display()),
*span,
)),
}
}
Expr::TupleIndex {
object,
index,
span,
} => {
let obj_ty = type_of_expr(ctx, object)?;
match obj_ty {
Type::Tuple(elems) => {
if (*index as usize) < elems.len() {
Ok(elems[*index as usize].clone())
} else {
Err(TypeError::new(
format!(
"tuple index {} out of bounds for {}",
index,
Type::Tuple(elems).display()
),
*span,
))
}
}
Type::Unknown => Ok(ctx.fresh()),
other => Err(TypeError::new(
format!("tuple index on non-tuple type {}", other.display()),
*span,
)),
}
}
Expr::ArrayIndex {
object,
index,
span,
} => {
let obj_ty = type_of_expr(ctx, object)?;
let idx_ty = type_of_expr(ctx, index)?;
if !types_compatible(ctx, &idx_ty, &Type::I64) {
return Err(TypeError::new(
format!("array index must be i64, got {}", idx_ty.display()),
*span,
));
}
match obj_ty {
Type::Array(inner, _) => Ok(*inner),
Type::Unknown => Ok(ctx.fresh()),
other => Err(TypeError::new(
format!("array index on non-array type {}", other.display()),
*span,
)),
}
}
Expr::StructInit { name, fields, span } => {
let declared_fields = ctx
.structs
.get(name)
.cloned()
.ok_or_else(|| TypeError::new(format!("unknown struct `{}`", name), *span))?;
if fields.len() != declared_fields.len() {
return Err(TypeError::new(
format!(
"struct `{}` expects {} fields, got {}",
name,
declared_fields.len(),
fields.len()
),
*span,
));
}
let abstract_vars = ctx
.struct_type_param_vars
.get(name)
.cloned()
.unwrap_or_default();
let (inst, type_args) = build_instance_subst(ctx, &abstract_vars);
for init in fields {
let declared = declared_fields.get(&init.name).ok_or_else(|| {
TypeError::new(
format!("struct `{}` has no field `{}`", name, init.name),
init.span,
)
})?;
let declared_inst = declared.apply(&inst);
let value_ty = type_of_expr(ctx, &init.value)?;
if !types_compatible(ctx, &value_ty, &declared_inst) {
return Err(TypeError::new(
format!(
"field `{}.{}` expects {}, got {}",
name,
init.name,
declared_inst.display(),
value_ty.display()
),
init.span,
));
}
}
Ok(Type::Struct(name.clone(), type_args))
}
Expr::EnumVariant {
enum_name,
variant,
args,
span,
} => {
let payload_types = ctx
.enums
.get(enum_name)
.and_then(|vs| vs.get(variant))
.cloned();
match payload_types {
Some(types) => {
if types.len() != args.len() {
return Err(TypeError::new(
format!(
"enum `{}::{}` expects {} arguments, got {}",
enum_name,
variant,
types.len(),
args.len()
),
*span,
));
}
let abstract_vars = ctx
.enum_type_param_vars
.get(enum_name)
.cloned()
.unwrap_or_default();
let (inst, type_args) = build_instance_subst(ctx, &abstract_vars);
for (arg, expected) in args.iter().zip(types.iter()) {
let expected_inst = expected.apply(&inst);
let arg_ty = type_of_expr(ctx, arg)?;
if !types_compatible(ctx, &arg_ty, &expected_inst) {
return Err(TypeError::new(
format!(
"enum payload expects {}, got {}",
expected_inst.display(),
arg_ty.display()
),
arg.span(),
));
}
}
Ok(Type::Enum(enum_name.clone(), type_args))
}
None => {
if enum_name == "Option" {
for arg in args {
let _ = type_of_expr(ctx, arg)?;
}
return Ok(Type::Option(Box::new(Type::Unknown)));
}
Err(TypeError::new(
format!("unknown enum variant `{}::{}`", enum_name, variant),
*span,
))
}
}
}
Expr::ArrayLiteral { elements, span } => {
let mut elem_ty: Option<Type> = None;
for e in elements {
let t = type_of_expr(ctx, e)?;
match &elem_ty {
None => elem_ty = Some(t),
Some(et) => {
if !types_compatible(ctx, et, &t) {
return Err(TypeError::new(
format!(
"array elements have differing types {} and {}",
et.display(),
t.display()
),
*span,
));
}
}
}
}
Ok(Type::Array(
Box::new(elem_ty.unwrap_or_else(|| ctx.fresh())),
elements.len() as i64,
))
}
Expr::TupleLiteral { elements, .. } => {
let mut tys = Vec::with_capacity(elements.len());
for e in elements {
tys.push(type_of_expr(ctx, e)?);
}
Ok(Type::Tuple(tys))
}
Expr::Cast { expr, target, span } => {
let from_ty = type_of_expr(ctx, expr)?;
let to_ty = Type::from_expr(target, &ctx.types);
match (&from_ty, &to_ty) {
(Type::I64, Type::F64) | (Type::F64, Type::I64) => Ok(to_ty),
(Type::Unknown, _) | (_, Type::Unknown) => Ok(to_ty),
(a, b) if a == b => Ok(to_ty),
_ => Err(TypeError::new(
format!("cannot cast {} to {}", from_ty.display(), to_ty.display()),
*span,
)),
}
}
Expr::Placeholder { .. } => Ok(ctx.fresh()),
Expr::ClosureRef { .. } => Ok(ctx.fresh()),
Expr::Closure { params, body, .. } => {
ctx.push_scope();
for param in params {
let t = match ¶m.type_expr {
Some(t) => Type::from_expr(t, &ctx.types),
None => ctx.fresh(),
};
bind_pattern(ctx, ¶m.pattern, t);
}
let _body_ty = type_of_block(ctx, body)?;
ctx.pop_scope();
Ok(ctx.fresh())
}
Expr::MethodCall {
receiver,
method,
args,
span,
} => {
let receiver_ty = type_of_expr(ctx, receiver)?;
let receiver_resolved = receiver_ty.apply(&ctx.subst);
let head = match type_head_name(&receiver_resolved) {
Some(h) => h,
None => return Ok(ctx.fresh()),
};
let mut resolved: Option<String> = None;
for trait_name in ctx.traits.keys() {
let candidate = format!("{}::{}::{}", trait_name, head, method);
if ctx.functions.contains_key(&candidate) {
resolved = Some(candidate);
break;
}
}
let mangled = resolved.unwrap_or_default();
let sig = match ctx.functions.get(&mangled).cloned() {
Some(s) => s,
None => {
return Err(TypeError::new(
format!(
"type `{}` has no method `{}` from any trait in scope",
head, method
),
*span,
));
}
};
let expected = sig.params.len();
let actual = args.len() + 1;
if expected != actual {
return Err(TypeError::new(
format!(
"method `{}` expects {} arguments (including receiver), got {}",
method, expected, actual
),
*span,
));
}
let (inst_params, inst_return, _fresh_vars) = instantiate_sig(ctx, &sig);
if let Some(first) = inst_params.first()
&& !types_compatible(ctx, &receiver_resolved, first)
{
return Err(TypeError::new(
format!(
"method `{}` receiver expects {}, got {}",
method,
first.display(),
receiver_resolved.display()
),
receiver.span(),
));
}
for (arg, param_ty) in args.iter().zip(inst_params.iter().skip(1)) {
let arg_ty = type_of_expr(ctx, arg)?;
if !types_compatible(ctx, &arg_ty, param_ty) {
return Err(TypeError::new(
format!(
"method `{}` argument expects {}, got {}",
method,
param_ty.display(),
arg_ty.display()
),
arg.span(),
));
}
}
Ok(inst_return)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::tokenize;
use crate::parser::parse;
fn check_src(src: &str) -> Result<(), TypeError> {
let tokens = tokenize(src).expect("lex");
let program = parse(&tokens).expect("parse");
check(&program)
}
fn compile_src(src: &str) -> Result<(), alloc::string::String> {
let tokens = tokenize(src).expect("lex");
let program = parse(&tokens).expect("parse");
crate::compiler::compile(&program)
.map(|_| ())
.map_err(|e| e.message)
}
#[test]
fn simple_function_type_checks() {
check_src("fn main() -> i64 { 1 + 2 }").unwrap();
}
#[test]
fn return_type_mismatch_rejected() {
let err = check_src("fn main() -> i64 { true }").unwrap_err();
assert!(err.message.contains("returns i64"));
}
#[test]
fn arithmetic_type_mismatch_rejected() {
let err = check_src("fn main() -> i64 { 1 + 2.0 }").unwrap_err();
assert!(err.message.contains("cannot add"));
}
#[test]
fn function_call_arg_count_checked() {
let err = check_src("fn add(a: i64, b: i64) -> i64 { a + b }\nfn main() -> i64 { add(1) }")
.unwrap_err();
assert!(err.message.contains("expects 2"));
}
#[test]
fn function_call_arg_type_checked() {
let err =
check_src("fn double(x: i64) -> i64 { x * 2 }\nfn main() -> i64 { double(true) }")
.unwrap_err();
assert!(err.message.contains("expects i64"));
}
#[test]
fn let_binding_type_mismatch_rejected() {
let err = check_src("fn main() -> i64 { let x: i64 = true; 0 }").unwrap_err();
assert!(err.message.contains("declared as i64"));
}
#[test]
fn let_binding_inferred_from_value() {
check_src("fn main() -> i64 { let x = 1; x + 1 }").unwrap();
}
#[test]
fn if_branch_mismatch_rejected() {
let err = check_src("fn main() -> i64 { if true { 1 } else { false } }").unwrap_err();
assert!(err.message.contains("if branches"));
}
#[test]
fn struct_field_access_checks() {
check_src(
"struct P { x: i64, y: i64 }\nfn main() -> i64 { let p = P { x: 1, y: 2 }; p.x }",
)
.unwrap();
}
#[test]
fn struct_unknown_field_rejected() {
let err = check_src("struct P { x: i64 }\nfn main() -> i64 { let p = P { x: 1 }; p.y }")
.unwrap_err();
assert!(err.message.contains("no field"));
}
#[test]
fn cast_int_to_float_admissible() {
check_src("fn main() -> f64 { let x: i64 = 1; x as f64 }").unwrap();
}
#[test]
fn cast_bool_to_int_rejected() {
let err = check_src("fn main() -> i64 { true as i64 }").unwrap_err();
assert!(err.message.contains("cannot cast"));
}
#[test]
fn undefined_identifier_rejected() {
let err = check_src("fn main() -> i64 { x }").unwrap_err();
assert!(err.message.contains("undefined"));
}
#[test]
fn undefined_function_rejected() {
let err = check_src("fn main() -> i64 { foo() }").unwrap_err();
assert!(err.message.contains("undefined function `foo`"));
}
#[test]
fn used_native_accepted() {
check_src("use math::sqrt\nfn main() -> f64 { math::sqrt(9.0) }").unwrap();
}
#[test]
fn qualified_call_treated_as_native() {
check_src("fn main() -> () { audio::do_thing(1, 2.0) }").unwrap();
}
#[test]
fn enum_pattern_unknown_variant_rejected() {
let err = check_src(
"enum Color { Red, Green }\n\
fn main() -> i64 { match Color::Red() { Color::Blue() => 1, _ => 0 } }",
)
.unwrap_err();
assert!(
err.message.contains("no variant `Blue`")
|| err.message.contains("unknown enum variant"),
"unexpected error: {}",
err.message
);
}
#[test]
fn enum_pattern_wrong_arity_rejected() {
let err = check_src(
"enum Shape { Square(i64), Circle(i64) }\n\
fn main() -> i64 { match Shape::Square(1) { Shape::Square(a, b) => 0, _ => 1 } }",
)
.unwrap_err();
assert!(
err.message.contains("payload elements"),
"unexpected error: {}",
err.message
);
}
#[test]
fn tuple_pattern_wrong_arity_rejected() {
let err =
check_src("fn main() -> i64 { match (1, 2) { (a, b, c) => 0, _ => 1 } }").unwrap_err();
assert!(err.message.contains("tuple pattern"));
}
#[test]
fn tuple_pattern_against_non_tuple_rejected() {
let err = check_src("fn main() -> i64 { match 5 { (a, b) => 0, _ => 1 } }").unwrap_err();
assert!(err.message.contains("tuple pattern"));
}
#[test]
fn literal_pattern_type_mismatch_rejected() {
let err = check_src("fn main() -> i64 { match 5 { true => 1, _ => 0 } }").unwrap_err();
assert!(err.message.contains("literal pattern"));
}
#[test]
fn enum_match_missing_variant_rejected() {
let err = check_src(
"enum Color { Red, Green, Blue }\n\
fn main() -> i64 { match Color::Red() { Color::Red() => 0, Color::Green() => 1 } }",
)
.unwrap_err();
assert!(err.message.contains("non-exhaustive match"));
assert!(err.message.contains("Blue"));
}
#[test]
fn enum_match_with_wildcard_accepted() {
check_src(
"enum Color { Red, Green, Blue }\n\
fn main() -> i64 { match Color::Red() { Color::Red() => 0, _ => 1 } }",
)
.unwrap();
}
#[test]
fn enum_match_with_all_variants_accepted() {
check_src(
"enum Color { Red, Green }\n\
fn main() -> i64 { match Color::Red() { Color::Red() => 0, Color::Green() => 1 } }",
)
.unwrap();
}
#[test]
fn bool_match_missing_arm_rejected() {
let err = check_src("fn main() -> i64 { match true { true => 1 } }").unwrap_err();
assert!(err.message.contains("non-exhaustive match"));
}
#[test]
fn bool_match_complete_accepted() {
check_src("fn main() -> i64 { match true { true => 1, false => 0 } }").unwrap();
}
#[test]
fn i64_match_without_wildcard_rejected() {
let err = check_src("fn main() -> i64 { match 1 { 1 => 1, 2 => 2 } }").unwrap_err();
assert!(err.message.contains("non-exhaustive match"));
}
#[test]
fn i64_match_with_wildcard_accepted() {
check_src("fn main() -> i64 { match 1 { 1 => 1, _ => 0 } }").unwrap();
}
#[test]
fn vargen_allocates_fresh_variables() {
let mut g = VarGen::default();
let a = g.fresh();
let b = g.fresh();
match (a, b) {
(Type::Var(0), Type::Var(1)) => {}
other => panic!("expected fresh variables 0 and 1, got {:?}", other),
}
assert_eq!(g.count(), 2);
}
#[test]
fn unify_identical_primitives() {
let mut s = Subst::new();
unify(&Type::I64, &Type::I64, &mut s).unwrap();
unify(&Type::Bool, &Type::Bool, &mut s).unwrap();
unify(&Type::Unit, &Type::Unit, &mut s).unwrap();
unify(&Type::Str, &Type::Str, &mut s).unwrap();
assert!(s.is_empty());
}
#[test]
fn unify_distinct_primitives_fails() {
let mut s = Subst::new();
let err = unify(&Type::I64, &Type::F64, &mut s).unwrap_err();
match err {
UnifyError::Mismatch { left, right } => {
assert_eq!(left, Type::I64);
assert_eq!(right, Type::F64);
}
other => panic!("expected Mismatch, got {:?}", other),
}
}
#[test]
fn unify_var_with_concrete_binds() {
let mut s = Subst::new();
unify(&Type::Var(0), &Type::I64, &mut s).unwrap();
assert_eq!(s.get(0), Some(&Type::I64));
}
#[test]
fn unify_concrete_with_var_binds() {
let mut s = Subst::new();
unify(&Type::I64, &Type::Var(0), &mut s).unwrap();
assert_eq!(s.get(0), Some(&Type::I64));
}
#[test]
fn unify_var_with_var_binds_one_to_other() {
let mut s = Subst::new();
unify(&Type::Var(0), &Type::Var(1), &mut s).unwrap();
assert_eq!(s.len(), 1);
}
#[test]
fn unify_same_var_succeeds_with_no_binding() {
let mut s = Subst::new();
unify(&Type::Var(0), &Type::Var(0), &mut s).unwrap();
assert!(s.is_empty());
}
#[test]
fn unify_tuple_pairwise() {
let mut s = Subst::new();
let t1 = Type::Tuple(alloc::vec![Type::Var(0), Type::Bool]);
let t2 = Type::Tuple(alloc::vec![Type::I64, Type::Var(1)]);
unify(&t1, &t2, &mut s).unwrap();
assert_eq!(s.get(0), Some(&Type::I64));
assert_eq!(s.get(1), Some(&Type::Bool));
}
#[test]
fn unify_tuple_arity_mismatch() {
let mut s = Subst::new();
let t1 = Type::Tuple(alloc::vec![Type::I64, Type::Bool]);
let t2 = Type::Tuple(alloc::vec![Type::I64]);
let err = unify(&t1, &t2, &mut s).unwrap_err();
match err {
UnifyError::TupleArityMismatch { left, right } => {
assert_eq!(left, 2);
assert_eq!(right, 1);
}
other => panic!("expected TupleArityMismatch, got {:?}", other),
}
}
#[test]
fn unify_array_length_mismatch() {
let mut s = Subst::new();
let t1 = Type::Array(Box::new(Type::I64), 3);
let t2 = Type::Array(Box::new(Type::I64), 4);
let err = unify(&t1, &t2, &mut s).unwrap_err();
match err {
UnifyError::ArrayLengthMismatch { left, right } => {
assert_eq!(left, 3);
assert_eq!(right, 4);
}
other => panic!("expected ArrayLengthMismatch, got {:?}", other),
}
}
#[test]
fn unify_array_element_types_unify() {
let mut s = Subst::new();
let t1 = Type::Array(Box::new(Type::Var(0)), 3);
let t2 = Type::Array(Box::new(Type::I64), 3);
unify(&t1, &t2, &mut s).unwrap();
assert_eq!(s.get(0), Some(&Type::I64));
}
#[test]
fn unify_option_inner_types_unify() {
let mut s = Subst::new();
let t1 = Type::Option(Box::new(Type::Var(0)));
let t2 = Type::Option(Box::new(Type::Bool));
unify(&t1, &t2, &mut s).unwrap();
assert_eq!(s.get(0), Some(&Type::Bool));
}
#[test]
fn unify_named_struct_same_name_succeeds() {
let mut s = Subst::new();
unify(
&Type::Struct("Point".to_string(), Vec::new()),
&Type::Struct("Point".to_string(), Vec::new()),
&mut s,
)
.unwrap();
}
#[test]
fn unify_named_struct_different_name_fails() {
let mut s = Subst::new();
let err = unify(
&Type::Struct("Point".to_string(), Vec::new()),
&Type::Struct("Square".to_string(), Vec::new()),
&mut s,
)
.unwrap_err();
assert!(matches!(err, UnifyError::Mismatch { .. }));
}
#[test]
fn unify_occurs_check_rejects_self_reference() {
let mut s = Subst::new();
let t1 = Type::Var(0);
let t2 = Type::Tuple(alloc::vec![Type::Var(0), Type::I64]);
let err = unify(&t1, &t2, &mut s).unwrap_err();
assert!(matches!(err, UnifyError::OccursCheck { .. }));
}
#[test]
fn apply_substitution_resolves_variable() {
let mut s = Subst::new();
s.insert(0, Type::I64);
let t = Type::Tuple(alloc::vec![Type::Var(0), Type::Bool]);
let resolved = t.apply(&s);
assert_eq!(resolved, Type::Tuple(alloc::vec![Type::I64, Type::Bool]));
}
#[test]
fn apply_substitution_resolves_chain() {
let mut s = Subst::new();
s.insert(0, Type::Var(1));
s.insert(1, Type::Bool);
let resolved = Type::Var(0).apply(&s);
assert_eq!(resolved, Type::Bool);
}
#[test]
fn unify_propagates_through_existing_substitution() {
let mut s = Subst::new();
unify(&Type::Var(0), &Type::I64, &mut s).unwrap();
unify(&Type::Var(0), &Type::Var(1), &mut s).unwrap();
let resolved = Type::Var(1).apply(&s);
assert_eq!(resolved, Type::I64);
}
#[test]
fn generic_identity_function_typechecks() {
check_src("fn id<T>(x: T) -> T { x }\nfn main() -> i64 { id(42) }").unwrap();
}
#[test]
fn generic_function_called_with_two_types_separately() {
check_src(
"fn id<T>(x: T) -> T { x }\n\
fn main() -> i64 {\n\
let a = id(1);\n\
let b = id(true);\n\
a\n\
}",
)
.unwrap();
}
#[test]
fn generic_function_with_two_type_params() {
check_src(
"fn first<T, U>(a: T, b: U) -> T { a }\n\
fn main() -> i64 { first(1, true) }",
)
.unwrap();
}
#[test]
fn generic_function_arity_mismatch_rejected() {
let err =
check_src("fn id<T>(x: T) -> T { x }\nfn main() -> i64 { id(1, 2) }").unwrap_err();
assert!(err.message.contains("expects 1 arguments"));
}
#[test]
fn generic_struct_with_one_param_typechecks() {
check_src(
"struct Cell<T> { value: T }\n\
fn main() -> i64 {\n\
let c = Cell { value: 42 };\n\
c.value\n\
}",
)
.unwrap();
}
#[test]
fn generic_struct_with_two_params_typechecks() {
check_src(
"struct Pair<T, U> { a: T, b: U }\n\
fn main() -> i64 {\n\
let p = Pair { a: 1, b: true };\n\
p.a\n\
}",
)
.unwrap();
}
#[test]
fn generic_struct_field_access_uses_instantiation() {
check_src(
"struct Cell<T> { value: T }\n\
fn main() -> i64 {\n\
let p = Cell { value: 1 };\n\
let q = Cell { value: true };\n\
let _ = q.value;\n\
p.value\n\
}",
)
.unwrap();
}
#[test]
fn generic_enum_construction_typechecks() {
check_src(
"enum Maybe<T> { Just(T), Nothing }\n\
fn main() -> i64 {\n\
let m = Maybe::Just(42);\n\
0\n\
}",
)
.unwrap();
}
#[test]
fn generic_struct_pattern_match_on_enum() {
check_src(
"enum Maybe<T> { Just(T), Nothing }\n\
fn main() -> i64 {\n\
let m = Maybe::Just(42);\n\
match m {\n\
Maybe::Just(x) => x,\n\
Maybe::Nothing => 0,\n\
}\n\
}",
)
.unwrap();
}
#[test]
fn generic_struct_referenced_by_field_type() {
check_src(
"struct Cell<T> { value: T }\n\
struct Wrap<T> { inner: Cell<T> }\n\
fn main() -> i64 {\n\
let w = Wrap { inner: Cell { value: 7 } };\n\
w.inner.value\n\
}",
)
.unwrap();
}
#[test]
fn trait_declaration_parses_and_typechecks() {
check_src(
"trait Numeric { fn one() -> i64; }\n\
impl Numeric for i64 { fn one() -> i64 { 1 } }\n\
fn use_it<T: Numeric>(x: T) -> T { x }\n\
fn main() -> i64 { use_it(7) }",
)
.unwrap();
}
#[test]
fn trait_bound_satisfied_by_impl() {
check_src(
"trait Tag { fn tag() -> i64; }\n\
impl Tag for bool { fn tag() -> i64 { 1 } }\n\
fn use_tag<T: Tag>(x: T) -> i64 { 0 }\n\
fn main() -> i64 { use_tag(true) }",
)
.unwrap();
}
#[test]
fn trait_bound_unsatisfied_rejects_call() {
let err = check_src(
"trait Tag { fn tag() -> i64; }\n\
impl Tag for bool { fn tag() -> i64 { 1 } }\n\
fn use_tag<T: Tag>(x: T) -> i64 { 0 }\n\
fn main() -> i64 { use_tag(7) }",
)
.unwrap_err();
assert!(
err.message.contains("does not implement"),
"unexpected error: {}",
err.message,
);
}
#[test]
fn unbounded_type_param_admits_any_type() {
check_src(
"fn id<T>(x: T) -> T { x }\n\
fn main() -> i64 { id(42) }",
)
.unwrap();
}
#[test]
fn multiple_trait_bounds_on_one_param() {
check_src(
"trait A { fn a() -> i64; }\n\
trait B { fn b() -> i64; }\n\
impl A for i64 { fn a() -> i64 { 1 } }\n\
impl B for i64 { fn b() -> i64 { 2 } }\n\
fn use_both<T: A + B>(x: T) -> i64 { 0 }\n\
fn main() -> i64 { use_both(7) }",
)
.unwrap();
}
#[test]
fn impl_method_with_extra_method_rejected() {
let err = check_src(
"trait T { fn one() -> i64; }\n\
impl T for i64 {\n\
fn one() -> i64 { 1 }\n\
fn extra() -> i64 { 2 }\n\
}\n\
fn main() -> i64 { 0 }",
)
.unwrap_err();
assert!(err.message.contains("not in the trait"));
}
#[test]
fn impl_method_arity_mismatch_rejected() {
let err = check_src(
"trait T { fn one() -> i64; }\n\
impl T for i64 { fn one(x: i64) -> i64 { x } }\n\
fn main() -> i64 { 0 }",
)
.unwrap_err();
assert!(err.message.contains("parameter"));
}
#[test]
fn monomorphize_generic_method_dispatch() {
check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 { use_doubler(21) }",
)
.unwrap();
}
#[test]
fn method_call_resolves_to_impl() {
check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn main() -> i64 {\n\
let n: i64 = 21;\n\
n.double()\n\
}",
)
.unwrap();
}
#[test]
fn method_call_unknown_method_rejected() {
let err = check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn main() -> i64 {\n\
let n: i64 = 21;\n\
n.triple()\n\
}",
)
.unwrap_err();
assert!(err.message.contains("no method"));
}
#[test]
fn closure_executes_end_to_end() {
check_src(
"fn main() -> i64 {\n\
let f = |x: i64| x + 1;\n\
f(41)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_function_call() {
check_src(
"fn make42() -> i64 { 42 }\n\
fn id<T>(x: T) -> T { x }\n\
fn main() -> i64 { id(make42()) }",
)
.unwrap();
}
#[test]
fn recursive_closure_rejected_by_compile_pipeline() {
let err = compile_src(
"fn main() -> i64 {\n\
let fact = |n: i64| if n <= 1 { 1 } else { n * fact(n - 1) };\n\
fact(5)\n\
}",
)
.unwrap_err();
assert!(
err.contains("MakeRecursiveClosure") || err.contains("CallIndirect"),
"unexpected error: {}",
err,
);
}
#[test]
fn recursive_closure_with_capture_rejected_by_compile_pipeline() {
let err = compile_src(
"fn main() -> i64 {\n\
let base: i64 = 1000;\n\
let fact = |n: i64| if n <= 1 { base } else { n * fact(n - 1) };\n\
fact(3)\n\
}",
)
.unwrap_err();
assert!(
err.contains("MakeRecursiveClosure") || err.contains("CallIndirect"),
"unexpected error: {}",
err,
);
}
#[test]
fn monomorphize_inference_through_field_access() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
struct Holder { value: i64 }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
let h = Holder { value: 21 };\n\
use_doubler(h.value)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_enum_specialization_round_trip() {
check_src(
"enum Maybe<T> { Just(T), Nothing }\n\
fn main() -> i64 {\n\
let m = Maybe::Just(42);\n\
match m {\n\
Maybe::Just(x) => x,\n\
Maybe::Nothing => 0,\n\
}\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_struct_field_method_dispatch() {
check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
struct Cell<T> { value: T }\n\
fn main() -> i64 {\n\
let c = Cell { value: 21 };\n\
c.value.double()\n\
}",
)
.unwrap();
}
#[test]
fn closure_passed_as_argument() {
check_src(
"fn apply<F>(f: F, x: i64) -> i64 { f(x) }\n\
fn main() -> i64 {\n\
let g = |x: i64| x + 1;\n\
apply(g, 41)\n\
}",
)
.unwrap();
}
#[test]
fn closure_captures_outer_local() {
check_src(
"fn main() -> i64 {\n\
let n: i64 = 10;\n\
let f = |x: i64| x + n;\n\
f(5)\n\
}",
)
.unwrap();
}
#[test]
fn closure_no_param_callable() {
check_src(
"fn main() -> i64 {\n\
let f = || 42;\n\
f()\n\
}",
)
.unwrap();
}
#[test]
fn closure_nested_inside_closure_typechecks() {
check_src(
"fn main() -> i64 {\n\
let outer = |x: i64| {\n\
let inner = |y: i64| x + y;\n\
inner(5)\n\
};\n\
outer(7)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_tuple_index() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
let t = (21, true);\n\
use_doubler(t.0)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_method_call() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
use_doubler((21).double())\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_unary_op() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
let n: i64 = 21;\n\
use_doubler(-n)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_bin_op() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
let n: i64 = 10;\n\
use_doubler(n + 11)\n\
}",
)
.unwrap();
}
#[test]
fn monomorphize_inference_through_array_index() {
compile_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> i64 { x + x } }\n\
fn use_doubler<T: Doubler>(x: T) -> i64 { x.double() }\n\
fn main() -> i64 {\n\
let a: [i64; 2] = [21, 42];\n\
use_doubler(a[0])\n\
}",
)
.unwrap();
}
#[test]
fn closure_nested_capturing_outer_local_typechecks() {
check_src(
"fn main() -> i64 {\n\
let base: i64 = 100;\n\
let outer = |x: i64| {\n\
let inner = |y: i64| base + x + y;\n\
inner(3)\n\
};\n\
outer(7)\n\
}",
)
.unwrap();
}
#[test]
fn impl_method_param_type_mismatch_rejected() {
let err = check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: bool) -> i64 { 0 } }\n\
fn main() -> i64 { 0 }",
)
.unwrap_err();
assert!(
err.message.contains("parameter") || err.message.contains("type"),
"unexpected error: {}",
err.message,
);
}
#[test]
fn impl_method_return_type_mismatch_rejected() {
let err = check_src(
"trait Doubler { fn double(x: i64) -> i64; }\n\
impl Doubler for i64 { fn double(x: i64) -> bool { true } }\n\
fn main() -> i64 { 0 }",
)
.unwrap_err();
assert!(err.message.contains("returns"));
}
#[test]
fn impl_for_unknown_trait_rejected() {
let err = check_src(
"impl Nonexistent for i64 { fn x() -> i64 { 0 } }\n\
fn main() -> i64 { 0 }",
)
.unwrap_err();
assert!(err.message.contains("unknown trait"));
}
#[test]
fn missing_one_of_multiple_bounds_rejected() {
let err = check_src(
"trait A { fn a() -> i64; }\n\
trait B { fn b() -> i64; }\n\
impl A for i64 { fn a() -> i64 { 1 } }\n\
fn use_both<T: A + B>(x: T) -> i64 { 0 }\n\
fn main() -> i64 { use_both(7) }",
)
.unwrap_err();
assert!(err.message.contains("does not implement"));
}
#[test]
fn generic_struct_same_type_param_constraint() {
let err = check_src(
"struct SamePair<T> { a: T, b: T }\n\
fn main() -> i64 {\n\
let p = SamePair { a: 1, b: true };\n\
0\n\
}",
)
.unwrap_err();
assert!(
err.message.contains("expects") || err.message.contains("type"),
"unexpected error message: {}",
err.message,
);
}
}