Add host prelude embedding API
This commit is contained in:
parent
6316939438
commit
e23b31da46
38
crates/decodal-core/examples/host_prelude.rs
Normal file
38
crates/decodal-core/examples/host_prelude.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use decodal_core::{Data, EmptyLoader, Engine, HostValue};
|
||||
|
||||
fn main() -> decodal_core::Result<()> {
|
||||
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)?),
|
||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
|
||||
let module = engine.add_root_source(
|
||||
"embedded-main",
|
||||
"embedded-main",
|
||||
r#"
|
||||
Service & {
|
||||
name = "api";
|
||||
port = 9443;
|
||||
}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let value = engine.eval_module(module)?;
|
||||
let data = engine.materialize(&value)?;
|
||||
|
||||
if let Data::Object(fields) = data {
|
||||
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));
|
||||
} else {
|
||||
panic!("expected object");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
164
crates/decodal-core/src/embedding.rs
Normal file
164
crates/decodal-core/src/embedding.rs
Normal file
|
|
@ -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 {
|
||||
|
|
|
|||
97
doc/manual/souce/design/embedding-api.md
Normal file
97
doc/manual/souce/design/embedding-api.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Embedding API
|
||||
|
||||
Decodal core can be embedded without giving the core crate access to a filesystem.
|
||||
The host supplies imported sources through `SourceLoader` and may also provide global bindings through the host prelude API.
|
||||
|
||||
## Host prelude
|
||||
|
||||
`Engine` owns a prelude environment.
|
||||
Bindings in this environment are visible from every module loaded by the engine.
|
||||
|
||||
```text
|
||||
prelude env
|
||||
↓
|
||||
module root env
|
||||
↓
|
||||
let / function env
|
||||
```
|
||||
|
||||
Module top-level bindings shadow prelude bindings.
|
||||
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
||||
|
||||
## Global bindings
|
||||
|
||||
The host can bind values before adding or evaluating user sources.
|
||||
|
||||
```rust
|
||||
use decodal_core::{EmptyLoader, Engine, HostValue};
|
||||
|
||||
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)?),
|
||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
```
|
||||
|
||||
A user source can then refer to `Service` without importing it.
|
||||
|
||||
```dcdl
|
||||
Service & {
|
||||
name = "api";
|
||||
port = 9443;
|
||||
}
|
||||
```
|
||||
|
||||
## HostValue
|
||||
|
||||
`HostValue` is the public builder-facing value representation for embedding.
|
||||
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
|
||||
|
||||
```text
|
||||
HostValue =
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
Array(Vec<HostValue>)
|
||||
Object(Vec<HostField>)
|
||||
Abstract { constraints, default }
|
||||
```
|
||||
|
||||
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
|
||||
|
||||
## Abstract host objects
|
||||
|
||||
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
|
||||
|
||||
```rust
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
||||
])
|
||||
```
|
||||
|
||||
Conceptually this becomes:
|
||||
|
||||
```text
|
||||
Concrete(Object {
|
||||
name -> Thunk(Abstract { constraints: [String], default: none })
|
||||
port -> Thunk(Abstract { constraints: [Int, > 443], default: 8443 })
|
||||
})
|
||||
```
|
||||
|
||||
This matches the runtime model used for Decodal source-defined schema objects.
|
||||
|
||||
## SourceLoader and prelude together
|
||||
|
||||
`SourceLoader` and host prelude bindings are independent mechanisms.
|
||||
|
||||
- Use `SourceLoader` when user sources should explicitly import host-provided modules.
|
||||
- Use prelude bindings when host-provided schemas or constants should be globally available.
|
||||
|
||||
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.
|
||||
|
|
@ -15,3 +15,4 @@ bytecode VM や JIT ではなく、AST を demand-driven に評価すること
|
|||
3. [Thunk and Lazy Evaluation](./thunk-and-lazy-evaluation.md)
|
||||
4. [Composition and Materialization](./composition-and-materialization.md)
|
||||
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
|
||||
6. [Embedding API](./embedding-api.md)
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
|
||||
4. [Composition and Materialization](./design/composition-and-materialization.md)
|
||||
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
||||
6. [Embedding API](./design/embedding-api.md)
|
||||
4. [Open Issues](./open-issues.md)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user