DESIGN.md@crates/kaname · git:20261107.d281d9c · 2026-11-07 · sha256 76cdf279fddf195b
DESIGN.md@crates/kaname git:20261107.d281d9cB
Immutable. This exact content never changes and is served at /api/v1/blob/76cdf279fddf195b.
# kaname 設計文書
Functional Core を書くための小さな汎用の言語。ファイルは `.kn`、コマンドは `ritsu kaname`、クレートは `crates/kaname`。プログラムのうち、入出力を持たずに計算して決める部分(状態とイベントから次の状態を決めること、入力の検証と正規化、集計と差分、木の変換、規則・日付・帳簿を呼ぶ手前と後ろの計算)を、関数、レコード、直和型、リスト・辞書・集合、文字列、ジェネリクス、関数の値で書く。型は ritsu のもの(単位、通貨と税区分、率の刻み、範囲、列挙、日付)で、範囲、丸め、int64 を、規則・日付・帳簿と同じ決まりで確かめ、関数ごとにどこまで確かめたかをエフェクトとして型に持たせる。rulec の規則、koyomi の日付の関数、chobo の帳簿の振替は、ritsu の型のまま呼べる。確かめた関数を、ritsu の一つの生成パッケージの TypeScript・Python・Go にし、Rust にもする(36 章)。
名前は要(かなめ)から取った。扇の骨を一つに留める部品である。
この文書は、ritsu 0.35.0 の上の kaname の設計である(2026-10-22)。言語(字句と構文、名前と読むファイル、型、エフェクト、範囲の解析と例を探すこと、逃げ道の警告、言語をまたぐ判定とそのための口)、コマンド(`check`、`run`、`vectors`、`doc`、`api`、`explain`)、`ritsu gen` が書く TypeScript・Python・Go・Rust、意味の中心の Lean のモデルと定理は、ここに書いたとおりに実装してある。例の 16 の `.kn`(八つの例の英語と日本語の版)は `ritsu kaname check` を警告も無く通り、その `examples` も成り立つ。そのうち四つの例は、生成したコードを TypeScript・Python・Go のホストから呼ぶプログラムも持つ(17.5)。生成したコードは、例とテストのモジュールの全部のベクタで、TypeScript・Python・Go・Rust の四つの出力先とも参照インタプリタと同じ答えを返す(16.4)。Rust の生成物は kaname の生成の関数が書き、`ritsu gen --target rust` がクレートにする(36 章)。範囲の解析と参照インタプリタは、Lean のモデルと、例とテストのモジュールの全部で同じ答えを出す(15.3)。実装したものの出力は、実物を貼ってある(4 章、5 章、7 章、8 章、9 章、10 章、11.1、12 章、13 章、14 章、15.3、16.4、17 章)。内側のファイル(`.rule`・`.cal`・`.book`)は、ritsu 0.34.1 の `ritsu check` を通し、規則の `examples` も走らせてある。丸めの五つの仕方と日の番号の計算は rulec と koyomi の手順と照らし合わせ、rulec のコーパスの `derive` と `define` の式の全部は、kaname の式として読んで型と値が rulec と同じになる(16.2、16.3)。段階ごとの作業と完了の条件は PLAN.md にある。
この文書の並び:
- 0 章:全体像。芯(入出力を持たない汎用の言語と、層の決まり)、前提、ほかの言語との分担、語。
- 1 章:書くものと書かないもの。
- 2〜3 章:文法と型。
- 4〜5 章:エフェクトと、範囲の解析。
- 6〜7 章:規則・日付・帳簿を呼ぶことと、表に書くべき判断の警告。
- 8〜9 章:参照インタプリタとベクタ。
- 10〜13 章:生成したコード、コマンド、診断、人が読むページ。
- 14 章:言語をまたぐ判定(ritsu-cross と口)。
- 15〜16 章:Lean のモデルとテスト。
- 17 章:例。
- 18〜20 章:捨てた形、まだやらないこと、決めたこと。
- 21〜28 章:汎用のコアを書く型と式。汎用の言語としての芯、範囲を書かない値、直和型と `match`、辞書と集合、文字列、ジェネリクスと `result`、関数の値、再帰する型と `private`。
- 29〜30 章:入れないものと、rulec との関係。
- 31〜36 章:あとから足した決まり(サイトのコードの色、規則の答えの範囲と `running_totals`、Lean の関係、`ritsu check` の JSON、確かめの語、Rust の出力先とパッケージの Wasm)。
## 0. 全体像
```
.kn(モジュール)── 字句・構文 ── 名前の解決(use で .rule・.cal・.book・ほかの .kn を読む。内側の層は口から)── 型
│
┌─────────────────────────────────────────────────────────────────────────────────────────────────┤
├── 範囲の解析(区間、分岐で狭める、二つの値の関係)
│ ├── kaname 自身の確かめ:渡す先の範囲、int64 のあふれ、0 で割ること、単位
│ └── 口 Programs ── ritsu-cross(X2・X3・X4・X6)── 口 Verdicts ──┐
├── エフェクト(total、失敗しうる、止まらないかもしれない)◄──────────────┘
├── 逃げ道の警告(表にすべき判断)
├── 参照インタプリタ(内側の層は口の eval)── examples・run・vectors・例を探す
├── doc(人が読むページ)・api(外のツールのための JSON)
└── 生成:ritsu gen の一つのパッケージの glue/(TypeScript・Python・Go。Rust は 36 章)
```
例(`examples/order_flow/order_flow.kn`。17.1)。注文の状態を、状態ごとにフィールドの違う直和型の値で持ち、イベントで次の状態か、イベントを拒む理由を答える:
```kn
record Line
sku : string count >=1 <=20
qty : number range >=1 <=100
price : money[USDc] range >=1USDc <=1_000_000USDc
enum Order
cart
lines : list[Line] count <=50
placed
lines : list[Line] count >=1 <=50
total : money[USDc]
on : date
paid
lines : list[Line] count >=1 <=50
on : date
shipped
lines : list[Line] count >=1 <=50
tracking : string
cancelled
reason : string
# the order an event moves it to, or why the event is refused
fn next(order: Order, event: Event) -> result[Order, string]
match order
cart c =>
match event
add a =>
if c.lines.count() < 50
ok(Order.cart { lines: c.lines.append(a.line) })
else
err("a cart holds 50 lines")
remove r => ok(Order.cart { lines: c.lines.filter(l => l.sku != r.sku) })
place p =>
if c.lines.count() = 0
err("an empty cart is not placed")
else
ok(Order.placed { lines: c.lines, total: total(lines: c.lines), on: p.on })
cancel x => ok(Order.cancelled { reason: x.reason })
else => err("a cart is placed first")
placed p =>
match event
pay y =>
if y.amount = p.total
ok(Order.paid { lines: p.lines, on: p.on })
else
err("the amount is not the total")
cancel x => ok(Order.cancelled { reason: x.reason })
else => err("a placed order is paid or cancelled")
paid d =>
match event
ship s => ok(Order.shipped { lines: d.lines, tracking: s.tracking })
else => err("a paid order is shipped")
shipped, cancelled => err("the order is closed")
```
`match` が注文のどの値にもイベントのどの値にも一度だけ答えることは、kaname が確かめる(E106)。カートに明細を足すと、明細のリストは一件長くなる。`Order.cart` のフィールドは 50 件までなので、範囲の解析は、`if c.lines.count() < 50` の中では明細が 0〜49 件で、足したあとも 50 件に収まると示す。確定するところでは、`else` の中の明細が 1 件以上なので `Order.placed` の `count >=1` に収まり、合計(`total`。明細の単価と数量の積の和)も int64 に収まる。こうしてどの場所も示せるので、`next` は宣言した範囲のどの入力にも答える(total)。注文を持ち、イベントを読み、答えで注文を置き換えるのはホストである(17.5 に、TypeScript・Python・Go のホストがある)。
規則・日付・帳簿を呼ぶ例(`examples/late_payment/late_payment.kn`。17.7):
```kn
module late_payment v1
description "What a supplier claims for an invoice paid late: the day it fell due, how many days late it was paid, and the fixed sum and the interest the rule gives for them"
use dates terms from "dates/payment_terms.cal"
use rule charges from "rules/late_payment_charges.rule"
record Invoice
id : string
invoiced : date range >=2026-01-01 <=2027-12-31
amount : money[GBPc] range >=1GBPc <=100_000_000GBPc
record Claim
due : date
days_late : number range >=1 <=1500
compensation : money[GBP]
interest : money[GBPc]
fn claim(invoice: Invoice, paid_on: date range >=2026-01-01 <=2029-12-31, reference_rate: rate[step 0.01%] range >=0% <=15%) -> Claim? total
description "None when the invoice was paid by the day it fell due"
let due = terms.due(invoiced: invoice.invoiced)
let late = paid_on - due
if late >= 1
let c = charges(debt: invoice.amount, days_late: late, reference_rate: reference_rate)
Claim { due: due, days_late: late, compensation: c.compensation, interest: c.interest }
else
none
```
期日は koyomi の日付の関数が、補償と利息は rulec の規則が決める。kaname が書くのは、そのあいだの計算(期日から支払日までの日数)と、規則を呼ぶ前に日数を規則の入力の範囲に狭める分岐と、答えの組み立てである。kaname は、`paid_on - due` が `-758` から `1431` までの日数になることを、koyomi が数えた期日の集合と `paid_on` の範囲から求め、分岐の中では `1` から `1431` までに狭まるので、規則の入力 `days_late`(`>=1 <=1500`)に収まると示す。koyomi の入力 `invoiced` も、規則の入力 `debt` と `reference_rate` も同じく示せるので、この関数はどの入力でも失敗しない。`total` と書いてあるので、kaname はそれを確かめ、示せなければエラーにする。
### 0.1 芯:入出力を持たない汎用の言語と、層の決まり
ritsu の芯は、言語と言語の境目で証明を切らないことである(ritsu の DESIGN 0.1)。規則・日付・帳簿は、それぞれの言語の検査を受けている。ところが、プログラムのうち計算して決める部分の多く(状態とイベントから次の状態を決めること、入力の検証と正規化、集計と差分、木の変換)は、ホストの言語で書かれ、どの検査の外にもある。規則・日付・帳簿を呼ぶホストのコード(規則に入力を渡す手前と、出力を組み合わせる後ろ)も同じである。
- 規則と帳簿を dandori を通さずにホストのコードでつなぐとき、`money[USD]` の規則の出力を `unit USD scale 2` の帳簿に渡す取り違えは、ritsu 0.30.0 からホストの言語の型の検査(TypeScript の `tsc --strict`、Python の `mypy --strict`、Go のコンパイラ)が止める。`ritsu gen` が書くパッケージは、単位ごとに一つの型を持つからである(ritsu の DESIGN 9.3)。それでも、そのコードが何を計算するか(丸めの書き忘れ、規則が受け取る範囲を外れる合計、規則の前提を破る途中までの合計、int64 を超える積)は、どの検査も読まない。
- 集約(カート全体の重さ、どの明細もメディアか)は、rulec が対象の外に置き、呼ぶ側で一つの値にまとめてから渡すとした部分で(rulec の DESIGN §0.1 の「集合・集約の判定」)、ホストのコードにある。rulec の `shape` と `from` は、契約の一つのコレクションのフィールドを数える・足す形までを生成する(rulec の reference §3.3)。明細ごとの掛け算、二つの入力からの組み立て、規則の出力をほかの規則に渡すことは、そこに入らない。
- 按分は、rulec の `allocate` が一明細の分を出し、明細を回すのは呼び出し側である(rulec の reference §8)。
kaname は、この計算を書くための、入出力を持たない汎用の言語である。よく言う functional core, imperative shell のうち、コアの側を言語にする。規則・日付・帳簿を呼ぶ計算は、そのコアの一つの形である。設計にあたって、ritsu のほかの言語の前提を次のように見直した。
**数学から来ていて、外しても残るもの。** rulec が完全性と重なりを厳密に決められるのは、セルが自分の列への単項テストだからである(rulec の DESIGN §0)。dandori から規則や帳簿に渡る値の範囲が正確に分かるのは、`.flow` が比較も計算もしないからである(dandori の DESIGN 0.1 の P1)。計算もループもある言語では、この種の性質は一般には決められない。だから kaname を作っても、この狭さは消えない。言語と言語の境目で守っていた狭さが、一つのプログラムの中の層の決まりに移る。
**外したもの。** rulec の P7(汎用言語にしない)は kaname には効かない。kaname は関数、再帰、レコード、直和型、リスト・辞書・集合、文字列、ジェネリクス、関数の値を持つ(3 章、21〜28 章)。ritsu の P1(言語ごとに分ける)は「プログラムは一つ、ファイルは読む人ごと」に読み替える。プログラムは `.rule`・`.cal`・`.book`・`.kn` のモジュールの集まりで、表のファイルは承認する人が、コードのファイルはエンジニアが読む。一つのファイルに表とコードを混ぜない。
**層の決まりとして残すもの。** 内側の層(rulec の表、koyomi の日付、chobo の勘定)は、今の言語のまま、今の検査をそのまま受ける。同じ診断が出て、同じページになる。内側の層から kaname の関数は呼べない(rulec・koyomi・chobo は kaname を知らない)。
**外さないもの。** rulec の P3(規則は純関数で、時刻も在庫も入力として渡す)と P5(実行時のランタイムを持たず、読めるコードを生成する)、ritsu の P3(依存の決まり)、P4(言語をまたぐ層は問いを渡して答えを受け取るだけ)、P5(結果は三つで、決められないことを黙って通さない)、P7(意味の中心を Lean でモデルにする)。kaname は入出力を持たない。入出力・状態・時刻・乱数はホストの言語に残す。
**層の決まりは、関数ごとの型に出る(4 章)。** 内側の層の関数は、宣言した範囲のどの入力にも答えを返す。rulec はそれを完全性の検査で、koyomi は範囲のすべての入力の計算で示している。kaname の関数がそうであるかは、関数ごとに違う。そこで、関数ごとに、どこまで確かめられたかをエフェクトとして型に持たせる。
- **total**:内側の層と同じく、宣言した範囲のどの入力にも答えを返すと言える。
- **失敗しうる**(`fails`):実行時に失敗しうる場所を持つ。範囲で示せず、生成したコードが実行時に確かめる場所である。規則の入力の範囲と前提、日付の関数の入力、振替の額、int64 のあふれ、0 で割ること。どの種類の失敗が起きうるかも型に載る。
- **止まらないかもしれない**(`diverges`):再帰を持つ(直接でも、ほかの関数を通してでも)。
層の決まりを守れているところ(境目で示せたところ)は total として型に出て、守れていないところは、どの種類の確かめを実行時に持ち越したかとして型に出る。人が読むページは、表にある判断、total な関数、実行時に確かめる関数を分けて見せる(13 章)。
### 0.2 前提
- **P1:入出力を持たない。** ファイル、ネットワーク、時刻、乱数、書き換えられる状態を持たない。同じ入力には同じ出力を返す。時刻や帳簿の残高が要るなら、ホストが読んで引数として渡す。
- **P2:意味を決めるのは参照インタプリタ一つ。** 内側の層は、それぞれの言語の口の `eval` で評価する。生成したコードは、参照インタプリタが作るベクタで、出力先ごとに突き合わせる。
- **P3:型と式は rulec のスーパーセット。** 型の名前、リテラル、演算子と優先順位、関数、丸めの五つ、範囲の書き方、列挙の宣言は、rulec と同じ綴りと同じ意味にする。rulec の `derive` と `define` が受け付ける式は、kaname でも同じ型と同じ値になる。広げるのは rulec が受け付けないもの(値での除算、比較の組み合わせ、日付の差、21〜28 章の型と式)だけで、広げても「丸めていない値は、丸めを書くまで金額・量・率・数として渡せない」を守る(30 章)。
- **P4:値は受け渡す整数で、int64 に収まる。** お金は宣言した単位、率は刻みの数、日付は 1970-01-01 からの日数で受け渡す(rulec と同じ)。丸めていない値は正確な有理数で持つ。収まることを範囲で示せない計算は、生成したコードが実行時に確かめ、どの出力先でも同じ失敗にする。範囲を書かない値は型の全体をとる。数、お金、量はその単位で数えた int64 の全体、日付は 0001-01-01 から 9999-12-31 である(22 章)。
- **P5:決められないことを黙って通さない。** 呼び出しの場所の確かめは、示した・例がある・決められないの三つのどれかを言う。例があればエラー、決められなければ警告で、生成したコードがその場所で実行時に確かめる。「例がある」と言うのは、その例になる関数の入力を実際に見つけたときだけである(5.7)。
- **P6:内側の層は口でだけ読む。** kaname のクレートは rulec・koyomi・chobo のクレートに依存しない(ritsu の DESIGN 3.1)。テストの dev-dependency は許す。
- **P7:言語をまたぐ判定は ritsu-cross が持つ。** 規則の前提(X2)、koyomi の日付の集合と規則の範囲(X3 の (a))、振替の額と拒否の理由(X4)、koyomi の日付の入力(X6)は、ritsu-cross の判定を kaname の呼び出しにも効かせる。単位の突き合わせ(X1)と、規則の入力の範囲との突き合わせは、dandori(E003、E014)と同じく kaname 自身の検査にする。
- **P8:再帰は書ける。止まることは確かめない。** 再帰を持つ関数は `diverges` のエフェクトを持つ。
- **P9:関数の値は、渡すことと呼ぶことにだけ使う。** ラムダと値として書いた関数は、関数の型の引数に渡すこと、`let` で名前を付けること、呼ぶことのほかに使えない。関数を答える関数も、関数の値を入れたデータも無い。ラムダの本体は、それを書いた関数の実行の中でだけ評価されるので、範囲の解析とエフェクトは、呼ぶ場所で渡した関数の中まで読めば足りる(27.1)。
### 0.3 rulec・koyomi・chobo・dandori とホストとの分担
| 言語 | 受け持つこと | kaname との関係 |
|---|---|---|
| rulec | 判断(表)。完全性、重なり、単位、丸め、int64 | kaname が呼ぶ。判断を kaname のコードに書くと逃げ道の警告になる(7 章) |
| koyomi | 期日(締め、営業日、月の足し算) | kaname が呼ぶ。日付の足し算は koyomi に書く(kaname は日付の差だけを持つ。3.4) |
| chobo | 数を動かす(勘定の上限と下限、キー、仮押さえ) | kaname は振替の呼び出しを値として組み立てて返す。当てるのはホスト(6.3) |
| dandori | 待ちと外への副作用のあるワークフロー | いまは互いに呼ばない。dandori のフローから total な kaname の関数を呼ぶことは、まだやらない(19 章) |
| ホスト(TypeScript・Python・Go・Rust) | 入出力、状態、時刻、乱数。kaname の関数に値を渡して答えを受け取り、振替を帳簿に当てる | 生成したコードを呼ぶ(10 章、17.5)。kaname からホストの関数は呼べない(29 章) |
| rulec の `shape` と `from` | 契約のコレクションの一つのフィールドを数え、足し、調べて、規則の入力にするコードの生成 | それで足りるものは rulec に書く。kaname はその外(明細ごとの計算、二つ以上の入力の組み立て、規則から規則へ)を受け持つ(1.2) |
### 0.4 ほかの言語にそろえるもの
- キーワードは英語の一種類だけ。名前(モジュール、関数、引数、レコード、フィールド、列挙と値)は日本語で書ける。生成するコードに出る名前には、rulec と同じく丸括弧で ASCII の別名を付ける(2.6)。
- 見出しは `module <名前> v1`、次に `description "…"`。`v1` は人のための目印で、生成物の頭と `api` に書き出すだけである(rulec と同じ)。
- 内側のファイルは `use rule|dates|book <名前> from "<パス>"` で読む(dandori と sekisho と同じ)。ほかの `.kn` は `use module <名前> from "<パス>"`。
- 数は rulec の単位の型で書く(`money[GBP]`、`range >=1GBPc <=100_000_000GBPc`)。日付は `date`。
- 診断の形と台帳は ritsu の土台のもの(`error[E301]: <ファイル>:<行>:<列>: …`、原文の行、注)。英語が既定で、`--lang ja` で日本語。`ritsu kaname explain` が台帳を引く。
- コマンドとフラグは一枚の表に置き、知らないフラグは exit 2(ritsu の DESIGN 4.4)。終了コードは 0、1、2。
- 例とテストは英語が先で、日本語の版を `<名前>.ja.kn` として横に置く(ritsu の DESIGN 10.10)。
- 生成物は依存の無い読めるコードで、頭に元の `.kn` とそのハッシュを書く(ritsu の DESIGN 9.2)。
### 0.5 語
- **コア**(Functional Core):プログラムのうち、入出力を持たずに計算して決める部分。kaname が書くもの。**つなぐコード**:そのうち、規則・日付・帳簿を呼ぶ手前と後ろの計算。
- **内側の層**:rulec・koyomi・chobo のファイルと、その言語の検査。kaname から見て、呼ぶ相手。
- **ホスト**:kaname の生成したコードを呼ぶ、TypeScript・Python・Go・Rust のプログラム。入出力を持つのはホストである。
- **モジュール**:一つの `.kn`。**プログラム**:`use` でつながったモジュールと内側のファイルの全部。
- **呼び出しの場所**:kaname の関数が、規則・日付の関数・ほかの関数を呼ぶところと、振替を組み立てるところ。
- **渡す先**:範囲や型の決まった、値を渡すところ。規則の入力、日付の関数の入力、関数の引数と結果、レコードのフィールド、振替の額。
- **丸めていない値**:rulec と同じ。演算が作る中間の値で、丸めを書くまで金額・量・率・数として渡せない。
- **丸めの刻み**:丸めの関数の二つ目の引数(`down(x, 1GBPc)` の `1GBPc`)。**率の刻み**(`rate[step 0.01%]` の `0.01%`)とは別のもの。
- **範囲の解析**:値がとりうる範囲を、宣言した範囲から計算で求めること(5 章)。**狭める**:`if` と `match` の分岐の中で、条件が成り立つ値に範囲を絞ること。
- **型の端**:範囲を書かない値がとる、型の全体の端。数なら int64 のいちばん小さい数と大きい数、日付なら 0001-01-01 と 9999-12-31 である。範囲の解析は、区間の端がそこから来たかの印を持つ(22.2)。**型の端から来たあふれ**:int64 を超える理由が、型の端から来る値だけにある int64 のあふれの場所。診断を出さず、生成したコードが確かめ、エフェクトに `overflow` が入る(22.2)。
- **直和型**:フィールドを持つ値が一つでもある列挙(23.1)。**値の型**:直和型の一つの値の型(`Order.placed`)。その値のフィールドを持つレコードで、値は直和型の値として渡せる(23.2)。
- **鍵にできる型**:辞書の鍵、集合の要素、並べ替えの鍵にできる型。数、お金、量、刻みのある率、日付、文字列、真偽と、フィールドを持たない列挙である(24.1)。**鍵の順**:kaname が鍵にできる型ごとに決めた一つの順。辞書と集合は、作った順にかかわらず、この順で回る(24.1)。
- **コードポイント**:Unicode のスカラー値(0 から U+10FFFF のうち、サロゲートの U+D800〜U+DFFF を除くもの)。kaname の文字列はコードポイントの並びで、長さも大小もコードポイントで決める(25.1)。
- **メソッドの形**:組み込みの関数を、初めの引数のあとに `.` で続けて書く形(`xs.sort_by(x => x.due)`)。**関数の形**:`sum(xs, x => x.qty)` のように書く形(24.3)。
- **型の引数**:関数、レコード、列挙の名前のあとの角括弧に書き、使うところで型を当てる名前(`fn first_or[T](…)` の `T`)。**総称の関数、総称のレコード、総称の列挙**:型の引数を持つもの(26.1)。**鍵に限った型の引数**:`K: key` と書いた型の引数で、鍵にできる型だけを当てられる。
- **中の関数**:ホストが呼ばず、モジュールの関数とほかのモジュールの生成物だけが呼ぶ関数。型の引数を持つ関数、関数の型の引数を持つ関数、`private` の関数がそれで、入口の確かめもベクタも持たない(21.3)。**ホストが呼ぶ関数**:それ以外の関数。
- **読み直す関数**:呼ぶところごとに、呼び出しが渡す値で本体をもう一度解析する関数。型の引数を持つ関数と、関数の型の引数を持つ関数がそれである。**読み直し**:その解析で、呼び出しの答えとエフェクトを決める(26.5、26.6、27.4)。
- **関数の値**:ラムダ(`x => …`)と、値として書いた関数(`total_by(xs: lines, amount: line_cost)` の `line_cost`)。関数の型(`(T) -> U`)の値で、渡すことと呼ぶことにだけ使う(27.1)。**関数を取る関数**:関数の型の引数を持つ関数。**通るエフェクト**(`through`):関数の型にエフェクトを書かない引数の、渡した関数のエフェクト。関数を取る関数のエフェクトには入らず、それを渡した関数のエフェクトに入る(27.5)。
- **`private`**:そのモジュールの中でだけ使う関数、レコード、列挙の印(28.2)。
- **自分を含む型**(再帰する型):フィールドの型が、リスト、辞書、集合、オプショナルな値、`result`、ほかのレコードと直和型を通して、その型自身に戻るレコードと直和型(`Category` の `children : list[Category]`)。互いを含み合う二つの型(`Doc` と `Member`)も、どちらも自分を含む型である(28.1)。
- **入れ子の深さ**:ホストから入る値の中で、自分を含む型の値がいくつ重なっているか。引数から一つの道をたどり、自分を含む型の値を一つ通るたびに一段と数える。200 段まで渡せる(28.1)。
- **`result`**:失敗を値で扱う組み込みの型 `result[T, E]`。値は `ok`(型 `T` の値を持つ)と `err`(型 `E` の値を持つ)である(26.3)。
- **例**:ある確かめが成り立たないことを示す、関数の実際の入力と、そのとき渡す先に渡る値。範囲の端は、例を探す手がかりで、例そのものではない(5.7)。
- **エフェクト**:関数の型に付く、どこまで確かめられたかの印(4 章)。**total**、**失敗しうる**、**止まらないかもしれない**の三つ。失敗の種類は `rule`・`date`・`range`・`overflow`・`zero` の五つ(4.2)。
- **逃げ道**:表に書くべき判断を、kaname のコードの `if` や `match` に書いてしまうこと(7 章)。
- 「証明」は ritsu の DESIGN 0.4 のとおり、しらみつぶしに示したものと Lean の定理にだけ使う。範囲の解析が示したことは「示した」と書き、ベクタで突き合わせたことは「確かめた」と書く。
## 1. 対象の範囲
### 1.1 書くもの
kaname が書くのは、プログラムのうち、入出力を持たずに計算して決める部分(コア)である。値はホストから引数で受け取り、答えを返す。よく書く形は次のとおりで、どれも 17 章に例がある。
1. **状態とイベントから次の状態**:状態を、状態ごとにフィールドの違う直和型の値にし、`match` で状態とイベントの組ごとに、次の状態か、イベントを拒む理由を答える(23 章、26.3。`examples/order_flow`)。
2. **集計と差分**:辞書と集合で、鍵ごとの数をまとめ、二つの数を鍵ごとに比べる(24 章。`examples/stock_diff`)。
3. **検証と正規化**:文字列をコードポイントの並びとして整え、決まりに合わない理由を `result` で返す(25 章、26.3。`examples/handles`)。
4. **木の変換**:自分を含む型の値を、関数の値を渡して作り替え、たどる(26〜28 章。`examples/catalog`)。
5. **規則・日付・帳簿を呼ぶ手前と後ろ**:内側の層を呼ぶ計算で、四つの形がある(6 章。`examples/payout` ほか)。
- **入力の組み立てと集約**:ホストのデータ(カート、請求書、明細のリスト)から、規則の入力を作る。明細ごとの掛け算と合計(品物の重さ × 数量の合計)、どの明細も満たすか、単位の換算(ドルをセントに)、二つ以上のレコードからの組み立て。
- **按分**:一つの額を明細に配る。rulec の `allocate` が一明細の分を出し、kaname が明細を回して途中までの合計を渡す。配った分の合計が配る額にちょうど一致することは、途中までの合計の渡し方で決まる(3.6、17.9)。
- **出力の組み合わせ**:規則の出力をほかの規則の入力にする、日付の関数の答えから日数を求めて規則に渡す、答えをホストに返すレコードにまとめる。
- **振替の組み立て**:帳簿のどの振替の、どの操作を、どの引数で呼ぶかを、型の付いた値として返す。当てるのはホストである(6.3)。
値に範囲を書けば、範囲の解析がその中で確かめ、書かなければ、値は型の全体をとる(22 章)。どの形でも、新しい判断(しきい値で金額や区分を選び分けること)はしない。判断を kaname に書けば、警告になる(7 章)。判断は rulec の表に書き、kaname はその答えを運び、組み合わせる。
### 1.2 rulec の `shape` と `from` との境目
rulec は、規則の入力を契約(JSON Schema か `.proto`)のどこから取るかを書け、そこから入力を取り出すコードを生成する(rulec の reference §3.3)。書ける形は七つで、パス、`any`、`all`、`count`、`sum … of <フィールド>`、`max`、`min` である。一つのコレクションの一つのフィールドを、自分の列への単項テストで絞って数える形に限られ、結合、入れ子の量化、セルの中のパスは受け付けない。
**決定**:rulec の七つの形で書けるものは rulec に書く。kaname はその外を受け持つ。
- 明細ごとに計算してから集める(重さ × 数量の合計、値引き後の額の合計)。
- 二つ以上の入力から一つの値を作る(請求書と支払日から遅れた日数)。
- 規則の出力をほかの規則や日付の関数や振替に渡す。
- 単位の換算と丸め(ドルからセントへ、ポンドの小数を刻みに)。
- 振替の値を組み立てる。
理由:rulec の射影は、入力を取り出す処理を規則の検査の内側に置いたまま生成できる。kaname に書けば、同じことをもう一つの言語で書くことになり、承認する人が読むファイル(`.rule`)から入力の出どころが消える。逆に、射影の外の計算を rulec に足すと、セルの言語が崩れる(rulec の DESIGN §0)。
### 1.3 対象の外
- **入出力**:データベース、HTTP、ファイル、時計、乱数(P1)。帳簿に書くこと(振替を当てること)もホストがする。
- **判断**:しきい値で金額・率・区分を選ぶこと(7 章)。rulec の表に書く。
- **待ちと副作用の順序**:ワークフローは dandori に書く。
- **日付の足し算と営業日**:koyomi に書く。kaname は日付の差(日数)だけを持つ(3.4)。
- **浮動小数点**:持たない。どの値も整数か、正確な有理数である。
入れないものと、その理由の全部は 29 章にある。
## 2. 文法
### 2.1 ファイルの形
一つの `.kn` が一つのモジュールである。節の順序は決まっている。見出し、`description`、`use` の行、宣言(`enum`・`record`・`fn`。この三つは混ぜて並べてよい)、`examples` の順で、`module` 以外はどれも省ける(順序が違えば E003)。`enum` と `record` と `fn` は、モジュールの中で前にも後ろにも参照できる(相互の再帰と、互いを指すレコードのため)。関係の深いレコードと関数を並べて書けるよう、三つの順は決めない。関数の本体の中の `let` は、上に書いたものだけを参照できる(rulec と同じく、前へ向かう参照は無い)。
字下げがブロックを表す。空白で書き、タブは E001。コメントは `#` から行末まで。丸括弧・角括弧・波括弧の中では改行と字下げを読まない(長い呼び出し、レコード、リストを何行にも書ける)。
### 2.2 キーワード
キーワードは英語の一種類だけで、`src/kw.rs` の一枚の表に置く(koyomi と sekisho と同じ)。
| 位置 | 語 |
|---|---|
| 行頭(モジュール) | `module` `description` `use` `enum` `record` `fn` `examples`、`private`(`enum` `record` `fn` の前。28.2) |
| `use` のあと | `rule` `dates` `book` `module` `from` |
| 型 | rulec の型の語(`number` `bool` `string` `date` `rate` `money` `mass` `length` `area` `volume` `duration` `temperature` `sound`)、`list`、`running`、`dict` `set` `entry` `indexed` `result`(すぐあとに `[` があるときだけ)、`step`、`incl_tax` `excl_tax`、`range`、`count`、`?` |
| 型の引数 | `key`(型の引数の名前のあとの `:` のあと。26.1) |
| 関数の型 | `(` `)` `->` と、答えの型のあとのエフェクト `total` `fails` `diverges` と失敗の種類(27.1) |
| 関数の見出しの終わり | `total` `fails` `diverges` と、失敗の種類 `rule` `date` `range` `overflow` `zero` |
| 本体 | `description` `let` `try` `if` `then` `else` `match` `some` `none` `true` `false` `and` `or` `not` `in` |
| 記号 | `=` `!=` `<` `<=` `>` `>=` `+` `-` `*` `/` `×` `÷` `−` `≦` `≧` `->` `=>` `:` `,` `.` `..` `(` `)` `[` `]` `{` `}`、`#` |
名前にできない語は、式と文の中で別の意味に読めてしまう 14 語だけである(`let` `try` `if` `then` `else` `match` `some` `none` `true` `false` `and` `or` `not` `in`。E002)。`try` は `let x =` のすぐあとに書き、そこでは名前と読み分けられないので、名前にできない語にした(26.3)。ほかのキーワードは、その位置でしか読まないので、名前と同じ綴りでも読み違えない。`total` も `count` も `description` も、フィールドや引数の名前にできる(`running_totals` の答えのフィールド `total` もそうである)。`else` は、`if` の続きのほかに、`match` の分岐の値の位置で、上の分岐が受けない値を全部受ける分岐を言う(23.3)。`..` は、レコードの `{` のすぐあとに書き、ほかの値のフィールドを取る(23.4)。dandori が `dates` と `book` をその位置でだけ読む語にしたのと同じ考えである(dandori の DESIGN 1.16)。
組み込みの関数の名前(`sum` `map` など。3.7、24.3、25.2)は予約しない。呼び出しの位置でだけ組み込みの関数として読む。関数の形で書く組み込みの関数と同じ名前をモジュールの関数に付けると E006 にする(どちらを呼ぶかが読み手に分からなくなるため。`numbers`、`char`、`ok`、`err` も)。メソッドの形だけで書く組み込みの関数の名前(`get`、`sort_by`、`trim`)は、モジュールの関数に付けてよい。どちらを呼ぶかは書き方で決まるからである。型の語 `dict`・`set`・`entry`・`indexed`・`result` は、すぐあとに `[` があるときだけ型の語として読むので、`set` という名前のレコードやフィールドも書ける(24.2)。`key` は型の引数のあとの `:` のあとでだけ読む。`ok` と `err` は、パターンと呼び出しの位置でだけ読む。`private` は、行頭の `enum`・`record`・`fn` の前でだけ読むので、引数やフィールドの名前にできる(28.2)。
rulec のキーワードと同じ語は、同じ意味でだけ使う。`range`(範囲の宣言)、`step`(率の刻み)、`examples`(実行できる仕様)、`description`、`not`・`none`・`true`・`false`(rulec ではセルの語で、kaname では式の語。意味は同じ)、`count`(rulec では要素を数える要約。kaname では、リスト・辞書・集合の要素の数と文字列のコードポイントの数の宣言と、それを数える組み込みの関数)、`sum`(列の合計)、`any`・`all`(rulec の射影の語。どれかが・どれもが成り立つか)、`min`・`max`、`then`(rulec の `clause` の答え。kaname の一行の `if` の答え)、`from`(ファイルから読む)。rulec の型の語 `length`(長さの次元)と重ならないよう、リストの要素の数は `length` ではなく `count` で書く。
`in` は rulec のキーワードではないが、dandori(`for x in xs`)と sekisho(`principal in clerk`)が別の意味で使っている。kaname では単位の換算(`fee in USDc`、「セントでの手数料」)にだけ使う。英語として読める語を選んだが、言語をまたいで語の意味がそろわないので、換算を関数(`convert(fee, USDc)` のような形)にする案と比べて決めた(20 章の 8)。
### 2.3 EBNF
```ebnf
file = header, [description], {use}, {["private"], (enum | record | fn)}, [examples] ; (* private は 28.2 *)
header = "module", name, version, NL ;
version = "v", digit, {digit} ;
description = "description", string, NL ;
use = "use", ("rule" | "dates" | "book" | "module"), word, "from", string, NL ;
enum = "enum", name, [tparams],
( "=", value, {("|" | "<"), value}, NL (* rulec と同じ。"<" は順序のある列挙 *)
| NL, INDENT, variant, {variant}, DEDENT ) ; (* 下に値を並べる形。23.1 *)
value = name, [string] ; (* 表示名。rulec と同じ *)
variant = value, NL, [INDENT, field, {field}, DEDENT] ; (* フィールドを持つ値。23.1 *)
record = "record", name, [tparams], NL, INDENT, field, {field}, DEDENT ;
field = name, ":", type, [bounds], NL ;
fn = "fn", name, [tparams], "(", [param, {",", param}], ")", "->", type, [bounds], [effect], NL,
INDENT, [description], block, DEDENT ;
tparams = "[", tparam, {",", tparam}, "]" ; (* 型の引数。26.1 *)
tparam = name, [":", "key"] ;
param = name, ":", type, [bounds] ;
effect = "total" | "fails", kind, {",", kind}, ["diverges"] | "diverges" ;
kind = "rule" | "date" | "range" | "overflow" | "zero" ;
type = base, ["?"] | fntype ;
fntype = "(", [type, {",", type}], ")", "->", type, [effect] ; (* 関数の型。27.1。引数のリストの中では、kind のあとの "," は、
次の語が kind で、そのあとに ":" も "(" も無いときだけ kind を続ける *)
base = "number" | "bool" | "string" | "date" | "rate", ["[", "step", literal, "]"]
| unit_word, "[", unit, ["," tax], "]" (* money[GBP, incl_tax]、mass[oz] *)
| "list", "[", type, "]"
| "running", "[", type, ",", type, "]" (* running_totals の答えの要素。3.6 *)
| "dict", "[", type, ",", type, "]" | "set", "[", type, "]" (* 辞書と集合。24.2 *)
| "entry", "[", type, ",", type, "]" | "indexed", "[", type, "]" (* 組み込みのレコード。24.3 *)
| "result", "[", type, ",", type, "]" (* 26.3 *)
| qualified, ["[", type, {",", type}, "]"] ; (* Invoice、charges.band、ledger.capture.do、Page[Invoice]、型の引数 T *)
bounds = {("range" | "count"), bound, [bound]} ; (* どちらも一度まで。順は問わない。count はリスト・辞書・集合 *)
bound = (">=" | "<=" | ">" | "<"), ["-"], (number_with_unit | date) ;
block = {let}, expr_line ;
let = "let", word, [":", type], "=", (["try"], expr, NL | NL, INDENT, block, DEDENT) ; (* try は答えに続くブロックだけ。26.3 *)
expr_line = if_block | match_block | expr, NL ;
if_block = "if", expr, NL, INDENT, block, DEDENT,
{"else", "if", expr, NL, INDENT, block, DEDENT},
"else", NL, INDENT, block, DEDENT ;
match_block = "match", expr, NL, INDENT, arm, {arm}, DEDENT ;
arm = pattern, {",", pattern}, "=>", (expr, NL | NL, INDENT, block, DEDENT) ;
pattern = "none" | "some", word | "true" | "false" | "else" (* else は残りの値の全部。23.3 *)
| ("ok" | "err"), word (* result の値と、中の値に付ける名前。26.3 *)
| qualified, [word] (* 列挙の値。フィールドを持つ値には名前を付けられる。23.3 *)
| ["-"], number_with_unit | date | string ; (* リテラル。23.3 *)
expr = or ;
or = and, {"or", and} ;
and = not, {"and", not} ;
not = "not", not | cmp ;
cmp = conv, [("=" | "!=" | "<" | "<=" | ">" | ">="), conv] ; (* 比べは一つまで。つなぐのは and と or *)
conv = sum, ["in", unit] ; (* 単位の換算。3.2 *)
sum = product, {("+" | "-"), product} ;
product = unary, {("*" | "/"), unary} ;
unary = ["-"], postfix ;
postfix = primary, {".", word | ["[", type, {",", type}, "]"], "(", [args], ")"} ; (* `xs.sort_by(…)` は組み込みの関数のメソッドの形。24.3。`f[T](…)` は型の引数を書いた呼び出し。26.1 *)
primary = literal | word | "(", expr, ")"
| "if", expr, "then", expr, "else", expr (* 一行の if *)
| qualified, "{", [(word, ":", expr | "..", expr), {",", word, ":", expr}, [","]], "}" (* レコードとフィールドを持つ値。`..` は一つ目だけ。23.4 *)
| "[", [expr, {",", expr}, [","]], "]" (* リスト *)
| "[", ":", "]" | "[", expr, ":", expr, {",", expr, ":", expr}, [","], "]" (* 辞書。24.2 *)
| lambda ;
args = arg, {",", arg}, [","] ;
arg = word, ":", expr | expr ; (* 名前で渡す(規則、日付、振替、モジュールの関数)か、順に渡す(組み込みの関数と関数の値)。逆は E107 *)
lambda = word, "=>", expr | "(", [lparam, {",", lparam}], ")", "=>", expr ; (* 式として読む。関数の値を置けないところでは E113。27.1 *)
lparam = word, [":", type] ; (* 型を書かない引数は、置かれた場所の関数の型から型が決まる *)
examples = "examples", NL, INDENT, example, {example}, DEDENT ;
example = postfix, ("=", expr | "fails", kind), NL ; (* 関数の呼び出し。深く字下げした行は前の行の続き *)
name = word, ["(", alias, ")"] ; (* 日本語の名前と ASCII の別名 *)
qualified = word, {".", word} ;
literal = ["-"], number_with_unit | date | string | "true" | "false" | "none" ;
```
`×`・`÷`・`−`・`≦`・`≧` と全角の数字は、rulec と同じく `*`・`/`・`-`・`<=`・`>=` と半角の数字として読む。数のリテラルは rulec と同じ書き方である(`_` で桁を区切れる、`万`・`億`・`兆`、単位をすぐあとに付ける、`,` での桁の区切りは E001。rulec の E049 と同じ理由)。文字列は `"` で囲み、エスケープを持たない(rulec と同じ)。
定数のすぐ前の `-`(`-18℃`、`-1USD`)は、どの単位でも零より下の定数である(`unary` の `-` と定数。3.10)。
`if` の行に `then` が無ければ、下に字下げしたブロックを取る形(`if_block`)として読み、`then` があれば一行の `if` として読む。`let x =` で行が終われば、値は下に字下げしたブロックである。ブロックの答えのあとに行があれば E111 にする(途中の値には `let` で名前を付ける)。左端に書けない語で始まる行や、順序の違う行(E003)の下の行は、その行と一緒に読まずに飛ばす。
### 2.4 rulec との対応
| | rulec | kaname |
|---|---|---|
| 型の名前 | `money[JPY, incl_tax]`、`mass[g]`、`rate[step 1%]`、`rate`、`number`、`date`、`bool`、`string`、`T?`、列挙 | 同じ。足すのは `list[T]`、`dict[K, V]`、`set[T]`、レコード、規則・日付・振替の型(3.8、24.2) |
| リテラル | `1200JPY`、`2kg`、`3%`、`2026-04-01`、`1000万円` | 同じ |
| 演算子と優先順位 | 比べ(`<= >= < > =`)、`+ -`、`* /` | 同じ。足すのは `!=`、`and`・`or`・`not`、`in`(単位の換算)。比べは一つの式に一つまでで、`a < b < c` は E001。`=` と `!=` は、レコード・リスト・直和型・オプショナルな値も中身で比べる(23.5) |
| 関数 | `min`、`max`、`mod`、`isqrt`、`allocate`、丸めの五つ(`up`・`down`・`half_up`・`half_down`・`half_even`) | 同じ綴り、同じ意味(`mod` は次の行)。足すのは、辞書・集合・リストと文字列の組み込みの関数(3.7、24.3) |
| `mod` | 割る数は正の整数の定数(E115)。答えは床の余り | 割る数は定数でも値でもよい整数で、余りは 0 以上、割る数の絶対値より小さい(ユークリッドの形)。割る数が正の定数なら同じ答え。0 をとりうる割る数は `zero` の場所(24.3) |
| 範囲 | `range >=… <=…` | 同じ。リストの要素の数と、辞書と集合の組と要素の数は `count >=… <=…` |
| 列挙の宣言 | `enum x = a \| b`、`<` で順序、表示名、`default`、`open` | 同じ。ただし `default` と `open` は E003(kaname には表も寄与も無いので、意味を持たない)。足すのは、値を下に一行ずつ並べる形と、フィールドを持つ値(直和型。23.1) |
| 文字列 | 引用符のあいだをそのまま読み、エスケープを持たない | 同じ。足すのは、文字列の大小(コードポイントの順)と、コードポイントで数える組み込みの関数(25 章) |
| 割り算 | 定数で割る。値で割れるのは丸めの一つ目の引数のいちばん外の演算だけ(E115)と `allocate` | どこでも値で割れる。0 になりうる割る数は、範囲の解析で示せなければ実行時に確かめる(`zero`)。答えは丸めていない値 |
| 日付 | 比べと範囲だけ(E048) | 足すのは日付の差(`b - a` が日数の `number`)。日付の足し算は持たない(3.4) |
| 真偽 | `define` の条件は、一つの比べ(E113) | 比べを `and`・`or`・`not` で組み合わせられる |
| 丸めていない値 | 演算が作り、丸めの関数だけが丸めた値に戻す(rulec の DESIGN §2.4) | 同じ。丸めていない値は、関数の引数と結果、レコードのフィールド、規則・日付の入力、振替の額に渡せない(E103) |
rulec の `derive` と `define` の右辺は、kaname の `let` の右辺にそのまま書け、同じ型と同じ値になる。`define x : money[JPY] = 式` の型の書き方(丸めていない値でも次元と単位だけを書く)は、kaname の `let x : money[JPY] = 式` と同じに読む。これを rulec のコーパスの全部の式で確かめる(16.2)。
kaname が受け付けない rulec の書き方は、式の外のもの(表、セル、`inputs`、`outputs`、`fold`、`machine` など)だけである。
### 2.5 内側のファイルとほかのモジュールを読む宣言
```kn
use rule card_fee from "rules/card_fee.rule"
use dates terms from "dates/payment_terms.cal"
use book ledger from "books/ledger.book"
use module pricing from "pricing.kn"
```
**決定**:dandori と sekisho の `use <種類> <名前> from "<パス>"` にそろえる。種類の語は、そのファイルの一行目の語(`rule`・`dates`・`book`・`module`)である。パスは、書いたファイルのディレクトリからの相対で、区切りは `/`。ファイルが無いとき、拡張子が種類に合わないとき(`.rule`・`.cal`・`.book`・`.kn`)は E201 にする。読めなかったファイルの中のものを指す名前と型は、`use` の行の E201 のほかには言わない。
理由:同じプロジェクトの `.flow` と `.gate` が同じ書き方で同じファイルを読むので、読む人は一つの書き方を覚えればよい。ritsu の参照の書き方(`rulec "rules/card_fee.rule"`。ritsu の DESIGN 6.2)は、ファイルの中の一つのものを指すための形で、ツール名とパスと種類と名前を並べる。kaname は、読んだファイルの中のものを、`use` に付けた名前のあとに `.` で書く(`card_fee(…)`、`terms.due(…)`、`ledger.capture.do(…)`、`charges.outputs`)。`use` の名前はモジュールの中の名前で、規則と日付のファイルと帳簿とモジュールが名前を共有する(同じ名前は E006)。
- `use rule`:rulec の規則を読む。規則の事実(入力と出力の型と範囲、列挙、前提、並び、生成したコードの呼び方)は口 `Rules` の `facts` から受け取る。rulec の検査を通らない規則は E201 で、rulec が言うことを注に挙げる(dandori の E005 と同じ形)。出力の無い規則(rulec の `enums` のファイル)は、その列挙を型として使うために読める。呼ぶためのコードは生成されないので、その規則を呼ぶところと、出力を型に書くところは E201 である(2.7)。寄与のファイル(`contribution`)は読めない。寄与は `ritsu check` と `ritsu gen` がプロジェクトの規則に合わせる(ritsu の DESIGN 8.1)ので、kaname は合わせた規則を読む。
- `use dates`:koyomi の日付のファイルを読む。`calendar` のファイルは読まない(19 章)。
- `use book`:chobo の帳簿を読む。
- `use module`:ほかの `.kn` を読む。読んだモジュールは、それだけで検査し(一回の実行で一度)、検査を通らなければ E201 にして、その診断を三つまで注に挙げる。読み込みが輪になれば E203 で、輪の中のモジュールはどれも E203 を言う。プログラムは、モジュールを点、`use module` を辺とする、輪の無いグラフである。関数の相互の再帰は一つのモジュールの中でだけ書ける。
kaname のクレートのバイナリ(`kaname`)は内側の言語を持たない。`use rule`・`use dates`・`use book` のあるモジュールは、最初のその行で E202 を出し、`ritsu kaname` で走らせるよう注に書いて exit 2 で終わる(dandori の E018、sekisho の E209 と同じ)。`use module` で読むモジュールが内側の言語を読むときも、その `use module` の行で E202 にする。E202 のほかの診断は出さない。`use module` だけのプログラムは、クレートのバイナリでも全部の機能で動く。
### 2.6 名前と別名
名前は rulec と同じ字句で、漢字、かな、ラテン文字が使える。名前は書いたとおりに比べ、NFC に正規化しない。rulec も、依存を持たず Unicode の表を持たないので、正規化を約束していない(rulec の DESIGN §15.70)。生成するコードに出る名前(モジュール、関数、引数、レコード、フィールド、列挙、列挙の値)が ASCII でなければ、丸括弧で ASCII の別名を付ける(無ければ E005。別名が ASCII の名前でなくても E005)。ASCII の名前には要らない。別名か、別名の無い名前が、出力先(TypeScript の strict mode、Python、Go)の予約語か、補助のモジュールの名前(`kaname`、`KanameError` など。10.1)なら E007 にする。Rust の予約語は生の識別子(`r#ref`)にするので E007 にしない。モジュールの名前が `crate`、`self`、`Self`、`mod`、`door`(Rust がモジュールに付けられない語と、kaname が Rust の `glue/` に書くファイルの名前)なら、壊れるのは Rust だけなので、`check` は W001 の警告にし、`ritsu gen --target rust` だけが E008 で止まる(36.3)。`let` の名前とラムダの引数は、生成するコードの関数の中の名前なので、別名を付けない(rulec が内部の名前に日本語の識別子をそのまま使うのと同じ。TypeScript・Python・Go のどれも、関数の中の日本語の識別子を受け付け、Rust もクレートの根で字の lint を許せば受け付ける。36.8)。出力先が名前に取らない字を持つ名前(`m²`)は、その出力先の生成物では `v` になる(36.3)。
**決定(宣言したものは、名前でも別名でも指せる)**:レコードとフィールド、列挙とその値、関数とその引数、規則の入力と出力は、宣言した名前でも別名でも指せる(sekisho と同じ)。英語の版と日本語の版が同じ別名を持つので、日本語の版の中で英語の別名を使っても同じものを指す。そのかわり、名前と別名は一つの名前空間にあり、ほかの名前とも別名とも重なれない(E006)。規則の列挙とその値も、規則のファイルに書いた名前か別名で指す(`due.緊急度`、`due.urgency`、`due.urgency.later`)。口は、列挙の名前と、規則が括弧の中に書いた別名(列挙の公開名、`urgency`)を渡す(`RuleEnum::public`。14.1)。生成するコードの型の名前(`Urgency`)では指さない。
**決定(モジュールの名前空間は一つ)**:`use` の名前と、`enum`・`record`・`fn` の名前と別名は、一つの名前空間にある(重なれば E006)。関数と `use` には、組み込みの関数の名前を付けられない(E006)。関数の中の名前(引数、`let`、ラムダの引数、`some` の名前)は、その場で見えるほかの名前と重なれず、`use` の名前も取れない(E006)。kaname の値は書き換えられないので、一つの名前が関数の中で一つのものを指すようにした。並んだ二つのブロックの中の同じ名前は、別の名前である。呼び出しは関数、規則、組み込みの関数を指し、名前の値は呼べないので、`let count = count(lines)` と書いたあとも `count(…)` は組み込みの関数である。
関数の名前の別名は、名前のすぐあとの丸括弧に書く。引数の丸括弧と続くが、別名の丸括弧の中は語一つで `:` を含まないので、読み違えない。
```kn
fn 遅れの請求(claim)(請求書(invoice): 請求書, 支払日(paid_on): date range >=2026-01-01 <=2029-12-31) -> 請求? total
…
```
関数の中の名前(引数、`let`)と型の名前は、別の名前空間にある。型を書く位置と値を書く位置は構文で決まるので、`請求書(invoice): 請求書` のように、引数とその型に同じ語を使える。日本語では大文字と小文字で分けられないためである。
英語の版と日本語の版(`<名前>.kn` と `<名前>.ja.kn`)は、モジュールの別名だけを変え(`late_payment` と `late_payment_ja`)、ほかの別名をそろえる。二つは一つのパッケージに並んで入り、生成するコードの名前は、モジュールの名前のほかは同じになる。sekisho の例の二つの版と同じ形である(sekisho の DESIGN 5.7)。
### 2.7 出力の無い規則の `use rule` と、フィールドの無い値の E101 の注(2026-10-21)
**きっかけ**:二つある。一つは、出力の無い規則(rulec の `enums` のファイル)を `use rule` で読むモジュールが、その規則を呼んでも検査を通ったことである。rulec は `enums` のファイルから型のモジュールだけを生成し(rulec の DESIGN §15.212)、口はそのファイルに Connect のサービスを答えない(rulec の DESIGN §15.258)。呼ぶためのコードが無いのに、kaname はそれを呼ぶ関数を total と言った。`let r = regions()` だけを書いた関数と、その例を持つモジュール(`called.kn`)を、この直しの前の kaname で検査したもの(実物):
```text
ok called.kn: 1 function, total
```
もう一つは、E101 の注が、並べるものの無いフィールドの一覧を、空のまま文に入れたことである。出力の無い規則の出力を読むモジュール(ritsu の `tests/projects/regions/region.kn`)と、フィールドの無い直和型の値のフィールドを読む関数を、この直しの前の kaname で検査したもの(実物):
```text
error[E101]: region.kn:7:13: There is no field `region` in regions.outputs
7 | regions().region = honshu
= The fields of regions.outputs are .
```
```text
エラー[E101]: sum.kn:9:5: Order.cancelled にフィールド `reason` はありません
9 | c.reason
= Order.cancelled のフィールドは です。
```
**原因**:`use rule` は、ファイルが読めるかと、規則が rulec の検査を通るかだけを見ていた(`src/check.rs` の `uses`)。出力の無い規則も、フィールドの無いレコード `<規則>.outputs` を答える規則として読むので、出力を読まない呼び出しは何も言われなかった。E101 の注は、フィールドの名前を `, ` でつないで文に入れるだけで、名前が一つも無いときの文を持たなかった(`src/types.rs` の二か所)。
**決定**:
- 出力の無い規則は、`use rule` で読め、その列挙を `<規則>.<列挙>` と書いて型として使える。生成したコードは、その規則の型のモジュールを読むだけで、呼ぶためのコードは要らない。その規則を呼ぶところ(`src/types.rs` の `call`)と、出力を型に書くところ(`<規則>.outputs`。`src/names.rs`)を E201 にする。dandori が `enums` のファイルを呼ぶところを、呼び方によらず E005 で止めるのと同じ考えで(dandori の DESIGN 10 章)、kaname の台帳の、読むファイルを読めないときのコードを使う。注には、読めるのはその列挙で、型として使うことを書く。
- 呼べない規則の答えのように、置かれた場所の型がもう誤りと言われたところの名前(`regions().region = honshu` の `honshu`)は、どの列挙の値かが分からないので、E101 を言わない。一つの誤りを一度だけ言う。
- E101 の注は、フィールドが一つも無ければ「Order.cancelled has no fields.」「Order.cancelled にはフィールドがありません。」と言う(`fields_note`)。フィールドがあれば、これまでどおり並べる。
**捨てたもの**:
- 出力の無い規則を `use rule` で読めば、その行で E201 にすること(列挙の型だけを使うモジュールも止める)。列挙の型だけを使うモジュールは、生成したコードが規則の型のモジュールを読むだけで害が無く、止めると、規則の列挙をモジュールの型に書けなくなる。
- E101 の注を、フィールドが無ければ出さないこと。注の無い E101 は、名前を書き違えたのか、そもそも読めるフィールドが無いのかを言わない。
**変わること**:
- 出力の無い規則を呼ぶモジュールは、呼ぶところで E201 になる。前は、呼ぶだけなら total、出力を読めば E101 だった。列挙の型だけを使うモジュールは、これまでどおり通る。木の中では、ritsu の `tests/projects/regions` の `region.kn` と `region.ja.kn` の `check`(英日と JSON)・`vectors`・`doc`・`api` の出力が変わる(7 行目の E101 二つが、7 行目の E201 一つになる)。ritsu の `tests/enums_files.rs` の期待を替え、列挙の型だけを使う材料(`region_kinds.kn` と `region_kinds.ja.kn`)を足した。
- E101 の注の文(フィールドの無い値と、出力の無い規則の出力)。
- 台帳の E201 の「いつ出るか」と「直し方」に、出力の無い規則を呼ぶことと、出力を型に書くことを足した。
**確かめたこと**:
- `tests/names.rs` の `a_rule_with_no_outputs_is_read_for_its_enums_and_not_called`:英語の `enums` のファイルと日本語の版で、列挙の型だけを使う二つのモジュールが通り、呼ぶ、出力を読む、出力を型に書く、の五つのモジュール(英語が三つ、日本語が二つ)が、呼ぶところか型を書いたところの E201 一つだけで止まり、英日の文が下のとおりであること。
- `tests/names.rs` の `a_value_with_no_fields_says_it_has_none`:フィールドの無い直和型の値(`Order.cancelled`、`注文.取消`)のフィールドを読む E101 の注が、英語と日本語で下のとおりになり、フィールドのあるレコードはこれまでどおり並べること。
- ritsu の `tests/enums_files.rs`:`region.kn` と `region.ja.kn` が 7 行目の E201 一つで止まり、英日の文が出ること。`region_kinds.kn` と `region_kinds.ja.kn` が通ること。dandori の E005 と sekisho の E101 の期待は、この直しでは変わらない。列挙の型だけを使うモジュールの `ritsu gen` が、三つの出力先で `tsc`、`mypy --strict`、`go vet` を通ることも、手元で確かめた。
- 直しのあとの実物(`called.kn` と、上のフィールドの無い値):
```text
error[E201]: called.kn:6:11: The rule `regions` has no outputs, and is not called
6 | let r = regions()
= Nothing to call is generated from a rule with no outputs (an `enums` file). What a module reads it for is its enums, written `regions.<enum>` where a type goes.
エラー[E201]: called.kn:6:11: 規則 `regions` には出力が無いので、呼べません
6 | let r = regions()
= 出力の無い規則(`enums` のファイル)からは、呼ぶためのコードが生成されません。読めるのはその列挙で、`regions.<列挙>` と書いて型として使います。
```
```text
error[E101]: sum.kn:9:5: There is no field `reason` in Order.cancelled
9 | c.reason
= Order.cancelled has no fields.
エラー[E101]: sum.kn:9:5: Order.cancelled にフィールド `reason` はありません
9 | c.reason
= Order.cancelled にはフィールドがありません。
```
**見つけて直していないもの**:無い。
## 3. 型
### 3.1 型の一覧
| 型 | 書き方 | 値(受け渡す形) |
|---|---|---|
| 数 | `number` | 単位の無い整数 |
| お金、量 | `money[GBP]`、`money[USDc, excl_tax]`、`mass[oz]`、`duration[d]` など(rulec の型の全部) | 宣言した単位で数えた整数 |
| 率 | `rate[step 0.01%]`、`rate` | 刻みの数(刻みの無い `rate` は、`let` にだけ書ける。3.9) |
| 順序だけの量 | `temperature[℃]`、`sound[dB]` | 整数。比べと範囲だけ(rulec と同じ) |
| 日付 | `date` | 1970-01-01 からの日数。JSON では `"YYYY-MM-DD"` |
| 真偽 | `bool` | |
| 文字列 | `string` | コードポイントの並び。JSON の文字列(25.1) |
| 列挙 | モジュールの `enum`、規則の列挙 `<use の名前>.<列挙>` | 値の名前 |
| 直和型 | フィールドを持つ値のあるモジュールの `enum`(23.1) | フィールドの無い値は値の名前、フィールドを持つ値は `{"<値の名前>": {フィールド}}`(23.6) |
| 値の型 | `<列挙>.<値>`(`Order.placed`。23.2) | 直和型の値と同じ形 |
| レコード | `record` で宣言したもの(自分を含んでよい。28.1)、`<モジュール>.<レコード>` | フィールドの名前ごとの値 |
| リスト | `list[T]` | 並び |
| 辞書 | `dict[K, V]`(`K` は鍵にできる型。24.1) | 鍵の順の組の配列 `[[鍵, 値], …]`(24.4) |
| 集合 | `set[T]`(`T` は鍵にできる型) | 鍵の順の要素の配列 |
| 辞書の組、番号の付いた要素 | `entry[K, V]`(`entries` の答えの要素)、`indexed[T]`(`indexed` の答えの要素。24.3) | `key` と `value`、`index` と `item` |
| オプショナルな値 | `T?` | 値か `none` |
| `result` | `result[T, E]`(26.3) | `{"ok": 値}` か `{"err": 値}` |
| 型の引数 | 総称の宣言の中の `T`(26.1) | 当てた型の値 |
| 総称のレコードと列挙に型を当てた型 | `Page[Invoice]`、`Found[string]`(26.1) | 型の引数を当てたフィールドのレコード、直和型の値 |
| 規則の出力 | `<規則>.outputs` | 出力の名前ごとの値(3.8) |
| 規則の並びの一要素 | `<規則>.<並び>` | 要素のフィールド(3.8) |
| 振替の呼び出し | `<帳簿>.<振替>.<操作>` | 帳簿、振替、操作、引数(3.8、6.3) |
| 途中までの合計 | `running[T, W]`(`running_totals` の答えの要素。3.6) | `item`、`before`、`upto`、`total` |
| 関数の型 | `(T, U) -> R`、`(T) -> R total`(27.1) | 受け渡さない。関数の型の引数と `let` にだけ置ける(27.1) |
### 3.2 数と単位
数の型と演算の決まりは rulec のものである(rulec の reference §3、§8、DESIGN §2.3)。
- 単位は型の一部で、ritsu-units の表で読む(`Unit::parse`、同じ単位かは `Unit::same`)。`JPY` と `円` は同じ単位、`kg` と `g` は違う単位、税込と税抜は違う型である。
- 足し引きと比べは同じ型どうしだけ。違う単位や違う税区分を混ぜれば E102 で、rulec の E103 と同じ理由である。次元の掛け算(`mass × length`、`money × money`)と、数や率を量で割ることは E105。温度と音量は比べと範囲だけで、演算は E105(rulec と同じ)。
- 数(`number`)と率は、金額や量に掛けられる。数と率の積は率である(rulec と同じ)。定数でも値でも割れる。金額や量を同じ単位の金額や量で割ると比になり、比は、丸めの刻みや渡す先の型に合わせて `number` か率になる。
- **リテラル**は、書いた単位の整数で書く(`0.5USD` と単位の無い `0.4` は E104。割合は率で `40%` と書く。rulec と同じ)。比べたり足したりする相手の単位で読み、税区分も相手のものになる(`mass[oz]` と比べる `15lb` は 240 オンス、`money[GBP, incl_tax]` に足す `40GBP` は税込)。温度は単位ごとに零点が違うので、零点も読み替える(`temperature[℉]` と比べる `30℃` は 86℉、`-20℃` は −4℉、`temperature[℃]` の引数に渡す `41℉` は 5℃)。温度は比べと範囲だけの量なので、黙って読み替えるのは定数だけである(値は `in` で換算する。3.10)。定数のすぐ前の `-` は、どの単位でも零より下の定数である(`t <= -18℃`。3.10)。相手の単位で整数にならなければ E104(`mass[g]` と比べる `1lb`)。単位の無い `0` は、どの単位の 0 としても読む(`amount > 0`)。rulec の式はリテラルを書いた単位で読み、違う単位なら E103 にするので、ここは rulec から広げたところである。
**決定(丸めているかは、値の刻みで決める)**:値の型は、単位に加えて、値がどの刻みの整数倍になるかを持つ(rulec の §15.190 の刻み)。宣言した型の値は、単位の整数(率は宣言した刻み)の上にある。足し引きの答えは二つの刻みがともに割り切る刻みに、掛け算の答えは刻みの積に、定数での割り算の答えは刻みを定数で割ったものに、丸めの答えは丸めの刻みに乗る。値での割り算の答えは、刻みが分からない。渡す先(関数の引数と答え、レコードのフィールド、規則と日付の入力、振替の引数、`running_totals` の重み)は、そこに渡す値が、その単位の整数か率の刻みの上にあることを求める(`running_totals` の重みの率には、刻みを与える宣言が無いので、重みの型が言う刻みを求める。3.6)。そうでない値と刻みの分からない値が**丸めていない値**で、渡せば E103 にし、注に丸め方と刻みの書き方を出す(`half_up(amount * 3%, 1USDc)`)。丸めていない値は正確な有理数で、比べにはそのまま使える。`let` には丸めていない値を入れられる。`let x : money[GBP] = …` の型は次元と単位を書くだけで、刻みは式から決まる(rulec の `define` と同じ)。
式の形(「金額 × 率は丸めていない」)でなく刻みで決めたのは、rulec が `define` の値を刻みで持ち、スーパーセットのテスト(16.2)が rulec の付ける型と比べるからである。式の形の決まりはこの決まりの一つの場合で、そのほかに、金額に 200% を掛けた値は単位の整数のままなので、そのまま渡せる。
**決定(`let` に書いた型は、値の型が決めていないことを決める)**:`let x : T = 式` に書いた型は、二つのことを値の型に代わって決める。一つは、無次元の値が数か率かである。数と率はどちらも無次元で、値も同じ分数なので、書いた型が `rate` なら率として、`number` なら数として受ける(`let ratio : rate = total / 50`、`let pts : number = base * multiplier`)。もう一つは金額の税区分で、税区分の書かれていない金額は書いた型の税区分を受け、税区分のある金額を税区分の無い型の `let` に入れれば、税区分の無い金額になる(`let cost : money[JPY, incl_tax] = 10_000JPY * rate`。リテラルから作った金額には税区分が無い)。rulec が `define` と `derive` の宣言にだけ許していることと同じで(rulec の DESIGN §2.1 の無次元の値の決定と、`check_same`)、rulec のコーパスの 8 行がこの書き方を使っている(16.2)。ほかの渡す先と、足し算と比べには許さない。率と数を足すことと、税区分の違う金額を比べることは E102 である。
**値での割り算**(rulec から広げたもの):`a / b` の `b` は値でよい。答えは丸めていない値で、型は rulec の割り算の決まり(同じ単位どうしなら比、`number` や率で割れば左の型)のとおりである。`b` が 0 になりうると範囲の解析で示せない割り算は、生成したコードが割る前に確かめ、失敗の種類は `zero` である(5.6)。
**単位の換算 `in`**(rulec から広げたもの):`x in USDc` は、`x` を同じ次元の別の単位で表す。換算の係数は ritsu-units の表の正確な有理数で、刻みも同じ係数で換わる。答えが整数にならないことがあれば丸めていない値になる(`fee in USDc` はドルをセントにするので整数のまま、`weight in lb` はオンスをポンドにするので丸めていない値)。温度は零点も換算する(`t in ℉` は `t × 9/5 + 32`、`0℃ in ℉` は 32℉、`-20℃ in ℉` は −4℉)。換算した値は零点のずれだけ動くので、刻みは、係数を掛けた刻みとずれがともに整数倍になる刻みである(℃ の整数を ℉ で読んだ値は 1/5 の倍数。3.10)。単位の無い数と率は換算できない(E104)。通貨の違うお金(`GBP` と `USD`)、税込と税抜、次元の違う量のあいだには換算が無い(E104。為替の率は ritsu の中に無い。rulec の §15.18)。
**決定(`in` を明示の換算にする)**:暗黙の換算はしない。dandori が境目で換算しない(dandori の DESIGN 1.2)のと同じく、`money[USD]` を `money[USDc]` の渡す先に黙って渡せば、どこで 100 倍したかが読めなくなる。ドルをセントの帳簿に渡すのは E102 で、注が `in USDc` を勧める(17.6)。
### 3.3 丸め
丸めの関数は rulec の五つ(`up`・`down`・`half_up`・`half_down`・`half_even`)で、二つ目の引数が丸めの刻みである。刻みは正の定数で書き(そうでなければ E104)、丸める値の単位で読む(`down(x, 1GBPc)`。`money[GBP]` の値を `down(x, 100GBPc)` で丸めれば 1 ポンドの刻み。違う次元なら E102)。比を丸めるときは、刻みが `number`(`1`)なら答えは `number`、率(`0.1%`)なら率になる(rulec の、値での割り算を含む丸めと同じ)。負の値での向きも rulec と同じで、`up` は 0 から遠ざけ、`down` は 0 へ寄せ、半分ちょうどは `half_up` が 0 から遠ざけ、`half_down` が 0 へ寄せ、`half_even` が偶数へ寄せる(rulec の reference §4 の表)。
**決定(丸めを kaname に持つ)**:rulec の `RoundMode` と `round_to`(`crates/rulec/src/num.rs`)は rulec のクレートの中にあり、kaname は依存の決まりで使えない(ritsu の DESIGN 3.1。ritsu-units には丸めを置いていない。ritsu の DESIGN 5.1)。kaname は自分の丸めを持ち、rulec と同じ答えになることを、dev-dependency の rulec を使うテストで確かめる(16.3)。ritsu-units に移すかは、あとで決める。
参照インタプリタと生成するコードの丸めは、どちらも整数だけで計算する同じ手順にする。有理数 `n/d`(`d > 0`)を刻み `g = gn/gd`(`gn, gd > 0`)に丸めるとき、`x = n·gd`、`y = d·gn` として、`|x|` を `y` で割った商 `q` と余り `r` を求め、余りが 0 でなければ、向きごとに `q` か `q + 1` を選ぶ(`up` はいつも `q + 1`、`half_up` は `2r ≥ y` で、`half_down` は `2r > y` で、`half_even` は `2r > y` か、`2r = y` で `q` が奇数のときに `q + 1`)。符号を戻し、刻みを掛ける。言語の素の割り算(Python の `//` は負の無限大へ、Go は 0 へ)を使わず、絶対値の上で割るので、どの出力先でも同じ答えになる(rulec の DESIGN §7.3 と同じ理由)。
この手順(`src/num.rs`)が rulec の `round_to` と同じ答えになることは、`tests/rounding.rs` が rulec の `round_to` を直に呼んで確かめている。分子 −300〜300、分母 1〜12 の有理数と、刻み 1・10・1/2・1/100・3・5/2・100・1/10・5・1/1000、五つの向きの組の全部(360,600 通り)で、一つも食い違わない(16.3)。
### 3.4 日付
`date` は、タイムゾーンを持たない暦の一日である。値は 1970-01-01 からの日数で持ち、JSON と表示では `YYYY-MM-DD` で書く。比べと範囲は rulec と同じに書ける。
**日付の差**(rulec から広げたもの):`b - a` は、日付 `a` から日付 `b` までの日数で、型は `number` である。rulec は日付の引き算を E048 で止め、「差が判断に要るなら呼ぶ側で求めて渡す」と書いている(rulec の reference §3)。その呼ぶ側が kaname である。日数を `number` にしたのは、規則が日数を受け取る入力を `number` で宣言しているからである(rulec のコーパスの猶予の残り日数など)。
**決定(日付の足し算は持たない)**:`date + number` は E105 にする。日付に日数を足すこと、月を足すこと、営業日を数えることは、koyomi が範囲のすべての日で確かめる計算である(koyomi の DESIGN 0.1)。kaname に足し算を置けば、koyomi を通らない期日の計算が書ける。日数を入力にとる日付は、koyomi の整数の入力で書ける(`+ n days`。koyomi の DESIGN 1.6)。
日の番号と年月日のあいだの換算は、Howard Hinnant の手順(`days_from_civil` と `civil_from_days`)を kaname に持つ(`src/date.rs`)。ritsu-ports の `day_text`、koyomi、rulec と同じ手順で、0001-01-01 から 9999-12-31 までの全部の日(3,652,059 日)で、四つとも同じ日の番号と年月日になる(`tests/dates.rs`。16.3)。
### 3.5 列挙
列挙の宣言は、rulec と同じ一行の形(`enum tier = basic < silver < gold`)と、`enum` の行の下に値を一行に一つずつ並べる形がある。下に並べた値には、レコードと同じ形でフィールドを書け、フィールドを持つ値のある列挙が直和型である(23 章)。順序のある列挙(一行の形の `<`)は、比べと `min`・`max` に使える。表示名(`"…"`)も書ける。`default` と `open` は E003 で、注に、表を持たない kaname では意味を持たないことを書く。
**決定(列挙は、どこで宣言したかで決まる)**:ritsu の DESIGN 5.6 のとおり、列挙の型は宣言した場所で決まる。モジュールの `enum tier = …` と、規則の列挙 `charges.band` は、値の名前が同じでも別の型である。
- 規則の列挙は `<use の名前>.<列挙の名前>` で書き、値もそのまま使える。kaname のコードの中で規則の列挙を使えば、読み替えは要らない。
- 二つの規則が同じ `enums` のファイルから同じ列挙を丸ごと読んでいれば(rulec の `import enum … from`)、二つは同じ型である。口の `RuleEnum::origin`(14.1)が、読んだ元のファイルとそこでの列挙の名前を言う。kaname はファイルを正規のパスにして比べ、値の並びも同じときに同じ型と読む。寄与が値を足した規則は値の並びが違うので、別の型のままである。
- 列挙に順序があるかは、口の `RuleEnum::ordered` が言う。値を `<` で区切った規則の列挙は、モジュールの列挙と同じく、大小の比べと `min`・`max`・`max_of`・`min_of` に使える。`|` で区切った列挙を比べれば E105 である。
- 規則は、順序のある列挙の入力に、値で範囲を書ける(`campaign : tier range >=basic <=gold`。rulec の §15.210)。口の列の型 `ColumnType::Enum` が、範囲に入る値の並びを渡す(ritsu の DESIGN 3.2)。kaname は、その入力に渡す値を範囲の解析で確かめる(6.1)。モジュールの列挙には範囲を書けない(E108)。範囲の外の値を除くのは、`match` の分岐と、一つの値と比べる `if`(`campaign != tiers.tier.platinum`)である(5.3)。
- 別の型の列挙のあいだは、`match` で対応づける。列挙の値を答える `match` は、逃げ道の警告にしない(7 章、23.9)。
値をどの列挙のものとして読むかは、置かれた場所の型で決まる(rulec と同じ)。型が決まらない場所では `<列挙>.<値>` と書く(規則の列挙は `<use の名前>.<列挙>.<値>`)。同じ綴りの名前と値が両方読めるときは E110 にし、どちらかの名前を替えるよう言う(rulec の E075 と同じ考え)。
### 3.6 レコード、リスト、オプショナルな値
**レコード**:`record` で宣言する。フィールドの数と日付には範囲を、リストには要素の数を書ける(書かなければ型の全体。3.9)。作るときはフィールドを全部書く(`Claim { due: …, days_late: … }`)。`T?` のフィールドは省けて、省けば `none` である。同じ型のほかの値の一部を替えて作るときは、`{` のすぐあとに `..x` と書き、書かないフィールドを `x` から取る(`Item { ..i, price: price }`。23.4)。フィールドは `.` で読む。フィールドの型は、リスト、辞書、集合、`T?`、`result`、ほかのレコードと直和型を通して、そのレコード自身を含んでよい(`children : list[Category]`、`next : Node?`)。自分を含まない値が一つも作れないレコード(`next : Node`)は E108 である。自分を含む型の決まりと、ホストから入る値の入れ子の深さ、範囲の解析とベクタと生成したコードでの扱いは 28.1 にある。
**リスト**:`list[T]`。作るのはリストのリテラル(`[a, b]`)と組み込みの関数だけで、添え字で読む書き方は無い(範囲の外の添え字という失敗の種類を作らないため)。中を読むのは組み込みの関数である(3.7)。
**オプショナルな値**:`T?`。`T` の値は `T?` の場所に渡せる。`T?` の値を `T` として使うには、`match` で `none` と `some x` に分ける。算術と大小の比べには使えない(rulec と同じ。E105)。`=` と `!=` では比べられる(`x = none`、`x = 2`。23.5)。値が無いことは一つしかない。オプショナルな値をさらにオプショナルにしても(オプショナルな値のリストに `first` をかけたとき)、要素が `none` であることと、要素が無いことは、同じ `none` である。TypeScript の `T | null` と Python の `T | None` が、入れ子にしても一つの `null` と `None` になるのと同じである。
**決定(オプショナルな値のオプショナルな値は、型でも一つ)**:値と同じく、型も一つにまとめる。オプショナルな値のリストに `first` をかけた答えは `T?` で、`T??` ではない。その答えを `match` で分けると、`some x` の分岐の `x` は値そのもの(`T`)で、そのまま計算に使える。値はもともと一つにまとめていて、生成する Go の `*T` も、TypeScript と Python の型と同じく入れ子にならない。捨てたものは二つある。一つは `T??` を型の誤り(E1xx)にすることで、値が無いことがある要素をリストから除く組み込みの関数が無いので、そのリストに `first` をかける書き方が無くなる。もう一つは、型では `T??` を許したまま値だけを一つにする形で、`some x` の `x` が `T?` になり、`x` を分けたときの `none` の分岐に決して来ない。
**途中までの合計**:組み込みの関数 `running_totals(xs, x => w)` は、リストの要素ごとに、その要素(`item`)、その手前までの重みの合計(`before`)、その要素までの合計(`upto`)、全部の合計(`total`)を持つレコード `running[T, W]` のリストを返す。重みが 0 以上だと範囲の解析で示せるとき、範囲の解析は、どの要素でも `0 ≤ before ≤ upto ≤ total` と `upto − before = w(item)` が成り立つことを知っている(5.3)。按分の規則(rulec の `allocate` を使う規則)は、この三つの値を入力にとり、前提に `before <= upto` と `upto <= total` を書く。前提は、この関係から示せる(17.9)。途中までの合計を明細ごとに手で書けば、`before` と `upto` の関係を範囲の解析が知らず、前提を決められない。按分を正しく書く形を、言語の組み込みの関数として一つに決めた。
重みは `running[T, W]` の `before`・`upto`・`total` に入るので、レコードのフィールドと同じく渡す先で、丸めた値を求める(3.2。丸めていない重みは E103)。数・お金・量の重みは、単位の整数でなければならない。率の重みには、刻みを与える宣言が無いので、重みの型が言う刻みの上にあればよい。丸めていない値になるのは、値で割って刻みが分からなくなった率だけである。重みが丸めた値でなければ、`running_totals` の答えは型を持たないとして、その答えを受ける渡す先では何も言わない(同じ誤りを一度だけ言う)。
**決定(`running_totals` の重みは渡す先)**:生成したコードは、`running[T, W]` のレコードのフィールドを、ほかのレコードと同じく境目の形(単位の整数、率は刻みの数)で持つ(10.2)。重みに丸めていない値(`x => 1 / x`)を許すと、型の検査も範囲の解析も通るのに、生成したコードは途中までの合計(`[2, 3]` なら 1/2 と 5/6)を境目の形にできずに失敗し(`1/2 is no whole number`)、参照インタプリタと答えが食い違う。丸めていない値は渡す先に渡せないという決まり(3.2)にそろえて、重みを渡す先として扱った。按分の規則に渡す途中までの合計は、規則の入力の単位の整数でなければもともと渡せないので、按分を書く形はこの決まりで狭まらない。**捨てたもの**:生成したコードで、丸めていない重みのレコードを有理数のまま持つこと。同じ `running[T, W]` のレコードが、重みが丸めてあるかで違う形になり、ホストに渡す形(10.2)が型だけで決まらなくなる。
### 3.7 関数、ラムダ、組み込みの関数
**関数**:`fn` で宣言する。引数と結果には型を書く(数と日付の引数には範囲も。3.9)。結果の範囲は書いてもよく、書けば確かめる。関数の名前は、モジュールの中で前にも後ろにも参照できる。再帰を書ける(`diverges`。4 章)。呼ぶときは引数を名前で書く(`weight_of(lines: lines)`)。同じ型の引数を取り違えないためで、rulec の生成した Go が入力を構造体で受け取るのと同じ理由である。
**ラムダと関数の値**:ラムダ(`x => 式`、`(acc, x) => 式`、`(l: Line) => 式`)は、組み込みの関数の引数と、関数の型(`(T) -> U`)の引数に書き、`let` で名前を付けられる。モジュールの関数も、名前を書いて関数の型の引数に渡せる(`total_by(xs: lines, amount: line_cost)`、`lines.map(line_cost)`)。関数の値は、渡すことと呼ぶことにだけ使い、関数の答えやデータには入らない(27 章)。組み込みの関数にその場で書いたラムダは、範囲の解析がその場で要素の範囲について読み、生成するコードもその場のループの本体にする(10.3)。ラムダの本体は式一つで、長くなるなら関数に名前を付けて呼ぶ。
組み込みの関数:
| 関数 | 答え | 備考 |
|---|---|---|
| `min(a, b)`、`max(a, b)`、`isqrt(x)`、`allocate(t, c, s)` | rulec と同じ | `allocate` は `down(t × c / s, 1<単位>)` と同じ。`s` が 0 になりうれば `zero`。`isqrt` の引数が負になりうれば `range`(8 章) |
| `mod(x, n)` | `number` | 余りは 0 以上、割る数の絶対値より小さい(ユークリッドの形。割る数が正の定数なら rulec と同じ)。割る数は値でもよく、0 になりうれば `zero`(24.3) |
| `numbers(a, b)` | `list[number]` | `a` から `b` まで(両端を含む)。`b < a` なら空。要素の数は `overflow` の場所(24.3) |
| `char(n)` | `string` | コードポイント `n` 一つの文字列。`n` はスカラー値を書いた整数の定数(25.2) |
| `up`・`down`・`half_up`・`half_down`・`half_even(x, 刻み)` | 丸めた値 | 3.3 |
| `count(xs)`、`count(xs, x => 条件)` | `number` | 要素の数、条件を満たす要素の数(`count(d)` と `count(s)` は辞書の組の数と集合の要素の数、文字列はコードポイントの数) |
| `sum(xs, x => 式)` | 式の型 | 空なら 0 |
| `any(xs, x => 条件)`、`all(xs, x => 条件)` | `bool` | 空なら `false`、`true`。答えの決まった要素で止まる(8 章) |
| `map(xs, x => 式)` | `list[式の型]` | |
| `filter(xs, x => 条件)` | `list[T]` | |
| `first(xs, x => 条件)`、`first(xs)` | `T?` | 条件を満たす最初の要素。そこで止まる。要素が `T?` でも答えは `T?`(3.6)。条件を書かなければ初めの要素 |
| `max_of(xs, x => 式)`、`min_of(xs, x => 式)` | `式の型?` | 空なら `none`。rulec の要約の `max`・`min` は `empty` を求めるが、kaname はオプショナルな値で返す |
| `running_totals(xs, x => 重み)` | `list[running[T, W]]` | 3.6 |
| `reduce(xs, 初め, (acc, x) => 式)` | `初め` の型 | 上の関数で書けないときのため。答えの範囲は、範囲の解析が広げて求める(5.5) |
| `starts_with(s, p)` | `bool` | バイト列の前方一致(rulec のセルの `starts_with` と同じ) |
| `concat(s, …)` | `string` | リストを渡せば、リストをつないだリスト(24.3) |
| `text(x)` | `string` | `number`、日付(`YYYY-MM-DD`)、列挙(値の名前)、真偽、お金と量(`1200JPY` のように、単位の整数と単位の綴り)、文字列(そのまま)。丸めていない値と率と直和型の値は E109 |
組み込みの関数は、関数の形(`sum(xs, x => x.qty)`)か、初めの引数のあとに `.` で続けるメソッドの形(`xs.sum(x => x.qty)`)で呼ぶ。上の表の関数は、`numbers` と `char` が関数の形だけで、ほかはどちらの形でも書ける。辞書・集合・リストの関数(24.3)と文字列の関数(25.2)は、メソッドの形だけで書く(違う形は E101)。組み込みの関数の引数は、名前を付けずに順に渡す(名前を付けたり、数が違ったりすれば E107)。関数を取る引数に関数でないものを渡したとき、渡した関数の引数の数が違うとき、組み込みの関数を値として書いたときは E109 にする(組み込みの関数は、ラムダで包んで渡す。`xs.map(x => isqrt(x))`)。`mod` の割る数と `numbers` の両端は整数で書く(E109)。`mod` の余りは、割る数の符号によらず 0 以上である。言語の `%` は割る数か割られる数の符号を答えるので、生成したコードは補助のモジュールの関数で計算する(24.6)。`sum` と `running_totals` の重み、`max_of` と `min_of` の式は数を答える(`max_of` と `min_of` は日付と順序のある列挙も)。`running_totals` の重みは、そのうえ丸めた値でなければならない(3.6。E103)。`reduce` の答えの型は、初めの値と渡す関数の答えをあわせた型である(合わなければ E102)。
文字列の関数(長さ、切り出し、位置、分けることとつなぐこと、空白を除くこと、ASCII の大文字と小文字、数と日付を読むこと)と文字列の大小は、コードポイントで決める(25 章)。`text` が書く形は、kaname が決めた一通り(数は 10 進の整数、お金は rulec のリテラルの形)で、生成するコードは言語の書式に頼らず自分で書く。
### 3.8 規則・日付・振替の型
- **規則の呼び出し** `charges(debt: …, days_late: …, reference_rate: …)` は、出力のレコード `charges.outputs` を返す。入力は名前か別名で書く(2.6)。フィールドは出力の名前で、型は口が渡す型(単位、刻み、オプショナル、列挙)である。出力が一つの規則でも、`.fee` のようにフィールドで読む(dandori と sekisho と同じ)。生成するコードでは、一つの出力の規則の関数がその値を直に返すので、kaname の生成物はそれをレコードに包まずに読む(10.5)。
- **規則の並び**(rulec の `elements`):並びの名前を入力の名前として、要素のレコード `<規則>.<並び>` のリストを渡す(`shipping(lines: map(xs, x => shipping.lines { amount: x.amount }), tier: t)`)。dandori は並びをたどる規則を読まない(dandori の E005)が、kaname はリストを持つので読む。口の `Rules::eval` は並び(`Value::List`)を受け取る。
- **規則のオプショナルな入力と出力**(`T?`):kaname はオプショナルな値を持つので、そのまま渡し、受け取る。dandori は読まない(dandori の E005)。
- **日付の呼び出し** `terms.due(invoiced: …)` は、その日付の日(`date`)を返す。日付の入力に渡せるのは `date`、整数の入力に渡せるのは `number` である。koyomi の日付の時刻(`at`)は読まない(19 章)。
- **振替の呼び出し** `ledger.capture.do(payment: …, seller: …, amount: …)` は、型 `ledger.capture.do` の値を返す。何も書かない。引数は振替の引数の名前のとおりに書き、`do` と `hold` は全部、`post` と `void` はキーの引数を書く。`post` に額の引数も書けば一部の確定(全部書くか、一つも書かないか。dandori の DESIGN 1.16 と同じ)。額の型は帳簿の単位から決まる。お金と量の単位は ritsu の単位の型(`unit USD scale 2` は `money[USDc]`。ritsu の DESIGN 5.4)で、名前だけの数え方(`unit pcs`)は `number` である。単位の違う値を渡せば E102(ドルの額をセントの帳簿に渡すこと。17.6)。振替や操作が無い、引数が足りない・余るは E204。
### 3.9 範囲と要素の数の宣言
**決定(範囲は、書けば約束、書かなければ型の全体)**:数と日付には `range` を、リスト・辞書・集合と文字列には `count` を書ける。書いた範囲は、ホストから入る値の約束で、三つの役をする。範囲の解析の前提(あふれと渡す先の確かめ)、ベクタの範囲、生成したコードの入口の確かめ(ホストが範囲の外の値を渡せば、関数は計算せずに `input` の失敗にする。4.4)である。約束の中の入力で失敗するのはプログラムの誤りなので、例があればエラーにする(5.7)。書かなければ、その値は型の全体をとる。数、お金、量はその単位で数えた int64 の全体、日付は 0001-01-01 から 9999-12-31、要素の数とコードポイントの数は 0 から 2⁶³ − 1 である(22.1)。型の全体も、入口で確かめ、範囲の解析の前提にする。型の全体の端から来る int64 のあふれは、型の限りで、プログラムの誤りではないので、診断を出さずに生成したコードが確かめ、関数のエフェクトに入れる(22.2)。業務の金額や件数に上限があるなら、書けば範囲の解析がそれを使い、ホストとの約束にもなる。
- 答えにだけ使うレコードのフィールド、関数の結果、`let`、規則の出力のレコードに範囲を書けば、確かめる(収まると示せなければ W301 と実行時の確かめ。5.7)。書かなければ、範囲の解析が求める。
- 範囲の端は、置かれた型の単位で読む(`range >=1GBPc <=100_000_000GBPc`、`mass[oz]` に `range <=15lb` と書けば 240 オンス)。整数にならない端と、率の刻みの上にない端は E104、型の値でない端は E102。`>` と `<` の端は、型の刻み一つぶん内側の端として読む(整数の `>0` は `>=1`、`rate[step 0.01%]` の `<15%` は `<=14.99%`、日付の `>2026-01-01` はその翌日)。
- リストに書いた `range` はその要素の範囲で、`count` は要素の数の範囲である(`list[number] count <=3 range >=0 <=9`)。リストのリストは内側の要素の数を書けず、内側の要素の数は型の全体である(要素の数の上限が要るなら、あいだにレコードを置く)。範囲の片方の端だけを書けば、書かない端は型の端である。
- 刻みのある率は、範囲を省ける。省けば 0% から 100% である(rulec の入力と同じ)。
- 率の刻みは、関数の引数と結果と、どのレコードのフィールドにも書く(無ければ E108)。率は刻みの数で受け渡し(9 章)、値が丸めてあるかも刻みで決まる(3.2)ので、刻みの無い率には、ホストに渡す形も、丸めていない値を止める決まりも無い。範囲と違って、答えにだけ使うレコードでも省けない。刻みを省けるのは `let` だけである。
- 文字列、真偽、列挙(直和型も)、レコードそのものには範囲が無く、`range` を書けば E108 にする。文字列の `count` はコードポイントの数である(25.1)。リスト・辞書・集合・文字列でないものの `count`、下の端が上の端より大きい範囲、端を二度書いた範囲も E108。
E108 は、引数の率に刻みが無ければその引数に、関数の答えとフィールドの率に刻みが無ければその型に出す。
### 3.10 温度を別の単位で読むときの零点と、零より下の定数(2026-10-21)
**きっかけ**:二つある。一つは、温度の値を `in` で別の単位に換算する関数で、生成したコードが参照インタプリタと違う答えを返したことである。`fn in_fahrenheit(t: temperature[℃] range >=-40℃ <=40℃) -> temperature[℉]` の本体 `half_up(t in ℉, 1℉)` に 0℃ を渡すと、参照インタプリタは 32℉ と答え、TypeScript・Python・Go は 0℉ と答えた。この直しの前と後の、生成した TypeScript のその行(実物):
```ts
return kaname.whole(kaname.round(kaname.mul(kaname.q(t), kaname.q(9n, 5n)), "half_up", kaname.q(1n, 1n))) as units.Fahrenheit;
return kaname.whole(kaname.round(kaname.add(kaname.mul(kaname.q(t), kaname.q(9n, 5n)), kaname.q(32n, 1n)), "half_up", kaname.q(1n, 1n))) as units.Fahrenheit;
```
温度を別の単位で読むところを全部見直すと、型の検査とベクタにも、零点を読み替えないところがあった。下の「確かめたこと」の `tests/modules/temps.kn` のうち、温度を別の単位で読む初めの十二の関数(68 行目まで)を、この直しの前の kaname で検査したもの(実物。注の行を省いた):
```text
error[E104]: temps.kn:50:6: A constant read in temperature[℃] is no whole number (20/9)
50 | if t >= 20℃ in ℉
error[E104]: temps.kn:62:11: The argument `t` of the function `cool` takes temperature[℃] on the step 1, and 5 is not on it
62 | cool(t: 41℉)
error[E104]: temps.kn:65:17: The field `at` of Reading takes temperature[℃] on the step 1, and 25 is not on it
65 | Reading { at: 77℉, where: "porch" }
error[E103]: temps.kn:68:3: An unrounded value goes to the answer of `floor_of`: a value of temperature[℃] that comes with a fraction, in steps of 1/9
68 | max(r.at, 14℉)
```
もう一つは、温度のしきい値を零より下に書くと(`t <= -18℃`)、E105 になったことである。範囲には `range >=-40℃` と書けるのに、式にも `examples` の行にも零下の温度を書けなかった。冷凍の温度を確かめる一つの関数のモジュール(`fn frozen(t: temperature[℃] range >=-40℃ <=40℃) -> bool` の本体が `t <= -18℃`、例が `frozen(t: -20℃) = true`)を、この直しの前の kaname で検査したもの(実物):
```text
error[E105]: frozen.kn:5:8: A value of temperature[℃] takes no `-`
5 | t <= -18℃
error[E105]: frozen.kn:8:13: A value of temperature[℃] takes no `-`
8 | frozen(t: -20℃) = true
```
**原因**:温度は、単位ごとに零点の違うただ一つの次元である(ritsu-units の表で零点のずれを持つのは ℉ だけで、値は「値 × 係数 + ずれ」で次元の基準の単位に読む)。参照インタプリタ(`interp::convert`)、範囲の解析(`factor_of`)、定数を比べの相手の単位で読むところ(`konst_in` の値)は、零点も読み替えていた。ほかのところは、単位のあいだの係数(1 を読み替えた値と 0 を読み替えた値の差)だけを使っていた。お金と量は零点が同じなので係数だけで足り、違いは温度でだけ出る。係数だけを使っていたのは五か所である。
1. 生成したコードの換算(`gen/plan.rs` の `convert`)。書いたリテラルは、コードを書くときに参照インタプリタと同じ換算で読むが、ほかは係数を掛けるだけだった。`t in ℉`(値)、`100℃ in ℉`(`in` の式の定数)、`let` に入れた定数を別の単位の値と比べるところ(`let line = 30℃` と、`temperature[℉]` の `t` の `t >= line`)が、零点を足さなかった。
2. 型の検査が `in` の式に付ける定数の値(`types.rs` の `conversion`)。`20℃ in ℉` を 36℉ と持ち、それを ℃ の値と比べると、36℉ を ℃ で読んだ 20/9 が整数でないので E104 になった。
3. 別の単位で読んだ定数の刻み(`konst_in`)。リテラルの刻みはその値である(3.2)。`41℉` の刻みは 41 で、℃ で読むと値は 5 なのに、刻みは係数を掛けた 205/9 だった。そのため ℃ の引数やフィールドに渡すと、5 が刻み 1 の上に無いという E104 になり、`max(r.at, 14℉)` は刻み 1/9 の丸めていない値(E103)になった。
4. `in` で換算した値の刻み(`conversion`)。℃ の整数を ℉ で読んだ値は「32 + 9/5 の倍数」で、9/5 の倍数ではない。範囲の解析は、分岐で狭めるとき、値が型の刻みの上にあるとして端を刻みに寄せる(5.3)。`let x = t in ℉` を `x < 34℉` で狭めると、上の端を 9/5 の倍数の 32.4℉ にしたが、実際には 33.8℉(1℃)がある。`tests/modules/bounds.kn` の `shifted`(答えの範囲は `>=32℉ <=33℉`)は、これで total と言われたのに、1℃ で 34℉ を答えて `range` で失敗した。範囲の解析が上からの見積もりでなかったところである(15.3)。定数の `in` も同じ刻みで、零点のずれを足すと、刻みが整数なのに値が整数でないことがある(`99℉ in ℃` は刻み 55 で値 335/9)。
5. ベクタの候補(`vectors.rs` の `convert_num`)。比べるリテラルを、引数の単位に係数だけで読み替えていた。`temperature[℉]` の引数と比べる `30℃` の候補は 53℉・54℉・55℉ で、境目の 86℉ から遠かった。`temperature[℃]` の引数と比べる `50℉` は 250/9℃ と読み、刻みの上に無いので候補にならなかった。
零より下の定数が書けなかったのは、字句が `-18℃` を、`-` と定数 `18℃` の二つに分けて読むからである。範囲の端の `-` だけは、構文が端の一部として読む(2.3 の `bound`)。型の検査は、式の `-x` を値の符号を替える演算として扱い、比べと範囲だけの量(温度と音量。3.2)には演算が無いので E105 にしていた。お金と量には `-` の演算があるので、`-1USD` は書けていた。温度の値の符号を替える演算は、零点が単位ごとに違うので意味を持たない(`10℃` は `50℉` だが、`-10℃` は `-50℉` でなく `14℉` である)。演算として許すことはできない。
**決定**:温度を別の単位で読むところは、どこでも零点を読み替える。定数のすぐ前の `-` は、どの単位でも零より下の定数として読む。
- 生成したコードの換算は、零点のずれがあれば、係数を掛けてからずれを足す。int64 の確かめは、参照インタプリタと同じく最後に一度だけする。リテラルは、零より下のリテラルも、コードを書くときに読み替える。
- 型の検査が `in` の式に付ける定数の値を、零点を読み替えた値にする(`20℃ in ℉` は 68℉、`-20℃ in ℉` は −4℉)。
- 零点の違う二つの単位のあいだでは、定数の刻みをその値にし(リテラルと同じ)、値の刻みを、係数を掛けた刻みと零点のずれがともに整数倍になる刻みにする(`ty.rs` の `step_in`)。℃ の整数を ℉ で読んだ値は 1/5 の倍数、℉ の整数を ℃ で読んだ値は 5/9 の倍数である(ずれの −160/9 が 5/9 の倍数なので、こちらはこれまでの刻みのまま)。
- ベクタの候補を、零点を読み替えた値にする。
- 型の検査は、`-` の相手が定数なら(括弧の中の定数、定数を入れた `let` も)、零より下の定数の型にする。値の `-`(`-t`)は、これまでどおり温度と音量では E105 である。
- 参照インタプリタ、範囲の解析、ベクタ、出力先の生成は、`-` と数のリテラルの組を一つのリテラルとして読む。`match` の分岐にも書ける(`-18℃ =>`。23.3)。
**捨てたもの**:
- 温度の `in` を E104 にすること。生成したコードの誤りは無くなるが、温度を別の単位で表すことが書けなくなり、参照インタプリタが答えていた関数が通らなくなる。
- 値の刻みを、刻みとずれの組(「32 + 9/5 の倍数」)で持つこと。狭めるときに正確な端がとれるが、刻みを読むところ(丸めているかの判定、和と積の刻み、生成したコードの表し方)の全部を組に合わせることになる。いまの刻み(二つがともに整数倍になる刻み)でも、狭め方が少し粗くなるだけで、上からの見積もりのままである。
- 字句で `-18℃` を一つの定数として読むこと。`a -18℃` と `a - 18℃` で読み方が変わり(引き算か、二つの値か)、`-` を演算子として読む rulec の字句ともずれる。
- 温度と音量の値に `-` を許すこと。符号を替えた値が単位によって違う温度を指し、温度と音量を比べと範囲だけの量とした決まり(rulec と同じ)が崩れる。
**変わること**:
- 温度を `in` で換算する関数と、`let` に入れた温度の定数を別の単位の値と比べる関数で、生成したコードの答えが参照インタプリタと同じになる(`0℃ in ℉` が 0℉ から 32℉ に)。参照インタプリタの答えは変わらない。
- `in` で換算した温度の定数をもとの単位の値と比べるところ(`t >= 20℃ in ℉`)と、別の単位の温度の定数を引数・フィールド・`max`・`min` に渡すところ(`cool(t: 41℉)`、`Reading { at: 77℉ }`、`max(r.at, 14℉)`)が、E104・E103 でなくなる。整数にならない定数(`40℉` は 40/9℃)は、これまでどおり E104 である。
- `in` で ℃ から ℉ に換算した値の刻みが 9/5 から 1/5 になる。丸めずに渡したときの E103 の文は「in steps of 1.8」から「in steps of 0.2」になる。その値を分岐で狭めるところは端が正しくなり、示していた場所が、例か決められない場所になることがある(`shifted` は E301)。
- ベクタの候補が変わる(`hot` は 53℉・54℉・55℉ から 85℉・86℉・87℉ に、`cool` には 9℃・10℃・11℃ が加わる)。
- 零より下の温度と音量の定数(`-18℃`、`-10dB`)を、式、`match` の分岐、`examples` の行に書ける。
- 定数を温度の別の単位で読む比べの答え(3.2)は、これまでも零点を読み替えていたので変わらない。例、テストのモジュール、`tests/docs` と `tests/doc` の材料、ritsu の `tests/projects` の `.kn` の `check`(英日と JSON)・`vectors`(英日)・`doc`(英日)・`api`・`run`、`explain --all`、`ritsu gen`(14 のプロジェクトの 783 のファイル)は、替える前と後で一字も変わらない(温度の換算を使うものも、零より下の温度と音量の定数も無かった)。
**確かめたこと**:
- `tests/modules/temps.kn` と `temps.ja.kn`(23 の関数。英語の版と、横に置く日本語の版):初めの十二の関数は、別の単位の定数との比べ(リテラルと、`let` に入れたもの)、`in` で換算した値の比べと丸め、定数の `in` と、それをもとの単位の値と比べること、整数のあいだに来る定数の `in` を丸めること、別の単位の定数を引数・フィールド・`max` に渡すことで、替える前は上の四つが E104・E103 だった。あとの十一の関数は、零下のしきい値を同じ単位と別の単位で比べること、零下の定数の `in`、零下の定数を引数・フィールド・`max`・`match` の分岐に書くこと、零より下の音量・金額・長さの定数である。いまは、検査が `frozen` の W501(しきい値を値にする比べ。7 章)のほかに何も言わずに通り、`examples` が成り立ち、範囲の解析の健全性(16.1)、出力先の突き合わせ(16.4)、Lean との突き合わせ(15.3)で違いが無い。
- 上の一つの関数のモジュールは、検査を通り(`t <= -18℃` を値にしているので、しきい値を値にする比べとして W501 が出る。7 章)、例が成り立つ。
- 直しを一つずつ外して、何が落ちるかを確かめた。生成したコードの換算から零点のずれを外すと、出力先の突き合わせで 24 件が食い違う(temps と temps.ja の 11 件ずつ。`hot_by_line` の 85℉、`in_fahrenheit` と `in_celsius` の三件ずつ、`feverish` の 45℃、`boiling`、`fever_line`、`freezer_limit`。それと `shifted` の 2 件)。`in` の式の刻みを係数を掛けただけに戻すと、生成した TypeScript の `fever_line` が「kaname: 335/9 is no whole number」で止まり、`shifted` は 1℃ で答えの範囲を確かめずに 34℉ を返す(3 件)。値の刻みの直しだけを外すと、Lean のモデルが `feverish` と `shifted` の比べで `type` で止まり(値が型の刻みの上に無い。7 件)、突き合わせの中の健全性の確かめで `shifted` の見た値が抽象の値の外に出る(33.8℉ が上の端 32.4℉ の上)。
- `tests/types.rs`:上の E104・E103 が出ないこと、整数にならない定数が E104 のままであること、換算した値の刻み(E103 の文の 0.2 と 5/9)。零下の定数は温度と音量でも通り、別の単位で整数にならない零下の定数(`temperature[℉]` と比べる `-18℃`)は E104、値の `-`(`-t`)は E105 のままであること。`tests/interp.rs`:参照インタプリタの答え(別の単位で読んだ定数と、`in` の値。`-20℃` は −4℉、`0℃` は 32℉)。`tests/vectors.rs`:候補。`tests/ranges.rs`:`shifted` に見つけた例(1℃)が、その場所で失敗すること。
- ほかの言語:rulec(検査、例の行、ベクタ、12 の出力先の生成、版をまたぐ記録の比べ)、dandori(範囲の端と、その生成物)、sekisho(条件の定数と Cedar)は、温度の定数を検査のときに ritsu-units の `Unit::convert` で読み、零点も読み替えていた。実物で確かめた。rulec は ℉ の入力の `>=30℃` を 86、℃ の入力の `<50℉` を 10、℉ の出力の `30℃` を 86 と、12 の出力先のどれにも書き、`diff` は 5℉ と −15℃ を同じ温度と読み、42℉ と 6℃ の差を 4/9℃ とする。dandori は ℃ の入力の `range >=50℉ <=212℉` を、Temporal(Python)と ASL で 10 と 100 と書く。sekisho は ℃ の属性の `<= 86℉` を、Cedar で `<= 30` と書く。生成したコードが実行のときに値を換算するのは kaname だけである。koyomi・chobo・geas・yuen・sakai は温度を換算しない(chobo の ℃ は、その名前の個数である)。
**見つけて直していないもの**:
- rulec の式で、温度の値を別の単位のリテラルと比べる(`define is_hot : bool = outside >= 30℃`)と E103 で、見出しは「Mixed units: adding temperature[℃] to temperature[℉]」、注は「Values of different dimensions cannot be added」である。リテラルを書いた単位で読む rulec の決まりのとおりに止まるが、比べにも、同じ次元の二つの単位にも合わない言い方である。換算の誤りではないので、この直しでは rulec を変えていない。
- 括弧で囲んだ式の位置が括弧を含まないので、式を引用する診断が括弧の片方を落とす。`t <= -(18℃)` の W501 は「`t <= -(18℃` makes a value …」と言い、`(t + u) <= 18USD` の W501 は列を `(` の次の字に置き、「`t + u) <= 18USD`」と引用する。零下の定数に限らない、前からの形である(構文が、括弧の式に括弧を含む位置を付けていない)。
## 4. エフェクト
### 4.1 三つ
関数のエフェクトは、失敗の種類の集合と、止まらないかもしれないかの組である。
| エフェクト | 意味 | 書き方(関数の見出しの終わり) |
|---|---|---|
| total | 宣言した範囲のどの入力にも、答えを返す | `total` |
| 失敗しうる | 実行時に失敗しうる場所を持つ。種類を集合で持つ | `fails rule, overflow` |
| 止まらないかもしれない | 再帰を持つ(直接でも、ほかの関数を通してでも) | `diverges`(失敗とともに `fails zero diverges`) |
失敗の種類の集合が空で、再帰も持たない関数が total である。total は、内側の層の関数と同じことを言う。rulec の規則は、宣言した範囲のどの入力にも、ちょうど一つの行で答える(完全性と重なりの検査)。koyomi の日付の関数は、範囲のどの入力でも日付を計算できる(範囲のすべての入力の計算)。kaname の total な関数は、その二つを呼び、そのあいだの計算でも、どの入力でも失敗しない。
### 4.2 失敗の種類
| 種類 | 起きうる場所 | だれが実行時に確かめるか | 決める検査 |
|---|---|---|---|
| `rule` | 規則の入力に渡す値が、その入力の範囲に収まると示せない。規則の前提を示せない。koyomi の日付を規則の日付の入力に渡し、その日が規則の範囲に収まると示せない | 規則の生成したコードが入口で受け付けず、kaname の生成したコードがそれを `rule` の失敗にする | 入力の範囲は kaname(E301・W301)、前提は ritsu の X2(E201・W201)、koyomi の日は X3 の (a)(E202・W202) |
| `date` | 日付の関数の日付の入力に渡す日が、その入力の範囲に収まると示せない。整数の入力も同じ | koyomi の生成したコードが入口で受け付けず(`KoyomiError` の `range`)、kaname の生成したコードがそれを `date` の失敗にする | 日付の入力は ritsu の X6(E205・W205)、整数の入力は kaname(E301・W301) |
| `range` | 関数の引数、関数の結果、レコードとフィールドを持つ値のフィールドに置く値が、宣言した範囲や要素の数に収まると示せない。振替の額が 0〜2⁶³ − 1 に収まると示せない。`isqrt` に渡す値が 0 以上と示せない | kaname の生成したコード | 渡す先と `isqrt` は kaname(E301・W301)、振替の額は ritsu の X4(E203・W203) |
| `overflow` | 途中の値が int64 に収まると示せない。範囲を書かない値の、型の端から来たあふれも(22.2) | kaname の生成したコード(確かめつきの算術) | kaname(E302・W302。型の端から来たあふれは、診断を出さない) |
| `zero` | 割る数が 0 にならないと示せない(`allocate` の三つ目の引数も) | kaname の生成したコード | kaname(E303・W303) |
規則の生成したコードが入口で受け付けないものは、kaname の生成したコードがもう一度確かめない。規則がそれを確かめるからで、kaname は、その答えを失敗の種類と呼び出しの行に直すだけである。振替の額は、帳簿のクライアントが失敗にする(chobo の DESIGN 1.5)が、その失敗はホストが振替を当てるときに起きる。kaname の関数から遠く離れたところで起きるので、kaname の生成したコードが、振替の値を作るところで確かめる。
例がある場所(エラー)も、失敗しうる場所として数える。エラーのあるモジュールは `check` を通らないが、`doc` と `api` は、どの場所がなぜ失敗しうるかを見せるためにエフェクトを出す。
### 4.3 推論と、書いたときの確かめ
**決定**:エフェクトは推論を既定にし、書いたときだけ確かめる。
- 関数ごとに、本体の失敗しうる場所(4.2)の種類を集める。呼んだ関数のエフェクトは、呼んだ側に伝わる。呼んだ関数の失敗の種類は呼んだ側の種類に加わり、呼んだ関数が止まらないかもしれなければ、呼んだ側もそうなる。組み込みの関数に渡したラムダの中の場所も、その関数のものである。ラムダの本体の場所は、どこで読んでも、そのラムダを書いた関数のものである。値として書いた関数は、それを書いた関数が呼ぶ関数に数える(27.5)。
- 型の引数を持つ関数の呼び出しは、呼ぶところで本体を読み直し(26.5)、その読み直しで示せなかった場所の種類と、読み直しの中の呼び出しのエフェクトを、呼んだ側に入れる。呼ばれる関数そのもののエフェクトは入れない。4 段を越えて読み直さない呼び出しと、輪の中の呼び出しは、これまでどおり呼ばれる関数のエフェクトを入れる(26.6)。関数の型の引数を持つ関数の呼び出しも、同じに読み直す(27.4)。
- 関数の型にエフェクトを書いた引数を呼ぶところは、そのエフェクトを、その関数自身のエフェクトに入れる。エフェクトを書かない引数を呼ぶところは、何も入れない。その引数を通るエフェクト(`through`)は、関数を渡した側のエフェクトに入る(27.5)。
- 呼び出しのグラフで輪になる関数(強連結成分が二つ以上の関数を持つか、自分を呼ぶ関数)は、どれも `diverges` を持つ。関数を値として書くことは、輪を求めるときも、呼ぶことと同じに数える(27.5)。止まることは確かめない(P8)。輪の中の失敗の種類は、輪の全部の関数の和にする。
- 見出しに `total` と書いた関数は、推論したエフェクトが total でなければ E401 にする。注に、失敗しうる場所(行と、何が示せなかったかと、それを決める検査の診断のコード)か、輪を作る呼び出しを書く。型の端から来たあふれの場所には診断が無いので、コードの代わりに種類の `overflow` を書く(22.3)。
- `fails rule, overflow` と書いた関数は、推論した種類がその中に収まらなければ E401。書いた種類のうち起きないものは、何も言わない(広く書くのは自由で、生成するコードの型がその分広くなるだけである)。`diverges` と書かずに再帰があれば E401。
- 何も書かなければ、推論したものがその関数のエフェクトである。
呼び出しのグラフの輪は、強連結成分(Tarjan の手順。`src/effects.rs` の `rings`)で求める。範囲の解析とエフェクトは、呼ばれる側の輪から順に進め、輪の中の関数は、互いの答えを結果に書いた範囲で読む(5.4)。
言語をまたぐ判定(X2・X3 の (a)・X4・X6)の答えは、ritsu-cross が口 `Verdicts` で返す(14 章)。kaname は、その答えを受け取ってからエフェクトを決める。`ritsu kaname` と `ritsu check` は ritsu-cross をつないで kaname を走らせるので、どちらも同じエフェクトを出す。内側の言語を持たないクレートのバイナリは、内側の層を呼ぶモジュールを読まない(E202)ので、言語をまたぐ判定の要らないモジュールだけを扱う。
**決定(言語をまたぐ判定の答えでエフェクトを決める)**:ritsu-cross が示した場所は、エフェクトから外す。決められない場所と例のある場所は、生成したコードが実行時に確かめる失敗として、エフェクトの種類に数え、書いたエフェクトとの比べ(E401)にも使う。E401 の注は、その場所と、ritsu-cross が言うコードを `ritsu W203` の形で挙げる(kaname の台帳と ritsu の台帳は番号が重なるため)。`ritsu kaname` と `ritsu check` では、例の関数は、どれも total である(17 章)。
答えを受け取らない実行(ritsu-cross をつながない kaname のクレートのテスト)では、その場所は答えを待つ場所で、エフェクトの種類には数えるが、E401 には使わない。答えをまだ尋ねていないことは、示せないこととは違うからである。`--format json` はその場所を `"outcome": "waiting"` と書き、確かめる判定(`"check": "transfer_amount"` など。35 章)を添える。捨てたものは二つある。答えを待つ場所も E401 に使うと、答えが来れば消えるエラーを出す。答えを待つ場所をエフェクトに数えないと、答えが来るまで、生成したコードが確かめるはずの場所がエフェクトに出ない。
**入出力のエフェクトは無い**(P1)。kaname は時刻も乱数も状態も持たないので、それを型に出す必要が無い。
### 4.4 入口とエフェクト
関数の引数の範囲は、その関数の約束である。ホストが範囲の外の値を渡せば、生成したコードは計算せずに失敗する。自分を含む型の値の入れ子の深さ(200 段まで。28.1)も同じ約束で、それより深い値も計算せずに失敗する。この失敗の種類は `input` で、エフェクトに入れない。total は「宣言した範囲のどの入力にも答えを返す」ことで、範囲の外の入力は約束の外だからである。rulec が、範囲の外の入力を `RuleInputError` で受け付けないのに、規則を完全と言うのと同じ考えである。
- TypeScript と Python は、どの関数でも `KanameError`(`kind` が `input`)を投げる。
- Go の total な関数は `error` を返さない(4.5)。範囲の外の入力では、`*KanameError`(`Kind` が `input`)で panic する。ホストが確かめたいときのために、関数ごとに引数を確かめるだけの関数(`Check<関数>(…) error`)を生成する。失敗しうる関数は、`input` も、ほかの種類と同じく `error` で返す。
kaname の関数がほかの kaname の関数を呼ぶところは、入口ではない。そこでは引数が範囲に収まるかを範囲の解析で確かめ、示せなければ `range` の失敗しうる場所になる(4.2)。自分を含む型の値は、その中を確かめない。そのフィールドは、作るところか入口で、宣言した範囲と要素の数を確かめてあるからである(28.1)。生成したコードは、示せた呼び出しでは入口の確かめを通らずに呼ぶ(10.4)。
型の引数を持つ関数、関数の型の引数を持つ関数、`private` の関数(中の関数。21.3)は、ホストが呼ばないので入口を持たない。生成したコードは、入口の確かめの無い一つの関数だけを書く(26.7、27.6、28.2)。
### 4.5 使い道
- **人が読むページ**(13 章):関数を三つに分けて見せる。表にある判断(呼ぶ規則と日付と、組み立てる振替)、total な関数、実行時に確かめる関数(種類と、確かめる場所ごとに、なぜ示せなかったか)。止まらないかもしれない関数は、別に挙げる。
- **`check` の最後の行**:関数のエフェクトを、関数の見出しの書き方(`total`、`fails rule, range`、`diverges`、`fails overflow diverges`)で数える。そろえば `ok late_payment.kn: 1 function, fails date`(日本語は「関数 1 個、fails date」)、分かれれば `3 functions: 2 fails range; 1 total`(「関数 3 個: fails range 2 個、total 1 個」)と書く。
- **`--format json` と `api`**:関数ごとに、推論したエフェクト、書いたエフェクト、失敗しうる場所の一覧(行、種類、決める判定、診断のコード、結果)、呼んだ関数。`check --format json` の `functions` は、17.6 の手数料を引く形(支払いの金額を 100 セント以上にしたもの)で次のとおり(実物。`ritsu kaname check --format json` を整形して一つの関数だけ):
```json
{"function": "settle", "effect": {"fails": ["range"], "diverges": false}, "written": {"total": true, "fails": [], "diverges": false},
"places": [{"line": 21, "kind": "range", "check": "transfer_amount", "code": "W203", "outcome": "undecided"}], "calls": []}
```
`places` の `check` は、その場所を決める判定(kaname 自身なら `"kaname"`、言語をまたぐ判定なら、`ritsu check` の境目の記録と同じ語の `"precondition"`・`"rule_date_input"`・`"transfer_amount"`・`"koyomi_date_input"`。35 章)、`code` は出した診断のコード(kaname 自身の場所は E301〜W303、言語をまたぐ判定の場所は ritsu の E201〜W205。答えを待つ場所は `null`)、`outcome` は `"example"`・`"undecided"`・`"unbounded"`(型の端から来たあふれ。`code` は `null`。22.3)・`"waiting"` のどれかである。示した場所は載せない。
- **生成するコードの形**(10 章):Go は、失敗しうる関数だけが `(T, error)` を返し、total な関数は `T` を返す。TypeScript と Python は、投げる `KanameError` の種類を、関数ごとの型の別名(TypeScript の `type ClaimFails = "rule" | "overflow"`、Python の `Literal`)とドキュメントのコメントに出す。止まらないかもしれない関数は、コメントにそう書く。
- **dandori から呼ぶときの制限**(19 章。まだやらない):dandori のフローから kaname の関数を呼ぶなら、total な関数だけにする。ワークフローの中で、失敗の種類ごとの扱いと、止まらない呼び出しの期限を書かずに済むようにするためである。
- **Lean**(15 章):「total と推論した関数は、宣言した範囲のどの入力でも失敗しない」を定理にする。
### 4.6 捨てたもの
- **失敗の種類を一つにまとめる**:ホストは種類で扱いを分ける(`rule` は規則とのつなぎの誤りで、`overflow` は範囲の宣言の不足)。また、種類があると、ページで、どの検査が示せなかったかを読める。
- **エフェクトをいつも書かせる**:推論できるものを書かせても、書く手間が増えるだけである。書くのは、そう約束したい関数(ホストに total と言いたい関数)だけでよい。
- **kaname の中で失敗を受け止める書き方(`try` など)**:失敗しうる場所は、検査が示せなかった場所である。受け止めて `none` にすれば、示せなかったことが答えの中に紛れる。範囲を狭める分岐を書けば、範囲の解析がそれを読んで、失敗しうる場所そのものが消える。
- **入出力のエフェクトを持つ**:P1 のとおり、kaname には入出力が無い。
## 5. 範囲の解析
範囲の解析は `src/ranges.rs` にある。型の確かめを通ったモジュールの関数を一つずつ、引数に宣言した型と範囲から読み、式ごとの値がとりうるものを求める。値は、参照インタプリタと同じく、その式の型の単位で数える(8 章)。参照インタプリタが値をほかの単位で読むところ(`in`、定数を相手の単位で読むところ)では、同じ係数で読み替え、温度は零点のずれも足す。
### 5.1 抽象の値
範囲の解析は、式ごとに、値がとりうるものの上からの見積もり(抽象の値)を求める。見積もりは本当にとる値を全部含むが、とらない値も含みうる。
| 型 | 抽象の値 |
|---|---|
| 数、お金、量、率 | 区間(両端は正確な有理数で、無いこともある)と、既約分数にしたときの分母の上限。端と分母の上限は、型の端から来たかの印を持つ(22.2) |
| 日付 | 日の番号の区間 |
| 列挙、真偽 | とりうる値の集合 |
| 直和型、値の型 | とりうる値の名前ごとに、フィールドの抽象の値のレコード(フィールドの無い値は空のレコード。23.7) |
| `result` | 直和型と同じ形で、`ok` の `value` と `err` の `error` のレコード(26.3) |
| 型の引数の値 | 何も分からない値(`Abs::Any`)から始める。呼ぶところで読み直すときは、呼び出しが渡す値(26.5) |
| 文字列 | コードポイントの数の区間(25.6) |
| `T?` | `none` になりうるか、値があるときの抽象の値 |
| レコード | フィールドごとの抽象の値と、フィールドどうしの関係(`running_totals` の要素の `before <= upto`) |
| リスト | 要素の数の区間と、要素の抽象の値(全部の要素を合わせたもの) |
| 辞書 | 組の数の区間と、鍵の抽象の値、値の抽象の値(24.5) |
| 集合 | 要素の数の区間と、要素の抽象の値(24.5) |
| 振替の呼び出し | 引数ごとの抽象の値 |
ほかに二つの値がある。「何も分からない」と、「値が来ない」(いつも 0 で割る式のように、評価がその式から先に進まない)である。値が来ない式のあとの式(同じ呼び出しのあとの引数、ブロックのあとの行)は解析しない。そこには決して来ないので、失敗しうる場所にも数えない。
分母の上限は、型の刻みからも決まる(`money[USDc]` の値は 1、`rate[step 0.01%]` の値は 10,000)。区間の端は、絶対値が 2⁷⁰ を超えれば端の無い区間にし、分母が 2²⁴ を超える端はその格子の外側へ動かす。どちらも見積もりを広げるだけなので、上からの見積もりのままであり、端どうしの計算は i128 に収まる。
関数の引数は、宣言した型と範囲と要素の数から始める(範囲を書かない刻みのある率は 0% から 100%。3.9)。範囲と要素の数の書かない端は型の端で、その端に型の端の印を付ける(22.2)。ホストから入る値は入口で確かめる(4.4)ので、関数の中ではその範囲にあると言える。モジュールのレコードの値は、作るとき(参照インタプリタの `make_record`)と入口でフィールドの範囲を確かめるので、どこから来たものでも、フィールドは書いた範囲にある。自分を含む型の値の中では、範囲を書かない刻みのある率を 0% から 100% と読まない。kaname の関数を呼ぶところでは、自分を含む型の値の中を確かめないので、kaname の中で作った 100% を超える率が来うるからである。自分を含む型の値は、一つの道の上で同じ型の値を二つまでたどり、その先を何も分からない値にする(28.1)。規則の出力は、口の `output_values`(数の出力の最小と最大)と、列挙と真偽の出力の値の全部から始める。rulec が範囲を持たない数の出力(範囲の無い入力を読む出力)は、型の全体に型の端の印を付けたものから始める。生成したコードがその出力を返す型(お金なら int64)の値だからである(32 章)。koyomi の日付の答えは、口の `values`(範囲のすべての入力で日付がとる日の集合)の両端と、日付の入力に渡した日に口の `days`(日付の入力からその日付までの日数の最小と最大)を足した区間の、重なりから始める。
### 5.2 演算
区間の算術を、正確な有理数の上でする。`+`・`-`・`*` は端どうしの組み合わせ、`min`・`max`・`isqrt` と正の定数で割る `mod` は rulec の届く区間の求め方(rulec の reference §8)と同じ(値で割る `mod` は 24.5)、日付の差は端どうしの差である。比べは真偽の集合を返す(両方の区間が離れていれば一つの値に決まる)。
- **丸め**:五つの向きのどれも、値について単調である(値が大きければ丸めた値も小さくならない)。そこで、両端を同じ向きに丸めた区間を、丸めた値の区間にする。両端を刻みの外側へ丸めた区間より狭く、上からの見積もりのままである。端を丸めた値が int64 を超えるときは、端の外側の整数(下の端はその下、上の端はその上の整数)から丸め直し、それも超えれば、その端を持たない。内側の整数から丸め直すと、端にならないことがある。`a / b`(`a` は −9,200,000,000,000,000,099 から −9,200,000,000,000,000,000、`b` は 98 か 99)を刻み 1% で 0 へ寄せて丸めるとき、上の端 −9,200,000,000,000,000,000/99 を丸めた −92,929,292,929,292,929.29 は、既約分数の分子が int64 に収まらない。その下の整数 −92,929,292,929,292,930 から丸め直した値を上の端にすると、`a` が −9,200,000,000,000,000,031 で `b` が 99 の値を丸めた −92,929,292,929,292,929.6(既約分数は −464,646,464,646,464,648/5 で、int64 に収まる)が、その端より上に出る(`tests/modules/bounds.kn` の `hundredths`)。
- **値での割り算**:割る数の区間が 0 を含まなければ、商は端どうしの組み合わせである。0 を含むとき、0 でない割る数は、分母の上限 D から 1/D 以上 0 から離れているので、負の側と正の側に分けて、それぞれの商を合わせる。割る数がいつも 0 なら、その式に値は来ない。
- **分母の上限**:足し引きで二つの上限の積、掛け算で積、値での割り算で、割られる数の分母の上限と、割る数の分子の絶対値の上限の積にする。`sum` と `running_totals` の和は、型の刻みが決める上限だけを持ち、刻みを言わない型(丸めていない値)の和は上限を持たない。和の分母は要素の分母の上限を超えうるからである(1/2 + 1/3 は 5/6。`bounds.kn` の `thirds`)。区間の両端の絶対値の大きいほうと分母の上限の積が、既約分数にしたときの分子の絶対値の上限である。この二つで、int64 のあふれを見る(5.6)。
- **int64 の決まりのあと**:参照インタプリタは、int64 に収まらない値で評価を止める。そこで、確かめる演算の答えは、そのあとは −2⁶³ から 2⁶³ − 1 の中にあるとして読む。int64 の端に寄せた端は、寄せる前の端の印を持つ(22.2)。
### 5.3 分岐で狭める
`if` の条件と `match` の分岐は、その中で値の抽象の値を狭める。
- `x op c`(`c` は定数か、範囲の決まった値):`then` の中では `x` の区間を条件が成り立つ側に、`else` の中では成り立たない側に狭める。丸めた値は刻みの上で狭める(整数の `x < 5` は `x <= 4`。`x != 0` は 0 が区間の端なら一つ内側へ)。
- `and` は `then` の中で両方を、`or` は `else` の中で両方の否定を使う。`not` は入れ替える。`and` の右と `or` の右は、左が決めなかったときだけ評価するので、左が成り立った(成り立たなかった)ものとして読む。
- `match` の分岐は、列挙と真偽の値の集合をその分岐の値に、`some x` は `x` を値の側に、`none` はその変数を `none` に狭める。直和型の値に名前を付けた分岐(`placed p`)は、分ける値をその値に狭め、付けた名前にその値のフィールドを入れる。`else` は、上の分岐が受けない値に狭める(オプショナルな値では、分岐に入るかだけを決める)。リテラルの分岐は、数と日付をその分岐のリテラルを合わせた区間に狭め、リテラルの `else` では狭めない(23.7)。どの値も来ない分岐は解析しない。
- 真偽の名前そのものの条件(`if q.eligible`)は、その名前を `true` か `false` に狭める。
- `count(xs) = 0` のように、条件の付かない `count` の答えと比べる条件は、リスト `xs` の要素の数を狭める(辞書と集合の数、文字列のコードポイントの数も。24.5、25.6)。空のリストを除く分岐(`if count(xs) = 0 then none else … / count(xs)`)の割る数は、0 にならないと示せる。両辺のどちらも単位を読み替えない比べに限る(次の関係と同じ)。
- 狭める相手は、変数と、変数のフィールドをたどったパス(`invoice.amount`)である。kaname の変数は書き換えられないので、一度狭めたパスは、その分岐の中でずっと狭いままである。
**決定(答えた呼び出しのあとも狭める)**:規則を呼んで答えが返ったなら、渡した値はその入力の範囲にあった(範囲の外なら、規則が受け付けず評価が止まる)。kaname の関数の引数、日付の整数の入力、`isqrt` の引数、範囲を書いたレコードのフィールドも同じである。そこで、渡した値が変数かパスなら、呼び出しのあとのブロックの終わりまで、その範囲に狭める。分岐を外して規則を呼ぶと、その呼び出しは E301 になるが、そのあとで同じ日数をレコードのフィールドに置くところは、狭めた日数で示せる(17.7)。
**決定(列挙の入力の範囲では、答えた呼び出しのあとに狭めない)**:規則の列挙の入力に書いた範囲(6.1)では、呼び出しのあとに、渡した名前を範囲に入る値に狭めない。上からの見積もりのままだが、同じ名前を同じ範囲の入力にもう一度渡すと、二つ目の呼び出しも E301 になる(一つ目の呼び出しを `match` の分岐に入れれば、どちらも示せる)。数と同じく狭めるには、Lean のモデル(15 章)の狭め方に、値の集合で狭める形とその健全性を足すことになる(19 章)。
**二つの値の関係**:条件 `a op b`(どちらも変数かパス)は、分岐の中で関係として覚える(`op` は `<`・`<=`・`=` と、向きを替えたもの)。覚えるのは、二つが数どうしか日どうしで、比べがどちらの側も単位を読み替えないときだけである。比べが読み替えるのは定数を書いた名前(`let k = 1USD` を `money[USDc]` の値と比べる)で、そのとき条件が言うのは、読み替えた値と相手の関係であり、名前の値どうしの関係ではない。関係は推移をたどって増やす(`a <= b` と `b <= c` から `a <= c`)。`running_totals` の要素は、重みが 0 以上なら `before <= upto` と `upto <= total` を持つ(3.6)。リストをフィールドで足した値(`sum(xs, x => x.amount)`)は、比べの一方の側として、リストの名前とフィールドのパスで覚える。`running_totals(xs, x => x.amount)` の要素の `total` は、同じリストを同じフィールドで足した値と等しいとして覚える(ラムダの引数の名前は違ってよい。32 章)。関係を覚えた二つの名前の差(`n < m` の分岐の `m - n`)は、その向きの区間に狭める(`m - n` は刻み一つ以上)。引き算がどちらの側も単位を読み替えないときに限る。
関係は、規則の前提(入力どうしの関係 `left op right`)を呼び出しの場所で決めるのに使う。区間だけでは、`before` と `upto` がどちらも 0 から合計までの値をとると分かるだけで、`before <= upto` を示せない。ritsu-cross の X2 は、渡す二つの値の区間のいちばん厳しい角で前提を確かめる(ritsu の DESIGN 7.4)ので、関係を知らなければ、起きない角(`before` が合計で `upto` が 0)で前提を破ると言うことになる(kaname の呼び出しでは、角を例にせず、関数のベクタの実行で確かめる。6.4)。kaname は、規則の呼び出しごとに、そこで成り立つ入力どうしの関係を残し(`FnFacts::relations`、`CallSite::relations`)、口 `Programs` で ritsu-cross に渡す(14.2)。関係が前提を言えば、X2 は区間を見ずに成り立つとする(14.3)。
### 5.4 関数の呼び出しと再帰
関数は一つずつ解析する。呼ばれる関数は、宣言した引数の型と範囲から一度だけ解析し、その結果の抽象の値(結果に書いた範囲と、書かない端は型の全体と重ねたもの)を、呼ぶ側が使う。呼ぶ側は、渡す値が引数の範囲に収まるかを確かめる(収まると示せなければ `range` の失敗しうる場所。4.2)。ほかのモジュールの関数は、そのモジュールの解析の答えを読む。
型の引数を持つ関数(読み直す関数)は、呼ぶところで、呼び出しが渡す値で本体をもう一度解析し、その答え(宣言した結果と重ねたもの)を呼び出しの答えにする(26.5)。入れ子は 4 段までで、それを越えた呼び出しと、呼んだ関数の輪の中の呼び出しは、一度だけの解析の答えを使う。ほかの関数は、呼ぶ場所ごとに中を読み直さないので、解析はモジュールの大きさに、読み直す呼び出しの数を掛けたものに比例する。
ラムダは、それを受け取る組み込みの関数の中で、要素の抽象の値と、外の変数の抽象の値と関係のもとで、その場で解析する。関数の型の引数を持つ関数も読み直す関数で、呼ぶところで、渡した関数の集まりを引数に置いて本体を読み直す。関数の値の呼び出しは、それがとりうるラムダの本体を、呼び出しの値で読む(27.4)。
再帰の輪の中の関数の結果は、結果に範囲を書いていればその範囲、無ければ型の全体(数なら int64 の全体)に型の端の印を付けたものにする(22.2)。不動点を求めて範囲を狭めることはしない(19 章)。輪の関数の答えを規則の入力に渡すような確かめは、たいてい決められない(W)になり、生成したコードが実行時に確かめる。範囲を書かない答えから来る int64 のあふれ(`tests/modules/rec.kn` の `countdown(n: n - 1) + 1`)は、型の端から来たあふれで、診断を出さない。範囲を書いた輪の関数は、結果がその範囲に収まるかを、関数の中で確かめる(範囲の外になる例があれば E301、示せなければ W301)。自分を含む型の値をたどる関数(`tests/modules/trees.kn` の `size`)も、再帰を持つので輪の中にあり、答えはこの決まりで読む(28.1)。
### 5.5 リストと組み込みの関数
| 関数 | 答えの抽象の値 |
|---|---|
| `count(xs)` | 要素の数の区間 |
| `count(xs, p)` | 0 から要素の数の上の端まで(条件がいつも成り立つ・成り立たないと分かればそれに合わせる) |
| `sum(xs, f)` | 要素の数の区間と `f` の区間の積の区間(`f` が負にもなれば、下の端も数の上の端で掛ける。空になりうるリストなら 0 も含める) |
| `map(xs, f)` | 要素の数は同じ、要素は `f` の抽象の値 |
| `filter(xs, p)` | 要素の数は 0 から上の端まで、要素は `p` で狭めた抽象の値 |
| `first(xs, p)` | `none` になりうる、値は `p` で狭めた要素の抽象の値(要素が `T?` なら、その値の側。3.6) |
| `max_of`、`min_of` | 要素の数の下の端が 0 なら `none` になりうる、値は `f` の抽象の値 |
| `any`、`all` | 真偽の集合(空になりうるリストでは、`any` は `false`、`all` は `true` もとりうる) |
| `running_totals(xs, w)` | `total` は `sum(xs, w)` の区間、`before` は要素の数の上の端より一つ少ない数までの和、`upto` は一つ以上、上の端までの和。重みが 0 以上なら関係も。`w` がフィールドをたどるだけのラムダなら、`total` は同じ `xs` を同じフィールドで足した `sum` と等しいという関係も(5.3、32 章) |
| `reduce(xs, a, f)` | `a` から始めて `f` を重ね、重ねた回数ごとの値を合わせる。要素の数の上の端が 16 以下ならその回数だけ重ねる。それより多いか決まらなければ三回重ね、落ち着かなければ動いた端を外して(数なら端の無い区間。直和型は値ごとにフィールドを同じに外し、始めに無かった値のフィールドは何でもありうる値にする)、もう一度重ねる |
**決定(`reduce` を要素の数だけ重ねる)**:要素の数の上の端が小さいリスト(引数のリストは要素の数の上限を書く。3.9)では、その回数だけ重ねても解析は軽く、答えは本当にとる値に近い。`product(xs: list[number] count <=6 range >=1 <=9)` の掛け算の答えは 1 から 531,441 で、int64 に収まると示せる。三回で止めて端を外すと、端の無い区間になり、W302 を出す。16 回を超えるリストは、三回で落ち着かなければ端を外す。
辞書と集合、24.3 で足したリストの関数、`numbers` と値で割る `mod` の答えは、24.5 の表のとおりで、文字列の関数の答えは 25.6 の表のとおりである。関数を取るもの(`sort_by`、`group_by`、`map_values`、`merge_with` など)のラムダも、要素(辞書では組か値)の抽象の値で、その場で解析する。
### 5.6 int64 のあふれと、0 で割ること
**決定(あふれの決まり)**:途中の値を既約分数にしたとき、分子が int64 に収まり(−2⁶³ から 2⁶³ − 1)、分母が 1 から 2⁶³ − 1 に収まらなければ、失敗する(種類は `overflow`)。途中の値は、その式の型の単位で数えた値である(お金は宣言した単位、量はその単位、率は分数そのもの、数はそのまま。8 章)。丸めた金額と量と数は分母が 1 なので、値そのものが int64 に収まることで、率の分母は刻みから決まる。定数を相手の単位で読むところ(`15lb` を `mass[oz]` と比べる)では、読んだ値(240)も、この決まりで確かめる。
既約分数の分子と分母で決めたのは、決まりが値だけで決まり、表し方(刻みの静的な倍率、途中の分数の形)に依らないからである。rulec は、値を「実行時の整数 × 型に付いた静的な倍率」で持ち、その整数が int64 に収まることを E108 で示す(rulec の DESIGN §7.1)。kaname は値で割れるので、倍率が静的に決まらない。値で決めれば、参照インタプリタと四つの出力先が同じところで失敗する。
一つの演算の途中の積(分数の足し算の `n1·d2 + n2·d1`)は、どの値も int64 に収まっていれば、128 ビットの整数に収まる。試作で確かめた桁数は、最大 127 ビットの絶対値である。参照インタプリタは i128 で計算して、答えを既約にしてから確かめる。生成したコードは、TypeScript が `bigint`、Python が `int` で計算してから同じ確かめをし、Go は和と差を符号で見て、積と丸めていない値を標準ライブラリの `math/big` で計算する(10.4)。
範囲の解析は、確かめる演算(`+`・`-`・`*`・`/`、符号の `-`、`in`、丸め、`allocate` の積と商と丸め、`sum` と `running_totals` の途中の和、定数を読み替えたところ)ごとに、区間と分母の上限から、分子と分母が収まると示せるかを見る。示せる演算では、生成したコードは確かめずに計算する。示せない演算は W302 で、生成したコードが確かめる。例がある演算は E302 である(5.7)。示せない理由が型の端から来る値だけなら、型の端から来たあふれで、診断を出さず、例も探さずに、生成したコードが確かめる(22.2)。
単位を読み替えるところでは、読み替えた一つ一つの数が int64 の決まりを受ける。リストの中の数とオプショナルの中の数も同じで、値が何でもありうるときも、示せない場所として残す。リストの定数 `[600_000_000_000_000_000lb]` を `list[mass[oz]]` の答えにする関数は、16 倍した数が int64 を超えるので、ここに場所が要る(`bounds.kn` の `many`。E302)。`x in u` の場所は、値が数のときのほか、何でもありうるときとオプショナルのときにも置く(型の検査はオプショナルの `in` を E104 にし、数の値は何でもありうる値にならないので、検査を通ったモジュールでは数のときだけが届く)。何でもありうる値は、オプショナルの中にあっても、int64 に収まると示さない。
割る数が 0 になりうるかは、割る数の区間が 0 を含むかで見る(`allocate` の三つ目の引数も)。分岐で 0 を除いた割る数(`if count > 0` の中の `total / count`)は示せる。示せなければ W303 と実行時の確かめ、例があれば E303。
### 5.7 三つの結果と、例を探すこと
渡す先の確かめ(規則の入力と日付の整数の入力、関数の引数と結果、レコードのフィールド、`isqrt` の引数)、あふれ、0 で割ることは、どれも次の三つのどれかを言う(P5)。
1. **示した**:抽象の値が渡す先の範囲に収まる(あふれない、0 にならない)。何も言わない。生成したコードは確かめない。
2. **例がある**:その関数の実際の入力のうち、そこで範囲の外になる(あふれる、0 で割る)ものを見つけた。エラー(E301・E302・E303)で、注にその入力と、そのとき渡る値を書く。
3. **決められない**:どちらでもない。警告(W301・W302・W303)で、範囲の解析の見積もりと、生成したコードがその場所で実行時に確かめることを注に書く。
int64 のあふれの場所は、示せない理由がどれも型の端の印から来るなら、四つ目の結果の**型の端から来たあふれ**である。何も言わず、例も探さない。生成したコードがその場所で確かめ、関数のエフェクトに `overflow` が入る(22.2)。
**決定(例は、関数の入力で示す)**:範囲の解析の見積もりは上からのもので、区間の端は本当にはとらない値のことがある。端を例にすれば、起きないことをエラーと言うことになる(P5 が禁じること)。そこで、抽象の値が収まらない場所では、例を探す(`src/witness.rs`)。
- その関数のベクタ(9 章)を、参照インタプリタで流す。参照インタプリタは、示せなかった場所の式に来た値を見張って残す(`Interp::watch`)。渡す先に来た値を、渡す先の型で読んで確かめ、外れていればそれが例である。あふれと 0 で割ることは、その式で止まった失敗が例である。
- 渡す先に来る値が、引数(やそのフィールド)の定数倍の和(`late = paid_on - due` のように。9 章)なら、その和が渡す先の範囲の一つ外(あふれなら int64 の一つ外、割る数なら 0、`isqrt` なら −1)になる入力を、引数を一つずつ動かして作り、流す(動かす値は、その引数の範囲と刻みの中に限る)。引数をそのまま運ぶ場所は、この形の一つである。
- 見つかれば例がある、見つからなければ決められない。
例を探すのは、示せなかった場所があるときだけである。どの場所も示せる関数(17 章の例の全部)は、`check` でベクタを流さない。
区間の見積もりが外れる典型は、二つの値の相関を失うことである。一つの値を二度使う式(`side * (10 - side)` の `side` は、同じ値なのに、二つの区間として掛ける)と、規則の出力を使う式がそうである。カードの支払いから手数料を引いた額(`payment.amount - fee`)を振替の額にすると、`fee` は規則の出力で、kaname は規則の中身を計算し直さない(ritsu の P4)。規則の出力の区間は全部の入力についての最小と最大(49 セントから 4,990,049 セント)なので、引いた額の区間は負の値を含む。金額が 1 セントなら手数料は 49 セントで、引いた額は −48 セントになる。これはベクタに入る入力なので、例があると言える。金額の範囲を 100 セント以上に狭めると、本当の引いた額はいつも 46 セント以上になる(100 セントの海外のカードで、手数料は 54 セント)が、区間は負の値を含んだままで、ベクタにも例が無い。そのときは決められない(W)と言い、生成したコードが確かめる。振替の額を決めるのは ritsu-cross の X4(6.3、14.3)で、1 セントの支払いの形は ritsu の E203、100 セント以上の形は ritsu の W203 になる(17.6、12 章に実物)。入力を狭めた範囲で数の出力の区間を rulec に尋ねる口があれば、この見積もりは細かくなる(19 章)。
## 6. 内側の層を呼ぶ
### 6.1 規則
kaname は規則を、口 `Rules` の `facts`(`RuleFacts`)で読む。入力と出力の名前、型(単位、税区分、率の刻み、範囲、オプショナル、列挙)、列挙の値、前提、並び、生成したコードの呼び方(`Call`)がそこにある。`.rule` を自分で読み解くことはしない(P6)。
- **入力**:引数を入力の名前で書く。足りない入力、無い入力、二度書いた入力は E107。オプショナルな入力(`T?`)は省けて、省けば `none` を渡す。型と単位が合わなければ E102(ritsu の X1 にあたる。dandori の E003 と同じく、言語の検査に置く。ritsu の DESIGN 7.3)。値が入力の範囲に収まるかは、範囲の解析で確かめる(E301・W301)。率は刻みの数で比べる。順序のある列挙の入力に値で範囲を書いた規則(`campaign : tier range >=basic <=gold`)では、渡す値がとりうる列挙の値が、どれも口の `ColumnType::Enum` が渡す範囲に入る値かを見る(`ranges::What::RuleEnum`。場所の鍵は、数の入力と同じ `input:<名前>`)。どれも入れば示した場所とし、入らない値がありうれば、関数のベクタでその値を渡す入力を探して、見つかれば E301、無ければ W301 にする。見出しは数と同じ文で、範囲は範囲に入る値の最初と最後で `>=basic <=gold` と書く(dandori の E014、sekisho の E206 と同じ)。W301 の注には、範囲の解析が見た値を列挙の順に挙げる。範囲の外の値は、参照インタプリタでは口の `eval` が、生成したコードでは規則の生成物の入口が受け付けず、どちらも `rule` の失敗になる(10.4)。
- **出力**:出力のレコード `<規則>.outputs` を返す。数の出力の範囲は `output_values` の最小と最大、列挙と真偽の出力は、宣言した値の全部から始める。`output_values` が言う範囲は、rulec が確かめたもので、出力の行に書いた範囲か、出力を決めるもの(表の行が決める値、同じ名前の `define`、並びをたどる行や開いた値が宣言した範囲、`result` の式が入力の範囲と `constraint` のもとでとる値)を丸めの刻みへ外向きに広げたものである(rulec の §15.268、§15.269)。rulec が範囲を持たない出力は、型の全体から始める(5.1、32 章)。
- **前提**(rulec の `constraint`、並びの合計と長さ、`range from koyomi` の日):ritsu-cross の X2 が決める(ritsu の DESIGN 7.4)。kaname は、渡す値の区間、出どころ、二つの値の関係、並びの長さの上限を口 `Programs` で渡し、区間で示せない前提があれば、見た値も渡す(14.2)。並びの長さは、kaname がリストの要素の数を知っているので、dandori と違って決められることがある(口の `preconditions_hold` の `max_len` に渡す)。
- **koyomi の日を日付の入力に渡す**:X3 の (a)(ritsu の DESIGN 7.5)。渡す日が koyomi の日付の答えなら、koyomi が数えた日の集合で、kaname の区間の日付なら、その区間の日で決める(14.2)。
- **並び**(`elements`):並びの名前の引数に、要素のレコード `<規則>.<並び>` のリストを渡す。要素のフィールドの範囲も、入力と同じく確かめる。
- **生成したコードの呼び方**:口の `Call`(TypeScript・Python・Go・Rust のモジュール、関数、引数の名前と型、列挙のメンバー)から書く。rulec の生成物を読まずに書ける。
### 6.2 日付の関数
kaname は日付のファイルを、口 `Dates` の `facts`(`DateFacts`)で読む。`use dates terms from "…"` の下の日付の名前ごとに、`terms.<日付>(<入力>: …)` で呼べる。
- 日付の入力には `date` を、整数の入力には `number` を渡す。日付ごとに、それが読む入力だけを渡す(口が日付ごとに読む入力を言う)。
- 日付の入力に渡す日が入力の範囲に収まるかは、ritsu-cross の X6 が決める(ritsu の DESIGN 7.8)。整数の入力は kaname が範囲の解析で確かめる(E301・W301)。dandori と同じ分け方である。
- 答えは、その日付の日(`date`)である。範囲の解析は、`values`(範囲のすべての入力で日付がとる日の集合)の両端から始める。日の集合そのものは、koyomi の日を規則に渡すところ(X3 の (a))で ritsu-cross が使う。
- koyomi の検査を通らない日付のファイルは E201 で、koyomi が言うことを注に挙げる。
### 6.3 帳簿の振替の値
**決定(振替は値)**:kaname の関数は帳簿に書かない。振替の呼び出し(どの振替の、どの操作を、どの引数で)を型の付いた値として返し、ホストが chobo のクライアントで当てる。帳簿の残高が計算に要るなら、ホストが読んで引数として渡す。
```kn
record Settlement
capture : ledger.capture.do
fee : ledger.processing_fee.do
fn settle(payment: Payment) -> Settlement total
let fee = card_fee(amount: payment.amount, international: payment.international).fee
Settlement {
capture: ledger.capture.do(payment: payment.id, seller: payment.seller, amount: payment.amount),
fee: ledger.processing_fee.do(payment: payment.id, seller: payment.seller, fee: fee),
}
```
- **型**:`<帳簿>.<振替>.<操作>`。値は、帳簿、振替、操作と、引数の名前ごとの値(文字列は文字列、額は帳簿の単位で数えた整数)を持つ。同じ帳簿のファイルを読む二つのモジュールの同じ振替の操作は、同じ型である(列挙と同じく、宣言した場所で決まる。3.5)。
- **引数と単位**:口 `Books` の `facts` の振替の引数と単位で確かめる(E102、E204。3.8)。
- **額**:額の引数に渡す値が 0〜2⁶³ − 1 に収まるかは、ritsu-cross の X4 が決める(ritsu の DESIGN 7.6)。dandori の X4 は、規則の出力から来る額だけを見て、ほかの額を dandori の範囲の検査(E014)に任せている。kaname の額は、規則の出力をそのまま運ぶものも、kaname が計算したものも、どれも X4 で見る(14.3)。
- **拒否の理由**:振替の操作が、どの理由で拒否されうるかは、ritsu-cross が chobo に尋ねる(`Books::refusals`。額を、その場所の額の区間に限った探索)。ritsu-cross は、組み立てる場所ごとに、見つけた理由を返す(14.3)。`Books::refusals` は理由に至る操作の例を口で渡さないので、例は返さない。kaname は、モジュールの全部の組み立てる場所の答えを、操作ごとにまとめる(`api` の `transfers`。11.1)。生成するコードは、その操作の振替の値に、拒否されうる理由の型(帳簿の境界の理由のうち探索が見つけたものと、chobo が操作ごとに決めている理由。`key_conflict`、`already_refused` など。chobo の DESIGN 2.7)を付ける(10.6)。探索が決められない場所があれば、その操作の型には、境界の理由の全部を載せ、ritsu-cross が W204 で言う。拒否を扱うのはホストなので、kaname の中で、拒否を扱っていないことをエラーにはしない。
- **当て方**:生成するパッケージに、帳簿ごとに、振替の値を chobo のクライアントの呼び出しに直す関数(`applyLedger(book, call)` など)を置く(10.6)。答えは chobo のクライアントの答え(`done`、`done_before`、`refused` と理由)のままである。
- **いくつもの振替**:一つの関数が返す振替の一つ一つは、それぞれ帳簿の一度の書き込みである。全部が通るか、どれも通らないかが要るなら、帳簿の一つの振替に移動を並べて書く(chobo の DESIGN 1.4)。kaname がいくつもの振替を一つの書き込みにまとめることはしない。どの順に当て、途中で拒否されたらどうするかは、ホストか dandori のワークフローが決める。
### 6.4 三つの結果、診断、生成したコードでの確かめ
呼び出しの場所の確かめと、それを決めるもの:
| 確かめ | 決める | 例がある | 決められない | 実行時に確かめるもの |
|---|---|---|---|---|
| 単位と型(X1) | kaname | E102(型の誤りなので、決められないことは無い) | — | — |
| 規則の入力(数と、範囲を書いた順序のある列挙)と日付の整数の入力の範囲 | kaname | E301 | W301 | 規則と koyomi の生成したコードの入口(`rule`・`date`) |
| 規則の前提(X2) | ritsu-cross | ritsu の E201 | ritsu の W201 | 規則の生成したコードの入口(`rule`) |
| koyomi の日を規則に(X3 の (a)) | ritsu-cross | ritsu の E202 | ritsu の W202 | 規則の生成したコードの入口(`rule`) |
| 振替の額(X4) | ritsu-cross | ritsu の E203 | ritsu の W203 | kaname の生成したコード(`range`) |
| 振替の拒否の理由(X4) | ritsu-cross | — | ritsu の W204 | (ホストが扱う) |
| koyomi の日付の入力(X6) | ritsu-cross | ritsu の E205 | ritsu の W205 | koyomi の生成したコードの入口(`date`) |
**決定(言語をまたぐ判定でも、例は関数の入力で示す)**:ritsu-cross の判定は、dandori の呼び出しでは、範囲の角や端の値を例にする(dandori の範囲は宣言した範囲で、値ごとに独立にどの値もとりうるからである。ritsu の DESIGN 7.4)。kaname の区間は計算で求めた上からの見積もりなので、角は起きないことがある。そこで ritsu-cross は、kaname の区間で示せない判定があるときだけ、その関数のベクタで見た値(関数の入力と、そのとき渡った値)を口 `Programs::seen` で尋ね、見た値の中にその確かめを破るものがあるかを見る。あればそれを例にエラー、無ければ決められない(警告)にする。破るかの判定は、前提なら rulec に、その一点の範囲で尋ねる(`preconditions_hold` に両端の同じ範囲を渡す。ritsu の P4)。並びの合計の前提は、rulec が「要素の数 × いちばん大きい要素」で上から決めるので、要素がどれも同じリストのときだけ、その一点の答えを例にする。日と額は、範囲の外か、chobo の受け取る額の外かを、渡った値でそのまま見る。
ritsu の台帳のコード(E201〜W205)は、dandori の呼び出しと kaname の呼び出しの両方に使う(14.4)。注の文は呼ぶ側の言語で変え、kaname の呼び出しでは「kaname の生成したコードが、走らせたときにその場所で確かめます」と書く。
生成したコードでは、決められない場所と例がある場所で、次のように確かめる(10.4)。
- 規則と日付の呼び出し:生成物の関数が入口で受け付けなければ(rulec の `RuleInputError`、koyomi の `KoyomiError`)、`KanameError` の `rule` か `date` にする。行は呼び出しの行で、文は規則か日付を名前で言い、生成物の例外を原因として持たせる。示した呼び出しでは包まない。そこで生成物がエラーを返すのは ritsu の不具合なので、Go では panic にする(koyomi の生成物が、起きないはずの `none` で panic にするのと同じ)。
- 振替の額と、kaname の渡す先:値を渡す前に範囲を比べ、外れれば `KanameError` の `range`。
- あふれと 0 で割ること:確かめつきの算術と、割る前の比べ。
## 7. 逃げ道の警告
kaname は計算を持つので、表に書くべき判断を `if` や `match` で書けてしまう。表に書けば、rulec が完全性と重なりを確かめ、承認する人がページで読める。kaname に書けば、どちらも無くなる。そこで、その形を W501 で警告する(`src/hatch.rs`)。
**決定(警告にする二つの形)**:
1. **定数を選び分ける分岐**:`if` か `match` が、分岐のどれかで業務の定数を答えにする。`if` が数える業務の定数は、お金、量、率、日付のリテラル、列挙の値、`true` と `false`、それだけでできたレコード(値の無いフィールドは省ける)である。`match` が数えるのは、お金、量、率、日付のリテラルと、それだけでできたレコード(フィールドを持つ値も)だけである(23.9)。`..x` から作るレコードは数えない。答えは分岐のブロックの最後の式で見る。`else if` の続きは一つの判断として、最初の `if` で一度だけ言う。
2. **しきい値を値にする比べ**:業務の値と、0 でない業務のリテラル(お金、量、率、日付、数。符号の付いたものも)の比べを、`if` の条件と `match` で分ける値のほかで使う。`let` に入れる、引数に渡す、答えにする、組み込みの関数の条件にする(`count(xs, x => x.weight > 2lb)`)。`and`・`or`・`not` でつないだ比べは、全体が `if` の条件なら警告にしない。
**決定(警告にしないもの)**:
- 答えが `none` の分岐。値が無いことは業務の定数ではない。
- 分岐のどれかが規則か日付の関数を呼び、ほかの分岐が業務の定数を答えにしないもの。規則に渡す値を、規則の入力の範囲に狭めるための分岐(`if late >= 1` の中で規則を呼び、外は `none`)がこの形である。
- 列挙の値、`true` と `false`、数を答える `match`。どの値にも一度だけ答えることは、kaname が E106 で確かめる(23.9)。規則の列挙のあいだの読み替えも、この形である。
- `match` で `none` と `some x` に分けるだけのもの。値が無いときの既定の値(`none => 0USDc`)を答えても、警告にしない。
- 0 との比べを値にするもの(`count > 0`、`amount >= 0USD`)。空かどうか、正か負かを見るもので、業務のしきい値ではない。ただし、0 と比べて業務の定数を選ぶ分岐は、1 の形で警告になる。
- `if` が数を答えにするもの。再帰の終わり(`if n <= 0 then 0 else …`)と空のリストの場合分け(`if count(xs) = 0 then 0 else sum(…)`)がこの形で、`if` の数の答えまで数えると、つなぐコードの普通の書き方が警告になる。
**決定(例で確かめて直した形)**:7 章の例と、その周りの形を `tests/hatch.rs` で確かめ、次のように直した。
- `true` と `false` を選ぶ `if`(`if total >= 35USD then true else false`)は、2 の形の `let free = total >= 35USD` と同じ判断である。真偽を業務の定数に入れないと、`if` で書き直すだけで警告が消える(少なすぎる)。そこで真偽も業務の定数にした。
- 列挙の値ごとに数を答える `match`(`basic => 1`、`silver => 2`、`gold => 3`)は、警告にしない。区分ごとの点数の表とも読めるが、`match` はどの値にも一度だけ答えることを kaname が E106 で確かめるので、表に書く理由の一つ(完全性と重なり)を kaname の検査が受け持つ。状態遷移の `match` が列挙の値を答えるのも同じである(23.9)。
- `else if` を続けた判断は、`if` ごとに言うと、一つの判断に警告が二つ以上出る(多すぎる)。最初の `if` で一度だけ言う。
- 分岐が規則を呼ぶかは、答えの式だけでなく、分岐のブロックの全部で見る(`let c = charges(…)` のあとで `c.compensation` を答える分岐も、規則を呼ぶ分岐である)。そういう判断には、`none` を答えるか、規則に行を足すかを勧める注を付ける。
例(どれも警告になる):
```kn
fn shipping(total: money[USD] range >=0USD <=10_000USD) -> money[USD]
if total >= 35USD then 0USD else 5USD # 1 の形。しきい値も送料も表に書く
```
```kn
fn surcharge(tier: tier) -> money[USD]
match tier
gold => 0USD # 1 の形。区分ごとの金額は表
silver, basic => 3USD
```
```kn
fn quote(order: Order) -> money[USD]
let free = order.total >= 35USD # 2 の形。しきい値が値になる
shipping(total: order.total, free: free).fee
```
規則を呼ぶ分岐と `none` の分岐は警告にならない(`examples/late_payment` の `claim`。17.7)。答えを補償の額 `money[GBP]` にして、外を `0GBP` にすると、1 の形で警告になる(実物):
```text
warning[W501]: late_payment.kn:22:3: This `if` chooses a constant of the business by the values it compares (`0GBP`): a decision, which belongs in a table
22 | if late >= 1
= Write the decision in a rulec table and call the rule: there, rulec checks it is complete and without overlap, and the person who approves it reads it on its page.
= Another branch calls a rule. If no answer is meant in this case, answer `none`; if the rule decides this case too, give it a row and widen the range of its input.
```
```text
警告[W501]: late_payment.kn:22:3: この `if` は、比べた値によって業務の定数(`0GBP`)を選んでいます。表に書く判断です
22 | if late >= 1
= 判断を rulec の表に書いて、規則を呼んでください。表に書けば、完全性と重なりを rulec が確かめ、承認する人がページで読めます。
= ほかの分岐は規則を呼んでいます。この場合に答えが要らないなら、`none` を答えにしてください。この場合も規則が決めるなら、規則に行を足し、その入力の範囲を広げてください。
```
この形を警告にするのは、「遅れていなければ 0」は規則が決められることだからである(遅れた日数 0 の行を表に足し、`days_late` の範囲を 0 からにする)。kaname がそれを決めれば、承認する人は、0 日のときの答えを規則のページで読めない。
**決定(エラーにせず警告にする)**:判断がつなぐコードにあることが正しい場合もある(ホストの都合で、規則の外に置くと決めた選び分け)。警告は、それがどこにあるかを `check` と人が読むページ(13 章)に必ず出す。警告を消す印(意図を書いて黙らせる書き方)は、いまは作らない(19 章)。消せない警告は、判断を規則に移す理由になる。
例の四つ(17 章)の八つの `.kn` は、どれも警告が出ない(`tests/hatch.rs`)。
## 8. 参照インタプリタ
kaname の意味は、参照インタプリタ一つが決める(P2)。`src/interp.rs` にあり、`check` の `examples`、`run`、`vectors` は、どれもそれを通す。例を探すこと(5.7)と、生成したコードの突き合わせ(16.4)も、それを通す。検査を通ったモジュールだけを評価し、検査が式ごとに求めた型と、名前が指すもの(`check::Checked` の `typed`)を読む。
- **値**(`src/value.rs`):数、真偽、文字列(コードポイントで数える。25.5)、日付(日の番号)、列挙の値(宣言した名前)、フィールドを持つ値(値の名前と、フィールドを宣言した順に。23.6)、レコード(宣言した名前のフィールドを宣言した順に)、リスト、辞書(鍵の順の組の並び)、集合(鍵の順の要素の並び)、`none`、振替の値、関数の値(ラムダの式と、作ったときに見えていた名前の値。27.3)。値のある `T?` は、値そのものである。辞書と集合の鍵の順は `key_cmp` の一つの関数が決め、作るところが同じ鍵を一つにして並べる(24.1、24.4)。
- **数と単位**:数は正確な有理数で、その式の型の単位で数える。お金は宣言した単位、量はその単位、率は分数そのもの(3.49% は 349/10000)、数はそのままである。どの単位で数えるかは型が決めるので、検査が定数を相手の単位で読んだところ(`15lb` を `mass[oz]` と比べる。3.2)では、評価も読み替える(240 オンス)。値を渡す先(関数の引数と答え、レコードのフィールド、規則と日付の入力、振替の引数、型を書いた `let`)では、渡す先の型の単位で読む。数と率は、どちらも分数そのもので数えるので、読み替えても値は変わらない。
- **算術**:i128 で計算し、演算ごとに答えを既約にして、5.6 の決まりで確かめる。収まらなければ `overflow` の失敗、割る数が 0 なら `zero` の失敗である。単位を読み替えた値も同じに確かめる。
- **丸め**:3.3 の整数の手順(`src/num.rs`)。**日付**:3.4 の Hinnant の手順(`src/date.rs`)。
- **等しさ**:`=` と `!=` は、両辺を比べる型の単位で読んでから、数と日付は値で、ほかの型は中身で比べる(23.5)。**`match`**:上の分岐から順に、値が合う最初の分岐を選ぶ(23.6)。
- **組み込みの関数**:3.7、24.3、25.2 の表のとおり。並べ替えは安定な手順で、鍵の順で比べる。`numbers` は、要素が 1,000,000 を超えれば、深すぎる呼び出しと同じく評価を止める(24.3)。文字列の関数は `src/text.rs` にあり、`pad_start` と `pad_end` が 1,000,000 コードポイントより長く伸ばすときも、評価を止める(25.5)。`allocate` は、`t × c` も途中の値として確かめる(rulec も、割る前の積を int64 に収める。rulec の DESIGN §15.102)。`isqrt` は、負の値に `range` で失敗する。rulec では E139 が検査で止める値で、kaname では範囲の解析が、`isqrt` の引数を 0 以上の範囲の渡す先として確かめる(4.2)。`mod`・`isqrt`・`allocate`・丸めの数の引数が数でなければ、0 と読まずに、そこで評価を止める(Rust の panic)。型の検査を通ったモジュールでは起きないことで、失敗の種類ではない。Lean のモデルも、そこで止まる(15.1 の `type`)。
- **内側の層**:規則は口の `Rules::eval` で評価する。値は rulec の受け渡す整数にして渡し(率は刻みの数、日付は `YYYY-MM-DD`、並びは `Value::List`、無い値は `Value::None`)、出力を同じ形で受け取る。規則が入口で受け付けなければ(`RuleError::Input`)`rule` の失敗である。日付の関数は `Dates::eval` で評価する。koyomi の口は日付のファイルの入力を全部求めるので、その日付が読まない入力には、入力の範囲の下の端を渡し、先に計算される別の日付がそこで止まれば、上の端で計算し直す。日付が止まるか、入力が範囲の外なら `date` の失敗である。振替は値を作るだけで、帳簿を開かない。額が 0 より小さければ `range` の失敗にする(chobo が受け取らない額。6.4)。
- **入口**:ホストが呼ぶ関数(`run` の関数、`examples` の行の関数、ベクタの一件ごとの関数)の引数を、自分を含む型の値の入れ子の深さで、次に宣言した型と範囲と要素の数で確かめ、外れれば計算を始める前に `input` の失敗にする。入れ子の深さは、引数から一つの道をたどり、自分を含むレコードと直和型の値を一つ通るたびに一段と数え、200 段を超える値があれば、その型の名前を言って失敗する(「The argument `c` of `size` nests `Category` more than 200 deep」。28.1)。深さを先に確かめるので、範囲の確かめは 200 段より深くたどらない。引数が届くモジュールのレコードのフィールドとリストの要素(リストのリストの要素も、自分を含む型の値の中のどの深さのものも)も確かめ、範囲を書かなかった刻みのある率は 0% から 100% として、範囲の書かない端のある数と日付は型の全体(int64、0001-01-01 から 9999-12-31)として確かめる(3.9、22.1)。JSON から読む値は、int64 に収まらない数と、`YYYY-MM-DD` の 0001-01-01 から 9999-12-31 の日でない日付が、読むところで `input` の失敗である。文字列は、書いた `count`(コードポイントの数)を確かめる。孤立したサロゲートのある JSON の文字列は、JSON として読めない(25.5)。規則の並びの要素のフィールドは、規則が自分の範囲で確かめる。`run` の JSON は、引数とフィールドと列挙の値を、宣言した名前でも別名でも書ける。
- **渡す先**:kaname の関数がほかの kaname の関数を呼ぶところでは、引数を、入口と同じに確かめる。ただし、自分を含む型の値の中は確かめない(28.1)。関数の答えに範囲を書いていれば答えを、モジュールのレコードを作るところではフィールドに書いた範囲と要素の数を確かめる。外れれば `range` の失敗である(4.2)。生成したコードは、範囲の解析が示した場所では確かめない(10.4)が、参照インタプリタはいつも確かめる。示した場所では外れないので、答えは変わらない。
- **失敗**:種類(`input` と 4.2 の五つ)、モジュールとそのファイル、行、文(英語と日本語)と、どの関数の呼び出しの中だったか(関数の名前と、呼んだ行の並び)を持つ。失敗したら、その場で評価を止める。
- **評価の順**:引数は書いた順に左から右へ(メソッドの形の初めの引数は `.` の前のもの)、`let` は上から下へ、レコードのフィールドは書いた順に評価する(`..x` はフィールドより先)。辞書のリテラルは、組ごとに鍵、値の順で、それぞれをその場で辞書の型で読む(24.4)。渡す先の範囲は、引数やフィールドを全部評価してから、呼ばれる関数の引数とレコードのフィールドの宣言の順に確かめる。`and` と `or` は左から評価し、左で決まれば右を評価しない(右の 0 で割る式は、左が決めれば失敗しない)。`any`、`all`、`first` は、答えの決まった要素で止まり、残りの要素に関数を当てない。`last` は、終わりから見て条件を満たす最初の要素で止まる(24.3)。生成したコードも同じ順にする。
- **関数の値の呼び出し**:呼ぶ値、引数の順に左から評価し、引数を関数の型の引数の単位で読んでから、ラムダの本体を、作ったときの名前の値と引数で評価する。関数の値の呼び出しは、呼び出しの深さに一段として数える(27.3)。
- **再帰**:関数の呼び出しの深さが 200 を超えたら、評価を止めて「止まらなかった」と言う。これは失敗の種類ではない(`diverges` の関数で起きうることを、参照インタプリタがそこで打ち切ったということ)。`run` は終了コード 1、`vectors` はその一件を `diverged` と書き、`examples` は E601 にする。生成したコードとは突き合わせない。
- **トレース**:内側の層の呼び出しとその答え、作った振替を、起きた順に持つ(`run` と `vectors --trace`)。`run --trace` は、`let` ごとの値も同じ順に持つ。
- **見張り**:式に来た値を、モジュールと式の番号とともに残せる(`Interp::watch`。残す式を限ることもできる)。例を探すこと(5.7)と、範囲の解析のテスト(16.1)が読む。あふれと 0 で割ることの失敗は、止まった式の番号も持つ(`Failure::expr`)。
**決定(呼び出しの深さの上限は 200)**:理由は二つある。一つは出力先の Python で、CPython は既定で 1,000 の深さで止まる。生成した Python では、kaname の関数の呼び出しの一段が Python の一段になる(組み込みの関数はその場のループで、そこに書いたラムダは段を使わない。関数の値の呼び出しは一段である。10.3、27.3)。200 段の呼び出しは、Python の再帰の上限が 203 あれば答え、既定の 1,000 の中に収まる(`tests/python.rs` で測った。16.4)。参照インタプリタが答える深さを出力先が最後まで計算できなければ、ベクタの突き合わせ(16.4)が、出力先のスタックの都合で食い違う。もう一つは、参照インタプリタが Rust のスタックの上で評価することである。デバッグのビルドで一段に 6〜11 KiB を使い(実測)、10,000 段は 100 MiB 近くになる。200 段は 2 MiB ほどで、`ritsu` の 8 MiB のスタックに収まる。つなぐコードの再帰は、明細や期間をたどる浅いものなので、200 で足りる。**捨てたもの**:設計の段階の 10,000 と、それを保つために、参照インタプリタをスタックを使わない形(評価の途中を値として持つ機械)にする案。深くしても、出力先の Python が先に止まるので、突き合わせられない。
## 9. ベクタ
`ritsu kaname vectors <file.kn> [--function <名前>] [--trace]` は、関数ごとに入力を選び、参照インタプリタの答えを添えて、一件一行の JSON で書く(`src/vectors.rs`)。ベクタの役は三つある。例を探すこと(5.7、6.4)、生成したコードの突き合わせ(16.4)、人が答えを確かめる材料である。
**入力の選び方**(引数ごとの候補。引数の中のフィールドと要素も同じに選ぶ):
- 数、お金、量、率:範囲の両端、0(範囲の中にあれば)、関数がその引数(やフィールド)と比べるリテラル(`match` のリテラルの分岐も)と、その刻み一つ前後(リテラルは引数の単位で読み、温度は零点も読み替える。`temperature[℉]` の引数と比べる `30℃` は 86℉。3.10)、その引数をそのまま渡す規則と日付の入力の範囲の両端と、その一つ外。範囲の書かない端は型の端(int64 のいちばん小さい数と大きい数)で、範囲を書かない数は 1 と −1 もとる(22.4)。比べるリテラルとそのまま渡す先は、関数の本体のほか、呼ぶ kaname の関数の中と、組み込みの関数に渡すラムダの中からも集める。引数をそのまま運ぶ名前(`let a = invoice.amount`、リストの要素を受けるラムダの引数、`some x`)も、その引数として読む。
- 日付:範囲の両端、比べる日付のリテラルとその前後の日、その引数をそのまま渡す日付の関数と規則の入力の範囲の両端と、その一つ外。範囲の書かない端は 0001-01-01 と 9999-12-31 である。
- 列挙と真偽:値の全部(真偽は `true`、`false` の順)。オプショナルな値:`none` と、値の候補。
- 直和型:どの値も、基本のフィールドで一つずつ。フィールドを持つ値ごとに、フィールドを一度に一つずつ候補で動かし、ほかを基本の値にしたもの(レコードと同じ)。値の型の引数は、その値だけ(23.7)。`match` で名前を付けた値のフィールドは、引数のその部分として読む(比べるリテラルと、そのまま渡す先を集める)。
- 文字列:`"a"` と、関数の中で比べる文字列のリテラル、`starts_with` の前置き(それで始まるもの。`"a"` が前置きで始まるときは、始まらない `"b"` も)。`count` を書いた文字列は、その両端の数の `"a"` の並びもとり、どの候補も `count` に収める。文字列の組み込みの関数に渡す引数と、大小を比べる引数は、出力先の言語が違って読む 15 の文字列もとる(25.8)。
- 辞書と集合:リストと同じ数(下の端、0、1、2、3、上の端)の、互いに違う鍵の値を並べたもの(辞書の値は基本の値)と、一つ目の組の値と鍵(集合は一つ目の要素)を候補で動かしたもの(24.4)。
- リスト:要素の数の下の端、0、1、2、3、上の端のうち、範囲の中にあるもの(2,000 を超える数は使わない)。要素は基本の値である。さらに、一つ目の要素を要素の型の候補で動かしたもの。
- レコード:基本の値と、フィールドを一度に一つずつ候補で動かし、ほかを基本の値にしたもの。
- `result`:`ok` の値の候補と、`err` の値の候補(26.3)。基本の値は、`ok` の基本の値である。
- 自分を含む型:基本の値は、自分を含まない値である(`T?` は `none`、空にできるリスト・辞書・集合は空、直和型はフィールドが自分を含まない最初の値、`result` は自分を含まない側)。候補は、一つの道の上で同じ型の値を二つまで持つ。自分を含む値の中に、自分を含まない値を一つ置いたものまでで(`Category` なら、子の無い分類と、子が一つから三つの分類)、`T?` の値を置いた候補の理由は「値がある」である(28.1)。
- 型の引数を持つ関数(中の関数。21.3)は、ベクタを持たない。それを呼ぶ関数のベクタが、具体的な型の値でその本体を通る。`vectors --function` に中の関数を書けば、注を一行出す(26.4)。
どの候補も、渡す先の宣言の中にとる。外にとっても、入口の `input` の失敗になるだけで、関数について何も分からない。**基本の値**は、範囲の真ん中に近い値(数。刻みの上で下に寄せる。範囲を書かない数は 0)、範囲の下の端(日付。範囲を書かない日付は 1970-01-01)、最初の値(列挙。直和型は最初の値を基本のフィールドで)、`true`、`"a"`(`count` の下の端が 1 より大きければその数の `"a"`)、基本の要素を要素の数の下の端だけ並べたもの(リスト。下の端が 0 なら 1 個)、同じ数の違う鍵(辞書と集合。辞書の値は基本の値)、基本のフィールド(レコード)、値の基本の値(オプショナルな値)である。
**決定(入力の範囲の一つ外)**:そのまま渡す規則と日付の入力の範囲は、両端に加えて、その一つ外もとる(関数の引数の範囲の中にあれば)。規則や日付の関数が入口で受け付けない入力(`rule` と `date` の失敗)を、ベクタが持つようにするためで、例を探すこと(5.7)と、生成したコードの失敗の突き合わせ(16.4)に使う。
**組み合わせ**:引数ごとの候補の積が 256 件以下なら全部(最後の引数がいちばん速く動く)。超えるなら、基本の値と、一度に一つの引数を動かしてほかを基本の値にしたものと、同じ比べか同じ呼び出し(規則、日付の関数、kaname の関数、振替)に出てくる二つの引数の、候補の組の全部。同じ入力は一度だけ書く。一つの関数で 2,000 件までで、超えれば、そこで止め、止めたことを標準エラーに一行書く。どれも決まった順で選び、乱数を使わない。
**決定(計算した値の境目)**:比べの両辺が、引数(やそのフィールド、リストの最初の要素のフィールド)の定数倍と定数の和で表せるとき(範囲の解析が求める。5 章)、左辺から右辺を引いた和が、境目の一つ前、境目、一つ後(比べる値の刻み一つずつ)になる入力を、ベクタに足す。入力は基本の値から、和に入る引数を一つずつ動かして作る。動かす値は、その引数の範囲と刻みの中にあるものに限る(範囲の外の値は入口の `input` の失敗になるだけで、関数について何も分からない)。引数そのものとリテラルの比べ(`weight <= 15lb` の `weight` が引数のとき)は、もうある候補(そのリテラルと前後の刻み)と同じなので足さない。
`examples/late_payment` の `late >= 1` では、`late` が `paid_on - due` で、`due` は、koyomi が日付の入力(`invoice.invoiced`)のいつも 29 日後と言う日付である(口の `days` の最小と最大が同じ 29)。そこで `late - 1` は `paid_on − invoice.invoiced − 30` の和になり、それが −1、0、1 になる `paid_on`(基本の値の請求日 2026-01-01 に対して 2026-01-30、2026-01-31、2026-02-01)の三件が足される。請求日を動かす解は請求日の範囲の外なので足さない。足された三件(実物の一件目と二件目):
```json
{"function":"claim","in":{"invoice":{"id":"a","invoiced":"2026-01-01","amount":50000000},"paid_on":"2026-01-30","reference_rate":750},"out":null,"why":"paid_on: a step below the boundary of `late >= 1`"}
{"function":"claim","in":{"invoice":{"id":"a","invoiced":"2026-01-01","amount":50000000},"paid_on":"2026-01-31","reference_rate":750},"out":{"due":"2026-01-30","days_late":1,"compensation":100,"interest":21233},"why":"paid_on: on the boundary of `late >= 1`"}
```
理由:比べる値が計算した値だと、引数の候補(範囲の両端、比べるリテラル)を組み合わせても、境目に届く入力がほとんど無い。境目の前後の入力が無ければ、分岐の両方の答えを突き合わせられず(16.4)、渡す先の範囲の端の両側も試せない。例を探すこと(5.7)も、同じ和を解いて、渡す先の範囲の一つ外の入力を作る。
組み合わせと上限への効き方:境目の入力は、引数ごとの候補には入れず、入力の組として足す。そのため、候補の積が 256 件以下かどうか(全部の組み合わせをとるか)は変わらない。全部の組み合わせをとるときはそのあとに、とらないときは一つずつ動かした入力のあと、二つの引数の組の前に足す。一つの比べで足すのは、和に入る引数の数の三倍までである。2,000 件の上限は境目の入力も含めて数え、組の前に足すので、上限で切られるときは組のほうが先に切られる。
足せない形(範囲の解析が、引数の定数倍の和と言えないもの):二つの値の積(`l.weight * l.qty`)と値での割り算、規則の出力と kaname の関数の答えを通した値、日付の関数の答え(koyomi が、日付の入力からいつも同じ日数とは言わないもの)、丸め・`min`・`max`・`mod`・`isqrt`・`allocate` の答え、リストを畳んだ値(`sum`、`count` など)、`if` と `match` の答え、リストの二つ目からの要素。呼んだ kaname の関数の中の比べも足さない(その関数のベクタが受け持つ)。
**形**(rulec の `vectors` に寄せた形)。`examples/late_payment` の一行目と、その関数から分岐を外し、遅れた日数をそのまま規則に渡す関数(`fn interest`。`tests/vectors.rs` の材料)の一行目(実物)。この関数は、規則が受け付けない日数を渡す入力があるので検査で E301 になり(12 章)、`vectors` のコマンドは書かない。下の行は、型の確かめを通ったそのモジュールの一行目を、ライブラリの `vectors::line` が書いたものである:
```json
{"function":"claim","in":{"invoice":{"id":"a","invoiced":"2026-01-01","amount":50000000},"paid_on":"2026-01-01","reference_rate":0},"out":null,"why":"paid_on: the lower end; reference_rate: the lower end"}
{"function":"interest","in":{"invoice":{"id":"a","invoiced":"2026-01-01","amount":50000000},"paid_on":"2026-01-01","reference_rate":0},"fails":{"kind":"rule","module":"late_payment","line":13,"message":"The rule `charges` refuses its inputs"},"why":"paid_on: the lower end; reference_rate: the lower end"}
```
例を探すこと(5.7)と、言語をまたぐ判定が使う見た値(14.2)があるので、検査を通るモジュールのベクタが失敗するのは、決められない場所(kaname の W301〜W303 と、ritsu の W201〜W205)だけである。言語をまたぐ判定の答えを受け取らない実行では、答えを待つ場所でも失敗しうる。
値は受け渡す形(`run` の入力と同じ)で、お金は宣言した単位、率は刻みの数、日付は `YYYY-MM-DD`、列挙は値の名前、フィールドを持つ値は `{"<値の名前>": {フィールド}}`(23.6)、`result` は `{"ok": 値}` か `{"err": 値}`(26.4)、レコードはフィールドの名前(別名ではなく宣言した名前)のオブジェクト、リストは配列、辞書は鍵の順の組の配列 `[[鍵, 値], …]`、集合は鍵の順の要素の配列(24.4)、`none` は `null`、振替の値は `{"book", "transfer", "op", "args"}` である。関数が失敗すれば、`out` のかわりに `"fails": {"kind", "module", "line", "message"}` を、止まらなければ `"diverged": {"module", "line", "depth"}` を書く。`why` は、その入力を選んだ理由で、基本の値から動かした引数(やフィールド)ごとに、その候補の理由を並べる(どれも基本の値なら `the base values`)。`--lang ja` で日本語になる。`--trace` を付けると、内側の層の呼び出しとその答え、作った振替(`calls`。`run --format json` と同じ形)も書く。
英語の版と日本語の版(`x.kn` と `x.ja.kn`)のベクタは、名前を別名で読み替えれば同じになる(16.6)。
## 10. 生成
### 10.1 パッケージの中の置き場所
**決定**:kaname の生成は、`ritsu gen` の一つのパッケージ(ritsu の DESIGN 9.3)の中だけに書く。出力先は、パッケージと同じ TypeScript・Python・Go と、Rust である(Rust のパッケージは一つのクレート。36 章)。kaname のモジュールは、同じパッケージの `rules/` と `dates/` の生成物を呼ぶので、それを持たない単独の生成(`kaname gen`)では動くコードにならない。sekisho の TypeScript・Python・Go がパッケージの `rules/` と `dates/` を読むのと同じ形である(sekisho の DESIGN 5.6)。
```
generated/typescript/ generated/python/<name>/ generated/go/
glue/index.ts glue/__init__.py glue/ のインデックス
glue/kaname.ts glue/kaname.py glue/kaname/kaname.go 補助のモジュール
glue/<モジュール>.ts glue/<モジュール>.py glue/<パッケージ>/<パッケージ>.go モジュールごと
glue/books/index.ts glue/books/__init__.py glue/books/ のインデックス
glue/books/<帳簿>.ts glue/books/<帳簿>.py glue/books/<パッケージ>/… 振替の値の型(帳簿ごと)
glue/books/<帳簿>_apply.ts glue/books/<帳簿>_apply.py glue/books/<パッケージ>apply/… 振替を当てる関数(帳簿ごと)
```
`<モジュール>` はモジュールの別名、Go の `<パッケージ>` はその別名から作るパッケージの名前(`late_payment` は `latepayment`)である。
Rust のパッケージは一つのクレートで、kaname はその `glue/` の下を書く(36 章)。`glue.rs` と `glue/books.rs` は Rust のモジュールの木をつなぐファイル(TypeScript の `index.ts` にあたる)、`glue/door.rs` は、ホストが呼ぶ関数を JSON の一行で呼ぶ入口(36.11)である。
```
<クレート>/
glue.rs glue/ のモジュールの宣言
glue/kaname.rs 補助のモジュール
glue/<モジュール>.rs モジュールごと
glue/door.rs JSON の一行の呼び出しに答える入口
glue/books.rs glue/books/ のモジュールの宣言
glue/books/<帳簿>.rs 振替の値の型(帳簿ごと)
glue/books/<帳簿>_apply.rs 振替を当てる関数(帳簿ごと。wasm32 では外す)
```
パッケージの Wasm には、クレートの根に二つのファイルが要る。canonical ABI の三つの関数の `wasm.rs`(根は wasm32 のときだけ読む)と、パッケージの `.wit` である。中身は kaname の `kaname::r#gen::wasm` が作り、クレートを組み立てる側(`ritsu gen --target rust`。36.17)が置く(36.10)。
- **補助のモジュール**(`glue/kaname.*`):丸めていない値(有理数)、確かめつきの算術、丸めの五つ、`allocate` と `isqrt`、日付と日の番号の換算、`KanameError`、入口と渡す先の確かめの答えの型、`text` の書き方。パッケージに一つだけ書き、kaname のどのモジュールもそれを読む。rulec と koyomi は、補助の関数を生成物ごとに書く(依存の無い一つのファイルにするため)。kaname は、モジュールどうしが互いのレコードと振替の型を読むので、補助の型(`KanameError` など)を一つにしなければ、モジュールをまたいで値を渡せない。Python と Go の型は名前で比べるからである。パッケージの中の生成物で、パッケージの外に依存しないことは変わらない(rulec の P5)。
- **インデックス**:`glue/` と `glue/books/` のインデックス(TypeScript の `index.ts`、Python の `__init__.py`)は kaname が書き、モジュール、補助のモジュール `kaname`、`books` を名前で並べる。パッケージの頭のインデックス(`index.ts`、`<name>/__init__.py`)は ritsu が書き、そこに `glue` を足す。Go にはインデックスが無い。Rust では `glue.rs` と `glue/books.rs` を kaname が書き、クレートの根(`lib.rs`)が `glue` を宣言する。
- **振替の値の型**(`glue/books/<帳簿>.*`):帳簿ごとに一度だけ書く。同じ帳簿の振替の値を作る二つのモジュールが、同じ型を使うためである(6.3)。chobo のクライアントを読まない。
- **振替を当てる関数**(`glue/books/<帳簿>_apply.*`):振替の値を chobo のクライアントの呼び出しに直す。chobo のクライアントを読むので、別のファイル(Go は別のパッケージ)にした。振替の値を作るだけのモジュールは、帳簿のドライバ(`pg`、`tigerbeetle-node` など)を読まずに済む。
- 依存を書くファイル(`package.json`、`pyproject.toml`、`doc.go`)に足すものは無い。kaname の生成物は、各言語の標準ライブラリしか使わない。Rust も同じで、帳簿のクライアントのクレート(sqlx、tigerbeetle)を読むのは、chobo のクライアントだけである。
`crates/ritsu/tests/projects/settlement`(17.6 の `payout` と 17.7 の `late_payment` の英語の版と、それが読む規則、日付のファイル、帳簿)を `ritsu gen` で書いたときの、TypeScript の `glue/` の全部である(`crates/ritsu/tests/golden/gen/settlement.txt`。ファイルごとに、頭が言う書いた言語と、元のファイル):
```
glue/books/index.ts: kaname
glue/books/ledger.ts: kaname
glue/books/ledger_apply.ts: kaname
glue/index.ts: kaname
glue/kaname.ts: kaname
glue/late_payment.ts: kaname <- late_payment.kn
glue/payout.ts: kaname <- payout.kn
```
### 10.2 型と値
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| お金、量 | パッケージの `units` の型(`units.USDc`。`bigint` に単位の印を付けたもの) | `units` の型(`int` の `NewType`) | `units` の型(名前付きの `int64`) | `units` の型(`i64` のタプル構造体 `units::USDc`) |
| `number` | `bigint` | `int` | `int64` | `i64` |
| 刻みのある率 | 刻みの数の `bigint` | 刻みの数の `int` | 刻みの数の `int64` | 刻みの数の `i64` |
| 関数の中で計算する値 | 刻みが整数の値は `bigint`、ほかは補助の `Q`(`bigint` の分子と分母) | `int`、ほかは `fractions.Fraction` | `int64`、ほかは補助の `Q`(`*big.Rat`) | `i64`、ほかは補助の `Q`(`i128` の分子と分母) |
| `bool`、`string` | `boolean`、`string` | `bool`、`str` | `bool`、`string` | `bool`、`String`(引数は `&str`) |
| `date` | `"YYYY-MM-DD"` の `string`(koyomi の生成物と同じ) | `datetime.date` | 補助の `Date`(`Year`、`Month`、`Day`) | 補助の `Date`(`year`、`month`、`day`) |
| モジュールの列挙 | 凍結したオブジェクトと値の合併型(rulec の生成物と同じ形) | `enum.Enum`(値は値の名前) | 名前付きの `string` と定数(`SizeSmall`) | フィールドの無い `enum`(`Size::Small`) |
| 規則の列挙 | 規則の生成物の列挙をそのまま使う | 同じ | 同じ | 同じ |
| 直和型 | 値の型の合併型(`OrderDraft \| OrderPlaced \| …`) | 値の型の `Union` | 値の型を見分けるメソッドを持つインターフェース(23.8) | 値ごとの構造体を持つ `enum`(`Order::Placed(OrderPlaced)`) |
| 値の型 | 値の名前の `kind` を持つ `interface` | `@dataclass(frozen=True)` | 構造体 | 構造体 |
| レコード | `interface`(読み取り専用のフィールド) | `@dataclass(frozen=True)` | 構造体 | 構造体 |
| `list[T]` | `readonly T[]` | `list[T]` | `[]T` | `Vec<T>`(引数は `&[T]`) |
| `dict[K, V]` | `ReadonlyMap<K, V>` | `Mapping[K, V]`(作るのは `dict`) | `map[K]V` | `BTreeMap<K, V>` |
| `set[T]` | `ReadonlySet<T>` | `AbstractSet[T]`(作るのは `frozenset`) | `map[T]struct{}` | `BTreeSet<T>` |
| `entry[K, V]`、`indexed[T]` | 補助のモジュールの `interface` | 補助のモジュールの `@dataclass(frozen=True)` | 補助のモジュールの構造体 | 補助のモジュールの構造体 |
| `T?` | `T \| null` | `T \| None` | `*T` | `Option<T>` |
| `result[T, E]` | 補助の `Result<T, E>`(`{ kind: "ok", value }` か `{ kind: "err", error }`) | 補助の `Ok`・`Err` と `Result = Union[Ok[T], Err[E]]` | 補助の `Result[T, E]`(interface)と `Ok[T, E]`・`Err[T, E]` | 標準の `Result<T, E>` |
| 型の引数 | 関数と型の型の引数(`<T>`) | モジュールの `TypeVar` と `Generic[T]` | 型の引数(`[T any]`、鍵に限ったものは `[K comparable]`) | 型の引数(`<T: Clone + PartialEq>`、鍵に限ったものは `<K: Ord + Clone>`) |
| 振替の値 | `interface`(`book`・`transfer`・`op`・`args`) | `@dataclass(frozen=True)`(`book`・`transfer`・`op` はクラスの値) | 構造体 | 構造体と、帳簿ごとの `enum Call` |
| 関数の型 `(T) -> U` | `(x: T) => U` | `Callable[[T], U]` | `func(T) U`(27.6) | `&dyn Fn(T) -> Result<U, KanameError>`(27.6、36.2) |
**決定(境目では受け渡す形、関数の中では計算する形)**:関数の引数と答え、レコードのフィールド、規則と日付と帳簿に渡す値(境目)は、表の上の形で受け渡す。お金と量はパッケージの `units` の型、数は整数、刻みのある率は刻みの数の整数(3.49% は刻み 0.01% の 349)で、率の型は作らない。関数の中では、刻みが整数の値(数、お金、量、日付の差)を整数で、ほかの値(率、丸めていない値)を有理数で計算し、境目で直す。レコードは境目の形で値を持つ。辞書と集合も、どこでも境目の形で持つ(鍵を値で比べるため。24.6)。`running_totals` の答えの要素も同じで、重みが丸めた値であることは型の検査が確かめる(3.6)。率の重みは重みの型の刻みの数で持ち、いつも `0%` の重み(型の刻みが 0)は 0.01% の刻みの数で持つ(どの刻みでも 0 個である)。規則の生成物は規則ごとに率の型 `Rate` を持つので、規則に率を渡すところでは、規則の刻みの数に直してその型にする。
単位の型:生成パッケージの中では、お金と量の額は、パッケージの単位のモジュール(TypeScript の `units.ts`、Python の `<name>/units.py`、Go の `<module>/units`、Rust の `units.rs`)が宣言する、単位ごとに一つの型である(ritsu の DESIGN 9.3)。規則の生成物と帳簿のクライアントもその型で受け渡すので、kaname の生成物も初めからその型を使い、規則と帳簿を呼ぶところで型を直さない(PLAN の D.6)。kaname の生成物が名前を出す単位は、`ritsu gen` が、規則と帳簿の単位と合わせて単位のモジュールに入れる。計算した整数を単位の型にするところは、TypeScript では型を付け(`x as units.USDc`。リストは要素ごと)、Python では型を作る呼び出し(`_units.USDc(x)`)、Go では変換(`units.USDc(x)`。リストとオプショナルな値は補助の `kaname.Ints` と `kaname.Int`)である。Rust ではタプル構造体を作る(`units::USDc(x)`)。単位の型の値は、TypeScript と Python ではそのまま整数として読め、Go では `int64(x)` で、Rust では `x.0` で読む。
自分を含む型(28.1)は、ほかのレコードと直和型と同じ形に書く。TypeScript の `interface` と、Python のデータクラス(生成物は `from __future__ import annotations` で、型の注釈を前に置いた型の名前でも読む)は、そのまま自分を指せる。Go の構造体は自分をじかには持てないが、自分を含むのはいつも、オプショナルな値の `*T`、スライス、マップ、直和型のインターフェースを通してなので書ける(`tests/modules/trees.kn` の `Category`、`Node`、`Tree[T]` を生成したもの。実物):
```go
type Category struct {
Name string
Children []Category
}
type Node struct {
Value int64
Next *Node
}
type Tree[T any] interface {
isTree(T)
}
type TreeNode[T any] struct {
Children []Tree[T]
}
```
Rust の構造体も自分をじかには持てない。同じ強連結成分の型へのフィールドのうち、`Vec`・`BTreeMap`・`BTreeSet` を通らないものを `Box` にする(36.2。同じ型の Rust の実物。ドキュメントのコメントと derive の行を省いた):
```rust
pub struct Category {
pub name: String,
pub children: Vec<Category>,
}
pub struct Node {
pub value: i64,
pub next: Option<Box<Node>>,
}
pub enum Tree<T: Clone + PartialEq> {
Leaf(TreeLeaf<T>),
Node(TreeNode<T>),
}
pub struct TreeNode<T: Clone + PartialEq> {
pub children: Vec<Tree<T>>,
}
```
### 10.3 関数
- 名前:関数は別名で、Go はパスカルケース(`weight_of` は `WeightOf`)。Go で、モジュールのレコードか列挙の型と同じ名前になる関数は `Fn` を足す(`fn claim` と `record Claim` の `ClaimFn`)。レコードと列挙の型はパスカルケース、フィールドは TypeScript と Python が別名のまま、Go がパスカルケース。モジュールの列挙の値は、TypeScript と Python が大文字(`Size.SMALL`)、Go が型の名前を前に付けたもの(`SizeSmall`)。直和型の値の型は、列挙と値の別名をつなげたパスカルケース(`Order.placed` は `OrderPlaced`。23.8)。Rust は、関数とフィールドを別名で、型と列挙の値をパスカルケースで書き(`Size::Small`)、型と同じ名前の関数にも何も足さない(関数と型の名前空間が別だからである)。Rust の予約語は生の識別子にし(`r#ref`)、生の識別子にできない語と、生成物が名前だけで書く標準の型と値の名前には `_` を足す(`self_`、`Option_`、`Some_`。36.3)。`api` の `generated` も、この名前を言う(11.1)。
- 関数の中の名前:TypeScript・Python・Rust は、引数を別名で、`let`・ラムダの引数・`some` の名前を書いたとおりに書く。Go は、どれも小文字で始まるキャメルケースにする(`paid_on` は `paidOn`、`days_late` は `daysLate`)。同じパッケージの rulec の Go が、中の名前を `roundDown` や `minInt` と書くのにそろえた。中の関数(入口を通らない関数)の名前は、TypeScript・Python・Rust が `_` と別名(`_weight_of`)、Go が `_` とキャメルケース(`_weightOf`)である。どの出力先も、出力先が名前に取らない字を持つ関数の中の名前は `v` にする(`m²`。36.3)。
- **決定(名前がぶつかったとき)**:関数の中の名前が、出力先の予約語か、生成したコードが使う名前(TypeScript の `BigInt`・`Math`・`Error` など、Python の `len`・`int`・`str`・`range`・`min`・`max` など、Go の `err`・`ctx`・`fmt`・`big`、補助のモジュールの `kaname` と単位のモジュールの `units`)か、モジュールのコードの名前(関数、型、読み込んだモジュール)か、前に付けた名前と同じになるときは、名前を足して分ける。TypeScript と Python の引数は `_` を足し(`len` は `len_`。それも取られていれば `len_2`、`len_3`)、`let` とラムダの引数と `some` の名前は、2 からの番号を `_` のあとに足す(`int` は `int_2`)。Go は、キャメルケースにした名前に 2 からの番号を足す(`type` は `type2`、`len` は `len2`)。キャメルケースにすると同じになる二つの名前(`paid_on` と `paidOn`)も、あとのほうが番号を取る(`paidOn` と `paidOn2`)。Rust は TypeScript と Python と同じ形で `_` と番号を足す。分けるのは、モジュールのコードの名前(関数、`check_<関数>`、`_MODULE`)、生の識別子にできない語、`Some`・`None`・`Ok`・`Err` などで、パスの頭は `let` に隠されないので `kaname` と `units` は分けない(36.3)。`tests/modules/names.kn` が、この形を四つの出力先で確かめる(16.4)。
- 引数:TypeScript・Go・Rust は並べた順に、Python はキーワードだけで受け取る(`def claim(*, invoice, paid_on, reference_rate)`)。Rust は `Copy` の型を値で、ほかを借りて受け取る(`&Invoice`、`&str`、`&[T]`。36.4)。kaname の呼び出しが名前で書くのに合わせ、Python では取り違えを書けなくした。Python のホストが呼ぶ関数のキーワードは別名のままで、中の関数には、上の決まりで分けた名前で渡す(`names.kn` の `words` なら `return _words(Type=Type, len_=len, max_=max, items=items)`)。ホストが呼ぶ関数の本体は、引数の名前が隠すかもしれない組み込みの名前を使わない(要素の数を文に書くのは補助の `kaname.itoa` である)。TypeScript と Go の引数の取り違えは、単位の型が違えば型の検査が言う。
- 入口:ホストが呼ぶ関数には、引数を型と範囲と要素の数で確かめて外れれば `input` で失敗する公開する関数(`claim`)と、入口を通らない中の関数(`_claim`)を書く(4.4)。同じモジュールの関数どうしの呼び出しは中の関数を、ほかのモジュールの関数は公開する関数を呼ぶ。範囲の解析が引数を示せなかった呼び出しは、呼ぶ前に確かめる(`range`)。型の引数を持つ関数は、入口の確かめの無い一つの関数だけを書き、ほかのモジュールの生成物から呼べるように外に見せる(TypeScript の `export function _first_or`、Python のモジュールの `_first_or`、Go の `FirstOr`、Rust の `pub fn _first_or`。21.3、26.7)。鍵に限った型の引数の順は、隠れた一つ目の引数として渡す(26.7。Rust は渡さず、`K: Ord` の境界で書く。36.2)。
- エフェクト:Go は、失敗しうる関数だけが `(T, error)` を返し、total な関数は `T` を返す(入口の外れは panic。4.4)。どの関数にも、引数を確かめるだけの `Check<関数>(…) error` を書く。Rust も同じ分け方で、失敗しうる関数が `Result<T, kaname::KanameError>` を、total な関数が `T` を返し(入口の外れは panic)、どの関数にも `check_<関数>(…) -> Result<(), kaname::KanameError>` を書く(36.4)。TypeScript と Python は、関数ごとに失敗の種類の型の別名(`export type ClaimFails = "rule" | "overflow"`、`ClaimFails = Literal["rule", "overflow"]`)を書き、ドキュメントのコメントに `@throws` と `Raises:` を書く。total な関数は `never`(TypeScript)と `NoReturn`(Python)で、`input` のほかは投げないと書く。止まらないかもしれない関数は、そうコメントに書く。
- 本体:`let` は定数の宣言(Go は型を書いた `var`)、`if` と `match` はその言語の `if` 文で、答えを関数の `return` にするか、宣言した変数に入れる。直和型の `match` は、TypeScript と Python が `kind` と `isinstance` の `if` の並び、Go が型の switch である(23.8)。Rust は型を書いた `let` と、式の `if` と `match` で、直和型も `match` である。`let` の行は、生成物の行のコメントに元の行を書く(`// 20: let due = terms.due(invoiced: invoice.invoiced)`)。
- 関数の値:ラムダは、TypeScript のアロー関数、Python の入れ子の `def`、Go の関数のリテラル、Rust のクロージャ(`&|l: &Line| -> … { … }`)にし、値として書いた関数は、それを呼ぶラムダにする(27.6)。関数の型の引数を持つ関数と `private` の関数は、入口の確かめの無い一つの関数だけを書く。関数を取る関数は外に見せ(型の引数を持つ関数と同じ)、`private` の関数とレコードと列挙は見せない(TypeScript は `export` を付けず、Python と Go は名前を `_` で始め、Rust は `pub` を付けずに名前を `_` で始める。28.2)。
- 組み込みの関数:その場のループにする。そこに書いたラムダは出力先の関数にせず、要素の名前を置いた本体として書く。`any`・`all`・`first` は答えの決まった要素でループを抜ける(8 章)。生成した Python で、kaname の呼び出しの一段が Python の一段になるのは、このためである(8 章、16.4)。
- 評価の順:参照インタプリタの順(8 章)を保つ。引数とフィールドは書いた順に評価し、渡す先の型で読み、渡す先の確かめは宣言の順にする。呼び出しが引数を宣言の順に並べ直すとき、渡す先の確かめが間に入るとき、読み替えが失敗しうるときは、書いた順に一時的な名前に入れてから並べる。`tests/modules/calc.kn` の `right_first` は、宣言の順と逆に引数を書いた呼び出しで、あふれる引数が先に評価される:
```kn
fn right_first(a: number range >=1 <=9, z: number range >=0 <=0) -> number
pair(y: a * 4_000_000_000_000_000_000, x: down(a / z, 1))
```
```ts
function _right_first(a: bigint, z: bigint): bigint {
const _t1: bigint = kaname.heldInt(a * 4000000000000000000n, _module, 45, "The value of `a * 4_000_000_000_000_000_000` does not fit in int64");
const _t2: bigint = kaname.whole(kaname.round(kaname.over(kaname.q(a), kaname.q(z), _module, 45, "The divisor in `a / z` is 0"), "down", kaname.q(1n, 1n)));
const _t3: bigint = _pair(_t2, _t1);
return _t3;
}
```
### 10.4 実行時の確かめ
確かめる場所(4.2、6.4)と同じところで、同じ順に確かめ、同じ `KanameError` にする。
- `KanameError` は、種類(`input`・`rule`・`date`・`range`・`overflow`・`zero`)、モジュール、`.kn` の行、文を持つ。TypeScript は `Error` の、Python は `Exception` の子で、Go は `error` を満たす `*KanameError`(`Kind`、`Module`、`Line`、`Message`)、Rust は `std::error::Error` を持つ `kaname::KanameError`(`kind`、`module`、`line`、`message`、`cause`。36.5)である。文は生成するときの `--lang` の言語で、参照インタプリタの失敗の文と同じ文字列にする(英語は最初の ASCII の文字を大文字に、日本語は ASCII の語と日本語のあいだに空白を入れる。ritsu-base の `spaced` と同じ)。突き合わせのテストが比べる(16.4)。
- 規則の呼び出し:生成物の `RuleInputError` と `RuleContradictionError`(Go はそのエラー、Rust は `RuleError`)を `rule` にする。日付の呼び出し:`KoyomiError` を `date` にする。文は「The rule `charges` refuses its inputs」「The date `terms.due` cannot be worked out」(日本語は「規則 `charges` が入力を受け付けません」「日付 `terms.due` が求まりません」)で、理由を書かず、生成物の例外を原因として持たせる(TypeScript の `cause`、Python の `raise … from e`、Go の `Err` と `Unwrap`、Rust の `cause` と `source`)。示した呼び出しでは包まない。そこで生成物がエラーを返すのは ritsu の不具合なので、Go と Rust では panic にする(koyomi の生成物が、起きないはずの `none` で panic にするのと同じ)。規則の列挙の入力に書いた範囲(`campaign : tier range >=basic <=gold`)も、数の範囲と同じく範囲の解析の場所で(6.1)、示せなければ包む。範囲の外の値は、参照インタプリタ(口の `eval`)でも生成物(入口)でも受け付けられず、どちらも `rule` の失敗になる(`tests/modules/lift.kn` を四つの出力先で流す。16.4)。
- あふれ:示せなかった演算だけ、確かめつきで計算する。TypeScript と Python は計算してから int64 の範囲と比べ(`heldInt`、`held_int`)、Go は和と差を符号で、積を `math/big` で見る。Rust は `checked_add` などで見る(補助の `kaname::add` など)。丸めていない値は、演算ごとに既約にしてから分子と分母を比べる(5.6)。
- 0 で割る:示せなかった割り算の前に比べる。
- 渡す先の範囲と要素の数:値を渡す前に、モジュールに一つずつ書く確かめの関数(`_outside1` など)で比べ、外れれば `range`(入口では `input`)。レコードのフィールドは、レコードごとの確かめ(TypeScript と Python の `_outside_<レコード>`、Go の `KanameOutside` と Rust の `kaname_outside` のメソッド)で見る。直和型のフィールドは、値ごとに見る(`_outside_Order` と、Go の値の型ごとの `KanameOutside`。23.8)。振替の額は、0 より小さければ `range`(6.3)。
- 入れ子の深さ:ホストが呼ぶ関数の入口は、自分を含む型の値を持ちうる引数ごとに、範囲の確かめの前に、型ごとに書く深さの関数(`_nest1` など。自分を含む型の値を一つ通るたびに一段足す)で数え、200 段を超えれば `input` で失敗する。自分を含むレコードと直和型の値のフィールドは、入口だけの確かめ(TypeScript と Python の `_enter_<型>`、Go の `KanameEnter` と Rust の `kaname_enter` のメソッド)で、どの深さのものも見る。kaname の関数を呼ぶところの確かめ(`_outside_<型>`、`KanameOutside`)は、自分を含む型には書かず、その値の中を見ない(28.1)。
- 文字列:入口では、正しい文字列か(TypeScript と Python は孤立したサロゲート、Go は正しくない UTF-8)と、書いた `count`(コードポイントの数)を確かめ、外れれば `input` である。Rust の `&str` はいつも正しい UTF-8 なので、`count` だけを確かめ、正しくない文字列は JSON の一行で呼ぶ入口が言う(36.5、36.11)。文は「The argument `s` of `place` is no well-formed string of Unicode」と「… has 13 code points, outside its count <=12」(25.7)。
- 範囲を書かない値:入口と、kaname の関数の引数に渡すところでは、型の全体で確かめる。TypeScript の `bigint` と Python の `int` は int64 に収まるかを、TypeScript の文字列の日付は `kaname.isDate` で、Go の `Date` は `Valid` で、Rust の `Date` は `valid()` で、0001-01-01 から 9999-12-31 の正しい日かを確かめる。Go と Rust の `int64`(`i64`)と Python の `datetime.date` は、その外の値を持てないので確かめない。文は参照インタプリタの入口と同じで、範囲に型の全体を書く(`outside its range >=-9223372036854775808 <=9223372036854775807`。22.5)。
**決定(確かめるのは、示したでない場所だけ)**:生成したコードが確かめるのは、範囲の解析の答え(`Place::outcome`)が示したでない場所だけである。`ritsu gen` は、`ritsu check` と同じく言語をまたぐ判定の答え(`ProgramVerdicts`)を渡してモジュールを検査するので、言語をまたぐ判定で示せた場所は確かめない。答えを渡さない生成(kaname のクレートのテスト)では、その場所は答えを待つ場所(4.3)で、確かめを書く。`late_payment` の期日(X6)は、`ritsu gen` では示せた場所で、包まない(`examples/late_payment` を `ritsu gen` で書いたもの):
```ts
// 20: let due = terms.due(invoiced: invoice.invoiced)
const _t2: string = dates_payment_terms.due(invoice.invoiced);
```
答えを渡さない生成(kaname のクレートのテスト)では、答えを待つ場所なので包む:
```ts
// 20: let due = terms.due(invoiced: invoice.invoiced)
let _t2: string;
try {
_t2 = dates_payment_terms.due(invoice.invoiced);
} catch (e) {
if (e instanceof dates_payment_terms.KoyomiError) {
kaname.fail("date", _module, 20, "The date `terms.due` cannot be worked out", e);
}
throw e;
}
```
### 10.5 内側の層の生成物を呼ぶ
- **読み込み**:パッケージのモジュールを、名前空間ごと別名で読む(TypeScript の `import * as rule_card_fee from "../rules/card_fee";`、Python の `from ..rules import card_fee as _rule_card_fee`、Go の `"<module>/rules/cardfee"`、Rust の `use crate::rules::card_fee as rule_card_fee;`)。sekisho の生成物と同じ形で、モジュールの中の名前とぶつからない。
- **規則**:関数の名前と引数の順と型は、口の `Call` から書く(6.1。Rust は `RuleFacts::rust`。36.6)。Go は入力の構造体を作る。出力が一つの規則は値を直に返し、二つ以上ならレコード(Python の `Output`、Go の構造体)を返すので、kaname の出力のレコードに読み替える。率は規則の `Rate` の型で、規則の刻みの数にして渡し、受け取る。並びは、規則の生成物の要素のレコード(`LinesElement` など)のリストにして渡す。
- **日付**:rulec の生成物は日付を日の番号で受け取り、返す。koyomi の生成物は、TypeScript が `YYYY-MM-DD` の文字列、Python が `datetime.date`、Go が koyomi のパッケージの `Date`、Rust が koyomi のモジュールの `Date` で受け取り、返す(Rust は、kaname の `Date` とフィールドを入れ替えて組み直す。36.6)。kaname の生成物は、呼ぶところで換算する(dandori のつなぐコードと sekisho の生成物と同じ。dandori の DESIGN 1.17)。
- **列挙**:規則の列挙の値は、規則の生成物の列挙のメンバー(口の `CallEnum`)で渡し、受け取る。モジュールの列挙と規則の列挙のあいだは、kaname の `match` が書いたとおりに対応づける。
### 10.6 振替の値と、ホストでの当て方
振替の値の型は、帳簿ごとに一つのファイルに書く。操作ごとに、引数の型と、拒否されうる理由の合併型を持つ(6.3)。`settlement` の TypeScript(`ritsu gen` が書いたもの):
```ts
// glue/books/ledger.ts
import type * as units from "../../units";
/** capture.do: the reasons it can be refused with, at the amounts the modules build it with, are its type's `Refusal` */
export interface CaptureDo {
readonly book: "ledger";
readonly transfer: "capture";
readonly op: "do";
readonly args: { readonly "payment": string; readonly "seller": string; readonly "amount": units.USDc };
}
/** The reasons capture.do can be refused with. */
export type CaptureDoRefusal = "key_conflict";
/** processing_fee.do: the reasons it can be refused with, at the amounts the modules build it with, are its type's `Refusal` */
export interface ProcessingFeeDo {
readonly book: "ledger";
readonly transfer: "processing_fee";
readonly op: "do";
readonly args: { readonly "payment": string; readonly "seller": string; readonly "fee": units.USDc };
}
/** The reasons processing_fee.do can be refused with. */
export type ProcessingFeeDoRefusal = "insufficient_balance" | "key_conflict" | "already_refused";
/** Every transfer of ledger the modules build. */
export type LedgerCall = CaptureDo | ProcessingFeeDo;
```
```ts
// glue/books/ledger_apply.ts
import type { Book, Result } from "../../books/ledger";
import type * as calls from "./ledger";
/** Makes `call` through the client `book`: what the client answers. */
export async function applyLedger(book: Book, call: calls.LedgerCall): Promise<Result> {
if (call.transfer === "capture" && call.op === "do") return book["capture"].do(call.args);
if (call.transfer === "processing_fee" && call.op === "do") return book["processing_fee"].do(call.args);
throw new Error("kaname: no such transfer");
}
```
当てる関数は、chobo のクライアントの答え(`{ result: "done" } | { result: "done_before" } | { result: "refused"; reason: … }`)をそのまま返す。答えの `reason` の型は、chobo のクライアントでは帳簿の全部の理由の合併型である。振替の値の型ごとの合併型(`ProcessingFeeDoRefusal`)は、その操作で起きうる理由だけを並べるので、ホストは `switch` でそれだけを扱えばよい。Python は、操作ごとの `@dataclass(frozen=True)` と `Literal` の別名(理由が無ければ `NoReturn`)、当てる関数 `apply_ledger(book, call)` である。Go は、操作ごとの構造体と、理由の名前付きの `string` と定数(`CaptureDoRefusalKeyConflict`)、当てる関数 `ledgerapply.Apply(ctx, book, call)` で、帳簿のクライアントのパッケージを `client`、振替の型のパッケージを `calls` という名前で読む(帳簿のパッケージの名前が、引数の名前 `book` と同じになっても読めるように。日本語の名前の帳簿のパッケージは `book` である)。
Rust は、操作ごとの構造体(帳簿と振替と操作の名前を `BOOK`・`TRANSFER`・`OP` の定数で持つ)と拒否の理由の列挙(`CaptureDoRefusal`。`as_str()` が帳簿の書く名前を返す)、帳簿ごとの `enum Call` と `From`、当てる関数 `apply(book, call)` である。クライアントのモジュールを `client`、振替の型のモジュールを `calls` という名前で読むのは Go と同じで、トレイト境界は帳簿の置き場所ごとに書く(PostgreSQL と MySQL の系統は `Database`、TigerBeetle は `TigerBeetleClient`。36.6)。`examples/payout` の台帳を kaname のテストで書いたもの(実物。`glue/books/ledger_apply.rs`):
```rust
use crate::books::ledger as client;
use crate::glue::books::ledger as calls;
/// Makes `call` through the client `book`: what the client answers.
pub async fn apply<D: client::Database>(book: &client::Book<D>, call: &calls::Call) -> Result<client::Outcome, client::Error> {
match call {
calls::Call::CaptureDo(c) => book.capture().r#do(client::CaptureArgs { payment: &c.payment, seller: &c.seller, amount: c.amount }).await,
calls::Call::ProcessingFeeDo(c) => book.processing_fee().r#do(client::ProcessingFeeArgs { payment: &c.payment, seller: &c.seller, fee: c.fee }).await,
}
}
```
### 10.7 `ritsu gen` へのつなぎ方
- `ritsu gen` は、プロジェクトの `.kn` を、ほかの言語のファイルと同じく検査する。全部の言語をつなぎ、言語をまたぐ判定の答え(`ProgramVerdicts`)を渡して、`ritsu check` と同じに検査し(10.4)、通らなければ何も書かない。`.kn` が読む規則、日付のファイル、帳簿、ほかの `.kn` は、プロジェクトのファイルでなければならない(ワークフローとゲートと同じ)。モジュールだけを渡せば、こう言って何も書かない:
```
error: `payout.kn` reads rules/card_fee.rule, which is not one of the project's files; a package is made of the project's files, so give ritsu gen that file too
```
- kaname のブロックは、`crates/ritsu/src/package.rs` の sekisho のブロックのあとにある。種類は `glue` で、パッケージの頭のインデックスが `glue` を読む(`glue/` のインデックスは kaname のもの。10.1)。検査を通るかの確かめ(`checked`)に kaname が入る。拡張子を並べた文には `.kn` があり、パッケージの説明の文(TypeScript の `index.ts` と `package.json`、Python の `__init__.py` と `pyproject.toml`)、`doc.go` の文、`ritsu gen --help` の文は、kaname のモジュールを「Functional Core(kaname のモジュール)」(英語では the functional cores (kaname's modules))と呼ぶ。置き場の名前は `glue/` のままである。ホストのコードはこのパスでモジュールを読むので、名前を変えるとホストのビルドが壊れる。
- 生成物の頭は ritsu の DESIGN 9.2 の形:`// Code generated by kaname <バージョン>. DO NOT EDIT.` と `// Source: <ルートからのパス> (module <名前> v<版>, sha256:<16 桁>)`。補助のモジュール、帳簿ごとのファイル、インデックスは一行目だけ(元のファイルを持たないため)。`--check` が、もう生成しないファイルを見つけるのも、ほかの生成物と同じく、この頭による。
- `--check` と `--format json` は、ほかの種類と同じに扱う。生成物のコメントと `KanameError` の文は `--lang` の言語で書く。`.kn` を変えれば、`--check` はそのモジュールの三つの言語のファイルだけを言う(`crates/ritsu/tests/gen.rs`):
```
generated file is stale or hand-edited: <out>/typescript/glue/payout.ts
generated file is stale or hand-edited: <out>/python/generated/glue/payout.py
generated file is stale or hand-edited: <out>/go/glue/payout/payout.go
```
- Rust:`ritsu gen` は、まだ Rust のパッケージを書かない(19 章)。Rust の生成物を書くのは kaname の生成の関数(`package_in` に `Target::Rust`)で、kaname のテストがパッケージの残りを書いて確かめる(36.9)。
### 10.8 捨てた形
- **補助の関数を、モジュールごとの生成物に書く**(rulec と koyomi の形):モジュールをまたいで同じ型が要る(10.1)。
- **浮動小数点や、言語の十進数の型**:P4。言語ごとに丸め方と精度が違う。
- **言語の丸めと日付の関数**:rulec の DESIGN §7.3 と koyomi の DESIGN 1.7 と同じ理由。
- **単独の `kaname gen`**:10.1。
- **振替の値を作る関数に、chobo のクライアントを読ませる**:振替を作るだけのコードが、帳簿のドライバに依存することになる。
- **組み込みの関数に渡すラムダを、出力先の関数にする**(TypeScript の `xs.map(x => …)`、Python の内包表記と `lambda`、Go の関数リテラル):生成した Python の再帰の段が、kaname の一段より多くなる(8 章)。`any`・`all`・`first` の止まり方と評価の順を、言語の関数ごとに確かめることにもなる。
- **率の型を、規則と同じく作る**:規則ごとの `Rate` は規則の生成物のもので、kaname の関数の引数と答えに使えば、どの規則の型かを選ぶことになる。率は刻みの数の整数で受け渡し、規則に渡すところでその規則の型にする(10.2)。
## 11. 入口
### 11.1 コマンド
```
ritsu kaname check <file.kn>... [--format json] [--root <dir>] [--lang en|ja]
ritsu kaname run <file.kn> <function> --input <file.json | -> [--trace] [--format json] [--root <dir>] [--lang en|ja]
ritsu kaname vectors <file.kn> [--function <name>] [--trace] [--lang en|ja]
ritsu kaname doc <file.kn> [--format markdown|html] [--root <dir>] [--lang en|ja]
ritsu kaname api <file.kn> [--root <dir>]
ritsu kaname explain <code> | --all [--format markdown|json]
```
- `check`:字句と構文、名前、読むファイル、型、範囲の解析(kaname 自身の確かめと、ritsu-cross の答え)、エフェクト、逃げ道の警告、`examples` を確かめる。ritsu-cross の答えは、`ritsu kaname` と `ritsu check` が渡す(4.3)。言語をまたぐ判定の診断(ritsu の E201〜W205)は、`ritsu check` が ritsu-cross の診断として出し、`ritsu kaname check` は、その答えでエフェクトを決めて E401 の注に挙げる。範囲の解析とエフェクトと逃げ道の警告は、型の確かめを通ったモジュールにだけかけ、`examples` は、それまでにエラーの無いモジュールにだけ流す。最後の行は、ファイルごとに関数の数とエフェクトを言う(「ok late_payment.kn: 1 function, fails date」、日本語は「ok late_payment.ja.kn: 関数 1 個、fails date」。4.5)。`--format json` はファイルごとに一つの JSON で、`{"kaname", "file", "ok", "summary", "functions", "diagnostics"}` の順に書く(`file` と診断の `file` はルートからのパス、`summary` は最後の行の `:` のあと、エラーのあるファイルは `null`。`functions` は関数ごとのエフェクトで、型の確かめを通らないファイルは `null`。中の関数には `"inner": true` を足し(21.3)、関数の型の引数を持つ関数のエフェクトには、通るエフェクトの引数の名前(`"through": ["amount"]`)を足す(27.5)。4.5)。
- `run`:関数を一つ、JSON で渡した入力で流す(`--input -` は標準入力を読む)。中の関数(型の引数を持つ関数、関数を取る関数、`private` の関数)は、ホストから呼ばないので流さず、exit 2 で終わる(26.4、27.3、28.2)。入力は `vectors` の `in` と同じ形で、引数とフィールドと列挙の値は、宣言した名前でも別名でも書ける。テキストは、一行目に呼び出しを、次に内側の層の呼び出しとその答えと作った振替を起きた順に、最後に `= 答え` か、失敗(種類、行、文)を書く。`--trace` は、`let` ごとの値も同じ順に書く。`--format json` は `{"kaname", "file", "module", "function", "input", "calls", "result" | "failure" | "diverged"}` の順で、`--trace` では `calls` のあとに `lets` が入る。`file` はルートからのパス、`input` は渡したとおりの JSON、`calls` は `vectors --trace` と同じ形である。17.7 の GOV.UK の例を `--trace` で流したもの(実物):
```text
$ ritsu kaname run late_payment.kn claim --input claim.json --trace
claim(invoice: Invoice { id: "inv_1", invoiced: 2026-03-02, amount: 100000GBPc }, paid_on: 2026-05-20, reference_rate: 0.5%)
line 20: terms.due(invoiced: 2026-03-02) = 2026-03-31
line 20: let due = 2026-03-31
line 21: let late = 50
line 23: charges(debt: 100000GBPc, days_late: 50, reference_rate: 0.5%) = { compensation: 70GBP, interest: 1150GBPc }
line 23: let c = { compensation: 70GBP, interest: 1150GBPc }
= Claim { due: 2026-03-31, days_late: 50, compensation: 70GBP, interest: 1150GBPc }
```
入口で外れた入力は、関数の失敗として答える(実物):
```text
fails: input (line 18): The argument `invoice.amount` of `claim` is 0GBPc, outside its range >=1GBPc <=100000000GBPc
```
```text
失敗: input(18 行目): 関数 `遅れの請求` の引数 `請求書.金額` の 0GBPc は、範囲 >=1GBPc <=100000000GBPc の外です
```
- `vectors`:9 章。`doc`:13 章。`explain`:kaname の台帳(12 章)。
- `api`:外のツールのための一つの JSON で、`{"kaname", "file", "module", "generated", "wasm", "uses", "enums", "records", "functions", "transfers"}` の順に書く。`module` は名前と別名と版と説明と SHA-256、`generated` はモジュールの生成物のファイル(10.1)、`wasm` はパッケージの Wasm でこのモジュールを呼ぶ形(export の名前、呼び出しのキーとこのモジュールを選ぶ `module` の値、呼べる関数、答えの形。ホストが呼ぶ関数が無ければ `null`。36.11)、`uses` は読むファイル(種類、名前、書いたパス、ルートからのパス、行)、`enums` と `records` は宣言(型と範囲と要素の数は kaname の書き方で、生成するコードの型の名前を添える。直和型の値は、フィールドの宣言と、生成するコードの値の型の名前も持つ)、`functions` は関数ごとの引数と結果、推論したエフェクトと書いたエフェクト、ほかの言語の呼び出し(`crossings`。呼び出しごとに、言語をまたぐ判定の場所の判定、何についてか、結果、ritsu のコード)、kaname 自身の示せない場所(`places`。`outcome` は 4.5 と同じ語)、呼んだ関数、生成するコードの名前(TypeScript と Python は別名、Go はパスカルケースで、型と同じ名前になれば `Fn` を足したもの。10.3)、`transfers` は組み立てる振替の操作ごとの、拒否されうる理由(`do` と `hold` だけ。決められない場所があれば `null`)である。総称の宣言には型の引数(`type_params`。名前、別名、鍵に限ったか)を、中の関数には `"inner": true` を、`private` の関数・レコード・列挙には `"private": true` を足し、関数の型の引数を持つ関数のエフェクトには `"through"` を足す(27.5)。中の関数の生成するコードの名前は、その一つの関数の名前(TypeScript と Python の `_first_or`、Go の `FirstOr`。`private` の関数は Go も `_placeOf`)を言う(26.7、28.2)。関数の型の引数の型は `(T) -> money[USDc]` と書く。ほかの宣言の形は変えない。名前と型が検査を通るモジュールには、範囲の解析やエフェクトがエラーを言っても答える(どこがなぜ失敗しうるかを見せるため。4.2)。`examples/payout` で `ritsu kaname api payout.kn --root .` を走らせた答えの一部(実物):
```json
"crossings": [
{"line": 19, "col": 13, "calls": "rule", "callee": "card_fee", "checks": []},
{"line": 21, "col": 14, "calls": "transfer", "callee": "ledger.capture.do",
"checks": [{"line": 21, "col": 85, "check": "transfer_amount", "about": "amount", "outcome": "shown", "code": null}]},
{"line": 22, "col": 10, "calls": "transfer", "callee": "ledger.processing_fee.do",
"checks": [{"line": 22, "col": 85, "check": "transfer_amount", "about": "fee", "outcome": "shown", "code": null}]}
],
...
"transfers": [
{"type": "ledger.capture.do", "book": "books/ledger.book", "operation": "capture.do", "refusals": ["key_conflict"]},
{"type": "ledger.processing_fee.do", "book": "books/ledger.book", "operation": "processing_fee.do",
"refusals": ["insufficient_balance", "key_conflict", "already_refused"]}
]
```
直和型の値の一つ(`tests/modules/sums.kn` の `Event` の `pay`。実物):
```json
{
"name": "pay",
"alias": "pay",
"label": null,
"fields": [
{
"name": "cents",
"alias": "cents",
"type": "money[USDc]",
"range": ">=1USDc <=2000000USDc",
"count": null,
"line": 25
}
],
"generated": {
"typescript": "EventPay",
"python": "EventPay",
"go": "EventPay"
}
}
```
- 終了コード:0(問題なし、警告だけ)、1(エラーがある。`run` では、モジュールにエラーがあるか、評価が止まらなかった)、2(使い方の誤り、読めないファイル、内側の言語の無いバイナリでの `use`。`run` では、JSON として読めない入力と、無い関数も)。`run` は、関数が失敗しても、最後まで評価できれば 0 で、失敗は答えとして出す(`ritsu run` がフローの失敗を答えとして出すのと同じ)。
- `--lang` が無ければ `KANAME_LANG`、次に `RITSU_LANG`、どれも無ければ英語(ほかの言語の `<名前>_LANG` と同じ。`KANAME_LANG` は `ritsu check` の中では読まない。ritsu の DESIGN 8.1)。
### 11.2 ritsu への組み込み
- **`ritsu kaname …`**:kaname のコマンドを、rulec・koyomi・chobo の口と、ritsu-cross の `Verdicts`(14.5)をつないで走らせる(`crates/ritsu/src/languages.rs` に `kaname` を足す。`ritsu dandori` が ritsu-cross の `Undecided` を渡すのと同じ形)。
- **`ritsu check`**:プロジェクトの `.kn` を読む順は、rulec、koyomi、chobo、geas、`.proto` のあと、dandori の前(`ritsu-project` の `ORDER`)。kaname は rulec・koyomi・chobo を受け取り、いまは何も出さない。dandori のフローから kaname を呼ぶようになれば、dandori の前にあることが要る(19 章)。言語をまたぐ検査の境目の数には、kaname の呼び出しの場所の X2・X3 の (a)・X4・X6 が入る。`ritsu check` は ritsu-cross の `ProgramVerdicts` を一つ作り、kaname の検査(`Project::check_with`)と言語をまたぐ検査(`ritsu_cross::check_with`)の両方に渡す。一つのモジュールの判定は一回の実行で一度だけ行い、kaname のエフェクトと ritsu の診断は同じ判定から出る(14.5)。
- **`ritsu gen`**:10.7。**`ritsu explain`**:ritsu の台帳のコード(E201 など)は、dandori と kaname の両方の呼び出しを言うように文を広げる(14.4)。kaname の台帳は `ritsu kaname explain` で引く。
- **`ritsu --help`**:言語の一覧の九つ目が `kaname` である(`ritsu kaname <command> ...` の行)。README、サイトの頭のページ、リリース、スキルにも、九つ目の言語として載っている。`ritsu check --help` の拡張子の文にも `.kn` がある。
- **サイト**:ritsu のサイト(`website/`)に、kaname のページが英語と日本語で五つずつある(`docs/kaname/` と `docs-ja/kaname/`。ナビゲーションでは sekisho のページの次)。kaname が何か(`order_flow` のモジュールと、ホストから呼ぶこと)、書き方(範囲を書く値と書かない値、直和型と `match`、辞書と集合、文字列と `result`、ジェネリクスと関数の値、自分を含む型、持たないもの)、Functional Core の四つの例(ホストが呼ぶ宣言、TypeScript・Python・Go のホストをタブに並べたもの、出力)、検査が保証すること(エフェクト、範囲を書かない値、渡す先の範囲、出力先の突き合わせ、範囲の解析の健全性、Lean)、規則・日付・帳簿を呼ぶこと(`late_payment`、それぞれの型、言語の境目)の五つである。日本語のページはモジュールを日本語の版で見せ、ホストとその出力は英語の版のものを見せる(ホストは英語の版のモジュールを呼ぶ)。`kn` の塊には、サイトを組むときに `website/tools/knlexer.py` が色を付ける(31 章)。ritsu の `tests/website.rs` が、`kn` の塊が例とテストの `.kn` の行であること、コマンドが見せるとおりに出力すること、ホストの塊が例のホストの行で、続く出力が `host/output.txt` であること、例のページが四つの例の 12 のホストを全部見せること、英日のページが同じ種類と数の塊と表を見せること、kaname の README と根の README がページへリンクすること、組んだページの例のページに三つのタブの組が四つあること、組んだページの `kn` の塊に色が付くことを確かめる。サイトの頭のページの言語の一覧と kaname の段落も、ここへリンクする。
### 11.3 kaname のクレートのバイナリと、`kaname` という名前のリンク
**決定**:kaname のクレートに `src/main.rs` のバイナリを置く。内側の言語を持たないので、`use rule`・`use dates`・`use book` のあるモジュールには E202 を出して exit 2 で終わる(2.5)。`use module` だけのモジュールは全部のコマンドで動く。sekisho と dandori のクレートのバイナリと同じ形で、`cargo run -p kaname` でテストの材料を手早く確かめられる。
リリースでは、ほかの言語と同じく、`kaname` という名前の `ritsu` へのリンクを配る(アーカイブ、Homebrew、`.deb`・`.rpm`、npm のパッケージ)。リンクは `ritsu kaname` と同じに動く(ritsu の DESIGN 8.2)。`cargo install` が入れるのは `ritsu` だけで、これまでどおりである。
## 12. 診断
ritsu の土台の診断(`ritsu_base::diag`)と台帳(`ledger`)で書く。番号の帯は koyomi と sekisho にそろえる:E0xx は字句と構文、E1xx は名前と型、E2xx は内側の層とモジュール、E3xx は範囲の解析、E4xx はエフェクト、W5xx は逃げ道、E6xx は例、9xx はセキュリティの検査(ritsu の DESIGN 16 の帯で、どの言語でも同じ番号が同じ検査を言う)。警告は同じ帯の W。どのコードにも、英語の小さなモジュールの再現を台帳に置く(日本語の版も。`ritsu kaname explain` が引き、`tests/codes.rs` が走らせる)。
| コード | 見出し(英語) | 見出し(日本語) | 出る例 |
|---|---|---|---|
| E001 | This cannot be read as kaname | kaname として読めません(読めない文字、閉じていない文字列、タブ、`==` や `&&`、`,` での桁の区切り、続けた比べ) | `if a == b`、閉じていない `"` |
| E002 | A keyword cannot be a name | 予約語は名前にできません(十四語。`try` も) | `let in = 3`、`fn f(try: number)` |
| E003 | The module is not put together as kaname reads it | モジュールの組み立てが違います(見出し、節の順序、一度だけの行、列挙の `default` と `open`、値の無い列挙) | `use` の行が `fn` のあと、列挙の値に `default` |
| E004 | The indentation does not line up | 字下げがそろっていません | そろわない字下げ |
| E005 | A name the generated code takes needs an ASCII alias | 生成するコードに出る名前には、ASCII の別名が要ります(別名が ASCII の名前でないときも) | `fn 請求する(…)` に別名が無い |
| E006 | The same name is given twice | 同じ名前が二度あります(モジュールの `use` の名前と宣言、一つのレコードのフィールド、一つの関数の引数、一つの列挙の値、関数の形で書く組み込みの関数の名前、関数の中の名前、生成するコードの値の型の名前、モジュールの関数と同じ名前の関数の値) | `fn quote` が二つ、`fn sum`、`fn numbers`、`fn char`、`fn ok`、引数と同じ名前の `let`、`record OrderPlaced` と値 `Order.placed`、レコードと同じ名前の型の引数 |
| E007 | A name the generated code cannot take | 生成するコードが使えない名前です(出力先の予約語、補助のモジュールの名前、直和型の値のフィールドの `kind`) | フィールドの別名が `KanameError` |
| E008 | A name the generated Rust cannot give a module | 生成する Rust が、モジュールの名前に使えない名前です(W001 の名前で、`ritsu gen --target rust` のとき) | `module door` を `ritsu gen --target rust` で書く |
| W001 | A name the generated Rust cannot give a module | 生成する Rust が、モジュールの名前に使えない名前です(`check` は通す) | `module door` |
| E101 | Nothing has this name | この名前のものはありません(値の型も。メソッドの形でだけ書く組み込みの関数を関数の形で、関数の形でだけ書くものをメソッドの形で書いたとき、ほかのモジュールの `private` のものも) | `charges(…).fees`(無い出力)、`Order.shipped`(無い値の型)、`keys(prices)`、`trim(name)` |
| E102 | The types do not agree | 型が合いません(単位と税区分を含む。`..` のあとの値、比べる二つの値、関数の型、関数の型の引数に渡す関数でないものも) | ドルの額をセントの帳簿の振替に渡す(17.6) |
| E103 | An unrounded value is given where a rounded one goes | 丸めた値を受け取るところに、丸めていない値を渡しています(辞書の鍵と値、集合の要素、型の引数も) | `amount × 3%` を `money[USDc]` の引数に渡す、`running_totals(xs, x => 1 / x)`、`prices.map_values(p => p * 8%)` |
| E104 | A grid, a step or a conversion does not fit | 丸めの刻み、率の刻み、単位の換算が合いません(int64 に収まらない `match` のリテラルも) | `x in GBP`(`x` が `money[USD]`)、`mass[g]` の範囲に `1lb`、`0.5USD` |
| E105 | The type has no such operation | その型にない演算です(温度の値の `-`、直和型の値のフィールドを `.` で読むこと、フィールドの無い値を `{ … }` で作ること、型の引数の値の算術・`match`・フィールド、関数の値でない値を呼ぶことも) | `paid_on + 30`、`a < b` の `a` が `bool`、`x + x` の `x` が型の引数の値 |
| E106 | The match leaves values out | `match` が取りこぼす値があります(上の分岐がもう受けた値、`else` の無いリテラルの `match`、`else` と名前を付けた値の書き方も) | 列挙の値の一つが無い `match` |
| E107 | The call does not give what it takes | 呼び出しの引数が合いません(足りない、取らない、二度書いた、名前の無い引数。組み込みの関数と関数の値に名前を付けた引数、関数の値に渡す引数の数、レコードの足りないフィールド、名前だけで書いたフィールドを持つ値、値として書いた規則や帳簿、辞書のリテラルの同じ定数の鍵) | 規則の入力 `debt` を書かない呼び出し、`["tea": 300USDc, "tea": 350USDc]` |
| E108 | A declaration is missing or cannot hold | 宣言が足りないか、成り立ちません(引数と関数の答えとフィールドの率の刻み。空の範囲、二度書いた端、数と日付のほかの `range`、リスト・辞書・集合・文字列のほかの `count`、自分を含まない値が一つも作れない、自分を含むレコードと直和型、互いに含み合う型に型の引数から作った型を渡すこと、型の引数の値の `range`、`private` でないものに書いた `private` の型) | `-> rate`、`fn f(r: rate)`(刻みが無い)、`range >=9 <=1`、`x: T range >=1`、`record Node` の `next : Node`、`private record Bounds` を引数にとる関数 |
| E109 | A builtin function is used in a way it has not | 組み込みの関数の使い方が違います(`mod`・`numbers`・`at`・`take`・`drop`・`slice`・`pad_start`・`pad_end` に整数でない値、`char` に整数の定数でないものかスカラー値でない数も) | `text` に率か直和型の値、値として書いた組み込みの関数(`xs.map(isqrt)`)、お金で割る `mod`、`char(n)`(`n` が値) |
| E110 | A word reads two ways | 二通りに読める語です(名前と列挙の値、`{ … }` の前のレコードと値の名前) | 列挙の値 `gold` と、`let gold = …` が同じ場所で読める |
| E111 | A block has no answer | ブロックに答えがありません(最後が式でない、`if` に `else` が無い) | `else` の無い `if` |
| E112 | A type argument cannot be worked out, or does not fit | 型の引数が決まらないか、合いません(数が合わない、二つの型が当たる、値の無いことがある型か関数の型を当てた、ラムダの引数の型が決まらない。26.2、27.2) | `[]` だけを渡して決まらない、ドルのリストとセントの値を一つの `T` に渡す、`Page[number?]`、`let twice = x => x * 2` |
| E113 | A function value is put where it cannot go | 関数の値を、置けないところに置いています(関数の答え、フィールド、リスト・辞書・集合の要素、値の無いことがある値、`=` と `!=`、ほかの型の中の関数の型、値として書いた関数を取る関数。27.1) | `-> (number) -> number`、関数の型のフィールド、関数の値を `=` で比べる |
| E114 | This type cannot be a key | この型は鍵にできません(辞書の鍵、集合の要素、並べ替えと `distinct`・`max_by`・`min_by`・`to_dict`・`group_by` の鍵、鍵に限っていない型の引数の値の大小。24.1、26.2) | `dict[Line, number]`、`xs.sort_by(x => x.lines)`、`fn f[T](a: T, b: T)` の `a < b` |
| E115 | `try` cannot be written here | ここには `try` を書けません(答えに続かないブロック、`result` でも値の無いことがある値でもない値、答えの型、失敗の型の違い。26.3) | `let y =` の下のブロックの `try`、答えが `result` でない関数の `try` |
| E201 | A file this module uses cannot be read | 読むファイルを読めません(無いファイル、種類に合わない拡張子、その言語の検査を通らないファイル、出力の無い規則を呼ぶことと出力を型に書くこと。2.7) | rulec の検査を通らない規則を `use rule`、`enums` のファイルの規則を呼ぶ |
| E202 | Rules, dates and books are read with ritsu kaname | 規則と日付と帳簿は、`ritsu kaname` で読みます | クレートのバイナリで `use rule`(`use module` で読むモジュールが読むものも) |
| E203 | The modules use each other in a ring | モジュールの読み込みが輪になっています | `a.kn` が `b.kn` を、`b.kn` が `a.kn` を読む |
| E204 | The transfer is not one of the book's | 振替の呼び出しが、帳簿に合いません | `ledger.capture.hold`(すぐに確定する振替) |
| E301 | A value can be outside the range of where it is given | 値が、渡す先の範囲の外になることがあります | 分岐の外で、遅れた日数を規則に渡す(17.7) |
| W301 | Whether a value stays in the range of where it is given cannot be decided | 値が渡す先の範囲に収まるかを決められません | 再帰の関数の答えを規則に渡す |
| E302 | A value can leave int64 | 途中の値が int64 を超えることがあります | 範囲の上の端どうしの積が int64 を超え、ベクタにその入力がある |
| W302 | Whether a value stays in int64 cannot be decided | 途中の値が int64 に収まるかを決められません | `reduce` で積み上げた値の掛け算 |
| E303 | A divisor can be 0 | 割る数が 0 になることがあります | 0 を含む範囲の値で割る |
| W303 | Whether a divisor stays away from 0 cannot be decided | 割る数が 0 にならないかを決められません | `reduce` の答えで割る |
| E401 | The function does more than the effect written on it | 関数のエフェクトが、書いたエフェクトに収まりません | `total` と書いた関数が、示せない額を振替に渡す |
| E402 | The function given does more than the effect its parameter writes | 渡した関数のエフェクトが、引数の型に書いたエフェクトに収まりません(27.5) | `(T) -> U total` の引数に、あふれうるラムダを渡す |
| W501 | A decision is written in code; a table should hold it | 表に書くべき判断が、コードにあります | `if total >= 35USD then 0USD else 5USD`(7 章) |
| E601 | An example does not hold | 例が成り立ちません | `examples` の答えが違う |
| E602 | An example is not a call with an answer of its type | 例の書き方が違います | `examples` の行が関数の呼び出しでない |
| W901 | A value in the shape of a key is written | 鍵の形の値が書かれています | コメントに書いた Google の API キー |
言語をまたぐ判定の診断は、ritsu の台帳のコード(E201〜W205)で ritsu-cross が出す(6.4、14.4)。範囲を書かない値の、型の端から来る int64 のあふれは、E302 も W302 も出さない。生成したコードが確かめ、E401 の注と `doc` のページと `--format json` の場所が言う(22.3)。
**W901(ファイルに書いた鍵)**:ほかの言語の W901 と同じ検査と文である(ritsu の DESIGN 16.3。見つけ方は ritsu-base の `secrets`)。`.kn` の全文を、文字列もコメントも、構文の誤りがあっても調べる(`src/security.rs`。鍵は、ファイルが読めるかどうかに関係なくリポジトリにある)。診断は鍵の種類と接頭辞と長さだけを言い、鍵もその行も見せない。ほかの診断が鍵のある行を見せるときは、土台の `Diag::source` が鍵を伏せる。同じ行のコメントに `ritsu: test secret` と書いた値は言わない。台帳の再現は、コメントに偽の鍵を書いたモジュールである(実物。`tests/golden/codes/W901.en.txt`):
```text
warning[W901]: example.kn:3:41: A Google API key is written here (AIza…, 39 characters)
= A key in a file reaches everyone who can read the repository, its history and its builds. Keep it where the code runs (an environment variable, the platform's connection or secret store) and read it from there.
= If this key is real, revoke it with Google first: taking it out of the file leaves it in the history of the repository.
= If it is a value for tests, write `ritsu: test secret` in a comment on the same line.
ok example.kn: 1 function, total
```
直和型と `match` と一部を替えたレコードの診断(E003 のほか、上の表で括弧に足したもの)の実物は、`tests/golden/codes/sums.en.txt` と `sums.ja.txt` にある(23.10)。関数の値と `private` の診断(E113、E112、E102、E107、E109、E402、E108 の `private` の場合)の実物は、`tests/golden/codes/hof.en.txt` と `hof.ja.txt` にある(27.7)。ジェネリクスと `result` の診断(E112、E114、E105、E108、E115、E002、E006、E103 の型の引数の場合)の実物は、`tests/golden/codes/generics.en.txt` と `generics.ja.txt` にある(26.8)。自分を含む型の診断(E108 の、自分を含まない値が作れない型と、型の引数から作った型を渡す型)の実物は、`tests/golden/codes/trees.en.txt` と `trees.ja.txt` にある(28.1)。
台帳の 35 のコードは、どれも出る。どのコードにも英語と日本語の再現を台帳に置き、`tests/codes.rs` が、再現ごとにそのコードだけが出ることと、出る文が `tests/golden/codes/` と同じことを確かめる。
例が成り立たないとき(17.7 の一つ目の例の利息を 1,151 ペンスと書き違えたもの。実物)。答えがレコードなら、違うフィールドを注に挙げる:
```text
error[E601]: late_payment.kn:30:3: The example answers Claim { due: 2026-03-31, days_late: 50, compensation: 70GBP, interest: 1150GBPc }, and the answer written is Claim { due: 2026-03-31, days_late: 50, compensation: 70GBP, interest: 1151GBPc }
30 | claim(invoice: Invoice { id: "inv_1", invoiced: 2026-03-02, amount: 100_000GBPc }, paid_on: 2026-05-20, reference_rate: 0.5%)
= The field `interest` differs: the answer has 1150GBPc, and 1151GBPc is written.
= `check` runs the function of an example in the reference interpreter, as a host would call it, and compares the answer: correct the function, or the answer the example writes.
```
```text
エラー[E601]: late_payment.ja.kn:30:3: 例の答えは 請求 { 期日: 2026-03-31, 遅れた日数: 50, 定額の補償: 70GBP, 利息: 1150GBPc } です。書いてある答えは 請求 { 期日: 2026-03-31, 遅れた日数: 50, 定額の補償: 70GBP, 利息: 1151GBPc } です
30 | 遅れの請求(請求書: 請求書 { 番号: "inv_1", 請求日: 2026-03-02, 金額: 100_000GBPc }, 支払日: 2026-05-20, 基準金利: 0.5%)
= 違うのはフィールド `利息` です。答えは 1150GBPc、書いてあるのは 1151GBPc です。
= `check` は、例の行の関数を、ホストが呼ぶときと同じように参照インタプリタで流し、答えを比べます。関数か、例に書いた答えのどちらかを直してください。
```
17.6 の `payout.kn` の `examples` の前に、ドルで数えた手数料を受け取って振替を組み立てる関数(`fn charge(payment: Payment, fee: money[USD] range >=0USD <=1_000USD) -> ledger.processing_fee.do`)を足したとき(実物):
```text
error[E102]: payout.kn:26:78: The argument `fee` of the transfer `ledger.processing_fee.do` is money[USDc]; this is money[USD]
26 | ledger.processing_fee.do(payment: payment.id, seller: payment.seller, fee: fee)
= The unit `USD scale 2` of the book `ledger` counts money[USDc].
= `<value> in USDc` converts it: money[USD] and money[USDc] are two units of one dimension, and nothing converts one into the other unasked.
```
```text
エラー[E102]: payout.kn:26:78: 振替 `ledger.processing_fee.do` の引数 `fee` は money[USDc] です。渡しているのは money[USD] です
26 | ledger.processing_fee.do(payment: payment.id, seller: payment.seller, fee: fee)
= 帳簿 `ledger` の単位 `USD scale 2` は、money[USDc] で数えます。
= money[USD] と money[USDc] は、同じ次元の違う単位です。黙って換算はしないので、`<値> in USDc` のように換算を書いてください。
```
規則に渡す日数が範囲の外になりうるとき(17.7 の分岐を外し、答えを `Claim` にしたもの。実物)。例は、その関数の入力と、そのとき規則に渡る値である:
```text
error[E301]: late_payment.kn:22:52: `late` can be outside >=1 <=1500, the range of the input `days_late` of the rule `charges`
22 | let c = charges(debt: invoice.amount, days_late: late, reference_rate: reference_rate)
= For example, `claim(invoice: Invoice { id: "a", invoiced: 2026-01-01, amount: 50000000GBPc }, paid_on: 2026-01-01, reference_rate: 0%)` gives it -29.
= Narrow it to that range with an `if` before it goes there, or widen the range it goes to.
```
```text
エラー[E301]: late_payment.ja.kn:22:35: `遅れ` が、規則 `請求額` の入力 `遅れた日数` の範囲 >=1 <=1500 の外になることがあります
22 | let 額 = 請求額(債権額: 請求書.金額, 遅れた日数: 遅れ, 基準金利: 基準金利)
= たとえば `遅れの請求(請求書: 請求書 { 番号: "a", 請求日: 2026-01-01, 金額: 50000000GBPc }, 支払日: 2026-01-01, 基準金利: 0%)` のとき、-29 になります。
= 渡す前に `if` でその範囲に狭めるか、渡す先の範囲を広げてください。
```
その次の行で同じ日数をレコードのフィールド `days_late`(`range >=1 <=1500`)に置くところは、規則が答えたあとなので示せる(5.3)。
`total` と書いて、示せない場所があるとき(17.8 の `weight_of` の要素の数を 0 からにしたもの。実物)。注は、示せない場所と、それを決める診断のコードである:
```text
error[E401]: media_mail.kn:15:1: `quote` is written `total`, but it can fail with `rule`
15 | fn quote(lines: list[Line] count >=1 <=30) -> money[USDc]? total
= Line 19: whether `weight` stays in >=1oz <=240oz, the range of the input `weight` of the rule `media_mail`, cannot be decided (W301)
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
warning[W301]: media_mail.kn:19:24: Whether `weight` stays in >=1oz <=240oz, the range of the input `weight` of the rule `media_mail`, cannot be decided
19 | media_mail(weight: weight, media_only: all(lines, l => l.media)).price
= The analysis of ranges finds the value in >=0oz <=240oz, and no input among the function's vectors takes it out.
= The generated code checks it there when it runs.
= Narrow it with an `if`, which the analysis reads, and it is shown.
```
```text
エラー[E401]: media_mail.ja.kn:15:1: 関数 `見積もる` には `total` と書いてありますが、`rule` で失敗することがあります
15 | fn 見積もる(quote)(明細(lines): list[明細] count >=1 <=30) -> money[USDc]? total
= 19 行目: `重さ` が、規則 `メディアメール` の入力 `重さ` の範囲 >=1oz <=240oz に収まるかを決められません(W301)
= 書いたエフェクトに収まるよう、失敗しうる場所を示せるようにする(渡す値を `if` で範囲に狭める)か、エフェクトを広げて書いてください。
```
ほかのコード(W301 の答えの範囲、E302、W302、E303、W303、再帰と型の端から来たあふれの E401、W501)の実物は、台帳の再現が出す文で、`tests/golden/codes/` にある(`ritsu kaname explain` が引く)。
言語をまたぐ判定が示せない場所も、E401 の注に入る。17.6 の、手数料を引いた額を売り手に渡す形で、支払いの金額を 100 セント以上にしたもの(`ritsu kaname check`。実物)。振替の額は ritsu-cross の X4 が決められず(ritsu の W203)、`total` と書いた `settle` は E401 になる:
```text
error[E401]: payout.kn:17:1: `settle` is written `total`, but it can fail with `range`
17 | fn settle(payment: Payment) -> Settlement total
= Line 21: whether chobo takes `payment.amount - fee`, given to `amount` of the transfer `ledger.capture.do`, as an amount cannot be decided (ritsu W203)
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
```
```text
エラー[E401]: payout.kn:17:1: 関数 `settle` には `total` と書いてありますが、`range` で失敗することがあります
17 | fn settle(payment: Payment) -> Settlement total
= 21 行目: 振替 `ledger.capture.do` の `amount` に渡す `payment.amount - fee` を、chobo が額として受け取るかを決められません(ritsu W203)
= 書いたエフェクトに収まるよう、失敗しうる場所を示せるようにする(渡す値を `if` で範囲に狭める)か、エフェクトを広げて書いてください。
```
W203 そのものは、`ritsu check` が ritsu の診断として出す(14.4)。
## 13. 人が読むページ(`doc`)
`ritsu kaname doc <file.kn>` は、モジュールのページを Markdown か、一つの HTML のファイルで書く(`src/doc/`。ritsu-base の `docpage` の枠。ritsu の DESIGN 4.8)。読む人は二人いる。承認する人は、判断がどこにあるか(表にあるか、コードにあるか)を読む。エンジニアは、どの関数がどこまで確かめられたか(total か、実行時にどこで何を確かめるか)を読む。名前と型が検査を通るモジュールなら、範囲の解析やエフェクトがエラーを言っても書く(どこがなぜ失敗しうるかを見せるため。20 章の 53)。`ritsu kaname doc` は言語をまたぐ判定の答えを渡して描くので、その場所は、示した・例がある・決められないのどれかで書かれる。答えを渡さずに描く(kaname のクレートのテスト)と、答えを待つ場所として書かれる。
ページに置くもの(この順):
1. **頭**:生成したことを言うコメント(kaname の版、`.kn` とそのハッシュ)、モジュールの名前と別名、説明、ファイルとハッシュ、読むファイル(規則・日付のファイル・帳簿・ほかのモジュール)、kaname の版。そのあとに、検査が言ったことを数文にまとめた注(関数の数とエフェクト、呼ぶ規則と日付の数、組み立てる振替の数、コードにある判断の数)。
2. **表にある判断**:モジュールが呼ぶ規則と日付のファイルと、組み立てる振替。規則と日付のファイルには、どの関数の何行目で呼ぶかと、その言語のページ(口の `Rules::doc` と `Dates::doc`)を置く。振替は、操作ごとに、組み立てる関数、引数、拒否されうる理由を表にする(理由は、ritsu-cross が渡す額の範囲で chobo の探索に尋ねた答え。決められなければそう書く)。
3. **関数**:エフェクトで三つに分ける。total な関数、実行時に確かめる関数、止まらないかもしれない関数。関数ごとに、エフェクトの文(見出しに書いたものか、kaname が推論したものか)、説明、関数の行(見出しから本体の最後の行まで)を置く。実行時に確かめる関数には、失敗しうる場所の一覧(行、種類、何がなぜ示せなかったか、それを言う診断のコード、例があればその入力と値)を付ける。型の端から来たあふれは、診断が無いので、範囲を書かない値から来るので int64 を超えることがあると書く(22.3)。直和型の値を分ける `match`、値を作る式、一部を替えたレコードも、関数の行のまま載る(`tests/golden/doc/sums.en.md`)。型の引数を持つ関数には、ホストは呼ばず、モジュールの関数が呼び、呼び出しの答えとエフェクトは呼ぶところで読み直して決まる、と一文足す(21.3、26.5)。関数を取る関数には、渡す値と関数で読み直して決まることと、渡した関数のエフェクトが呼ぶ側に伝わる引数(無ければ、渡す関数は引数の型に書いたエフェクトに収まること)を、`private` の関数には、このモジュールの関数だけが呼ぶことを書く(27.5、28.2)。
4. **コードにある判断**:逃げ道の警告(W501)の全部を、行と形で並べる。列挙の値や数を答える `match` は、ここに挙がらない(23.9)。無ければ「このモジュールの判断は、どれも表にあります」と書く。
5. **例**:`examples` の行と、参照インタプリタの答え。
**決定(規則と日付のページを、Markdown でもページの中に置く)**:設計の段階では、Markdown のページは規則と日付のページへのリンクにするとしていた。リンクの先は、別に描いて置いておかなければ無いページで、モジュールのページだけを受け取った人には読めない。そこで Markdown でも、その言語のページを `<details>` の畳んだ節としてページの中に置く(GitHub は畳んだ節をそのまま開ける)。HTML は外から何も読まない一つのファイルで、規則と日付のページは、ボタンか `#page=<n>` でページの上に開くシートに置く(dandori の図が規則のページを開くのと同じ形。dandori の DESIGN 5.2)。シートの要素の id は `kn-pages` と `kn-sheet` である。どちらの形でも、中に置くページは、rulec と koyomi の `doc` がいま描くものと一字も違わない(`tests/doc.rs`)。
**決定(帳簿のページは置かない)**:chobo の口 `Books` には `doc` が無いので、振替は、操作と引数と理由の表だけにする(19 章)。
`examples/late_payment` の英語のページの頭(`ritsu kaname doc late_payment.kn --root .`。実物。畳んだ日付のファイルのページの中を省いた):
```text
<!-- Generated by kaname 0.44.0 from late_payment.kn (sha256:56982857c3a1dff6). This is a read-only rendering; the source of truth is the .kn file. Edits cannot be carried back. -->
# late_payment
What a supplier claims for an invoice paid late: the day it fell due, how many days late it was paid, and the fixed sum and the interest the rule gives for them
- File: `late_payment.kn` (module late_payment v1, sha256:56982857c3a1dff6)
- Reads: the dates file `terms` (`dates/payment_terms.cal`); the rule `charges` (`rules/late_payment_charges.rule`)
- kaname: 0.44.0
kaname 0.44.0 made this page by checking the file above and the files it reads. If the file's digest is no longer what it says here, the page is out of date.
> [!NOTE]
> 1 function, total. It calls 1 rule and 1 date of koyomi. It builds no transfer. No decision is written in its code.
## Decisions in tables
The decisions the module uses are in the files it reads, each checked by its own language: the rules by rulec, the dates by koyomi, the books by chobo. The module carries their answers and joins them.
### The dates file `terms`
`dates/payment_terms.cal`: it calls the date `due` on line 20 (`claim`).
<details>
<summary>The page of <code>dates/payment_terms.cal</code>, as <code>koyomi doc</code> draws it</summary>
…
</details>
```
日本語の版のページの注は「関数 1 個、total。呼ぶのは規則 1 個と、koyomi の日付 1 個です。振替は組み立てません。コードに書いた判断はありません。」である。実行時に確かめる関数は、失敗しうる場所を並べる(`tests/modules/places.kn` のページ。`tests/golden/doc/places.en.md` の一部):
```text
#### `outer`
It checks 1 place when it runs, and can fail with `range`. Its head writes no effect: this is what kaname infers.
…
Where it can fail:
- Line 12 (`range`): `n` can be outside what the parameter `n` of `inner` declares (range >=0 <=5) (E301). For example, `outer(n: 9)` gives it 9.
```
`--lang ja` で日本語のページになる。ページの文は、DESIGN.md の言い回しを持ち込まず、読む人の言葉で書く。ページの例の版は `crates/ritsu/tests/golden/kaname-doc/`(四つの例の英語と日本語の Markdown と、支払いの英日の HTML。`ritsu kaname doc` が描いたもの)と、`tests/golden/doc/`(失敗しうる場所、コードにある判断、止まらないかもしれない関数、答えを待つ場所の材料)にある。
## 14. ritsu-cross と口
kaname の呼び出しで、言語をまたぐ判定(X2・X3 の (a)・X4・X6)は、ワークフローの呼び出しと同じく ritsu-cross が持つ(ritsu の P4、P7)。kaname は、モジュールの呼び出しと、渡す値について自分が知っていることを口 `Programs` で渡し、ritsu-cross は判定の答えを口 `Verdicts` で返す。ritsu-cross は kaname のクレートに依存せず、kaname も ritsu-cross に依存しない(ritsu の DESIGN 3.1。`cargo xtask deps` が通る)。二つをつなぐのは `ritsu-project` と入口の `ritsu` である。規則の入力の範囲(数の範囲と、順序のある列挙の入力に書いた範囲)は、dandori の E014 と同じく kaname 自身が範囲の解析で確かめ(6.1)、ritsu-cross には渡さない(ritsu-cross の X2 は前提だけを見る。ritsu の DESIGN 3.2)。
### 14.1 rulec の口に足したもの
`RuleEnum` に三つのフィールドを足した。埋めるのは rulec の `codegen::rule_enums`(口の `facts` が使う)で、ほかに `RuleEnum` を作る口は無い。`RuleEnum` は `Default` を持つ。
- **`public`**:規則が括弧の中に書いた列挙の別名(`緊急度(urgency)` なら `urgency`)。無ければ名前。値の `EnumValue::public` と同じ決め方である。kaname は規則の列挙を、名前か公開名で指す(2.6)。口の `alias`(生成するコードの型の名前、`Urgency`)は公開名の綴りを変えたものなので、比べには使わない。
- **`ordered`**:値を `<` で区切った列挙か(rulec の `Checked::ordered`)。kaname は順序のある規則の列挙を比べられる(3.5)。
- **`origin`**:`import enum <名前> from "<ファイル>"` で丸ごと読んだ列挙の、元の `enums` のファイル(規則のディレクトリからたどったパス)と、そこでの列挙の名前。規則が自分で宣言した列挙と、`.proto` の列挙とステートマシンの状態は None である。kaname は二つの規則の列挙が同じ元から来ていれば、同じ型と読む(3.5)。元を参照の書き方(`rulec "<ファイル>" enum <名前>`)で持つ形は捨てた。`facts` の問いはルートを取らないので、ルートからのパスを書けないからである。届くパスを渡し、kaname が正規のパスにして比べる。
### 14.2 口 `Programs`(kaname が答える)
dandori の `Flows::crossings` にあたる問いである。型は `ritsu-ports` の `programs.rs` に置いた。
```rust
pub struct At { pub line: usize, pub col: usize } // a place in the module, from 1
pub type Ends = (Option<i128>, Option<i128>); // a range on the wire, both ends in
pub struct Given { // one value a call gives
pub at: At,
pub to: String, // the rule's input, the date's input, the transfer's parameter
pub shown: String, // as the module writes it: `paid_on - due`
pub range: Ends, // what kaname's analysis of ranges finds, on the wire
pub from: Vec<Origin>, // Day / Output carried unchanged, Range where kaname computes it
pub fields: Vec<(String, Ends)>, // a list a rule walks: the range of each numeric field
}
pub struct ProgramRuleCall { pub at: At, pub function: String, pub rule: PathBuf, pub name: String,
pub args: Vec<Given>, pub relations: Vec<(String, String, String)>, pub max_len: Option<i128> }
pub struct ProgramDateCall { pub at: At, pub function: String, pub file: PathBuf, pub name: String,
pub date: String, pub day: Given }
pub struct ProgramTransfer { pub at: At, pub function: String, pub book: PathBuf, pub transfer: String,
pub op: String, pub name: String, pub amounts: Vec<Given> }
pub struct ProgramCrossings { pub rules: Vec<ProgramRuleCall>, pub dates: Vec<ProgramDateCall>,
pub transfers: Vec<ProgramTransfer> }
pub struct Seen { pub at: At, pub call: String, pub input: String, pub given: Values }
pub trait Programs {
fn crossings(&self, file: &Path, ports: &Ports) -> Result<ProgramCrossings, Vec<Said>>;
fn seen(&self, file: &Path, ports: &Ports, function: &str) -> Vec<Seen>;
fn joined(&self) -> bool { true }
}
```
- `crossings` は、名前と型が検査を通るモジュールに答える(範囲の解析がそこまでのモジュールを読むので、解析やエフェクトがエラーを言っても答える)。通らないモジュールには、検査のエラーを `Said` で返す。答えは範囲の解析(5 章)が呼び出しごとに残したもの(`ranges::CallSite`)から作る。
- 渡す値は、kaname の型(`Given`)にした。dandori の `CallArg` と `Amount` を使う形は捨てた。kaname の値にはいつも範囲の解析の範囲があり(dandori の `range` と `unknown` の分け方が要らない)、判定の答えを返す場所(行と列)も要るからである。出どころには dandori と同じ `Origin` を使う。koyomi の日付の答えをそのまま運んだ値は `Origin::Day`、規則の数の出力をそのまま運んだ値は `Origin::Output`、kaname が計算した値とホストから入る値は `Origin::Range` である。`if` と `match` で一つの場所に来る値は、出どころを合わせる。そのまま運んだ値も、分岐で狭まっていることがあるので、`range` はいつも kaname の見積もりである。kaname は時刻を持たないので `Origin::Now` は出さない。
- `relations` は、呼び出しの場所で成り立つ、規則の入力どうしの関係(5.3 の関係を入力の名前に読み替えたもの。`<`、`<=`、`=`)。`max_len` は、規則がたどる並びに渡すリストの要素の数の上限(二つ以上なら大きいほう)。
- `ProgramDateCall` は、日付の入力に渡す日だけを持つ。整数の入力は kaname 自身が確かめる(E301・W301。dandori と同じ分け方)。
- **決定(見た値は、示せない判定があるときにだけ尋ねる)**:見た値(`seen`)を呼び出しごとに `crossings` の答えに入れる形は捨てた。それでは、どの判定も示せる関数(17 章の例の全部)でも、`check` のたびにベクタを流すことになる(5.7 の決まりに反する)。そこで `seen` を別の問いにし、ritsu-cross は、区間で示せない判定があったときだけ、その関数について一度尋ねる。`Seen` は、関数の入力(`ritsu kaname run --input` が読む JSON)と、それを kaname が書く形の呼び出し(`settle(payment: Payment { … })`)と、その場所に渡った値(受け渡す形)を持つ。値は、参照インタプリタが呼び出しの直前に残す(`Interp::given`)。規則が受け付けない呼び出しや、負の額で失敗する振替でも、渡そうとした値は残る。ベクタ(9 章)のほかに、渡す値が引数の定数倍の和なら、振替の額を −1 に、日を日付の入力と規則の日付の入力の範囲の一つ外にする入力も流す(5.7 と同じ作り方)。
### 14.3 ritsu-cross の判定を、kaname の呼び出しに効かせる
ritsu-cross の `src/programs.rs` が、モジュールごとに `Programs::crossings` を尋ねて、ワークフローのものと同じ判定を使う。判定の関数(`borders::days_given`、`amounts_given`、`amounts_hull`、`input_range`、`preconditions::decide`)は変えていない。`preconditions::decide` はモジュールの中だけの関数だったので `pub(crate)` にした。
- **X2(規則の前提)**:二つの入力の関係の前提は、まず `relations` を見る。渡す値の関係が前提を言っていれば(向きが同じで、`<` は `<=` も言い、`=` は `<=` と `>=` を言う)成り立つ。言っていなければ、kaname の値を dandori の `CallArg` の形にして `preconditions::decide` に渡す(rulec に区間で尋ねる)。入力を足した側のある前提(rulec の §15.216)は、すぐに `preconditions::decide` に渡す。並びの合計と長さの前提は、`max_len` と、並びの要素のフィールドの範囲を `preconditions_hold` に渡す(dandori は並びの長さを知らないので決められないが、kaname は決められることがある)。`range from koyomi` の日の前提は、渡す値の日(下の X3 (a) と同じ集め方)が、規則の日の集合に入るかで見る。関係の段が言うことは、Lean で証明してある。渡す関係が前提を言えば、その関係を保つどの値でも前提は成り立ち(`RitsuCross.kept_sound`)、kaname が呼び出しで残す関係は、その場所の値どうしで成り立つ(`KanameCore.callRels_sound`。二つを合わせたものが `KanameCore.kept_holds`。15.2)。
- **X3 の (a)(規則の日付の入力)**:範囲を宣言した日付の入力ごとに、渡す値がとる日を集める。koyomi の日付の答えをそのまま運んだ値は、koyomi が数えた日(`Dates::values`)のうち kaname の範囲に入るもの、そのほかの値は kaname の範囲の日(100 万日を超える範囲や端の無い範囲は、決められない理由にする)。それを `days_given` に、規則の範囲(`Rules::date_range`)と一緒に渡す。範囲を `range from koyomi` にした入力は、日が規則の前提なので X2 が受け持ち、ここでは数えない(ワークフローと同じ。kaname の範囲の解析も、その入力に X3 の場所を作らない)。
- **X6(koyomi の日付の入力)**:X3 の (a) と同じ日の集め方で、日付の入力の範囲(`input_range`)に入るかを見る。
- **X4(振替の額)**:額は、出どころが何でも、kaname の範囲で `amounts_given` に渡す(規則の出力をそのまま運んだ額も、kaname の範囲は規則の出力の範囲と同じか、分岐で狭まっている)。chobo の決まり(0〜2⁶³ − 1)は ritsu-cross の `borders::MAX_AMOUNT` が一か所で持つ。
- **X4(拒否の理由)**:`do` と `hold` の組み立てる場所ごとに、渡す額の全部の範囲(`amounts_hull`)で `Books::refusals` を尋ね、その操作の理由を答えとして返す。chobo が答えれば成り立つと数え、答えなければ決められない(W204)。kaname の呼び出しには扱う理由の宣言が無いので、`refusals_met` の「扱っていない理由」の判定(E204)は使わない。
- **例**:区間で示せない判定は、`Programs::seen` で見た値を尋ね、確かめを破る値を渡した実行があれば、それを例にエラー(E201、E202、E203、E205)、無ければ決められない(W201、W202、W203、W205)にする(6.4)。
境目の数(`Borders`)に、kaname の呼び出しの判定を一つずつ数える。`ritsu check` の最後の行の「言語の境目: 確かめた n か所、決められない m か所」に入る(17 章の例は、`payout` の二つの額と二つの振替の拒否の理由、`late_payment` の koyomi の日付の入力、`discount_split` の二つの前提)。
診断の見出しは、X2・X3 の (a)・X6 ではワークフローの呼び出しと同じ文にした。X4 は、タスクの代わりに、モジュールが書いた振替(`ledger.capture.do`)と渡す値を言う。注は kaname の呼び出しのもので、例は関数の入力で言い、決められないときは範囲の解析の見積もりと、kaname の生成したコードがその場所で確かめることを言う。`reduce` で `upto` を組み立てた `discount_split`(17.9)の `ritsu check`(実物):
```text
warning[ritsu W201]: discount_split.kn:17:41: Whether the call of the rule discount_share keeps its precondition `before <= upto` cannot be decided
17 | Share { label: r.item.label, share: discount_share(discount: discount, before: r.before, upto: reduce([r.item.amount], r.before, (acc, x) => acc + x), total: r.total).share })
= The analysis of ranges finds `r.before` in >=0 <=90000000 and `reduce([r.item.amount], r.before, (acc, x) => acc + x)` in >=0 <=100000000; at the ends of the ranges, before = 90000000, upto = 0, `before <= upto` does not hold, and no input among the function's vectors gives such values.
= kaname's generated code checks it there when it runs: where the rule's generated code refuses the call at its door, the function fails there with `rule`.
= Branch on the precondition with an `if` before the call, or give values whose relation kaname keeps (the running totals of `running_totals`), and it is shown.
```
```text
警告[ritsu W201]: discount_split.kn:17:41: 規則 discount_share を呼ぶところで、前提 `before <= upto` が保たれるかを決められません
17 | Share { label: r.item.label, share: discount_share(discount: discount, before: r.before, upto: reduce([r.item.amount], r.before, (acc, x) => acc + x), total: r.total).share })
= 範囲の解析では、`r.before` は >=0 <=90000000、`reduce([r.item.amount], r.before, (acc, x) => acc + x)` は >=0 <=100000000 です。範囲の端の before = 90000000, upto = 0 では `before <= upto` が成り立ちませんが、関数のベクタに、そういう値を渡す入力はありませんでした。
= kaname の生成したコードが、走らせたときにその場所で確かめます。規則から生成したコードが入口で受け付けなければ、関数はそこで `rule` で失敗します。
= 呼ぶ前に前提を `if` で確かめて分けるか、kaname が関係を知っている値(`running_totals` の途中までの合計など)を渡すと、示せるようになります。
```
### 14.4 診断のコード
**決定(新しい X も、新しいコードも作らない)**:kaname の呼び出しの確かめは、X2・X3 の (a)・X4・X6 そのものである。確かめることが同じなので、コードも同じにし、ritsu の台帳(ritsu-cross の `codes.rs`)の見出しと文を、ワークフローと kaname の関数の両方を言うように広げた。見出しを変えたのは、ワークフローの言い方だけだった四つである(E202「規則の日付の入力に渡す日が、入力の範囲を外れます」、W202、E203「振替に渡す額が、chobo の受け取らない額になることがあります」、W203)。「いつ出るか」は E201〜W205 のうち E204 のほかの九つで、直し方は E201、W201、E203、W203、E205 で、kaname の場合を足した。E204 はタスクが理由を扱うかの確かめで、kaname には効かない。そのことを、E204 の「いつ出るか」に一文で足した(kaname の関数が組み立てる振替では、拒否を扱うのは振替を当てるホストで、理由は生成する振替の型に載る。W204)。`crates/ritsu-cross/docs/codes.md` と `codes.ja.md` を取り直した。
**決定(台帳の再現に、kaname のモジュールを入れる)**:E201〜W205 のうち、E204 と W204 のほかの八つの再現(`ritsu explain` の `Repro`)は、ワークフローと同じ規則・日付のファイル・帳簿を呼ぶ `.kn` を一つずつ持つ。`ritsu check .` を走らせると、そのコードがワークフローの呼び出しとモジュールの呼び出しの両方に出る。E201 は払った額を超えうる求めをそのまま規則に渡し、W201 は払った額で頭打ちにした求め(`min(asked, paid)`。区間では示せず、ベクタの実行も破らない)を渡す。E202 と E205 は koyomi の期日を規則とほかの日付に渡し、W202 と W205 は、約束した日を、期日から数えた日数が 0 以上のときに渡す(`if` が狭めるのは日数で、日そのものではないので、区間では示せない)。E203 は規則が返す負の座席の数を、W203 は必要な数から持っている数を引いた数(`needed - min(held, needed)`。0 を下回らないが、区間では示せない)を振替に渡す。E204 と W204 の再現は、モジュールを持たない。E204 は kaname には出ず、W204 は chobo が答えないときにだけ kaname に出るからである。`crates/ritsu/tests/codes.rs` の `the_reproductions_of_the_borders_call_from_a_module_too` が、八つの再現で、モジュールの行にそのコードが英語と日本語で出ることを確かめる。根の README とサイトの頭のページが見せる E201 の再現は、規則とワークフローの二つのファイルである(README のテストは、ページが見せるファイルだけを置いて走らせる)。
ワークフローの呼び出しの診断の文は変えていない。替える前(段階 C2 の木)と後の `ritsu` で、dandori・sekisho・yuen・sakai の例と ritsu のテストのプロジェクトの `ritsu check`(英語と日本語、JSON)、各言語の `check`、dandori の生成したコード、`ritsu check ritsu.ctx`、`ritsu explain --all` を突き合わせると(180 回)、違うのは、台帳の文を広げた九つのコードの `explain --all`(英語と日本語)と、kaname の例(当時の `design/` の下書き)の `ritsu check`(エフェクトと境目の数)だけだった(段階 C3)。このほかに、ツール名を並べる文(yuen の E011 など)に kaname が入る(14.6)。
### 14.5 口 `Verdicts`(ritsu-cross が答え、kaname が受け取る)
dandori の `Undecided` にあたる口である。
```rust
pub enum Crossing { Precondition, RuleDay, Amount, Refusals, DateDay } // X2, X3 (a), X4, X4, X6
pub struct CallVerdict {
pub at: At, // the place, as `crossings` gave it
pub check: Crossing,
pub about: String, // the precondition (`before <= upto`), the input, the parameter, the operation
pub outcome: Answer<Seen>, // Holds, Fails(the run that breaks it), Undecided(why)
pub said: Option<(String, Text)>, // ritsu's code and the headline, where it does not hold
pub refusals: Option<Vec<String>>, // for the refusals of an operation
}
pub trait Verdicts { fn verdicts(&self, file: &Path) -> Vec<CallVerdict>; }
```
- ritsu-cross の `ProgramVerdicts` が答える(`Joined` から作る)。モジュールごとに一度だけ判定し、答えを覚える。`ritsu check` は一つの `ProgramVerdicts` を、kaname の検査と言語をまたぐ検査の両方に渡す(11.2)。`ritsu-project` は ritsu-cross に依存できないので、`Project::check_with(joined, verdicts, lang)` が答えを引数で受け取る(`Project::check` は答え無しで呼ぶ)。`ritsu kaname` は `languages.rs` で `ProgramVerdicts` を作り、kaname の `Suite::with_verdicts` で渡す。
- kaname は、範囲の解析の最後(`ranges::analyze`)で、言語をまたぐ判定の場所に答えを当てる。場所は、行と列(`At`)と、判定の種類と、何についてか(前提の文、入力、引数)で引く。示せれば `Outcome::Shown`、例があれば `Outcome::Example`(例の関数の入力は `Seen::input` を参照インタプリタの入口で読み直したもの、値はその場所に渡った値。前提は呼び出しについての判定なので値を持たない)、決められなければ `Outcome::Undecided` にし、ritsu のコードと見出しを `Place::across` に残す。振替の拒否の理由は `FnFacts::refusals` に残す(生成が振替の型に、`api` が `transfers` に使う。10.6)。そのあとでエフェクトを推論し、E401 を確かめる(4.3)。
- 答えの無い場所(ritsu-cross が判定しなかった場所)は、答えを待つ場所のままである。
### 14.6 中のものと参照(`Items`、`References`)
- ツール名 `kaname`(ritsu-base の `Tool::Kaname`。拡張子 `kn`)を `Tool::ALL` に入れた。種類は `function`、`record`(下に `field`)、`enum`(下に `value`、フィールドを持つ値の下に `field`)。参照の書き方は `kaname "pricing.kn" function quote`、値のフィールドは三つ目の組で `kaname "orders.kn" enum Order value placed field on` と書く(ritsu-base の `Tool::grandchildren`。ほかのツールの参照は組が二つまで)。ritsu-base の `tests/fixtures/naming.tsv` の kaname の行は、英語の名前の行が五つ、日本語の名前の行が二つ、誤りの行が四つ(値の下のフィールドの下にもう一つ組を書いた行)である(表の全部で JSON 54 行、誤り 32 行)。sakai の誤りの文は、置けない種類を、三つ目の組のものでも言う。
- `Items`:関数、レコードとフィールド、列挙と値。定義の文は、書いた行から作る(コメントと前後の空白を除き、続いた空白を一つにし、字下げを深さごとに空白二つに直したもの。dandori と sekisho と同じく `ritsu_base::definition` で作る)。関数は見出しから本体の最後の行まで(何行にもわたるレコードやリストの答えも入る)、レコードは宣言の塊、フィールドと列挙はその行(下に値を並べる列挙は、その行の塊)、列挙の値は名前と別名(`金(gold)`)、フィールドを持つ値は値の行とフィールドの行の塊、値のフィールドはその行である。
- `References`:`use` の行の先(`rulec "…"`、`koyomi "…"`、`chobo "…"`、`kaname "…"`。参照の仕方は `use rule` などの行の語)。`Joined` の索引に、kaname の `Items` と `References` を足した。
- sakai は、kaname の `use rule`・`use dates`・`use module` を、境界を越える参照として確かめる(sakai の `Kind::ModuleUse`。sakai の DESIGN 18 章)。モジュールから生成したコードは、読む規則と日付とほかのモジュールから生成したコードを同じプロセスで呼ぶので、使うのはそのもの(相手の内側)である。境界の向こうのものは、二つの共有カーネルに並べたときだけ読める(ゲートの `use` と同じ扱い)。規則が相手の公表された言語に入っていても、それは Connect のサービスとして呼ぶ形のためなので通らない(E202)。地図の最後の行は、kaname の参照の数を `kaname 1` のように数える。
- **決定(sakai は `use book` を確かめない)**:モジュールは振替の値を組み立てて返すだけで、帳簿に当てるのはホストである(`ritsu gen` が書く当てる関数を、ホストが帳簿のクライアントで呼ぶ。10.6)。帳簿をほかのコンテキストから使うのは、帳簿の後ろに立つサービスを通すときで(sakai の DESIGN 4.5)、その参照はホストのものになる。dandori の `use book` も sakai は数えない(dandori が書く実装が `Transport` の `book` で帳簿のクライアントを呼ぶ)。sekisho の `use book` も同じで、三つの言語でそろう。台帳を別のコンテキストに置いた地図で、境界を越える参照が規則の一つだけと数えられることを、ritsu の `tests/kaname.rs` が確かめる。
- sakai のキーワードに、ツール名 `kaname` と種類の語 `function` を足した(sakai の DESIGN 2.2 の決まり。ツール名と種類の語は、地図、コンテキスト、語、下流の値の名前にできない)。W101 の注の成果物の種類と、`owns` に書けるものを言う E002 の注に、`.kn` を足した。
- yuen は、kaname のモジュールの関数・レコード・列挙を索引で引き(`satisfied by kaname "x.kn" function quote`)、モジュールのファイル全体(kaname の検査)を `verified by` に書ける。yuen の台帳の E011 の文と、ツール名を並べる誤りの文(yuen、sakai、ritsu-base が `Tool::ALL` から作る)に kaname が入った。
## 15. Lean
ritsu の P7 と DESIGN 11 章にそって、範囲の解析と丸めの型の決まりの中心を Lean 4 でモデルにして証明し、Rust の実装と、テストが作る入力で突き合わせる。置き場所は根の `proofs/` の Lake のパッケージで、kaname のモデルと定理はライブラリ `KanameCore`(31 のファイル、およそ 24,700 行)に、kaname の呼び出しでの X2 の判定はライブラリ `RitsuCross` の `Program.lean`(判定)と `ProgramSound.lean`(定理)にある。mathlib を入れず、依存も足していない(ritsu の DESIGN 11.4)。
### 15.1 モデルにするもの
- **値**(`Value.lean`):数(Lean の `Rat`。式の型の単位で数えた正確な有理数)、日(日の番号)、文字列(Lean の `String`。コードポイントの `Char` の並び)、真偽、列挙の値(宣言した名前)、フィールドを持つ値(値の名前とフィールド。`Val.variant`)、`none`、レコード(フィールドを宣言の順に)、リスト、辞書(鍵の順の組の並び。`Val.dict`)、集合(鍵の順の要素の並び。`Val.set`)、振替(呼ぶ操作と引数)、関数の値(ラムダの番号と、作ったときの名前の値。`Val.closure`。27.8)。int64 の決まり(5.6)は、既約分数の分子と分母を見る述語 `fits` で言う。
- **式**(`Syntax.lean`):型の検査を通ったあとの kaname の式の全部。リテラル、名前、フィールド、`-` と `not`、`x in u`、四則、日の差、比べ、`and`・`or`、`if`、`match`、`let`、ブロック、関数・規則・日付の呼び出し、振替とレコードとリストを作ること、フィールドを持つ値を作ることと一部を替えたレコード(`Expr.record` の値の名前と `..x`)、`match` の名前を付けた値・リテラル・`else`(`Pat.value`・`Pat.lit`・`Pat.rest`)、組み込みの関数(`count`・`sum`・`any`・`all`・`map`・`filter`・`first`・`max_of`・`min_of`・`running_totals`・`reduce`・`min`・`max`・`mod`・`isqrt`・`allocate`・丸めの五つ・`starts_with`・`concat`・`text`)、辞書のリテラル(`Expr.dict`)と、辞書・集合・リストの組み込みの関数(関数を取らない 32 の関数の `Expr.prim`、関数を取る 10 の関数の `Expr.walkK`、`Expr.toDict`、`Expr.mergeWith`。24.7)、文字列の組み込みの関数(`Expr.prim` のもう 22 の関数。25.9)、ラムダを値にすることと関数の値を呼ぶこと(`Expr.lam`、`Expr.callV`。ラムダはモジュールの表 `Lam` に置く。27.8)。単位の読み替えと、型の言う刻みは、式に書き込んである。
- **内側の層**:規則と日付ファイルの答えは、モデルの外から与える(`Oracle`)。rulec と koyomi が自分の検査で示していることを前提(`OracleOK`)として受け取る。規則の出力は宣言した値の中にあって int64 に収まり、前提の無い規則は、受け付ける入力(数は範囲の中、列挙の入力に書いた範囲なら範囲に入る値)に答える。日付は、ファイルの最初と最後の日のあいだにあり、読む日からの日数の範囲の中にある。
- **評価**(`Eval.lean`):`eval : Ctx → Nat → Nat → Env → Expr → Except Stop Val`。参照インタプリタ(`interp.rs`)と同じ順で評価する。失敗は、エフェクトの五つの種類(`rule`・`date`・`range`・`overflow`・`zero`)と `input` と、モデルだけの `type` である。`type` は、型の検査が通さないもの(型の言う刻みから外れた値、引数の型の外の値、どの分岐も受けない `match` など)で、検査を通ったモジュールでは起きない。モデルに型は無いので、評価が型の約束に頼るところで確かめて、成り立たなければ `type` で止まる。関数の呼び出しが 200 より深くなれば `diverged`(8 章)。ホストが呼ぶ関数の入口(`Slot.enterD`)は、自分を含む型ごとの場所の表(`Module.slots`)を、場所が表の番号で指すところ(`Slot.ref`)からたどって、どの深さの値も宣言で確かめ、自分を含む型の値が 200 段(`NESTED`)より深く重なれば `input` で止まる。kaname の関数を呼ぶところの確かめ(`Slot.outsideD`)と、それを示したかの判定(`keepsD`)は、`ref` の場所の中を見ない(28.1)。要素が 1,000,000 を超える `numbers` と、要素・組・要素の数が 2⁶³ − 1 を超えるリスト・集合・辞書を作るところも `diverged` である(24.3。参照インタプリタは、あとのものを作れない)。文字列の関数は `Text.lean` の `List Char` の上の定義で計算する。1,000,000 コードポイントより長く伸ばす `pad_start`・`pad_end`、2⁶³ − 1 より長い文字列を作るところと分けるところ(`split`、`words`)も `diverged` である(25.9)。評価には手数の上限を渡し、尽きたら止まる。評価と範囲の解析の相互再帰の関数は、どれも手数の引数で構造的に再帰する(`termination_by structural`。Lean が整礎な再帰にすると、手数 0 の場合を定義のとおりに読めなくなる)。参照インタプリタも、`mod`・`isqrt`・`allocate`・丸めの数の引数が数でなければ止まる(8 章)。
- **範囲の解析**(`Iv.lean`・`Analysis.lean`):`ranges.rs` を関数ごとに書き直したもの。抽象の値(区間と分母の上限、値の集合、オプショナル、文字列のコードポイントの数、リストの要素の数と要素、辞書の組の数と鍵と値、集合の要素の数と要素、レコードのフィールドと関係、直和型の値ごとのフィールド、振替)、演算ごとの見積もり、条件と `match` と答えた呼び出しで狭めること、関係(条件で覚えたもの、`running_totals` の三つ、`running_totals` の要素の `total` と同じリストを同じフィールドで足した値が等しいこと。関係が結ぶのは、名前か、名前のリストをフィールドで足した値 `Term.sum` である。32 章、33 章)、失敗しうる場所の一覧とそれぞれを示したかどうか(規則の列挙の入力に書いた範囲の場所 `What.oneOf` も)、呼んだ関数、モジュールを輪の順に解析することとエフェクトの推論、規則の呼び出しごとに、渡す入力どうしの関係(`callRels`。口 `Programs` が ritsu-cross に渡すもの。14.2)。型の引数を持つ関数の呼び出しを、呼ぶところで、渡す値で読み直すこと(`Fn.reread`、`ACtx.depth`、`rereads`・`rereadCtx`・`rereadEnd`。読み直しの場所と呼び出しは、呼んだ関数の場所と呼び出しに入る。26.9)。関数の値(`Abs.fns`)の呼び出しで、とりうるラムダの本体を読むことと、読み直さない関数を取る関数の呼び出しで、渡したラムダを一度読むこと(`lamsAbs`・`callVAbs`・`onceAll`。27.8)。
- **言語をまたぐ判定の答え**:ritsu-cross の答えは、モデルの外から与える(モジュールの `verdicts`。場所の式と鍵と、そこで判定が成り立つか)。解析のあとで、答えを待つ場所に当て(成り立てば示した場所、成り立たなければ、決まったが示していない場所)、エフェクトを推論し直す(`analyzeWith`。`ranges.rs` の `across` と同じ順)。答えをどの場所に当てるか(行と列と判定の種類と、何についてかで引くこと)はモデルに入れず、突き合わせが、Rust が当てた場所をそのまま渡す。
- **kaname の呼び出しでの X2**(RitsuCross の `Program.lean`):ritsu-cross の `programs.rs` の判定を書き直したもの。kaname が渡す関係が前提を言うか(関係の段。`kept`)、ワークフローの呼び出しと同じ、区間の角での判定(`x2`)、関数のベクタの実行がその二つの入力に渡した値が前提を破るか、の順に決める(`x2k`)。
### 15.2 証明したこと
1. **丸め**(`Round.lean`、`IvSound.lean`):`roundInt_eq_roundQ`(3.3 の整数の手順は、有理数の上の定義と同じ答えを返す。`tests/rounding.rs` が rulec と照らし合わせていること)、`roundInt_onGrid` と `roundInt_roundsTo`(答えは刻みの上にあり、刻みで切り下げたものと切り上げたもののあいだにある。RulecCert の `roundsTo` の形)、`roundInt_mono`(五つの向きのどれも単調)、`In_round`(区間を丸めたものは、区間の値を丸めたものを全部含む)。
2. **範囲の解析の健全性**(`Sound.lean` から `SoundAll.lean` まで):`ranges_sound`。解析の環境に入るどの環境でも、式の評価が値を返せば、その値は式の抽象の値に入り、解析がそのあとに使う環境もその環境を含み、条件として狭めたことは、その条件が出た側で成り立つ。評価が五つの種類のどれかで失敗すれば、示していない場所か、失敗しうる関数の呼び出しが、解析の一覧にある。評価の手数についての帰納法(`sound_all`)で示す。読み直した呼び出しの本体の評価は、呼び出しより一歩短いので、一歩少ない評価の全部について言う形(`sound_upto`)の強い帰納法にした。読み直した呼び出しは `SoundReread.lean` の `reread_case` が言う(26.9)。ラムダを値にすることと関数の値の呼び出しは `SoundFns.lean` の `case_lam` と `case_callV` が言う(27.8)。部品として、狭めること(`Sat.apply`、`Sat.narrowCmp`、`armScope_sound`)、`match` の分岐(`arms_sound`、`arms_mono`)、作るところと `..x`(`record_case`)、直和型の等しさが決まること(`decideVariants_sound`)、辞書と集合と組み込みの関数(`SoundPrim.lean` の関数ごとの補題、`SoundDict.lean` の `case_dict`・`case_prim`・`case_walkK`・`case_toDict`・`case_mergeWith`)、文字列(`SoundText.lean` の関数ごとの補題と、組み込みの関数をまとめた `primAbs_sound`、`case_concat`・`case_text`。25.9)、関係(`relation_sound`、`subRel_sound`)、足した値の関係(`summed_eval`:`sum(xs, x => x.f)` の評価の答えは、足した値の項の値である。`totals_eval`:`running_totals(xs, x => x.f)` の答えのどの要素も、`total` が同じ項の値である。`Sat.bindLam`:組み込みの関数がその答えをたどるとき、ラムダの引数を置いたあとも、覚えた関係はどれも成り立つ。33 章)、`running_totals`(`running_mem`)、比べが決まること(`decide'_sound`)。関数について `analyzeFn_sound`(引数が宣言の値をとるどの呼び出しでも、答えは関数の結果の抽象の値に入り、五つの種類の失敗は関数の場所か呼んだ関数にある)、モジュールについて `module_sound`(輪の順に解析した全部の関数でそうなる)。規則の呼び出しが残す関係について `callRels_sound`(`Verdicts.lean`。残す関係はどれも、二つの入力に渡す二つの名前の値どうしの関係で、呼び出しの場所の解析の環境に入るどの環境でも成り立つ)。
3. **失敗しない**(`Places.lean`、`Total.lean`):`shown_sound`(示した場所は、失敗する値を取らない。規則の列挙の入力の範囲の場所は、渡す値がとりうる列挙の値がどれも範囲に入るときに示し、`enumWithin_sound` がその値を受け付けると言う)、`no_wit`、`no_failure`(式の場所が全部示され、呼ぶ関数が失敗しないなら、その式の評価は五つの種類のどれでも失敗しない)、`fn_noFail`(関数について同じ)、`total_sound`(モジュールの解析が total と推論した関数は、宣言した範囲のどの入力でも失敗しない。4.5)。言語をまたぐ判定の答えについて(`Verdicts.lean`)、`decideAcross_other`(答えが変えるのは、答えを待つ場所だけ)、`analyzeWith_nil`(答えが一つも無ければ、答えを当てた解析は `analyzeModule` と同じで、1 から 3 の定理がそのまま言える)、`total_with_sound`(答えを当てた事実が健全なら、つまり成り立つと言った答えがそのとおりなら、答えから推論して total の関数は、宣言した範囲のどの入力でも失敗しない)。
4. **前提の関係**(RitsuCross の `ProgramSound.lean` と、KanameCore の `Cross.lean`):`kept_sound`(kaname が渡す関係が前提を言うとき、関係を保つどの値でも前提は成り立つ)、`x2k_holds`(kaname の呼び出しで X2 が成り立てば、関係を保ち、kaname の区間の中にある、呼び出しが渡しうるどの値の組でも前提は成り立つ)、`x2k_fails`(例は関数のベクタの実行の一つで、前提を破る。関係は前提を言っていなかった)、`x2k_undecided`(決められないのは、関係が前提を言わず、区間の角でも成り立つと言えず、どの実行も前提を破らないときだけ)。`kept_holds` は、2 の `callRels_sound` と `kept_sound` を合わせたものである。規則の呼び出しが残す関係が前提を言えば、前提は、二つの入力に渡す名前の値どうしで、呼び出しの場所の解析の環境に入るどの環境でも成り立つ。2 の健全性は、評価の環境が呼び出しの場所の解析の環境に入ることを言うので、kaname の呼び出しで X2 が関係の段で成り立つと言ったことは、その呼び出しで本当に成り立つ。
1 から 3 の定理の前提は二つである。内側の層の答え(`OracleOK`。rulec が受け付けない入力は `RuleInput.refuses` が言い、範囲の外の数と、範囲に入らない列挙の値と、範囲の外の並びの要素のフィールドである。ritsu の口の `eval` と同じ)と、`reduce` が走る値の端を手放していないこと(関数の事実の `widened` が偽。15.4)である。見えない関数を呼んだ解析も `widened` にして外し、関数そのものの答えと場所は、関数を取らない関数について言う。`total_sound` は、エフェクトに `through` の無い関数について言う(27.8)。自分を含む型の引数を持つ関数も、この範囲の中にある。定理の前提の、引数が呼ぶところの確かめ(`Slot.outsideD`)を通ることは、入口の確かめ(`Slot.enterD`)が確かめることの一部である(`enterD` は、`outsideD` の確かめに、表をたどることと深さを足したもの。28.1)。関数の答えを呼ぶ側が使うことの前提(`ResultsOK`)は、モジュールの解析から出す(`module_sound`)。`total_with_sound` は、さらに、答えを当てた事実が健全であることを前提にする。成り立つと言った答えがそのとおりであることは、ritsu-cross の判定ごとの定理(X2 の関係の段の `kept_sound`、区間の段の `x2_holds`、X3 の (a) と X6 の `daysGiven_holds`、X4 の `amountsGiven_holds`。ritsu の DESIGN 11.2 の 3)が言うことで、関係の段のほかは、kaname のモデルの評価と結んでいない(15.4)。
**決定(total の定理を Lean の範囲に入れる)**:total は、ホストにも、あとで dandori のフローにも約束することで(4.5)、範囲の解析の健全性から直に出る。範囲の解析を証明するなら、失敗しない定理は、それに失敗しうる場所の一覧を足すだけで書ける。止まることは扱わない(再帰を持つ関数は total でないので、定理の外にある)。
**決定(言語をまたぐ判定の答えは、解析のあとで当てる)**:`ranges.rs` と同じく、範囲の解析は答えを知らずに場所を作り、答えはそのあとで当てる。こうすると、1 から 3 の定理は答えの無い解析についての定理のまま変わらず、答えの当て方については、待つ場所だけを変えることと、答えが無いときに元の解析と同じであることを示せば足りる。捨てたものは、解析の中で場所に答えを当てる形である。範囲の解析の健全性の帰納法の全部に、答えの正しさを前提として通すことになる。
### 15.3 Rust との突き合わせ
`ritsu-model` の `kaname` の問いが、モデルの評価と解析を走らせる。`crates/ritsu-model/tests/kaname.rs` が、モジュールを `ritsu kaname` と同じに(言語を全部つなぎ、ritsu-cross の答えを渡して)検査し、モデルの読む形(型の検査を通ったあとの式と関数、規則と日付ファイルの宣言、kaname が当てた答え)に書き出し、`vectors` の選ぶ入力を一行ずつ渡す。規則と日付の答えは、参照インタプリタがその入力で得たものを渡す。モデルの答え(値か、失敗の種類と行)を参照インタプリタのものと一行ずつ比べ、解析を、関数ごとの結果、場所(式、何の場所か、種類、取る値、示したか、答えを待つか)、エフェクト(失敗しうる種類、決まった種類、止まらないことがあるか)、規則の呼び出しごとの関係、式ごとの抽象の値で比べる。参照インタプリタが見た値が、その式の抽象の値の中にあることも確かめる。
材料は 60 のモジュールである。例の八つ(英語と日本語で十六)、kaname の `tests/modules/` のモジュールの三十四(`calls.kn` と `gcalls.kn` はほかのモジュールの関数を呼ぶので除く。15.4。自分を含む型は `trees.kn` と `trees.ja.kn`(28.1)、関数の値と `private` は `hof.kn` と `hof.ja.kn`(27.9)、総称の宣言と読み直しと `result` は `generic.kn`・`generic.ja.kn`・`results.kn`・`results.ja.kn`・`glib.kn`(26.10)、規則の列挙の入力の範囲は `lift.kn` と `lift.ja.kn`、範囲を書かない値は `open.kn` と `open.ja.kn`、直和型と `match` は `sums.kn` と `sums.ja.kn`、温度を別の単位で読むことと零より下の定数は `temps.kn` と `temps.ja.kn`(3.10)、辞書と集合とリストの組み込みの関数は `dicts.kn`・`dicts.ja.kn`・`lists2.kn`・`lists2.ja.kn`、文字列は `texts.kn`・`texts.ja.kn`)、それに、例を書き換えて言語をまたぐ判定の答えが三つとも出るようにしたモジュールの九つと、足した値の関係を確かめるモジュール `refunds` である。九つは、手数料を引いた額を振替に渡すもの(1 セントの支払いが例になるものと、1 ドル以上の支払いに限って決められないもの)、`reduce` で組み立てた `upto` と、入れ替えた `before` と `upto`、支払日を期日の計算に渡すもの、規則の範囲より早い koyomi の日を渡すものと、それを `if` で狭めたもの、koyomi の日付の日だけをとる日付の入力に、その日付の日と別の日付の日を渡すものである。`refunds` は、足した値の関係で規則の呼び出しの関係が変わるモジュールで(33 章)、明細の `running_totals` の要素ごとに、前提 `amount <= total` を持つ規則を呼ぶ六つの関数を持つ。どれも、足した値より大きい額を `if` で外したあとに呼ぶ。同じリストを同じフィールドで足した値と比べる二つ(関数の形と、値のあとの形でラムダの引数の名前を変えたもの)では関係が前提を言い、ほかのリスト、ほかのフィールド、フィールドをたどるだけでないラムダで足した値と比べる三つと、`let` で名前を付けた `running_totals` の答えをたどる一つでは言わない。自分を含む型の引数には、`vectors` の選ぶ入力のほかに、その引数を 200 段と 201 段に入れ子にした値も渡す(答えが JSON で比べられないほど深いものは比べない)。14,860 の評価、1,136 の場所(そのうち答えを受けた場所が 45)、5,424 の式、見た値 344,804 で、違いは無い(実物):
```
$ cargo test -p ritsu-model --test kaname -- --nocapture
compared kaname: 60 modules, 14860 runs, 1136 places (45 with a verdict), 5424 expressions, 344804 values seen
```
kaname の呼び出しでの X2 は、`crates/ritsu-model/tests/cross.rs` が確かめる。制約を持つ規則を呼ぶ小さなモジュール(関係を言う分岐の中の呼び出し、分岐の無い呼び出し、入れ替えた値、同じ値、`min` と `max` の七つを、四つの比べと四つの範囲の組で)を ritsu-cross に判定させ、口 `Programs` が渡す関係と範囲と見た値をモデルの `x2k` に渡して、答えを比べる。分岐に決して来ない呼び出し 16 を除いた 96 行で、違いは無い(成り立つ 34、そのうち関係の段で 26、実行が破る 61、決められない 1)。
モデルを一か所ずつわざと壊すと、どれも突き合わせが落ちる(壊したところと落ちたものの表は PLAN の F.10 と、この文書の 23.10、24.7、25.9、26.9、27.8、28.1、33 章)。壊したのは 47 か所である。丸めの `half_up`、引き算、`<=`、区間の和の上の端、`<` で狭めること、int64 の場所をいつも示すこと、呼んだ関数の止まらないことをエフェクトに入れないこと、`reduce` の回数、等しい二つの名前の差、レコードのフィールドの関係、`some x` の値、答えをいつも成り立つとすること、規則の呼び出しの関係を一つの向きでしか探さないこと、kaname の呼び出しの X2 で `<=` の関係が `<` も言うとすること、和の分母の上限に要素の上限をとること、リストの中の数を読み替えるところで int64 を見ないこと、区間を丸めて上の端を下の整数から丸め直すこと、単位を読み替えた比べで関係を覚えること、`match` の `else` で上の分岐が受けた値を除かないこと、リテラルの分岐で区間をリテラルの値に狭めないこと、`..x` から取るフィールドを `none` にすること、直和型の値の名前が交わらない等しさを決めないこと、`put` の数の上の端に 1 を足さないこと、`merge` の値に二つ目の辞書の値を合わせないこと、`slice` の 0 より前の場所を終わりから数えること、文字列の鍵を逆の順に比べること、`mod` の余りを割られる数の符号にすること、空白の表から U+0085 を除くこと、空の文字列の `index_of` を `none` にすること、`split` の前後の空の文字列を除くこと、文字列を長さで比べること、読み直しの深さを 3 にすること、読み直しの答えを宣言した答えと重ねないこと、読み直しの中のラムダの場所を数えないこと、読み直さないときのラムダの解析を外すこと、読み直しの中のラムダの呼び出しを数えないこと、名前で渡すことを輪に数えないこと、入口で入れ子の深さを数えないこと、入口で自分を含む型の値を表の場所でたどらないこと、`keepsD` が表を指す場所を示さないこと、自分を含む型の値の中で範囲を書かない刻みのある率を 0% から 100% と読むこと、呼ぶところで自分を含む型の値を示したとしないこと(名前で渡すことを輪に数えないものは Rust の `effects::rings` を、最後の二つは Rust の `ranges.rs` を壊した)。それに、足した値の関係の五つである。足した値のフィールドを見ないこと、足した値のリストを見ないこと、フィールドをたどるだけでないラムダも足した値にすること、ラムダの引数に `total` の関係を置かないこと、条件の `sum` の側を関係に覚えないこと(33 章)。壊すと証明が通らなくなるので、突き合わせを走らせる問いだけを組んで試した。書き出しの側では、範囲を書かない引数の `Slot` を型の全体にせずに書き出すと、`tests/modules/open.kn` の `next_of` の引数の場所とエフェクトで落ちる(22.6)。
範囲の解析が上からの見積もりになっていなかった六か所は、`ranges.rs` をモデルと同じ形に直した(5.2、5.3、5.6)。届く四か所の関数を `tests/modules/bounds.kn` に書き、直す前の形で何が見えるか(`tests/ranges.rs` の健全性の確かめと、Lean との突き合わせ)を確かめてから直した。
| 場所 | 前の形 | 届く関数 | 直す前に見えたこと |
|---|---|---|---|
| 刻みを言わない値の和(`sum`、`running_totals`) | 要素の分母の上限を、和の上限にした | `thirds` | 見た値が 47 回、抽象の値の外に出た(5/6 の分母が上限 4 を超える) |
| 単位を読み替えたリストの数 | リストの中の数と、何でもありうる値を int64 で見なかった | `many` | 実行の 201 回が `overflow` で失敗したのに、エフェクトは total だった |
| `x in u` の int64 の場所 | 値が数のときだけ置いた | なし | 型の検査がオプショナルの `in` を E104 にし、数の値は何でもありうる値にならないので、届くモジュールが書けない |
| 何でもありうる値のオプショナル | int64 に収まると示した | なし | 上と同じ |
| 区間を丸めた上の端 | 下の整数から丸め直した | `hundredths` | 見た値が 45 回、上の端より上に出た |
| 単位を読み替えた比べ | 関係を覚え、要素の数を狭め、引き算で関係を使った | `kept` | 値は外に出ないが、規則の呼び出しが、読み替えた比べから覚えた関係(`before <= upto` など)を ritsu-cross に渡した |
温度を `in` で別の単位に換算した値の刻み(3.10)も、解析が上からの見積もりにならない形だった。直したのは型の検査(`ty.rs` の `step_in`)で、届く関数を `bounds.kn` の `shifted` に書いた。直す前の形では、Lean との突き合わせが、見た値の三つが抽象の値の外に出ること(33.8℉ が上の端 32.4℉ の上)と、モデルが比べで `type` で止まること(33.8℉ は刻み 9/5 の上に無い)を見つけた。
直したあとは、健全性の確かめで外れが無く(いまの材料で、見た値 3,452,025、実行の失敗 1,617 のうちエフェクトの言わないもの 0)、Lean との突き合わせにも違いが無い。辞書と集合とリストの組み込みの関数(24.5)と、文字列の `join`(25.6)で、証明を書く中で上からの見積もりでないと分かったところも、Rust とモデルの両方で直した(20 章の 146、159)。
モデルと Rust がまだ違うところは九つである。
- 評価で、四則の答えは、int64 に収めるまえに、型の言う刻みの上にあることを確かめる。参照インタプリタは確かめない(型の検査を通ったモジュールでは成り立つ)。
- 評価で、要素・組・要素の数が 2⁶³ − 1 を超えるリスト・集合・辞書と、それより長い文字列を作るところ、要素がそれを超えるリストの `group_by` と、それより長い文字列の `split` と `words` で止まる(`diverged`)。参照インタプリタは、そういう値を作れない(24.3、25.9)。
- 二つの鍵が同じかを、モデルは `==` で、Rust は鍵の順の比べ(`key_cmp`)で決める。鍵にできる型では、どちらも同じ答えである(24.1)。
- 答えをどの場所に当てるか(行と列と判定の種類と、何についてかで引くこと)は、Rust だけが持つ。
- モデルは一つのモジュールを読み、ほかのモジュールの関数の呼び出しを持たない(`calls.kn`、`gcalls.kn`)。
- モデルは型を持たない。鍵に限った型の引数の値の順は、書き出しが、モジュールとその規則の列挙の値をつないだ一つの順で渡し、参照インタプリタは、呼び出しが当てた型の順で比べる。二つの列挙が同じ名前の値を違う順で持てば、書き出しは止まる(いまの材料には無い)。`result` の `ok x` と `err e` は、書き出しが、値に名前を付けるパターンとフィールドを読む式に読み替える(26.9)。
- Rust は、読み直しの場所と呼び出しを関数の場所と呼び出しから分けて持つ(`rplaces`、`rcalls`)。モデルは一緒に持ち、突き合わせは Rust の二つを合わせて比べる。
- 関数の値の抽象の値で、Rust はラムダごとに書いた関数と解析の枠を持ち、モデルは、その関数自身の解析で作ったかの印を持つ。突き合わせは、ラムダの番号の並びで比べる。参照インタプリタの見た値の確かめ(`Abs::has`)は、クロージャの捕まえた名前の値まで見るが、モデルの `has` は見えない関数がありうるかだけを見る(モデルの評価は、`has` を関数の型の引数の入口でだけ使う。27.8)。
- リストをフィールドで足した値(`sum(xs, x => x.amount)`)を、Rust は、リストのパスのあとに印 `⅀` と足すフィールドのパスを並べた名前として関係に持ち、モデルは名前とは別の項(`Term.sum`)として持つ(33 章)。モデルは、足した値と `running_totals` の答えを項にするところで、ラムダの答えを単位の読み替えなしに読むこと(`walk` の `c` が無いこと)も確かめ、ラムダの引数がたどるリストと同じ名前なら `total` の関係を置かない。型の検査は、`sum` と `running_totals` に、ラムダの答えの型を与え、関数の中で一つの名前を二度置くことを E006 で止める。検査を通ったモジュールでは、二つは同じ答えを出す。
これは突き合わせで、Rust の実装がモデルと同じ関数であることの証明ではない(ritsu の DESIGN 11.3 と同じ)。
### 15.4 範囲の外
生成したコード(出力先ごとのテストで確かめる)、止まること(定理は、手数が尽きた評価と、呼び出しが深すぎて止まった評価については何も言わない)、型の検査(モデルは型の約束を `type` の失敗として持つだけ)、`reduce` の広げ方(走る値の端を手放した解析は、定理の前提から外す。5.5)、ほかのモジュールの関数の呼び出し、言語をまたぐ判定がそれぞれの場所で言うことと評価との結び付き。X2 の関係の段は `kept_holds` で結んだ。区間の段と、X3 の (a)・X4・X6 は、RitsuCross の定理が言うことで、`total_with_sound` は、成り立つと言った答えがそのとおりであることを前提として受け取る。
## 16. テスト
### 16.1 種類と置き場所
| テスト | 置き場所 | 見るもの |
|---|---|---|
| 字句と構文、名前、型 | `crates/kaname/tests/parse.rs`、`names.rs`、`types.rs` | EBNF の全部の形、位置、読むファイル(E2xx)、単位と丸めの決まり(E1xx)。内側の層は、本物の口(dev-dependency の rulec・koyomi・chobo。`tests/common/mod.rs`)でつなぐ |
| 診断の台帳 | `tests/codes.rs` | どのコードも、英語と日本語の再現が自分のコードだけを出し、出る文が `tests/golden/codes/` と同じ。`docs/codes.md` と `codes.ja.md` が `explain --all --format markdown` の出力と同じ |
| 変異 | `tests/mutants.rs`(sekisho の形) | 例を一か所ずつ壊したモジュールが、決まったコードを出す |
| rulec のスーパーセット | `tests/superset.rs` | 16.2 |
| 丸めと日付 | `tests/rounding.rs`、`tests/dates.rs` | 16.3 |
| 参照インタプリタ | `tests/interp.rs` | 内側の層を本物の口(dev-dependency の rulec・koyomi・chobo)でつなぎ、例とテストのモジュールの答え、失敗の種類、評価の順 |
| 範囲の解析 | `tests/ranges.rs` | 例とテストのモジュール(`tests/modules/` の全部)の全部の関数で、ベクタと、引数の宣言から決まった順で選んだ 200 件の入力を流し、見た値が、どの式でも抽象の値に入る(解析が上からの見積もりであること)。実行の失敗は、どれも関数のエフェクトが言う種類である(失敗しうる場所の見落としが無いこと)。5,009 の式の 3,452,025 の値、139,754 回の評価(失敗 1,617)で、外れるものは 0(式は、モジュールごとに数える。テストのモジュールはどれも `t.kn` として読むので、ファイルと式の番号だけでは、別のモジュールの式を一つに数える)。型の端の印のある端 646 のうち 200 を、実行がとる(22.7)。型の引数の値の引数には数を、`result` の引数には `ok` か `err` を引く。例の値と関係、答えたあとと分岐で狭めること、見つけた例がその場所で失敗すること、規則の列挙の入力の範囲(`tests/modules/lift.kn` と `lift.ja.kn`。6.1)、温度を別の単位で読むこと(`tests/modules/temps.kn` と `temps.ja.kn`、`bounds.kn` の `shifted`。3.10)、`match` の分岐の狭め方(`tests/modules/sums.kn`。23.7)、辞書と集合とリストの組み込みの関数(`tests/modules/dicts.kn`、`lists2.kn`。24.5)、文字列の組み込みの関数(`tests/modules/texts.kn`。25.6)、総称の関数と読み直しと `result`(`tests/modules/generic.kn`、`results.kn`、`gcalls.kn`。26.10)、関数の値と `private`(`tests/modules/hof.kn`。27.9)、自分を含む型(`tests/modules/trees.kn`。28.1) |
| エフェクト | `tests/effects.rs` | 推論、伝わり方(ほかのモジュールからも)、輪、書いたエフェクトの確かめ(E401)、答えを待つ場所の数え方、言語をまたぐ判定の答えの当て方(16.5)、12 章の材料 |
| 逃げ道の警告 | `tests/hatch.rs` | 7 章の警告にする形としない形の全部(`match` が数える定数。23.9)、例で確かめて直した形、例の 16 の `.kn` |
| 鍵(W901) | `tests/secrets.rs` | コメントと文字列の鍵が、見つけた場所ごとに種類と接頭辞と長さで出て、鍵も行も出ないこと。読めないモジュールにも出ること。`ritsu: test secret` の行は言わないこと。ほかの診断が見せる行の鍵が伏せられること(12 章) |
| 例 | `tests/examples.rs` | 八つの例に英語と日本語の版がそろうこと、どの例も全部の言語をつないだ `check` を警告も無く通り、`examples` が成り立つこと、汎用の四つの例が `use` を持たず、ホストのプログラム(`host/`)を持つこと(17.5)、規則が転記した文書を、横に置いて固定したコピーから引用すること(八つの規則が固定する出典は 10)、日付のファイルが、規則の固定したコピーをコメントで名指すこと(17.11)。英語と日本語の版が同じ答えを返すことは `tests/vectors.rs`(16.6) |
| ベクタ、run、doc、api | `tests/vectors.rs`、`tests/run.rs`、`tests/cli.rs`、`tests/doc.rs`、`tests/api.rs` | golden(`tests/golden/vectors/`、`tests/golden/run/`、`tests/golden/doc/`、`tests/golden/api/`)。英語と日本語の版のベクタ(16.6)。ページが中に置く規則と日付のページが、rulec と koyomi の `doc` が描くものと一字も違わないこと、HTML が外から何も読まないこと、Chrome でページが描け、中のページがボタンと `#page=<n>` で開くこと(13 章)。コマンドと終了コード |
| 口 | `tests/ports.rs` | `Programs` の答え(区間、出どころ、関係、並びの長さ、見た値)、`Items`、`References` |
| 出力先の突き合わせ | `tests/typescript.rs`、`tests/python.rs`、`tests/go.rs`、`tests/rust.rs` | 16.4 |
| パッケージの Wasm | `tests/wasm.rs` | Rust の突き合わせと同じケースに、wasm32 に組んだモジュールが同じ行で答えること、契約の外の入力の表、component にして wasmtime で呼べること、WIT の名前(36.9) |
| 名前の字 | `tests/idents.rs` | 生成したコードの名前にする字の表(ritsu-emit の `xid.rs`)が通す名前を、rustc、Python、Node と tsc、Go が名前に取り、表が外す名前を取らないこと(36.3、36.9) |
| 文書とスキル | `tests/docs.rs`、`tests/skill.rs`、`tests/design.rs` | README と `docs/reference.md` とスキルの `kn` の行が、例かテストの `.kn` の行であること、見せるコマンドが kaname のコマンドであること(出力を添えたものは ritsu の `tests/kaname.rs` が走らせる)、リファレンスが全部のキーワードを挙げること、リンクの先があること、サイトで `kn` に色を付ける字句の定義(`website/tools/knlexer.py`)の語の一覧と数の文字が `kw.rs` と `lex.rs` のものと同じであること(31 章)。スキルのコピーが `skills/sync.sh` の作るものと同じで、スキルの外へのリンクが無いこと。この文書の `kn` の塊が構文として通ること |
| 言語をまたぐ判定 | `crates/ritsu-cross/tests/programs.rs` | 14.3 の判定(関係、見た値での確かめ、日付の区間、どこから来た額も)。16.5 |
| ritsu の入口 | `crates/ritsu/tests/kaname.rs`、`check.rs`、`cross.rs`、`gen.rs`、`codes.rs`、`readme.rs`、`website.rs` | `ritsu kaname` のコマンド(`api` と `doc` も)、`ritsu check` のテキストが `ritsu kaname check` を並べたものと同じこと、言語をまたぐ判定の診断と境目の数、sakai と yuen が中のものと参照を読むこと、`ritsu gen` の `glue/`、台帳の再現(八つにモジュールがあること)、例のページ(`tests/golden/kaname-doc/`)、例のホストのプログラムが `ritsu gen` のパッケージを呼んで `output.txt` のとおりに出力すること(17.5)、kaname の README とリファレンスとスキルが出力を添えて見せるコマンド、根の README の kaname の節(`order_flow` のホストの行と出力も)、サイトの頭のページと kaname のページ(11.2。組んだページの `kn` の塊の色は 31 章) |
| Lean との突き合わせ | `crates/ritsu-model/tests/kaname.rs`、`cross.rs`(kaname の呼び出しでの X2)、`proofs.rs`(どの宣言も三つの公理のほかに立たないこと) | 15.3 |
### 16.2 rulec のスーパーセットのテスト
rulec のコーパス(`crates/rulec/tests/corpus` の `.rule`、英語と日本語の双子を含む)の、`derive` と `define` の右辺の全部を、kaname の式として読む(`tests/superset.rs`)。
- 読み方:式ごとに一つのモジュールを書く。式が読む名前のうち、rulec が外から受け取るもの(入力、表の出力、並びの要約、寄与に開いた値)を関数の引数にして、rulec の型(単位、税区分、率の刻み、列挙)で宣言する。式が読む `derive` と `define` を rulec の順に `let` に書き、最後にその式を `let <名前> : <型> = <式>`(型は規則に書いたとおり)と書く。行の終わりの出典の引用(`@…`)、`derive` の範囲、コメントは式ではないので外す。
- 型:kaname がその `let` の名前に付ける型(単位と税区分、刻み、列挙、真偽)が、rulec の型と、rulec が求めた刻み(`Checked` の `grids`)と同じであること。
- 値:rulec のベクタ(`rulec::vectors::generate`)の入力ごとに、rulec の評価器がその名前に計算した値(`rulec::eval::run_bindings`)と、kaname の参照インタプリタがその `let` に計算した値が、同じ有理数であること。
- 読めない式は、理由ごとに数えて `-- --nocapture` の行に出す(`apply` が取り込んだ名前 `base:decay_rate` など)。飛ばした件数の上限をテストに書き、理由が増えれば落ちる。
ritsu 0.30.0 のコーパスでは、85 のファイルの `derive` と `define` の 143 行の全部を kaname が読み、143 行とも rulec と同じ型で、rulec のベクタの 20,033 の入力で値も同じである。読めない式は無く、上限は 0 にしてある。rulec が宣言にだけ許している二つのこと(無次元の値を数とも率とも宣言できること、税区分の無い金額に税区分を宣言できること)は、kaname の `let` にも許した(3.2)。コーパスの 8 行がそれを使っている。
広げたところ(値での割り算、比べの組み合わせ、日付の差、`in`)では、丸めていない値を渡す先に渡すと E103 になることを、別の材料で確かめる(同じファイル)。
### 16.3 rulec・koyomi と同じ答え
- **丸め**:kaname の丸めと、rulec の `RoundTo::round_to`(dev-dependency)を、分子 −300〜300 と分母 1〜12 の有理数、刻み(1、10、1/2、1/100、3、5/2 と、通貨と量の刻みの 100、1/10、5、1/1000)、五つの向きの全部の組(360,600 通り)で比べる(`tests/rounding.rs`)。
- **日付**:kaname の日の番号と年月日を、ritsu-ports の `day_text`、koyomi の `days_from_civil` と `civil_from_days`、rulec の `date_ord` と、0001-01-01 から 9999-12-31 までの全部の日(3,652,059 日)で比べる。日付の差は、`payment_terms.cal` の入力の 730 日の全部で、koyomi の `days` が数える日数(29)と同じになる(`tests/dates.rs`)。
### 16.4 出力先の突き合わせ
例(`examples/` の八つの例の、英語と日本語の版で十六)とテストのモジュール(`tests/modules/` の三十四と、`calls.kn` が読む `lib.kn`、`gcalls.kn` が読む `glib.kn`)の 52 のモジュールで、テストの中で一つの生成パッケージを作る。規則、日付のファイル、帳簿の生成物は、rulec・koyomi・chobo の生成の関数を dev-dependency で、`ritsu gen` と同じに呼んで書く(帳簿のクライアントは PostgreSQL のもの。`tests/common/targets.rs`)。Rust は `ritsu gen` がまだ書かないので、規則はパッケージの形の rulec の Rust、日付と帳簿は koyomi と chobo の Rust で書き、一つのクレートにする(36.9)。そこに出力先ごとのハーネス(`kaname::r#gen::harness`。パッケージには入れない。Rust のハーネスは、パッケージの入口 `glue::door::answer` を呼ぶ小さな `main` である)を置き、三つの入力を流して、参照インタプリタの答えと比べる。
- 全部の関数の全部のベクタ(9 章)。止まらないベクタ(200 段を超える呼び出し。8 章)は流さない。
- 引数の宣言の外の入力:引数の一つの部分(数の範囲の端の一つ外、日付の範囲の端の次の日と前の日、リストの要素の数の一つ多いものと一つ少ないもの、文字列の `count` の一つ外、正しくない文字列。25.8)だけを外し、ほかを基本の値にした入力。範囲を書かない値は型の端の一つ外で、数は TypeScript と Python で、日付(と正しくない日 2026-13-01)は TypeScript と Go と Rust で流す(22.4)。入口の失敗 `input`(4.4)を見る。ベクタはどれも宣言の中にあるので、ベクタだけでは入口の確かめの文に届かない。
- 自分を含む型の値を入れ子にした入力:自分を含む型の値を持ちうる引数ごとに、ホストが渡せるいちばん深い 200 段の値と、入口が `input` で止める 201 段の値(`vectors::nested_value`)を、ほかを基本の値にして流す(28.1)。答えが止まらないもの(値をたどる関数をほかの関数から呼んで、呼び出しが 200 段を超えるもの)は流さない。200 段の値をそのまま答えるもの(`trees.kn` の `named` と `simplify`、`catalog.kn` の `pruned`)も、出力先の答えの行を、参照インタプリタの答えの深さまで読んで比べる(`harness::compare`。28.1)。
比べるのは、答えの値、失敗の種類とモジュールと行、`KanameError` の文で、英語と日本語の両方の文で流す。型の検査も通す:TypeScript は `tsc --strict --erasableSyntaxOnly`(型を剥がすだけで走る書き方。TypeScript 7.0.2)、Python は `mypy --strict`(2.4.0)、Go は gofmt と `go vet`(go.mod が求める Go 1.26)、Rust は `cargo clippy --all-targets -- -D warnings` をエディション 2021 と 2024 の二度(`rust-version` は 1.94。36.8)。道具は `tools/`(`package.json` と `package-lock.json`、`requirements.txt`、`go/go.mod` と `go.sum`、`rust/Cargo.toml` と `Cargo.lock`)に置き、CI の `tools.yml` が入れる(Rust のクレートの依存は `cargo fetch --locked` で先に取ってくる)。どれも `tools` の段で、道具が無ければ SKIP の行を出して通す(ritsu-testkit)。kaname の生成物は第三者のパッケージを使わないので、ランナーの依存は型の検査の道具と、帳簿のクライアントが読むもの(Go の pgx、Rust の sqlx・tokio・tigerbeetle)と、Rust のテストが振替を帳簿に当てるプログラムで JSON を読む serde_json だけである。
```
$ cargo test -p kaname --test typescript --test python --test go --test rust -- --nocapture --test-threads 1
gofmt -l and go vet (go version go1.26.10 darwin/arm64) pass the package in English and in Japanese
En: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
Ja: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
mypy --strict (mypy 2.4.0 (compiled: yes)) passes the package and the harness in English and in Japanese
Python 3.13.11
En: 16790 of 16790 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2604 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
a call of kaname is 1 frame of Python: 200 calls answer at a limit of 203 (100 at 103), within CPython's 1000
a call through a function value is 1 frame of Python too: steps_down(66), 199 calls, answers at a limit of 202 (steps_down(33), 100 calls, at 103)
a value nested one deeper is 2 frames of Python held at the entry and compared: same of two chains of categories 200 deep answers at a limit of 405 (100 deep at 205), within CPython's 1000
Ja: 16790 of 16790 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2604 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
cargo clippy --all-targets -- -D warnings (cargo 1.94.1 (29ea6fb6a 2026-03-24), rustc 1.94.1 (e408947bf 2026-03-25)) passes the crate on the 2021 and the 2024 editions in English and in Japanese
En: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
Ja: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
30 vectors of 4 modules answer 57 transfers of 3 books: the reference makes 40 done, 17 refused
on postgres (PostgreSQL) 18.0: the 57 transfers come to what the reference says, and so do the balances and the holds
on TigerBeetle TigerBeetle version 0.17.9+cc1c06a: the 57 transfers come to what the reference says, and so do the balances and the holds
the crate with chobo's clients of the MySQL family builds and passes clippy, and is not run
tsc --strict --erasableSyntaxOnly (Version 7.0.2) passes the package in English and in Japanese
En: 16874 of 16874 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2688 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
Ja: 16874 of 16874 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2688 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules
```
(テストの行のほかを省き、`test … ...` の前置きを外した。)
14,096 件のベクタのうち 4,076 件が例の、10,020 件がテストのモジュールのもので、答えは値が 14,047 件(例の 4,076 件は全部が値)、失敗が 49 件(`overflow` が 24、`rule` が 11、`range` が 7、`zero` が 5、`date` が 2)である。例のベクタの多くは Functional Core の四つの例のもので、`order_flow` が 1,657 件ずつ(直和型の値ごとの候補が、状態とイベントの組の全部を試す。17.1)、`stock_diff` が 124 件ずつ、`handles` が 64 件ずつ、`catalog` が 140 件ずつである。宣言の外の入力は、TypeScript が 2,688 件、Python が 2,604 件、Go と Rust が 2,584 件で(例のものは TypeScript が 584 件、Python が 530 件、Go と Rust が 560 件。出力先で数が違うのは、型の端の一つ外の値を、持てる出力先でだけ流すからで、例では `order_flow` の範囲を書かないお金と日付である。22.4)、どれも `input` で失敗する。自分を含む型の値を入れ子にした入力は、どの出力先も 90 件で(`trees.kn` と `trees.ja.kn` の 36 件ずつ、`catalog.kn` と `catalog.ja.kn` の 9 件ずつ)、201 段の値はどれも `input` で失敗する。答えが止まらない 14 件(`trees.kn` の `leaf_sum` と `sizes`、`catalog.kn` の `paths`、`slugged`、`items`、`names`、`cheapest` に渡す 200 段の値。値をたどる関数をほかの関数から呼ぶので、呼び出しが 200 段を超える)は流さない。規則の列挙の入力の範囲は、`tests/modules/lift.kn` と `lift.ja.kn`(列挙の引数を一つとる四つの関数で、ベクタは列挙の四つの値)で確かめる。範囲に入らない `platinum`(`プラチナ`)をそのまま渡すベクタは、四つの出力先とも、規則の生成物の入口が受け付けずに `rule` で失敗し、参照インタプリタと同じ答えになる。生成したコードの名前を分ける決まり(10.3、36.3)は、`tests/modules/names.kn`(162 件。Go のキャメルケースで同じになる二つの引数、出力先の語と同じ名前の引数と `let`、Rust の予約語と生の識別子にできない語の名前、標準の型と値と同じ名前の型と関数、出力先が名前にできない字)で確かめる。直和型(受け渡す形、`match` の分岐、作る式、一部を替えたレコード、等しさ。23 章)は `tests/modules/sums.kn` と `sums.ja.kn`(ベクタは 696 件ずつ)で、温度を別の単位で読む換算と零より下の定数(3.10)は `temps.kn` と `temps.ja.kn`(88 件ずつ)で、辞書と集合とリストの組み込みの関数(24 章)は `dicts.kn` と `dicts.ja.kn`(611 件ずつ)、`lists2.kn` と `lists2.ja.kn`(331 件ずつ)で、文字列(25 章)は `texts.kn` と `texts.ja.kn`(1,922 件ずつ。宣言の外の入力は 86 件ずつ)で、総称の宣言と `result`(26 章)は `generic.kn` と `generic.ja.kn`(206 件ずつ)、`results.kn` と `results.ja.kn`(280 件ずつ)、ほかのモジュールの総称の関数を呼ぶ `gcalls.kn`(27 件)で確かめる(`tsc`、`mypy --strict`、`go vet` が総称の型と、鍵に限った型の引数に渡す順を通すこと。鍵に限った型の引数と関数の型の引数を持つ `index_by` で、隠れた順の引数が先頭に並ぶことも。clippy が、`Ord` を境界にした鍵の型の引数と、`::<…>` で型の引数を書いた呼び出しを通すこと。36.2)。関数の値と `private`(27 章、28.2)は `hof.kn` と `hof.ja.kn`(205 件ずつ。値が 199、`overflow` の失敗が 5、止まらないものが 1)で確かめる(`tsc`、`mypy --strict`、`go vet` がアロー関数、入れ子の `def`、関数のリテラルと、`private` の名前を通すこと。clippy が `&dyn Fn` で借りた関数の値とクロージャを通すこと)。自分を含む型(28.1)は `trees.kn` と `trees.ja.kn`(213 件ずつ。どれも値)で確かめる(`tsc`、`mypy --strict`、`go vet` が、自分を指す `interface` とデータクラス、`*T` とスライスで自分を持つ構造体、総称の `Tree[T]` を通すこと。clippy が `Box` と `Vec` で自分を持つ構造体と列挙を通すこと)。同じ鍵の要素の並べ替えの順、Map と dict を回す順、負の数の `mod` の答えも、コードポイントで数える長さと切り出しと位置、文字列の大小と並べ替え、空白と ASCII の大文字と小文字、数と日付を読むこと、正しくない文字列の入口の失敗も、四つの出力先で参照インタプリタと同じである。
- **呼び出しの深さ**:生成した Python で、kaname の呼び出しの一段は Python の一段である。`tests/python.rs` は、`tests/modules/rec.kn` の `countdown` が答える最小の再帰の上限を二分探索で求め(100 段で 103、200 段で 203)、200 段の呼び出しが、CPython の既定の上限 1,000 のまま答えることを確かめる(8 章)。関数の値の呼び出しも一段で、`tests/modules/hof.kn` の `steps_down`(一段下りるごとに、`steps_down`、`via`、`steps_down` を呼ぶラムダの三つを呼ぶ)は、100 段で 103、199 段で 202 の上限で答える(27.3)。自分を含む型の値は、入れ子の一段が、入口の確かめと比べで Python の二段になる。`tests/modules/trees.kn` の `same` は、200 段の分類の連なりを二つ受け取り、上限 405 で答える(100 段なら 205。28.1)。
- **Rust の帳簿**:`tests/rust.rs` の `the_generated_rust_makes_the_transfers_in_the_books` は、振替を作る四つのモジュール(`payout` の英日と、`tests/modules/` の `inner.kn` と `walks.kn`)のベクタの答えにある振替(30 件のベクタの 57 件)を、`glue/books/` の当てる関数と chobo の Rust のクライアントで、PostgreSQL(使い捨てのクラスタ)と TigerBeetle に当てる。クライアントの答えと、帳簿の残高と保留が、chobo の参照インタプリタの言うとおりになる。MySQL の系統のクライアントでは、クレートを組んで clippy を通し、走らせない(36.9)。
- **Rust の呼び出しの深さ**:生成した Rust は段を数えず、200 段の呼び出しと値をスタックで受ける。デバッグのビルドのハーネスは、16,770 件に、メインのスレッドのスタックが 1.5 MiB でも答え、1 MiB では `trees.kn` の `leaf_sum` に渡す 200 段の値であふれた(36.15)。リリースのビルドの Wasm は、スタックが 64 KiB でも 16,770 件に同じ行で答えた(既定は 1 MiB。36.15)。
- **暦に無い日**:`tests/typescript.rs`、`tests/go.rs`、`tests/rust.rs` の `a_day_of_no_calendar_is_outside_every_range` は、ホストの作れる暦に無い日(2026-02-30、2026-13-01、10000-01-01、TypeScript では日付の形でない `2026-0x-yy`)を、`tests/modules/calc.kn` の `days`(範囲の両端を書いた引数)と `open.kn` の `days`(端を型に任せた引数)に渡し、どれも範囲の外の `input` で失敗することを確かめる(22.5。出力は 36.15)。両端を書いた範囲で宣言の外の入力(上の二つ目)が流す日付は、端の次の日と前の日で、どれも暦にある日なので、その範囲の確かめが暦を見ないことには届かない(暦に無い 2026-13-01 を流すのは、端を型に任せた範囲だけである)。
- **`ritsu gen` の生成物**:kaname のクレートのテストは、言語をまたぐ判定の答えを渡さずに生成するので、答えを待つ場所に確かめを書く(10.4)。`ritsu gen` の生成物は、`crates/ritsu/tests/gen.rs` が同じハーネスで確かめる。`tests/projects/settlement` を `ritsu gen` で書き、三つの型の検査を通し、ベクタと宣言の外の入力を流す(自分を含む型は無い)。答えを渡すと、言語をまたぐ判定の場所の確かめが無くなる:
```
$ cargo test -p ritsu --test gen -- the_code_of_the_modules --nocapture
the code checks 0 places with the verdicts of the checks across the borders, 3 without them (3 of them across the borders)
TypeScript: 36 of 36 cases agree with the reference: 23 vectors and 13 inputs outside what the parameters declare
Python: 36 of 36 cases agree with the reference: 23 vectors and 13 inputs outside what the parameters declare
Go: 36 of 36 cases agree with the reference: 23 vectors and 13 inputs outside what the parameters declare
```
### 16.5 言語をまたぐ判定
kaname のクレートは ritsu-cross に依存しない(dev-dependency にもしない)ので、本物の判定は ritsu の側で確かめ、kaname の側では答えの当て方を確かめる。
- `crates/ritsu-cross/tests/programs.rs`:判定(14.3)を、本物の rulec・koyomi・chobo の答えと kaname の口の答えで確かめる。関係が前提を言う按分(英語と日本語の版。示せる)、`upto` を `reduce` で組み立てて関係が無くなった按分(区間の端では破るが、ベクタの実行は破らない。決められない)、`before` と `upto` を入れ替えた按分(実行が破る。例)。期日の請求の日付の入力(示せる)と、そこに支払日を渡した呼び出し(例)。koyomi の日を、範囲の始まりが遅い規則の日付の入力に渡す呼び出し(例)、範囲を広げた規則(示せる)、`if` で狭めた日(示せる)。支払いの額と手数料(示せる。二つの操作の拒否の理由も)、手数料を引いた額(1 セントの支払いで −48。例)、1 ドル以上の支払いに限った同じ額(区間は 0 を下回るが、実行は下回らない。決められない)。名前が通らないモジュールに答えが無いこと。
- `crates/ritsu/tests/cross.rs` の `kaname_calls_across_the_borders`:同じ材料を七つのプロジェクトにして(X4 の三つ、X2 の三つ、X3 の (a) と X6 の一つ)、`ritsu check` のテキスト(英語と日本語。`tests/golden/cross/kaname-*`)、終了コード、境目の数(成り立つ、破る、決められない)を確かめる。
- `crates/ritsu/tests/kaname.rs`:例の八つの `.kn` が `ritsu kaname check` で(判定の答えを渡されて)total になること、`ritsu check` の境目の数、`ritsu kaname api` が判定の答えと拒否の理由を言うこと、sakai が kaname の `use rule` を共有カーネルでだけ通し(順応者からは E202、関係が無ければ E201)、ほかのコンテキストの帳簿の `use book` を数えないこと、yuen が関数とモジュールを引くこと、`ritsu kaname doc` が例のページを描くこと。`check.rs` は、支払いの例のプロジェクトの境目の数(型の通らないモジュールの呼び出しは数えない)。`codes.rs` は、台帳の八つの再現のモジュールが、そのコードを出すこと(14.4)。
- `crates/kaname/tests/effects.rs`:口 `Verdicts` に作り物の答えを渡して、答えを待っていた場所が示せた・例・決められないになり、エフェクトと E401 がそれに従うこと。
- `crates/kaname/tests/ports.rs`:口 `Programs` の答え(区間、出どころ、関係、並びの長さ、見た値)と、`Items` と `References`。
### 16.6 例と双子
例の英語の版と日本語の版(`x.kn` と `x.ja.kn`、内側のファイルも二つの版)は、モジュールの別名と名前と、関数が答える文字列の言葉のほかは同じである。ベクタの名前を別名で読み替え(直和型の値とそのフィールド、`result`、辞書と集合の中も)、日本語の版の関数が書いた文字列を、英語の版の同じところの文字列に読み替えて比べ、入力と答えが同じことを確かめる(rulec のコーパスの双子、sekisho の例の双子と同じ考え)。日本語の帳簿の振替と引数には別名が無いので、振替は、帳簿の中の振替と引数の並びの位置で読み替える。八つの例の 2,038 件が同じ答えになる(`tests/vectors.rs`)。
### 16.7 golden と段
golden は `crates/kaname/tests/golden/` に置き、`KANAME_BLESS=1` で取り直す。ritsu の側のもの(`crates/ritsu/tests/golden/`)は `RITSU_BLESS=1`。テストの段(ritsu の DESIGN 10.2)は、出力先の突き合わせと Lean との突き合わせが `tools`、ほかは `fast` である。
## 17. 例
例は `crates/kaname/examples/` の下の八つで、どれも英語の版が先にあり、日本語の版を `<名前>.ja.kn` として横に置く(17.10)。初めの四つ(17.1〜17.4)は、ほかの言語を読まない汎用のコアで、状態とイベント、辞書と集合、文字列と `result`、再帰する型と関数の値を一つずつ使う。どれも、生成したコードを TypeScript・Python・Go から呼ぶホストのプログラムを持つ(17.5)。あとの四つ(17.6〜17.9)は、規則・日付・帳簿を ritsu の型のまま呼ぶ例で、横に置いた内側のファイルを読む。
16 の `.kn` は、どれも `ritsu kaname check`(構文、名前、読むファイル、型、範囲の解析、言語をまたぐ判定、エフェクト、逃げ道の警告)を警告も無く通り、`examples` も成り立つ(`tests/examples.rs`、`tests/types.rs`、`tests/ranges.rs`、`crates/ritsu/tests/kaname.rs`)。エフェクトは、木をたどる 17.4 の関数が `diverges`(再帰を持つ。P8)で、ほかの例の関数はどれも total である。`run` と `vectors` の実物は `tests/golden/run/` と `tests/golden/vectors/` に、`api` の実物は `tests/golden/api/` にある(`api` の golden は、ritsu-cross の答えを渡さない kaname のテストのものなので、言語をまたぐ判定の場所は `waiting` である。答えを渡した `api` は `crates/ritsu/tests/kaname.rs` が確かめる)。ページ(13 章)は `crates/ritsu/tests/golden/kaname-doc/` にある。規則・日付・帳簿を呼ぶ例は公開の文書(法令、公開の手引き、公開の料金表)を使い、規則が転記したところを、横に置いて固定したコピーから引用する(17.11)。内側のファイルは、ritsu 0.30.0 で `ritsu check` を通してある(下に貼った出力)。kaname が言うことは、どれも実物である。
### 17.1 注文の状態とイベント(`examples/order_flow`)
注文を状態から状態へ進める。状態はカート、確定済み、支払い済み、発送済み、取り消し済みの五つで、状態ごとに持つフィールドが違うので、直和型 `Order` の値にする(23.1)。イベント(明細を足す、除く、確定する、払う、発送する、取り消す)も直和型 `Event` の値である。0 章に `Line`、`Order`、`next` を貼った。
- `next(order, event)` は、状態とイベントの組ごとに、次の状態(`ok`)か、イベントを拒む理由(`err`)を `result[Order, string]` で答える(26.3)。拒むのは業務の手順として拒むことで、検査が示せなかった場所(`fails`)とは別である。`match` の網羅と、`else` が受ける値が残ることは、kaname が確かめる(E106)。
- `replay(events)` は、空のカートから、イベントを順に当てる。`reduce` に、`result` を受け取って次の `result` を答える関数(`after`)を渡すので、最初に拒まれたところで止まる。`open(order)` は、注文がまだイベントを受け付けるか。
- 範囲を書いたのは明細(品番のコードポイントの数、数量、単価)とリストの件数で、額(`total`、`amount`)と日付と追跡番号は範囲を書かない(22 章)。合計は、50 件まで、数量 100 まで、単価 1,000,000 セントまでの積の和で、5,000,000,000 セントに収まると示せる。カートに足すところは `if c.lines.count() < 50` の中で、確定するところは `else` の中で、リストの件数が `Order` のフィールドの `count` に収まると示せる。どの関数も total である。
```text
$ ritsu check . --root .
ok order_flow.ja.kn: 5 functions, total
ok order_flow.kn: 5 functions, total
ritsu check: 2 files (kaname 2): all pass; borders between the languages: 0 checked, 0 undecided
```
`run` の実物(`tests/golden/run/order_flow.txt`)。五つのイベントで、注文は発送済みになる:
```text
replay(events: [Event.add { line: Line { sku: "pen", qty: 2, price: 150USDc } }, Event.add { line: Line { sku: "ink", qty: 1, price: 400USDc } }, Event.place { on: 2026-10-22 }, Event.pay { amount: 700USDc }, Event.ship { tracking: "1Z-42" }])
line 78: let start = ok(Order.cart { lines: [] })
= ok(Order.shipped { lines: [Line { sku: "pen", qty: 2, price: 150USDc }, Line { sku: "ink", qty: 1, price: 400USDc }], tracking: "1Z-42" })
```
ベクタは英語と日本語の版で 1,657 件ずつで、直和型の値ごとの候補(9 章)が、状態とイベントの組の全部を試す。
### 17.2 二回の棚卸しの差(`examples/stock_diff`)
倉庫の在庫を、品番から棚にある数への辞書 `dict[string, number]` で持ち、二回の棚卸しを品番ごとに比べる(24 章)。
- `changes(before, after)` は、二つの辞書の鍵を集合にして和をとり(`union`)、品番ごとに、増えた(`added`)、無くなった(`removed`)、数が変わった(`changed`)を直和型 `Change` の値で答え、変わらない品番を除く(`filter_map`)。辞書と集合は、作った順にかかわらず鍵の順で回るので、答えはどの出力先でも品番の順に並ぶ(24.1)。
- `net(moves)` は、一日の入出庫を品番ごとにまとめ(`group_by`、`map_values`)、`moved(stock, moves)` は、それを棚卸しの数に足して(`merge_with`)、0 になった品番を除く(`filter`)。`to_reorder(stock, floor)` は、置いておく数より少ない品番の集合を、`units(stock)` は数の合計を答える。
- 辞書の値に範囲(0 から 1,000,000)を、辞書とリストに件数を書いたので、足し算と合計は int64 に収まると示せる。どの関数も total である。
```text
$ ritsu kaname run examples/stock_diff/stock_diff.kn changes --input tests/inputs/stock_diff.json --trace
changes(before: ["ink": 4, "pad": 2, "pen": 10], after: ["pad": 2, "pen": 7, "tape": 3])
line 34: let skus = ["ink", "pad", "pen", "tape"].to_set()
= [Change.removed { sku: "ink", qty: 4 }, Change.changed { sku: "pen", was: 10, now: 7 }, Change.added { sku: "tape", qty: 3 }]
```
入力の JSON は、辞書を `[[鍵, 値], …]` で書く(24.4。`tests/inputs/stock_diff.json` は `"before": [["ink", 4], ["pen", 10], ["pad", 2]]`)。書いた順は鍵の順でなくてよく、受け取った辞書は鍵の順に並ぶ。
### 17.3 ハンドルの正規化と確かめ(`examples/handles`)
人が入力したハンドル(サービスで使う名前)を、サービスが持つ形にしてから、決まりに合うかを確かめる(25 章)。
- `normalized(raw)` は、両端の空白を除いて中の空白の続きをハイフン一つにし(`words`、`join`)、A〜Z を a〜z にする(`lower_ascii`)。空白は Unicode の White_Space の 25 文字で、大文字と小文字は ASCII の文字だけを替える(25.3)。
- `problems(handle)` は、合わない理由を全部、順に答える。長さ(3〜20 のコードポイント)、使える文字(a〜z、0〜9、`-`)、英字で始まること、サービスが使う語でないこと。理由ごとの文字列か `none` をリストにして、`filter_map` で `none` を除く。
- `handle(raw)` は、形を整えたハンドルか、理由の全部を `result[string, list[string]]` で答える。`tag(text)` は `<ハンドル>#<番号>` を読み、ハンドルを `let h = try handle(…)` で受け取り(`err` ならそのまま答える。26.3)、番号を `parse_number` で読む(25.4)。番号は `if` で 1〜9999 に狭めてから、`Tag` を作る。
- 文字列の長さはコードポイントで数えるので、`"ünïcode"` は 7 文字で、ü と ï は使えない文字である。文字列を受け取る関数のベクタには、出力先の言語が違って読む 15 の文字列(25.8)が入る。英語と日本語の版で 64 件ずつで、どの関数も total である。
```text
$ ritsu kaname run examples/handles/handles.kn tag --input tests/inputs/handles.json
tag(text: "Ada Lovelace#42")
= ok(Tag { handle: "ada-lovelace", number: 42 })
```
### 17.4 カタログの分類の木(`examples/catalog`)
カタログの分類は木の形で、分類はそれぞれ品物と、下の分類のリストを持つ。`Category` は自分を含むレコードである(28.1)。
- `paths(c)` は、品物ごとに、上からの分類の名前を ` / ` でつないだパスを答える。`pruned(c)` は、その分類にもその下にも品物を一つも持たない分類を除いた木(`Category?`。全部が空なら `none`)を、一部を替えたレコード(`{ ..c, children: kept }`。23.4)で作る。
- `relabel(c, label)` は、分類の名前を、渡された関数(`(string) -> string`)で替えた木を答え、`collect[T](c, pick)` は、渡された関数が分類ごとに選んだものを、上から順に集める(26・27 章)。どちらも関数の値を取るので中の関数で、ホストは呼ばない(21.3)。ホストが呼ぶのは、それを呼ぶ `slugged`(名前をウェブのアドレスに使う形にする)、`items`、`names`、`cheapest`(いちばん安い品物。`min_by`)である。
- どの関数も木をたどる再帰を持つ(または、それを呼ぶ)ので `diverges` で、ほかに失敗しうる場所は無い(「9 functions, diverges」)。止まることは確かめない(P8)。ホストから入る木は、入れ子が 200 段までで、入口で確かめる(28.1)。
```text
$ ritsu kaname run examples/catalog/catalog.kn paths --input tests/inputs/catalog.json
paths(c: Category { name: "Garden", items: [], children: [Category { name: "Hand Tools", items: [Item { sku: "sp-1", name: "Spade", price: 1999USDc }, Item { sku: "tr-2", name: "Trowel", price: 899USDc }], children: [] }, Category { name: "Seeds", items: [], children: [Category { name: "Bulbs", items: [], children: [] }] }, Category { name: "Hoses", items: [Item { sku: "hr-1", name: "Hose reel", price: 4500USDc }], children: [] }] })
= ["Garden / Hand Tools / Spade", "Garden / Hand Tools / Trowel", "Garden / Hoses / Hose reel"]
```
### 17.5 ホストから呼ぶ
17.1〜17.4 の例は、`host/` の下に、生成したコードを呼ぶ TypeScript・Python・Go のプログラム(`main.ts`、`main.py`、`main.go`)と、三つが出力するもの(`output.txt`)を持つ。kaname は入出力を持たないので、注文を持ち続けること、入力を読むこと、答えを書き出すことはホストの仕事で、ホストはその途中で kaname の関数を呼ぶ。
例のディレクトリで `ritsu gen --root . --out gen --module example.com/order_flow` と走らせると、`gen/typescript/`、`gen/python/`(パッケージ `generated`)、`gen/go/`(モジュールのパス `example.com/order_flow`)にパッケージができる(10 章)。ホストのプログラムは、その横に置く。`order_flow` の TypeScript のホスト(`host/main.ts`):
```ts
import { next, type Event, type Order } from "./glue/order_flow";
import type { USDc } from "./units";
const events: Event[] = [
{ kind: "add", line: { sku: "pen", qty: 2n, price: 150n as USDc } },
{ kind: "add", line: { sku: "ink", qty: 1n, price: 400n as USDc } },
{ kind: "place", on: "2026-10-22" },
{ kind: "pay", amount: 650n as USDc },
{ kind: "pay", amount: 700n as USDc },
{ kind: "ship", tracking: "1Z-42" },
{ kind: "cancel", reason: "too late" },
];
let order: Order = { kind: "cart", lines: [] };
for (const event of events) {
const r = next(order, event);
if (r.kind === "err") {
console.log(`${event.kind}: refused: ${r.error}`);
continue;
}
order = r.value;
console.log(`${event.kind}: ${order.kind}`);
}
```
三つのホストが出力するもの(`host/output.txt`):
```text
add: cart
add: cart
place: placed
pay: refused: the amount is not the total
pay: paid
ship: shipped
cancel: refused: the order is closed
```
- 型の対応は 10.2 のとおりである。直和型の値は、TypeScript では `kind` を持つオブジェクト、Python ではデータクラス(`flow.EventAdd(line=…)`)、Go では値の構造体(`flow.EventAdd{Line: …}`)で、ホストは `kind`、`isinstance`、型の switch で分ける。`result` は、TypeScript の `{ kind: "ok", value }`・`{ kind: "err", error }`、Python の `kaname.Ok`・`kaname.Err`、Go の `kaname.Ok[T, E]`・`kaname.Err[T, E]` である。数は TypeScript の `bigint`、お金はパッケージの単位の型(`USDc`)、日付は TypeScript の `YYYY-MM-DD` の文字列、Python の `datetime.date`、Go の `kaname.Date` である。
- 辞書と集合(17.2)は、TypeScript の `Map`・`Set` と Python の `dict` が鍵の順で返り、Python の `frozenset` と Go の `map` は順を持たないので、ホストが並べる(`sorted(…)`、`slices.Sorted(maps.Keys(…))`)。
- 範囲や件数の外の引数をホストが渡すと、生成したコードは計算を始める前に `input` で失敗する(10.4)。Go では、`CheckNext` のような関数で先に確かめられる。
ritsu の `tests/kaname.rs`(`the_hosts_of_the_examples_call_the_generated_code`)は、四つの例のそれぞれで `ritsu gen` を走らせ、TypeScript を tsc(`--strict --erasableSyntaxOnly`)で組んで Node で、Python を `mypy --strict` を通して、Go を `go vet` と gofmt を通して走らせ、三つの出力が `output.txt` と一字も違わないことを確かめる。
### 17.6 支払いの手数料と、台帳の振替(`examples/payout`)
マーケットプレイスが売り手のためにカードの支払いを受け、決済の事業者の手数料を売り手の残高から引く。規則が手数料を出し、kaname が帳簿の振替を二つ組み立てて返す。当てるのはホストである。
- 出典:事業者が公開している米国の加盟店の手数料(国内のカードで 3.49% と 49 セント、海外のカードで 1.50% を足す)。rulec のコーパスが保存しているコピー(`crates/rulec/tests/corpus/sources/paypal-us-fees.md`、2026-09-21 に取ったもの)をそのまま `rules/sources/` に置き、規則はコーパスの `paypal_fee.rule` と同じ形にした。
- ファイル:`rules/card_fee.rule`(手数料。セントで数える `money[USDc]`)、`books/ledger.book`(`unit USD scale 2`。売り手の残高は 0 を下回れない)、`payout.kn`。
```text
$ ritsu check rules/card_fee.rule books/ledger.book --root .
ok rules/card_fee.rule
books/ledger.book: ok
capture.do may be refused: key_conflict
processing_fee.do may be refused: insufficient_balance (seller_balance(seller) at least 0.00), key_conflict, already_refused
key: capture once per payment; a second call that differs only in seller or amount is refused with key_conflict
key: processing_fee once per payment; a second call that differs only in seller or fee is refused with key_conflict
ritsu check: 2 files (rulec 1, chobo 1): all pass; borders between the languages: 0 checked, 0 undecided
```
kaname が言うこと:
- 規則の入力 `amount` に渡す値は `payment.amount`(1〜100,000,000 セント)で、規則の範囲と同じなので示せる。
- 振替の額は、`payment.amount` と、規則の出力 `fee`(範囲の解析では、rulec が数える出力の範囲と同じ 49〜4,990,049 セント)で、ritsu-cross の X4 は、どちらも chobo が受け取る額だと示す。`settle` は total である。
- 拒否の理由(X4):ritsu-cross が渡す額の範囲で chobo の探索に尋ね、`capture.do` は `key_conflict` で、`processing_fee.do` は `insufficient_balance`、`key_conflict`、`already_refused` で拒否されうると答える(`capture.do` の額で起きる境界の理由は無い)。`ritsu kaname api` の `transfers` がそれを言う。生成する振替の型は、それぞれ `"key_conflict"` と `"insufficient_balance" | "key_conflict" | "already_refused"` を持つ(10.6 に実物)。
- **ドルとセントの取り違え**:ドルで数えた手数料(`money[USD]`。規則の出力を `fee : money[USD] round up(1USD)` にしたときも)を `ledger.processing_fee.do(…, fee: …)` に渡すと E102 になる。帳簿の単位 `USD scale 2` はセントで数える `money[USDc]` だからである(12 章に実物を貼った)。ホストのコードでも、ritsu 0.30.0 からは、生成パッケージの単位の型で型の検査が止める(0.1)。kaname が受け持つのは、その先の計算(丸め、範囲、規則の前提、int64)である。
- 手数料を引いた額(`payment.amount - fee`)を売り手に渡す形に書き換えると、額が負になりうる。範囲の解析の区間は −4,990,048〜99,999,951 セントで、区間では示せないので、ritsu-cross は関数のベクタの実行を尋ねる(14.2)。1 セントの支払いの手数料は 49 セントで、引いた額は −48 セントになる。その入力があるので、ritsu-cross は例を添えて E203 を出し、`total` と書いた `settle` は E401 になる(下の二つ目の出力)。支払いの金額を 100 セント以上にすると、区間は 0 を下回る(−4,989,949 から)が、どの実行も下回らないので例は無く、ritsu の W203 で、`settle` は E401 になる(12 章に実物)。
例のディレクトリの `ritsu check . --root .` の最後の 3 行。二つのモジュールが total で、境目の 8 か所(モジュールごとに、二つの額と二つの操作の拒否の理由)を確かめる:
```text
ok payout.ja.kn: 1 function, total
ok payout.kn: 1 function, total
ritsu check: 6 files (rulec 2, chobo 2, kaname 2): all pass; borders between the languages: 8 checked, 0 undecided
```
手数料を引いた額を渡す形(`examples` を外したもの。`crates/ritsu/tests/golden/cross/kaname-x4-E203.en.txt` の後半):
```text
error[kaname E401]: payout.kn:17:1: `settle` is written `total`, but it can fail with `range`
17 | fn settle(payment: Payment) -> Settlement total
= Line 21: `payment.amount - fee`, given to `amount` of the transfer `ledger.capture.do`, can be an amount chobo does not take (ritsu E203)
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
error[ritsu E203]: payout.kn:21:85: `payment.amount - fee`, given to `amount` of the transfer `ledger.capture.do`, can be an amount chobo does not take
21 | capture: ledger.capture.do(payment: payment.id, seller: payment.seller, amount: payment.amount - fee),
= For example, `settle(payment: Payment { id: "a", seller: "a", amount: 1USDc, international: true })` gives it -48.
= chobo takes an amount from 0 to 9223372036854775807 in the unit's smallest step.
= kaname's generated code checks the amount where it builds the transfer, and the function fails there with `range`.
= Narrow the amount with an `if` before the transfer, or make a negative amount a transfer the other way.
ritsu check: 3 files (rulec 1, chobo 1, kaname 1): 1 fail (2 errors); borders between the languages: 4 checked, 0 undecided
```
### 17.7 期日と、遅れた日数と、遅延の請求(`examples/late_payment`)
支払いの日を決めていない事業者間の請求書が遅れて払われたとき、売り手が請求できる定額の補償と法定の利息を出す。koyomi が期日を、規則が補償と利息を決め、kaname が期日から支払日までの日数を求めて、規則の入力の範囲に狭めてから規則を呼ぶ。
- 出典:英国の Late Payment of Commercial Debts (Interest) Act 1998 の第 4 条と第 5A 条 (2)、GOV.UK の手引き「Late commercial payments: charging interest and debt recovery」。第 4 条では、利息は relevant day の翌日から付き((2))、支払いの日を決めていなければ relevant day は「relevant 30-day period」の最後の日である((2A)(b))。その 30 日は、売り手が義務を果たした日と、買い手が債権の額を知った日(など)のうち、遅い日から始まる((2H))。第 5A 条 (2) の定額は、1,000 ポンド未満が 40 ポンド、1,000 ポンド以上 10,000 ポンド未満が 70 ポンド、10,000 ポンド以上が 100 ポンドである。手引きは、利息を 8% に基準金利を足した率で、一年の利息を 365 で割った一日の利息をペンスにしてから日数を掛けて求め、1,000 ポンド、基準金利 0.5%、50 日の例で 11.50 ポンドとする。どれも、この例として読んだもので、法令の読み方を示すものではない(ファイルの説明にそう書いた)。
- ファイル:`dates/payment_terms.cal`(期日。請求日から 29 日後)、`rules/late_payment_charges.rule`(定額の表と、一日の利息をペンスに丸めて日数を掛ける `define`。端数の向きはページに無いので半分を上げる仮置き)、`late_payment.kn`。
**決定(期日は請求日の 29 日後)**:「the period of 30 days beginning with」ある日、はその日を一日目に数えるので、30 日の最後の日はその日の 29 日後である。期日はその最後の日で、利息はその翌日から付く。遅れた日数 `paid_on - due` が、利息の付く日の数になる。設計の段階の下書きは期日を 30 日後としていて、一日遅かった。
```text
$ ritsu check dates/payment_terms.cal rules/late_payment_charges.rule --root .
ok rules/late_payment_charges.rule
dates/payment_terms.cal: ok — every date computes on all 730 days of invoiced (2026-01-01..2027-12-31); 2 examples match
ritsu check: 2 files (rulec 1, koyomi 1): all pass; borders between the languages: 0 checked, 0 undecided
```
規則の `examples` の一行目は GOV.UK の例で、rulec の評価器が 70 ポンドと 1,150 ペンスを返して通っている。`late_payment.kn` の `examples` の一行目も同じ例で、2026-03-02 の請求書が 2026-05-20 に払われたものとした。`check` が参照インタプリタで流し、koyomi の期日 2026-03-31、50 日、70 ポンド、1,150 ペンスで成り立つ(11.1 の `run` の実物)。二行目は期日の 2026-03-31 に払われた請求書で、`none` になる。
kaname が言うこと:
- 期日は koyomi が数えた 2026-01-30〜2028-01-29(口の `values` の両端と、請求日に口の `days` の 29 日を足した区間の重なり)、`paid_on - due` は −758〜1431 日、`if late >= 1` の中では 1〜1431 日で、規則の `days_late`(1〜1500)に収まる。規則の入力 `debt` と `reference_rate` も、答えのレコードのフィールド `days_late` も示せる(実物。`tests/ranges.rs`)。
- koyomi の入力 `invoiced` は、レコードの範囲が koyomi の入力の範囲と同じなので、X6 が成り立ち、`claim` は total である(0 章の説明)。例のディレクトリの `ritsu check . --root .` は、二つのモジュールの X6 の 2 か所を確かめる(「borders between the languages: 2 checked, 0 undecided」)。`invoiced` に支払日 `paid_on` を渡すと、範囲の外の日(2027-12-31 より後)を渡す実行があるので、例を添えた ritsu の E205 になる(`crates/ritsu-cross/tests/programs.rs`)。
- `if late >= 1` は逃げ道の警告にならない(7 章)。分岐の中で規則を呼び、外は `none` を答える。外を `0GBP` にすれば警告になる(7 章に実物)。
- 分岐を外して規則を呼ぶと E301 になり、例として、請求日に払われた(期日の 29 日前の)請求書の入力を添える(12 章に実物)。その形の関数のベクタには、規則が入口で受け付けない入力がある(9 章の二行目)。
- `late >= 1` の `late` は計算した値なので、その境目の前後に届く三件の入力がベクタに入る(9 章に実物)。
### 17.8 カートの重さと、Media Mail の料金(`examples/media_mail`)
カートの明細から、品物の重さに数量を掛けて足した重さと、どの品物もメディアかを求め、Media Mail の料金の規則に渡す。rulec の `from sum … of` は一つのフィールドの合計なので、明細ごとの掛け算は kaname が受け持つ(1.2)。
- 出典:米国郵便公社の料金表 Notice 123(2026-10-04 からの料金)の Media Mail の窓口の料金(1 ポンドまで 4.39 ドル、2 ポンドまで 5.13 ドル、…、15 ポンドまで 14.71 ドル)。コピーは 70 ポンドまでの 70 行の表で、規則はそのうち 15 ポンドまでを持ち、転記した行の一つずつがコピーの表を引用する。中身がどれもメディアでなければ、Media Mail の小包にならない。
- ファイル:`rules/media_mail.rule`(重さはオンス `mass[oz]` で受け取り、表の境目はポンドで書く。rulec がポンドをオンスに読む)、`media_mail.kn`。
```text
$ ritsu check rules/media_mail.rule --root .
ok rules/media_mail.rule
ritsu check: 1 file (rulec 1): all pass; borders between the languages: 0 checked, 0 undecided
```
**決定(規則の出力は一つのオプショナルな料金)**:規則は、料金を一つの出力 `price : money[USDc]? round down(1USDc)` にし、メディアでない行の答えを `none` にする。モジュールは分岐の中で `media_mail(…).price` をそのまま答える。設計の段階の下書きは、Media Mail になるか(`eligible`)と料金(ならなければ 0)の二つの出力を持ち、kaname がオプショナルな値に直していた。そのころの rulec では、オプショナルな金額の出力が検査を通らなかったからである。rulec 0.30.0 では、オプショナルな出力のセルに `none` を、出力に丸めを書ける(rulec の §15.249)ので、判断を表に戻した。
kaname が言うこと(どれも実物):
- `weight_of` は total(書いていないが推論する)。重さは 1〜96,000 オンス(明細は 1 から 30 まで、数量は 20 まで、品物は 160 オンスまで)で、int64 に収まる。最後の行は「ok media_mail.kn: 2 functions, total」である。
- `weight_of` の引数の要素の数を 1 以上にしてあるのは、関数を一つずつ解析するからである(5.4)。0 個を許すと、`weight_of` の答えは 0 からになり、`quote` の分岐の中の重さも 0 を含んで、規則の `weight`(1 オンスから)に収まると示せない(`quote` が要素を 1 以上と言っていても、`weight_of` の中では分からない)。そのとき kaname は、ベクタに例が無いので W301 を言い、`quote` に書いた `total` は E401 になる(12 章に実物)。
- `quote` は total である。`if weight <= 15lb` の中では重さが 1〜240 オンスで、規則の `weight` の範囲に収まる。`15lb` は規則の範囲の上の端と同じで、この分岐は規則の入力に狭めるためのものなので、逃げ道の警告にならない。外は `none`。
- 規則に前提は無く、日付も振替も呼ばないので、言語をまたぐ判定の場所は無い(`ritsu check` の境目は 0 か所)。規則の入力 `weight` の範囲は kaname 自身が確かめる(E301・W301)。
### 17.9 値引きを明細に按分する(`examples/discount_split`)
請求書の値引きを、明細の金額の比で配る。rulec の `allocate` を使う規則を明細ごとに一回ずつ呼び、途中までの合計を渡す。配った分の合計は、値引きにちょうど一致する。
- 出典:バージニア州の売上税と使用税の規則 23VAC10-210-250(値引き)の C と Example 3。工賃 50 ドルと部品 100 ドルの請求書から 10 ドルを値引くとき、部品の分を 100/150 × 10 = 6.67 ドルとし、工賃に 3.33 ドルを配る。
- ファイル:`rules/discount_share.rule`(rulec のコーパスの `pro_rata_allocation.rule` をセントで書いたもの。前提に `before <= upto` と `upto <= total`)、`discount_split.kn`。
```text
$ ritsu check rules/discount_share.rule --root .
ok rules/discount_share.rule
ritsu check: 1 file (rulec 1): all pass; borders between the languages: 0 checked, 0 undecided
```
規則の `examples` は、工賃を先に、部品を後に並べたときの二回の呼び出しで、333 セントと 667 セントを返して通っている。`allocate` は切り捨てで、端数は最後の明細が受け持つ(rulec の reference §8)。部品を最後にすれば、バージニア州の例の 6.67 ドルと 3.33 ドルになる。
kaname が言うこと:
- `running_totals` の要素は、金額が 1 セント以上なので、`0 <= before <= upto <= total` を持つ(3.6、5.3)。規則の呼び出しには、入力どうしの関係 `before <= upto` と `upto <= total` を残す(実物。`tests/ranges.rs`)。範囲は、`before` が 0〜90,000,000(十件のうち九件までの和)、`upto` と `total` が 1〜100,000,000 セントで、規則の入力の範囲に収まると示せる(実物)。
- 規則の前提の二つは、残した関係から成り立つ(14.3 の X2 の一つ目の段)。`split` は total である。例のディレクトリの `ritsu check . --root .` は、二つのモジュールの前提の 4 か所を確かめる(「borders between the languages: 4 checked, 0 undecided」)。
- 途中までの合計を `running_totals` を使わずに書く(`upto` を `reduce` で積み上げる)と、範囲の解析は `before` と `upto` の関係を知らない。X2 は区間の端(`before` が 90,000,000、`upto` が 0)で前提を破ると言うが、関数のベクタの実行にそういう値は現れないので、例は無く、決められない(ritsu の W201。14.3 に実物)。生成したコードは規則の入口の確かめに任せ、`split` のエフェクトは `fails rule` で、`total` と書いてあるので E401 になる。
- `before` と `upto` を入れ替えて渡すと、`before` が `upto` を超える実行(明細が 5,000,000 セントの一件)があるので、例を添えた ritsu の E201 になる(`crates/ritsu/tests/golden/cross/kaname-x2-E201.en.txt`)。
- 配った分の合計が値引きに一致することは、`examples` と、英語と日本語の版のベクタの 12 件ずつの全部で確かめている(`tests/vectors.rs`)。範囲の解析が示すことではないので、ページでもそう書く。規則の側では、rulec の Lean の `runTotal_exact` が、途中までの合計を正しく渡したときに合計が一致することを証明している。
### 17.10 日本語の版
八つの例のどれにも、日本語の名前の版がある。`order_flow.ja.kn`、`stock_diff.ja.kn`、`handles.ja.kn`、`catalog.ja.kn`、`payout.ja.kn`、`late_payment.ja.kn`、`media_mail.ja.kn`、`discount_split.ja.kn` と、内側のファイル `rules/カード手数料.rule`、`books/台帳.book`、`dates/支払期限.cal`、`rules/遅延の請求額.rule`、`rules/メディアメール.rule`、`rules/値引きの配分.rule` である。名前を日本語で書き、生成するコードに出るものに英語の版と同じ別名を付けた。モジュールと内側のファイルの別名だけは、一つのパッケージに並んで入るよう `_ja` を付けた(`order_flow_ja`、`payout_ja`、`card_fee_ja`。sekisho の例と同じ)。日本語の版の説明は、最後に「<名前>.kn の日本語の版」と書く。関数が答える文字列(`order_flow` がイベントを拒む理由、`handles` の合わない理由)は、日本語の版では日本語で書き、英語の版と同じところに同じ数だけ置く。`tests/vectors.rs` は、二つの版のベクタを、名前を別名で読み、日本語の版の文字列を同じところの英語の文字列に読み替えて比べ、答えが同じことを確かめる(16.6)。品番やテストの入力のような値の文字列は、二つの版で同じである。出典のコピーは英語の版と同じファイルを読み、出典の名前を日本語で付ける(`手数料表(fees)`、`法令(act)`、`手引き(guide)`、`料金表(usps)`、`規則(va)`)。内側の日本語のファイルも、ritsu 0.30.0 の `ritsu check` を通してある。
```text
ok rules/カード手数料.rule
books/台帳.book: ok
入金.do may be refused: key_conflict
決済手数料.do may be refused: 残高不足 (売り手の残高(売り手) at least 0.00), key_conflict, already_refused
…
dates/支払期限.cal: ok — every date computes on all 730 days of 請求日 (2026-01-01..2027-12-31); 2 examples match
ok rules/遅延の請求額.rule
ok rules/メディアメール.rule
ok rules/値引きの配分.rule
```
### 17.11 出典のコピー
**決定(規則が、横に置いたコピーを固定して引用する)**:rulec のコーパスのやり方にそろえ、例の規則は、転記した文書を `rules/sources/` に置いたコピーから `source <名前> = file "sources/<コピー>" sha256:<16 桁>` で固定し、転記した表は断片のハッシュ(`table1 sha256:…`)でも固定する(rulec の reference §3.2)。表の行と `define` と `derive` は `@<名前>` で引用する。コピーの頭には、どこから、いつ取ったかを書く。
| 例 | コピー | 元 | 取った日 |
|---|---|---|---|
| payout | `paypal-us-fees.md`(と `table1` の断片) | 事業者の加盟店の手数料のページ(rulec のコーパスのコピーをそのまま) | 2026-09-21 |
| late_payment | `late-payment-act-1998.md` | legislation.gov.uk の第 4 条と第 5A 条(Open Government Licence v3.0) | 2026-10-18 |
| late_payment | `late-commercial-payments.md`(と `table1` の断片) | GOV.UK の手引き(Open Government Licence v3.0) | 2026-10-18 |
| media_mail | `notice-123-media-mail.md`(と `table1` の断片。70 行) | 米国郵便公社の Notice 123 の Media Mail の窓口の料金 | 2026-10-18 |
| discount_split | `23vac10-210-250.md` | バージニア州の行政規則集の 23VAC10-210-250 の A〜C と Example 3 | 2026-10-18 |
日付のファイルは、koyomi が e-Gov の法令しか固定しない(koyomi の E004)ので `source` の行を持たず、規則が固定した法令のコピー(`rules/sources/late-payment-act-1998.md`)をコメントで名指す。`tests/examples.rs` が、八つの規則の固定する 10 の出典のどれも、コピーのハッシュが固定と同じで、規則がそれを引用していることと、日付のファイルの名指すコピーがあることを確かめる。規則に転記した数は、コピーを取った日に、それぞれのページで確かめたものである。
## 18. 捨てた形
ほかの章に書いたもの(4.6、10.8 など)のほかに:
- **rulec に関数とリストと再帰を足す**:rulec の検査はセルの言語の狭さの上に立っている(0.1)。計算を足せば完全性と重なりが決められなくなる。
- **dandori の式に計算を足す**:dandori の P1。dandori の範囲の解析が正確なのは、式が計算しないからである。
- **ホストの言語の型だけで守る**(生成した型を共通にするだけ):単位の取り違えは、パッケージの中で単位の型を共通にすれば、型の検査が止められる(ritsu 0.30.0 の生成パッケージの `units` のモジュール。ritsu の DESIGN 9.3)。ただ、つなぐコードの計算(丸め、範囲、規則の前提、int64)は、ホストの言語の型では確かめられない。kaname は、その計算を内側の層と同じ決まりで確かめる。
- **入出力を持つ汎用の言語にする**:入出力を持てば、同じ入力に同じ答えを返すことが崩れ、参照インタプリタとベクタで突き合わせられない。コアだけを言語にする(0.1)。
- **関数を答える関数と、関数の値をデータに入れること**:ラムダが書いた場所の外に逃げ、どこで呼ばれるかを追う解析が要る(27.1)。
- **kaname が振替を当てる**(帳簿に書く):入出力になる。いくつもの振替を一度の書き込みにしたいなら、帳簿の一つの振替に移動を並べて書く(6.3)。
- **日付の足し算**:3.4。koyomi を通らない期日の計算が書けてしまう。
- **区間だけの解析**(関係を持たない):按分の規則の前提(`before <= upto`)が決められない(5.3)。
- **区間の端を、そのまま例にする**:上からの見積もりなので、起きない値をエラーと言うことになる(5.7、6.4)。dandori の呼び出しでは、宣言した範囲の値が独立にどれもとりうるので、ritsu-cross は角を例にしてよい。kaname の区間はそうでない。
- **逃げ道をエラーにする**:つなぐコードに判断を置くことが正しい場合もある(7 章)。
- **kaname の呼び出しのために、新しい X と新しいコードを足す**:14.4。
- **規則の判定を kaname が自分で解く**(前提を kaname が区間で確かめる):ritsu の P4。規則の前提の意味は rulec が持ち、ritsu-cross が尋ねる。kaname は、渡す値について自分が知っていること(区間、関係、見た値)だけを渡す。
- **リストの要素の数を `length` で書く**:rulec の型の語 `length`(長さの次元)と同じ語が別の意味になる。`count` で書く(2.2)。
- **丸めを ritsu-units に移してから使う**:ritsu-units は単位の表と有理数を持ち、丸めは持たない。kaname が自分の丸めを持ち、rulec と同じ答えになることをテストで確かめる(3.3)。移すかはあとで決める。
## 19. まだやらないこと
どれも、作る理由が見えたら作る。黙って消えたように見せないために書く。
- **dandori のフローから kaname の関数を呼ぶこと**:total な関数だけを許す(4.5)。dandori の `use module`(仮)と、タスクとしての呼び方、ワークフローのコードの中で呼ぶか、アクティビティにするか(規則と同じ理由でアクティビティになるはず。dandori の DESIGN 4.2)を、そのとき決める。
- **ブラウザで試すページの kaname**:ブラウザで試すページは、まだ `.kn` を扱わない。そのテストが並べる拡張子は、八つの言語の固定の表である(`crates/ritsu/tests/playground.rs`)。扱うようになれば、サイトの kaname のページ(11.2)から例を開くリンクを置く。
- **koyomi のカレンダー(`use calendar`)と、日付の時刻(`at`)と時刻の型**:営業日かを尋ねることと、期日の時刻。sekisho が持つ形(`today is open in <カレンダー>`)を参考にする。
- **入力を狭めた範囲で、規則の数の出力の区間を尋ねる口**(`Rules` の `output_values` に入力の区間を渡す形):5.7 の見積もりを細かくする。sekisho の DESIGN 15 章にも同じ候補がある。
- **再帰の結果の範囲を、不動点で求めること**:5.4。
- **`run` の入力を、段の数の上限なしに読むこと**:`run` は入力を、引数の型の JSON が入れ子にできる深さまで、ただし 1,024 段までで読む(11.1)。自分を含む型の一段が JSON の 5 段を超える型(自分のリストのリストのリストのリストのリストを持つレコードなど)は、200 段の値を `run` に渡せない。読み手の再帰をやめれば上限は要らないが、受け取った値をたどる確かめ(入口、比べ、表示)も、同じ深さで再帰する。
- **総称の関数の呼び出しの輪を、型の引数の流れで細かく許すこと**:呼び出しの輪の中では、型の引数をそのまま渡すか、型の引数を含まない型を渡す(26.2。E112)。Go が受け付けないのは、型の引数から作った型を渡す呼び出しが、型の引数の流れの輪の上にあるときだけである(輪の中の別の関数が、その型の引数を自分の型の引数に戻さなければ、Go は受け付ける)。その流れを追えば、いまの決まりより多くの輪を許せる。
- **列挙の入力の範囲で、答えた呼び出しのあとに狭めること**:規則が答えたなら、渡した列挙の値は範囲に入っていた。数の範囲と同じく狭めれば、同じ名前を同じ範囲の入力に二度渡す二つ目の呼び出しも示せる(5.3)。Lean のモデルの狭め方に、値の集合で狭める形とその健全性を足すことになる。
- **式の値のあいだの関係**:範囲の解析が覚える関係は、名前とそのフィールドのあいだのものと、`count(xs)` とリストの要素の数と、リストをフィールドで足した値(`sum(xs, x => x.amount)`。`running_totals` の `total` と等しいことを含む。32 章)だけである(5.3)。`let` で名前を付けた `running_totals` の答えの `total` と、フィールドをたどるだけでないラムダで足した値は読まない(Lean のモデルも同じである。33 章)。`any(xs, p)` が成り立つときの `count(filter(xs, p))` が 1 以上であることと、一つの値を二度使う式(`side * (10 - side)`)の相関は読まない。W301 と W303 の多くはここから出る(台帳の再現の W301 と W303 がこの形)。名前を付けて `if` で狭めれば示せる。
- **関数についての性質(どの入力でも、配った分の合計が値引きになる、など)を書いて、ベクタで確かめること**:いまは `examples` の行で確かめる。
- **rulec の `enums` のファイルを、規則を通さずに `use` すること**。
- **値の型の引数をとる関数を、`examples` の行で呼ぶこと**:値を作る式の型は直和型なので(23.2)、`examples` の行で `Order.placed { … }` を値の型の引数に渡すと E102 になる。値の型の値として作る書き方が要る。ベクタと `run` は、値の型の引数に `{"placed": {…}}` を渡せる。
- **帳簿の口からページを読むこと**(`Books` に `doc` を足して、chobo のページを埋め込む):いまは振替の操作と理由を kaname のページに並べるだけである。
- **逃げ道の警告を、意図を書いて消す書き方**:7 章。
- **`fmt` と LSP**:ritsu の LSP と同じく作らない(ritsu の DESIGN 15 章)。
- **四つの例の Rust のホスト**:例のホストのプログラムは TypeScript・Python・Go のままである(17.5)。Rust から呼ぶ形は `docs/reference.md` が見せる(36.17)。
- **モジュールを Rust のライブラリにすること**(`ritsu gen --library --target rust`):ライブラリの形(ritsu の DESIGN 17.16)は npm・PyPI・Go のモジュールで書き、Rust では exit 2 で止まる。足すには三つを決める。一つ目は crates.io のクレートの名前で、モジュールの正規の書き方の連なりと別名から、crates.io が受け付ける名前を作る決まりである。二つ目は依存の版の書き方で、Cargo の版の範囲(`^`)と、ritsu の DESIGN 17.16 の「ちょうどの版」で書く決まりをどう合わせるかである。三つ目はライブラリの単位の型で、`units` をライブラリごとに持つか、共通のクレートから読むかである。単位の型が別のクレートで別の型になると、ライブラリの額とパッケージの額を同じ型で受け渡せない。
- **TypeScript・Python・Go・Rust のほかの出力先**(rulec の 12 の出力先のような)。
- **前提を破る入力を解いて、見た値に足すこと**:見た値の入力は、ベクタと、振替の額と日を渡す先の範囲の一つ外にする入力だけである(14.2)。二つの入力の関係(`before <= upto`)を破る入力は解かないので、区間の端で破る前提は、ベクタに破る実行が無ければ決められない(W201)になる。
## 20. 決めたこと
設計の段階で決めたことを、理由の書いてある節とともに並べる。★は、言語の性格にかかわる決めで、見直す余地が大きいものである。
1. ★ キーワードと書き方:見出し `module`、関数 `fn`、`use <種類> <名前> from "<パス>"`、リストの要素の数 `count`、一行の `if … then … else`、引数は名前で書く(2 章、3.7)。
2. ★ エフェクトの綴り:`total`、`fails <種類>`、`diverges`。種類は `rule`・`date`・`range`・`overflow`・`zero` の五つで、入口の外れ `input` はエフェクトに入れない(4 章)。
3. ★ 例(エラー)は、関数の実際の入力を見つけたときだけ言う。言語をまたぐ判定でも、kaname の呼び出しではそうする(5.7、6.4)。
4. ★ 関数の引数と、引数の型から届くフィールドの数と日付には範囲を、リストには要素の数の上限を、率には刻みを書く。答えにだけ使うレコードのフィールドは、範囲を省ける(3.9)。
5. ★ 逃げ道の警告の二つの形と、警告にしないもの。0 ポンドを答えにする分岐も警告にする。警告を消す印は作らない(7 章)。
6. ★ 言語をまたぐ判定のコードは、ritsu の台帳の E201〜W205 を使い、新しい X とコードを作らない(14.4)。
7. ★ 振替の拒否の理由は、kaname では扱わず、生成する振替の型に、起きうる理由として載せる。dandori のタスクのような、扱う理由の宣言を作らない(6.3、14.3)。
8. ★ 日付は差だけを持ち、足し算は持たない(3.4)。単位の換算は `in` で明示する(3.2)。
9. ★ int64 の決まりは、既約分数の分子と分母で言う(5.6)。
10. ★ 生成物はパッケージの `glue/` に置き、補助のモジュール `glue/kaname.*` を一つ持つ。単独の `kaname gen` は作らない(10.1)。
11. ★ Go の total な関数は `error` を返さず、入口の外れでは panic する。引数を確かめる `Check<関数>` を一緒に書く(4.4)。Python の引数はキーワードだけで受け取る(10.3)。
12. ★ kaname のクレートのバイナリを作り、リリースでは `kaname` という名前のリンクを配る(11.3)。
13. ★ Lean の範囲に、total と推論した関数が失敗しない定理を入れる(15.2)。
14. 口の名前は `Programs`(kaname が答える)と `Verdicts`(ritsu-cross が答える)。処理系自身の地図のコンテキストは `Programs(kaname)`(14 章、PLAN)。
15. 按分の書き方を `running_totals` に決め、範囲の解析はその関係を知っている(3.6)。
16. 関数を値にするのは、組み込みの関数の引数だけ(3.7)。
17. 列挙は宣言した場所で決まり、規則の列挙の出どころを口に足す(3.5、14.1)。
18. 自分を含むレコードは、自分を含まない値が一つは作れれば書ける(3.6、28.1)。
19. 丸めと日付の計算を kaname に持ち、rulec と koyomi と同じ答えになることをテストで確かめる(3.3、3.4)。
20. 読む順は `.proto` のあと、dandori の前(11.2)。
21. ブラウザで試すページの `.kn` は、まだ作らない(19 章)。サイトには kaname のページを置く(11.2、218)。
段階 B(構文、名前、型、`check`)で決めたこと:
22. ★ 丸めているかは、式の形でなく値の刻みで決める(rulec の §15.190 の刻み。3.2)。金額に 200% を掛けた値は、丸めずに渡せる。
23. ★ リテラルは、書いた単位の整数で書き(rulec と同じ)、比べたり足したりする相手の単位と税区分で読む(rulec から広げたもの)。単位の無い `0` は、どの単位の 0 としても読む(3.2)。
24. ★ 範囲を求めるのは、関数の引数と、引数の型から届くレコードのフィールドだけ。答えにだけ使うレコードは省ける。刻みのある率は範囲を省け、省けば 0% から 100%。E108 は範囲の無いフィールドの行に出し、届く引数とその行を注に挙げる(3.9)。
25. ★ `running[T, W]` を型として書ける(3.1、2.3)。
26. 宣言したものは、名前でも別名でも指せる。規則の列挙とその値は、規則のファイルに書いた名前か別名で指す。モジュールの `use` の名前と宣言は一つの名前空間にあり、関数の中の名前は、見えるほかの名前とも `use` の名前とも重ならない。呼び出しは関数、規則、組み込みの関数を指す(2.6)。
27. 名前は書いたとおりに比べ、NFC に正規化しない(rulec と同じ。2.6)。
28. 規則の列挙の順序は、口が渡すものを読む(3.5、14.1。54)。
29. 例の型の確かめ(E602)は、型の確かめと一緒に段階 B で出す。例を流して比べる E601 は段階 C1。
段階 C1(参照インタプリタ、`run`、`vectors`、`examples`)で決めたこと:
30. ★ 率の刻みは、関数の答えと、どのレコードのフィールドにも書く。刻みを省けるのは `let` だけである(3.1、3.9)。率は刻みの数で受け渡し、丸めてあるかも刻みで決まるため。
31. ★ `let` に書いた型は、無次元の値が数か率かと、税区分の無い金額の税区分を決める。rulec が `define` と `derive` の宣言に許していることと同じで、ほかの渡す先と、足し算と比べには許さない(3.2)。rulec のコーパスの 8 行がこれを使う。
32. ★ 関数の呼び出しの深さの上限は 200 である(8 章)。生成した Python は既定の 1,000 の深さで止まり、参照インタプリタは Rust のスタックの上で評価するため。
33. 参照インタプリタは、数をその式の型の単位で数え、検査が定数を相手の単位で読んだところと、値を渡す先で読み替える。int64 の決まりは、その単位で数えた途中の値に効く(5.6、8 章)。
34. `isqrt` に負の値を渡せば `range` の失敗である(3.7、4.2、8 章)。
35. `any`、`all`、`first` は、答えの決まった要素で止まる(3.7、8 章)。
36. 範囲を書かなかった刻みのある率は、入口と、kaname の関数を呼ぶところで、0% から 100% として確かめる。関数の答えとレコードを作るところでは、書いた範囲だけを確かめる(8 章)。
37. 規則の並びの要素のフィールドは、規則が自分の範囲で確かめる。kaname の入口と渡す先では確かめない(8 章)。
38. 日付の関数が読まない入力には、入力の範囲の下の端を渡し、先に計算される別の日付がそこで止まれば、上の端で計算し直す(8 章)。
39. ベクタは、そのまま渡す規則と日付の入力の範囲の一つ外もとる。真偽の候補は `true`、`false` の順で、リストの基本の値は要素の数の下の端の個数(0 なら 1 個)、2,000 件を超えれば止めて標準エラーに一行書く(9 章)。
40. `run` の入力は、宣言した名前でも別名でも書ける。`--format json` は `{"kaname", "file", "module", "function", "input", "calls", "result" | "failure" | "diverged"}` で、`--trace` は `lets` を足す(11.1)。
41. 例が成り立たないことは E601 で言い、答えがレコードなら、違うフィールドを注に挙げる(12 章)。
42. 値が無いことは一つで、入れ子のオプショナルな値の `none` も一つにまとめる(3.6)。
段階 C2(範囲の解析、例を探すこと、エフェクト、逃げ道の警告)で決めたこと:
43. ★ 言語をまたぐ判定の答えを待つ場所は、決められない場所としてエフェクトに数え、書いたエフェクトとの比べ(E401)には使わない。答えをまだ尋ねていないことは、示せないこととは違うため(4.3)。答えを待つのは、ritsu-cross の答えを渡さない実行(口だけをつなぐ kaname のクレートのテストなど)である(57)。
44. ★ オプショナルな値のオプショナルな値は、型でも一つのオプショナルな値にする(3.6)。
45. ★ 逃げ道の警告は、真偽を業務の定数に入れ、`match` では数のリテラルも数え、`if` の数の答えは数えない。`else if` の続きは一度だけ言う(7 章)。
46. ★ 計算した値の境目に届く入力をベクタに足す。境目の一つ前、境目、一つ後の三件を、引数を一つずつ動かして作り、入力の組として足す(引数ごとの候補には入れない)。koyomi が日付の入力のいつも同じ日数後と言う日付は、その和として読む(9 章)。
47. 呼び出しが答えたあとは、渡した名前を、その入力(引数、フィールド、`isqrt` の引数)の範囲に狭める(5.3)。
48. `count(xs)` と比べる条件は、リストの要素の数を狭める。関係を覚えた二つの名前の差は、その向きの区間に狭める(5.3)。
49. 丸めた区間は、両端を同じ向きに丸めたもの(丸めは単調)。値での割り算の割る数が 0 を含むとき、0 でない値は分母の上限 D から 1/D 以上離れているとして商を求める。int64 で確かめた値は、そのあと int64 の中にあるとして読む(5.2)。
50. `reduce` は、要素の数の上の端が 16 以下ならその回数だけ重ねる。それより多ければ三回重ねて、落ち着かなければ動いた端を外す(5.5)。
51. 値が来ない式(いつも 0 で割る式、それを引数にした呼び出し)のあとの式は解析せず、そこにある場所は数えない(5.1)。
52. 例は、ベクタと、引数の定数倍の和を解いて渡す先の範囲の一つ外にする入力から探し、参照インタプリタが見張った値で確かめる(5.7)。
53. `check` の最後の行に関数のエフェクトを、`--format json` に関数ごとの `functions` を書く(4.5、11.1)。型の確かめを通ったモジュールは、範囲の解析と `examples` がエラーを言っても、解析の答えとともに残す(`Outcome::analysed`。テストと `api` と `doc` が読む)。`run` と `vectors` は、これまでどおり検査を通るモジュールだけを流す。
段階 C3(口と ritsu-cross、中のものと参照、`api`)で決めたこと:
54. 口の `RuleEnum` に、公開名 `public`、順序 `ordered`、丸ごと読んだ元 `origin`(規則のディレクトリからたどった `enums` のファイルと、そこでの名前)を足す。埋めるのは rulec だけで、ほかに `RuleEnum` を作る口は無い。kaname は、規則の列挙を名前か公開名で指し、同じ元から来た同じ値の並びの列挙を同じ型と読み、`<` で区切った列挙を比べる(2.6、3.5、14.1)。
55. 口 `Programs` は、渡す値を kaname の型 `Given`(場所、受ける名前、書いたとおりの式、範囲の解析の範囲、出どころ、並びの要素のフィールドの範囲)で渡し、規則の呼び出しには、入力どうしの関係と、並びに渡すリストの要素の数の上限を添える。dandori の `CallArg` は使わない(14.2)。
56. 見た値(`Programs::seen`)は、区間で示せない判定があったときだけ、関数ごとに一度尋ねる。どの判定も示せる関数のベクタを `check` で流さないため(5.7、14.2)。
57. 言語をまたぐ判定の答えが来た場所は、示せればエフェクトから外し、例のある場所と決められない場所は、実行時に確かめる失敗として数えて、書いたエフェクトとの比べ(E401)にも入れる。E401 の注は、その場所を ritsu のコードで言う(4.3、12 章、14.5)。
58. ★ X2 は、kaname が呼び出しの場所で知っている入力どうしの関係が前提を言えば、成り立つとする。関係が無ければ区間で rulec に尋ね、示せなければ見た値の実行で確かめる。並びの合計と長さの前提は、kaname が知っているリストの要素の数の上限で尋ねる(14.3)。ワークフローの X2 には無い示し方である。
59. X3 の (a) と X6 の日は、koyomi の日付の答えをそのまま運んだ値なら koyomi が数えた日(kaname の範囲に入るもの)で、ほかは kaname の範囲の日で数える。100 万日を超える範囲と端の無い範囲は、決められない理由にする。範囲を `range from koyomi` にした入力は X2 が受け持つ(14.3)。
60. X4 の額は、出どころが何でも、kaname の範囲で確かめる(14.3)。
61. ★ X4 の拒否の理由は、組み立てる場所ごとに、渡す額の全部の範囲で chobo の探索に尋ね、答えがあれば成り立つと数え、無ければ W204 にする。扱う理由の確かめ(E204)は kaname には効かせない。モジュールの答えは、操作ごとに理由を合わせ、決められない場所があれば理由を言わない(6.3、11.1、14.3)。
62. ★ ritsu の台帳の E201〜W205 の文を、ワークフローと kaname の関数の両方を言うように広げ、見出しを四つ(E202、W202、E203、W203)変える。診断の見出しは、X2・X3 の (a)・X6 ではワークフローと同じ文で、X4 では振替の名前を言う。台帳の再現は、段階 C3 ではワークフローのままにし、段階 E で kaname のモジュールを足した(85、14.4)。
63. ritsu-cross はモジュールごとに一度だけ判定し、`ritsu check` は同じ答え(`ProgramVerdicts`)を kaname の検査と言語をまたぐ検査に渡す。`ritsu-project` は ritsu-cross に依存できないので、`Project::check_with` が答えを引数で受け取る(14.5)。
64. ★ ツール名 `kaname` を `Tool::ALL` に入れ、参照の書き方を `kaname "<ファイル>" function <名前>`(ほかに `record` と `field`、`enum` と `value`)にする。ツール名を並べる文(yuen の E011 など)に kaname が入る。`ritsu --help` の一覧には、段階 E で九つ目として載せた(11.2、14.6)。
65. ★ sakai は、kaname の `use rule`・`use dates`・`use module` を、相手の内側を使う参照として確かめ(境界の向こうのものは共有カーネルでだけ読める)、`use book` は確かめない。ワークフローとゲートの `use book` と同じく、振替を当てるのはホストだからである(段階 E で理由を 14.6 と sakai の DESIGN 18.2 に書いた)。
66. `Items` の定義の文は、関数は見出しから本体の最後の行まで、レコードは宣言の塊、フィールドと列挙はその行、列挙の値は名前と別名である(14.6)。
67. `api` は、名前と型が検査を通るモジュールに、範囲の解析やエフェクトがエラーを言っても答え、`{"kaname", "file", "module", "generated", "uses", "enums", "records", "functions", "transfers"}` の順に書く。生成するコードの名前は、段階 D で生成物と突き合わせる(11.1)。
68. kaname のクレートは ritsu-cross に、dev-dependency でも依存しない。本物の判定は ritsu-cross と ritsu のテストで、答えの当て方は kaname のテストで作り物の答えを渡して確かめる(16.5)。
段階 D(生成と突き合わせ)で決めたこと:
69. ★ 規則と日付の呼び出しが受け付けられないときの文は、理由を書かない「The rule `charges` refuses its inputs」「The date `terms.due` cannot be worked out」にする(日本語は「規則 `charges` が入力を受け付けません」「日付 `terms.due` が求まりません」)。理由は、参照インタプリタではトレース(`run --trace` と `vectors --trace` の呼び出し)に、生成したコードでは原因の例外に残す(10.4)。rulec と koyomi の生成物の例外の文(`campaign is out of range`)は、口の評価器の文(``the input `days_late` is outside its range``)と違い、日本語の文も無いので、理由を書けば出力先と参照インタプリタの文がそろわない。**捨てたもの**:理由を書いたまま、突き合わせで文の理由の部分を比べないこと(`KanameError` の文が出力先ごとに違ってしまう)。
70. ★ 境目(関数の引数と答え、レコードのフィールド、規則と帳簿に渡す値)では、お金と量をパッケージの `units` の型で、率を刻みの数の整数で受け渡し、率の型は作らない。関数の中では、刻みが整数の値を整数で、ほかを有理数で計算する(10.2)。**捨てたもの**:率の型を作ること(規則ごとの `Rate` のどれかを選ぶことになる)と、関数の中も境目の形で計算すること(丸めていない値を言語の整数で書けない)。
71. ★ Go の関数の名前はパスカルケースで、モジュールのレコードか列挙の型と同じ名前になるものは `Fn` を足す(`claim` の `ClaimFn`)。`api` の `generated` も同じ名前を言う(10.3、11.1)。
72. どの関数にも、入口で引数を確かめる公開する関数と、入口を通らない中の関数を書く。同じモジュールの関数どうしは中の関数を、ほかのモジュールの関数は公開する関数を呼ぶ(10.3)。
73. 組み込みの関数はその場のループにし、ラムダを出力先の関数にしない。生成した Python で kaname の呼び出しの一段が Python の一段になり、200 段の呼び出しが CPython の既定の上限の中で答える(8 章、10.3、16.4)。
74. 評価の順(8 章)を、生成したコードでも保つ。引数とフィールドを宣言の順に並べ直すとき、確かめが間に入るとき、読み替えが失敗しうるときは、書いた順に一時的な名前に入れる(10.3)。
75. 生成したコードが確かめるのは、範囲の解析が示したでない場所だけである。`ritsu gen` は `ritsu check` と同じ答え(`ProgramVerdicts`)を渡して検査し、言語をまたぐ判定で示せた場所を確かめない。モジュールが読むファイルは、プロジェクトのファイルでなければならない(10.4、10.7)。
76. `glue/` のインデックスは kaname が書き、パッケージの頭のインデックス、説明、`doc.go`、`ritsu gen --help` が `glue/` と「Functional Core(kaname のモジュール)」を言う(10.1、10.7、220)。
77. Go の当てる関数は、帳簿のクライアントのパッケージを `client`、振替の型のパッケージを `calls` という名前で読む(10.6)。
78. 出力先の突き合わせのハーネスと、ベクタと宣言の外の入力を参照インタプリタで流した答えは、`kaname::r#gen::harness` に置き、パッケージには入れない。ritsu のテストが、`ritsu gen` の生成物を同じハーネスで確かめるためである。突き合わせの道具は、kaname の `tools/` にロックファイルと一緒に置く(TypeScript 7.0.2 と `@types/node`、mypy 2.4.0、Go のモジュールの pgx。16.4)。
段階 E(doc、例、文書、九つ目の言語)で決めたこと:
79. ★ Go の引数とローカルの名前を、小文字で始まるキャメルケースにする(`paid_on` は `paidOn`)。中の関数は `_` とキャメルケース(`_weightOf`)にする。同じパッケージの rulec の Go にそろえるためである(10.3)。
80. ★ 生成したコードの中の名前がぶつかるとき(出力先の予約語、生成したコードが使う名前、モジュールのコードの名前、前に付けた名前と同じとき)、TypeScript と Python の引数は `_` を足し、それも取られていれば `_2` から番号を足す。`let` とラムダの引数と `some` の名前は `_2` から番号を足す。Go は、キャメルケースにした名前に 2 から番号を足す(`type2`、`paidOn2`)。Python のホストが呼ぶ関数は別名のキーワードで受け取り、中の関数には分けた名前で渡す。ホストが呼ぶ関数の本体は、引数の名前が隠すかもしれない組み込みの名前を使わない(要素の数の文は `kaname.itoa`)(10.3)。
81. ★ 遅延の請求の例の期日は、請求日の 29 日後にする(「the period of 30 days beginning with」ある日の、最後の日)。設計の段階の下書きの 30 日後は、一日遅かった(17.7)。
82. ★ Media Mail の例の規則は、一つのオプショナルな出力 `price : money[USDc]? round down(1USDc)` にし、メディアでない行を `none` にする(17.8)。
83. 例を `design/` から `examples/` に移し、下書きの一行目のコメントを外す。日本語の版の説明は、最後に「<名前>.kn の日本語の版」と書く(17 章、17.10)。
84. ★ 例の規則は、転記した文書のコピーを `rules/sources/` に置いて固定し、表の行と式から引用する。日付のファイルは、koyomi が e-Gov の法令しか固定しないので、規則が固定したコピーをコメントで名指す(17.11)。
85. ★ ritsu の台帳の E201〜W205 のうち、E204 と W204 のほかの八つの再現に、kaname のモジュールを足す。E204 は kaname には出ず、W204 は chobo が答えないときにだけ kaname に出る。根の README とサイトの頭のページは、E201 の再現のうち規則とワークフローの二つを見せ、そのテストはページが見せるファイルだけを置いて走らせる(14.4)。
86. ★ `doc` の Markdown のページは、規則と日付のページを `<details>` の畳んだ節として中に置く(設計の段階ではリンクにするとしていた)。HTML は外から何も読まない一つのファイルで、中のページはシート(`kn-pages`、`kn-sheet`)としてページの上に開く。帳簿のページは置かない(13 章)。
87. sakai のキーワードに、ツール名 `kaname` と種類の語 `function` を足し、W101 と E002 の注に `.kn` を足す。sakai は kaname の `use book` を数えない(ワークフローとゲートの `use book` と同じ)(14.6、sakai の DESIGN 18 章)。
88. ritsu の言語の一覧(`ritsu --help`、根の README、サイトの頭のページ、スキル、リリース)で kaname を九つ目にし、スキルを十にする。ブラウザで試すページの `.kn` は、まだ作らない(11.2、19 章)。
89. kaname のスキル(`skills/kaname/`)の `SKILL.md` は手で書き、`reference.md` と `codes.md` は `crates/kaname/skills/sync.sh` が `docs/` からコピーする(ほかの言語のスキルと同じ)。
段階 F(Lean、範囲の解析の六か所、W901、`running_totals` の重み)で決めたこと:
90. ★ Lean のモデルは型を持たないので、評価が型の約束に頼るところで確かめ、成り立たなければ、エフェクトの五つの種類に入らない `type` で止まる。四則の答えは、int64 に収めるまえに型の言う刻みの上にあることを確かめる。参照インタプリタは確かめないが、型の検査を通ったモジュールでは同じ答えになる。`mod`・`isqrt`・`allocate`・丸めの数の引数が数でなければ、モデルも参照インタプリタも、そこで止まる(8 章、15.1)。捨てたもの:型の検査をモデルに入れること。
91. 範囲の解析が上からの見積もりになっていなかった六か所を、Lean のモデルと同じ形に直す。刻みを言わない値の和は分母の上限を持たない。単位を読み替えるところでは、リストとオプショナルの中の数と、何でもありうる値も int64 で見る。`x in u` の場所は、何でもありうる値とオプショナルにも置く。何でもありうる値は、オプショナルの中でも int64 に収まると示さない。丸めた上の端は、外側の整数から丸め直す。単位を読み替える比べは、関係を覚えず、要素の数も狭めず、引き算も関係を使わない。届く四か所は `tests/modules/bounds.kn` に書いた。そのうち三か所は、直す前の形で健全性の確かめが外れを見つけ、単位を読み替える比べは、規則の呼び出しが渡す関係が変わる。残りの二か所は、型の検査を通るモジュールでは届かない(5.2、5.3、5.6、15.3)。
92. Lean の定理は、`reduce` が走る値の端を手放していない解析(どの関数の事実も `widened` が偽)と、止まった評価のほかについてだけ言う(15.4)。
93. Lean との突き合わせのために書いた四つの角(半分ちょうどの丸め、止まらないことのある関数を輪の外から呼ぶこと、三回目で落ち着く `reduce`、等しいと分かった二つの名前の差)は、`tests/modules/corners.kn` に置き、範囲の解析のテストと出力先の突き合わせにも入れる。範囲の解析のテストは、`tests/modules/` のモジュールを全部読む(16.1)。
94. ★ 言語をまたぐ判定の答えは、モデルでも解析のあとで当てる(`analyzeWith`)。答えの無い解析の定理はそのまま残し、答えを当てた解析の total は、成り立つと言った答えがそのとおりであることを前提にする(`total_with_sound`)。答えをどの場所に当てるかは、突き合わせが、Rust が当てた場所をそのまま渡す。突き合わせには、例を書き換えて答えが三つとも出る九つのモジュールを足す(15.1、15.2、15.3)。
95. 規則の呼び出しが残す関係を、モデルでも求めて Rust と突き合わせ(`callRels`)、残す関係が成り立つことと、関係の段の判定が言うことを証明する(`callRels_sound`、`kept_sound`、`kept_holds`)。kaname の呼び出しでの X2 の判定は、RitsuCross に `x2k` として書き、ritsu-cross の答えと突き合わせる(14.3、15.2、15.3)。
96. ★ 範囲の解析のテストで、実行の失敗の種類が関数のエフェクトにあることも確かめる。見た値が抽象の値に入ることだけでは、失敗しうる場所の見落としが見えないため(`bounds.kn` の `many`。16.1)。
97. W901 を kaname の `check` に足す。ほかの言語と同じ検査と文で、`.kn` の全文を、構文の誤りがあっても調べる(12 章)。
98. ★ Lean との突き合わせは、ほかのモジュールの関数を呼ぶ `calls.kn` を除く。モデルは一つのモジュールを読む(15.3、15.4)。
99. 和の分母の上限の直しは、`sum` の関数(`bounds.kn` の `thirds`)で確かめる。`running_totals` の和も同じ関数(`sums`)を通るが、重みに丸めていない値を渡す形は E103 になる(100)ので、`bounds.kn` に置けない。
100. ★ `running_totals` の重みは渡す先で、丸めた値を求める(E103)。数・お金・量の重みは単位の整数で、率の重みは重みの型が言う刻みの上にあればよく、値で割って刻みが分からなくなった率だけが丸めていない値である。丸めていない重みの `running_totals` の答えは型を持たないとして、誤りを一度だけ言う。いつも `0%` の率の重みは、生成したコードが 0.01% の刻みの数で持つ(3.2、3.6、10.2)。捨てたもの:生成したコードで、丸めていない重みのレコードを有理数のまま持つこと。
段階 G(main の 0.31.0 への乗せ直しと、規則の列挙の入力の範囲)で決めたこと:
101. 規則が順序のある列挙の入力に値で書いた範囲は、口の `ColumnType::Enum` が渡す範囲に入る値の並び(ritsu の DESIGN 3.2)で読み、数の範囲と同じく範囲の解析の場所にする(`What::RuleEnum`、鍵は `input:<名前>`)。示せなければ関数のベクタで例を探し、E301・W301、生成したコードの確かめ(`rule`)、エフェクトに入れる(6.1、10.4)。
102. ★ 範囲は `Slot` に持たせず、規則の列から読む(`names::column_values`)。`Slot` の範囲は、モジュールにも書ける数と日付の範囲で、列挙の範囲は規則にしか書けない(E108)からである。捨てたもの:`Slot` に値の並びを足し、関数の引数とフィールドにも持たせること。
103. ★ 列挙の入力の範囲では、答えた呼び出しのあとに、渡した名前を範囲に入る値に狭めない。上からの見積もりのままで、同じ名前を同じ範囲の入力にもう一度渡す二つ目の呼び出しも E301 になる(5.3、19 章)。
104. 見出しは数の範囲と同じ文にし、範囲は範囲に入る値の最初と最後で `>=basic <=gold` と書く(dandori の E014、sekisho の E206 と同じ)。直し方は、`match` か、一つの値と比べる `if` で狭めることと、規則の範囲を広げること。W301 の注には、範囲の解析が見た値を列挙の順に挙げる。台帳の E301・W301 の「いつ出るか」と直し方も、列挙の入力と `match` を言うように広げた(6.1、12 章)。
105. 試験の材料は、規則の側を rulec のコーパスの `member_tier_shipping.rule` と `会員段階の送料.rule` にし、モジュールを `tests/modules/lift.kn`(英語)と `lift.ja.kn`(日本語)にする。そのままの値(E301)、`match` と `if` で狭めた値(示す)、規則の答えの値(W301)を渡す四つの関数で、範囲に狭める分岐のもう一方は `none` を答える(`false` を答えると W501 になる。7 章)。範囲の解析のテスト、出力先の突き合わせ、Lean との突き合わせに入れる(15.3、16.1、16.4)。
106. Lean のモデルにも同じ場所を足す(`RuleInput.values`、`RuleInput.enumOut`、`What.oneOf`、`enumPlace`)。rulec が範囲に入らない列挙の値を受け付けないことを `RuleInput.refuses` に入れたので、`OracleOK` は口の `eval` のとおりになり、健全性の定理はその場所まで含む(`enumWithin_sound`、`ruleStep_wit`。15.2)。
温度を別の単位で読むときの零点(3.10)で決めたこと:
107. ★ 温度を別の単位で読むところは、どこでも零点を読み替える。生成したコードの換算(`in` と、リテラルでない定数を相手の単位で読むところ)、型の検査が `in` の式に付ける定数の値、ベクタの候補である(3.2、3.10)。
108. ★ 零点の違う単位のあいだでは、定数の刻みをその値にし、値の刻みを、係数を掛けた刻みと零点のずれがともに整数倍になる刻みにする(℃ から ℉ は 1/5、℉ から ℃ は 5/9)。刻みとずれの組は持たない(3.2、3.10)。
109. 試験の材料は `tests/modules/temps.kn`(英語)と `temps.ja.kn`(日本語)にし、範囲の解析のテスト、出力先の突き合わせ、Lean との突き合わせに入れる。値の刻みのために範囲の解析が上からの見積もりでなかった形は、`bounds.kn` の `shifted` に置く(15.3、16.1、16.4)。
段階 H1(範囲を書かない値)で決めたこと:
110. ★ 範囲(`range`)と要素の数(`count`)は、どこでも書かなくてよい。書かない値は型の全体をとる。数、お金、量はその単位で数えた int64 の全体、日付は 0001-01-01 から 9999-12-31、要素の数は 0 から 2⁶³ − 1 である。範囲の片方の端だけを書けば、書かない端は型の端である。刻みのある率は、これまでどおり 0% から 100% で、刻みの無い率は E108 である(3.9、22.1)。
111. ★ int64 のあふれの場所で、示せない理由がどれも型の端から来るなら(型の端から来たあふれ、`Outcome::Unbounded`)、診断を出さず、例を探さない。生成したコードがその場所で確かめ、関数のエフェクトに `overflow` が入る。`--format json` と `api` は `"outcome": "unbounded"` と `"code": null` を書く。書いた範囲から来るあふれと、ほかの場所は、これまでどおり例があればエラー、無ければ警告である(22.2)。
112. ★ 区間の端と既約分数の分母の上限に、型の端から来たかの印を持たせる。和は二つの端のどちらかの印、積は選んだ端の組の印(一方の端に印があり、もう一方が 0 でなければ印を持つ)、値での割り算の分母の上限は割る数の絶対値の上限の印を持つ。二つの値を合わせるときは選んだ端の印(同じ値ならどちらかの印)、分岐で狭めた端は比べた相手の端の印、int64 に寄せた端は寄せる前の印を持ち、`reduce` で外した端は印を持たない。分母の上限の印は、`5 / n` の商のように、端の値でなく分母の上限が型の端から int64 を超える場所のためにある(22.2)。
113. 呼び出しの輪の中の関数の答えは、答えに範囲を書いていなければ、型の全体に印を付けたものにする。輪の外の関数の答えは、答えに書いた範囲と型の全体で切り、切って動いた端には印を付けない(5.4、22.2)。
114. ★ 範囲を書かない関数の引数は、kaname の関数を呼ぶところでも、型の全体を宣言した引数として確かめる。範囲の解析が手放した値(`reduce` で端を外した値)を渡せば W301 で、生成したコードが確かめる。Lean との突き合わせは、引数の `Slot` の範囲と要素の数を、書かない端を型の端にして書き出す(22.2、22.6)。
115. 入口では、範囲を書かない値を型の全体で確かめる。TypeScript と Python は数が int64 に収まるかを、TypeScript と Go は日付が 0001-01-01 から 9999-12-31 の正しい日かを(補助のモジュールの `isDate` と `Date.Valid`)確かめる。文は参照インタプリタと同じで、範囲に型の全体を書く。レコードの確かめの関数(`_outside_<レコード>`、Go の `KanameOutside`)も、範囲を書かないフィールドを型の全体で確かめる(10.4、22.5)。
116. 参照インタプリタの入口と渡す先の確かめは、リストのリストの要素までたどる。生成したコードと同じ確かめにするためである(8 章、22.5)。
117. 範囲を書かない数のベクタの候補は、型の両端、0、1、−1 と、比べるリテラルと渡す先の範囲から来るもの、日付は型の両端と同じく来るものである。基本の値は、数が 0、日付が 1970-01-01、リストが要素 1 個(9 章、22.4)。
118. 出力先の突き合わせの宣言の外の入力に、数の 2⁶³ と −2⁶³ − 1(TypeScript と Python。Go の `int64` は持てない)と、日付の 10000-01-01、0000-12-31、2026-13-01(TypeScript と Go。Python の `datetime.date` は持てない)を足す。2026-13-01 は参照インタプリタが持てない日なので、10000-01-01 の答えの日をその日に書き替えたものを答えとする。ケースは流す出力先を持つ(`harness::Case::targets`)(16.4、22.4)。
119. 台帳の E108 の再現を刻みの無い率の答え(`fn half() -> rate`)に、W302 の再現を `reduce` で重ねた積に替える。輪の中の関数の答えに 1 を足すこれまでの W302 の再現は、型の端から来たあふれになって何も言わないからである。その形に `total` を書いたものを E401 の再現にして、輪の注と型の端から来たあふれの注を一つの再現で見せる。E401 の注と `doc` のページは、型の端から来たあふれの場所を「`n * 2` の値は、範囲を書かない値から来るので、int64 を超えることがあります(`overflow`)」の形で言う(22.3)。
段階 H2(直和型と `match`、一部を替えたレコード、等しさ、W501)で決めたこと:
120. ★ 直和型は `enum` を広げて書く。`enum` の行の下に値を一行に一つずつ並べ、フィールドを持つ値は、その下にレコードと同じ形でフィールドを書く。下に値を並べる形の列挙は順序を持たず、値が一つも無ければ E003 である。フィールドの名前と別名、範囲と要素の数、率の刻み(無ければ E108)は、レコードのフィールドと同じ決まりで、直和型も、自分を含まない値が一つは作れれば自分を含められる(23.1、28.1)。
121. ★ 値ごとの型(値の型 `Order.placed`)を書ける。値の型の値は直和型の場所に渡せ、逆は E102 である。値を作る式(`Order.placed { … }`、置かれた場所の型が分かれば `placed { … }`)の型は直和型で、フィールドは `match` で名前を付けた値から読む(`.` で読めば E105)。そのため、値の型の引数をとる関数は、`examples` の行で呼べない(19 章)。捨てたもの:作った式に値の型を付けること。`let o = Order.placed { … }` のあとの `match o` が、値の型を `match` で分けられないので書けなくなる(23.2)。
122. `match` は、値に名前を付ける分岐(`placed p`)、残りを全部受ける `else`、数・お金・量・日付・文字列のリテラルの分岐を持つ。名前を付けた値と `else` は、ほかの値と一つの分岐に並べられない。`else` は最後の分岐で、受ける値が残っていなければ E106 である。リテラルで分ける `match` には `else` が要り、int64 に収まらないリテラルは E104 である(23.3)。
123. 一部を替えたレコードは `{ ..x, f: v }` と書き、`..x` は `{` のすぐあとに一度だけ書ける(ほかは E001)。替えないフィールドの場所は `..x` の式に置き、範囲を書いたフィールドは、替えたものも替えないものも確かめる(23.4)。
124. ★ 作ったレコードは、フィールドのあいだの関係を持たない。関係を持つのは `running_totals` の答えの要素だけで、`{ … }` でそれを作れないからである。仕様の段階で挙げた「関係の両方のフィールドを替えないときだけ残す」は要らず、Lean で壊して落ちることを確かめる場所を「`..x` から取るフィールド」に替えた(23.7、23.10)。
125. ★ `=` と `!=` は、どの型でも中身で比べる(レコードと値の型はフィールドごと、リストは要素ごと、直和型は値とフィールド、オプショナルな値は `none` どうしか値どうし)。数・お金・量の定数は、リストとオプショナルな値の中でも相手の単位で読む。型が違えば E102 で、比べても型は狭まらない(23.5)。
126. 範囲の解析は、直和型の値を `Abs::Variants`(値の名前ごとのフィールドのレコード)で持つ。`else` は直和型・列挙・真偽を残りの値に狭め、オプショナルな値では分岐に入るかだけを決める。リテラルの分岐は、数と日付を、その分岐のリテラルを合わせた区間に狭める。直和型の等しさは、値の名前が交わらないときと、どちらもフィールドの無い一つの同じ値のときに決める。`reduce` で端を外すときは、始めに無かった値のフィールドを何でもありうる値にする(23.7、5.5)。
127. 受け渡す形は、フィールドを持たない値が値の名前、フィールドを持つ値が `{"<値の名前>": {フィールド}}` で、値の型の値も同じ形である。読むときは名前でも別名でも受け付ける。ベクタの直和型の候補は、どの値も基本のフィールドで一つずつと、フィールドを一度に一つずつ動かしたものである(23.6、23.7、9 章)。
128. ★ 生成したコードの `match` は、TypeScript と Python で `if` の並び(`kind` と `isinstance` で分け、最後の分岐を条件なしで受ける)、Go で型の switch(`default` で panic)にする。仕様の段階で挙げた TypeScript の `switch (o.kind)` と、来ない値のための補助の関数 `kaname.unreachable` は置かない。上から順に最初の分岐を選ぶ参照インタプリタの順が、そのまま `if` の並びになり、リテラルと `else` も同じ形で書ける。来る値を全部受けることは、kaname が E106 で確かめている(23.8)。
129. ★ 等しさの生成:TypeScript は補助の `kaname.eq`(整数、真偽、文字列、日付、フィールドの無い列挙の値と、そのオプショナルな値は `===`)。Python は `==` で、直和型の値だけを補助の `kaname.eq` で比べる(`isinstance` で狭めた二つの値を、`mypy --strict` が `==` で比べさせないため。仕様の段階では `==` としていた)。Go は型ごとに生成する比べの関数で、名前は `_eq1` からの番号にする(リストやオプショナルな値の型には、名前に入れる型の名前が無いため)(23.8)。
130. 直和型の値の型の、生成したコードの名前は、列挙と値の別名をパスカルケースでつなげたもの(`OrderPlaced`)で、モジュールのレコード・列挙・ほかの値の型の名前と同じになれば E006 である。TypeScript が値を見分けるフィールド `kind` は、フィールドの名前にできない(E007)。フィールドを持たない値も、直和型の中では `{ kind: "reopen" }` のオブジェクト、フィールドの無いデータクラス、フィールドの無い構造体にする(23.8)。
131. ★ W501 の `match` は、お金・量・率・日付と、それだけでできたレコード(フィールドを持つ値を含み、`..x` から作るものを除く)を答えるときだけ警告する。列挙の値、真偽、数を答える `match` は警告にしない。列挙の値を別の列挙の同じ名前の値に対応づける `match` を外す決まりは、要らなくなったので消した(7 章、23.9)。
132. `api` の列挙は、直和型なら値ごとにフィールドの宣言(`fields`)と、生成したコードの値の型の名前(`generated`)を持つ。フィールドを持つ値の無い列挙は、これまでの形のままである。口の `Items` は、下に値を並べる列挙をその行の塊で、フィールドを持つ値を値の行とフィールドの行の塊で返す。参照の書き方に、値の下のフィールド(`kaname "orders.kn" enum Order value placed field on`)を足した(ritsu-base の `naming`。sakai の誤りの文も、三つ目の組の種類を言う)(11.1、14.6)。
133. Lean のモデルでは、フィールドを持つ値が `Abs.variants` に入ることを、値の名前があり、フィールドが一つ以上あり、どのフィールドもその値の宣言にあって抽象の値に入ることとする(`Mem.variant`)。同じ値の名前の二つのレコードを合わせるときは、フィールドの名前の和をとる(型の検査を通ったモジュールでは、どちらも同じフィールドを持つ)。`..x` の値に、作る型のフィールドが無ければ、モデルは `type` で止まる(23.10、15.1)。
零より下の単位の定数(3.10)で決めたこと:
134. 定数のすぐ前の `-` は、どの単位でも零より下の定数として読む。型の検査が定数の型にし、参照インタプリタ・範囲の解析・ベクタ・生成は、`-` と数のリテラルを一つのリテラルとして読む。温度と音量の値の `-` は E105 のままである。捨てたもの:字句で一つの定数として読むこと、温度と音量の値に `-` を許すこと(3.10)。
段階 H3(辞書と集合、リストの組み込みの関数)で決めたこと:
135. ★ 型の語 `dict`・`set`・`entry`・`indexed` は、すぐあとに `[` があるときだけ型の語として読む。`set` という名前のレコード、フィールド、引数も、これまでどおり書ける。名前にできない語は増やさない(2.2、24.2)。
136. 鍵にできる型は、数、お金、量、刻みのある率、日付、文字列、真偽と、フィールドを持たない列挙(規則の列挙も)で、ほかは E114 である。並べ替えと `max_by`・`min_by` の鍵には、鍵にできる型のフィールドだけのレコードも書け、フィールドを宣言の順に比べる。二つの鍵が同じかは等しさ(23.5)で決める(24.1)。
137. ★ 辞書の鍵と値、集合の要素、`distinct` が見分ける要素、`to_dict` と `group_by` の答えの鍵と値、`indexed` の `item` は、丸めた値でなければならない(E103)。生成したコードがこれらを受け渡す形で持ち、Map の鍵を値で比べるからで、レコードのフィールドと同じ決まりである。並べ替えと `max_by`・`min_by` の鍵は、比べるだけなので丸めていない値でもよい(24.1、24.6)。
138. ★ 3.7 の組み込みの関数は関数の形とメソッドの形のどちらでも書け、24.3 で足した関数はメソッドの形だけ、`numbers` は関数の形だけで書く。違う形は E101 で、注に書き方を出す。モジュールの関数の名前とぶつかる(E006)のは関数の形で書く組み込みの関数だけで、メソッドの形だけの名前(`get`、`sort_by`)は、モジュールの関数に付けてよい。どちらを呼ぶかは書き方で決まるからである(2.2、24.3)。
139. `xs.last(x => 条件)` は、リストの終わりから要素を見て、条件を満たす最初の要素で止まり、残りの要素に関数を当てない(`first` と同じ)。`first()` と `last()` は、関数を取らない形も持つ(8 章、24.3)。
140. ★ `mod` の割る数は、定数でも値でもよい整数である(ほかは E109)。余りは 0 以上、割る数の絶対値より小さいユークリッドの形で、割る数が正の定数なら rulec の `mod`(床の余り)と同じ答えである。0 をとりうる割る数は `zero` の場所である。生成したコードは、言語の `%` を使わず、補助のモジュールの関数で計算する(2.4、24.3、24.6)。
141. ★ `numbers(a, b)` は a から b まで(両端を含む)で、b が a より小さければ空のリストである。要素の数が int64 を超えれば `overflow` の場所で、1,000,000 を超えれば、参照インタプリタは深すぎる呼び出しと同じく評価を止める(`diverged`。生成したコードとは突き合わせない)。`numbers` という名前のモジュールの関数は E006 になる(24.3)。
142. 辞書のリテラルは、組ごとに鍵、値の順で左から評価し、それぞれをその場で辞書の鍵と値の型で読む(生成したコードと同じ順)。定数の鍵を二度書けば E107、値の鍵で同じ鍵が二度来れば、あとの組が勝つ。`[:]` は、置かれた場所の型が辞書の型を言うところにだけ書ける(`let d = [:]` は E102)(24.2、24.4)。
143. 受け渡す形は、辞書が鍵の順の `[[鍵, 値], …]`、集合が鍵の順の配列である。`run` と `vectors` の入力は鍵の順でなくてよく、同じ鍵を二度書けば `input` の失敗である。ベクタの候補は、リストと同じ数の互いに違う鍵の値と、一つ目の組の値と鍵(集合は一つ目の要素)を候補で動かしたものである(9 章、24.4)。
144. ★ 生成したコードは、辞書と集合をどこでも受け渡す形(Map・dict・map の鍵と値は境目の型)で持ち、回すところでは、作った順に頼らず、いつも鍵の順に並べてから回す。TypeScript の Map と Python の dict は鍵の順に作るので、ホストが受け取るものも鍵の順で回る(24.6)。
145. ★ 生成した Go で、直和型のオプショナルな値を答えるところが、値の型の値を `kaname.Some(OrderPlaced{…})` と包んでいて、`*Order` を答える関数で型が合わなかった(23 章の材料に、直和型のオプショナルな値を答える関数が無かった)。`kaname.Some` の型の引数に、包む型(`Order`)を書くように直した(`dicts.kn` の `change`)(10.2、23.8)。
146. ★ 範囲の解析の 24.5 の形のうち、モデルと証明を書く中で上からの見積もりでないと分かったところを、Rust とモデルの両方で直した。二つの下の端の小さいほう(どちらかが無ければ端を持たない)、`flatten` と `flat_map` の数の下の端(0 より下の端を 0 として掛ける)、何でもありうる値を `none` になりうるものにするところ(何でもありうる値のまま)、`has` と `contains` の引数(鍵と要素の型で読んだ値)、空のリストどうしの `concat`、辞書の鍵の単位の読み替え、辞書と集合の要素の範囲を渡す先で確かめること(リストと同じく中まで)。参照インタプリタの辞書のリテラルの評価の順も、生成したコードと同じ順(140)に直した(15.3)。
147. ★ Lean のモデルは、要素・組・要素の数が 2⁶³ − 1 を超えるリスト・集合・辞書を作るところで評価を止める(`sized`。`diverged`)。メモリが尽きることを言語の意味の外に置く(24.3)のと同じで、参照インタプリタはそういう値を作れない。`group_by` は、要素がそれを超えるリストで止まる(グループの数の区間を int64 で切るため)。モデルは型の約束(`at`・`take`・`drop`・`slice`・`numbers`・`mod` の整数、`concat`・`flatten`・`flat_map` のリスト)を `type` の失敗で確かめ、辞書と集合の値に、鍵と要素が一つずつであることを求める(`Mem.dict`、`Mem.members`)。どれも、型の検査を通ったモジュールで参照インタプリタと答えが違うことは無い(15.1、15.3)。
148. 試験の材料は `tests/modules/dicts.kn`・`dicts.ja.kn`(34 の関数)と `lists2.kn`・`lists2.ja.kn`(24 の関数)である。範囲の解析の健全性、出力先の突き合わせ、Lean との突き合わせに入れる。`dicts.kn` の `overlay` は、値の範囲の違う二つの辞書を `merge` で合わせる関数で、`merge` の値の合わせ方を壊したときに突き合わせが落ちるように足した(24.7)。
段階 H4(文字列)で決めたこと:
149. ★ 文字列は、Unicode のスカラー値(コードポイント)の並びである。長さ、切り出し、位置、大小を、どれもコードポイントで決め、正規化はしない。大小は鍵の順と同じ一つの比べで、文字列の大小と `min`・`max` を E105 にする決まりを外した(25.1、25.2)。
150. 空白は Unicode の White_Space の 25 文字、大文字と小文字は ASCII の文字だけである。参照インタプリタ、四つの出力先の補助のモジュール、Lean のモデルが同じ表を持つ(25.3)。
151. `parse_number` と `parse_date` は、kaname が決めた一つの形だけを読み、読めなければ `none` を答える。答えの区間は型の全体で、端に型の端の印を付ける(25.4、25.6)。
152. 文字列のエスケープは入れない。書けない文字は `char(n)` で書き、`n` はスカラー値を書いた整数の定数に限る(ほかは E109)。値の数からは `to_text` で作る。`char` という名前のモジュールの関数は E006 である(25.2)。
153. 足した文字列の関数は、メソッドの形だけで書く。`slice`・`take`・`drop`・`contains` はリストと同じ名前で、`.` の前の値の型で分ける。どれも失敗しない。`pad_start` と `pad_end` が 1,000,000 コードポイントより長く伸ばすときは、`numbers` と同じく評価を止める(25.2、25.5)。
154. 文字列の引数とフィールドに `count`(コードポイントの数)を書ける。生成したコードの入口は、ホストから入る文字列が正しい文字列か(孤立したサロゲート、正しくない UTF-8)と、書いた `count` を確かめ、外れれば `input` で失敗する。`count` を書かない文字列も、正しい文字列かは確かめる(25.1、25.7)。
155. ★ 文字列を渡す先で確かめるのは、書いた `count` だけである。書かない文字列は型の端を確かめない。そこを超える文字列は作れないからである(25.6)。
156. 範囲の解析は、文字列をコードポイントの数の区間(`Abs::Str(Iv)`)で持ち、25.6 の表で関数ごとに求める。`count` との比べは、リストと同じく長さを狭める(5.3、25.6)。
157. ★ 出力先の言語が違って読む 15 の文字列は、文字列の組み込みの関数に渡す引数と、大小を比べる引数のベクタにだけ足す。これまでの例とテストのモジュールのベクタは変わらない。宣言の外の入力に、`count` の一つ外の文字列と、出力先ごとの正しくない文字列を足し、参照インタプリタの答えは `input` の失敗と決めて比べる(25.8)。
158. Lean のモデルは、文字列の関数を `Text.lean` の `List Char` の上の定義で持ち、型の約束(`join` と `concat` の文字列、`text` の値)を `type` で確かめる。2⁶³ − 1 より長い文字列を作るところと分けるところで止まる(`diverged`)。組み込みの関数をまとめた `primAbs_sound` と、`concat`・`text` の場合は `SoundText.lean` に移した(25.9)。
159. ★ `join` の長さの下の端は、要素と区切りの長さの下の端も 0 より下を 0 として読む。証明を書く中で、上からの見積もりにならない区間(要素の数の下の端が 0 と 1 のあいだの分数で、区切りの長さの下の端が 0 より下)が見つかったので、Rust とモデルの両方で直した。いまの材料の答えは変わらない(25.9、15.3)。
160. 生成した Go の辞書の確かめで、値に確かめが無いときは、値の変数を宣言しない。文字列の鍵(正しい文字列かを確かめる)と確かめの無い値の辞書で、使わない変数を `go vet` が止めた(24.6、25.7)。
161. 試験の材料は `tests/modules/texts.kn` と `texts.ja.kn`(29 の関数ずつ)である。範囲の解析の健全性、出力先の突き合わせ、Lean との突き合わせに入れる(25.9)。
出力の無い規則の `use rule` と、フィールドの無い値の E101 の注(2.7)で決めたこと:
162. ★ 出力の無い規則(rulec の `enums` のファイル)は、`use rule` で読め、その列挙を `<規則>.<列挙>` と書いて型として使える。その規則を呼ぶところと、出力を型に書くところを E201 にする。生成したコードは規則の型のモジュールを読むだけで、呼ぶためのコードは要らないからである。置かれた場所の型がもう誤りと言われたところの名前は、どの列挙の値かが分からないので E101 を言わない(2.5、2.7)。
163. E101 の注は、フィールドが一つも無ければ、そのことを言う文にする(2.7)。
段階 H5(ジェネリクスと `result`)で決めたこと:
164. ★ 型の引数は、関数・レコード・列挙の名前のあとの角括弧に書く。制約は `key`(鍵にできる型)の一つだけである。型の引数の名前は型の名前空間にあり、一つの宣言のほかの型の引数、モジュールのレコードと列挙と重なれば E006 である(26.1)。
165. ★ 呼ぶ側では型の引数を書かず、定数でない引数を左から、次に定数を、次に置かれた場所の型で決める。決まらないか、二つの型が当たれば E112、丸めていない値は E103 である。決まらないときは、関数の名前のあとに書ける(`f[number](…)`)(26.2)。
166. ★ 値の無いことがある型(`number?`)は、型の引数にできない(E112)。仕様の段階には無かった決まりである。総称の関数の中の `T?` が二つの `none` を持つことになり、値を包まない参照インタプリタと、`*T` を重ねる生成した Go で答えが分かれる(`T` に `number?` を当てた `first_or` に `[none, 1]` を渡すと、参照インタプリタは `xs.first()` を値の無いこととして `fallback` を答え、Go の `kaname.First` は `none` を指す値を返して `none` を答える)。値の無いことがあるところは `T?` と書く(26.1)。
167. ★ 型の引数を持つ関数は、ホストが呼ばない中の関数である。入口の確かめもベクタも持たず、`run` は流さずに exit 2 で、`vectors --function` は注を出す。生成したコードは、入口の無い一つの関数を外に見せる(TypeScript の `export function _first_or`、Python の `_first_or`、Go の `FirstOr`)(21.3、26.4、26.7)。
168. ★ 失敗を値で扱う組み込みの型 `result[T, E]`(値は `ok` と `err`)と、`let x = try e` を入れた。`try` は関数の答えに続くブロックの `let` にだけ書け、ほかは E115 である。型の検査のあとで `match` に読み替え、読み替えた `err` の名前は `try`(名前にできない語)にする。`fails` のエフェクトは変えず、失敗を受け止める書き方は作らない(26.3)。
169. ★ 型の引数を持つ関数は、呼ぶところごとに、呼び出しが渡す値で本体を読み直す。入れ子は 4 段までで、それを越えた呼び出しと輪の中の呼び出しは、一度だけの解析の答えを使う。読み直しの答えは宣言した答えと重ね、読み直しで見つけた場所と呼び出しは、呼んだ関数の `rplaces` と `rcalls` に入れて、その呼び出しのエフェクトを決める。生成したコードが確かめる場所は、一度だけの解析で決まる(26.5、26.6)。
170. ★ 生成したコードで比べ方を渡すのは、`key` と書いた型の引数の順だけで、本体が順を使うかによらず、いつも渡す。仕様の段階では、型の引数ごとに等しさと順を渡し、渡す型の引数を呼び出しをたどって決めるとしていた。等しさは、TypeScript の `kaname.eq` と Python の `==` が中身で比べ(23.8)、Go も補助のモジュールの `kaname.Equal`(リフレクションで中身を比べる)で同じにした。渡す形にすると、総称の型ごとに総称の比べの関数を生成し、呼び出しをたどることになる。生成した関数の引数は宣言だけで決まり、本体を書き替えても変わらない(26.7)。
171. 生成したコードの型の引数の名前は別名で、モジュールのコードの名前、出力先の語、型の名前と重なれば `_`(Go は番号)を付ける。E006 にはしない。Python は型の変数をモジュールに一度宣言し、同じ別名の型の引数は宣言のあいだで共有する(26.7)。
172. Python の総称のデータクラスと `kaname.Ok`・`kaname.Err` は、作るところで型の引数を書く(`Page[str](…)`)。mypy が `Ok(value=[])` の型を決めないためである。`isinstance` は型の引数の無いクラスで分ける。TypeScript と Go の総称の関数の呼び出しは、型の引数をいつも書く(Go は答えの型から決めない)。Go の鍵に限った型の引数は `comparable`(辞書と集合の鍵にするため)で、直和型の印のメソッドは型の引数を引数に持つ(26.7)。
173. 総称のレコードと列挙に具体的な型を当てた型を、ホストの関数が受け取るところは、その型ごとの確かめの関数で確かめる(宣言ごとの確かめは、総称の宣言には作らない)。`result` の値は、`ok` と `err` の中の値を確かめ、パスは `r.ok.qty` である。参照インタプリタの入口も、`result` の中の値を確かめるようにした(前は確かめず、範囲の解析が宣言の範囲を前提にするのと合わなかった)(26.4、26.7)。
174. `api` は総称の宣言にだけ `type_params`(名前、別名、`key`)を、型の引数を持つ関数にだけ `"inner": true` を足す。`check --format json` も型の引数を持つ関数に `"inner": true` を足す。ほかの宣言の形と、ほかのモジュールの出力は変えない。`doc` のページは、型の引数を持つ関数に一文を足す(11.1、13 章)。
175. ★ Lean のモデルの読み直しは、`Fn.reread` と `ACtx.depth` で書き、読み直しの場所と呼び出しを、呼んだ関数の場所と呼び出しにそのまま入れる。健全性は `reread_case` と、`sound_all` の強い帰納法(`sound_upto`)で示し、`total_sound` は変えずに通る。仕様の段階で挙げた `Pat.payload` は足さず、書き出しが `ok x` と `err e` を、値に名前を付けるパターンとフィールドを読む式に読み替える。モデルの `match` とフィールドの定理がそのまま効き、新しいパターンの定理が要らないからである。鍵に限った型の引数の値の順は、モジュールとその規則の列挙の値をつないだ一つの順で書き出す(26.9)。
176. 試験の材料は `tests/modules/generic.kn` と `generic.ja.kn`(40 の関数ずつ。`index_by` と `lines_by_sku` は 27 章の関数の値を使う)、`results.kn` と `results.ja.kn`(13 の関数ずつ)、`glib.kn` と `gcalls.kn`(ほかのモジュールの総称の関数を呼ぶこと。英語だけ)である。範囲の解析の健全性、出力先の突き合わせ、Lean との突き合わせ(`gcalls.kn` は除く)に入れる(26.10)。
177. 鍵に限った型の引数のフィールドを持つレコード(`Pair[K]`、`indexed[K]`)も、ほかのフィールドが鍵にできれば、並べ替えと `max_by`・`min_by` の鍵になる。その順を、Go はモジュールに一度だけ作る関数で書き、使うところに書かない。gofmt が式の中の関数の本体を行に分け、生成したコードが gofmt を通らなくなるからである(26.2、26.7)。
段階 H6(関数の値と `private`)で決めたこと:
178. ★ 関数の値は、渡すことと呼ぶことにだけ使う。関数と組み込みの関数の関数の型の引数、`let` の値(`if`・`match`・ブロックの答えとして選んだものも)、呼ばれる位置にだけ置ける。ほかは E113 で、型の引数に関数の型が当たるところは E112 で言う(27.1)。
179. ★ 渡した関数のエフェクトは、渡した側に伝わる。関数の型にエフェクトを書いた引数を呼ぶところは、そのエフェクトを自身のエフェクトに入れ、書かない引数を呼ぶところは入れない(`through`)。渡したラムダの本体の場所と、名前で渡した関数のエフェクトは、渡した側のエフェクトに入る。関数の型に書いたエフェクトに収まらない関数を渡せば E402 である(27.5)。
180. 関数の値の呼び出し(ラムダも、名前で渡した関数も)は、呼び出しの深さに一段として数える。組み込みの関数にその場で書いたラムダと、名前で渡した関数の値を呼ぶ組み込みの関数のラムダは、これまでどおり段を使わない(27.3)。
181. ★ 名前で渡した関数は、型の検査のあとで、それを呼ぶラムダに読み替える(`src/wrapped.rs`)。組み込みの関数に名前で渡した関数の値も、組み込みの関数のラムダにする。仕様の段階の `Value::Fn` と Lean の `Val.fnRef` は置かない。値の形が一つで済み、範囲の解析も Lean のモデルも、名前で渡した関数を特別に扱わずに済むからである(27.3、27.8)。
182. ★ 関数の型は、ほかの型の中(リストの要素、関数の型の引数と答え、オプショナルな値)に書けない(E113)。そのため、関数を取る関数は値にできない(E113)。関数の型の引数と、関数の値を名前に付けた `let` は、モジュールの関数と同じ名前にできない(E006。`f(…)` がどちらを呼ぶかが読めなくなるため)(27.1)。
183. 関数の値は、引数を名前を付けずに順に渡して呼ぶ(E107)。ラムダの引数の型は、置かれた場所の関数の型から決まり、決まらないところでは書く(E112)。総称の関数に渡すラムダは、ほかの引数が型の引数を決めたあとで読む(27.1、27.2)。
184. 関数の型の引数を持つ関数は、中の関数で、読み直す関数である。一度だけの解析では、関数の型の引数を見えない関数として、その呼び出しに答えの型の全体を答えさせる。読み直さない呼び出し(4 段を越えたものと、輪の中のもの)は、渡したラムダをその引数の型の全体で一度読み、宣言した答えを答える(27.4)。
185. ラムダの本体の場所は、どこで読んでも、それを書いた関数の場所にする。ほかの関数の読み直しの中で読んだラムダは、書いた関数の解析の枠に渡し、本体の式の値は分けて持って、関数の解析の終わりに合わせる(27.4)。
186. ★ Go の関数の型は `func(T) U` にし、関数の値の失敗も、ほかの失敗と同じく panic で運び、ホストが呼ぶ関数の入口で `error` にする。仕様の段階の `func(T) (U, error)` にはしない。関数の値を呼ぶたびに `error` を確かめる文が要り、ほかの呼び出しと失敗の運び方が分かれるからである(27.6)。
187. Python のラムダは入れ子の `def` に、TypeScript のラムダは文の塊のアロー関数に、Go のラムダは関数のリテラルにする。範囲の確かめと確かめつきの算術の文を、本体に書けるようにするためである(27.6)。
188. Go の `kaname.Equal` は、関数の値を受け取らない(関数の値は、等しさにも、データにも、型の引数にも入らない)ので、比べ方を変えない(27.6)。
189. ★ `private` のレコードと列挙は、`private` でない関数の引数と答えにも、`private` でないレコードと列挙の値のフィールドにも書けない(E108)。仕様の段階では、ホストが呼ぶ関数の引数と答えだけだった。ほかのモジュールから使える宣言が、そのモジュールで作れない型を名指すことも防ぐ(28.2)。
190. `private` の生成したコードは、TypeScript が `export` を付けず、Python と Go が名前を `_` で始める(Go の `_placeOf` は、ほかのパッケージから読めない)。`api` は `"private": true` を足し、`doc` のページは、このモジュールの関数だけが呼ぶと書く(28.2)。
191. ★ Lean の定理は、見えない関数を呼んだ解析を `widened` と同じに外し、関数そのものの答えと場所(`FnSound`、`ResultsOK`)を、関数を取らない関数について言う。`total_sound` は、エフェクトが total で `through` の無い関数について言う。関数を取る関数の振る舞いは渡される関数で決まり、呼ぶ側の読み直しを通して確かめるからである(27.8)。
192. `=` と `!=` の両辺が関数の値なら、左の一つだけを E113 で言う。呼ぶ値がもう間違っていると言った呼び出しは、引数のラムダについて重ねて言わない(27.7)。
193. `doc` のページは、関数を取る関数に、渡した関数のエフェクトが呼ぶ側に伝わる引数を名指して書き、どの引数にもエフェクトを書いていれば、渡す関数がそれに収まると書く(27.5)。
194. 試験の材料は `tests/modules/hof.kn` と `hof.ja.kn`(32 の関数ずつ)で、名前で渡す失敗しうる関数(`doubled`)も入れる。読み直しの中のラムダの呼び出しを数えないモデルを、突き合わせが見つけられるようにするためである(27.8、27.9)。
195. 台帳の E109 の再現を、組み込みの関数を値にしたもの(`xs.map(isqrt)`)に替える。これまでの再現 `let twice = x => x * 2` は、ラムダの引数の型が決まらない E112 になる(27.7)。
段階 H7(再帰する型)で決めたこと:
196. ★ レコードと直和型は、自分を含んでよい。自分を含まない値が一つも作れない型だけを E108 にする。作れるかは、型の輪(強連結成分)ごとに、作れると分かった型から広げて求め、輪の外の型は作れるものとして数える(28.1)。
197. ★ 総称の型の輪の中では、型の引数に、型の引数そのものか、型の引数を含まない型だけを渡せる。型の引数から作った型を渡せば E108 である。仕様に無かった決まりで、型が限りなく増える形を、生成した Go が「instantiation cycle」で通らなくなる前に、検査で止める(28.1)。
198. ホストから入る値の入れ子の深さは 200 段まで。引数から一つの道をたどり、自分を含む型の値を一つ通るたびに一段と数え(互いを含む二つの型は、どちらの値も一段)、入口で、範囲の確かめの前に数える。超えれば `input` の失敗で、文は深すぎる型の名前を言う(8 章、28.1)。
199. ★ kaname の関数を呼ぶところでは、自分を含む型の値の中を確かめない。そのフィールドは、作るところか入口で確かめてある。仕様に無かった決めで、呼ぶたびに値の全体をたどると、木をたどる再帰の関数の手間が値の大きさの二乗になるからである。範囲を書かない刻みのある率を 0% から 100% と読む決まりは、自分を含む型の値の中では前提にしない(5.1、28.1)。
200. ★ 範囲の解析は、自分を含む型の値を、一つの道の上で同じ型の値を二つまでたどり、その先を何も分からない値にする(`UNFOLD`)。仕様は「いまと同じく型を 8 段までたどる」だったが、自分を含む型を 8 段までたどると、`tests/modules/trees.kn` の `check` に 95 秒と 17 GiB のメモリがかかった(28.1)。
201. ベクタは、自分を含む型の基本の値を自分を含まない値にし、候補は、一つの道の上で同じ型の値を二つまで持つ。基本の値が `none` の `T?` に値を置いた候補は、理由を「値がある」と書く(9 章、28.1)。
202. 生成したコードは、ホストが呼ぶ関数の入口で、自分を含む型の値を持ちうる引数ごとに、型ごとの深さの関数で数えてから、入口だけの確かめ(`_enter_<型>`、Go の `KanameEnter`)でどの深さのフィールドも見る。呼ぶところの確かめ(`_outside_<型>`、`KanameOutside`)は、自分を含む型には書かない(10.4、28.1)。
203. 出力先の突き合わせに、自分を含む型の引数ごとの 200 段と 201 段の値を足す。答えが止まらないものは流さずに数え、200 段の値をそのまま答えるものも比べる。突き合わせの行は、ベクタ、宣言の外の入力、入れ子の値を分けて数える(16.4)。
204. Lean のモデルは、自分を含む型ごとの場所を表(`Module.slots`)に一度だけ持ち、場所は表の番号でその型を指す(`Slot.ref`)。入口(`Slot.enterD`)は表をたどって深さを数え、呼ぶところの確かめ(`Slot.outsideD`)とそれを示す判定(`keepsD`)は、`ref` の場所の中を見ない。健全性の定理の前提と範囲は変えず、自分を含む型の引数を持つ関数も、その中にある(15.1、15.2)。
205. 試験の材料は `tests/modules/trees.kn` と `trees.ja.kn`(23 の関数ずつ)で、仕様の例の `Tree[T]`、互いを含む `Doc` と `Member`、`next : Node?` でつながるリスト、100% を超える率を作って渡す `wide_shares` を入れる(28.1)。
206. 台帳の E108 の文(いつ出るかと直し方)に、自分を含まない値が作れない型と、型の引数から作った型を渡す型を足す。再現は刻みの無い率の答えのまま変えない(12 章)。
207. ★ `run` は入力の JSON を、引数の型の値が自分を含む型を 201 段まで入れ子にしたときの深さまで読む。ritsu の読み手の 256 段より浅くは読まず、1,024 段より深くは読まない。ritsu-base の `parse` の深さは変えず、深さを渡して読む `parse_nested` を足す(28.1)。
段階 H8(例と文書、総称の関数の呼び出しの輪、`run` の入力の深さ)で決めたこと:
208. ★ 総称の関数の呼び出しの輪の中では、型の引数をそのまま(入れ替えてもよい)渡すか、型の引数を含まない型を渡す。型の引数から作った型を渡せば E112 である。自分を含む総称の型の決まり(197)と同じ決まりで、生成した Go が「instantiation cycle」で組めない形を検査で止める。言うところは呼び出しなので、E108 でなく、呼び出しの型の引数を言う E112 にした。Go が止める形だけを止める、型の引数の流れを追う決まりは、19 章に残す(26.2)。
209. 輪は、名前で渡した関数も呼び出しに数える(27.5 と同じ)。呼び出しごとに一つだけ言い、注に書き方を書く。台帳の E112 の文に足す。材料は `tests/modules/generic.kn` と `generic.ja.kn` の `swap_down`・`settle_down`・`rings`(四つの出力先が組めることを確かめる)と、`tests/types.rs`、`tests/codes.rs` の英日の再現である(26.2)。
210. 値を読む関数(`from_json`)と入口の確かめ(`outside`)を、値の種類ごとの関数に分ける(ふるまいは同じ)。再帰する一段ごとのスタックが小さくなり、デバッグのビルドで、`Category` の 200 段の値を読んで確かめるのに使うスタックが、6 MiB 余りから 3 MiB 以下になる(28.1)。
211. 突き合わせのハーネスは、出力先の答えの行を、参照インタプリタの答えの JSON の深さまで読む(`harness::compare` が `json::parse_nested` で読む。ritsu の読み手の 256 段より浅くは読まない)。200 段の値をそのまま答える関数(`trees.kn` の `named` と `simplify`、`catalog.kn` の `pruned`)の答えも、そうして比べる(16.4、28.1)。
212. ★ kaname は「Functional Core を書くための小さな汎用の言語」で、README、DESIGN の頭と 0・1 章、リファレンス、スキル、サイトの説明は、この姿で書く。規則・日付・帳簿を ritsu の型のまま呼べることは、汎用の言語の強みの一つとして書く。
213. ★ 例は八つにする。汎用のコアの四つ(`order_flow`、`stock_diff`、`handles`、`catalog`)を先に、規則・日付・帳簿を呼ぶ四つをあとに並べる(17 章)。仕様の作業の割り付け(`day_plan`)の代わりに、木の変換(`catalog`。再帰する型と関数の値)を入れた。汎用の四つは、範囲を書く値と書かない値を混ぜ、`catalog` のほかは total、`catalog` は木をたどる再帰で `diverges` である。
214. ★ 汎用の四つの例は、生成したコードを呼ぶ TypeScript・Python・Go のホストのプログラム(`host/`)と、三つが出力するもの(`output.txt`)を持つ。ritsu の `tests/kaname.rs` が、`ritsu gen` のパッケージの横に置いて走らせる(17.5)。
215. 例の日本語の版は、関数が答える文字列を日本語で書く。双子の突き合わせは、二つの版の関数が書いた文字列を、同じところどうしで読み替えて比べる(16.6、17.10)。
216. 17 章を、汎用の四つ(17.1〜17.4)、ホストから呼ぶこと(17.5)、規則・日付・帳簿を呼ぶ四つ(17.6〜17.9)、日本語の版(17.10)、出典のコピー(17.11)の順にする。
217. 入れないもの(29 章)と rulec との関係(30 章)を置き、まだやらないこと(19 章)と分ける。19 章から、済んだ `run` の入力の深さを外し、`run` の読む深さの上限と、総称の関数の輪の細かい決まりを足す。
218. ★ サイトの kaname のページは、ritsu のサイトのページとして `kaname/` の下に五つ置き(kaname が何か、書き方、Functional Core の四つの例、検査が保証すること、規則・日付・帳簿を呼ぶこと)、sekisho のページと同じく ritsu のナビゲーションに並べる。chobo のようにサイトを分けない。ホストの三つの言語はタブ(`pymdownx.tabbed`)に並べ、一つのタブで選んだ言語は、ページのほかのタブの組でも選ばれる(`content.tabs.link`)。日本語のページはモジュールを日本語の版で見せ、ホストとその出力は英語の版のものを見せる(11.2)。
219. 根の README の kaname の節は、Functional Core の例 `order_flow`(直和型と `match`、`ritsu check`、TypeScript のホストとその出力)を先に、規則・日付・帳簿を呼ぶ `late_payment` をあとに見せ、サイトの kaname のページへリンクする。日本語の README は、ほかの言語の節と同じく英語の版を見せる。ホストの行と出力は ritsu の `tests/readme.rs` が確かめる(16.1)。
220. ★ 生成パッケージの説明(`index.ts`、`package.json`、`__init__.py`、`pyproject.toml`)、`doc.go`、`ritsu gen --help`、`package.rs` の頭のコメントは、kaname のモジュールを「Functional Core(kaname のモジュール)」と呼ぶ。規則・日付・帳簿をつなぐコードに限らない汎用の言語だからである。置き場のディレクトリ `glue/` の名前は変えない。ホストのコードはこのパスでモジュールを読み、変えればホストのビルドが壊れる。生成し直したパッケージは、この四つの説明と `doc.go` の文だけが変わる(10.7)。
## 21. 汎用の言語としての芯
### 21.1 範囲
21〜28 章は、汎用のコア(17.1〜17.4 の形)を書くための型と式の決まりである。値に範囲を書かなくてよく(22 章)、状態を直和型の値で持ち(23 章)、辞書と集合で数え(24 章)、文字列を扱い(25 章)、型の引数と `result` で部品を書き(26 章)、関数を渡し(27 章)、木の形のデータを持てる(28 章)。どの決まりも、規則・日付・帳簿と同じ型と検査の決まりのまま書けるようにし、規則・日付・帳簿を呼ぶ計算(6 章)も同じ型と式で書く。入出力を持たないことは変えない(P1)。時刻、乱数、ファイル、ネットワーク、書き換えられる状態はホストの側に残り、kaname の関数はホストから値を受け取って値を返す。
| 章 | 決まり |
|---|---|
| 22 | 範囲を書かない値 |
| 23 | 直和型(フィールドを持つ列挙の値)、`match` の `else` とリテラル、一部を替えたレコード、等しさ |
| 24 | 辞書と集合、鍵にできる型と鍵の順、リストの組み込みの関数、組み込みの関数のメソッドの形、`numbers` と値で割る `mod` |
| 25 | 文字列(コードポイントの並び、文字列の組み込みの関数) |
| 26 | ジェネリクス(型の引数と `key`)、呼ぶところでの読み直し、`result` と `try` |
| 27 | 関数の値(関数の型、ラムダ、名前で渡す関数、関数の値の呼び出しとそのエフェクト) |
| 28 | 再帰する型(自分を含むレコードと直和型、ホストから入る値の入れ子の深さ)と `private` |
入れないものとその理由は 29 章、rulec との関係は 30 章にある。
### 21.2 五つの保証の守り方
どの決まりも、次の五つを崩さない形にしてある。
| 保証 | 守り方 |
|---|---|
| 入出力を持たない | 範囲を書かない値も、ホストから受け取る値である。直和型の値を分けて作ること、一部を替えたレコード、等しさも、値から値を計算するだけである。辞書と集合も値で、どの組み込みの関数も新しい値を答え、渡したものを変えない(24.2)。型の引数を持つ関数は中の関数で、ホストは呼ばない(21.3)。`result` も値で、`try` は関数の答えを決めるだけである(26.3) |
| 参照インタプリタと四つの出力先が同じ答えを返す | 範囲を書かない値の入口の確かめ(数は int64、日付は 0001-01-01 から 9999-12-31)を、参照インタプリタと四つの出力先が同じ文で言う。int64 の外の数と、その外の日付を、出力先の突き合わせの宣言の外の入力に入れる(16.4、22.4、22.5)。直和型の値の受け渡す形を一つに決め(23.6)、中身での等しさは補助の関数と生成した比べの関数(Rust は `derive(PartialEq)`)で書き、言語の比べ(参照や番地の比べ)に頼らない(23.8)。辞書と集合は、作った順に頼らず鍵の順で回し、鍵の順は補助のモジュールの一つの比べで書く(Rust は、鍵の順と同じ `Ord` で並ぶ `BTreeMap` と `BTreeSet`。36.2)。並べ替えは四つの出力先とも安定な手順で、`mod` は言語の `%` を使わず補助の関数で計算する(24.6)。総称の関数の鍵に限った型の引数の順は、呼ぶ側が具体的な型の順を渡し(Rust は `Ord` の境界)、型の引数の値の等しさは、補助の関数が中身で比べる(Rust は `PartialEq`。26.7)。自分を含む型の値の入れ子の深さを、参照インタプリタと四つの出力先が入口で同じ文で言い、200 段と 201 段の値を出力先の突き合わせに入れる(28.1、16.4) |
| 範囲の解析が健全で、Lean のモデルがそれを示す | 範囲を書かない値は、型の全体の区間から始める。区間の端の印は、int64 のあふれの場所の言い方だけを変え、場所が示されたかどうかも抽象の値も変えないので、モデルの意味は変わらない(22.2、22.6)。直和型の値と抽象の値、`match` の狭め方、作るところ、等しさの答えも `KanameCore` にあり、健全性の定理はそれを含む(23.10)。辞書と集合の値と抽象の値、24.5 の表もモデルにあり、組み込みの関数ごとの補題で健全性を示す(24.7)。呼ぶところでの読み直しは、その呼び出しの値の上からの見積もりで本体を解析し直すだけなので、健全性はそのまま言え、モデルで示す(`reread_case`。26.9)。自分を含む型の値は、同じ型を二つまでたどって、その先を何も分からない値にする。呼ぶところでその中を確かめないことは、モデルの確かめ(`Slot.outsideD`)と判定(`keepsD`)が表を指す場所の中を見ないことで同じに言うので、定理の範囲は変わらない(28.1、15.1) |
| エフェクトを推し量れる | 型の端から来たあふれも `overflow` の場所で、関数のエフェクトに入る。範囲を書いたフィールドを持つ値を作るところは、レコードと同じ `range` の場所である。辞書・集合・リストの組み込みの関数は失敗せず、場所を持つのは `numbers` の要素の数と値で割る `mod` だけである。読み直した呼び出しのエフェクトは、読み直しの場所と呼び出しで決まる。自分を含む型をたどる関数は再帰を持つので `diverges` で、入れ子の深さの外れは入口の `input` である。失敗の種類は増やさない(22.3、23.10、24.7、26.6、28.1) |
| 規則・日付・帳簿を ritsu の型のまま呼べる | 範囲を書かない値を規則と日付の入力に渡せば、規則と日付の入力の範囲で確かめる(E301・W301 と、言語をまたぐ判定。22.2)。直和型の値と、辞書と集合、`result` は、規則・日付・振替に渡せない(E102)。渡すには、`match` で分けるか、リストや値に直す |
### 21.3 ホストが呼ぶ関数と中の関数
**決定(ホストが呼ぶ関数は、具体的な型だけを持つ)**:関数は二つに分かれる。
- **ホストが呼ぶ関数**:型の引数も関数の型の引数も持たず、`private` でない関数。引数と答えに、総称のレコードや列挙に具体的な型を当てた型(`Page[Line]`)と `result` を書ける。生成したコードは、入口で引数を確かめる公開する関数と、入口を通らない中の関数の二つを書き(10.3)、ベクタ(9 章)を作る。
- **中の関数**:型の引数を持つ関数、関数の型の引数を持つ関数(27.2)、`private` の関数(28.2)。モジュールの中と、`use module` で読むほかのモジュールから(`private` の関数はモジュールの中だけから)呼ぶ。入口の確かめもベクタも持たず、呼ぶ側の関数のベクタと解析を通して確かめる。`run` は流さずに exit 2 で終わり、`vectors --function` は注を出す(26.4)。
生成したコードでは、中の関数も、ほかのモジュールの生成物から呼ぶので外に見せる(TypeScript の `export function _first_or`、Python のモジュールの `_first_or`、Go の `FirstOr`、Rust の `pub fn _first_or`)。`private` の関数は見せない(28.2)。ドキュメントのコメントに「kaname の生成したコードが呼ぶ関数で、入口で引数を確かめない」と書く(26.7)。`api` と `check --format json` は、中の関数に `"inner": true` を書く(11.1)。
理由:型の引数を持つ関数は、入口でどの範囲を確かめるかも、ベクタにどの値を選ぶかも型から決まらない。関数を取る関数も、渡される関数が値として入口を通らない(27.1)。具体的な型の関数だけをホストに見せれば、入口の確かめもベクタも、引数の型から作れる。
## 22. 範囲を書かない値
### 22.1 書き方と意味
範囲(`range`)と要素の数(`count`)は、どこでも書かなくてよい。関数の引数と、引数から届くレコードのフィールドも同じである。書かなければ、その値は型の全体をとる。
| 型 | `range`・`count` を書かないときにとる値 |
|---|---|
| `number`、お金、量 | その単位で数えた int64 の全体(−9,223,372,036,854,775,808 から 9,223,372,036,854,775,807) |
| 刻みのある率 | 0% から 100%(rulec の入力と同じ。3.9) |
| `date` | 0001-01-01 から 9999-12-31(`YYYY-MM-DD` で書ける日の全部) |
| リスト | 要素の数が 0 から 2⁶³ − 1 |
| 文字列 | コードポイントの数が 0 から 2⁶³ − 1(25.1) |
範囲の片方の端だけを書けば(`range >=1`)、書かない端は型の端である。
```kn
fn next_id(last: number) -> number
last + 1
```
刻みの無い率は E108 である。率は刻みの数で受け渡すので、刻みが無いと受け渡す形が決まらない(3.9)。
**決定(範囲を書かない値は、型の全体)**:書かない範囲を「何でもよい」として、範囲の解析の前提から外す形は捨てた。生成したコードの値は、どの出力先でも int64 に収まり、日付は 0001 年から 9999 年の日である(入口でそう確かめる。22.5)。型の全体と読めば、範囲の解析は、書いた範囲と同じ決まりで値を追える。
### 22.2 範囲の解析:型の端の印
範囲の解析の区間(`Iv`)は、端ごとに、その端が**型の端**(範囲を書かない値がとる、型の全体の端)から来たかの印を持つ(`Iv::open_lo`、`open_hi`)。既約分数の分母の上限(5.1)も、同じ印を持つ(`open_den`)。
- 範囲を書かない引数とフィールドは、型の全体の区間から始め、両方の端に印を付ける。片方の端だけを書いた値は、書かない端に印を付ける。
- 演算の答えの端は、その端を作った端に印があれば、印を持つ。`a + b` の上の端は、`a` と `b` の上の端のどちらかに印があれば印を持つ。掛け算は、選んだ端の組で決める(一方の端に印があり、もう一方の端が 0 でなければ印を持つ)。値での割り算の答えの分母の上限は、割る数の絶対値の上限が印のある端から来れば、印を持つ。
- 二つの区間を合わせる(`if` の二つの答え、`match` の分岐、`min` と `max`)ときは、選んだ端の印を持ち、同じ値の端なら、どちらかの印を持つ。
- 分岐で狭めた端は、比べた相手の端の印を持つ。`if n >= 0` の中の下の端 0 のように定数と比べて狭めた端には印が無く、`if n < m` の中の `n` の上の端は、`m` の上の端の印を持つ。狭めても動かない端は、自分の印のままである。
- int64 で確かめた演算のあと(5.2)、int64 の端に寄せた端は、寄せる前の印を持つ。そこにある値は、寄せる前の値から来るからである。
- 呼び出しの輪の中の関数の答え(5.4)は、答えに範囲を書いていなければ、型の全体に印を付けたものにする。輪の外の関数の答えは、答えに書いた範囲と型の全体で切り、切って動いた端には印を付けない。範囲の解析が求めた端は、自分の印のままである。
- `reduce` で動いた端を外したとき(5.5)は、印の無い、端の無い区間にする。範囲の解析が追うのをやめたところで、型の端ではないからである。
int64 のあふれの場所(5.6)が示せないとき、示せない理由がどれも印から来るなら、その場所の結果は**型の端から来たあふれ**(`Outcome::Unbounded`)である。理由になるのは、値の無い端、分母の上限を掛けると int64 を超える端、int64 を超えるか分からない分母の上限である。端には端か分母の上限の印が、分母の上限にはその印が要る。
- 診断は出さない。生成したコードがその場所で確かめ、関数のエフェクトに `overflow` が入る。
- 例を探さない。範囲を書かない値が型の端をとる入力は、いつもベクタにある(22.4)ので、探せば例がいつも見つかり、エラーになってしまう。
- 印の無い端が int64 を超える場所は、例があれば E302、無ければ W302 である。
あふれのほかの場所(規則と日付の整数の入力、関数の引数と答え、フィールド、`isqrt`、0 で割ること)と、言語をまたぐ確かめ(X2、X3 の (a)、X4、X6)は、印を見ない。範囲を書かない値を規則の入力に渡せば、型の端の値を渡す入力が例になり、E301 である。範囲を書かない関数の引数は、kaname の関数を呼ぶところでも、型の全体を宣言した引数として確かめる。値はどれも int64 に収まるので、その場所で失敗することは無いが、範囲の解析が手放した値(`reduce` で端を外した値)を渡せば、示せずに W301 になり、生成したコードが確かめる(`tests/modules/open.kn` の `next_of`)。
```kn
fn next_id(last: number) -> number
last + 1 # 型の端から来たあふれ:何も言わず、生成したコードが確かめる(fails overflow)
fn per_month(total: number, months: number range >=1 <=12) -> number
down(total / months, 1) # 0 で割らないとは示せる。商は型の端から来たあふれ(fails overflow)
fn per(total: number, n: number) -> number
down(total / n, 1) # n が 0 の入力を例にした E303。商は型の端から来たあふれ
```
`per` の割り算の商は、分母の上限が `n` の型の端(2⁶³)から来る。分母の上限に印を持たせるのは、このような場所のためである。割られる数が定数の `5 / n` は、商の端(−5 と 5)に印が無く、分母の上限の印だけが、型の端から来たあふれであることを言う。
同じ二つの関数の引数に int64 の全体を範囲に書くと、`total / months` は W302(商の既約分数の分子の見積もりが、端の値と分母の上限の積なので int64 を超える)、`total / n` は E302(例 `per(total: 9223372036854775807, n: -9223372036854775808)`。商の分母が 2⁶³ になる)と E303 になる。範囲を書かない形では、二つのあふれの場所は印から来るので何も言わず、E303 は残る(実物。`ritsu kaname check`):
```text
error[E303]: example.kn:13:8: The divisor of `total / n` can be 0
13 | down(total / n, 1)
= For example, `per(total: -9223372036854775808, n: 0)` divides by 0.
= Leave 0 out with an `if` before the division, or out of the divisor's range.
```
**決定(型の端から来たあふれは、エラーにしない)**:int64 の全体を範囲に書いた引数では、`last + 1` は例つきの E302 である(`next_id(last: 9223372036854775807)`)。書いた範囲は約束で、約束の中の入力で失敗するのはプログラムの誤りだからである。範囲を書かないことは、約束をしないことである。その値が int64 を超えるのは、どの出力先の整数にもある限りで、関数は `overflow` で失敗しうると型に出せば足りる。
**捨てたもの**:
- 型の端から来たあふれにも W302 を出すこと。範囲を書かない整数を足し引きするたびに警告になり、ほかの警告が読めなくなる。どこで確かめるかは、エフェクトと、`check --format json` の場所の一覧と、`doc` のページが言う。
- 範囲を書かない整数のために、`number` とは別の型(`int`)を作ること。rulec の型の語に無い型が一つ増え、同じ整数に、範囲を書いたかどうかで二つの型ができる。
- 端だけに印を持ち、分母の上限に印を持たないこと。`5 / n` の場所が、例のある E302 になる(ベクタの `n: -9223372036854775808` で商の分母が 2⁶³ になる)。範囲を書かない値で割る場所が、どれもエラーになる。
### 22.3 エフェクトと診断
- 型の端から来たあふれの場所は、エフェクトの `overflow` に数える。見出しに `total` と書いた関数は E401 で、注は、場所の行と文と、診断のコードの代わりの種類 `overflow` である(実物):
```text
error[E401]: example.kn:6:1: `doubled` is written `total`, but it can fail with `overflow`
6 | fn doubled(n: number) -> number total
= Line 7: the value of `n * 2` can leave int64, as it comes from a value whose range is not written (`overflow`)
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
```
```text
エラー[E401]: example.kn:6:1: 関数 `doubled` には `total` と書いてありますが、`overflow` で失敗することがあります
6 | fn doubled(n: number) -> number total
= 7 行目: `n * 2` の値は、範囲を書かない値から来るので、int64 を超えることがあります(`overflow`)
= 書いたエフェクトに収まるよう、失敗しうる場所を示せるようにする(渡す値を `if` で範囲に狭める)か、エフェクトを広げて書いてください。
```
- `check --format json` と `api` の場所は `"outcome": "unbounded"` と書き、`"code"` は `null` である(4.5。実物):
```json
{"function":"next_id","effect":{"fails":["overflow"],"diverges":false},"written":null,"places":[{"line":4,"kind":"overflow","check":"kaname","code":null,"outcome":"unbounded"}],"calls":[]}
```
- `doc` のページは、実行時に確かめる関数の場所の一覧に、同じ文と、生成したコードが確かめることを書く(13 章。`tests/golden/doc/rec.en.md`):
```text
- Line 3 (`overflow`): The value of `countdown(n: n - 1) + 1` can leave int64, as it comes from a value whose range is not written. The generated code checks it there when it runs.
```
- E108 は、範囲と要素の数が無いことは言わない。E108 が言うのは、刻みの無い率、空の範囲、二度書いた端、数と日付のほかに書いた `range`、リストのほかに書いた `count`、自分を含まない値が作れない、自分を含む型である(12 章、28.1)。引数が届くリストのリストも、要素の数を書かずに受け取れる。台帳の E108 の再現は、刻みの無い率の答え(`fn half() -> rate`)である。
呼び出しの輪の中の関数の答え(範囲を書かないもの)に 1 を足す場所(`tests/modules/rec.kn` と `corners.kn` の `countdown(n: n - 1) + 1`)は、型の端から来たあふれで、警告を出さない。エフェクトは `fails overflow diverges` である。台帳の W302 の再現は、`reduce` で重ねた積(`fn product(xs: list[number] count <=20 range >=1 <=9) -> number`)で、E401 の再現は、輪の答えに 1 を足す形に `total` を書いたもの(`fn steps(n: number range >=0 <=100) -> number total`)である。E401 の再現は、輪の注と型の端から来たあふれの注を一つで見せる。
### 22.4 ベクタと例を探すこと
範囲を書かない値の候補(9 章)は、型の両端と、比べるリテラルとその刻み一つ前後、そのまま渡す規則と日付の入力の範囲の両端とその一つ外、計算した値の境目である。
- 数、お金、量:−2⁶³、2⁶³ − 1、0、1、−1(その単位で数える)。範囲の片方の端だけを書いた値は、書かない端の側の型の端。
- 日付:0001-01-01、9999-12-31。
- リスト:要素の数 0、1、2、3(上の端は使わない)。
- 基本の値は、数なら 0、日付なら 1970-01-01、要素の数なら 1 である(9 章の決まりのとおり)。型の全体の真ん中は、ほかの候補と組み合わせるたびにあふれるからである。
候補を選んだ理由は「型の下の端」「型の上の端」と書く(実物。`ritsu kaname vectors`):
```json
{"function":"next_id","in":{"last":-9223372036854775808},"out":-9223372036854775807,"why":"last: the lower end of the type"}
{"function":"next_id","in":{"last":9223372036854775807},"fails":{"kind":"overflow","module":"example","line":4,"message":"The value of `last + 1` does not fit in int64"},"why":"last: the upper end of the type"}
```
宣言の外の入力(16.4)には、型の端の一つ外を足す。数は 2⁶³ と −2⁶³ − 1 で、TypeScript と Python で流す(Go の `int64` と Rust の `i64` はその値を持てない)。日付は 10000-01-01 と 0000-12-31 と、正しくない日 2026-13-01 で、TypeScript と Go と Rust で流す(Python の `datetime.date` はそういう日を作れない。Go の `Date` と Rust の `kaname::Date` は年と月と日の整数なので持てる)。参照インタプリタは 2026-13-01 を値として持てないので、10000-01-01 の答え(`input` の失敗)の日を 2026-13-01 に書き替えたものを答えとする。ケースをどの出力先で流すかは、ケースが持つ出力先の一覧で決める(`harness::Case::targets`)。
### 22.5 生成したコード
- 入口:範囲を書かない数は、TypeScript の `bigint` と Python の `int` で int64 に収まるかを確かめ、外れれば `input` で失敗する。Go の `int64` と Rust の `i64` は確かめるまでもない。範囲の書かない端のある日付は、TypeScript の文字列が `YYYY-MM-DD` の 0001-01-01 から 9999-12-31 の正しい日であること(補助のモジュールの `kaname.isDate`)、Go の `Date` と Rust の `kaname::Date` が同じく正しい日であること(`Date.Valid`、`valid()`)を確かめる。Python の `datetime.date` は、作った時点で正しい日である。
- 文は参照インタプリタの入口と同じで、範囲に型の全体を書く。`tests/modules/open.kn` から生成した TypeScript の確かめ:
```ts
function _outside1(v: bigint, what: string): kaname.Outside | null {
if (v < (-9223372036854775808n) || v > 9223372036854775807n) {
return { path: what, shown: kaname.numText(kaname.q(v), ""), range: ">=-9223372036854775808 <=9223372036854775807", count: null };
}
return null;
}
```
Rust では、範囲を書かない数だけを受け取る関数の確かめは何も見ず、日付の確かめは `valid()` を見る(同じモジュールの `next_id` の確かめと、`days` と `since` が日付を確かめる関数。実物):
```rust
pub fn check_next_id(_: i64) -> Result<(), kaname::KanameError> {
Ok(())
}
```
```rust
fn _outside1(v: kaname::Date, what: &str) -> Option<kaname::Outside> {
if !v.valid() {
return Some(kaname::outside(what, v.to_string(), ">=0001-01-01 <=9999-12-31"));
}
None
}
```
範囲の両端を書いた日付も、暦にある日かを確かめる。日の番号だけを両端と比べると、2026-02-30 は 2026-03-02 の番号なので、両端のあいだに入るからである。TypeScript の文字列は、日付の形でなくても両端のあいだに入りうる(`2026-0x-yy`)。TypeScript と Go は、端を書かない日付と同じ `kaname.isDate` と `Date.Valid` で確かめる。Rust は `on_calendar()` で確かめる(年は両端の中にある)。`examples/late_payment` の `paid_on` の確かめ(実物。TypeScript と Go):
```ts
function _outside9(v: string, what: string): kaname.Outside | null {
if (!kaname.isDate(v) || (v < "2026-01-01" || v > "2029-12-31")) {
return { path: what, shown: v, range: ">=2026-01-01 <=2029-12-31", count: null };
}
return null;
}
```
```go
func _outside7(v kaname.Date, what string) *kaname.Outside {
if !v.Valid() || (v.Days() < 20454 || v.Days() > 21914) {
return &kaname.Outside{Path: what, Shown: v.String(), Range: ">=2026-01-01 <=2029-12-31", Count: -1}
}
return nil
}
```
Rust の、`tests/modules/calc.kn` の `days` の確かめ(実物):
```rust
fn _outside11(v: kaname::Date, what: &str) -> Option<kaname::Outside> {
if !v.on_calendar() || (v.days() < 20454 || v.days() > 20818) {
return Some(kaname::outside(what, v.to_string(), ">=2026-01-01 <=2026-12-31"));
}
None
}
```
- レコードの確かめの関数(TypeScript と Python の `_outside_<レコード>`、Go の `KanameOutside`、Rust の `kaname_outside` のメソッド)は、引数が届いたレコードのフィールドを確かめる関数なので、範囲を書かないフィールドも型の全体で確かめる。答えにだけ使うレコードのこの関数も同じ形になる(`examples/late_payment` の `Claim` の `due`、`compensation`、`interest`)。
- リストのリストは、内側のリストの要素も確かめる。参照インタプリタの確かめも、同じに入れ子をたどる(8 章)。
- 場所:示せなかったあふれは、確かめつきで計算する(10.4)。型の端から来たあふれも同じである。
### 22.6 Lean
モデルの意味は変えない。`crates/ritsu-model/tests/kaname.rs` は、引数の宣言(`Slot`)の範囲と要素の数を、書かない端を型の端にして書き出す。モデルの入口の確かめと引数の場所の確かめが、参照インタプリタと範囲の解析と同じ範囲を読むためである。引数とフィールドの始まりの抽象の値と、答えを切る宣言の抽象の値も、型の全体で書き出す。端の印は書き出さず、Rust の区間とモデルの区間は印を外して比べる。印はあふれの場所の言い方だけを変え、場所が示されたかどうかと、範囲の解析の健全性には関わらないからである。
突き合わせの材料に `tests/modules/open.kn` と `open.ja.kn` が入る。引数の `Slot` を書いた範囲のまま(型の全体にせず)書き出すと、`open.kn` の `next_of` の引数の場所(Rust は示せず、モデルは示せる)とエフェクトの `range` で、突き合わせが落ちる。
### 22.7 テスト
- `tests/modules/open.kn`(と `open.ja.kn`):範囲を書かない数・お金・日付・リスト・リストのリスト・文字列の引数とフィールド。型の端から来たあふれ(`next_id`、`total`、`cents`、`cells`、`steps`。何も言わず、エフェクトに入る)、印の無い端のあふれ(`doubled` の E302、`product` の W302)、規則の入力に渡す範囲を書かない値(`discount` の E301)、`if` で狭めて示せる場所(`halved`、`narrowed`)、範囲を書かない答えの輪の関数(`steps`)、範囲の解析が手放した値を範囲を書かない引数に渡す呼び出し(`next_of` の W301)。
- `tests/ranges.rs`:見た値が抽象の値に入ること、実行の失敗がエフェクトの言う種類であること(16.1)を、`open.kn` でも確かめる。端の印は、印の付いた端と同じ値をとる実行があったか(型の端の値をとるベクタ)を数えて確かめる。例とテストのモジュールの式の抽象の値の、印のある端 646 のうち 200 を、実行がとる(ほかは、`+ 1` のあとの端のように、どの入力でもとらない端と、失敗した実行の先の端である)。
- `tests/effects.rs`:型の端から来たあふれが `overflow` に数えられ、呼ぶ関数に伝わり、`total` と書けば E401 になって注にその場所が出ること。分岐で狭めれば示せること。int64 の全体を範囲に書いた引数では E302 になること。
- 出力先の突き合わせ(16.4):`open.kn` の全部のベクタ(型の端の値で、あふれて失敗するものを含む)と、宣言の外の入力(22.4)。
- 台帳(`tests/codes.rs`):E108 の再現は刻みの無い率の答え、W302 の再現は `reduce` で重ねた積、E401 の再現は答えの範囲を書かない再帰で、E401 の golden に、輪の注と型の端から来たあふれの注が出る。`tests/names.rs` は、範囲と要素の数を書かない引数が通ること、刻みの無い率が E108 になることを確かめる。
## 23. 直和型と `match`、一部を替えたレコード、等しさ
### 23.1 フィールドを持つ値
列挙の値の下に、レコードと同じ形でフィールドを書けば、その値はフィールドを持つ。フィールドを持つ値が一つでもある列挙が**直和型**である(`tests/modules/sums.kn`)。
```kn
enum Order
draft
items : list[Item] count <=20
placed
items : list[Item] count >=1 <=20
on : date range >=2026-01-01 <=2027-12-31
paid
items : list[Item] count >=1 <=20
paid : money[USDc] range >=1USDc <=2_000_000USDc
cancelled
reason : string
```
- 書き方は二つある。一行の形(`enum tier = basic < silver < gold`)は、フィールドを持てない。下に値を並べる形は、`enum` の行の下に字下げして値を一行に一つずつ書き、フィールドを持つ値は、その下にさらに字下げしてフィールドを書く。フィールドを持たない値も並べてよい(`Event` の `reopen`)。どの値もフィールドを持たなければ、`|` で区切った一行の列挙と同じものである。`enum` の行の下に値が一つも無ければ E003。
- 値の名前と別名、表示名(`paid "Paid"`)は、一行の形の値と同じである。フィールドの名前と別名、範囲と要素の数、率の刻みは、レコードのフィールドと同じで(3.6、3.9)、範囲を書かなければ型の全体として読む(22 章)。
- 下に値を並べる形の列挙には順序が無い。`<` で区切れるのは一行の形だけで、下に値を並べる形の値の大小を比べれば E105 である。
- `default` と `open` は、どちらの形でも E003。
- 直和型は、フィールドの型が、リストや `T?` やレコードやほかの直和型を通して自分になってよい。どの値も自分を含み、自分を含まない値が一つも作れなければ E108 である(3.6 のレコードと同じ決まり。28.1)。
**決定(直和型は `enum` で書く)**:キーワード(`union`、`variant`、`type`)を足す形は捨てた。rulec と同じ列挙はフィールドの無い場合としてそのまま残り、`match` の網羅も一つの決まり(E106)で済む。フィールドの書き方をレコードにそろえたので、範囲と要素の数と別名の決まりも一つである。
### 23.2 型
- 直和型の名前(`Order`)が型である。値ごとの型(**値の型**)も書ける。`Order.placed` は、値 `placed` のフィールドを持つレコードの型で、その値は `Order` を受け取るところに渡せる。値の型は、関数の引数と答え、レコードとフィールドを持つ値のフィールド、`let` の型に書ける。ほかのモジュールの直和型の値の型は `<use の名前>.Order.placed` と書く。列挙に無い値の型(`Order.shipped`)と、フィールドを持つ値の無い列挙の値の型は E101 である。
- 作るときは、値の型のあとにフィールドを書く。`Order.placed { items: xs, on: d }` である。置かれた場所の型が `Order` か `Order.placed` と分かるところでは、`placed { items: xs, on: d }` とも書ける(3.5 の列挙の値と同じ)。フィールドを持たない値は、一行の列挙の値と同じく `Event.reopen` か、置かれた場所の型が分かれば `reopen` と書く。
- フィールドを持つ値を名前だけで書けば E107(`Order.placed`)、フィールドの無い値を `{ … }` で作れば E105(`Event.reopen { }`)。フィールドが足りないか二度書けば、レコードと同じく E107。
- `{ … }` の前の語が、レコードの名前としても、置かれた場所の直和型の値の名前としても読めれば E110 である。
- 作った式の型は直和型(`Order`)である。フィールドを読むには、`match` で分けて値に名前を付ける(23.3)。直和型の値のフィールドを `.` で読めば E105 で、注に `match` での分け方を出す(実物。`tests/golden/codes/sums.en.txt`):
```text
error[E105]: example.kn:9:5: A field of a value of Order is not read before a `match` names the value (`.on`)
9 | o.on
= Its fields depend on its value: split it with a `match`, and read the field of the value an arm names, as in `placed p => p.on`.
```
```text
エラー[E105]: example.kn:9:9: 注文 の値のフィールドは、そのままでは読めません(`.日`)
9 | 現在の注文.日
= 値によってフィールドが違うので、`match` で値ごとに分け、`確定 p => p.日` のように、名前を付けた値のフィールドを読んでください。
```
- 値の型の値は直和型の値として渡せるが、直和型の値は値の型の場所に渡せない(E102。注に、`match` で分けて名前を付けた値を渡すことを出す)。作った式も直和型なので、値の型の引数をとる関数は、`examples` の行では呼べない(19 章)。`if` と `match` の答え、リストの要素、比べの両辺が、値の型と直和型か、同じ直和型の二つの値の型なら、型は直和型である。
- 値の型は、生成したコードでは列挙の名前と値の名前をつなげた名前になる(`Order.placed` は `OrderPlaced`。23.8)。その名前が、モジュールのレコードか列挙、ほかの値の型の名前と同じなら E006。生成した TypeScript が値を見分けるフィールド `kind` は、フィールドの名前(別名)にできない(E007)。
### 23.3 `match`
```kn
fn next(o: Order, e: Event) -> Step
match o
draft d =>
match e
place p =>
if count(d.items) = 0
Step.refused { why: "an empty order is not placed" }
else
Step.moved { order: Order.placed { items: d.items, on: p.on } }
cancel c => Step.moved { order: Order.cancelled { reason: c.reason } }
else => Step.refused { why: "a draft is placed first" }
placed p =>
match e
pay y =>
if y.cents = total(items: p.items)
Step.moved { order: Order.paid { items: p.items, paid: y.cents } }
else
Step.refused { why: "the amount is not the total" }
place q => Step.moved { order: Order.placed { ..p, on: q.on } }
cancel c => Step.moved { order: Order.cancelled { reason: c.reason } }
reopen => Step.moved { order: Order.draft { items: p.items } }
paid, cancelled => Step.refused { why: "the order is closed" }
```
`Event`(値 `place`・`pay`・`cancel`・`reopen`)と `Step`(値 `moved`・`refused`)も同じ形の直和型で、`total` はモジュールの関数である(`tests/modules/sums.kn`)。
- **値と名前**:`placed p =>` は、`o` が `placed` のとき、その値に名前 `p`(型 `Order.placed`)を付けて分岐に入る。名前を付けなければ `placed =>` である。名前を付けた値は、ほかの値と一つの分岐に並べられない(`some x` と同じ。E106)。フィールドを持つ値の無い列挙の値に名前を付けても E106 である。
- **`else =>`**:最後の分岐に書き、上の分岐が受けない値を全部受ける。受ける値が一つも残っていなければ E106 である(上の分岐がもう受けた値を書いたのと同じ扱い)。`else` のあとに分岐を書けば、その分岐には値が来ないので E106、`else` をほかの値と一つの分岐に並べても E106 である。
- **リテラル**:数、お金、量、日付、文字列の値を、リテラルで分けられる(`-1, 0 =>`、`0USDc =>`、`-18℃ =>`、`2026-12-25 =>`、`"A" =>`)。数とお金と量のリテラルは、分ける値の単位で読む(3.2)。リテラルで分ける `match` には `else` が要り、無ければ E106 である。同じリテラルを二度書けば E106、int64 に収まらないリテラルは E104、分ける値の型でないリテラル(単位の違うものも)は E102。大小の範囲で分けるときは `if` で書く。
- **網羅(E106)**:列挙・直和型・真偽・オプショナルな値は、どの値にも分岐があるか、`else` があること。数・お金・量・日付・文字列は、`else` があること。レコードとリストは `match` で分けられない(E105)。
- パターンの入れ子(`placed { on: 2026-01-01 }`)は持たない。分けた値に、もう一度 `match` か `if` を書く。
リテラルの `match` に `else` が無いとき(実物):
```text
error[E106]: example.kn:4:3: A `match` that splits by literals needs `else`
4 | match code
= Numbers, amounts, quantities, dates and strings have too many values to list: take the ones no literal takes with `else =>`. A split by ranges is written with `if`.
```
**決定(分けた値に名前を付ける形)**:フィールドを名前で取り出す形(`placed(items: xs, on: d) =>`)は捨てた。`some x` と同じ一つの形で済み、名前を付けた値は値の型を持つので、そのまま関数に渡せる(`placed p => placed_on(p: p)`)。
### 23.4 一部を替えたレコード
```kn
fn repriced(i: Item, price: money[USDc] range >=1USDc <=100_000USDc) -> Item
Item { ..i, price: price }
fn moved_to(p: Order.placed, on: date range >=2026-01-01 <=2027-12-31) -> Order
Order.placed { ..p, on: on }
```
- `{` のすぐあとの `..x` は、型の同じ値 `x`(そのレコードか、その値の型)の全部のフィールドのうち、`..x` のあとに書いたフィールドのほかを、そのまま取る。`x` の型が違えば E102 で、`x` が直和型の値なら、注が `match` で分けて名前を付けた値を書くことを勧める。同じフィールドを二度書けば E107、`..x` を `{` のすぐあとのほかに書けば E001 である。
- 範囲を書いたフィールドは、替えたものも替えないものも、レコードを作るところと同じに確かめる(5 章の `Field` の場所)。替えないフィールドの場所は `..x` の式にあり、`x` の抽象の値のフィールドが範囲に収まれば示せる。同じ型の値から取るので、たいていは示せる。
### 23.5 等しさ
- `=` と `!=` は、どの型でも、同じ型の二つの値を比べる。レコードと値の型はフィールドごとに、リストは長さと要素ごとに、直和型は値とフィールドで、オプショナルな値は、どちらも `none` か、どちらも値があって等しいときに等しい。丸めていない値は、正確な有理数として比べる。数・お金・量の定数は、リストとオプショナルな値の中にあっても相手の単位で読む(`[1USD]` と `money[USDc]` のリストを比べれば、`[100USDc]` と比べる)。
- 型が違えば E102 である。値の無いことがあるものも比べられる(`x = none`、`x = 2`)。比べても型は狭まらないので、値を使うには `match` で分ける。
- 大小(`<`、`<=`、`>`、`>=`)と `min`・`max` は、数・お金・量・率・日付と、順序のある列挙だけである。レコード、リスト、オプショナルな値、直和型、文字列、真偽の大小は E105 である。
**決定(等しさは中身)**:状態が変わったか、二つの検証の答えが同じかを確かめるのは、汎用のコアのふつうの計算である。比べられないと、フィールドを一つずつ比べる関数を手で書くことになる(`tests/modules/sums.kn` の `stays` は、イベントのあとの注文が前と同じかを、注文の全体で比べる)。
### 23.6 参照インタプリタとやりとりする形
- 値(`src/value.rs`):フィールドを持つ値は `Value::Variant(値の名前, フィールド)` で、フィールドを持たない値は、直和型の中でも `Value::Enum(値の名前)` である。
- 受け渡す形(9 章、`run` と `vectors` の JSON):フィールドを持たない値は、値の名前の文字列(`"reopen"`)。フィールドを持つ値は、値の名前を鍵とする一組のオブジェクト(`{"placed": {"items": [...], "on": "2026-01-01"}}`)である。値の型の値も同じ形で受け渡す。読むときは値とフィールドを名前でも別名でも受け付け、書くときは宣言した名前で書く。`vectors` の一行(実物。`tests/modules/sums.kn` の `next`):
```json
{"function":"next","in":{"o":{"placed":{"items":[{"sku":"a","qty":50,"price":50000}],"on":"2026-01-01"}},"e":{"place":{"on":"2027-12-31"}}},"out":{"moved":{"order":{"placed":{"items":[{"sku":"a","qty":50,"price":50000}],"on":"2027-12-31"}}}},"why":"o: placed; e.place.on: the upper end"}
```
- 評価の順:`..x` をほかのフィールドより先に評価し、フィールドは書いた順に評価して、宣言の順に確かめる(8 章のレコードと同じ)。
- `match` は、上の分岐から順に、値がそのパターンに合う最初の分岐を選ぶ。`else` はどの値にも合い、リテラルは、分ける値の型で読んだ値と比べる。
### 23.7 範囲の解析
- **抽象の値**:直和型の値は `Abs::Variants` で、とりうる値の名前ごとに、フィールドの抽象の値のレコードを持つ。フィールドを持たない値は空のレコードである。二つの抽象の値を合わせるときは、値の名前の和をとり、同じ名前のフィールドを合わせる。フィールドを持つ値の無い列挙は、値の集合(`Abs::Set`)である。直和型の引数はどの値でもありえ、値の型の引数はその値だけで、フィールドは宣言した型と範囲から始める(5.1)。
- **作るところ**:その値だけの `Variants` である。範囲を書いたフィールドは `Field` の場所で、何の場所かは値の型で言う(`Order.placed` のフィールド `on`)。
- **`match` で狭める**:分ける値が変数かパスなら、名前を付けた分岐の中で、その値を分岐の値の名前だけに狭め、付けた名前にはその値のフィールドのレコードを入れる。付けた名前は、引数のその値の部分として読む(ベクタの候補と、例を探すことに使う。9 章、5.7)。`else` の分岐は、直和型・列挙・真偽なら上の分岐の受けない値の名前に狭め、オプショナルな値なら、`none` か値が残るときだけ分岐に入る(狭めない)。リテラルの分岐は、数と日付を、その分岐のリテラルのうち値がとりうるものを合わせた区間に狭める。リテラルの `else` では狭めない。どの値も来ない分岐は解析しない(5.3 と同じ)。
- **等しさの答え**:数と日付は区間で決める(離れていれば決まり、どちらも一つの同じ値なら等しい)。列挙と真偽は、値の集合がどちらも一つの同じ値か、交わらないときに決まる。直和型は、とりうる値の名前が交わらないとき(等しくない)と、どちらもフィールドの無い一つの同じ値のとき(等しい)に決まる(`sums.kn` の `reopened` は、`reopen` の分岐の中で `e = Event.reopen` を `true` に決める)。ほか(レコード、リスト、オプショナルな値、フィールドを持つ値)は、両方をとりうる。
- **一部を替えたレコード**:書いたフィールドは書いた式の抽象の値、書かないフィールドは `x` の抽象の値のそのフィールドで、範囲を書いたフィールドは範囲で切る。作ったレコードは、フィールドのあいだの関係を持たない。関係を持つのは `running_totals` の答えの要素(`running[T, W]`)だけで、`{ … }` でそれを作ることはできないからである。
- **ベクタ**(9 章):直和型の候補は、どの値も基本のフィールドで一つずつと、フィールドを持つ値ごとに、フィールドを一度に一つずつ候補で動かし、ほかを基本の値にしたものである(レコードと同じ)。基本の値は、最初の値の基本のフィールドである。
### 23.8 生成したコード
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| 直和型 `Order` | `export type Order = OrderDraft \| OrderPlaced \| …` | `Order = Union[OrderDraft, OrderPlaced, …]` | `type Order interface { isOrder(); KanameOutside(what string) *kaname.Outside }` | `pub enum Order { Draft(OrderDraft), Placed(OrderPlaced), … }` |
| 値の型 `Order.placed` | `export interface OrderPlaced { readonly kind: "placed"; readonly items: readonly Item[]; readonly on: string }` | `@dataclass(frozen=True) class OrderPlaced`(フィールドの無い値も、フィールドの無いデータクラス) | `type OrderPlaced struct { Items []Item; On kaname.Date }` と `func (OrderPlaced) isOrder() {}` | `pub struct OrderPlaced { pub items: Vec<Item>, pub on: kaname::Date }`(フィールドの無い値も、フィールドの無い構造体 `EventReopen {}`) |
| 作る | `{ kind: "placed", items: d.items, on: p.on }` | `OrderPlaced(items=d.items, on=p.on)` | `OrderPlaced{Items: d.Items, On: p.On}` | `Order::Placed(OrderPlaced { items: d.items.clone(), on: p.on })` |
| `match` | `if (o.kind === "draft") { const d: OrderDraft = o; … } else if …` | `if isinstance(o, OrderDraft): d: OrderDraft = o …` | `switch _t1 := o.(type) { case OrderDraft: var d OrderDraft = _t1 … }` | `match o { Order::Draft(d) => { … } … _ => { … } }` |
| 一部を替えたレコード | `{ ...p, on: q.on }` | `dataclasses.replace(p, on=q.on)` | 値をコピーしてからフィールドを入れる | `OrderPlaced { on: q.on, ..p_2.clone() }`(構造体の更新の書き方) |
| 等しさ | 補助の `kaname.eq(a, b)`(整数、真偽、文字列、日付、フィールドの無い列挙の値と、それのオプショナルな値は `===`) | `==`(データクラスとリストの比べ)。直和型の値は補助の `kaname.eq(a, b)` | 型ごとに生成する比べの関数(`_eq1(a, b Order) bool`)。`==` で中身を比べられる型は `==` | `==`(`derive(PartialEq)` が中身で比べる) |
- TypeScript の `kind` は値を見分けるためのフィールドなので、フィールドの名前(別名)に `kind` を付ければ E007 である。フィールドを持たない値も、直和型の中では `{ kind: "reopen" }` のオブジェクトにする(同じ型の値を同じ形にするため)。フィールドを持つ値の無い列挙は、10.2 の形のままである。
- 直和型の `match` は、TypeScript と Python では上の分岐から順に試す `if` の並びで、最後の分岐は条件を書かずに受ける(来る値を全部受けることは、kaname が E106 で確かめている)。来ない値のための補助の関数は置かない。Go は型の switch で、分岐ごとに `case` を書き、`else` を `default` にする。`else` の無い `match` には、`default` の `panic("kaname: no arm takes the value")` を置く。Go のコンパイラは、型の switch がどの値も受けるとは見ないので、答えを返す switch の後ろに `return` か `panic` が要るからである。リテラルの `match` は、どの出力先でも `if` の並びで、`else` が最後の分岐である。
- Rust の直和型の `match` は Rust の `match` で、分岐ごとに値の構造体を借りて名前に置く(`Order::Draft(d)` の `d` は `&OrderDraft`)。最後の分岐は TypeScript と Python と同じく条件を書かずに受け(`_ =>`)、`else` の無い `match` にも `panic` は要らない。分岐の名前が、同じ関数の前の名前と同じなら、10.3 の決まりで番号を足す(`next` の `placed p` は、前の `place p` と同じ名前なので `p_2` になる。上の表の一部を替えたレコード)。
- Rust の等しさは `derive(PartialEq)` の `==` で、フィールドの型の `PartialEq` が中身で比べる(`Vec` は要素を、`Box` は指す先を)。Go のような型ごとの比べの関数は要らない。一部を替えたレコードは、借りた値をコピーしてから替えるフィールドを入れる、構造体の更新の書き方である。
- Python の `kaname.eq`:`isinstance` で一方を狭めた二つの直和型の値は、型が重ならないとして `mypy --strict` が `==` での比べを通さない。そこで直和型の値の比べだけを、`==` を呼ぶ補助の関数にした。
- Go の比べの関数:`==` では中身で比べられない型(スライスとマップは比べられず、`*T` は指す先でなく番地を比べ、インターフェースは中の値の型が比べられないと panic する)を含むので、モジュールの中で型ごとに一つ生成する。
- 一部を替えたレコードで、範囲を確かめる場所は、作るところと同じに置く(10.4)。入口では、直和型の引数を値ごとに確かめる(TypeScript と Python の `_outside_Order`、Go の値の型ごとの `KanameOutside` のメソッド、Rust の列挙の `kaname_outside` のメソッド。値の構造体のメソッドを呼ぶ)。文のパスは `o.placed.on` の形である。
`tests/modules/sums.kn` から生成したもの(実物。TypeScript と Rust の `next` の始めと、Go の比べの関数):
```ts
function _next(o: Order, e: Event): Step {
if (o.kind === "draft") {
const d: OrderDraft = o;
if (e.kind === "place") {
const p: EventPlace = e;
if (BigInt(d.items.length) === 0n) {
return { kind: "refused", why: "an empty order is not placed" };
} else {
return { kind: "moved", order: { kind: "placed", items: d.items, on: p.on } };
}
```
```rust
fn _next(o: &Order, e: &Event) -> Step {
match o {
Order::Draft(d) => {
match e {
Event::Place(p) => {
if (d.items.len() as i64) == 0 {
Step::Refused(StepRefused { why: "an empty order is not placed".to_string() })
} else {
Step::Moved(StepMoved { order: Order::Placed(OrderPlaced { items: d.items.clone(), on: p.on }) })
}
}
```
```go
func _eq1(a, b Order) bool {
switch x := a.(type) {
case OrderDraft:
y, ok := b.(OrderDraft)
return ok && _eq2(x.Items, y.Items)
case OrderPlaced:
y, ok := b.(OrderPlaced)
return ok && _eq2(x.Items, y.Items) && x.On == y.On
case OrderPaid:
y, ok := b.(OrderPaid)
return ok && _eq2(x.Items, y.Items) && x.Paid == y.Paid
case OrderCancelled:
y, ok := b.(OrderCancelled)
return ok && x.Reason == y.Reason
}
return false
}
```
### 23.9 W501 を狭める
**決定**:W501 の一つ目の形(定数を選び分ける分岐。7 章)のうち、`match` が数える業務の定数を、お金、量、率、日付のリテラルと、それだけでできたレコード(フィールドを持つ値を含む)に狭める。列挙の値、フィールドを持たない値、真偽、数を答える `match` は警告にしない。`..x` から作るレコードは、定数だけでできたレコードに数えない。`if` の決まりと、二つ目の形(しきい値を値にする比べ)は変えない。
理由:状態遷移の `match` は、どの値にも答えるか、二度答えないかを、kaname 自身が E106 で確かめる。表に書く理由の一つ(完全性と重なり)は、`match` では kaname の検査が受け持っている。列挙だけで書いた状態遷移に W501 が二つ出た(机の上の確かめ)。値の種類ごとの金額は、承認する人がページで読む表なので、警告する。
区分ごとに数を答える `match` と、列挙の値を答える `match`(列挙の値を別の列挙の同じ名前の値に対応づけるものも)は警告しない。区分ごとの金額を答える `match`(7 章の `surcharge`)は警告になる(`tests/hatch.rs`)。
### 23.10 エフェクト、診断、Lean、テスト
- **エフェクト**:新しい種類は無い。範囲を書いたフィールドを持つ値を作るところは、レコードと同じく `range` の場所である。直和型のフィールドの範囲は、作るところと、値の型と直和型の引数の入口で確かめる。
- **診断**:E106(`else`、リテラル、名前を付けた値)、E101(無い値の型)、E102(`..x` の型、値の型の場所に渡す直和型の値、比べられない二つの型)、E104(int64 に収まらないリテラル)、E105(直和型のフィールドを `.` で読む、フィールドの無い値を `{ … }` で作る、順序の無い列挙の大小)、E107(名前だけで書いたフィールドを持つ値)、E108(値のフィールドの率の刻み、自分を含まない値が作れない直和型)、E110(レコードと値の名前)、E006 と E007(生成したコードの名前)。どれも英語と日本語の一番小さな再現を `tests/codes.rs` に置き、文を `tests/golden/codes/sums.en.txt` と `sums.ja.txt` で確かめる。台帳の文(E003、E006、E007、E101、E102、E104、E105、E106、E107、E108、E110、W501)も、この章の決まりを言う。
- **Lean**(`KanameCore`):
- 値と抽象の値(`Value.lean`):`Val.variant (tag) (fields)` と `Abs.variants`。フィールドを持たない値は `Val.enum` のままで、`Abs.variants` の中では空のレコードの値である。
- 式とパターン(`Syntax.lean`):`Expr.record` に値の名前(`tag`)と `..x` の式(`base`)を持たせ、`Pat` は `value (v) (bind)`、`lit (v)`、`rest`(`else`)を持つ。引数の宣言 `Slot` は、直和型の値ごとのフィールドの宣言(`values`)を持つ。
- 評価(`Eval.lean`):`Pat.takes` が、名前のパターンを値の名前で、リテラルを `==` で、`rest` をどの値にも合うとして、分岐を選ぶ。`=` と `!=` は、両辺を比べる型で読み替えてから `==` で比べる。レコードと値を作る式は、`..x` を先に評価する。
- 範囲の解析(`Analysis.lean`):`match` の分岐の狭め方 `armScope`(`restScope`・`litScope`・`nameScope` を順にかける)と、上の分岐の受けた値を持って進む `matchArms`、作るところ(`recordAbs` が値の名前と `..x` の抽象の値を取る)、等しさの答え(直和型の値の名前で決める)。
- 定理:`ranges_sound`・`no_failure`・`total_sound` の帰納法は、直和型の値を作る式、`match`、一部を替えたレコード、等しさの場合を持つ。部品は、`arms_sound` と `arms_mono`(選んだ分岐の値が、その分岐で狭めた環境に入る。上の分岐が値を受けないことを前提に)、`armScope_sound`、`record_case`(作った値と `..x` から取ったフィールドが、作るところの抽象の値に入る)、`decideVariants_sound`(直和型の等しさを決めた答えが、評価の比べと同じ)、`Mem.variant`(フィールドを持つ値が `Abs.variants` に入ること。値の名前があり、フィールドがその値の宣言の中にあり、どのフィールドの値も抽象の値に入る)。
- 突き合わせ(15.3):`crates/ritsu-model/tests/kaname.rs` の書き出す形と、モデルの `Json.lean` の読む形を合わせ、材料に `sums.kn`・`sums.ja.kn` が入る。わざと壊して突き合わせが落ちることを確かめた四か所は、`else` の分岐の狭め方(上の分岐が受けた値を除かないと、`reset` の答えに `paid` と `placed` が残る)、リテラルの分岐の狭め方(区間をリテラルの値に狭めないと、`bumped` の答えの上の端が 1001 になる)、`..x` から取るフィールド(`none` にすると、`next` と `repriced` の答えが変わる)、等しさの答え(値の名前が交わらない二つを決めないと、`reopened` の `else` の比べが決まらない)である。作ったレコードはフィールドの関係を持たないので(23.7)、一部を替えたレコードは、関係でなく `..x` から取るフィールドを壊して確かめる。
- **テスト**:`tests/modules/sums.kn` と `sums.ja.kn`(注文とイベントの状態遷移、`else` とリテラルの `match`、一部を替えたレコード、リスト・オプショナルな値・レコード・直和型の等しさ、値の型を引数にとる関数。21 の関数)。範囲の解析の健全性(`tests/ranges.rs`)、出力先の突き合わせ(英日。16.4)、Lean との突き合わせ(15.3)に入る。`tests/parse.rs`(下に値を並べる列挙、名前を付けた値、`else`、リテラル、`..x`)、`tests/types.rs`(網羅、作る式、等しさ、値の型)、`tests/interp.rs`(分岐の選び方、作る式、受け渡す形)、`tests/hatch.rs`(23.9)、`tests/codes.rs`(上の診断)、`tests/doc.rs`(`tests/golden/doc/sums.en.md` と `sums.ja.md`)、`tests/api.rs`(値のフィールドと生成したコードの型の名前)、`tests/ports.rs`(`Items` の値とフィールド)。
## 24. 辞書と集合、リストの組み込みの関数
### 24.1 鍵にできる型と、鍵の順
**鍵にできる型**は、数、お金、量、刻みのある率、日付、文字列、真偽と、フィールドを持たない列挙(規則の列挙も)である。辞書の鍵と集合の要素の型、並べ替え(`sort`・`sort_by`・`sort_by_desc`)と `distinct`・`max_by`・`min_by`・`to_dict`・`group_by` の鍵の型が、鍵にできる型でなければ E114 である。レコード、リスト、辞書、集合、値の無いことがある値、直和型は、鍵にできない。並べ替えと `max_by`・`min_by` の鍵には、鍵にできる型のフィールドだけを持つレコードも書け、フィールドを宣言の順に比べる(初めのフィールドで決まらなければ次へ。`tests/modules/dicts.kn` の `ranked`)。
**鍵の順**は、kaname が型ごとに決めた一つの順である(`src/value.rs` の `key_cmp`)。
| 型 | 順 |
|---|---|
| 数、お金、量、率 | 小さい順(その単位の正確な有理数で比べる) |
| 日付 | 早い順 |
| 文字列 | コードポイントの順 |
| 真偽 | `false`、`true` |
| 列挙 | 宣言の順(`\|` で区切った、順序の無い列挙も。`<` で比べられるのは、`<` で区切った列挙だけ) |
| レコード(並べ替えと `max_by`・`min_by` の鍵) | フィールドを宣言の順に |
二つの鍵が同じかは、等しさ(23.5)で決める。鍵にできる型では、鍵の順で同じところに来る二つの値が、等しい二つの値である。
辞書の鍵と値、集合の要素、`distinct` が見分ける要素、`to_dict` と `group_by` の答えの鍵と値、`indexed` の `item` は、丸めた値でなければならない(E103)。生成したコードは、これらをレコードのフィールドと同じく受け渡す形で持つ(24.6)からである。並べ替えと `max_by`・`min_by` の鍵は、比べるだけで値を持たないので、丸めていない値でもよい。
鍵にできない型を辞書の鍵にしたとき(実物。`tests/golden/codes/E114.en.txt` と `E114.ja.txt`):
```text
error[E114]: example.kn:7:24: Line cannot be a key
7 | fn lines(by_line: dict[Line, number]) -> number
= A key is a number, an amount, a quantity, a rate with a step, a date, a string, a bool, or a value of an enum whose values have no fields: kaname puts keys in the order it gives each such type, so that a dict and a set go round in one order in every target.
```
```text
エラー[E114]: example.kn:7:36: 明細 は鍵にできません
7 | fn 明細の数(lines)(明細ごと(by_line): dict[明細, number]) -> number
= 鍵にできるのは、数、お金、量、刻みのある率、日付、文字列、bool と、フィールドの無い列挙の値です。kaname は鍵を型ごとに決めた順で並べるので、辞書と集合は、どの出力先でも同じ順で回ります。
```
**決定(鍵の順で回す)**:入れた順で回す形は捨てた。二つの辞書が同じ組を持っていても、作った順で回る順が違えば、等しい二つの値を同じ関数に渡して違う答えが出る。鍵の順なら、値が同じなら回る順も同じである。文字列の順をコードポイントの順にしたのは、出力先の言語の文字列の比べ(UTF-16 の単位で比べる TypeScript、コードポイントで比べる Python、バイトで比べる Go)に頼らず、補助のモジュールの一つの比べで書けるからである。
### 24.2 辞書と集合
```kn
fn diff(before: dict[string, number] count <=4, after: dict[string, number] count <=4) -> list[Change]
let skus = before.keys().to_set().union(after.keys().to_set()).to_list()
skus.filter_map(sku => change(sku: sku, before: before.get(sku), after: after.get(sku)))
```
`Change` は値 `added`・`removed`・`changed` の直和型で、`change` は二つの在庫の一つの品番の差を答えるモジュールの関数である(`tests/modules/dicts.kn`)。
- **型**:`dict[K, V]`、`set[T]`。`K` と `T` は鍵にできる型である(E114)。宣言には、`count`(組と要素の数)と、辞書の値と集合の要素が数か日付なら `range` を書ける(3.9。書かなければ型の全体。22.1)。`dict`・`set` と、組み込みのレコードの型 `entry[K, V]`(フィールド `key`、`value`)と `indexed[T]`(フィールド `index`、`item`)は、すぐあとに `[` があるときだけ型の語として読む。`set` という名前のレコードやフィールドも書ける。
- **書き方**:辞書のリテラルは `["a": 1, "b": 2]`、空の辞書は `[:]` である。`[:]` は、置かれた場所の型が辞書の型を言うところにだけ書ける(`let d = [:]` は E102)。同じ定数の鍵を二度書けば E107。鍵が値なら、あとに書いた組が勝つ。集合はリストから作る(`[a, b].to_set()`、空は `[].to_set()`。置かれた場所の型が要素の型を決める)。
- **値**:鍵の順に並んだ組(集合は要素)の並びである。どの組み込みの関数も新しい辞書と集合を答え、渡したものは変わらない。回す関数(`keys`、`values`、`entries`、`to_list` と、24.3 の関数を取るもの)は、鍵の順に回る。
- **等しさ**:二つの辞書は同じ組を持つとき、二つの集合は同じ要素を持つときに等しい(23.5)。
### 24.3 組み込みの関数(メソッドの形)
組み込みの関数は、関数の形(`sum(xs, x => x.qty)`)か、初めの引数のあとに `.` で続ける**メソッドの形**(`xs.sum(x => x.qty)`)で呼ぶ。メソッドの形は、初めの引数を `.` の前に置くほかは関数の形と同じで、評価の順(初めの引数から)も型の決まりも変わらない。3.7 の組み込みの関数は、どちらの形でも書ける(関数を取らない `first(xs)` と `xs.first()` も)。24.2 と 24.3 の表の辞書・集合・リストの関数はメソッドの形だけで、`numbers` は関数の形だけで書く。違う形で書けば E101 で、注に書き方を出す(実物。`tests/golden/codes/dicts.en.txt`):
```text
error[E101]: example.kn:4:3: There is no function `keys`
4 | keys(prices)
= `keys` is a builtin written after a value, with a `.`: `<value>.keys(…)`.
```
モジュールの関数の名前とぶつかる(E006)のは、関数の形で書く組み込みの関数の名前だけである(`numbers` も)。メソッドの形だけの名前(`get`、`sort_by`)は、モジュールの関数に付けてよい。どちらを呼ぶかは、書き方で決まるからである。
辞書:
| 関数 | 答え | 備考 |
|---|---|---|
| `d.get(k)` | `V?` | 無い鍵は `none` |
| `d.put(k, v)` | `dict[K, V]` | 入れるか、値を置き換える |
| `d.remove(k)` | `dict[K, V]` | 無い鍵なら同じ辞書 |
| `d.has(k)` | `bool` | |
| `d.keys()`、`d.values()` | `list[K]`、`list[V]` | 鍵の順 |
| `d.entries()` | `list[entry[K, V]]` | 鍵の順 |
| `d.count()` | `number` | 組の数(関数の形の `count(d)` も) |
| `d.map_values(v => …)` | `dict[K, U]` | 答えは丸めた値(E103) |
| `d.filter(e => …)` | `dict[K, V]` | `e` は `entry[K, V]` |
| `d.merge(o)` | `dict[K, V]` | 同じ鍵は `o` の値 |
| `d.merge_with(o, (a, b) => …)` | `dict[K, V]` | 同じ鍵は、`d` の値を `a`、`o` の値を `b` にした関数の答え |
集合:
| 関数 | 答え |
|---|---|
| `s.has(x)`、`s.add(x)`、`s.remove(x)` | `bool`、`set[T]`、`set[T]` |
| `s.union(o)`、`s.intersect(o)`、`s.difference(o)` | `set[T]` |
| `s.to_list()` | `list[T]`(鍵の順) |
| `s.count()` | `number`(`count(s)` も) |
リスト(3.7 の関数のほか):
| 関数 | 答え | 備考 |
|---|---|---|
| `xs.append(x)`、`xs.concat(ys, …)` | `list[T]` | 関数の形の `concat` は、文字列もリストもつなぐ |
| `xs.at(i)` | `T?` | 0 から数える。範囲の外は `none` |
| `xs.first()`、`xs.last()`、`xs.last(x => 条件)` | `T?` | `last` は終わりから見て、条件を満たす最初の要素で止まる |
| `xs.slice(from, to)`、`xs.take(n)`、`xs.drop(n)` | `list[T]` | `from` から `to` の手前まで。0 から要素の数のあいだに切り詰める(範囲の外でも失敗しない) |
| `xs.reverse()` | `list[T]` | |
| `xs.sort()`、`xs.sort_by(x => 鍵)`、`xs.sort_by_desc(x => 鍵)` | `list[T]` | 鍵の順。同じ鍵の要素は元の順を保つ(`sort_by_desc` でも)。`sort` は要素が鍵にできる型のとき |
| `xs.distinct()` | `list[T]` | 要素が鍵にできる型。初めに現れたものを残す |
| `xs.contains(x)` | `bool` | 等しさ(23.5)で探す |
| `xs.flatten()`、`xs.flat_map(x => リスト)` | `list[U]` | |
| `xs.filter_map(x => オプショナルな値)` | `list[U]` | `none` を除く |
| `xs.indexed()` | `list[indexed[T]]` | `index` は 0 から |
| `xs.max_by(x => 鍵)`、`xs.min_by(x => 鍵)` | `T?` | 同じ鍵なら初めのもの |
| `xs.to_set()` | `set[T]` | |
| `xs.to_dict(x => 鍵, x => 値)` | `dict[K, V]` | 同じ鍵は、あとの要素が勝つ |
| `xs.group_by(x => 鍵)` | `dict[K, list[T]]` | 要素は元の順 |
| `xs.join(sep)` | `string` | 文字列のリスト |
関数の形で書くもの:
| 関数 | 答え | 備考 |
|---|---|---|
| `numbers(from, to)` | `list[number]` | `from` から `to` まで(両端を含む)。`to < from` なら空 |
| `mod(x, n)` | `number` | 割る数 `n` は定数でも値でもよい整数で、余りは 0 以上、`n` の絶対値より小さい(ユークリッドの形)。`n` が 0 をとりうれば `zero` の場所 |
`at`・`take`・`drop`・`slice` の場所と数、`numbers` の両端、`mod` の割る数は整数の `number` で、ほかは E109 である。`numbers` の要素の数 `to − from + 1` は、int64 のあふれの場所である(`from` が int64 のいちばん小さい数で、`to` がいちばん大きい数のとき)。とても長いリストを作ってメモリが尽きることは、止まらない評価と同じく、言語の意味の外に置く。参照インタプリタは、要素が 1,000,000 を超える `numbers` で、深すぎる呼び出しと同じく評価を止める(`interp::NUMBERS`。8 章)。
`mod` の割る数が正の定数なら、余りは rulec の `mod`(床の余り)と同じである。値で割る `mod` の、割る数が負のときの答えも一つに決めてある。Python の `%` は割る数の符号、Go と TypeScript の `%` は割られる数の符号の余りを返すので、生成したコードは補助のモジュールの関数で計算する(24.6)。
**決定(範囲の外でも失敗しない形にする)**:添え字と切り出しが範囲の外のときに失敗する形(Go の切り出しは panic する)は捨てた。`at` は `none` を、`slice`・`take`・`drop` は切り詰めた答えを返す。失敗の種類を増やさず、エフェクトが変わらない。
### 24.4 参照インタプリタとやりとりする形
- **値**(`src/value.rs`):`Value::Dict`(鍵の順の組の並び)、`Value::Set`(鍵の順の要素の並び)。鍵の順は `key_cmp` の一つの関数が決め、辞書と集合を作るところ(`dict_of`、`set_of`)が、同じ鍵を一つにして鍵の順に並べる。
- **評価の順**:辞書のリテラルは、組ごとに鍵、値の順で左から評価し、それぞれをその場で辞書の鍵と値の型で読む。`to_dict` は、要素ごとに鍵、値の順に関数を当てる。並べ替えは安定な手順で、鍵の順で比べる。
- **受け渡す形**(9 章、`run` と `vectors` の JSON):辞書は組の配列 `[[鍵, 値], …]`、集合は要素の配列で、どちらも鍵の順である。`run` と `vectors` の入力は鍵の順でなくてよく、同じ鍵を二度書けば `input` の失敗にする。`vectors` の一行(実物。`tests/modules/dicts.kn` の `combined`、`a.merge_with(b, (x, y) => x + y)`):
```json
{"function":"combined","in":{"a":[["a",50]],"b":[["a",50],["b",50]]},"out":[["a",100],["b",50]],"why":"b: 2 pairs"}
```
- **ベクタの候補**(9 章):辞書と集合は、リストと同じ要素の数(下の端、0、1、2、3、上の端)の、互いに違う鍵の値を並べたもの(辞書の値は基本の値)と、一つ目の組の値と、一つ目の鍵(集合は一つ目の要素)を候補で動かしたものである。基本の値は、リストと同じ数の違う鍵である。
### 24.5 範囲の解析
| 式 | 答えの抽象の値 |
|---|---|
| 辞書の値 | `Abs::Dict(組の数の区間, 鍵の抽象の値, 値の抽象の値)` |
| 集合の値 | `Abs::Members(要素の数の区間, 要素の抽象の値)` |
| 辞書のリテラル | 数は `[min(1, n), n]`(`n` は書いた組の数。同じ鍵の組は一つになりうる)、鍵と値は書いたものを合わせたもの |
| `get` | 値の抽象の値の、`none` になりうるもの |
| `put(k, v)` | 数は `[max(1, c.lo), c.hi + 1]`、鍵と値は `k`・`v` と合わせたもの |
| `remove` | 数は `[max(0, c.lo − 1), c.hi]` |
| `keys`、`values`、`entries`、`to_list` | 数の同じリスト |
| `map_values`、`filter` | 数は同じ(`filter` は `[0, c.hi]`)、値は関数の答え(`filter` は条件で狭めた組の鍵と値) |
| `merge`、`merge_with` | 数は `[max(a.lo, b.lo), a.hi + b.hi]`、値は合わせたもの(`merge_with` は関数の答えも) |
| `union`、`intersect`、`difference` | 数は `[max(a.lo, b.lo), a.hi + b.hi]`、`[0, min(a.hi, b.hi)]`、`[max(0, a.lo − b.hi), a.hi]` |
| `append`、`concat` | 数の和 |
| `at`、`last(x => 条件)` | 要素の、`none` になりうるもの(`last` は条件で狭めた要素) |
| `first()`、`last()` | 要素の、要素の数の下の端が 0 なら `none` になりうるもの |
| `take(n)`、`drop(n)`、`slice(a, b)` | 数は `[min(c.lo, n.lo), min(c.hi, n.hi)]`、`[max(0, c.lo − n.hi), c.hi − n.lo]`、`[0, min(c.hi, b.hi − a.lo)]`(`n`・`a`・`b` の負の端は 0 に読む) |
| `reverse`、`sort`、`sort_by`、`sort_by_desc` | 同じ |
| `distinct`、`to_set` | 数は `[min(1, c.lo), c.hi]` |
| `flatten`、`flat_map` | 数は内と外の数の積(2⁶³ − 1 で切る) |
| `filter_map` | 数は `[0, c.hi]`、要素は関数の答えの値のある側 |
| `indexed` | `index` は `[0, c.hi − 1]` |
| `max_by`、`min_by` | 要素の、要素の数の下の端が 0 なら `none` になりうるもの |
| `to_dict`、`group_by` | 数は `[min(1, c.lo), c.hi]`、`group_by` の値は数 `[1, c.hi]` のリスト |
| `numbers(a, b)` | 要素は `[a.lo, b.hi]`、数は `[max(0, b.lo − a.hi + 1), max(0, b.hi − a.lo + 1)]` |
| `mod(x, n)` | 0 から、`n` の両端の絶対値の大きいほうより 1 小さい数まで |
組と要素の数は、0 から 2⁶³ − 1 に切った区間である。ラムダは、要素(辞書の `filter` では組、`map_values` では値、`merge_with` では二つの値)の抽象の値で、その場で解析する(5.5)。要素の数を比べる条件(`count(xs) = 0`、`d.count() > 0`)は、リストと同じく、その名前の要素の数を狭める(5.3)。数の区間は、組と要素の値の両方の区間と合わせて、範囲を書いた渡す先(24.2 の宣言)で確かめる。
### 24.6 生成したコード
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| `dict[K, V]` | `ReadonlyMap<K, V>` | `Mapping[K, V]`(作るのは `dict`) | `map[K]V` | `BTreeMap<K, V>`(引数は `&BTreeMap<K, V>`) |
| `set[T]` | `ReadonlySet<T>` | `AbstractSet[T]`(作るのは `frozenset`) | `map[T]struct{}` | `BTreeSet<T>` |
| 鍵の順に回す | `kaname.sortedKeys(m, 比べ)` | `kaname.keys_of(d, 比べ)` | `kaname.SortedKeys(m, 比べ)` | `iter()`(`BTreeMap` と `BTreeSet` は鍵の順に回る) |
| 並べ替え | `Array.prototype.sort`(安定)と鍵の比べ | `sorted(…, key=…)`(安定) | `slices.SortStableFunc` | `kaname::sorted` と `kaname::by_key`(`sort` と `sort_by`。安定) |
| `entry[K, V]`、`indexed[T]` | 補助のモジュールの `interface` | 補助のモジュールの `@dataclass(frozen=True)` | 補助のモジュールの構造体 | 補助のモジュールの構造体(`kaname::Entry<K, V>`、`kaname::Indexed<T>`) |
| `mod(x, n)` | `kaname.mod` | `kaname.mod` | `kaname.Mod` | `kaname::r#mod` |
- 辞書と集合は、どこでも受け渡す形で持つ。鍵と値は境目の型(10.2)で、鍵は、TypeScript の Map がそのまま値で比べる型(`bigint`、`string`、`boolean`。日付は `YYYY-MM-DD` の文字列、列挙の値は名前の文字列)にだけなる。丸めた値でなければ辞書と集合に入れられない(24.1)のは、このためである。Rust の鍵も受け渡す形の型で、その `Ord` が鍵の順である(36.2)。
- 鍵の順の比べは、型ごとに補助のモジュールの関数を使う(文字列はコードポイントの順の `cmpStr`、列挙は宣言の順の番号 `byRank`)。辞書と集合を作る関数(`kaname.mapOf`、`kaname.dict_of`)は、TypeScript の Map と Set と Python の dict に鍵の順で入れるので、ホストが受け取る Map と dict は鍵の順で回る。Go の map は順を持たない。生成したコードの中で回すところは、作った順に頼らず、いつも鍵の順に並べてから回す。Rust の `BTreeMap` と `BTreeSet` は鍵の順で持つので、並べずにそのまま回す。
- `put` と `remove` は、新しい Map・dict・map・`BTreeMap` を作る(元のものを書き換えない。Rust は `kaname::put` と `kaname::remove` がコピーしてから入れる)。一つ入れるたびに全部をコピーするので、たくさんの組を作るときは、一度に作る `to_dict` と `group_by` を使う。
- 鍵が同じ要素の元の順を保つ並べ替えは、四つの出力先とも標準の安定な並べ替えを使う。Go の `sort.Slice` と Rust の `sort_unstable` は安定でないので使わない。
- 等しさ(23.8):TypeScript の `kaname.eq` は、Map を組の数と鍵ごとの値で、Set を要素で比べる。Python は `==`(`Mapping` と `frozenset` の比べ)、Go は型ごとに生成する比べの関数、Rust は `==`(`BTreeMap` と `BTreeSet` の `PartialEq`)である。
`tests/modules/dicts.kn` の `combined` から生成したもの(実物。TypeScript、Python、Go、Rust の順):
```ts
function _combined(a: ReadonlyMap<string, bigint>, b: ReadonlyMap<string, bigint>): ReadonlyMap<string, bigint> {
const _t1 = new Map<string, bigint>(a);
for (const _t2 of kaname.sortedKeys(b, kaname.cmpStr)) {
if (a.has(_t2)) {
_t1.set(_t2, (a.get(_t2) as bigint) + (b.get(_t2) as bigint));
} else {
_t1.set(_t2, (b.get(_t2) as bigint));
}
}
return kaname.mapOf(_t1, kaname.cmpStr);
}
```
```python
def _combined(*, a: Mapping[str, int], b: Mapping[str, int]) -> Mapping[str, int]:
_t1: dict[str, int] = dict(a)
for _t2 in kaname.keys_of(b, kaname.natural):
if (_t2 in a):
_t3: int = a[_t2]
_t4: int = b[_t2]
_t1[_t2] = _t3 + _t4
else:
_t1[_t2] = b[_t2]
return kaname.dict_of(_t1.items(), kaname.natural)
```
```go
func _combined(a map[string]int64, b map[string]int64) map[string]int64 {
var _t1 = kaname.Merge(a, map[string]int64{})
for _, _t2 := range kaname.SortedKeys(b, kaname.CmpStr) {
if kaname.Has(a, _t2) {
var _t3 int64 = a[_t2]
var _t4 int64 = b[_t2]
_t1[_t2] = _t3 + _t4
} else {
_t1[_t2] = b[_t2]
}
}
return _t1
}
```
```rust
fn _combined(a: &BTreeMap<String, i64>, b: &BTreeMap<String, i64>) -> BTreeMap<String, i64> {
let mut _t1: BTreeMap<String, i64> = a.clone();
for (_t2, _t3) in b.iter() {
if let Some(_t4) = a.get(_t2) {
_t1.insert(_t2.clone(), *_t4 + *_t3);
} else {
_t1.insert(_t2.clone(), *_t3);
}
}
_t1.clone()
}
```
### 24.7 エフェクト、診断、Lean、テスト
- **エフェクト**:辞書、集合、リストの組み込みの関数は、どれも失敗しない。場所を持つのは、`numbers` の要素の数(`overflow`)と、値で割る `mod`(`zero`)と、引数を辞書の鍵と値、集合の要素、答えの型で読むところ(単位を読み替える `overflow`。5.6)だけである。辞書と集合の引数と答えに書いた範囲と数は、リストと同じく渡す先で確かめる(`range`)。
- **診断**:E114(鍵にできない型)、E107(辞書のリテラルの同じ定数の鍵、組み込みの関数の引数の数)、E109(`mod`・`numbers`・`at`・`take`・`drop`・`slice` に整数でない値)、E101(メソッドの形でだけ書く関数を関数の形で、関数の形でだけ書く関数をメソッドの形で書いた。注に書き方)、E103(丸めていない値を辞書と集合に入れる)、E102(`[:]` だけの `let`、辞書でない値の `keys`)、E006(`numbers` という名前の関数)、E108(数と日付でない値の `range`、リスト・辞書・集合でない値の `count`)。どれも英語と日本語の一番小さな再現を `tests/codes.rs` に置き、文を `tests/golden/codes/dicts.en.txt` と `dicts.ja.txt`、E114 の台帳の再現を `E114.en.txt` と `E114.ja.txt` で確かめる。
- **Lean**(`KanameCore`):
- 値と抽象の値(`Value.lean`):`Val.dict (List (Val × Val))`、`Val.set (List Val)` と、`Abs.dict`・`Abs.members`。値が抽象の値に入る(`Mem.dict`、`Mem.members`)のは、数が区間に入り、鍵と値(要素)が抽象の値に入り、鍵(要素)が一つずつのときである。
- 鍵(`Eval.lean`):鍵の順の比べ `keyCmp`(`key_cmp` と同じ)と、それで並べる `byKey`。二つの鍵が同じかは `==` で決める(`sameKey`)。辞書と集合を作るところは、どれも同じ鍵を一つにしてから `List.mergeSort`(安定)で並べる(`dictFrom` はあとの組を、`setFrom` と `groupsOf` は初めのものを残す)。
- 式(`Syntax.lean`):辞書のリテラル `Expr.dict`、関数を取らない組み込みの関数 `Expr.prim`(`Prim` の 32 の関数)、関数を取るもの `Expr.walkK`(`WalkK` の 10 の関数)、`Expr.toDict`、`Expr.mergeWith`。評価は `primVal`、`foldK` と、組と要素ごとに関数を当てる `evalPairs`・`evalEach2`・`evalMerge`。`mod` は、値の割る数と、0 の割る数の `zero` の場所を持つ。
- 範囲の解析(`Analysis.lean`):24.5 の表(`primAbs`、`pairsAbs`、`ev` の新しい場合)と、引数を場所の型で読む `convAll`。
- 定理:`ranges_sound`・`no_failure`・`total_sound` の帰納法(`SoundAt` に、辞書のリテラル、`to_dict`、`merge_with` の三つの場合)の場合を持つ。関数ごとの補題は `SoundPrim.lean`(答えの組と要素が元の組と要素から来ること、数の区間。`primAbs_sound`、`mergedC_sound`、`countIv_sound`、`numbers_sound` など)に、式ごとの場合は `SoundDict.lean`(`case_dict`、`case_prim`、`case_walkK`、`case_toDict`、`case_mergeWith`)にある。並べ替えの答えが元の並べ替えであることは `List.mergeSort_perm` で言う。
- 突き合わせ(15.3):材料に `dicts.kn`・`dicts.ja.kn`・`lists2.kn`・`lists2.ja.kn` が入る。わざと壊して突き合わせが落ちることを確かめた五か所は、`put` の数(上の端に 1 を足さないと、`restock` の答えの組の数が 1 から 3 になり、Rust の 1 から 4 と違う)、`merge` の値の合わせ方(`o` の値を合わせないと、`overlay` の答えの値が 0 から 5 になり、Rust の 0 から 20 と違う)、`slice` の切り詰め(0 より前の場所を終わりから数えると、`cut` の 6 件の答えが違う)、並べ替えの鍵の順(文字列を逆の順に比べると、`diff` ほか `dicts.kn` の 118 件の答えが違う)、`mod` の向き(割られる数の符号の余りにすると、`walks.kn` の 1 件と、`dicts.kn` の `turn` と `wrapped` の 2 件の答えが違う)である。
- **テスト**:`tests/modules/dicts.kn` と `dicts.ja.kn`(在庫の差分、価格の表、`group_by` と `to_dict`、集合の和と積と差、同じ鍵の要素の並べ替え、鍵の順、辞書と集合の等しさ、値で割る `mod` と `numbers`。34 の関数)、`lists2.kn` と `lists2.ja.kn`(リストの組み込みの関数と、3.7 の関数のメソッドの形。24 の関数)。範囲の解析の健全性(`tests/ranges.rs`)、出力先の突き合わせ(英日。16.4。四つの出力先で、同じ鍵の要素の並べ替えの順、Map と dict と `BTreeMap` を回す順、負の数の `mod` が参照インタプリタと同じ)、Lean との突き合わせ(15.3)に入る。`tests/parse.rs`(辞書のリテラル、型の語)、`tests/types.rs`(E114、メソッドの形、関数の形、組み込みの関数の型)、`tests/interp.rs`(鍵の順、受け渡す形、評価の順)、`tests/effects.rs`(`numbers` と `mod` の場所)、`tests/codes.rs`(上の診断)。
## 25. 文字列
### 25.1 文字列の値
**決定(文字列はコードポイントの並び)**:`string` の値は、Unicode のスカラー値(サロゲートを除くコードポイント)の並びである。長さ、切り出し、位置は、どれもコードポイントで数える。大小は、コードポイントの値を初めから一つずつ比べる順(コードポイントの順)で決める。二つの文字列が等しいのは、同じコードポイントの並びのときで、正規化はしない(e と U+0301 の並びと、一つのコードポイントの é は、違う文字列である)。
理由:出力先の言語で、長さ(TypeScript は UTF-16 の単位、Go と Rust はバイトで数える)と比べる順(TypeScript は UTF-16 の単位の順)が実際に違った。コードポイントはどの出力先の言語でも数えられ(Rust では `char`)、Lean の `Char` も同じものである。
- リテラルは、引用符のあいだをそのまま読み、エスケープを持たない(rulec と同じ)。改行のように書けない文字は、コードポイントの数で `char(10)` と書く(25.2)。
- 文字列の引数、答え、フィールドには、コードポイントの数の範囲を `count` で書ける(`name : string count <=40`)。書かなければ 0 から 2⁶³ − 1 である(22.1)。文字列に `range` は書けない(E108)。
- ホストから入る文字列は、入口で、正しい文字列であることと、書いた `count` に収まることを確かめる(25.7)。
### 25.2 組み込みの関数
表の関数は、どれもメソッドの形で書く(24.3)。`count`、`starts_with`、`concat`、`text` はどちらの形でも書け、`char` は関数の形だけで書く。`slice`、`take`、`drop`、`contains` はリストの関数と同じ名前で、`.` の前の値が文字列なら文字列の関数である。どの関数も、渡した文字列を変えずに、新しい値を答える。
| 関数 | 答え | 備考 |
|---|---|---|
| `s.count()`、`count(s)` | `number` | コードポイントの数 |
| `s.chars()` | `list[string]` | 一つずつのコードポイントの文字列 |
| `s.code_points()` | `list[number]` | 一つずつのコードポイントの数(0〜1,114,111) |
| `xs.to_text()` | `string?` | コードポイントの数のリストから作る。スカラー値でない数があれば `none` |
| `char(n)` | `string` | `n` はスカラー値を書いた整数の定数(`char(10)`)。定数でなければ、スカラー値でなければ E109 |
| `s.slice(from, to)`、`s.take(n)`、`s.drop(n)` | `string` | コードポイントで数え、0 から長さのあいだに切り詰める(リストと同じ。24.3) |
| `s.index_of(t)` | `number?` | `t` が初めに現れる位置(コードポイントで 0 から)。`t` が空なら 0、無ければ `none` |
| `s.contains(t)`、`starts_with(s, t)`、`s.ends_with(t)` | `bool` | |
| `s.split(sep)` | `list[string]` | 左から重ならないように切る。前後の空の文字列も残す。`sep` が空なら、一つずつのコードポイント(`chars()` と同じ) |
| `s.words()` | `list[string]` | 空白(25.3)の続きで切り、空の文字列を除く |
| `xs.join(sep)` | `string` | 文字列のリストを、あいだに `sep` を入れてつなぐ(24.3) |
| `s.replace(old, new)` | `string` | `old` を左から重ならないように、全部 `new` に替える。`old` が空なら `s` のまま |
| `s.trim()`、`s.trim_start()`、`s.trim_end()` | `string` | 両端、初め、終わりの空白(25.3)を除く |
| `s.lower_ascii()`、`s.upper_ascii()` | `string` | A〜Z を a〜z に、a〜z を A〜Z に替える。ほかの文字は替えない |
| `s.pad_start(width, fill)`、`s.pad_end(width, fill)` | `string` | 長さが `width` になるまで `fill` を繰り返して前(後ろ)に足し、超えたぶんを切る。`fill` が空か、長さがもう `width` 以上なら `s` のまま |
| `s.parse_number()` | `number?` | 25.4 |
| `s.parse_date()` | `date?` | 25.4 |
大小:`<`、`<=`、`>`、`>=`、`min`、`max`、並べ替え(`sort` と、`sort_by` などの鍵)、辞書と集合の鍵は、どれもコードポイントの順である。鍵の順(24.1)と同じ一つの比べを使う。
`char` に定数でない値と、スカラー値でない数を渡したとき(実物):
```text
error[E109]: diag.kn:4:8: `char` takes a whole number written as a constant, a code point; this is number
4 | char(n)
= Write the code point as a number: a new line is `char(10)`, a tab `char(9)`. A string is made from numbers that are values with `[n].to_text()`.
error[E109]: diag.kn:7:8: `char` is given 55296, which is no Unicode scalar value
7 | char(55296)
= A scalar value is a number from 0 to 1114111 (U+10FFFF), but for the surrogates, 55296 to 57343 (U+D800 to U+DFFF).
```
```text
エラー[E109]: diag.kn:4:8: `char` が取るのは、コードポイントを書いた整数の定数です。これは number です
4 | char(n)
= 改行は `char(10)`、タブは `char(9)` のように、コードポイントを数で書いてください。値の数から文字列を作るには `[n].to_text()` を使います。
エラー[E109]: diag.kn:7:8: `char` に渡す 55296 は、Unicode のスカラー値ではありません
7 | char(55296)
= スカラー値は 0 から 1114111(U+10FFFF)までの数で、サロゲートの 55296〜57343(U+D800〜U+DFFF)を除きます。
```
**決定(`char` は定数だけを取る)**:`char` は、リテラルに書けない文字を書くためのもので、検査がスカラー値かを決めるので失敗しない。値の数から文字列を作るときは `to_text` を使い、スカラー値でない数は `none` で受ける。`char` という名前のモジュールの関数は E006 である(`numbers` と同じく関数の形で書く組み込みの関数の名前だから。24.3)。
### 25.3 空白と、大文字と小文字
**決定(空白は White_Space の 25 文字)**:`trim`・`trim_start`・`trim_end` と `words` が空白と読むのは、Unicode の White_Space の性質を持つ 25 のコードポイントである。U+0009〜U+000D、U+0020、U+0085、U+00A0、U+1680、U+2000〜U+200A、U+2028、U+2029、U+202F、U+205F、U+3000。この表を、参照インタプリタ(`src/text.rs` の `SPACES`)、四つの出力先の補助のモジュール、Lean のモデル(`Text.lean`)が同じに持つ。
理由:標準の `trim` が除くものが、三つの言語で違った。TypeScript は U+FEFF を除いて U+0085 を残し、Python は U+0085 と U+001C を除き、Go(White_Space の表を使う)は U+0085 を除いて U+FEFF と U+001C を残した。Unicode が決めた表なら、どの言語でも同じ文字を除ける。
**決定(大文字と小文字は ASCII だけ)**:`lower_ascii` と `upper_ascii` は、A〜Z と a〜z だけを替える。Unicode の全部の文字の大文字と小文字は持たない。U+0130 を小文字にすると、TypeScript と Python は 2 文字に、Go は 1 文字にし、ß を大文字にすると、TypeScript と Python は SS に、Go は替えなかった。全部の文字を同じに替えるには、出力先ごとに大きな表を持つことになる。名前に `ascii` を付けて、替えないものがあることを呼ぶ側で読めるようにした。
### 25.4 数と日付を読む
- `parse_number` が読むのは、`+` か `-` を一つ前に置いてもよい、ASCII の数字(0〜9)の一つ以上の並びだけで、int64 に収まるときに数を答える。前後の空白、`_`、全角やほかの文字体系の数字、小数点、指数は読まず、`none` を答える。単位は付かない(お金にするには、`n * 1USDc` のように単位の値を掛ける)。
- `parse_date` が読むのは `YYYY-MM-DD`(0001 年から 9999 年の正しい日)だけである。
理由:Python の `int` は前後の空白、`_`、全角とアラビア・インドの数字を読み、TypeScript の `BigInt` は前後の空白を読み、Go の `ParseInt` はどれも読まなかった。補助のモジュールに kaname の手順で書けば、四つの出力先が同じものを読む。
### 25.5 参照インタプリタとやりとりする形
- **値**:Rust の `String`(いつも正しい UTF-8)で、受け渡す形は JSON の文字列である。組み込みの関数は `src/text.rs` にあり、どれもコードポイント(Rust の `char`)で数える。文字列の大小は、鍵の順の比べ(`key_cmp`)と同じで、`str` の比べ(UTF-8 のバイトの順は、コードポイントの順と同じ)である。
- **長い文字列**:`pad_start` と `pad_end` が、文字列を 1,000,000 コードポイントより長く伸ばすときは、`numbers` と同じく評価を止める(`diverged`。生成したコードとは突き合わせない。24.3)。数の引数で文字列を伸ばすのは、この二つだけである。
- **入口**:書いた `count` を確かめ、外れれば `input` の失敗にする。JSON の文字列に孤立したサロゲート(`"\ud800a"`)を書けば、読むところで止まる。`run` の入力で(実物):
```text
error: bad.json is not JSON: byte 13: a high surrogate without its low surrogate
```
`count` を外れた文字列(`tests/modules/texts.kn` の `place` に 13 コードポイントの `s`。実物):
```text
fails: input (line 73): The argument `s` of `place` has 13 code points, outside its count <=12
失敗: input(73 行目): 関数 `place` の引数 `s` のコードポイントは 13 個で、コードポイントの数の範囲 <=12 の外です
```
### 25.6 範囲の解析
文字列の抽象の値は、コードポイントの数の区間を持つ `Abs::Str(Iv)` である(中身は追わない)。区間は、リストの要素の数と同じく、0 から 2⁶³ − 1 に切った整数の区間である。
| 式 | コードポイントの数の区間 |
|---|---|
| リテラル | その長さ |
| 引数とフィールド | 書いた `count`。書かなければ 0〜2⁶³ − 1(上の端に型の端の印。22.2) |
| `concat` | 渡した文字列の数の区間の和 |
| `join(sep)` | 下の端は、要素の数の下の端 × 要素の長さの下の端 + あいだの数 × `sep` の長さの下の端、上の端も同じ形(下の端は、どれも 0 より下を 0 として読む) |
| `slice`、`take`、`drop` | リストの要素の数と同じ(24.5) |
| `trim`、`trim_start`、`trim_end` | 0 から、元の上の端まで |
| `lower_ascii`、`upper_ascii` | 元と同じ |
| `pad_start`、`pad_end` | `[max(n.lo, w.lo), max(n.hi, w.hi)]`(`fill` が空になりうれば下の端は `n.lo`) |
| `replace` | 0〜2⁶³ − 1(上の端に型の端の印) |
| `chars`、`code_points` | 要素の数が元の長さのリスト。要素は長さ 1 の文字列と、0〜1,114,111 の数 |
| `to_text` | 長さがリストの要素の数の文字列の、`none` になりうるもの |
| `split` | 要素の数は 1〜元の上の端 + 1(`sep` が空になりうれば、下の端は元の下の端と 1 の小さいほう)、要素の長さは 0〜元の上の端 |
| `words` | 要素の数は 0〜元の上の端 + 1、要素の長さは 0〜元の上の端 |
| `index_of(t)` | `none` になりうる、0〜(元の上の端 − `t` の下の端)。それが 0 より下なら、いつも `none` |
| `text(x)` | 数は 1〜20、お金と量は 1〜20 に単位の綴りの長さを足したもの、日付は 10、列挙と真偽は値の名前の長さの最小から最大、文字列はその区間、率は 0 から(上の端は無い) |
| `char(n)` | 1 |
| `parse_number`、`parse_date` | `none` になりうる、int64 の全体と、0001-01-01 から 9999-12-31(端に型の端の印) |
`s.count()` と比べる条件(`if name.count() <= 40`)は、リストと同じく、その名前のコードポイントの数を狭める(5.3)。`tests/modules/texts.kn` の `entry` は、`code.count() <= 8 and label.count() >= 1 and label.count() <= 24` の分岐の中で作るレコードの `count >=1 <=24` のフィールドを、この狭め方で示す。`short` は、`count` を書かない引数の `s.take(5)` を `count <=5` の答えにすることを示す。
**決定(文字列の場所は、書いた `count` だけを確かめる)**:文字列を渡す先(答え、フィールド、関数の引数)で確かめるのは、書いた `count` だけである。`count` を書かない文字列は、型の端(2⁶³ − 1 のコードポイント)を確かめない。数の型の端と違って、そこを超える文字列は作れず、確かめる場所を置いても何も確かめないからである。
**決定(`parse_number` と `parse_date` の答えは型の全体)**:読んだ数と日付の区間は、型の全体の端に型の端の印を付けたものである。読んだ数に 1 を足すところは、型の端から来たあふれ(22.2)になり、診断を出さずに生成したコードが確かめる。文字列の中身を追わないので、それより狭い区間は言えない。
### 25.7 生成したコード
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| 長さ(`count`) | `kaname.cpCount(s)` | `len(s)` | `kaname.CpCount(s)` | `kaname::cp_count(s)` |
| `slice`、`take`、`drop` | `kaname.cpSlice`、`cpTake`、`cpDrop` | `kaname.text_slice`、`text_take`、`text_drop` | `kaname.TextSlice`、`TextTake`、`TextDrop` | `kaname::text_slice`、`text_take`、`text_drop` |
| `index_of` | `kaname.cpIndexOf` | `kaname.index_of` | `kaname.IndexOf` | `kaname::index_of` |
| 大小 | `kaname.cmpStr`(コードポイントの順) | `<`(`str` はコードポイントの並び) | `<`(正しい UTF-8 のバイトの順は、コードポイントの順と同じ) | `<`(`str` の比べは UTF-8 のバイトの順で、コードポイントの順と同じ) |
| 空白、大文字と小文字、数と日付を読む | 補助のモジュールの表と手順 | 同じ(`str.strip`、`str.lower`、`int` は使わない) | 同じ | 同じ(`str::trim` と `to_lowercase` は使わない) |
| 入口の正しい文字列 | `kaname.isText`(孤立したサロゲートが無い) | `kaname.is_text`(サロゲートが無い) | `kaname.IsText`(`utf8.ValidString`) | 確かめない(`&str` と `String` は正しい UTF-8 しか持てない。入口の `glue::door` が JSON を読むところで言う。36.11) |
- TypeScript の `string` は UTF-16 の単位の並びなので、長さ、切り出し、位置、比べは、補助のモジュールのコードポイントで数える関数で書く。`for (const c of s)` はコードポイントを一つずつ回るので、補助のモジュールはそれを使う。`s.length` と `<` は使わない。
- Python の `str` はもともとコードポイントの並びなので、長さと比べはそのまま書く。Go の `string` は UTF-8 で、長さと切り出しと位置は `[]rune` と `utf8` で数える。Rust の `str` も UTF-8 で、長さと切り出しと位置は補助のモジュールが `chars()` で数え、比べは `<` のまま書く。
- 入口では、引数とそれが届く文字列(レコードのフィールド、リストの要素、直和型の値のフィールドも)を、正しい文字列かと、書いた `count` で確かめる。確かめの関数の答え(`kaname.Outside`)は、文字列の `count` の外れを言う `chars` と、正しくない文字列を言う `bad` を持ち、文はそれで分ける。`count` を書かない文字列も、正しい文字列かは確かめる。Rust の確かめは `count` だけを見る(`kaname::outside_chars`)。
- 文字列の組み込みの関数は、どれも補助のモジュールの関数の呼び出しで、その場で計算する。
`tests/modules/texts.kn` の `place`(`s.index_of(t)`)から生成したもの(実物。ホストが呼ぶ関数と中の関数。TypeScript、Python、Go、Rust の順。Go と Rust は、ホストが呼ぶ関数の前に、引数を確かめる `CheckPlace` と `check_place` がある。Rust の三つの関数は 36.4):
```ts
export function place(s: string, t: string): bigint | null {
const o_s = _outside8(s, "s");
if (o_s !== null) {
if (o_s.bad === true) {
kaname.fail("input", _module, 73, "The argument `" + o_s.path + "` of `place` is no well-formed string of Unicode");
}
kaname.fail("input", _module, 73, "The argument `" + o_s.path + "` of `place` has " + String(o_s.count) + " code points, outside its count " + o_s.range);
}
const o_t = _outside12(t, "t");
if (o_t !== null) {
if (o_t.bad === true) {
kaname.fail("input", _module, 73, "The argument `" + o_t.path + "` of `place` is no well-formed string of Unicode");
}
kaname.fail("input", _module, 73, "The argument `" + o_t.path + "` of `place` has " + String(o_t.count) + " code points, outside its count " + o_t.range);
}
return _place(s, t);
}
function _place(s: string, t: string): bigint | null {
return kaname.cpIndexOf(s, t);
}
```
```python
def _place(*, s: str, t: str) -> int | None:
return kaname.index_of(s, t)
def _outside8(v: str, what: str) -> kaname.Outside | None:
if not kaname.is_text(v):
return kaname.Outside(path=what, shown="", range="", count=None, bad=True)
if len(v) > 12:
return kaname.Outside(path=what, shown="", range="<=12", count=len(v), chars=True)
return None
```
```go
func _place(s string, t string) *int64 {
return kaname.IndexOf(s, t)
}
func _outside8(v string, what string) *kaname.Outside {
if !kaname.IsText(v) {
return &kaname.Outside{Path: what, Shown: "", Range: "", Count: -1, Bad: true}
}
if kaname.CpCount(v) > 12 {
return &kaname.Outside{Path: what, Shown: "", Range: "<=12", Count: int(kaname.CpCount(v)), Chars: true}
}
return nil
}
```
```rust
fn _place(s: &str, t: &str) -> Option<i64> {
kaname::index_of(s, t)
}
fn _outside7(v: &str, what: &str) -> Option<kaname::Outside> {
if kaname::cp_count(v) > 12 {
return Some(kaname::outside_chars(what, kaname::cp_count(v), "<=12"));
}
None
}
```
### 25.8 ベクタ
文字列の候補は、`"a"`、比べるリテラル、`starts_with` の前置きと、次のものである。どれも、書いた `count` に収まるものだけをとる(基本の値も、`count` の下の端が 1 より大きければ、その数の `"a"` にする)。
- `count` を書いた文字列には、その両端の数の `"a"` の並び。
- 文字列の組み込みの関数(25.2 の表のものと、文字列の `count`・`slice`・`take`・`drop`・`contains`・`min`・`max`)に渡す引数と、大小を比べる引数には、出力先の言語が違って読む 15 の文字列(`src/vectors.rs` の `HARD`):空の文字列、`" a "`、`"a b"`、`"É"`、e と U+0301、U+1F44D と U+1F3FD(UTF-16 で二単位ずつのコードポイント二つ)、U+0085 と `"a"`、U+FEFF と `"a"`、U+00A0 と `"a"`、全角の `"A1"`、`"-12"`、`"+12"`、`"9223372036854775808"`、`"2024-02-29"`、`"2026-02-29"`。
難しい文字列を足すところを限ったのは、文字列の操作をしない関数のベクタを、文字列の候補の組み合わせで増やさないためである。`tests/modules/texts.kn` の `amount`(`text.parse_number()`、`text` は `count <=20`)のベクタの三行(実物):
```json
{"function":"amount","in":{"text":"-12"},"out":-12,"why":"text: a number below zero"}
{"function":"amount","in":{"text":"9223372036854775808"},"out":null,"why":"text: a number past int64"}
{"function":"amount","in":{"text":"aaaaaaaaaaaaaaaaaaaa"},"out":null,"why":"text: the upper end of the count"}
```
宣言の外の入力(16.4)は、`count` を書いた文字列の、上の端より一つ多いコードポイントの文字列と、出力先ごとに正しくない文字列を一つずつ持つ。正しくない文字列は、ハーネスが出力先の言語で作る(TypeScript は `String.fromCharCode(0xD800)`、Python は `chr(0xD800)`、Go は `string([]byte{0xff})`)。参照インタプリタはこの値を持てないので、答えは `input` の失敗(文は「The argument `…` of `…` is no well-formed string of Unicode」)と決めて比べる。
### 25.9 エフェクト、診断、Lean、テスト
- **エフェクト**:文字列の組み込みの関数は、どれも失敗しない(範囲の外の切り出しは切り詰め、読めない数と日付は `none`)。場所を持たない。
- **診断**:E109(`char` に整数の定数でないもの、スカラー値でない数。`slice`・`take`・`drop`・`pad_start`・`pad_end` に整数でない値)、E102(文字列でない値の文字列の関数、コードポイントの整数でないリストの `to_text`)、E101(メソッドの形でだけ書く関数を関数の形で、`char` をメソッドの形で書いた)、E108(リスト・辞書・集合・文字列でない値の `count`)、E006(`char` という名前の関数)。文字列は大小を比べられ、真偽の大小は E105 である(実物):
```text
error[E105]: diag2.kn:4:3: A value of bool compares with `=` and `!=` only
4 | a < b
```
- **Lean**(`KanameCore`):
- 定義(新しい `Text.lean`):文字列を `List Char` として、25.2 の関数を参照インタプリタと同じ手順で書いたもの。空白の表(`SPACES`)、ASCII の大文字と小文字、数と日付を読む手順(`parseNumber`、`parseDate`。日付は Hinnant の `days_from_civil`)、スカラー値(`isScalar`)、`pad` が伸ばす長さの上限(`LONGEST`)も持つ。Lean の `String.trim` は ASCII の空白しか除かないので使わない。
- 値と抽象の値:`Val.str` はそのままで、`Abs.str (c : Iv)` がコードポイントの数の区間を持つ。文字列が抽象の値に入る(`Mem.str`)のは、長さが区間に入るときである。入口(`outsideD`)は、書いた `count` を長さで確かめる(`strOut`)。
- 評価(`Eval.lean`):文字列の組み込みの関数は `Expr.prim` の 22 の関数(`Prim.chars` から `Prim.charOf`)で、`primVal` が `Text.lean` の定義で計算する。型の約束は `type` の失敗で確かめる(`join` と `concat` が文字列でないものを受け取る、`text` に型の言わない値。`textVal`、`textOK`)。2⁶³ − 1 より長い文字列を作るところ(`sized`)と、それを分けるところ(`split`、`words`)は、要素の多いリストと同じく `diverged` である。参照インタプリタはそういう文字列を作れない。`pad_start` と `pad_end` が 1,000,000 コードポイントより長く伸ばすところも、参照インタプリタと同じく `diverged` である。
- 範囲の解析(`Analysis.lean`):25.6 の表(`primAbs` の文字列の場合、`strCount`、`textCount`、`joinedEnd`)と、`count` との比べで長さを狭めること(`narrowCount`)、文字列の場所が書いた `count` だけを確かめること(`strOK`)。
- 定理:関数ごとの補題と、組み込みの関数をまとめた `primAbs_sound` は `SoundText.lean` にある。切り出しの長さは、同じ長さのリストの切り出しの補題(`take_sound`、`drop_sound`、`slice_sound`)に戻して言う。ほかは、`findFrom_bound`(見つけた位置と探す文字列の長さの和は、元の長さを超えない)、`splitAux_facts` と `wordsAux_facts`(切った文字列の数と、一つずつの長さ)、`padded_sound`、`joined_lo` と `joined_hi`(`join` の長さの両端)、`parseNumber_range` と `parseDate_range`、`int_text_length`(int64 の整数は 1〜20 の文字で書く)、`dateText_length`、`names_count`、`concat_counts` である。`concat` と `text` の式の場合は `case_concat` と `case_text`(`SoundText.lean`)である。
- `join` の長さの下の端は、要素と `sep` の長さの下の端の 0 より下を 0 として読む。要素の数の下の端が 0 と 1 のあいだの分数で、`sep` の長さの下の端が 0 より下の区間(型の検査を通ったモジュールの解析では出ない)でも、上からの見積もりになる。Rust とモデルが同じ読み方をする。
- 突き合わせ(15.3):材料に `texts.kn`・`texts.ja.kn` が入る。わざと壊して突き合わせが落ちることを確かめた四か所は、空白の表から U+0085 を除くこと(`texts.kn` と `texts.ja.kn` の 4 件ずつの答えが違う。`handle`、`initials`、`slug`、`tidy_ends`)、空の文字列の `index_of` を `none` にすること(16 件ずつ。`place`)、`split` の前後の空の文字列を除くこと(23 件ずつ。`fields`)、文字列をコードポイントの順でなく長さで比べること(264 件ずつ。`before`、`compared`、`earliest`、`ordered`、`by_code` ほか)である。
- **テスト**:`tests/modules/texts.kn` と `texts.ja.kn`(29 の関数ずつ。名前をそろえること(`trim`、`words`、`join`、`lower_ascii`、`replace`)、コマンドを確かめること(`count`、`index_of`、`ends_with`)、数と日付を読むこと、コードポイントで数え、切り出し、探すこと、`split` と `char`、詰めること、書いた `count` を答えが守ること、文字列の大小と並べ替え)。範囲の解析の健全性(`tests/ranges.rs`)、出力先の突き合わせ(英日。難しい文字列と正しくない文字列を含む。16.4)、Lean との突き合わせ(15.3)に入る。`tests/types.rs`(文字列の大小と `min`・`max` が通ること、真偽の大小の E105、文字列の関数の型、`char` の E109)、`tests/codes.rs`(E108 の文)。
## 26. ジェネリクスと `result`
### 26.1 型の引数の書き方
```kn
record Page[T]
items : list[T] count <=100
next : string?
enum Found[T]
hit
value : T
at : number range >=0 <=99
miss
why : string
enum Tree[T]
leaf
value : T
node
children : list[Tree[T]]
fn first_or[T](xs: list[T], fallback: T) -> T
match xs.first()
none => fallback
some x => x
fn largest[K: key](xs: list[K]) -> K?
xs.sort().last()
```
- 関数、レコード、列挙の名前(と別名)のすぐあとの角括弧に、型の引数を書く。`K: key` は、`K` が鍵にできる型(24.1)であることを求める。制約はこの一つだけである。
- 型の引数の名前は型の名前空間にある。一つの宣言のほかの型の引数、モジュールのレコードと列挙と重なれば E006 である。生成するコードに出るので、ASCII でなければ別名を付ける(E005)。
- 使うときは、型のあとの角括弧に型を書く(`Page[Invoice]`、`list[Found[string]]`、`glib.Box[Item]`)。数が合わなければ E112 である。値の無いことがある型(`number?`)は、型の引数にできない(E112)。総称の関数の中の `T?` が、`T` の `none` と `T?` の `none` を持つことになり、参照インタプリタ(値を包まない)と、生成した Go(`*T` を重ねる)で答えが分かれるからである。値の無いことがあるところは、`?` の無い型を当てて `T?` と書く。
- 呼ぶときは、型の引数を書かない。引数の型と、置かれた場所の型から決める(26.2)。決まらないときは、関数の名前のあとに書ける(`empty_page[string]()`)。
- 総称のレコードと列挙も、自分を含められる(`Tree[T]` の `children : list[Tree[T]]`)。自分を含む輪の中では、型の引数そのものか、型の引数を含まない型だけを渡す(E108。28.1)。
- 型の引数を持つ関数は、ホストが呼ばない中の関数である(21.3)。
### 26.2 型の決まり
- **決め方**:呼び出しの引数を、定数でないものから左から順に、次に定数を、引数の宣言の型と突き合わせて、型の引数を一つの型に決める。決まらない型の引数は、置かれた場所の型(関数の答え、型を書いた `let`、引数の宣言)から決める。`none`、`[]`、`[:]` だけの式は何も言わない。それでも決まらないか、二つの引数が一つの型の引数に違う型を言えば E112 である。単位の違うお金(`money[USD]` と `money[USDc]`)は違う型である。丸めていない値は型の引数にならない(E103)。
- 型の引数を書いた呼び出し(`f[number](…)`)は、書いた型で引数を確かめる。数が合わなければ E112、`key` の型の引数に鍵にできない型を書けば E114 である。
- **総称の関数の中の型の引数の値**:渡すこと、レコード・リスト・辞書の値・オプショナルな値・`result` に入れること、`=` と `!=` で比べること(中身で比べる。23.5)ができる。大小の比べ、`min`・`max`、辞書と集合の鍵、並べ替えの鍵には `K: key` が要る(無ければ E114)。算術、`match`、フィールドを読むことはできない(E105)。型の引数の値には範囲を書けない(`range` は E108)。型の引数のリストには `count` を書ける。
- 鍵にできる型のフィールドだけを持つレコードは、並べ替えと `max_by`・`min_by` の鍵になる(24.1)。鍵に限った型の引数のフィールドも、鍵にできるフィールドに数える(フィールド `key : K` を持つ `Pair[K]`、`item` が `K` の `indexed[K]`)。総称の関数は、そのレコードを、受け取った `K` の順とほかのフィールドの順で、宣言の順にフィールドを比べて並べる。
- 総称のレコードと列挙の、型の引数でないフィールドにも、範囲と要素の数を書ける(`Page[T]` の `items` の `count <=100`、`Found[T]` の `at` の範囲)。具体的な型を当てた型のフィールドは、宣言の型に型の引数を当てた型である。
- **呼び出しの輪の中の総称の関数**:呼び出しの輪(自分を呼ぶ関数と、互いを呼ぶ関数の組。名前で渡すことも呼ぶことに数える。27.5)の中で、総称の関数を呼ぶところは、その関数の型の引数に、呼ぶ側の型の引数をそのまま渡すか(入れ替えてもよい)、型の引数を含まない型を渡す。型の引数から作った型(`wrap_deep(x: [x], …)` の `list[T]`)を渡せば E112 で、輪の中のほかの関数を通しても、関数を名前で渡しても同じである。
**決定(呼び出しの輪の中では、型の引数から作った型を渡さない)**:自分を含む総称の型の決まり(28.1。輪の中の型には、型の引数そのものか、型の引数を含まない型を渡す)と同じ決まりを、呼び出しの輪にも置く。型の引数から作った型を輪の中で渡すと、呼び出しが輪を回るたびに型の引数が一段ずつ深くなり(`list[T]`、`list[list[T]]`、…)、呼ぶ型が限りなく増える。参照インタプリタは型の引数を消して評価するので答えを出し、生成した TypeScript と Python も `tsc --strict` と `mypy --strict` を通るが、生成した Go は、型の引数ごとに関数を作る形なので「instantiation cycle」で組めない。検査で止めれば、四つの出力先が同じモジュールを受け付ける。型の引数をそのまま渡す形、入れ替えて渡す形、型の引数を含まない型を渡す形は、どれも Go が組める(`tests/modules/generic.kn` の `count_down`、`swap_down`、`settle_down`。出力先の突き合わせが、`tsc`、`mypy --strict`、`go vet`、clippy を通すことを確かめる)。
**捨てたもの**:Go が止める形だけを止めること。Go が止めるのは、型の引数から作った型を渡す呼び出しが、型の引数の流れの輪(呼ばれた関数の型の引数が、輪を回って呼ぶ側の型の引数に戻る道)の上にあるときだけで、その流れを追えば、いまの決まりより多くの輪を許せる。いまの決まりは、輪の中の呼び出しを一つずつ見るだけで言える(19 章)。
```text
error[E112]: example.kn:4:24: Where `wrap_deep` calls `wrap_deep` in a ring of calls, it gives `list[T]`, a type argument made from a type parameter
4 | if n = 0 then 0 else wrap_deep(x: [x], n: n - 1)
= Each time the calls go round, the type argument grows one deeper (`list[list[T]]`, …), and the types it is called with never end; nor can the generated Go write them. In a ring of calls, give the type parameters as they are, or types that name none.
error[E112]: example.kn:9:3: The type parameter `T` of `first_or` is given two types, money[USD] and money[USDc]
9 | first_or(xs: dollars, fallback: cents)
= A type parameter takes one type: amounts or quantities of two units are of two types too, and `in` converts one to the other.
error[E114]: example.kn:4:3: A value of the type parameter `T` is compared with `<` only where it is held to keys
4 | a < b
= To make a value of a type parameter a key, to compare it by `<` or to sort by it, hold the parameter to keys: `[T: key]`. Only the types that can be keys are then given to it.
```
### 26.3 `result` と `try`
**決定(失敗を値で扱う型を組み込みにする)**:組み込みの型 `result[T, E]` は、値 `ok`(型 `T` の値を一つ持つ)と `err`(型 `E` の値を一つ持つ)の直和型である。
```kn
fn parse_order(c: Command) -> result[Order, list[string]]
let items = try parse_lines(lines: c.lines)
let on = try parse_day(text: c.on)
if items.count() = 0
err(["no item"])
else
ok(Order { items: items, on: on })
```
- 作るときは `ok(x)` と `err(e)`。型は置かれた場所から決める(決まらなければ E112)。`ok` と `err` は関数の形で書く組み込みの関数で、モジュールの関数の名前にできない(E006)。
- `match` では、値を直に名前で受ける:`ok x =>`(`x` は `T`)、`err e =>`(`e` は `E`)。`some x` と同じ形である。
- **`let x = try e`**:`e` が `result[T, E]` なら、`err` のときはその `err` を関数の答えにし、`ok` のときは中の値を `x` にして続ける。`e` が `T?` なら、`none` のときは `none` を関数の答えにする。関数の答えの型は、`result[U, E]`(`E` が同じ)か `U?` でなければならない(E115)。
- `try` を書けるのは、**関数の答えに続くブロック**の `let` だけである。関数の本体のブロックと、そこから答えにつながる `if` と `match` の分岐のブロックがそれで、`let y =` の下のブロックや、組み込みの関数に渡す関数の中には書けない(E115)。`try` は名前にできない語である(E002)。
- `try` は、型の検査のあとで `match` に読み替えてから、範囲の解析、参照インタプリタ、生成に渡す(`src/tried.rs`)。`let x = try e` と残りの行は、`match e` の `err try => err(try)` と `ok x => 残りの行` に(`T?` なら `none => none` と `some x => 残りの行` に)なる。`try` という名前は、モジュールのどの名前にもなれないので、ほかの名前と重ならない。読み替えた式は、`let` の行と `try` の列を持ち、診断と失敗の行は書いたところを言う。
理由:検証のように、失敗するかもしれない手順が続くコードは、`match` の入れ子が深くなる。`try` を答えに続くブロックだけに限れば、読み替えは同じブロックの中で済み、関数から途中で抜ける仕組みが要らない。組み込みの型にしたのは、モジュールごとに自分の `Result` を宣言すると、宣言した場所で型が決まる(3.5)ので、二つのモジュールのあいだで受け渡せないからである。
**`fails` との関係**:エフェクトの `fails` は、検査が示せなかった場所(あふれ、0 で割ること、範囲)で、プログラムを直すか範囲を書けば消えるものである。`result` は、業務の手順として拒むこと(検証に通らない入力、在庫の足りない注文)である。二つは混ぜない。`fails` の失敗を受け止めて `result` にする書き方は作らない(4.6)。
### 26.4 参照インタプリタとやりとりする形
- 型の引数は、型の検査のあとで消える。参照インタプリタは、型の引数のない関数と同じに評価する。ただし、鍵に限った型の引数(`K: key`)の値の順は、その型の順(列挙は宣言の順。24.1)なので、呼び出しが型の引数に当てた型を、呼ばれた関数の評価のあいだ持つ(`interp.rs` の `Frame::subst`)。
- `result` の値は `Value::Variant("ok", [("value", v)])` と `Value::Variant("err", [("error", e)])` で、受け渡す形は、直和型と同じく `{"ok": 値}` と `{"err": 値}` である。
- 入口の確かめ(4.4)は、`result` の値の中の値も、引数が届くところとして確かめる。パスは `r.ok.qty` と書く。総称のレコードと列挙に具体的な型を当てた型(`Page[Line]`)は、型の引数を当てたフィールドで確かめる。
- 型の引数を持つ関数は、ホストから呼ばないので、`run` は流さずに exit 2 で終わり、`vectors --function` は注を出す(21.3):
```text
error: `first_or` takes type parameters, an inner function no host calls, so it is not run: run a function that calls it
note: `first_or` takes type parameters, an inner function, which has no vectors: the vectors of the functions that call it try it
```
### 26.5 範囲の解析:呼ぶ場所での読み直し
型の引数の値には範囲が無いので、総称の関数を一度だけ解析すると、その値は何でもありうるとしか言えない(`first_or` の答えは `Abs::Any`)。
**決定(呼ぶ場所ごとに読み直す)**:型の引数を持つ関数を、**読み直す関数**とする。
- **一度だけの解析**:読み直す関数も、宣言した引数の型から一度だけ解析する。型の引数の値は `Abs::Any` から始める。この解析が、関数の中の場所(4.2)とその結果、式の値を決める。生成したコードは、この解析が示せなかった場所を確かめる。
- **呼ぶ場所での読み直し**:読み直す関数を呼ぶところでは、呼ばれる関数の本体を、その呼び出しが渡す値(引数の宣言の単位で読んだもの。引数の無い引数は宣言した値)で、もう一度解析する。答えの抽象の値は、この読み直しの答えを、宣言した答えの範囲と重ねたもの(`cut`)である。読み直しで見つけた場所と、読み直しの中の呼び出しは、呼ばれる関数の場所の一覧には入れず、呼んだ関数の `rplaces` と `rcalls` に入れる(`ranges.rs` の `reread`)。それが、その呼び出しのエフェクトを決める(26.6)。読み直しの値は、呼ばれる関数の式の値にも入れない。
- **深さと輪**:読み直しの中でまた読み直す関数を呼べば、それも読み直す。入れ子は 4 段まで(`REREAD`)で、それを越えた呼び出しと、呼んだ関数の呼び出しの輪の中の呼び出しは、一度だけの解析の答えを使う。読み直す本体は、呼ばれる関数の輪の中で解析する。
- 別のモジュールの総称の関数も、同じに読み直す。そのモジュールの関数の答えは、そのモジュールの解析の答えである。
```kn
fn cheapest(fees: list[money[USDc]] count >=1 <=5 range >=49USDc <=5000USDc) -> money[USDc] range >=49USDc <=5000USDc total
first_or(xs: fees.sort(), fallback: 0USDc) # 読み直すと 49〜5,000 セント(要素が 1 つ以上なので none の分岐に来ない)。一度だけの解析では何でもありうる
```
理由:読み直しは、その呼び出しの値の上からの見積もりで本体を解析し直すだけなので、範囲の解析の健全性はそのまま言える(Lean の `reread_case`。26.9)。一度だけの解析とは、答えとエフェクトの精度だけが違う。生成したコードが確かめる場所は一度だけの解析で決まるので、呼ぶ側ごとに生成したコードを変えずに済む。
**捨てたもの**:型の引数ごとに、引数の値から答えの値への対応をまとめて覚える形(関数の要約)。型の引数を持つ値は、引数から運ばれるだけであること(パラメトリシティ)に頼ることになり、型を持たない Lean のモデルでは、それを示す定理が別に要る。
### 26.6 エフェクト
総称の関数そのもののエフェクト(`check` の最後の行、見出しの E401、`doc` のページ)は、一度だけの解析の場所の種類と、呼ぶ関数のエフェクトから決まる。
**決定(読み直した呼び出しのエフェクトは、読み直しで決める)**:読み直した呼び出しが呼ぶ側のエフェクトに入れるのは、その読み直しで示せなかった場所の種類と、読み直しの中の呼び出し(読み直さなかったもの)のエフェクトである。読み直さなかった呼び出し(4 段を越えたものと、輪の中のもの)は、呼ばれる関数そのもののエフェクトを入れる。E401 の注は、読み直しの場所と呼び出しを、それと分かる文で言う(実物。`generic.kn` の `far` に `total` と書いたもの):
```text
error[E401]: generic.kn:152:1: `far` is written `total`, but it can fail with `overflow`
152 | fn far(n: number range >=0 <=5) -> number total
= A function read again calls `bump`, whose effect is `fails overflow`
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
```
`generic.kn` の `bump[T](x: T, n: number) -> number`(`n + 1`)は、それだけでは `fails overflow` で、0 から 5 の数を渡して三段で届く `near` は total、五段で届く `far` は `fails overflow` である。
理由:読み直しは、その呼び出しの値の上からの見積もりなので、そこで示せた場所は、その呼び出しでは失敗しない。呼ばれる関数そのもののエフェクトを入れると、渡した値ではあふれない呼び出しも、いつも `fails overflow` になる。生成したコードは一つの関数を全部の呼び出しで使うので、一度だけの解析が示せなかった場所は確かめるが、読み直しで示せた呼び出しでは、その確かめは失敗しない。
### 26.7 生成したコード
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| `fn first_or[T](…)` | `export function _first_or<T>(…)` | `T = TypeVar("T")` と `def _first_or(*, xs: list[T], fallback: T) -> T` | `func FirstOr[T any](…)` | `pub fn _first_or<T: Clone + PartialEq>(xs: &[T], fallback: &T) -> T` |
| `fn largest[K: key](…)` | `export function _largest<K>(order_K: kaname.Order<K>, xs: readonly K[]): K \| null` | `def _largest(key_K: Callable[[K], Any], /, *, xs: list[K]) -> K \| None` | `func Largest[K comparable](orderK func(a, b K) int, xs []K) *K` | `pub fn _largest<K: Ord + Clone>(xs: &[K]) -> Option<K>`(順は渡さない) |
| `record Page[T]` | `interface Page<T>` | `@dataclass(frozen=True) class Page(Generic[T])` | `type Page[T any] struct` | `pub struct Page<T: Clone + PartialEq>` |
| `enum Found[T]`(直和型) | `type Found<T> = FoundHit<T> \| FoundMiss<T>` と値ごとの `interface FoundHit<T>` | `Found = Union[FoundHit[T], FoundMiss[T]]` と値ごとの `class FoundHit(Generic[T])` | `type Found[T any] interface { isFound(T) }` と値ごとの `type FoundHit[T any] struct` | `pub enum Found<T: Clone + PartialEq> { Hit(FoundHit<T>), Miss(FoundMiss) }` と値ごとの `pub struct FoundHit<T: Clone + PartialEq>` |
| `result[T, E]` | 補助のモジュールの `Result<T, E>`(`{ kind: "ok", value }` か `{ kind: "err", error }`) | 補助のモジュールの `Ok`・`Err` と `Result = Union[Ok[T], Err[E]]` | 補助のモジュールの `Result[T, E any]`(`isResult(T, E)` のメソッドを持つ interface)と `Ok[T, E]`・`Err[T, E]` | 標準の `Result<T, E>`(`Ok(x)` と `Err(e)`) |
- **中の関数の名前と見せ方**:型の引数を持つ関数は、入口のある公開の関数を持たず、一つの関数だけを生成する。ほかのモジュールの生成物から呼ぶので外に見せる(TypeScript は `export`、Python はモジュールの `_first_or`、Go は大文字で始める `FirstOr`、Rust は `pub fn _first_or`)。ドキュメントのコメントに「kaname の生成したコードが呼ぶ関数で、入口で引数を確かめない」と書く。
- **型の引数の名前**:別名をそのまま使い、モジュールのコードの名前、出力先の語、型の名前(TypeScript の `ReadonlyMap`、Python の `typing` の名前、Go の `any`、Rust の `Option` や `i64`)と重なれば、TypeScript と Python と Rust は `_` を、Go は番号を付ける(`T_`、`T2`)。Python は型の変数をモジュールに一度だけ宣言し、同じ別名の型の引数は宣言のあいだで同じ変数を使う。Python 3.10 で動く書き方(`TypeVar` と `Generic`)である。
- **値の持ち方**:型の引数の値は、受け渡す形(9 章)で持つ。総称の関数を呼ぶところは、具体的な型を当てた引数の型で値を受け渡す形にし、答えを具体的な型で読む。TypeScript と Go と Rust の呼び出しは、型の引数をいつも書く(`_first_or<units.USDc>(…)`、`FirstOr[units.USDc](…)`、`_first_or::<units::USDc>(…)`。Go は答えの型から型の引数を決めない)。Python は書かず、mypy が引数と置かれた場所から決める。
- **比べ方を渡す**:鍵に限った型の引数(`K: key`)ごとに、その順を、隠れた一つ目の引数として渡す(TypeScript の `kaname.Order<K>`、Python の鍵の関数 `Callable[[K], Any]`(位置で渡す引数。`/` の前)、Go の `func(a, b K) int`)。`key` と書いた型の引数には、本体が順を使うかによらず、いつも渡す。呼ぶ側は、具体的な型の順(TypeScript の `kaname.cmpInt`・`kaname.cmpStr`・`kaname.cmpBool`・`kaname.byRank(_rank1)`、Python の `kaname.natural` と `_rank1`、Go の `kaname.CmpInt[units.USDc]`・`kaname.CmpStr`・`kaname.Compare`・`kaname.ByRank(_rank1)`)か、自分が受け取った順を渡す。Go の `K` は、辞書と集合の鍵にするために `comparable` にする(比べには使わない)。ほかの型の引数は `any` である。Rust は順を渡さず、`K` を `K: Ord + Clone` にする。鍵にできる型の `Ord` が鍵の順だからである(36.2)。
- **鍵に限った型の引数を持つレコードの順**:`Pair[K]` や `indexed[K]` を鍵にして並べる総称の関数の本体では、レコードの順が、関数の受け取った `K` の順を使う。TypeScript と Python は、使うところに書く関数(`(a: Pair<K>, b: Pair<K>): number => order_K(a.key, b.key) || kaname.cmpInt(a.rank, b.rank) || 0`、`lambda r: (key_K(r.key), r.rank,)`)で、Go は、モジュールに一度だけ作る関数(`func _cmp1[K comparable](orderK func(a, b K) int) func(a, b Pair[K]) int`)を、受け取った順で呼ぶ(`_cmp1[K](orderK)`)。gofmt は、式の中に書いた関数の本体を行に分けるので、Go では使うところに書かない。Rust では何も書かない。レコードの `derive(Ord)` がフィールドを宣言の順に比べ、`K` の順もその `Ord` だからである(`sort_by(p => p)` は、レコードを鍵にした `kaname::by_key`)。
- **型の引数の値の等しさ**:渡さずに、中身で比べる補助の関数を使う。TypeScript は `kaname.eq`(23.8)、Python と Rust は `==`(Rust は `T: PartialEq` の境界)、Go は補助のモジュールの `kaname.Equal`(リフレクションで中身を比べ、値の無いリスト・辞書・集合は `nil` かどうかによらず同じとする。`reflect.DeepEqual` はそれを分けるので使わない)である。型の引数を含む型(`list[T]`、`Page[T]`)の等しさも同じで、Go の型ごとの比べの関数(`_eq1`)は、型の引数を含まない型にだけ作る。
- **総称のレコードと直和型**:TypeScript は型の引数を持つ `interface` と union、Python は `Generic` のデータクラスで、作るところで型の引数を書き(`Page[str](items=…, next=None)`、`kaname.Ok[int](value=…)`。mypy は `Ok(value=[])` の型を決めないため)、`isinstance` は型の引数の無いクラスで分ける。Go は型の引数を持つ型で、直和型の印のメソッドは型の引数を引数に持つ(`isFound(T)`。型の引数の違う二つの型が、同じ interface を満たさないようにするため)。Rust は型の引数を持つ構造体と列挙で、型の引数に `Clone + PartialEq` の境界を付け、作るところで型の引数を書く(`FoundHit::<T> { … }`、`Page::<T> { … }`)。値の構造体は、フィールドが使う型の引数だけを持つ(`FoundMiss` は持たない。Rust は使わない型の引数を受け付けない)。
- **`result` を作ること、分けること**:TypeScript は `{ kind: "ok", value: x }`、Python は `kaname.Ok[A](value=x)`、Go は `kaname.Ok[A, E]{Value: x}`。`match` は直和型と同じで、TypeScript は `kind`、Python は `isinstance(v, kaname.Ok)` で分け、Go は型の switch(`case kaname.Ok[A, E]:`)である。名前を付けた値は、`value` と `error` から読む。Rust は標準の `Ok(x)` と `Err(e)` で作り、`match` で分ける。`try` も `?` でなく `match` で書き、`err` の値をそのまま答える(36.2)。
- **入口と、場所の確かめ**:総称の宣言には、宣言ごとの確かめ(`_outside_Page`、Go の `KanameOutside`)を作らない。ホストの関数が具体的な型を当てた型(`Page[Line]`)を受け取るところは、その型ごとの確かめの関数(`_outside3` など)を作り、型の引数を当てたフィールドを確かめる。`result` は、`ok` と `err` の中の値を確かめる。総称の関数の本体で確かめる場所の関数は、型の引数を宣言する(TypeScript の `<T>`、Go の `[T any]`、Rust の `<T: Clone + PartialEq>`)。
`tests/modules/results.kn` の `parse_item`(`let n = try parse_qty(text: qty)`)と、`tests/modules/generic.kn` の `cheapest` の終わり(`first_or` を呼ぶところ)から生成した Rust(実物):
```rust
fn _parse_item(sku: &str, qty: &str) -> Result<Item, String> {
let _t1: Result<i64, String> = _parse_qty(qty);
match &_t1 {
Err(r#try) => {
Err(r#try.clone())
}
Ok(n) => {
if kaname::cp_count(sku) == 0 {
Err("no sku".to_string())
} else {
Ok(Item { sku: sku.to_string(), qty: *n })
}
}
}
}
```
```rust
let _t6: units::USDc = _first_or::<units::USDc>(&_t5, &units::USDc(0));
units::USDc(_t6.0)
}
```
### 26.8 診断
- E112(型の引数が決まらないか、合わない):決まらない、二つの型が当たる、数が合わない、値の無いことがある型を当てた、`ok`・`err` の置かれた場所の型が `result` でない。
- E114(`key` の要る使い方):鍵に限っていない型の引数の値の大小、並べ替え、辞書と集合の鍵。`key` の型の引数に鍵にできない型を当てた。
- E115(`try` を書けない場所):答えに続かないブロック、組み込みの関数に渡す関数の中、`result` でも値の無いことがある値でもない値、関数の答えの型が `result` でも値の無いことがある型でもない、失敗の型が違う。
- E108(型の引数の値の範囲)、E105(型の引数の値の算術、`match`、フィールド)、E103(丸めていない値を型の引数に渡した)。
- `try` は名前にできない語である(E002。十四語)。`ok` と `err` という名前の関数は E006 である。
```text
error[E115]: example.kn:7:11: The `err` of the `try` holds string, and the `err` the function answers holds list[string]
7 | let h = try half(n: n)
= `try` makes the `err` the function's answer as it is, so the two `err`s hold values of one type: split it with a `match`, and make the `err` of the function's type.
```
### 26.9 Lean
- **モデルが持つもの**(`KanameCore`):関数が読み直す関数か(`Fn.reread`。書き出しの `"reread"`)と、解析の読み直しの深さ(`ACtx.depth`)。呼び出しの解析(`Analysis.lean` の `evI` の `call`)は、読み直す関数(`rereads`:読み直す関数で、深さが `REREAD`(4)より浅く、呼んだ関数の輪の中でない)なら、引数を場所として持ったあと(`callStep`)、呼ばれる関数の本体を、引数の値で始める範囲(`rereadCtx`:引数ごとに最初の引数を単位で読んだもの、無ければ宣言した値。呼ばれる関数の輪の中で、一段深い)で解析し、答えを関数の解析と同じに読む(`rereadEnd`:答えの単位で読み、答えの範囲の場所を持ち、宣言した答えと重ねる)。読み直しの場所と呼び出しは、呼んだ関数の場所と呼び出しにそのまま入り、式の値と規則の呼び出しの関係は入れない。Rust は場所を `rplaces`・`rcalls` に分けて持つが、エフェクトは両方から決めるので、モデルの関数のエフェクトと同じになる。
- **定理**:`SoundReread.lean` の `reread_case`。読み直した呼び出しの答えは、読み直しの答え(宣言した答えと重ねたもの)に入り、呼び出しの中の失敗は、読み直しの示せなかった場所か、読み直しの中で呼んだ関数の失敗である。本体の評価は呼び出しより一歩短いので、`SoundAt` を、一歩少ない評価の全部について言う形(`sound_upto`)にし、`sound_all` を強い帰納法で示した。読み直しの範囲が評価の環境を持つこと(`sat_reread`)は、呼び出しの引数の値が解析の値に入ることから言う。`total_sound` は変えずに通る。読み直しで全部の場所を示した呼び出しは、その場所も呼び出しも呼んだ関数のものなので、呼んだ関数のエフェクトが total なら失敗しない。`sorry` も公理の足しも無い(`tests/proofs.rs`)。
- **書き出し**(`crates/ritsu-model/tests/kaname.rs`):モデルは型を持たない。型の引数の値の引数は、Rust の解析と同じく何でもありうる値(`Abs.any`)で書き出し、総称の関数の呼び出しの引数は、型の引数を当てた引数の型で読む。`ok x` と `err e` は、`result` の値に名前を付ける値のパターン(`Pat.value`)と、その名前から `value` か `error` を読む式に書き出す。名前の値の式は、モジュールに無い番号で、突き合わせから外す。鍵に限った型の引数の値の順は、モジュールとその規則の列挙の値を宣言の順につないだ一つの順で書き出す(数、日付、文字列、真偽は、モデルが値で比べる)。突き合わせは、Rust の関数の場所に、同じモジュールの読み直しの場所を合わせて比べる。
- **壊して落ちることを確かめた場所**:読み直しの深さを 3 にすると(`generic.kn` と `generic.ja.kn` で 3 件ずつ。`hop1` と `far` の場所)、読み直しの答えを宣言した答えと重ねないと(2 件ずつ。`plot` の答えと、その式の値)、突き合わせが落ちる。
### 26.10 テスト
- 材料:`tests/modules/generic.kn` と `generic.ja.kn`(40 の関数ずつ。`first_or`、`Page[T]`、`Found[T]`、`largest` と `least` と `tally`(`K: key`)、二つの頁の比べ、鍵に限った型の引数のフィールドを持つレコードを鍵にした並べ替えと `max_by`(`sorted_pairs`、`best_pair`、`ranked` と、それを列挙と文字列で呼ぶ `pairs_by_tier`、`best_by_sku`、`ranked_tiers`)、具体的な型を当てた総称のレコードと列挙を受け取る関数、読み直しが答えとエフェクトを細かくする呼び出し(`cheapest`、`near`)、4 段を越える呼び出し(`far`)、輪の中の呼び出し(`ring`)、読み直しの答えを宣言した答えと重ねる呼び出し(`plot`)、鍵に限った型の引数と関数の型の引数で辞書を作る `index_by` と、それで明細を品番ごとにする `lines_by_sku`(27 章))。`tests/modules/results.kn` と `results.ja.kn`(13 の関数ずつ。`result` と `try` の検証の手順、失敗がリストのもの、値の無いことがある値の `try`、答えに続く分岐の `try`、`result` の `match`・リスト・比べ)。`tests/modules/glib.kn` と `gcalls.kn`(ほかのモジュールの総称のレコード・列挙・関数を呼ぶこと。英語だけ)。
- `tests/types.rs` の `generics_results_and_try`(型の引数の決め方と、E112・E114・E105・E108・E115)、`tests/effects.rs` の `a_call_read_again_takes_the_effect_of_that_reading`、`tests/codes.rs` の `the_cases_of_generics_and_results_say_their_codes`(`tests/golden/codes/generics.*.txt`)。範囲の解析の健全性(`tests/ranges.rs`。型の引数の値の引数には数を、`result` の引数には `ok` か `err` を引く)、出力先の突き合わせ(16.4)、Lean との突き合わせ(15.3)に入る。
## 27. 関数の値(自分で書く高階の関数)
### 27.1 書き方と、置ける場所
```kn
fn total_by[T](xs: list[T], amount: (T) -> money[USDc]) -> money[USDc]
xs.sum(x => amount(x))
fn apply_all[T](x: T, steps: list[Adjustment] count <=10, adjust: (T, Adjustment) -> T) -> T total
steps.reduce(x, (acc, s) => adjust(acc, s))
fn order_total(lines: list[Line] count <=50) -> money[USDc]
let line_total = (l: Line) => l.unit * l.qty
total_by(xs: lines, amount: line_total)
fn adjusted(price: money[USDc] range >=0USDc <=100_000USDc, steps: list[Adjustment] count <=10) -> money[USDc]
apply_all(x: price, steps: steps, adjust: adjust_price)
```
- **関数の型**:`(T1, T2) -> R`。引数の無い関数は `() -> R` である。答えの型のあとにエフェクトを書ける(`(T) -> R total`、`(T) -> R fails range, overflow`)。引数のリストの中では、種類のあとの `,` は、次の語が種類で、そのあとに `:` も `(` も無いときだけ種類を続ける(`f: (T) -> U fails rule, zero: number` の `zero` は次の引数)。書かなければ、どのエフェクトの関数も渡せ、そのエフェクトは渡した側に伝わる(27.5)。関数の型は、ほかの型の中(リストの要素、関数の型の引数と答え、オプショナルな値)に書けない(E113)。
- **ラムダ**:`x => 式`、`(a, b) => 式`、`() => 式`。引数の型は、置かれた場所の関数の型から決まる。決まらないところ(型を書かない `let`)では、`(l: Line) => …` と書く(無ければ E112)。書いた型は、置かれた場所の関数の型と合わなければならない(E102)。本体は式一つで、長くなるなら名前を付けた関数にする。ラムダの引数は、その場で見えるほかの名前と重なれない(E006)。
- **名前で渡す**:モジュールの関数は、関数の型が置かれた場所に名前で書ける(`total_by(xs: lines, amount: line_cost)`、`lines.map(line_cost)`、`use module` で読んだ `pricing.line_cost`)。置かれた場所の引数を、宣言の順に関数の引数として受け取る。総称の関数は、置かれた場所の型から型の引数を決める。組み込みの関数は値にできない(E109。ラムダで包む)。関数を取る関数も値にできない(E113。関数の型は関数の型を引数に取らないため)。
- **呼ぶ**:関数の値は、引数を順に並べて呼ぶ(`amount(x)`)。名前を付けて渡せば E107 である(名前で書くのは、宣言した関数と規則の呼び出しである)。関数の型の引数と、関数の値を名前に付けた `let` は、モジュールの関数と同じ名前にできない(E006。`f(…)` がどちらを呼ぶかが読めなくなるため)。
- **捕まえる名前**:ラムダは、書いたところで見える名前(引数、`let`、ほかのラムダの引数)の値を使える。値は書き換えられないので、作ったときの値のままである。
**決定(関数の値は、渡すことと呼ぶことにだけ使う)**:関数の値を置けるのは、次の三つだけである。
1. 関数と組み込みの関数の、関数の型の引数(受け取った関数の値を、ほかの関数にそのまま渡すことも含む)
2. `let` の値(`if` と `match` の答え、ブロックの答えとして選んだ関数の値も)
3. 呼び出される位置
関数の答え(`-> (T) -> U`)、レコードと列挙の値のフィールド、リスト・辞書・集合の要素、オプショナルな値、`=` と `!=`、型の引数、`examples` の答えに、関数の型を書くか関数の値を置けば E113 である。型の引数に関数の型が当たるところは、型の引数の決まりとして E112 で言う。関数の型の引数を持つ関数は中の関数(21.3)なので、ホストが呼ぶ関数の引数と答えにも関数の型は来ない。
```text
error[E113]: example.kn:3:38: The answer of a function cannot be of a function type
3 | fn adder(n: number range >=0 <=9) -> (number) -> number
= A function value is given as an argument of a function or a builtin, named by a `let`, and called, and goes nowhere else: not to a function's answer, a field of a record or of a value of an enum, an element of a list, a dict or a set, a value that may be none, nor `=` and `!=`. Instead of a function that answers a function, write one that takes the argument too, or write `x => f(x: x, n: n)` where it is called.
```
理由:こうすると、ラムダの本体は、それを書いた関数の実行の中で、それを渡した呼び出しが続いているあいだにしか評価されない。範囲の解析は、呼ぶ場所で渡した関数の中まで読めば(27.4)、ラムダの本体が評価されるどの場面も読んだことになる。関数を答えにしたりデータに入れたりできると、ラムダが書いた場所の外に逃げ、どこで呼ばれるかを追う解析(関数の値の行き先を集める解析)と、その健全性の定理が要る。
**捨てたもの**:関数を答える関数(`fn adder(n) -> (number) -> number`)と、関数のリスト(`list[(Cmd) -> string?]`)。前者は、引数を足した関数を書くか、呼ぶ側で `x => add(x: x, n: n)` と書けば足りる。後者は、検証の関数を順に呼んで答えをリストにする(`[check_name(c: c), check_email(c: c)].filter_map(x => x)`)。
### 27.2 型の決まり
- 関数の型が合うのは、引数の数と型、答えの型が同じときである(お金と量は単位も同じ。丸めていない値は答えにできない)。合わなければ E102 である。名前で渡した関数は、置かれた場所の引数がその関数の引数に入り、その答えが置かれた場所の答えに入れば渡せる。
- 関数の型にエフェクトを書いた引数には、そのエフェクトに収まる関数だけを渡せる(E402。27.5)。
- 関数の型の引数を持つ関数は、中の関数である(21.3)。読み直す関数でもある(27.4)。
- 総称の関数を呼ぶところで、ラムダの引数の型は、ほかの引数が決めた型の引数を当てた型である。ラムダを渡す引数は、ほかの引数のあとに読む(`total_by(xs: lines, amount: l => l.unit * l.qty)` の `l` は、`xs` が決めた `T` の `Line`)。
```text
error[E102]: example.kn:7:13: The parameter `s` of the lambda is of string, and the function type of the place says number
7 | twice(f: (s: string) => 1, x: 1)
= Two function types are one where their parameters are as many and of the same types, and their answers of one type (amounts and quantities in one unit). A function given by its name goes where each parameter of the place goes into its own, and its answer into the place's.
```
### 27.3 参照インタプリタ
- **値**:ラムダの値は `Value::Closure`(モジュール、ラムダの式の番号、作ったときに見えていた名前の値)である。
- **決定(名前で渡した関数は、それを呼ぶラムダにする)**:型の検査のあとで、値として書いた関数の名前を、その関数を呼ぶラムダに読み替える(`src/wrapped.rs`)。`adjust_price` は `(#1, #2) => adjust_price(price: #1, a: #2)` になる。組み込みの関数に名前で渡した関数の値(`xs.map(f)`)も、組み込みの関数のラムダ `(#1) => f(#1)` にする。`#` はどの名前の綴りにもならないので、ほかの名前と重ならない。参照インタプリタ、範囲の解析、生成は、ラムダだけを読む。呼び出しの失敗(関数の引数の範囲の外)は名前を書いた行で言い、その場所は、名前を書いた関数の場所である。値の形として `Value::Fn`(モジュールと関数)は置かない。値の形が一つで済み、範囲の解析も Lean のモデルも、名前で渡した関数を特別に扱わずに済むからである。
- 関数の値を呼ぶことは、呼び出しの深さ(8 章の 200)に一段として数える。生成した Python で、関数の値の呼び出しが Python の一段になる(27.6)ので、深さの決まりが出力先とそろう。組み込みの関数に、その場で書いたラムダ(`xs.map(x => …)`)と、名前で渡した関数の値を呼ぶ組み込みの関数のラムダは、段を使わない。名前で渡した関数を呼ぶラムダは、その関数の呼び出しで一段、ラムダの呼び出しで一段を使う。
- 評価の順:呼ぶ値、引数を左から評価し、引数を関数の型の引数の単位で読んでから呼ぶ。本体の答えは、ラムダの答えの型の単位で読む。
- 関数の型の引数を持つ関数は、中の関数なので `run` で流さない(21.3)。関数の値は、`run` の入力にも答えにもならない。
```text
error: `via` takes a function, an inner function no host calls, so it is not run: run a function that calls it
```
### 27.4 範囲の解析
- **抽象の値**:`Abs::Fns(ラムダの集まり, 見えない関数)`。ラムダの一つずつが、式の番号と、それを書いた関数(`owner`)と、そのラムダを作った解析の枠(`frame`。その関数自身の解析か、ほかの関数の読み直しか)と、作ったときの名前の抽象の値を持つ。見えない関数は、関数の型の引数のように、解析が中身を知らない関数がありうるかで、その型に書いたエフェクトを持つ。合わせるときは、ラムダの和をとる。関数の値はデータに入らないので、集まりはいつも、その関数の中で書いたか受け取った、数えるほどのラムダである。
- **関数の値の呼び出し**:集まりのラムダを一つずつ、呼び出しの引数の抽象の値で解析し、答えを合わせる。ラムダは、捕まえた名前の抽象の値を戻してから本体を解析する。ラムダの本体の場所は、**そのラムダを書いた関数の場所**として、その関数の解析の答えに入れる(同じ場所に来る値は合わせる。5.7)。同じ枠で読むラムダはその場で、ほかの枠(ラムダを書いた関数が、それを渡した関数を読み直しているところ)では、書いた関数の枠に渡して読む。ラムダの本体の式の値は、読み直しが読んでも外さないように分けて持ち、関数の解析の終わりに式の値に合わせる。見えない関数の呼び出しは、答えの型の全体(型の端の印つき)を答え、場所を作らず、型に書いたエフェクトをその関数のエフェクトに入れる(27.5)。
- **一度だけの解析**:関数の型の引数は、見えない関数で始める。関数の型の引数を呼ぶところは、答えの型の全体を答え、場所を作らない。その関数がどの関数を渡されるかは、呼ぶ側が読み直すときに分かるからである。
- **呼ぶ場所での読み直し**:関数の型の引数を持つ関数は読み直す関数で、呼ぶところでは、渡した関数の集まりを引数に置いて、本体を読み直す(26.5 と同じ深さの決まり)。呼ぶ側が書いたラムダの本体は、読み直しの中で、呼ばれる関数がそれを呼ぶときの値で解析され、その場所が呼ぶ側の場所になる。
- **読み直さないとき**:4 段を越えたときと、呼び出しの輪の中では、読み直さない。そのとき、渡したラムダの本体を、引数をラムダの引数の型の全体(型の端の印つき)にして一度解析し、その場所を呼ぶ側の場所にする。どのラムダの場所も、どこかで必ず解析される。呼び出しの答えは、関数を取る関数の一度だけの解析の答えでなく、宣言した答えである(一度だけの解析は、見えない関数の答えを型の全体と見ただけだからである)。
```kn
fn apply_each[T](xs: list[T], f: (T) -> T) -> list[T]
xs.map(x => f(x))
fn shifted(xs: list[number] count <=5 range >=0 <=9) -> list[number]
apply_each(xs: xs, f: x => x * 1_000_000_000_000_000_000) # 読み直すと x は 0〜9 で、積は int64 に収まると示せる
```
`hof.kn` の `deep` は、型の引数を持つ関数を五段(`hop1` から `hop5`)通してラムダ `y => y * 2` を呼ぶ。四段で読み直しが止まり、`hop5` の呼び出しは読み直さないので、ラムダは `y` をどの数にもして一度読まれ、あふれうる(`fails overflow`)。
理由:27.1 の決まりで、ラムダの本体が評価されるのは、それを渡した呼び出しの中か、それを `let` に入れた関数の中だけである。読み直しとその代わりの解析は、その全部の場面を上から見積もるので、範囲の解析の健全性が保たれる(27.8)。
### 27.5 エフェクト
**決定(渡した関数のエフェクトは、渡した側に伝わる)**:
- 関数**自身のエフェクト**は、本体の示せない場所(そこに書いたラムダの本体の場所も)、呼ぶか名前で渡す関数のエフェクト、呼び出しの輪から決まる(4.3)。関数を名前で渡すことは、呼び出しの輪を求めるときも、呼ぶことと同じに数える(`hof.kn` の `steps_down` は、自分を `via` に渡すので、自分を呼ぶ輪の中にあり、`diverges` である)。
- 関数の型にエフェクトを書いた引数を呼ぶところは、そのエフェクトを、その関数自身のエフェクトに入れる(`sum_with` の `f: (number) -> number fails overflow` を呼ぶ `sum_with` は `fails overflow`)。
- エフェクトを書かない関数の型の引数を呼ぶところは、その関数自身のエフェクトに入れない。その引数を通るエフェクト(`through`)として持つ。渡す側では、渡したラムダの本体の場所は初めから渡す側の場所であり、名前で渡した関数は渡す側が呼ぶ関数に数えるので、どちらも渡す側のエフェクトに入る。
- 見出しの `total` や `fails …` は、その関数自身のエフェクトと比べる(E401)。`apply_all` は、自身の場所を持たないので `-> T total` と書け、失敗しうる関数を渡せば、その場所は渡した側のエフェクトに入る。`total_by` は、要素の数を書かないリストの和があふれうる(型の端から来たあふれ。22.2)ので、自身のエフェクトが `fails overflow` である。それでも `order_total` からの呼び出しは、読み直すと、要素が 50 以下で、一つずつの額も `Line` のフィールドに書いた範囲から決まるので、和があふれないと示せる。そのとき `order_total` のエフェクトに `overflow` は入らない(26.6)。
- 関数の型にエフェクトを書いた引数に、そのエフェクトに収まらない関数を渡せば E402 である。ラムダのエフェクトは、その本体の示せない場所と、本体が呼ぶ関数のエフェクトで、名前で渡した関数のエフェクトはその関数のものである。
- `check --format json` と `api` の、関数の型の引数を持つ関数のエフェクトに、`"through": ["amount"]`(エフェクトを書かない関数の型の引数)を書く。`doc` のページは、関数を取る関数に、「`amount` に渡した関数のエフェクトは、呼ぶ側に伝わります」か、どの引数にもエフェクトを書いていれば「渡す関数は、引数の型に書いたエフェクトに収まります」と書く。
```json
{"function": "total_by", "inner": true, "effect": {"fails": ["overflow"], "diverges": false, "through": ["amount"]}, "written": null, "places": [{"line": 24, "kind": "overflow", "check": "kaname", "code": null, "outcome": "unbounded"}], "calls": []}
```
```text
error[E401]: example.kn:6:1: `bumped` is written `total`, but it can fail with `overflow`
6 | fn bumped(xs: list[number] count <=9) -> list[number] total
= Line 7: the value of `x + 1` can leave int64, as it comes from a value whose range is not written (`overflow`)
= Make the places shown (narrow the values with an `if` before they go there), or write a wider effect.
error[E402]: example.kn:7:28: The parameter `test` of `all_within` writes `total` on its type, and the function given does more
7 | all_within(xs: xs, test: x => x + 1 > x)
= The function given is `fails overflow`.
= Make the places of the function given shown (narrow the values with an `if` in the lambda), or widen the effect written on the parameter's type.
```
理由:エフェクトの変数(`fn total_by[T, e](…, amount: (T) -> money[USDc] e) -> money[USDc] e`)を書かせずに、渡した関数ごとに正しいエフェクトを、渡した側の関数に出せる。中の関数はホストから呼ばれないので、ホストに約束する `total`(4.5)は、ホストが呼ぶ関数の、渡した関数を含めたエフェクトである。
### 27.6 生成したコード
| kaname | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| 関数の型 `(T) -> U` | `(x: T) => U` | `Callable[[T], U]` | `func(T) U` | `&dyn Fn(&T) -> Result<U, kaname::KanameError>`(`total` と書いた型は `-> U`) |
| ラムダ | アロー関数(本体は文の塊) | その文の前に書く入れ子の `def _t1(…)` | 関数のリテラル | 借りたクロージャ(`&\|l: &Line\| -> Result<…> { … }`。本体は文の塊) |
| 名前で渡した関数 | その関数を呼ぶアロー関数 | その関数を呼ぶ入れ子の `def` | その関数を呼ぶ関数のリテラル | その関数を呼ぶクロージャ |
| 関数の値を呼ぶ | `f(x)` | `f(x)` | `f(x)` | `f(x)?`(`total` と書いた型は `f(x)`) |
- ラムダの本体の式は、範囲の確かめや確かめつきの算術の文を要することがあるので、Python では式の `lambda` でなく、入れ子の `def` にし、TypeScript のアロー関数と Rust のクロージャも文の塊にする。範囲の解析が示した算術は確かめない(`order_total` の `l.unit * l.qty` は、読み直しで 1,000,000 セント以下と示せるので、そのまま掛ける)。
- **決定(Go の関数の型は `func(T) U` にする)**:Go の生成したコードは、kaname の失敗を `*KanameError` の panic で運び、ホストが呼ぶ関数の入口で `kaname.Recover` が `error` にする(4.4、10.4)。関数の値の失敗も同じに運ぶので、関数の型に `error` を足さない。`func(T) (U, error)` にすると、関数の値を呼ぶたびに `error` を確かめる文が要り、ほかの呼び出しと失敗の運び方が分かれる。
- **Rust の関数の型**:Rust の生成物は、kaname の失敗を `Result` で運ぶ(36.4)。関数の値の失敗も同じに運ぶので、関数の型は答えを `Result<U, kaname::KanameError>` にし、呼ぶところは `?` で失敗を渡す。`total` と書いた関数の型だけが `U` を答え、total な関数を渡すところは `Ok(…)` で包む。関数の値は借りて渡す(`&dyn Fn`。36.2)。範囲の解析が失敗しないと示した呼び出し(`order_total` が `total_by` を呼ぶところ)は、起きない `Err` を `unwrap_or_else(|e| panic!("{e}"))` で受ける(36.4)。
- 組み込みの関数に関数の値を渡したとき(`xs.map(f)`)は、その場のループの中で、その関数の値を呼ぶ。
- 関数を取る関数は中の関数で、入口の確かめの無い一つの関数を外に見せる(TypeScript の `export function _total_by`、Python の `_total_by`、Go の `TotalBy`、Rust の `pub fn _total_by`。26.7 と同じ)。
- Go の補助のモジュールの `kaname.Equal`(26.7)は、関数の値を受け取らない。関数の値は `=` にも、データにも、型の引数にも入らないからである(E113、E112)。
`hof.kn` の `order_total` と `bumped` と `adjusted` から生成したもの(実物。TypeScript、Python、Go の順。そのあとに Rust の三つ):
```ts
function _order_total(lines: readonly Line[]): units.USDc {
// 34: let line_total = (l: Line) => l.unit * l.qty
const _t1 = (l: Line): units.USDc => {
return (l.unit * l.qty) as units.USDc;
}
const line_total: (x: Line) => units.USDc = _t1;
const _t2: units.USDc = _total_by<Line>(lines, line_total);
return _t2 as units.USDc;
}
```
```python
def _bumped(*, xs: list[int]) -> list[int]:
def _t1(x: int) -> int:
return kaname.held_int(x + 1, _MODULE, 68, "The value of `x + 1` does not fit in int64")
_t2: list[int] = _apply_each(xs=xs, f=_t1)
return _t2
```
```go
func _adjusted(price units.USDc, steps []Adjustment) units.USDc {
var _t1 int64 = int64(price)
_t2 := func(v units.USDc, v2 Adjustment) units.USDc {
var _t3 int64 = int64(v)
var _t4 units.USDc = _adjustPrice(units.USDc(_t3), v2)
return units.USDc(int64(_t4))
}
var _t5 units.USDc = ApplyAll[units.USDc](units.USDc(_t1), steps, _t2)
return units.USDc(int64(_t5))
}
```
```rust
fn _order_total(lines: &[Line]) -> units::USDc {
// 34: let line_total = (l: Line) => l.unit * l.qty
let _t1 = &|l: &Line| -> Result<units::USDc, kaname::KanameError> {
Ok(units::USDc(l.unit.0 * l.qty))
};
let line_total: &dyn Fn(&Line) -> Result<units::USDc, kaname::KanameError> = _t1;
let _t2: units::USDc = _total_by::<Line>(lines, line_total).unwrap_or_else(|e| panic!("{e}"));
units::USDc(_t2.0)
}
fn _bumped(xs: &[i64]) -> Result<Vec<i64>, kaname::KanameError> {
let _t1 = &|&x: &i64| -> Result<i64, kaname::KanameError> {
kaname::add(x, 1, _MODULE, 68, "The value of `x + 1` does not fit in int64")
};
let _t2: Vec<i64> = _apply_each::<i64>(xs, _t1)?;
Ok(_t2.clone())
}
fn _adjusted(price: units::USDc, steps: &[Adjustment]) -> units::USDc {
let _t1 = &|&v: &units::USDc, v_2: &Adjustment| -> Result<units::USDc, kaname::KanameError> {
let _t2: units::USDc = _adjust_price(units::USDc(v.0), v_2);
Ok(units::USDc(_t2.0))
};
let _t3: units::USDc = _apply_all::<units::USDc>(&units::USDc(price.0), steps, _t1).unwrap_or_else(|e| panic!("{e}"));
units::USDc(_t3.0)
}
```
### 27.7 診断
- E113(関数の値を置けないところ)、E402(渡した関数のエフェクトが、引数の型に書いたエフェクトに収まらない)、E112(ラムダの引数の型が決まらない、型の引数に関数の型が当たる)、E107(関数の値に名前を付けて引数を渡した、引数の数が違う)、E109(組み込みの関数を値にした)、E102(関数の型が合わない、関数の型の引数に関数でないものを渡した)、E105(関数の値でない値を呼んだ)、E006(モジュールの関数と同じ名前の関数の値)。
- `=` と `!=` の両辺が関数の値なら、左の一つだけを E113 で言う。呼ぶ値がもう間違っていると言われた呼び出しは、引数のラムダについて重ねて言わない。
- 実物は `tests/golden/codes/hof.en.txt` と `hof.ja.txt`(`tests/codes.rs` の `the_cases_of_function_values_say_their_codes`)にある。
### 27.8 Lean
- **モデルが持つもの**(`KanameCore`):値 `Val.closure`(ラムダの番号と、作ったときの名前の値)、式 `Expr.lam`(ラムダを値にする)と `Expr.callV`(関数の値を呼ぶ。引数ごとの単位の読み方と、見えない関数の答えの型の全体を持つ)、モジュールのラムダの表 `Lam`(番号、書いた関数、引数とその型の全体、本体、答えの読み方)、関数を取るかの印 `Fn.takesFn`、抽象の値 `Abs.fns`(ラムダごとの番号と名前の抽象の値と、その関数自身の解析で作ったか。見えない関数と、その型に書いたエフェクト)。評価は、関数の値の呼び出しを深さに数え、関数の値でない値を呼べば `type` で止まる(型の検査を通ったモジュールでは起きない)。名前で渡した関数は書き出しの前にラムダになっているので、`Val.fnRef` は無い。
- **解析**:関数の値の呼び出し(`callVAbs`)は、ラムダごとに本体を読み(`lamsAbs`。捕まえた名前の抽象の値と呼び出しの値で、書いた関数の輪の中で)、答えを合わせる。読み直さない関数を取る関数の呼び出しは、渡したラムダを引数の型の全体で一度読み(`onceAll`)、宣言した答えを答える。その関数自身の解析で作ったラムダの本体の式の値は分けて持ち(`Out.lvals`)、関数の解析の終わりに合わせる。見えない関数の呼び出しは、型に書いたエフェクトを持ち(`Out.thru`)、エフェクトは `through` の印を持つ(関数を取る関数か、見えない関数を呼んだか、そういう関数を読み直さずに呼んだか)。
- **定理**:`Mem` は関数の値の二つの場合を持つ。見えない関数がありうる抽象の値はどのクロージャも持ち(`Mem.closAny`)、ラムダの集まりは、そのラムダを作ったときの名前の抽象の値に、クロージャの捕まえた名前の値が入るクロージャを持つ(`Mem.clos`)。`SoundFns.lean` の `case_lam` と `case_callV` が、`ranges_sound` に `lam` と `callV` の場合を足す。関数の値の呼び出しは、本体の評価についての帰納法の仮定(一歩少ない評価)と、捕まえた名前と呼び出しの値が、本体を読む範囲の名前に入ること(`sat_lam`)から言う。定理は `widened`(走る値を手放した `reduce`)の無い解析についてのもので、見えない関数を呼んだ解析も `widened` にして外す。関数を取る関数そのものの答えと場所(`FnSound`、`ResultsOK`)は、関数を取らない関数について言い、関数を取る関数は、呼ぶ側の読み直しを通して確かめる。`total_sound` は、エフェクトが total で `through` の無い関数について言う。ホストが呼ぶ関数は、関数を取る関数を読み直さずに呼ばない限り、`through` を持たない。`sorry` も公理の足しも無い(`tests/proofs.rs`)。
- **書き出し**(`crates/ritsu-model/tests/kaname.rs`):関数の値の抽象の値はラムダの番号の並び(`{"fns": [番号, 見えない関数]}`)で、捕まえた名前の抽象の値はモデルに任せ、ラムダの本体の式の値の突き合わせで確かめる。
- **壊して落ちることを確かめた場所**(四か所。どれも突き合わせが落ちる):読み直しの中のラムダの場所を数えないと、`hof.kn` と `hof.ja.kn` で 15 件ずつ(`order_total`、`order_cost`、`all_doubled`、`adjusted` などの場所)。読み直さないときのラムダの解析を外すと 5 件ずつ(`deep` のエフェクトと場所と式の値)。読み直しの中のラムダの呼び出しを数えない(名前で渡した関数のエフェクトが渡した側に伝わらない)と 1 件ずつ(`all_doubled` のエフェクト)。名前で渡すことを輪に数えない(Rust の `effects::rings` を壊す)と、`steps_down` のエフェクトの `diverges` が Rust だけ落ちて 1 件ずつ食い違い、`tests/effects.rs` も落ちる。
### 27.9 テスト
- 材料:`tests/modules/hof.kn` と `hof.ja.kn`(32 の関数ずつ。`total_by`、`apply_all`、`apply_each`、ラムダを `let` で名前にして渡す `order_total`、名前で渡す `order_cost` と `costs` と `adjusted`、名前で渡す失敗しうる関数 `doubled` を渡す `all_doubled`、読み直しで示せる `shifted`、失敗しうるラムダを渡す `bumped`、エフェクトを書いた関数の型(`all_within` の `total`、`sum_with` の `fails overflow`)、ラムダが捕まえた名前で狭める `per_person`、`if` で選んで呼ぶ `pick`、組み込みの関数に名前で渡す `scaled`、関数を自分に渡す輪の `steps_down`、五段の `deep`、`private` のレコードと列挙と関数を使う `sides`)。`tests/modules/generic.kn` の `index_by`(`K: key` と関数の型の引数で、辞書を作る)。
- `tests/types.rs` の `function_values_and_what_is_private`、`tests/effects.rs` の `the_effects_of_function_values`、`tests/parse.rs` の `function_types_lambdas_and_private`、`tests/names.rs` の `what_is_private_is_its_modules_alone`、`tests/typescript.rs` の `what_is_private_is_hidden_from_the_host`、`tests/codes.rs` の `the_cases_of_function_values_say_their_codes`、`tests/python.rs` の深さの測り方(関数の値を通る `steps_down`)。範囲の解析の健全性(16.1)、出力先の突き合わせ(16.4)、Lean との突き合わせ(15.3)に入る。
## 28. 再帰する型と `private`
### 28.1 再帰する型
```kn
record Category
name : string
children : list[Category]
enum Expr
num
value : number range >=-1000 <=1000
add
left : Expr
right : Expr
mul
left : Expr
right : Expr
neg
inner : Expr
fn size(c: Category) -> number
1 + c.children.sum(x => size(c: x))
fn value_of(e: Expr) -> number
match e
num n => n.value
add a => value_of(e: a.left) + value_of(e: a.right)
mul m => value_of(e: m.left) * value_of(e: m.right)
neg g => 0 - value_of(e: g.inner)
```
レコードと直和型のフィールドの型は、その型自身を、リスト、辞書、集合、オプショナルな値、`result`、ほかのレコードと直和型を通して含んでよい。二つの型が互いを含んでもよい(`tests/modules/trees.kn` の `Doc` の値 `object` のフィールドは `list[Member]` で、`Member` のフィールド `value` は `Doc` である)。輪になる宣言は、どれも一つのモジュールの中にある。モジュールの読み込みは輪にならない(2.5)からである。
**決定(自分を含まない値が一つは作れること)**:自分を含む型には、自分を含まない値が一つは作れなければならず、無ければ E108 である。作れるとは、その型の値を、輪の中の型の値を使わずに作れることをいう。レコードなら、どのフィールドも作れること(`T?` は `none`、要素の数の下の端が 0 のリスト・辞書・集合は空、`result` は `ok` と `err` のどちらかが作れればよい)、直和型なら、フィールドがどれも作れる値が一つはあることである。値の型(`E.node`)は、その値のフィールドがどれも作れることで、同じ直和型のほかの値が作れることでは足りない。輪の外の型は作れるものとして数える(作れなければ、その型の宣言のところで言う)。kaname は、型の輪(強連結成分)ごとに、作れると分かった型から順に広げて求める。`next : Node` で自分を含むレコード、`count >=1` のリストで自分を含むレコード、どの値も自分を含む直和型は E108 である(実物):
```text
error[E108]: example.kn:3:8: Every value of the record `Node` holds another `Node` (through `Node.next`), so none can be made
3 | record Node
= A type that holds itself needs a value that does not: make a field that holds it optional (`T?`) or a list that may be empty, or give the enum a value that does not hold it.
```
理由:作れない型の値は、ホストも kaname も作れないので、その型を引数にとる関数は呼べない。ベクタ(9 章)も基本の値を作れない。
**決定(輪の中では型の引数をそのまま渡す)**:総称の型の輪の中では、型の引数に、型の引数そのものか、型の引数を含まない型だけを渡せる。型の引数から作った型(`Wrap[list[T]]`)を渡せば E108 である。含むたびに型の引数が一段ずつ深くなり(`Wrap[T]`、`Wrap[list[T]]`、`Wrap[list[list[T]]]`、…)、型が限りなく増えるからで、Go もこの形を「instantiation cycle」として受け付けない(確かめた)。型の引数をそのまま渡す形(`Tree[T]` の `children : list[Tree[T]]`)、入れ替えて渡す形(`Swap[T, U]` の `next : Swap[U, T]?`)、型の引数を含まない型を渡す形(`A` の `b : B[number]?` と、`B[T]` の `a : A?`)は、Go も受け付ける:
```text
error[E108]: example.kn:5:11: Where `Wrap` holds itself (`Wrap[list[T]]`), it gives `list[T]`, a type argument made from a type parameter
5 | inner : Wrap[list[T]]?
= Each time the types go round, the type argument grows one deeper (`list[list[T]]`, …), and the types never end; nor can the generated Go write them. Where a type holds itself, give it the type parameters as they are, or types that name none.
```
**値の入れ子の深さ**:ホストから入る値は、自分を含む型の値を 200 段(`interp::NESTED`)まで重ねられる。引数から一つの道をたどり、自分を含むレコードと直和型の値を一つ通るたびに一段と数える(互いを含む `Doc` と `Member` は、どちらの値も一段)。入口で、範囲の確かめの前に数え、200 段を超える値があれば `input` の失敗にして、その型の名前を言う(「The argument `c` of `size` nests `Category` more than 200 deep」。8 章)。kaname の中で作る値は、`reduce` などで、もっと深くできる。その値を比べたり受け渡したりするときに出力先のスタックの限りに当たることは、メモリが尽きることと同じく、言語の意味の外に置く(ベクタと突き合わせの値は浅い)。
**決定(入口の深さは 200)**:生成したコードは、入口の確かめと等しさで、値を再帰でたどる。生成した Python で、200 段の値を二つ受け取って比べる `trees.kn` の `same` は、Python の再帰の上限が 405 あれば答え(100 段なら 205)、既定の 1,000 の中に収まる(`tests/python.rs`。16.4)。`Category` の入口の確かめは、値の一段を、型の関数とリストの関数の二段の呼び出しでたどる。kaname の中で作る値にも上限を置くには、値ごとに深さを覚えておく形が要り、ホストに見える型が変わるので、置かない。
**決定(呼ぶところでは中を確かめない)**:kaname の関数がほかの kaname の関数を呼ぶところでは、自分を含む型の値の中を確かめない(参照インタプリタの `Hold::Call`。生成したコードは、呼ぶところの確かめ `_outside_<型>`・`KanameOutside` を自分を含む型には書かない)。その値のフィールドは、作るところで宣言した範囲と要素の数を、入口ではどの深さのものも、確かめてあるからである。呼ぶたびに値の全体をたどると、木をたどる再帰の関数は、一段ごとに下の全部を確かめ直し、手間が値の大きさの二乗になる。ただし、範囲を書かない刻みのある率を 0% から 100% と読む決まり(3.9)は、ホストとの約束で、作るところでは確かめない。kaname の中で作った値では 100% を超えうるので、範囲の解析は、自分を含む型の値の中ではこの決まりを前提にしない(5.1)。`trees.kn` の `wide_shares` は、500% の取り分を作って `shares` に渡し、答える。
- 自分を含む型をたどる関数は、再帰を持つので `diverges` である(P8。止まることは確かめない)。
- **範囲の解析**:自分を含む型の値は、一つの道の上で同じ型の値を二つまでたどり(その値と、その中の一つ)、その先を何も分からない値にする(`ranges.rs` の `UNFOLD`)。ほかの型と同じく、型を 8 段までたどる決まりも残る。
**決定(同じ型は二つまでたどる)**:自分を含むフィールドを二つ持つ値(`Expr` の `add` と `mul`)があると、一段たどるごとに抽象の値が何倍にもなる。`UNFOLD` を 100 にして、自分を含む型もほかの型と同じく 8 段までたどると、`trees.kn` の `check` に 95 秒と 17 GiB のメモリがかかった(二つまでなら 0.4 秒と 12 MB)。
- **ベクタ**:基本の値は自分を含まない値で、候補は、自分を含む値の中に自分を含まない値を一つ置いたものまでである(9 章)。出力先の突き合わせは、自分を含む型の引数ごとに、200 段と 201 段の値も流す(16.4)。
- **生成したコード**:TypeScript の `interface` と Python のデータクラスは、そのまま自分を指せる。Go の構造体は、自分を含むのがいつも `*T`、スライス、マップ、直和型のインターフェースを通してなので書ける(10.2)。Rust の構造体と列挙は、同じ強連結成分の型へのフィールドのうち `Vec`・`BTreeMap`・`BTreeSet` を通らないものを `Box` で持つ(`left: Box<Expr>`、`next: Option<Box<Node>>`。36.2)。入口は、型ごとの深さの関数(`_nest1` など)で数えてから、自分を含む型ごとの入口だけの確かめ(`_enter_<型>`、`KanameEnter`、Rust の `kaname_enter` のメソッド)でどの深さのフィールドも見る(10.4)。中の関数(`_size`)は、引数を確かめずに自分を呼ぶ。`size` の入口(TypeScript。実物):
```ts
export function size(c: Category): bigint {
const n_c = _nest1(c, 0);
if (n_c !== null) {
kaname.fail("input", _module, 55, "The argument `c` of `size` nests `" + n_c + "` more than 200 deep");
}
const o_c = _outside17(c, "c");
if (o_c !== null) {
if (o_c.bad === true) {
kaname.fail("input", _module, 55, "The argument `" + o_c.path + "` of `size` is no well-formed string of Unicode");
}
kaname.fail("input", _module, 55, "The argument `" + o_c.path + "` of `size` is " + o_c.shown + ", outside its range " + o_c.range);
}
return _size(c);
}
```
```ts
function _nest1(v: Category, above: number): string | null {
const here = above + 1;
if (here > 200) {
return "Category";
}
const o2 = _nest2(v.children, here);
if (o2 !== null) {
return o2;
}
return null;
}
```
Rust の `size` の確かめと深さの関数(実物。Rust の文字列はいつも正しく、`Category` に範囲を書いたフィールドは無いので、確かめは深さだけを見る):
```rust
pub fn check_size(c: &Category) -> Result<(), kaname::KanameError> {
if let Some(n_c) = _nest1(c, 0) {
return Err(kaname::fail("input", _MODULE, 55, format!("The argument `c` of `size` nests `{n_c}` more than 200 deep")));
}
Ok(())
}
```
```rust
fn _nest1(v: &Category, above: i64) -> Option<&'static str> {
let here = above + 1;
if here > 200 {
return Some("Category");
}
if let Some(o2) = _nest2(&v.children, here) {
return Some(o2);
}
None
}
```
- **Lean**:`crates/ritsu-model/tests/kaname.rs` は、自分を含む型ごとの場所を一度だけ表(`Module.slots`)に書き出し、自分を含む型の値を持つ場所は、表の番号でその型を指す(`Slot.ref`)。モデルの入口の確かめ(`Slot.enterD`)は、表をたどってどの深さでも確かめ、深さを数える。kaname の関数を呼ぶところの確かめ(`Slot.outsideD`)と、それを示したかの判定(`keepsD`)は、`ref` の場所の中を見ない(15.1)。モデルを壊して落ちることを確かめたのは、入口で深さを数えないこと、入口で表をたどらないこと、`keepsD` が `ref` の場所を示さないことで、Rust の側では、自分を含む型の値の中で範囲を書かない刻みのある率を 0% から 100% と読むことと、呼ぶところで自分を含む型の値を示したとしないことである(15.3)。
- **`run` の入力**:**決定(`run` は、引数の型が入れ子にできる深さまで入力を読む)**:`run` は入力の JSON を、引数の型の値が自分を含む型を 201 段まで入れ子にしたときの JSON の深さに、引数のオブジェクトの一段を足した深さまで読む(`Program::json_depth`)。JSON の段は、レコードが一つ、フィールドを持つ値が二つ(`{"<値>": {…}}`)、リストと集合が一つ、辞書が二つ(`[[鍵, 値], …]`)、`result` が一つで、オプショナルな値は増やさない。ritsu の読み手の 256 段より浅くは読まず、1,024 段より深くは読まない(`interp::READ_DEPTH`)。200 段の値は読んで答え、201 段の値は、入口の確かめが型の名前を言って `input` で失敗する(生成したコードと同じ文)。それより深い値は JSON として読めない。`Category` の一段は JSON の二段なので、`size` の入力は 1 + 2 × 201 = 403 段まで読む:
```text
$ ritsu kaname run trees.kn size --input cat201.json
fails: input (line 55): The argument `c` of `size` nests `Category` more than 200 deep
$ ritsu kaname run trees.kn size --input cat260.json
error: cat260.json is not JSON: byte 5434: nested deeper than 403 levels
```
理由:生成したコードには、ホストが 200 段まで渡せる。同じ値を参照インタプリタで流せなければ、`run` で確かめられない。読む深さを引数の型から決めれば、自分を含む型を持たない関数の入力は、ritsu の読み手と同じ 256 段で読む。自分を含む型の一段が JSON の 5 段までなら、どの型の 201 段の値も 1,024 段に収まる(1 + 5 × 201 = 1,006)。ritsu-base の `parse` の深さ(256 段)は変えず、深さを渡して読む `parse_nested` を足したので、ほかの言語の読み手は変わらない。上限の 1,024 段は、読み手と、受け取った値をたどる確かめ(値を読む `from_json`、入口の確かめ `outside`、表示)が、どれも再帰するためである。値を読む関数と入口の確かめは、値の種類ごとの部分を別の関数にして、一段ごとのスタックを小さくした。デバッグのビルドで、`Category` の 200 段を読んで確かめるのに 6 MiB 余りを使っていたのが、3 MiB に収まる(`ulimit -s` で測った)。それでも x86_64 の Linux のデバッグのビルドでは一段のスタックがもっと大きく、メインスレッドの 4 MiB では 200 段であふれた。そこで kaname と ritsu のバイナリは、コマンドを、256 MiB のスタックを持つスレッドで走らせる(スタックは予約するだけで、使った分しか取らない。WASI のビルドはスレッドを持たないので、そのまま)。`tests/run.rs` は、kaname のクレートのバイナリを、プロセスのメインスレッドの 8 MiB の半分(`ulimit -s 4096`)で走らせ、`Category`・`Expr`・`Node` の 200 段の値に答え、201 段の値で `input` で失敗し、一段が JSON の 5 段の型(`dict[string, list[Deep]]` を持つ値)の 1,006 段の入力を読んで、200 段の値に答えることを確かめる。
**捨てたもの**:ritsu-base の読み手の深さ(256 段)を上げること。ほかの言語の入力の読み方も変わる。`run` だけ固定の深さで読むこと。浅い型には深すぎ、深い型には足りない。
- **テスト**:`tests/modules/trees.kn` と `trees.ja.kn`(23 の関数ずつ。分類の木の大きさ、深さ、名前、式の木の値と、反転の反転を取り除くこと、部品表の費用と末端の部品の数、総称の木 `Tree[T]` の葉、値を入れ子にした文書と、互いを含む `Member`、`next` でつながるリスト、取り分の内訳、木を比べること、`result` に入れること、辞書に入れること)。`tests/types.rs` の `types_that_hold_themselves`、`tests/names.rs` の `ranges_that_cannot_hold`、`tests/codes.rs` の `the_cases_of_types_that_hold_themselves_say_their_codes`(`tests/golden/codes/trees.*.txt`)、`tests/interp.rs` の `values_that_hold_themselves`、`tests/effects.rs` の `the_effects_of_functions_over_values_that_hold_themselves`、`tests/vectors.rs` の `values_that_hold_themselves_are_tried_one_inside_another`、`tests/python.rs` の深さの測り方。範囲の解析の健全性、出力先の突き合わせ、Lean との突き合わせに `trees.kn` と `trees.ja.kn` を入れる。
### 28.2 `private`
```kn
private record Bounds
low : number range >=0 <=99
high : number range >=0 <=99
private fn place_of(x: number range >=0 <=99, b: Bounds) -> number
if x < b.low then 0 else if x > b.high then 2 else 1
```
- `fn`、`record`、`enum` の前に `private` を書ける。`private` のものは、そのモジュールの中からだけ使える。`use module` で読むほかのモジュールから使えば E101 で、注に `private` であることを書く。ホストからは呼べない(`private` の関数は中の関数で、入口の確かめもベクタも持たない。21.3)。
- `private` でない関数の引数と答え、`private` でないレコードのフィールド、`private` でない列挙の値のフィールドに、`private` のレコードと列挙は書けない(E108)。ほかのモジュールもホストも、その型の値を作れないからである。
- 生成したコード:TypeScript は `export` を付けず(`function _place_of`、`interface _Bounds`)、Python と Go は名前を `_` で始める(`_place_of`、`class _Bounds`、`_placeOf`、`type _Bounds struct`)。Go の `_` で始まる名前は、ほかのパッケージから読めない。Rust は `pub` を付けず、名前を `_` で始める(`fn _place_of`、`struct _Bounds`、`enum _Side`)。`pub` の無いものは、そのモジュールの外から使えない。
- `api` は、`private` の関数・レコード・列挙に `"private": true` を書く。`doc` のページは、`private` の関数に、ホストは呼ばず、このモジュールの関数だけが呼ぶと書く。
- `private` は、行頭の `enum`・`record`・`fn` の前でだけ読む語で、名前にもできる(2.2)。書かなければ、ほかのモジュールからもホストからも使える。
```text
error[E108]: example.kn:6:11: The parameter `b` of `low`, which is not `private`, is of the `private` record `Bounds`
6 | fn low(b: Bounds) -> number
= A `private` record or enum is used in its module alone: no other module and no host can make a value of it. Make what uses it `private` too, or take the `private` off the type.
```
## 29. 入れないもの
汎用の言語がふつう持つもののうち、kaname が持たないものと、その理由である。どれも、21.2 の五つの保証のどれかを崩すか、出力先の言語で同じ答えを返せなくなる。まだ作っていないだけのもの(作る理由が見えたら作るもの)は 19 章にある。
| 入れないもの | 理由 |
|---|---|
| 浮動小数点 | 出力先と範囲の解析と Lean が同じ答えを出せるのは、値が正確な有理数で、int64 に収まるからである(P4) |
| 入出力、時刻、乱数、書き換えられる状態 | P1。ホストが読んで、引数として渡す |
| ホストの関数を呼ぶこと、ホストから関数の値を受け取ること | P1。ホストのコードは kaname の検査の外にあり、入出力を持たないことを確かめられない(21.3) |
| 関数を答える関数、関数の値をデータに入れること | 関数の値の行き先を追う解析と、その健全性の定理が要る。渡すことと呼ぶことだけなら、呼ぶ場所で渡した関数の中まで読めば足りる(27.1) |
| 比べ方の関数で並べ替えること | 推移律を満たさない比べ方は、出力先の並べ替えの手順ごとに違う答えになる。並べ替えは鍵で決める(24.1) |
| `key` のほかの型の制約 | 制約ごとに、出力先に隠れた引数で渡す関数が増える(26.7) |
| 呼び出しの輪の中で、型の引数から作った型で総称の関数を呼ぶこと | 生成した Go が組めない(26.2。E112) |
| 失敗を受け止める書き方(`try`/`catch`) | 検査が示せなかった場所の失敗が、業務の答えに紛れる。業務として拒むことは `result` で書く(26.3) |
| 止まることの確かめ | P8。再帰を持つ関数は `diverges` である |
| 書き換えられる変数とループの文 | 値は書き換えない。畳み込み(`reduce`)と再帰で書く |
| 任意の大きさの整数 | P4。値は int64 に収まる |
| 添え字での読み書き(`xs[i]`) | 範囲の外という失敗の種類が増える。`xs.at(i)` が `T?` を答える(24.3) |
| タプル | レコードならフィールドに名前が付き、どの出力先でも名前で読める |
| 演算子の多重定義と、自分で書く演算子 | 一つの式を一通りにだけ読むため |
| 正規表現 | 出力先の言語で書き方と文字の種類の扱いが違い、補助のモジュールに一つ書くには大きすぎる |
| Unicode の正規化と、ASCII の外の大文字と小文字 | Go と Rust の標準ライブラリに正規化が無く、大文字と小文字の替え方は TypeScript・Python・Go で違った(25.3) |
| 言語や地域に合わせた比べ方と書式 | 出力先と動かす環境で答えが変わる |
| 文字列のエスケープ | rulec と同じに読むため(P3)。書けない文字は `char(n)` で作る(25.2) |
## 30. rulec との関係
- **型と式は rulec のスーパーセットである(P3)。** rulec の `derive` と `define` の式は、kaname でも同じ型と同じ値になる(16.2)。21〜28 章の型と式も、rulec の式の読み方を変えない。文字列のエスケープを入れないのも、このためである(29 章)。
- **rulec は表の言語のまま、kaname が汎用の側を受け持つ。** rulec の P7(汎用言語にしない)は rulec に残る。判断を表に書けば、完全性と重なりを rulec が確かめ、承認する人がページで読める。kaname は規則を呼び、規則は kaname を呼ばない(0.1 の層の決まり)。kaname の検査は、業務の定数を選ぶ判断をコードに書いたところを W501 で言い、表へ移すことを勧める(7 章、23.9)。
- **層は三つである。** 内側の層(rulec の表、koyomi の日付、chobo の勘定。それぞれの言語の検査のまま)、純粋な層(kaname。入出力を持たない汎用の言語)、外側の層(ホスト。入出力)。内側の層から純粋な層は呼べず、純粋な層から外側の層は呼べない。
- **規則に渡すところは、ほかの値と同じ決まりで確かめる。** 範囲を書かない値、直和型、辞書、関数の値は、そのまま規則に渡せない。規則の型の値に直し、規則の入力の範囲に狭めてから渡す(E301・W301 と、言語をまたぐ判定。6 章、14 章)。
- **rulec の `shape` と `from` との境目は 1.2 のとおりである。** 契約の一つのコレクションの一つのフィールドを数えて足す形は rulec に書き、kaname が受け持つのは、その外の計算である。
## 31. サイトのコードの色(2026-10-22)
**きっかけ**:ritsu のサイトの kaname のページ(`website/docs/kaname/` と `docs-ja/kaname/`)で、`kn` の塊に色が付いていなかった。rulec・dandori・chobo のサイトは、それぞれの言語の字句の定義(Pygments の `RegexLexer`)をつないで色を付けている。ritsu のサイトの本体の二つの設定(`website/zensical.toml` と `zensical.ja.toml`)は字句の定義を一つもつないでおらず、kaname には字句の定義そのものが無かった。Pygments は字句の定義を知らない塊を `text` として組むので、この直しの前に組んだ kaname のページでは、`kn` の塊が英日それぞれ 15 個とも `language-text` のブロックで、全体が一色だった。頭のページと sekisho のページの規則とフローの塊も同じだった。
**原因**:kaname の字句の定義が無かった。サイトの本体の設定と `website/build.sh` も、どの言語の字句の定義も読み込んでいなかった。
**決定**:
- `website/tools/knlexer.py` を足す。三つのサイトの字句の定義と同じ書き方で、モジュールを Markdown の拡張として設定に挙げると読み込まれ、読み込まれると Pygments の表に自分を載せる。サイトの本体の二つの設定は、`knlexer` と、三つのサイトの `rulelexer`・`flowlexer`・`booklexer` を挙げる。三つはコピーせず、それぞれのサイトのものを使う。`website/build.sh` は、四つのディレクトリを `PYTHONPATH` に置いて本体を組む(根の DESIGN 13.2)。
- 語には、パーサーがその位置で読むとおりの色を付ける(2.2)。行頭の語、`use` のあとの語、本体の語(`let` `try` `if` `then` `else` `match` `some`)、範囲(`range` `count`)、エフェクト(`total` `fails` `diverges`)はキーワードにする。型の語は型、`true` `false` `none` は定数、`and` `or` `not` `in` は演算子と同じトークンの種類にする。宣言が付ける名前(モジュール、レコード、列挙、関数、`use`)、`fails` のあとの失敗の種類、呼んだ組み込みの関数、名前の ASCII の別名、単位の付いた数と日付、文字列、コメント、演算子も、それぞれ種類を分ける。色そのものは、テーマの明るい配色と暗い配色が種類ごとに決める。
- 名前にもなる語は、置かれた場所で読む。`count` は型のあとでは範囲、呼び出しでは組み込みの関数である。`total` は関数の答えの型のあとでだけエフェクトで、フィールドや引数の名前(`total: total(lines: …)`)では名前である。`dict` `set` `entry` `indexed` `result` は、すぐあとに `[` があるときだけ型にする(24.2)。`money` と量の型、`list`、`running` も、`[` があるときだけ型にする。パーサーがそこに `[` を求めるので、検査を通るコードでは同じことになる。`:` の前の語は、つづりによらず、フィールド・引数・名前で渡す値の名前として読む。組み込みの関数は、呼ぶところ(`sum(…)`、`xs.map(…)`)でだけ組み込みの関数の種類にし、メソッドの形だけで書くもの(`get`、`sort_by` など)は `.` のあとでだけそう読む。
- 字句の定義が持つ語の一覧は `kw.rs` のコピーで、`TABLE`(位置ごとの語)、`RESERVED`、`KINDS`、`BUILTINS`、`METHODS` を、同じ名前で、語も順も同じに持つ。数と語を読む文字(単位を始める文字、桁の語 `万` `億` `兆`、それだけで語になる `℃` `℉`)は `lex.rs` のコピーである。`tests/docs.rs` の `the_site_colours_the_words_of_the_language` が、どれも元と一字も違わないことを確かめる。言語に語を足して字句の定義に足し忘れれば、そこで落ちる。
**捨てたもの**:
- 組み込みの関数に色を付けないこと。モジュールの関数(`change(…)`)と組み込みの関数(`.filter_map(…)`)が同じ色になり、言語のものを呼んでいるのか、モジュールのものを呼んでいるのかが、色からは分からない。
- 字句の定義を `kw.rs` から生成すること。生成には Rust のビルドか、サイトを組む前の手順が要る。サイトは Zensical だけで組める形にしてあり(作ったものはコミットする)、三つのサイトの字句の定義も、手で書いてテストで元に合わせている。
- `pymdownx.superfences` の `custom_fences` に `kn` を足すこと。`custom_fences` は、Mermaid の図のように Pygments を通さずに組む塊のための仕組みである。三つのサイトと同じく、字句の定義を Pygments の表に載せ、ほかの言語の塊と同じく `pymdownx.highlight` に組ませる。
**変わること**:kaname のページの `kn` の塊(英日それぞれ 15 個)に色が付く。この直しの前と後に `website/build.sh` で組んだ木を突き合わせると、変わったのは ritsu のページのうち `kn`・`rule`・`flow` の塊を持つ 14 ページ(kaname の十、頭の二つ、sekisho の二つ)だけで、変わったところはその塊の中だけ、塊の文字は一字も変わらない。言語のサイトの木は、この直しでは変わらない。
**確かめたこと**:
- kaname の `tests/docs.rs` の `the_site_colours_the_words_of_the_language`。字句の定義の `METHODS` から語を一つ消す、`TABLE` の位置の名前を変える、`TABLE` のエフェクトの位置から失敗の種類を一つ消す、`UNIT_CHARS` から文字を一つ消す、`MULTIPLIERS` から桁の語を一つ消す、の五つで、それぞれ落ちることを確かめた(字句の定義は元に戻した)。
- ritsu の `tests/website.rs` の `build_sh_builds_the_tree_that_is_published`(Zensical が `website/.venv` にあるとき)。組んだ木で、ritsu のページの `kn`・`rule`・`flow`・`book` の塊が、どれもフェンスと同じ数だけその言語のブロックとして組まれ、空白・区切り・ただの名前のほかの種類の語を持ち、字句の定義が読めなかった文字(`err`)を持たないこと。頭のページ(英日)に規則とフローのブロックがあること。kaname のページが英日それぞれで、キーワード(`fn`)、型(`money`)、単位の付いた数、日付、コメント、文字列、演算子(`>=`)を、それぞれの種類の `span` で見せること。日本語の設定から `knlexer` を外すと、日本語の kaname の五つのページで落ちることも確かめた。
- 木の `.kn`(kaname の例とテストの 57 と、ritsu の試験のプロジェクトの 8、あわせて 65 ファイル)を字句の定義に通し、読めない文字が 0 であること。フィールドや引数や変数の名前として書いた `total`、`count`、`number`、`text` などが名前になり、答えの型のあとのエフェクトと、関数の型に書いたエフェクト(`(number) -> number fails overflow`)がキーワードと失敗の種類になることを、語ごとの種類を並べて確かめた。
- 組んだページを Chrome(headless)で開き、kaname のページ(英日)と頭のページの塊に色が付くことを目で確かめた。
**見つけて直していないもの**:
- ブラウザで試すページのエディターは、kaname の語の一覧を持たない。kaname はまだそこで開けないためである(19 章)。開けるようにするときは、ritsu の `tests/playground.rs` の `language_words` と `words.json` に `kn` を足す。
- 型の語と同じつづりの名前(`tests/modules/names.kn` の変数 `string`)は、`:` の前と `.` のあとのほかでは型の種類になる。字句の定義は、モジュールが宣言した名前を知らないためである。
## 32. 規則の答えの範囲、`running_totals` の `total`、W301 の文、`run` の入力(2026-10-28)
**きっかけ**:請求書の明細に返金を配る関数を書いた。rulec の規則が一行ぶんの配分を二つの `allocate` の差で決め(`result`)、kaname の関数が明細を `running_totals` でたどって規則を呼び、配った額を税率ごとに合わせる。そこで四つのことが起きた。
- 規則が答える配分を、範囲の解析は端の無い値として読んだ。配った額を足すところが W302 になり、関数のエフェクトに `overflow` が入った。
- `if 返金額 > sum(明細, 一件 => 一件.残り)` で外したあとで `running_totals(明細, 一件 => 一件.残り)` の `total` を規則の `合計` に渡しても、規則の前提 `返金額 <= 合計` は ritsu の W201(決められない)になった。
- 配った額の並びを、`count <=20` だけを書いた引数に渡すと W301 になった。文は、だれも書いていない範囲(`range >=-9223372036854775808円 <=9223372036854775807円 count <=20`)を挙げ、決められないのが要素の範囲か、要素の数かを言わなかった。規則の出力をフィールドに持つレコードの並びを、範囲も `count` も書かない引数に渡しても W301 になった。
- `kaname run` の入力に金額を `"1000円"` と書くと、文は型と、文字列が渡されたことだけを言った。失敗の場所の `input (line 4)` は、入力の 4 行目ではなく、`.kn` で関数を宣言した行だった。
**原因**:
- rulec の証明書の `ranges` に、`result` で決まる出力が載っていなかった。口の `output_values` はその出力の端を持たず、kaname はそういう出力を、端も型の端の印も無い区間から始めていた(rulec の §15.268 で、`result` で決まる出力も `ranges` に載せた)。
- 二つの値の関係(5.3)は、名前(変数かパス)どうしでだけ覚えていた。`sum(xs, f)` は名前でないので条件から覚えず、`running_totals` の要素の `total` は、`before` と `upto` とのあいだの関係しか持たなかった。
- 範囲を書かない引数は型の全体を受け取る(22.1)のに、端の無い規則の出力は、型の全体に収まるとも示せなかった。W301 の文は、引数の宣言を、書かない端を型の端の数で埋めて言っていた。示したかを決める `keeps` は真か偽だけを答え、値のどの部分が示せないのかを持たなかった。
- 入力を読むところ(`Program::from_json`)は、違う形の値に、型と渡された値の種類だけを言っていた。`run` は失敗の場所を、関数の本体の中の失敗と同じく行だけで書いていた。
**決定**:
- ★ rulec が範囲を渡さない規則の数の出力(範囲を書かない入力を読む出力)は、型の全体に型の端の印を付けた区間から始める(5.1)。生成したコードはその出力を型の値(お金なら int64)で返すので、範囲を書かない引数と同じ読み方になる(22.1、22.2)。型の端からだけ int64 を外れうる場所は、どの診断にもならず、エフェクトに `overflow` が入る。`result` で決まる出力は、rulec が `output_values` に範囲を渡すようになったので、表で決まる出力と同じに読む。そのために kaname の側で足したものは無い。
- `running_totals(xs, x => x.f)` の要素の `total` は、同じ `xs` を同じフィールドで足した `sum(xs, x => x.f)` と等しいとする。関係を覚える名前に、リストのパスのあとに印 `⅀` と足すフィールドのパスを並べた、足した値の名前を足す(`sum_term`。どのフィールドの名前とも重ならない)。条件の一方の側が `sum` の呼び出しなら、その名前で関係を覚える(`summed`)。組み込みの関数(`map`、`filter`、`sum` など)が `running_totals(xs, x => x.f)` の答えをそのままたどるときは、そのラムダの本体を読むあいだ、引数の `total` が足した値と等しいという関係を置く(`walk`、`elem_total_fact`)。関係は推移をたどるので、`返金額 <= sum(…)` と `途中.total = sum(…)` から `返金額 <= 途中.total` が出て、規則の呼び出しが ritsu-cross に渡す関係に入る(14.2)。「同じ」は、リストが同じ名前で、二つのラムダがどちらも引数のフィールドをたどるだけで(`x => x.f`、`x => x`)、たどるフィールドが同じことである。ラムダの引数の名前は違ってよく、組み込みの関数は関数の形でも値のあとの形でもよい。名前は関数の中で一つの値を指す(E006)ので、二か所の `xs` は同じリストである。
- ★ 足した値の関係を Lean のモデル(15 章)に入れることは、33 章に分けた。入れるには、足した値を評価する項と、`relation_sound` のその場合が要る。
- W301 の場所が引数のとき、見出しは、値のどの部分が示せないか(値そのもの、各要素、各要素のフィールド、直和型の値のフィールド、辞書の各鍵)と、範囲か要素の数か(文字列ならコードポイントの数か)を言い、そこに書いてある範囲か数を挙げる。範囲を書いていなければ「<型> の値に収まるか」、数を書いていなければ「int64 に収まるか」と言い、型の端の数を挙げない。注は、範囲の解析がその部分に見つけた区間か、端が無いことを言う。値そのものは見出しが式で挙げているので、注では、ほかの場所の注と同じく「値」と言う。フィールドの場所と答えの場所の注も、示せないのが要素の数なら、要素の数の区間を言う。関数のベクタに例のある E301 の見出しは、例の値がどこで外れるかを見せるので、前のとおり宣言の全体を言う。宣言を言う文(`decl_text`)は書いたものだけを言い、書かない端を型の端で埋めない。値のどの部分か(`breach` が答える `Breach`。`Part` と `Held`)は、`keeps` と同じ手順で探し、`keeps` はそれが無いことにした。
- `run` の入力の JSON の書き方は変えない(量は単位を付けない整数、率は刻みの数の整数。`vectors` の `in` とシナリオと同じ)。違う形の値には、型と、その型の書き方と、型から作った例と、渡された値の種類を言う(`` `amount` は money[JPY] で、単位を付けない整数で渡します(`"amount": 1` が 1JPY)。渡されたのは文字列です ``)。例のキーは引数かフィールドの名前で、リストの要素には付けない。`T?` は「か null」を足す。数・お金・量、率、真偽、文字列、日付、列挙、レコード、リスト、集合、辞書、振替のどれにも同じ形で言い、整数でない数にも書き方を言う。直和型と `result` は、前から書き方を言っていたので変えない。
- 失敗 `input` の場所は関数を宣言した行で、そう言う(``失敗: input(18 行目で宣言した関数 `claim`)``、``fails: input (`claim`, declared on line 18)``)。`--format json` の `failure` の `line` は変えない。
- `run --help` の `--input` の下に、型ごとの書き方を一行ずつ、十三行挙げる。`vectors` の `in` と同じ書き方である。
**捨てたもの**:
- 入力に単位を付けた文字列(`"1000円"`)を読むこと。`vectors`、シナリオ、ほかの言語の入力と比べて、同じ値に二つの書き方ができる。
- 範囲の無い規則の出力だけを W301 から外すこと。規則の出力を特別に扱うことになり、生成したコードが返す値と、解析の値が食い違ったまま残る。
- 二つのラムダが同じ関数かを、式で決めること(`x => x.left * 2` と `y => 2 * y.left`)。式の等しさを決める手順が要る。按分の形は、フィールドをたどるラムダで書ける。
- W301 の見出しに、書かない端を型の端の数で挙げること。だれも書いていない数は、どこを直せばよいかを言わない。
**変わること**:
- 範囲の無い規則の出力を、範囲も `count` も書かない引数やフィールドに渡すところは、W301 にならない。範囲の無い規則の出力で計算するところは、W302 にならず、エフェクトに `overflow` が残る。
- `result` で決まる規則の出力を読む関数は、示せる場所が増え、エフェクトが狭くなることがある(`fails overflow` や `fails range` が total になる)。Go の生成したコードでは、total になった関数は `(T, error)` でなく `T` を返すので、それを呼ぶホストのコードは書き直しが要る。TypeScript と Python では、失敗の種類の型の別名が `never` と `NoReturn` になる。
- `if` で `sum` と比べてから `running_totals` の `total` を規則に渡す形は、規則の前提が関係の段で成り立つ。前は ritsu の W201 だった。
- W301 の見出しと注の文、`run` の失敗の行の文と違う形の入力の文、`run --help`。
**確かめたこと**:
- `tests/ports.rs` の `the_total_of_the_running_totals_is_the_sum_of_the_same_list`:同じリストと同じフィールドなら、ラムダの引数の名前を変えても、組み込みの関数を値のあとの形で書いても、規則の呼び出しの関係に `amount <= total` が入り、`running_totals` の二つの関係も残る。ほかのリスト、ほかのフィールド、フィールドをたどるだけでないラムダでは入らない。
- `tests/ports.rs` の `the_share_of_the_rule_has_the_range_its_result_comes_to`:例 `discount_split` の規則の `share`(`result` で決まる)を、rulec の範囲(0USDc から 10,000,000USDc)で読む。その範囲を書いたフィールドには何も言わず、上の端を一つ狭めると E301 になる(関数に `total` と書いてあるので、E401 も出る)。
- `tests/ranges.rs` の `a_rule_output_with_no_range_takes_the_whole_of_its_type`:範囲の無い規則の出力を、範囲を書かない要素やフィールドに渡すと何も言わない。範囲を書いた要素には W301 で、見出しは「各要素」が書いた範囲に収まるかを言い、注は端が無いことを言い、どちらも int64 の端の数を言わない。リストの要素の数と、要素のフィールドの要素の数も、それぞれの文で言う(英日)。
- `tests/run.rs` の `a_value_of_the_wrong_kind_says_how_its_type_is_written`:違う形の値の文を十一(英語)と二つ(日本語)、`--help` の十三の型の行、`vectors` が選ぶ入力をどれも `run` が `input` にせず読むこと。前からの失敗の行の試験も、新しい文にした。
- ritsu-model の `tests/kaname.rs`:書き出しも、範囲の無い規則の出力を型の全体から始める。59 のモジュール、14,092 の評価、1,080 の場所、5,287 の式、見た値 314,492 で、Lean のモデルと違いが無い(数は前と同じ)。
- 前と後のバイナリで、木の `.kn` の全部(65 ファイル)の `kaname check`(英日と JSON)と、言語をまたぐファイルを持つ 52 のディレクトリの `ritsu check` を比べ、違いが無い。きっかけの四つを小さなモジュールで再現し、前と後で比べた(W302、W201、W301 が消え、`run` の文が変わる)。
**見つけて直していないもの**:
- `let` で名前を付けた `running_totals` の答え(`let rs = running_totals(…)` のあとの `map(rs, …)`)の `total` は、足した値と結ばない。名前に、どの式の答えかを覚えさせることになる。
- 同じ値が、作るレコードのフィールドの場所と、そのレコードを受け取る引数の場所の二か所で W301 になることがある(`tests/ranges.rs` の最後の場合。前から)。
- 括弧で終わる式(`side * (10 - side)`)を、文が `side * (10 - side` と、閉じ括弧を落として挙げる。パーサーが、括弧の中の式の場所を括弧を含めずに持つためで、前からある。
## 33. `running_totals` の `total` と `sum` が等しいという関係を、Lean のモデルに入れる(2026-10-29)
**きっかけ**:32 章で、範囲の解析は、`running_totals(xs, x => x.f)` の要素の `total` が `sum(xs, x => x.f)` と等しいという関係を覚えるようになった。`if 返金額 > sum(明細, 一件 => 一件.残り)` で大きい額を外したあとで、`running_totals` の `total` を規則の `合計` に渡すと、規則の前提 `返金額 <= 合計` は関係の段で成り立つ。ところが、Lean のモデル(15 章)の関係は名前どうしのものだけで、この関係を持たなかった。`callRels_sound` が言うのは名前どうしの関係で、足した値を通って出る関係(`返金額 <= 途中.total`)はその外にあった。突き合わせの材料にも、この関係で答えが変わる場所が無かった。ritsu の X2 が関係の段で「成り立つ」と言う呼び出しのうち、この形のものは、証明にも突き合わせにも支えられていなかった。
**原因**:モデルの関係が結ぶものは名前(`Sym`。名前とその下のフィールド)で、評価の側の意味も名前の値だけだった。Rust は、足した値を、リストのパスのあとに印 `⅀` と足すフィールドのパスを並べた名前として持つ。`⅀` はどの識別子にもならないので、Rust ではほかの名前と重ならない。モデルの名前とフィールドはどんな文字列でもよいので、同じ形をそのまま使うと、名前と足した値の区別がつかない。32 章は、足した値を評価する項と、`relation_sound` のその場合を足すことを別にした。
**決定**:
- ★ 関係が結ぶものを、名前から項(`Term`)に広げる。項は、名前(`Term.name`)か、名前のリストをフィールドで足した値(`Term.sum`。リストの名前とフィールドのパスと、ラムダが引数からたどるフィールドのパス)である。項の値(`termVal`)は、名前ならその値、足した値なら、リストのどの要素もそのパスに値を持つときの、その値の和である。Rust の名前の形は使わない。
- 「同じ」の決め方は Rust と同じにする。`fieldPath`・`sumTerm`・`summed`・`totalsOf` が、Rust の `field_lambda`・`sum_term`・`summed`・`totals_of` にあたる。リストは同じ名前(とフィールド)で、二つのラムダはどちらも引数のフィールドをたどるだけで(`x => x.f`、`x => x`)、たどるフィールドが同じこと。ラムダの引数の名前は違ってよい。組み込みの関数の関数の形と値のあとの形は、モデルの式ではどちらも同じ `Expr.walk` である。
- 比べの一方の側が足した値なら、その項で関係を覚える(`factTerm`、`narrowCmp`)。値を狭めるのは、前と同じく名前の側だけである。
- 組み込みの関数が `running_totals(xs, x => x.f)` の答えをそのままたどるときは、ラムダの引数を置くところ(`ACtx.bindLam`)で、引数の `total` と足した値が等しいという関係を足す。足すのは、Rust の `walk` が `elem_total_fact` を置く組み込みの関数(`count`・`sum`・`any`・`all`・`map`・`first`・`last`・`max_of`・`min_of`・`running_totals`・`sort_by`・`sort_by_desc`・`max_by`・`min_by`・`flat_map`・`filter_map`・`group_by`・リストの `filter`)と同じで、辞書の `filter`・`map_values`・`to_dict`・`merge_with` と `reduce` では足さない。
- 健全さは、`running_totals` の三つの関係(`before <= upto <= total`)と同じ形で言う。評価の側で、`sum(xs, x => x.f)` の答えは足した値の項の値であること(`summed_eval`)と、`running_totals(xs, x => x.f)` の答えのどの要素も、`total` がその項の値であること(`totals_eval`)を示す。関係を覚えるところでは、比べの両側の値が、それぞれの項の値である(`SymOK` の `sum`)。ラムダの引数を置くところでは、たどる要素の `total` が項の値なので、足した関係も成り立つ(`Sat.bindLam`)。組み込みの関数がラムダの本体を読むところの帰納法(`Sound.lean` の `each`)は、要素ごとに、本体を読む解析の環境で覚えた関係が成り立つ形に広げた。こうして `Sat`(覚えた関係はどれも成り立つ)が保たれ、`relation_sound`・`ranges_sound`・`callRels_sound`・`kept_holds` が、足した値を通って出る関係についても言う。定理の言い方は変わらない。
- モデルは、足した値と `running_totals` の答えを項にするところで、ラムダの答えを単位の読み替えなしに読むこと(`walk` の `c` が無いこと)も確かめ、ラムダの引数がたどるリストと同じ名前なら `total` の関係を置かない。どちらも Rust は確かめないが、型の検査が `sum` と `running_totals` にラムダの答えの型を与え、関数の中で一つの名前を二度置くことを E006 で止めるので、検査を通ったモジュールでは同じ答えになる(15.3)。
**捨てたもの**:
- 足した値を、Rust と同じ名前の形(印 `⅀` を挟んだパス)でモデルに持つこと。`⅀` という名前のフィールドを持つ値で名前と足した値が一つになり、定理に「その名前のフィールドは無い」という前提が要る。
- Rust の解析を、モデルに合わせて項の形に書き直すこと。Rust では名前の形でも重ならず、同じ答えになる。kaname の Rust と、診断と、生成したコードを変えずに済む。
- `let` で名前を付けた `running_totals` の答えも、モデルでだけ結ぶこと。Rust と答えが割れ、突き合わせが落ちる。結ぶなら二つで一緒に、名前に、どの式の答えかを覚えさせることになる(32 章)。
**変わること**:
- 規則の呼び出しが ritsu-cross に渡す関係のうち、足した値を通って出るもの(`amount <= total`)も、`callRels_sound` と `kept_holds` が言う。ritsu の X2 が関係の段で「成り立つ」と言うことは、この形の呼び出しでも、モデルの評価で成り立つ。
- kaname の Rust、診断、生成したコード、口は変わらない。変わるのは Lean のモデルと証明(`Analysis.lean`・`Narrow.lean`・`Grows.lean`・`Sound.lean`・`SoundAll.lean`・`SoundWalk.lean`・`SoundDict.lean`・`Verdicts.lean`)と、突き合わせの材料である。`KanameCore` は 24,444 行から 24,738 行になった。
- ritsu-model の `tests/proofs.rs` が数える宣言は、17,500 個から 17,676 個になり、そのうち定理は 8,761 個から 8,848 個になった(rulec の §15.269 と、言語をまたぐ検査の X4 の分を含む)。
**確かめたこと**:
- ritsu-model の `tests/kaname.rs` に、材料のモジュール `refunds` を足した(15.3)。規則 `refund_share`(前提 `before <= upto`・`upto <= total`・`amount <= total`。按分の差を `result` で返す)を、明細の `running_totals` の要素ごとに呼ぶ六つの関数である。どれも、足した値より大きい額を `if` で外したあとに呼ぶ。
- `same`(`sum(lines, l => l.left)` と比べ、`map(running_totals(lines, l => l.left), r => …)` で呼ぶ)と `apart`(同じことを値のあとの形で書き、ラムダの引数を `x` と `y` にしたもの)では、規則の呼び出しの関係に `amount <= total` が入る。
- `another_list`(ほかのリスト `others` を足す)、`another_field`(ほかのフィールド `other` で足す)、`more_than_a_field`(`l => l.left * 2` で足し、同じラムダで `running_totals` する)、`named`(`let rs = running_totals(…)` のあとで `map(rs, …)`)では入らない。
替える前のモデルでは、突き合わせが `same` と `apart` で落ちる(Rust の関係は `[["amount","<=","total"],["before","<=","total"],["before","<=","upto"],["upto","<=","total"]]`、モデルの関係は `amount <= total` の無い三つ)。替えた後は、60 のモジュール、14,860 の評価、1,136 の場所(答えを受けた場所が 45)、5,424 の式、見た値 344,804 で違いが無い。ほかの四つの関数では、前も後も、Rust とモデルのどちらも `amount <= total` を言わない。
- モデルを一か所ずつ壊すと、どれも `refunds` の突き合わせが落ちる(15.3。壊したのは `Analysis.lean` で、突き合わせを走らせる問いだけを組んで試した)。
| 壊したところ | 落ちたところ |
|---|---|
| 足した値のフィールドを見ない(`sumTerm` がいつも空のパス) | `another_field`:モデルだけが `amount <= total` を言う |
| 足した値のリストを見ない(`sumTerm` がいつも同じリスト) | `another_list`:同じ |
| フィールドをたどるだけでないラムダも足した値にする(`fieldPath` がいつも空のパス) | `more_than_a_field`:同じ |
| ラムダの引数に `total` の関係を置かない(`bindLam`) | `same` と `apart`:Rust だけが `amount <= total` を言う |
| 条件の `sum` の側を関係に覚えない(`factTerm` が名前だけ) | `same` と `apart`:同じ |
- `lake build` が通る。ritsu-model の `tests/proofs.rs` で、どのファイルにも `sorry`・`axiom`・`native_decide`・`implemented_by` が無く、六つのライブラリのどの宣言も、Lean そのものが立つ三つの公理のほかに立っていない。名前を挙げて確かめる定理の一覧に、`summed_eval`・`totals_eval`・`Sat.bindLam` を足した。
- kaname の Rust は変えていないので、kaname の試験と診断と生成したコードは前と同じである。
**見つけて直していないもの**:無い。`let` で名前を付けた `running_totals` の答えを結ばないことは、32 章のとおりで、モデルも同じである。
## 34. `ritsu check` の JSON に渡す、モジュールのオブジェクト(2026-10-31)
`ritsu check --format json` の `files` の各項目は、`check` に、その言語の `check --format json` がそのファイルについて書くオブジェクトを持つ(ritsu の DESIGN 8.3)。`check::unit` は、診断と最後の行に加えて、`kaname check --format json` がファイルごとに書くオブジェクト(4.3 の `{kaname, file, ok, summary, functions, diagnostics}`)を、コマンドと同じ `to_json` で作り、`ritsu_ports::Checked::json` で渡す。ritsu は `file` と `diagnostics` を除いた `{kaname, ok, summary, functions}` を入れるので、関数ごとの `effect` を `ritsu check` の JSON から読める。読めないファイルは `null` になる。kaname のコマンドの出力は変えていない。
確かめたこと:ritsu の `tests/check.rs` の `each_files_check_in_the_json_is_what_its_language_writes` で、ritsu のテストのプロジェクト `cold_chain` と `regions`(型の確かめを通らない二つのファイルを含む)、例の `payout` の `.kn` の `check` が、`ritsu kaname check --format json` のオブジェクトから二つのキーを除いたものと同じであること(`payout.kn` は `summary` が `1 function, total` で、関数の `effect` の `fails` が空)。ritsu の `tests/cross.rs` の `each_border_has_its_record` で、日付を渡す関数の `effect` が `{"fails": ["rule"], "diverges": false}` であること。golden の `crates/ritsu/tests/golden/modules/shop.json` と `店.json`。
## 35. 言語をまたぐ確かめを、ritsu の境目の記録と同じ語で呼ぶ(2026-10-31)
**きっかけ**:`kaname check --format json` の `functions[].places[].check` と、`kaname api` の `crossings[].checks[].check` は、言語をまたぐ確かめを ritsu の DESIGN の番号(`X2`・`X3`・`X4`・`X6`)で書いていた。ritsu の DESIGN 4.3 は、利用者に見える出力に設計の文書の番号を書かないと決めている。同じ日に、`ritsu check --format json` の境目の記録が、同じ確かめを語(`precondition` など。ritsu の DESIGN 7.15)で呼ぶようになり、二つの出力が同じ確かめを別の名前で呼ぶことになった。`X4` は、振替の額と拒否の理由という二つの確かめを、一つの番号で呼んでいた。
**決定**:kaname の出力も、境目の記録と同じ語で確かめを呼ぶ。語は口の `ritsu_ports::Crossing::check` が持ち(`precondition`・`rule_date_input`・`transfer_amount`・`transfer_refusals`・`koyomi_date_input`)、`places` の `check`(`ranges::What::across`)も、場所の `Crossing`(`ranges::crossing_of`)から引く。ritsu-cross の `Check::of` が `Crossing` を境目の記録の確かめに移し、ritsu-cross の `tests/borders.rs` が、どの `Crossing` でも二つの語が同じであることを確かめる。
**変わること**:`kaname check --format json` の `places` と、`kaname api` の `checks` の `check` の値が変わる。`X2` は `precondition`、`X3` は `rule_date_input`、`X4` は `transfer_amount`(拒否の理由の確かめは `transfer_refusals`)、`X6` は `koyomi_date_input` になる。`doc` のページで答えを待つ場所の文(ritsu-cross をつながない実行)も、番号ではなくコードの書式の語で言う(「ritsu's check `transfer_amount` has not answered in this run.」、帳簿の表の拒否の理由の欄は `transfer_refusals`)。`check` のテキストの出力は変わらない。
**確かめたこと**:`tests/api.rs`(golden 6 本を `KANAME_BLESS=1` で取り直し、差は `check` の値だけだった。`what_the_api_says_and_what_stops_it` は答えを待つ日付の場所を `koyomi_date_input` で見る)、`tests/doc.rs`(答えを待つページの golden 2 本を取り直し、差は二つの場所の文だけだった)、`tests/cli.rs`、`tests/effects.rs`、`tests/ranges.rs`(場所の `check` の語)、ritsu-cross の `tests/borders.rs` の `kaname_names_a_check_by_the_word_of_its_record`。kaname の fast の段(200 件)と、ritsu・ritsu-cross・ritsu-ports の fast の段が通った。
## 36. Rust の出力先と、パッケージの Wasm(2026-11-01)
### 36.1 きっかけと範囲
**きっかけ**:rulec と koyomi は Rust と Wasm を書き、chobo は Rust のクライアントを書く。kaname の生成物は TypeScript・Python・Go だけだったので、Rust のホストは、規則と日付と帳簿を直に呼べても、それをつなぐ kaname のモジュールを呼べなかった。Wasm のホスト(ブラウザや、ほかの言語のランタイム)も同じである。
**決定**:kaname の出力先に Rust(キーは `rust`、`kaname::r#gen::Target::Rust`)を足す。H1〜H7 の全部(21〜28 章、32〜35 章)を Rust で書き、出力先の突き合わせ(16.4)を英語と日本語で通す。Rust のパッケージは一つのクレートで、kaname はその `glue/` を書く(10.1)。パッケージの Wasm は、そのクレートを wasm32 に組んだもので、rulec・koyomi と同じ `call: func(input: string) -> string` を export し、`glue::door::answer` で答える(36.10〜36.12)。
**原因**(Go の形をなぞるだけでは足りない理由):Go の生成物は、中の関数の失敗を panic で運び、入口の `kaname.Recover` で `error` に直す(10.4)。wasm32 の Rust は `panic=abort` で組む(rulec と koyomi の組み方と同じ)ので、panic はトラップになり、「どの入力にもトラップせずに答える」という Wasm の約束が守れない。そこで Rust では、失敗を `Result` で運ぶ(36.4)。もう一つは値の持ち方で、Rust の値には持ち主があるので、借りるかコピーするかを生成物が決める(36.4)。
いまの範囲:Rust の生成物は kaname の生成の関数(`kaname::r#gen::package_in` に `Target::Rust`)が書き、パッケージの Wasm に要る二つのファイル(クレートの根の `wasm.rs` とパッケージの `.wit`)の中身は `kaname::r#gen::wasm` が作る(36.10)。kaname のテスト(`tests/rust.rs`、`tests/wasm.rs`)が、規則・日付・帳簿の Rust と一つのクレートにし、ネイティブと wasm32 に組んで確かめる(36.9)。`ritsu gen --target rust` が、パッケージの残りと `Cargo.toml` を書き、Wasm の二つのファイルを置く(36.17)。
### 36.2 型と値(10.2 の表の Rust の列)
受け渡す形(引数、答え、レコードのフィールド)と、計算のあいだの形を、ほかの出力先と同じに分ける(10.2)。引数は、`Copy` の型を値で、ほかを借りて受け取る(36.4)。
| kaname | 受け渡す形(レコード・答え) | 引数 | 計算のあいだ |
|---|---|---|---|
| お金・量 | パッケージの単位の型 `units::USDc`(`pub struct USDc(pub i64)`) | 同じ | `i64`、丸めていなければ `kaname::Q` |
| `number` | `i64` | `i64` | `i64` か `kaname::Q` |
| 刻みのある率 | 刻みの数の `i64` | `i64` | `kaname::Q`(`kaname::rate(k, step)`) |
| `bool` | `bool` | `bool` | `bool` |
| `string` | `String` | `&str` | `String` |
| `date` | `kaname::Date { year: i32, month: u32, day: u32 }` | 同じ(`Copy`) | 同じ |
| モジュールの列挙(値にフィールドが無い) | `pub enum Size { Small, Large }` | 値で(`Copy`) | 同じ |
| 規則の列挙 | rulec の Rust の列挙(`rule_coupon::Kind`) | 値で(`Copy`) | 同じ |
| 直和型 | `pub enum Order { Draft(OrderDraft), … }` と、値ごとの `pub struct OrderDraft { … }` | `&Order` | 同じ |
| レコード | `pub struct Invoice { pub id: String, … }` | `&Invoice` | 同じ |
| `list[T]` | `Vec<T>` | `&[T]` | `Vec<T>` |
| `dict[K, V]` | `BTreeMap<K, V>` | `&BTreeMap<K, V>` | 同じ |
| `set[T]` | `BTreeSet<T>` | `&BTreeSet<T>` | 同じ |
| `entry[K, V]`・`indexed[T]` | 補助のモジュールの `kaname::Entry<K, V>`・`kaname::Indexed<T>` | 借りる | 同じ |
| `T?` | `Option<T>` | `Option<T>`(`Copy` の型)か `Option<&T>`(`string?` は `Option<&str>`) | 同じ |
| `result[T, E]` | 標準の `Result<T, E>` | `&Result<T, E>`(`T` と `E` が `Copy` なら値で) | 同じ |
| 型の引数 `[T]`・`[K: key]` | `T: Clone + PartialEq`、`K: Ord + Clone` | — | — |
| 関数の型 `(A) -> R` | (受け渡さない。27.1) | `&dyn Fn(A) -> Result<R, kaname::KanameError>`(`total` と書いた型は `-> R`) | — |
| 振替の値 | `glue::books::<帳簿>::CaptureDo` と、帳簿ごとの `enum Call` | 借りる | 同じ |
- **数**:`kaname::Q` は、既約の分数を `i128` の分子と分母(分母は 1 以上)で持つ。kaname の値はどれも int64 の決まり(5.6。分子は int64、分母は 1 から 2⁶³ − 1)に収まるので、演算の途中の積は `i128` に収まり、途中であふれて答えが変わることはない。演算と丸めは参照インタプリタの `num.rs` と同じ手順で、言語の割り算に頼らない(ほかの出力先と同じ)。
- **日付**:`kaname::Date` は、koyomi の Rust の `Date` と同じフィールドの型にした。koyomi を呼ぶところは、フィールドを入れ替えて組み直すだけである(36.6)。暦に無い日(2026-13-01)、9999-12-31 の次の日、0001-01-01 の前の日も持てるので、入口が `valid()` で確かめる(Go の `Date.Valid` と同じ。22.5)。範囲は日の番号(`days()`。1970-01-01 からの日数)で比べる。
- **辞書と集合**:`BTreeMap` と `BTreeSet` で持つ。鍵にできる型(24.1)の Rust の順(`Ord`)が、どれも kaname の鍵の順と同じだからである。数とお金と量と率は受け渡す形の整数で比べる(一つの鍵の型の単位は一つなので、整数の順が値の順になる)。文字列は `String` の順(UTF-8 のバイトの順は、コードポイントの順と同じ)、`bool` は `false` が先、日付は `(year, month, day)` の順(正しい日どうしでは早い順。入口を通らない日は鍵にならない)、モジュールの列挙は `derive(Ord)` の宣言の順である。規則の列挙も、パッケージの rulec の Rust が、どの列挙にも宣言の順の `Ord` を付ける(36.6)。回す順は鍵の順、等しさは `BTreeMap` と `BTreeSet` の `PartialEq`(同じ組、同じ要素)である。Go のように順の関数を受け渡す(`SortedKeys`、隠れた一つ目の引数)必要は無い。
- **型の引数**:鍵に限った型の引数(`[K: key]`)は `K: Ord + Clone` にし、ほかの出力先が隠れた一つ目の引数で渡す順(26.7)は渡さない。上のとおり、鍵にできる型の `Ord` が鍵の順だからである。鍵に限らない型の引数は `T: Clone + PartialEq` にする(要素を新しいリストに入れるので `Clone`、`=` を書けるので `PartialEq`)。
- **derive**:レコード、直和型、値ごとの構造体は `Clone, Debug, PartialEq, Eq` で、フィールドの型がどれも順を持てば `PartialOrd, Ord` を足す(`Gen::orderable`)。鍵にできる型のフィールドだけを持つレコードは並べ替えと `max_by`・`min_by` の鍵になり、フィールドを宣言の順に比べる(24.1)。`derive(Ord)` の比べ方がそれと同じである。順を持たないのは、振替の値、関数の値、規則の要素と、出力が二つ以上の規則の答え(rulec の構造体)を持つ型である。フィールドの無い列挙は `Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash`、振替の値は `Clone, Debug, PartialEq, Eq`、拒否の理由の列挙は `Clone, Copy, Debug, PartialEq, Eq, Hash` である。受け渡す形には浮動小数点数が無く、規則の列挙も `Eq` を持つので、`Eq` はいつも付けられる。
- **`result`**:標準の `Result` を値の型として使う。ホストがそのまま読めるからである。失敗しうる関数が `result` を答えると、`Result<Result<T, E>, kaname::KanameError>` になる。`try`(26.3)は `?` でなく `match` で書く。`?` は kaname の失敗(`KanameError`)を返す書き方で、`err` の値を答えるのとは別だからである(26.7)。
- **関数の型**:関数の値は、渡すことと呼ぶことにしか使えず、書いた関数の実行の外へ出ない(27.1)。そこで `&dyn Fn` で借りる。`Box` も型の引数も要らず、総称の関数の実体がラムダごとに増えることもない。エフェクトを書かない関数の型は、どのエフェクトの関数も受け取る(27.5)ので、答えを `Result<R, kaname::KanameError>` にし、total な関数を渡すときは `Ok(…)` で包む。`total` と書いた関数の型だけが `R` を答える(27.6)。
- **自分を含む型**:型のグラフで同じ強連結成分に入る型へのフィールドのうち、`Vec`・`BTreeMap`・`BTreeSet` を通らないものを `Box` にする(`next: Option<Box<Node>>`、`left: Box<Expr>`)。`Vec` などを通る辺はもう間接なので `Box` にしない(`children: Vec<Category>`。28.1)。
- **振替の値**:帳簿ごとに `glue/books/<帳簿>.rs` に、振替の操作ごとの構造体(`CaptureDo`。帳簿と振替と操作の名前を `BOOK`・`TRANSFER`・`OP` の定数で持つ)、拒否の理由の列挙(`CaptureDoRefusal`。`as_str()` が帳簿の書く名前を返す)、帳簿ごとの `enum Call`(`From<CaptureDo> for Call`)を書く(10.6)。帳簿のクライアントを読まないので、Wasm にも入る。
### 36.3 名前(10.3 の Rust の版)
- **宣言の名前**(関数、引数、フィールド、型、列挙の値)は、ほかの出力先と同じく ASCII の別名から作る(検査を通るモジュールでは、別名の無い名前は ASCII である)。関数は別名のまま(`late_payment`)。Rust では関数と、名前付きフィールドの構造体が別の名前空間にあるので、Go のように型と同じ名前の関数に `Fn` を足すことはない(`fn claim` と `record Claim` は `claim` と `Claim`)。型はパスカルケース(`ritsu_emit::ident::pascal`)、フィールドは別名のまま、モジュールの列挙の値は、値の別名のパスカルケースで型の下に置く(`Size::Small`)。直和型の値の型は Go と同じ名前(`OrderPlaced`。23.8)で、値は `Order::Placed(OrderPlaced { … })` になる。
- **関数の中の名前**(`let`、ラムダの引数、`some` の名前)は書いたとおりにする(TypeScript と Python と同じ)。日本語の `let` の名前もそのまま置く。引数は別名で書く。どれも、生成物が使う名前(モジュールの関数、`check_<関数>`、`_<関数>`、`_MODULE`、生の識別子にできない語、`Some`・`None`・`Ok`・`Err`、`std`・`core`・`alloc`)か前に付けた名前と同じになれば、TypeScript と Python の決まり(10.3)で `_` と番号を足す。値の名前空間で、引数と `let` はモジュールの関数を隠すからである(`tests/modules/names.kn` の `helpers(units, ctx)` の `units` は、同じモジュールに関数 `units` があるので `units_` になる)。パスの頭(`kaname::add` の `kaname`、`units::USDc` の `units`、`rule_late_payment_charges::late_payment_charges` の `rule_late_payment_charges`)は型の名前空間で探すので、`let` と引数には隠されない。そこで Rust では、TypeScript と Go が分けている `kaname` と `units` を分けない(36.15)。
- **Rust の予約語**:E007 は、TypeScript の strict モードの予約語、Python と Go の予約語、補助のモジュールの名前を、宣言の名前から外している(`names::taken_word`)。Rust の予約語はそこに足さない。足すと、`ref`、`box`、`move`、`use` のような、いま通っているモジュールの名前が止まるからである。代わりに、Rust の予約語は生の識別子にする(`r#ref`、`r#move`、`r#impl`、`r#dyn`、`r#where`、`r#mut`、`r#loop`。エディション 2024 の予約語 `gen` も `r#gen` にし、これは 2021 でも書ける)。生の識別子にできない五つ(`crate`、`self`、`super`、`Self`、`_`。`ritsu_emit::ident::RUST_UNRAW`)は、フィールドと関数と型なら `_` を足す(`self_`、`crate_`、`Self_`。chobo の Rust のクライアントと同じ)。関数の中の名前と引数は、上の決まりで `_` と番号を足す。
- **標準の型と値の名前**:モジュールの型の名前が、生成物が名前だけで書く標準の型かトレイト(`Option`、`Result`、`Vec`、`String`、`Box`、`BTreeMap`、`BTreeSet`、`Fn`、`FnOnce`、`Clone`、`Ord`、`PartialOrd`、`PartialEq`、`Eq`、`From`。`plan::RUST_STD_TYPES`)と同じなら、`_` を足す(`record Vec` は `Vec_`、`enum Option` は `Option_`)。モジュールの型が標準の型を隠すと、生成物の `Option<…>` がその型を指すからである。関数の名前が、生成物が名前だけで書く値(`Some`、`None`、`Ok`、`Err`。`plan::RUST_STD_VALUES`)と同じなら、関数にも `_` を足す(`fn Some` は `Some_`、中の関数は `_Some`、確かめは `check_Some`)。関数は値の名前空間にあり、`Some(…)` を隠すからである。名前付きフィールドの構造体は値の名前空間に入らないので、型の名前は値を隠さない(36.15)。モジュールのコードの名前(関数)が標準の型と同じ名前のときは、生成物がその型をパスで書く(`::std::vec::Vec`。`Gen::std_type`)。
- **モジュールの名前(W001、E008)**:Rust では、モジュールの名前が `glue/` の Rust のモジュールとそのファイルの名前になる。`crate`、`self`、`Self` は Rust がモジュールに付けられない語で、`mod` と `door` は kaname が `glue/` に書くファイル(ディレクトリのもの、パッケージの入口)の名前である(`names::NO_RUST_MODULE`。`super` は TypeScript の予約語として、もう E007 である)。TypeScript・Python・Go はどれも名前に取り、壊れるのは Rust だけなので、`check` は W001 の警告にして通し、`ritsu gen --target rust` だけが E008 で止まる(`r#gen::stops`。36.17)。ほかの Rust の予約語(`match` など)は生の識別子のモジュールにする(`pub mod r#match;` は `match.rs` を読む。36.15)。
```text
warning[W001]: example.kn:1:8: The generated Rust cannot take `door` as the name of a module
1 | module door v1
= In the generated Rust, a module's name is the name of its Rust module in `glue/` and of its file: `crate`, `self` and `Self` are words Rust names no module, and `mod` and `door` are the files kaname writes into `glue/` itself (the directory's own, and the package's door).
= `ritsu gen --target rust` stops at this module (E008). If you do not generate Rust, leave it; if you do, choose another name or alias.
ok example.kn: 1 function, total
```
```text
警告[W001]: example.kn:1:11: 生成する Rust は、`door` をモジュールの名前に使えません
1 | module 入口(door) v1
= 生成する Rust では、モジュールの名前が `glue/` の Rust のモジュールとそのファイルの名前になります。`crate`、`self`、`Self` は Rust がモジュールに付けられない語で、`mod` と `door` は kaname が `glue/` に書くファイル(ディレクトリのもの、パッケージの入口)の名前です。
= `ritsu gen --target rust` はこのモジュールで止まります(E008)。Rust を生成しないなら、このままで構いません。生成するなら、別の名前か別名にしてください。
ok example.kn: 関数 1 個、total
```
`ritsu gen --target rust` は、同じ場所で次のように止まる。
```text
error[E008]: example.kn:1:8: The generated Rust cannot take `door` as the name of a module
1 | module door v1
= In the generated Rust, a module's name is the name of its Rust module in `glue/` and of its file: `crate`, `self` and `Self` are words Rust names no module, and `mod` and `door` are the files kaname writes into `glue/` itself (the directory's own, and the package's door).
= Choose another name or alias. For a target you do not use, name the ones you use with `--target`.
```
```text
エラー[E008]: example.kn:1:11: 生成する Rust は、`door` をモジュールの名前に使えません
1 | module 入口(door) v1
= 生成する Rust では、モジュールの名前が `glue/` の Rust のモジュールとそのファイルの名前になります。`crate`、`self`、`Self` は Rust がモジュールに付けられない語で、`mod` と `door` は kaname が `glue/` に書くファイル(ディレクトリのもの、パッケージの入口)の名前です。
= 別の名前か別名にしてください。使わない出力先なら、`--target` で使う出力先を挙げてください。
```
- **生成物が足す名前**:入口の確かめは `check_<関数>`(別名から作る。モジュールの関数の名前と同じになれば、2 からの番号を足す)、入口を通らない中の関数は `_<関数>`(10.3 の `_claim`)、モジュールの名前は `const _MODULE: &str`、計算のあいだの値は一時的な名前(`_t4`)にする。
- **`private`**:`pub` を付けず、名前を `_` で始める(`struct _Bounds`、`fn _place_of`。同じモジュールからしか使わない。28.2)。型の引数を持つ関数と関数を取る関数は、ほかのモジュールの生成物から呼ぶので `pub fn _first_or<T…>(…)` にする(TypeScript と同じ名前)。ドキュメントのコメントに「kaname の生成したコードが呼ぶ関数で、入口で引数を確かめない」と書く(26.7)。
- **出力先が名前にできない字**:kaname は `m²`(`²` は一般カテゴリ No の数)や `कीमत`(二字目の `ी` は結合文字 Mc)を名前に取る。出力先の識別子の決まりはそれぞれ違い、Rust と Python は XID_Start か `_` で始まって XID_Continue が続く名前、TypeScript は ID_Start と ID_Continue(XID に、NFKC で別の字になる少しの字を足したもの。`ͺ` と半角の `゙`)、Go は文字(Lu、Ll、Lt、Lm、Lo)か `_` で始まって文字と十進の数字(Nd)が続く名前を取る。そこで、関数の中の名前は、出力先がその字を名前に取るときだけ書いたとおりに書き、取らなければ `v` にする(`plan::ident_ok`)。字の表は ritsu-emit の `xid.rs` で、Unicode Character Database 17.0.0 の二つのファイルから作る(`crates/ritsu-emit/tools/xid/`。chobo の E063 と Rust のクライアントも同じ表を読む)。宣言の名前は ASCII なので、新しい診断は要らない。`names.kn` の `letters`(実物。Rust):
```rust
fn _letters(n: i64) -> i64 {
// 64: let m² = n * n
let v: i64 = n * n;
// 65: let कीमत = m² + 1
let कीमत: i64 = v + 1;
// 66: let Ⅻ = कीमत + 12
let Ⅻ: i64 = कीमत + 12;
// 67: let ゙ = Ⅻ * 2
let v_2: i64 = Ⅻ * 2;
```
Go はこのうち `कीमत` と `Ⅻ`(一般カテゴリ Nl)も `v` にし、TypeScript は `゙` を書いたとおりに書く。表のほうがツールの Unicode より新しいと、表が通す字をツールがまだ知らないことがある(Python 3.13 は Unicode 15.1.0、Go 1.26 は 15.0.0、rustc 1.94 は 17.0.0)。その字を持つ名前は、新しい Unicode で加わった文字で、kaname の字句が文字と読むものである。古いツールはそれを名前に取らない(36.16)。
- **NFC**:Rust は識別子を NFC にしてから比べる。NFC で同じになる二つの関数の中の名前(ハングルの字母を並べたものと、合わせた音節)は、kaname では別の名前で、Rust では同じ名前になり、後の `let` が前の名前を隠す。kaname は名前を正規化しない(36.16)。
### 36.4 関数の形とエフェクト
ホストが呼ぶ関数(21.3)一つに、三つの関数を書く。引数を確かめるだけの `check_<関数>`、ホストが呼ぶ関数、入口を通らない中の関数である。`tests/modules/texts.kn` の `place`(total。実物):
```rust
/// Whether the arguments are what `place` takes: a `KanameError` of the kind input when one is
/// outside what its parameter declares.
pub fn check_place(s: &str, t: &str) -> Result<(), kaname::KanameError> {
if let Some(o_s) = _outside7(s, "s") {
return Err(kaname::fail("input", _MODULE, 73, format!("The argument `{}` of `place` has {} code points, outside its count {}", o_s.path, kaname::itoa(o_s.count), o_s.range)));
}
if let Some(o_t) = _outside11(t, "t") {
return Err(kaname::fail("input", _MODULE, 73, format!("The argument `{}` of `place` has {} code points, outside its count {}", o_t.path, kaname::itoa(o_t.count), o_t.range)));
}
Ok(())
}
/// Effect: total (it answers every input). It panics when an argument is outside what its parameter declares; `check_place` says so
/// first, with a `KanameError` of the kind input.
pub fn place(s: &str, t: &str) -> Option<i64> {
if let Err(e) = check_place(s, t) {
panic!("{e}");
}
_place(s, t)
}
fn _place(s: &str, t: &str) -> Option<i64> {
kaname::index_of(s, t)
}
```
失敗しうる関数は、入口の外れも `Err` で返す。`tests/modules/trees.kn` の `size`(実物。28.1):
```rust
/// Effect: fails overflow (it checks some places when it runs). It recurses, and may not stop. It returns a `KanameError` of the kind input when an argument is outside what its
/// parameter declares, and of a kind of its effect.
pub fn size(c: &Category) -> Result<i64, kaname::KanameError> {
check_size(c)?;
_size(c)
}
```
- **決定(失敗しうる関数は `Result`、total な関数は値と panic)**:失敗しうる関数(`fails`)は `Result<R, kaname::KanameError>` を答え、入口の外れも `Err`(種類 `input`)で返す。total な関数は `R` を答え、入口の外れでは panic する。Go と同じ分け方で(10.3)、エフェクトが型に出る。Rust の標準のライブラリも、呼ぶ側が約束を破った入力(範囲の外の添字)では panic する。信用できない入力は、先に `check_<関数>` で確かめる。`kaname api` は二つの名前を `check` に書く(36.14)。
**捨てたもの**:どの関数も `Result` を答える形(rulec と koyomi の Rust はそうしている)。total な関数と失敗しうる関数の違いが型から消え、total な関数をほかのモジュールから呼ぶたびに、起こらない `Err` を扱うことになる。
- **中の関数**:失敗しうる中の関数は `Result` を答え、呼ぶ側は `?` で運ぶ。total な中の関数は `R` を答える。同じモジュールの関数は中の関数を、ほかのモジュールの関数は公開する関数を呼ぶ(10.3 と同じ)。範囲の解析が引数を示せなかった呼び出しは、呼ぶ前に確かめて `range` で失敗する(「The argument `…` given to `…`」の文)ので、ほかのモジュールの total な公開する関数の panic には届かない。関数を取る関数は、渡された関数の型がエフェクトを書かなければ、渡された関数の失敗が通るので(27.5 の `through`)、`Result` を答える。
- **示した場所**:解析が失敗しないと示した呼び出し(規則と日付の関数、関数の型の値、`Result` を答える中の関数)の `Err` は、`.unwrap_or_else(|e| panic!("{e}"))` で受ける。届かない枝である(Go の `panic(_t3)` と同じ)。示した演算は素の `+`・`*` で書く。示したとおりなら、デバッグのビルドのあふれの panic にも届かない。
- **評価の順と一時的な名前**:参照インタプリタの順(8 章)を保つ形は、ほかの出力先と同じ計画(`_t1`、`_t2`)を使い、`let _t1: T = …;` と書く。関数の終わりの値は、`let` に入れずに式で返す(clippy の `let_and_return`)。
- **借りるかコピーするか**:引数は、`Copy` の型(`i64`、単位の型、`bool`、`kaname::Date`、`kaname::Q`、フィールドの無い列挙と規則の列挙、関数の型の値、それらの `Option` と `Result`)を値で、ほかを借りて受け取る(`&Invoice`、`&str`、`&[T]`、`&BTreeMap<K, V>`、`Option<&T>`)。`let` は値を持つか、借りた値を名前で持つ(`let c: &rule_late_payment_charges::Output = &_t4;`)。計画の値に、持っている値か借りている値かの印を持たせ、使う場所ごとに決める。借りる先に渡すなら `&x`(すでに借りているなら `x`)、持つところ(レコードのフィールド、リストの要素、答え)に置くならコピーする(`x.clone()`。`&str` は `.to_string()`、`&[T]` は `.to_vec()`、`Option<&T>` は `.cloned()`)。`Copy` の型は clone しない(clippy の `clone_on_copy`)。最後に使う場所でコピーせずに移す解析はしない。kaname の値は書き換えないので、コピーしても答えは変わらず、コピーの量は値の大きさまでである。
- **ラムダ**:関数の型の引数に渡すラムダは Rust のクロージャにし(`&|l: &Line| -> Result<units::USDc, kaname::KanameError> { … }`)、借りて渡す。名前で渡した関数は、それを呼ぶクロージャにする(27.6)。組み込みの関数に渡すラムダは、ほかの出力先と同じく、その場のループの本体にする。クロージャではないので、本体の `?` はそのまま関数から失敗を返す。並べ替えは、先に鍵を順に求め(失敗すればそこで返る)、鍵と要素の組を安定な `sort_by` で並べる(Go と同じ。`slice::sort_by` は安定)。
- **再帰の深さ**:参照インタプリタは 200 段で「止まらない」と言う(8 章)。生成物は段を数えないので、200 段まで答える入力を Rust のスタックが受け止めればよい。テストのハーネスはデバッグのビルドをメインのスレッドで走らせ、全部のケースに答える(36.9)。止まらない入力では、ネイティブではスタックがあふれてプロセスが止まり、Wasm はトラップする(36.11)。
### 36.5 実行時の確かめと `KanameError`(10.4)
```rust
pub enum Kind { Input, Rule, Date, Range, Overflow, Zero }
#[derive(Debug)]
pub struct KanameError {
pub kind: Kind,
pub module: &'static str,
pub line: u32,
pub message: String,
pub cause: Option<Box<dyn std::error::Error + Send + Sync>>,
}
```
`Display` は文、`std::error::Error::source` は `cause`(規則の `RuleError`、日付の `KoyomiError`。どちらも `std::error::Error` を持つ)である。`Kind::as_str` は kaname の種類の名前(`input` など)を返す。原因の型が `PartialEq` を持たないので、`KanameError` の derive は `Debug` だけにした。文は生成するときの `--lang` の言語で、ほかの出力先と同じく ritsu-base の `spaced` の形にする(補助のモジュールの `spaced`)。
- **あふれ**:示さなかった整数の演算は、補助のモジュールの `kaname::add`・`sub`・`mul`・`neg` で書き、`?` で `overflow` を返す(27.6 の `_bumped`)。`kaname::Q` の値が int64 の決まりに収まるかは `kaname::held` で見る。
- **0 で割る**:`kaname::over(a, b, …)` が、`b` が 0 なら `zero` を返す。
- **範囲と要素の数**:入口の `check_<関数>` が、引数を宣言の順に `_outsideN(v, what) -> Option<kaname::Outside>` で見る(Go の `_outsideN` と同じ)。`kaname::Outside` は Go と同じく、パス、見せる値、範囲、要素の数、それがコードポイントの数か、を持つ。レコードは `pub fn kaname_outside(&self, what: &str) -> Option<kaname::Outside>` を持ち、ほかのモジュールの入口からも呼ぶ(Go の `KanameOutside`。22.5)。数の範囲は端ごとに比べる(`if v.0 < 1 || v.0 > 100000000 { … }`。ほかの出力先と同じ形。36.8)。
- **入れ子の深さ**:自分を含む型の引数は、入口で型ごとの深さの関数(`_nest1` など)で 200 段まで数え、201 段なら `input` で失敗する(28.1)。
- **文字列**:Rust の `String` と `&str` は、いつも正しい UTF-8 である。ホストは正しくない文字列を渡せないので、入口の「Unicode の文字列として正しくない」の確かめ(25.8)は書かない。要素の数はコードポイントで数える(`kaname::cp_count`)。正しくない文字列は、JSON の一行で呼ぶ入口(`glue::door::answer`)にだけ届き、そこで入口と同じ文で答える(36.11)。
- **範囲を書かない値の型の端(22.5)**:数は `i64` が int64 そのものなので確かめない(Go と同じ。`pub fn check_next_id(_: i64) -> Result<(), kaname::KanameError> { Ok(()) }`)。2⁶³ と −2⁶³ − 1 の入力は、Rust では流さない。日付は `valid()` で、0001-01-01 から 9999-12-31 の正しい日かを見る。10000-01-01、0000-12-31、2026-13-01 の入力(22.4)は `kaname::Date` が持てるので、Rust でも流す(TypeScript と Go と同じ)。
- 示した場所の確かめは書かない(10.4)。`ritsu gen` が渡す、言語をまたぐ判定の答えも同じである。
### 36.6 内側の層を呼ぶ(10.5、10.6)
**規則(rulec の Rust)**。パッケージの規則のモジュールは、rulec のパッケージの形の Rust である(`rulec::codegen::package_parts_at` に `rust`。ritsu の DESIGN 9.3)。`rulec gen` の Rust と同じ関数を持ち、違いは三つである。お金と量の型を、自分で宣言せずにパッケージの単位のモジュールから読んで公開する(`pub use crate::units::{GBP, GBPc};`。TypeScript・Python・Go と同じ。rulec の §15.248)。どの列挙にも、宣言の順の `PartialOrd` と `Ord` を付ける(36.2)。頭の許しは `#![allow(non_snake_case, unused_parens, dead_code)]` で、字の lint はクレートの根が許す(36.8)。率は規則ごとの `Rate(pub i64)`(刻みの数)、日付は日の番号の `i64` のままである。
- 呼び方は、口の `RuleFacts::rust`(`Call`。モジュール、関数、引数と出力の名前と型、列挙のメンバー)から書く。rulec の `ports.rs` が、`rulec api` の `rust` と同じ名前を、同じ関数(`Gen::calls`)で埋める。rulec の `tests/ports.rs` が、全部の規則で二つが同じことを確かめる。sekisho が構造体のリテラルで作る `RuleFacts`(`names.rs`)も `rust` を持つ。
- 規則が失敗すれば、`KanameError` は種類 `rule`、文は「The rule `charges` refuses its inputs」、`cause` は `RuleError` になる。示した呼び出しは包まない(36.4)。額は単位の型のまま渡し、規則の答えも単位の型で受け取る。`examples/late_payment` の `_claim`(実物。言語をまたぐ判定の答えを渡さない生成なので、期日の呼び出しは包む。10.4):
```rust
fn _claim(invoice: &Invoice, paid_on: kaname::Date, reference_rate: i64) -> Result<Option<Claim>, kaname::KanameError> {
let _t1: kaname::Q = kaname::rate(reference_rate, kaname::frac(1, 10000));
// 20: let due = terms.due(invoiced: invoice.invoiced)
let _t3: dates_payment_terms::Date = dates_payment_terms::due(dates_payment_terms::Date { year: invoice.invoiced.year, month: invoice.invoiced.month, day: invoice.invoiced.day }).map_err(|e| kaname::refused("date", _MODULE, 20, "The date `terms.due` cannot be worked out", e))?;
let _t2: kaname::Date = kaname::Date { year: _t3.year, month: _t3.month, day: _t3.day };
let due: kaname::Date = _t2;
// 21: let late = paid_on - due
let late: i64 = kaname::between(paid_on, due);
if late >= 1 {
// 23: let c = charges(debt: invoice.amount, days_late: late, reference_rate: reference_rate)
let _t4: rule_late_payment_charges::Output = rule_late_payment_charges::late_payment_charges(units::GBPc(invoice.amount.0), late, rule_late_payment_charges::Rate(kaname::steps(_t1, kaname::frac(1, 10000)))).unwrap_or_else(|e| panic!("{e}"));
let c: &rule_late_payment_charges::Output = &_t4;
Ok(Some(Claim { due, days_late: late, compensation: units::GBP(c.compensation.0), interest: units::GBPc(c.interest.0) }))
} else {
Ok(None)
}
}
```
**日付(koyomi の Rust)**。モジュールごとに `pub fn <日付の別名>(…) -> Result<Date, KoyomiError>` がある(`koyomi::codegen::rust::module`)。引数は口の `DateFunction::params` の順。koyomi の `Date` はそのモジュールの型なので、上のようにフィールドを入れ替えて組み直して渡し、答えも組み直す。失敗は、種類 `date`、文「The date `terms.due` cannot be worked out」、`cause` は `KoyomiError` である。
**読み込み**。パッケージのモジュールをモジュールごとに別名で読む(`use crate::rules::late_payment_charges as rule_late_payment_charges;`、`use crate::dates::payment_terms as dates_payment_terms;`、`use crate::glue::kaname;`、`use crate::units;`)。依存の部分は、そのディレクトリの下のパス(`crate::deps::<名前>::…`。`ritsu_emit::package::rust_path`)で読む。
**帳簿(chobo の Rust のクライアント)**。振替の値の型は kaname が書く(36.2)。当てる関数は、帳簿ごとに `glue/books/<帳簿>_apply.rs` に置く(10.6 の実物)。
- クライアントのモジュール、メソッド(`capture()`)、引数の構造体の名前(`CaptureArgs`)とフィールドは、口の `BookFacts::rust` から読む。文字列は借りて(`&c.payment`)、額は値で渡す。
- **決定(トレイト境界は帳簿の置き場所ごと)**:PostgreSQL と MySQL の系統のクライアントは `Book<D: Database>`(chobo が書く `Database` のトレイト)、TigerBeetle のものは `Book<C: TigerBeetleClient>` で、トレイトの名前が違う。そこで kaname の生成の選択(`kaname::r#gen::Options`)に帳簿の置き場所(`Store::Sql` と `Store::TigerBeetle`。既定は `Sql`)を足し、当てる関数はその置き場所のトレイトを書く。TypeScript・Python・Go の当てる関数は置き場所によらないので変わらない。
**捨てたもの**:chobo のクライアントが、置き場所によらない別名(`pub use … as Driver`)を書く形。chobo の Rust のクライアントの出力が変わる。
- 当てる関数は async で、Wasm には入れない(`glue/books.rs` が `#[cfg(not(target_arch = "wasm32"))]` で外す。36.10)。
### 36.7 補助のモジュール(`glue/kaname.rs`)
パッケージに一つ置く。中身は次のとおり。
- `Kind`、`KanameError`、`fail`・`fails`・`refused`、`At`、文の書き方の `spaced`。
- `Q` とその演算(和、差、積、商、比べ、整数との行き来、刻みの数と率)、あふれを見る整数の演算(`add`、`sub`、`mul`、`neg`)、`held`・`over`、五つの丸め(`round`)、`allocate`・`mod`・`isqrt`・`root`。
- `Date`(`valid`、`days`、`Display`)と、日の番号からの `date_of`、`between`。
- `Outside`、`Entry`・`Indexed`・`Running`、値を文に書く関数(入口の失敗の文の `1.5%`、`70GBP`)。
- 辞書と集合の関数(`put`、`remove`、`merge`、`union`、`intersect`、`difference` など)、リストの関数(`slice`、`take`、`drop`、`sorted`、`by_key`、`distinct`、`numbers` など)、文字列の関数(コードポイントで数える・切る・探す、`pad`、kaname の空白の決まりの `trim`、ASCII の大文字と小文字、`split`・`words`、数と日付を読む)。
一つだけ置く理由は 10.1 と同じで、Rust ではもっと強い。モジュールどうしで `kaname::Date` や `kaname::Q` を渡すので、型が一つでなければならない(Go の `kaname` のパッケージが一つなのと同じ)。依存のモジュールも、パッケージの根の `crate::glue::kaname` を読む。中身は版と言語のほかは決まった文なので、`include_str!` で読む `src/gen/runtime/kaname.rs` に `@HEAD@` と `@LANG@` を置いて埋める(ほかの出力先と同じ)。外のクレートは使わない。
### 36.8 生成物の頭、`#![allow(…)]`、エディション、rustc の版
- **頭**:`// Code generated by kaname <版>. DO NOT EDIT.` と `// Source: <パス> (module <名前> v<n>, sha256:<16 桁>)`(10.7、ritsu の DESIGN 9.2)。続けて `//! The module <名前> of kaname: <説明>`。補助のモジュール、帳簿ごとのファイル、`glue.rs`、`glue/books.rs`、`glue/door.rs` は一行目だけ。
- **決定(モジュールの `#![allow(…)]`)**:kaname のモジュールのファイルは、どれも同じ一行を持つ(`module::RUST_ALLOW`)。
```rust
#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, clippy::too_many_arguments, clippy::type_complexity, clippy::large_enum_variant, clippy::result_large_err, clippy::eq_op, clippy::identity_op, clippy::erasing_op, clippy::if_same_then_else, clippy::nonminimal_bool, clippy::explicit_auto_deref, clippy::manual_range_contains, clippy::needless_late_init, clippy::match_like_matches_macro, clippy::manual_map, clippy::manual_filter, clippy::collapsible_if, clippy::collapsible_else_if, clippy::collapsible_match)]
```
中身は三つの組である。テストのモジュールの全部に二つのエディションで clippy をかけ、要るものだけを残した。
1. ソースが書いた名前:`non_snake_case`(`paidOn`、`Type`、`check_Some`)、`non_camel_case_types`(`Option_`)、`non_upper_case_globals`。
2. 生成物の形:`clippy::too_many_arguments`(引数が八つ以上の関数)、`clippy::type_complexity`(入れ子の型)、`clippy::large_enum_variant`(大きさの違う値を持つ直和型)、`clippy::result_large_err`(大きなレコードを `err` に持つ `result`)、`clippy::explicit_auto_deref`(借りた値を、どこでも同じ書き方で読むこと)、`clippy::needless_late_init`(`if` と `match` の答えを入れる名前を、先に宣言すること)。ほかの出力先と同じ形が当たるものもある。範囲を端ごとに比べること(`manual_range_contains`)、分岐ごとに名前に値を入れること(`manual_map`、`manual_filter`、`match_like_matches_macro`)である。エディション 2021 の形(let のチェーンを書かない)も、2024 の clippy の `collapsible_if` と `collapsible_else_if` に当たる。`try` の続きは `match` の腕の中に書くので、続きが `if` だけの関数は Rust 1.99 の clippy の `collapsible_match` に当たる。
3. ソースが書いたとおりの式:`clippy::eq_op`(`a = a`。既定でエラー)、`clippy::identity_op`(`x * 1`)、`clippy::erasing_op`(`x * 0`)、`clippy::if_same_then_else`、`clippy::nonminimal_bool`。kaname の検査が言わない冗長さを、clippy が生成物の上で言うことになるからである。
生成物で避けられるものは避ける。`Copy` の値を clone しない、`&String` と `&Vec` を引数にしない(`ptr_arg`)、フィールドの省略形を使う(`Claim { due, … }`。`redundant_field_names`)、負のリテラルを括弧で包まない(`unused_parens`、`double_parens`)、終わりの値を式で返す(`let_and_return`)。
- **ほかの生成物の許し**:帳簿ごとの振替の値のファイルは `#![allow(non_snake_case, non_camel_case_types)]`、補助のモジュールと `glue/door.rs` は `#![allow(dead_code)]` である(パッケージの関数の全部を使うとは限らない)。
- **クレートの根に要るもの**(kaname のテストの `lib.rs`):`#![allow(confusable_idents, mixed_script_confusables, uncommon_codepoints)]`。三つともクレートの単位の lint で、モジュールに書いても効かない(36.15。モジュールに書いた `allow(uncommon_codepoints)` は「is ignored unless specified at crate level」の警告になり、`confusable_idents` はモジュールで許しても出る)。関数の中の日本語の名前を書いたとおりに置く限り要る。カタカナが紛らわしい字(`ロ`、`エ`、`ニ`)だけのクレートは `mixed_script_confusables` に、`ロ` と `口` の二つの名前は `confusable_idents` に当たる。NFKC で変わる字(全角の英字、半角のカナ)は `uncommon_codepoints` に当たる(chobo の DESIGN 14.6)。
- **規則と日付の部分**:rulec と koyomi の Rust のモジュールは、clippy に当たる形を持つ(規則の `manual_range_contains`、エディション 2024 の日付の `collapsible_if`)。どちらも生成器ごとの形で、kaname からは直せない。そこでテストのパッケージの `rules.rs` と `dates.rs` は、モジュールの宣言ごとに `#[allow(clippy::all)]` を付ける。rustc の lint は許さない。それぞれの中身は、それぞれの言語のテストが確かめる(ritsu の DESIGN 9.3 の「中身ごとの突き合わせ」と同じ考え)。
- **二つのエディション**:生成物は、エディションで意味の変わる書き方を使わない。let のチェーン(2024 だけ)を書かない。`gen` は `r#gen` にする。`impl Trait` を答えに書かない。`static mut` を使わない。テストは、同じクレートを `edition = "2021"` と `"2024"` で `cargo clippy --all-targets -- -D warnings` にかける(36.9)。
- **rustc の版**:テストのクレートの `rust-version` は `1.94` にした。ここで確かめるいちばん古いツールチェーンで、帳簿のクライアントの sqlx 0.9.0 も 1.94 を求める(chobo の DESIGN 14)。
### 36.9 試験(16.4 の Rust の段)
- **`tests/rust.rs`**(`go.rs` と同じ形):例の 16 と `tests/modules/` の 34(と、それが読むモジュール)を一つのパッケージにし、英語と日本語の二つを書く(`tests/common/targets.rs` の `write_package` に `Target::Rust`)。規則は rulec のパッケージの形の Rust、日付は `koyomi::codegen::rust::module`、帳簿は chobo の PostgreSQL の Rust のクライアント、ほかに `units.rs`(rulec の `units_module_in("rust")`)、kaname の `glue/`、テストが書く `lib.rs`・`rules.rs`・`dates.rs`・`books.rs` と `Cargo.toml` を置く。クレートの依存は `tools/rust/`(`Cargo.toml` と `Cargo.lock`)から持ってきて `--locked` で組む。chobo のテストがランナーの `Cargo.lock` を使うのと同じ形である。`the_crates_are_those_the_clients_name` が、sqlx、tokio、tigerbeetle の版が `chobo::client::rust::crates` の版と同じであることを確かめる。ritsu の `tests/audit.rs` は、`tools/` の下の `Cargo.toml` の横に `Cargo.lock` があることを確かめ、監査もその lock を読む。
- **ハーネス**:JSON の一行で呼ぶ入口 `glue::door::answer(&str) -> String`(36.11)を、ネイティブのまま呼ぶ小さな `main` にした(`harness::call_lines` が書くケースを一行ずつ渡し、panic すれば `{"error":"the call panicked"}` と書く)。ケースは一行に一つの呼び出し(`{"module":…,"function":…,"in":…}`)で、答えの行の `out` か `fails` を参照インタプリタの答えと比べ、ベクタのケースは、行の全体(`kaname vectors` の行から `why` を除いたもの)とも一字一句比べる。Wasm も同じ入口を通るので、Wasm のテストは、組み方とホストの違いだけを見ればよい。
**捨てたもの**:Go のように型ごとの JSON の読み書きを持つハーネスを別に生成すること。入口と同じものを二度書くことになる。
- **三つの入力**:全部のベクタ、宣言の外の入力、入れ子の 200 段と 201 段(16.4)。宣言の外の入力のうち Rust で流さないのは、int64 の外の数である。正しくない文字列は、JSON の孤立したサロゲートのエスケープ(`"\ud800"`)で書き、入口が答える(36.11)。数は Go と同じ 2,584 件になる。
- **型の検査**:英語と日本語のパッケージのそれぞれで、`cargo clippy --all-targets -- -D warnings` を、エディション 2024 と 2021(`Cargo.toml` の `edition` を書き換えて二度)で走らせる。警告が一つでも出れば落とす。
- **帳簿**:`the_generated_rust_makes_the_transfers_in_the_books` が、振替を作る四つのモジュールのベクタから振替を作り、`glue/books/` の当てる関数と chobo のクライアントで、PostgreSQL(使い捨てのクラスタ)と TigerBeetle に当てる。クライアントの答え(`done`、`done_before`、`refused` と理由)と、帳簿の残高と保留が、chobo の参照インタプリタの言うとおりであることを見る。MySQL の系統のクライアントでは、クレートを組んで clippy を通し、走らせない(手元にも CI にも MySQL の系統のサーバーを置かないため)。
- **名前の字**:`tests/idents.rs` が、kaname の名前が取れる字の全部を、名前の一字目(`ä`)と二字目より後(`aä`)に置いた 296,529 の名前にし、rustc(`cargo check`)、Python の `str.isidentifier`、Node(`new Function`)と tsc、Go の `go/token.IsIdentifier` に通す。どのツールも、ritsu-emit の表が通す名前を取り、外す名前を取らない。ツールの Unicode が表より古いところ(36.3)は分けて数える。
- **暦に無い日**:`a_day_of_no_calendar_is_outside_every_range` が、ホストの作る `kaname::Date { year: 2026, month: 2, day: 30 }` などを入口の確かめ(`check_days`)に渡し、範囲の両端を書いた引数でも型の範囲の引数でも、範囲の外の `input` で失敗することを確かめる(36.11)。
- **ターゲットのディレクトリ**:`tests/rust.rs` の三つの試験は、帳簿のクレートを一度だけ組むために、ターゲットのディレクトリを共有する。cargo は、名前と版の同じクレートを、ディレクトリが違っても一つのライブラリとして扱う。前に組んだときに読んだファイルを手元のディレクトリで探し、それより新しいものが無ければ組み直さない。そこで、試験ごとのクレートの版を、バイナリの名前から作る(`0.0.<n>`。`Cargo.lock` の根の行も合わせる)。版が同じだったときは、ほかの試験が後から組んだライブラリでハーネスが組まれ、全部のケースが「There is no module `order_flow`」になった。
- **`tests/wasm.rs`**:同じ形のパッケージを、使う人と同じ `ritsu_emit::wasm::CARGO_LINE` で wasm32 に組み(36.10)、次の四つを確かめる。帳簿のクレートは wasm32 では読まないので、試験ごとに自分のターゲットのディレクトリで組む。
- `the_wasm_answers_as_the_reference_does`:英語と日本語のパッケージを、組む前に二つのエディションで `cargo clippy --lib --target wasm32-unknown-unknown -- -D warnings` にかける。組んだモジュールを自前のホスト(Node。import が空であることを確かめ、canonical ABI の三つの関数で呼ぶ)に渡し、ネイティブのハーネスと同じ 16,770 件を一行ずつ呼ぶ。答えは参照インタプリタの答えと比べ、ベクタは行の全体を一字一句比べる。
- `an_input_outside_the_contract_is_answered`:契約の外の入力の表(英語の三つのモジュールに 35、日本語の一つのモジュールに 11)を、どれもトラップせずに表の行と一字一句同じに答えること。UTF-8 でないバイト列、空の入力、オブジェクトでない JSON、後ろの `,`、後ろに続く値、文字列の中の制御文字、2,000 段の入れ子、キーの `\u` のエスケープ、二度書いたキー、整数のいろいろな書き方、int64 の外の数、孤立したサロゲート、中の関数、無いモジュールと関数、暦に無い日、数字の形でない日付である。最初の呼び出しを 2 万回くり返して、メモリが増えないこと(答えを解放していること)も見る。
- `the_world_makes_the_package_a_component_wasmtime_calls`:`.wit` でモジュールを component にし(`ritsu_emit::wasm::component_line`)、`wasm-tools validate` と `wasm-tools component wit` を通す。wasmtime の `run --invoke` で呼んだ答えが、ホストの答えと同じであること。パッケージは三つで、日本語の一つのモジュール、英語の二つのモジュール、帳簿を使う英語のモジュール(振替を値で答える)である。
- `every_package_name_gives_a_world_wit_reads`:`ritsu gen` の `--name` が取る名前のうち、WIT の予約語の全部と、`_` が二つ続く名前、`_` で終わる名前、数字で始まる語のある名前から作った `.wit` を、版のあるものと無いもの、英語と日本語のドキュメントで、wasm-tools が読むこと。
- 道具が無ければ SKIP の行を出して通す。CI の tools の段は SKIP を許さない(`ci/skips/tools.txt` に足す行は無い)。CI は tools の段で `tools/rust` の依存を先に取ってくる(`cargo fetch --locked`)。
### 36.10 Wasm:どの関数を入れ、どう組むか
- パッケージの Wasm には、パッケージの kaname の全部のモジュールの、ホストが呼ぶ関数(21.3。型の引数も関数の型の引数も持たず、`private` でない)を入れる。ベクタを持つ関数と同じ集まりで、`glue::door::answer` が答える関数である。中の関数は入れない(ベクタも入口も無い)。
- 帳簿を使うモジュールも入る。振替の値はデータで、vectors と同じ `{"book","transfer","op","args"}` で答える。当てる関数と chobo のクライアントは入れない(async で、データベースのドライバに依る)。クレートの根は帳簿のクライアントを `#[cfg(not(target_arch = "wasm32"))]` を付けて宣言し(`kaname::r#gen::wasm::BOOKS`)、`glue/books.rs` は当てる関数を同じく外す。`Cargo.toml` は帳簿のクレートを `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]` に書く(テストの `tools/rust/Cargo.toml`。`Cargo.lock` は変わらず、`--locked` で組める)。
- **決定(パッケージに一つの Wasm)**:Wasm はパッケージに一つで、呼ぶモジュールを入力の `module` で選ぶ。
**捨てたもの**:Wasm をモジュールごとに一つ組む形(rulec と koyomi のファイルごとと同じ)。vectors の行を `module` 無しで渡せるが、パッケージのクレートをモジュールの数だけ組むことになり、テストのパッケージ(52 のモジュール)では組むだけで数分かかる。一つのクレートを組んだものがネイティブのハーネスと同じコードになる、という利点も無くなる。
- **ファイル**:Wasm のための二つのファイルの中身は `kaname::r#gen::wasm` が作り、クレートを組み立てる側(`ritsu gen --target rust` とテスト)が置く(36.17)。
- `abi(lang)`:クレートの根の `wasm.rs`(`FILE`)。canonical ABI の三つの関数で、`call` は入力が UTF-8 であることを確かめてから `crate::glue::door::answer` に渡し、UTF-8 でなければ `crate::glue::door::not_utf8()` の拒否の行を返す。三つの関数の本体と doc コメントは `ritsu_emit::wasm::abi` が書き、koyomi の Wasm と同じである(36.12)。
- `DECLARE`:根がこのファイルを読む行 `#[cfg(target_arch = "wasm32")] mod wasm;`。wasm32 のほかに組めば三つの関数は無い。パッケージのクレートを依存に読んで自分の Wasm を組むクレートとも、export の名前がぶつからない。三つの関数が根でないモジュールにあっても export されることは、36.15 で確かめた。
- `wit(modules, name, version, lang)`:パッケージの `.wit`(`wit_file(name)`)。名前は 36.11 のとおりで、ドキュメントのコメントに、呼び方と答えの形、範囲と失敗は `kaname api` が言うこと、振替の扱い(帳簿を使うモジュールがあるときだけ)、止まらない入力のこと、呼べるモジュールと関数の型(`kaname api` の型の書き方)を書く。実物は 36.15 にある。
- **組む行**:`ritsu_emit::wasm::CARGO_LINE`(`cargo rustc --lib --release --target wasm32-unknown-unknown --crate-type cdylib` に、リリースのプロファイルの四つを `--config` で渡す。36.15)。モジュールは `<ターゲット>/wasm32-unknown-unknown/release/<クレート>.wasm` に出る。テストのパッケージ(52 のモジュール)では、英語が 1,760,771 バイト、日本語が 1,779,314 バイトで、`examples/late_payment` の日本語の版だけのパッケージは 66,360 バイトだった。どれも import は無い。
- **スタック**:リンカーの既定の 1 MiB のままにする。テストのパッケージの全部のケースは、64 KiB でも 1 MiB と同じに答えた(36.15)。
- 何も止めないので、診断は無い。入る関数と「振替は値で答え、当てる関数は入らない」は、`.wit` のドキュメントのコメントに書く。
### 36.11 Wasm:受け渡し
- **形**:rulec と koyomi と同じ `call: func(input: string) -> string`、`cabi_realloc`、`cabi_post_call`、`memory`。import は無い。モジュールごとの呼び方は、`kaname api` の `wasm` の項が機械に言う(この節の終わり)。
- **入力**:`{"module": <モジュール>, "function": <関数>, "in": {<引数の名前>: <値>, …}}`。関数と引数とフィールドと列挙の値は、vectors の行と同じく書いたとおりの名前で書く(別名ではない。日本語の名前もある)。モジュールは、手元のモジュールなら書いたとおりの名前(`支払いの遅れ`)、依存のモジュールなら正規の書き方の連なりを前に付けたもの(`@pricing/fees`。ritsu の DESIGN 17.8)。ほかのキーは読まない。ホストが呼ぶ関数を持つモジュールがパッケージに一つなら、`module` を省ける。そうすれば vectors の行をそのまま渡せる(`out` と `why` は読まない)。二つ以上なら `module` が要る。
- **読み方**:どの JSON のエンコーダーが書いたものも読む(koyomi の 13.2 と同じ)。キーの `\u` のエスケープ(Python の `json.dumps` は既定で日本語の名前をそう書く)、空白、整数の値のいろいろな書き方(`20`、`20.0`、`2e1`。rulec の §15.205 と同じ)を読む。同じキーが二度あれば、後の値を読む(JavaScript の `JSON.parse` と同じ)。
- **値**:vectors の値の形(9 章)。お金は宣言した単位の整数、率は刻みの数、日付は `YYYY-MM-DD`、列挙は値の名前、フィールドを持つ値は `{"<値の名前>": {フィールド}}`、`result` は `{"ok": 値}` か `{"err": 値}`、レコードはフィールドの名前のオブジェクト、リストは配列、辞書は組の配列、集合は配列、`none` は `null`、振替の値は `{"book", "transfer", "op", "args"}`。日付は「4 桁以上の年-2 桁の月-2 桁の日」の数字の形なら `kaname::Date` に読み、暦に無い日(2026-02-30、2026-13-01)も 9999 年より後の日(10000-01-01)も年 0 の日も、入口が確かめて `input` の失敗で答える(Go のハーネスの読み方と同じ)。Rust の入口は、範囲の両端を書いた日付でも暦にある日かを確かめる(`on_calendar()`。22.5)。2026-02-30 の日の番号は 2026-03-02 のもので、両端のあいだに入るからである。
- **答え**:その入力の vectors の行から `why` を除いたもの(`{"function":…,"in":…,"out":…}`、失敗なら `"fails": {"kind","module","line","message"}`)で、文字も同じにする。`in` は、読んだ値を出力と同じ書き方で書き直したもの(辞書と集合は鍵の順、レコードは宣言の順)。文字列の書き方は ritsu-base の JSON(serde_json と同じ。`\n`・`\t` などは名前で、ほかの制御文字は 16 進の 4 桁のエスケープ、日本語はそのまま)に合わせる。
- **`KanameError` と拒否の行**:関数の失敗(`input` を含む六つの種類)は、拒否ではなく、vectors と同じ `fails` の行で答える。入口の外れも、先に `check_<関数>` を呼んで `fails` で答え、panic しない。拒否の行 `{"refused":"input","error":"<文>"}` は、入力を呼び出しとして読めないときだけに使い、種類はいつも `input` である(rulec と koyomi の Wasm の、自分で拒む入力と同じ)。UTF-8 でないバイト列、JSON でない、オブジェクトでない、そのモジュールか関数が無い(中の関数も)、`in` が無いかオブジェクトでない、引数が無いか JSON の種類が違う、整数でない数、int64 の外の数、日付の数字の形でない文字列、列挙の値でない名前、深すぎる入れ子、がそれにあたる(「The input is not UTF-8」「The input is not a JSON object」「There is no module `…`」「The module `…` has no function `…` a host calls」「The argument `…` of `…` is outside int64」など)。文は生成の言語(`--lang`)で書く。
- **深すぎる入れ子**:読む JSON の深さの上限(`DEPTH`)は、引数の型の 201 段の入れ子が JSON でとる深さ(生成のときに型から求める)と 1,024 の大きいほうにする。201 段の入力は読んで入口が `input` で答え、それより深いものは読まずに拒否する(「The input nests deeper than 1024」)。スタックを守るためである。
- **正しくない文字列**:JSON の `"\ud800"` のような、Unicode のスカラー値にならないエスケープは、入口の失敗「The argument `<パス>` of `<関数>` is no well-formed string of Unicode」(25.8)の `fails` の行で答える。TypeScript と Python の入口が答えるのと同じである。Rust の値はその文字列を持てないので、入口は引数を読むあいだに、確かめる順(宣言の順、フィールドの宣言の順、要素の順)で最初の正しくない文字列の場所を覚え、範囲の確かめより先に言う。ほかの出力先の入口は範囲の外れと同じ順で確かめるので、一つの入力に正しくない文字列と範囲の外れの両方があると、答える失敗が違いうる(宣言の外の入力のケースは、どれも一か所だけを外す)。int64 の外の数は持てないので拒否する(その入力は Rust では流さない。Go と同じ)。
- **止まらない入力**:参照インタプリタが止まらないと言う入力(200 段を超える再帰)では、生成物が段を数えないので、Wasm のスタックを使い切ってトラップすることがある。`call` がトラップしないと言えるのは、参照インタプリタが止まる入力までである。そう `.wit` と文書に書く。
- **`.wit` の名前**:`package kaname:<world>@<版>; world <world> { export call: func(input: string) -> string; }`。`<world>` はパッケージの名前(`ritsu gen` の `--name`)を、koyomi と同じ `world()` で直したもの(`_` を `-` にし、続く `-` と端の `-` をまとめ、WIT の予約語なら前に `%` を付ける)。版は `wit` が受け取ったもの(semver)で、無ければ書かない。`ritsu gen` は `ritsu.mod` の `version` を渡す(36.17)。`--name` が取る名前のうち、Python の予約語でもある WIT の予約語(`as`、`async`、`from`、`import`、`with`)は、`ritsu gen` が先に止める。
- **`kaname api` の `wasm` の項**(11.1):ホストが呼ぶ関数を持つモジュールでは、`call`、`call_signature`、`post_return`、`realloc`、`memory`(rulec と koyomi の `api` の `wasm` と同じ名前)、`input`(呼び出しのキー `["module", "function", "in"]` と、このモジュールを選ぶ `module` の値)、`functions`(呼べる関数の名前)、`answers`(答えの三つの形。`answer` は `function`・`in`・`out`、`failure` は `function`・`in`・`fails` と `fails` のキー、`refusal` は `refused`・`error` と `refused` の値 `input`)を持つ。振替を作るモジュールでは、振替の値のキー(`transfers`)も持つ。ホストが呼ぶ関数を持たないモジュールは Wasm に入らないので、項は `null` である。`input.module` は、モジュールが書いた名前である。依存のモジュールとしてパッケージに入れば、呼び出しは依存の名前の連なりを前に付けて書く(`@pricing/fees`)が、一つのモジュールの `api` はそれを知らない。
### 36.12 Wasm の書き出しを共通にする(`ritsu-emit` の `wasm`)
**決定**:koyomi の Wasm の書き出しのうち、koyomi の生成物を一字も変えずに分けられる部分を `ritsu_emit::wasm` に置き、koyomi と kaname と rulec がそれを使う。
- `ritsu_emit::wasm` にあるもの:`RUSTC_FLAGS`(`-C opt-level=s -C lto -C panic=abort -C strip=symbols --target wasm32-unknown-unknown --crate-type cdylib`)と、同じ設定を cargo で書いた `CARGO_LINE`(36.15。`-- -C lto` は cargo では組めず、`--config profile.release.*` で渡す)、`CALL_SIGNATURE`、WIT の予約語の表(`WIT_KEYWORDS`)、`world`、`semver`、`package`(`<言語>:<world>@<版>`)、component を作る二つのコマンド(`component_line`)、ABI の三つの関数(`abi`。`REALLOC`、`call`、`POST_CALL`)とそれが読む `use`(`ALLOC_USE`)、JavaScript のホストが `call` を呼ぶ関数(`NODE_CALL`)。
- `call` は、答えを作る式と、UTF-8 でない入力に答える式を受け取る。koyomi は `_answer(text)` と `_refusal("input", …)` を、kaname は `crate::glue::door::answer(text)` と `crate::glue::door::not_utf8()` を、rulec は `answer(text)` と `not_utf8()` を渡す。どれも、入力を `std::str::from_utf8` で確かめてから読む。
- koyomi に残すもの:JSON を読む部品(`_space`、`_word`、`_number`、`_value`、`_string`)、整数を読む `_whole`、拒否の行を作る `_refusal`、答えを書く部品。kaname の入口(`glue/door.rs`)には同じ役の部品があるが、中身が違うので共通にしない。koyomi の `call` は配列と真偽を読み捨てて 64 段まで読み、正しくないエスケープを U+FFFD にし、制御文字を全部 16 進のエスケープで書く。kaname の入口は、配列と真偽を持ち、36.11 の深さまで読み、正しくない文字列に印を付け、serde_json の書き方で書く。整数も、koyomi の `_whole` は「整数でない」と「int64 の外」を分けずに `None` を返すが、kaname は二つを別の文で拒否する(36.11)。拒否の行も、koyomi は種類(`input`、`range`)を持ち、kaname はいつも `input` である。部品を一つにすれば、0.36.0 で出た koyomi の生成物の文字が、kaname の読み方の直しにつれて変わることになる。
- 確かめ方:koyomi の生成物(木の全部の `.cal` の `gen` の英日と `api`)が、替える前(0.40.0 の公式のバイナリ)と一字も同じであること。rulec の生成物(木の全部の `.rule` の `gen` と `api`)は、`wasm/<別名>_wasm.rs` のほかが同じであること(36.15)。
- **rulec**:JSON を読む部品と拒否の行とその文は rulec のものを残し、ABI の三つの関数、組み立てのフラグ、`call` の署名、`.wit` の world と package の名前、component の二つのコマンド、Node のランナーの `call` を `ritsu_emit::wasm` で書く(rulec の DESIGN §15.278)。rulec の生成物で変わるのは `wasm/<別名>_wasm.rs` だけである。export が `#[no_mangle]` から `#[unsafe(no_mangle)]` になってエディション 2024 でも組め、`call` が UTF-8 でない入力を確かめずに `String::from_raw_parts` で `String` にしていた(未定義の動作)ところが、拒否の行になった。`.wit` の `package` と `world` の名前も `world()` で作るので、`__` を持つ別名、`_` で終わる別名、WIT の予約語の別名でも、wasm-tools が WIT を読む(前の名前は読めなかった。36.15)。木の規則には、名前の変わるものは無い。使う人の `rulec gen --check` は、前の版が書いた `_wasm.rs` を古いと言う。
### 36.13 捨てたもの
- **辞書と集合を、鍵の順に並べた `Vec` と順の関数で持つ形**(Go の `SortedKeys` と同じ考え)。どの鍵の型の `Ord` も鍵の順になるので、`BTreeMap` で同じことが標準のライブラリだけでできる。ホストも普通の `BTreeMap` を渡せる。
- **鍵に限った型の引数の順を、隠れた一つ目の引数で渡す形**(26.7)。`Ord` のトレイト境界で足りる。
- **値を全部持つ形**(引数も値で受け取り、使うたびにコピーする)。生成は易しいが、ホストが呼ぶたびにレコードとリストをコピーさせることになる。
- **関数の型を型の引数(`impl Fn`)にする形**。ラムダごとに総称の関数の実体が増え、エフェクトを書かない関数の型の扱いも楽にならない。
- **生成物の失敗を panic にして、Wasm だけ `catch_unwind` で受ける形**。wasm32 は `panic=abort` で組むので受けられない。
- **Rust では ASCII でない名前を全部 `v` などに替える形**。TypeScript と Python と Go は書いたとおりにしているので、生成物をソースと並べて読めなくなる。代わりに、クレートの根で三つの lint を許し(36.8)、出力先が名前にできない字だけを替える(36.3)。
- **Rust の予約語を E007 に足す形**。いま通っているモジュールが止まる(36.3)。
- **モジュールの名前の五つを、どの出力先でも E007 で止める形**。TypeScript・Python・Go では何も壊れないので、Rust を書かない人にも名前を替えさせることになる(36.3)。
- **標準の型と同じ名前のモジュールの型を、名前を変えずに置き、標準の型をそのモジュールではパスで書く形**。生成物のどこで標準の型を書くかを全部パスにすることになり、ホストの書くコードでも、モジュールの型と標準の型が同じ名前になる。`_` を足す形(`Option_`)は、生の識別子にできない語と同じ決まりで済む。
- **規則の Rust の名前を、口の言語によらない事実(規則の別名、入力と出力の名前と順)から、rulec の Rust の名前の決まりで kaname が作る形**。決まりが二か所に分かれ、ずれればパッケージが組めない。口の `RuleFacts::rust` に足し、rulec が `api` と同じ関数で埋めるほうが確かである(36.6)。
### 36.14 変わること
- `kaname::r#gen::Target` に `Rust`(キー `rust`)を足した。`Target::ALL` に入るので、`kaname api` の `generated` に `rust` のキーが増える。増えるキーは、頭の `generated.rust`(モジュールのファイル。`glue/late_payment.rs`)、`enums[].generated.rust`、`enums[].values[].generated.rust`、`records[].generated.rust`、`functions[].generated.rust` である。あわせて、`functions[]` に `check` を足した。入口の確かめの名前(`{"go": "CheckClaimFn", "rust": "check_claim"}`)で、中の関数は `null` である。ほかのキーは変わらない。`api` の golden は、`KANAME_BLESS=1` で取り直した。
- `kaname::r#gen::Options` に帳簿の置き場所(`store`)を足した(Rust の当てる関数だけが読む)。
- 生成物のファイル:`glue/kaname.rs`、`glue/<別名>.rs`、`glue/books/<帳簿>.rs`、`glue/books/<帳簿>_apply.rs`、`glue.rs` と `glue/books.rs`(Rust のモジュールの木をつなぐファイル)、`glue/door.rs`(JSON の一行の呼び出しに答える入口)。
- **W001、E008**:モジュールの名前か別名が `crate`、`self`、`Self`、`mod`、`door` のとき、`check` は W001 の警告を言う。止まるのは `ritsu gen --target rust`(E008)だけで、`check` と TypeScript・Python・Go の生成は止まらない(36.3)。例とテストのモジュールと、ritsu のテストのプロジェクトには、この名前のモジュールは無い。
- **関数の中の名前の字**:TypeScript・Python・Go の生成物で、出力先が名前に取らない字を持つ関数の中の名前が `v` になる(36.3)。変わるのは、前の生成物を各言語が読めなかった行だけで、木の全部の `.kn` では `names.kn` の `letters` の行だけである(36.15)。
- 口の `RuleFacts` に `rust` を足した(36.6)。
- `kaname api` に `wasm` の項を足した(36.11。`generated` の後に入る)。ほかのキーは変わらない。`api` の golden は `KANAME_BLESS=1` で取り直し、16 本とも差は `wasm` の項だけだった。
- `ritsu_emit::wasm` を足し、koyomi と rulec の Wasm の書き出しはそれを使う(36.12)。koyomi の生成物は変わらず、rulec の生成物は `wasm/<別名>_wasm.rs` だけが変わる(36.15、rulec の DESIGN §15.278)。
- **範囲の両端を書いた日付**:TypeScript と Go の入口も、範囲の両端を書いた日付が暦にある日かを確かめる(22.5)。前は日の番号(TypeScript は文字列)を両端と比べるだけで、ホストが渡した 2026-02-30 を 2026-03-02 として答え、TypeScript では日付の形でない文字列が入口を通って、中の計算が JavaScript の例外で止まった(36.15)。生成物で変わるのは、そういう引数とフィールドを持つモジュールの、TypeScript と Go の入口の確かめの `if` の行だけで、使う人の `ritsu gen --check` はそのファイルを古いと言う。Python の `datetime.date` は、いつも暦にある日である。
### 36.15 確かめたこと
**前と後の比べ**。替える前は 0.40.0 の公式のバイナリで、替えた後は手元のビルドで、同じ入力から取ったものを一字ずつ比べた(木をコピーした場所で、ディレクトリの全部、標準出力、標準エラー、終了コード)。
- `rulec gen` と `rulec api`:コーパスと例の `.rule` の全部(英日の 994 ファイル)の `gen` を、形と言語はそのまま、`--format json`、`--lang ja` の三通りで、ほかに 160 のディレクトリで、`api` を一つずつ、4,136 回。`gen` には、0.40.0 には何も足さず、替えた後には 0.40.0 と同じファイルを書く rulec の `targets::UP_TO_0_40` の引数(rulec の DESIGN §15.277)を足した。違ったのは `gen` の 1,931 回で、違うファイルはどれも `wasm/<別名>_wasm.rs` だった(2,210。36.12)。ほかに書いた 127,041 のファイルと、`api` の 994 回は同じだった。
- kaname と `ritsu gen`:木の全部の `.kn`(例、`tests/modules/`、ritsu の `tests/projects/`)の `check` 218 回、`vectors` 52 回、`ritsu gen` の TypeScript・Python・Go(帳簿の置き場所の三つと `--library`)399 回。違ったのは 33 回である。30 回は、範囲の両端を書いた日付の引数かフィールドを持つモジュール(例の `late_payment` の英日、`tests/modules/` の `dicts`・`results`・`sums` の英日と `lists`・`walks`、ritsu の `tests/projects/settlement`)の TypeScript と Go で、差は入口の確かめの `if` の行だけだった(ファイルごとに 1 行か 2 行。36.14)。残りの 3 回は `tests/modules/names.kn` の生成物で、差は `letters` の行である。前の生成物はそこで組めなかった:
```text
(0.40.0 の Go を gofmt -l で)
glue/names/names.go:374:7: illegal character U+00B2 '²'
glue/names/names.go:376:9: illegal character U+0940 'ी'
glue/names/names.go:376:28: illegal character U+00B2 '²'
glue/names/names.go:378:6: illegal character U+216B 'Ⅻ'
glue/names/names.go:378:21: illegal character U+0940 'ी'
glue/names/names.go:380:18: illegal character U+216B 'Ⅻ'
glue/names/names.go:381:2: expected ';', found 'return'
(0.40.0 の Python を ast.parse で)
File "glue/names.py", line 363
m²: int = n * n
^
SyntaxError: invalid character '²' (U+00B2)
(0.40.0 の TypeScript を tsc --noEmit --strict で)
glue/names.ts(359,10): error TS1127: Invalid character.
glue/names.ts(359,11): error TS1134: Variable declaration expected.
glue/names.ts(361,25): error TS1127: Invalid character.
glue/names.ts(361,27): error TS1134: Variable declaration expected.
```
後の生成物では、三つとも何も言わない(16.4 の型の検査も通る)。検査を通らない 9 のモジュールは、どちらの版の `ritsu gen` も書かないので、その生成物は出力先の突き合わせのテストだけが確かめる。
- koyomi の生成物(木の `.cal` とカレンダーの 283 回。Wasm を含む全部の出力先)と chobo の生成物(110 の帳簿の 14 の出力先、1,540 回)は、どれも同じだった。chobo の E063 と Rust のクライアントは、ritsu-emit に移した表を読む。
**パッケージの形のクレート**(形の実験)。0.40.0 の `ritsu gen --target go` が `crates/ritsu/tests/projects/settlement` に書く Go の `glue/` を見て、同じ関数を 36.2〜36.8 の形の Rust で手で書き、rulec 0.40.0 の Rust、koyomi 0.40.0 の Rust、単位のモジュールと合わせて、一つのクレートにして組んだ。根に三つの lint の許しだけを置いたとき(rustc 1.94.1、エディション 2021 と 2024 で同じ)、rustc の警告は十三件とも rulec の単独の形の二つのモジュールのもので、kaname の形のファイルからは一件も出なかった:
```text
1 warning: 13 warnings emitted
2 warning: allow(uncommon_codepoints) is ignored unless specified at crate level
2 warning: function `max_i64` is never used
2 warning: function `min_i64` is never used
2 warning: function `round_bankers` is never used
1 warning: function `round_down` is never used
2 warning: function `round_half_down` is never used
2 warning: function `round_up` is never used
```
この二つはパッケージの形の規則のモジュール(36.6)で無くなった。clippy の指摘も、規則と日付のファイルのものだけだった:
```text
== clippy edition 2021
error: manual `!RangeInclusive::contains` implementation
--> rules/late_payment_charges.rs:110:8
error: aborting due to 1 previous error
== clippy edition 2024
error: manual `!RangeInclusive::contains` implementation
--> rules/late_payment_charges.rs:110:8
error: this `if` statement can be collapsed
--> dates/payment_terms.rs:120:5
error: this `if` statement can be collapsed
--> dates/payment_terms.rs:121:9
error: aborting due to 3 previous errors
```
**クレートの単位の lint**(モジュールにだけ許しを書いたとき):
```text
warning: found both `ロ` and `口` as identifiers, which look alike
= note: `#[warn(confusable_idents)]` on by default
warning: allow(uncommon_codepoints) is ignored unless specified at crate level
= note: `#[warn(unused_attributes)]` (part of `#[warn(unused)]`) on by default
```
カタカナが `エ カ タ ト ニ ハ ロ` だけのクレート:
```text
warning: the usage of Script Group `Japanese, Katakana` in this crate consists solely of mixed script confusables
= note: `#[warn(mixed_script_confusables)]` on by default
```
`々`、`〇`、`ヶ`、`ー` を持つ名前は `uncommon_codepoints` に当たらなかった。
**名前空間**(rustc 1.94.1、二つのエディションで `-D warnings`)。`use crate::glue::kaname;` のあるモジュールで、`let kaname = 1;` の後の `kaname::add(kaname, …)` が通った。名前付きフィールドの `pub struct Some { … }` を置いたモジュールで `Some(3)` が通った。引数 `r#gen` が通った。`pub mod r#match;` は `rules/match.rs` を読んだ。止まったものは次のとおり:
```text
(pub struct Option { … } を置いたモジュールの Option<i64>)
error[E0107]: struct takes 0 generic arguments but 1 generic argument was supplied
error[E0308]: mismatched types
(rules.rs の pub mod r#mod;)
error[E0761]: file for module `rules` found at both "a/rules.rs" and "a/rules/mod.rs"
(エディション 2024 の #[no_mangle])
error: unsafe attribute used without unsafe
```
**スタック**。テストのハーネス(デバッグのビルド。36.9)は、英語のパッケージの 16,770 件に、メインのスレッドのスタックが 1.5 MiB でも 8 MiB と同じ行で答えた。1 MiB では、`trees.kn` の `leaf_sum` に渡す 200 段の値(16,722 件目)でスタックがあふれた(macOS の arm64、`ulimit -s`)。
**WIT の名前**。`package rulec:<名前>@1.0.0; world <名前> { export call: func(input: string) -> string; }` を `wasm-tools component wit` で読んだ。`a--b`、`late-`、`result` は、前の rulec の `wit_name` が別名 `a__b`、`late_`、`result` から作っていた名前である(いまの rulec は `world()` で、`a-b`、`late`、`%result` にする。36.12):
```text
a--b => error: identifiers must have characters between '-'s
late- => error: identifiers must have characters between '-'s
result => error: expected an identifier or string, found keyword `result`
r-1st => ok
%result => ok
```
**`call` がクレートの根でなくてもよいこと**。`#[unsafe(no_mangle)]` の三つの関数を、クレートの根ではない `mod door` に置いたライブラリを、`RUSTC_FLAGS` で wasm32 の cdylib に組んだ。二つのエディションとも次を export し、import は無かった(15,551 バイト):
```text
(export "memory" (memory 0))
(export "cabi_post_call" (func 0))
(export "cabi_realloc" (func 2))
(export "call" (func 6))
(export "__data_end" (global 1))
(export "__heap_base" (global 2))
```
スタックの頭は `(global (;0;) (mut i32) i32.const 1048576)` で、スタックは 1 MiB である。
**cargo で組む行**。同じライブラリを `[lib] path = "lib.rs"` のクレートにし、cargo 1.94.1 で組んだ:
```text
$ cargo rustc --lib --release --target wasm32-unknown-unknown --crate-type cdylib -- -C lto
error: options `-C embed-bitcode=no` and `-C lto` are incompatible
$ cargo rustc --lib --release --target wasm32-unknown-unknown --crate-type cdylib \
--config 'profile.release.opt-level="s"' --config 'profile.release.lto=true' \
--config 'profile.release.panic="abort"' --config 'profile.release.strip="symbols"'
exit 0
```
二つ目は、rustc で組んだものと同じ 15,551 バイトで、同じ六つを export した。
**koyomi の Wasm の書き出しを替える前と後**。木の全部の `.cal`(347)の `gen`(英語と日本語)と `api` を、0.40.0 の公式のバイナリと手元のビルドで走らせ、上と同じく比べた。1,041 回のうち 973 回が同じだった(書いたファイルは 4,928)。違った 68 回は 34 のファイルの `gen` の英日で、0.40.0 の後に入った E016(koyomi の DESIGN 15 章)で、新しい側が何も書かずに 1 で終わる。その 34 のファイルを `--target wasm` だけで書くと、26 のファイルの英日の 52 回は同じものを書き(208 のファイル)、残りの 8 のファイルは、W001 が Wasm を挙げるので E016 で止まった。
**パッケージの Wasm**(`tests/wasm.rs` の出力):
```text
cargo clippy -- -D warnings passes the crate built for wasm32 on the 2021 and the 2024 editions in English and in Japanese, and Node v24.21.0 calls the module
En: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules (the module, 1760771 bytes, imports nothing)
Ja: 16770 of 16770 cases agree with the reference: 14096 vectors (5 that do not stop are not run), 2584 inputs outside what the parameters declare and 90 values of types that hold themselves nested 200 and 201 deep (14 that do not stop are not run), in 52 modules (the module, 1779314 bytes, imports nothing)
en: 35 inputs answered as the table says, and the memory the same after 20,000 calls
ja: 11 inputs answered as the table says, and the memory the same after 20,000 calls
wasmtime called the component of the package late_payment
wasmtime called the component of the package fees
wasmtime called the component of the package payout
names ritsu gen does not take (words Python keeps): as async from import with
```
契約の外の表から、入力と答えをいくつか挙げる(試験はどれも一字一句比べる):
```text
{"module":"calc","function":"fee","in":{"a":10.00E2}}
=> {"function":"fee","in":{"a":1000},"out":84}
{"module":"calc","function":"fee","in":{"a":99999999999999999999}}
=> {"refused":"input","error":"The argument `a` of `fee` is outside int64"}
{"module":"calc","function":"days","in":{"a":"2026-02-30","b":"2026-03-01"}}
=> {"function":"days","in":{"a":"2026-02-30","b":"2026-03-01"},"fails":{"kind":"input","module":"calc","line":29,"message":"The argument `a` of `days` is 2026-02-30, outside its range >=2026-01-01 <=2026-12-31"}}
{"module":"generic","function":"first_or","in":{"xs":[],"fallback":1}}
=> {"refused":"input","error":"The module `generic` has no function `first_or` a host calls"}
([ が 2,000 個と ] が 2,000 個)
=> {"refused":"input","error":"The input nests deeper than 1024"}
(バイト列 7B 22 FF 22 7D)
=> {"refused":"input","error":"The input is not UTF-8"}
{"function":"遅れの請求","in":{"請求書":{"番号":"inv_1","請求日":"2026-03-02","金額":100000},"支払日":"2026-5-20","基準金利":50}}
=> {"refused":"input","error":"関数 `遅れの請求` の引数 `支払日` が YYYY-MM-DD と書いた日付ではありません"}
```
`examples/late_payment` の日本語の版だけのパッケージの `.wit`(`wit` の出力。名前は `late_payment`、版は 1.0.0):
```wit
// Code generated by kaname 0.40.0. DO NOT EDIT.
package kaname:late-payment@1.0.0;
/// パッケージ late_payment の kaname のモジュールにある、ホストが呼ぶ関数
world late-payment {
/// 一回の呼び出し。input は JSON のオブジェクト {"module": <モジュール>, "function": <関数>, "in": {<引数>: <値>, …}} で、名前はどれもモジュールが書いたとおり、値は kaname vectors の書き方で書く。kaname vectors の行も同じに読む。
/// モジュールは一つなので、"module" は省ける。
/// 返すのは、kaname vectors がその入力に書く行から why を除いたもの {"function":…,"in":…,"out":…} で、関数が失敗すれば out の代わりに "fails": {"kind","module","line","message"} を持つ。入力が、ホストが呼ぶ関数の呼び出しとして読めなければ、拒否の行 {"refused":"input","error":"<文>"} を返す。
/// 引数の範囲と要素の数、関数がどう失敗しうるかは、モジュールごとの kaname api が言う。
/// 参照インタプリタが止まらないと言う呼び出し(深すぎる再帰)では、モジュールのスタックが尽きてトラップすることがある。
///
/// モジュールと関数:
/// 支払いの遅れ
/// 遅れの請求(請求書: 請求書, 支払日: date, 基準金利: rate[step 0.01%]) -> 請求?
export call: func(input: string) -> string;
}
```
その component(66,688 バイト)を wasm-tools と wasmtime 44.0.0 で読んで呼んだ:
```text
$ wasm-tools component wit late_payment.component.wasm
package root:component;
world root {
export call: func(input: string) -> string;
}
$ wasmtime run --invoke 'call("{\"function\":\"遅れの請求\",\"in\":{\"請求書\":{\"番号\":\"inv_1\",\"請求日\":\"2026-03-02\",\"金額\":100000},\"支払日\":\"2026-05-20\",\"基準金利\":50}}")' late_payment.component.wasm
"{\"function\":\"遅れの請求\",\"in\":{\"請求書\":{\"番号\":\"inv_1\",\"請求日\":\"2026-03-02\",\"金額\":100000},\"支払日\":\"2026-05-20\",\"基準金利\":50},\"out\":{\"期日\":\"2026-03-31\",\"遅れた日数\":50,\"定額の補償\":70,\"利息\":1150}}"
```
**パッケージの Wasm のスタック**。上の英語と日本語のモジュールを `CARGO_LINE` で組み直し、スタックの頭(`(global (;0;) (mut i32) i32.const 1048576)`)を小さくして、試験の 16,770 件を同じホストで呼んだ:
```text
en 1024 KiB: 16770 calls, 0 trapped
en 64 KiB: 16770 calls, 0 trapped
en 48 KiB: 16770 calls, 18 trapped
en 32 KiB: 16770 calls, 36 trapped
en 16 KiB: 16770 calls, 86 trapped
en 8 KiB: 16770 calls, 90 trapped
```
日本語も同じ数だった。64 KiB の答えは 1 MiB の答えと一字も違わない。48 KiB でトラップしたのは、`catalog.kn` とその日本語の版に 200 段と 201 段の値を渡す 18 件(JSON の深さ 403〜405)で、32 KiB では `trees.kn` とその日本語の版の 18 件が加わる。要るスタックは 48 KiB と 64 KiB のあいだで、既定の 1 MiB はその 16 倍より大きい。データはスタックの上(`(data (;0;) (i32.const 1048576)`)から始まるので、スタックがあふれれば 0 番地の下を指し、データを書きつぶさずに「memory access out of bounds」でトラップする。
**暦に無い日**(`tests/rust.rs`、`tests/go.rs`、`tests/typescript.rs` の `a_day_of_no_calendar_is_outside_every_range` の出力。16.4)。Rust と Go は、ホストの作った日付を入口の確かめ(`check_days`、`CheckDays`)に渡し、TypeScript は関数(`days`)に渡して、答えか文を一行ずつ書く。Rust と Go は同じ六行を書く:
```text
calc 2026-02-30: The argument `a` of `days` is 2026-02-30, outside its range >=2026-01-01 <=2026-12-31
calc 2026-13-01: The argument `a` of `days` is 2026-13-01, outside its range >=2026-01-01 <=2026-12-31
calc 2026-03-02: ok
open 2026-02-30: The argument `first` of `days` is 2026-02-30, outside its range >=0001-01-01 <=9999-12-31
open 10000-01-01: The argument `first` of `days` is 10000-01-01, outside its range >=0001-01-01 <=9999-12-31
open 2026-03-02: ok
```
TypeScript:
```text
calc 2026-02-30: The argument `a` of `days` is 2026-02-30, outside its range >=2026-01-01 <=2026-12-31
calc 2026-0x-yy: The argument `a` of `days` is 2026-0x-yy, outside its range >=2026-01-01 <=2026-12-31
calc 2026-03-02: -1
open 2026-02-30: The argument `first` of `days` is 2026-02-30, outside its range >=0001-01-01 <=9999-12-31
open 10000-01-01: The argument `first` of `days` is 10000-01-01, outside its range >=0001-01-01 <=9999-12-31
open 2026-03-02: -1
```
両端を書いた日付を暦で確かめる前は、`calc` の一行目が違った。Rust と Go では `calc 2026-02-30: ok` で、TypeScript では `calc 2026-02-30: -1`(2026-03-02 として答えた)と、二行目の `calc 2026-0x-yy: The number NaN cannot be converted to a BigInt because it is not an integer` だった(Rust と Go の 2026-13-01 は、日の番号が 2027-01-01 のものなので、前も範囲の外だった)。前の生成器では、三つのテストとも落ちる。
### 36.16 見つけて直していないもの
- rulec と koyomi の Rust のモジュールは、clippy に当たる形を持つ(36.8)。どちらも単独の生成物と同じ形で、変えると使う人の `--check` が落ちる。
- 表のほうがツールの Unicode より新しいとき、表が通す字をツールが名前に取らない(36.3)。`tests/idents.rs` で、Python 3.13.11(Unicode 15.1.0)は 18,040、Go 1.26.10(Unicode 15.0.0)は 19,226、tsc 7.0.2 は 18,040 の名前がそれにあたる。kaname の字句(`starts_name`、`is_name_char`)は Rust の `char` の Unicode(17.0.0)で読むので、その字を名前に取る。ツールの Unicode に合わせて表を分けることはしていない。
- Rust は識別子を NFC にしてから比べる(36.3)。NFC で同じになる二つの関数の中の名前は、Rust で同じ名前になる。Python の NFKC と同じ形の穴で、確かめていない(chobo の DESIGN 14.6 と同じ)。
### 36.17 `ritsu gen --target rust`(2026-11-04)
**決定**:`ritsu gen --target rust` が Rust のパッケージを書く。クレートの形(`Cargo.toml`、`lib.rs`、部分をつなぐファイル、帳簿の依存、ワークフローとゲートを入れないこと)は ritsu の DESIGN 9.3 にある。ここには kaname の側を書く。
- **書くもの**:`glue/` の下の全部(36.14)は `r#gen::package_for` が、帳簿の置き場所を受け取って書く。根の `wasm.rs` と `<name>.wit` は、`kaname::r#gen::wasm` が作る中身を ritsu が置く(36.10)。`.wit` の版は `ritsu.mod` の `version` を書かれたまま渡す。無ければ書かない(36.11)。依存のモジュールの kaname は `deps/<名前>/glue/` に入り、入口はチェーンの名前で答える(ritsu の DESIGN 17.8)。
- **モジュールの宣言**:ritsu が書くインデックスは `ritsu_emit::package::rust_mod_line` で宣言を作り、kaname のテストがクレートを組み立てるときも同じ関数を読む(`tests/common/targets.rs`)。
- **止めるもの**:`ritsu gen` は、出力先ごとに `r#gen::stops` に尋ねる。答えるのは `Target::Rust` の E008 だけで、W001 と同じ場所と文で、最後の注だけを直し方に替える(36.3)。ほかの出力先は同じモジュールを書く。
- **出力先の表で名前にならない字**:止める診断を足さない。宣言の名前は、E005 で ASCII の別名を持つので、どの出力先でも名前になる。関数の中の名前は、出力先が取らない字なら `v` になる(36.3)。どちらも、ある出力先だけで止まることが無いので、W001 と E008 の形は要らない。
**確かめたこと**:`tests/codes.rs` が W001 と E008 の例を golden(英日)と比べ、E008 の例は `r#gen::stops` を通す。`crates/ritsu/tests/gen.rs` の `what_breaks_the_rust_stops_its_package` は、五つの名前を、名前そのものと別名で、`ritsu check` が警告で通し、`--target go --target rust` が何も書かずに止まり、TypeScript・Python・Go は書くことを確かめる。`the_packages_of_rust_build_with_no_warning` は kaname のモジュールを持つプロジェクトのクレートを警告をエラーにして組み、`crates/ritsu/tests/modules_gen.rs` の `a_dependencys_modules_and_books_are_in_the_crate` は、依存の kaname と帳簿を持つクレートの Wasm が、ネイティブと同じに答えることを確かめる。
ホストから読む形は `docs/reference.md` に実物を貼ってある。`late_payment` を `ritsu gen --target rust` で書き、パスの依存で読むクレートを手で一度走らせた答えである(試験には入れていない)。