コンテンツにスキップ

ルール(.rule)を書く

.rule ファイルは上から順に決まった形をしています。あとに出てくる名前は先に使えないので、上から読めば「何が何に依存しているか」がそのまま読めます。キーワードは英語、名前とセルは日本語のまま書けます。

書くのは、たいていエージェントです

手で書いても構いませんし、このページはその書き方でもあります。ただこのツールが想定しているのは、エージェントが規約や Excel から表に転記し、人はできあがった表を読んで確かめるという形です。だからここで身につけてほしいのは、書き方より先に読み方のほうです(材料は同じなので、読めれば書けます)。

そして読むときも一人ではありません。人が読む資料は rulec doc が出します — 表を読んだだけでは分からないことが、そこに添えられます。表にひとまとめで書いてある一語が、具体的には何を指しているのか。どの行が、手前の行に隠されているのか。どの丸めが、根拠のない仮置きなのか。同じ資料は一枚の HTML にもなり(--format html)、自分の件を入れると当てはまった行に色が付いて結果が出ます。変更なら「何件がいくら動くか」も、入れる前に出ます。どちらもエージェントに頼めるもので、分からない行はその場で訊けます。Excel からなら rulec import xlsx が、ブックをそのまま読んで下書きを起こします(推定した箇所には全部その印が付きます)。エージェント側の手順はエージェント向けにあります。

行頭に書けるのは次の語だけで、これで全部です。

書くもの 何を宣言するか
rule ファイルの先頭。規則名と版
description 一行の説明
import 列挙の値の集合を持ち込む(組み込み、.proto、JSON Schema)
enum 閉じた列挙
group 列挙の値をいくつかまとめて名前を付けたもの
inputs 規則の引数
elements 規則が順にたどる並びの、一要素ぶんのフィールド
outputs 規則の結果
derive 入力どうしの足し算・引き算(と定数倍)。数量のまま表の列に置ける唯一の途中の値
define 真偽や、計算した途中の値
constraint 入力どうしの関係。起きない組み合わせを言う
fold 要素ごとの判定を、一つの答えに畳む
count 並びの要素のうち、条件に当てはまるものの数
sum 並びの要素の、ある列の合計
sequence 例がたどる並びを、名前を付けて書く
table 決定表。この言語の本体
policy その表のヒットポリシー(unique か first)
overrides 同じ出力を定める表が上にあるとき、こちらの表の行がそれに優先すると書く。policy の次の行。行を指すなら 表:行ラベル
clause 表にならない一行の決まりを、文のまま書く。when <列> <セル> and …(条件が無ければ when always)、then <値>、必要なら overrides
source 規則の転記元の文書。法令データベースにある法令(law [<データベース>] "<ID>" asof <日付>)か、隣に置いたファイル(file "<ファイル>" sha256:…)。表・節・行・導出・定義の行末に @出典 第20条 と書いて引用する
shape 呼び出し側のオブジェクトの形。すでにある契約を借りる(jsonschema "<ファイル>" "<ポインタ>" か proto "<ファイル>" <メッセージ>)。入力の行末に from <shape の名前>.<フィールド> と書いて、そこから取り出す
apply ほかの規則ファイルを、入力を読み替えて準用する。<元の規則の入力> = <この規則の値>、except <準用しない定義>、<元の規則の出力> -> <名前>
result 出力の組み立て
machine 表を、続いていく案件の一歩として読む。どの出力を次の呼び出しでどの入力として渡すか、案件の始まりと終わり、どんな呼び出しの並びでも起きてはいけないこと
examples 実行される仕様
scenario 何回かの呼び出しにわたって走る例。どの呼び出しも、一つ前が答えた状態から始まる

一本まるごとの例

規則を一本、頭から終わりまで見ておきます。これはこのまま検査を通ります(リポジトリのテストが毎回確かめています)。

rule 送料例(fee_demo) v1
description "サイトの例。そのまま rulec check を通る"

import std/都道府県

enum サイズ区分(size_class) = S60(s60) | S80(s80) | S100(s100)
group 近畿圏(kinki) = 滋賀県, 京都府, 大阪府, 兵庫県, 奈良県, 和歌山県

inputs
  あて先(dest)    : 都道府県
  三辺合計(girth) : length[cm]  range >=1cm <=100cm
  重量(weight)    : mass[g]   range >=1g <=25kg  contract_only

outputs
  運賃(fee) : money[円, incl_tax]  round up(10円)

table サイズ判定(size_of)
policy first
| 三辺合計 | -> サイズ(size) : サイズ区分 |
| <=60cm   | S60                          |
| <=80cm   | S80                          |
| -        | S100                         |

table 運賃表(fee_table)
policy unique
| あて先      | サイズ | -> 運賃(fee) : money[円, incl_tax] |
| 近畿圏      | S60    | 990円                              |
| 近畿圏      | S80    | 1310円                             |
| 近畿圏      | S100   | 1620円                             |
| not: 近畿圏 | S60    | 880円                              |
| not: 近畿圏 | S80    | 1200円                             |
| not: 近畿圏 | S100   | 1500円                             |

examples
| あて先 | 三辺合計 | 重量 | -> 運賃 |
| 大阪府 | 55cm     | 1kg  | 990円   |
| 東京都 | 90cm     | 3kg  | 1500円  |

名前と ASCII のエイリアス

宣言の括弧の中は ASCII のエイリアス(別名)で、生成コードの公開名になります(漢字は Go の公開識別子になれないため)。

enum 会員区分(member_kind) = 一般(basic) | ゴールド(gold) | プラチナ(platinum)

別名が要るのは、ASCII でない名前が、外から呼ぶときの名前になるときだけです — 規則名・入力・出力。漢字は大文字を持てず、Go の公開識別子になれないからです。名前がもとから ASCII なら別名は要りません。全部英語で書けば、括弧はどこにも出てきません。

table band_of
policy unique
| distance         | intra_eu | -> band : band |
| <=1500km         | -        | short          |
| >1500km          | true     | medium         |
| >1500km <=3500km | false    | medium         |
| >3500km          | false    | long           |

