Add unknown ranges and open object values
This commit is contained in:
@@ -75,5 +75,5 @@ fallback は有限で明示的な仕組みに限定する。
|
||||
- `default`: 未指定値の fallback。
|
||||
- `match`: 有限 pattern に基づく分岐。
|
||||
|
||||
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
|
||||
それらは diagnostic として報告する。
|
||||
`Unknown` は明示的に書く最上位のabstract rangeであり、評価エラーを包む値ではない。
|
||||
field 不在や未解決 identifier は `Unknown` へ変換せず、diagnostic として報告する。
|
||||
|
||||
@@ -17,23 +17,23 @@ let / function env
|
||||
```
|
||||
|
||||
Module top-level bindings shadow prelude bindings.
|
||||
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host 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, HostValue};
|
||||
use decodal::{EmptyLoader, Engine, Value};
|
||||
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
|
||||
engine.bind_global(
|
||||
"Service",
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", Value::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
```
|
||||
@@ -47,40 +47,49 @@ A user source can then refer to `Service` without importing it.
|
||||
} as Service
|
||||
```
|
||||
|
||||
## HostValue
|
||||
## Value
|
||||
|
||||
`HostValue` is the public builder-facing value representation for embedding.
|
||||
`Value` is the public builder-facing value representation for embedding.
|
||||
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
|
||||
|
||||
```text
|
||||
HostValue =
|
||||
Value =
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
Array(Vec<HostValue>)
|
||||
ArrayConstraint { item, constraints, default }
|
||||
MapConstraint { value, constraints, default }
|
||||
Object(Vec<HostField>)
|
||||
Abstract { constraints, default }
|
||||
Array(Vec<Value>)
|
||||
ArrayRange { item, constraints, default }
|
||||
MapRange { value, constraints, default }
|
||||
Object { fields, rest: Option<Value> }
|
||||
Range { constraints, default }
|
||||
```
|
||||
|
||||
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
|
||||
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.
|
||||
|
||||
`HostValue::array_of(item)` builds an array constraint with a required element schema.
|
||||
There is no host API for an unconstrained abstract array.
|
||||
`Value::array_of(item)` builds an array constraint with a required element schema.
|
||||
There is no `Value` constructor for an unconstrained abstract array.
|
||||
|
||||
`HostValue::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 `IntoHostValue` using this representation.
|
||||
`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.
|
||||
|
||||
## Abstract host objects
|
||||
|
||||
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
|
||||
`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
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
||||
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)?),
|
||||
])
|
||||
```
|
||||
|
||||
@@ -100,9 +109,28 @@ This matches the runtime model used for Decodal source-defined schema objects.
|
||||
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 `HostValue` schema that can be passed to `Engine::bind_global`.
|
||||
- `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};
|
||||
|
||||
@@ -202,20 +230,21 @@ run_stdio(|initialize| {
|
||||
`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 `HostValue` and immediately revalidate the open Decodal documents that import it.
|
||||
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 `HostValue`, allowing a host to parse non-Decodal resources such as Markdown into an application-specific structure.
|
||||
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(
|
||||
{ frontmatter: {...}, body: "..." }
|
||||
)
|
||||
-> Engine internalizes HostValue
|
||||
-> LoadedImport::Value {
|
||||
key: "content/post.md",
|
||||
value: { frontmatter: {...}, body: "..." }
|
||||
}
|
||||
-> Engine internalizes Value
|
||||
-> normal composition and materialization
|
||||
```
|
||||
|
||||
@@ -223,7 +252,7 @@ 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 host value by its stable import key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||
`HostValue` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance.
|
||||
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.
|
||||
|
||||
@@ -71,10 +71,10 @@ ModuleRegistry:
|
||||
import 先 module は、この段階で全て読み込む必要はない。
|
||||
|
||||
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。
|
||||
loader は module key、表示名、および DCDL source text または構造化済み `HostValue` を返す。
|
||||
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 でキャッシュする。
|
||||
構造化値の場合、host が返した `Value` を runtime value に internalize した root thunk を key でキャッシュする。
|
||||
同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
|
||||
|
||||
つまり source import は module を即時評価しない。
|
||||
|
||||
@@ -10,7 +10,7 @@ Core に入れる機能は、基本的に deterministic な value transformation
|
||||
- arithmetic / logical / comparison operators
|
||||
- array concat
|
||||
- object / constraint composition
|
||||
- asymmetric range refinement and homogeneous map constraints
|
||||
- asymmetric range refinement, `Unknown`, map constraints, and object rest constraints
|
||||
- default materialization
|
||||
- pure function evaluation
|
||||
- host supplied import evaluation
|
||||
@@ -24,7 +24,7 @@ Core に入れないものは以下である。
|
||||
- arbitrary host function calls
|
||||
- symbolic constraint solving beyond simple normalization
|
||||
|
||||
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。
|
||||
未解決 identifier や missing field は明示的な `Unknown` range とは異なり、diagnostic になる。
|
||||
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
|
||||
|
||||
## Constraint reasoning
|
||||
|
||||
@@ -14,9 +14,9 @@ RuntimeValue =
|
||||
`Concrete` は明示的な値である。
|
||||
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
||||
|
||||
Decodal は `unknown`、`any`、`null` のような「存在するが意味が未確定な値」を runtime value として持たない。
|
||||
識別子や field が解決できない場合は、その場で diagnostic になる。
|
||||
未解決値を後続の演算へ流して推論することはしない。
|
||||
Decodal は `Unknown` を、すべてのDecodal値を包含する最上位の abstract range として持つ。
|
||||
これは存在する具体値の型情報を消す `Any` ではない。具体値を `Unknown` に対して検証した場合は具体値を保持し、`Unknown` のままmaterializeしようとした場合は diagnostic になる。
|
||||
識別子や field が解決できない状態とは区別し、それらは従来どおりその場で diagnostic になる。
|
||||
|
||||
## ConcreteValue
|
||||
|
||||
@@ -37,10 +37,15 @@ object は concrete structure として扱う。
|
||||
```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 になる。
|
||||
@@ -122,6 +127,7 @@ constraint は concrete value とは別の型として扱う。
|
||||
|
||||
```text
|
||||
Constraint =
|
||||
Unknown
|
||||
Type(PrimitiveType)
|
||||
ArrayItems(ThunkId)
|
||||
MapValues(ThunkId)
|
||||
@@ -137,6 +143,8 @@ Constraint =
|
||||
`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 は必要になった時点で追加する。
|
||||
|
||||
|
||||
@@ -82,12 +82,27 @@ Int & >= 10 & <= 10 # OK
|
||||
最小の組み込み制約は以下である。
|
||||
|
||||
```dcdl
|
||||
Unknown
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
```
|
||||
|
||||
`Unknown` はすべてのDecodal値を含む最上位rangeである。検査を無効化する `Any` ではなく、具体的な値またはより狭いrangeがまだ決まっていないことを表す。
|
||||
|
||||
```dcdl
|
||||
Int & Unknown # Int
|
||||
42 as Unknown # 42
|
||||
Unknown as Int # エラー
|
||||
```
|
||||
|
||||
`Unknown` 自体は具体値を持たないためmaterializeできない。defaultを与えるか、具体値で絞り込む必要がある。
|
||||
|
||||
```dcdl
|
||||
Unknown default {} # {}
|
||||
```
|
||||
|
||||
追加の述語制約はライブラリまたは組み込みとして提供できる。
|
||||
|
||||
```dcdl
|
||||
@@ -150,7 +165,16 @@ Services = {...{
|
||||
```
|
||||
|
||||
key は schema で列挙せず、空 object も許容する。
|
||||
固定 object field と任意 key の value constraint を混在させる構文は現在サポートしない。
|
||||
固定 object field と任意 key の value constraint は、末尾の `...T` で混在できる。
|
||||
|
||||
```dcdl
|
||||
{
|
||||
enabled = Bool default true;
|
||||
...Unknown
|
||||
}
|
||||
```
|
||||
|
||||
このrest constraintは明示されていないfieldだけに適用され、field自体は生成しない。
|
||||
|
||||
## default
|
||||
|
||||
|
||||
@@ -63,4 +63,26 @@ services = {
|
||||
右辺にしかない field は default の有無にかかわらず abstract のまま残る。
|
||||
host から渡した object は識別子構文に収まらない文字列 key も保持できるが、DCDL source の object field name は通常の識別子に限られる。
|
||||
|
||||
固定 field と任意 key を一つの object schema に混在させる rest-field 構文は、現在サポートしない。
|
||||
## Object rest constraint
|
||||
|
||||
固定 field と任意 key の value range は、一つの object に混在できる。
|
||||
|
||||
```dcdl
|
||||
OpenConfig = {
|
||||
enabled = Bool default true;
|
||||
...Unknown
|
||||
};
|
||||
```
|
||||
|
||||
`...T` は明示されていない残余 field にだけ適用する。明示 field にはそれぞれの field range を適用し、rest range を重ねて適用しない。
|
||||
rest constraint は field を生成せず、materialize 時には実際に存在する追加 field だけを出力する。
|
||||
|
||||
```dcdl
|
||||
{
|
||||
enabled = false;
|
||||
plugin = { name = "cache"; };
|
||||
} as OpenConfig
|
||||
```
|
||||
|
||||
`...T` は object の末尾に一つだけ書ける。省略した object は閉じており、`as` の左辺に未宣言 field があればエラーになる。
|
||||
名前付き field を持たない `{...T}` は従来どおり抽象的な map constraint であり、単独でmaterializeするにはdefaultまたは具体値が必要になる。
|
||||
|
||||
@@ -13,4 +13,4 @@ config.feature_hoge.enable
|
||||
参照先 field は必要になるまで評価されない。
|
||||
|
||||
存在しない field への参照は diagnostic になる。
|
||||
Decodal は unknown / Any のような値を伝播せず、field の有無を曖昧にしない。
|
||||
明示的な `Unknown` range は存在しないfieldを表さないため、fieldの有無を曖昧にはしない。
|
||||
|
||||
@@ -71,7 +71,9 @@ literal = string | integer | float | "true" | "false" | regex ;
|
||||
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
|
||||
|
||||
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
|
||||
object = "{" , [ field_definition , { ";" , field_definition }
|
||||
, [ ";" , object_rest ] , [ ";" ] ] , "}" ;
|
||||
object_rest = "..." , expression ;
|
||||
map_constraint = "{" , "..." , expression , "}" ;
|
||||
field_definition = field_path , "=" , expression ;
|
||||
field_path = identifier , { "." , identifier } ;
|
||||
|
||||
@@ -43,11 +43,11 @@ CLI では canonical path を key とする。
|
||||
|
||||
### 構造化 import
|
||||
|
||||
`ImportLoader::load` は DCDL source または host が構築した `HostValue` を import 結果として返す。
|
||||
`ImportLoader::load` は DCDL source または host が構築した `Value` を import 結果として返す。
|
||||
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
|
||||
|
||||
```rust
|
||||
use decodal::{HostValue, ImportLoader, LoadedImport};
|
||||
use decodal::{Value, ImportLoader, LoadedImport};
|
||||
|
||||
struct ContentLoader;
|
||||
|
||||
@@ -62,9 +62,9 @@ impl ImportLoader for ContentLoader {
|
||||
let parsed = parse_frontmatter(&markdown)?;
|
||||
return Ok(LoadedImport::value(
|
||||
parsed.key,
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
("frontmatter", parsed.frontmatter),
|
||||
("body", HostValue::string(parsed.body)),
|
||||
("body", Value::string(parsed.body)),
|
||||
]),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ tags = [...String];
|
||||
```
|
||||
|
||||
現在の primitive type は `String`、`Int`、`Float`、`Bool` である。
|
||||
`Unknown` はprimitive typeではなく、すべてのDecodal値を含む最上位の抽象rangeである。具体値またはdefaultがない `Unknown` はmaterializeできない。
|
||||
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
|
||||
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user