Add host prelude embedding API
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
use alloc::{boxed::Box, string::String, vec::Vec};
|
||||
|
||||
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
|
||||
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum HostValue {
|
||||
String(String),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<HostValue>),
|
||||
Object(Vec<HostField>),
|
||||
Abstract {
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HostField {
|
||||
pub name: String,
|
||||
pub value: HostValue,
|
||||
}
|
||||
|
||||
impl HostValue {
|
||||
pub fn string(value: impl Into<String>) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
|
||||
pub fn int(value: i64) -> Self {
|
||||
Self::Int(value)
|
||||
}
|
||||
|
||||
pub fn float(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
|
||||
pub fn bool(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
|
||||
pub fn array<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = HostValue>,
|
||||
{
|
||||
Self::Array(items.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn object<I, N>(fields: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, HostValue)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Self::Object(
|
||||
fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| HostField {
|
||||
name: name.into(),
|
||||
value,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn string_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||
}
|
||||
|
||||
pub fn int_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||
}
|
||||
|
||||
pub fn float_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||
}
|
||||
|
||||
pub fn bool_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||
}
|
||||
|
||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
}
|
||||
|
||||
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
|
||||
Self::Abstract {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
||||
match &mut self {
|
||||
Self::Abstract { constraints, .. } => constraints.push(constraint),
|
||||
_ => {
|
||||
self = Self::Abstract {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: Some(Box::new(self)),
|
||||
};
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gt(self, value: i64) -> Self {
|
||||
self.with_constraint(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(value)))
|
||||
}
|
||||
|
||||
pub fn gte(self, value: i64) -> Self {
|
||||
self.with_constraint(Constraint::Compare(
|
||||
CompareOp::Gte,
|
||||
LiteralValue::Int(value),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn lt(self, value: i64) -> Self {
|
||||
self.with_constraint(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(value)))
|
||||
}
|
||||
|
||||
pub fn lte(self, value: i64) -> Self {
|
||||
self.with_constraint(Constraint::Compare(
|
||||
CompareOp::Lte,
|
||||
LiteralValue::Int(value),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn default(self, value: HostValue) -> Result<Self> {
|
||||
match self {
|
||||
Self::Abstract {
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::Abstract {
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::Abstract { .. } => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
)),
|
||||
concrete => Ok(Self::Abstract {
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(concrete)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_string(self, value: impl Into<String>) -> Result<Self> {
|
||||
self.default(Self::string(value))
|
||||
}
|
||||
|
||||
pub fn default_int(self, value: i64) -> Result<Self> {
|
||||
self.default(Self::int(value))
|
||||
}
|
||||
|
||||
pub fn default_float(self, value: f64) -> Result<Self> {
|
||||
self.default(Self::float(value))
|
||||
}
|
||||
|
||||
pub fn default_bool(self, value: bool) -> Result<Self> {
|
||||
self.default(Self::bool(value))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
ExprId, SourceForm, SourceId, Span,
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal},
|
||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||
embedding::HostValue,
|
||||
module::{EmptyLoader, LoadedSource, Module, SourceLoader},
|
||||
parse_source_with_source_id,
|
||||
runtime::{
|
||||
@@ -15,6 +16,7 @@ use crate::{
|
||||
|
||||
pub struct Engine<L = EmptyLoader> {
|
||||
loader: L,
|
||||
prelude_env: EnvId,
|
||||
modules: Vec<Module>,
|
||||
thunks: Vec<Thunk>,
|
||||
envs: Vec<Env>,
|
||||
@@ -39,12 +41,29 @@ impl<L: SourceLoader> Engine<L> {
|
||||
pub fn new(loader: L) -> Self {
|
||||
Self {
|
||||
loader,
|
||||
prelude_env: EnvId(0),
|
||||
modules: Vec::new(),
|
||||
thunks: Vec::new(),
|
||||
envs: Vec::new(),
|
||||
envs: vec![Env {
|
||||
parent: None,
|
||||
bindings: Vec::new(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
|
||||
let value = self.internalize_host_value(value)?;
|
||||
let thunk = self.add_value_thunk(value);
|
||||
self.bind(self.prelude_env, name.into(), thunk);
|
||||
Ok(thunk)
|
||||
}
|
||||
|
||||
pub fn bind_global_runtime(&mut self, name: impl Into<String>, value: RuntimeValue) -> ThunkId {
|
||||
let thunk = self.add_value_thunk(value);
|
||||
self.bind(self.prelude_env, name.into(), thunk);
|
||||
thunk
|
||||
}
|
||||
|
||||
pub fn add_root_source(
|
||||
&mut self,
|
||||
key: impl Into<String>,
|
||||
@@ -136,7 +155,7 @@ impl<L: SourceLoader> Engine<L> {
|
||||
source_form: SourceForm,
|
||||
) -> ModuleId {
|
||||
let module = ModuleId(self.modules.len() as u32);
|
||||
let root_env = self.new_env(None);
|
||||
let root_env = self.new_env(Some(self.prelude_env));
|
||||
let root_thunk = if source_form == SourceForm::Fields {
|
||||
if let Expr::Object(fields) = ast.get(root).expr.clone() {
|
||||
let object = self
|
||||
@@ -699,6 +718,61 @@ impl<L: SourceLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
||||
match value {
|
||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||
HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
|
||||
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||
HostValue::Array(items) => {
|
||||
let mut thunks = Vec::new();
|
||||
for item in items {
|
||||
let value = self.internalize_host_value(item)?;
|
||||
thunks.push(self.add_value_thunk(value));
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
||||
}
|
||||
HostValue::Object(fields) => {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
for field in fields {
|
||||
if object
|
||||
.fields
|
||||
.iter()
|
||||
.any(|existing| existing.name == field.name)
|
||||
{
|
||||
return Err(Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
Span::default(),
|
||||
format!("duplicate host object field `{}`", field.name),
|
||||
));
|
||||
}
|
||||
let value = self.internalize_host_value(field.value)?;
|
||||
let value = self.add_value_thunk(value);
|
||||
object.fields.push(ObjectField {
|
||||
name: field.name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
||||
}
|
||||
HostValue::Abstract {
|
||||
constraints,
|
||||
default,
|
||||
} => {
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value(*default)?;
|
||||
Some(self.add_value_thunk(value))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints,
|
||||
default,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn force(&mut self, id: ThunkId) -> Result<RuntimeValue> {
|
||||
let index = id.0 as usize;
|
||||
match self.thunks[index].state.clone() {
|
||||
@@ -1045,4 +1119,51 @@ mod tests {
|
||||
assert_eq!(fields[1].name, "result");
|
||||
assert!(matches!(fields[1].value, Data::Object(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_prelude_provides_abstract_object() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global(
|
||||
"Service",
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
(
|
||||
"port",
|
||||
HostValue::int_type().gt(443).default_int(8443).unwrap(),
|
||||
),
|
||||
(
|
||||
"enabled",
|
||||
HostValue::bool_type().default_bool(true).unwrap(),
|
||||
),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"main",
|
||||
"main",
|
||||
r#"Service & { name = "api"; port = 9443; }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields[0].value, Data::String(String::from("api")));
|
||||
assert_eq!(fields[1].value, Data::Int(9443));
|
||||
assert_eq!(fields[2].value, Data::Bool(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_binding_shadows_host_prelude() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine.bind_global("Service", HostValue::int(1)).unwrap();
|
||||
let module = engine
|
||||
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields[1].value, Data::String(String::from("local")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ extern crate alloc;
|
||||
|
||||
pub mod ast;
|
||||
pub mod diagnostic;
|
||||
pub mod embedding;
|
||||
pub mod eval;
|
||||
pub mod lexer;
|
||||
pub mod module;
|
||||
@@ -13,11 +14,12 @@ pub mod span;
|
||||
|
||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||
pub use embedding::{HostField, HostValue};
|
||||
pub use eval::Engine;
|
||||
pub use lexer::{Lexer, Token, TokenKind};
|
||||
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||
pub use runtime::{Data, ExprRef, ModuleId, RuntimeValue};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||
pub use span::{SourceId, Span};
|
||||
|
||||
pub fn version() -> &'static str {
|
||||
|
||||
Reference in New Issue
Block a user