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
@@ -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) で定義する。