Implement evaluator materializer and CLI
This commit is contained in:
parent
ddcee75c8d
commit
c33c484bac
|
|
@ -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<String, Diagnostic> {
|
||||
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 <file.dcdl> Materialize a DCDL file");
|
||||
println!(" decodal eval <file.dcdl> Materialize a DCDL file");
|
||||
println!(" decodal check <file.dcdl> 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!("\"");
|
||||
}
|
||||
|
|
|
|||
772
crates/decodal-core/src/eval.rs
Normal file
772
crates/decodal-core/src/eval.rs
Normal file
|
|
@ -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<Thunk>,
|
||||
envs: Vec<Env>,
|
||||
}
|
||||
|
||||
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<RuntimeValue> {
|
||||
self.eval_expr(self.root, EnvId(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 eval_expr(&mut self, id: ExprId, env: EnvId) -> Result<RuntimeValue> {
|
||||
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<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 eval_object(&mut self, fields: &[Field], env: EnvId) -> Result<RuntimeValue> {
|
||||
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<ExprId>,
|
||||
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_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<bool> {
|
||||
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<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.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<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 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::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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
137
crates/decodal-core/src/runtime.rs
Normal file
137
crates/decodal-core/src/runtime.rs
Normal file
|
|
@ -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<ThunkId>),
|
||||
Object(ObjectValue),
|
||||
Function(FunctionValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectValue {
|
||||
pub fields: Vec<ObjectField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectField {
|
||||
pub name: String,
|
||||
pub value: ThunkId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FunctionValue {
|
||||
pub params: Vec<FunctionParam>,
|
||||
pub body: ExprId,
|
||||
pub env: EnvId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FunctionParam {
|
||||
pub name: String,
|
||||
pub constraint: Option<ExprId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AbstractValue {
|
||||
pub constraints: Vec<Constraint>,
|
||||
pub default: Option<ThunkId>,
|
||||
}
|
||||
|
||||
#[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<Data>),
|
||||
Object(Vec<DataField>),
|
||||
}
|
||||
|
||||
#[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<EnvId>,
|
||||
pub bindings: Vec<Binding>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Binding {
|
||||
pub name: String,
|
||||
pub value: ThunkId,
|
||||
}
|
||||
|
|
@ -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) =>
|
||||
{
|
||||
# ...
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
- 演算子の優先順位。
|
||||
- `rec` の扱い。
|
||||
- コメント構文を `#` のみにするか。
|
||||
- パス import と文字列 import の扱い分け。
|
||||
|
||||
## 型・制約
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user