Refocus manual on public language usage

This commit is contained in:
2026-08-14 08:17:25 +09:00
parent e0a9a8efb7
commit e0970acdcc
41 changed files with 611 additions and 1982 deletions
+56
View File
@@ -0,0 +1,56 @@
# Developing Decodal
Development notes live here rather than in the public manual. The manual describes released language behavior and public integration APIs; implementation plans and unresolved design work belong in issues or pull requests.
## Repository map
- `crates/decodal-core`: parser, evaluator, public Rust API, and tests.
- `crates/decodal-derive`: `Decodal` derive macros.
- `crates/decodal-language-service`: host-configurable semantic analysis and completion.
- `crates/decodal-language-tools`: source-preserving formatter and tooling.
- `crates/decodal-lsp`: LSP transport and document synchronization.
- `crates/decodal-wasm`: WebAssembly bindings.
- `packages/decodal-wasm`: generated npm and JSR runtime package.
- `packages/decodal-codemirror`: CodeMirror language support and formatter WebAssembly.
- `editors/lezer-decodal` and `editors/tree-sitter-decodal`: downstream editor grammars.
- `site/decodal-site`: documentation site and playground.
- `doc/manual/souce`: public manual consumed by the site.
## Runtime architecture
The host owns I/O. `ImportLoader` turns an import specifier into DCDL source or a structured `Value`, and `HostEnvironment` installs the same loader and globals for production and editor tooling.
Evaluation proceeds through parse, lazy expression evaluation, and materialization. Object fields, module roots, imports, arguments, and defaults are represented by memoized thunks. A thunk re-entered while it is being evaluated produces a cycle diagnostic.
Runtime values distinguish concrete values from abstract ranges. Composition and `as` preserve that distinction; materialization selects defaults and rejects unresolved ranges or functions. `Data` is the output-only representation.
Diagnostics retain Decodal source spans. Structured host values use their stable import key and logical value path rather than synthetic source spans.
## Checks
Run the release validation commands documented in [`RELEASING.md`](../../RELEASING.md). For a focused change, run the tests for the affected crate or package first, then the full workspace and site checks before release.
The documentation site can be checked without regenerating committed WebAssembly artifacts:
```sh
npm --prefix site/decodal-site test
npm --prefix site/decodal-site run build
```
Use `npm --prefix site/decodal-site run build:wasm` when Rust bindings or formatter behavior changed.
## Syntax changes
When syntax changes:
1. Update the public EBNF in `doc/manual/souce/language/grammar.md` and the relevant language page.
2. Update the Rust lexer/parser and tests.
3. Update the Lezer and Tree-sitter grammars and regenerate their committed outputs.
4. Update formatter, playground, and language-service tests as applicable.
5. Run the validation commands in `RELEASING.md`.
The Rust parser is authoritative for evaluation. Lezer and Tree-sitter are downstream editor integrations and are not runtime or formatter dependencies.
## Releases
Package versions, validation commands, publish order, and registry commands are maintained only in [`RELEASING.md`](../../RELEASING.md).
+54 -113
View File
@@ -1,140 +1,81 @@
# Components
# Packages and Integrations
Decodal is split into a small runtime core and separate syntax tooling components.
The public surface is intentionally organized by use case: execute Decodal with Rust or WebAssembly, edit Decodal on the Web with Lezer and CodeMirror, and integrate Decodal into general-purpose editors with Tree-sitter.
Decodal は実行、ホスト組み込み、言語サービス、エディタ統合を用途別の package として提供する。
## Runtime components
## Rust
### Rust crate
### `decodal`
The `decodal` crate is the primary Rust runtime and embedding API.
It owns parsing, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits.
Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program.
Rust アプリケーション向けの runtime embedding API である。
source の parse・evaluate・materialize、host global、source または structured value の import を扱う。
Important paths:
```text
crates/decodal-core/
crates/decodal-derive/
```toml
[dependencies]
decodal = "0.4"
```
`decodal-derive` provides optional derive macros for Rust struct integration.
It is a companion to the runtime crate rather than an editor or syntax-highlighting component.
Rust struct から Decodal schema と decoder を生成する場合は `derive` feature を有効にする。
### WebAssembly package
`decodal-wasm` exposes the runtime to browsers.
The documentation site playground uses it to evaluate Decodal entirely in the browser.
Important paths:
```text
crates/decodal-wasm/
packages/decodal-wasm/
```toml
[dependencies]
decodal = { version = "0.4", features = ["derive"] }
```
The npm package is `decodal-wasm`.
The JSR package is `@hare/decodal-wasm`.
正規表現制約を検証する場合は `regex` feature を有効にする。
この feature は `std` を必要とする。
The generated files in `packages/decodal-wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
The WebAssembly package is for execution, not syntax highlighting.
### `decodal-language-service`
## Language tools
transport に依存しない semantic evaluation と completion を提供する。
アプリケーションの `HostEnvironment` をそのまま受け取るため、本番評価と編集時評価で global bindings と import 規則を共有できる。
Semantic editor integration lives in the host-configurable language service crate.
It depends only on the runtime and accepts the same `HostEnvironment` implementation used by a production application.
The LSP crate adapts that service to the Language Server Protocol over stdin/stdout.
It provides full document synchronization, semantic diagnostics, and whole-document formatting.
A host-specific LSP binary injects its loader and global schema configuration without reimplementing evaluation rules.
### `decodal-lsp`
Important paths:
language service を Language Server Protocol に接続する。
stdio server のほか、client の `InitializeParams` からアプリケーション固有の environment を構築する library API を提供する。
```text
crates/decodal-language-service/
crates/decodal-lsp/
### `decodal-language-tools`
source formatter など、source text を扱う共通ツールを提供する。
formatter は Rust、LSP、WebAssembly の各統合から同じ実装を利用できる。
## JavaScript and WebAssembly
### `decodal-wasm`
browser と WebAssembly 対応 runtime 向けの evaluator と language service である。
```sh
npm install decodal-wasm
```
The default `decodal-lsp` binary reads Decodal imports from the filesystem.
Embedded hosts can call its library entry point with a custom `LspEnvironment` to reuse structured imports and to make unsaved external documents, such as Markdown, visible to the loader.
JSR では `@hare/decodal-wasm` として提供される。
`DecodalLanguageService``globals``loadImport``completeImport` を渡すことで、JavaScript が所有する environment を評価と補完で共有できる。
Source formatting lives in a separate Rust language tools crate.
It uses the canonical Decodal AST together with the runtime lexer's lossless syntax tokens, so native LSP and WebAssembly callers execute the same formatter implementation.
### `decodal-codemirror`
This component is responsible for operations that must preserve source text details such as comments and whitespace.
It is used by the CodeMirror package's bundled formatter WebAssembly and can also be used by an LSP adapter for formatting.
It does not depend on Tree-sitter or Lezer.
CodeMirror 6 向けの language support である。
syntax highlighting、folding、indentation、Decodal formatter との統合を提供する。
Important paths:
```text
crates/decodal-language-tools/
```sh
npm install decodal-codemirror
```
The current language tools crate exposes the formatter.
JSR では `@hare/decodal-codemirror` として提供される。
semantic evaluation と completion が必要な場合は `decodal-wasm` の language service と組み合わせる。
## Web editor components
## Editor syntax
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
Lezer provides syntax highlighting, folding, indentation, and editor syntax tree behavior. The browser formatter command calls the canonical Rust formatter compiled to WebAssembly.
Tree-sitter grammar は、Tree-sitter を採用するエディタで構文解析と highlighting を行うための統合である。
Decodal runtime、formatter、LSP の利用には Tree-sitter は必要ない。
Important paths:
用途ごとの選択は次の通りである。
```text
editors/lezer-decodal/decodal.grammar
packages/decodal-codemirror/src/decodal.js
packages/decodal-codemirror/src/decodal-parser.js
packages/decodal-codemirror/src/decodal-parser.terms.js
packages/decodal-codemirror/src/format.js
packages/decodal-codemirror/wasm/
```
- Rust での実行と組み込み: `decodal`
- browser での実行と semantic tooling: `decodal-wasm`
- CodeMirror 6: `decodal-codemirror`
- transport 非依存の Rust language service: `decodal-language-service`
- 一般的な editor client: `decodal-lsp`
- Tree-sitter 採用 editor の syntax grammar: Tree-sitter integration
The npm package is `decodal-codemirror`.
The JSR package is `@hare/decodal-codemirror`.
The Lezer grammar is derived from the canonical grammar documentation, but it is not a literal copy of the EBNF.
Precedence and token conflict handling are represented in the Lezer grammar in the form CodeMirror needs.
## General editor components
Tree-sitter is the portable editor-integration grammar.
Editors such as Zed, Neovim, Helix, and Emacs should consume this component when they need Decodal parsing or highlighting outside the Web playground.
Important paths:
```text
editors/tree-sitter-decodal/grammar.js
editors/tree-sitter-decodal/queries/highlights.scm
editors/tree-sitter-decodal/queries/locals.scm
editors/tree-sitter-decodal/src/
```
The generated parser sources under `editors/tree-sitter-decodal/src/` are committed so editor integrations can consume the grammar without regenerating it first.
## Canonical grammar
The human-readable grammar lives in:
```text
doc/manual/souce/language/grammar.md
```
This EBNF is the language-level reference.
The Rust parser, Lezer grammar, and Tree-sitter grammar should be kept aligned with it, but each implementation may encode precedence and recovery behavior in the form required by its parser generator or runtime.
## Syntax token API
The `decodal` crate exposes `tokenize_source`, `tokenize_source_with_source_id`, `SyntaxToken`, and `SyntaxTokenKind` for source-preserving tooling.
Comments have explicit tokens, while whitespace is represented by gaps between token spans and can be recovered from the original source.
The evaluator parser and formatter therefore share one lexical definition without making Tree-sitter an upstream dependency.
Consumers should otherwise use the component matching their environment:
- Rust execution and embedding: `decodal`
- Browser execution: `decodal-wasm`
- Web formatting and editor syntax: canonical formatter / CodeMirror / Lezer
- Semantic editor analysis: `decodal-language-service`
- Language Server Protocol integration: `decodal-lsp`
- Rust formatting: `decodal-language-tools`
- General editor syntax: Tree-sitter
Tree-sitter and Lezer remain downstream editor grammars and are not dependencies of the runtime formatter.
具体的な environment の構築方法は [Embedding](./embedding.md) を参照する。
@@ -1,143 +0,0 @@
# Composition and Materialization
`&``//``as``default`、materialize は、runtime value の variant に基づいて処理する。
## `&`
`&` は制約を保った合成である。
```text
compose_and(a: RuntimeValue, b: RuntimeValue) -> RuntimeValue | Diagnostic
```
基本規則:
```text
Abstract(a) & Abstract(b)
-> Abstract {
constraints: a.constraints + b.constraints,
default: merge_default(a.default, b.default)
}
Abstract(a) & Concrete(v)
-> if satisfies(v, a.constraints) then Concrete(v)
else constraint diagnostic
Concrete(v) & Abstract(a)
-> if satisfies(v, a.constraints) then Concrete(v)
else constraint diagnostic
Concrete(Object(a)) & Concrete(Object(b))
-> Concrete(Object(fieldwise_and(a, b)))
Concrete(a) & Concrete(b)
-> if a == b then Concrete(a)
else conflict diagnostic
```
`Abstract & Concrete` が成功した場合、default は消える。
明示値があるなら fallback は不要だからである。
## object の合成
object は concrete structure だが、field の値は thunk 経由で concrete / abstract のどちらにもなりうる。
object 同士の `&` は field ごとに再帰合成する。
```dcdl
MyConfig = {
host = String;
port = Int default 8080;
};
Config = MyConfig & {
host = "localhost";
};
```
`host``Abstract(String) & Concrete("localhost")` として検証され、成功すれば `Concrete("localhost")` になる。
`port` は右辺に明示値がないため、`Abstract(Int, default 8080)` のまま残る。
## default の合成
`&` で片方だけが default を持つ場合、その default を保持する。
同じ thunk 由来の default 同士は同一 default として保持する。
異なる default 同士は conflict とする。
```text
merge_default(None, None) -> None
merge_default(Some(a), None) -> Some(a)
merge_default(None, Some(b)) -> Some(b)
merge_default(Some(a), Some(b)) -> if same(a, b) then Some(a) else conflict
```
`//` では右辺 default が左辺 default を置き換える。
## `//`
`//` は右辺優先の deep patch である。
```text
patch(a: RuntimeValue, b: RuntimeValue) -> RuntimeValue
```
基本規則:
- object / object は field ごとに再帰 patch する。
- object / object 以外は右辺で置き換える。
- 左辺にしかない field は保持する。
- 右辺にしかない field は追加する。
- 配列、scalar、function は右辺置換とする。
`//` は制約を保持するための演算子ではない。
対称な制約合成には `&` を使い、左辺が右辺より狭いことを確認しながら合成する場合は `as` を使う。
## `as`
`as` は narrower と wider の向きを固定した範囲包含確認と合成である。
```text
apply_as(narrower: RuntimeValue, wider: RuntimeValue)
-> RuntimeValue | Diagnostic
```
abstract / abstract では、左辺 constraints が右辺 constraints を包含することを確認し、左辺を結果にする。
concrete / abstract では、concrete value が右辺 constraints を満たすことを確認する。
object / object では右辺を field domain とする。左辺にしかない field は診断し、両側にある field は再帰的に `apply_as` し、右辺にしかない field は thunk を force せずそのまま結果へ残す。
この扱いは default の有無に依存しない。default は包含判定の根拠でも `as` の補完値でもなく、後の materialize だけが選択する。
`ArrayItems(wider)` は concrete array の全要素を、`MapValues(wider)` は concrete object の全 field value を同じ関数で絞り込む。
## materialize
materialize は runtime value を出力可能な `Data` に変換する。
```text
materialize(RuntimeValue) -> Data | Diagnostic
```
処理規則:
```text
Concrete(String/Int/Float/Bool) -> Data
Concrete(Array(items)) -> each item を force して materialize
Concrete(Object(fields)) -> each field を force して materialize
Concrete(Function) -> materialize 不能
Abstract { constraints, default: Some(d) }
-> force(d)
-> result が constraints を満たすか検証
-> materialize(result)
Abstract { constraints, default: None }
-> 未解決 abstract value として diagnostic
```
materialize は default を採用する唯一の段階である。
通常評価中に明示値が得られた場合、default は採用されない。
## Diagnostic context
Composition and materialization keep source spans for object fields, constraints, defaults, and thunks where possible.
When a conflict occurs, the diagnostic should identify the operation span and related participant spans, such as the left field, right field, constraint, or default value.
For object materialization errors, diagnostics also include the field path being processed when known.
@@ -1,79 +0,0 @@
# Diagnostics and Fallback
エラーは runtime value ではなく diagnostic として扱う。
処理系はエラー内容に基づく汎用的な実行時分岐を提供しない。
## Diagnostic
```text
Diagnostic {
kind: DiagnosticKind
span: Span
message: String
labels: Vec<DiagnosticLabel>
notes: Vec<String>
}
DiagnosticLabel {
span: Span
message: String
}
```
`span` は primary location を示す。
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
`labels` は同じ error に関係する追加 location を示す。
`notes` は source location を持たない semantic context を示す。
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
代表的な diagnostic kind:
- syntax error
- unresolved identifier
- type mismatch
- constraint violation
- composition conflict
- default conflict
- cycle dependency
- import failure
- match failure
- materialization failure
Structured imports use semantic provenance rather than synthetic source spans.
Constraint failures report the imported value's stable key and logical field or array path alongside the source span of the Decodal constraint that rejected it.
## エラーは値ではない
評価失敗は `RuntimeValue` ではなく `Diagnostic` を返す。
そのため、通常の式はエラー内容に基づいて分岐できない。
```text
Result<RuntimeValue, Diagnostic>
```
これにより、制約違反、未定義識別子、循環依存、import 失敗などが通常値として流れることを避ける。
## 合成と materialize の diagnostic
合成や materialize の失敗は、以下を示す。
1. どの段階で失敗したか: composition、patch、materialization
2. どの field path を処理中だったか
3. どの constraint、value、default が衝突したか
4. なぜ合成または materialize できないか
例えば default が constraint を満たさない場合は、constraint の位置と default value の位置の両方を label として持つ。
object field の合成で concrete value が衝突する場合は、左辺 field と右辺 field の位置を label として持つ。
## `try / catch` は core に入れない
汎用 `try / catch` は core に入れない。
エラーを制御フローとして扱うと、どの失敗を捕捉できるか、捕捉後の thunk state をどう扱うか、制約違反を握りつぶしてよいか、といった仕様が重くなる。
fallback は有限で明示的な仕組みに限定する。
- `default`: 未指定値の fallback。
- `match`: 有限 pattern に基づく分岐。
`Unknown` は明示的に書く最上位のabstract rangeであり、評価エラーを包む値ではない。
field 不在や未解決 identifier は `Unknown` へ変換せず、diagnostic として報告する。
-258
View File
@@ -1,258 +0,0 @@
# 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<Value>)
ArrayRange { item, constraints, default }
MapRange { value, constraints, default }
Object { fields, rest: Option<Value> }
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<String, T>` and, with `std`, `HashMap<String, T>` 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<String, Data>,
}
```
The schema generated for `extra` is `...Unknown`. A typed receiver such as `BTreeMap<String, String>` 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<String, T>` and, with `std`, `HashMap<String, T>`. `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<Self::Loader>,
) -> 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.
@@ -1,163 +0,0 @@
# Execution Pipeline
処理系は、source を AST に変換し、必要な値だけを AST interpreter で評価する。
```text
source
lexer / parser
desugar
register root module
demand-driven evaluation
├─ force thunk
├─ load imported module on demand
├─ evaluate expression
├─ compose `&`
├─ patch `//`
└─ validate `as`
materialize
data / diagnostics
```
Production applications and semantic language services can construct this pipeline through the same host-defined `HostEnvironment`.
The environment creates the `ImportLoader` and installs host globals before the root module is registered.
## lexer / parser
lexer / parser は source を AST に変換する。
構文エラーはこの段階で diagnostic として報告する。
AST は arena に格納し、式や pattern は `ExprId``PatternId` のような ID で参照する。
## desugar
desugar は、意味論を単純にするための表層構文変換を行う。
例として、dot-path field は nested object に変換できる。
```dcdl
{
feature_hoge.enable = false;
}
```
```dcdl
{
feature_hoge = {
enable = false;
};
}
```
この変換により、評価器は object field の再帰構造だけを扱えばよい。
## module registry
module registry は、読み込んだ module を loader が返す安定 key で管理する。
CLI では canonical path を key とする。
組み込み利用では、resource name や static source table の key を使える。
```text
ModuleRegistry:
ModuleKey -> ModuleId
```
処理系は、まず root module を parse / desugar して registry に登録する。
import 先 module は、この段階で全て読み込む必要はない。
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。
loader は module key、表示名、および DCDL source text または構造化済み `Value` を返す。
DCDL source の場合、module registry は key が未登録なら対象 module を parse / desugar して登録する。
登録された source module は module root thunk を持つ。
構造化値の場合、host が返した `Value` を runtime value に internalize した root thunk を key でキャッシュする。
同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
つまり source import は module を即時評価しない。
module を読み込み、module root を thunk として登録するだけにする。
structured import も root value を thunk として保持し、object field や array item の既存 thunk model を利用する。
AST の `ExprId` は module-local である。
そのため runtime が保持する式参照は `ExprRef { module, expr }` として module-qualified にする。
## demand-driven evaluation
評価器は、必要になった thunk だけを force する。
未参照の field、let binding、import 先の field は評価しない。
この方式により、module 間に循環 import があっても、実際に force された thunk の依存が循環しない限り評価できる。
## composition
composition は、module 全体に後からかける global pass ではない。
`&``//` の式を評価するときに、demand-driven evaluation の中で呼ばれる演算である。
```text
eval(A & B):
a = eval(A)
b = eval(B)
compose_and(a, b)
eval(A // B):
a = eval(A)
b = eval(B)
patch(a, b)
eval(A as S):
narrower = eval(A)
wider = eval(S)
apply_as(narrower, wider)
```
`&` は制約を保った対称な合成を行い、`//` は右辺優先の deep patch を行う。
`as` は左辺が右辺より狭いことを再帰的に確認する。左辺で絞られた部分は左辺を使い、右辺だけの部分は abstract のまま保持する。
default は `as` では選択せず、materialize まで遅延する。
詳細は [Composition and Materialization](./composition-and-materialization.md) に置く。
## resolver / binder
resolver / binder は初期実装では必須ではない。
評価時に environment lookup を行えば、識別子参照は実装できる。
ただし、将来的には optional phase として追加できる余地を残す。
```text
source
lexer / parser
desugar
resolver / binder
register root module
demand-driven evaluation
```
resolver / binder を追加すると、以下を早期に診断しやすくなる。
- 未定義識別子
- shadowing の扱い
- reserved word の扱い
- symbol interning
- import path の一部静的解決
- span 付き diagnostic の精度向上
ただし、Decodal の制約検証は独立した type checking pass ではなく、`&` の合成時、`as` の適用時、materialize 時に行う。
## materialize
通常の評価結果は runtime value であり、抽象値や default を含みうる。
外部へ出力するときだけ materialize を行い、出力可能な data に変換する。
materialize は以下を行う。
- 必要な thunk を force する。
- abstract value の default を必要に応じて force する。
- concrete value が constraint を満たすか検証する。
- 未解決の abstract value、function value などを diagnostic にする。
-68
View File
@@ -1,68 +0,0 @@
# Features
Decodal は embedded use と小さい runtime を優先する。
言語機能を追加するときは、値の合成・検証・materialization に直接必要なものを core に残し、重い依存や高度な推論は optional feature または外部 tooling に分ける。
## Core feature boundary
Core に入れる機能は、基本的に deterministic な value transformation に限る。
- arithmetic / logical / comparison operators
- array concat
- object / constraint composition
- asymmetric range refinement, `Unknown`, map constraints, and object rest constraints
- default materialization
- pure function evaluation
- host supplied import evaluation
Core に入れないものは以下である。
- filesystem / network / environment access
- time / random
- mutation
- reflection or existence probing
- arbitrary host function calls
- symbolic constraint solving beyond simple normalization
未解決 identifier や missing field は明示的な `Unknown` range とは異なり、diagnostic になる。
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
## Constraint reasoning
Constraint normalization は軽量な範囲に留める。
primitive type conflict や明らかな numeric bound conflict は合成時に検出してよい。
一方で、symbolic arithmetic、boolean algebra、regex intersection、array length dependent typing のような重い推論は行わない。
評価済みの concrete value に対する検証は runtime / materialization で行う。
静的に完全な型検査フェーズを増やすのではなく、parse、evaluate、compose、materialize の各段階で自然に分かる error を diagnostic として返す。
## Core defaults
`decodal` defaults to `std` only.
```toml
[features]
default = ["std"]
std = []
regex = ["std", "dep:regex"]
```
Building `decodal` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
## Regex
Regex constraints are implemented behind the `regex` feature.
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
```sh
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
```
Regex constraints are accumulated during `&` composition.
The implementation does not try to prove whether the intersection of two regex constraints is empty.
Concrete strings must match every regex constraint attached to the abstract value.
## CLI features
`decodal-cli` exposes a matching `regex` feature that enables `decodal/regex`.
The feature is not enabled by default so the default CLI binary remains small.
-19
View File
@@ -1,19 +0,0 @@
# 処理系設計
この章では、言語仕様を実装するための処理系モデルを定義する。
言語仕様そのものは [Language Specification](../language/index.md) に置き、この章では AST interpreter、遅延評価、thunk、runtime value、materialize の実装方針を扱う。
## 方針
初期処理系は AST interpreter として実装する。
bytecode VM や JIT ではなく、AST を demand-driven に評価することで、遅延評価、循環参照、`default``&``//``as` の意味論を小さく実装する。
## 構成
1. [Execution Pipeline](./execution-pipeline.md)
2. [Runtime Model](./runtime-model.md)
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)
7. [Features](./features.md)
-179
View File
@@ -1,179 +0,0 @@
# Runtime Model
処理系の評価結果は、具体値と抽象値を区別した runtime value として扱う。
`value``constraints``default` を横並びに持つ構造にはしない。
## RuntimeValue
```text
RuntimeValue =
Concrete(ConcreteValue)
Abstract(AbstractValue)
```
`Concrete` は明示的な値である。
`Abstract` は、まだ具体値に確定していない制約付きの値である。
Decodal は `Unknown` を、すべてのDecodal値を包含する最上位の abstract range として持つ。
これは存在する具体値の型情報を消す `Any` ではない。具体値を `Unknown` に対して検証した場合は具体値を保持し、`Unknown` のままmaterializeしようとした場合は diagnostic になる。
識別子や field が解決できない状態とは区別し、それらは従来どおりその場で diagnostic になる。
## ConcreteValue
```text
ConcreteValue =
String(String)
Int(i64)
Float(f64)
Bool(bool)
Array(Vec<ThunkId>)
Object(ObjectValue)
Function(FunctionValue)
```
object は concrete structure として扱う。
ただし、各 field の中身は concrete value でも abstract value でもよい。
```text
ObjectValue:
fields: Map<Symbol, ObjectField>
rest: Option<ObjectRest>
ObjectField:
value: ThunkId
span: Span
ObjectRest:
value: ThunkId
span: Span
```
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
```dcdl
MyConfig = {
host = String;
port = Int default 8080;
};
```
概念的には以下である。
```text
Concrete(Object {
host -> Thunk(Abstract { constraints: [String], default: none })
port -> Thunk(Abstract { constraints: [Int], default: 8080 })
})
```
## AbstractValue
```text
AbstractValue {
constraints: Vec<ConstraintEntry>
default: Option<ThunkId>
}
ConstraintEntry {
constraint: Constraint
span: Span
}
```
`default``AbstractValue` にだけ存在する。
明示的な concrete value がある場合、default は保持しない。
```dcdl
port = Int default 8080;
```
これは以下の runtime value になる。
```text
Abstract {
constraints: [Type(Int)]
default: Some(Thunk(8080))
}
```
```dcdl
port = 8000;
```
これは以下である。
```text
Concrete(Int(8000))
```
## Runtime scope
Decodal runtime は application runtime ではなく、pure value evaluator である。
同じ source、同じ import results、同じ host globals が与えられた場合、評価結果は決定的である。
runtime が扱う責務は以下に限る。
- expression を評価する。
- thunk を必要に応じて force する。
- concrete / abstract value を合成する。
- materialize 時に constraint を検証する。
runtime は filesystem、network、environment variable、time、random、mutation を扱わない。
core における import は host supplied source または structured value を受け取る境界であり、filesystem access ではない。
## Constraint
constraint は concrete value とは別の型として扱う。
```text
Constraint =
Unknown
Type(PrimitiveType)
ArrayItems(ThunkId)
MapValues(ThunkId)
Compare(Op, Literal)
Regex(Pattern)
BuiltinPredicate(Symbol)
ObjectConstraint(...)
```
`ArrayItems` は配列そのものの型と、すべての要素へ合成する schema thunk を表す。
配列用の primitive type は持たず、抽象配列には必ず要素制約が必要である。
`MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。
連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。
`ObjectValue.rest` は名前付きfieldを持つobjectの残余field rangeを表す。rest自体はfieldを生成せず、materializeは実在するfieldだけを出力する。
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
object 全体にかかる constraint は必要になった時点で追加する。
## Data
materialize 後の出力可能な値は runtime value とは別型にする。
```text
Data =
String(String)
Int(i64)
Float(f64)
Bool(bool)
Array(Vec<Data>)
Object(Map<Symbol, Data>)
```
`Function`、未解決の `Abstract`、未評価の thunk は `Data` にはならない。
## 命名
実装内部では `RuntimeValue` を短く `Value` と呼んでもよい。
ただし、materialize 後の出力値とは区別する。
推奨する区別:
```text
RuntimeValue / Value 言語内部の評価結果。Abstract を含む。
ConcreteValue 明示的な具体値。
AbstractValue constraint と default を持つ抽象値。
Data 外部へ出力可能な最終データ。
```
@@ -1,88 +0,0 @@
# Thunk and Lazy Evaluation
thunk は、まだ評価していない式をあとで評価できるように包んだ遅延計算である。
この処理系では、循環検出と memoize の単位として thunk を使う。
## Thunk
```text
Thunk {
expr: ExprRef
env: EnvId
state: ThunkState
}
ExprRef {
module: ModuleId
expr: ExprId
}
ThunkState =
Unevaluated
Evaluating
Evaluated(RuntimeValue)
Error(Diagnostic)
```
`expr` は評価対象の AST node を module-qualified に指す。
`ExprId` は module-local な ID なので、runtime では `ModuleId` と組み合わせた `ExprRef` を保持する。
`env` は、その式を評価するときに使う lexical environment を指す。
式だけではなく environment も保持するのは、遅延評価された式が定義時の名前解決文脈を必要とするためである。
## force
thunk を評価する操作を force と呼ぶ。
```text
force(thunk):
Unevaluated -> Evaluating -> Evaluated(value)
Evaluated(value) -> value
Evaluating -> cycle diagnostic
Error(diagnostic) -> diagnostic
```
一度 `Evaluated` になった thunk は memoize される。
同じ thunk を複数回 force しても、式は一度だけ評価される。
## 循環検出
評価中の thunk を再度 force しようとした場合は循環依存である。
```dcdl
{
a = b;
b = a;
}
```
`a` を force すると、`a -> b -> a` と戻る。
このとき `a``Evaluating` なので cycle diagnostic を返す。
## 遅延評価の単位
thunk は主に以下に使う。
- module root
- object field
- let binding
- function argument
- default expression
object は field ごとに thunk を持つ。
そのため、object の一部だけが必要な場合、他の field は評価されない。
## module import
import は module を登録するが、module 全体を即時評価しない。
module root や field は thunk として保持され、参照されたときだけ force される。
これにより、module 間に循環 import があっても、force された thunk の依存が循環しなければ評価できる。
## function call
関数引数は thunk として関数の environment に束縛する。
関数本体で引数が参照されたときだけ force する。
関数呼び出し結果そのものはグローバルには memoize しない。
field に束縛された関数呼び出し結果は、その field thunk の評価結果として memoize される。
-242
View File
@@ -1,242 +0,0 @@
# Development
This document describes the development workflow for Decodal itself.
## Rust checks
Run the normal Rust checks from the repository root.
```sh
cargo fmt --check
cargo test
cargo check -p decodal --no-default-features
nix flake check
```
Run the default stdio language server with:
```sh
cargo run -q -p decodal-lsp
```
The protocol integration tests use an in-memory LSP connection and can be run independently:
```sh
cargo test -p decodal-lsp
```
Regex support is optional and should be tested explicitly when touched.
```sh
cargo test -p decodal --features regex
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
```
## crates.io release
The crates.io release contains `decodal`, `decodal-derive`, `decodal-language-service`, `decodal-language-tools`, and `decodal-lsp`.
`decodal-cli` and the Rust source crate `decodal-wasm` remain repository-only packages.
The generated WebAssembly package under `packages/decodal-wasm/` is published to npm and JSR.
The CodeMirror package bundles the generated formatter WebAssembly from `decodal-language-tools`.
The project is dual licensed as `MIT OR Apache-2.0`.
The authoritative validation commands, dependency order, and publish commands are maintained in `RELEASING.md` at the repository root.
## Web site and playground
The Astro documentation site and browser playground are kept in:
```text
site/decodal-site/
```
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
The playground loads `decodal-wasm` for evaluation and `decodal-codemirror` for editor features and source formatting.
Important files:
```text
site/decodal-site/src/pages/docs/[...slug].astro
site/decodal-site/src/pages/playground.astro
site/decodal-site/src/scripts/playground.js
site/decodal-site/src/scripts/playground-examples.js
site/decodal-site/src/layouts/ManualLayout.astro
site/decodal-site/src/lib/docs.js
site/decodal-site/src/lib/highlight.js
packages/decodal-codemirror/src/decodal.js
packages/decodal-codemirror/src/decodal-parser.js
packages/decodal-wasm/decodal_wasm.js
packages/decodal-wasm/decodal_wasm_bg.wasm
packages/decodal-codemirror/wasm/decodal_language_tools.js
packages/decodal-codemirror/wasm/decodal_language_tools_bg.wasm
crates/decodal-wasm/src/lib.rs
crates/decodal-language-tools/src/lib.rs
```
Build the WebAssembly packages before building the site:
```sh
cd site/decodal-site
npm install
npm run build:wasm
npm run build
```
`npm run build:wasm` builds both generated WebAssembly packages.
`npm run build:wasm:runtime` writes generated runtime files into `packages/decodal-wasm/`.
`npm run build:wasm:formatter` writes generated formatter files into `packages/decodal-codemirror/wasm/`.
`npm run build` builds only the static site from the current package files.
Use `npm run build:all` to regenerate both WebAssembly packages and then build the static site in one command.
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
Publish the generated WebAssembly package from its package directory:
```sh
cd packages/decodal-wasm
npm publish
npx jsr publish
```
Run dry-runs first when preparing a release:
```sh
npm pack --dry-run
npx jsr publish --dry-run
```
The npm package name is `decodal-wasm`.
The JSR package name is `@hare/decodal-wasm`.
The npm package advertises `MIT OR Apache-2.0`; JSR metadata uses `MIT` because its publisher requires a single recognized license identifier. Both license files are included in the package.
The playground editor uses the `decodal-codemirror` package with the generated Lezer parser in `packages/decodal-codemirror/src/decodal-parser.js`.
The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
Publish the CodeMirror package from its package directory:
```sh
cd packages/decodal-codemirror
npm install
npm publish
npx jsr publish
```
Run dry-runs first when preparing a release:
```sh
npm install
npm pack --dry-run
npx jsr publish --dry-run
```
The npm package name is `decodal-codemirror`.
The JSR package name is `@hare/decodal-codemirror`.
As with `decodal-wasm`, JSR metadata uses `MIT` while both the MIT and Apache-2.0 license files remain included.
The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time.
To run the site locally:
```sh
cd site/decodal-site
npm run dev
```
Deploy the static site to Cloudflare Pages with Wrangler direct upload:
```sh
cd site/decodal-site
npm run deploy
```
The deploy script runs `npm run build` and then uploads `dist/` to the Pages project named `decodal-site` on branch `master`.
Cloudflare Pages treats this as production when the project production branch is `master`.
Use `CLOUDFLARE_PROJECT_NAME` when deploying to a differently named Pages project.
```sh
CLOUDFLARE_PROJECT_NAME=my-pages-project npm run deploy
```
If the committed WASM package must be regenerated before deploy, use:
```sh
npm run deploy:all
```
## Tree-sitter grammar
The Tree-sitter grammar is kept in:
```text
editors/tree-sitter-decodal/
```
Important files:
```text
editors/tree-sitter-decodal/grammar.js
editors/tree-sitter-decodal/queries/highlights.scm
editors/tree-sitter-decodal/queries/locals.scm
editors/tree-sitter-decodal/corpus/basic.txt
```
The grammar is intended to be portable across editors such as Zed, Neovim, Helix, and Emacs.
Zed support should consume this grammar rather than relying on a TextMate grammar.
## Tree-sitter commands
From the grammar directory:
```sh
cd editors/tree-sitter-decodal
npm install
npx tree-sitter generate
npx tree-sitter test
```
To inspect a parse tree:
```sh
npx tree-sitter parse ../../examples/advanced/main.dcdl
```
The generated parser files under `editors/tree-sitter-decodal/src/` are committed so editor integrations can consume the grammar without regenerating it first.
`node_modules/` is ignored and must not be committed.
## Updating the grammar
When the Decodal syntax changes:
1. Update the canonical EBNF in `doc/manual/souce/language/grammar.md`.
2. Update the Rust parser/lexer as needed.
3. Update `editors/tree-sitter-decodal/grammar.js` and run `npx tree-sitter generate` / `npx tree-sitter test`.
4. Update `editors/lezer-decodal/decodal.grammar` and regenerate the CodeMirror parser:
```sh
cd site/decodal-site
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o ../../packages/decodal-codemirror/src/decodal-parser.js
```
5. Add or update corpus/tests/examples.
6. Run Rust and site checks from the repository root.
## Development shell
The Nix development shell includes Rust tooling, Node.js, Tree-sitter CLI tooling, and wasm-pack tooling.
```sh
nix develop
```
The shell provides:
- `cargo`
- `rustc`
- `rustfmt`
- `clippy`
- `node`
- `npm`
- `wasm-pack`
- `lld`
- `tree-sitter`
- `nixfmt`
+193
View File
@@ -0,0 +1,193 @@
# Embedding
Decodal runtime は filesystem、network、environment variable を直接読み込まない。
ホストは global bindings と import の解決方法を注入し、評価結果を `Data` または application type として受け取る。
## Rust runtime
`Engine``ImportLoader` と global bindings を持つ。
外部 resource を必要としない source には `EmptyLoader` を使える。
```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)?),
]),
)?;
let module = engine.add_root_source(
"service.dcdl",
"service.dcdl",
r#"{ name = "api"; port = 9443; } as Service"#,
)?;
let value = engine.eval_module(module)?;
let data = engine.materialize(&value)?;
```
`Value` は concrete value と host-defined range の両方を表す公開型である。
主な constructor は次の通りである。
- `Value::string``int``float``bool``array``object`: concrete value。
- `Value::string_type``int_type``float_type``bool_type`: primitive range。
- `Value::unknown`: 最上位 range `Unknown`
- `Value::array_of`: 全要素へ同じ range を適用する array range。
- `Value::map_of`: 全 field value へ同じ range を適用する associative-array range。
- `Value::object_with_rest`: named fields と残りの field range を持つ object。
## Typed Rust integration
`derive` feature の `Decodal` derive は、Rust struct から `DecodalSchema``DecodalDecode` を生成する。
```rust
use decodal::{Decodal, DecodalDecode, DecodalSchema};
#[derive(Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(rename = "feature.enable", default = true)]
feature_enabled: bool,
}
```
`DecodalSchema::decodal_schema()``Engine::bind_global` に渡せる `Value` を返す。
materialize 済みの `Data``DecodalDecode::decodal_decode` で Rust type に変換できる。
追加 field を受け取る struct では、map field に `#[decodal(rest)]` を付ける。
```rust
use std::collections::BTreeMap;
use decodal::{Data, Decodal};
#[derive(Decodal)]
struct OpenConfig {
enabled: bool,
#[decodal(rest)]
extra: BTreeMap<String, Data>,
}
```
`BTreeMap<String, Data>``...Unknown``BTreeMap<String, String>``...String` に対応する。
named fields は rest map から除外される。
`#[decodal(rest)]` は struct ごとに1つだけ指定でき、`rename``default`、field constraints とは併用できない。
## Imports
`ImportLoader::load``LoadedImport::Source` または `LoadedImport::Value` を返す。
ホストは specifier の解決、filesystem や他の storage からの読み込み、content type の判定をすべて管理する。
`LoadedImport::Source` は DCDL source を返す。
`LoadedImport::Value` は Markdown、JSON、TOML などをホスト独自の規則で構造化して返す用途に使える。
```text
import "./post.md"
-> host reads and parses Markdown
-> LoadedImport::Value {
key: "content/post.md",
value: { frontmatter: {...}, body: "..." }
}
-> normal Decodal validation and materialization
```
stable `key` は import の同一性と diagnostic の provenance に使われる。
構造化した値が Decodal constraint に違反した場合、diagnostic は import key と logical value path、および違反した DCDL constraint の source span を示す。
外部形式そのものの parse error と位置情報は loader が報告する。
## Shared host environment
`HostEnvironment` は loader の作成と global binding の設定を1つにまとめる。
同じ environment を runtime、language service、LSP へ渡すことで、実行時と編集時の評価規則を一致させられる。
```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<Self::Loader>,
) -> decodal::Result<()> {
engine.bind_global("Site", site_schema())?;
Ok(())
}
}
```
transport 非依存の tooling は `decodal_language_service::LanguageService::new(&environment)` で構築する。
LSP integration では `decodal_lsp::LspEnvironment` を実装し、environment factory から server を起動する。
```rust
use decodal_lsp::{LspEnvironment, run_stdio};
impl LspEnvironment for AppEnvironment {}
run_stdio(|initialize| {
let _ = initialize;
Ok(AppEnvironment)
})?;
```
factory は server capability の応答前に client の `InitializeParams` を一度受け取る。
`LspEnvironment` の document lifecycle hooks を使うと、未保存の DCDL、Markdown、その他の resource を host-owned overlay に反映できる。
## JavaScript and browser environments
`decodal-wasm``DecodalLanguageService` は filesystem や仮想 project を仮定しない。
JavaScript 側が `globals``loadImport``completeImport` を提供する。
```js
import init, { DecodalLanguageService } from 'decodal-wasm';
await init();
const files = {
'schema.dcdl': 'Server = { port = Int; };',
};
const service = new DecodalLanguageService({
globals: {
App: {
enabled: { $decodal: 'Bool', default: true },
},
},
loadImport(_currentKey, specifier) {
const key = specifier.replace(/^\.\//, '');
return { kind: 'source', key, name: key, source: files[key] };
},
completeImport() {
return ['./schema.dcdl'];
},
});
const result = service.evaluate('main.dcdl', 'main.dcdl', 'App');
```
JavaScript の primitive、array、plain object は concrete value になる。
range は `$decodal` descriptor で指定する。
- `{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }`
- `{ $decodal: 'Unknown' }`
- `{ $decodal: 'Array', item }`
- `{ $decodal: 'Map', value }`
- `{ $decodal: 'Object', fields, rest }`
- `{ $decodal: 'Range', constraints, default }`
import callbacks は同期 API である。
network resource などの非同期入力は、評価の前に preload または cache しておく。
+17 -24
View File
@@ -1,20 +1,20 @@
# Decodal Manual
このディレクトリには、Decodal のマニュアル文書を置く
このマニュアルでは、Decodal の言語仕様と、アプリケーションへ組み込むための公開 API を説明する
Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェクト名である。
## 目次
1. [Introduction](./introduction.md)
2. [Components](./components.md)
3. [Language Specification](./language/index.md)
2. [Language Specification](./language/index.md)
1. [Lexical Structure and Syntax](./language/syntax.md)
2. [Value](./language/value/index.md)
2. [Grammar](./language/grammar.md)
3. [Value](./language/value/index.md)
1. [String](./language/value/string.md)
2. [Int](./language/value/int.md)
3. [Float](./language/value/float.md)
4. [Bool](./language/value/bool.md)
3. [Expression](./language/expression/index.md)
4. [Expression](./language/expression/index.md)
1. [Literal](./language/expression/literal.md)
2. [Identifier](./language/expression/identifier.md)
3. [Path Reference](./language/expression/path-reference.md)
@@ -28,22 +28,15 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
11. [Composition](./language/expression/composition.md)
12. [Range Refinement](./language/expression/ascription.md)
13. [Default](./language/expression/default.md)
14. [String Interpolation](./language/expression/string-interpolation.md)
4. [Constraints and Defaults](./language/constraints-and-defaults.md)
5. [Composition Operators](./language/operators.md)
6. [Functions](./language/functions.md)
7. [Modules and Imports](./language/modules-and-imports.md)
8. [Evaluation Semantics](./language/evaluation.md)
9. [Materialization and Errors](./language/materialization-and-errors.md)
10. [Naming Conventions](./language/naming.md)
11. [Examples](./language/examples.md)
4. [Implementation Design](./design/index.md)
1. [Execution Pipeline](./design/execution-pipeline.md)
2. [Runtime Model](./design/runtime-model.md)
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)
7. [Features](./design/features.md)
5. [Development](./development.md)
6. [Open Issues](./open-issues.md)
14. [Arithmetic](./language/expression/arithmetic.md)
15. [Logical and Comparison](./language/expression/logical-and-comparison.md)
5. [Constraints and Defaults](./language/constraints-and-defaults.md)
6. [Operators](./language/operators.md)
7. [Functions](./language/functions.md)
8. [Modules and Imports](./language/modules-and-imports.md)
9. [Evaluation Semantics](./language/evaluation.md)
10. [Materialization and Errors](./language/materialization-and-errors.md)
11. [Naming](./language/naming.md)
12. [Examples](./language/examples.md)
3. [Embedding](./embedding.md)
4. [Packages and Integrations](./components.md)
+29 -69
View File
@@ -1,94 +1,54 @@
# Introduction
このマニュアルは、Decodal の目的、設計方針、言語仕様をまとめる。
Decodal は **Deferred Constraint Data Language**、略称 **DCDL** のプロジェクト名である
ファイル拡張子は `.dcdl` とする。
Decodal は **Deferred Constraint Data Language**、略称 **DCDL** である。
ファイル拡張子には `.dcdl` を使う
Decodal は、設定値・スキーマ・制約・派生設定を同じ式体系で扱い、組み込み環境でも実装しやすい小さな言語核を提供することを目指す
Decodal は、設定値・スキーマ・制約・派生設定を同じ式体系で記述し、検証済みの構造化データを得るための言語である
## 目的
一般的な設定ファイルでは、値の記述、スキーマ定義、デフォルト値、派生設定、バリデーションが別々の仕組みとして扱われやすい。
この言語では、それらを単一の式体系に寄せる
例えば、以下のように制約と値を同じ構文で合成できる。
一般的な設定システムでは、値、スキーマ、デフォルト値、派生設定、バリデーションが別々の仕組みになりやすい。
Decodal では、それらを合成可能な式として扱う
```dcdl
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
MyConfig = {
Service = {
host = String;
port = NarrowedPort default 8080;
port = Port default 8080;
};
Config = MyConfig & {
Config = {
host = "127.0.0.1";
port = 8000;
};
} as Service;
```
`MyConfig` は設定の形と制約を表し、`Config` はそこへ具体値を合成した設定を表す。
具体値は対応する制約を満たす必要がある。
## 設計目標
- データ記述とスキーマ記述を同じ構文で表現できる。
- 制約を `&` で合成し、値が制約を満たすか検証できる。
- 設定値やスキーマを `//` で構造的に patch できる。
- `default` により、最終評価時の fallback 値を定義できる。
- フィールド単位の遅延評価により、未使用値の評価を避ける。
- import 循環があっても、必要なフィールド依存が循環していなければ評価できる。
- 処理系を組み込み向けに小さく保てるよう、意味論を明示的かつ決定的にする。
## 非目標
初期仕様では以下を必須にしない。
- 高度な型推論。
- match の完全な網羅性検査。
- 到達不能分岐の静的検査。
- 任意の関数呼び出し結果のグローバル memoize。
- 正規表現エンジンの必須搭載。
- 汎用 `try / catch` の core 搭載。
- 完全なプログラミング言語としての汎用性。
`Service` は許容する値の範囲を表す。
`Config` の各値は `Service` によって検証され、`Service` にだけ存在する範囲は抽象的なまま結果へ残る。
materialize 時には明示値が優先され、値がない範囲には `default` が使われる。
## 中心概念
この言語の中心概念は以下である。
- 値と制約は同じ式として参照・合成できる。
- `&` は両辺の制約を保つ対称な合成を行う。
- `as` は左辺が右辺より具体的で狭いことを検証しながら合成する。
- `//` は右辺優先の構造的な patch を行う。
- `default` は制約ではなく、materialize 時にだけ選ばれる fallback である。
- object field、関数引数、import は必要になった時点で評価される。
- `Unknown` は任意の具体値を受け入れる最上位 range だが、抽象的なまま materialize はできない。
- 値と制約を同じ式として扱う。
- `&` で制約を保った合成を行う。
- `//` で右辺優先の構造的 patch を行う。
- `default` は制約ではなく、最終評価時の fallback として扱う。
- フィールド単位で遅延評価する。
- import 循環は、実際に必要なフィールド依存が循環しない限り許容する。
## 適用範囲
## 組み込み向けの方針
Decodal は、設定、スキーマ、制約、派生データの記述に特化している。
汎用的な状態変更、時刻・乱数・ネットワークアクセス、例外を値として捕捉する `try / catch` は言語機能に含まれない。
filesystem や外部形式の読み込みはホストが提供し、Decodal の評価は同じ source・import 結果・global bindings に対して決定的に動作する。
この言語は、汎用プログラミング言語を目指すものではない
主対象は、設定、スキーマ、制約、派生データの記述である
正規表現制約は利用する runtime の `regex` feature に依存する
高度な型推論、match の網羅性検査、到達不能分岐の静的検査は提供しない
そのため、言語核は「値・制約・構造の合成」と「遅延評価」に寄せる。
便利な機能であっても、実装サイズ・評価モデル・エラー決定性を大きく複雑にするものは optional feature または将来拡張として扱う。
## 次に読む章
## ドキュメント構成
言語仕様の解説は [Language Specification](./language/index.md) にまとめる
`language/` 配下には、構文、値、式、制約、演算子、評価意味論など、言語仕様そのものの説明だけを置く。
処理系の設計は [Implementation Design](./design/index.md) にまとめる。
ここでは、AST interpreter、runtime value、thunk、合成処理、materialize、diagnostic の扱いを説明する。
主な章は以下である。
- [Value](./language/value/index.md): `String``Int``Float``Bool` などの値・プリミティブ制約。
- [Expression](./language/expression/index.md): literal、object、array、function、let、match、import などの式。
- [Constraints and Defaults](./language/constraints-and-defaults.md): 制約と `default` の意味。
- [Composition Operators](./language/operators.md): `&``//` の意味。
- [Evaluation Semantics](./language/evaluation.md): 遅延評価、thunk、循環検出。
- [Materialization and Errors](./language/materialization-and-errors.md): 最終評価とエラー分類。
- [Runtime Model](./design/runtime-model.md): concrete value と abstract value の内部表現。
- [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md): 遅延計算と循環検出の処理系モデル。
未確定事項は [Open Issues](./open-issues.md) に集約する。
- [Language Specification](./language/index.md): 構文、値、式、制約、評価、materialization。
- [Embedding](./embedding.md): Rust・JavaScript からの実行、host environment、structured import、language service。
- [Packages and Integrations](./components.md): 用途ごとに選ぶ crate と JavaScript package
@@ -1,10 +1,6 @@
# 制約と default
この章では、制約と `default` の意味を定義する
## 制約
制約は、値が満たすべき条件を表す。
constraint は、値が満たすべき範囲を表す
```dcdl
Int
@@ -14,148 +10,104 @@ String
/Hello! .*/
```
制約`&` により合成できる。
constraint `&` 合成できる。
```dcdl
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
```
制約合成の意味は、すべての制約を同時に満たすことである。
```text
A & B = A と B の両方を満たす値または制約
```
矛盾する制約はエラーになる。
```dcdl
Int & String # エラー
> 10 & < 5 # エラー
Int & > 10 & < 11 # エラー。整数値の候補が存在しない
```
## 制約の正規化
`&` によって abstract value 同士を合成した場合、処理系は軽量に判定できる制約を正規化する。
正規化対象:
- primitive type 制約。
- 数値比較制約。
primitive type 制約は、異なる型が同時に要求された場合 conflict になる。
```dcdl
Int & Float
Int & String
```
数値比較制約は上下限として正規化される。
```dcdl
Int & >= 1 & <= 65535 & > 443
```
これは概念的に以下へ正規化される。
```text
Type(Int)
> 443
<= 65535
```
上下限の交差が空であれば conflict になる。
`Int` 制約がある場合は、整数候補が存在するかも判定する。
合成結果は両辺を同時に満たす範囲になる。
両立しない constraint は conflict になる。
```dcdl
Int & String # conflict
> 10 & < 5 # conflict
Int & > 10 & < 11 # conflict
Int & >= 10 & <= 10 # OK
Int & > 10 & < 11 # conflict: integer candidate does not exist
```
`Int` の比較制約は整数リテラルを使う。
`Float` の比較制約は整数リテラルまたは浮動小数リテラルを使える。
## Primitive and comparison constraints
## 組み込み制約
最小の組み込み制約は以下である。
組み込みの primitive range は次の通りである。
```dcdl
Unknown
String
Int
Float
Bool
```
`Unknown` はすべてのDecodal値を含む最上位rangeである。検査を無効化する `Any` ではなく、具体的な値またはより狭いrangeがまだ決まっていないことを表す
異なる primitive type を `&` で合成すると conflict になる
数値比較 constraint は上下限として合成され、空の範囲になる場合は conflict になる。
```dcdl
Int & >= 1 & <= 65535 & > 443
```
`Int` の比較 constraint は integer literal を使う。
`Float` の比較 constraint は integer または float literal を使える。
## Unknown
`Unknown` はすべての Decodal value を含む最上位 range である。
検査を無効化して具体値の情報を消す `Any` ではなく、値の範囲がまだ絞られていないことを表す。
```dcdl
Int & Unknown # Int
42 as Unknown # 42
Unknown as Int # エラー
Unknown as Int # conflict
```
`Unknown` 自体は具体値を持たないためmaterializeできない。defaultを与えるか、具体値で絞り込む必要がある。
`Unknown` 自体は concrete value を持たないため materialize できない。
concrete value で絞り込むか `default` を指定する必要がある。
```dcdl
Unknown default {} # {}
Unknown default {}
```
追加の述語制約はライブラリまたは組み込みとして提供できる。
## Regex constraints
regex literal は string constraint である。
```dcdl
IPv4Address
Host = /^api-[0-9]+$/;
```
## 正規表現制約
正規表現リテラルは文字列制約として使える。
```dcdl
Host = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
```
正規表現制約は積み重ね可能である。
複数の正規表現制約が同じ abstract value に付与された場合、具体文字列はすべての正規表現制約に一致しなければならない。
複数の regex constraint を合成した場合、concrete string はすべてに一致する必要がある。
```dcdl
String & /^a/ & /z$/
```
処理系は、正規表現制約同士の交差が空であるかを合成時に判定する必要はない。
つまり、以下は合成時には conflict にならず、具体値検証時に失敗する。
regex constraint 同士の交差は合成時に判定されない。
そのため、次の範囲は合成時には conflict にならず、concrete value の検証時に失敗する。
```dcdl
String & /^a$/ & /^b$/
```
正規表現エンジンは optional feature にできる。
正規表現 feature が無効な処理系では、正規表現制約の検証は unsupported feature diagnostic になる。
軽量実装では代表的な制約を組み込み述語として提供してもよい。
Rust runtime では regex engine を `regex` feature で有効にする。
feature が無効な場合、regex constraint の concrete value 検証は unsupported feature diagnostic になる。
```dcdl
Host = IPv4Address;
```
## Array constraints
## 配列制約
配列制約には要素制約が必須であり、`[...T]` と書く。
array constraint は `[...T]` と書き、すべての要素へ `T` を適用する。
要素 constraint は必須である。
```dcdl
Names = [...String];
PositiveInts = [...(Int & > 0)];
```
複数の配列制約を `&` で合成した場合、各 concrete 要素をすべての要素 range で絞り込
要素が object range の場合、右辺にしかない field は default の有無にかかわらず abstract のまま各要素へ残る
左辺にしかない field は右辺の field domain 外なのでエラーになる。
要素制約のない `Array` primitive type は存在しない。
concrete array と合成した場合、各要素は `T` に対して `as` と同じ規則で絞り込まれる
空 array は任意の array constraint を満たす
## 連想配列制約
要素 constraint が object range の場合、右辺にだけある field は abstract のまま各要素へ残る。
左辺にしかない field は右辺の field domain 外なので conflict になる。
連想配列制約は `{...T}` と書き、object の任意の field value を `T` に対して絞り込む。
## Associative-array and object rest constraints
associative-array constraint は `{...T}` と書き、object の任意の field value へ `T` を適用する。
```dcdl
Services = {...{
@@ -164,8 +116,8 @@ Services = {...{
}};
```
key は schema で列挙せず、空 object も許容る。
固定 object field と任意 key の value constraint は、末尾 `...T` で混在できる。
key は列挙されず、空 object も許容される。
named field を持つ object の末尾 `...T` を書くと、列挙されていない field だけに `T` を適用できる。
```dcdl
{
@@ -174,78 +126,38 @@ key は schema で列挙せず、空 object も許容する。
}
```
このrest constraintは明示されていないfieldだけに適用され、field自体は生成しない。
rest constraintfield生成しない。
実在する追加 field の検証にだけ使われる。
## default
`default`制約ではない。
`default` は、最終評価時に明示値が存在しない場合だけ使われる fallback である。
`default` constraint ではない。
materialize 時に concrete value がない場合だけ使われる fallback である。
```dcdl
port = NarrowedPort default 8080;
```
Port = Int & >= 1 & <= 65535;
これは概念的には以下を表す。
```text
Abstract {
constraints: [NarrowedPort]
default: 8080
}
```
明示値が合成された場合、`default` は採用されない。
```dcdl
MyConfig = {
port = NarrowedPort default 8080;
};
Config = MyConfig & {
port = 8000;
Service = {
port = Port default 8080;
};
```
この場合、最終値は `8000` である
`8080` は評価されない、または評価されても採用されない。
明示値が合成された場合は明示値が使われ、`default` は評価されない
明示値がない場合、最終 materialize 時に `default` が採用される。
採用された default 値は、同じフィールドに定義された制約を満たす必要がある。
```text
Abstract {
constraints: [NarrowedPort]
default: 8080
}
finalize => 8080 が NarrowedPort を満たせば成功
```dcdl
Config = {
port = 9000;
} as Service;
```
## default の内部表現
明示値がない場合、materialize 時に `8080` が採用され、`Port` を満たすか検証される。
`default` は abstract value に付随する fallback thunk として保持できる。
これにより、default 値自体も必要になるまで評価しない。
## default composition
```text
RuntimeValue =
Concrete(ConcreteValue)
Abstract {
constraints: Vec<Constraint>
default: Option<Thunk>
}
```
- `&` で片方だけが `default` を持つ場合、その `default` は保持される。
- `&` で両辺が異なる `default` を持つ場合は conflict になる。
- constraint と concrete value の `&` が成功した場合、結果は concrete value になり `default` は残らない。
- `//` は右辺優先なので、同じ field では右辺の値または `default` が左辺を置き換える。
- `as` は右辺の `default` を左辺へ注入しない。ただし右辺にだけ存在する field は、その field が持つ `default` とともに abstract なまま結果へ残る。
明示値は `Concrete` として表現し、`default` を保持しない
`Abstract & Concrete` が成功した場合、制約検証後に `Concrete` になり、default は消える。
## default の合成
`default` の合成規則は以下である。
- `&` で片方だけが default を持つ場合、その default を保持する。
- `&` で両方が異なる default を持つ場合、conflict になる。
- `Abstract & Concrete` が成功した場合、結果は concrete value になり default は消える。
- `//` では右辺が左辺を置き換える。object/object の場合は field ごとに再帰 patch されるため、右辺 field の default が左辺 field の default を置き換える。
`default` thunk は materialize 時に必要になった時点で評価する。
評価された default value は、同じ abstract value に残っている制約を満たす必要がある。
`default` expression は materialize 時に必要になった時点で評価され、同じ range の constraint を満たす必要がある
+28 -58
View File
@@ -1,51 +1,35 @@
# 遅延評価
この言語はフィールド単位で遅延評価する。
Decodal は値を必要になった時点で評価する。
## 基本方針
## 遅延評価される値
次の値は参照または materialize されるまで評価されない。
- module の root と top-level field
- object field
- `let` binding
- function argument
- `default` expression
- import 先の値
同じ binding を複数回参照した場合、その評価結果は再利用される。
このため、未参照の field や function argument にある失敗は、値が必要になるまで発生しない。
```dcdl
{
schema = {
hoge = String;
};
result = expensive(schema);
}
let
safe = 42;
unused = missing_name;
in
safe
```
`schema` のみが必要な場合、`result` は評価されない
## thunk
各フィールドや let 束縛は thunk として保持できる。
```text
Thunk {
expr: ExprId
env: EnvRef
state: Unevaluated | Evaluating | Evaluated(Value) | Error
}
```
評価済み thunk は memoize する。
同じフィールドを複数回参照しても、評価は一度だけでよい。
## 評価状態
thunk は以下の状態を持つ。
```text
Unevaluated 未評価
Evaluating 評価中
Evaluated 評価済み
Error 評価失敗
```
`Evaluating` の thunk を再度評価しようとした場合、循環依存として扱う。
この式は `unused` を参照しないため `42` になる
## 循環検出
評価中の値が自身へ再び依存した場合は cycle diagnostic になる。
```dcdl
{
a = b + 1;
@@ -53,26 +37,12 @@ Error 評価失敗
}
```
この場合、`a` または `b` を評価すると循環エラーになる。
module や import の参照関係自体が循環していても、実際に評価される field の依存関係が循環していなければ評価できる。
一方、同じモジュール内または import 間に循環があっても、評価対象のフィールドが循環していなければ成功する。
## 評価と materialize
## 評価と materialize の分離
通常の評価結果には、constraint、`Unknown``default`、function などの abstract value が残り得る。
外部へ concrete data として取り出すときに materialize を行う。
通常の評価では、制約や default を含む中間値が残ることがある。
外部へデータとして出力する段階で materialize を行う
この分離により、以下が可能になる。
- スキーマを値として扱う。
- default を必要になるまで評価しない。
- import されたモジュールの未使用フィールドを評価しない。
- 制約だけのフィールドを中間状態として保持する。
## 関数呼び出しとの関係
関数引数は thunk として渡せる。
関数本体内で引数が参照されたときに評価する。
関数呼び出し結果そのものはグローバルには memoize しない。
ただし、フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
この分離により、schema を値として合成し、必要な field だけを評価し、`default` の選択を出力時まで遅らせられる。
詳細は [materialize とエラー](./materialization-and-errors.md) を参照する
+7 -7
View File
@@ -1,11 +1,11 @@
# 例
この章は、仕様を説明するための例を置く
この章は、Decodal の主要な記法を組み合わせた例を示す
## 基本的な設定スキーマ
```dcdl
Host = IPv4Address;
Host = String;
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
@@ -144,23 +144,23 @@ Patched = Base // {
## 循環 import
```dcdl
# main.n
# main.dcdl
{
schema = {
hoge = String;
};
result = (import ./func.n)(schema);
result = (import "./func.dcdl")(schema);
}
```
```dcdl
# func.n
(input: (import ./main.n).schema) =>
# func.dcdl
(input: (import "./main.dcdl").schema) =>
{
# ...
}
```
`func.n``main.n` を import しているが、参照しているのは `main.schema` である。
`func.dcdl``main.dcdl` を import しているが、参照しているのは `main.schema` である。
`main.schema``main.result` に依存していなければ、この循環 import は成立する。
@@ -49,7 +49,7 @@ object の要素 range では左辺にしかない field がエラーになり
要素制約のない抽象配列型は提供しない。
旧来の `Array` primitive type は使用できず、`[...T]``T` は必須である。
`[String]` は配列制約ではなく、未解決の `String` 制約を 1 要素に持つ concrete array になる。
長さ制約、位置別の tuple 制約、unique 制約は現在サポートしない。
配列制約は要素範囲だけを表す。長さ制約、位置別の tuple 制約、unique 制約は持たない。
## Array concat
@@ -60,7 +60,7 @@ primitive constraint や合成 constraint は通常どおり値を検証する
右辺が concrete scalar または array literal の場合は、左辺も同じ値または同じ長さ・要素構造である必要がある。
function は右辺の範囲として使用できない。
左辺は concrete value に限らない。処理系が包含を確認できる constraint 同士であれば abstract value も使用できる。primitive type、numeric bound、同一 regex / predicate、array/map の要素範囲は包含確認の対象になる。
左辺は concrete value に限らない。包含関係を判定できる constraint 同士であれば abstract value も使用できる。primitive type、numeric bound、同一 regex / predicate、array/map の要素範囲は包含確認の対象になる。
関数 parameter の `name: range` も、引数が force された時点で `as` と同じ絞り込み規則を使う。
@@ -24,4 +24,4 @@ Patched = Base // {
};
```
詳細は [合成演算子](../operators.md) に置く
詳細は [合成演算子](../operators.md) を参照する
@@ -7,9 +7,9 @@ port = Int default 8080;
```
`default` は制約ではない。
詳細は [制約と default](../constraints-and-defaults.md) に置く
詳細は [制約と default](../constraints-and-defaults.md) を参照する
## 評価
fallback 値は thunk として保持できる。
明示値がある場合、default採用されない。
fallback expression は materialize 時に必要になった場合だけ評価される。
明示値がある場合、`default` は評価も採用されない。
@@ -8,10 +8,7 @@ increment(41)
## 評価
引数は thunk として渡せる。
関数本体内で引数が参照されたときに評価する。
引数は関数本体から参照された時点で評価される。
同じ引数を複数回参照した場合は評価結果が再利用される。
parameter に range が指定されている場合、引数は `narrower as wider` と同じ規則で絞り込まれる。
parameter 側だけにある field は abstract のまま残り、default はこの時点では選択されない。
関数呼び出し結果そのものはグローバルには memoize しない。
フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
@@ -6,9 +6,9 @@ function expression は、引数を受け取り式を返す値である。
(value: Int) => value + 1
```
関数仕様の詳細は [関数](../functions.md) に置く
関数仕様の詳細は [関数](../functions.md) を参照する
## 評価
関数は定義時の環境を参照として保持する。
関数は定義された lexical scope の bindings を参照する。
関数本体は、関数値の生成時ではなく呼び出し時に評価される。
@@ -1,6 +1,6 @@
# Identifier Expression
identifier expression は、現在の環境に束縛された名前を参照する式である。
identifier expression は、lexical scope に束縛された名前を参照する式である。
```dcdl
Port
@@ -10,5 +10,5 @@ mkConfig
## 評価
識別子は対応する束縛の thunk を参照する。
識別子は対応する binding の値を必要になった時点で評価する。
束縛が存在しない場合は未定義識別子エラーになる。
@@ -1,15 +1,14 @@
# Import Expression
import expression は、外部ファイルを読み込み、そのファイルの評価結果を返す。
import expression は、ホストが解決した DCDL module または structured value を返す。
```dcdl
import ./config.n
import "./config.n"
import "./config.dcdl"
```
import 仕様の詳細は [モジュールと import](../modules-and-imports.md) に置く
specifier は string literal であり、path や resource name としての解釈はホストが定義する
詳しくは [モジュールと import](../modules-and-imports.md) を参照する。
## 評価
import 先はモジュール単位で読み込まれる
ただし、各 field は thunk として保持され、必要になるまで評価されない。
import 先は遅延評価され、参照されない field は評価されない
@@ -20,8 +20,7 @@ Expr
├─ import
├─ composition
├─ range refinement (`as`)
─ default
└─ string interpolation
─ default
```
各式の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
+2 -2
View File
@@ -12,5 +12,5 @@ in
## 評価
let 束縛は thunk として保持される。
参照されない束縛は評価されない
binding は参照された時点で評価される。
参照されない binding は評価されず、同じ binding を複数回参照した場合は評価結果が再利用される
@@ -50,7 +50,7 @@ Services = {...{
```
`{...T}` は key の集合を固定せず、すべての value が `T` を満たす object を表す。
空 object も許容される。runtime と materialize 後の表現は通常の object と共通であり、別の map value variant は持たない
空 object も許容される。materialize 後は named fields を持つ object と同じ object data になる
```dcdl
services = {
@@ -85,4 +85,4 @@ rest constraint は field を生成せず、materialize 時には実際に存在
```
`...T` は object の末尾に一つだけ書ける。省略した object は閉じており、`as` の左辺に未宣言 field があればエラーになる。
名前付き field を持たない `{...T}`従来どおり抽象的な map constraint であり、単独でmaterializeするにはdefaultまたは具体値が必要になる。
named field を持たない `{...T}` abstract な map constraint であり、単独で materialize するには `default` または concrete value が必要になる。
@@ -1,8 +0,0 @@
# String Interpolation
文字列補間は初期実装には含めない。
Decodal の string literal は、現時点では literal text として扱う。
式を埋め込む構文は定義しない。
必要になった場合は、文字列連結や明示的な formatting function として別途設計する。
+11 -29
View File
@@ -1,6 +1,6 @@
# 関数
関数は構造を受け取り、構造を返す式として扱う
関数はを受け取り、を返す純粋な式である
## 構文
@@ -17,7 +17,7 @@ in
increment(41)
```
複数引数も指定できる。
複数の parameter を指定できる。
```dcdl
(input_a: { hoge = Int & >= 0; }, input_b: { fuga = Int; }) =>
@@ -27,33 +27,15 @@ in
}
```
## 関数の意味
parameter range は省略できる。
range がある場合、引数は参照された時点で `as` と同じ規則によって検証・絞り込みされる。
関数は runtime value として扱えるが、最終データとして materialize することはできない。
未適用の関数値が materialize 対象に残っている場合は diagnostic になる。
## Scope and evaluation
関数opaque であり、関数値同士の等価性は提供しない
`&` で関数値同士を合成すると conflict になる
関数は lexical scope を持ち、定義された場所の bindings を参照する
引数は遅延評価され、関数本体から参照されない引数は評価されない
再帰的な field または argument の依存は cycle diagnostic になる。
## 評価方針
関数は以下の方針で評価する。
- 関数は純粋である。
- 関数はレキシカルスコープを持つ。
- 関数は定義時の環境を参照として保持する。
- 引数は thunk として渡され、必要になるまで評価されない。
- parameter range がある引数は、force 時に `as` と同じ範囲包含・絞り込み規則で合成される。
- 関数呼び出し結果そのものはグローバルには memoize しない。
- フィールドに束縛された関数呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
- 再帰的な依存は thunk cycle として diagnostic になる。
関数値の内部モデル例:
```text
Function {
params: Vec<Param>
body: ExprId
env: EnvRef
}
```
関数は中間値として参照・呼び出しできるが、data として materialize できない。
未適用の関数が materialize 対象に残っている場合は diagnostic になる。
関数値同士の等価性は定義されず、`&` で関数値同士を合成すると conflict になる。
+1 -7
View File
@@ -1,7 +1,6 @@
# Grammar
This page is the canonical grammar reference for Decodal syntax.
Parser implementations such as the Rust parser, Tree-sitter grammar, and Lezer grammar should follow this grammar and may add implementation-specific precedence annotations where needed.
This page defines the grammar of Decodal source text.
## Lexical grammar
@@ -112,8 +111,3 @@ Precedence is highest first.
12. `as`
Binary operators are left-associative except `default`, which is right-associative.
## Tooling mapping
Syntax tooling should derive token categories from this grammar rather than making a tool-specific grammar canonical.
Tree-sitter and Lezer grammars are implementation artifacts that follow this page.
+8 -8
View File
@@ -1,12 +1,12 @@
# 言語仕様
このディレクトリは、Decodal / DCDL の仕様本文を章ごとに分割して管理する。
この章では、Decodal source の構文と評価結果を決める規則を説明する。
目次はマニュアル直下の [Manual Index](../index.md) に集約する。
このファイルは `Language Specification` 章の入口としてだけ使う。
- syntax と grammar
- primitive、object、array、function
- constraint、`Unknown``default`
- `&``//``as` とその他の operators
- module、import、lazy evaluation
- materialization と diagnostics
## Language
言語仕様は、構文、値、式、制約、合成演算子、関数、モジュール、評価意味論、materialize、エラーを定義する。
詳細な章構成と各ファイルへのリンクは [Manual Index](../index.md) を参照する。
章の一覧は [Manual Index](../index.md) を参照する。
@@ -1,37 +1,34 @@
# materialize とエラー
通常の評価では、制約や default を含む中間値が残ることがある。
外部へデータとして出力する段階では、materialize を行う
通常の評価結果には constraint、`default`、function などが残り得る。
materialize は評価結果を外部へ渡せる concrete data に変換する
## materialize の責務
## materialize の規則
materialize は以下を行う。
materialize は次の処理を行う。
- 必要なフィールドを評価する。
- 明示値ない abstract value に default を適用する。
- 採用された値が制約を満たすか検証する。
- default を持たない未解決の abstract value をエラーにする。
- 未適用の関数など、データとして出力できない値をエラーにする。
## 例
- 必要な field を評価する。
- 明示値ない abstract range に `default` を適用する。
- concrete value と採用した `default` が constraint を満たすか検証する。
- `default` のない `Unknown` や他の未解決 range を拒否する。
- 未適用の function など、data に変換できない値を拒否する。
```dcdl
MyConfig = {
Service = {
host = String;
port = Int default 8080;
};
```
`MyConfig` materialize すると、`host` は具体値も default もないためエラーになる。
`port``8080` が採用される。
`Service` をそのまま materialize すると、`host` に concrete value も `default` もないため失敗する。
```dcdl
Config = MyConfig & {
Config = {
host = "localhost";
};
} as Service;
```
`Config` を materialize すると以下になる。
`Config` を materialize すると次の data になる。
```dcdl
{
@@ -40,46 +37,31 @@ Config = MyConfig & {
}
```
## default の適用
明示値がある field では `default` は採用されない。
`default` は materialize 時にのみ fallback として採用される。
## Diagnostics
```text
Abstract {
constraints: [Int]
default: 8080
}
```
エラーは通常の値ではなく diagnostic として返される。
式は diagnostic の種類や内容に基づいて分岐できない。
この abstract value を materialize すると、`8080` が採用され、`Int` を満たすか検証される。
代表的な diagnostic は次の通りである。
明示値がある場合、default は採用しない。
明示値は concrete value として表現され、default を保持しない。
- syntax error
- unresolved identifier または field
- type mismatch と constraint violation
- `&` または `default` の conflict
- cycle dependency
- import failure
- match failure
- materialization failure
```text
Concrete(Int(9000))
```
diagnostic は問題のある DCDL source span を示す。
複数の式が conflict した場合は、関係する field、constraint、value、`default` の位置も示される。
structured import の値に source span がない場合は、host が返した stable key と logical value path が示される。
この値の最終値は `9000` である。
## Fallback
## エラー分類
代表的なエラー:
- 構文エラー
- 未定義識別子
- 型不一致
- 制約違反
- `&` の conflict
- `default` の conflict
- 循環依存
- import 失敗
- match の非網羅による失敗
- materialize 不能な値の出力
## match の失敗
`match` に fallback 分岐がなく、どの分岐にも一致しなかった場合はエラーになる。
`match` に fallback arm がなく、どの arm にも一致しない場合は diagnostic になる。
```dcdl
match value {
@@ -87,13 +69,5 @@ match value {
}
```
`value``10` 未満であれば失敗する
## エラーは値ではない
エラーは runtime value ではなく diagnostic として扱う。
通常の式はエラー内容に基づいて分岐できない。
汎用 `try / catch` は core には含めない。
fallback は `default``match` で表現する。
optional import や optional field access は core には含めない。
Decodal は diagnostic を捕捉する汎用 `try / catch`、optional import、optional field access を提供しない
値がない場合の fallback は `default`、有限の値分岐は `match` で表現する。
+32 -103
View File
@@ -1,6 +1,6 @@
# モジュールと import
`import`外部ファイルを読み込み、そのファイルの評価結果を返す。
`import`ホストが解決した DCDL module または structured value を返す。
## 構文
@@ -8,79 +8,37 @@
import "./config.dcdl"
```
import specifier は文字列リテラルとする。
パスリテラル構文は採用しない
import specifier は string literal である。
specifier が path、URL、resource name のどれを表すかはホストが決める
## モジュール
import 先はモジュール単位で読み込まれる。
ただし、モジュール全体を即時評価する必要はない
各フィールドは thunk として保持され、必要になったときだけ評価される。
top-level に field 定義列を書いた module は、recursive module scope を作る。
つまり、top-level field は同じ module の他の top-level field から識別子として参照できる。
top-level に field 定義列を書いた module は recursive module scope を作る。
top-level field は同じ module の他の top-level field から identifier として参照できる
```dcdl
schema = {
hoge = String;
name = String;
};
result = schema;
```
この場合、`result` の右辺の `schema` は同じ module の top-level field `schema` を参照する
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
通常の object literal の field は sibling field を identifier として暗黙参照しない
object 内の値を参照する場合は、外側で束縛された値または明示的な path reference を使う
## ImportLoader
module とその field は遅延評価される。
import した module の未参照 field は評価されない。
`import` specifier の解決は処理系 core ではなく host 側の `ImportLoader` が行う。
CLI では、specifier を現在の module path からの相対 path として解決する。
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
## Host-defined resolution
module cache の key は loader が返す安定 key を使う
CLI では canonical path を key とする
Decodal は import specifier に対する filesystem や network の規則を定義しない
ホストが import 元の module と specifier を受け取り、次のどちらかを返す
### 構造化 import
- DCDL source
- ホストが構築した structured value
`ImportLoader::load` は DCDL source または host が構築した `Value` を import 結果として返す
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
```rust
use decodal::{Value, 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,
Value::object([
("frontmatter", parsed.frontmatter),
("body", Value::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 側から次のように扱える。
後者を使うと、Markdown、JSON、TOML などをホスト独自の規則で構造化し、通常の Decodal value として扱える
```dcdl
Post = {
@@ -91,69 +49,40 @@ Post = {
body = String;
};
post = Post & import "./hello.md";
title = post.frontmatter.title;
body = post.body;
post = (import "./hello.md") as Post;
```
`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 を直接返す。
structured value も path reference、composition、constraint validation、materialization では DCDL source 由来の値と同じ規則に従う
loader API と diagnostic provenance は [Embedding](../embedding.md#imports) を参照する
## 循環 import
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
例:
module 間に循環参照があっても、実際に評価される field の依存関係が循環していなければ成功する。
```dcdl
# main.dcdl
{
schema = {
hoge = String;
name = String;
};
result = (import "./func.dcdl")(schema);
}
```
```dcdl
# func.dcdl
(input: (import "./main.dcdl").schema) =>
{
# ...
}
(input: (import "./main.dcdl").schema) => input
```
`func.dcdl``main.dcdl` を import しているが、参照しているのは `main.schema` である。
`main.schema``main.result` に依存していなければ、この循環 import は成立する。
この例で `func.dcdl``main.dcdl` を import るが、参照する `schema``result` に依存しないため評価できる。
評価中の同じ field へ再び到達した場合は循環依存の diagnostic になる。
## import の評価単位
## import の失敗
実装上は、以下の単位で管理するのが自然である。
次の状態は import failure になる。
```text
Module main
schema -> thunk
result -> thunk
Module func
root -> thunk
```
各 thunk は一度だけ評価して memoize する。
評価中に同じ thunk へ戻った場合は循環依存としてエラーにする。
## import 失敗
以下は import 失敗として扱う。
- ファイルが存在しない。
- ファイルが読めない。
- import 先の構文解析に失敗する。
- import 先の評価で必要な値がエラーになる。
- host による非 DCDL content の読み込みまたは構造化に失敗する。
- 実装が禁止する import 循環に該当する。
- ホストが specifier を解決できない。
- resource を読み込めない。
- DCDL source の構文解析に失敗する。
- 必要な import 先の値を評価できない。
- structured value の読み込みまたは変換に失敗する。
- 評価対象の依存関係が循環する。
+10 -20
View File
@@ -1,27 +1,17 @@
# 命名規約
具体値と抽象値がグラデーションになるため、大文字・小文字による厳密な意味分けは設けない。
identifier の大文字・小文字に言語上の意味はない。
値、constraint、schema、派生設定はいずれも同じ式として扱われる。
ただし、読みやすさのために慣習を定める。
読みやすさのため、次の命名を推奨する。
## 推奨規約
- object 値: `lower_snake`
- 関数: `lowerCamel`
- 組み込み型・抽象的な制約名: `UpperCamel`
例:
- object value: `lower_snake`
- function: `lowerCamel`
- primitive、schema、抽象的な constraint: `UpperCamel`
```dcdl
IPv4Address
MyConfig
new_config
mkConfig
Port = Int & >= 1;
Service = { port = Port; };
service = { port = 8080; } as Service;
mkService = (port: Port) => { port = port; };
```
## 厳密な規則にしない理由
この言語では、値・制約・スキーマ・派生設定が同じ式体系に乗る。
そのため、ある名前が「具体値」か「抽象的な制約」かは文脈によってグラデーションになる。
大文字なら型、小文字なら値、のような厳密な規則を設けると、実際の利用に対して過剰に硬くなる可能性がある。
+1 -1
View File
@@ -58,7 +58,7 @@ host = "127.0.0.1"; # trailing comment
## 識別子
識別子は ASCII 英字で始まり、ASCII 英数字または `_` を続けられる。
慣習として`lower_snake``lowerCamel``UpperCamel` を使える想定とする。
命名規則に`lower_snake``lowerCamel``UpperCamel` を使用できる。
```dcdl
my_config
+1 -1
View File
@@ -12,7 +12,7 @@ enable = Bool default true;
tags = [...String];
```
現在の primitive type は `String``Int``Float``Bool` である。
primitive type は `String``Int``Float``Bool` である。
`Unknown` はprimitive typeではなく、すべてのDecodal値を含む最上位の抽象rangeである。具体値またはdefaultがない `Unknown` はmaterializeできない。
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
@@ -2,6 +2,15 @@
`String` は文字列値を表す primitive type constraint である。
文字列リテラルは `"` で囲む。
```dcdl
"hello"
"line 1\nline 2"
```
文字列内の `$``{}` に特別な意味はなく、文字列補間は行わない。
## 例
```dcdl
-23
View File
@@ -1,23 +0,0 @@
# 未確定事項
今後決める必要がある事項を管理する。
実装または仕様方針が固まった項目は、該当する仕様ファイルへ反映してここから外す。
## Materialize target and host projection
Decodal の評価結果は、制約・default・関数値を含む中間値になり得る。
そのため、Decodal 単独で常に「最終成果物」を一意に決めるのではなく、host 側が期待する型や出力形式を与えて materialize / decode する経路を明確にする必要がある。
決めること:
- Rust API で評価結果の field/path を選択して decode / materialize できるようにするか。
- `decodal-derive` の struct schema と評価結果を合成して decode する経路を、主要な materialize path として位置づけるか。
- CLI / WASM では target path を指定して JSON-compatible value へ materialize する形にするか。
- 制約や関数値が残った値を出力したい場合、materialize ではなく inspect/debug API として分けるか。
現時点の案:
- Rust では `evaluate -> select field/path -> expected schema と合成 -> decode` を主経路にする。
- CLI / WASM では、明示された target path または module 全体を JSON-compatible value として materialize する。
- materialize できない unresolved abstract value、default のない制約値、関数値は diagnostic にする。
- Decodal 言語内には materialize 構文を追加せず、host API / CLI / WASM の責務として扱う。
+21 -26
View File
@@ -7,19 +7,26 @@ const modules = import.meta.glob('../../../../doc/manual/souce/**/*.md', {
eager: true,
});
export const docs = Object.fromEntries(
Object.entries(modules).map(([path, content]) => {
const slug = path
const documentEntries = Object.entries(modules).map(([path, content]) => {
const sourcePath = path
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/souce\//, '')
.replace(/\.md$/, '')
.replace(/\/index$/, '');
return [slug || 'index', content];
}),
.replace(/\.md$/, '');
const parts = sourcePath.split('/');
parts.pop();
const slug = sourcePath.replace(/\/index$/, '') || 'index';
return { slug, content, base: parts };
});
export const docs = Object.fromEntries(
documentEntries.map(({ slug, content }) => [slug, content]),
);
const docBases = Object.fromEntries(
documentEntries.map(({ slug, base }) => [slug, base]),
);
export const nav = [
{ title: 'Introduction', slug: 'introduction' },
{ title: 'Components', slug: 'components' },
{
title: 'Language Specification',
slug: 'language',
@@ -51,10 +58,10 @@ export const nav = [
{ title: 'Match', slug: 'language/expression/match' },
{ title: 'Import', slug: 'language/expression/import' },
{ title: 'Composition', slug: 'language/expression/composition' },
{ title: 'Range Refinement', slug: 'language/expression/ascription' },
{ title: 'Default', slug: 'language/expression/default' },
{ title: 'Arithmetic', slug: 'language/expression/arithmetic' },
{ title: 'Logical and Comparison', slug: 'language/expression/logical-and-comparison' },
{ title: 'String Interpolation', slug: 'language/expression/string-interpolation' },
],
},
{ title: 'Constraints and Defaults', slug: 'language/constraints-and-defaults' },
@@ -67,21 +74,8 @@ export const nav = [
{ title: 'Examples', slug: 'language/examples' },
],
},
{
title: 'Implementation Design',
slug: 'design',
children: [
{ title: 'Execution Pipeline', slug: 'design/execution-pipeline' },
{ title: 'Runtime Model', slug: 'design/runtime-model' },
{ title: 'Thunk and Lazy Evaluation', slug: 'design/thunk-and-lazy-evaluation' },
{ title: 'Composition and Materialization', slug: 'design/composition-and-materialization' },
{ title: 'Diagnostics and Fallback', slug: 'design/diagnostics-and-fallback' },
{ title: 'Embedding API', slug: 'design/embedding-api' },
{ title: 'Features', slug: 'design/features' },
],
},
{ title: 'Development', slug: 'development' },
{ title: 'Open Issues', slug: 'open-issues' },
{ title: 'Embedding', slug: 'embedding' },
{ title: 'Packages and Integrations', slug: 'components' },
];
const renderer = new marked.Renderer();
@@ -105,12 +99,13 @@ export function renderMarkdown(slug) {
const html = marked.parse(source ?? '# Not found\n');
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
const target = normalizeDocLink(slug, href);
return `href="/docs/${target}/${hash}"`;
const pathname = target === 'index' ? '/docs/' : `/docs/${target}/`;
return `href="${pathname}${hash}"`;
});
}
function normalizeDocLink(currentSlug, href) {
const base = currentSlug.includes('/') ? currentSlug.split('/').slice(0, -1) : [];
const base = docBases[currentSlug] ?? [];
const parts = [...base, ...href.split('/')];
const out = [];
for (const part of parts) {
+7 -1
View File
@@ -1,3 +1,9 @@
---
return Astro.redirect('/docs/introduction/');
import ManualLayout from '../../layouts/ManualLayout.astro';
import { renderMarkdown } from '../../lib/docs.js';
const html = renderMarkdown('index');
---
<ManualLayout title="Decodal Manual" active="index">
<article class="markdown" set:html={html} />
</ManualLayout>