これは EU 旅客権利規則 (EC) No 261/2004 第 7 条をコーパスに入れてあるものの一部で、ほかの規則と同じように毎回検査・生成・実行されています。EUR と km で最後まで英語のまま書いた全体は、例で見るにあります。

それ以外の場所では別名は任意で、書けば生成コードがその名前を使います。derive 残余(margin) は margin、group 遠隔地(remote) は _remote / isRemote、表の出力列 -> サイズ(size) は size になります。書かなければ宣言した名前がそのまま識別子になります(外に出ない名前なら、どの言語も日本語の識別子を受け付けます)。

table の別名だけは例外で、いまは受け付けるだけで使われません。表は一つの関数にインライン展開されるので、行き先が無いためです。SQL 生成で表そのものに名前が要るので、構文としては残してあります。

型

十で全部です。

型 書き方 押さえどころ
真偽 bool
列挙 会員区分 値が決まりきった集合(閉じた列挙)。enum で宣言するか import で持ち込む
数量 mass[g] length[cm] area[m2] volume[L] duration[h] 単位が型の一部。2kg は 2000g を書きやすくしただけ(シンタックスシュガー)で、実行時の値は宣言した単位の整数一本。質量は mg g kg t oz lb、長さは mm cm m km in ft yd mi、面積は mm2 cm2 m2 a ha km2 坪 in2 ft2 yd2 mi2 ac、体積は mm3 cm3 m3 mL L kL、時間は ms s min h d w。次元どうしは掛け合わせられません — 面積は面積という型で、縦 × 横 は E103 です
順序だけの数量 temperature[℃] sound[dB] 比べることと range だけができて、足し引きはできません(E048)。41℉ はちょうど 5℃ で、リテラルは行き来しますが、二つの温度の差は温度ではありません。デシベルは対数なので、二つ足しても二つぶんの音になりません
金額 money[円, incl_tax] money[USD, excl_tax] 通貨と税区分の二つで別の型になる。incl_tax と excl_tax は足せません。通貨は 円 か ISO 4217 のコードで、その 1/100 はコードに c(money[USD] は整数ドル、money[USDc] は整数セント)。通貨どうしは換算されません — 為替レートはこのツールの中に無いので、混ぜると E103 で止まります
率 rate[step 1%] rate 内部は刻みを単位にした整数のまま(rate[step 1%] なら 10% は 10)。入力には刻みを書きます。計算で出す率は省けて、そのときは列に現れたリテラルから刻みが決まります。rate[step 0.1%] のように 1% より細かくもできます
数 number 単位のない整数。個数、日数、点数のような、数えるだけの数。金額を同じ通貨の金額で割ると、単位が消えてこれになります
日付 date 比較と範囲だけ。加減算はありません
文字列 string 表の列には置けません(E110)。出力か、素通しの入力にだけ使えます。分岐に使う値は enum にしてください
optional 会員区分? 「値が無い」があり得る型。セルの none でだけ受けられます

数量・金額・率・数・日付はすべて整数で持ちます。日付は通算日、率は刻みの個数です。浮動小数点はどこにも現れません — 端数がどう決まるかは下の round が決め、言語の除算には任せません。

列挙は閉じています。値を後から自由に足せる「開いた列挙」はありません — 値が増えたら、それを見ていない表が完全性検査で割れるのが狙いです。

enum 会員区分(member_kind) = 一般(basic) default | ゴールド(gold) default | プラチナ(platinum)

default は「この値に専用の行は要らない、- に吸われるのが正しい」という宣言です。付けないと「どの行にも現れません」と警告されます。

グループ(group)は列挙の一部に名前を付けたもので、セルの中で値と同じように使えます。検査のときは必ず元の値に展開されるので、グループで書いた表に穴があっても完全性検査が捕まえます。

group 遠隔地(remote) = 北海道, 沖縄県

取り込み

import で始まる行は二つあって、どちらも運んでくるのは列挙の値の集合だけです。行も金額もほかの規則も越えてきません。規則は一ファイルのままです。

行 何が来るか 集合を持っているのは
import std/<名前> 組み込みの列挙 rulec(凍結済み)
import proto "<ファイル>" <列挙> -> <この規則の列挙> .proto の列挙の値の集合 その .proto(規則の外)
import jsonschema "<ファイル>" "<ポインタ>" -> <この規則の列挙> JSON Schema の列挙の値の集合(OpenAPI も同じ) そのファイル(規則の外)

rulec import は語が同じだけの別物です

rulec import csv と rulec import xlsx はコマンドで、表計算から .rule の下書きを一度だけ書き起こします。ファイルには何も残らず、あとから読み直されることもありません。上の二行は rulec check のたびに読まれます。

組み込みの列挙

組み込みの std/都道府県(47 値)は import std/都道府県 で使えます。ほかの十二か国の一段目の区分も組み込みで、import std/us/states(米国の 56)、std/gb/nations、std/cn/provinces、std/tw/divisions、std/kr/provinces、std/in/states、std/fr/regions、std/es/communities、std/it/regions、std/de/states、std/au/states、std/br/states で使えます。値は英語の名前を ASCII にしたもの(New_York、Bavaria)で、セルには現地の綴り(Bayern)や ISO の符号(NY)でも書けます。どれも同じ値です。都道府県を英語の綴りで持つ std/jp/prefectures もあり、std/都道府県 と同じ 47 の区分です(どちらでも 東京都 と Tokyo の両方を書けます)。

値を決めるのが自分たちでないとき

会員区分やステータスのような列挙は .proto で定義されていることが多く、値が増えるかどうかはこの規則の外で決まります。

import proto "api/v1/order.proto" MemberTier -> 会員区分
enum 会員区分(tier) = 一般(basic) | ゴールド(gold) | プラチナ(platinum) default

