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
+13 -7
View File
@@ -5,10 +5,16 @@ use std::{
};
use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
Span, format_diagnostic_with,
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, SourceId, Span,
format_diagnostic_with,
};
struct SourceInput {
key: String,
name: String,
source: String,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
@@ -69,7 +75,7 @@ fn materialize_path(path: &str) -> Result<Data, String> {
.map_err(|error| engine.format_diagnostic(&error))
}
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
fn read_root_source(path: &str) -> Result<SourceInput, Diagnostic> {
if path == "-" {
use std::io::Read;
let mut source = String::new();
@@ -82,7 +88,7 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
format!("failed to read stdin: {error}"),
)
})?;
Ok(LoadedSource {
Ok(SourceInput {
key: String::from("<stdin>"),
name: String::from("<stdin>"),
source,
@@ -112,11 +118,11 @@ impl ImportLoader for FsLoader {
} else {
PathBuf::from(path)
};
load_path(&path).map(LoadedImport::Source)
load_path(&path).map(|source| LoadedImport::source(source.key, source.name, source.source))
}
}
fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
fn load_path(path: &Path) -> Result<SourceInput, Diagnostic> {
let canonical = path.canonicalize().map_err(|error| {
Diagnostic::new(
DiagnosticKind::Import,
@@ -132,7 +138,7 @@ fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
)
})?;
let key = canonical.to_string_lossy().into_owned();
Ok(LoadedSource {
Ok(SourceInput {
name: key.clone(),
key,
source,
+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));
}
}
+106 -5
View File
@@ -35,12 +35,38 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
let mut schema_fields = Vec::new();
let mut decode_fields = Vec::new();
let mut known_field_roots = Vec::new();
let mut rest_field = None;
for field in fields {
let ident = field.ident.expect("named field");
let ty = field.ty;
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
if attrs.rest {
if attrs.rename_explicit || attrs.default.is_some() || !attrs.constraints.is_empty() {
return Err(syn::Error::new(
ident.span(),
"`rest` cannot be combined with rename, default, or field constraints",
));
}
if rest_field.is_some() {
return Err(syn::Error::new(
ident.span(),
"Decodal derive supports only one `rest` field",
));
}
rest_field = Some((ident, ty));
continue;
}
let path = attrs.rename.clone();
known_field_roots.push(
path.split('.')
.next()
.expect("field paths are non-empty")
.to_owned(),
);
let schema_value = schema_expr(&ty, &attrs)?;
let decode_value = decode_expr(&ty, &path, &attrs);
@@ -52,14 +78,40 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
});
}
if let Some((ident, ty)) = &rest_field {
decode_fields.push(quote! {
#ident: <#ty as ::decodal::DecodalRest>::decodal_decode_rest(
data,
&[#(#known_field_roots),*],
)?
});
}
let schema_entries = if schema_fields.is_empty() {
quote! { ::core::iter::empty::<(&'static str, ::decodal::Value)>() }
} else {
quote! { [#(#schema_fields),*] }
};
let build_schema = if let Some((_, ty)) = &rest_field {
quote! {
::decodal::Value::object_from_paths_with_rest(
#schema_entries,
<#ty as ::decodal::DecodalRest>::decodal_rest_schema(),
)
}
} else {
quote! {
::decodal::Value::object_from_paths(#schema_entries)
}
};
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
fn decodal_schema() -> ::decodal::HostValue {
::decodal::HostValue::object_from_paths([
#(#schema_fields),*
])
fn decodal_schema() -> ::decodal::Value {
#build_schema
}
}
@@ -76,6 +128,8 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
#[derive(Default)]
struct FieldAttrs {
rename: String,
rename_explicit: bool,
rest: bool,
default: Option<DefaultAttr>,
constraints: Vec<ConstraintAttr>,
}
@@ -95,6 +149,14 @@ impl FieldAttrs {
if meta.path.is_ident("rename") {
let value: LitStr = meta.value()?.parse()?;
output.rename = value.value();
output.rename_explicit = true;
return Ok(());
}
if meta.path.is_ident("rest") {
if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
return Err(meta.error("`rest` does not accept a value"));
}
output.rest = true;
return Ok(());
}
if meta.path.is_ident("default") {
@@ -182,7 +244,7 @@ fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
};
tokens.extend(quote! {
schema = schema
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
.default(::decodal::IntoValue::into_value(#default_expr))
.expect("Decodal derive generated a valid default");
});
}
@@ -210,3 +272,42 @@ fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_multiple_rest_fields() {
let input: DeriveInput = syn::parse_quote! {
struct Invalid {
#[decodal(rest)]
first: std::collections::BTreeMap<String, String>,
#[decodal(rest)]
second: std::collections::BTreeMap<String, String>,
}
};
assert!(
expand_decodal(input)
.unwrap_err()
.to_string()
.contains("only one `rest` field")
);
}
#[test]
fn rejects_rest_field_modifiers() {
let input: DeriveInput = syn::parse_quote! {
struct Invalid {
#[decodal(rest, default)]
extra: std::collections::BTreeMap<String, String>,
}
};
assert!(
expand_decodal(input)
.unwrap_err()
.to_string()
.contains("cannot be combined")
);
}
}
+119 -1
View File
@@ -1,4 +1,6 @@
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
use std::collections::BTreeMap;
use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value};
use decodal_derive::Decodal;
#[derive(Debug, PartialEq, Decodal)]
@@ -24,6 +26,28 @@ struct Fleet {
workers: Vec<Worker>,
}
#[derive(Debug, PartialEq, Decodal)]
struct OpenConfig {
name: String,
#[decodal(default = true, rename = "feature.enabled")]
enabled: bool,
#[decodal(rest)]
extra: BTreeMap<String, Data>,
}
#[derive(Debug, PartialEq, Decodal)]
struct StringLabels {
name: String,
#[decodal(rest)]
labels: BTreeMap<String, String>,
}
#[derive(Debug, PartialEq, Decodal)]
struct OpenBag {
#[decodal(rest)]
values: BTreeMap<String, Data>,
}
#[test]
fn derives_schema_and_decode() {
let mut engine = Engine::new(EmptyLoader);
@@ -148,3 +172,97 @@ fn vec_schema_applies_nested_struct_defaults() {
}
);
}
#[test]
fn rest_map_opens_the_derived_object_and_collects_additional_fields() {
let schema = OpenConfig::decodal_schema();
let Value::Object {
fields,
rest: Some(rest),
} = schema
else {
panic!("derived schema should have an object rest range")
};
assert_eq!(fields.len(), 2);
assert!(matches!(
*rest,
Value::Range { ref constraints, .. }
if constraints == &[Constraint::Unknown]
));
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global("OpenConfig", OpenConfig::decodal_schema())
.unwrap();
let module = engine
.add_root_source(
"test",
"test",
r#"
{
name = "api";
plugin = { name = "cache"; };
retries = 3;
} as OpenConfig
"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let config = OpenConfig::decodal_decode(&data).unwrap();
assert_eq!(config.name, "api");
assert!(config.enabled);
assert_eq!(config.extra.get("retries"), Some(&Data::Int(3)));
assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_))));
assert!(!config.extra.contains_key("name"));
assert!(!config.extra.contains_key("feature"));
}
#[test]
fn typed_rest_map_constrains_each_additional_field() {
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global("StringLabels", StringLabels::decodal_schema())
.unwrap();
let module = engine
.add_root_source(
"test",
"test",
r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let labels = StringLabels::decodal_decode(&data).unwrap();
assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into()));
assert_eq!(labels.labels.get("tier"), Some(&"edge".into()));
let module = engine
.add_root_source(
"invalid",
"invalid",
r#"{ name = "service"; priority = 1; } as StringLabels"#,
)
.unwrap();
assert!(engine.eval_module(module).is_err());
}
#[test]
fn rest_only_struct_collects_every_field() {
let schema = OpenBag::decodal_schema();
assert!(matches!(
schema,
Value::Object {
ref fields,
rest: Some(_),
} if fields.is_empty()
));
let data = Data::Object(vec![decodal::runtime::DataField {
name: "answer".into(),
value: Data::Int(42),
}]);
let bag = OpenBag::decodal_decode(&data).unwrap();
assert_eq!(bag.values.get("answer"), Some(&Data::Int(42)));
}
@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, HashMap};
use decodal::{
Engine, HostEnvironment, HostValue, ImportLoader, LoadedImport, Result, RuntimeValue,
Engine, HostEnvironment, ImportLoader, LoadedImport, Result, Value, runtime::RuntimeValue,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -116,12 +116,12 @@ pub(crate) fn complete<E: HostEnvironment>(
let mut loader = environment.create_loader();
let loaded = loader.load(Some(key), specifier)?;
detail = match &loaded {
LoadedImport::Source(source) => source.name.clone(),
LoadedImport::Value(value) => value.key.clone(),
LoadedImport::Source { name, .. } => name.clone(),
LoadedImport::Value { key, .. } => key.clone(),
};
fields = match loaded {
LoadedImport::Source(source) => collect_fields(&tokenize(&source.source)),
LoadedImport::Value(value) => fields_from_host_value(&value.value),
LoadedImport::Source { source, .. } => collect_fields(&tokenize(&source)),
LoadedImport::Value { value, .. } => fields_from_value(&value),
};
} else if let Some(local) = local_fields.get(root) {
fields = local.clone();
@@ -229,6 +229,7 @@ fn builtin_items() -> Vec<CompletionItem> {
("Int", CompletionKind::Type, "integer constraint", 5),
("Float", CompletionKind::Type, "float constraint", 5),
("Bool", CompletionKind::Type, "boolean constraint", 5),
("Unknown", CompletionKind::Type, "unknown top range", 5),
]
.into_iter()
.map(|(label, kind, detail, priority)| CompletionItem {
@@ -267,24 +268,24 @@ fn fields_from_runtime<L: ImportLoader>(
Ok(tree)
}
fn fields_from_host_value(value: &HostValue) -> FieldTree {
fn fields_from_value(value: &Value) -> FieldTree {
match value {
HostValue::Object(fields) => fields
Value::Object { fields, .. } => fields
.iter()
.map(|field| (field.name.clone(), fields_from_host_value(&field.value)))
.map(|field| (field.name.clone(), fields_from_value(&field.value)))
.collect(),
HostValue::ArrayConstraint {
Value::ArrayRange {
default: Some(value),
..
}
| HostValue::MapConstraint {
| Value::MapRange {
default: Some(value),
..
}
| HostValue::Abstract {
| Value::Range {
default: Some(value),
..
} => fields_from_host_value(value),
} => fields_from_value(value),
_ => FieldTree::new(),
}
}
@@ -688,7 +689,7 @@ mod tests {
use std::collections::BTreeMap;
use decodal::{
Diagnostic, DiagnosticKind, EmptyLoader, HostValue, ImportCandidate, LoadedImport, Span,
Diagnostic, DiagnosticKind, EmptyLoader, ImportCandidate, LoadedImport, Span, Value,
};
use super::*;
@@ -708,15 +709,15 @@ mod tests {
if specifier == "./post.md" {
return Ok(LoadedImport::value(
"post.md",
HostValue::object([
Value::object([
(
"frontmatter",
HostValue::object([
("title", HostValue::string("Hello")),
("draft", HostValue::bool(false)),
Value::object([
("title", Value::string("Hello")),
("draft", Value::bool(false)),
]),
),
("body", HostValue::string("# Hello")),
("body", Value::string("# Hello")),
]),
));
}
@@ -758,12 +759,9 @@ mod tests {
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
engine.bind_global(
"App",
HostValue::object([(
Value::object([(
"config",
HostValue::object([
("enabled", HostValue::bool_type()),
("port", HostValue::int_type()),
]),
Value::object([("enabled", Value::bool_type()), ("port", Value::int_type())]),
)]),
)?;
Ok(())
@@ -854,6 +852,7 @@ mod tests {
.unwrap();
let labels = labels(result);
assert!(labels.contains(&"String".into()));
assert!(labels.contains(&"Unknown".into()));
assert!(labels.contains(&"service".into()));
assert!(labels.contains(&"value".into()));
assert!(labels.contains(&"App".into()));
@@ -881,6 +880,10 @@ mod tests {
.unwrap()
.unwrap();
assert!(labels(result).contains(&"String".into()));
let result = complete(&EmptyEnvironment, "main.dcdl", "Unk", 3, false)
.unwrap()
.unwrap();
assert!(labels(result).contains(&"Unknown".into()));
}
struct EmptyEnvironment;
+14 -14
View File
@@ -92,7 +92,7 @@ impl SemanticAnalysis {
#[cfg(test)]
mod tests {
use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value,
};
use super::*;
@@ -100,11 +100,11 @@ mod tests {
const ROOT: &str = r#"Post & import "./post.md""#;
struct ContentEnvironment {
draft: HostValue,
draft: Value,
}
struct ContentLoader {
draft: HostValue,
draft: Value,
}
impl ImportLoader for ContentLoader {
@@ -122,15 +122,15 @@ mod tests {
}
Ok(LoadedImport::value(
"content/post.md",
HostValue::object([
Value::object([
(
"frontmatter",
HostValue::object([
("title", HostValue::string("Hello")),
Value::object([
("title", Value::string("Hello")),
("draft", self.draft.clone()),
]),
),
("body", HostValue::string("# Hello")),
("body", Value::string("# Hello")),
]),
))
}
@@ -148,15 +148,15 @@ mod tests {
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
engine.bind_global(
"Post",
HostValue::object([
Value::object([
(
"frontmatter",
HostValue::object([
("title", HostValue::string_type()),
("draft", HostValue::bool_type()),
Value::object([
("title", Value::string_type()),
("draft", Value::bool_type()),
]),
),
("body", HostValue::string_type()),
("body", Value::string_type()),
]),
)?;
Ok(())
@@ -173,7 +173,7 @@ mod tests {
#[test]
fn injected_environment_matches_direct_evaluation() {
let environment = ContentEnvironment {
draft: HostValue::bool(false),
draft: Value::bool(false),
};
let direct = evaluate_direct(&environment).unwrap();
let service = LanguageService::new(&environment);
@@ -186,7 +186,7 @@ mod tests {
#[test]
fn injected_environment_preserves_import_diagnostics() {
let service = LanguageService::new(ContentEnvironment {
draft: HostValue::string("maybe"),
draft: Value::string("maybe"),
});
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
+57 -9
View File
@@ -1,8 +1,8 @@
use std::{error::Error, fmt};
use decodal::{
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Param, SourceForm, Span, SyntaxToken,
SyntaxTokenKind,
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, ObjectRest, Param, SourceForm, Span,
SyntaxToken, SyntaxTokenKind,
ast::{MatchArm, UnaryOp},
parse_source, tokenize_source,
};
@@ -81,7 +81,7 @@ impl<'a> Formatter<'a> {
fn format(&self, root: ExprId, source_form: SourceForm) -> String {
let mut out = String::new();
if source_form == SourceForm::Fields {
let Expr::Object(fields) = &self.ast.get(root).expr else {
let Expr::Object { fields, .. } = &self.ast.get(root).expr else {
unreachable!("field-form sources parse to an object")
};
self.write_field_list(&mut out, fields, 0, self.source.len(), 0);
@@ -145,7 +145,7 @@ impl<'a> Formatter<'a> {
if self.has_comment(node.span)
&& !matches!(
node.expr,
Expr::Object(_) | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
Expr::Object { .. } | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
)
{
out.push_str(self.raw(node.span));
@@ -162,7 +162,9 @@ impl<'a> Formatter<'a> {
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
out.push_str(self.raw(node.span));
}
Expr::Object(fields) => self.write_object(out, node.span, fields, indent),
Expr::Object { fields, rest } => {
self.write_object(out, node.span, fields, rest.as_ref(), indent)
}
Expr::Array(items) => self.write_array(out, node.span, items, indent),
Expr::ArrayConstraint { item } => {
out.push_str("[...");
@@ -241,15 +243,50 @@ impl<'a> Formatter<'a> {
}
}
fn write_object(&self, out: &mut String, span: Span, fields: &[Field], indent: usize) {
fn write_object(
&self,
out: &mut String,
span: Span,
fields: &[Field],
rest: Option<&ObjectRest>,
indent: usize,
) {
let (start, end) =
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace);
if fields.is_empty() && !self.has_comment_between(start, end) {
if fields.is_empty() && rest.is_none() && !self.has_comment_between(start, end) {
out.push_str("{}");
return;
}
out.push_str("{\n");
self.write_field_list(out, fields, start, end, indent + INDENT);
let field_end = rest.map_or(end, |rest| rest.span.start as usize);
self.write_field_list(out, fields, start, field_end, indent + INDENT);
if let Some(rest) = rest {
write_indent(out, indent + INDENT);
out.push_str("...");
let value_start = self.ast.span(rest.value).start as usize;
let ellipsis = self
.tokens_between(rest.span.start as usize, value_start)
.find(|token| token.kind == SyntaxTokenKind::Ellipsis)
.expect("parsed object rest constraints contain `...`");
if self.has_comment_between(ellipsis.span.end as usize, value_start) {
self.write_between(
out,
ellipsis.span.end as usize,
value_start,
Some(ellipsis.span.end as usize),
indent + INDENT,
);
write_indent(out, indent + INDENT);
}
self.write_expr(out, rest.value, indent + INDENT, 0);
self.write_between(
out,
rest.span.end as usize,
end,
Some(rest.span.end as usize),
indent + INDENT,
);
}
write_indent(out, indent);
out.push('}');
}
@@ -524,7 +561,7 @@ impl<'a> Formatter<'a> {
Expr::As { narrower, wider } => {
self.is_inline_expr(*narrower) && self.is_inline_expr(*wider)
}
Expr::Object(_) | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
Expr::Object { .. } | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
false
}
}
@@ -707,6 +744,17 @@ mod tests {
);
}
#[test]
fn formats_unknown_and_object_rest_constraints() {
let source =
"value={hoge=Int;fuga=String;# remaining fields\n...# unconstrained\nUnknown};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"value = {\n hoge = Int;\n fuga = String; # remaining fields\n ... # unconstrained\n Unknown\n};\n"
);
}
#[test]
fn preserves_regex_and_escaped_strings() {
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
+8 -11
View File
@@ -7,7 +7,7 @@ use std::{
use decodal::{
Diagnostic as DecodalDiagnostic, DiagnosticKind, HostEnvironment, ImportCandidate,
ImportLoader, LoadedImport, LoadedSource, SourceId, Span,
ImportLoader, LoadedImport, SourceId, Span,
};
use decodal_language_service::{
CompletionKind as ServiceCompletionKind, CompletionResult, LanguageService, SemanticAnalysis,
@@ -667,11 +667,11 @@ impl ImportLoader for FileSystemLoader {
)
})?
};
Ok(LoadedImport::Source(LoadedSource {
Ok(LoadedImport::Source {
key: key.clone(),
name: key,
source,
}))
})
}
fn complete_import(
@@ -743,7 +743,7 @@ mod tests {
time::Duration,
};
use decodal::{Engine, HostValue};
use decodal::{Engine, Value};
use lsp_server::RequestId;
use serde_json::json;
@@ -772,12 +772,12 @@ mod tests {
.is_some_and(|source| source == "draft: false");
Ok(LoadedImport::value(
"content/post.md",
HostValue::object([(
Value::object([(
"draft",
if draft {
HostValue::bool(false)
Value::bool(false)
} else {
HostValue::string("maybe")
Value::string("maybe")
},
)]),
))
@@ -794,10 +794,7 @@ mod tests {
}
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
engine.bind_global(
"Post",
HostValue::object([("draft", HostValue::bool_type())]),
)?;
engine.bind_global("Post", Value::object([("draft", Value::bool_type())]))?;
Ok(())
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use decodal_language_service::{CompletionKind, LanguageService};
use wasm_bindgen::prelude::*;
#[cfg(any(target_arch = "wasm32", test))]
mod host_value;
mod value;
#[cfg(target_arch = "wasm32")]
mod web_environment;
@@ -1,41 +1,41 @@
use decodal::{CompareOp, Constraint, HostValue, LiteralValue, PrimitiveType};
use serde_json::{Map, Value};
use decodal::{CompareOp, Constraint, LiteralValue, PrimitiveType, Value};
use serde_json::{Map, Value as JsonValue};
pub(crate) fn from_json(value: &Value) -> Result<HostValue, String> {
pub(crate) fn from_json(value: &JsonValue) -> Result<Value, String> {
match value {
Value::Null => Err(String::from("null is not a Decodal host value")),
Value::Bool(value) => Ok(HostValue::bool(*value)),
Value::Number(value) => number(value),
Value::String(value) => Ok(HostValue::string(value)),
Value::Array(items) => items
JsonValue::Null => Err(String::from("null is not a Decodal value")),
JsonValue::Bool(value) => Ok(Value::bool(*value)),
JsonValue::Number(value) => number(value),
JsonValue::String(value) => Ok(Value::string(value)),
JsonValue::Array(items) => items
.iter()
.enumerate()
.map(|(index, value)| {
from_json(value).map_err(|error| format!("array item {index}: {error}"))
})
.collect::<Result<Vec<_>, _>>()
.map(HostValue::array),
Value::Object(fields) => object(fields),
.map(Value::array),
JsonValue::Object(fields) => object(fields),
}
}
fn number(value: &serde_json::Number) -> Result<HostValue, String> {
fn number(value: &serde_json::Number) -> Result<Value, String> {
if let Some(value) = value.as_i64() {
return Ok(HostValue::int(value));
return Ok(Value::int(value));
}
if let Some(value) = value.as_u64() {
return i64::try_from(value)
.map(HostValue::int)
.map_err(|_| String::from("integer host value is outside the signed 64-bit range"));
.map(Value::int)
.map_err(|_| String::from("integer value is outside the signed 64-bit range"));
}
value
.as_f64()
.filter(|value| value.is_finite())
.map(HostValue::float)
.ok_or_else(|| String::from("invalid numeric host value"))
.map(Value::float)
.ok_or_else(|| String::from("invalid numeric value"))
}
fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
fn object(fields: &Map<String, JsonValue>) -> Result<Value, String> {
let Some(descriptor) = fields.get("$decodal") else {
return fields
.iter()
@@ -45,7 +45,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
.map_err(|error| format!("field `{name}`: {error}"))
})
.collect::<Result<Vec<_>, _>>()
.map(HostValue::object);
.map(Value::object);
};
let descriptor = descriptor
@@ -59,6 +59,14 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
.map(Box::new);
match descriptor {
"Unknown" => {
let mut all_constraints = vec![Constraint::Unknown];
all_constraints.extend(constraints);
Ok(Value::Range {
constraints: all_constraints,
default,
})
}
"String" | "Int" | "Float" | "Bool" => {
let primitive = match descriptor {
"String" => PrimitiveType::String,
@@ -69,7 +77,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
};
let mut all_constraints = vec![Constraint::Type(primitive)];
all_constraints.extend(constraints);
Ok(HostValue::Abstract {
Ok(Value::Range {
constraints: all_constraints,
default,
})
@@ -78,7 +86,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
let item = fields
.get("item")
.ok_or_else(|| String::from("Array descriptor requires `item`"))?;
Ok(HostValue::ArrayConstraint {
Ok(Value::ArrayRange {
item: Box::new(from_json(item)?),
constraints,
default,
@@ -88,21 +96,49 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
let value = fields
.get("value")
.ok_or_else(|| String::from("Map descriptor requires `value`"))?;
Ok(HostValue::MapConstraint {
Ok(Value::MapRange {
value: Box::new(from_json(value)?),
constraints,
default,
})
}
"Abstract" => Ok(HostValue::Abstract {
"Object" => {
if !constraints.is_empty() {
return Err(String::from(
"Object descriptor does not accept `constraints`; constrain its fields or rest range",
));
}
if default.is_some() {
return Err(String::from(
"Object descriptor does not accept `default`; place defaults on its fields",
));
}
let object_fields = fields
.get("fields")
.and_then(JsonValue::as_object)
.ok_or_else(|| String::from("Object descriptor requires object `fields`"))?;
let value_fields = object_fields
.iter()
.map(|(name, value)| {
from_json(value)
.map(|value| (name.clone(), value))
.map_err(|error| format!("field `{name}`: {error}"))
})
.collect::<Result<Vec<_>, _>>()?;
let rest = fields
.get("rest")
.ok_or_else(|| String::from("Object descriptor requires `rest`"))?;
Ok(Value::object_with_rest(value_fields, from_json(rest)?))
}
"Range" => Ok(Value::Range {
constraints,
default,
}),
name => Err(format!("unknown Decodal host descriptor `{name}`")),
name => Err(format!("unknown Decodal value descriptor `{name}`")),
}
}
fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
fn parse_constraints(value: Option<&JsonValue>) -> Result<Vec<Constraint>, String> {
let Some(value) = value else {
return Ok(Vec::new());
};
@@ -118,12 +154,13 @@ fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
.collect()
}
fn parse_constraint(value: &Value) -> Result<Constraint, String> {
fn parse_constraint(value: &JsonValue) -> Result<Constraint, String> {
let fields = value
.as_object()
.ok_or_else(|| String::from("constraint must be an object"))?;
let kind = required_string(fields, "kind")?;
match kind {
"unknown" => Ok(Constraint::Unknown),
"type" => match required_string(fields, "value")? {
"String" => Ok(Constraint::Type(PrimitiveType::String)),
"Int" => Ok(Constraint::Type(PrimitiveType::Int)),
@@ -154,29 +191,29 @@ fn parse_constraint(value: &Value) -> Result<Constraint, String> {
}
}
fn literal(value: &Value) -> Result<LiteralValue, String> {
fn literal(value: &JsonValue) -> Result<LiteralValue, String> {
match value {
Value::String(value) => Ok(LiteralValue::String(value.clone())),
Value::Bool(value) => Ok(LiteralValue::Bool(*value)),
Value::Number(value) => match number(value)? {
HostValue::Int(value) => Ok(LiteralValue::Int(value)),
HostValue::Float(value) => Ok(LiteralValue::Float(value)),
JsonValue::String(value) => Ok(LiteralValue::String(value.clone())),
JsonValue::Bool(value) => Ok(LiteralValue::Bool(*value)),
JsonValue::Number(value) => match number(value)? {
Value::Int(value) => Ok(LiteralValue::Int(value)),
Value::Float(value) => Ok(LiteralValue::Float(value)),
_ => unreachable!(),
},
_ => Err(String::from("comparison value must be a primitive literal")),
}
}
fn required_string<'a>(fields: &'a Map<String, Value>, name: &str) -> Result<&'a str, String> {
fn required_string<'a>(fields: &'a Map<String, JsonValue>, name: &str) -> Result<&'a str, String> {
fields
.get(name)
.and_then(Value::as_str)
.and_then(JsonValue::as_str)
.ok_or_else(|| format!("constraint requires string `{name}`"))
}
#[cfg(test)]
mod tests {
use decodal::{Constraint, HostValue, LiteralValue, PrimitiveType};
use decodal::{Constraint, LiteralValue, PrimitiveType, Value};
use super::from_json;
@@ -188,11 +225,11 @@ mod tests {
});
assert_eq!(
from_json(&value).unwrap(),
HostValue::object([
("body", HostValue::string("# Hello")),
Value::object([
("body", Value::string("# Hello")),
(
"frontmatter",
HostValue::object([("draft", HostValue::bool(false))]),
Value::object([("draft", Value::bool(false))]),
),
])
);
@@ -210,8 +247,8 @@ mod tests {
});
assert_eq!(
from_json(&value).unwrap(),
HostValue::ArrayConstraint {
item: Box::new(HostValue::Abstract {
Value::ArrayRange {
item: Box::new(Value::Range {
constraints: vec![
Constraint::Type(PrimitiveType::Int),
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
@@ -219,10 +256,7 @@ mod tests {
default: None,
}),
constraints: Vec::new(),
default: Some(Box::new(HostValue::array([
HostValue::int(1),
HostValue::int(2),
]))),
default: Some(Box::new(Value::array([Value::int(1), Value::int(2),]))),
}
);
}
@@ -236,10 +270,41 @@ mod tests {
});
assert_eq!(
from_json(&value).unwrap(),
HostValue::MapConstraint {
value: Box::new(HostValue::bool_type()),
Value::MapRange {
value: Box::new(Value::bool_type()),
constraints: Vec::new(),
default: Some(Box::new(HostValue::object([] as [(&str, HostValue); 0]))),
default: Some(Box::new(Value::object([] as [(&str, Value); 0]))),
}
);
}
#[test]
fn converts_unknown_and_open_object_descriptors() {
let value = serde_json::json!({
"$decodal": "Object",
"fields": {
"name": { "$decodal": "String" },
},
"rest": { "$decodal": "Unknown" },
});
assert_eq!(
from_json(&value).unwrap(),
Value::object_with_rest([("name", Value::string_type())], Value::unknown(),)
);
}
#[test]
fn converts_general_range_descriptors() {
let value = serde_json::json!({
"$decodal": "Range",
"constraints": [{ "kind": "predicate", "value": "available" }],
"default": true,
});
assert_eq!(
from_json(&value).unwrap(),
Value::Range {
constraints: vec![Constraint::BuiltinPredicate("available".into())],
default: Some(Box::new(Value::bool(true))),
}
);
}
+6 -6
View File
@@ -1,17 +1,17 @@
use std::collections::BTreeMap;
use decodal::{
Diagnostic, DiagnosticKind, Engine, HostEnvironment, HostValue, ImportCandidate, ImportLoader,
LoadedImport, Span,
Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader,
LoadedImport, Span, Value as DecodalValue,
};
use js_sys::{Function, Reflect};
use serde_json::Value;
use wasm_bindgen::{JsCast, JsValue};
use crate::host_value;
use crate::value;
pub(crate) struct JsEnvironment {
globals: BTreeMap<String, HostValue>,
globals: BTreeMap<String, DecodalValue>,
load_import: Option<Function>,
complete_import: Option<Function>,
}
@@ -37,7 +37,7 @@ impl JsEnvironment {
globals
.iter()
.map(|(name, value)| {
host_value::from_json(value)
value::from_json(value)
.map(|value| (name.clone(), value))
.map_err(|error| {
JsValue::from_str(&format!("invalid global `{name}`: {error}"))
@@ -133,7 +133,7 @@ fn loaded_import(value: &Value) -> Result<LoadedImport, String> {
let value = fields
.get("value")
.ok_or_else(|| String::from("value import requires `value`"))?;
let value = host_value::from_json(value)
let value = value::from_json(value)
.map_err(|error| format!("invalid imported value: {error}"))?;
Ok(LoadedImport::value(key, value))
}