This commit is contained in:
2026-06-16 00:45:10 +09:00
commit 5d6a03b74f
32 changed files with 1443 additions and 0 deletions
@@ -0,0 +1,134 @@
# 制約と default
この章では、制約と `default` の意味を定義する。
## 制約
制約は、値が満たすべき条件を表す。
```n
Int
String
>= 1
<= 65535
/Hello! .*/
```
制約は `&` により合成できる。
```n
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
```
制約合成の意味は、すべての制約を同時に満たすことである。
```text
A & B = A と B の両方を満たす値または制約
```
矛盾する制約はエラーになる。
```n
Int & String # エラー
>= 10 & <= 5 # エラーになりうる
```
## 組み込み制約
最小の組み込み制約は以下である。
```n
String
Int
Float
Bool
```
追加の述語制約はライブラリまたは組み込みとして提供できる。
```n
IPv4Address
```
## 正規表現制約
正規表現リテラルは文字列制約として使える候補である。
```n
Host = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
```
ただし、組み込み向け実装では正規表現エンジンを optional feature にできる。
軽量実装では代表的な制約を組み込み述語として提供してもよい。
```n
Host = IPv4Address;
```
## default
`default` は制約ではない。
`default` は、最終評価時に明示値が存在しない場合だけ使われる fallback 値である。
```n
port = NarrowedPort default 8080;
```
これは概念的には以下を表す。
```text
constraint = NarrowedPort
value = none
default = 8080
```
明示値が合成された場合、`default` は採用されない。
```n
MyConfig = {
port = NarrowedPort default 8080;
};
Config = MyConfig & {
port = 8000;
};
```
この場合、最終値は `8000` である。
`8080` は評価されない、または評価されても採用されない。
明示値がない場合、最終 materialize 時に `default` が採用される。
採用された default 値は、同じフィールドに定義された制約を満たす必要がある。
```text
constraint = NarrowedPort
value = none
default = 8080
finalize => 8080 が NarrowedPort を満たせば成功
```
## default の内部表現
`default` は fallback thunk として保持できる。
これにより、default 値自体も必要になるまで評価しない。
```text
Cell {
constraint: ConstraintSet
value: Option<Value>
default: Option<Thunk>
}
```
## default の合成
同じフィールドに複数の `default` が合成された場合の詳細規則は未確定である。
現時点の単純な方針は以下である。
- `&` による default 同士の衝突はエラーにする。
- 同一 default 値は許可してよい。
- `//` による patch では右辺 default が左辺 default を置き換える。
この方針により、`&` は制約を保った合成、`//` は上書き操作として説明できる。
+78
View File
@@ -0,0 +1,78 @@
# 遅延評価
この言語はフィールド単位で遅延評価する。
## 基本方針
```n
{
schema = {
hoge = String;
};
result = expensive(schema);
}
```
`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 を再度評価しようとした場合、循環依存として扱う。
## 循環検出
```n
{
a = b + 1;
b = a + 1;
}
```
この場合、`a` または `b` を評価すると循環エラーになる。
一方、同じモジュール内または import 間に循環があっても、評価対象のフィールドが循環していなければ成功する。
## 評価と materialize の分離
通常の評価では、制約や default を含む中間値が残ることがある。
外部へデータとして出力する段階で materialize を行う。
この分離により、以下が可能になる。
- スキーマを値として扱う。
- default を必要になるまで評価しない。
- import されたモジュールの未使用フィールドを評価しない。
- 制約だけのフィールドを中間状態として保持する。
## 関数呼び出しとの関係
関数引数は thunk として渡せる。
関数本体内で引数が参照されたときに評価する。
任意の関数呼び出し結果をグローバルに memoize することは必須ではない。
ただし、フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
+142
View File
@@ -0,0 +1,142 @@
# 例
この章には、仕様を説明するための例を置く。
## 基本的な設定スキーマ
```n
rec {
Host = IPv4Address;
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
MyConfig = {
host = Host;
port = NarrowedPort default 8080;
feature_hoge = {
enable = Bool default true;
fuga = Int default 10;
};
};
}
NewConfig = MyConfig & {
host = "127.0.0.1";
port = 8000;
};
disabled_config = NewConfig & {
feature_hoge.enable = false;
};
enabled_config = NewConfig;
```
## 関数と文字列生成
```n
let
maybe_hw = String & /Hello! .*/;
part = {
greet = String;
target = String;
};
mk_hw = (part: part) =>
maybe_hw & "${part.greet}! ${part.target}";
in
mk_hw({
greet = "Hello";
target = "World";
})
```
評価結果:
```text
"Hello! World"
```
## match
```n
(
input_a: {
hoge = Int & >= 0;
},
input_b: {
fuga = 20;
}
) =>
let
inputs = {
a = input_a;
b = input_b;
};
in
{
foo = match inputs.a.hoge {
>= 20: {
value = 200;
};
>= 10: {
value = 100;
};
_: {
value = 300;
};
};
}
```
`match` は上から順に評価されるため、広い条件より狭い条件を先に書く。
## deep patch
```n
Base = {
feature_hoge = {
enable = Bool default true;
fuga = Int default 10;
};
};
Patched = Base // {
feature_hoge.enable = false;
};
```
`Patched` は以下に相当する。
```n
{
feature_hoge = {
enable = false;
fuga = Int default 10;
};
}
```
## 循環 import
```n
# main.n
{
schema = {
hoge = String;
};
result = (import ./func.n)(schema);
}
```
```n
# func.n
(input: (import ./main.n).schema) =>
{
# ...
}
```
`func.n``main.n` を import しているが、参照しているのは `main.schema` である。
`main.schema``main.result` に依存していなければ、この循環 import は成立する。
@@ -0,0 +1,15 @@
# Array Expression
array expression は、順序付きの値の列を表す。
```n
[1, 2, 3]
["a", "b", "c"]
```
## 未確定事項
- 配列要素の制約表現。
- 異種配列を許可するか。
- `//` による patch を右辺置換だけにするか。
- append / prepend / remove などの操作を提供するか。
@@ -0,0 +1,24 @@
# Composition Expression
composition expression は、複数の値・制約・構造を合成する式である。
## `&`
`&` は制約を保った合成を行う。
```n
Port = Int & >= 1 & <= 65535;
Config = MyConfig & { port = 8000; };
```
## `//`
`//` は右辺優先の構造的 patch を行う。
```n
Patched = Base // {
feature_hoge.enable = false;
};
```
詳細は [合成演算子](../operators.md) に置く。
@@ -0,0 +1,15 @@
# Default Expression
`default` expression は、明示値が存在しない場合に materialize 時に採用される fallback を指定する。
```n
port = Int default 8080;
```
`default` は制約ではない。
詳細は [制約と default](../constraints-and-defaults.md) に置く。
## 評価
fallback 値は thunk として保持できる。
明示値がある場合、default は採用されない。
@@ -0,0 +1,17 @@
# Function Call Expression
function call expression は、関数値を引数に適用する式である。
```n
mk_hw({
greet = "Hello";
target = "World";
})
```
## 評価
引数は thunk として渡せる。
関数本体内で引数が参照されたときに評価する。
任意の関数呼び出し結果をグローバルに memoize することは必須ではない。
@@ -0,0 +1,18 @@
# Function Expression
function expression は、引数を受け取り式を返す値である。
```n
(part: {
greet = String;
target = String;
}) =>
"${part.greet}! ${part.target}"
```
関数仕様の詳細は [関数](../functions.md) に置く。
## 評価
関数は定義時の環境を参照として保持する。
関数本体は、関数値の生成時ではなく呼び出し時に評価される。
@@ -0,0 +1,14 @@
# Identifier Expression
identifier expression は、現在の環境に束縛された名前を参照する式である。
```n
Port
MyConfig
mkConfig
```
## 評価
識別子は、対応する束縛の thunk を参照する。
束縛が存在しない場合は未定義識別子エラーになる。
@@ -0,0 +1,15 @@
# Import Expression
import expression は、外部ファイルを読み込み、そのファイルの評価結果を返す。
```n
import ./config.n
import "./config.n"
```
import 仕様の詳細は [モジュールと import](../modules-and-imports.md) に置く。
## 評価
import 先はモジュール単位で読み込まれる。
ただし、各 field は thunk として保持され、必要になるまで評価されない。
@@ -0,0 +1,24 @@
# Expression
この章では、言語が評価対象として持つ式の分類を定義する。
式は、値・制約・構造・計算を表す基本単位である。
```text
Expr
├─ literal
├─ identifier
├─ path reference
├─ object
├─ array
├─ function
├─ function call
├─ let
├─ match
├─ import
├─ composition
├─ default
└─ string interpolation
```
各式の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
@@ -0,0 +1,18 @@
# Let Expression
let expression は、ローカル束縛を作る。
```n
let
part = {
greet = "Hello";
target = "World";
};
in
"${part.greet}! ${part.target}"
```
## 評価
let 束縛は thunk として保持される。
参照されない束縛は評価されない。
@@ -0,0 +1,20 @@
# Literal Expression
literal expression は、ソース上に直接書かれる具体値である。
## 種類
```n
"hello"
123
3.14
true
false
```
## 対応する primitive type
- 文字列リテラルは `String` を満たす。
- 整数リテラルは `Int` を満たす。
- 浮動小数リテラルは `Float` を満たす。
- `true` / `false``Bool` を満たす。
@@ -0,0 +1,24 @@
# Match Expression
match expression は、対象値を上から順に pattern と照合し、最初に一致した分岐を採用する。
```n
foo = match inputs.a.hoge {
>= 20: {
value = 200;
};
>= 10: {
value = 100;
};
_: {
value = 300;
};
};
```
`_` は fallback pattern である。
## 順序
分岐は順序付きであり、「最も具体的な pattern」を自動選択しない。
広い条件を先に書くと、後続の狭い条件には到達しない。
@@ -0,0 +1,39 @@
# Object Expression
object expression は、名前付き field の集合を表す。
```n
{
host = "127.0.0.1";
port = 8000;
}
```
object は設定値にもスキーマにも使う。
```n
MyConfig = {
host = String;
port = Int default 8080;
};
```
## Dot-path Field
ネストした field はドットパスでも定義できる。
```n
{
feature_hoge.enable = false;
}
```
これは以下と同じ構造を表す。
```n
{
feature_hoge = {
enable = false;
};
}
```
@@ -0,0 +1,15 @@
# Path Reference Expression
path reference expression は、object のフィールドを参照する式である。
```n
config.host
config.feature_hoge.enable
```
## 評価
左側の式を object として評価し、指定された field を参照する。
参照先 field は必要になるまで評価されない。
存在しない field への参照をエラーにするか、open schema として扱うかは未確定である。
@@ -0,0 +1,12 @@
# String Interpolation Expression
string interpolation は、文字列内に式を埋め込む候補機能である。
```n
"${part.greet}! ${part.target}"
```
## 評価
補間式の評価タイミングは通常の遅延評価に従う。
文字列補間を初期実装に含めるかは未確定である。
+91
View File
@@ -0,0 +1,91 @@
# 関数
関数は構造を受け取り、構造を返す式として扱う。
## 構文
```n
(part: {
greet = String;
target = String;
}) =>
"${part.greet}! ${part.target}"
```
関数呼び出しは通常の呼び出し構文で行う。
```n
mk_hw({
greet = "Hello";
target = "World";
})
```
複数引数の構文は候補として以下を想定する。
```n
(
input_a: {
hoge = Int & >= 0;
},
input_b: {
fuga = 20;
}
) =>
{
# ...
}
```
## 関数の意味
関数は値として扱える。
ただし、最終データとして関数値を出力できるかどうかは別途定める。
設定ファイルを materialize する段階では、未適用の関数値は出力不能な値として扱うのが自然である。
## 評価方針
関数は以下の方針を基本とする。
- 関数は純粋である。
- 関数はレキシカルスコープを持つ。
- 関数は定義時の環境を参照として保持する。
- 引数は必要になるまで評価しない。
- フィールドに束縛された関数呼び出し結果は、そのフィールド評価結果として memoize される。
- 任意の関数呼び出しそのものをグローバルに memoize することは必須ではない。
関数値の内部モデル例:
```text
Function {
params: Vec<Param>
body: ExprId
env: EnvRef
}
```
## 関数と重さ
関数の文法・パーサー自体は大きくない。
実装上の重さは、主に評価モデルと意味論から発生する。
注意点:
- クロージャが環境を掴む。
- 引数を lazy にするか strict にするかを決める必要がある。
- 関数呼び出し結果をどこまで memoize するかを決める必要がある。
- 再帰関数を許可するかを決める必要がある。
- 関数値同士の `&` をどう扱うかを決める必要がある。
- 関数値を object に入れたとき、materialize 可能かを決める必要がある。
- import 循環と関数適用が絡んだときの cycle detection が必要になる。
軽量に保つため、初期仕様では以下の制限を検討できる。
- 関数は pure。
- lexical closure は許可。
- 引数は thunk として渡す。
- 関数本体は必要になるまで評価しない。
- 関数値は opaque。
- 関数同士の `&` は、同一関数参照以外は conflict。
- 関数値は最終データとして出力できない。
- 再帰は cycle error として扱う、または v1 では禁止する。
+12
View File
@@ -0,0 +1,12 @@
# 言語仕様
このディレクトリは、遅延評価データ記述言語の仕様本文を章ごとに分割して管理する。
目次はマニュアル直下の [Manual Index](../index.md) に集約する。
このファイルは `Language Specification` 章の入口としてだけ使う。
## Language
言語仕様は、構文、値、式、制約、合成演算子、関数、モジュール、評価意味論、materialize、エラーを定義する。
詳細な章構成と各ファイルへのリンクは [Manual Index](../index.md) を参照する。
@@ -0,0 +1,102 @@
# materialize とエラー
通常の評価では、制約や default を含む中間値が残ることがある。
外部へデータとして出力する段階では、materialize を行う。
## materialize の責務
materialize は以下を行う。
- 必要なフィールドを評価する。
- 明示値がないフィールドに default を適用する。
- 採用された値が制約を満たすか検証する。
- 未解決の制約だけが残っているフィールドをエラーにする。
- 未適用の関数など、データとして出力できない値をエラーにする。
## 例
```n
MyConfig = {
host = String;
port = Int default 8080;
};
```
`MyConfig` を materialize すると、`host` は具体値も default もないためエラーになる。
`port``8080` が採用される。
```n
Config = MyConfig & {
host = "localhost";
};
```
`Config` を materialize すると以下になる。
```n
{
host = "localhost";
port = 8080;
}
```
## default の適用
`default` は materialize 時にのみ fallback として採用される。
```text
constraint = Int
value = none
default = 8080
```
この cell を materialize すると、`8080` が採用され、`Int` を満たすか検証される。
明示値がある場合、default は採用しない。
```text
constraint = Int
value = 9000
default = 8080
```
この cell の最終値は `9000` である。
## エラー分類
代表的なエラー:
- 構文エラー
- 未定義識別子
- 型不一致
- 制約違反
- `&` の conflict
- `default` の conflict
- 循環依存
- import 失敗
- match の非網羅による失敗
- materialize 不能な値の出力
## match の失敗
`match` に fallback 分岐がなく、どの分岐にも一致しなかった場合はエラーになる。
```n
match value {
>= 10: "large";
}
```
`value``10` 未満であれば失敗する。
## try / catch
`try / catch` は将来的な候補である。
組み込み向けの小さな核では必須ではない。
失敗は主に以下から発生する。
- assert 相当の制約検証。
- `&` による合成。
- import。
- materialize。
@@ -0,0 +1,73 @@
# モジュールと import
`import` は外部ファイルを読み込み、そのファイルの評価結果を返す。
## 構文
```n
import ./config.n
import "./config.n"
```
パス表記の詳細は未確定である。
パスリテラルと文字列リテラルの両方を許可するか、どちらかに統一するかは今後決める。
## モジュール
import 先はモジュール単位で読み込まれる。
ただし、モジュール全体を即時評価する必要はない。
各フィールドは thunk として保持され、必要になったときだけ評価される。
## 循環 import
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
例:
```n
# main.n
{
schema = {
hoge = String;
};
result = (import ./func.n)(schema);
}
```
```n
# func.n
(input: (import ./main.n).schema) =>
{
# ...
}
```
`func.n``main.n` を import しているが、参照しているのは `main.schema` である。
`main.schema``main.result` に依存していなければ、この循環 import は成立する。
## import の評価単位
実装上は、以下の単位で管理するのが自然である。
```text
Module main
schema -> thunk
result -> thunk
Module func
root -> thunk
```
各 thunk は一度だけ評価して memoize する。
評価中に同じ thunk へ戻った場合は循環依存としてエラーにする。
## import 失敗
以下は import 失敗として扱う。
- ファイルが存在しない。
- ファイルが読めない。
- import 先の構文解析に失敗する。
- import 先の評価で必要な値がエラーになる。
- 実装が禁止する import 循環に該当する。
+27
View File
@@ -0,0 +1,27 @@
# 命名規約
具体値と抽象値がグラデーションになるため、大文字・小文字による厳密な意味分けは設けない。
ただし、読みやすさのために慣習を定める。
## 推奨規約
- object 値: `lower_snake`
- 関数: `lowerCamel`
- 組み込み型・抽象的な制約名: `UpperCamel`
例:
```n
IPv4Address
MyConfig
new_config
mkConfig
```
## 厳密な規則にしない理由
この言語では、値・制約・スキーマ・派生設定が同じ式体系に乗る。
そのため、ある名前が「具体値」か「抽象的な制約」かは文脈によってグラデーションになる。
大文字なら型、小文字なら値、のような厳密な規則を設けると、実際の利用に対して過剰に硬くなる可能性がある。
+146
View File
@@ -0,0 +1,146 @@
# 合成演算子
この章では、`&``//` の意味を定義する。
## `&`: 制約合成
`&` は値・制約・構造を合成する演算子である。
```n
A & B
```
基本規則:
- 両方が制約なら、両方を満たす制約になる。
- 制約と具体値なら、具体値が制約を満たす必要がある。
- 両方が同じ具体値なら、その値になる。
- 両方が異なる具体値なら conflict になる。
- 両方が object なら、フィールドごとに合成する。
- 同じフィールドが両方にある場合、そのフィールド値を `&` で合成する。
- 矛盾が発生した場合はエラーになる。
例:
```n
Port = Int & >= 1 & <= 65535;
NarrowedPort = Port & > 443;
MyConfig = {
port = NarrowedPort default 8080;
};
Config = MyConfig & {
port = 8000;
};
```
`Config.port` は概念的には以下になる。
```text
NarrowedPort & 8000
```
`8000``NarrowedPort` を満たすため成功する。
一方、以下は失敗する。
```n
BadConfig = MyConfig & {
port = 80;
};
```
`80``> 443` を満たさないためである。
## `//`: patch 合成
`//` は右辺優先の構造的 patch 演算子である。
`&` が制約を保った合成であるのに対し、`//` は設定やスキーマを上書き・変更するために使う。
```n
A // B
```
基本規則:
- 両方が object なら、フィールドごとに再帰的に patch する。
- 同じフィールドが object/object なら、さらに再帰的に patch する。
- 同じフィールドが object/object 以外なら、右辺で置き換える。
- 左辺にしかないフィールドは保持する。
- 右辺にしかないフィールドは追加する。
- 配列はデフォルトでは右辺で置き換える。
- 関数値はデフォルトでは右辺で置き換える。
つまり `//` は shallow merge ではなく deep patch とする。
```n
Base = {
feature_hoge = {
enable = Bool default true;
fuga = Int default 10;
};
};
Patched = Base // {
feature_hoge = {
enable = false;
};
};
```
`Patched` は以下に相当する。
```n
{
feature_hoge = {
enable = false;
fuga = Int default 10;
};
}
```
ドットパスを使うと以下のようにも書ける。
```n
Patched = Base // {
feature_hoge.enable = false;
};
```
## object 全体の置換
`//` が deep patch である場合、object 全体を置き換えたいときの escape hatch が必要になる。
候補として、`replace(...)` を組み込み関数として提供する。
```n
Replaced = Base // {
feature_hoge = replace({
enable = false;
});
};
```
この場合、`feature_hoge` は再帰 patch されず、右辺の object に丸ごと置き換わる。
## `&` と `//` の使い分け
`&` は制約を満たす具体化に使う。
```n
ValidConfig = MyConfig & {
port = 8000;
};
```
`//` は既存構造の上書きや変形に使う。
```n
ModifiedSchema = MyConfig // {
port = Int default 9000;
};
```
`//` は右辺優先の patch であり、左辺の制約を常に保持するとは限らない。
制約を保持したい場合は `&` を使う。
+95
View File
@@ -0,0 +1,95 @@
# 構文と字句
この章では、表層構文の方針をまとめる。
厳密な EBNF は未確定であり、今後このファイルに詳細化する。
## コメント
コメントは `#` から行末までとする。
```n
# comment
host = "127.0.0.1"; # trailing comment
```
## セミコロン
オブジェクトフィールド、let 束縛、match 分岐はセミコロンで区切る。
末尾セミコロンは許可する。
```n
{
host = "127.0.0.1";
port = 8000;
}
```
## 識別子
識別子の厳密な字句規則は未確定である。
慣習としては `lower_snake``lowerCamel``UpperCamel` を使える想定とする。
```n
my_config
mkConfig
IPv4Address
```
## パス参照
ドットによるフィールド参照を許可する。
```n
config.host
config.feature_hoge.enable
```
オブジェクト内では、ドットパスによるフィールド定義も許可する。
```n
{
feature_hoge.enable = false;
}
```
これは以下と同じ構造を表す。
```n
{
feature_hoge = {
enable = false;
};
}
```
## 予約語候補
以下は予約語または予約構文として扱う候補である。
```text
let
in
match
import
default
true
false
rec
```
`rec` の扱いは未確定である。
## 演算子
主要な演算子は以下である。
```text
& 制約合成
// patch 合成
default fallback 指定
=> 関数
. フィールド参照 / ドットパス定義
```
演算子の優先順位は未確定である。
詳細は [合成演算子](./operators.md) で定義する。
+19
View File
@@ -0,0 +1,19 @@
# Bool
`Bool` は真偽値を表す primitive type constraint である。
## 例
```n
enable = Bool default true;
disable = Bool default false;
```
## リテラル
`Bool` が受け入れるリテラルは以下である。
```n
true
false
```
+15
View File
@@ -0,0 +1,15 @@
# Float
`Float` は浮動小数値を表す primitive type constraint である。
## 例
```n
ratio = Float;
threshold = Float default 0.5;
```
## 未確定事項
`Int``Float` の暗黙変換を許可するかは未確定である。
軽量実装では、両者を明確に分ける方が単純である。
+17
View File
@@ -0,0 +1,17 @@
# Value
この章では、言語が扱う値の分類を定義する。
primitive type は、通常のデータ値ではなく、値が満たすべき組み込み制約として扱う。
```n
name = String;
retry = Int default 3;
ratio = Float;
enable = Bool default true;
```
現在の primitive type は `String``Int``Float``Bool` である。
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
+18
View File
@@ -0,0 +1,18 @@
# Int
`Int` は整数値を表す primitive type constraint である。
## 例
```n
retry = Int default 3;
port = Int & >= 1 & <= 65535;
```
## 制約合成
数値比較制約と合成できる。
```n
NarrowedPort = Int & >= 1 & <= 65535 & > 443;
```
+20
View File
@@ -0,0 +1,20 @@
# String
`String` は文字列値を表す primitive type constraint である。
## 例
```n
name = String;
greeting = String default "hello";
```
## 制約合成
`String` は文字列制約と合成できる。
```n
message = String & /Hello! .*/;
```
正規表現制約を必須機能にするかは未確定である。