use super::ast::{Expr, Literal, Macro};
use super::functions::{Function, LEFT_OUT};
use super::ops::BinaryOp;
use super::overload::{self, INT_HINT};
use super::types::Type;
use super::{Error, FunctionTypes, Scope};
use crate::nearest::nearest;
pub(crate) fn check(
expr: &Expr,
scope: &Scope,
functions: &dyn FunctionTypes,
) -> Result<Type, Error> {
Checker {
scope,
functions,
locals: Vec::new(),
guarded: Vec::new(),
}
.check(expr)
}
fn path(expr: &Expr) -> Option<Vec<&str>> {
match expr {
Expr::Ident(name, _) => Some(vec![name]),
Expr::Select(operand, field, _) => {
let mut names = path(operand)?;
names.push(field);
Some(names)
}
_ => None,
}
}
fn proven(condition: &Expr) -> Vec<Vec<&str>> {
match condition {
Expr::Has(operand, field, _) => path(operand)
.map(|mut names| {
names.push(field);
names
})
.into_iter()
.collect(),
Expr::And(left, right, _) => proven(left).into_iter().chain(proven(right)).collect(),
_ => Vec::new(),
}
}
struct Checker<'a> {
scope: &'a Scope,
functions: &'a dyn FunctionTypes,
locals: Vec<(&'a str, Type)>,
guarded: Vec<Vec<&'a str>>,
}
impl<'a> Checker<'a> {
fn check(&mut self, expr: &'a Expr) -> Result<Type, Error> {
match expr {
Expr::Literal(literal, _) => Ok(literal_type(literal)),
Expr::Ident(name, at) => self.lookup(name).ok_or_else(|| {
let names = self
.locals
.iter()
.map(|(n, _)| *n)
.chain(self.scope.names());
unknown(*at, "name", name, names)
}),
Expr::Select(operand, field, at) => {
let operand_type = self.check(operand)?;
self.select(expr, &operand_type, field, *at)
}
Expr::Index(operand, index, at) => {
let (operand, index_type) = (self.check(operand)?, self.check(index)?);
self::index(&operand, &index_type, index, *at)
}
Expr::Unary(op, operand, at) => {
let operand = self.check(operand)?;
op.result_type(&operand).ok_or_else(|| {
Error::type_error(
*at,
format!("'{}' can't be applied to {operand}", op.name()),
)
})
}
Expr::Binary(op, left, right, _) => self.binary(*op, left, right),
Expr::Call {
target,
name,
args,
at,
} => self.call(target.as_deref(), name, args, *at),
Expr::And(left, right, at) => {
self.boolean(left, *at, "&&")?;
self.guarded_by(proven(left), |checker| checker.boolean(right, *at, "&&"))?;
Ok(Type::Bool)
}
Expr::Or(left, right, at) => {
self.boolean(left, *at, "||")?;
self.boolean(right, *at, "||")?;
Ok(Type::Bool)
}
Expr::Conditional(condition, then, otherwise, at) => {
let condition_type = self.check(condition)?;
if !condition_type.fits(&Type::Bool) {
return Err(Error::type_error(
*at,
format!("the condition of '?:' must be a bool, not {condition_type}"),
));
}
let then = self.guarded_by(proven(condition), |checker| checker.check(then))?;
Ok(then.join(self.check(otherwise)?))
}
Expr::List(items, _) => {
let types = items
.iter()
.map(|item| self.check(item))
.collect::<Result<Vec<_>, _>>()?;
Ok(Type::list(Type::joined(types)))
}
Expr::Map(entries, _) => self.map(entries),
Expr::Has(operand, _, at) => match self.check(operand)? {
Type::Record(_) | Type::Map(..) | Type::Dyn => Ok(Type::Bool),
other => Err(Error::type_error(
*at,
format!("has() needs a map, not {other}"),
)),
},
Expr::Comprehension {
kind,
range,
var,
filter,
body,
at,
} => {
let element = self.element(*kind, range, *at)?;
self.locals.push((var, element.clone()));
let result = self.comprehension(*kind, element, filter.as_deref(), body, *at);
self.locals.pop();
result
}
}
}
fn element(&mut self, kind: Macro, range: &'a Expr, at: usize) -> Result<Type, Error> {
match self.check(range)? {
Type::List(element) => Ok((*element).clone()),
Type::Map(key, _) => Ok((*key).clone()),
Type::Record(_) => Ok(Type::String),
Type::Dyn => Ok(Type::Dyn),
other => Err(Error::type_error(
at,
format!("{}() needs a list or a map, not {other}", kind.name()),
)),
}
}
fn binary(&mut self, op: BinaryOp, left: &'a Expr, right: &'a Expr) -> Result<Type, Error> {
let at = left.start();
let (left, right) = (self.check(left)?, self.check(right)?);
op.result_type(&left, &right).ok_or_else(|| {
let hint = if op == BinaryOp::Remainder {
int_hint(&[left.clone(), right.clone()])
} else {
""
};
Error::type_error(
at,
format!(
"'{}' can't be applied to {left} and {right}{hint}",
op.name()
),
)
})
}
fn map(&mut self, entries: &'a [(Expr, Expr)]) -> Result<Type, Error> {
let (mut keys, mut values) = (Vec::new(), Vec::new());
for (key, value) in entries {
let key_type = self.check(key)?;
if !matches!(
key_type,
Type::Int | Type::Uint | Type::Bool | Type::String | Type::Dyn
) {
return Err(Error::type_error(
key.at(),
format!("a map key must be an int, uint, bool or string, not {key_type}"),
));
}
keys.push(key_type);
values.push(self.check(value)?);
}
Ok(Type::map(Type::joined(keys), Type::joined(values)))
}
fn lookup(&self, name: &str) -> Option<Type> {
let local = self
.locals
.iter()
.rev()
.find(|(n, _)| *n == name)
.map(|(_, t)| t);
local.or_else(|| self.scope.get(name)).cloned()
}
fn guarded_by<T>(
&mut self,
guards: Vec<Vec<&'a str>>,
check: impl FnOnce(&mut Self) -> T,
) -> T {
let count = guards.len();
self.guarded.extend(guards);
let result = check(self);
self.guarded.truncate(self.guarded.len() - count);
result
}
fn boolean(&mut self, side: &'a Expr, at: usize, operator: &str) -> Result<(), Error> {
let side = self.check(side)?;
if side.fits(&Type::Bool) {
Ok(())
} else {
Err(Error::type_error(
at,
format!("'{operator}' takes bools, not {side}"),
))
}
}
fn select(&self, expr: &Expr, operand: &Type, field: &str, at: usize) -> Result<Type, Error> {
match operand {
Type::Record(fields) => match fields.get(field) {
Some(ty) => Ok(ty.clone()),
None if path(expr).is_some_and(|path| self.guarded.contains(&path)) => {
Ok(Type::Dyn)
}
None => Err(unknown(
at,
"field",
field,
fields.keys().map(String::as_str),
)),
},
Type::Map(key, value) if Type::String.fits(key) => Ok((**value).clone()),
Type::Dyn => Ok(Type::Dyn),
other => Err(Error::type_error(at, format!("{other} has no fields"))),
}
}
fn comprehension(
&mut self,
kind: Macro,
element: Type,
filter: Option<&'a Expr>,
body: &'a Expr,
at: usize,
) -> Result<Type, Error> {
let predicate = |checker: &mut Self, expr: &'a Expr| -> Result<(), Error> {
let ty = checker.check(expr)?;
if ty.fits(&Type::Bool) {
Ok(())
} else {
Err(Error::type_error(
at,
format!("{}() needs a bool expression, not {ty}", kind.name()),
))
}
};
if let Some(filter) = filter {
predicate(self, filter)?;
}
match kind {
Macro::Map => Ok(Type::list(self.check(body)?)),
Macro::Filter => {
predicate(self, body)?;
Ok(Type::list(element))
}
Macro::All | Macro::Exists | Macro::ExistsOne => {
predicate(self, body)?;
Ok(Type::Bool)
}
}
}
fn call(
&mut self,
target: Option<&'a Expr>,
name: &str,
args: &'a [Expr],
at: usize,
) -> Result<Type, Error> {
if matches!(target, Some(Expr::Ident(namespace, _)) if namespace == "md") {
return self.md_call(name, args, at);
}
let receiver = target.map(|t| self.check(t)).transpose()?;
let arg_types = args
.iter()
.map(|a| self.check(a))
.collect::<Result<Vec<_>, _>>()?;
if LEFT_OUT.contains(&name) {
return Err(Error::type_error(
at,
format!("{name}() isn't in the CompMD CEL profile"),
));
}
overload::type_of::<Function>(name, receiver.as_ref(), &arg_types)
.map_err(|message| Error::type_error(at, message))
}
fn md_call(&mut self, name: &str, args: &'a [Expr], at: usize) -> Result<Type, Error> {
let arg_types = args
.iter()
.map(|a| self.check(a))
.collect::<Result<Vec<_>, _>>()?;
self.functions
.result_type(name, &arg_types)
.map_err(|message| Error::type_error(at, message))
}
}
fn int_hint(given: &[Type]) -> &'static str {
if given.contains(&Type::Double) {
INT_HINT
} else {
""
}
}
fn literal_type(literal: &Literal) -> Type {
match literal {
Literal::Int(_) => Type::Int,
Literal::Uint(_) => Type::Uint,
Literal::Double(_) => Type::Double,
Literal::String(_) => Type::String,
Literal::Bool(_) => Type::Bool,
Literal::Null => Type::Null,
}
}
fn index(operand: &Type, index_type: &Type, index: &Expr, at: usize) -> Result<Type, Error> {
match operand {
Type::List(element) if index_type.fits(&Type::Int) => Ok((**element).clone()),
Type::List(_) => Err(Error::type_error(
at,
format!(
"a list index must be an int, not {index_type}{}",
int_hint(std::slice::from_ref(index_type))
),
)),
Type::Map(key, value)
if index_type.fits(key) || (key.is_numeric() && index_type.is_numeric()) =>
{
Ok((**value).clone())
}
Type::Record(fields) if index_type.fits(&Type::String) => match index {
Expr::Literal(Literal::String(field), _) => fields
.get(field)
.cloned()
.ok_or_else(|| unknown(at, "field", field, fields.keys().map(String::as_str))),
_ => Ok(Type::Dyn),
},
Type::Dyn => Ok(Type::Dyn),
other => Err(Error::type_error(
at,
format!("{other} can't be indexed with {index_type}"),
)),
}
}
fn unknown<'n>(
at: usize,
what: &str,
name: &str,
known: impl IntoIterator<Item = &'n str>,
) -> Error {
match nearest(name, known) {
Some(near) => Error::type_error(
at,
format!("unknown {what} '{name}'; did you mean '{near}'?"),
),
None => Error::type_error(at, format!("unknown {what} '{name}'")),
}
}