Add host-structured imports

This commit is contained in:
2026-08-11 18:33:49 +09:00
parent 46b2b3b1b6
commit 165daada23
12 changed files with 402 additions and 46 deletions
+163 -13
View File
@@ -6,7 +6,7 @@ use crate::{
constraints::normalize_constraints,
diagnostic::{Diagnostic, DiagnosticKind, Result},
embedding::HostValue,
module::{EmptyLoader, LoadedSource, Module, SourceLoader},
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
parse_source_with_source_id,
runtime::{
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
@@ -19,10 +19,16 @@ pub struct Engine<L = EmptyLoader> {
loader: L,
prelude_env: EnvId,
modules: Vec<Module>,
imported_values: Vec<ImportedValue>,
thunks: Vec<Thunk>,
envs: Vec<Env>,
}
struct ImportedValue {
key: String,
root: ThunkId,
}
impl Engine<EmptyLoader> {
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
let mut this = Self::new(EmptyLoader);
@@ -38,12 +44,13 @@ impl Engine<EmptyLoader> {
}
}
impl<L: SourceLoader> Engine<L> {
impl<L: ImportLoader> Engine<L> {
pub fn new(loader: L) -> Self {
Self {
loader,
prelude_env: EnvId(0),
modules: Vec::new(),
imported_values: Vec::new(),
thunks: Vec::new(),
envs: vec![Env {
parent: None,
@@ -236,10 +243,37 @@ impl<L: SourceLoader> Engine<L> {
.map(|index| ModuleId(index as u32))
}
fn load_import(&mut self, current: ModuleId, specifier: &str) -> Result<ModuleId> {
fn eval_import(&mut self, current: ModuleId, specifier: &str) -> Result<RuntimeValue> {
let current_key = self.modules[current.0 as usize].key.clone();
let LoadedSource { key, name, source } = self.loader.load(Some(&current_key), specifier)?;
self.add_source(key, name, &source)
let loaded = self.loader.load(Some(&current_key), specifier)?;
match loaded {
LoadedImport::Source(LoadedSource { key, name, source }) => {
if let Some(value) = self.find_imported_value(&key) {
return self.force(value);
}
let module = self.add_source(key, name, &source)?;
self.eval_module(module)
}
LoadedImport::Value(LoadedValue { key, value }) => {
if let Some(module) = self.find_module(&key) {
return self.eval_module(module);
}
if let Some(value) = self.find_imported_value(&key) {
return self.force(value);
}
let value = self.internalize_host_value(value)?;
let root = self.add_value_thunk(value);
self.imported_values.push(ImportedValue { key, root });
self.force(root)
}
}
}
fn find_imported_value(&self, key: &str) -> Option<ThunkId> {
self.imported_values
.iter()
.find(|value| value.key == key)
.map(|value| value.root)
}
fn eval_expr(&mut self, reference: ExprRef, env: EnvId) -> Result<RuntimeValue> {
@@ -303,10 +337,7 @@ impl<L: SourceLoader> Engine<L> {
let_env,
)
}
Expr::Import(specifier) => {
let module = self.load_import(reference.module, &specifier)?;
self.eval_module(module)
}
Expr::Import(specifier) => self.eval_import(reference.module, &specifier),
Expr::Path { base, field } => {
let base = self.eval_expr(
ExprRef {
@@ -1964,8 +1995,8 @@ mod tests {
sources: Vec<(String, String)>,
}
impl SourceLoader for MapLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedSource> {
impl ImportLoader for MapLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedImport> {
let source = self
.sources
.iter()
@@ -1974,11 +2005,11 @@ mod tests {
.ok_or_else(|| {
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
})?;
Ok(LoadedSource {
Ok(LoadedImport::Source(LoadedSource {
key: specifier.into(),
name: specifier.into(),
source,
})
}))
}
}
@@ -2003,6 +2034,125 @@ mod tests {
assert_eq!(fields[0].value, Data::Int(9000));
}
struct MarkdownLoader {
source: String,
}
impl ImportLoader for MarkdownLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedImport> {
if specifier != "./post.md" {
return Err(Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
format!("unknown content import `{specifier}`"),
));
}
let value = parse_test_markdown(&self.source)?;
Ok(LoadedImport::value("content/post.md", value))
}
}
fn parse_test_markdown(source: &str) -> Result<HostValue> {
let source = source.strip_prefix("---\n").ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
"markdown frontmatter must start with `---`",
)
})?;
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
"markdown frontmatter is not terminated",
)
})?;
let mut fields = Vec::new();
for line in frontmatter.lines() {
let (name, value) = line.split_once(':').ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
"invalid markdown frontmatter field",
)
})?;
let value = match value.trim() {
"true" => HostValue::bool(true),
"false" => HostValue::bool(false),
value => HostValue::string(value.trim_matches('"')),
};
fields.push((name.trim(), value));
}
Ok(HostValue::object([
("frontmatter", HostValue::object(fields)),
("body", HostValue::string(body)),
]))
}
#[test]
fn imports_host_structured_content() {
let mut engine = Engine::new(MarkdownLoader {
source: String::from("---\ntitle: \"Hello\"\ndraft: false\n---\n# Hello\n\nBody text."),
});
let module = engine
.add_root_source(
"main.dcdl",
"main.dcdl",
r#"
let
Post = {
frontmatter = {
title = String;
draft = Bool;
};
body = String;
};
post = Post & import "./post.md";
in {
title = post.frontmatter.title;
draft = post.frontmatter.draft;
body = post.body;
}
"#,
)
.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("Hello")));
assert_eq!(fields[1].value, Data::Bool(false));
assert_eq!(
fields[2].value,
Data::String(String::from("# Hello\n\nBody text."))
);
}
struct StructuredAliasLoader;
impl ImportLoader for StructuredAliasLoader {
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
Ok(LoadedImport::value(
"shared-value",
HostValue::int_type().default_int(1)?,
))
}
}
#[test]
fn structured_imports_share_cached_runtime_value_by_key() {
let mut engine = Engine::new(StructuredAliasLoader);
let module = engine
.add_root_source(
"main.dcdl",
"main.dcdl",
r#"(import "./first.data") & (import "./second.data")"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
assert_eq!(data, Data::Int(1));
}
#[test]
fn top_level_fields_are_recursive_module_scope() {
let mut engine = Engine::new(EmptyLoader);
+1 -1
View File
@@ -21,7 +21,7 @@ pub use decodal_derive::Decodal;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use eval::{Engine, format_diagnostic_with};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
pub use module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
pub use span::{SourceId, Span};
+38 -5
View File
@@ -1,7 +1,7 @@
use alloc::string::String;
use crate::{
Ast, ExprId, SourceForm, SourceId,
Ast, ExprId, HostValue, SourceForm, SourceId,
runtime::{EnvId, ThunkId},
};
@@ -24,15 +24,48 @@ pub struct LoadedSource {
pub source: String,
}
pub trait SourceLoader {
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource>;
#[derive(Debug, Clone)]
pub struct LoadedValue {
pub key: String,
pub value: HostValue,
}
#[derive(Debug, Clone)]
pub enum LoadedImport {
Source(LoadedSource),
Value(LoadedValue),
}
impl LoadedImport {
pub fn source(
key: impl Into<String>,
name: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self::Source(LoadedSource {
key: key.into(),
name: name.into(),
source: source.into(),
})
}
pub fn value(key: impl Into<String>, value: HostValue) -> Self {
Self::Value(LoadedValue {
key: key.into(),
value,
})
}
}
pub trait ImportLoader {
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedImport>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyLoader;
impl SourceLoader for EmptyLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource> {
impl ImportLoader for EmptyLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedImport> {
Err(crate::Diagnostic::new(
crate::DiagnosticKind::Import,
crate::Span::default(),