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 { loader: L, modules: Vec, thunks: Vec, envs: Vec, } impl Engine { pub fn from_parse(ast: Ast, root: ExprId) -> Self { let mut this = Self::new(EmptyLoader); this.register_parsed( String::from(""), String::from(""), SourceId(0), ast, root, SourceForm::Expr, ); this } } impl Engine { 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, name: impl Into, source: &str, ) -> Result { self.add_source(key.into(), name.into(), source) } pub fn eval_module(&mut self, module: ModuleId) -> Result { let thunk = self.modules[module.0 as usize].root_thunk; self.force(thunk) } pub fn eval_root(&mut self) -> Result { self.eval_module(ModuleId(0)) } pub fn materialize(&mut self, value: &RuntimeValue) -> Result { 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 { 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 { self.modules .iter() .position(|module| module.key == key) .map(|index| ModuleId(index as u32)) } fn load_import(&mut self, current: ModuleId, specifier: &str) -> Result { 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 { 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 { 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 { 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, caller_module: ModuleId, caller_env: EnvId, span: Span, ) -> Result { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, rhs: Option, span: Span, ) -> Result> { 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 { 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(_))); } }