この資料は英語です
リファレンス(文法・形式・生成物・エージェント向けの手順)は、第一の読み手がエージェントなので英語で書いています。日本語で読めるのは、ホーム・インストール・表を書く・何を証明するか・生成して呼ぶ・突き合わせと再生・例で見る・自分のルールが入るか、そして診断コードの台帳と互換性です。
The .rule language
The complete definition of the syntax. It is meant to be read by something that has to write
a .rule correctly the first time, so it states what is allowed rather than motivating it.
The reasoning behind each restriction is in DESIGN.md; the diagnostics you get for breaking
one are in codes.md, and rulec explain <CODE> prints any of them.
Two things to know before anything else.
- Keywords are English and there is exactly one spelling of each. No synonyms and no abbreviations. Names and cell values are the business's own words: the rules in this repository are written with English ones, and each one transcribed from a Japanese document has its Japanese original beside it.
- The file is read top to bottom and there is no forward reference. A name is declared above every line that uses it.
1. File structure
One .rule is one rule, and becomes one generated function. Sections appear in this order;
every one except rule is optional, but an order that differs from this one is an error.
rule <name>(<alias>) v<version>
description "<one line>"
import std/<name>
import proto "<file>" <Enum> -> <enum of this rule>
import jsonschema "<file>" "<pointer>" -> <enum of this rule>
import enum <enum> from "<file>" [-> <enum of this rule>]
source <name> = law [<database>] "<law id>" asof <date>
<fragment> sha256:<digest>
source <name> = file "<file>" [url "<url>"] sha256:<digest>
shape <name>(<alias>) = jsonschema "<file>" "<pointer>"
shape <name>(<alias>) = proto "<file>" <Message>
enum …
group …
inputs
…
outputs
…
derive … ┐
define … │ these three interleave freely, in dependency order
table … ┘
policy …
overrides …
clause … (a one-row definition written as prose; interleaves with the three above)
apply … (another rule, applied with its inputs bound; interleaves too)
<callee input> = <value>
except <definition>, …
<callee output> -> <name>
result …
prerequisites <name>(<alias>) over <enum> (which values of the enum need which, §6.5)
start <value>, …
<value> requires <value>, <value> or <value>, …
<value> requires nothing "<reason>"
ordered by <table>
machine <name>(<alias>) over <table> (the rule is one step of a state machine, §6.4)
carry <input> -> <output>
initial <state>
final <state>, …
never <state>, … after <state>, …
once <output> <cell>
scenario <name>(<alias>) (a sequence of calls from the initial state, §6.4)
| … |
examples
…
ruleis the first line of the file.v1is the version; it is free text and appears in the generated header.descriptionis one line, in quotes.inputsandoutputsare followed by indented declarations, one per line.derive,defineandtableare a pipeline: each may use anything declared above it.policybelongs to thetableimmediately above it, and so doesoverrides: the tables or labelled rows, declared above, that every row of this table takes precedence over (§7).applyis an item likederive,defineandtable: another rule file, applied with every one of its inputs bound to a value of this rule (§7, "Applying another rule"). The lines under it are indented by two spaces.machinenames the table that decides the state it carries;scenarioblocks follow it.examplescomes last. There may be more than one section of them (§9).
A second kind of file starts with enums <name>(<alias>) v<version> instead of rule. It holds
enums that several rules share, and nothing that decides: description, import, enum,
group and prerequisites lines only (§3.1, "Enums several rules share"; §6.5).
A blank line separates sections. # starts a comment that runs to the end of the line;
comments may appear anywhere, including at the end of a table row and on a line of their own
in the middle of a block — a line that holds nothing but a comment does not end the
section, where a blank one does. A comment at the end of a declaration, of a table line or
of a row is shown on the page rulec doc renders, which is where the source a table or a row
was transcribed from belongs.
2. Lexical structure
Encoding is UTF-8 without a BOM. Identifiers are normalised to NFC. A cell never contains a line break.
Identifiers start with a letter or _ and run to the next delimiter. Kanji, kana and
Latin letters are all identifier characters; - never is (it is always an operator or the
don't-care cell). Anything else at the start of a name is E002.
ASCII aliases are written in parentheses after the name: 届け先(dest). They become the
identifiers the generated code uses.
An alias is required wherever a non-ASCII name reaches the public surface — the rule
name, an input, an output, a sequence and a field of its elements, which the caller passes and
fills in as it does an input — because a kanji has no uppercase and so cannot begin an exported
Go identifier, and the name of a field of the .proto is ASCII alone. A missing one is E011. A name that is already ASCII needs no alias: it is
its own identifier, so a rule written entirely in ASCII carries no parentheses at all.
rule bulk_fee v1 # no alias: the name is already an identifier
inputs
weight : mass[g] range >=1g <=40kg
Everywhere else an alias is optional, and writing one changes what the generated code
calls the value: derive 残余(margin) becomes margin, group 遠隔地(remote) becomes
_remote / isRemote, and a table output column -> サイズ(size) becomes size. Leave the
alias out and the declared name is used as it stands — every target language accepts a
Japanese identifier for something that does not have to be exported.
The alias on a table names no function, since a table is inlined into the one generated
function rather than becoming a function of its own; it names the column of the SQL query that
holds the number of the row that matched (<alias>_row), and the harness of the Rust proof that
counts the table's rows.
An alias that is a word one of the targets has taken is W121. A builtin is the quiet case:
the rule's alias names the generated function and an enum's alias names a type, so sum or
list there hides Python's. A keyword is the hard one — type as an input's alias generates
Rust that does not compile — and an alias stays as written, since it is the identifier its
author chose. A name with no alias is its own identifier, and there the generated code does
the renaming: an input, an output, a derive, a define, a table's output column, a sequence
or one of its fields called type, class, def or from — a keyword in some of the
languages that write a name as it is — is written with _ after it in every language
(class_), rulec api names it so, and the name a value travels under — the keys of the input
JSON, the records — stays the one declared. Where class_ is already the identifier of another
name of the rule, a number goes after it (class_2). W121 says which languages took the word,
and an alias chooses the identifier instead. A parameter or a local hides nothing outside its own
body, so the warning is raised for those only when the word is a keyword. It is a warning
because a target you do not generate cannot hurt you.
An identifier rulec makes — that _, and those of everything an apply or a contribution brings
in (below) — is never one the rule has already, in any language's spelling of it: as it is, in
camelCase or PascalCase, with case set aside (SQLite reads "Credit" and "credit" as one
column), or cut to the 63 bytes PostgreSQL keeps of a name. Where it would be, it takes the first
of _2, _3, … that is not. The names the rule declares keep theirs, and are held to those
spellings below.
One name, one declaration. A name a file declares names one thing, and check stops at the
second declaration of a name, with the first one marked (E034): an input declared again with
another range, an enum with a value written twice, two tables, two groups or two shapes of one
name. The values — the inputs, the sequences and the fields of their elements, the outputs, and
what derive, define, count, sum, max, min, open and the output columns of tables and
clauses decide — are one list of names. An output shares its name with what decides it, and
several tables and clauses may decide one name (§7); any other two values of one name stop the
check, an output named like an input among them (to return an input as it is, an output of its own
name is decided by define <output> : <type> = <input>). A group named like a value of an enum
stops it too, as does an input a contribution adds under a name the rule declares as another value
(§6.7). A heading names each of its columns once (E034 in a table or the examples, E055 in a
scenario, E026 in a named sequence). A fold's answer is held the same way by E021, and the name an
apply gives a callee output by E041.
The identifiers are held the same way, within each list whose names the generated code writes:
the values, the tables and clauses, the enums, the values of one enum, the groups, the shapes and
the prerequisites blocks. Two names of one list with one identifier — the alias of one is the
alias or the name of the other — stop the check, and so does one name given two aliases (an output
and the definition that decides it: writing the alias on the output's line is enough). An alias
that is the name of another value which has an alias of its own passes: weight(size) beside
size(extent) is the value size to the rule and extent to the generated code, and nothing reads
it two ways. In an enums file, an enum named like another enum's alias is E034, since import
enum finds an enum by its name or by its alias. Two names whose identifiers differ stop the check
too where every language that writes them spells them as one: the types of two enums and two values
of one enum, which are written in PascalCase (tier_level and tierLevel are both TierLevel,
basic and Basic both Basic), the functions of two prerequisites blocks, named in snake_case
(core_path and corePath), the element types of a rule's two sequences (OrderLinesElement), and
a shape and an input or a sequence the caller passes as it is, which are parameters of one function,
the one that reads the caller's objects.
Two names one language spells alike. Each language writes an identifier its own way, and
check holds the identifiers the rule declares to all of them. Two names whose identifiers differ
as written but are one in some language's spelling are W121, which names the languages, the
identifier each writes and why. The identifiers stay as the author wrote them, the generated code
is written for every language, and an alias on one of the two sets them apart.
base_fee and baseFee, for one, are both baseFee in Swift and Java.
Two inputs whose fields are one in Go's Input struct (fee and fee_, both Fee), or whose
fields in the .proto have one JSON name, meet there.
afee and aFee are one column of the query to SQLite, and two long names that agree for the 63
bytes PostgreSQL keeps of a name are one name to PostgreSQL. Two values of an enum that upper case
makes one (gold_plus and goldplus, both GOLDPLUS) meet wherever the values are written in
upper case.
A name a language cannot take where its code puts it is W121 too: one that starts with a capital,
which Ruby reads as a constant; an output of a rule with two or more, or a field of an element,
that starts with _, which Python's NamedTuple does not take; an output or a table's row column
longer than 63 bytes, whose column PostgreSQL returns under a cut name; a name of _ alone, which
four of the languages take for no variable and no field; a name rulec writes with _ after it
that starts with a capital (True_), a constant there as well; a value whose
alias is true or false in any case (True), which SQLite gives no column; and an enum on the
wire whose name is not in ASCII, which the .proto cannot write. An alias makes the name one the
language takes.
A name the generated code uses itself. The code each language writes declares names of its
own beside the rule's: the types of the rows that matched (Fired), of the answer of a rule of two
outputs or more (Output) and of one element, the errors, the brands of the units, the helpers
that round and that write a record, the locals it keeps, and the names the language has built in
that it reads; and the .proto declares its messages, its service, the field trace of
DecideResponse that carries the rows that matched, and the zero value <TYPE>_UNSPECIFIED of
every enum. A name of the rule's that a language writes as one of these is W121, which names the
languages and what the name meets there: an output called trace and a value of an enum on the
wire called unspecified (Connect), an enum called output in a rule of two outputs (ten
languages), a definition called round_down (the helper one language rounds an output with), and
a rule called main (a package name the go command keeps, and the file a program starts in). The
generated code keeps its own names for every rule, and an alias on the rule's name sets the two
apart.
Numbers are digits, optionally with _ as a separator, optionally preceded by -,
optionally followed by a multiplier and then a unit:
| part | values |
|---|---|
| multiplier | 万 (10⁴), 億 (10⁸), 兆 (10¹²) |
| unit | mass mg g kg t oz lb · length mm cm m km in ft yd mi · rate % · money 円 銭, or an ISO 4217 code, whose hundredth is that code plus c (USD and USDc). Two currencies never convert: there is no exchange rate here, so mixing them is E103 |
1000万円 is 10,000,000 yen, which 10_000_000JPY writes as well. A number in a cell must carry its unit: a bare 2000 where
a quantity is expected is an error. 2kg and 2000g are the same value; the stored integer
is always in the unit the type declares. In an expression a number is read in the unit it is
written in, and it has to be a value of it: a bare number is a number, which is whole, so a
fraction is written as the rate it is — amount * 40%, not amount * 0.4 — and 0.5JPY is
no amount at all. Either is E103, and for a bare decimal its fix.text is the % form.
A thousands separator is not written (E049): , separates the members of a set, so the
1,000JPY a document prints would be two values. Write 1000JPY, or 1_000JPY to group the
digits; the diagnostic's fix.text is the literal rewritten.
Dates are written YYYY-MM-DD. They are held as day ordinals, which is what makes an
interval over dates exact.
Strings are double-quoted and must close on the same line (E001). There is no escape: a
string holds every character up to the next ", a backslash included, so it cannot hold a ".
Symbols. The canonical spellings are ASCII:
| written | means |
|---|---|
-> |
the boundary between input and output columns |
, |
the separator inside a set, and between type attributes |
- |
don't care (a cell of its own) |
<= >= < > |
comparison |
+ - * / |
arithmetic in an expression |
\| |
a table cell boundary |
: ( ) [ ] ? |
declarations |
→, ・, 、, ,, ≦, ≧, <, >, +, ×, ÷ and − are read as the symbols
above. rulec fmt writes the first nine in ASCII where the lexer reads them as a symbol of their
own; ×, ÷ and − stay as written. A fullwidth digit starts no word, and rulec fmt writes it
in ASCII where a word would start, so that a number typed with an IME becomes one. Inside a name,
a fullwidth digit and + are characters of the name (第1案, 松+竹), and stay. .. is never
read: see §7.
3. Types
Fourteen, and no others.
| type | written | notes |
|---|---|---|
| boolean | bool |
|
| enum | the enum's name | a closed finite set, declared with enum or brought in with import |
| mass | mass[g], mass[lb], … |
the unit is part of the type. mg g kg t oz lb |
| length | length[cm], length[in], … |
mm cm m km in ft yd mi |
| area | area[m2], area[坪], … |
mm2 cm2 m2 a ha km2 坪 in2 ft2 yd2 mi2 ac. A dimension of its own: width * depth is E103, because there is no dimensional analysis here (§2.1) |
| volume | volume[m3], volume[L], … |
mm3 cm3 m3 mL L kL. cm3 and mL are the same size, and so are m3 and kL. There is no gallon — the US one is 3.785411784 L and the imperial one 4.54609 L |
| duration | duration[h], duration[min], … |
ms s min h d w. A minute is min, because m is the metre. This is a span of time; a calendar day is date |
| temperature | temperature[℃], temperature[℉] |
℃ ℉. Ordered, not arithmetic: comparison and range only (E048). 41℉ is exactly 5℃, and a literal converts between them; a difference of two temperatures is not written at all |
| sound | sound[dB] |
dB, a sound pressure level. Ordered, not arithmetic (E048): a decibel is a logarithm, so adding two of them is not two sounds' worth |
| money | money[JPY, incl_tax], money[USD, excl_tax] |
currency and tax flag are both part of the type. Any ISO 4217 code, or 円; the hundredth of a currency is its code plus c, so money[USD] counts dollars and money[USDc] counts cents. Two currencies never convert — there is no exchange rate here, and mixing them is E103 |
| rate | rate[step 1%], rate[step 0.1%], rate |
the stored integer counts steps. An input declares its step (E103 without one): what the caller passes is that integer, and its meaning cannot change when a row is added. An output travels at the step it declares, or with none declared at its rounding grid, which every answer sits on — never at whatever its rows happen to write, so no row can change what the integer counts. A rounding grid that is not a whole number of the declared step is E114. A computed rate that is not an output may leave the step out; it is held at whatever its literals need |
| number | number |
a whole number with no unit — a count of things, a number of days, a score |
| date | date |
comparison and range only. There is no date arithmetic (E048) |
| string | string |
cannot be a table column (E110). Use it for an output, or for an input that only passes through. A value that decides a branch belongs in an enum |
| optional | member_kind? |
any of the above, plus the absent value, which the cell none tests. Its range and its step are those of the type it wraps and hold the value when there is one; it never appears in an expression or a constraint (an expression that works with one is E103; a define that only names one hands it on). A comparison, a literal and a set of values match only a value that is there, while - and not: match the absent value too. Completeness asks for a row for the absent value and for the whole declared range beside it (E101), and the generated code guards a value that is there as it guards the type it wraps. An output may be optional too: a row answers none for no value, or a value of the type it wraps, read in that type (439USDc under money[USDc]?), and an optional number declares its rounding like any other (E104) |
Every quantity, money, rate, number and date is an integer internally. No floating point appears anywhere in the tool or in the generated code.
Money of different currencies or different tax flags cannot be added or compared, and neither can values of different units (E103). A conversion is written as a table, never as a formula.
There are no compound dimensions. weight * length and width * depth are both E103, as amount * amount
always was: this tool does no dimensional analysis, and a dimension invented to hold a product
would be one nobody declared. Where the product itself is what a rule decides on — a floor
area, a volume of water — take it as an input, or look it up in a table. For the same reason a
divisor carrying a unit has to be written in the left side's unit: a divisor is read at the
unit it is written in and never converted, so weight / 2kg, with weight a mass[g], is E103 rather than a
division by 2000.
A value is read through its unit, wherever it is written. A range bound, a table cell, an
expected value in examples, a rounding grid, a step inside a type's brackets, an answer a
fold gives, a member of a group: each is a value of the thing it sits in, and one that is
not is E103 (E012 for a name). round up(10USDc) on a money[USD] output is refused, and
round up(1000USDc) is accepted as the ten dollars it is worth — the unit is read, not merely
recognised.
Three types are ordered but not arithmetic: date, temperature and sound. They are
written in cells, compared, and given a range, and that is all — + - × ÷ over any of them
is E048. A ℃ is a scale whose zero is displaced, so temp * 2 means nothing; a decibel is a
logarithm, so adding two of them is not two sounds' worth; a date is a calendar day, and there
is no type to hold the result of subtracting one. Where a difference is itself what the rule
decides on, compute it on the calling side and pass it in — as a duration, or a number.
Declaring an enum
A value may be marked default:
which declares "this value needs no row of its own; being caught by a - row is correct" and
silences W111 for it.
An ordered enum separates its values with < instead of |, lowest first:
A cell of a column of it may then compare against a value: >=gold is gold and platinum,
<gold is basic and silver, and >=silver <platinum is the two between (§7). An expression
may compare two values of it (campaign > member) and take the later or the earlier of two
(max(member, campaign), min(…)); a value of the enum itself is written in a cell, not in an
expression (E012). An input of the enum may declare a range (range >=basic <=gold, §4): a
value outside it is refused at the door as a number outside its range is, and the tables are
asked to cover only the values inside. The two separators do not mix in one declaration, and
nothing compares the values of an enum declared with |, whose order means nothing (E069).
Completeness and overlap are checked over the values, as for any enum.
A display name is a string after a value:
enum zone = local "Same city" < regional "Same region" < national "Rest of the country" < overseas "Abroad"
It is the name the value is shown by: on the page rulec doc renders, in the article for
customers, and through one function or method in each language's generated code
(zone_label(Zone.NATIONAL) is "Rest of the country" in Python; generated-code.md lists
each language's). A value without one is shown by its declared name. A display name is text:
anything but a line break, not empty, and no two values of one enum share one (E069). It never
travels: the wire and the records carry the value's name.
open written after the last value — enum channel = web | store default | phone default open
— marks an enum a contribution file may add values to (extend, §6.7). Within the rule's own
check the enum is closed, as every enum is; merged with a contribution, every table that reads it
is asked for a row of the value added, or for default on the line that adds it (E101, E033).
Two enums may have values of the same name — Stripe's capture_method and
confirmation_method both have automatic and manual. A value is read in the enum of the
place it is written: the column of a cell, the output of an output cell, the carried enum in
the lines under machine. A value of one enum written where another is expected is E103,
in an output cell as in any other. A value may be spelled like one of the reserved words
(§12), except the eleven its own position would read as something else.
Declaring a group
A group is a named subset of an enum and may be used in a cell wherever a value may. Groups are always expanded before checking, so a hole in a table written with groups is still found. Its members are values of one enum (E103 when they mix two); when two enums both have every member, the group belongs to the enum of the first column it is written in, and a column of the other enum is E103.
3.1 import
Four lines start with import, and each brings in the values of an enum — nothing else
crosses a file boundary. A rule is still one file: what is imported is a set of names, not
rows, not amounts, not another rule.
| line | what it brings | who owns the set |
|---|---|---|
import std/<name> |
a built-in enum | rulec, frozen |
import proto "<file>" <Enum> -> <enum of this rule> |
the value set of an enum in a .proto |
that .proto, outside this rule |
import jsonschema "<file>" "<pointer>" -> <enum of this rule> |
the value set of an enum in a JSON Schema, OpenAPI included | that file, outside this rule |
import enum <enum> from "<file>" [-> <name>] |
an enum of an enums file, whole: its values, their order, their display names and default marks, and its groups |
that enums file, which several rules read |
rulec import csvandrulec import xlsxare a different thing that shares the word: a command that writes a first draft of a.rulefrom a spreadsheet, once. It leaves no line in the file and nothing is read again afterwards. These two lines are read on everyrulec check. A header or a value the lexer does not read as a name (Weight (kg),re-quote,<=2000g) is named for now by its column's alias (c2) or byv1,v2…, and so is a second column headed alike. A name the code generated for some language cannot take as written, or would make one identifier with another (Zone, which Ruby reads as a constant;Northbesidenorth), is given an alias: the importer checks the draft asrulec checkdoes, and spells such a name otherwise until the check says nothing of it (Zone(zone),North(v2)). Each word is kept in the# guessnote on the line that declares it, andcheckreads the draft as it is written.
Built-in enums
import std/<country>/<kind> brings in the first-level divisions of a country: ISO 3166-1's two
letters in lower case, and the kind of division. The enum is named <country>_<kind, singular>.
| import | enum | values | generated type |
|---|---|---|---|
std/us/states |
us_state |
56 (the 50 states, DC, 5 territories) | UsState |
std/gb/nations |
gb_nation |
4 | GbNation |
std/cn/provinces |
cn_province |
33 (Taiwan is std/tw/divisions) |
CnProvince |
std/tw/divisions |
tw_division |
22 | TwDivision |
std/kr/provinces |
kr_province |
17 | KrProvince |
std/in/states |
in_state |
36 (28 states, 8 union territories) | InState |
std/fr/regions |
fr_region |
18 | FrRegion |
std/es/communities |
es_community |
19 (17 communities, Ceuta, Melilla) | EsCommunity |
std/it/regions |
it_region |
20 | ItRegion |
std/de/states |
de_state |
16 | DeState |
std/au/states |
au_state |
8 (6 states, 2 territories) | AuState |
std/br/states |
br_state |
27 (26 states, the Federal District) | BrState |
std/jp/prefectures |
jp_prefecture |
47 | Prefecture |
A value is the division's English name in ASCII, with _ for spaces and hyphens (California,
New_York, Bavaria, Ile_de_France, Sao_Paulo, Tokyo); that is the spelling every
output, the wire and the generated code use. Inside a rule a division may also be written in
the country's own language (Bayern, 北京, 서울특별시, Île_de_France, São_Paulo) or by
its ISO 3166-2 code without the country (CA, BY, NSW; a code that starts with a digit,
such as Japan's, Korea's and Italy's, cannot be a name). Every spelling of a division is the
same value: completeness, overlap, the vectors and the certificate see one value.
import std/都道府県 brings in the same 47 prefectures as std/jp/prefectures, as the enum
都道府県 with the values spelled in Japanese (東京都); both accept both spellings, and the
generated code is the same. One rule imports one country once (E013). A word that is no value
gets the closest spelling, or the namespace of the country that has it, in E012's note; a code
two imported countries share (WA is Washington and Western Australia) is E012 too, and is
written by its name instead. The names come from Unicode CLDR (see THIRD_PARTY_NOTICES).
Anything else after import std/ is E013.
An enum a .proto owns
When the values come from a service contract, the set is not the rule's to decide. The line
above says where it comes from, and rulec check reads that file on every run and holds the
two together. The path is followed from the directory of the .rule.
The two sides carry different things, and neither can be derived from the other. The .proto
owns which values exist; the .rule owns what they are called here and how each one is
treated (a value named in Japanese takes its name from the rule; the proto has only the ASCII one).
So what is checked is that they agree:
- the value names are matched by the ASCII alias, with the enum's own name taken off the front
(
MEMBER_TIER_GOLDisgold), which is the prefix conventionbuf lintenforces. The prefix is the name split into words the way buf splits it, soHTTPMethodasks forHTTP_METHOD_andTier2forTIER2_. It is taken off only when what is left begins with a letter:SIZE_60issize_60, since an alias cannot begin with a digit; - the zero value is proto3's "not set" when it is named
…_UNSPECIFIED, so it is not a value the table answers for — the generated code refuses it at the entry like any other non-member. A zero value named anything else is a value like any other; - a value on one side only is E032, in either direction;
- and once the sets agree, a value that no row names and no
defaultmarks is E033.
E033 is the reason this exists. Adding a value to an enum is a compatible change on the wire,
so the tools that guard the contract let it through; a table with a - row then passes the
completeness check, and the new tier quietly takes the default amount. For a value you wrote
yourself that state is a warning (W111). For a value that arrived through the contract it is an
error, because nobody has read it yet. Marking it default is how you say you did.
An enum a JSON Schema owns
import jsonschema "api/openapi.json" "#/components/schemas/MemberTier" -> tier
enum tier = basic | gold | platinum default
The same binding, for the other place a value set is declared. Everything about E032 and E033 is the same; two things differ.
How the enum is named. One document holds hundreds of enums, so one is named by a JSON
Pointer rather than by a name. The pointer may land on the schema (its enum is then read) or
on the array itself, and a leading # and a leading / are both optional. An enum written
inline in a property — the usual shape in OpenAPI — is reached the same way
(#/components/schemas/Order/properties/status). A pointer that does not resolve comes back
with the keys that were there.
How the values map. They are the aliases exactly: nothing is taken off the front and
no case is folded. A .proto earns its transformation from a convention buf lint enforces;
a schema has no such convention, and inventing one would mean a rule and a schema that look
like they agree while sending different strings. An enum of numbers or nulls is refused by
name: the values of a rule's enum are names.
YAML is not read. It is the usual spelling of an OpenAPI document, and the reason for the refusal is not that a reader would be hard to start: a reader for the subset one file happens to use is a reader that goes wrong quietly on the next one, and a value set that is quietly wrong is the one thing this must never be. Point at a JSON form of the document, which most toolchains can write.
Enums several rules share
enums catalog v1
description "The product categories and the delivery zones that the shipping and the returns rules share"
enum category = food "Food & drink" | apparel "Clothing & shoes" | electronics "Electronics" | furniture "Furniture"
enum zone = local "Same city" < regional "Same region" < national "Rest of the country" < overseas "Abroad"
group fragile = electronics, furniture
rule catalog_shipping v1
import enum category from "shared/catalog.rule"
import enum zone from "shared/catalog.rule"
A file that starts with enums holds enums and decides nothing: description, import, enum
and group lines, and any other section is E066. It generates no function. A rule reads one of
its enums with import enum <enum> from "<file>", the path followed from the rule's directory,
and -> <name> gives the enum another name in the rule. What comes is the whole enum — its
values, their order, their display names, their default marks, and every group of it in the
file — and none of it is written again. The file is read and checked on every check and
pinned by nothing, as a .proto is: a value it gains reaches every rule that reads it, and
every table there that does not look at the value fails completeness. A file that cannot be
read, is not an enums file, or holds no such enum is E013; an enum or a group the rule
declares under a name an import brings is E034.
The generated code of a rule declares none of these types. Each language imports them from the
one module the enums file becomes, which rulec gen writes beside the rule's whether or not
the file is named on the command line (generated-code.md), so an app that calls two rules hands
both the same value. rulec api of an enums file names that module in each language, and
rulec doc renders its enums with their order and display names. The commands that are about
a decision — schema, adapter, verify, replay, fixtures lint, diff, and doc as
HTML or for customers — refuse one, and a contribution to one, which stands for it there (§6.7).
3.2 source — the documents a rule transcribes
source stamp_act = law "342AC0000000023" asof 2026-04-01
別表第一 sha256:0ba69792e960021e
source measures_act = law "332AC0000000026" asof 2026-04-01
第91条 sha256:85faf53f6f6e8196
source osha = law ecfr "29 CFR 1910" asof 2026-01-01
"§1910.157" sha256:c2a9ce966c7e2269
source japanpost = file "yupack_tariff.pdf" sha256:9e4edb5b6a1c0f42
source terms = file "tariff.md" url "https://raw.githubusercontent.com/o/r/a1b2c3d/docs/tariff.md" sha256:4f1e0a77b2c3d5e6
table1 sha256:a583ec8586bbf596
table base @stamp_act 別表第一
policy unique
| stated | amount | -> tax |
| false | - | 200JPY | @stamp_act 別表第一 # no amount stated
A source names a document, after import. A law is a law in a statute database, by the
id that database gives it, read as of a date: the API returns one fragment at a time, so each
fragment the rule cites is kept as a copy beside the rule
(sources/law/<law id>@<date>/<element>.xml) and pinned by its digest on the line under the
source.
Which database is the word after law, and there are two.
| word | the database | the id | a fragment |
|---|---|---|---|
(none), or egov |
e-Gov, the Japanese government's statute database | the law id, 342AC0000000023 |
第91条, 第20条の2第3項, 別表第一, 附則第3条 |
ecfr |
the Electronic Code of Federal Regulations: US federal regulations as in force on a date | a title and a part, 29 CFR 1910 |
a section, §1910.157 (or 1910.157) |
The word is left out for e-Gov, so a rule written before there was a choice reads the same.
What differs between the two is the shape of the id, the shape of a fragment, and where a
copy comes from — not what a copy is, nor what it is held to. A fragment the language cannot
read as one word is quoted, in the citation and on the pin line alike: @osha "§1910.157".
A paragraph of a CFR section ((d)(2)) is not addressed yet, because the eCFR serves a
section at a time and cutting the copy up here would make it worse evidence than the one the
government served. A file is a document beside the rule — a tariff
sheet, a workbook, a policy in Markdown — pinned whole on its own line. A url on
it says where that copy came from, so rulec source fetch can bring it again and rulec source
outdated can ask whether the original has moved on; a document that arrived from a person has
no address and leaves it out.
A document's fragments are its tables. table1 is the first table of the document in document
order and 表1 is the same name in Japanese; for a workbook a table is a sheet. A cited table
is taken out of the document by rulec source fetch, written beside it as
<document>.fragments/table1.tsv (tariff.md.fragments/table1.tsv), and pinned under the source line exactly as a law's articles
are — so a revision of the document that moves a cited table fails the check, and one that does
not, does not. The formats read here are csv, md, xlsx and docx — a workbook's table is a sheet, a Word
document's is a w:tbl, and a merged cell leaves the column it took empty rather than being
filled in, because the copy is evidence. A PDF or a scan needs an extractor that is not this
program: rulec source fetch --via <cmd> runs one as a child process and reads the tables it
hands back (formats.md, the extraction protocol), and the name it gives itself is
kept beside the copies and shown on the page for people. A document with no extractor to hand is
cited whole.
The rows are then held to that copy, which is the one thing a completeness proof cannot do:
it says the table is consistent with itself, not that it says what the document says. An amount
a row writes that the copy does not show is E116. Where the copy has a heading that says a
word of the row's cells — 関東, or 一般 for a row the copy heads 一般の事業 — the amount is
looked for in that heading's row and column only, since the amount of the next row down is in
the copy too; and a rate the copy writes per thousand, 5/1,000 or 1,000分の5, reads as the
0.5% it is. A number the copy states as a
whole cell that no row of the citing table uses is W120, and the two together are what a
mistyped digit looks like (890円 is nowhere in the copy, and the copy's 880円 is used by
nobody). A threshold is rewritten as it is
transcribed — 1,949,000円まで becomes <=1949000円 — so its text cannot be compared, but one
thing survives the rewriting: which of the two bands the boundary value itself falls in.
The copy's 60cm以下 and 60cmを超え both put 60cm in the band below, and so do <=60cm and
>60cm; a row that puts it in the other band is E119, reported on both of the rows that
share the boundary. The word is read beside the number (60cm以下, Under 18, Not over
$11,925) or in a heading over its column (円以上 and 円未満, as a premium table writes
them). A boundary the copy words neither way — 18 to 20, 60〜80 — is left alone, because
naming the numbers that bound a band does not say which band holds them. W120 asks only about cells that are nothing but a
number, and a row that merges what the copy lists one by one (<=3kg over its 1kg, 2kg and
3kg) accounts for all of them. A table that transcribes only part of a fragment moves its
citation from the table line onto the rows that came from it: a row's citation says only where
that row came from, so the rest goes unasked while the amounts are still checked.
@<source> <fragment> at the end of a table, clause, derive or define line, or after
the last bar of a row, says which fragment the definition transcribes: 第91条, 第20条の2,
第20条第2項, 第20条第2項第3号, 別表第一, and the supplementary provisions as 附則第3条 (the law's
own) or 附則(令和七年三月三一日法律第一三号)第3条 (an amending law's, its number spelled as the law's
heading spells it); several are separated by ,. A file is cited
whole, @japanpost, or by one of its tables, @japanpost table1; a law cited with no article is E037, and so
is a document fragment named anything but 表<n> or table<n>. The citation goes before the # comment. check holds the pins to the copies and never reads the network: a cited
fragment without a pin is E037 (the fix is the pin line), a pin that differs from the copy is
E038 (naming the definitions that cite it), a fragment with no copy is E039, and a pin no
citation uses is W119. rulec source fetch brings the copies — from e-Gov for a law, from the url for a file, and
out of the document itself for a cited table —
rulec source pin writes the pins, and rulec source outdated asks whether the original has
moved on, which is the one question check cannot answer offline. For a law it asks e-Gov
whether an amendment enforced after the date changes the text of a cited fragment (a revision
that only re-marks the XML is not a change).
For a file it depends on what the url names, and the difference is worth knowing before
writing one. A URL that names a commit cannot go stale, so the question is asked of the
repository instead: on a GitHub raw URL whose revision is a commit
(raw.githubusercontent.com/<owner>/<repo>/<commit>/<path>), outdated asks what has touched
that path since, and answers with the commits, their dates and their subjects, and the URL to
pin next. A URL that names a branch, or any other URL, can only be fetched and compared with
the pin: the answer is then that the bytes differ, which for a PDF or a spreadsheet is all
anything can say — and a page that changes its footer says it too. A commit is to a file what
asof is to a law, and pinning one is what makes this question worth asking. GITHUB_TOKEN
or GH_TOKEN is passed on when it is set; without one the API allows sixty requests an hour. The
page for people quotes the fragment under the definition that cites it: an article as its text,
with the date that text came into force and the amending law; a document's table as the table
itself, beside the rows transcribed from it. Every generated file names the sources in its header
(Cites: measures_act = law 332AC0000000026 asof 2026-04-01 (第91条 sha256:…)), and rulec api lists
them under sources, a file source carrying its url in both.
3.3 shape and from — where the caller's object holds an input
An input may say where it comes from, and a shape says which contract the object it comes
from is already described by:
shape order = jsonschema "api/order.json" "#/$defs/Order"
shape order = proto "api/v1/order.proto" shop.v1.Order
inputs
dest : jp_prefecture from order.shipping.prefecture
cold : bool from any order.lines where category = "chilled"
lines : number range >=0 <=200 from count order.lines
It changes no check of the table. What comes out of a projection is a scalar input like
any other: the region analysis never learns that it was projected, and completeness, overlap,
units and overflow are decided exactly as they would be without it. What the two declarations
buy is that the glue between the caller's object and the rule's flat inputs is generated
rather than written by hand, and that a field the contract renamed is E121 rather than a
KeyError in production.
from comes last on the line and runs to its end. Seven shapes, and no more:
| written | what it yields |
|---|---|
from <shape>.<field>… |
the value at the path |
from any <shape>.<collection> where <field> = <value> |
bool — some element passed |
from all <shape>.<collection> where <field> = <value> |
bool — every element did |
from count <shape>.<collection> [where …] |
number — how many passed |
from sum <shape>.<collection> of <field> [where …] |
a number — the total of one field over the elements that passed, 0 when none did |
from max <shape>.<collection> of <field> [where …] empty <value> |
a number or a date — the largest value of one field over the elements that passed, the empty value when none did |
from min <shape>.<collection> of <field> [where …] empty <value> |
the same, the smallest |
A sequence the rule walks can be taken from a collection of the contract too, each field of an element saying where it stands in the element:
shape order = jsonschema "contracts/order_lines.schema.json" "#/$defs/Order"
inputs
total : money[USD] range >=0USD <=100_000USD from sum order.lines of amount_usd
top_fragile : money[USD] range >=0USD <=5_000USD from max order.lines of amount_usd where fragile = true empty 0USD
elements lines from order.lines
quantity : number range >=1 <=99 from quantity
amount : money[USD] range >=1USD <=5000USD from amount_usd
sum adds the field up, and the input that takes it is a number, money, a quantity or a rate:
the field travels as a whole number in the input's unit, and a field the contract carries as a
fraction is E120, since it cannot be added up exactly. max and min pick one value of the
field, a number or a date, and empty is required (E074): it is what the input is when the
collection is empty or no element passes the where, which is the author's to decide. It is
written as a value of the input, in its unit (empty 0USD, empty 2030-12-31), and has to be
one the input takes (E074 again: otherwise the function that reads it out would hand the rule a
value its door refuses). None of the three gives none, so none of them goes into an optional
input. of names one field of an element, as where does; of belongs to these three and
empty to max and min, and either written elsewhere is E074.
elements <name> from <shape>.<collection> takes the sequence from the collection, every
element of it, nothing filtered. Every field of such a sequence says with its own from where
it is read in an element — a path from the element and nothing else (from qty,
from unit.kind) — and a field that says none, or a field that says one under a sequence not
taken from a contract, is E074. The elements are then elements like any other: the checks, the
walks and the vectors are exactly what they are for a sequence the caller passes. A message
names a field of an element by the collection with [] after it, order.lines[].qty.
One collection, and a unary test on a field of an element. A join, a nested quantifier
and a path inside a cell are all refused, for the reason §0 gives: the cell language is where
this tool's boundary is, and a path in a cell would put the caller's object model inside the
checks. The test after where takes the same forms a cell does (= a, = a, b, not: a,
<=100), and it compares the raw value the contract carries, so a literal in it carries
no unit — a contract has none, and comparing a scaled number with a raw one is refused (E120).
From a .proto, the raw value is what protojson writes: an enum is its value's name
(where temp = TEMP_FROZEN), a 64-bit integer is compared as the number it is though it
arrives as a string, and bytes or a message is nothing a where can compare (E120).
The contract is read on every check, resolved against the directory of the rule, and
carries no digest — the same footing as import proto (§3.1): what holds the two together
is the paths. A JSON Schema is read far enough to resolve a path: properties wherever an
object may have them — directly, or under allOf, anyOf, oneOf and if/then/else —
items, and a $ref that stays inside the document. The keywords written beside a $ref
count as well from JSON Schema 2019-09 on, which OpenAPI 3.1 follows, and are ignored before,
as draft-07 and OpenAPI 3.0 say. A .proto is read far enough for the same: messages and
their fields, repeated included, with map skipped rather than guessed at; the members of a
oneof are fields of the message, each with presence of its own like an optional field.
A path that cannot be resolved is E121, which says how far it got and which names were
there. A path that goes on through a collection is E121 too: a path reads one value, and each
element has its own. It names every collection along the path and, where the rest of the path
reads a value in one element, the ways above that read it across them — any, all, count,
sum, max and min, or the elements taken as a sequence. A type that does not fit is
E120; a shape no input and no sequence is taken from is W122.
The contract says which values can come, too, and that is held to the input. A path says
where a value comes from; the contract's validation says which values pass — Protovalidate's
(buf.validate.field) rules on a field of a .proto (gte, lte, in and the rest, the count
of a repeated, the listed values of a string), and a schema's minimum, maximum,
exclusiveMinimum, exclusiveMaximum, enum, minItems, maxItems and required. Each is
compared with what the input takes — its range, the values of its enum (those its range
lets in, for an ordered enum that has one, §15.251), the range of a count — and a value that passes the contract but not the input is E122: the generated
code would refuse, at the door, something the caller's own validation let through. A proto3
number field with no rule lets 0 through, because that is what an unset field is; an input
that does not take 0 stops there. A schema that lets a value be null — "type": ["integer",
"null"], or OpenAPI 3.0's nullable — is E122 for an input that is not optional. A row whose
cell on a projected input admits nothing the contract lets through is W123. Rules this does
not read — a predefined rule, a pattern, the part of a CEL expression below — are read as not
there, which reads the contract as wider than it is: a finding it did not need to make is
possible, a missed one is not. fix.text of an E122 is the option or keywords to write in the
contract (narrow_contract); whether the contract or the rule is the side to change is a
person's decision. Still no check of the table moves: what is compared is the contract with
the input's own declaration.
The same holds for sum, max, min and a sequence taken from the contract. A total reaches
as far as the field's values times how many elements the contract lets through: ten lines of
at most 100 come to 1000, and an input that takes at most 500 is E122, with "maxItems": 5 as
the narrowing (with no end on either, the fix is none, since no one keyword bounds a total). A
largest or smallest value is one of the field's, held to the input as a value read straight from
a path is. A sequence taken from a contract has each field of its elements held as an input is
(its required, its null, its range, its enum's values, its proto3 default); its length held
to the cap its counts put on it; and its total of a column to the upper end of each sum over
it, the column's largest value times the longest sequence the contract and the cap let through.
A field of an element is no input, so its E122 carries no example under inputs.
Conditions across fields are read too. A contract relates fields to each other: a CEL
expression on a .proto message ((buf.validate.message).cel, cel_expression) or on a field,
a oneof or a (buf.validate.message).oneof that lets one of its fields be set, JSON Schema's
allOf, anyOf, oneOf, not and if/then/else. Of CEL, the part that is a condition
on whole numbers, strings and booleans is read: sums and differences of fields and constants,
a product with a constant, the six comparisons, in against a list of literals, size() of a
repeated field, has(), !, &&, || and ? :, and a string-valued rule that passes when it
returns "". What is not — division, %, string functions, macros such as all, a double — is
taken as true once every negation has been pushed down to the conditions it applies to, so an
unread part can only widen what the contract lets through. Three things follow. A condition
on another field that narrows this one narrows what E122 compares (this.w >= 1 && this.w <=
100 on the message is a range). A constraint between two inputs of one contract that the
contract does not keep — some request passes its validation, inside the inputs' ranges, and
breaks the constraint — is E123, with that request as the example and, for a .proto, the
(buf.validate.message).cel that would promise it as fix.text; JSON Schema has no way to
compare two fields, so there it has no fix on the contract's side. A row whose cells, each a
value the contract lets through, ask for a combination the contract's conditions never let
through — express above 5 kg where the contract says !this.express || this.weight_g <= 5000,
two members of one oneof both set — is W124.
A field the contract lets an object leave out can only be taken by an optional input (T?),
and the projection function reads a missing one — or a missing object on the way to it — as
none. In a JSON Schema that is a field not in required, and a required input read from one
is E122, since the function could not read it.
A .proto shape is read in the JSON form protojson gives it. A field is read under its
JSON name — json_name, or the lowerCamelCase of its name, so zone_code is zoneCode — or
under the name the .proto writes, which protojson's readers accept as well. A field protojson
leaves out is read as proto reads it: a number as 0, a string as "", a bool as false, a
repeated as no elements, an enum as its value numbered 0, and a message as one with every
field left out. The defaults arrive, then, and they are held to the input like any other value.
Under a message field that is not required, and in an optional field, Protovalidate
validates nothing while it is unset, so the default arrives whatever the rules on the field
say; an input that does not take it is E122, with (buf.validate.field).required = true as the
fix. An optional input (T?) reads an unset message or optional field as none instead — a
field without presence cannot be missing apart from its default. A date read from a string is
held to whether "" passes, since protojson leaves an empty string out and "" is not a date.
A contract says how a value travels, and the rule says what it means: an enum and a date arrive as strings, and money and a quantity as whole numbers in the unit the rule declares.
The projection function holds every value it reads to what the rule takes, as the rule's
door holds an argument: a whole number (one written with a point that is whole too; from a
.proto, the decimal string protojson writes a 64-bit integer as), JSON's true or false, a
string, a YYYY-MM-DD string of a day the calendar has, a value of the enum. A field a where
tests is held to what the contract carries there, and a date there is compared as the day it
is. A value that is not — 1.5, true or "7" where an amount is read, "yes" where a
where reads a boolean — is refused with RuleInputError and a sentence naming its path as the
rule writes it, order.lines[].amount_usd is not an integer; so are a place on the way that is
not an object, a value that is not there, and a collection that is not an array. The five
languages read in one order — the inputs as declared, then the sequences; element by element,
the where and then the value — and stop at the same value (DESIGN §15.234).
Five of the twelve targets generate the projection function — Python, TypeScript,
JavaScript, Ruby and PHP, where the caller's object is a plain map and needs no name; a sequence
taken from the object is built there, one element of the rule's own type for each element of
the collection, and is no longer a parameter. Go,
Swift, Java and Rust hold it as a type, and the only ways to name that type would be to
generate it (this tool makes no domain object model) or to follow the caller's own; SQL takes
a relation of flat columns, NumPy takes columns, and the Wasm ABI takes one JSON object of the
rule's own inputs. rulec gen names the seven and says why, and rulec api lists them under
projection.without. The path check applies to every target equally — it happens in check,
before anything is generated. rulec api lists the contracts, the path of every projected
input, and the function in each language that reads them, under projection.
4. inputs and outputs
inputs
dest : jp_prefecture
weight : mass[g] range >=1g <=40kg
total : money[JPY, incl_tax] range >=0JPY <=10_000_000JPY
member : member_kind
outputs
fee : money[JPY, incl_tax] round up(10JPY)
range — required on every numeric input and every derive
One declaration does three jobs.
- Overflow proof. The reachable interval of every intermediate value is computed from the declared ranges and steps, and must fit in int64 (E108).
- The universe of the completeness check. "Every input matches some row" means every input within these ranges.
- The entry guard of the generated code. A call outside the range returns an error instead of silently computing something.
The form is range followed by one or two bounds: range >=0JPY <=10_000_000JPY, range >=1g.
A derive whose declared range does not contain what it can actually reach is E112, and the
message states the interval to widen to. An input of an ordered enum (§3) may declare a range
over its values, range >=basic <=gold: the bounds are values of the enum, and the three jobs
are the same over them. What the rule tells its callers says it as it says a number's range:
rulec schema lists only the values inside it, rulec api and rulec graph give its two ends,
the generated MCP tool, the .proto and the page say the same, and rulec diff tells a range
that moved as a change of what the rule takes (§15.251). A rule that applies one with such an
input is held to its range as to a number's: every value it can pass has to be inside (E043,
§7). A rate input may leave range out: it is then >=0% <=100%, and the guard enforces that,
so a rate that can exceed 100% declares its range like any other number. A table's numeric output column needs no range of its own — its cells
are its range.
Where another table reads that column, it is not held to its cells. A table whose column
is a number another table writes — or a number an applied rule answers, an open table's output
among them — is checked over every number, so its rows cover the whole line: >=1 and <=0,
not >=1 and 0. That is also what keeps it complete whatever rows a contribution adds to an
open table (§6.7). A word a table writes — a value of an enum, a truth value — is read as the
values its rows write, and a table needs no row for a value nothing writes. A value whose range
the rule states — an input, a derive, a count, a sum, an open value — is read over that
range, and what a walk answers (max, min, a fold's answer) over the range the check works
out for it (§6.2, §6.3).
contract_only marks an input that is only ever an entry check and appears in no table:
Without it, an input no table uses is W111.
range from koyomi — the days a koyomi date comes to
A date input can take its range from a date of a koyomi file instead of from bounds. koyomi computes the date on every input of its own range, so the set of days it comes to is known exactly, and the rule is checked over those days and no other:
What follows from names the date the way every language of ritsu names one: koyomi, the
file (a path from the rule's directory), date and the date's name. With payment_terms.cal
closing on the 20th and paying on the 10th of the next month, pay_day takes twelve days, the
10th of each month, and a table needs no row for the days between them:
table pick
policy unique
| pay_day | -> batch : run |
| <=2026-06-30 | first_half |
| >=2026-07-10 <=2026-12-10 | second_half |
| >=2027-01-01 | year_end |
Over every day from 2026-02-10 to 2027-01-10 this table leaves 2026-07-01 to 07-09 uncovered; over the payment days it is complete. The four jobs a range does all follow the set:
- Completeness, overlaps and unreachable rows are about the days in the set. A day of the set no row takes is E101 with that day as the example; a row that takes none of them is E102.
- The examples and the vectors use those days only. An example on another day is E019.
- The generated code refuses any other day at its door, in every backend.
- The certificate carries the days and where they come from — the koyomi file and its date
as the rule names them, and the file's SHA-256 — and both re-checkers (
tools/recheck.pyand the Lean one inproofs/) rule a box out when it holds none of the days.
The set is read through ritsu's port of dates, so the rule has to be checked with koyomi joined:
ritsu rulec check or ritsu check. The binary of rulec's own crate has no koyomi; there the
file is not checked over every day instead, and the run says so (E129, exit 2). A set that cannot be had — the file is not there, does not pass koyomi's
check, has no such date, or comes to no day — is E130, with what koyomi says. Only a date
input takes range from koyomi (E065); an output, a derive or a field of an element cannot
borrow its range from another file. The days are listed as a precondition in rulec api
("kind": "days"), and the page for people lists them beside the input.
round — required on every numeric output
Five modes, each pinned down for negative values:
| mode | direction | at grid 1 |
|---|---|---|
up |
away from zero | −4.2 → −5 |
down |
toward zero | −4.8 → −4 |
half_up |
an exact half goes away from zero | −4.5 → −5 |
half_down |
an exact half goes toward zero | 4.5 → 4, 4.6 → 5 |
half_even |
an exact half goes to the even neighbour | 2.5 → 2, 3.5 → 4 |
The value in parentheses is the grid: up(10JPY) rounds to a multiple of 10 yen. Rounding
is applied once per output, last. A missing one is E104; an output literal that is not a
multiple of the grid is E106.
An optional number (price : money[USD]? round down(1USD)) declares its rounding the same
way, written in the unit of the type it wraps. It applies where the output has a value; where a
row answers none, the output has none and there is nothing to round.
range on an output — the range other languages read it in
An output's line may write a range after its rounding:
It is held to what the output comes to: the cells of the rows that decide it, a define of its
name, or what the result reaches under the inputs' ranges and the constraint lines, put out to
the grid of its rounding. A range that leaves out a value the output can come to is E140, and the
message states the range to widen to (fix.text holds it). A range that cannot be checked is
E140 too: the output reads an input with no range, or is no number. A range with one end written
takes the other end from what the output comes to.
The range written is then the output's range wherever an input's or a derive's is said: the
certificate's ranges and output_ranges, rulec schema and rulec api, the MCP tool's output schema, the comments
of the SQL and of the .proto, the page for people (beside the output), and ritsu's port, through
which kaname and the other languages read what a rule answers. rulec diff tells a range that
moved as a change of what the rule answers (output_range). The range is checked, so the
generated code checks nothing more when it runs.
Where no range is written, a numeric output still has a range: what decides it — what the rows of
the tables that decide it write, a define of its name, what its result reaches, the range a
walk or an open value declares — put out to the grid of its rounding, since the rule answers that
rounded. It is said wherever a written range is but beside the output on the page for people: the
certificate's output_ranges, rulec schema and rulec api, the MCP tool's output schema, the
comments of the SQL and of the .proto, the script of the page, rulec graph, and the port, each
as the integers that go on the wire. rulec diff compares it as it compares a written one, and
replay, fixtures and verify count an answer outside it as out of reach. Both re-checkers of
the certificate hold that range, written or not, to what the output answers. The certificate's
ranges keeps what decides the output, which its other claims are worked out from.
range from koyomi (E065) and random (E070) are an input's alone. A word an output's line does
not read, anything but range and round, is E047, as on every declaration line.
5. derive
A linear combination of inputs, which can be used as a table column while still being a quantity.
The right-hand side may use inputs, +, -, and multiplication by a constant. range is
required. What the derive can actually come to is its expression computed over the inputs'
ranges, the constraint lines and the values the tables above write: the range has to contain
it (E112), and a row that asks the derive only for values outside it is E102, however wide the
range is written. So is a row that asks it, beside the value a table above writes into another
column, only for values it never comes to while the rows that write that value fire: when low
is written only for an amount up to 50 pounds, low beside an excess over the limit past 50
pounds is E102, and E101 asks no row for it. A constant that
is a fraction puts the values between whole units — amount * 10% comes in tenths of a pound —
and the checks read them on that step: rows <=1GBP and >=2GBP leave 1.1GBP to no row, and
E101 says so with the amount that makes it (amount = 11).
6. define
A named boolean or intermediate value. A boolean define may be used as a table column.
define bulk : bool = total >= 30_000JPY
define a_earlier : bool = a_due <= b_due
define rate_off : money[JPY, incl_tax] = list * rate
define effective : tier = max(member, campaign)
The condition of a boolean define must be one of exactly two shapes (E113):
- a unary test on one input or one derived value — that value compared with a constant;
- a comparison of two values of a type whose difference cannot be derived (two dates, for instance, or two values of an ordered enum, §3).
A direct comparison of two numbers is neither. Declare the difference as a derive and
compare that with a constant; the analysis is exact that way, and the message says so.
A define that computes a number may be a table column too. What it can come to is its
expression computed over the intervals of what it reads — the inputs' ranges, what the derives
and the other defines come to, the values a table above writes — whatever the expression's shape
(a product, a rounding, min, a remainder, a root): define share : rate = score / 200 with score from 0 to 200
comes to 0% up to 100%. The checks read that as they read a derive's reach: E101 asks no row for a
share past 100%, and a row that asks only for one is E102.
A boolean define of one comparison is read the same way for the truth value it takes. Where the
intervals of its two sides settle the comparison over the inputs' ranges — made <= 2027-03-31
with made declared up to the end of 2026 — it is always true, or always false: E101 asks no row
for the other value, a row that asks only for that is E102, and two rows that meet only there do
not overlap.
A define of a number whose expression only adds, subtracts and multiplies by constants is an
equation as well as an interval, and the checks read it as one, as they read a derive's
(§15.209 of DESIGN.md). Each column alone may come to the value a row asks for, and the columns
still never come to them together: with excess = added - limit, an excess of one or more over a
limit of one or more is an added of two or more, so a row asking for that excess, that limit
and an added of at most one is E102, and E101 asks no row for it. The certificate carries the
equation as a fact of the define (formats.md).
Where a row asks a computed column (a derive, a define) for values and neither an input that
reaches the row nor a proof that none does was found, the check says the row is undecided
(W128) rather than passing it as reached: the product of two numbers is 997 only as one box of
997 or 997 boxes of one, which no candidate the search walks need be. An example that reaches the
row decides it, and the warning goes. So does the gap the row leaves: taken out of the table, the
row leaves inputs no other row covers, the completeness check finds one, and that input — run
through the rule to confirm it — reaches the row; the vectors take it too. A row of inputs and
enums alone is never undecided: its point is an input.
6.1 constraint
A relation between two inputs that the caller guarantees. It computes nothing; it says which combinations of inputs can happen.
The shape is constraint <input> <comparison> <input>, with one of <=, <, >=, >
(E017), an input on each side (E018), and both of a type that has an order — money, a
quantity, a rate, a number or a date. Several lines all hold at once, and A = B, when
it is ever wanted, is the two lines A <= B and A >= B.
One side may add inputs up:
Only inputs joined by +, and on one side only: a + b <= c + d and a constant or a factor in
the sum (a + 3 <= b, 2 * a <= b) are E017, and something other than an input is E018. The
inputs of a sum are of the other side's type (E103), and a date is not added (E048). A sum is
read everywhere a pair is: the completeness check drops what the inputs' ranges show it rules
out and takes it into the elimination with the other linear relations, so a table needs no row
for a remainder below zero when used + requested <= quota; the witnesses and the vectors keep
to it; the generated code refuses an input that breaks it at the door; an example that breaks it
is E019.
Three things follow from one line.
- The completeness check stops demanding rows for combinations that cannot happen. A table that covers everything reachable is complete, and the gap E101 used to report — with a witness nobody could ever produce — is gone.
- A witness is one of the combinations that can happen. Every input the checks construct satisfies the constraints, so what a diagnostic hands back is a case somebody could really send.
- The generated code refuses a violating input at the door, with the constraint quoted, the way it refuses a number outside its range. The proof assumed the constraint, so the code has to insist on it.
The vectors follow the same line: rulec vectors never produces a combination a constraint
excludes, and an examples row that breaks one is an error (E019) rather than a case. Two
dates are compared by the days they are, in all of these as at the door.
Writing - in a cell says "this column does not matter here". Before constraints it also
had to stand in for "this cannot happen", and the two read the same on the page. A constraint
is how the second one is said out loud.
6.2 elements and fold
A rule is one decision about one case. Sometimes the case carries a sequence — the rows
of a tariff sheet, the candidates a predicate left — and the answer comes from walking it.
elements declares what one element carries; fold declares how the walk ends. A case may
carry more than one sequence — an order's lines and the payments made against it — and each is
declared under an elements of its own, with a name, and walked by the lines that name it after
over (§6.3). A sequence nothing walks is E020. A sequence the caller's object already holds can
be taken from it with elements <name> from <shape>.<collection>, each field saying where it is
read in an element (§3.3).
elements fee_rows
row_zone : zone
threshold : money[JPY, incl_tax] range >=0JPY <=1_000_000JPY
row_fee : money[JPY, incl_tax] range >=0JPY <=100_000JPY
table row_of
policy unique
| row_zone | threshold | -> verdict : pick |
| kinki | <=1000JPY | take |
| …
fold verdict over fee_rows
skip -> next
halt -> stop with 0JPY
take -> take_unique row_fee
hold -> keep_max row_fee by threshold
empty -> 0JPY
exhausted -> held
The fields of an element are declared exactly like inputs, and a table may use them as
columns; what differs is that the caller passes them once per element. The table's own
checks are unchanged — one element is one case, and completeness, overlap, units and
overflow are proved over it as they always were.
The arms:
| arm | what it does |
|---|---|
next |
leave this element, look at the next |
stop |
end the walk; the answer is what exhausted says |
stop with <value> |
end the walk with this answer |
take_unique <value> |
take this element's value; a second element that also takes is an error at run time |
take_first <value> |
take the first, ignore any later one |
keep_max <value> by <key> |
hold this element's value, replacing what is held when the key is larger |
take_weighted <value> by <weight> |
pick one element with the chance of its weight, by a drawn value (§6.6) |
empty -> <value> |
the answer when there are no elements. Required |
exhausted -> <value> |
the answer when the walk reached the end. Required; held is the value taken (take_unique, take_first), else the value kept (keep_max), else the empty answer |
Four things are checked, and each of them is a loop somebody has written by hand and got wrong:
- the answer for an empty sequence is declared (E022);
- the answer for a walk that reached the end is declared (E023);
- every verdict the table can produce has an arm (E024), and an arm nothing can reach is named (W115);
take_uniqueortake_first— there is no baretake. Whether a second matching element is an error or is ignored is a decision, and the grammar makes it one (E021).
Naming a fold's answer
Without more, a fold answers the rule's first output. -> <name> after the heading gives the
answer a name, and the items below read it like any other value (§15.219 of DESIGN.md):
fold pick over lines -> discount
top -> take_first top_off
rest -> next
empty -> 0USD
exhausted -> held
define balance : money[USD] = subtotal - discount - paid
A name that is an output is that output, and the answer is of its type. A name that is not one
is a value of the rule's own, and takes its type after a colon: fold flag over items ->
any_flagged : bool. Every arm's value is read in that type. It is E021 when the name is already
an input, a field of an element, a sequence or a value declared above, when a name that is no output has no
type, when an output's name is given another type, and when another item decides the same name —
a fold with no -> answers the first output, so a table or a sum that decides that output
too leaves two answers for one name. A numeric answer that the items below read takes the hull
of the values the fold answers with as its range, the way a sum takes its declared one, so a
define over it is held to int64 like any other (E108), and a table that reads it is checked
over that range: a fold that answers a member's number from 1 to 4, or empty -> 0, needs rows
for 0 and for 1 to 4 and none below 0. A date answer takes the hull of the days the same way.
Before the walks, on each walk, after them
A rule that walks sequences runs each of its items at one place, and which one is derived from what the item reads rather than declared (§15.208 and §15.219 of DESIGN.md):
- before the walks, once: an item that reads neither a value of one element nor what a walk
answers — a table over the ordinary inputs, a
definecomputed from them; - on each element of one walk: an item that reads a value of one element (a field of
elements, or an item that reads one). It runs on the first walk over its sequence after the walks that find every summary it reads: an item of one element that reads nothing a walk answers runs on the first walk, and one that reads a summary — amaxthe first walk found — runs on a second walk over the sequence, once that summary is known; - after a walk, once: an item that reads what the walks answer — a fold's answer, a
count, asum, amaxor amin(§6.3) — and no value of one element. It runs once the last walk it reads from is over.
A summary ends the walk it is on: every summary of one sequence whose column is there on the first walk is found on that walk, each reading every element, and one whose column is worked out from a summary is found on the walk after. Nothing in the rule says which walk comes first. How many walks there are is bounded by how many summaries there are, whatever the length of the sequence, and no walk runs inside another.
define opening : number = reserve + fee # before: inputs only
define offer : number = max(amount, opening) # on each element: `amount` is a field
fold verdict over bids
counted -> keep_max offer by offer
ignored -> next
empty -> opening # decided before any element
exhausted -> held
table review_of # after: it reads `best`, the answer
policy unique
| best | -> review : bool |
| >=500 | true |
| <500 | false |
So opening exists when the sequence is empty, an element's item can read it, and a table can
read what the walk answered.
A second walk, in an order's settlement: the highest price is known only once every line has been read, and the line that takes the discount is the one at that price.
sum subtotal over lines of line_total range >=0USD <=25_000USD # walk 1 over the lines
max top_price over lines of price empty 0USD # walk 1 over the lines
sum paid over payments of amount range >=0USD <=25_000USD # walk 1 over the payments
define gap : money[USD] = top_price - price # walk 2: a field and a max
table pick_line # walk 2
policy unique
| gap | -> pick : line_pick |
| <=0USD | top |
| >0USD | rest |
fold pick over lines -> discount # walk 2
top -> take_first top_off
rest -> next
empty -> 0USD
exhausted -> held
define balance : money[USD] = subtotal - discount - paid # after walk 2
What cannot be written is said where it is written:
- an item that reads the elements of two sequences (E031): a value of one element exists only while its sequence is walked, so reading both would walk one sequence again at every element of the other — a walk inside a walk, whose number of walks grows with the lengths;
- an output that is a value of one element (E031), and a
resultline that reads one: once the walk is over there is no one value to give; - an answer that reads what is not there where it is decided (E021):
emptyandexhaustedreading a value of one element,heldanywhere butexhausted, and any answer or key reading what is worked out from the fold's own answer — a fold whose verdict waits for its answer.
The fold's answers are of the answer's type — the type of the name after ->, or of the first
output: a word that is a value of its enum is that value (empty -> unavailable), a word that
is neither a value nor a declared name is E012, and a value of another type — true for an
enum — is E103.
Why a fold does not cost the checks their decidability
The checks work because a cell narrows its own column and nothing else, so a row is a box in the input space and gaps and overlaps are arithmetic. An arbitrary loop would end that. A fold does not: the table is complete and unique, so every element lands on exactly one verdict, and the verdict's type is a finite enum. The walk is therefore a reduction of a string over a finite alphabet — a small automaton — and how many elements there are at run time does not change what can be said about it.
What a fold generates
rulec gen writes the walk in Python, TypeScript, JavaScript, Rust, Ruby, PHP, Go, Swift, Java and Wasm —
every target but SQL and NumPy, which are refused by name below.
The sequence is an argument like any other input — an array of objects on the wire (§10.2),
each element's fields integers in their canonical unit — and every field gets the same entry
guard an input gets. A rule that walks two sequences takes two such arguments, in the order the
elements are declared. Each walk is one loop: the summaries of one sequence found on one walk
share it, and a second walk is a second loop over the same sequence, which works out again the
values of each element it needs. rulec test runs the generated walks over the vectors and holds
them to the reference evaluator, the same as for any rule.
SQL is refused by name. One query has nowhere to carry a value from row to row and stop
early. A window function or a recursive CTE can be made to look like a walk, but only by
giving up the 1:1 between a branch of the generated query and a row of the rule — which is the
reason the SQL backend exists. So rulec gen names SQL, skips it, and writes the other seven.
Writing an example of a walk
An example is a row of cells and a sequence does not fit in one, so the sequence is written once, under a name, and the cell names it:
sequence near
| row_zone | threshold | row_fee |
| kinki | 500JPY | 800JPY |
| kinki | 2000JPY | 1500JPY |
sequence no_rows
| row_zone | threshold | row_fee |
examples
| dest | total | fee_rows | -> fee |
| kinki | 5000JPY | near | 800JPY |
| kinki | 5000JPY | no_rows | 0JPY |
The columns of a sequence are the fields of elements, all of them, and every cell is a
value — a range or a - is how a table's cell is written, and this is one element as the
caller would really pass it (E026). A block with no rows is the empty sequence, which is a
case worth writing: it is the one a hand-written loop forgets.
The examples of a rule that walks a sequence must carry a column for it — a rule that walks two
carries a column for each (E025) — the cell must name a sequence of the right sequence's
fields that exists (E027), and a sequence no example names is reported (W116).
Each example runs through the reference evaluator at rulec check like any other (E107), and
each one joins the generated vector suite.
An input with no answer
A sequence where take_unique matches twice is a contradiction: the reference evaluator has
no answer and the generated code raises. Such an input is still part of the suite — it goes to
vectors/<alias>.refused.jsonl, and rulec test requires every generated language to refuse
it, as the contradiction it is. That is what makes the last fold transition covered rather than
merely named. The same file holds an input for every reason the door turns one away — the
sequence and the fields of its elements among them — and every language has to refuse each for
that reason, with the sentence the reference evaluator says
(formats.md, §15.204).
6.3 count, sum, max and min
fold ends a walk with one answer. count and sum end it with a number, max and min
with one value of a column, and the rule goes on from there like any other.
The shape is count <name>(<alias>) over <sequence> where <column> = <value>, with range
(E028, E030). The column is one of one element — a field of elements, or a column a
per-element table produces — and its values have to be a closed set, a bool or an enum
(E029). A bool column needs no = <value>: where name_match counts the elements where it
is true.
A rule that counts runs at the places a fold does (§6.2), and which item belongs to which is derived rather than declared: an item is part of a walk exactly when it reads something that only one element has. So the table that classifies an element runs once per element, what reads only the ordinary inputs runs once before the walks (an element's item can read it), and what reads the counts runs once, after.
table row_of # the walk: it reads a field of an element
policy unique
| name_match | addr_match | -> hit : match_kind |
| true | true | same |
| true | false | diff |
| false | - | diff |
count hits over candidates where hit = same range >=0 <=100
table verdict_of # after the walk: it reads the count
policy unique
| hits | -> result : outcome |
| 0 | no_match |
| 1 | one |
| >=2 | several |
The range is required, and it says two things. It is the universe the completeness check quantifies over once the count is a column — without it the check would ask for a row covering a count of −1 — and it is the cap on the sequence: the generated code refuses a sequence longer than the smallest bound any count over it declares, the way it refuses a number outside its range. A count cannot leave the space the proof was made over.
sum
sum totals one column of the elements instead of counting them.
elements lines
amount : money[JPY] range >=0JPY <=100_000JPY
sum total over lines of amount range >=0JPY <=1_000_000JPY
The shape is sum <name>(<alias>) over <sequence> of <column>, with range. The column is
one of one element, the same as a count's, and it has to be a number — an amount, a
quantity, number or rate (E029). The total keeps that column's type, so total above is
money[JPY] and a table over it is written in yen.
The summed column has to be non-negative (range >=0…, E029). That is what lets the
running total move one way only: the walk refuses the moment it passes the declared maximum,
so one test at the end is a test everywhere, and the accumulator never leaves maximum plus
one element — which is what keeps E108's int64 claim true of a sequence whose length nothing
caps. To take a difference, sum two non-negative columns and subtract.
max and min
max and min answer the largest and the smallest value of one column over the elements a
where lets in (every element without one), or the empty value when there is none:
max most_up over adjustments of shift where raises empty 0
min most_down over adjustments of shift where lowers empty 0
min earliest over tasks of due where done = false empty 2027-12-31
The shape is max <name>(<alias>) over <sequence> of <column> [where <column> [= <value>]] empty
<value> (E028). The column is one of one element, and a number, an amount, a date or a
value of an ordered enum (enum grade = bronze < silver < gold; E029 otherwise, with the title
"This column cannot be summarised"); the where tests a column of one element the way count
does. empty is required (E022): it is the answer for an empty sequence and for one where no
element passes the where, and what to answer then is the author's to say. Two elements of the
same value leave one answer, since the answer is the value; which element holds it is a fold's
question (keep_max).
The range needs no writing. It is the hull of the column's range and the empty value, since
the answer is one of those — min earliest above takes the range of due with 2027-12-31
inside it — and the tables over the answer are checked over that range. A range written on the
line has to hold the hull (E112, which writes the one that would), and then it is the universe,
the way a declared one is; a max of an enum takes no range (E069).
More than one summary
Nothing else accumulates. A fold's arms choose an element; a count adds one per element that
passes a test; a sum adds one column; a max and a min keep one. There is no average arm and
no arm that carries a number forward. An average is a sum over a count, divided after the walk
inside a rounding (§8): define avg : money[JPY] = half_up(total / max(n, 1), 1JPY).
A rule may have any number of count, sum, max, min and fold lines, over one sequence
or several, each over the sequence its over names (§15.219 of DESIGN.md). The summaries found
on one walk read every element: a fold that stops ends its own reading, and the count beside
it still sees the elements after. A count, a sum, a max or a min named like an output is
that output, and answers it. Every target but SQL and NumPy generates them, for the reason SQL
gets no walk at all.
6.4 machine and scenario
A rule decides one call. Some rules are one step of something that goes on: an order that
is paid, shipped and delivered, an application that is filed, reviewed and decided, a
membership that moves up a tier each year. The state lives with the caller — in the order's row
of a database — and every call is passed the state and answers the next one. machine says so:
machine order over step
carry state -> next_state
held amount_paid
initial received
final delivered, cancelled
never shipped after cancelled
once refund >0JPY
| line | what it says |
|---|---|
machine <name>(<alias>) over <table> |
the table whose rows decide the carried output: its rows are the transitions |
carry <input> -> <output> |
the output the caller passes back as that input on the next call. Both are one enum. Required. A second and later carry line carries a further value along with the state (below) |
held <input>, … |
inputs one case passes with the same value on every call, from its first to its last: the amount of an order, the class of the person who applied (E056 when one is not an input, or is the carried one) |
initial <state> |
the state a case starts in. Required |
final <state>, … |
the states a case ends in |
never <state>, … after <state>, … |
no sequence of calls reaches the first states once the case has been in one of the second |
once <output> <cell> |
in one case, at most one call answers an output the cell accepts: a refund paid, a point granted |
initial <input> <value> |
where a further carried value starts. One for every carry line after the first (E072) |
always <input> <cell> and … when <state>, … |
in every state a case can reach — every one on the when list, when there is one — each carried value named fits its cell |
carry is the whole of what is new about the meaning. The generated function does not
change — it takes the state as an argument and answers the next one, and keeps nothing — so
a rule with a machine is still a pure function (DESIGN P3). What the section adds is claims
about every sequence of calls the caller can make, and rulec check proves them:
| check | what it finds | code |
|---|---|---|
| completeness of the table | a state and an event with no answer — what happens when a cancel request arrives after shipment? — including the calls that leave the state where it is | E101 |
| overlap | a state and an event with two answers | E105 |
| final states | a call that moves a case out of a final state |
E124 |
| finishing | a state a case can reach from which no final state can be reached |
E125 |
never |
a sequence of calls that reaches the first states after the second | E126 |
once |
a sequence of calls in which two calls answer what the cell accepts | E127 |
always |
a sequence of calls that reaches a state the line speaks of with a carried value its cell does not accept | E135 |
| a carried number's range | a sequence of calls whose last call answers a carried number outside its input's range | E134 |
| reach | a state no sequence of calls reaches, and a row that applies only in such states | W125, W126 |
Without held, every input may take any value on every call, and a sequence that changes the
amount of an order halfway can come back as a counterexample nobody could send. With it, a
case keeps those inputs' values from its first call to its last, and the claims are about the
sequences that keep them: the check walks each world — each class the rule's own
boundaries cut the held inputs into — on its own. A row that applies only with a value a case reaching its state never has is
W126 then, with a note saying so.
A broken claim comes back with the shortest sequence of calls that breaks it, every call an
input the rule takes (witness.trace in the JSON, one line per call in the text):
error[E126]: A sequence of calls reaches shipped after cancelled
--> rules/order_lifecycle.rule:37 machine order
Calls (from received):
1. at received, event = cancel, amount_paid = 0JPY → cancelled (table step row 2)
2. at cancelled, event = pay, amount_paid = 0JPY → paid (table step row 10)
3. at paid, event = ship, amount_paid = 0JPY → shipped (table step row 4)
Carrying a number, a truth value or another enum
A case often holds more than its state: the days left of a grace period, the attempts made, a
mark that a free trial was taken. A carry line after the first carries such a value along,
and an initial line with the input's name says where it starts:
machine subscription over step
carry status -> next_status
carry days_left -> next_days_left
initial active
initial days_left 0
final suspended
never active after suspended
always days_left >=1 <=7 when in_grace
always days_left 0 when active, suspended
What can be carried is a number whose input declares both ends of its range (number, an
amount, a quantity, a rate), a truth value, or another enum; the input and the output of a line
are one type (E051), and every carried value has its initial (E072). The first carry line is
the state: the one-word initial, final and never speak of it, and the other values ride
along. A row writes a carried value the way it writes any output — a value (7), the input
itself (days_left, kept as it was), or a computed name (one_less from
define one_less : number = days_left - 1). There is no special way to count down: the row for
the last day has to be written, and that is where a reader sees what happens when the count
runs out.
What a case is in is then the state together with those values, and every claim — final,
never, once, finishing, the states no case reaches — is about that. Two more come with it:
alwaysnames carried inputs with a cell each, joined byand, and the states it holds in afterwhen(every state, without it). A sequence of calls that reaches one of those states with a value the cell does not accept is E135, the shortest one attached.- a carried number stays within its range. A call from a state a case reaches that answers
it outside the input's range leaves a value no next call can be passed: E134, with the calls
that get there. A row that would do so only from values no case holds is W126 instead — in
the example, a night of grace at zero days, since a grace period always starts at seven.
Such a case has nowhere to go, and E134 is what says so: neither
always(E135) nor finishing (E125) looks at a value past its range again.
While every combination of the state and the carried values fits the budget, each value of a
number is a place of its own and the claims are exact. Past that, a number is cut into
intervals at the boundaries that matter to it — the cells of the tables, the values the rows
write, the always cells and its start — and a call from an interval goes to every interval its
next value can fall in. That is more calls than happen, so a claim that holds there holds; a
sequence that breaks a claim there is looked for again among calls that really happen, from
where a case starts, and is reported as broken only when it is found. Otherwise the claim is
W127, and so is whether a case can still finish, which the intervals cannot show. --budget, or
a narrower range, brings the walk back to one value at a time.
Why the claims stay decidable
The state is a finite enum, and every other input is cut by the rule's own boundaries into
finitely many classes — the cells rulec diff walks (§15.122). Each class is settled by an
input that realizes it, and what the rule answers there is a transition from the state the
class has to the state the rule answers. The claims are then searches over a finite graph.
The price is the lines this language does not have, and they are left out on purpose:
- no number without a range is carried from one call to the next. A total so far — refunds paid, a balance with no limit — is kept by the caller and passed in, as it always was. A number with no end to its range would make the state a counter, and whether a counter reaches a value is not decidable in general; with both ends, the values are finitely many.
- one machine per rule. Two machines running side by side is where message order and races live; that is what TLA+ and P are for.
- no calendar. "Within 14 days" is an input — the days elapsed, counted by the caller — compared with a constant, as every date comparison is.
once counts a call whose output the cell accepts. Where the output is read off an input or
a computed value, the value's own boundaries are cut at the cell's, so that one input stands
for its whole class; a claim that turns on a class no input was built for and none shown
impossible is reported as undecided (W127), never as holding. A space larger than the budget
is E128: --budget counts fifty region nodes per cell.
A held input is followed by the class its own column cuts it into. Every value of a class
allows the same calls as long as nothing else reads the input; when a computed column or a
constraint reads it too, one value may allow a call another value of its class does not. The
claims that nothing bad is reached still hold then, since the walk of a class takes in every
value's calls, but whether a case can always still reach a final state is reported as
undecided (W127), and so is a counterexample that could only be followed by changing the value
partway.
scenario
A scenario is the examples of a machine: a sequence of calls from the initial state, one
row per call, with what each call answers.
scenario late_pay
| event | amount_paid | -> next_state | refund | accepted |
| pay | 3000JPY | paid | 0JPY | true |
| cancel | 3000JPY | cancelled | 3000JPY | true |
| pay | 3000JPY | cancelled | 0JPY | false |
The carried input has no column, and neither has any further carried value: the first call
starts in the initial state with each carried value at its initial, and every later one in
what the call before it answered. Every other input and every output has a column (E055, E111). rulec check runs the calls in order through the reference evaluator; a
call that does not answer what its row says is E107, reported with the calls that led to it.
What a machine generates
The function is the one every rule gets. Beside it, each language with a module of its own gets the constants a caller needs to keep the state: where a case starts, and whether it has ended.
| language | initial | final states | has it ended |
|---|---|---|---|
| Python | INITIAL |
FINAL |
is_final(state) |
| NumPy | rule.machine["initial"] |
rule.machine["final"] |
— |
| TypeScript, JavaScript | INITIAL |
FINAL |
isFinal(state) |
| Rust | INITIAL |
FINAL |
is_final(state) |
| Ruby | INITIAL |
FINAL |
final?(state) |
| PHP | INITIAL |
FINAL_STATES |
is_final($state) |
| Go | Initial |
Final |
IsFinal(s) |
| Swift | initialState |
finalStates |
isFinal(_:) |
| Java | INITIAL |
FINAL |
isFinal(state) |
| SQL, Wasm | — | — | — |
final is a keyword in PHP whatever its case, and in Swift, hence the two other spellings.
A further carried value gets the value it starts at, named after its input: INITIAL_DAYS_LEFT
in Python, TypeScript, JavaScript, Rust, Ruby, PHP and Java, InitialDaysLeft in Go,
initialDaysLeft in Swift, and machine.more[k].initial in the NumPy plan. An amount, a
quantity or a rate is the language's branded type (JPY(-5000) in Python), a truth value its
True or true, an enum's value its member. SQL and Wasm have no place for it either: rulec
api gives it under machine.carried, with each language's name for it.
The NumPy plan is data, so the state names are the wire form. A target with one door and no
module has no place for a constant; its caller reads the two off rulec api, which lists every
language's spelling under machine.constants.
The vector suite gets traces as well as single calls: for every transition a case can make
and every two that can follow one another, the shortest sequence of calls from the initial
state that ends with them, and every scenario. rulec test has each language's runner play
them — the state one call answers is handed to the next call as that language holds it —
and prints its constants first, so the traces hold the hand-over and the constants as well as
the answers (docs/formats.md). A further carried value is handed over the same way: the trace's
first call is passed where each one starts, and every later one what the language answered.
6.5 prerequisites
Some values of an enum can be had only after others: a course after the courses it requires, a
certification after the one below it, a module of a product after the modules it builds on. A
prerequisites block says which, once, for every value of one enum:
prerequisites enrollment over course
start intro_programming, discrete_math, calculus
statistics requires calculus
algorithms requires data_structures, discrete_math
machine_learning requires algorithms, statistics or linear_algebra
mainframe_cobol requires nothing "withdrawn from the catalog in 2026"
ordered by level_of
| line | what it says |
|---|---|
prerequisites <name>(<alias>) over <enum> [open] |
the block, its name — which names the generated functions, so a name that is not ASCII takes an alias (E011) — and the enum whose values it is about. open marks a block a contribution may add lines to, for the values it adds to the enum (§6.7) |
start <value>, … |
the values that need nothing. Every other value is reached from them. Required |
<value> requires <value>, … |
what a case has to hold before it can take the value. , separates groups that are all needed; or joins the values of a group, any one of which is enough: a, b or c is a, and b or c |
<value> requires nothing "<reason>" |
a value no case can take, and why: a retired course, a certification no longer examined |
ordered by <table> |
a table of this rule, declared above the block, whose answer for a value is never below its answers for what the value needs. It tests one column of the enum and writes one value of an ordered enum (§3) or one number in each row |
Every value of the enum is in exactly one place: on start, or on a line of its own (E071). A
value added to the enum without its prerequisites stops the check rather than passing as one that
needs nothing — a value a contribution adds too, unless the contribution adds its line to an open
block (§6.7).
The block stands in a rule or in an enums file (§3.1); ordered by only in a rule, since an
enums file has no tables. None of its words is reserved: start, requires, or, nothing
and ordered are read by their place in the line, so a value may be called any of them (start
requires … is the line of a value called start, and nothing is the word only before a
string), and a declaration may be called prerequisites.
What rulec check proves of a block:
| check | what it finds | code |
|---|---|---|
| shape | a name that is not a value, a value in two places or in none, no start, an ordered by table that is not there, is below the block or has another shape |
E071 |
| cycle | a value that, through what it needs, needs itself — the values of an or followed too |
E136 |
| reach | a value that cannot be reached from start, because it waits on one no case can take |
E137 |
| order | a value whose ordered by answer is below the answer for something it needs |
E138 |
| redundancy | a prerequisite another prerequisite of the same value already needs (the values of an or left out) |
W130 |
The graph is finite, so each of these is decided by walking it, and the answer is exact. A cycle is said once for each set of values that reach one another; what cannot be reached while there is a cycle is not said on top. A value that waits on one no case can take is said where the wait starts, with the values behind it in a note:
error[E137]: operating_systems cannot be reached from `start`
--> tests/mutants/m_e137.rule:40 prerequisites enrollment
|
40 | operating_systems requires data_structures, mainframe_cobol
| ^^^^^^^^^^^^^^^ no case can take this
|
operating_systems needs mainframe_cobol, which no case can take.
mainframe_cobol: `requires nothing "withdrawn from the catalog in 2026"`
Until operating_systems can be reached, neither can networks.
Drop the prerequisite no case can take, or if operating_systems cannot be had either, write `operating_systems requires nothing "<reason>"`.
What a block generates
Two functions, in the module of the file that holds the block — a rule's, or an enums file's —
in each language with modules:
<name>_requires(value): what the value needs, as a list of groups, each a list of values any one of which is enough. A value onstarthas no group ([]); one no case can take has one group with no value in it ([[]]), which nothing held can meet.<name>_available(value, held): whether the value can be taken now: it is not inheld, and every group has a value inheld.
| language | what a value needs | whether it can be taken |
|---|---|---|
| Python, Rust, Ruby, PHP | enrollment_requires(value) |
enrollment_available(value, held) |
| TypeScript, JavaScript, Swift, Java | enrollmentRequires(value) |
enrollmentAvailable(value, held) |
| Go | EnrollmentRequires(value) |
EnrollmentAvailable(value, held) |
| NumPy | rule.requires("enrollment", value) |
rule.available("enrollment", value, held) |
| SQL, Wasm | — | — |
SQL and Wasm have one entry and no room for a second function: rulec gen says so when it writes
them, rulec api names them under without, and rulec test notes that nothing was compared
there.
NumPy's plan carries the groups as data, and its runtime answers both; an enums file has no plan
there.
The vector suite holds every language to the reference evaluator over a handful of cases per value
— nothing held, one value of each group, that less the value of one group, each other value of an
or, the value itself besides, every other value — and the certificate carries the graph with the
orders that show it has no cycle and that every value is reached (docs/formats.md).
6.6 random and chances
Some inputs are not chosen but drawn: the number an audit draws for each claim to decide whether
it is inspected, the number of a lottery ticket, the roll that decides one round of a contest. Mark
such an input random, and the rule can say — and rulec check can prove — how likely each answer
is:
inputs
sample_number : number range >=0 <=99 random
category : category
amount : money[JPY] range >=0JPY <=10_000_000JPY
random at the end of an input's line says that the caller draws the value uniformly from what
the declaration lets in, independently of the other inputs, and passes it in. The function the
rule generates does not change: the caller draws, and the same inputs give the same answer. What
can be drawn is finite and countable:
| type | drawn from |
|---|---|
number, money, a quantity, rate |
every value of the range in the steps its type is held in, both ends included — range >=0 <=13 is 14 values. Both ends are required |
| an enum | its values, or those its range lets in (§3) |
bool |
true and false |
Where a constraint names a random input, the draw is uniform over the points that meet it:
with constraint roll < challenger_strength + holder_strength, roll is drawn from 0 to the
two strengths added together, less one. Completeness and overlap are proved over the random
inputs as over any other, and a machine's claims (§6.4) treat a random input as any value it can
be, since they are about what can happen.
E070 stops what cannot be drawn: a number whose range lacks an end, an input that may be absent
(?), a date, a string, an input read from the caller's object (§3.3), one a machine carries,
a field of an element (drawn once per element, so the number of points would turn on the length
of the sequence), and random on an output. The points of the random inputs multiplied together
are at most 2^62, since every language counts the ways in an int64: past that is E108.
chances
A chances table claims how likely an answer is:
chances
| category | amount | -> inspect | chance |
| entertainment | - | true | 1 |
| travel | - | true | 1/10 |
| expenses | >=100_000JPY | true | 1/4 |
| expenses | <100_000JPY | true | 5% |
| supplies | - | true | 1/50 |
| - | - | true | >=1/50 |
| column | what it holds |
|---|---|
before -> |
every input that is not random, each cell a condition like a table's (- for any value). A rule that walks a sequence has a column for it, whose cells name a sequence (§6.2), as in examples. A random input has no column: it is what is counted |
after -> |
the outputs the claim is about, each cell a condition like a table's. An output with no column may be anything |
chance, last |
an exact rational (5/14, 0, 1), a rate (35%, 0.1%), or comparisons with one (<=1/100, >=1/4 <=1/2) |
A row says: for every combination of the inputs that are not random its cells let in, with the
random inputs drawn uniformly, the chance that the outputs match its output cells is the value
written — or passes the comparisons written. A chance is a count of points over a count of points,
so it is always an exact rational, and rulec check compares it exactly.
How rulec check counts: the inputs that are not random are cut at the rule's own boundaries into
classes — the cells diff and the machine walk (§15.122, §15.148 of DESIGN.md) — and so are the
axes of the random inputs. Where nothing the rule computes reads a random input together with one
that is not, and no constraint ties the two, each cell of that walk is a class crossed with a box
of the random axes, the answer is one value over it, and the box holds the product of the lengths
of its intervals: the claim is counted exactly without looking at a single point, however wide the
ranges. Where an answer moves inside a box, or the two kinds of input are read together, the points
are run one by one, up to a million for one claim (one fiftieth of --budget's nodes).
| check | what it finds | code |
|---|---|---|
| shape | a column missing or of the wrong kind, a cell that is not a chance, a table where nothing is drawn | E070 |
| claim | a combination of the inputs that are not random where the chance is not the one written | E133 |
| budget | a claim whose points to run one by one are past its share of the budget, which is said to be neither holding nor broken | W129 |
A claim that fails names the combination and the chance there:
error[E133]: A claim of `chances` does not hold (row 4)
--> tests/mutants/m_e133.rule:29 chances
|
29 | | false | third | 6% |
| ^^ 6% is written here
|
With campaign = false, the chance of prize = third is 1/20 (5.0%).
A claim speaks of every combination its cells let in, with the random inputs drawn uniformly. If the table is right, correct the claim; if the claim is the business truth, correct the table's rows.
Picking by weight: take_weighted and draw
A fold can pick one element with the chance of its weight — a case for one of the reviewers who are in, in proportion to their capacity:
inputs
roll : number range >=0 <=9999 random
fold presence over reviewers
away -> next
in_office -> take_weighted reviewer_id by capacity
draw -> roll
empty -> 0
exhausted -> held
| line | what it says |
|---|---|
<verdict> -> take_weighted <value> by <weight> |
add the weight of each element that lands on the verdict to a running total, in order, and take the value of the first element at which the drawn value is below that total. An element is taken with the chance of its weight over the total of the weights |
draw -> <input> |
the drawn value: an input marked random, a number whose range starts at 0 |
The drawn value has to be below the total of the weights: the generated code checks it once the
walk is over and refuses the call otherwise, as it refuses an input out of its range, and the
draw is uniform over the values below the total. When the weights add up to 0 the walk answers
exhausted, with held the empty answer. A rule picks by weight once, the arms of that fold are
next and take_weighted alone, and a weight is a number that cannot be negative (E073).
What chances generate
A rule with random inputs generates one more function in every language with modules: it takes the inputs that are not random (and the sequences), and answers how many draws give each answer and how many draws there are. A chance is the count over the total.
| language | function | result |
|---|---|---|
| Python | prize_lottery_chances(campaign) |
Chances(NamedTuple): total: int, ways: list[tuple[Prize, int]] |
| TypeScript, JavaScript | prizeLotteryChances(campaign) |
{ total: bigint, ways: [Prize, bigint][] } |
| Rust | prize_lottery_chances(campaign) |
Result<Chances, RuleError>, Chances { total: i64, ways: Vec<(Prize, i64)> } |
| Ruby | PrizeLottery.prize_lottery_chances(campaign) |
Chances = Struct.new(:total, :ways) |
| PHP | \PrizeLottery\prize_lottery_chances($campaign) |
final class Chances(int $total, array $ways) |
| Go | PrizeLotteryChances(in Input) |
(Chances, error), Chances{Total, Ways []Way{Out, Count}}; the random fields of in are not read |
| Swift | prizeLotteryChances(campaign:) |
throws -> Chances, Chances{total, ways: [Way]} |
| Java | PrizeLottery.prizeLotteryChances(campaign) |
record Chances(long total, List<Way> ways) |
| NumPy | rule.chances(**fixed) |
(total, [(outputs, count), …]) |
| SQL, Wasm | — | — |
The answers come in the order they are first met — the first random input outermost, each from its
lowest value. A point the door refuses (a constraint,
a drawn value past the weights) is no point. The function runs the rule's own function on every
point, so it never disagrees with it; a rule whose one random input only the draw line reads is
counted from the weights instead, one walk for the running totals and one run per element weighed.
A function that runs the rule over more than a million points on every call is W131. SQL and Wasm
have one entry and no room for a second function: rulec gen says so, rulec api names them under
without, and rulec test notes that nothing was compared there.
rulec test holds the function in every language to the reference evaluator over
vectors/<alias>.chances.jsonl (docs/formats.md), the certificate carries the counts box by box
(docs/formats.md), and the page for people shows each claim beside what the check counted, and
counts the chances of the case entered in its panel.
6.7 open values and contributions
A rule may leave places for other files to add to, and a contribution file adds to them: a
campaign that adds a discount, a product line that adds a kind of product with its markup and its
tax rate, a policy that takes precedence over a clause. The rule says where it may be added to; the
contribution names the rule and says what it adds; and rulec merges the two into one rule before
anything is checked, so the checks, the evaluator, the generated code, the page and the certificate
all see one ordinary rule.
| the rule writes | a contribution adds |
|---|---|
open <name> : <type> combine … range … |
a term to the value (into) |
enum <name> = … open (§3) |
a value to the enum (extend) |
table <name> open, clause <name> -> <output> open |
a table or a clause of the same output that takes precedence over it (overrides) |
prerequisites <name> over <enum> open (§6.5) |
the requires lines of the values it adds |
Which contributions are merged is decided by the files a command is given, and by nothing else:
rulec check order_discount.rule contrib/ checks the rule with every contribution under
contrib/ that adds to it, and rulec check order_discount.rule the rule alone. There is no list of
what to load, and no order to keep: the four ways of putting a value together do not depend on the
order of their terms.
open — a value its terms put together
# Discounts that stack: every one applies.
open stackable : money[USD] combine sum range >=0USD <=100USD
member_off
channel_off
# Discounts that do not stack: only the largest applies.
open exclusive : money[USD] combine max empty 0USD range >=0USD <=60USD
coupon_off
# The cap on the whole discount: the lowest any campaign sets, 50 dollars when none does.
open cap : money[USD] combine min empty 50USD range >=0USD <=50USD
define discount : money[USD] = min(min(stackable + exclusive, cap), subtotal)
The line declares a value as a define does — a name, with an alias when it is not ASCII, and a
type — then how its terms are put together and the range it stays inside. Under it, one to a line,
are the names of its terms: values decided above it (an output of a table or a clause, a derive,
a define, a summary, a fold's answer, another open value, a name an apply brought in). A rule
may give an open value no term of its own (cap above); the contributions give it theirs.
combine |
the value | types |
|---|---|---|
sum |
0 plus every term | number, money, the quantities, rate |
product |
1 (100% for a rate) times every term | number, rate |
max |
the largest of empty and every term |
any numeric type, temperature and sound too |
min |
the smallest of empty and every term |
the same |
empty is written for max and min, and only for them. It is the value the terms start from,
as 0 is for sum and 1 for product, and it always takes part: it is the value when there is no
term, and a floor (max) or a ceiling (min) every term is compared with — cap above is 50
dollars, or less when a campaign sets less. That is not the empty of a max or a min that walks
a sequence (§6.3), of a fold (§6.2), or of from max and from min (§3.3), which is the answer
when there is no element to look at and takes no part otherwise. A term that cannot move the value
whatever it takes — at or below empty in a max, at or above it in a min, always 0 in a sum,
always 1 in a product — changes no answer, and W132 says so: a cap written as a max is the
usual way to get one. Only a numeric value can be open — not a value of an enum, a date, a truth
value, a string or an optional type — and a term is on the value's step or a coarser one (E068).
The range is required, and the rule is checked over it. A table that reads an open value is asked to cover the whole declared range, not just what the terms written today reach, since a contribution may move the value anywhere inside it: that is what keeps the rule's own check true whatever is added later. Whether the terms, put together, stay inside the range is checked on the merged rule (E131), by interval arithmetic over each term's range, with the term that takes it out, the file that term came from, and an input that does it:
error[E131]: The open value stackable can leave the range it declares
--> tests/corpus/order_discount.rule:34
|
34 | open stackable : money[USD] combine sum range >=0USD <=100USD
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ range: 0USD .. 100USD
|
An input that takes it out of the range: member = true, via = web, north = true
Its terms together come to 0USD .. 103USD.
member_off: 0USD .. 5USD (this rule)
channel_off: 0USD .. 3USD (this rule)
regional_campaign:campaign_off: 0USD .. 95USD (contribution regional_campaign (tests/mutants/m_e131.rule))
The range is the promise that the value stays inside it whatever is added, and the tables below are checked over the whole of it. Widen it (the tables below are then checked over the wider range), or narrow what the contribution adds. Which is a business decision.
The value is generated as an ordinary definition at the place its line is written, and the page lists its terms with the file each came from.
A contribution file
contribution regional_campaign(regional_campaign) v1
description "A campaign of the north region: 7 dollars off that stacks with the rest, a 20-dollar voucher that does not, and a cap of 30 dollars for its orders"
to "../../order_discount.rule" signature:fe7307da2694d75a
inputs
north(north) : bool
table campaign_parts
policy unique
| north | -> campaign_off : money[USD] | voucher_off : money[USD] | campaign_cap : money[USD] |
| true | 7USD | 20USD | 30USD |
| false | 0USD | 0USD | 50USD |
into stackable
campaign_off
into exclusive
voucher_off
into cap
campaign_cap
examples
| subtotal | member | coupon | via | north | -> discount |
| 200USD | true | true | web | true | 30USD |
| 200USD | false | true | store | true | 27USD |
| 200USD | true | true | web | false | 18USD |
| 9USD | false | false | store | true | 9USD |
| line | what it says |
|---|---|
contribution <name>(<alias>) v<version> |
that the file is a contribution, and its name, which comes before the names it brings in. First |
to "<file>" signature:<16 hex> |
the file it adds to, relative to this one: a rule, an enums file, or another contribution, whose rule it then adds to as well. Required, once |
after "<file>" signature:<16 hex> |
a contribution this one builds on: merged before it, its names read here as <contribution>:<name>. Any number |
inputs |
the inputs the merged function takes besides the rule's. An input of the same name as one of the rule's is that input, and has its type and range (E042). One may be read out of a contract the rule declares (from, §3.3); none is drawn at random (random, E067) |
extend <enum> with <value>(<alias>) ["<label>"] [after <value>] [default] |
a value added to an open enum: at the end, or right after the value named, which is where it falls in the order of an ordered enum (the after is required there) |
tables, clauses, derive, define, count, sum, max, min, fold, open |
what it decides, written as in a rule |
into <open value>, and names under it |
terms it adds to an open value |
prerequisites <block>, and <value> requires … lines under it |
the prerequisites of the values it adds, in an open block |
sequence, examples |
its examples, with the rule's inputs and its own, and the rule's outputs |
What decides the shape of the function — outputs, result, apply, machine, the sequences a rule
walks, enums and imports, sources and constraints — belongs to the rule, and a contribution that
writes one is E067.
Names. The names a contribution declares come into the merged rule as <contribution>:<name>
(regional_campaign:campaign_off), and the generated code spells them with _ (alias
regional_campaign_campaign_off), so two contributions may each have a campaign_off and neither
meets a name of the rule. A contribution reads the rule's names as they are written, and the names
of a contribution it comes after as <contribution>:<name>.
Where a contribution's lines go. A contributed table or definition is placed after everything
it reads and before the first thing that reads it. A table or a clause of an open output goes right
after the output's last definition, since the tables of one output are decided where the last of
them is; one that would read a value decided below that place is E067. The contributions are merged
in the order their after lines put them, the rest in the order of their paths; after lines that
go round are E067.
extend — a value added to an open enum
contribution garden_kind(garden_kind) v1
description "The garden line: garden supplies, a new kind of product. It ships the way any other kind does, which `default` says"
to "../../shared/product_kinds.rule" signature:3b66d8e9012538cc
extend kind with garden "Garden supplies" default
Every table of every rule merged with it that reads the enum is asked for a row of the new value: a
table that has none is E101, and the value it misses says which contribution added it. A table that
is not open, so that no contribution can add a row to it, catches the new value in its - row or in
a cell that compares; the value then takes a row its writer never saw, and that is E033 unless the
extend line marks it default — the same word, meaning the same, as on an enum line. W111 is
not said of a value a contribution added.
A definition that contributions may take precedence over
open after a table's name, or at the end of a clause's heading, lets a contribution add a table or
a clause of the same output with overrides naming it, or naming the clause of a contribution it
comes after (overrides strict_policy:strict_decay). The tables of the output are checked
together, as tables that share an output always are (§7). Taking precedence over a definition that
is not open is E067.
A table that reads a number an open table writes covers every number, not only those the rule's
rows write (§4, range): a contribution's row may write any value of
the output's type, and the reading table is complete over all of them in the rule's own check. A
table that reads such a weight to tell a candidate that is weighed from one that is not has >=1
and <=0 rows, not >=1 and 0.
Contributions, chances and contracts. What is drawn and what is claimed of its chances (§6.6)
is the rule's: a contribution draws no input and writes no chances (E067). The rule's claims hold
whatever is added — the merged rule is held to them, an input a contribution adds is a - column of
every claim, and a contribution that moves the chance of a claimed answer is E133, which names the
contribution's rows that take part in deciding it. A rule that reads its inputs out of the caller's
object (§3.3) lends its contracts to its contributions: an input a contribution adds may be read
from them, the projection function of the merged rule reads it from the object, and an input
added without from comes after the objects among its parameters.
What check checks
Given a rule and contributions to it, rulec check checks
- the rule alone, with its examples, each open value over its declared range;
- the rule merged with every contribution given that reaches it — completeness, overlap, unreachable rows, the values contributions add and the ranges of the open values (E101, E102, E105, E033, E131), each naming the file and the contribution of the row it is about. The examples of the files are not run here: each was written for the files it was written with;
- each contribution, merged with the rule and the contributions it comes
after, and nothing else, with its own examples. What the rule cites — its contracts, the enums it reads from a.protoor a schema, the koyomi files of its dates, the copies of its sources — is found from the rule's directory there as in the rule's own check, and a koyomi file a contribution's input names from the contribution's (DESIGN §15.235).
A diagnostic about a line of a contribution points into that file. A finding said for one file is
not said again for another: a file whose errors were all said above gets one line that says so. The
ok lines say what each file was merged with:
$ rulec check tests/corpus/shared/product_kinds.rule tests/corpus/product_price.rule tests/corpus/product_tax.rule tests/corpus/product_shipping.rule tests/corpus/contrib/product_line
ok tests/corpus/shared/product_kinds.rule (1 contribution: tests/corpus/contrib/product_line/garden_kind.rule)
ok tests/corpus/product_price.rule (2 contributions: tests/corpus/contrib/product_line/garden_price.rule, tests/corpus/contrib/product_line/garden_kind.rule)
ok tests/corpus/product_tax.rule (2 contributions: tests/corpus/contrib/product_line/garden_tax.rule, tests/corpus/contrib/product_line/garden_kind.rule)
note tests/corpus/product_shipping.rule: 1 shadow pairs (1 structural, 0 equivalent, 0 needs review)
ok tests/corpus/product_shipping.rule (1 contribution: tests/corpus/contrib/product_line/garden_kind.rule)
ok tests/corpus/contrib/product_line/garden_kind.rule (contributes to tests/corpus/shared/product_kinds.rule)
ok tests/corpus/contrib/product_line/garden_price.rule (contributes to tests/corpus/product_price.rule)
ok tests/corpus/contrib/product_line/garden_tax.rule (contributes to tests/corpus/product_tax.rule)
The pins: signature:
A contribution relies on the shape of what it adds to, not on its rows: a campaign that adds to the
discount is still right when a row of the member discount changes, and may be wrong when the range of
stackable or the type of an input it reads changes. signature: on the to and after lines is
the first 16 hex digits of a SHA-256 of that shape — the file's name, its inputs and outputs with
their types, ranges and rounding, its open values' lines, its open enums (with their values, when
they are ordered), the outputs its open tables and clauses decide, its open blocks, and the
declaration of each name the contribution reads. rulec source pin <contribution> writes them. A
shape that moved is E040, which says what moved when the repository holds the version the signature
was taken at (it looks through the file's last 60 commits). Here the range of stackable was
narrowed to 80 dollars after the campaign was pinned:
error[E040]: The shape of `../order_discount.rule` this contribution relies on has changed
--> contrib/regional_campaign.rule:3
|
3 | to "../order_discount.rule" signature:fe7307da2694d75a
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pinned: signature:fe7307da2694d75a
|
The shape now: signature:377d5b0bbf770662
`open stackable : money[USD] combine sum range >=0USD <=100USD` is now `open stackable : money[USD] combine sum range >=0USD <=80USD`
Read this contribution against the new shape; once it holds, make the line as `fix.text` says (`rulec source pin` writes it too).
Every command, on the merged rule
Every command that takes rules takes their contributions beside them — gen, doc, api,
certificate, coverage, vectors, graph, schema and adapter answer for the merged rule, and
a contribution given without its rule answers for the rule it adds to, merged with it. The generated
code names each contribution with its file, its signature and the digest of its bytes in its header
(generated-code.md); rulec api lists them under contributions, with the inputs each adds, and
every open value under opens, with its terms (formats.md); the page has a section for what adds to
each open value and one for each contribution, with its tables drawn from its own file; and the
certificate reads every row out of the file it is written in.
A rule that applies a rule a contribution is merged into takes the contribution with the applied
rule's rows, so it is part of that rule too: its ok line names the contribution's file, and the
header of its generated code and rulec api name the contribution under the apply's name, as the
names an apply brings in are written — elig:m_elig in a rule that applies elig (DESIGN §15.237).
7. Tables
table base
policy unique
| dest | weight | -> base : money[JPY, incl_tax] |
| remote | <=2000g | 1200JPY |
| remote | >2000g | 1800JPY |
| not: remote | <=2000g | 800JPY |
| not: remote | >2000g | 1100JPY |
The first row is the header. Left of -> are input columns, right of it output columns. A
column may name an input, a derive, a boolean or enum define, or an output of an earlier
table. An output column that introduces a new name declares its type there. One under a name
that already has a type — an output, or a name a table above decides — may write that type
again and no other (E103): what the column decides is what the name answers.
Policies
| policy | meaning |
|---|---|
unique (the default) |
no two rows may overlap. Reordering the rows cannot change the meaning |
first |
the earliest matching row wins |
There is no Any, Priority or Collect, and completeness cannot be waived: a table with a hole is E101 whichever policy it uses.
Row labels
A row may carry a label before its first bar:
| stated | amount | -> tax : money[JPY] |
exempt | true | <10_000JPY | 0JPY |
r3 | true | >=10_000JPY <=100_000JPY | 200JPY |
A label names the row wherever a name is needed — in an overrides line, in the trace a
generated function returns ({"table":"base","row":2,"label":"exempt"}), in a later version —
and it is unique within its table (E034). Rows without one are still counted by position, and
rulec fmt aligns the labels as a column of their own.
Tables that share an output
Several tables may define the same output: a main rule and its special cases, each transcribed
from its own source. Each then has exactly one output column (E045), and which takes
precedence is written on the table that wins, right after policy:
table reduced_rate
policy unique
overrides base
| reduced | stated | amount | -> tax |
| true | true | >100_000JPY <=500_000JPY | 200JPY |
overrides names tables declared above, or one labelled row of one (base:r3), and says that
every row of this table takes precedence over them. The exception is written after what it
excepts: a target that is missing or declared below is E035, one that defines a different
output is E036. The tables of one output are checked together — completeness over their
union (E101), every overlap either ordered by an overrides line or reported (E105 / W114),
and a row that the rows taking precedence over it cover entirely (E102). An overrides line
whose rows meet none of its target's is W117. The generated code tries the tables from the last
declared to the first and takes the first row that applies, and the trace names the table the
row was written in.
Clauses
A definition whose conditions do not line up as columns — a proviso, a main rule written as
one sentence — is a clause: one row, written as prose.
clause regular -> fee
when always
then base
clause free -> fee
when total >=3900JPY and member true
then 0JPY
overrides regular
The heading names the clause and the output it defines (an outputs name, an intermediate an
earlier table or clause introduced, or a new one with -> name : type; on a name that already
has a type, : type may only repeat it, as on a table's heading). when is a list of
<column> <cell> joined by and, the cell being any of the seven kinds above, the column
anything a table's column may be; a clause with no condition writes when always (leaving the
line out is E046, for the reason a blank cell is E008). then holds what an output cell holds:
a literal or a name. overrides is the same line a table may carry. A clause is a table of one
row whose other columns are -: it is checked, evaluated and generated as one, fires in the
trace as {"table":"free","row":1}, and the page shows its condition and its value as written.
Applying another rule
A provision applied mutatis mutandis — "the rule of Article 20 applies, reading 'years of
service' as 'period in office'" — is an apply: another rule file, used once more with its
inputs bound to values of this rule.
apply retirement = "retirement_pay.rule" meaning:aadc7468b292ff52 # Source: Article 31
years = tenure
reason = end_reason with term_end -> retirement_age, resignation -> voluntary
base_pay = monthly_pay
except reduction
allowance -> part_time_allowance
The heading names the apply, the callee (a path relative to this file) and the pin of the
callee's meaning: the first 16 hex digits of a SHA-256 of its lines with comments, the
description and the citations left out, so that a comment moves nothing and a row does.
rulec source pin writes it, and check holds the callee to it (E040, which shows the lines that
moved when the repository holds the version the pin was taken at: a callee that changed is looked
at with rulec diff before it is pinned again). A sha256: pin, the digest of the file's bytes,
is read as it was, and rulec source pin writes it again as meaning:. Under the heading, one
line per callee input binds it to an input, a derived value, a definition, a table or clause
output of this rule, or a literal — every input, exactly once (E041). The types agree (E042);
two enums are mapped with with, which covers every value of this rule's enum, values spelled
the same on both sides mapping by themselves. What this rule passes has to stay inside the
callee's declared ranges and satisfy its constraints (E043, never fixed by adding a row: the
callee is another unit of approval). Where the callee's input is not optional, what is passed has
to have a value: one that may be absent — an input or a field of an element declared T?, the
output of a table a row of which answers none, a definition that only names one — is E043 too.
with none -> <value> reads this rule's absent value as a value of the callee's (tier = member
with none -> basic), beside the values that stand for it already; it is written only where the
value may be absent and the callee's input takes no absent value (E042 elsewhere). A number is held there by interval arithmetic. For an input
the callee holds to a range of an ordered enum (campaign : tier range >=basic <=gold), every
value the binding can pass, read as the callee's through with, has to be one the range lets in:
an input can pass its range (every value of its enum without one), a literal its value, a table's
output what its rows write (what a name can take, for a row that writes one), a definition
that only names another value what that value can, and a max or a min of two values what the
two can take that it can come to — read as a number's interval is, from the two ends: a max
comes to nothing before the later of their first values, a min to nothing after the earlier of
their last ones (min(campaign_tier, member_tier) stays inside >=basic <=gold where
campaign_tier does). A value a fold or another expression decides is taken to be any value of
its enum, since check does not work out which ones those come to, and E043 then says what is
passed is not known. except leaves a table, a clause or
a labelled row (table:row) of the callee out; a hole that leaves in the main rule is this
rule's E101 to fill. The callee's outputs become definitions of this rule, under their
own names or renamed with ->, rounded as the callee declares and then, if one is an output
here, rounded once more as this rule declares. A name this rule declares already — an input, a
sequence or a field of its elements, an open value, or what an item decides — is E041 there, with
where it is declared, and so is a second name for one output. An optional output of the callee that is not a
number hands the absent value on, through with as well; an optional number would be a
definition that rounds a value that may be absent, and is E044.
An output of an enum the callee declares is read in this rule's values with with, from the
callee's value to this rule's — the way the value goes, as the with of an input goes from this
rule's value to the callee's:
apply grade = "grade.rule" meaning:…
score = points
band -> verdict with low -> needs_work, high -> fine
Values spelled the same on both sides map by themselves. An output read without with keeps the
callee's enum, which comes into this rule as <apply>:<enum>; an enum both files import from one
enums file is the same enum on both sides and needs no with.
The callee is expanded into this rule: its tables, clauses, derives and defines appear under
<apply>:<name> (retirement:schedule), which is how a clause of this rule takes precedence over
one of them (overrides retirement:reduction, defining retirement:allowance) and how the trace names a
row ({"table":"retirement:schedule","row":1,"label":"short"}). <apply>:<name> is a name of this
rule like any other: an expression may read it (define strict_rate : number = 2 * base:decay_rate)
and a clause may test it (when strict true and base:above >0). Rows of the callee this rule never
reaches — its ranges are usually narrower — are silent, and the page lists them under the
apply; a whole table none of whose rows is reached is W118. In the generated code, what an apply
brings in is written as the apply's alias, or its name, followed by the identifier the callee's own
code gives the name: apply キャンペーン(campaign) brings 値引き(credit) in as campaign_credit.
Where that is already an identifier of this rule — credit -> campaign_credit, or a second apply
whose name runs into it — a number goes after it (campaign_credit_2), so one rule may be applied
any number of times and an output named anything without a rename.
A callee may apply a rule in turn: it is expanded as its own check expands it, and what it brought
in comes in under both names (a:b:name). A callee that walks a sequence is applied by binding
each of its sequences to one of this rule's — mentors = coaches, the fields of an element mapping
by name, or as with <this rule's field> -> <callee's field>, … says — and its summaries and folds
then walk this rule's sequence. An input of a callee that does not walk may be bound to a field of
the elements this rule walks (kg = weight): the callee is then applied to each element. Applies that go round — a rule that applies itself, or a callee that
applies, directly or through others, the rule that applies it — are E044, said where the ring is
entered and naming every file of it; so is a callee that cannot be read or does not pass check.
The callee's sources come along with its definitions, and the page draws its tables from its own
file, under a heading that says what was applied and how. Generated files name the callee and its
pin in their header, rulec api lists them under applies, and rulec diff old@rev new reads the
callee at the same revision as the rule.
The eight kinds of cell
| written | means |
|---|---|
- |
any value. A blank cell is a syntax error (E008): a blank cannot be told from a forgotten entry |
1200JPY 2000g true 2026-04-01 "abc" |
equality with a literal. A quantity must carry its unit |
basic, gold |
a set. Each element is a literal or a group name; on a column of numbers each is a value of its own (100, 200) |
not: basic |
the complement of a set |
<=2000g |
comparison. <=, >=, <, >. On a column of an ordered enum (§3) it is against a value: >=gold |
>=1000JPY <20000JPY |
an interval — two comparisons side by side mean "and" |
starts_with "CH-" |
a prefix, on a string column. Two or more are separated by a comma |
none |
an optional that is absent. - and not: match it too; a comparison, a literal and a set of values do not |
A string column takes a prefix and nothing else (E110). Strings cannot be enumerated,
so equality and sets have no finite reading here — where the values can be listed, make it
an enum. A prefix can: the prefixes a column's cells name cut the strings into one class per
prefix, plus "under none of them", and that is all §6.2's compression asks of a column. So
completeness and overlap work on a string column exactly as they do on an enum, and the
witness for a gap is a string you can paste. The match is on the bytes: no case folding and no
Unicode normalization, so all eleven targets answer the same. (NumPy declines a table with a
string column, for the reason it declines a walk: the runtime there reads integer columns.)
A cell tests its own column only. There is no expression, no reference to another column, and no function call inside a cell; that restriction is what makes a row a box and the completeness and overlap checks exact.
.. is a syntax error (E010). 0g..1000g does not say whether 1000g is included; a
comparison operator does.
An output cell holds a literal or the name of a value declared above (an input, a derive or
a define). It never holds an expression.
In a column of T? it may hold none, which answers that there is no value. Its other cells
are read in T: 439USDc under money[USDc]?, media under service?. A name of T writes a
value that is there; a name of T? passes on its value or the absence of one. none under a
column that is not optional — an output, a column a table decides, an expected value — is
E103, which says which declaration or heading takes the ?, and so is a name that may have no
value under such a column. A table below that reads an optional column is asked a row for the
absent value wherever a row above can write none or pass on a name that may have none, and
the generated code answers no value the way its language writes one
(generated-code.md).
A word in an output cell is read in the type of its column (§15.208 of DESIGN.md). In the
column of an enum, a value of that enum is the value, even where an output or an input of
another type has the same name (gold, a value of outcome, beside an output gold that is a
number). Where a name of that same enum is spelled the same — an input current : plan
beside the value current of plan — the word reads either way, and the check asks for one of
them to be renamed (E075); the answers of a fold are read the same way. A cell that passes an
input through (| review, approved | false | current |) writes only what that row's own cell
on the input allows, here review and approved, and the checks of the tables below read it
so. The expected value of an example is always a value.
8. result
Assembles the first output when it is not simply looked up from a table.
result is sugar for the first output and reaches no other. Every output — the first one
included — is otherwise taken from the binding of its own name: a define or a table output
column called fee is what the output fee returns. Naming a later output in a result is
E015; a second result line is E016.
Operators, from loosest to tightest: comparison (<= >= < > =), then + -, then * /.
Parentheses group. The functions are min(a, b), max(a, b), allocate(t, c, s), mod(x, n)
and isqrt(x), and the five rounding modes may also be called as functions: down(x, 1JPY),
up(x, 10JPY), half_up(x, 1JPY), half_down(x, 1JPY), half_even(x, 1JPY). There are no
others, and each takes the number of arguments written here: anything else is E118. mod and
isqrt are read as functions only where a call is written, which a name never is, so they are not
reserved: a rule may still call an input mod (compatibility, item 2).
A divisor is a constant (E115), with two exceptions: allocate, below, and the division a
rounding makes of its own argument.
allocate — one line's share of an amount
constraint before <= upto
constraint upto <= base
derive to_before : money[JPY] = allocate(total_off, before, base) range >=0JPY <=1_000_000JPY
derive to_upto : money[JPY] = allocate(total_off, upto, base) range >=0JPY <=1_000_000JPY
result share = to_upto - to_before
allocate(<amount>, <running total>, <whole>) is <amount> × <running total> ÷ <whole>
rounded down to a whole unit. It is the one place a rule may divide by something that is
not a constant, and it is allowed because of what it is for: handing an amount out over a
run of lines in the ratio of their prices.
The subtraction above is the whole point. Each line is given the share up to it minus the
share up to the line before, so the remainders telescope: the parts add up to the amount
exactly, with nothing left over and nothing conjured, and the line that carries the odd yen
is the last one. proofs/ states and proves that (runTotal_exact), which is what makes it
a property of the rule rather than a thing the examples happen to show.
What it asks for, all of it E117: three names with declared ranges, an amount and a
running total that cannot be negative, a positive whole, and a constraint saying the
running total never passes the whole. The last one is not decoration — without it a share
could exceed the amount being handed out, and the interval E108 is proved against would be
the product of two ranges rather than the amount. Chains count: two constraint lines that
meet in the middle say what a single one would.
What the subtraction comes to is read as what it is. Two shares of one amount over one whole,
the one taken away at a running total the constraint lines keep at or below the other's, never
pass each other, since floor(T × C ÷ S) rises with C: the difference is never below zero, and
never above the amount. Above, share comes to 0JPY to 1,000,000JPY rather than −1,000,000JPY to
1,000,000JPY, and an output range written on it, or a language that reads it, is held to that.
One call decides one line. The loop stays with the caller, as everything else here does.
Dividing by a value inside a rounding
define average_price : money[JPY] = half_up(total / max(units, 1), 1JPY)
define win_permille : number = half_even(attack * 1000 / (attack + defense), 1)
Inside one of the five roundings, the outermost operation of the first argument may divide by a
value. The quotient is the exact fraction, and the rounding puts it on its grid the way its mode
says, below zero as above it — the same in every generated language, since none of them is left
to its own division. One division to a rounding, and it has to be the outermost operation:
down(fee * used / days, 1JPY) divides by a value, down(fee * (used / days), 1JPY) is E115.
- Its type is a division's (§3): an amount over an amount of the same unit is a ratio, which
is a
numberor arateas the grid says (half_up(a / b, 1),half_up(a / b, 0.1%)), and anything over anumberor a rate keeps its type. The grid is written in the quotient's unit (E103). - The divisor is never zero (E132). Its interval comes from what it reads — the inputs'
ranges, the derives and defines, what a table above writes — and has to stay above zero or
below it. The message names an input that makes it zero. The ways out: narrow a range, divide
by
max(units, 1), or put a table that picks another divisor where it would be zero and divide by that table's output. A row that writes one of its own table's columns writes only what its cell there lets through, so| >=1 | units |beside| 0 | 1 |makes a column that is never zero (tests/corpus/average_unit_price_split.rule). Aconstraintis not read for this: underconstraint a < b, divide bymax(b - a, 1), which is the same value there. - The two integers the generated code divides — the dividend and the divisor, brought to one step — have to fit int64 (E108).
- What the answer can come to is the quotient's interval (the four corners of the two intervals) pushed out to the grid, and a table can use it as a column.
Any other division by a value is E115, as it was.
mod — the remainder
mod(x, n) is what is left of x once the multiples of n below it are taken out: from 0 up to
n − 1, whatever the sign of x (mod(-1, 3) is 2). n is a positive whole constant (E115),
and x a whole number (E103) — not an amount, a quantity, a rate or a date. What it can come to
is x's range moved down by the multiples of n below it when that range stays inside one
period of n, and 0 to n − 1 otherwise, so a table over turn above needs rows for 0, 1 and 2.
There is no % operator: 7% is a rate.
isqrt — the whole square root
isqrt(x) is the largest whole number whose square does not pass x. x is a number — not an
amount, a quantity, a rate or a date (E103); of a fraction it is the root of the whole part — and
cannot go below zero (E139, with an input that takes it there; max(x, 0) is a way out). What it
can come to is the roots of the ends of x's range. There is no sqrt that
answers a fraction and no pow: a power is a product, floor(n^1.5) is isqrt(n * n * n), and a
root to two places is isqrt(x * 10000), which counts hundredths.
There is no loop and no recursion.
9. examples
| dest | weight | total | member | -> fee |
| Okinawa | 2500g | 40_000JPY | basic | 0JPY |
| Tokyo | 1999g | 12_000JPY | platinum | 400JPY |
examples is an executable specification: rulec check runs every row through the
reference evaluator, and a row that does not hold is E107, reported with the rows that fired.
Every output must have a column (E111). With two or more outputs, writing -> before the
later output columns is optional; rulec fmt folds it to the canonical form.
A rule may have more than one examples section — the published worked examples in one,
the cases a person added in another. Each has a header of its own, and every one runs.
Its cells are held to the types of the columns they sit under, exactly as a table's are: a
heading that names nothing is E012, a value that is not one of the column's is E012, and a
literal the column cannot hold — 1lb under mass[g], a bare number where a unit is
required, an expected 800kg under a money[JPY] output — is E103. An expected value that
cannot be read would otherwise leave nothing to hold the rule to. A heading may write its
output's type after the name, as a table's may, and no other type (E103).
An expected none says the output has no value, which only an optional output can say: under
one that is not optional it is E103, and where the rule answers a value it is E107, as an
expected value the rule does not answer is.
An example naming an enum value is not a row for it: W111 still asks which values no table names.
10. What cannot be written
- Nested objects (
order.shipping.prefecture). Flatten at the boundary and pass the scalar in. - Collections and iteration. A rule is one decision; the order and the repetition belong to the caller.
- Date arithmetic. Comparison and range only.
- State kept between calls. A
machinemakes claims about the calls a caller makes one after another, and the state stays with the caller: the function is passed it and answers the next one (§6.4).
Allowing any of the first three would make the completeness and overlap checks unable to terminate; the fourth would take the replay of past records with it.
11. The canonical form
rulec fmt is the one and only formatter and it is idempotent. It
- aligns the columns of every table by East Asian width — a CJK or fullwidth character, and ℃ and ℉, are two columns — so Japanese lines up in a terminal and in any face that draws Japanese twice as wide as Latin,
- rewrites
→ ・ 、 , ≦ ≧ < > +to their ASCII forms where the lexer reads them as symbols, and fullwidth digits where a word starts. It changes no word the lexer reads: a name keeps every character it was written with (松+竹,第1案), and a symbol whose ASCII form would join its neighbour into another token is set apart by a space (->becomes- >, not the output marker->), - folds the
->of the second and later output columns ofexamples, - indents the lines under
machineby two spaces and lines their values up in one column, - indents the lines under
prerequisitesby two spaces and pads each value, andstart, to the widest, so thatrequiresand the start values line up in one column (§6.5), - leaves the inside of a string and of a comment alone. A comment starts where the lexer starts
one, at the first
#outside a string, so a|,#or,in a string or in a comment is that string's or that comment's and cuts nothing; on a table row, the comment follows the last|the lexer reads.
rulec fmt --check names the files that are not in canonical form and exits 1, which is how
it belongs in CI.
12. Reserved words
These cannot be used as a name (E009). A declaration whose name is one of them would be read by the line-oriented parser as the start of a section and silently dropped, which is why it is caught at parse time. An ASCII alias is not read by that parser, so it may be one; W121 says when a target language takes it badly.
An enum value never starts a line, so it may be any of them but eleven: not none
true false starts_with (the words of a cell), default (the mark after a value in its
enum line), after (which divides a never line), and empty exhausted by with
(the words of a fold arm). A state may be called initial, held or final.
| line heads | rule description import enum group inputs elements outputs derive define constraint table fold count sum sequence policy overrides clause source apply result machine scenario examples |
| modifiers | range round contract_only default |
| cells | not none starts_with true false |
| rounding | up down half_up half_down half_even |
| functions | min max allocate |
| fold arms | over next stop with take_unique take_first keep_max by empty exhausted held |
| count and sum | where of (and over, above) |
| clause body | when then always |
| apply body | except (and with, above) |
| machine body | carry initial final never after once (and over and held, above; always and when, which a clause has too) |
step (inside rate[step 1%]), unique, first, the type words (money mass length
area volume duration temperature sound rate number bool date string), the
money attributes (incl_tax excl_tax), std, enums (the heading of a file of enums, read
on its first line only) and open (the mark after an enum's last value, §3) are part of the
vocabulary but are told apart by position, so they are not reserved as names. A value may be
called open too: the mark is only the word that follows the last value, and open starts a line
of an open value only outside every block (§6.7), so open(o) : bool under inputs is an input.
So are prerequisites — a line head outside every block, and a name inside one, as
prerequisites(p) : number under inputs — and the words under it, start, requires, or,
nothing and ordered (§6.5). So are the four words of §6.6: chances, a line head; random,
read only at the end of an input's line; take_weighted, read only after the -> of an arm; and
draw, read only as draw -> <name> under a fold — draw -> next stays the arm of a verdict
called draw. So are the words of §6.7: combine and product, read on an open line;
contribution (a file's first line), to, into and extend (lines of a contribution file, as
after is); and the pins meaning: and signature: (§6.7, §7).
A test holds this table to src/kw.rs, which is the single place the vocabulary is defined.