Improve composition diagnostics
This commit is contained in:
@@ -148,6 +148,15 @@ fn print_diagnostic(error: &Diagnostic) {
|
||||
end = error.span.end,
|
||||
message = error.message,
|
||||
);
|
||||
for label in &error.labels {
|
||||
eprintln!(
|
||||
" note {source}:{start}..{end}: {message}",
|
||||
source = label.span.source.0,
|
||||
start = label.span.start,
|
||||
end = label.span.end,
|
||||
message = label.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_data(data: &Data, indent: usize) {
|
||||
|
||||
@@ -3,103 +3,154 @@ use alloc::{string::String, vec::Vec};
|
||||
use crate::{
|
||||
Diagnostic, DiagnosticKind, Span,
|
||||
ast::CompareOp,
|
||||
runtime::{Constraint, LiteralValue, PrimitiveType},
|
||||
runtime::{Constraint, ConstraintEntry, LiteralValue, PrimitiveType},
|
||||
};
|
||||
|
||||
pub fn normalize_constraints(
|
||||
constraints: Vec<Constraint>,
|
||||
constraints: Vec<ConstraintEntry>,
|
||||
span: Span,
|
||||
) -> crate::Result<Vec<Constraint>> {
|
||||
let mut primitive = None;
|
||||
let mut lower: Option<Bound> = None;
|
||||
let mut upper: Option<Bound> = None;
|
||||
) -> 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 constraint in constraints {
|
||||
match constraint {
|
||||
for entry in constraints {
|
||||
match entry.constraint {
|
||||
Constraint::Type(next) => match primitive {
|
||||
Some(current) if current != next => {
|
||||
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),
|
||||
None => primitive = Some((next, entry.span)),
|
||||
},
|
||||
Constraint::Compare(op, value) => {
|
||||
let number = Number::from_literal(&value).ok_or_else(|| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::TypeMismatch,
|
||||
span,
|
||||
entry.span,
|
||||
"comparison constraints require numeric literals",
|
||||
)
|
||||
})?;
|
||||
match op {
|
||||
CompareOp::Gt => merge_lower(&mut lower, Bound::new(number, false)),
|
||||
CompareOp::Gte => merge_lower(&mut lower, Bound::new(number, true)),
|
||||
CompareOp::Lt => merge_upper(&mut upper, Bound::new(number, false)),
|
||||
CompareOp::Lte => merge_upper(&mut upper, Bound::new(number, true)),
|
||||
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));
|
||||
merge_upper(&mut upper, Bound::new(number, true));
|
||||
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(Constraint::Regex(pattern)),
|
||||
Constraint::BuiltinPredicate(name) => rest.push(Constraint::BuiltinPredicate(name)),
|
||||
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))
|
||||
&& (lower.is_some() || upper.is_some())
|
||||
if matches!(
|
||||
primitive,
|
||||
Some((PrimitiveType::String | PrimitiveType::Bool, _))
|
||||
) && (lower.is_some() || upper.is_some())
|
||||
{
|
||||
return Err(Diagnostic::new(
|
||||
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 primitive == Some(PrimitiveType::Int)
|
||||
if matches!(primitive, Some((PrimitiveType::Int, _)))
|
||||
&& lower
|
||||
.iter()
|
||||
.chain(upper.iter())
|
||||
.any(|bound| !matches!(bound.number, Number::Int(_)))
|
||||
.any(|(bound, _)| !matches!(bound.number, Number::Int(_)))
|
||||
{
|
||||
return Err(Diagnostic::new(
|
||||
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 {
|
||||
normalized.push(Constraint::Type(primitive));
|
||||
if let Some((primitive, primitive_span)) = primitive {
|
||||
normalized.push(ConstraintEntry {
|
||||
constraint: Constraint::Type(primitive),
|
||||
span: primitive_span,
|
||||
});
|
||||
}
|
||||
if let Some(lower) = lower {
|
||||
normalized.push(Constraint::Compare(
|
||||
if lower.inclusive {
|
||||
CompareOp::Gte
|
||||
} else {
|
||||
CompareOp::Gt
|
||||
},
|
||||
lower.number.into_literal(),
|
||||
));
|
||||
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 {
|
||||
normalized.push(Constraint::Compare(
|
||||
if upper.inclusive {
|
||||
CompareOp::Lte
|
||||
} else {
|
||||
CompareOp::Lt
|
||||
},
|
||||
upper.number.into_literal(),
|
||||
));
|
||||
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)
|
||||
@@ -147,18 +198,22 @@ impl Number {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_lower(current: &mut Option<Bound>, next: Bound) {
|
||||
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, *current_bound) => *current_bound = next,
|
||||
Some((current_bound, _)) if is_stricter_lower(next.0, *current_bound) => {
|
||||
*current = Some(next)
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_upper(current: &mut Option<Bound>, next: Bound) {
|
||||
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, *current_bound) => *current_bound = next,
|
||||
Some((current_bound, _)) if is_stricter_upper(next.0, *current_bound) => {
|
||||
*current = Some(next)
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -178,28 +233,32 @@ fn is_stricter_upper(next: Bound, current: Bound) -> bool {
|
||||
}
|
||||
|
||||
fn ensure_bounds_non_empty(
|
||||
primitive: Option<PrimitiveType>,
|
||||
lower: Option<Bound>,
|
||||
upper: Option<Bound>,
|
||||
primitive: Option<(PrimitiveType, Span)>,
|
||||
lower: Option<(Bound, Span)>,
|
||||
upper: Option<(Bound, Span)>,
|
||||
span: Span,
|
||||
) -> crate::Result<()> {
|
||||
if primitive == Some(PrimitiveType::Int) {
|
||||
let min = lower.map(int_lower_bound).unwrap_or(i128::from(i64::MIN));
|
||||
let max = upper.map(int_upper_bound).unwrap_or(i128::from(i64::MAX));
|
||||
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));
|
||||
return Err(empty_numeric_bounds(span, lower, upper));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let (Some(lower), Some(upper)) = (lower, upper) {
|
||||
let lower_value = lower.number.as_f64();
|
||||
let upper_value = upper.number.as_f64();
|
||||
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));
|
||||
return Err(empty_numeric_bounds(span, lower, upper));
|
||||
}
|
||||
if lower_value == upper_value && !(lower.inclusive && upper.inclusive) {
|
||||
return Err(empty_numeric_bounds(span));
|
||||
if lower_value == upper_value && !(lower_bound.inclusive && upper_bound.inclusive) {
|
||||
return Err(empty_numeric_bounds(span, lower, upper));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -227,12 +286,23 @@ fn int_upper_bound(bound: Bound) -> i128 {
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_numeric_bounds(span: Span) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
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)]
|
||||
@@ -240,13 +310,20 @@ 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![
|
||||
Constraint::Type(PrimitiveType::Int),
|
||||
Constraint::Type(PrimitiveType::String),
|
||||
entry(Constraint::Type(PrimitiveType::Int)),
|
||||
entry(Constraint::Type(PrimitiveType::String)),
|
||||
],
|
||||
Span::default(),
|
||||
)
|
||||
@@ -259,9 +336,9 @@ mod tests {
|
||||
assert!(
|
||||
normalize_constraints(
|
||||
alloc::vec![
|
||||
Constraint::Type(PrimitiveType::Int),
|
||||
Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10)),
|
||||
Constraint::Compare(CompareOp::Lt, LiteralValue::Int(11)),
|
||||
entry(Constraint::Type(PrimitiveType::Int)),
|
||||
entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10))),
|
||||
entry(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(5))),
|
||||
],
|
||||
Span::default(),
|
||||
)
|
||||
@@ -270,15 +347,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_regex_constraints_without_intersection_check() {
|
||||
let constraints = normalize_constraints(
|
||||
alloc::vec![
|
||||
Constraint::Regex(String::from("^a$")),
|
||||
Constraint::Regex(String::from("^b$")),
|
||||
],
|
||||
Span::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(constraints.len(), 2);
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use alloc::string::String;
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::span::Span;
|
||||
|
||||
@@ -9,6 +9,13 @@ pub struct Diagnostic {
|
||||
pub kind: DiagnosticKind,
|
||||
pub span: Span,
|
||||
pub message: String,
|
||||
pub labels: Vec<DiagnosticLabel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiagnosticLabel {
|
||||
pub span: Span,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
@@ -17,12 +24,21 @@ impl Diagnostic {
|
||||
kind,
|
||||
span,
|
||||
message: message.into(),
|
||||
labels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn syntax(span: Span, message: impl Into<String>) -> Self {
|
||||
Self::new(DiagnosticKind::Syntax, span, message)
|
||||
}
|
||||
|
||||
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
|
||||
self.labels.push(DiagnosticLabel {
|
||||
span,
|
||||
message: message.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
+271
-67
@@ -9,9 +9,9 @@ use crate::{
|
||||
module::{EmptyLoader, LoadedSource, Module, SourceLoader},
|
||||
parse_source_with_source_id,
|
||||
runtime::{
|
||||
AbstractValue, Binding, ConcreteValue, Constraint, Data, DataField, Env, EnvId, ExprRef,
|
||||
FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField, ObjectValue,
|
||||
PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
|
||||
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
|
||||
EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
|
||||
ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,6 +84,14 @@ impl<L: SourceLoader> Engine<L> {
|
||||
}
|
||||
|
||||
pub fn materialize(&mut self, value: &RuntimeValue) -> Result<Data> {
|
||||
self.materialize_with_path(value, &mut Vec::new())
|
||||
}
|
||||
|
||||
fn materialize_with_path(
|
||||
&mut self,
|
||||
value: &RuntimeValue,
|
||||
path: &mut Vec<String>,
|
||||
) -> Result<Data> {
|
||||
match value {
|
||||
RuntimeValue::Concrete(value) => match value {
|
||||
ConcreteValue::String(value) => Ok(Data::String(value.clone())),
|
||||
@@ -94,7 +102,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
let mut data = Vec::new();
|
||||
for item in items {
|
||||
let value = self.force(*item)?;
|
||||
data.push(self.materialize(&value)?);
|
||||
data.push(self.materialize_with_path(&value, path)?);
|
||||
}
|
||||
Ok(Data::Array(data))
|
||||
}
|
||||
@@ -102,30 +110,51 @@ impl<L: SourceLoader> Engine<L> {
|
||||
let mut fields = Vec::new();
|
||||
for field in &object.fields {
|
||||
let value = self.force(field.value)?;
|
||||
path.push(field.name.clone());
|
||||
let value = self.materialize_with_path(&value, path);
|
||||
path.pop();
|
||||
fields.push(DataField {
|
||||
name: field.name.clone(),
|
||||
value: self.materialize(&value)?,
|
||||
value: value?,
|
||||
});
|
||||
}
|
||||
Ok(Data::Object(fields))
|
||||
}
|
||||
ConcreteValue::Function(_) => Err(Diagnostic::new(
|
||||
DiagnosticKind::Materialize,
|
||||
Span::default(),
|
||||
"cannot materialize function value",
|
||||
ConcreteValue::Function(_) => Err(self.with_path_context(
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Materialize,
|
||||
Span::default(),
|
||||
"cannot materialize function value",
|
||||
),
|
||||
"materializing",
|
||||
path,
|
||||
)),
|
||||
},
|
||||
RuntimeValue::Abstract(abstract_value) => {
|
||||
let Some(default) = abstract_value.default else {
|
||||
return Err(Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::Materialize,
|
||||
Span::default(),
|
||||
constraint_primary_span(&abstract_value.constraints),
|
||||
"cannot materialize unresolved abstract value without default",
|
||||
));
|
||||
);
|
||||
for constraint in &abstract_value.constraints {
|
||||
diagnostic = diagnostic.with_label(
|
||||
constraint.span,
|
||||
"constraint still requires a concrete value or default",
|
||||
);
|
||||
}
|
||||
return Err(self.with_path_context(diagnostic, "materializing", path));
|
||||
};
|
||||
let default_span = self.thunk_span(default);
|
||||
let value = self.force(default)?;
|
||||
self.ensure_satisfies(&value, &abstract_value.constraints, Span::default())?;
|
||||
self.materialize(&value)
|
||||
self.ensure_satisfies(
|
||||
&value,
|
||||
&abstract_value.constraints,
|
||||
default_span,
|
||||
Some((default_span, "default value checked here")),
|
||||
)
|
||||
.map_err(|diag| self.with_path_context(diag, "materializing", path))?;
|
||||
self.materialize_with_path(&value, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,12 +504,18 @@ impl<L: SourceLoader> Engine<L> {
|
||||
)
|
||||
})?;
|
||||
Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![Constraint::Compare(op, value)],
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::Compare(op, value),
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
}))
|
||||
}
|
||||
Expr::RegexConstraint(pattern) => Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![Constraint::Regex(pattern)],
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::Regex(pattern),
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
})),
|
||||
Expr::Wildcard => Err(Diagnostic::new(
|
||||
@@ -494,7 +529,10 @@ impl<L: SourceLoader> Engine<L> {
|
||||
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
|
||||
if let Some(primitive) = primitive_type(name) {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![Constraint::Type(primitive)],
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::Type(primitive),
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
}));
|
||||
}
|
||||
@@ -503,7 +541,10 @@ impl<L: SourceLoader> Engine<L> {
|
||||
}
|
||||
if name.chars().next().is_some_and(char::is_uppercase) {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![Constraint::BuiltinPredicate(String::from(name))],
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::BuiltinPredicate(String::from(name)),
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
}));
|
||||
}
|
||||
@@ -551,7 +592,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
));
|
||||
}
|
||||
if path.len() == 1 {
|
||||
let value = self.add_expr_thunk(ExprRef { module, expr }, env);
|
||||
let value = self.add_expr_thunk_with_span(ExprRef { module, expr }, env, span);
|
||||
if object.fields.iter().any(|field| field.name == path[0]) {
|
||||
return Err(Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
@@ -562,6 +603,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
object.fields.push(ObjectField {
|
||||
name: path[0].clone(),
|
||||
value,
|
||||
span,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
@@ -577,8 +619,10 @@ impl<L: SourceLoader> Engine<L> {
|
||||
));
|
||||
};
|
||||
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
||||
object.fields[index].value =
|
||||
self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(nested)));
|
||||
object.fields[index].value = self.add_value_thunk_with_span(
|
||||
RuntimeValue::Concrete(ConcreteValue::Object(nested)),
|
||||
span,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -586,7 +630,11 @@ impl<L: SourceLoader> Engine<L> {
|
||||
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
||||
object.fields.push(ObjectField {
|
||||
name: name.clone(),
|
||||
value: self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(nested))),
|
||||
value: self.add_value_thunk_with_span(
|
||||
RuntimeValue::Concrete(ConcreteValue::Object(nested)),
|
||||
span,
|
||||
),
|
||||
span,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -646,6 +694,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
value,
|
||||
&abstract_value.constraints,
|
||||
self.expr_span(pattern),
|
||||
None,
|
||||
)
|
||||
.map(|_| true)
|
||||
.or_else(|diag| match diag.kind {
|
||||
@@ -673,12 +722,17 @@ impl<L: SourceLoader> Engine<L> {
|
||||
(RuntimeValue::Abstract(mut lhs), RuntimeValue::Abstract(rhs)) => {
|
||||
lhs.constraints.extend(rhs.constraints);
|
||||
lhs.constraints = normalize_constraints(lhs.constraints, span)?;
|
||||
lhs.default = merge_default(lhs.default, rhs.default, span)?;
|
||||
lhs.default = self.merge_default(lhs.default, rhs.default, span)?;
|
||||
Ok(RuntimeValue::Abstract(lhs))
|
||||
}
|
||||
(RuntimeValue::Abstract(abstract_value), concrete @ RuntimeValue::Concrete(_))
|
||||
| (concrete @ RuntimeValue::Concrete(_), RuntimeValue::Abstract(abstract_value)) => {
|
||||
self.ensure_satisfies(&concrete, &abstract_value.constraints, span)?;
|
||||
self.ensure_satisfies(
|
||||
&concrete,
|
||||
&abstract_value.constraints,
|
||||
span,
|
||||
Some((span, "concrete value being composed here")),
|
||||
)?;
|
||||
Ok(concrete)
|
||||
}
|
||||
(
|
||||
@@ -711,10 +765,17 @@ impl<L: SourceLoader> Engine<L> {
|
||||
.iter()
|
||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||
{
|
||||
let lhs_field_span = lhs.fields[index].span;
|
||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
||||
let rhs_value = self.force(rhs_field.value)?;
|
||||
let value = self.compose_and(lhs_value, rhs_value, span)?;
|
||||
lhs.fields[index].value = self.add_value_thunk(value);
|
||||
let value = self
|
||||
.compose_and(lhs_value, rhs_value, span)
|
||||
.map_err(|diag| {
|
||||
diag.with_label(span, format!("while composing field `{}`", rhs_field.name))
|
||||
.with_label(lhs_field_span, format!("left field `{}`", rhs_field.name))
|
||||
.with_label(rhs_field.span, format!("right field `{}`", rhs_field.name))
|
||||
})?;
|
||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
||||
} else {
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
@@ -742,7 +803,8 @@ impl<L: SourceLoader> Engine<L> {
|
||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
||||
let rhs_value = self.force(rhs_field.value)?;
|
||||
let value = self.patch(lhs_value, rhs_value)?;
|
||||
lhs.fields[index].value = self.add_value_thunk(value);
|
||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
||||
lhs.fields[index].span = rhs_field.span;
|
||||
} else {
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
@@ -753,11 +815,12 @@ impl<L: SourceLoader> Engine<L> {
|
||||
fn ensure_satisfies(
|
||||
&mut self,
|
||||
value: &RuntimeValue,
|
||||
constraints: &[Constraint],
|
||||
constraints: &[ConstraintEntry],
|
||||
span: Span,
|
||||
value_label: Option<(Span, &'static str)>,
|
||||
) -> Result<()> {
|
||||
for constraint in constraints {
|
||||
self.satisfies(value, constraint, span)?;
|
||||
self.satisfies(value, constraint, span, value_label)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -765,17 +828,20 @@ impl<L: SourceLoader> Engine<L> {
|
||||
fn satisfies(
|
||||
&mut self,
|
||||
value: &RuntimeValue,
|
||||
constraint: &Constraint,
|
||||
constraint: &ConstraintEntry,
|
||||
span: Span,
|
||||
value_label: Option<(Span, &'static str)>,
|
||||
) -> Result<()> {
|
||||
match constraint {
|
||||
let constraint_value = &constraint.constraint;
|
||||
match constraint_value {
|
||||
Constraint::Type(primitive) => {
|
||||
if value_matches_primitive(value, *primitive) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
Err(self.constraint_violation(
|
||||
constraint,
|
||||
span,
|
||||
value_label,
|
||||
"value does not satisfy primitive type constraint",
|
||||
))
|
||||
}
|
||||
@@ -783,18 +849,69 @@ impl<L: SourceLoader> Engine<L> {
|
||||
Constraint::Compare(op, expected) => compare_value(value, *op, expected)
|
||||
.then_some(())
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
self.constraint_violation(
|
||||
constraint,
|
||||
span,
|
||||
value_label,
|
||||
"value does not satisfy comparison constraint",
|
||||
)
|
||||
}),
|
||||
Constraint::Regex(pattern) => satisfies_regex(value, pattern, span),
|
||||
Constraint::Regex(pattern) => {
|
||||
satisfies_regex(value, pattern, constraint.span).map_err(|diag| {
|
||||
let diag = diag.with_label(constraint.span, "regex constraint declared here");
|
||||
if let Some((span, message)) = value_label {
|
||||
diag.with_label(span, message)
|
||||
} else {
|
||||
diag
|
||||
}
|
||||
})
|
||||
}
|
||||
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
|
||||
DiagnosticKind::UnsupportedFeature,
|
||||
span,
|
||||
constraint.span,
|
||||
format!("builtin predicate `{name}` is not implemented"),
|
||||
)),
|
||||
)
|
||||
.with_label(span, "constraint checked during this operation")),
|
||||
}
|
||||
}
|
||||
|
||||
fn constraint_violation(
|
||||
&self,
|
||||
constraint: &ConstraintEntry,
|
||||
operation_span: Span,
|
||||
value_label: Option<(Span, &'static str)>,
|
||||
message: &'static str,
|
||||
) -> Diagnostic {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
constraint.span,
|
||||
message,
|
||||
)
|
||||
.with_label(constraint.span, "constraint declared here")
|
||||
.with_label(operation_span, "constraint checked during this operation");
|
||||
if let Some((span, message)) = value_label {
|
||||
diagnostic = diagnostic.with_label(span, message);
|
||||
}
|
||||
diagnostic
|
||||
}
|
||||
|
||||
fn merge_default(
|
||||
&self,
|
||||
lhs: Option<ThunkId>,
|
||||
rhs: Option<ThunkId>,
|
||||
span: Span,
|
||||
) -> Result<Option<ThunkId>> {
|
||||
match (lhs, rhs) {
|
||||
(None, None) => Ok(None),
|
||||
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
|
||||
(Some(lhs), Some(rhs)) if lhs == rhs => Ok(Some(lhs)),
|
||||
(Some(lhs), Some(rhs)) => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
span,
|
||||
"conflicting defaults",
|
||||
)
|
||||
.with_label(self.thunk_span(lhs), "left default value")
|
||||
.with_label(self.thunk_span(rhs), "right default value")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,6 +948,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
object.fields.push(ObjectField {
|
||||
name: field.name,
|
||||
value,
|
||||
span: Span::default(),
|
||||
});
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
||||
@@ -845,6 +963,13 @@ impl<L: SourceLoader> Engine<L> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let constraints = constraints
|
||||
.into_iter()
|
||||
.map(|constraint| ConstraintEntry {
|
||||
constraint,
|
||||
span: Span::default(),
|
||||
})
|
||||
.collect();
|
||||
let constraints = normalize_constraints(constraints, Span::default())?;
|
||||
Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints,
|
||||
@@ -906,7 +1031,12 @@ impl<L: SourceLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn add_expr_thunk(&mut self, expr: ExprRef, env: EnvId) -> ThunkId {
|
||||
self.add_thunk(ThunkKind::Expr { expr, env })
|
||||
let span = self.expr_span(expr);
|
||||
self.add_expr_thunk_with_span(expr, env, span)
|
||||
}
|
||||
|
||||
fn add_expr_thunk_with_span(&mut self, expr: ExprRef, env: EnvId, span: Span) -> ThunkId {
|
||||
self.add_thunk(ThunkKind::Expr { expr, env }, span)
|
||||
}
|
||||
|
||||
fn add_constrained_thunk(
|
||||
@@ -916,23 +1046,32 @@ impl<L: SourceLoader> Engine<L> {
|
||||
value: ExprRef,
|
||||
value_env: EnvId,
|
||||
) -> ThunkId {
|
||||
self.add_thunk(ThunkKind::Constrained {
|
||||
constraint,
|
||||
constraint_env,
|
||||
value,
|
||||
value_env,
|
||||
})
|
||||
let span = self.expr_span(value);
|
||||
self.add_thunk(
|
||||
ThunkKind::Constrained {
|
||||
constraint,
|
||||
constraint_env,
|
||||
value,
|
||||
value_env,
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
fn add_value_thunk(&mut self, value: RuntimeValue) -> ThunkId {
|
||||
self.add_thunk(ThunkKind::Value(value))
|
||||
self.add_value_thunk_with_span(value, Span::default())
|
||||
}
|
||||
|
||||
fn add_thunk(&mut self, kind: ThunkKind) -> ThunkId {
|
||||
fn add_value_thunk_with_span(&mut self, value: RuntimeValue, span: Span) -> ThunkId {
|
||||
self.add_thunk(ThunkKind::Value(value), span)
|
||||
}
|
||||
|
||||
fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
|
||||
let id = ThunkId(self.thunks.len() as u32);
|
||||
self.thunks.push(Thunk {
|
||||
kind,
|
||||
state: ThunkState::Unevaluated,
|
||||
span,
|
||||
});
|
||||
id
|
||||
}
|
||||
@@ -977,9 +1116,28 @@ impl<L: SourceLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn expr_span(&self, reference: ExprRef) -> Span {
|
||||
self.modules[reference.module.0 as usize]
|
||||
.ast
|
||||
.span(reference.expr)
|
||||
self.modules
|
||||
.get(reference.module.0 as usize)
|
||||
.map(|module| module.ast.span(reference.expr))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn thunk_span(&self, thunk: ThunkId) -> Span {
|
||||
self.thunks[thunk.0 as usize].span
|
||||
}
|
||||
|
||||
fn with_path_context(
|
||||
&self,
|
||||
diagnostic: Diagnostic,
|
||||
phase: &'static str,
|
||||
path: &[String],
|
||||
) -> Diagnostic {
|
||||
if path.is_empty() {
|
||||
diagnostic
|
||||
} else {
|
||||
let span = diagnostic.span;
|
||||
diagnostic.with_label(span, format!("while {phase} `{}`", path.join(".")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,6 +1163,13 @@ fn primitive_type(name: &str) -> Option<PrimitiveType> {
|
||||
}
|
||||
}
|
||||
|
||||
fn constraint_primary_span(constraints: &[ConstraintEntry]) -> Span {
|
||||
constraints
|
||||
.first()
|
||||
.map(|constraint| constraint.span)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn literal_to_concrete(literal: Literal) -> ConcreteValue {
|
||||
match literal {
|
||||
Literal::String(value) => ConcreteValue::String(value),
|
||||
@@ -1343,23 +1508,6 @@ fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_default(
|
||||
lhs: Option<ThunkId>,
|
||||
rhs: Option<ThunkId>,
|
||||
span: Span,
|
||||
) -> Result<Option<ThunkId>> {
|
||||
match (lhs, rhs) {
|
||||
(None, None) => Ok(None),
|
||||
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
|
||||
(Some(lhs), Some(rhs)) if lhs == rhs => Ok(Some(lhs)),
|
||||
(Some(_), Some(_)) => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
span,
|
||||
"conflicting defaults",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1372,6 +1520,62 @@ mod tests {
|
||||
engine.materialize(&value).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constraint_violation_reports_related_spans() {
|
||||
let parsed = parse_source("port = Int & > 443 default 80;").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
let error = engine.materialize(&value).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message.contains("constraint declared here"))
|
||||
);
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message.contains("default value checked here"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_composition_conflict_reports_both_fields() {
|
||||
let parsed = parse_source("{ port = 80; } & { port = 443; }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Conflict);
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message == "left field `port`")
|
||||
);
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message == "right field `port`")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialize_error_reports_field_path() {
|
||||
let parsed = parse_source("service.port = Int;").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
let error = engine.materialize(&value).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Materialize);
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message.contains("while materializing `service.port`"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materializes_default() {
|
||||
let data = eval_data("port = Int & >= 1 default 8080;");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{ExprId, ast::CompareOp};
|
||||
use crate::{ExprId, Span, ast::CompareOp};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ModuleId(pub u32);
|
||||
@@ -37,6 +37,7 @@ pub struct ObjectValue {
|
||||
pub struct ObjectField {
|
||||
pub name: String,
|
||||
pub value: ThunkId,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -54,10 +55,16 @@ pub struct FunctionParam {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AbstractValue {
|
||||
pub constraints: Vec<Constraint>,
|
||||
pub constraints: Vec<ConstraintEntry>,
|
||||
pub default: Option<ThunkId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConstraintEntry {
|
||||
pub constraint: Constraint,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Constraint {
|
||||
Type(PrimitiveType),
|
||||
@@ -108,6 +115,7 @@ pub struct EnvId(pub u32);
|
||||
pub struct Thunk {
|
||||
pub kind: ThunkKind,
|
||||
pub state: ThunkState,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -125,14 +125,21 @@ fn normalize_path(path: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String {
|
||||
format!(
|
||||
let mut out = format!(
|
||||
"{:?} at {}:{}..{}: {}",
|
||||
diagnostic.kind,
|
||||
diagnostic.span.source.0,
|
||||
diagnostic.span.start,
|
||||
diagnostic.span.end,
|
||||
diagnostic.message,
|
||||
)
|
||||
);
|
||||
for label in diagnostic.labels {
|
||||
out.push_str(&format!(
|
||||
"\nnote at {}:{}..{}: {}",
|
||||
label.span.source.0, label.span.start, label.span.end, label.message,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn format_data(data: &Data, indent: usize) -> String {
|
||||
|
||||
Reference in New Issue
Block a user