Add host-structured imports
This commit is contained in:
@@ -5,8 +5,8 @@ use std::{
|
||||
};
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
||||
format_diagnostic_with,
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
|
||||
Span, format_diagnostic_with,
|
||||
};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -95,12 +95,12 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FsLoader;
|
||||
|
||||
impl SourceLoader for FsLoader {
|
||||
impl ImportLoader for FsLoader {
|
||||
fn load(
|
||||
&mut self,
|
||||
current_key: Option<&str>,
|
||||
specifier: &str,
|
||||
) -> Result<LoadedSource, Diagnostic> {
|
||||
) -> Result<LoadedImport, Diagnostic> {
|
||||
let path = Path::new(specifier);
|
||||
let path = if path.is_absolute() {
|
||||
PathBuf::from(path)
|
||||
@@ -112,7 +112,7 @@ impl SourceLoader for FsLoader {
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
};
|
||||
load_path(&path)
|
||||
load_path(&path).map(LoadedImport::Source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||
};
|
||||
|
||||
const POST: &str = r#"---
|
||||
title: Hello
|
||||
draft: false
|
||||
---
|
||||
# Hello
|
||||
|
||||
This body stays as Markdown.
|
||||
"#;
|
||||
|
||||
struct ContentLoader;
|
||||
|
||||
impl ImportLoader for ContentLoader {
|
||||
fn load(
|
||||
&mut self,
|
||||
_current_key: Option<&str>,
|
||||
specifier: &str,
|
||||
) -> decodal::Result<LoadedImport> {
|
||||
if specifier != "./post.md" {
|
||||
return Err(Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
Span::default(),
|
||||
format!("unknown content import `{specifier}`"),
|
||||
));
|
||||
}
|
||||
Ok(LoadedImport::value(
|
||||
"content/post.md",
|
||||
parse_markdown(POST)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(source: &str) -> decodal::Result<HostValue> {
|
||||
let source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
|
||||
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
|
||||
let mut fields = Vec::new();
|
||||
for line in frontmatter.lines() {
|
||||
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
|
||||
let value = match value.trim() {
|
||||
"true" => HostValue::bool(true),
|
||||
"false" => HostValue::bool(false),
|
||||
value => HostValue::string(value),
|
||||
};
|
||||
fields.push((name.trim(), value));
|
||||
}
|
||||
Ok(HostValue::object([
|
||||
("frontmatter", HostValue::object(fields)),
|
||||
("body", HostValue::string(body)),
|
||||
]))
|
||||
}
|
||||
|
||||
fn frontmatter_error() -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
Span::default(),
|
||||
"invalid Markdown frontmatter",
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> decodal::Result<()> {
|
||||
let mut engine = Engine::new(ContentLoader);
|
||||
let module = engine.add_root_source(
|
||||
"main.dcdl",
|
||||
"main.dcdl",
|
||||
r#"
|
||||
let post = import "./post.md";
|
||||
in {
|
||||
title = post.frontmatter.title;
|
||||
draft = post.frontmatter.draft;
|
||||
body = post.body;
|
||||
}
|
||||
"#,
|
||||
)?;
|
||||
let value = engine.eval_module(module)?;
|
||||
let data = engine.materialize(&value)?;
|
||||
|
||||
let Data::Object(fields) = data else {
|
||||
panic!("expected imported Markdown to produce an object")
|
||||
};
|
||||
assert_eq!(fields[0].value, Data::String(String::from("Hello")));
|
||||
assert_eq!(fields[1].value, Data::Bool(false));
|
||||
assert!(matches!(fields[2].value, Data::String(_)));
|
||||
Ok(())
|
||||
}
|
||||
+163
-13
@@ -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(¤t_key), specifier)?;
|
||||
self.add_source(key, name, &source)
|
||||
let loaded = self.loader.load(Some(¤t_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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
||||
Span, format_diagnostic_with,
|
||||
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, ImportLoader, LoadedImport,
|
||||
LoadedSource, SourceId, Span, format_diagnostic_with,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -76,12 +76,12 @@ struct VirtualLoader {
|
||||
files: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl SourceLoader for VirtualLoader {
|
||||
impl ImportLoader for VirtualLoader {
|
||||
fn load(
|
||||
&mut self,
|
||||
current_key: Option<&str>,
|
||||
specifier: &str,
|
||||
) -> decodal::Result<LoadedSource> {
|
||||
) -> decodal::Result<LoadedImport> {
|
||||
let key = resolve_import(current_key, specifier).ok_or_else(|| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
@@ -96,11 +96,11 @@ impl SourceLoader for VirtualLoader {
|
||||
format!("import `{specifier}` resolved to `{key}`, but that file does not exist"),
|
||||
)
|
||||
})?;
|
||||
Ok(LoadedSource {
|
||||
Ok(LoadedImport::Source(LoadedSource {
|
||||
key: key.clone(),
|
||||
name: key,
|
||||
source,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user