.proto が持っているのはどの値があるか、.rule が持っているのはそれを何と呼び、どう扱うかです。値の名前を日本語で書くなら、その名前は規則の側で決めます(proto にあるのは ASCII の名前だけです)。rulec check は走るたびにそのファイルを読んで、二つがそろっているかを見ます。

  • 片側にしか無い値があれば E032。増えたのはたいてい proto の側で、それはワイヤの上では互換な変更として通っています。
  • そろったあとで、どの行にも現れず default も付いていない値があれば E033。自分で書いた列挙なら警告(W111)で済むところが、取り込んだ列挙ではエラーです。増えた値は、まだ誰も読んでいない外の変更だからです。

- の行がある表は、新しい値が来ても完全性検査を通ってしまいます。その値には既定の額が黙って当たります。E033 が止めるのはそこです。

JSON Schema と OpenAPI から

同じことが JSON Schema でもできます。違うのは列挙の名指しかたで、一つの文書に列挙がいくつも入っているので、JSON ポインタで指します。

import jsonschema "api/openapi.json" "#/components/schemas/MemberTier" -> 会員区分
enum 会員区分(tier) = 一般(basic) | ゴールド(gold) | プラチナ(platinum) default

ポインタは、スキーマそのものを指しても、その enum の配列を指しても構いません。OpenAPI でよくある、property の中に直接書いた列挙も同じように指せます。

値はそのまま別名になります。.proto では接頭辞(MEMBER_TIER_)を落として小文字にしますが、あれは buf lint が守らせている慣習があるからです。スキーマにその慣習は無いので、こちらで変換すると、そろって見えるのに違う文字列を送る規則ができてしまいます。

YAML は読みません。 OpenAPI はたいてい YAML で書かれていますが、手元のファイルで使われている書き方にだけ合わせた読み手は、次のファイルで黙って間違えます。JSON にしたものを指してください。たいていのツールが書き出せます。

入力と出力

inputs
  届け先(dest)    : 都道府県
  重量(weight)    : mass[g]        range >=1g <=40kg
  注文金額(total) : money[円, incl_tax]  range >=0円 <=1000万円
  会員(member)    : 会員区分

outputs
  送料(fee) : money[円, incl_tax]  round up(10円)

出力は複数書けます。生成物は Python の NamedTuple、TypeScript の interface、JavaScript のただのオブジェクト、Ruby の Struct、PHP の final class、Java の record、Rust と Swift と Go の構造体、NumPy では出力ごとの配列、SQL では一つずつの列、Wasm のモジュールでは答えの observed オブジェクトのキーになり、丸めは出力ごとに一度ずつ掛かります。

outputs
  可否(ok)    : bool
  素割引(raw) : money[円, incl_tax]  round down(1円)

出力の値は、その出力と同じ名前の define か表の出力列から取られます。result はそれを短く書くためのもので、最初の出力にしか効きません — 二つ目以降を名指しすると E015、result を二本書くと E016 で止まります。

範囲と丸めは、書き忘れると止まる

上の例に出てくる range と round は、飾りではなく必須の宣言です。この二つが、この言語がやりたいことの中心にあります。

range は、数量と金額の入力すべてと、導出すべてに要ります。 一つの宣言が三つを兼ねます。

  1. オーバーフロー(桁あふれ)の証明 — 途中の値が int64 に収まることを、範囲と刻みから計算します
  2. 完全性検査の全体集合 — 「どの入力にも当てはまる行がある」の「どの入力」がこれで決まります
  3. 生成コードの入口ガード — 範囲外で呼ばれたら、黙って計算せずエラーを返します

導出の範囲は、入力の範囲から計算した「実際に取りうる幅」を含んでいないとエラーです(0 円〜100 万円 の入力から −10 万円 が出るなら、それも範囲に入っていなければなりません)。

範囲チェックとしてしか効かない入力には contract_only を付けます。「表の条件には出てこないが、呼び出し側との約束として範囲は守らせたい」という意思表示で、これを付けないかぎり「使われていません」の警告が出つづけます。

round は、数値の出力すべてに要ります。 端数がどう決まるかを宣言しないと、生成コードが黙って決めてしまうからです。五種あり、負の向きまで固定されています。

モード 向き 例(刻みが 1 円のとき)
up 0 から遠ざける(切り上げ) −4.2 → −5
down 0 へ寄せる(切り捨て) −4.8 → −4
half_up 半分ちょうどは 0 から遠ざける(四捨五入) −4.5 → −5
half_down 半分ちょうどは 0 へ寄せる。社会保険料の給与控除の「50銭以下は切り捨て、50銭を超えれば切り上げ」がこれです 4.5 → 4、4.6 → 5
half_even 半分ちょうどは偶数へ(銀行家丸め) 2.5 → 2、3.5 → 4

括弧の中が刻みです。up(10円) なら 10 円単位へ丸めるので、−4.2 円は −10 円になります。

負の向きまで決めてあるのは、Python と Ruby の整数除算は −∞ 方向、Rust・Swift・Go・Java・TypeScript・JavaScript・PHP の intdiv・SQL・Wasm・NumPy は 0 方向で食い違うからです。言語の素の除算に任せると、同じ規則が言語ごとに違う答えを出します。生成コードは自前のヘルパ関数を通し、どの言語でも答えが揃うことをテストが毎回確かめています。

入力を、呼び出し側のオブジェクトから取る(shape と from)

規則の入力は平たい値ですが、呼び出し側が持っているのは、たいていネストしたオブジェクトです。API のリクエストや、キューに流れるメッセージです。その形がもう JSON Schema か .proto で決まっているなら、shape で借りて、入力の行末の from で、その中のどこから来るかを書きます。

shape 注文(order) = jsonschema "contracts/order.schema.json" "#/$defs/Order"

inputs
  あて先(zone)   : 地域    from 注文.shipping.zone
  冷蔵あり(cold) : bool    from any 注文.lines where chilled = true
  明細数(lines)  : number  range >=1 <=50  from count 注文.lines

.proto なら、ファイルとメッセージの名前を書きます。shape 出荷(shipment) = proto "contracts/shipment.proto" shop.v1.CreateShipmentRequest です。

from の形は四つです。

書き方 返すもの
from 注文.shipping.zone そのフィールドの値
from any 注文.lines where chilled = true bool。要素のどれかが当てはまるか
from all 注文.lines where chilled = true bool。要素の全部が当てはまるか
from count 注文.lines number。要素の数(where を付ければ、当てはまる要素の数)

