Skip to content
docs
Arxo ↗

Numbers and money

For LLMs10 sections

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.

Arxo 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;
KindLiteralWhat it is
Integer7, 100a counter: pages, days, items
Decimal0.9, 3.0, 75 percentan exact decimal fraction; a percent is Decimal 0.75
Rationalno literal: only 7 / 2an exact fraction; produced by dividing two integers
Money150 KZT, 5000 KZTan 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.

Arxo Law
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);
}
Ordercopy_fee
7 pages1050 KZT

The test from this page, byte for byte:

Arxo Law
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.

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.

Arxo Law
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"));
}
Orderresearcher_fee
7 pages, researcher787.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.

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.

Arxo Law
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));
}
FactsAnswer
7 pagesbulk_order: NEITHER
120 pagesbulk_order: TRUE_ONLY
3 days overduelate_fee: 600 KZT
40 days overduelate_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 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.

Arxo Law
rule Instalment strict {
for o: CopyOrder;
for a: Money;
when copy_fee(o, a);
then instalment(o, div_round(a, INSTALMENTS, 2, "HALF_UP"));
}
ExpressionAnswer
1050 KZT / 3.0350 KZT
1000 KZT / 3.0issue INEXACT_DIVISION
div_round(1000 KZT, 3.0, 2, "HALF_UP")333.33 KZT
1050 KZT / 3issue TYPE_ERROR
7 / 2an exact Rational fraction
250 KZT / 1000 KZT0.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.

Arxo Law
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);
}
Factscontribution_share
restoration 4,000, reader contribution 1,0000.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(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:

Factscopy_fee(order1, 1050 KZT)Document
an order for 7 pages and the same order for 9TRUE_ONLYissue 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 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.

Arxo Law
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 factdiscounted_feeDocument
reader_discount(Ivanova, 0.9)945.00 KZT—
reader_discount(Ivanova, 1)NEITHERissue 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.

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:

Output
evaluate 7 * 150 KZT; → 1050 KZT
evaluate 25 percent; → 0.25
evaluate -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.

Comparison does not bind a variable. Remove the copy_order literal from BulkOrder, leaving one guard:

Output
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:

Output
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):

Output
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.