# Embedding API Decodal core can be embedded without giving the core crate access to a filesystem. The host supplies imports through `ImportLoader` 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`, plus the top range `Unknown`, 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::{EmptyLoader, Engine, Value}; let mut engine = Engine::new(EmptyLoader); engine.bind_global( "Service", Value::object([ ("name", Value::string_type()), ("port", Value::int_type().gt(443).default_int(8443)?), ("enabled", Value::bool_type().default_bool(true)?), ]), )?; ``` A user source can then refer to `Service` without importing it. ```dcdl { name = "api"; port = 9443; } as Service ``` ## Value `Value` is the public builder-facing value representation for embedding. It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly. ```text Value = String Int Float Bool Array(Vec) ArrayRange { item, constraints, default } MapRange { value, constraints, default } Object { fields, rest: Option } Range { constraints, default } ``` When a value is bound by the host, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults. `Value::array_of(item)` builds an array constraint with a required element schema. There is no `Value` constructor for an unconstrained abstract array. `Value::map_of(value)` builds a map constraint whose arbitrary object field values must satisfy `value`. `BTreeMap` and, with `std`, `HashMap` implement `DecodalSchema`, `DecodalDecode`, and `IntoValue` using this representation. `Value::unknown()` builds the top abstract range. `Value::object_with_rest(fields, rest)` builds an object with named fields and a range for all remaining fields. ```rust Value::object_with_rest( [("enabled", Value::bool_type().default_bool(true)?)], Value::unknown(), ) ``` ## Objects containing ranges A host-provided schema object is represented as a concrete object structure whose fields may contain ranges. ```rust Value::object([ ("name", Value::string_type()), ("port", Value::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. ## Typed Rust integration Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync. The derive implements two traits from the `decodal` crate: - `DecodalSchema`: builds a `Value` schema that can be passed to `Engine::bind_global`. - `DecodalDecode`: decodes materialized `Data` back into the Rust type. An explicitly marked map field can receive the open portion of an object. ```rust use std::collections::BTreeMap; use decodal::{Data, Decodal}; #[derive(Decodal)] struct OpenConfig { enabled: bool, #[decodal(rest)] extra: BTreeMap, } ``` The schema generated for `extra` is `...Unknown`. A typed receiver such as `BTreeMap` generates `...String` instead. During decode, named top-level fields are excluded and all remaining fields are collected into the map. Only one `#[decodal(rest)]` field is allowed. It must implement `DecodalRest`; the core provides implementations for `BTreeMap` and, with `std`, `HashMap`. `rename`, `default`, and field constraints cannot be combined with `rest`. Without a rest receiver, a derived struct remains a closed object range. ```rust use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine}; #[derive(Decodal)] struct Service { name: String, #[decodal(gt = 443, default = 8443)] port: i64, #[decodal(rename = "feature.enable", default = true)] feature_enabled: bool, } let mut engine = Engine::new(EmptyLoader); engine.bind_global("Service", Service::decodal_schema())?; let value = engine.eval_module(module)?; let data = engine.materialize(&value)?; let service = Service::decodal_decode(&data)?; ``` Supported field attributes are intentionally small: - `rename = "path.to.field"` - `default` - `default = value` - numeric constraints: `gt`, `gte`, `lt`, `lte` The derive does not add host callbacks or reflection. It only generates schema construction and typed decoding code. ## ImportLoader and prelude together `ImportLoader` and host prelude bindings are independent mechanisms. - Use `ImportLoader` when user sources should explicitly import host-provided sources or values. - 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. ## Shared host environment An embedded application can implement `HostEnvironment` to keep loader creation and global binding setup in one place. Both production evaluation and semantic editor tooling create their engines from this environment, preventing the editor from drifting onto a separate validation path. ```rust use decodal::{Engine, HostEnvironment}; struct AppEnvironment; impl HostEnvironment for AppEnvironment { type Loader = ContentLoader; fn create_loader(&self) -> Self::Loader { ContentLoader::new() } fn configure_engine( &self, engine: &mut Engine, ) -> decodal::Result<()> { engine.bind_global("Site", site_schema())?; Ok(()) } } let environment = AppEnvironment; let mut engine = environment.create_engine()?; ``` The semantic service accepts the same environment by value: ```rust use decodal_language_service::LanguageService; let service = LanguageService::new(&environment); let analysis = service.analyze("site.dcdl", "site.dcdl", source); ``` Each analysis uses a fresh engine and runs the normal parse, evaluate, and materialize pipeline. An environment may create loaders backed by shared filesystem, database, or editor-overlay state when repeated analysis needs a current workspace snapshot. The LSP adapter constructs that environment after receiving the client's initialization parameters: ```rust use decodal_lsp::{LspEnvironment, run_stdio}; impl LspEnvironment for AppEnvironment {} run_stdio(|initialize| { let _ = initialize; Ok(AppEnvironment) })?; ``` `LspEnvironment` adds document lifecycle hooks on top of `HostEnvironment`. A `run_stdio` environment factory always receives the client's `InitializeParams`; the host decides whether to use its workspace folders, initialization options, and capabilities or ignore them. A host that keeps unsaved buffers in shared state can update them from `open_document`, `change_document`, and `close_document`; every subsequent diagnostic pass creates the normal import loader from that updated environment. Synchronized non-Decodal documents are passed through these hooks but are not evaluated as Decodal roots, so a loader can parse an unsaved Markdown file into `Value` and immediately revalidate the open Decodal documents that import it. ## Structured imports `ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`. The value variant carries a `Value`, allowing a host to parse non-Decodal resources such as Markdown into an application-specific structure. ```text import "./post.md" -> host parses content -> LoadedImport::Value { key: "content/post.md", value: { frontmatter: {...}, body: "..." } } -> Engine internalizes Value -> normal composition and materialization ``` The core does not select content types or bundle Markdown/frontmatter parsers. The loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics. The stable loader key is also used to cache structured imports. When a structured value fails a Decodal constraint, the diagnostic keeps the Decodal constraint span and identifies the imported value by its stable key and logical value path, such as `content/post.md` and `frontmatter.draft`. `Value` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance. `load` is the single import hook: loaders dispatch by extension, media type, or another host-defined rule and return the appropriate variant directly.