これで起きることは四つです。

  • 取り出すコードが生成されます。 rulec gen は、規則の関数のほかに、注文のオブジェクトを丸ごと受け取る関数も書きます。規則の関数が order_shipping なら、この関数は order_shipping_from(order) です。from のとおりに入力を取り出し、規則の関数を呼んで答えを返すので、呼び出し側は注文をそのまま渡すだけで済みます。この関数が出るのは Python・TypeScript・JavaScript・Ruby・PHP です。どれも、読み込んだ JSON を連想配列(Python なら dict、Ruby なら Hash)のまま使うことの多い言語なので、関数の引数も連想配列にしてあります。Go・Swift・Java・Rust・SQL・NumPy・Wasm には出ません。.proto の形なら、protojson の JSON をそのまま読みます。フィールドの名前は lowerCamelCase でも .proto の名前でもよく、省かれたフィールドは proto の既定値として読みます。呼び方と、ほかの七つに出ない理由は生成して呼ぶにあります。
  • パスが契約に照らされます。 rulec check は走るたびに契約のファイルを読みます。名指したパスが無ければ E121(どこまで届いたかと、そこにあったフィールドを言います)、型が合わなければ E120、どの入力も使わない shape は W122 です。契約の側でフィールドの名前が変わっても、本番で KeyError になる前に CI で止まります。
  • 契約の検証が、入力の宣言と突き合わされます。 契約は通すのに入力が受け付けない値があれば E122 です。たとえば契約の lines に maxItems が無ければ、51 件の注文は契約を通りますが、range >=1 <=50 の 明細数 は受け付けません。fix.text は、契約に書き足す注釈やキーワードそのものです。契約が通さない値でしか当たらない行は W123 です。
  • 契約がフィールドのあいだに置く条件も、規則と突き合わされます。 メッセージに付けた CEL の式、oneof、JSON Schema の組み合わせは、フィールドどうしを関係づけます。その関係を契約が守っていない constraint は E123、契約が通さない組み合わせでしか当たらない行は W124 です。

表の検査は何も変わりません。射影から出てくるのはただのスカラーの入力で、完全性も重なりも、from が無いときと同じに決まります。

取り出せるのは、一つの並びと、要素のフィールドへの単項のテストまでです。 結合や量化のネスト、セルの中のパスは書けません。セルの言語がこのツールの境界だからです。

契約と並べた例が例で見るに二つあります(JSON Schema と .proto)。細部は文法にあります。

表

table 基本送料(base_fee)
policy unique
| 届け先      | 重量    | -> 基本送料(base) : money[円, incl_tax] |
| 遠隔地      | <=2000g | 1200円                                  |
| 遠隔地      | >2000g  | 1800円                                  |
| not: 遠隔地 | <=2000g | 800円                                   |
| not: 遠隔地 | >2000g  | 1100円                                  |

-> の左が入力の列、右が出力の列です。列に書けるのは入力・導出・真偽や列挙の途中の値、そして前の表が出した値です。

最後のものが表を重ねる仕組みで、これが複雑なルールの書き方です。table 重さ判定 が出した 区分 を table 帯判定 の列に書き、その 帯 をさらに次の表の列に書く — 段数に上限はありません。一つの表が出力列を複数持つこともできます。

方式は二つだけです。

  • unique(既定)— 行の重なりはすべてエラー。順序に意味が無いので、並べ替えても意味が変わりません
  • first — 最初に当てはまった行が勝ちます。「例外を先に、一般則を後に」という業務の書き方をそのまま受けるためのもの

DMN の Any / Priority / Collect は採りませんでした。完全性は宣言できず、常に必須です。穴を許したい表は書けません。

セルに書けるもの

七種で全部です。

書き方 意味
- 任意の値。空欄は書き忘れと区別がつかないので構文エラーです
1200円 2000g true 2026-04-01 リテラル一致。数量と金額には単位が必須(裸の 2000 はエラー)
北海道, 沖縄県 いくつかの値のどれか。要素はリテラルかグループ名
not: 遠隔地 それ以外
<=2000g 比較。<= >= < > の四種
>=1000円 <20000円 範囲(比較を並べると「かつ」の意味)
none optional の「値が無い」

記号はすべて ASCII です。→ や ・ 、 と書いても読めますが、rulec fmt が -> と , に直します。名前とセルの値以外に IME は要りません。

.. を使った範囲の書き方は構文エラーです。 「2000g まで」がその値を含むのか含まないのか読めないからで、比較演算子ならどちらかに決まります。境界のつなぎ間違いは、重なりの検査がそれを起こす入力つきで捕まえます。

導出・定義・結果

表は分岐だけを持ちます。計算は表の外の三か所です。

書けるもの 表の列に置けるか
derive 入力の一次結合(+、-、定数倍) 置けます(数量のまま)
define 真偽(形は二つ)、または計算した中間値 真偽と列挙なら置けます
result + - * /、カッコ、min max、丸め五種を関数としても —

率は掛けられます(基本送料 × 負担率)。率は最後まで率のまま運ばれ、丸めは一度だけです。全部整数で、浮動小数点はどこにも出てきません。

導出は入力どうしの足し算・引き算(と定数倍)だけでできていて、数量のまま表の列に置けます。

derive 適用後金額(net) : money[円, incl_tax] = 商品合計 - 割引額  range >=0円 <=100万円

「クーポン適用後の金額が 3,980 円以上なら」のように、値引きしたあとの額で判定する規約は実際にあります。これを列に書けないと、いちばん間違えやすい引き算が呼び出し側のむき出しの一行になってしまいます。range の扱いは入力と同じです(上の「範囲と丸め」)。

定義は真偽や途中の値に名前を付けます。真偽の定義は表の列に置けます。

define 大口(bulk) : bool = 注文金額 >= 3万円
define Aが早いか同じ(a_earlier) : bool = A期限 <= B期限

