use syntax::ast::{Expression, Literal, Span, UnaryOperator};
use syntax::program::CallKind;
use syntax::types::{SimpleKind, Type};
use crate::passes::walk::NodeCtx;
use crate::store::{ClosedDomain, ClosedMember, DomainValue, Store};
pub fn check_out_of_domain_value(expression: &Expression, ctx: &NodeCtx) {
match expression {
Expression::Literal { literal, ty, span } => {
let Some(domain) = closed_domain_of(ty, ctx.store) else {
return;
};
if ctx.claimed_spans.borrow().contains(span) {
return;
}
let Some(value) = DomainValue::from_literal(literal, domain.base) else {
return;
};
if !is_member(domain, &value) {
emit(*span, domain, ctx);
}
}
Expression::Unary {
operator: UnaryOperator::Negative,
expression: inner,
ty,
span,
} => {
let Some(domain) = closed_domain_of(ty, ctx.store) else {
return;
};
let Some((value, magnitude_span)) = negative_value(inner, domain.base) else {
return;
};
ctx.claimed_spans.borrow_mut().insert(magnitude_span);
if !is_member(domain, &value) {
emit(*span, domain, ctx);
}
}
Expression::Call {
call_kind: Some(CallKind::TupleStructConstructor),
args,
ty,
..
} => {
let Some(domain) = closed_domain_of(ty, ctx.store) else {
return;
};
let Some((value, span)) = args
.first()
.and_then(|arg| constructor_arg(arg, domain.base))
else {
return;
};
if !is_member(domain, &value) {
emit(span, domain, ctx);
}
}
_ => {}
}
}
fn closed_domain_of<'a>(ty: &Type, store: &'a Store) -> Option<&'a ClosedDomain> {
let Type::Nominal { id, .. } = ty else {
return None;
};
store.closed_domains.get(id.as_str())
}
fn negative_value(inner: &Expression, base: SimpleKind) -> Option<(DomainValue, Span)> {
let Expression::Literal { literal, span, .. } = inner.unwrap_parens() else {
return None;
};
Some((negate_literal(literal, base)?, *span))
}
fn constructor_arg(expression: &Expression, base: SimpleKind) -> Option<(DomainValue, Span)> {
match expression.unwrap_parens() {
Expression::Literal { literal, span, .. } => {
Some((DomainValue::from_literal(literal, base)?, *span))
}
Expression::Unary {
operator: UnaryOperator::Negative,
expression: inner,
span,
..
} => {
let Expression::Literal { literal, .. } = inner.unwrap_parens() else {
return None;
};
Some((negate_literal(literal, base)?, *span))
}
_ => None,
}
}
fn negate_literal(literal: &Literal, base: SimpleKind) -> Option<DomainValue> {
match literal {
Literal::Integer { value, .. } if base.is_signed_int() => {
Some(DomainValue::Int(-(*value as i128)))
}
_ => None,
}
}
fn is_member(domain: &ClosedDomain, value: &DomainValue) -> bool {
domain.members.iter().any(|member| member.value == *value)
}
fn emit(span: Span, domain: &ClosedDomain, ctx: &NodeCtx) {
ctx.sink.push(diagnostics::lint::out_of_domain_value(
&span,
&domain.type_display,
&render_valid(domain),
));
}
fn render_member(member: &ClosedMember) -> String {
match (&member.literal, &member.value) {
(Literal::Char(text), _) => format!("'{text}'"),
(_, DomainValue::Int(value)) => value.to_string(),
(_, DomainValue::Str(value)) => format!("\"{value}\""),
}
}
fn render_valid(domain: &ClosedDomain) -> String {
if domain.members.len() >= 2 && is_contiguous_integer_domain(domain) {
let first = &domain.members[0];
let last = &domain.members[domain.members.len() - 1];
return format!(
"{}={} ..= {}={}",
first.display_name,
render_member(first),
last.display_name,
render_member(last),
);
}
domain
.members
.iter()
.map(|member| format!("{}={}", member.display_name, render_member(member)))
.collect::<Vec<_>>()
.join(", ")
}
fn is_contiguous_integer_domain(domain: &ClosedDomain) -> bool {
let mut previous: Option<i128> = None;
for member in &domain.members {
let DomainValue::Int(value) = member.value else {
return false;
};
if previous.is_some_and(|p| value != p + 1) {
return false;
}
previous = Some(value);
}
previous.is_some()
}