4.9 KiB
モジュールと import
import は外部ファイルを読み込み、そのファイルの評価結果を返す。
構文
import "./config.dcdl"
import specifier は文字列リテラルとする。 パスリテラル構文は採用しない。
モジュール
import 先はモジュール単位で読み込まれる。 ただし、モジュール全体を即時評価する必要はない。 各フィールドは thunk として保持され、必要になったときだけ評価される。
top-level に field 定義列を書いた module は、recursive module scope を作る。 つまり、top-level field は同じ module の他の top-level field から識別子として参照できる。
schema = {
hoge = String;
};
result = schema;
この場合、result の右辺の schema は同じ module の top-level field schema を参照する。
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない。
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
ImportLoader
import specifier の解決は処理系 core ではなく host 側の ImportLoader が行う。
CLI では、specifier を現在の module path からの相対 path として解決する。
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
module cache の key は loader が返す安定 key を使う。 CLI では canonical path を key とする。
構造化 import
ImportLoader::load は DCDL source または host が構築した HostValue を import 結果として返す。
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
use decodal::{HostValue, ImportLoader, LoadedImport};
struct ContentLoader;
impl ImportLoader for ContentLoader {
fn load(
&mut self,
current_key: Option<&str>,
specifier: &str,
) -> decodal::Result<LoadedImport> {
if specifier.ends_with(".md") {
let markdown = read_content(current_key, specifier)?;
let parsed = parse_frontmatter(&markdown)?;
return Ok(LoadedImport::value(
parsed.key,
HostValue::object([
("frontmatter", parsed.frontmatter),
("body", HostValue::string(parsed.body)),
]),
));
}
let source = read_dcdl(current_key, specifier)?;
Ok(LoadedImport::source(
source.key,
specifier,
source.text,
))
}
}
read_content と parse_frontmatter は host 独自の処理であり、Decodal core は Markdown や YAML parser に依存しない。
上の loader を使うと、DCDL 側から次のように扱える。
Post = {
frontmatter = {
title = String;
draft = Bool default false;
};
body = String;
};
post = Post & import "./hello.md";
title = post.frontmatter.title;
body = post.body;
LoadedImport::Value は通常の concrete runtime value に internalize される。
そのため、path reference、object composition、constraint validation、materialize は source 由来の値と同じ規則を使う。
安定した key が同じ構造化 import は、engine 内で同じ値としてキャッシュされる。
load が唯一の import hook である。
loader は拡張子、media type、または host 独自の規則で振り分け、対応する LoadedImport variant を直接返す。
循環 import
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
例:
# main.dcdl
{
schema = {
hoge = String;
};
result = (import "./func.dcdl")(schema);
}
# func.dcdl
(input: (import "./main.dcdl").schema) =>
{
# ...
}
func.dcdl は main.dcdl を import しているが、参照しているのは main.schema である。
main.schema が main.result に依存していなければ、この循環 import は成立する。
import の評価単位
実装上は、以下の単位で管理するのが自然である。
Module main
schema -> thunk
result -> thunk
Module func
root -> thunk
各 thunk は一度だけ評価して memoize する。 評価中に同じ thunk へ戻った場合は循環依存としてエラーにする。
import 失敗
以下は import 失敗として扱う。
- ファイルが存在しない。
- ファイルが読めない。
- import 先の構文解析に失敗する。
- import 先の評価で必要な値がエラーになる。
- host による非 DCDL content の読み込みまたは構造化に失敗する。
- 実装が禁止する import 循環に該当する。