条件に書けるのは二つの形だけです。その列だけを見る比較(ある値を定数と比べる)か、引き算で差を作れない型どうしの比較(日付と日付など)です。数値どうしをそのまま比べると、「差を derive にしてから定数と比べてください」と言われます — そのほうが検査が正確にできるからです。

結果が出力を組み立てます。

result 送料 = 基本送料 × 負担率

使えるのは足し算・引き算、定数倍、率との掛け算、min max allocate、そして丸め五種(up down half_up half_down half_even)だけです。ループも再帰もありません。

allocate は、総額を明細に定価の比で割り付けます。 定数でないもので割れる唯一の場所です。

constraint ここまでの定価 <= 定価合計

derive ここまでの配分(to_upto) : money[円] = allocate(値引き総額, ここまでの定価, 定価合計)  range >=0円 <=100万円

result 配分額 = ここまでの配分 - 直前までの配分

一行ぶんは「ここまでの配分」から「直前までの配分」を引いた差です。この形なら端数は最後の行に寄り、配った合計は総額にぴったり一致します。proofs/ にその定理があります。書くときの約束が四つあります。三つとも範囲を宣言した名前にすること、配る額と累計が負にならないこと、全体が正であること、そして上の constraint を書くこと。足りなければ E117 で止まります。

複雑なものは、表を重ねて書く

セルが自分の列しか見られないぶん、表は何段でも重ねられます。前の表が出した値を、そのまま後の表の列に書けます。

表は何段でも重ねられる。表 重さ判定 が 重量 から 区分 を出し、その 区分 が 表 帯判定 の列になり、出てきた 帯 が 表 送料表 の列になる。導出した 支払額 も列として入り、最後の表が 送料 と 倍率 を同時に出して、請求額 と 付与点 になる 表は何段でも重ねられる。表 重さ判定 が 重量 から 区分 を出し、その 区分 が 表 帯判定 の列になり、出てきた 帯 が 表 送料表 の列になる。導出した 支払額 も列として入り、最後の表が 送料 と 倍率 を同時に出して、請求額 と 付与点 になる

見どころは同じ名前が二度出てくるところです。区分 が一つ目の表から出て、二つ目の表の列として入る。そこから出た 帯 が三つ目の表の列になる。導出した 支払額 も同じように列として入ります。

段をまたいでも、どこで何が起きたかは見えたままです。検査が落ちたときに出るのは、段の数だけの行です。

当てはまった行: 表 重さ判定 行2 / 表 帯判定 行4 / 表 送料表 行4

重ねるときに効くものが四つあります。

前の表の出力が、後の表の列になる 段数に上限はありません。検査の予算を超えたときだけ E109 で止まります
一つの表が出力列を複数持てる 上の 送料表 は 送料 と 倍率 を同時に出します
derive が列になる 「値引き後の金額で判定する」が、一本の式ではなく一つの列になります
完全性の検査が段をまたぐ E102 の二つ目の形が「上流の表がその値を決して出さない」です

動く例が 例で見る の「表を三段重ねて、出力を二つ返す」にあります。

本則と特例を、二つの表に分ける

料金表には、本則と、それに優先する特例がつきものです。「契約金額が 10 万円を超える契約書は、令和 9 年 3 月 31 日までは軽減税率」のような決まりです。一つの表に 軽減期間 の列を足して書くこともできますが、原文が二つ(印紙税法の別表第一と、租税特別措置法の第 91 条)なら、表も二つに分けたほうが原文と見比べられます。抜粋なので、行は一部です。

table 本則(base)  @法 別表第一
policy unique
   | 金額の記載あり | 契約金額          | -> 印紙税額(tax) : money[円] |
r1 | false          | -                 | 200円                        |
r3 | true           | >=1万円 <=10万円  | 200円                        |
r4 | true           | >10万円 <=50万円  | 400円                        |
r5 | true           | >50万円 <=100万円 | 1000円                       |

table 軽減(reduced_rate)  @措置法 第91条
policy unique
overrides 本則
| 軽減期間 | 金額の記載あり | 契約金額          | -> 印紙税額 |
| true     | true           | >10万円 <=50万円  | 200円       |
| true     | true           | >50万円 <=100万円 | 500円       |

overrides 本則 は「この表の行は、表 本則 の行に優先する」という宣言です。行の頭の r1 はラベルで、overrides 本則:r4 のように行を指したいときに使います。

検査は、同じ出力を定める表をまとめて一つの集合として見ます。完全性は二つの表を合わせて判定し、穴があれば、どちらの表に行を足すかは人が決めます。行が重なるところは、overrides があれば「特例が勝つ」で通り、無ければ E105 で止まります。特例の行に丸ごと覆われて出番の無い本則の行は E102、優先すると書いたのに一行も交わらなければ W117 です。

生成コードは、後に書いた表から順に試して、最初に当たった行を採ります。当たった行の記録は、表の名前と、その表の中で書いた位置の行番号(ラベルがあればラベルも)で返ります。人が読む資料には「表 軽減 は 表 本則 に優先します。交わる 10 対のすべてで、軽減の行は本則の行に収まります(例外)」という一文が入ります。

ただし書は文のまま書く(clause)

ただし書のように、条件が列に並ばない一行の決まりは、表にせず clause で書きます。

clause 通常(regular) -> 送料  # 第3条第1項(本文)
  when always
  then 基本運賃

clause 無料(free) -> 送料  # 第3条第2項ただし書
  when 注文金額 >=3900円 and 会員 true
  then 0円
  overrides 通常

when は <列> <セル> を and でつないだ条件で、セルには表と同じ七種が書けます。条件の無い決まりは when always と書きます(書き忘れと区別するためで、空欄を許さないのと同じ理由です)。then は出力セルと同じで、リテラルか名前です。

clause で書いた決まりを、この文書では節と呼びます。節は一行の表として扱われ、検査も生成も記録も表と同じ仕組みで動きます。記録には {"table":"無料","row":1} と出ます。表と混ぜて overrides でつなげます。

どこから転記したかを書く(source と @)

