Report structured import origins in diagnostics
This commit is contained in:
@@ -10,6 +10,7 @@ pub struct Diagnostic {
|
||||
pub span: Span,
|
||||
pub message: String,
|
||||
pub labels: Vec<DiagnosticLabel>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -25,6 +26,7 @@ impl Diagnostic {
|
||||
span,
|
||||
message: message.into(),
|
||||
labels: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +41,11 @@ impl Diagnostic {
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_note(mut self, message: impl Into<String>) -> Self {
|
||||
self.notes.push(message.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
+322
-29
@@ -21,6 +21,7 @@ pub struct Engine<L = EmptyLoader> {
|
||||
modules: Vec<Module>,
|
||||
imported_values: Vec<ImportedValue>,
|
||||
thunks: Vec<Thunk>,
|
||||
thunk_import_origins: Vec<Option<ImportedValueOrigin>>,
|
||||
envs: Vec<Env>,
|
||||
}
|
||||
|
||||
@@ -29,6 +30,39 @@ struct ImportedValue {
|
||||
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> {
|
||||
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
||||
let mut this = Self::new(EmptyLoader);
|
||||
@@ -52,6 +86,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
modules: Vec::new(),
|
||||
imported_values: Vec::new(),
|
||||
thunks: Vec::new(),
|
||||
thunk_import_origins: Vec::new(),
|
||||
envs: vec![Env {
|
||||
parent: None,
|
||||
bindings: Vec::new(),
|
||||
@@ -261,8 +296,9 @@ impl<L: ImportLoader> Engine<L> {
|
||||
if let Some(value) = self.find_imported_value(&key) {
|
||||
return self.force(value);
|
||||
}
|
||||
let value = self.internalize_host_value(value)?;
|
||||
let root = self.add_value_thunk(value);
|
||||
let origin = ImportedValueOrigin::root(key.clone());
|
||||
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.force(root)
|
||||
}
|
||||
@@ -836,17 +872,59 @@ impl<L: ImportLoader> Engine<L> {
|
||||
.iter()
|
||||
.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_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 value = self
|
||||
.compose_and(lhs_value, rhs_value, span)
|
||||
.compose_and(lhs_value.clone(), rhs_value.clone(), 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))
|
||||
let mut diagnostic = diag.with_label(
|
||||
span,
|
||||
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 {
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
@@ -872,9 +950,14 @@ impl<L: ImportLoader> Engine<L> {
|
||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||
{
|
||||
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 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;
|
||||
} else {
|
||||
lhs.fields.push(rhs_field);
|
||||
@@ -924,38 +1007,68 @@ impl<L: ImportLoader> Engine<L> {
|
||||
constraint,
|
||||
span,
|
||||
value_label,
|
||||
"value does not satisfy primitive type constraint",
|
||||
format!(
|
||||
"expected {primitive:?}, found {}",
|
||||
runtime_value_kind(&value)
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
Constraint::ArrayItems(item_constraint) => {
|
||||
let value_kind = runtime_value_kind(&value);
|
||||
let RuntimeValue::Concrete(ConcreteValue::Array(items)) = value else {
|
||||
return Err(self.constraint_violation(
|
||||
constraint,
|
||||
span,
|
||||
value_label,
|
||||
"value does not satisfy array element constraint",
|
||||
format!("expected Array, found {value_kind}"),
|
||||
));
|
||||
};
|
||||
let item_constraint_value = self.force(*item_constraint)?;
|
||||
let mut constrained_items = Vec::with_capacity(items.len());
|
||||
for (index, item) in items.into_iter().enumerate() {
|
||||
let item_span = self.thunk_span(item);
|
||||
let item_origin = self.thunk_import_origin(item).cloned();
|
||||
let item_value = self.force(item)?;
|
||||
let composition_span = if item_origin.is_some() {
|
||||
constraint.span
|
||||
} else {
|
||||
item_span
|
||||
};
|
||||
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| {
|
||||
diagnostic
|
||||
.with_label(
|
||||
let diagnostic = diagnostic.with_label(
|
||||
constraint.span,
|
||||
"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,
|
||||
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(
|
||||
constrained_items,
|
||||
@@ -996,7 +1109,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
constraint: &ConstraintEntry,
|
||||
operation_span: Span,
|
||||
value_label: Option<(Span, &'static str)>,
|
||||
message: &'static str,
|
||||
message: impl Into<String>,
|
||||
) -> Diagnostic {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
@@ -1032,6 +1145,14 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
|
||||
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 {
|
||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(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::Array(items) => {
|
||||
let mut thunks = Vec::new();
|
||||
for item in items {
|
||||
let value = self.internalize_host_value(item)?;
|
||||
thunks.push(self.add_value_thunk(value));
|
||||
for (index, item) in items.into_iter().enumerate() {
|
||||
let item_origin = origin.map(|origin| origin.index(index));
|
||||
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)))
|
||||
}
|
||||
@@ -1050,11 +1174,11 @@ impl<L: ImportLoader> Engine<L> {
|
||||
mut constraints,
|
||||
default,
|
||||
} => {
|
||||
let item = self.internalize_host_value(*item)?;
|
||||
let item = self.add_value_thunk(item);
|
||||
let item = self.internalize_host_value_with_origin(*item, origin)?;
|
||||
let item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value(*default)?;
|
||||
Some(self.add_value_thunk(value))
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1075,19 +1199,27 @@ impl<L: ImportLoader> Engine<L> {
|
||||
HostValue::Object(fields) => {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
for field in fields {
|
||||
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
|
||||
if object
|
||||
.fields
|
||||
.iter()
|
||||
.any(|existing| existing.name == field.name)
|
||||
{
|
||||
return Err(Diagnostic::new(
|
||||
let diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
Span::default(),
|
||||
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.add_value_thunk(value);
|
||||
let value = self
|
||||
.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 {
|
||||
name: field.name,
|
||||
value,
|
||||
@@ -1101,8 +1233,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
default,
|
||||
} => {
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value(*default)?;
|
||||
Some(self.add_value_thunk(value))
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1209,13 +1341,48 @@ impl<L: ImportLoader> Engine<L> {
|
||||
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 {
|
||||
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);
|
||||
self.thunks.push(Thunk {
|
||||
kind,
|
||||
state: ThunkState::Unevaluated,
|
||||
span,
|
||||
});
|
||||
self.thunk_import_origins.push(origin);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -1269,6 +1436,10 @@ impl<L: ImportLoader> Engine<L> {
|
||||
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(
|
||||
&self,
|
||||
diagnostic: Diagnostic,
|
||||
@@ -1327,6 +1498,9 @@ pub fn format_diagnostic_with<'a>(
|
||||
label.message,
|
||||
));
|
||||
}
|
||||
for note in &diagnostic.notes {
|
||||
out.push_str(&format!("\nnote: {note}"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -1401,6 +1575,45 @@ fn format_path(path: &[String]) -> String {
|
||||
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")]
|
||||
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
||||
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;
|
||||
|
||||
impl ImportLoader for StructuredAliasLoader {
|
||||
|
||||
@@ -11,6 +11,7 @@ Diagnostic {
|
||||
span: Span
|
||||
message: String
|
||||
labels: Vec<DiagnosticLabel>
|
||||
notes: Vec<String>
|
||||
}
|
||||
|
||||
DiagnosticLabel {
|
||||
@@ -22,6 +23,7 @@ DiagnosticLabel {
|
||||
`span` は primary location を示す。
|
||||
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
||||
`labels` は同じ error に関係する追加 location を示す。
|
||||
`notes` は source location を持たない semantic context を示す。
|
||||
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
||||
|
||||
代表的な diagnostic kind:
|
||||
@@ -37,6 +39,9 @@ DiagnosticLabel {
|
||||
- match 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` を返す。
|
||||
|
||||
@@ -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 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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user