Numbers and money
Until now the archive norms answered “yes”, “no”, and “not established”. But copy fees, overdue fines, and instalments are numbers, and numbers have their own rules: a page counter does not add up with a share, money does not divide by a whole number, and rounding is never implied. In the corpus, comparison stands in 5,518 rules across 293 packages, arithmetic in 3,944 across 210, money and percent literals in 228 across 28 (measured 05.09.2026).
The tariffs on this page are for teaching: 150 tenge a page, 200 a day for overdue items, and a 5,000 cap — example numbers, not enacted law.
language "law.core" version "0.2";package tutorial.archive version "0.13.0";namespace "urn:law:tutorial:archive";
entity Person;entity CopyOrder;
relation copy_order(o: CopyOrder, p: Person, pages: Integer) kind empirical { key(o); }relation researcher(p: Person) kind institutional;relation days_overdue(p: Person, days: Integer) kind empirical { key(p); }relation restoration_cost(p: Person, cost: Money) kind empirical { key(p); }relation reader_contribution(p: Person, paid: Money) kind empirical { key(p); }relation reader_discount(p: Person, rate: Decimal) kind empirical { key(p); }
relation copy_fee(o: CopyOrder, amount: Money) kind institutional { key(o); }relation researcher_fee(o: CopyOrder, amount: Money) kind institutional { key(o); }relation bulk_order(o: CopyOrder) kind institutional;relation late_fee(p: Person, amount: Money) kind institutional { key(p); }relation instalment(o: CopyOrder, amount: Money) kind institutional { key(o); }relation contribution_share(p: Person, share: Decimal) kind institutional { key(p); }relation discounted_fee(o: CopyOrder, amount: Money) kind institutional { key(o); }
const PAGE_RATE: Money = 150 KZT;const RESEARCHER_RATE: Decimal = 75 percent;const BULK_PAGES: Integer = 100;const DAILY_LATE_FEE: Money = 200 KZT;const LATE_FEE_CAP: Money = 5000 KZT;const INSTALMENTS: Decimal = 3.0;Four kinds of numbers
Section titled “Four kinds of numbers”| Kind | Literal | What it is |
|---|---|---|
Integer | 7, 100 | a counter: pages, days, items |
Decimal | 0.9, 3.0, 75 percent | an exact decimal fraction; a percent is Decimal 0.75 |
Rational | no literal: only 7 / 2 | an exact fraction; produced by dividing two integers |
Money | 150 KZT, 5000 KZT | an amount with a currency; the space is mandatory |
The core does not convert kinds implicitly. Integer and Decimal
differ: 7 does not equal 7.0, and when a whole n is compared with
100.5, the compiler refuses. Kinds meet only in multiplication: whole
and decimal numbers act as dimensionless scalars and multiply anything.
Tariff: a counter times money
Section titled “Tariff: a counter times money”rule CopyFee strict { for o: CopyOrder; for p: Person; for n: Integer; when copy_order(o, p, n); then copy_fee(o, n * PAGE_RATE);}| Order | copy_fee |
|---|---|
| 7 pages | 1050 KZT |
The test from this page, byte for byte:
test "семь страниц по 150 тенге — 1050" { given { context { decision_time @2026-04-20T09:00:00+05:00; knowledge_time @2026-04-20T09:00:00+05:00; legal_time @2026-04-20; timezone "Asia/Almaty"; } assert copy_order(entity_ref("urn:tutorial:order1"), entity_ref("urn:tutorial:ivanova"), 7) { id "assert-order"; origin case_input; } } evaluate truth(copy_fee(entity_ref("urn:tutorial:order1"), 1050 KZT)); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED;}The test name reads: “Seven pages at 150 tenge — 1050.”
The rule multiplies the page count by the tariff: a whole number scales
money and does not change the kind. A truth query asks whether exactly
this value is established: ask copy_fee(order1, 1000 KZT) and you get
NEITHER. “How much” can also be asked directly, with an expression
query, covered below.
Rate and rounding
Section titled “Rate and rounding”Researchers pay three quarters of the tariff. 75 percent is the
Decimal 0.75, and money times a decimal fraction is money of the same
currency. The result must be rounded explicitly: round takes three
arguments, none implied.
rule ResearcherFee strict { for o: CopyOrder; for p: Person; for n: Integer; for a: Money; when copy_order(o, p, n) and researcher(p) and copy_fee(o, a); then researcher_fee(o, round(a * RESEARCHER_RATE, 2, "HALF_UP"));}| Order | researcher_fee |
|---|---|
| 7 pages, researcher | 787.50 KZT |
The core never rounds by itself — neither money nor shares. If a norm’s result may come out inexact, it must name the precision and the mode; otherwise the engine will not answer “about 787” but refuse.
Threshold and cap
Section titled “Threshold and cap”Comparison is a body guard, and both its operands share one kind: a whole
page count against a whole constant. The fine cap is min over two sums
of one currency.
rule BulkOrder strict { for o: CopyOrder; for p: Person; for n: Integer; when copy_order(o, p, n) and n > BULK_PAGES; then bulk_order(o);}
rule LateFee strict { for p: Person; for d: Integer; when days_overdue(p, d); then late_fee(p, min(d * DAILY_LATE_FEE, LATE_FEE_CAP));}| Facts | Answer |
|---|---|
| 7 pages | bulk_order: NEITHER |
| 120 pages | bulk_order: TRUE_ONLY |
| 3 days overdue | late_fee: 600 KZT |
| 40 days overdue | late_fee: 5000 KZT |
Below the threshold the rule stays silent and answers NEITHER, not
“false”. Comparison does not bind a variable but checks it; candidates
are enumerated by the positive copy_order literal, and without it the
compiler refuses.
Division
Section titled “Division”Division is arranged most strictly: the specification lists the pairs
that divide, and the list is closed. Money divides by Decimal
and only by it; the engine does not silently round an inexact quotient
but rejects it. One term divides under a policy — div_round: it rounds
the exact quotient.
rule Instalment strict { for o: CopyOrder; for a: Money; when copy_fee(o, a); then instalment(o, div_round(a, INSTALMENTS, 2, "HALF_UP"));}| Expression | Answer |
|---|---|
1050 KZT / 3.0 | 350 KZT |
1000 KZT / 3.0 | issue INEXACT_DIVISION |
div_round(1000 KZT, 3.0, 2, "HALF_UP") | 333.33 KZT |
1050 KZT / 3 | issue TYPE_ERROR |
7 / 2 | an exact Rational fraction |
250 KZT / 1000 KZT | 0.25 — a share, not money |
The fourth row catches most often: a money divisor is written 3.0,
not 3. A whole number does not act as a scalar here, because the
scalar rule applies only in multiplication. The sixth row divides
two sums of one currency: the quotient is dimensionless and suits the
proportions set by the act itself.
rule ContributionShare strict { for p: Person; for c: Money; for paid: Money; when restoration_cost(p, c) and reader_contribution(p, paid); then contribution_share(p, paid / c);}| Facts | contribution_share |
|---|---|
| restoration 4,000, reader contribution 1,000 | 0.25 |
There is a catch: statics does not know which kind a Money / Money
quotient has, and treats it as Unknown, because the kind depends on
the value — a decimal fraction with a 10-smooth denominator, else
Rational. check will not catch an error around such an
expression; it will arrive at runtime.
Key: one value per order
Section titled “Key: one value per order”key(o) on copy_order says: the page count is a function of the order.
Two different counts on one order are not two facts but a contradiction:
| Facts | copy_fee(order1, 1050 KZT) | Document |
|---|---|---|
| an order for 7 pages and the same order for 9 | TRUE_ONLY | issue KEY_CONFLICT |
A key deletes and picks nothing: both facts remain, the tariff
is derived from each, and the discrepancy gets a name. Without a key the
predicate would answer one question with two sums and stay silent about
it. That is why computed heads also carry keys — copy_fee, late_fee,
instalment: this is the corpus-wide convention for all
relations whose value a calculation derives.
A fraction that became a whole
Section titled “A fraction that became a whole”A reader discount is a Decimal. Record it in a test as 0.9 and all
agrees; record the full rate as 1 and you get a runtime refusal with
clean parse, check, and lower.
rule DiscountedFee strict { for o: CopyOrder; for p: Person; for n: Integer; for a: Money; for r: Decimal; when copy_order(o, p, n) and copy_fee(o, a) and reader_discount(p, r); then discounted_fee(o, round(a * r, 2, "HALF_UP"));}| Discount fact | discounted_fee | Document |
|---|---|---|
reader_discount(Ivanova, 0.9) | 945.00 KZT | — |
reader_discount(Ivanova, 1) | NEITHER | issue TYPE_ERROR |
The literal 1 reads as Integer, the parameter is declared Decimal,
and the core does not mix them. The trap is insidious because the CLIR canon
strips the fractional tail off Decimal: a 1.0 value from the
case prints as 1, and text reproduced from the document verbatim changes
kind. Write 1.0; with money the currency suffix keeps the kind, and
that trouble does not exist.
Asking a number directly
Section titled “Asking a number directly”An expression’s value is asked without a rule — by an expression query
with an expect value expectation. Such tests live in
the page file; here — in text, together with the runtime refusals, which
are checked by observing issue:
evaluate 7 * 150 KZT; → 1050 KZTevaluate 25 percent; → 0.25evaluate -7 mod 3; → 2 (евклидов остаток)evaluate 1050 KZT + 0.5; → issue(TYPE_ERROR)evaluate 1 + (1 / 2); → issue(TYPE_ERROR): счётчик и доля не складываютсяevaluate 1000 KZT + 5 USD; → issue(CURRENCY_MISMATCH)The parenthetical notes read: “(Euclidean remainder)” and “a counter and a share do not add up”.
Runtime refuses where the compiler stays silent: check lets
1000 KZT + 5 USD through, because currency lives in the value. Guard
such places with a test.
Compiler refusals
Section titled “Compiler refusals”Comparison does not bind a variable. Remove the copy_order literal from
BulkOrder, leaving one guard:
error LDC-E4101: BulkOrder: head использует несвязанные переменные [o] —правило не range-restricted (§190: связывание только позитивнымestablished-конъюнктом конечного источника)The diagnostic reads: “‘BulkOrder’: head uses unbound variables [o] — the rule is not range-restricted: a variable must be bound by a positive established conjunct of a finite source”.
A threshold of the wrong kind — n > 100.5 with a whole n:
error LDC-E2108: «>»: несравнимые виды Integer и Decimal — §57/§48 требуютсовпадения видов, неявных конверсий нетThe diagnostic reads: “‘>’: incomparable kinds Integer and Decimal — kinds must match, no implicit conversions”.
Rounding without a policy — round(a * RESEARCHER_RATE):
error LDC-E2116: round: объявленная арность стандартного терма — 3;передано 1. Арность задана §257 и разделом семантики самого терма(§50/§58/§59/§84/§86); догадки по числу аргументов нетThe diagnostic reads: “‘round’: the declared arity of the standard term is 3; 1 passed. Arity is set by the term’s own semantic section; no guessing by argument count”.
All three refusals belong to the language: the compiler rejects such
variants. Note where statics stops: the compiler does not trace the kind of the
PAGE_RATE constant in an expression, and PAGE_RATE / n passes check
to fall with TYPE_ERROR at runtime. A literal and a variable the
compiler catches earlier; a constant is guarded by a test.
One number from one fact is half of calculations. The other half is a sum over many: a reader’s orders for a month, the count of unpaid ones, the average cheque. Those are aggregates, with their own trap: two identical sums that count as one, and an empty set that does not equal zero — the next tutorial.
The exercise for this page is /tutorials/exercise-numbers/.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.