規約や法令から転記した規則は、どこから転記したかを書けます。source で文書を宣言し、表・節・行・導出・定義の行末に @出典 と書いて引用します。

source 郵便 = file "ゆうパック基本運賃.pdf" sha256:9e4edb5b6a1c0f42
source 措置法 = law "332AC0000000026" asof 2026-04-01
  第91条 sha256:85faf53f6f6e8196
source osha = law ecfr "29 CFR 1910" asof 2026-01-01
  "§1910.157" sha256:c2a9ce966c7e2269

define 軽減期間(reduced) : bool = 作成日 <= 2027-03-31  @措置法 第91条

table 運賃表(fee_table)  @郵便
table distance          @osha "§1910.157"

文書は二種類で、引用の書き方と、コピーの扱いが少し違います。

文書 宣言 引用 コピー
隣に置いたファイル(規約の PDF、料金表、社内規程の Word) source 郵便 = file "<ファイル>" sha256:<ハッシュ> @郵便。転記したのが何番目の表かまで言うなら @郵便 表1 そのファイルがコピー。rulec source pin がハッシュを source の行に書く。表を引いたなら、その表も文書から取り出して隣に置く
法令 source 法 = law [<データベース>] "<ID>" asof <日付>。asof はいつの時点の条文か 箇所を必ず書く。書き方はそのデータベースの呼び方のまま rulec source fetch が条文を取り、規則の隣の sources/ に箇所ごとのコピーを保存する。rulec source pin がそのハッシュを source の下の行に書く

法令データベースは二つで、law の後の語がどちらかを言います。

語 データベース ID 箇所
省略、または egov e-Gov 法令検索(日本の政府の法令データベース) 342AC0000000023 第91条、第20条の2第3項、別表第一、附則第3条、改正法の附則は 附則(令和七年三月三一日法律第一三号)第3条
ecfr eCFR(米国の連邦規則集。その日に効いている条文が引ける) title と part で 29 CFR 1910 section ひとつ。§1910.157

語として読めない箇所は " で囲みます。引用でもピンの行でも同じです(@osha "§1910.157")。CFR の項((d)(2))はまだ引けません。eCFR が section の単位でしか渡さないので、コピーをこちらで切ることになるからです。

以後 rulec check は毎回、コピーがあること、コピーのハッシュが書いてあるとおりであることを確かめます。コピーが変わっていれば(ファイルを差し替えた、条文を取り直したら改正されていた)、その箇所を引用している表・節・行を名指しして止まります(E038)。読み直すのはそこだけで済みます。check 自体は通信しません。

rulec source outdated は、元の文書が動いたかどうかを問い合わせるコマンドです。法令なら asof より後の改正で条文の本文が変わるかをそのデータベースに訊き(e-Gov には施行日で、eCFR にはその section の改正日で。どちらも、体裁だけの直しは改正と数えません)、ファイルなら url "…" の先を見て、引いている表が変わったのか、この規則が転記していないところが変わっただけなのかを言います。コピーを取り直さない限り check は改正を知りようがないので、CI の定期実行に置く想定です。

文書の表を引くと、金額がコピーに縛られる

ファイルの出典で @郵便 表1 と書くと、rulec source fetch がその表を文書から取り出して隣に置きます。Excel(.xlsx)はシートが表、Word(.docx)は文書の中の表、Markdown と CSV はそのままです。PDF やスキャンは中身を読めないので、rulec source fetch --via <コマンド> で抽出器(docling など)を渡すか、@郵便 と丸ごと引きます。

取り出したコピーに縛られるのは、その表が書いた金額です。

$ rulec check rules/送料.rule --lang ja
エラー[E116]: 行4 の値が、引いた出典のコピーにありません
   |
24 | | not: 遠隔地 | >2000g  | 1000円      |
   |                           ^^^^^^ コピーに無い: 1000円
   |
 引いた出典のコピー: 規約 表1

警告[W120]: 表1 のコピーにある値を、どの行も使っていません
 どの行にも出てこない値: 1100円

一桁の打ち間違いは、この二つが同時に出て両側を名指しします。W120 のほうは行を一本転記し忘れたときにも出ます。閾値は転記するときに書き換わる(1,949,000円まで は <=1949000円 になる)ので、比べるのは金額だけです。

人が読む資料には、引用した条文や表がコピーから引いて載ります。

ほかの規則を準用する(apply)

「第 20 条の規定は、非常勤職員について準用する。この場合において、『勤続年数』とあるのは『在職期間』と読み替えるものとする」。法令のこの形は、第 20 条の規則を、入力を差し替えてもう一度使う、と言っています。apply はそれをそのまま書きます。

apply 退職手当(retirement) = "退職手当.rule" sha256:b58648ea2767ebbd  # 第31条
  勤続年数 = 在職期間
  退職事由 = 任期終了事由 with 任期満了 -> 定年, 辞職 -> 自己都合
  基本給 = 報酬月額
  except 減額
  手当 -> 非常勤手当

見出しに、元の規則のファイルと、そのファイルのハッシュを書きます。下の行が読み替えです。入力は一つ残らず、この規則の何にあてるかを書きます。あてられるのは、入力、導出、定義、前の表の出力、リテラルのどれかです。列挙どうしは with で値を対応づけます。同じ綴りの値は書かなくて構いません。except には、元の規則の定義のうち準用しないものを書きます(「第 20 条(第 2 項を除く。)」の形です)。出力はこの規則の値になり、-> で名前を付け替えられます。

rulec check は、まず元の規則をそれ自体として丸ごと検査します。それから、その表と節を 退職手当:支給表 のような名前でこの規則の中に展開し、一つの規則として検査します。だから完全性も重なりも、準用したあとの形で証明されます。加えて三つを確かめます。読み替えに漏れが無いこと(E041)と型が合うこと(E042)、そしてこの規則が渡す値が、元の規則の範囲に収まること(E043)。在職期間の範囲を 0 から書いてしまえば「在職期間 = 0 は、退職手当.rule の 勤続年数 の range >=1 <=40 の外です」と止まります。完全性はその範囲の上でしか証明されていないからで、範囲を狭めるか、はみ出す分をこの規則の節で定めるかは業務の判断です。

