Report structured import origins in diagnostics
This commit is contained in:
@@ -10,6 +10,7 @@ pub struct Diagnostic {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub labels: Vec<DiagnosticLabel>,
|
pub labels: Vec<DiagnosticLabel>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -25,6 +26,7 @@ impl Diagnostic {
|
|||||||
span,
|
span,
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
|
notes: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +41,11 @@ impl Diagnostic {
|
|||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_note(mut self, message: impl Into<String>) -> Self {
|
||||||
|
self.notes.push(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
+324
-31
@@ -21,6 +21,7 @@ pub struct Engine<L = EmptyLoader> {
|
|||||||
modules: Vec<Module>,
|
modules: Vec<Module>,
|
||||||
imported_values: Vec<ImportedValue>,
|
imported_values: Vec<ImportedValue>,
|
||||||
thunks: Vec<Thunk>,
|
thunks: Vec<Thunk>,
|
||||||
|
thunk_import_origins: Vec<Option<ImportedValueOrigin>>,
|
||||||
envs: Vec<Env>,
|
envs: Vec<Env>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,39 @@ struct ImportedValue {
|
|||||||
root: ThunkId,
|
root: ThunkId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ImportedValueOrigin {
|
||||||
|
key: String,
|
||||||
|
path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportedValueOrigin {
|
||||||
|
fn root(key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
path: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(&self, name: String) -> Self {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(name);
|
||||||
|
Self {
|
||||||
|
key: self.key.clone(),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> Self {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(format!("[{index}]"));
|
||||||
|
Self {
|
||||||
|
key: self.key.clone(),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Engine<EmptyLoader> {
|
impl Engine<EmptyLoader> {
|
||||||
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
||||||
let mut this = Self::new(EmptyLoader);
|
let mut this = Self::new(EmptyLoader);
|
||||||
@@ -52,6 +86,7 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
modules: Vec::new(),
|
modules: Vec::new(),
|
||||||
imported_values: Vec::new(),
|
imported_values: Vec::new(),
|
||||||
thunks: Vec::new(),
|
thunks: Vec::new(),
|
||||||
|
thunk_import_origins: Vec::new(),
|
||||||
envs: vec![Env {
|
envs: vec![Env {
|
||||||
parent: None,
|
parent: None,
|
||||||
bindings: Vec::new(),
|
bindings: Vec::new(),
|
||||||
@@ -261,8 +296,9 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
if let Some(value) = self.find_imported_value(&key) {
|
if let Some(value) = self.find_imported_value(&key) {
|
||||||
return self.force(value);
|
return self.force(value);
|
||||||
}
|
}
|
||||||
let value = self.internalize_host_value(value)?;
|
let origin = ImportedValueOrigin::root(key.clone());
|
||||||
let root = self.add_value_thunk(value);
|
let value = self.internalize_host_value_with_origin(value, Some(&origin))?;
|
||||||
|
let root = self.add_value_thunk_with_import_origin(value, origin);
|
||||||
self.imported_values.push(ImportedValue { key, root });
|
self.imported_values.push(ImportedValue { key, root });
|
||||||
self.force(root)
|
self.force(root)
|
||||||
}
|
}
|
||||||
@@ -836,17 +872,59 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||||
{
|
{
|
||||||
|
let lhs_thunk = lhs.fields[index].value;
|
||||||
let lhs_field_span = lhs.fields[index].span;
|
let lhs_field_span = lhs.fields[index].span;
|
||||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
let lhs_origin = self.thunk_import_origin(lhs_thunk).cloned();
|
||||||
|
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||||
|
let lhs_value = self.force(lhs_thunk)?;
|
||||||
let rhs_value = self.force(rhs_field.value)?;
|
let rhs_value = self.force(rhs_field.value)?;
|
||||||
let value = self
|
let value = self
|
||||||
.compose_and(lhs_value, rhs_value, span)
|
.compose_and(lhs_value.clone(), rhs_value.clone(), span)
|
||||||
.map_err(|diag| {
|
.map_err(|diag| {
|
||||||
diag.with_label(span, format!("while composing field `{}`", rhs_field.name))
|
let mut diagnostic = diag.with_label(
|
||||||
.with_label(lhs_field_span, format!("left field `{}`", rhs_field.name))
|
span,
|
||||||
.with_label(rhs_field.span, format!("right field `{}`", rhs_field.name))
|
format!("while composing field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
if lhs_origin.is_some() {
|
||||||
|
diagnostic = annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
lhs_origin.as_ref(),
|
||||||
|
Some(&lhs_value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
diagnostic = diagnostic.with_label(
|
||||||
|
lhs_field_span,
|
||||||
|
format!("left field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rhs_origin.is_some() {
|
||||||
|
diagnostic = annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
rhs_origin.as_ref(),
|
||||||
|
Some(&rhs_value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
diagnostic = diagnostic.with_label(
|
||||||
|
rhs_field.span,
|
||||||
|
format!("right field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
diagnostic
|
||||||
})?;
|
})?;
|
||||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
let result_span = match (lhs_origin.is_some(), rhs_origin.is_some()) {
|
||||||
|
(false, true) => lhs_field_span,
|
||||||
|
(true, false) => rhs_field.span,
|
||||||
|
_ => rhs_field.span,
|
||||||
|
};
|
||||||
|
let result_origin = rhs_origin.clone().or(lhs_origin.clone());
|
||||||
|
lhs.fields[index].value = self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
value,
|
||||||
|
result_span,
|
||||||
|
result_origin,
|
||||||
|
);
|
||||||
|
if lhs_origin.is_some() && rhs_origin.is_none() {
|
||||||
|
lhs.fields[index].span = rhs_field.span;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lhs.fields.push(rhs_field);
|
lhs.fields.push(rhs_field);
|
||||||
}
|
}
|
||||||
@@ -872,9 +950,14 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||||
{
|
{
|
||||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
let lhs_value = self.force(lhs.fields[index].value)?;
|
||||||
|
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||||
let rhs_value = self.force(rhs_field.value)?;
|
let rhs_value = self.force(rhs_field.value)?;
|
||||||
let value = self.patch(lhs_value, rhs_value)?;
|
let value = self.patch(lhs_value, rhs_value)?;
|
||||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
lhs.fields[index].value = self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
value,
|
||||||
|
rhs_field.span,
|
||||||
|
rhs_origin,
|
||||||
|
);
|
||||||
lhs.fields[index].span = rhs_field.span;
|
lhs.fields[index].span = rhs_field.span;
|
||||||
} else {
|
} else {
|
||||||
lhs.fields.push(rhs_field);
|
lhs.fields.push(rhs_field);
|
||||||
@@ -924,38 +1007,68 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
constraint,
|
constraint,
|
||||||
span,
|
span,
|
||||||
value_label,
|
value_label,
|
||||||
"value does not satisfy primitive type constraint",
|
format!(
|
||||||
|
"expected {primitive:?}, found {}",
|
||||||
|
runtime_value_kind(&value)
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Constraint::ArrayItems(item_constraint) => {
|
Constraint::ArrayItems(item_constraint) => {
|
||||||
|
let value_kind = runtime_value_kind(&value);
|
||||||
let RuntimeValue::Concrete(ConcreteValue::Array(items)) = value else {
|
let RuntimeValue::Concrete(ConcreteValue::Array(items)) = value else {
|
||||||
return Err(self.constraint_violation(
|
return Err(self.constraint_violation(
|
||||||
constraint,
|
constraint,
|
||||||
span,
|
span,
|
||||||
value_label,
|
value_label,
|
||||||
"value does not satisfy array element constraint",
|
format!("expected Array, found {value_kind}"),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let item_constraint_value = self.force(*item_constraint)?;
|
let item_constraint_value = self.force(*item_constraint)?;
|
||||||
let mut constrained_items = Vec::with_capacity(items.len());
|
let mut constrained_items = Vec::with_capacity(items.len());
|
||||||
for (index, item) in items.into_iter().enumerate() {
|
for (index, item) in items.into_iter().enumerate() {
|
||||||
let item_span = self.thunk_span(item);
|
let item_span = self.thunk_span(item);
|
||||||
|
let item_origin = self.thunk_import_origin(item).cloned();
|
||||||
let item_value = self.force(item)?;
|
let item_value = self.force(item)?;
|
||||||
|
let composition_span = if item_origin.is_some() {
|
||||||
|
constraint.span
|
||||||
|
} else {
|
||||||
|
item_span
|
||||||
|
};
|
||||||
let constrained = self
|
let constrained = self
|
||||||
.compose_and(item_constraint_value.clone(), item_value, item_span)
|
.compose_and(
|
||||||
|
item_constraint_value.clone(),
|
||||||
|
item_value.clone(),
|
||||||
|
composition_span,
|
||||||
|
)
|
||||||
.map_err(|diagnostic| {
|
.map_err(|diagnostic| {
|
||||||
diagnostic
|
let diagnostic = diagnostic.with_label(
|
||||||
.with_label(
|
constraint.span,
|
||||||
constraint.span,
|
"array element constraint declared here",
|
||||||
"array element constraint declared here",
|
);
|
||||||
|
if item_origin.is_some() {
|
||||||
|
annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
item_origin.as_ref(),
|
||||||
|
Some(&item_value),
|
||||||
)
|
)
|
||||||
.with_label(
|
} else {
|
||||||
|
diagnostic.with_label(
|
||||||
item_span,
|
item_span,
|
||||||
format!("array element [{index}] checked here"),
|
format!("array element [{index}] checked here"),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
constrained_items.push(self.add_value_thunk_with_span(constrained, item_span));
|
let constrained_span = if item_origin.is_some() {
|
||||||
|
constraint.span
|
||||||
|
} else {
|
||||||
|
item_span
|
||||||
|
};
|
||||||
|
constrained_items.push(self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
constrained,
|
||||||
|
constrained_span,
|
||||||
|
item_origin,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(
|
||||||
constrained_items,
|
constrained_items,
|
||||||
@@ -996,7 +1109,7 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
constraint: &ConstraintEntry,
|
constraint: &ConstraintEntry,
|
||||||
operation_span: Span,
|
operation_span: Span,
|
||||||
value_label: Option<(Span, &'static str)>,
|
value_label: Option<(Span, &'static str)>,
|
||||||
message: &'static str,
|
message: impl Into<String>,
|
||||||
) -> Diagnostic {
|
) -> Diagnostic {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::ConstraintViolation,
|
DiagnosticKind::ConstraintViolation,
|
||||||
@@ -1032,6 +1145,14 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
||||||
|
self.internalize_host_value_with_origin(value, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internalize_host_value_with_origin(
|
||||||
|
&mut self,
|
||||||
|
value: HostValue,
|
||||||
|
origin: Option<&ImportedValueOrigin>,
|
||||||
|
) -> Result<RuntimeValue> {
|
||||||
match value {
|
match value {
|
||||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||||
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||||
@@ -1039,9 +1160,12 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||||
HostValue::Array(items) => {
|
HostValue::Array(items) => {
|
||||||
let mut thunks = Vec::new();
|
let mut thunks = Vec::new();
|
||||||
for item in items {
|
for (index, item) in items.into_iter().enumerate() {
|
||||||
let value = self.internalize_host_value(item)?;
|
let item_origin = origin.map(|origin| origin.index(index));
|
||||||
thunks.push(self.add_value_thunk(value));
|
let value =
|
||||||
|
self.internalize_host_value_with_origin(item, item_origin.as_ref())?;
|
||||||
|
thunks
|
||||||
|
.push(self.add_value_thunk_with_optional_import_origin(value, item_origin));
|
||||||
}
|
}
|
||||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
||||||
}
|
}
|
||||||
@@ -1050,11 +1174,11 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
mut constraints,
|
mut constraints,
|
||||||
default,
|
default,
|
||||||
} => {
|
} => {
|
||||||
let item = self.internalize_host_value(*item)?;
|
let item = self.internalize_host_value_with_origin(*item, origin)?;
|
||||||
let item = self.add_value_thunk(item);
|
let item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
|
||||||
let default = if let Some(default) = default {
|
let default = if let Some(default) = default {
|
||||||
let value = self.internalize_host_value(*default)?;
|
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||||
Some(self.add_value_thunk(value))
|
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -1075,19 +1199,27 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
HostValue::Object(fields) => {
|
HostValue::Object(fields) => {
|
||||||
let mut object = ObjectValue { fields: Vec::new() };
|
let mut object = ObjectValue { fields: Vec::new() };
|
||||||
for field in fields {
|
for field in fields {
|
||||||
|
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
|
||||||
if object
|
if object
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.any(|existing| existing.name == field.name)
|
.any(|existing| existing.name == field.name)
|
||||||
{
|
{
|
||||||
return Err(Diagnostic::new(
|
let diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
Span::default(),
|
Span::default(),
|
||||||
format!("duplicate host object field `{}`", field.name),
|
format!("duplicate host object field `{}`", field.name),
|
||||||
|
);
|
||||||
|
return Err(annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
field_origin.as_ref(),
|
||||||
|
None,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let value = self.internalize_host_value(field.value)?;
|
let value = self
|
||||||
let value = self.add_value_thunk(value);
|
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?;
|
||||||
|
let value =
|
||||||
|
self.add_value_thunk_with_optional_import_origin(value, field_origin);
|
||||||
object.fields.push(ObjectField {
|
object.fields.push(ObjectField {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value,
|
value,
|
||||||
@@ -1101,8 +1233,8 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
default,
|
default,
|
||||||
} => {
|
} => {
|
||||||
let default = if let Some(default) = default {
|
let default = if let Some(default) = default {
|
||||||
let value = self.internalize_host_value(*default)?;
|
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||||
Some(self.add_value_thunk(value))
|
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -1209,13 +1341,48 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
self.add_thunk(ThunkKind::Value(value), span)
|
self.add_thunk(ThunkKind::Value(value), span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
origin: ImportedValueOrigin,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_value_thunk_with_optional_import_origin(value, Some(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_optional_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_value_thunk_with_span_and_import_origin(value, Span::default(), origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_span_and_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
span: Span,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_thunk_with_import_origin(ThunkKind::Value(value), span, origin)
|
||||||
|
}
|
||||||
|
|
||||||
fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
|
fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
|
||||||
|
self.add_thunk_with_import_origin(kind, span, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_thunk_with_import_origin(
|
||||||
|
&mut self,
|
||||||
|
kind: ThunkKind,
|
||||||
|
span: Span,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
let id = ThunkId(self.thunks.len() as u32);
|
let id = ThunkId(self.thunks.len() as u32);
|
||||||
self.thunks.push(Thunk {
|
self.thunks.push(Thunk {
|
||||||
kind,
|
kind,
|
||||||
state: ThunkState::Unevaluated,
|
state: ThunkState::Unevaluated,
|
||||||
span,
|
span,
|
||||||
});
|
});
|
||||||
|
self.thunk_import_origins.push(origin);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,6 +1436,10 @@ impl<L: ImportLoader> Engine<L> {
|
|||||||
self.thunks[thunk.0 as usize].span
|
self.thunks[thunk.0 as usize].span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thunk_import_origin(&self, thunk: ThunkId) -> Option<&ImportedValueOrigin> {
|
||||||
|
self.thunk_import_origins[thunk.0 as usize].as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn with_path_context(
|
fn with_path_context(
|
||||||
&self,
|
&self,
|
||||||
diagnostic: Diagnostic,
|
diagnostic: Diagnostic,
|
||||||
@@ -1327,6 +1498,9 @@ pub fn format_diagnostic_with<'a>(
|
|||||||
label.message,
|
label.message,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
for note in &diagnostic.notes {
|
||||||
|
out.push_str(&format!("\nnote: {note}"));
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1401,6 +1575,45 @@ fn format_path(path: &[String]) -> String {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn annotate_imported_value(
|
||||||
|
diagnostic: Diagnostic,
|
||||||
|
origin: Option<&ImportedValueOrigin>,
|
||||||
|
value: Option<&RuntimeValue>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let Some(origin) = origin else {
|
||||||
|
return diagnostic;
|
||||||
|
};
|
||||||
|
if diagnostic
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.any(|note| note.starts_with("imported `"))
|
||||||
|
{
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
let path = if origin.path.is_empty() {
|
||||||
|
String::from("<root>")
|
||||||
|
} else {
|
||||||
|
format_path(&origin.path)
|
||||||
|
};
|
||||||
|
let value = value
|
||||||
|
.map(|value| format!(" supplied a value of type {}", runtime_value_kind(value)))
|
||||||
|
.unwrap_or_default();
|
||||||
|
diagnostic.with_note(format!("imported `{}` at `{path}`{value}", origin.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_value_kind(value: &RuntimeValue) -> &'static str {
|
||||||
|
match value {
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::String(_)) => "String",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Int(_)) => "Int",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Float(_)) => "Float",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Bool(_)) => "Bool",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Array(_)) => "Array",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Object(_)) => "Object",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Function(_)) => "Function",
|
||||||
|
RuntimeValue::Abstract(_) => "abstract",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "regex")]
|
#[cfg(feature = "regex")]
|
||||||
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
||||||
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
|
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
|
||||||
@@ -2127,6 +2340,86 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_import_violation_reports_key_path_and_schema_span() {
|
||||||
|
let mut engine = Engine::new(MarkdownLoader {
|
||||||
|
source: String::from("---\ntitle: \"Hello\"\ndraft: maybe\n---\n# Hello"),
|
||||||
|
});
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
Post = {
|
||||||
|
frontmatter = {
|
||||||
|
title = String;
|
||||||
|
draft = Bool;
|
||||||
|
};
|
||||||
|
body = String;
|
||||||
|
};
|
||||||
|
in Post & import "./post.md"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = engine.eval_module(module).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
|
||||||
|
assert_eq!(error.message, "expected Bool, found String");
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.all(|label| label.span != Span::default())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.any(|label| label.message == "constraint declared here")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
error.notes,
|
||||||
|
["imported `content/post.md` at `frontmatter.draft` supplied a value of type String"]
|
||||||
|
);
|
||||||
|
assert!(engine.format_diagnostic(&error).contains(
|
||||||
|
"note: imported `content/post.md` at `frontmatter.draft` supplied a value of type String"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StructuredArrayLoader;
|
||||||
|
|
||||||
|
impl ImportLoader for StructuredArrayLoader {
|
||||||
|
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"content/navigation.json",
|
||||||
|
HostValue::object([(
|
||||||
|
"items",
|
||||||
|
HostValue::array([HostValue::string("home"), HostValue::int(2)]),
|
||||||
|
)]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_array_violation_reports_indexed_import_path() {
|
||||||
|
let mut engine = Engine::new(StructuredArrayLoader);
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"{ items = [...String]; } & import "./navigation.json""#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = engine.eval_module(module).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.message, "expected String, found Int");
|
||||||
|
assert_eq!(
|
||||||
|
error.notes,
|
||||||
|
["imported `content/navigation.json` at `items[1]` supplied a value of type Int"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
struct StructuredAliasLoader;
|
struct StructuredAliasLoader;
|
||||||
|
|
||||||
impl ImportLoader for StructuredAliasLoader {
|
impl ImportLoader for StructuredAliasLoader {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Diagnostic {
|
|||||||
span: Span
|
span: Span
|
||||||
message: String
|
message: String
|
||||||
labels: Vec<DiagnosticLabel>
|
labels: Vec<DiagnosticLabel>
|
||||||
|
notes: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
DiagnosticLabel {
|
DiagnosticLabel {
|
||||||
@@ -22,6 +23,7 @@ DiagnosticLabel {
|
|||||||
`span` は primary location を示す。
|
`span` は primary location を示す。
|
||||||
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
||||||
`labels` は同じ error に関係する追加 location を示す。
|
`labels` は同じ error に関係する追加 location を示す。
|
||||||
|
`notes` は source location を持たない semantic context を示す。
|
||||||
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
||||||
|
|
||||||
代表的な diagnostic kind:
|
代表的な diagnostic kind:
|
||||||
@@ -37,6 +39,9 @@ DiagnosticLabel {
|
|||||||
- match failure
|
- match failure
|
||||||
- materialization failure
|
- materialization failure
|
||||||
|
|
||||||
|
Structured imports use semantic provenance rather than synthetic source spans.
|
||||||
|
Constraint failures report the imported value's stable key and logical field or array path alongside the source span of the Decodal constraint that rejected it.
|
||||||
|
|
||||||
## エラーは値ではない
|
## エラーは値ではない
|
||||||
|
|
||||||
評価失敗は `RuntimeValue` ではなく `Diagnostic` を返す。
|
評価失敗は `RuntimeValue` ではなく `Diagnostic` を返す。
|
||||||
|
|||||||
@@ -159,4 +159,7 @@ The core does not select content types or bundle Markdown/frontmatter parsers.
|
|||||||
The loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics.
|
The loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics.
|
||||||
The stable loader key is also used to cache structured imports.
|
The stable loader key is also used to cache structured imports.
|
||||||
|
|
||||||
|
When a structured value fails a Decodal constraint, the diagnostic keeps the Decodal constraint span and identifies the host value by its stable import key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||||
|
`HostValue` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance.
|
||||||
|
|
||||||
`load` is the single import hook: loaders dispatch by extension, media type, or another host-defined rule and return the appropriate variant directly.
|
`load` is the single import hook: loaders dispatch by extension, media type, or another host-defined rule and return the appropriate variant directly.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user