Add unknown ranges and open object values

This commit is contained in:
2026-08-14 08:00:22 +09:00
parent 8c50dd202d
commit 848c7f169f
53 changed files with 9047 additions and 6572 deletions
+5 -5
View File
@@ -1,14 +1,14 @@
use decodal::{Data, EmptyLoader, Engine, HostValue};
use decodal::{Data, EmptyLoader, Engine, Value};
fn main() -> decodal::Result<()> {
let mut engine = Engine::new(EmptyLoader);
engine.bind_global(
"Service",
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?),
Value::object([
("name", Value::string_type()),
("port", Value::int_type().gt(443).default_int(8443)?),
("enabled", Value::bool_type().default_bool(true)?),
]),
)?;
@@ -1,6 +1,4 @@
use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
};
use decodal::{Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value};
const POST: &str = r#"---
title: Hello
@@ -33,22 +31,22 @@ impl ImportLoader for ContentLoader {
}
}
fn parse_markdown(source: &str) -> decodal::Result<HostValue> {
fn parse_markdown(source: &str) -> decodal::Result<Value> {
let source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
let mut fields = Vec::new();
for line in frontmatter.lines() {
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
let value = match value.trim() {
"true" => HostValue::bool(true),
"false" => HostValue::bool(false),
value => HostValue::string(value),
"true" => Value::bool(true),
"false" => Value::bool(false),
value => Value::string(value),
};
fields.push((name.trim(), value));
}
Ok(HostValue::object([
("frontmatter", HostValue::object(fields)),
("body", HostValue::string(body)),
Ok(Value::object([
("frontmatter", Value::object(fields)),
("body", Value::string(body)),
]))
}
+10 -1
View File
@@ -48,7 +48,10 @@ pub struct SpannedExpr {
pub enum Expr {
Literal(Literal),
Ident(String),
Object(Vec<Field>),
Object {
fields: Vec<Field>,
rest: Option<ObjectRest>,
},
Array(Vec<ExprId>),
ArrayConstraint {
item: ExprId,
@@ -116,6 +119,12 @@ pub struct Field {
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectRest {
pub value: ExprId,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
+4
View File
@@ -19,6 +19,10 @@ pub fn normalize_constraints(
for entry in constraints {
match entry.constraint {
Constraint::Unknown => rest.push(ConstraintEntry {
constraint: Constraint::Unknown,
span: entry.span,
}),
Constraint::Type(next) => match primitive {
Some((current, current_span)) if current != next => {
return Err(Diagnostic::new(
+483 -81
View File
@@ -5,14 +5,15 @@ use crate::{
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
constraints::normalize_constraints,
diagnostic::{Diagnostic, DiagnosticKind, Result},
embedding::HostValue,
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
module::{EmptyLoader, ImportLoader, LoadedImport, Module},
parse_source_with_source_id,
runtime::{
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
ObjectRest as RuntimeObjectRest, ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId,
ThunkKind, ThunkState,
},
value::Value,
};
pub struct Engine<L = EmptyLoader> {
@@ -94,8 +95,8 @@ impl<L: ImportLoader> Engine<L> {
}
}
pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
let value = self.internalize_host_value(value)?;
pub fn bind_global(&mut self, name: impl Into<String>, value: Value) -> Result<ThunkId> {
let value = self.internalize_value(value)?;
let thunk = self.add_value_thunk(value);
self.bind(self.prelude_env, name.into(), thunk);
Ok(thunk)
@@ -280,9 +281,9 @@ impl<L: ImportLoader> Engine<L> {
let module = ModuleId(self.modules.len() as u32);
let root_env = self.new_env(Some(self.prelude_env));
let root_thunk = if source_form == SourceForm::Fields {
if let Expr::Object(fields) = ast.get(root).expr.clone() {
if let Expr::Object { fields, rest } = ast.get(root).expr.clone() {
let object = self
.build_object(module, &fields, root_env)
.build_object(module, &fields, rest.as_ref(), root_env)
.expect("module object construction should not fail for parsed fields");
for field in &object.fields {
self.bind(root_env, field.name.clone(), field.value);
@@ -318,14 +319,14 @@ impl<L: ImportLoader> Engine<L> {
let current_key = self.modules[current.0 as usize].key.clone();
let loaded = self.loader.load(Some(&current_key), specifier)?;
match loaded {
LoadedImport::Source(LoadedSource { key, name, source }) => {
LoadedImport::Source { key, name, source } => {
if let Some(value) = self.find_imported_value(&key) {
return self.force(value);
}
let module = self.add_source(key, name, &source)?;
self.eval_module(module)
}
LoadedImport::Value(LoadedValue { key, value }) => {
LoadedImport::Value { key, value } => {
if let Some(module) = self.find_module(&key) {
return self.eval_module(module);
}
@@ -333,7 +334,7 @@ impl<L: ImportLoader> Engine<L> {
return self.force(value);
}
let origin = ImportedValueOrigin::root(key.clone());
let value = self.internalize_host_value_with_origin(value, Some(&origin))?;
let value = self.internalize_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)
@@ -354,8 +355,8 @@ impl<L: ImportLoader> Engine<L> {
match expr {
Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))),
Expr::Ident(name) => self.eval_ident(&name, env, span),
Expr::Object(fields) => self
.build_object(reference.module, &fields, env)
Expr::Object { fields, rest } => self
.build_object(reference.module, &fields, rest.as_ref(), env)
.map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
Expr::Array(items) => {
let thunks = items
@@ -699,6 +700,15 @@ impl<L: ImportLoader> Engine<L> {
}
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
if name == "Unknown" {
return Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![ConstraintEntry {
constraint: Constraint::Unknown,
span,
}],
default: None,
}));
}
if let Some(primitive) = primitive_type(name) {
return Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![ConstraintEntry {
@@ -738,9 +748,23 @@ impl<L: ImportLoader> Engine<L> {
&mut self,
module: ModuleId,
fields: &[Field],
rest: Option<&crate::ast::ObjectRest>,
env: EnvId,
) -> Result<ObjectValue> {
let mut object = ObjectValue { fields: Vec::new() };
let mut object = ObjectValue {
fields: Vec::new(),
rest: rest.map(|rest| RuntimeObjectRest {
value: self.add_expr_thunk_with_span(
ExprRef {
module,
expr: rest.value,
},
env,
rest.span,
),
span: rest.span,
}),
};
for field in fields {
self.insert_field(
&mut object,
@@ -805,7 +829,10 @@ impl<L: ImportLoader> Engine<L> {
return Ok(());
}
let mut nested = ObjectValue { fields: Vec::new() };
let mut nested = ObjectValue {
fields: Vec::new(),
rest: None,
};
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
object.fields.push(ObjectField {
name: name.clone(),
@@ -1032,6 +1059,7 @@ impl<L: ImportLoader> Engine<L> {
) -> Result<()> {
for wider_entry in &wider.constraints {
let implied = match &wider_entry.constraint {
Constraint::Unknown => true,
Constraint::Type(wider_type) => narrower.constraints.iter().any(|entry| {
matches!(entry.constraint, Constraint::Type(narrower_type) if narrower_type == *wider_type)
}),
@@ -1145,11 +1173,20 @@ impl<L: ImportLoader> Engine<L> {
));
};
for narrower_field in &narrower.fields {
if wider
.fields
let ObjectValue {
fields: narrower_fields,
rest: narrower_rest,
} = narrower;
let ObjectValue {
fields: wider_fields,
rest: wider_rest,
} = wider;
for narrower_field in &narrower_fields {
if wider_fields
.iter()
.all(|wider_field| wider_field.name != narrower_field.name)
&& wider_rest.is_none()
{
let origin = self.thunk_import_origin(narrower_field.value).cloned();
let diagnostic_span = if origin.is_some() {
@@ -1172,10 +1209,33 @@ impl<L: ImportLoader> Engine<L> {
}
}
let mut refined = ObjectValue { fields: Vec::new() };
for wider_field in wider.fields {
let Some(narrower_field) = narrower
.fields
if let Some(narrower_rest) = &narrower_rest
&& wider_rest.is_none()
{
let diagnostic = Diagnostic::new(
DiagnosticKind::ConstraintViolation,
source_span_or(narrower_rest.span, narrower_diagnostic_span),
"left side of `as` permits additional fields but the right side is closed",
)
.with_label(operation_span, "`as` applied here");
let diagnostic = with_source_label(
diagnostic,
narrower_rest.span,
"narrower object rest range declared here",
);
return Err(with_source_label(
diagnostic,
wider_span,
"closed wider object declared here",
));
}
let mut refined = ObjectValue {
fields: Vec::new(),
rest: None,
};
for wider_field in wider_fields.iter().cloned() {
let Some(narrower_field) = narrower_fields
.iter()
.find(|narrower_field| narrower_field.name == wider_field.name)
else {
@@ -1227,6 +1287,82 @@ impl<L: ImportLoader> Engine<L> {
span: narrower_field.span,
});
}
if let Some(wider_rest) = &wider_rest {
let wider_rest_value = self.force(wider_rest.value)?;
for narrower_field in narrower_fields.iter().filter(|narrower_field| {
wider_fields
.iter()
.all(|wider_field| wider_field.name != narrower_field.name)
}) {
let narrower_origin = self.thunk_import_origin(narrower_field.value).cloned();
let narrower_value = self.force(narrower_field.value)?;
let field_name = narrower_field.name.clone();
let narrowed = self
.apply_as(
narrower_value.clone(),
wider_rest_value.clone(),
operation_span,
narrower_field.span,
wider_rest.span,
)
.map_err(|diagnostic| {
let diagnostic = with_source_label(
diagnostic,
wider_rest.span,
"wider object rest range declared here",
);
if narrower_origin.is_some() {
annotate_imported_value(
diagnostic,
narrower_origin.as_ref(),
Some(&narrower_value),
)
} else {
with_source_label(
diagnostic,
narrower_field.span,
format!("additional field `{field_name}` checked here"),
)
}
})?;
refined.fields.push(ObjectField {
name: field_name,
value: self.add_value_thunk_with_span_and_import_origin(
narrowed,
narrower_field.span,
narrower_origin,
),
span: narrower_field.span,
});
}
}
refined.rest = match (narrower_rest, wider_rest) {
(Some(narrower_rest), Some(wider_rest)) => {
let narrower_origin = self.thunk_import_origin(narrower_rest.value).cloned();
let narrower_value = self.force(narrower_rest.value)?;
let wider_value = self.force(wider_rest.value)?;
let narrowed = self.apply_as(
narrower_value,
wider_value,
operation_span,
narrower_rest.span,
wider_rest.span,
)?;
Some(RuntimeObjectRest {
value: self.add_value_thunk_with_span_and_import_origin(
narrowed,
narrower_rest.span,
narrower_origin,
),
span: narrower_rest.span,
})
}
(None, Some(wider_rest)) => Some(wider_rest),
(None, None) => None,
(Some(_), None) => unreachable!("open narrower object was rejected above"),
};
Ok(RuntimeValue::Concrete(ConcreteValue::Object(refined)))
}
@@ -1321,7 +1457,46 @@ impl<L: ImportLoader> Engine<L> {
rhs: ObjectValue,
span: Span,
) -> Result<RuntimeValue> {
for rhs_field in rhs.fields {
let lhs_rest = lhs.rest.take();
let rhs_rest = rhs.rest;
let rhs_field_names = rhs
.fields
.iter()
.map(|field| field.name.clone())
.collect::<Vec<_>>();
if let Some(rhs_rest) = &rhs_rest {
let rhs_rest_value = self.force(rhs_rest.value)?;
for lhs_field in lhs
.fields
.iter_mut()
.filter(|field| !rhs_field_names.contains(&field.name))
{
let lhs_origin = self.thunk_import_origin(lhs_field.value).cloned();
let lhs_value = self.force(lhs_field.value)?;
let value = self
.compose_and(lhs_value.clone(), rhs_rest_value.clone(), span)
.map_err(|diagnostic| {
let diagnostic = with_source_label(
diagnostic,
rhs_rest.span,
"right object rest constraint declared here",
);
with_source_label(
diagnostic,
lhs_field.span,
format!("left field `{}` constrained here", lhs_field.name),
)
})?;
lhs_field.value = self.add_value_thunk_with_span_and_import_origin(
value,
lhs_field.span,
lhs_origin,
);
}
}
for mut rhs_field in rhs.fields {
if let Some(index) = lhs
.fields
.iter()
@@ -1381,9 +1556,65 @@ impl<L: ImportLoader> Engine<L> {
lhs.fields[index].span = rhs_field.span;
}
} else {
if let Some(lhs_rest) = &lhs_rest {
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
let rhs_value = self.force(rhs_field.value)?;
let lhs_rest_value = self.force(lhs_rest.value)?;
let value = self.compose_and(rhs_value, lhs_rest_value, span).map_err(
|diagnostic| {
let diagnostic = with_source_label(
diagnostic,
lhs_rest.span,
"left object rest constraint declared here",
);
with_source_label(
diagnostic,
rhs_field.span,
format!("right field `{}` constrained here", rhs_field.name),
)
},
)?;
rhs_field.value = self.add_value_thunk_with_span_and_import_origin(
value,
rhs_field.span,
rhs_origin,
);
}
lhs.fields.push(rhs_field);
}
}
lhs.rest = match (lhs_rest, rhs_rest) {
(Some(lhs_rest), Some(rhs_rest)) => {
let lhs_origin = self.thunk_import_origin(lhs_rest.value).cloned();
let lhs_value = self.force(lhs_rest.value)?;
let rhs_value = self.force(rhs_rest.value)?;
let value = self
.compose_and(lhs_value, rhs_value, span)
.map_err(|diagnostic| {
let diagnostic = with_source_label(
diagnostic,
lhs_rest.span,
"left object rest constraint declared here",
);
with_source_label(
diagnostic,
rhs_rest.span,
"right object rest constraint declared here",
)
})?;
Some(RuntimeObjectRest {
value: self.add_value_thunk_with_span_and_import_origin(
value,
rhs_rest.span,
lhs_origin,
),
span: rhs_rest.span,
})
}
(Some(rest), None) | (None, Some(rest)) => Some(rest),
(None, None) => None,
};
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
}
@@ -1398,6 +1629,7 @@ impl<L: ImportLoader> Engine<L> {
}
fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
let rhs_rest = rhs.rest;
for rhs_field in rhs.fields {
if let Some(index) = lhs
.fields
@@ -1418,6 +1650,9 @@ impl<L: ImportLoader> Engine<L> {
lhs.fields.push(rhs_field);
}
}
if rhs_rest.is_some() {
lhs.rest = rhs_rest;
}
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
}
@@ -1459,6 +1694,7 @@ impl<L: ImportLoader> Engine<L> {
constraint.span
};
match constraint_value {
Constraint::Unknown => Ok(value),
Constraint::Type(primitive) => {
if value_matches_primitive(&value, *primitive) {
Ok(value)
@@ -1547,8 +1783,12 @@ impl<L: ImportLoader> Engine<L> {
));
};
let value_schema = self.force(*value_constraint)?;
let mut constrained_fields = Vec::with_capacity(object.fields.len());
for field in object.fields {
let ObjectValue {
fields,
rest: object_rest,
} = object;
let mut constrained_fields = Vec::with_capacity(fields.len());
for field in fields {
let field_span = self.thunk_span(field.value);
let field_origin = self.thunk_import_origin(field.value).cloned();
let field_value = self.force(field.value)?;
@@ -1592,8 +1832,30 @@ impl<L: ImportLoader> Engine<L> {
span: field.span,
});
}
let constrained_rest = if let Some(object_rest) = object_rest {
let rest_origin = self.thunk_import_origin(object_rest.value).cloned();
let rest_value = self.force(object_rest.value)?;
let constrained = self.apply_as(
rest_value,
value_schema,
span,
object_rest.span,
constraint_span,
)?;
Some(RuntimeObjectRest {
value: self.add_value_thunk_with_span_and_import_origin(
constrained,
object_rest.span,
rest_origin,
),
span: object_rest.span,
})
} else {
None
};
Ok(RuntimeValue::Concrete(ConcreteValue::Object(ObjectValue {
fields: constrained_fields,
rest: constrained_rest,
})))
}
Constraint::Compare(op, expected) => compare_value(&value, *op, expected)
@@ -1671,40 +1933,39 @@ 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_value(&mut self, value: Value) -> Result<RuntimeValue> {
self.internalize_value_with_origin(value, None)
}
fn internalize_host_value_with_origin(
fn internalize_value_with_origin(
&mut self,
value: HostValue,
value: Value,
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))),
HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
HostValue::Array(items) => {
Value::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
Value::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
Value::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
Value::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
Value::Array(items) => {
let mut thunks = Vec::new();
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())?;
let value = self.internalize_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)))
}
HostValue::ArrayConstraint {
Value::ArrayRange {
item,
mut constraints,
default,
} => {
let item = self.internalize_host_value_with_origin(*item, origin)?;
let item = self.internalize_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_with_origin(*default, origin)?;
let value = self.internalize_value_with_origin(*default, origin)?;
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else {
None
@@ -1723,16 +1984,16 @@ impl<L: ImportLoader> Engine<L> {
default,
}))
}
HostValue::MapConstraint {
Value::MapRange {
value,
mut constraints,
default,
} => {
let value = self.internalize_host_value_with_origin(*value, origin)?;
let value = self.internalize_value_with_origin(*value, origin)?;
let value =
self.add_value_thunk_with_optional_import_origin(value, origin.cloned());
let default = if let Some(default) = default {
let value = self.internalize_host_value_with_origin(*default, origin)?;
let value = self.internalize_value_with_origin(*default, origin)?;
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else {
None
@@ -1751,8 +2012,11 @@ impl<L: ImportLoader> Engine<L> {
default,
}))
}
HostValue::Object(fields) => {
let mut object = ObjectValue { fields: Vec::new() };
Value::Object { fields, rest } => {
let mut object = ObjectValue {
fields: Vec::new(),
rest: None,
};
for field in fields {
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
if object
@@ -1763,7 +2027,7 @@ impl<L: ImportLoader> Engine<L> {
let diagnostic = Diagnostic::new(
DiagnosticKind::Conflict,
Span::default(),
format!("duplicate host object field `{}`", field.name),
format!("duplicate object field `{}`", field.name),
);
return Err(annotate_imported_value(
diagnostic,
@@ -1771,8 +2035,8 @@ impl<L: ImportLoader> Engine<L> {
None,
));
}
let value = self
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?;
let value =
self.internalize_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 {
@@ -1781,14 +2045,22 @@ impl<L: ImportLoader> Engine<L> {
span: Span::default(),
});
}
if let Some(rest) = rest {
let rest = self.internalize_value_with_origin(*rest, origin)?;
object.rest = Some(RuntimeObjectRest {
value: self
.add_value_thunk_with_optional_import_origin(rest, origin.cloned()),
span: Span::default(),
});
}
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
}
HostValue::Abstract {
Value::Range {
constraints,
default,
} => {
let default = if let Some(default) = default {
let value = self.internalize_host_value_with_origin(*default, origin)?;
let value = self.internalize_value_with_origin(*default, origin)?;
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else {
None
@@ -2252,6 +2524,7 @@ fn upper_bound_implies(
fn constraint_description(constraint: &Constraint) -> String {
match constraint {
Constraint::Unknown => String::from("the unknown top range"),
Constraint::Type(primitive) => format!("type {primitive:?}"),
Constraint::ArrayItems(_) => String::from("an array element range"),
Constraint::MapValues(_) => String::from("a map value range"),
@@ -2590,7 +2863,7 @@ fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::{LoadedSource, parse_source};
use crate::parse_source;
fn eval_data(source: &str) -> Data {
let parsed = parse_source(source).unwrap();
@@ -2887,6 +3160,29 @@ mod tests {
assert!(value.default.is_none());
}
#[test]
fn unknown_is_the_unmaterializable_top_range() {
assert_eq!(eval_data("42 as Unknown"), Data::Int(42));
assert_eq!(eval_data("Unknown default {}"), Data::Object(Vec::new()));
let parsed = parse_source("Unknown").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.message.contains("unresolved abstract value"));
let parsed = parse_source("Unknown as Int").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let error = engine.eval_root().unwrap_err();
assert!(error.message.contains("not known to be narrower"));
let parsed = parse_source("{...Unknown}").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let value = engine.eval_root().unwrap();
assert!(engine.materialize(&value).is_err());
}
#[test]
fn abstract_collection_ranges_can_be_proven_narrower() {
let parsed = parse_source("[...(Int & > 10)] as [...Int]").unwrap();
@@ -2980,6 +3276,100 @@ mod tests {
assert_eq!(eval_data("{} as {...String}"), Data::Object(vec![]));
}
#[test]
fn object_rest_constraint_accepts_and_preserves_additional_fields() {
let data = eval_data(
r#"
{
hoge = 1;
fuga = "two";
extra = true;
} as {
hoge = Int;
fuga = String;
...Unknown
}
"#,
);
assert_eq!(
data,
Data::Object(vec![
DataField {
name: String::from("hoge"),
value: Data::Int(1),
},
DataField {
name: String::from("fuga"),
value: Data::String(String::from("two")),
},
DataField {
name: String::from("extra"),
value: Data::Bool(true),
},
])
);
assert_eq!(
eval_data("{ hoge = 1; ...Unknown }"),
Data::Object(vec![DataField {
name: String::from("hoge"),
value: Data::Int(1),
}])
);
}
#[test]
fn object_rest_constraint_rejects_invalid_additional_fields() {
let parsed =
parse_source(r#"{ hoge = 1; extra = "no"; } as { hoge = Int; ...Int }"#).unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let error = engine.eval_root().unwrap_err();
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
assert!(error.message.contains("expected Int"));
assert!(
error
.labels
.iter()
.any(|label| label.message.contains("object rest range"))
);
let parsed = parse_source("{ hoge = 1; ...Unknown } as { hoge = Int; }").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let error = engine.eval_root().unwrap_err();
assert!(error.message.contains("permits additional fields"));
}
#[test]
fn object_rest_constraints_compose_as_ranges() {
assert_eq!(
eval_data("{ fixed = 1; ...Unknown } & { additional = 2; ...Unknown }"),
Data::Object(vec![
DataField {
name: String::from("fixed"),
value: Data::Int(1),
},
DataField {
name: String::from("additional"),
value: Data::Int(2),
},
])
);
let parsed =
parse_source("{ fixed = Int; ...Int } as { fixed = Unknown; ...Unknown }").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(matches!(
engine.eval_root().unwrap(),
RuntimeValue::Concrete(ConcreteValue::Object(_))
));
let parsed =
parse_source("{ fixed = Int; ...Unknown } as { fixed = Unknown; ...Int }").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
let error = engine.eval_root().unwrap_err();
assert!(error.message.contains("not known to be narrower"));
}
#[test]
fn removed_array_primitive_reports_migration() {
let parsed = parse_source("Array").unwrap();
@@ -3100,11 +3490,11 @@ mod tests {
.ok_or_else(|| {
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
})?;
Ok(LoadedImport::Source(LoadedSource {
Ok(LoadedImport::Source {
key: specifier.into(),
name: specifier.into(),
source,
}))
})
}
}
@@ -3147,7 +3537,7 @@ mod tests {
}
}
fn parse_test_markdown(source: &str) -> Result<HostValue> {
fn parse_test_markdown(source: &str) -> Result<Value> {
let source = source.strip_prefix("---\n").ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::Import,
@@ -3172,15 +3562,15 @@ mod tests {
)
})?;
let value = match value.trim() {
"true" => HostValue::bool(true),
"false" => HostValue::bool(false),
value => HostValue::string(value.trim_matches('"')),
"true" => Value::bool(true),
"false" => Value::bool(false),
value => Value::string(value.trim_matches('"')),
};
fields.push((name.trim(), value));
}
Ok(HostValue::object([
("frontmatter", HostValue::object(fields)),
("body", HostValue::string(body)),
Ok(Value::object([
("frontmatter", Value::object(fields)),
("body", Value::string(body)),
]))
}
@@ -3275,9 +3665,9 @@ mod tests {
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
Ok(LoadedImport::value(
"content/navigation.json",
HostValue::object([(
Value::object([(
"items",
HostValue::array([HostValue::string("home"), HostValue::int(2)]),
Value::array([Value::string("home"), Value::int(2)]),
)]),
))
}
@@ -3308,7 +3698,7 @@ mod tests {
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
Ok(LoadedImport::value(
"shared-value",
HostValue::int_type().default_int(1)?,
Value::int_type().default_int(1)?,
))
}
}
@@ -3351,16 +3741,10 @@ mod tests {
engine
.bind_global(
"Service",
HostValue::object([
("name", HostValue::string_type()),
(
"port",
HostValue::int_type().gt(443).default_int(8443).unwrap(),
),
(
"enabled",
HostValue::bool_type().default_bool(true).unwrap(),
),
Value::object([
("name", Value::string_type()),
("port", Value::int_type().gt(443).default_int(8443).unwrap()),
("enabled", Value::bool_type().default_bool(true).unwrap()),
]),
)
.unwrap();
@@ -3385,12 +3769,9 @@ mod tests {
engine
.bind_global(
"Services",
HostValue::map_of(HostValue::object([
("port", HostValue::int_type()),
(
"enabled",
HostValue::bool_type().default_bool(true).unwrap(),
),
Value::map_of(Value::object([
("port", Value::int_type()),
("enabled", Value::bool_type().default_bool(true).unwrap()),
])),
)
.unwrap();
@@ -3410,14 +3791,35 @@ mod tests {
}
#[test]
fn host_schema_errors_point_to_the_dcdl_application() {
fn host_prelude_provides_object_rest_and_unknown_ranges() {
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global(
"Service",
HostValue::object([("port", HostValue::int_type())]),
"OpenService",
Value::object_with_rest([("name", Value::string_type())], Value::unknown()),
)
.unwrap();
let module = engine
.add_root_source(
"main",
"main",
r#"{ name = "api"; metadata = { owner = "platform"; }; } as OpenService"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].value, Data::String(String::from("api")));
assert!(matches!(fields[1].value, Data::Object(_)));
}
#[test]
fn host_schema_errors_point_to_the_dcdl_application() {
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global("Service", Value::object([("port", Value::int_type())]))
.unwrap();
let module = engine
.add_root_source("main", "main", r#"{ port = "not-an-int"; } as Service"#)
.unwrap();
@@ -3435,7 +3837,7 @@ mod tests {
#[test]
fn module_binding_shadows_host_prelude() {
let mut engine = Engine::new(EmptyLoader);
engine.bind_global("Service", HostValue::int(1)).unwrap();
engine.bind_global("Service", Value::int(1)).unwrap();
let module = engine
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
.unwrap();
+6 -8
View File
@@ -5,7 +5,6 @@ extern crate alloc;
pub mod ast;
pub mod constraints;
pub mod diagnostic;
pub mod embedding;
pub mod environment;
pub mod eval;
mod lexer;
@@ -14,29 +13,28 @@ pub mod parser;
pub mod runtime;
pub mod span;
pub mod typed;
pub mod value;
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, ObjectRest, Param};
pub use constraints::normalize_constraints;
#[cfg(feature = "derive")]
pub use decodal_derive::Decodal;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use environment::HostEnvironment;
pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{
Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source,
tokenize_source_with_source_id,
};
pub use module::{
EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module,
};
pub use module::{EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, Module};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType};
pub use span::{SourceId, Span};
pub use typed::{
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
DecodalDecode, DecodalRest, DecodalSchema, DecodeError, DecodeResult, IntoValue, data_at_path,
decode_path, prefix_decode_error,
};
pub use value::{Value, ValueField};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
+16 -21
View File
@@ -1,7 +1,7 @@
use alloc::{string::String, vec::Vec};
use crate::{
Ast, ExprId, HostValue, SourceForm, SourceId,
Ast, ExprId, SourceForm, SourceId, Value,
runtime::{EnvId, ThunkId},
};
@@ -17,23 +17,18 @@ pub struct Module {
pub root_thunk: ThunkId,
}
#[derive(Debug, Clone)]
pub struct LoadedSource {
pub key: String,
pub name: String,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct LoadedValue {
pub key: String,
pub value: HostValue,
}
/// Content resolved by an [`ImportLoader`].
#[derive(Debug, Clone)]
pub enum LoadedImport {
Source(LoadedSource),
Value(LoadedValue),
Source {
key: String,
name: String,
source: String,
},
Value {
key: String,
value: Value,
},
}
/// An import specifier offered by host-owned language tooling.
@@ -63,18 +58,18 @@ impl LoadedImport {
name: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::Source(LoadedSource {
Self::Source {
key: key.into(),
name: name.into(),
source: source.into(),
})
}
}
pub fn value(key: impl Into<String>, value: HostValue) -> Self {
Self::Value(LoadedValue {
pub fn value(key: impl Into<String>, value: Value) -> Self {
Self::Value {
key: key.into(),
value,
})
}
}
}
+41 -9
View File
@@ -2,7 +2,10 @@ use alloc::{string::String, vec::Vec};
use crate::{
SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
ast::{
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, ObjectRest, Param,
UnaryOp,
},
diagnostic::{Diagnostic, Result},
lexer::{Lexer, Token, TokenKind},
};
@@ -59,7 +62,7 @@ impl Parser {
})
.unwrap_or_else(|| self.peek().span);
(
self.ast.push(Expr::Object(fields), span),
self.ast.push(Expr::Object { fields, rest: None }, span),
SourceForm::Fields,
)
} else {
@@ -338,23 +341,38 @@ impl Parser {
let mut fields = Vec::new();
if self.consume_kind(&TokenKind::RBrace).is_some() {
return Ok(self
.ast
.push(Expr::Object(fields), start_span.join(self.previous_span())));
return Ok(self.ast.push(
Expr::Object { fields, rest: None },
start_span.join(self.previous_span()),
));
}
let mut rest = None;
loop {
fields.push(self.parse_field()?);
if self.consume_kind(&TokenKind::Semicolon).is_some() {
if self.consume_kind(&TokenKind::RBrace).is_some() {
break;
}
if let Some(ellipsis) = self.consume_kind(&TokenKind::Ellipsis) {
let value = self.parse_expr(0)?;
rest = Some(ObjectRest {
value,
span: ellipsis.join(self.ast.span(value)),
});
let _ = self.consume_kind(&TokenKind::Semicolon);
self.expect_kind(
&TokenKind::RBrace,
"expected '}' after object rest constraint",
)?;
break;
}
continue;
}
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
break;
}
let span = start_span.join(self.previous_span());
Ok(self.ast.push(Expr::Object(fields), span))
Ok(self.ast.push(Expr::Object { fields, rest }, span))
}
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
@@ -697,13 +715,16 @@ mod tests {
#[test]
fn parses_top_level_fields_as_object() {
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap();
assert!(matches!(parsed.ast.get(parsed.root).expr, Expr::Object(_)));
assert!(matches!(
parsed.ast.get(parsed.root).expr,
Expr::Object { .. }
));
}
#[test]
fn parses_object_dot_field() {
let parsed = parse_source("{ feature.enable = false; }").unwrap();
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
panic!()
};
assert_eq!(fields[0].path, ["feature", "enable"]);
@@ -726,7 +747,7 @@ mod tests {
fn parser_accepts_the_public_lossless_token_stream() {
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
let parsed = Parser::new(tokens).parse().unwrap();
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
panic!()
};
assert!(matches!(
@@ -749,6 +770,17 @@ mod tests {
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_)));
}
#[test]
fn parses_object_rest_constraint_after_named_fields() {
let parsed = parse_source("{ hoge = Int; fuga = String; ...Unknown }").unwrap();
let Expr::Object { fields, rest } = &parsed.ast.get(parsed.root).expr else {
panic!()
};
assert_eq!(fields.len(), 2);
let rest = rest.as_ref().expect("object has a rest constraint");
assert!(matches!(parsed.ast.get(rest.value).expr, Expr::Ident(_)));
}
#[test]
fn parses_as_below_composition() {
let parsed = parse_source("value & override as Schema").unwrap();
+12
View File
@@ -11,6 +11,9 @@ pub struct ExprRef {
pub expr: ExprId,
}
/// Evaluator-owned representation of a lazy concrete value or unresolved range.
///
/// Embedders normally construct [`crate::Value`] and consume [`Data`] instead.
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeValue {
Concrete(ConcreteValue),
@@ -31,6 +34,13 @@ pub enum ConcreteValue {
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectValue {
pub fields: Vec<ObjectField>,
pub rest: Option<ObjectRest>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectRest {
pub value: ThunkId,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
@@ -67,6 +77,7 @@ pub struct ConstraintEntry {
#[derive(Debug, Clone, PartialEq)]
pub enum Constraint {
Unknown,
Type(PrimitiveType),
ArrayItems(ThunkId),
MapValues(ThunkId),
@@ -91,6 +102,7 @@ pub enum LiteralValue {
Bool(bool),
}
/// Fully materialized Decodal data.
#[derive(Debug, Clone, PartialEq)]
pub enum Data {
String(String),
+176 -51
View File
@@ -1,17 +1,24 @@
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
use crate::{Data, HostValue};
use crate::{Data, Value, runtime::DataField};
pub trait DecodalSchema {
fn decodal_schema() -> HostValue;
fn decodal_schema() -> Value;
}
pub trait DecodalDecode: Sized {
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
}
pub trait IntoHostValue {
fn into_host_value(self) -> HostValue;
pub trait IntoValue {
fn into_value(self) -> Value;
}
/// A map-like receiver for fields not named by a derived struct schema.
pub trait DecodalRest: Sized {
fn decodal_rest_schema() -> Value;
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self>;
}
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
@@ -80,9 +87,38 @@ fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
error
}
impl DecodalSchema for Data {
fn decodal_schema() -> Value {
Value::unknown()
}
}
impl DecodalDecode for Data {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
Ok(data.clone())
}
}
impl IntoValue for Data {
fn into_value(self) -> Value {
match self {
Data::String(value) => Value::string(value),
Data::Int(value) => Value::int(value),
Data::Float(value) => Value::float(value),
Data::Bool(value) => Value::bool(value),
Data::Array(items) => Value::array(items.into_iter().map(IntoValue::into_value)),
Data::Object(fields) => Value::object(
fields
.into_iter()
.map(|field| (field.name, field.value.into_value())),
),
}
}
}
impl DecodalSchema for String {
fn decodal_schema() -> HostValue {
HostValue::string_type()
fn decodal_schema() -> Value {
Value::string_type()
}
}
@@ -95,15 +131,15 @@ impl DecodalDecode for String {
}
}
impl IntoHostValue for String {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
impl IntoValue for String {
fn into_value(self) -> Value {
Value::string(self)
}
}
impl IntoHostValue for &str {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
impl IntoValue for &str {
fn into_value(self) -> Value {
Value::string(self)
}
}
@@ -111,8 +147,8 @@ macro_rules! impl_int_decode {
($($ty:ty),* $(,)?) => {
$(
impl DecodalSchema for $ty {
fn decodal_schema() -> HostValue {
HostValue::int_type()
fn decodal_schema() -> Value {
Value::int_type()
}
}
@@ -126,9 +162,9 @@ macro_rules! impl_int_decode {
}
}
impl IntoHostValue for $ty {
fn into_host_value(self) -> HostValue {
HostValue::int(self as i64)
impl IntoValue for $ty {
fn into_value(self) -> Value {
Value::int(self as i64)
}
}
)*
@@ -138,8 +174,8 @@ macro_rules! impl_int_decode {
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
impl DecodalSchema for f64 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
fn decodal_schema() -> Value {
Value::float_type()
}
}
@@ -153,15 +189,15 @@ impl DecodalDecode for f64 {
}
}
impl IntoHostValue for f64 {
fn into_host_value(self) -> HostValue {
HostValue::float(self)
impl IntoValue for f64 {
fn into_value(self) -> Value {
Value::float(self)
}
}
impl DecodalSchema for f32 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
fn decodal_schema() -> Value {
Value::float_type()
}
}
@@ -171,15 +207,15 @@ impl DecodalDecode for f32 {
}
}
impl IntoHostValue for f32 {
fn into_host_value(self) -> HostValue {
HostValue::float(f64::from(self))
impl IntoValue for f32 {
fn into_value(self) -> Value {
Value::float(f64::from(self))
}
}
impl DecodalSchema for bool {
fn decodal_schema() -> HostValue {
HostValue::bool_type()
fn decodal_schema() -> Value {
Value::bool_type()
}
}
@@ -192,15 +228,15 @@ impl DecodalDecode for bool {
}
}
impl IntoHostValue for bool {
fn into_host_value(self) -> HostValue {
HostValue::bool(self)
impl IntoValue for bool {
fn into_value(self) -> Value {
Value::bool(self)
}
}
impl<T: DecodalSchema> DecodalSchema for Vec<T> {
fn decodal_schema() -> HostValue {
HostValue::array_of(T::decodal_schema())
fn decodal_schema() -> Value {
Value::array_of(T::decodal_schema())
}
}
@@ -220,15 +256,15 @@ impl<T: DecodalDecode> DecodalDecode for Vec<T> {
}
}
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
fn into_host_value(self) -> HostValue {
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
impl<T: IntoValue> IntoValue for Vec<T> {
fn into_value(self) -> Value {
Value::array(self.into_iter().map(IntoValue::into_value))
}
}
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
fn decodal_schema() -> HostValue {
HostValue::map_of(T::decodal_schema())
fn decodal_schema() -> Value {
Value::map_of(T::decodal_schema())
}
}
@@ -248,19 +284,36 @@ impl<T: DecodalDecode> DecodalDecode for BTreeMap<String, T> {
}
}
impl<T: IntoHostValue> IntoHostValue for BTreeMap<String, T> {
fn into_host_value(self) -> HostValue {
HostValue::object(
impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
fn into_value(self) -> Value {
Value::object(
self.into_iter()
.map(|(name, value)| (name, value.into_host_value())),
.map(|(name, value)| (name, value.into_value())),
)
}
}
impl<T: DecodalSchema + DecodalDecode> DecodalRest for BTreeMap<String, T> {
fn decodal_rest_schema() -> Value {
T::decodal_schema()
}
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
let Data::Object(fields) = data else {
return Err(DecodeError::at_type("", "Object"));
};
fields
.iter()
.filter(|field| !known_fields.contains(&field.name.as_str()))
.map(decode_rest_field::<T>)
.collect()
}
}
#[cfg(feature = "std")]
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
fn decodal_schema() -> HostValue {
HostValue::map_of(T::decodal_schema())
fn decodal_schema() -> Value {
Value::map_of(T::decodal_schema())
}
}
@@ -282,17 +335,41 @@ impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
}
#[cfg(feature = "std")]
impl<T: IntoHostValue> IntoHostValue for std::collections::HashMap<String, T> {
fn into_host_value(self) -> HostValue {
impl<T: IntoValue> IntoValue for std::collections::HashMap<String, T> {
fn into_value(self) -> Value {
let mut fields: Vec<_> = self
.into_iter()
.map(|(name, value)| (name, value.into_host_value()))
.map(|(name, value)| (name, value.into_value()))
.collect();
fields.sort_by(|left, right| left.0.cmp(&right.0));
HostValue::object(fields)
Value::object(fields)
}
}
#[cfg(feature = "std")]
impl<T: DecodalSchema + DecodalDecode> DecodalRest for std::collections::HashMap<String, T> {
fn decodal_rest_schema() -> Value {
T::decodal_schema()
}
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
let Data::Object(fields) = data else {
return Err(DecodeError::at_type("", "Object"));
};
fields
.iter()
.filter(|field| !known_fields.contains(&field.name.as_str()))
.map(decode_rest_field::<T>)
.collect()
}
}
fn decode_rest_field<T: DecodalDecode>(field: &DataField) -> DecodeResult<(String, T)> {
T::decodal_decode(&field.value)
.map(|value| (field.name.clone(), value))
.map_err(|error| prefix_error(&field.name, error))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -302,10 +379,10 @@ mod tests {
fn string_maps_expose_map_schemas_and_decode_objects() {
assert!(matches!(
BTreeMap::<String, i64>::decodal_schema(),
HostValue::MapConstraint { value, .. }
Value::MapRange { value, .. }
if matches!(
*value,
HostValue::Abstract { ref constraints, .. }
Value::Range { ref constraints, .. }
if constraints == &[Constraint::Type(PrimitiveType::Int)]
)
));
@@ -317,4 +394,52 @@ mod tests {
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
assert_eq!(decoded.get("api"), Some(&8080));
}
#[test]
fn data_is_the_unknown_schema_and_round_trips_values() {
assert!(matches!(
Data::decodal_schema(),
Value::Range { ref constraints, .. }
if constraints == &[Constraint::Unknown]
));
let data = Data::Object(alloc::vec![DataField {
name: String::from("nested"),
value: Data::Array(alloc::vec![Data::Bool(true), Data::Int(2)]),
}]);
assert_eq!(Data::decodal_decode(&data).unwrap(), data);
assert!(matches!(data.into_value(), Value::Object { .. }));
}
#[test]
fn rest_maps_decode_only_fields_outside_the_known_domain() {
let data = Data::Object(alloc::vec![
DataField {
name: String::from("name"),
value: Data::String(String::from("api")),
},
DataField {
name: String::from("priority"),
value: Data::Int(3),
},
]);
let decoded = BTreeMap::<String, Data>::decodal_decode_rest(&data, &["name"]).unwrap();
assert_eq!(decoded.len(), 1);
assert_eq!(decoded.get("priority"), Some(&Data::Int(3)));
}
#[cfg(feature = "std")]
#[test]
fn hash_maps_can_receive_rest_fields() {
let data = Data::Object(alloc::vec![DataField {
name: String::from("region"),
value: Data::String(String::from("ap-northeast-1")),
}]);
let decoded =
std::collections::HashMap::<String, String>::decodal_decode_rest(&data, &[]).unwrap();
assert_eq!(
decoded.get("region").map(String::as_str),
Some("ap-northeast-1")
);
}
}
@@ -3,37 +3,46 @@ use alloc::{boxed::Box, string::String, vec::Vec};
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
/// A Decodal value supplied through the embedding API.
///
/// Unlike [`crate::Data`], a `Value` may contain unresolved ranges and
/// defaults. The evaluator converts it into its lazy runtime representation
/// when it is bound as a global or returned from an import loader.
#[derive(Debug, Clone, PartialEq)]
pub enum HostValue {
pub enum Value {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<HostValue>),
ArrayConstraint {
item: Box<HostValue>,
Array(Vec<Value>),
ArrayRange {
item: Box<Value>,
constraints: Vec<Constraint>,
default: Option<Box<HostValue>>,
default: Option<Box<Value>>,
},
MapConstraint {
value: Box<HostValue>,
MapRange {
value: Box<Value>,
constraints: Vec<Constraint>,
default: Option<Box<HostValue>>,
default: Option<Box<Value>>,
},
Object(Vec<HostField>),
Abstract {
Object {
fields: Vec<ValueField>,
rest: Option<Box<Value>>,
},
Range {
constraints: Vec<Constraint>,
default: Option<Box<HostValue>>,
default: Option<Box<Value>>,
},
}
/// A named field in a [`Value::Object`].
#[derive(Debug, Clone, PartialEq)]
pub struct HostField {
pub struct ValueField {
pub name: String,
pub value: HostValue,
pub value: Value,
}
impl HostValue {
impl Value {
pub fn string(value: impl Into<String>) -> Self {
Self::String(value.into())
}
@@ -52,65 +61,105 @@ impl HostValue {
pub fn array<I>(items: I) -> Self
where
I: IntoIterator<Item = HostValue>,
I: IntoIterator<Item = Value>,
{
Self::Array(items.into_iter().collect())
}
pub fn object<I, N>(fields: I) -> Self
where
I: IntoIterator<Item = (N, HostValue)>,
I: IntoIterator<Item = (N, Value)>,
N: Into<String>,
{
Self::Object(
fields
Self::Object {
fields: fields
.into_iter()
.map(|(name, value)| HostField {
.map(|(name, value)| ValueField {
name: name.into(),
value,
})
.collect(),
)
rest: None,
}
}
pub fn object_from_paths<I, N>(fields: I) -> Self
where
I: IntoIterator<Item = (N, HostValue)>,
I: IntoIterator<Item = (N, Value)>,
N: Into<String>,
{
let mut root = Vec::new();
for (path, value) in fields {
insert_path(&mut root, &path.into(), value);
}
Self::Object(root)
Self::Object {
fields: root,
rest: None,
}
}
pub fn object_from_paths_with_rest<I, N>(fields: I, rest: Value) -> Self
where
I: IntoIterator<Item = (N, Value)>,
N: Into<String>,
{
let mut root = Vec::new();
for (path, value) in fields {
insert_path(&mut root, &path.into(), value);
}
Self::Object {
fields: root,
rest: Some(Box::new(rest)),
}
}
pub fn object_with_rest<I, N>(fields: I, rest: Value) -> Self
where
I: IntoIterator<Item = (N, Value)>,
N: Into<String>,
{
Self::Object {
fields: fields
.into_iter()
.map(|(name, value)| ValueField {
name: name.into(),
value,
})
.collect(),
rest: Some(Box::new(rest)),
}
}
pub fn unknown() -> Self {
Self::range_with_constraint(Constraint::Unknown)
}
pub fn string_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
Self::range_with_constraint(Constraint::Type(PrimitiveType::String))
}
pub fn int_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
Self::range_with_constraint(Constraint::Type(PrimitiveType::Int))
}
pub fn float_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
Self::range_with_constraint(Constraint::Type(PrimitiveType::Float))
}
pub fn bool_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
Self::range_with_constraint(Constraint::Type(PrimitiveType::Bool))
}
pub fn array_of(item: HostValue) -> Self {
Self::ArrayConstraint {
pub fn array_of(item: Value) -> Self {
Self::ArrayRange {
item: Box::new(item),
constraints: Vec::new(),
default: None,
}
}
pub fn map_of(value: HostValue) -> Self {
Self::MapConstraint {
pub fn map_of(value: Value) -> Self {
Self::MapRange {
value: Box::new(value),
constraints: Vec::new(),
default: None,
@@ -118,11 +167,11 @@ impl HostValue {
}
pub fn builtin_predicate(name: impl Into<String>) -> Self {
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
Self::range_with_constraint(Constraint::BuiltinPredicate(name.into()))
}
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
Self::Abstract {
pub fn range_with_constraint(constraint: Constraint) -> Self {
Self::Range {
constraints: alloc::vec![constraint],
default: None,
}
@@ -130,12 +179,12 @@ impl HostValue {
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
match &mut self {
Self::Abstract { constraints, .. } => constraints.push(constraint),
Self::ArrayConstraint { constraints, .. } | Self::MapConstraint { constraints, .. } => {
Self::Range { constraints, .. } => constraints.push(constraint),
Self::ArrayRange { constraints, .. } | Self::MapRange { constraints, .. } => {
constraints.push(constraint)
}
_ => {
self = Self::Abstract {
self = Self::Range {
constraints: alloc::vec![constraint],
default: Some(Box::new(self)),
};
@@ -166,53 +215,53 @@ impl HostValue {
))
}
pub fn default(self, value: HostValue) -> Result<Self> {
pub fn default(self, value: Value) -> Result<Self> {
match self {
Self::ArrayConstraint {
Self::ArrayRange {
item,
constraints,
default: None,
} => Ok(Self::ArrayConstraint {
} => Ok(Self::ArrayRange {
item,
constraints,
default: Some(Box::new(value)),
}),
Self::ArrayConstraint {
Self::ArrayRange {
default: Some(_), ..
} => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict,
Span::default(),
"host value already has a default",
"value already has a default",
)),
Self::MapConstraint {
Self::MapRange {
value: map_value,
constraints,
default: None,
} => Ok(Self::MapConstraint {
} => Ok(Self::MapRange {
value: map_value,
constraints,
default: Some(Box::new(value)),
}),
Self::MapConstraint {
Self::MapRange {
default: Some(_), ..
} => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict,
Span::default(),
"host value already has a default",
"value already has a default",
)),
Self::Abstract {
Self::Range {
constraints,
default: None,
} => Ok(Self::Abstract {
} => Ok(Self::Range {
constraints,
default: Some(Box::new(value)),
}),
Self::Abstract { .. } => Err(Diagnostic::new(
Self::Range { .. } => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict,
Span::default(),
"host value already has a default",
"value already has a default",
)),
concrete => Ok(Self::Abstract {
concrete => Ok(Self::Range {
constraints: Vec::new(),
default: Some(Box::new(concrete)),
}),
@@ -236,8 +285,8 @@ impl HostValue {
}
}
impl HostField {
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
impl ValueField {
pub fn new(name: impl Into<String>, value: Value) -> Self {
Self {
name: name.into(),
value,
@@ -245,28 +294,40 @@ impl HostField {
}
}
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
fn insert_path(fields: &mut Vec<ValueField>, path: &str, value: Value) {
let mut parts = path.splitn(2, '.');
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
return;
};
if let Some(tail) = parts.next() {
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
if let HostValue::Object(children) = &mut field.value {
if let Value::Object {
fields: children, ..
} = &mut field.value
{
insert_path(children, tail, value);
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
field.value = HostValue::Object(children);
field.value = Value::Object {
fields: children,
rest: None,
};
}
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
fields.push(HostField::new(head, HostValue::Object(children)));
fields.push(ValueField::new(
head,
Value::Object {
fields: children,
rest: None,
},
));
}
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
field.value = value;
} else {
fields.push(HostField::new(head, value));
fields.push(ValueField::new(head, value));
}
}