改正で元の規則が変わると、ハッシュが合わなくなって E040 で止まります。rulec diff でこの規則のどの入力が動くかを見て(過去の記録があれば何件いくらまで出ます)、それでよければ rulec source pin でハッシュを書き直します。展開した表のうち、この規則の範囲では当たらない行は、エラーにはせず、人が読む資料に「この準用では当たらない行」として挙がります。表の全行が当たらないときだけ、W118 で知らせます。

生成コードには元の規則が展開されて入り、記録には {"table":"退職手当:支給表","row":1,"label":"短期"} と出ます。準用は一段までで、並びを順に見ていく規則は準用できません(E044)。

起きない組み合わせを言う

入力どうしの関係を、呼び出し側との約束として宣言できます。

constraint 適用開始日 <= 適用終了日

計算はしません。言っているのは「どの組み合わせが起こりうるか」だけです。一行から三つのことが決まります。

  • 完全性の検査が、起こらない組み合わせに行を要求しなくなります。 起こりうる入力を覆っていれば、その表は完全です。
  • 診断が返す入力が、本当に送られうる一件になります。 検査が組み立てる入力は、どれも制約を満たしています。
  • 生成コードは、破った入力を入口で受け付けません。 証明が制約を前提にした以上、コードの側で守らせるほかありません。

形は constraint <入力> <比較> <入力> で、比較は <= < >= > の四つ。両側とも inputs の名前で、順序のある型(金額・数量・率・number・日付)です。何行書いても全部が同時に成り立ち、A = B と言いたいときは A <= B と A >= B の二行に分けます。制約を破る例は、ケースではなく誤りです(E019)。

件数の決まらない並びを受ける

ここまでの規則は、決まった数の値を受けて一度で判定するものでした。件数の決まらない並びが来る場合 — 運賃表の行、絞り込みで残った候補 — は、elements で一件ぶんのフィールドを宣言し、fold でそのたどり方を書きます。

elements 運賃行(fee_rows)
  行ゾーン(row_zone) : ゾーン区分
  閾値(threshold)    : money[円, incl_tax]  range >=0円 <=100万円
  行運賃(row_fee)    : money[円, incl_tax]  range >=0円 <=10万円

table 行判定(row_of)
policy unique
| 行ゾーン | 閾値     | -> 採用(verdict) : 採用区分 |
| 近畿圏   | <=1000円 | 確定                        |
| …

fold 採用 over 運賃行
  スキップ  -> next
  打ち切り  -> stop with 0円
  確定      -> take_unique 行運賃
  持ち越し  -> keep_max 行運賃 by 閾値
  empty     -> 0円
  exhausted -> held

判定するのはいつもの表です。一件の要素が一件の判定なので、完全性も重なりも単位もこれまでどおり証明されます。フィールドの書き方も inputs と同じで、範囲も単位もそのまま効きます。違うのは、呼び出し側がそのフィールドを要素の数だけ埋めて渡すところです。

fold が足すのは、判定ごとに次にどうするかの一行だけです。

行き先 意味
next この要素は飛ばして、次へ
stop そこで打ち切る。答えは exhausted の言うとおり
stop with <値> そこで打ち切って、この値を答えにする
take_unique <値> この要素の値を採る。二件目も採ろうとしたら実行時にエラー
take_first <値> 最初の一件を採り、あとは無視する
keep_max <値> by <鍵> 鍵がいちばん大きい要素の値を持ち越す
empty -> <値> 要素がゼロ件のときの答え。必須
exhausted -> <値> 最後まで見終えたときの答え。必須。held は持ち越している値

表が出しうる判定には、全部行き先が要ります(E024)。表の完全性と同じ検査が、畳み込みの側にも当たります。どの要素もその判定にならないのに行き先を書いたときは、W115 が出ます。

これを足しても、検査が必ず終わることは変わりません。表が完全で重なりも無いので、要素は必ずちょうど一つの判定に落ち、並びは判定の連なりに置き換わります。たどり方は有限の状態しか持たないので、実行時に要素が何件来るかとは無関係です。

例を書くときは、並びのほうに名前を付けます。例のセルに書けるのは値一つだからです。

sequence 近い一件(near)
| 行ゾーン | 閾値   | 行運賃 |
| 近畿圏   | 500円  | 800円  |
| 近畿圏   | 2000円 | 1500円 |

examples
| 運賃行   | -> 運賃 |
| 近い一件 | 800円   |

行がゼロ本の sequence が、要素ゼロ件の例です。生成されるのは引数がもう一つ増えた同じ関数で、SQL にだけは生成しません — 一つの問い合わせには、行から行へ値を持ち越して途中で打ち切る場所がないからです。

動く例が 例で見る の「並びを順にたどって、一つの答えに畳む」にあります。

数えるだけなら count

fold は並びを一つの答えにします。count は並びを一つの数にして、そこから先はふつうの規則に戻します。

count 一致数(hits) over 候補 where 照合結果 = 一致  range >=0 <=50

where が指すのは要素ごとに決まる列です — 要素のフィールドか、要素ごとの表が出した列。値が有限の集合(真偽か列挙)である必要があり、列挙なら = <値> でどれを数えるかを書きます。真偽の列なら where 冷蔵品 だけで足ります。

数えたあとは number なので、表の列に置けます。

| 一致数 | 自動確定可 | -> 手続き(action) : 次の手 |
| 0      | -          | 新規登録                   |
| 1      | true       | 自動確定                   |
| 1      | false      | 目視確認                   |
| >=2    | -          | 目視確認                   |

これが count を fold と別に用意した理由です。数を判定に変えるのがふつうの表なら、その境界も検査に掛かります — 0 と 1 と >=2 のあいだに穴や重なりがあれば、いつもどおり止まります。

range は必須で、二つの意味を持ちます。 数えた結果を列に使ったときに完全性の検査が見る全体集合であり、並びの長さの上限でもあります。上限より長い並びは、生成コードが入口で受け付けません。範囲の外の数を受け付けないのと同じで、証明が置いた前提の外へは出さないためです。

