Improve composition diagnostics
This commit is contained in:
parent
8b3df21cfa
commit
cc5ab63922
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -117,3 +117,9 @@ Abstract { constraints, default: None }
|
|||
|
||||
materialize は default を採用する唯一の段階である。
|
||||
通常評価中に明示値が得られた場合、default は採用されない。
|
||||
|
||||
## Diagnostic context
|
||||
|
||||
Composition and materialization keep source spans for object fields, constraints, defaults, and thunks where possible.
|
||||
When a conflict occurs, the diagnostic should identify the operation span and related participant spans, such as the left field, right field, constraint, or default value.
|
||||
For object materialization errors, diagnostics also include the field path being processed when known.
|
||||
|
|
|
|||
|
|
@ -10,10 +10,19 @@ Diagnostic {
|
|||
kind: DiagnosticKind
|
||||
span: Span
|
||||
message: String
|
||||
notes: Vec<Note>
|
||||
labels: Vec<DiagnosticLabel>
|
||||
}
|
||||
|
||||
DiagnosticLabel {
|
||||
span: Span
|
||||
message: String
|
||||
}
|
||||
```
|
||||
|
||||
`span` は primary location を示す。
|
||||
`labels` は同じ error に関係する追加 location を示す。
|
||||
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
||||
|
||||
代表的な diagnostic kind:
|
||||
|
||||
- syntax error
|
||||
|
|
@ -38,6 +47,18 @@ Result<RuntimeValue, Diagnostic>
|
|||
|
||||
これにより、制約違反、未定義識別子、循環依存、import 失敗などが通常値として流れることを避ける。
|
||||
|
||||
## 合成と materialize の diagnostic
|
||||
|
||||
合成や materialize の失敗は、以下を示す。
|
||||
|
||||
1. どの段階で失敗したか: composition、patch、materialization
|
||||
2. どの field path を処理中だったか
|
||||
3. どの constraint、value、default が衝突したか
|
||||
4. なぜ合成または materialize できないか
|
||||
|
||||
例えば default が constraint を満たさない場合は、constraint の位置と default value の位置の両方を label として持つ。
|
||||
object field の合成で concrete value が衝突する場合は、左辺 field と右辺 field の位置を label として持つ。
|
||||
|
||||
## `try / catch` は core に入れない
|
||||
|
||||
汎用 `try / catch` は core に入れない。
|
||||
|
|
@ -47,21 +68,6 @@ fallback は有限で明示的な仕組みに限定する。
|
|||
|
||||
- `default`: 未指定値の fallback。
|
||||
- `match`: 有限 pattern に基づく分岐。
|
||||
- optional import: ファイル不存在など、限定された失敗だけを fallback 可能にする候補。
|
||||
- optional field access: field 不在だけを fallback 可能にする候補。
|
||||
- union / tagged schema: 複数 schema の選択を明示的に表す将来候補。
|
||||
|
||||
## optional fallback の扱い
|
||||
|
||||
optional import や optional field access を導入する場合も、捕捉できる失敗は限定する。
|
||||
|
||||
例として optional import は、ファイル不存在だけを fallback 可能にし、parse error や import 先の制約違反は diagnostic として報告する方がよい。
|
||||
|
||||
```text
|
||||
optional import:
|
||||
file not found -> fallback
|
||||
parse error -> diagnostic
|
||||
eval error -> diagnostic
|
||||
```
|
||||
|
||||
この方針により、fallback は通常の値選択として扱い、エラー内容に依存した実行時分岐は避ける。
|
||||
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
|
||||
それらは diagnostic として報告する。
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ object は concrete structure として扱う。
|
|||
|
||||
```text
|
||||
ObjectValue:
|
||||
fields: Map<Symbol, ThunkId>
|
||||
fields: Map<Symbol, ObjectField>
|
||||
|
||||
ObjectField:
|
||||
value: ThunkId
|
||||
span: Span
|
||||
```
|
||||
|
||||
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
||||
|
|
@ -61,9 +65,14 @@ Concrete(Object {
|
|||
|
||||
```text
|
||||
AbstractValue {
|
||||
constraints: Vec<Constraint>
|
||||
constraints: Vec<ConstraintEntry>
|
||||
default: Option<ThunkId>
|
||||
}
|
||||
|
||||
ConstraintEntry {
|
||||
constraint: Constraint
|
||||
span: Span
|
||||
}
|
||||
```
|
||||
|
||||
`default` は `AbstractValue` にだけ存在する。
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue
Block a user