Decodal/crates/decodal-core/src/eval.rs

1049 lines
36 KiB
Rust

use alloc::{format, string::String, vec, vec::Vec};
use crate::{
ExprId, SourceForm, SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal},
diagnostic::{Diagnostic, DiagnosticKind, Result},
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,
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,
modules: Vec::new(),
thunks: Vec::new(),
envs: Vec::new(),
}
}
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(None);
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(&current_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::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::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.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(_) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
"regex constraints require a future regex feature",
)),
Constraint::BuiltinPredicate(name) => Err(Diagnostic::new(
DiagnosticKind::UnsupportedFeature,
span,
format!("builtin predicate `{name}` is not implemented"),
)),
}
}
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
)
)
}
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 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(_)));
}
}