Add associative map constraints and range refinement

This commit is contained in:
2026-08-14 06:26:15 +09:00
parent 8198b615a8
commit cc6ab40807
52 changed files with 8951 additions and 6918 deletions
@@ -1,6 +1,6 @@
# Composition and Materialization
`&``//``default`、materialize は、runtime value の variant に基づいて処理する。
`&``//``as``default`、materialize は、runtime value の variant に基づいて処理する。
## `&`
@@ -89,7 +89,24 @@ patch(a: RuntimeValue, b: RuntimeValue) -> RuntimeValue
- 配列、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
+6 -2
View File
@@ -41,10 +41,10 @@ engine.bind_global(
A user source can then refer to `Service` without importing it.
```dcdl
Service & {
{
name = "api";
port = 9443;
}
} as Service
```
## HostValue
@@ -60,6 +60,7 @@ HostValue =
Bool
Array(Vec<HostValue>)
ArrayConstraint { item, constraints, default }
MapConstraint { value, constraints, default }
Object(Vec<HostField>)
Abstract { constraints, default }
```
@@ -69,6 +70,9 @@ When a host value is bound, the engine internalizes it into `RuntimeValue` and a
`HostValue::array_of(item)` builds an array constraint with a required element schema.
There is no host API 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.
## Abstract host objects
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
+11 -3
View File
@@ -16,7 +16,8 @@ demand-driven evaluation
├─ load imported module on demand
├─ evaluate expression
├─ compose `&`
─ patch `//`
─ patch `//`
└─ validate `as`
materialize
@@ -105,9 +106,16 @@ 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 を行う。
`&` は制約を保った対称な合成を行い、`//` は右辺優先の deep patch を行う。
`as` は左辺が右辺より狭いことを再帰的に確認する。左辺で絞られた部分は左辺を使い、右辺だけの部分は abstract のまま保持する。
default は `as` では選択せず、materialize まで遅延する。
詳細は [Composition and Materialization](./composition-and-materialization.md) に置く。
## resolver / binder
@@ -140,7 +148,7 @@ resolver / binder を追加すると、以下を早期に診断しやすくな
- import path の一部静的解決
- span 付き diagnostic の精度向上
ただし、Decodal の制約検証は独立した type checking pass ではなく、`&` の合成時materialize 時に行う。
ただし、Decodal の制約検証は独立した type checking pass ではなく、`&` の合成時`as` の適用時、materialize 時に行う。
## materialize
+1
View File
@@ -10,6 +10,7 @@ Core に入れる機能は、基本的に deterministic な value transformation
- arithmetic / logical / comparison operators
- array concat
- object / constraint composition
- asymmetric range refinement and homogeneous map constraints
- default materialization
- pure function evaluation
- host supplied import evaluation
+1 -1
View File
@@ -6,7 +6,7 @@
## 方針
初期処理系は AST interpreter として実装する。
bytecode VM や JIT ではなく、AST を demand-driven に評価することで、遅延評価、循環参照、`default``&``//` の意味論を小さく実装する。
bytecode VM や JIT ではなく、AST を demand-driven に評価することで、遅延評価、循環参照、`default``&``//``as` の意味論を小さく実装する。
## 構成
+4
View File
@@ -124,6 +124,7 @@ constraint は concrete value とは別の型として扱う。
Constraint =
Type(PrimitiveType)
ArrayItems(ThunkId)
MapValues(ThunkId)
Compare(Op, Literal)
Regex(Pattern)
BuiltinPredicate(Symbol)
@@ -133,6 +134,9 @@ Constraint =
`ArrayItems` は配列そのものの型と、すべての要素へ合成する schema thunk を表す。
配列用の primitive type は持たず、抽象配列には必ず要素制約が必要である。
`MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。
連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
object 全体にかかる constraint は必要になった時点で追加する。
+3 -2
View File
@@ -26,8 +26,9 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
9. [Match](./language/expression/match.md)
10. [Import](./language/expression/import.md)
11. [Composition](./language/expression/composition.md)
12. [Default](./language/expression/default.md)
13. [String Interpolation](./language/expression/string-interpolation.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)
@@ -133,10 +133,25 @@ Names = [...String];
PositiveInts = [...(Int & > 0)];
```
複数の配列制約を `&` で合成した場合、各 concrete 要素すべての要素制約を合成する
要素が object schema の場合は、その schema に含まれる default も各要素へ適用される。
複数の配列制約を `&` で合成した場合、各 concrete 要素すべての要素 range で絞り込む
要素が object range の場合、右辺にしかない field は default の有無にかかわらず abstract のまま各要素へ残る。
左辺にしかない field は右辺の field domain 外なのでエラーになる。
要素制約のない `Array` primitive type は存在しない。
## 連想配列制約
連想配列制約は `{...T}` と書き、object の任意の field value を `T` に対して絞り込む。
```dcdl
Services = {...{
port = Int;
enabled = Bool default true;
}};
```
key は schema で列挙せず、空 object も許容する。
固定 object field と任意 key の value constraint を混在させる構文は現在サポートしない。
## default
`default` は制約ではない。
+19 -2
View File
@@ -39,15 +39,32 @@ Services = [...{
port = Int default 8080;
}];
Services & [
[
{ name = "api"; },
{ name = "worker"; port = 9000; },
]
] as Services
```
抽象配列には要素制約が必須である。
この例では、1 番目の要素の `port``8080` に materialize される。
## 連想配列と範囲絞り込み
```dcdl
Service = {
port = Int;
enabled = Bool default true;
};
services = {
api = { port = 8080; };
worker = { port = 8081; enabled = false; };
} as {...Service};
```
`api``worker` は任意の key であり、それぞれの value は `Service` より狭い範囲へ絞り込まれる。
`as` の結果では `api.enabled` は abstract のまま残り、最終的に結果全体を materialize した時点で default の `true` が使われる。
## 関数と制約
```dcdl
@@ -30,8 +30,9 @@ Ports = [...(Int & >= 1 & <= 65535)];
[...String] & ["api", "worker"]
```
要素制約は object schema にもできる。
要素 schema の default は、配列へ制約を合成するときに各要素へ適用される。
要素制約は object range にもできる。
右辺だけにある default 付き field は、配列へ制約を合成した時点では abstract のまま各要素に残る。
その配列を後から materialize した場合にだけ default が使われる。
```dcdl
Services = [...{
@@ -42,6 +43,9 @@ Services = [...{
Services & [{ name = "api"; }]
```
配列制約を concrete array に適用すると、各要素は要素 range に対して `as` と同じ規則で絞り込まれる。
object の要素 range では左辺にしかない field がエラーになり、右辺にしかない field は abstract のまま残る。
要素制約のない抽象配列型は提供しない。
旧来の `Array` primitive type は使用できず、`[...T]``T` は必須である。
`[String]` は配列制約ではなく、未解決の `String` 制約を 1 要素に持つ concrete array になる。
@@ -0,0 +1,75 @@
# Range Refinement
`narrower as wider` は、左辺が右辺より具体的で狭い範囲であることを確認し、両者を非対称に合成する演算である。
```dcdl
Int & > 10 as Int & > 0
```
この例は成功し、結果は左辺の狭い範囲 `Int & > 10` のままになる。逆向きの `Int as Int & > 0` は、左辺が右辺を満たすとは限らないため失敗する。
## Object
object では、右辺を field domain として左辺の field を確認する。
- 左辺にしかない field は、右辺の domain 外なのでエラーになる。
- 両側にある field は、左辺が右辺より狭いかを再帰的に確認し、左辺の狭い結果を使う。
- 右辺にしかない field は、default の有無に関係なく未評価のまま結果へ残す。
- 結果の field order は右辺の順序に従う。
```dcdl
partial = {
port = 8080;
} as {
host = String;
port = Int;
enabled = Bool default true;
};
```
`partial.port``8080` に具体化される。`partial.host``partial.enabled` は右辺由来の abstract field のままであり、`as``enabled` の default を選択も force もしない。
その後 `partial` 全体を materialize すれば、通常の materialize 規則が適用される。この例では unresolved な `host` がエラーになり、`host` も具体化されていれば `enabled` の default がその時点で利用される。
## Default
default は範囲包含の根拠ではない。両側に同じ field がある場合、結果には左辺を使うため、右辺の default を左辺へ注入しない。
```dcdl
Int & > 0 as (Int default 1)
```
結果は default のない `Int & > 0` である。一方、右辺だけに残る object field は field 全体を保持するため、その abstract value が元から持つ default も保持される。
## Map and array ranges
`{...T}` は任意の object key を許可し、各 value が `T` より狭いことを確認する。
```dcdl
ports = {
http = 80;
https = 443;
} as {...(Int & >= 1 & <= 65535)};
```
`[...T]` を右辺に使う場合も、すべての array element を `T` に対して絞り込む。
## Concrete right-hand ranges
primitive constraint や合成 constraint は通常どおり値を検証する。
右辺が concrete scalar または array literal の場合は、左辺も同じ値または同じ長さ・要素構造である必要がある。
function は右辺の範囲として使用できない。
左辺は concrete value に限らない。処理系が包含を確認できる constraint 同士であれば abstract value も使用できる。primitive type、numeric bound、同一 regex / predicate、array/map の要素範囲は包含確認の対象になる。
関数 parameter の `name: range` も、引数が force された時点で `as` と同じ絞り込み規則を使う。
## Precedence
`as``default` より低い、最も低い優先順位を持ち、左結合である。
```dcdl
narrow & overrides as Wider
```
これは `(narrow & overrides) as Wider` と解釈される。
@@ -11,6 +11,9 @@ Port = Int & >= 1 & <= 65535;
Config = MyConfig & { port = 8000; };
```
object 同士では片側だけにある field も保持するため、`&` は方向を持たない。
左辺が右辺より狭いことを確認し、右辺を field domain として合成したい場合は [`as`](./ascription.md) を使う。
## `//`
`//` は右辺優先の構造的 patch を行う。
@@ -10,6 +10,8 @@ increment(41)
引数は thunk として渡せる。
関数本体内で引数が参照されたときに評価する。
parameter に range が指定されている場合、引数は `narrower as wider` と同じ規則で絞り込まれる。
parameter 側だけにある field は abstract のまま残り、default はこの時点では選択されない。
関数呼び出し結果そのものはグローバルには memoize しない。
フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
@@ -10,6 +10,7 @@ Expr
├─ identifier
├─ path reference
├─ object
├─ map constraint
├─ array
├─ array constraint
├─ function
@@ -18,6 +19,7 @@ Expr
├─ match
├─ import
├─ composition
├─ range refinement (`as`)
├─ default
└─ string interpolation
```
@@ -37,3 +37,30 @@ MyConfig = {
};
}
```
## Map constraint
連想配列の制約は、object の任意の field value に同じ schema を適用する。
```dcdl
Services = {...{
port = Int;
enabled = Bool default true;
}};
```
`{...T}` は key の集合を固定せず、すべての value が `T` を満たす object を表す。
空 object も許容される。runtime と materialize 後の表現は通常の object と共通であり、別の map value variant は持たない。
```dcdl
services = {
api = { port = 8080; };
worker = { port = 8081; enabled = false; };
} as Services;
```
各 entry は `as` によって右辺の field domain 内へ絞り込まれるため、左辺に `port``enabled` 以外の field があればエラーになる。
右辺にしかない field は default の有無にかかわらず abstract のまま残る。
host から渡した object は識別子構文に収まらない文字列 key も保持できるが、DCDL source の object field name は通常の識別子に限られる。
固定 field と任意 key を一つの object schema に混在させる rest-field 構文は、現在サポートしない。
+1
View File
@@ -43,6 +43,7 @@ in
- 関数はレキシカルスコープを持つ。
- 関数は定義時の環境を参照として保持する。
- 引数は thunk として渡され、必要になるまで評価されない。
- parameter range がある引数は、force 時に `as` と同じ範囲包含・絞り込み規則で合成される。
- 関数呼び出し結果そのものはグローバルには memoize しない。
- フィールドに束縛された関数呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
- 再帰的な依存は thunk cycle として diagnostic になる。
+5 -1
View File
@@ -35,8 +35,9 @@ module = { statement } ;
statement = field_definition , [ ";" ]
| expression , [ ";" ] ;
expression = default_expression ;
expression = as_expression ;
as_expression = default_expression , { "as" , default_expression } ;
default_expression = patch_expression , [ "default" , default_expression ] ;
patch_expression = compose_expression , { "//" , compose_expression } ;
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
@@ -56,6 +57,7 @@ path_suffix = "." , identifier ;
primary_expression = literal
| identifier
| comparison_constraint
| map_constraint
| object
| array_constraint
| array
@@ -70,6 +72,7 @@ comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
map_constraint = "{" , "..." , expression , "}" ;
field_definition = field_path , "=" , expression ;
field_path = identifier , { "." , identifier } ;
@@ -104,6 +107,7 @@ Precedence is highest first.
9. `&`
10. `//`
11. `default`
12. `as`
Binary operators are left-associative except `default`, which is right-associative.
+44 -3
View File
@@ -30,6 +30,7 @@
| `&` | `lhs & rhs` | composition | value / constraint / object | constraint-preserving composition |
| `//` | `lhs // rhs` | patch | object / value | right-biased structural patch |
| `default` | `base default fallback` | default | abstract value | materialization fallback |
| `as` | `narrower as wider` | range refinement | value / constraint / structure | narrower result plus untouched right-only ranges |
`concrete scalar``String``Bool``Int``Float` を指す。
@@ -48,6 +49,7 @@
9. `&`
10. `//`
11. `default`
12. `as`
同じ優先順位の二項演算子は左結合である。
`default` は右結合である。
@@ -91,6 +93,7 @@ A & B
- 両方が異なる具体値なら conflict になる。
- 両方が object なら、フィールドごとに合成する。
- 同じフィールドが両方にある場合、そのフィールド値を `&` で合成する。
- 片方にしかないフィールドは、そのまま結果に保持する。
- 矛盾が発生した場合はエラーになる。
例:
@@ -188,12 +191,44 @@ object field 全体を特別に置き換えるための `replace(...)` 構文や
object 全体を別構造にしたい場合は、patch 対象より外側で値を作り直す。
## `&` と `//` の使い分け
## `as`: range refinement
`&`制約を満たす具体化に使う
`as`左辺が右辺より具体的で狭い範囲であることを確認する、非対称な絞り込み演算子である
```dcdl
ValidConfig = MyConfig & {
Refined = {
port = 8000;
} as {
port = Int & >= 1 & <= 65535;
host = String;
enabled = Bool default true;
};
```
`port` は左辺の `8000` に具体化される。
右辺にしかない `host``enabled` は abstract のまま結果へ残る。default の有無は、右辺だけの field を保持するかどうかに影響しない。
`as` 自体は default を選択せず、後から結果を materialize した場合だけ通常の default 規則が働く。
左辺にしかない object field は右辺の field domain 外なのでエラーになる。
両側にある nested object、array constraint、map constraint は同じ規則で再帰的に絞り込む。
abstract range 同士も包含を確認できる。
```dcdl
Int & > 10 as Int & > 0 # 成功し、Int & > 10 を返す
Int as Int & > 0 # 失敗
```
`&` は対称な合成であり、片方だけにある object field を保持する。
したがって、左右に方向を持つ絞り込みを `&` で代用しない。
詳細は [Range Refinement](./expression/ascription.md) を参照する。
## `&`、`//`、`as` の使い分け
`&` は制約や部分構造を失わず、対称に合成する。
```dcdl
Combined = MyConfig & {
port = 8000;
};
```
@@ -208,3 +243,9 @@ ModifiedSchema = MyConfig // {
`//` は右辺優先の patch であり、左辺の制約を常に保持するとは限らない。
制約を保持したい場合は `&` を使う。
左辺が右辺より狭いことを検証しながら合成する場合は `as` を使う。
```dcdl
RefinedConfig = NarrowConfig as WideConfig;
```
+3 -1
View File
@@ -103,6 +103,7 @@ in
match
import
default
as
true
false
```
@@ -119,9 +120,10 @@ false
& 制約合成
// patch 合成
default fallback 指定
as 左辺から右辺への範囲包含確認と絞り込み
=> 関数
. フィールド参照 / ドットパス定義
... 配列の要素制約
... 配列または連想配列の値制約
```
演算子の優先順位は [合成演算子](./operators.md) で定義する。