366 lines
11 KiB
Rust
366 lines
11 KiB
Rust
use alloc::{string::String, vec::Vec};
|
|
|
|
use crate::{
|
|
Diagnostic, DiagnosticKind, Span,
|
|
ast::CompareOp,
|
|
runtime::{Constraint, ConstraintEntry, LiteralValue, PrimitiveType},
|
|
};
|
|
|
|
pub fn normalize_constraints(
|
|
constraints: Vec<ConstraintEntry>,
|
|
span: Span,
|
|
) -> crate::Result<Vec<ConstraintEntry>> {
|
|
let mut primitive: Option<(PrimitiveType, Span)> = None;
|
|
let mut lower: Option<(Bound, Span)> = None;
|
|
let mut upper: Option<(Bound, Span)> = None;
|
|
let mut rest = Vec::new();
|
|
|
|
for entry in constraints {
|
|
match entry.constraint {
|
|
Constraint::Type(next) => match primitive {
|
|
Some((current, current_span)) if current != next => {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
"primitive type constraints conflict",
|
|
)
|
|
.with_label(current_span, "first primitive constraint")
|
|
.with_label(entry.span, "conflicting primitive constraint"));
|
|
}
|
|
Some(_) => {}
|
|
None => primitive = Some((next, entry.span)),
|
|
},
|
|
Constraint::Compare(op, value) => {
|
|
let number = Number::from_literal(&value).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
entry.span,
|
|
"comparison constraints require numeric literals",
|
|
)
|
|
})?;
|
|
match op {
|
|
CompareOp::Gt => {
|
|
merge_lower(&mut lower, (Bound::new(number, false), entry.span))
|
|
}
|
|
CompareOp::Gte => {
|
|
merge_lower(&mut lower, (Bound::new(number, true), entry.span))
|
|
}
|
|
CompareOp::Lt => {
|
|
merge_upper(&mut upper, (Bound::new(number, false), entry.span))
|
|
}
|
|
CompareOp::Lte => {
|
|
merge_upper(&mut upper, (Bound::new(number, true), entry.span))
|
|
}
|
|
CompareOp::Eq => {
|
|
merge_lower(&mut lower, (Bound::new(number, true), entry.span));
|
|
merge_upper(&mut upper, (Bound::new(number, true), entry.span));
|
|
}
|
|
}
|
|
}
|
|
Constraint::Regex(pattern) => rest.push(ConstraintEntry {
|
|
constraint: Constraint::Regex(pattern),
|
|
span: entry.span,
|
|
}),
|
|
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
|
|
constraint: Constraint::BuiltinPredicate(name),
|
|
span: entry.span,
|
|
}),
|
|
}
|
|
}
|
|
|
|
if matches!(
|
|
primitive,
|
|
Some((
|
|
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
|
|
_
|
|
))
|
|
) && (lower.is_some() || upper.is_some())
|
|
{
|
|
let mut diagnostic = Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
"numeric comparison constraints conflict with non-numeric primitive type",
|
|
);
|
|
if let Some((_, primitive_span)) = primitive {
|
|
diagnostic = diagnostic.with_label(primitive_span, "non-numeric primitive constraint");
|
|
}
|
|
if let Some((_, lower_span)) = lower {
|
|
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
|
|
}
|
|
if let Some((_, upper_span)) = upper {
|
|
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
|
|
}
|
|
return Err(diagnostic);
|
|
}
|
|
|
|
if matches!(primitive, Some((PrimitiveType::Int, _)))
|
|
&& lower
|
|
.iter()
|
|
.chain(upper.iter())
|
|
.any(|(bound, _)| !matches!(bound.number, Number::Int(_)))
|
|
{
|
|
let mut diagnostic = Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
"Int comparison constraints must use integer literals",
|
|
);
|
|
if let Some((_, primitive_span)) = primitive {
|
|
diagnostic = diagnostic.with_label(primitive_span, "Int primitive constraint");
|
|
}
|
|
if let Some((bound, bound_span)) = lower {
|
|
if !matches!(bound.number, Number::Int(_)) {
|
|
diagnostic = diagnostic.with_label(bound_span, "non-integer comparison constraint");
|
|
}
|
|
}
|
|
if let Some((bound, bound_span)) = upper {
|
|
if !matches!(bound.number, Number::Int(_)) {
|
|
diagnostic = diagnostic.with_label(bound_span, "non-integer comparison constraint");
|
|
}
|
|
}
|
|
return Err(diagnostic);
|
|
}
|
|
|
|
ensure_bounds_non_empty(primitive, lower, upper, span)?;
|
|
|
|
let mut normalized = Vec::new();
|
|
if let Some((primitive, primitive_span)) = primitive {
|
|
normalized.push(ConstraintEntry {
|
|
constraint: Constraint::Type(primitive),
|
|
span: primitive_span,
|
|
});
|
|
}
|
|
if let Some((lower, lower_span)) = lower {
|
|
normalized.push(ConstraintEntry {
|
|
constraint: Constraint::Compare(
|
|
if lower.inclusive {
|
|
CompareOp::Gte
|
|
} else {
|
|
CompareOp::Gt
|
|
},
|
|
lower.number.into_literal(),
|
|
),
|
|
span: lower_span,
|
|
});
|
|
}
|
|
if let Some((upper, upper_span)) = upper {
|
|
normalized.push(ConstraintEntry {
|
|
constraint: Constraint::Compare(
|
|
if upper.inclusive {
|
|
CompareOp::Lte
|
|
} else {
|
|
CompareOp::Lt
|
|
},
|
|
upper.number.into_literal(),
|
|
),
|
|
span: upper_span,
|
|
});
|
|
}
|
|
normalized.extend(rest);
|
|
Ok(normalized)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
struct Bound {
|
|
number: Number,
|
|
inclusive: bool,
|
|
}
|
|
|
|
impl Bound {
|
|
fn new(number: Number, inclusive: bool) -> Self {
|
|
Self { number, inclusive }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum Number {
|
|
Int(i64),
|
|
Float(f64),
|
|
}
|
|
|
|
impl Number {
|
|
fn from_literal(value: &LiteralValue) -> Option<Self> {
|
|
match value {
|
|
LiteralValue::Int(value) => Some(Self::Int(*value)),
|
|
LiteralValue::Float(value) => Some(Self::Float(*value)),
|
|
LiteralValue::String(_) | LiteralValue::Bool(_) => None,
|
|
}
|
|
}
|
|
|
|
fn into_literal(self) -> LiteralValue {
|
|
match self {
|
|
Self::Int(value) => LiteralValue::Int(value),
|
|
Self::Float(value) => LiteralValue::Float(value),
|
|
}
|
|
}
|
|
|
|
fn as_f64(self) -> f64 {
|
|
match self {
|
|
Self::Int(value) => value as f64,
|
|
Self::Float(value) => value,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn merge_lower(current: &mut Option<(Bound, Span)>, next: (Bound, Span)) {
|
|
match current {
|
|
None => *current = Some(next),
|
|
Some((current_bound, _)) if is_stricter_lower(next.0, *current_bound) => {
|
|
*current = Some(next)
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
|
|
fn merge_upper(current: &mut Option<(Bound, Span)>, next: (Bound, Span)) {
|
|
match current {
|
|
None => *current = Some(next),
|
|
Some((current_bound, _)) if is_stricter_upper(next.0, *current_bound) => {
|
|
*current = Some(next)
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
|
|
fn is_stricter_lower(next: Bound, current: Bound) -> bool {
|
|
let next_value = next.number.as_f64();
|
|
let current_value = current.number.as_f64();
|
|
next_value > current_value
|
|
|| (next_value == current_value && !next.inclusive && current.inclusive)
|
|
}
|
|
|
|
fn is_stricter_upper(next: Bound, current: Bound) -> bool {
|
|
let next_value = next.number.as_f64();
|
|
let current_value = current.number.as_f64();
|
|
next_value < current_value
|
|
|| (next_value == current_value && !next.inclusive && current.inclusive)
|
|
}
|
|
|
|
fn ensure_bounds_non_empty(
|
|
primitive: Option<(PrimitiveType, Span)>,
|
|
lower: Option<(Bound, Span)>,
|
|
upper: Option<(Bound, Span)>,
|
|
span: Span,
|
|
) -> crate::Result<()> {
|
|
if matches!(primitive, Some((PrimitiveType::Int, _))) {
|
|
let min = lower
|
|
.map(|(bound, _)| int_lower_bound(bound))
|
|
.unwrap_or(i128::from(i64::MIN));
|
|
let max = upper
|
|
.map(|(bound, _)| int_upper_bound(bound))
|
|
.unwrap_or(i128::from(i64::MAX));
|
|
if min > max {
|
|
return Err(empty_numeric_bounds(span, lower, upper));
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
if let (Some((lower_bound, _)), Some((upper_bound, _))) = (lower, upper) {
|
|
let lower_value = lower_bound.number.as_f64();
|
|
let upper_value = upper_bound.number.as_f64();
|
|
if lower_value > upper_value {
|
|
return Err(empty_numeric_bounds(span, lower, upper));
|
|
}
|
|
if lower_value == upper_value && !(lower_bound.inclusive && upper_bound.inclusive) {
|
|
return Err(empty_numeric_bounds(span, lower, upper));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn int_lower_bound(bound: Bound) -> i128 {
|
|
let Number::Int(value) = bound.number else {
|
|
unreachable!()
|
|
};
|
|
if bound.inclusive {
|
|
i128::from(value)
|
|
} else {
|
|
i128::from(value) + 1
|
|
}
|
|
}
|
|
|
|
fn int_upper_bound(bound: Bound) -> i128 {
|
|
let Number::Int(value) = bound.number else {
|
|
unreachable!()
|
|
};
|
|
if bound.inclusive {
|
|
i128::from(value)
|
|
} else {
|
|
i128::from(value) - 1
|
|
}
|
|
}
|
|
|
|
fn empty_numeric_bounds(
|
|
span: Span,
|
|
lower: Option<(Bound, Span)>,
|
|
upper: Option<(Bound, Span)>,
|
|
) -> Diagnostic {
|
|
let mut diagnostic = Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
String::from("numeric comparison constraints have an empty intersection"),
|
|
);
|
|
if let Some((_, lower_span)) = lower {
|
|
diagnostic = diagnostic.with_label(lower_span, "lower bound constraint");
|
|
}
|
|
if let Some((_, upper_span)) = upper {
|
|
diagnostic = diagnostic.with_label(upper_span, "upper bound constraint");
|
|
}
|
|
diagnostic
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::runtime::LiteralValue;
|
|
|
|
fn entry(constraint: Constraint) -> ConstraintEntry {
|
|
ConstraintEntry {
|
|
constraint,
|
|
span: Span::default(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn detects_primitive_conflict() {
|
|
assert!(
|
|
normalize_constraints(
|
|
alloc::vec![
|
|
entry(Constraint::Type(PrimitiveType::Int)),
|
|
entry(Constraint::Type(PrimitiveType::String)),
|
|
],
|
|
Span::default(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_empty_int_range() {
|
|
assert!(
|
|
normalize_constraints(
|
|
alloc::vec![
|
|
entry(Constraint::Type(PrimitiveType::Int)),
|
|
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10))),
|
|
entry(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(5))),
|
|
],
|
|
Span::default(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_non_integer_int_bound() {
|
|
assert!(
|
|
normalize_constraints(
|
|
alloc::vec![
|
|
entry(Constraint::Type(PrimitiveType::Int)),
|
|
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Float(1.5))),
|
|
],
|
|
Span::default(),
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|