1384 lines
48 KiB
Rust
1384 lines
48 KiB
Rust
use alloc::{format, string::String, vec, vec::Vec};
|
|
|
|
use crate::{
|
|
ExprId, SourceForm, SourceId, Span,
|
|
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
|
|
constraints::normalize_constraints,
|
|
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
|
embedding::HostValue,
|
|
module::{EmptyLoader, LoadedSource, Module, SourceLoader},
|
|
parse_source_with_source_id,
|
|
runtime::{
|
|
AbstractValue, Binding, ConcreteValue, Constraint, Data, DataField, Env, EnvId, ExprRef,
|
|
FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField, ObjectValue,
|
|
PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
|
|
},
|
|
};
|
|
|
|
pub struct Engine<L = EmptyLoader> {
|
|
loader: L,
|
|
prelude_env: EnvId,
|
|
modules: Vec<Module>,
|
|
thunks: Vec<Thunk>,
|
|
envs: Vec<Env>,
|
|
}
|
|
|
|
impl Engine<EmptyLoader> {
|
|
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
|
let mut this = Self::new(EmptyLoader);
|
|
this.register_parsed(
|
|
String::from("<memory>"),
|
|
String::from("<memory>"),
|
|
SourceId(0),
|
|
ast,
|
|
root,
|
|
SourceForm::Expr,
|
|
);
|
|
this
|
|
}
|
|
}
|
|
|
|
impl<L: SourceLoader> Engine<L> {
|
|
pub fn new(loader: L) -> Self {
|
|
Self {
|
|
loader,
|
|
prelude_env: EnvId(0),
|
|
modules: Vec::new(),
|
|
thunks: Vec::new(),
|
|
envs: vec![Env {
|
|
parent: None,
|
|
bindings: Vec::new(),
|
|
}],
|
|
}
|
|
}
|
|
|
|
pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
|
|
let value = self.internalize_host_value(value)?;
|
|
let thunk = self.add_value_thunk(value);
|
|
self.bind(self.prelude_env, name.into(), thunk);
|
|
Ok(thunk)
|
|
}
|
|
|
|
pub fn bind_global_runtime(&mut self, name: impl Into<String>, value: RuntimeValue) -> ThunkId {
|
|
let thunk = self.add_value_thunk(value);
|
|
self.bind(self.prelude_env, name.into(), thunk);
|
|
thunk
|
|
}
|
|
|
|
pub fn add_root_source(
|
|
&mut self,
|
|
key: impl Into<String>,
|
|
name: impl Into<String>,
|
|
source: &str,
|
|
) -> Result<ModuleId> {
|
|
self.add_source(key.into(), name.into(), source)
|
|
}
|
|
|
|
pub fn eval_module(&mut self, module: ModuleId) -> Result<RuntimeValue> {
|
|
let thunk = self.modules[module.0 as usize].root_thunk;
|
|
self.force(thunk)
|
|
}
|
|
|
|
pub fn eval_root(&mut self) -> Result<RuntimeValue> {
|
|
self.eval_module(ModuleId(0))
|
|
}
|
|
|
|
pub fn materialize(&mut self, value: &RuntimeValue) -> Result<Data> {
|
|
match value {
|
|
RuntimeValue::Concrete(value) => match value {
|
|
ConcreteValue::String(value) => Ok(Data::String(value.clone())),
|
|
ConcreteValue::Int(value) => Ok(Data::Int(*value)),
|
|
ConcreteValue::Float(value) => Ok(Data::Float(*value)),
|
|
ConcreteValue::Bool(value) => Ok(Data::Bool(*value)),
|
|
ConcreteValue::Array(items) => {
|
|
let mut data = Vec::new();
|
|
for item in items {
|
|
let value = self.force(*item)?;
|
|
data.push(self.materialize(&value)?);
|
|
}
|
|
Ok(Data::Array(data))
|
|
}
|
|
ConcreteValue::Object(object) => {
|
|
let mut fields = Vec::new();
|
|
for field in &object.fields {
|
|
let value = self.force(field.value)?;
|
|
fields.push(DataField {
|
|
name: field.name.clone(),
|
|
value: self.materialize(&value)?,
|
|
});
|
|
}
|
|
Ok(Data::Object(fields))
|
|
}
|
|
ConcreteValue::Function(_) => Err(Diagnostic::new(
|
|
DiagnosticKind::Materialize,
|
|
Span::default(),
|
|
"cannot materialize function value",
|
|
)),
|
|
},
|
|
RuntimeValue::Abstract(abstract_value) => {
|
|
let Some(default) = abstract_value.default else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Materialize,
|
|
Span::default(),
|
|
"cannot materialize unresolved abstract value without default",
|
|
));
|
|
};
|
|
let value = self.force(default)?;
|
|
self.ensure_satisfies(&value, &abstract_value.constraints, Span::default())?;
|
|
self.materialize(&value)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn add_source(&mut self, key: String, name: String, source: &str) -> Result<ModuleId> {
|
|
if let Some(id) = self.find_module(&key) {
|
|
return Ok(id);
|
|
}
|
|
let source_id = SourceId(self.modules.len() as u32);
|
|
let parsed = parse_source_with_source_id(source_id, source)?;
|
|
Ok(self.register_parsed(
|
|
key,
|
|
name,
|
|
source_id,
|
|
parsed.ast,
|
|
parsed.root,
|
|
parsed.source_form,
|
|
))
|
|
}
|
|
|
|
fn register_parsed(
|
|
&mut self,
|
|
key: String,
|
|
name: String,
|
|
source: SourceId,
|
|
ast: Ast,
|
|
root: ExprId,
|
|
source_form: SourceForm,
|
|
) -> ModuleId {
|
|
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() {
|
|
let object = self
|
|
.build_object(module, &fields, 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);
|
|
}
|
|
self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
|
} else {
|
|
self.add_expr_thunk(ExprRef { module, expr: root }, root_env)
|
|
}
|
|
} else {
|
|
self.add_expr_thunk(ExprRef { module, expr: root }, root_env)
|
|
};
|
|
self.modules.push(Module {
|
|
key,
|
|
name,
|
|
source,
|
|
ast,
|
|
root,
|
|
source_form,
|
|
root_env,
|
|
root_thunk,
|
|
});
|
|
module
|
|
}
|
|
|
|
fn find_module(&self, key: &str) -> Option<ModuleId> {
|
|
self.modules
|
|
.iter()
|
|
.position(|module| module.key == key)
|
|
.map(|index| ModuleId(index as u32))
|
|
}
|
|
|
|
fn load_import(&mut self, current: ModuleId, specifier: &str) -> Result<ModuleId> {
|
|
let current_key = self.modules[current.0 as usize].key.clone();
|
|
let LoadedSource { key, name, source } = self.loader.load(Some(¤t_key), specifier)?;
|
|
self.add_source(key, name, &source)
|
|
}
|
|
|
|
fn eval_expr(&mut self, reference: ExprRef, env: EnvId) -> Result<RuntimeValue> {
|
|
let span = self.expr_span(reference);
|
|
let expr = self.expr(reference).clone();
|
|
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)
|
|
.map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
|
|
Expr::Array(items) => {
|
|
let thunks = items
|
|
.into_iter()
|
|
.map(|item| {
|
|
self.add_expr_thunk(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: item,
|
|
},
|
|
env,
|
|
)
|
|
})
|
|
.collect();
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
|
}
|
|
Expr::Let { bindings, body } => {
|
|
let let_env = self.new_env(Some(env));
|
|
for binding in bindings {
|
|
let name = field_name(&binding)?;
|
|
let thunk = self.add_expr_thunk(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: binding.value,
|
|
},
|
|
let_env,
|
|
);
|
|
self.bind(let_env, name, thunk);
|
|
}
|
|
self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: body,
|
|
},
|
|
let_env,
|
|
)
|
|
}
|
|
Expr::Import(specifier) => {
|
|
let module = self.load_import(reference.module, &specifier)?;
|
|
self.eval_module(module)
|
|
}
|
|
Expr::Path { base, field } => {
|
|
let base = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: base,
|
|
},
|
|
env,
|
|
)?;
|
|
let RuntimeValue::Concrete(ConcreteValue::Object(object)) = base else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"path base is not an object",
|
|
));
|
|
};
|
|
let Some(thunk) = object
|
|
.fields
|
|
.iter()
|
|
.find(|item| item.name == field)
|
|
.map(|item| item.value)
|
|
else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::UnresolvedIdentifier,
|
|
span,
|
|
format!("unknown field `{field}`"),
|
|
));
|
|
};
|
|
self.force(thunk)
|
|
}
|
|
Expr::Call { callee, args } => self.eval_call(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: callee,
|
|
},
|
|
args,
|
|
reference.module,
|
|
env,
|
|
span,
|
|
),
|
|
Expr::Function { params, body } => {
|
|
let params = params
|
|
.into_iter()
|
|
.map(|param| FunctionParam {
|
|
name: param.name,
|
|
constraint: param.constraint.map(|expr| ExprRef {
|
|
module: reference.module,
|
|
expr,
|
|
}),
|
|
})
|
|
.collect();
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Function(
|
|
FunctionValue {
|
|
params,
|
|
body: ExprRef {
|
|
module: reference.module,
|
|
expr: body,
|
|
},
|
|
env,
|
|
},
|
|
)))
|
|
}
|
|
Expr::Match { scrutinee, arms } => {
|
|
let value = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: scrutinee,
|
|
},
|
|
env,
|
|
)?;
|
|
for arm in arms {
|
|
let pattern = ExprRef {
|
|
module: reference.module,
|
|
expr: arm.pattern,
|
|
};
|
|
if self.matches_pattern(&value, pattern, env)? {
|
|
return self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: arm.body,
|
|
},
|
|
env,
|
|
);
|
|
}
|
|
}
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::MatchFailure,
|
|
span,
|
|
"no match arm matched",
|
|
))
|
|
}
|
|
Expr::Unary { op, expr } => {
|
|
let value = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr,
|
|
},
|
|
env,
|
|
)?;
|
|
match op {
|
|
UnaryOp::Neg => negate_number(value, span),
|
|
}
|
|
}
|
|
Expr::Binary { op, lhs, rhs } => {
|
|
let lhs = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: lhs,
|
|
},
|
|
env,
|
|
)?;
|
|
let rhs = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: rhs,
|
|
},
|
|
env,
|
|
)?;
|
|
match op {
|
|
BinaryOp::Add => arithmetic(lhs, rhs, ArithmeticOp::Add, span),
|
|
BinaryOp::Sub => arithmetic(lhs, rhs, ArithmeticOp::Sub, span),
|
|
BinaryOp::Mul => arithmetic(lhs, rhs, ArithmeticOp::Mul, span),
|
|
BinaryOp::Div => arithmetic(lhs, rhs, ArithmeticOp::Div, span),
|
|
BinaryOp::And => self.compose_and(lhs, rhs, span),
|
|
BinaryOp::Patch => self.patch(lhs, rhs),
|
|
}
|
|
}
|
|
Expr::Default { base, fallback } => {
|
|
let base = self.eval_expr(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: base,
|
|
},
|
|
env,
|
|
)?;
|
|
match base {
|
|
RuntimeValue::Abstract(mut abstract_value) => {
|
|
abstract_value.default = Some(self.add_expr_thunk(
|
|
ExprRef {
|
|
module: reference.module,
|
|
expr: fallback,
|
|
},
|
|
env,
|
|
));
|
|
Ok(RuntimeValue::Abstract(abstract_value))
|
|
}
|
|
concrete @ RuntimeValue::Concrete(_) => Ok(concrete),
|
|
}
|
|
}
|
|
Expr::CompareConstraint { op, value } => {
|
|
let value_ref = ExprRef {
|
|
module: reference.module,
|
|
expr: value,
|
|
};
|
|
let value = self.eval_expr(value_ref, env)?;
|
|
let value = literal_value_from_runtime(&value).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"comparison constraint operand must be a literal",
|
|
)
|
|
})?;
|
|
Ok(RuntimeValue::Abstract(AbstractValue {
|
|
constraints: vec![Constraint::Compare(op, value)],
|
|
default: None,
|
|
}))
|
|
}
|
|
Expr::RegexConstraint(pattern) => Ok(RuntimeValue::Abstract(AbstractValue {
|
|
constraints: vec![Constraint::Regex(pattern)],
|
|
default: None,
|
|
})),
|
|
Expr::Wildcard => Err(Diagnostic::new(
|
|
DiagnosticKind::UnsupportedFeature,
|
|
span,
|
|
"wildcard is only valid as a match pattern",
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
|
|
if let Some(primitive) = primitive_type(name) {
|
|
return Ok(RuntimeValue::Abstract(AbstractValue {
|
|
constraints: vec![Constraint::Type(primitive)],
|
|
default: None,
|
|
}));
|
|
}
|
|
if let Some(thunk) = self.lookup(env, name) {
|
|
return self.force(thunk);
|
|
}
|
|
if name.chars().next().is_some_and(char::is_uppercase) {
|
|
return Ok(RuntimeValue::Abstract(AbstractValue {
|
|
constraints: vec![Constraint::BuiltinPredicate(String::from(name))],
|
|
default: None,
|
|
}));
|
|
}
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::UnresolvedIdentifier,
|
|
span,
|
|
format!("unknown identifier `{name}`"),
|
|
))
|
|
}
|
|
|
|
fn build_object(
|
|
&mut self,
|
|
module: ModuleId,
|
|
fields: &[Field],
|
|
env: EnvId,
|
|
) -> Result<ObjectValue> {
|
|
let mut object = ObjectValue { fields: Vec::new() };
|
|
for field in fields {
|
|
self.insert_field(
|
|
&mut object,
|
|
module,
|
|
&field.path,
|
|
field.value,
|
|
env,
|
|
field.span,
|
|
)?;
|
|
}
|
|
Ok(object)
|
|
}
|
|
|
|
fn insert_field(
|
|
&mut self,
|
|
object: &mut ObjectValue,
|
|
module: ModuleId,
|
|
path: &[String],
|
|
expr: ExprId,
|
|
env: EnvId,
|
|
span: Span,
|
|
) -> Result<()> {
|
|
if path.is_empty() {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Syntax,
|
|
span,
|
|
"empty field path",
|
|
));
|
|
}
|
|
if path.len() == 1 {
|
|
let value = self.add_expr_thunk(ExprRef { module, expr }, env);
|
|
if object.fields.iter().any(|field| field.name == path[0]) {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
format!("duplicate field `{}`", path[0]),
|
|
));
|
|
}
|
|
object.fields.push(ObjectField {
|
|
name: path[0].clone(),
|
|
value,
|
|
});
|
|
return Ok(());
|
|
}
|
|
|
|
let name = &path[0];
|
|
if let Some(index) = object.fields.iter().position(|field| field.name == *name) {
|
|
let existing = self.force(object.fields[index].value)?;
|
|
let RuntimeValue::Concrete(ConcreteValue::Object(mut nested)) = existing else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
format!("field `{name}` is already defined as a non-object"),
|
|
));
|
|
};
|
|
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
|
object.fields[index].value =
|
|
self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(nested)));
|
|
return Ok(());
|
|
}
|
|
|
|
let mut nested = ObjectValue { fields: Vec::new() };
|
|
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
|
object.fields.push(ObjectField {
|
|
name: name.clone(),
|
|
value: self.add_value_thunk(RuntimeValue::Concrete(ConcreteValue::Object(nested))),
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn eval_call(
|
|
&mut self,
|
|
callee: ExprRef,
|
|
args: Vec<ExprId>,
|
|
caller_module: ModuleId,
|
|
caller_env: EnvId,
|
|
span: Span,
|
|
) -> Result<RuntimeValue> {
|
|
let callee = self.eval_expr(callee, caller_env)?;
|
|
let RuntimeValue::Concrete(ConcreteValue::Function(function)) = callee else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"callee is not a function",
|
|
));
|
|
};
|
|
if args.len() != function.params.len() {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"function call argument count mismatch",
|
|
));
|
|
}
|
|
let call_env = self.new_env(Some(function.env));
|
|
for (param, arg) in function.params.iter().zip(args) {
|
|
let arg_ref = ExprRef {
|
|
module: caller_module,
|
|
expr: arg,
|
|
};
|
|
let arg_thunk = if let Some(constraint) = param.constraint {
|
|
self.add_constrained_thunk(constraint, function.env, arg_ref, caller_env)
|
|
} else {
|
|
self.add_expr_thunk(arg_ref, caller_env)
|
|
};
|
|
self.bind(call_env, param.name.clone(), arg_thunk);
|
|
}
|
|
self.eval_expr(function.body, call_env)
|
|
}
|
|
|
|
fn matches_pattern(
|
|
&mut self,
|
|
value: &RuntimeValue,
|
|
pattern: ExprRef,
|
|
env: EnvId,
|
|
) -> Result<bool> {
|
|
match self.expr(pattern).clone() {
|
|
Expr::Wildcard => Ok(true),
|
|
Expr::CompareConstraint { .. } | Expr::RegexConstraint(_) | Expr::Ident(_) => {
|
|
let constraint = self.eval_expr(pattern, env)?;
|
|
match constraint {
|
|
RuntimeValue::Abstract(abstract_value) => self
|
|
.ensure_satisfies(
|
|
value,
|
|
&abstract_value.constraints,
|
|
self.expr_span(pattern),
|
|
)
|
|
.map(|_| true)
|
|
.or_else(|diag| match diag.kind {
|
|
DiagnosticKind::ConstraintViolation
|
|
| DiagnosticKind::UnsupportedFeature => Ok(false),
|
|
_ => Err(diag),
|
|
}),
|
|
other => Ok(&other == value),
|
|
}
|
|
}
|
|
_ => {
|
|
let pattern_value = self.eval_expr(pattern, env)?;
|
|
Ok(&pattern_value == value)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compose_and(
|
|
&mut self,
|
|
lhs: RuntimeValue,
|
|
rhs: RuntimeValue,
|
|
span: Span,
|
|
) -> Result<RuntimeValue> {
|
|
match (lhs, rhs) {
|
|
(RuntimeValue::Abstract(mut lhs), RuntimeValue::Abstract(rhs)) => {
|
|
lhs.constraints.extend(rhs.constraints);
|
|
lhs.constraints = normalize_constraints(lhs.constraints, span)?;
|
|
lhs.default = merge_default(lhs.default, rhs.default, span)?;
|
|
Ok(RuntimeValue::Abstract(lhs))
|
|
}
|
|
(RuntimeValue::Abstract(abstract_value), concrete @ RuntimeValue::Concrete(_))
|
|
| (concrete @ RuntimeValue::Concrete(_), RuntimeValue::Abstract(abstract_value)) => {
|
|
self.ensure_satisfies(&concrete, &abstract_value.constraints, span)?;
|
|
Ok(concrete)
|
|
}
|
|
(
|
|
RuntimeValue::Concrete(ConcreteValue::Object(lhs)),
|
|
RuntimeValue::Concrete(ConcreteValue::Object(rhs)),
|
|
) => self.compose_objects(lhs, rhs, span),
|
|
(RuntimeValue::Concrete(lhs), RuntimeValue::Concrete(rhs)) => {
|
|
if concrete_scalar_eq(&lhs, &rhs) {
|
|
Ok(RuntimeValue::Concrete(lhs))
|
|
} else {
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
span,
|
|
"concrete values conflict",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compose_objects(
|
|
&mut self,
|
|
mut lhs: ObjectValue,
|
|
rhs: ObjectValue,
|
|
span: Span,
|
|
) -> Result<RuntimeValue> {
|
|
for rhs_field in rhs.fields {
|
|
if let Some(index) = lhs
|
|
.fields
|
|
.iter()
|
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
|
{
|
|
let lhs_value = self.force(lhs.fields[index].value)?;
|
|
let rhs_value = self.force(rhs_field.value)?;
|
|
let value = self.compose_and(lhs_value, rhs_value, span)?;
|
|
lhs.fields[index].value = self.add_value_thunk(value);
|
|
} else {
|
|
lhs.fields.push(rhs_field);
|
|
}
|
|
}
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
|
}
|
|
|
|
fn patch(&mut self, lhs: RuntimeValue, rhs: RuntimeValue) -> Result<RuntimeValue> {
|
|
match (lhs, rhs) {
|
|
(
|
|
RuntimeValue::Concrete(ConcreteValue::Object(lhs)),
|
|
RuntimeValue::Concrete(ConcreteValue::Object(rhs)),
|
|
) => self.patch_objects(lhs, rhs),
|
|
(_, rhs) => Ok(rhs),
|
|
}
|
|
}
|
|
|
|
fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
|
|
for rhs_field in rhs.fields {
|
|
if let Some(index) = lhs
|
|
.fields
|
|
.iter()
|
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
|
{
|
|
let lhs_value = self.force(lhs.fields[index].value)?;
|
|
let rhs_value = self.force(rhs_field.value)?;
|
|
let value = self.patch(lhs_value, rhs_value)?;
|
|
lhs.fields[index].value = self.add_value_thunk(value);
|
|
} else {
|
|
lhs.fields.push(rhs_field);
|
|
}
|
|
}
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
|
}
|
|
|
|
fn ensure_satisfies(
|
|
&mut self,
|
|
value: &RuntimeValue,
|
|
constraints: &[Constraint],
|
|
span: Span,
|
|
) -> Result<()> {
|
|
for constraint in constraints {
|
|
self.satisfies(value, constraint, span)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn satisfies(
|
|
&mut self,
|
|
value: &RuntimeValue,
|
|
constraint: &Constraint,
|
|
span: Span,
|
|
) -> Result<()> {
|
|
match constraint {
|
|
Constraint::Type(primitive) => {
|
|
if value_matches_primitive(value, *primitive) {
|
|
Ok(())
|
|
} else {
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::ConstraintViolation,
|
|
span,
|
|
"value does not satisfy primitive type constraint",
|
|
))
|
|
}
|
|
}
|
|
Constraint::Compare(op, expected) => compare_value(value, *op, expected)
|
|
.then_some(())
|
|
.ok_or_else(|| {
|
|
Diagnostic::new(
|
|
DiagnosticKind::ConstraintViolation,
|
|
span,
|
|
"value does not satisfy comparison constraint",
|
|
)
|
|
}),
|
|
Constraint::Regex(pattern) => satisfies_regex(value, pattern, span),
|
|
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
|
|
DiagnosticKind::UnsupportedFeature,
|
|
span,
|
|
format!("builtin predicate `{name}` is not implemented"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn internalize_host_value(&mut self, value: HostValue) -> 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) => {
|
|
let mut thunks = Vec::new();
|
|
for item in items {
|
|
let value = self.internalize_host_value(item)?;
|
|
thunks.push(self.add_value_thunk(value));
|
|
}
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
|
}
|
|
HostValue::Object(fields) => {
|
|
let mut object = ObjectValue { fields: Vec::new() };
|
|
for field in fields {
|
|
if object
|
|
.fields
|
|
.iter()
|
|
.any(|existing| existing.name == field.name)
|
|
{
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Conflict,
|
|
Span::default(),
|
|
format!("duplicate host object field `{}`", field.name),
|
|
));
|
|
}
|
|
let value = self.internalize_host_value(field.value)?;
|
|
let value = self.add_value_thunk(value);
|
|
object.fields.push(ObjectField {
|
|
name: field.name,
|
|
value,
|
|
});
|
|
}
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
|
}
|
|
HostValue::Abstract {
|
|
constraints,
|
|
default,
|
|
} => {
|
|
let default = if let Some(default) = default {
|
|
let value = self.internalize_host_value(*default)?;
|
|
Some(self.add_value_thunk(value))
|
|
} else {
|
|
None
|
|
};
|
|
let constraints = normalize_constraints(constraints, Span::default())?;
|
|
Ok(RuntimeValue::Abstract(AbstractValue {
|
|
constraints,
|
|
default,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn force(&mut self, id: ThunkId) -> Result<RuntimeValue> {
|
|
let index = id.0 as usize;
|
|
match self.thunks[index].state.clone() {
|
|
ThunkState::Evaluated(value) => return Ok(value),
|
|
ThunkState::Evaluating => {
|
|
self.thunks[index].state = ThunkState::Error;
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Cycle,
|
|
Span::default(),
|
|
"cyclic thunk dependency",
|
|
));
|
|
}
|
|
ThunkState::Error => {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::Cycle,
|
|
Span::default(),
|
|
"thunk previously failed",
|
|
));
|
|
}
|
|
ThunkState::Unevaluated => {}
|
|
}
|
|
|
|
self.thunks[index].state = ThunkState::Evaluating;
|
|
let kind = self.thunks[index].kind.clone();
|
|
let result = match kind {
|
|
ThunkKind::Expr { expr, env } => self.eval_expr(expr, env),
|
|
ThunkKind::Constrained {
|
|
constraint,
|
|
constraint_env,
|
|
value,
|
|
value_env,
|
|
} => {
|
|
let span = self.expr_span(value);
|
|
let constraint = self.eval_expr(constraint, constraint_env)?;
|
|
let value = self.eval_expr(value, value_env)?;
|
|
self.compose_and(constraint, value, span)
|
|
}
|
|
ThunkKind::Value(value) => Ok(value),
|
|
};
|
|
match result {
|
|
Ok(value) => {
|
|
self.thunks[index].state = ThunkState::Evaluated(value.clone());
|
|
Ok(value)
|
|
}
|
|
Err(error) => {
|
|
self.thunks[index].state = ThunkState::Error;
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn add_expr_thunk(&mut self, expr: ExprRef, env: EnvId) -> ThunkId {
|
|
self.add_thunk(ThunkKind::Expr { expr, env })
|
|
}
|
|
|
|
fn add_constrained_thunk(
|
|
&mut self,
|
|
constraint: ExprRef,
|
|
constraint_env: EnvId,
|
|
value: ExprRef,
|
|
value_env: EnvId,
|
|
) -> ThunkId {
|
|
self.add_thunk(ThunkKind::Constrained {
|
|
constraint,
|
|
constraint_env,
|
|
value,
|
|
value_env,
|
|
})
|
|
}
|
|
|
|
fn add_value_thunk(&mut self, value: RuntimeValue) -> ThunkId {
|
|
self.add_thunk(ThunkKind::Value(value))
|
|
}
|
|
|
|
fn add_thunk(&mut self, kind: ThunkKind) -> ThunkId {
|
|
let id = ThunkId(self.thunks.len() as u32);
|
|
self.thunks.push(Thunk {
|
|
kind,
|
|
state: ThunkState::Unevaluated,
|
|
});
|
|
id
|
|
}
|
|
|
|
fn new_env(&mut self, parent: Option<EnvId>) -> EnvId {
|
|
let id = EnvId(self.envs.len() as u32);
|
|
self.envs.push(Env {
|
|
parent,
|
|
bindings: Vec::new(),
|
|
});
|
|
id
|
|
}
|
|
|
|
fn bind(&mut self, env: EnvId, name: String, value: ThunkId) {
|
|
self.envs[env.0 as usize]
|
|
.bindings
|
|
.push(Binding { name, value });
|
|
}
|
|
|
|
fn lookup(&self, env: EnvId, name: &str) -> Option<ThunkId> {
|
|
let mut current = Some(env);
|
|
while let Some(env) = current {
|
|
let frame = &self.envs[env.0 as usize];
|
|
if let Some(binding) = frame
|
|
.bindings
|
|
.iter()
|
|
.rev()
|
|
.find(|binding| binding.name == name)
|
|
{
|
|
return Some(binding.value);
|
|
}
|
|
current = frame.parent;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn expr(&self, reference: ExprRef) -> &Expr {
|
|
&self.modules[reference.module.0 as usize]
|
|
.ast
|
|
.get(reference.expr)
|
|
.expr
|
|
}
|
|
|
|
fn expr_span(&self, reference: ExprRef) -> Span {
|
|
self.modules[reference.module.0 as usize]
|
|
.ast
|
|
.span(reference.expr)
|
|
}
|
|
}
|
|
|
|
fn field_name(field: &Field) -> Result<String> {
|
|
if field.path.len() == 1 {
|
|
Ok(field.path[0].clone())
|
|
} else {
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::UnsupportedFeature,
|
|
field.span,
|
|
"nested let binding names are not supported",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn primitive_type(name: &str) -> Option<PrimitiveType> {
|
|
match name {
|
|
"String" => Some(PrimitiveType::String),
|
|
"Int" => Some(PrimitiveType::Int),
|
|
"Float" => Some(PrimitiveType::Float),
|
|
"Bool" => Some(PrimitiveType::Bool),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn literal_to_concrete(literal: Literal) -> ConcreteValue {
|
|
match literal {
|
|
Literal::String(value) => ConcreteValue::String(value),
|
|
Literal::Int(value) => ConcreteValue::Int(value),
|
|
Literal::Float(value) => ConcreteValue::Float(value),
|
|
Literal::Bool(value) => ConcreteValue::Bool(value),
|
|
}
|
|
}
|
|
|
|
fn literal_value_from_runtime(value: &RuntimeValue) -> Option<LiteralValue> {
|
|
match value {
|
|
RuntimeValue::Concrete(ConcreteValue::String(value)) => {
|
|
Some(LiteralValue::String(value.clone()))
|
|
}
|
|
RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(LiteralValue::Int(*value)),
|
|
RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(LiteralValue::Float(*value)),
|
|
RuntimeValue::Concrete(ConcreteValue::Bool(value)) => Some(LiteralValue::Bool(*value)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bool {
|
|
matches!(
|
|
(value, primitive),
|
|
(
|
|
RuntimeValue::Concrete(ConcreteValue::String(_)),
|
|
PrimitiveType::String
|
|
) | (
|
|
RuntimeValue::Concrete(ConcreteValue::Int(_)),
|
|
PrimitiveType::Int
|
|
) | (
|
|
RuntimeValue::Concrete(ConcreteValue::Float(_)),
|
|
PrimitiveType::Float
|
|
) | (
|
|
RuntimeValue::Concrete(ConcreteValue::Bool(_)),
|
|
PrimitiveType::Bool
|
|
)
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "regex")]
|
|
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
|
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
|
|
return Err(Diagnostic::new(
|
|
DiagnosticKind::ConstraintViolation,
|
|
span,
|
|
"regex constraints require a string value",
|
|
));
|
|
};
|
|
let regex = regex::Regex::new(pattern).map_err(|error| {
|
|
Diagnostic::new(
|
|
DiagnosticKind::Syntax,
|
|
span,
|
|
format!("invalid regex constraint `{pattern}`: {error}"),
|
|
)
|
|
})?;
|
|
if regex.is_match(value) {
|
|
Ok(())
|
|
} else {
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::ConstraintViolation,
|
|
span,
|
|
"value does not satisfy regex constraint",
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "regex"))]
|
|
fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<()> {
|
|
Err(Diagnostic::new(
|
|
DiagnosticKind::UnsupportedFeature,
|
|
span,
|
|
"regex constraints require the regex feature",
|
|
))
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum ArithmeticOp {
|
|
Add,
|
|
Sub,
|
|
Mul,
|
|
Div,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Number {
|
|
Int(i64),
|
|
Float(f64),
|
|
}
|
|
|
|
fn negate_number(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
|
|
match value {
|
|
RuntimeValue::Concrete(ConcreteValue::Int(value)) => value
|
|
.checked_neg()
|
|
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
|
.ok_or_else(|| arithmetic_error(span, "integer negation overflow")),
|
|
RuntimeValue::Concrete(ConcreteValue::Float(value)) => {
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Float(-value)))
|
|
}
|
|
_ => Err(Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"unary '-' expects a numeric value",
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn arithmetic(
|
|
lhs: RuntimeValue,
|
|
rhs: RuntimeValue,
|
|
op: ArithmeticOp,
|
|
span: Span,
|
|
) -> Result<RuntimeValue> {
|
|
let lhs = number_from_runtime(lhs).ok_or_else(|| arithmetic_type_error(span))?;
|
|
let rhs = number_from_runtime(rhs).ok_or_else(|| arithmetic_type_error(span))?;
|
|
match (lhs, rhs) {
|
|
(Number::Int(lhs), Number::Int(rhs)) => arithmetic_int(lhs, rhs, op, span),
|
|
(lhs, rhs) => arithmetic_float(number_to_f64(lhs), number_to_f64(rhs), op, span),
|
|
}
|
|
}
|
|
|
|
fn arithmetic_int(lhs: i64, rhs: i64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
|
|
match op {
|
|
ArithmeticOp::Add => lhs
|
|
.checked_add(rhs)
|
|
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
|
.ok_or_else(|| arithmetic_error(span, "integer addition overflow")),
|
|
ArithmeticOp::Sub => lhs
|
|
.checked_sub(rhs)
|
|
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
|
.ok_or_else(|| arithmetic_error(span, "integer subtraction overflow")),
|
|
ArithmeticOp::Mul => lhs
|
|
.checked_mul(rhs)
|
|
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
|
.ok_or_else(|| arithmetic_error(span, "integer multiplication overflow")),
|
|
ArithmeticOp::Div => {
|
|
if rhs == 0 {
|
|
return Err(arithmetic_error(span, "division by zero"));
|
|
}
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Float(
|
|
lhs as f64 / rhs as f64,
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn arithmetic_float(lhs: f64, rhs: f64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
|
|
if matches!(op, ArithmeticOp::Div) && rhs == 0.0 {
|
|
return Err(arithmetic_error(span, "division by zero"));
|
|
}
|
|
let value = match op {
|
|
ArithmeticOp::Add => lhs + rhs,
|
|
ArithmeticOp::Sub => lhs - rhs,
|
|
ArithmeticOp::Mul => lhs * rhs,
|
|
ArithmeticOp::Div => lhs / rhs,
|
|
};
|
|
Ok(RuntimeValue::Concrete(ConcreteValue::Float(value)))
|
|
}
|
|
|
|
fn number_from_runtime(value: RuntimeValue) -> Option<Number> {
|
|
match value {
|
|
RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(Number::Int(value)),
|
|
RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(Number::Float(value)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn number_to_f64(value: Number) -> f64 {
|
|
match value {
|
|
Number::Int(value) => value as f64,
|
|
Number::Float(value) => value,
|
|
}
|
|
}
|
|
|
|
fn arithmetic_type_error(span: Span) -> Diagnostic {
|
|
Diagnostic::new(
|
|
DiagnosticKind::TypeMismatch,
|
|
span,
|
|
"arithmetic operators expect numeric values",
|
|
)
|
|
}
|
|
|
|
fn arithmetic_error(span: Span, message: &'static str) -> Diagnostic {
|
|
Diagnostic::new(DiagnosticKind::Conflict, span, message)
|
|
}
|
|
|
|
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
|
|
match (value, expected) {
|
|
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
|
compare_f64(*actual as f64, op, *expected as f64)
|
|
}
|
|
(RuntimeValue::Concrete(ConcreteValue::Float(actual)), LiteralValue::Float(expected)) => {
|
|
compare_f64(*actual, op, *expected)
|
|
}
|
|
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Float(expected)) => {
|
|
compare_f64(*actual as f64, op, *expected)
|
|
}
|
|
(RuntimeValue::Concrete(ConcreteValue::Float(actual)), LiteralValue::Int(expected)) => {
|
|
compare_f64(*actual, op, *expected as f64)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn compare_f64(actual: f64, op: CompareOp, expected: f64) -> bool {
|
|
match op {
|
|
CompareOp::Gt => actual > expected,
|
|
CompareOp::Gte => actual >= expected,
|
|
CompareOp::Lt => actual < expected,
|
|
CompareOp::Lte => actual <= expected,
|
|
CompareOp::Eq => actual == expected,
|
|
}
|
|
}
|
|
|
|
fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
|
|
match (lhs, rhs) {
|
|
(ConcreteValue::String(lhs), ConcreteValue::String(rhs)) => lhs == rhs,
|
|
(ConcreteValue::Int(lhs), ConcreteValue::Int(rhs)) => lhs == rhs,
|
|
(ConcreteValue::Float(lhs), ConcreteValue::Float(rhs)) => lhs == rhs,
|
|
(ConcreteValue::Bool(lhs), ConcreteValue::Bool(rhs)) => lhs == rhs,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn merge_default(
|
|
lhs: Option<ThunkId>,
|
|
rhs: Option<ThunkId>,
|
|
span: Span,
|
|
) -> Result<Option<ThunkId>> {
|
|
match (lhs, rhs) {
|
|
(None, None) => Ok(None),
|
|
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
|
|
(Some(lhs), Some(rhs)) if lhs == rhs => Ok(Some(lhs)),
|
|
(Some(_), Some(_)) => Err(Diagnostic::new(
|
|
DiagnosticKind::DefaultConflict,
|
|
span,
|
|
"conflicting defaults",
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{LoadedSource, parse_source};
|
|
|
|
fn eval_data(source: &str) -> Data {
|
|
let parsed = parse_source(source).unwrap();
|
|
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
|
let value = engine.eval_root().unwrap();
|
|
engine.materialize(&value).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn materializes_default() {
|
|
let data = eval_data("port = Int & >= 1 default 8080;");
|
|
assert!(matches!(data, Data::Object(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn evaluates_arithmetic_expressions() {
|
|
let data = eval_data(
|
|
r#"
|
|
{
|
|
sum = 1 + 2 * 3;
|
|
diff = 10 - 4;
|
|
product = (2 + 3) * 4;
|
|
quotient = 5 / 2;
|
|
negative = -3 + 1;
|
|
}
|
|
"#,
|
|
);
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].value, Data::Int(7));
|
|
assert_eq!(fields[1].value, Data::Int(6));
|
|
assert_eq!(fields[2].value, Data::Int(20));
|
|
assert_eq!(fields[3].value, Data::Float(2.5));
|
|
assert_eq!(fields[4].value, Data::Int(-2));
|
|
}
|
|
|
|
#[test]
|
|
fn arithmetic_can_feed_constraints() {
|
|
let data = eval_data("port = Int & > 4000 + 42 default 8080;");
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].value, Data::Int(8080));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_division_by_zero() {
|
|
let parsed = parse_source("1 / 0").unwrap();
|
|
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
|
assert!(engine.eval_root().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn composes_schema_and_value() {
|
|
let data = eval_data(
|
|
r#"
|
|
let
|
|
MyConfig = { port = Int & > 443 default 8080; };
|
|
in
|
|
MyConfig & { port = 8000; }
|
|
"#,
|
|
);
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].name, "port");
|
|
assert_eq!(fields[0].value, Data::Int(8000));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_constraint_violation() {
|
|
let parsed = parse_source("{ port = Int & > 443; } & { port = 80; }").unwrap();
|
|
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
|
assert!(engine.eval_root().is_err());
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MapLoader {
|
|
sources: Vec<(String, String)>,
|
|
}
|
|
|
|
impl SourceLoader for MapLoader {
|
|
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedSource> {
|
|
let source = self
|
|
.sources
|
|
.iter()
|
|
.find(|(key, _)| key == specifier)
|
|
.map(|(_, source)| source.clone())
|
|
.ok_or_else(|| {
|
|
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
|
|
})?;
|
|
Ok(LoadedSource {
|
|
key: specifier.into(),
|
|
name: specifier.into(),
|
|
source,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn imports_module_on_demand() {
|
|
let mut engine = Engine::new(MapLoader {
|
|
sources: vec![(
|
|
String::from("dep"),
|
|
String::from("schema = { port = Int default 8080; }"),
|
|
)],
|
|
});
|
|
let module = engine
|
|
.add_root_source(
|
|
"main",
|
|
"main",
|
|
r#"(import "dep").schema & { port = 9000; }"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].value, Data::Int(9000));
|
|
}
|
|
|
|
#[test]
|
|
fn top_level_fields_are_recursive_module_scope() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
let module = engine
|
|
.add_root_source(
|
|
"main",
|
|
"main",
|
|
"schema = { port = Int default 8080; }; result = schema;",
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[1].name, "result");
|
|
assert!(matches!(fields[1].value, Data::Object(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn host_prelude_provides_abstract_object() {
|
|
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).unwrap(),
|
|
),
|
|
(
|
|
"enabled",
|
|
HostValue::bool_type().default_bool(true).unwrap(),
|
|
),
|
|
]),
|
|
)
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"main",
|
|
"main",
|
|
r#"Service & { name = "api"; port = 9443; }"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].value, Data::String(String::from("api")));
|
|
assert_eq!(fields[1].value, Data::Int(9443));
|
|
assert_eq!(fields[2].value, Data::Bool(true));
|
|
}
|
|
|
|
#[test]
|
|
fn module_binding_shadows_host_prelude() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine.bind_global("Service", HostValue::int(1)).unwrap();
|
|
let module = engine
|
|
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[1].value, Data::String(String::from("local")));
|
|
}
|
|
|
|
#[cfg(feature = "regex")]
|
|
#[test]
|
|
fn regex_constraint_matches_concrete_string() {
|
|
let data = eval_data(r#"value = String & /^api-[0-9]+$/ default "api-123";"#);
|
|
let Data::Object(fields) = data else { panic!() };
|
|
assert_eq!(fields[0].value, Data::String(String::from("api-123")));
|
|
}
|
|
|
|
#[cfg(feature = "regex")]
|
|
#[test]
|
|
fn regex_constraint_rejects_non_matching_string() {
|
|
let parsed =
|
|
crate::parse_source(r#"value = String & /^api-[0-9]+$/ default "web-123";"#).unwrap();
|
|
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
|
let value = engine.eval_root().unwrap();
|
|
assert!(engine.materialize(&value).is_err());
|
|
}
|
|
}
|