From c33c484bac3e53fc561fc3eee18f961c8eb949f4 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 16 Jun 2026 02:10:54 +0900 Subject: [PATCH] Implement evaluator materializer and CLI --- crates/decodal-cli/src/main.rs | 164 +++- crates/decodal-core/src/eval.rs | 772 ++++++++++++++++++ crates/decodal-core/src/lib.rs | 4 + crates/decodal-core/src/runtime.rs | 137 ++++ .../souce/language/modules-and-imports.md | 15 +- doc/manual/souce/open-issues.md | 1 - 6 files changed, 1082 insertions(+), 11 deletions(-) create mode 100644 crates/decodal-core/src/eval.rs create mode 100644 crates/decodal-core/src/runtime.rs diff --git a/crates/decodal-cli/src/main.rs b/crates/decodal-cli/src/main.rs index 31e1e0e..559d13b 100644 --- a/crates/decodal-cli/src/main.rs +++ b/crates/decodal-cli/src/main.rs @@ -1,3 +1,163 @@ -fn main() { - println!("Decodal {}", decodal_core::version()); +use std::{env, fs, process::ExitCode}; + +use decodal_core::{Data, Diagnostic, Engine, parse_source}; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + print_diagnostic(&error); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), Diagnostic> { + let mut args = env::args().skip(1); + let first = args.next().unwrap_or_else(|| String::from("--help")); + match first.as_str() { + "--help" | "-h" => { + print_help(); + Ok(()) + } + "--version" | "-V" => { + println!("Decodal {}", decodal_core::version()); + Ok(()) + } + "check" => { + let path = args.next().unwrap_or_else(|| String::from("-")); + let source = read_source(&path)?; + let parsed = parse_source(&source)?; + let mut engine = Engine::from_parse(parsed.ast, parsed.root); + let value = engine.eval_root()?; + engine.materialize(&value)?; + println!("ok"); + Ok(()) + } + "eval" | "materialize" => { + let path = args.next().unwrap_or_else(|| String::from("-")); + materialize_file(&path) + } + path => materialize_file(path), + } +} + +fn materialize_file(path: &str) -> Result<(), Diagnostic> { + let source = read_source(path)?; + let parsed = parse_source(&source)?; + let mut engine = Engine::from_parse(parsed.ast, parsed.root); + let value = engine.eval_root()?; + let data = engine.materialize(&value)?; + print_data(&data, 0); + println!(); + Ok(()) +} + +fn read_source(path: &str) -> Result { + if path == "-" { + use std::io::Read; + let mut source = String::new(); + std::io::stdin() + .read_to_string(&mut source) + .map_err(|error| { + Diagnostic::new( + decodal_core::DiagnosticKind::Import, + decodal_core::Span::default(), + format!("failed to read stdin: {error}"), + ) + })?; + Ok(source) + } else { + fs::read_to_string(path).map_err(|error| { + Diagnostic::new( + decodal_core::DiagnosticKind::Import, + decodal_core::Span::default(), + format!("failed to read `{path}`: {error}"), + ) + }) + } +} + +fn print_help() { + println!("Decodal - Deferred Constraint Data Language"); + println!(); + println!("Usage:"); + println!(" decodal Materialize a DCDL file"); + println!(" decodal eval Materialize a DCDL file"); + println!(" decodal check Evaluate and materialize without printing data"); + println!(" decodal - Read DCDL source from stdin"); +} + +fn print_diagnostic(error: &Diagnostic) { + eprintln!( + "error[{kind:?}] {start}..{end}: {message}", + kind = error.kind, + start = error.span.start, + end = error.span.end, + message = error.message, + ); +} + +fn print_data(data: &Data, indent: usize) { + match data { + Data::String(value) => print_string(value), + Data::Int(value) => print!("{value}"), + Data::Float(value) => print!("{value}"), + Data::Bool(value) => print!("{value}"), + Data::Array(items) => { + print!("["); + if !items.is_empty() { + println!(); + for (index, item) in items.iter().enumerate() { + print_indent(indent + 2); + print_data(item, indent + 2); + if index + 1 != items.len() { + print!(","); + } + println!(); + } + print_indent(indent); + } + print!("]"); + } + Data::Object(fields) => { + print!("{{"); + if !fields.is_empty() { + println!(); + for (index, field) in fields.iter().enumerate() { + print_indent(indent + 2); + print_string(&field.name); + print!(": "); + print_data(&field.value, indent + 2); + if index + 1 != fields.len() { + print!(","); + } + println!(); + } + print_indent(indent); + } + print!("}}"); + } + } +} + +fn print_indent(indent: usize) { + for _ in 0..indent { + print!(" "); + } +} + +fn print_string(value: &str) { + print!("\""); + for ch in value.chars() { + match ch { + '"' => print!("\\\""), + '\\' => print!("\\\\"), + '\n' => print!("\\n"), + '\r' => print!("\\r"), + '\t' => print!("\\t"), + ch => print!("{ch}"), + } + } + print!("\""); } diff --git a/crates/decodal-core/src/eval.rs b/crates/decodal-core/src/eval.rs new file mode 100644 index 0000000..ed27849 --- /dev/null +++ b/crates/decodal-core/src/eval.rs @@ -0,0 +1,772 @@ +use alloc::{format, string::String, vec, vec::Vec}; + +use crate::{ + Span, + ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal}, + diagnostic::{Diagnostic, DiagnosticKind, Result}, + runtime::{ + AbstractValue, Binding, ConcreteValue, Constraint, Data, DataField, Env, EnvId, + FunctionParam, FunctionValue, LiteralValue, ObjectField, ObjectValue, PrimitiveType, + RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState, + }, +}; + +pub struct Engine { + ast: Ast, + root: ExprId, + thunks: Vec, + envs: Vec, +} + +impl Engine { + pub fn from_parse(ast: Ast, root: ExprId) -> Self { + let mut this = Self { + ast, + root, + thunks: Vec::new(), + envs: Vec::new(), + }; + this.envs.push(Env { + parent: None, + bindings: Vec::new(), + }); + this + } + + pub fn eval_root(&mut self) -> Result { + self.eval_expr(self.root, EnvId(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 eval_expr(&mut self, id: ExprId, env: EnvId) -> Result { + let span = self.ast.span(id); + let expr = self.ast.get(id).expr.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.eval_object(&fields, env), + Expr::Array(items) => { + let thunks = items + .into_iter() + .map(|item| self.add_expr_thunk(item, env)) + .collect(); + Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks))) + } + Expr::Let { bindings, body } => { + let let_env = self.new_child_env(env); + for binding in bindings { + let name = field_name(&binding)?; + let thunk = self.add_expr_thunk(binding.value, let_env); + self.bind(let_env, name, thunk); + } + self.eval_expr(body, let_env) + } + Expr::Import(_) => Err(Diagnostic::new( + DiagnosticKind::UnsupportedFeature, + span, + "import evaluation is not implemented yet", + )), + Expr::Path { base, field } => { + let base = self.eval_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(callee, args, env, span), + Expr::Function { params, body } => { + let params = params + .into_iter() + .map(|param| FunctionParam { + name: param.name, + constraint: param.constraint, + }) + .collect(); + Ok(RuntimeValue::Concrete(ConcreteValue::Function( + FunctionValue { params, body, env }, + ))) + } + Expr::Match { scrutinee, arms } => { + let value = self.eval_expr(scrutinee, env)?; + for arm in arms { + if self.matches_pattern(&value, arm.pattern, env)? { + return self.eval_expr(arm.body, env); + } + } + Err(Diagnostic::new( + DiagnosticKind::MatchFailure, + span, + "no match arm matched", + )) + } + Expr::Binary { op, lhs, rhs } => { + let lhs = self.eval_expr(lhs, env)?; + let rhs = self.eval_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(base, env)?; + match base { + RuntimeValue::Abstract(mut abstract_value) => { + abstract_value.default = Some(self.add_expr_thunk(fallback, env)); + Ok(RuntimeValue::Abstract(abstract_value)) + } + concrete @ RuntimeValue::Concrete(_) => Ok(concrete), + } + } + Expr::CompareConstraint { op, value } => { + let value = self.eval_expr(value, 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 eval_object(&mut self, fields: &[Field], env: EnvId) -> Result { + let mut object = ObjectValue { fields: Vec::new() }; + for field in fields { + self.insert_field(&mut object, &field.path, field.value, env, field.span)?; + } + Ok(RuntimeValue::Concrete(ConcreteValue::Object(object))) + } + + fn insert_field( + &mut self, + object: &mut ObjectValue, + 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(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, &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, &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: ExprId, + args: Vec, + 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_child_env(function.env); + for (param, arg) in function.params.iter().zip(args) { + let arg_thunk = if let Some(constraint) = param.constraint { + self.add_constrained_thunk(constraint, function.env, arg, caller_env) + } else { + self.add_expr_thunk(arg, 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: ExprId, + env: EnvId, + ) -> Result { + match self.ast.get(pattern).expr.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.ast.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.ast.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: ExprId, env: EnvId) -> ThunkId { + self.add_thunk(ThunkKind::Expr { expr, env }) + } + + fn add_constrained_thunk( + &mut self, + constraint: ExprId, + constraint_env: EnvId, + value: ExprId, + 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_child_env(&mut self, parent: EnvId) -> EnvId { + let id = EnvId(self.envs.len() as u32); + self.envs.push(Env { + parent: Some(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 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::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()); + } +} diff --git a/crates/decodal-core/src/lib.rs b/crates/decodal-core/src/lib.rs index ef37d57..0330b36 100644 --- a/crates/decodal-core/src/lib.rs +++ b/crates/decodal-core/src/lib.rs @@ -4,14 +4,18 @@ extern crate alloc; pub mod ast; pub mod diagnostic; +pub mod eval; pub mod lexer; pub mod parser; +pub mod runtime; pub mod span; pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param}; pub use diagnostic::{Diagnostic, DiagnosticKind, Result}; +pub use eval::Engine; pub use lexer::{Lexer, Token, TokenKind}; pub use parser::{ParseOutput, Parser, parse_source}; +pub use runtime::{Data, RuntimeValue}; pub use span::Span; pub fn version() -> &'static str { diff --git a/crates/decodal-core/src/runtime.rs b/crates/decodal-core/src/runtime.rs new file mode 100644 index 0000000..cccf274 --- /dev/null +++ b/crates/decodal-core/src/runtime.rs @@ -0,0 +1,137 @@ +use alloc::{string::String, vec::Vec}; + +use crate::{ExprId, ast::CompareOp}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RuntimeValue { + Concrete(ConcreteValue), + Abstract(AbstractValue), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ConcreteValue { + String(String), + Int(i64), + Float(f64), + Bool(bool), + Array(Vec), + Object(ObjectValue), + Function(FunctionValue), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ObjectValue { + pub fields: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ObjectField { + pub name: String, + pub value: ThunkId, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FunctionValue { + pub params: Vec, + pub body: ExprId, + pub env: EnvId, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FunctionParam { + pub name: String, + pub constraint: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AbstractValue { + pub constraints: Vec, + pub default: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Constraint { + Type(PrimitiveType), + Compare(CompareOp, LiteralValue), + Regex(String), + BuiltinPredicate(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrimitiveType { + String, + Int, + Float, + Bool, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum LiteralValue { + String(String), + Int(i64), + Float(f64), + Bool(bool), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Data { + String(String), + Int(i64), + Float(f64), + Bool(bool), + Array(Vec), + Object(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DataField { + pub name: String, + pub value: Data, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ThunkId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvId(pub u32); + +#[derive(Debug, Clone)] +pub struct Thunk { + pub kind: ThunkKind, + pub state: ThunkState, +} + +#[derive(Debug, Clone)] +pub enum ThunkKind { + Expr { + expr: ExprId, + env: EnvId, + }, + Constrained { + constraint: ExprId, + constraint_env: EnvId, + value: ExprId, + value_env: EnvId, + }, + Value(RuntimeValue), +} + +#[derive(Debug, Clone)] +pub enum ThunkState { + Unevaluated, + Evaluating, + Evaluated(RuntimeValue), + Error, +} + +#[derive(Debug, Clone)] +pub struct Env { + pub parent: Option, + pub bindings: Vec, +} + +#[derive(Debug, Clone)] +pub struct Binding { + pub name: String, + pub value: ThunkId, +} diff --git a/doc/manual/souce/language/modules-and-imports.md b/doc/manual/souce/language/modules-and-imports.md index fd4e67b..79935eb 100644 --- a/doc/manual/souce/language/modules-and-imports.md +++ b/doc/manual/souce/language/modules-and-imports.md @@ -5,12 +5,11 @@ ## 構文 ```dcdl -import ./config.n -import "./config.n" +import "./config.dcdl" ``` -パス表記の詳細は未確定である。 -パスリテラルと文字列リテラルの両方を許可するか、どちらかに統一するかは今後決める。 +import specifier は文字列リテラルとする。 +パスリテラル構文は採用しない。 ## モジュール @@ -25,19 +24,19 @@ import 先はモジュール単位で読み込まれる。 例: ```dcdl -# main.n +# main.dcdl { schema = { hoge = String; }; - result = (import ./func.n)(schema); + result = (import "./func.dcdl")(schema); } ``` ```dcdl -# func.n -(input: (import ./main.n).schema) => +# func.dcdl +(input: (import "./main.dcdl").schema) => { # ... } diff --git a/doc/manual/souce/open-issues.md b/doc/manual/souce/open-issues.md index b983d5a..eb32ce2 100644 --- a/doc/manual/souce/open-issues.md +++ b/doc/manual/souce/open-issues.md @@ -9,7 +9,6 @@ - 演算子の優先順位。 - `rec` の扱い。 - コメント構文を `#` のみにするか。 -- パス import と文字列 import の扱い分け。 ## 型・制約