数えるのは count、足すのは sum です(sum 合計(total) over 明細 of 金額)。平均は書けません。件数で割るのは変数で割ることだからです。呼び出す手前で出して、値として渡してください。fold と count・sum を一つの規則に両方書くこともできません(E031)— どちらも同じ並びの終わり方で、fold は途中で打ち切れるからです。

動く例が 例で見る の「並びを数えて、その数で判定する」にあります。

呼び出しをまたいで状態を持ち越す(machine)

規則の中には、続いていく何かの一歩ぶんのものがあります。注文は入金され、出荷され、配達されるか、取り消されます。状態は呼び出す側が持ちます(データベースの注文の行など)。呼び出すたびに今の状態を渡し、次の状態を受け取ります。表はいつもどおりに書き、状態を列に、次の状態を出力に置きます。出力のセルに 状態 と書けば、状態をそのまま返します。

table 遷移(step)
policy unique
| 状態   | 出来事   | -> 次の状態 | 返金額 | 受理  |
| 受付   | 入金     | 入金済      | 0円    | true  |
| 受付   | 取消依頼 | 取消        | 0円    | true  |
| 入金済 | 取消依頼 | 取消        | 支払額 | true  |
| …
| 取消   | -        | 状態        | 0円    | false |

machine 注文(order) over 遷移
  carry   状態 -> 次の状態
  held    支払額
  initial 受付
  final   配達済, 取消
  never   出荷済 after 取消
  once    返金額 >0円

意味として新しいのは carry の一行だけです。呼び出しが返した出力を、次の呼び出しではその入力として渡す、という宣言です。生成される関数は変わりません — 何も覚えず、状態は呼び出す側が持ちます。この節が足すのは呼び出しのあらゆる並びについての主張で、check がそれを証明します。

行 主張 崩れたとき
final この状態から出ていく呼び出しはない E124
(いつも) たどり着けるどの状態からも、final のどれかにまだ行ける E125
never 出荷済 after 取消 一度 取消 になった案件が 出荷済 に着く呼び出しの並びはない E126
once 返金額 >0円 一つの案件で、返金を答える呼び出しは多くても一回 E127

held 支払額 は、一つの案件が呼び出しのたびに同じ支払額を渡すという宣言です。主張はそれを変えない呼び出しの並びについてのものになり、途中で支払額を変える並びが反例として出てくることはありません。

主張が崩れると、崩す最短の呼び出しの並びが返ってきます。一回ずつが、規則の受け付ける入力です。残りは表の検査の仕事です。状態とイベントの組み合わせに行が無ければ、これまでどおり E101 です。出荷のあとに取消依頼が来たらどうするかは、コードを走らせる前に聞かれます。

scenario は、何回かの呼び出しにわたって走る例です。持ち越す入力の列はありません。最初の呼び出しは initial から、そのあとの呼び出しは一つ前が答えた状態から始まります。

scenario 取消のあとの入金(late_pay)
| 出来事   | 支払額 | -> 次の状態 | 返金額 | 受理  |
| 入金     | 3000円 | 入金済      | 0円    | true  |
| 取消依頼 | 3000円 | 取消        | 3000円 | true  |
| 入金     | 3000円 | 取消        | 0円    | false |

生成コードには、関数のほかに、始まりの状態と、終わりの状態かどうかの判定が付きます。ベクタには呼び出しの並びが加わり、rulec test は各言語に、自分が返した状態をそのまま次の呼び出しへ渡させて照合します。二つの版の rulec diff は、違う答えを返す最短の呼び出しの並びと、処理中の案件が改定のあとで終わりに行けなくなる状態を言います。

決定可能なままなのは、状態が有限の列挙で、ほかに持ち越すものが無いからです。それまでの返金の合計や残高は、これまでどおり呼び出す側が持って渡します。

動く例が 例で見る の「続いていく注文の、一回のイベント」にあります。

例

examples
| 届け先 | 重量  | 注文金額 | 会員     | -> 送料 |
| 沖縄県 | 2500g | 40000円  | 一般     | 0円     |
| 東京都 | 1999g | 12000円  | プラチナ | 400円   |

examples は実際に走る仕様です。rulec check が参照評価器(rulec の中にある「正解」の実装)で全行を走らせ、外れたらどの表のどの行に当てはまったかつきで報告します。

-> は入力と出力の境目を一度だけ示します。出力が二つ以上あるときは、二列目以降に -> を書いても書かなくても構いません(rulec fmt が決まった形に揃えます)。

| 商品合計 | 種別   | 同商品適用済 | -> 可否 | 素割引 |
| 10000円  | 率引き | false        | true    | 1000円 |

期待値は出力を全部書きます。 列を落とすとエラーです。理由は、参照評価器と生成した各言語が同じ答えを返すかどうかの照合が、全員が同じ間違いをしていたら緑のままだからです。実際にこれが起きました — 出力が複数あるときの丸めが評価器と生成コードの全部で揃って抜けていて、照合は最後まで緑でした。それを破れるのは、人が書いた期待値だけです。

書けないもの

  • ネストしたオブジェクト(注文.配送先.都道府県)— 呼び出す手前でほどいて、値そのものを入力として渡します
  • 好きなところで回るくり返しと、再帰 — 並びを一度だけたどる fold はありますが(上の節)、それ以外のくり返し、たとえばクーポンの重ね掛けのように順番が効くものは、呼び出し側に置きます
  • 要素をまたぐ平均 — 件数で割ることになり、変数で割る式は書けません。呼び出す手前で出して、値として渡します(件数は count、合計は sum で出せます)
  • 呼び出しから呼び出しへ持ち越す数(残高、それまでの回数)— machine が持ち越すのは列挙の状態だけです。累計は呼び出す側が持って渡します
  • 日付の足し算・引き算 — 比較と範囲だけです

これを許すと、完全性と重なりの検査が必ず終わると言えなくなります。書けないことは、検査が必ず終わることの代償です。


文字の読み方から予約語まで含んだ完全な定義は 文法 にあります(英語)。

何を証明するか 文法