Improve composition diagnostics

This commit is contained in:
2026-06-22 17:37:51 +09:00
parent 8b3df21cfa
commit cc5ab63922
10 changed files with 518 additions and 175 deletions
+161 -83
View File
@@ -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()
);
}
}
+17 -1
View File
@@ -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
View File
@@ -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;");
+10 -2
View File
@@ -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)]