Compare commits

...

3 Commits

Author SHA1 Message Date
a2a6dee025
Show source names in diagnostics 2026-06-22 18:05:26 +09:00
cc5ab63922
Improve composition diagnostics 2026-06-22 17:37:51 +09:00
8b3df21cfa
Document lightweight runtime boundaries 2026-06-19 22:48:33 +09:00
12 changed files with 666 additions and 212 deletions

View File

@ -4,19 +4,22 @@ use std::{
process::ExitCode, process::ExitCode,
}; };
use decodal_core::{Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceLoader, Span}; use decodal_core::{
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
format_diagnostic_with,
};
fn main() -> ExitCode { fn main() -> ExitCode {
match run() { match run() {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
print_diagnostic(&error); eprintln!("{error}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
} }
fn run() -> Result<(), Diagnostic> { fn run() -> Result<(), String> {
let mut args = env::args().skip(1); let mut args = env::args().skip(1);
let first = args.next().unwrap_or_else(|| String::from("--help")); let first = args.next().unwrap_or_else(|| String::from("--help"));
match first.as_str() { match first.as_str() {
@ -51,12 +54,19 @@ fn run() -> Result<(), Diagnostic> {
} }
} }
fn materialize_path(path: &str) -> Result<Data, Diagnostic> { fn materialize_path(path: &str) -> Result<Data, String> {
let root = read_root_source(path)?; let root = read_root_source(path).map_err(format_raw_diagnostic)?;
let root_name = root.name.clone();
let mut engine = Engine::new(FsLoader); let mut engine = Engine::new(FsLoader);
let module = engine.add_root_source(root.key, root.name, &root.source)?; let module = engine
let value = engine.eval_module(module)?; .add_root_source(root.key, root.name, &root.source)
engine.materialize(&value) .map_err(|error| format_diagnostic_with_root(&error, &root_name))?;
let value = engine
.eval_module(module)
.map_err(|error| engine.format_diagnostic(&error))?;
engine
.materialize(&value)
.map_err(|error| engine.format_diagnostic(&error))
} }
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> { fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
@ -139,15 +149,12 @@ fn print_help() {
println!(" decodal - Read DCDL source from stdin"); println!(" decodal - Read DCDL source from stdin");
} }
fn print_diagnostic(error: &Diagnostic) { fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String {
eprintln!( format_diagnostic_with(error, |source| (source == SourceId(0)).then_some(root_name))
"error[{kind:?}] {source}:{start}..{end}: {message}", }
kind = error.kind,
source = error.span.source.0, fn format_raw_diagnostic(error: Diagnostic) -> String {
start = error.span.start, format_diagnostic_with(&error, |_| None)
end = error.span.end,
message = error.message,
);
} }
fn print_data(data: &Data, indent: usize) { fn print_data(data: &Data, indent: usize) {

View File

@ -3,103 +3,154 @@ use alloc::{string::String, vec::Vec};
use crate::{ use crate::{
Diagnostic, DiagnosticKind, Span, Diagnostic, DiagnosticKind, Span,
ast::CompareOp, ast::CompareOp,
runtime::{Constraint, LiteralValue, PrimitiveType}, runtime::{Constraint, ConstraintEntry, LiteralValue, PrimitiveType},
}; };
pub fn normalize_constraints( pub fn normalize_constraints(
constraints: Vec<Constraint>, constraints: Vec<ConstraintEntry>,
span: Span, span: Span,
) -> crate::Result<Vec<Constraint>> { ) -> crate::Result<Vec<ConstraintEntry>> {
let mut primitive = None; let mut primitive: Option<(PrimitiveType, Span)> = None;
let mut lower: Option<Bound> = None; let mut lower: Option<(Bound, Span)> = None;
let mut upper: Option<Bound> = None; let mut upper: Option<(Bound, Span)> = None;
let mut rest = Vec::new(); let mut rest = Vec::new();
for constraint in constraints { for entry in constraints {
match constraint { match entry.constraint {
Constraint::Type(next) => match primitive { Constraint::Type(next) => match primitive {
Some(current) if current != next => { Some((current, current_span)) if current != next => {
return Err(Diagnostic::new( return Err(Diagnostic::new(
DiagnosticKind::Conflict, DiagnosticKind::Conflict,
span, span,
"primitive type constraints conflict", "primitive type constraints conflict",
)); )
.with_label(current_span, "first primitive constraint")
.with_label(entry.span, "conflicting primitive constraint"));
} }
Some(_) => {} Some(_) => {}
None => primitive = Some(next), None => primitive = Some((next, entry.span)),
}, },
Constraint::Compare(op, value) => { Constraint::Compare(op, value) => {
let number = Number::from_literal(&value).ok_or_else(|| { let number = Number::from_literal(&value).ok_or_else(|| {
Diagnostic::new( Diagnostic::new(
DiagnosticKind::TypeMismatch, DiagnosticKind::TypeMismatch,
span, entry.span,
"comparison constraints require numeric literals", "comparison constraints require numeric literals",
) )
})?; })?;
match op { match op {
CompareOp::Gt => merge_lower(&mut lower, Bound::new(number, false)), CompareOp::Gt => {
CompareOp::Gte => merge_lower(&mut lower, Bound::new(number, true)), merge_lower(&mut lower, (Bound::new(number, false), entry.span))
CompareOp::Lt => merge_upper(&mut upper, Bound::new(number, false)), }
CompareOp::Lte => merge_upper(&mut upper, Bound::new(number, true)), 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 => { CompareOp::Eq => {
merge_lower(&mut lower, Bound::new(number, true)); merge_lower(&mut lower, (Bound::new(number, true), entry.span));
merge_upper(&mut upper, Bound::new(number, true)); merge_upper(&mut upper, (Bound::new(number, true), entry.span));
} }
} }
} }
Constraint::Regex(pattern) => rest.push(Constraint::Regex(pattern)), Constraint::Regex(pattern) => rest.push(ConstraintEntry {
Constraint::BuiltinPredicate(name) => rest.push(Constraint::BuiltinPredicate(name)), 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)) if matches!(
&& (lower.is_some() || upper.is_some()) primitive,
Some((PrimitiveType::String | PrimitiveType::Bool, _))
) && (lower.is_some() || upper.is_some())
{ {
return Err(Diagnostic::new( let mut diagnostic = Diagnostic::new(
DiagnosticKind::Conflict, DiagnosticKind::Conflict,
span, span,
"numeric comparison constraints conflict with non-numeric primitive type", "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 && lower
.iter() .iter()
.chain(upper.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, DiagnosticKind::Conflict,
span, span,
"Int comparison constraints must use integer literals", "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)?; ensure_bounds_non_empty(primitive, lower, upper, span)?;
let mut normalized = Vec::new(); let mut normalized = Vec::new();
if let Some(primitive) = primitive { if let Some((primitive, primitive_span)) = primitive {
normalized.push(Constraint::Type(primitive)); normalized.push(ConstraintEntry {
constraint: Constraint::Type(primitive),
span: primitive_span,
});
} }
if let Some(lower) = lower { if let Some((lower, lower_span)) = lower {
normalized.push(Constraint::Compare( normalized.push(ConstraintEntry {
constraint: Constraint::Compare(
if lower.inclusive { if lower.inclusive {
CompareOp::Gte CompareOp::Gte
} else { } else {
CompareOp::Gt CompareOp::Gt
}, },
lower.number.into_literal(), lower.number.into_literal(),
)); ),
span: lower_span,
});
} }
if let Some(upper) = upper { if let Some((upper, upper_span)) = upper {
normalized.push(Constraint::Compare( normalized.push(ConstraintEntry {
constraint: Constraint::Compare(
if upper.inclusive { if upper.inclusive {
CompareOp::Lte CompareOp::Lte
} else { } else {
CompareOp::Lt CompareOp::Lt
}, },
upper.number.into_literal(), upper.number.into_literal(),
)); ),
span: upper_span,
});
} }
normalized.extend(rest); normalized.extend(rest);
Ok(normalized) 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 { match current {
None => *current = Some(next), 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(_) => {} Some(_) => {}
} }
} }
fn merge_upper(current: &mut Option<Bound>, next: Bound) { fn merge_upper(current: &mut Option<(Bound, Span)>, next: (Bound, Span)) {
match current { match current {
None => *current = Some(next), 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(_) => {} Some(_) => {}
} }
} }
@ -178,28 +233,32 @@ fn is_stricter_upper(next: Bound, current: Bound) -> bool {
} }
fn ensure_bounds_non_empty( fn ensure_bounds_non_empty(
primitive: Option<PrimitiveType>, primitive: Option<(PrimitiveType, Span)>,
lower: Option<Bound>, lower: Option<(Bound, Span)>,
upper: Option<Bound>, upper: Option<(Bound, Span)>,
span: Span, span: Span,
) -> crate::Result<()> { ) -> crate::Result<()> {
if primitive == Some(PrimitiveType::Int) { if matches!(primitive, Some((PrimitiveType::Int, _))) {
let min = lower.map(int_lower_bound).unwrap_or(i128::from(i64::MIN)); let min = lower
let max = upper.map(int_upper_bound).unwrap_or(i128::from(i64::MAX)); .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 { if min > max {
return Err(empty_numeric_bounds(span)); return Err(empty_numeric_bounds(span, lower, upper));
} }
return Ok(()); return Ok(());
} }
if let (Some(lower), Some(upper)) = (lower, upper) { if let (Some((lower_bound, _)), Some((upper_bound, _))) = (lower, upper) {
let lower_value = lower.number.as_f64(); let lower_value = lower_bound.number.as_f64();
let upper_value = upper.number.as_f64(); let upper_value = upper_bound.number.as_f64();
if lower_value > upper_value { 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) { if lower_value == upper_value && !(lower_bound.inclusive && upper_bound.inclusive) {
return Err(empty_numeric_bounds(span)); return Err(empty_numeric_bounds(span, lower, upper));
} }
} }
Ok(()) Ok(())
@ -227,12 +286,23 @@ fn int_upper_bound(bound: Bound) -> i128 {
} }
} }
fn empty_numeric_bounds(span: Span) -> Diagnostic { fn empty_numeric_bounds(
Diagnostic::new( span: Span,
lower: Option<(Bound, Span)>,
upper: Option<(Bound, Span)>,
) -> Diagnostic {
let mut diagnostic = Diagnostic::new(
DiagnosticKind::Conflict, DiagnosticKind::Conflict,
span, span,
String::from("numeric comparison constraints have an empty intersection"), 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)] #[cfg(test)]
@ -240,13 +310,20 @@ mod tests {
use super::*; use super::*;
use crate::runtime::LiteralValue; use crate::runtime::LiteralValue;
fn entry(constraint: Constraint) -> ConstraintEntry {
ConstraintEntry {
constraint,
span: Span::default(),
}
}
#[test] #[test]
fn detects_primitive_conflict() { fn detects_primitive_conflict() {
assert!( assert!(
normalize_constraints( normalize_constraints(
alloc::vec![ alloc::vec![
Constraint::Type(PrimitiveType::Int), entry(Constraint::Type(PrimitiveType::Int)),
Constraint::Type(PrimitiveType::String), entry(Constraint::Type(PrimitiveType::String)),
], ],
Span::default(), Span::default(),
) )
@ -259,9 +336,9 @@ mod tests {
assert!( assert!(
normalize_constraints( normalize_constraints(
alloc::vec![ alloc::vec![
Constraint::Type(PrimitiveType::Int), entry(Constraint::Type(PrimitiveType::Int)),
Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10)), entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10))),
Constraint::Compare(CompareOp::Lt, LiteralValue::Int(11)), entry(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(5))),
], ],
Span::default(), Span::default(),
) )
@ -270,15 +347,16 @@ mod tests {
} }
#[test] #[test]
fn keeps_regex_constraints_without_intersection_check() { fn detects_non_integer_int_bound() {
let constraints = normalize_constraints( assert!(
normalize_constraints(
alloc::vec![ alloc::vec![
Constraint::Regex(String::from("^a$")), entry(Constraint::Type(PrimitiveType::Int)),
Constraint::Regex(String::from("^b$")), entry(Constraint::Compare(CompareOp::Gt, LiteralValue::Float(1.5))),
], ],
Span::default(), Span::default(),
) )
.unwrap(); .is_err()
assert_eq!(constraints.len(), 2); );
} }
} }

View File

@ -1,4 +1,4 @@
use alloc::string::String; use alloc::{string::String, vec::Vec};
use crate::span::Span; use crate::span::Span;
@ -9,6 +9,13 @@ pub struct Diagnostic {
pub kind: DiagnosticKind, pub kind: DiagnosticKind,
pub span: Span, pub span: Span,
pub message: String, pub message: String,
pub labels: Vec<DiagnosticLabel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticLabel {
pub span: Span,
pub message: String,
} }
impl Diagnostic { impl Diagnostic {
@ -17,12 +24,21 @@ impl Diagnostic {
kind, kind,
span, span,
message: message.into(), message: message.into(),
labels: Vec::new(),
} }
} }
pub fn syntax(span: Span, message: impl Into<String>) -> Self { pub fn syntax(span: Span, message: impl Into<String>) -> Self {
Self::new(DiagnosticKind::Syntax, span, message) 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -9,9 +9,9 @@ use crate::{
module::{EmptyLoader, LoadedSource, Module, SourceLoader}, module::{EmptyLoader, LoadedSource, Module, SourceLoader},
parse_source_with_source_id, parse_source_with_source_id,
runtime::{ runtime::{
AbstractValue, Binding, ConcreteValue, Constraint, Data, DataField, Env, EnvId, ExprRef, AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField, ObjectValue, EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState, ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
}, },
}; };
@ -84,6 +84,25 @@ impl<L: SourceLoader> Engine<L> {
} }
pub fn materialize(&mut self, value: &RuntimeValue) -> Result<Data> { pub fn materialize(&mut self, value: &RuntimeValue) -> Result<Data> {
self.materialize_with_path(value, &mut Vec::new())
}
pub fn source_name(&self, source: SourceId) -> Option<&str> {
self.modules
.iter()
.find(|module| module.source == source)
.map(|module| module.name.as_str())
}
pub fn format_diagnostic(&self, diagnostic: &Diagnostic) -> String {
format_diagnostic_with(diagnostic, |source| self.source_name(source))
}
fn materialize_with_path(
&mut self,
value: &RuntimeValue,
path: &mut Vec<String>,
) -> Result<Data> {
match value { match value {
RuntimeValue::Concrete(value) => match value { RuntimeValue::Concrete(value) => match value {
ConcreteValue::String(value) => Ok(Data::String(value.clone())), ConcreteValue::String(value) => Ok(Data::String(value.clone())),
@ -94,7 +113,7 @@ impl<L: SourceLoader> Engine<L> {
let mut data = Vec::new(); let mut data = Vec::new();
for item in items { for item in items {
let value = self.force(*item)?; let value = self.force(*item)?;
data.push(self.materialize(&value)?); data.push(self.materialize_with_path(&value, path)?);
} }
Ok(Data::Array(data)) Ok(Data::Array(data))
} }
@ -102,30 +121,51 @@ impl<L: SourceLoader> Engine<L> {
let mut fields = Vec::new(); let mut fields = Vec::new();
for field in &object.fields { for field in &object.fields {
let value = self.force(field.value)?; let value = self.force(field.value)?;
path.push(field.name.clone());
let value = self.materialize_with_path(&value, path);
path.pop();
fields.push(DataField { fields.push(DataField {
name: field.name.clone(), name: field.name.clone(),
value: self.materialize(&value)?, value: value?,
}); });
} }
Ok(Data::Object(fields)) Ok(Data::Object(fields))
} }
ConcreteValue::Function(_) => Err(Diagnostic::new( ConcreteValue::Function(_) => Err(self.with_path_context(
Diagnostic::new(
DiagnosticKind::Materialize, DiagnosticKind::Materialize,
Span::default(), Span::default(),
"cannot materialize function value", "cannot materialize function value",
),
"materializing",
path,
)), )),
}, },
RuntimeValue::Abstract(abstract_value) => { RuntimeValue::Abstract(abstract_value) => {
let Some(default) = abstract_value.default else { let Some(default) = abstract_value.default else {
return Err(Diagnostic::new( let mut diagnostic = Diagnostic::new(
DiagnosticKind::Materialize, DiagnosticKind::Materialize,
Span::default(), constraint_primary_span(&abstract_value.constraints),
"cannot materialize unresolved abstract value without default", "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)?; let value = self.force(default)?;
self.ensure_satisfies(&value, &abstract_value.constraints, Span::default())?; self.ensure_satisfies(
self.materialize(&value) &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 +515,18 @@ impl<L: SourceLoader> Engine<L> {
) )
})?; })?;
Ok(RuntimeValue::Abstract(AbstractValue { Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![Constraint::Compare(op, value)], constraints: vec![ConstraintEntry {
constraint: Constraint::Compare(op, value),
span,
}],
default: None, default: None,
})) }))
} }
Expr::RegexConstraint(pattern) => Ok(RuntimeValue::Abstract(AbstractValue { Expr::RegexConstraint(pattern) => Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![Constraint::Regex(pattern)], constraints: vec![ConstraintEntry {
constraint: Constraint::Regex(pattern),
span,
}],
default: None, default: None,
})), })),
Expr::Wildcard => Err(Diagnostic::new( Expr::Wildcard => Err(Diagnostic::new(
@ -494,7 +540,10 @@ impl<L: SourceLoader> Engine<L> {
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> { fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
if let Some(primitive) = primitive_type(name) { if let Some(primitive) = primitive_type(name) {
return Ok(RuntimeValue::Abstract(AbstractValue { return Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![Constraint::Type(primitive)], constraints: vec![ConstraintEntry {
constraint: Constraint::Type(primitive),
span,
}],
default: None, default: None,
})); }));
} }
@ -503,7 +552,10 @@ impl<L: SourceLoader> Engine<L> {
} }
if name.chars().next().is_some_and(char::is_uppercase) { if name.chars().next().is_some_and(char::is_uppercase) {
return Ok(RuntimeValue::Abstract(AbstractValue { return Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![Constraint::BuiltinPredicate(String::from(name))], constraints: vec![ConstraintEntry {
constraint: Constraint::BuiltinPredicate(String::from(name)),
span,
}],
default: None, default: None,
})); }));
} }
@ -551,7 +603,7 @@ impl<L: SourceLoader> Engine<L> {
)); ));
} }
if path.len() == 1 { 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]) { if object.fields.iter().any(|field| field.name == path[0]) {
return Err(Diagnostic::new( return Err(Diagnostic::new(
DiagnosticKind::Conflict, DiagnosticKind::Conflict,
@ -562,6 +614,7 @@ impl<L: SourceLoader> Engine<L> {
object.fields.push(ObjectField { object.fields.push(ObjectField {
name: path[0].clone(), name: path[0].clone(),
value, value,
span,
}); });
return Ok(()); return Ok(());
} }
@ -577,8 +630,10 @@ impl<L: SourceLoader> Engine<L> {
)); ));
}; };
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?; self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
object.fields[index].value = object.fields[index].value = self.add_value_thunk_with_span(
self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(nested))); RuntimeValue::Concrete(ConcreteValue::Object(nested)),
span,
);
return Ok(()); return Ok(());
} }
@ -586,7 +641,11 @@ impl<L: SourceLoader> Engine<L> {
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?; self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
object.fields.push(ObjectField { object.fields.push(ObjectField {
name: name.clone(), 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(()) Ok(())
} }
@ -646,6 +705,7 @@ impl<L: SourceLoader> Engine<L> {
value, value,
&abstract_value.constraints, &abstract_value.constraints,
self.expr_span(pattern), self.expr_span(pattern),
None,
) )
.map(|_| true) .map(|_| true)
.or_else(|diag| match diag.kind { .or_else(|diag| match diag.kind {
@ -673,12 +733,17 @@ impl<L: SourceLoader> Engine<L> {
(RuntimeValue::Abstract(mut lhs), RuntimeValue::Abstract(rhs)) => { (RuntimeValue::Abstract(mut lhs), RuntimeValue::Abstract(rhs)) => {
lhs.constraints.extend(rhs.constraints); lhs.constraints.extend(rhs.constraints);
lhs.constraints = normalize_constraints(lhs.constraints, span)?; 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)) Ok(RuntimeValue::Abstract(lhs))
} }
(RuntimeValue::Abstract(abstract_value), concrete @ RuntimeValue::Concrete(_)) (RuntimeValue::Abstract(abstract_value), concrete @ RuntimeValue::Concrete(_))
| (concrete @ RuntimeValue::Concrete(_), RuntimeValue::Abstract(abstract_value)) => { | (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) Ok(concrete)
} }
( (
@ -711,10 +776,17 @@ impl<L: SourceLoader> Engine<L> {
.iter() .iter()
.position(|lhs_field| lhs_field.name == rhs_field.name) .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 lhs_value = self.force(lhs.fields[index].value)?;
let rhs_value = self.force(rhs_field.value)?; let rhs_value = self.force(rhs_field.value)?;
let value = self.compose_and(lhs_value, rhs_value, span)?; let value = self
lhs.fields[index].value = self.add_value_thunk(value); .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 { } else {
lhs.fields.push(rhs_field); lhs.fields.push(rhs_field);
} }
@ -742,7 +814,8 @@ impl<L: SourceLoader> Engine<L> {
let lhs_value = self.force(lhs.fields[index].value)?; let lhs_value = self.force(lhs.fields[index].value)?;
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(value); lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
lhs.fields[index].span = rhs_field.span;
} else { } else {
lhs.fields.push(rhs_field); lhs.fields.push(rhs_field);
} }
@ -753,11 +826,12 @@ impl<L: SourceLoader> Engine<L> {
fn ensure_satisfies( fn ensure_satisfies(
&mut self, &mut self,
value: &RuntimeValue, value: &RuntimeValue,
constraints: &[Constraint], constraints: &[ConstraintEntry],
span: Span, span: Span,
value_label: Option<(Span, &'static str)>,
) -> Result<()> { ) -> Result<()> {
for constraint in constraints { for constraint in constraints {
self.satisfies(value, constraint, span)?; self.satisfies(value, constraint, span, value_label)?;
} }
Ok(()) Ok(())
} }
@ -765,17 +839,20 @@ impl<L: SourceLoader> Engine<L> {
fn satisfies( fn satisfies(
&mut self, &mut self,
value: &RuntimeValue, value: &RuntimeValue,
constraint: &Constraint, constraint: &ConstraintEntry,
span: Span, span: Span,
value_label: Option<(Span, &'static str)>,
) -> Result<()> { ) -> Result<()> {
match constraint { let constraint_value = &constraint.constraint;
match constraint_value {
Constraint::Type(primitive) => { Constraint::Type(primitive) => {
if value_matches_primitive(value, *primitive) { if value_matches_primitive(value, *primitive) {
Ok(()) Ok(())
} else { } else {
Err(Diagnostic::new( Err(self.constraint_violation(
DiagnosticKind::ConstraintViolation, constraint,
span, span,
value_label,
"value does not satisfy primitive type constraint", "value does not satisfy primitive type constraint",
)) ))
} }
@ -783,18 +860,69 @@ impl<L: SourceLoader> Engine<L> {
Constraint::Compare(op, expected) => compare_value(value, *op, expected) Constraint::Compare(op, expected) => compare_value(value, *op, expected)
.then_some(()) .then_some(())
.ok_or_else(|| { .ok_or_else(|| {
Diagnostic::new( self.constraint_violation(
DiagnosticKind::ConstraintViolation, constraint,
span, span,
value_label,
"value does not satisfy comparison constraint", "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( Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature, DiagnosticKind::UnsupportedFeature,
span, constraint.span,
format!("builtin predicate `{name}` is not implemented"), 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 +959,7 @@ impl<L: SourceLoader> Engine<L> {
object.fields.push(ObjectField { object.fields.push(ObjectField {
name: field.name, name: field.name,
value, value,
span: Span::default(),
}); });
} }
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object))) Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
@ -845,6 +974,13 @@ impl<L: SourceLoader> Engine<L> {
} else { } else {
None None
}; };
let constraints = constraints
.into_iter()
.map(|constraint| ConstraintEntry {
constraint,
span: Span::default(),
})
.collect();
let constraints = normalize_constraints(constraints, Span::default())?; let constraints = normalize_constraints(constraints, Span::default())?;
Ok(RuntimeValue::Abstract(AbstractValue { Ok(RuntimeValue::Abstract(AbstractValue {
constraints, constraints,
@ -906,7 +1042,12 @@ impl<L: SourceLoader> Engine<L> {
} }
fn add_expr_thunk(&mut self, expr: ExprRef, env: EnvId) -> ThunkId { 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( fn add_constrained_thunk(
@ -916,23 +1057,32 @@ impl<L: SourceLoader> Engine<L> {
value: ExprRef, value: ExprRef,
value_env: EnvId, value_env: EnvId,
) -> ThunkId { ) -> ThunkId {
self.add_thunk(ThunkKind::Constrained { let span = self.expr_span(value);
self.add_thunk(
ThunkKind::Constrained {
constraint, constraint,
constraint_env, constraint_env,
value, value,
value_env, value_env,
}) },
span,
)
} }
fn add_value_thunk(&mut self, value: RuntimeValue) -> ThunkId { 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); 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,
}); });
id id
} }
@ -977,9 +1127,28 @@ impl<L: SourceLoader> Engine<L> {
} }
fn expr_span(&self, reference: ExprRef) -> Span { fn expr_span(&self, reference: ExprRef) -> Span {
self.modules[reference.module.0 as usize] self.modules
.ast .get(reference.module.0 as usize)
.span(reference.expr) .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 +1174,46 @@ fn primitive_type(name: &str) -> Option<PrimitiveType> {
} }
} }
pub fn format_diagnostic_with<'a>(
diagnostic: &Diagnostic,
mut source_name: impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
let mut out = format!(
"{:?} at {}:{}..{}: {}",
diagnostic.kind,
format_source(diagnostic.span.source, &mut source_name),
diagnostic.span.start,
diagnostic.span.end,
diagnostic.message,
);
for label in &diagnostic.labels {
out.push_str(&format!(
"\nnote at {}:{}..{}: {}",
format_source(label.span.source, &mut source_name),
label.span.start,
label.span.end,
label.message,
));
}
out
}
fn format_source<'a>(
source: SourceId,
source_name: &mut impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
source_name(source)
.map(String::from)
.unwrap_or_else(|| format!("source {}", source.0))
}
fn constraint_primary_span(constraints: &[ConstraintEntry]) -> Span {
constraints
.first()
.map(|constraint| constraint.span)
.unwrap_or_default()
}
fn literal_to_concrete(literal: Literal) -> ConcreteValue { fn literal_to_concrete(literal: Literal) -> ConcreteValue {
match literal { match literal {
Literal::String(value) => ConcreteValue::String(value), Literal::String(value) => ConcreteValue::String(value),
@ -1343,23 +1552,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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -1372,6 +1564,62 @@ mod tests {
engine.materialize(&value).unwrap() 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] #[test]
fn materializes_default() { fn materializes_default() {
let data = eval_data("port = Int & >= 1 default 8080;"); let data = eval_data("port = Int & >= 1 default 8080;");

View File

@ -17,7 +17,7 @@ pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use constraints::normalize_constraints; pub use constraints::normalize_constraints;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result}; pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue}; pub use embedding::{HostField, HostValue};
pub use eval::Engine; pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{Lexer, Token, TokenKind}; pub use lexer::{Lexer, Token, TokenKind};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader}; pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id}; pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};

View File

@ -1,6 +1,6 @@
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
use crate::{ExprId, ast::CompareOp}; use crate::{ExprId, Span, ast::CompareOp};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModuleId(pub u32); pub struct ModuleId(pub u32);
@ -37,6 +37,7 @@ pub struct ObjectValue {
pub struct ObjectField { pub struct ObjectField {
pub name: String, pub name: String,
pub value: ThunkId, pub value: ThunkId,
pub span: Span,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -54,10 +55,16 @@ pub struct FunctionParam {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct AbstractValue { pub struct AbstractValue {
pub constraints: Vec<Constraint>, pub constraints: Vec<ConstraintEntry>,
pub default: Option<ThunkId>, pub default: Option<ThunkId>,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintEntry {
pub constraint: Constraint,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Constraint { pub enum Constraint {
Type(PrimitiveType), Type(PrimitiveType),
@ -108,6 +115,7 @@ pub struct EnvId(pub u32);
pub struct Thunk { pub struct Thunk {
pub kind: ThunkKind, pub kind: ThunkKind,
pub state: ThunkState, pub state: ThunkState,
pub span: Span,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use decodal_core::{ use decodal_core::{
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceLoader, Span, Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
Span, format_diagnostic_with,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -24,11 +25,18 @@ fn encode_result(result: Result<String, String>) -> String {
fn evaluate_inner(source: &str) -> Result<String, String> { fn evaluate_inner(source: &str) -> Result<String, String> {
let mut engine = Engine::new(EmptyLoader); let mut engine = Engine::new(EmptyLoader);
let module = engine let module = match engine.add_root_source("playground", "playground", source) {
.add_root_source("playground", "playground", source) Ok(module) => module,
.map_err(format_diagnostic)?; Err(error) => return Err(format_diagnostic_with_root(&error, "playground")),
let value = engine.eval_module(module).map_err(format_diagnostic)?; };
let data = engine.materialize(&value).map_err(format_diagnostic)?; let value = match engine.eval_module(module) {
Ok(value) => value,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
Ok(format_data(&data, 0)) Ok(format_data(&data, 0))
} }
@ -48,11 +56,18 @@ fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, Strin
.ok_or_else(|| format!("entry file `{entry}` was not found"))?; .ok_or_else(|| format!("entry file `{entry}` was not found"))?;
let mut engine = Engine::new(VirtualLoader { files }); let mut engine = Engine::new(VirtualLoader { files });
let module = engine let module = match engine.add_root_source(entry.clone(), entry.clone(), &source) {
.add_root_source(entry.clone(), entry.clone(), &source) Ok(module) => module,
.map_err(format_diagnostic)?; Err(error) => return Err(format_diagnostic_with_root(&error, &entry)),
let value = engine.eval_module(module).map_err(format_diagnostic)?; };
let data = engine.materialize(&value).map_err(format_diagnostic)?; let value = match engine.eval_module(module) {
Ok(value) => value,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
Ok(format_data(&data, 0)) Ok(format_data(&data, 0))
} }
@ -124,15 +139,10 @@ fn normalize_path(path: &str) -> Option<String> {
} }
} }
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String { fn format_diagnostic_with_root(diagnostic: &decodal_core::Diagnostic, root_name: &str) -> String {
format!( format_diagnostic_with(diagnostic, |source| {
"{:?} at {}:{}..{}: {}", (source == SourceId(0)).then_some(root_name)
diagnostic.kind, })
diagnostic.span.source.0,
diagnostic.span.start,
diagnostic.span.end,
diagnostic.message,
)
} }
fn format_data(data: &Data, indent: usize) -> String { fn format_data(data: &Data, indent: usize) -> String {
@ -230,4 +240,17 @@ mod tests {
let output = evaluate_project_inner("main.dcdl", files).unwrap(); let output = evaluate_project_inner("main.dcdl", files).unwrap();
assert!(output.contains("\"port\": 9443")); assert!(output.contains("\"port\": 9443"));
} }
#[test]
fn project_diagnostics_use_virtual_file_names() {
let files = r#"{
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 80; }",
"schemas/service.dcdl":"Service = { port = Int & > 443 default 8443; }"
}"#;
let error = evaluate_project_inner("main.dcdl", files).unwrap_err();
assert!(error.contains("main.dcdl:"));
assert!(error.contains("schemas/service.dcdl:"));
assert!(!error.contains("source 0:"));
assert!(!error.contains("source 1:"));
}
} }

View File

@ -117,3 +117,9 @@ Abstract { constraints, default: None }
materialize は default を採用する唯一の段階である。 materialize は default を採用する唯一の段階である。
通常評価中に明示値が得られた場合、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.

View File

@ -10,10 +10,20 @@ Diagnostic {
kind: DiagnosticKind kind: DiagnosticKind
span: Span span: Span
message: String message: String
notes: Vec<Note> labels: Vec<DiagnosticLabel>
}
DiagnosticLabel {
span: Span
message: String
} }
``` ```
`span` は primary location を示す。
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
`labels` は同じ error に関係する追加 location を示す。
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
代表的な diagnostic kind: 代表的な diagnostic kind:
- syntax error - syntax error
@ -38,6 +48,18 @@ Result<RuntimeValue, Diagnostic>
これにより、制約違反、未定義識別子、循環依存、import 失敗などが通常値として流れることを避ける。 これにより、制約違反、未定義識別子、循環依存、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 に入れない
汎用 `try / catch` は core に入れない。 汎用 `try / catch` は core に入れない。
@ -47,21 +69,6 @@ fallback は有限で明示的な仕組みに限定する。
- `default`: 未指定値の fallback。 - `default`: 未指定値の fallback。
- `match`: 有限 pattern に基づく分岐。 - `match`: 有限 pattern に基づく分岐。
- optional import: ファイル不存在など、限定された失敗だけを fallback 可能にする候補。
- optional field access: field 不在だけを fallback 可能にする候補。
- union / tagged schema: 複数 schema の選択を明示的に表す将来候補。
## optional fallback の扱い Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
それらは diagnostic として報告する。
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 は通常の値選択として扱い、エラー内容に依存した実行時分岐は避ける。

View File

@ -1,6 +1,39 @@
# Features # Features
Decodal keeps the embedded core small by making heavy functionality opt-in. Decodal は embedded use と小さい runtime を優先する。
言語機能を追加するときは、値の合成・検証・materialization に直接必要なものを core に残し、重い依存や高度な推論は optional feature または外部 tooling に分ける。
## Core feature boundary
Core に入れる機能は、基本的に deterministic な value transformation に限る。
- arithmetic / logical / comparison operators
- array concat
- object / constraint composition
- default materialization
- pure function evaluation
- host supplied import evaluation
Core に入れないものは以下である。
- filesystem / network / environment access
- time / random
- mutation
- reflection or existence probing
- arbitrary host function calls
- symbolic constraint solving beyond simple normalization
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
## Constraint reasoning
Constraint normalization は軽量な範囲に留める。
primitive type conflict や明らかな numeric bound conflict は合成時に検出してよい。
一方で、symbolic arithmetic、boolean algebra、regex intersection、array length dependent typing のような重い推論は行わない。
評価済みの concrete value に対する検証は runtime / materialization で行う。
静的に完全な型検査フェーズを増やすのではなく、parse、evaluate、compose、materialize の各段階で自然に分かる error を diagnostic として返す。
## Core defaults ## Core defaults

View File

@ -14,6 +14,10 @@ RuntimeValue =
`Concrete` は明示的な値である。 `Concrete` は明示的な値である。
`Abstract` は、まだ具体値に確定していない制約付きの値である。 `Abstract` は、まだ具体値に確定していない制約付きの値である。
Decodal は `unknown`、`any`、`null` のような「存在するが意味が未確定な値」を runtime value として持たない。
識別子や field が解決できない場合は、その場で diagnostic になる。
未解決値を後続の演算へ流して推論することはしない。
## ConcreteValue ## ConcreteValue
```text ```text
@ -32,7 +36,11 @@ object は concrete structure として扱う。
```text ```text
ObjectValue: ObjectValue:
fields: Map<Symbol, ThunkId> fields: Map<Symbol, ObjectField>
ObjectField:
value: ThunkId
span: Span
``` ```
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。 例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
@ -57,9 +65,14 @@ Concrete(Object {
```text ```text
AbstractValue { AbstractValue {
constraints: Vec<Constraint> constraints: Vec<ConstraintEntry>
default: Option<ThunkId> default: Option<ThunkId>
} }
ConstraintEntry {
constraint: Constraint
span: Span
}
``` ```
`default``AbstractValue` にだけ存在する。 `default``AbstractValue` にだけ存在する。
@ -88,6 +101,21 @@ port = 8000;
Concrete(Int(8000)) Concrete(Int(8000))
``` ```
## Runtime scope
Decodal runtime は application runtime ではなく、pure value evaluator である。
同じ source、同じ import sources、同じ host globals が与えられた場合、評価結果は決定的である。
runtime が扱う責務は以下に限る。
- expression を評価する。
- thunk を必要に応じて force する。
- concrete / abstract value を合成する。
- materialize 時に constraint を検証する。
runtime は filesystem、network、environment variable、time、random、mutation を扱わない。
core における import は host supplied source を受け取る境界であり、filesystem access ではない。
## Constraint ## Constraint
constraint は concrete value とは別の型として扱う。 constraint は concrete value とは別の型として扱う。