Add unknown ranges and open object values
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(¤t_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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user