Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

The standard operator table

On this page

Operators on standard types work by one fixed table rather than a library function per job, so price * 20% and distance / 2h read as they would on paper. The compiler knows each row and checks an operator as strictly as a call. Anything not in the table is a compile error naming the two types.

The table extends the rule for time in Arithmetic and comparison, where Instant - Instant gives a Duration. The rules for numbers and for + on text in Expressions and operators still hold.

Left Operator Right Gives Notes
Instant +, - Duration Instant Time
Instant - Instant Duration Time
Duration +, - Duration Duration Time
CalendarDate +, - CalendarPeriod CalendarDate Years, then months, then days; a day past the month's end clamps to its last day
CalendarPeriod +, - CalendarPeriod CalendarPeriod Part by part (P1M + P10D is P1M10D)
CalendarPeriod * Int CalendarPeriod Each part multiplied
A quantity +, -, *, / as tabled as tabled "Physical quantities" below
Money +, -, < ... Money Money, Bool One currency (below)
Money * a number or Percentage Money Rounded by the type's rounding:
Money / Decimal Money A rate (/ 1.20 takes VAT off), rounded by the type's rounding:
Money / Money Decimal One currency; as / gives for Decimal (Logic)
Money / Int refused type.money_split: use money.split or money.allocate
Money +, - Percentage refused Ambiguous: write price * 120% or price + price * 20%
Percentage +, - Percentage Percentage 20% + 5% is 25%
Percentage * a number or Percentage Percentage 50% * 20% is 10%
A number or quantity * Percentage the same kind distance * 10%

Money

  • Money in one currency. Where both types fix the currency (type CakePrice = Money { currency: Currency.gbp }), adding two currencies is a compile error. Where a currency is known only while the program runs, as in a SaaS app billing each customer in their own, +, - and comparisons can fail with the condition currency_mismatch. You map it as any construction rule is mapped: attempt subtotal + line.amount { currency_mismatch: InvoiceCurrencyMismatch }. Inside if (money.currency(a) == money.currency(b)) the checker has the proof and the operator is plain, as a range proves a construction (Conditions as proof).
  • Two amounts follow the named-type rule in Expressions and operators: +, - and / take two amounts of one named type, or one and a plain amount that an operator or a money operation made. A currency that both sides fix differently is reported first, as type.arithmetic_operand or type.ordering_operand.
  • What the checker knows of a result's currency. Operators give a plain Money, and the checker carries what it knows of its currency, as it carries a number's range: a type's currency:, a currency written out in money.from_minor, of or sum, one currency through +, -, * and /, and a condition that compares currencies.
  • == and != never fail. Two amounts in different currencies are unequal, as the contract's == says. Only <, <=, > and >= need one currency.
  • An attempt around a chain of operators covers every operator in it, as in attempt a + b + c { currency_mismatch: ... }, but not an operator inside a call. In an attempt, a block straight after a local's value (line.amount { ... }) is the attempt's mappings, not a value built from a block, since only a type builds one. attempt still starts a statement, so a comparison under attempt is written as a var.
  • The money is written first: price * 2, never 2 * price, which is type.arithmetic_operand, whose next step writes it the other way. %, -price and price * price are refused. Money / Money takes a divisor as / does, a type whose min is at least 1, and gives /'s Decimal.
  • Rounding is the type's. A Money type may declare rounding: (type CakePrice = Money { currency: Currency.gbp rounding: Rounding.half_up }). * and / round to a whole minor unit by the left amount's type's rounding:, or by half_even where it declares none, as constructing into a type with a fixed scale rounds (Values and types). So multiplying needs no call, and there is no money.multiply or money.divide.
  • / by an Int is refused, with a fix to money.split or money.allocate. Dividing money by a count is almost always sharing it out, and rounding division loses a minor unit: £10.00 / 3 is £3.33, and three of those make £9.99. split hands every penny to someone. / by a Decimal is allowed, because that is a rate, not a share: gross / 1.20 takes VAT off. Halving a price is price * 50%.

Percentages

  • Percentage is a type built into the language, held as the exact fraction it is (20% is 0.20). Like Money, it is a representation: a signature names a type built on it (type VatRate = Percentage { min: 0% max: 100% }), whose min: and max: are written with %, while a var may be a plain Percentage. Percentages are ordered.
  • The % literal. 20% is a Percentage: digits and % with nothing between. Its fraction keeps the places written and two more (12.5% is 0.125). It may be negative (-5%), as a quantity literal may.
  • % as remainder (Expressions and operators) is always written with spaces, as the formatter writes every binary operator, so the two never meet in formatted code. A number, % and a name or number all touching (20%3, 20%rate) is syntax.percent_ambiguous, naming both readings. It offers no edit, since either could be meant, and the text is read as the remainder so the file still parses.
  • A bare number where a percentage goes is refused, as an argument, in a comparison, beside + or as a bound, since confusing 0.2 and 20 is what the type exists to end. The error is type.mismatch (or type.incomparable, type.ordering_operand, type.arithmetic_operand or type.setting_value, depending on where the number stands). Its next step names both readings, 25% if 0.25 is a ratio, or 0.25%, with no edit.
  • A ratio becomes a Percentage by construction. Dividing two quantities gives a plain Decimal (lip / total), and Percentage(ratio) makes it a percentage for display: 0.25 gives 25%. Every value becomes a named type this way, so there is no units operation for it.
  • Percentage(ratio) takes a Decimal, a value or a decimal literal, into Percentage or any type built on it, checking that type's rules. A whole number (VatRate(20)) or an Int value is type.constructor_input, naming both readings. A named percentage never becomes the plain Percentage, as for any representation (Values and types).
  • A percentage scales what it follows. "The same kind" in the table, for a number, is a number: Int * Percentage gives a Decimal, as * by a Decimal does, rather than rounding to a whole number. A percentage is written after the amount, number or quantity it scales, as a number is (price * 20%). 20% * price and 15% * length are refused, with the fix written the other way, while 20% * 3 is a percentage, as the table's own row says. A quantity times a percentage rounds half to even to its kind's scale, as it does when multiplied by a number. + and - of two percentages follow the named-type rule.
  • No other row. Nothing divides a percentage (rate / 2 is refused; rate * 50% halves it), adds one to a number or a quantity, or negates a percentage value (-rate). A quantity and a percentage are reported as different kinds. Money + Percentage is type.arithmetic_operand, whose next step writes both readings with the percentage worked out: price * 120% or price + price * 20% (price * 80% or price - price * 20% for -), and for a percentage value, price * (100% + rate).
  • A percentage's text and wire. The canonical text is the fraction as a percentage with no trailing zeros: 20%, 12.5%. text.parse reads a number, one optional space and %, since French and German write 20 %; anything else is malformed. On the wire it is that text, "20%", rather than the fraction a Decimal would send, so no client reads 0.2 as 0.2%. A JSON number is the problem type, text without % is format, and OpenAPI shows a string with its pattern.
  • A percentage in storage is one numeric column holding the fraction, 0.15 for 15%, at whatever places the value has.
  • A percentage for people. text.format is ICU4X's percent formatter over CLDR's pattern: the locale's digits and separators, every place the value keeps as a percentage, and the % where the locale puts it, as in 12.5%, or 20 % in de_de with a no-break space. It takes no options. Display writes 20%.
  • A percentage in a message. In a message's wording, {rate, number, percent} is the style for a field built on Percentage, as {price, number, currency} is money's, and shows it as text.format does. {rate} alone for a percentage, or either style on a field of another kind, is translation.placeholder_style, whose edit writes the right one. Any other style is translation.wording_invalid.

Ratios and fractions

Ratio and Fraction are standard objects. A Ratio's numerator and denominator are Decimals of any value, as an exchange rate's are. A Fraction's are whole numbers, its denominator at least 1.

Each is held as written, so 16:9 and 32:18, or 1/2 and 2/4, are two different values, as any object's fields compare. Neither is ordered: no operation reduces them or compares them by value.

Their canonical texts are 16:9 (each number with the places it was given) and 3/4, with no spaces, and they are read back exactly so; a denominator below 1 is malformed. Each is sent as that text, shown as it whatever the locale, and stored in a column per part: numeric for a Ratio and bigint for a Fraction. type.standard_exists matches an object of a numerator and a denominator, by what they are built on.

No user-defined operators

Your app's types can't declare operators, so a + b always means a row of this table or of Expressions and operators. A reviewer never has to find an overload to know what an operator does, whether it can fail or what it costs.

VAT, offcuts and pass rates

A bakery adds VAT to its cakes, trims its shelves and reports how much of a batch passed, each with a percentage:

example
type CakePrice = Money {    currency: Currency.gbp}type VatRate = Percentage {    min: 0%    max: 100%}type PassRate = Percentage {}type Offcut = Length {}type Share = Decimal {    scale: 4}/// The VAT on a cake's price at `rate`, rounded half to even to a penny.function vat_on(price: CakePrice, rate: VatRate) -> CakePrice {    return CakePrice(price * rate)}/// A cake's price with VAT added: the whole price and the rate on top.function with_vat(price: CakePrice, rate: VatRate) -> CakePrice {    return CakePrice(price * (100% + rate))}/// What is trimmed off a shelf cut from a 2.5262 m board: 15% of it.function offcut() -> Offcut {    var total: Length = 2metre + 45cm + 3inch    var trim: Length = total * 15%    return Offcut(trim)}/// The offcut for the customer, in centimetres.function offcut_label(customer: Locale) -> PresentationText {    return text.format(offcut(), locale: customer, unit: LengthUnit.centimetre)}/// How much of a batch passed, from the share that did.function pass_rate(good_share: Share) -> PassRate {    return PassRate(good_share)}/// A rate for the customer, as their locale writes a percentage.function rate_label(rate: PassRate, customer: Locale) -> PresentationText {    return text.format(rate, locale: customer)}/// Each of four people's share of a cake.function cake_share() -> Fraction {    return Fraction {        numerator: 1        denominator: 4    }}
example
test "a percentage scales money, numbers and quantities" {    var cake: CakePrice = CakePrice(money.from_minor(1250, Currency.gbp))    assert vat_on(cake, 20%) == money.from_minor(250, Currency.gbp)    assert with_vat(cake, 20%) == money.from_minor(1500, Currency.gbp)    var penny_cake: CakePrice = CakePrice(money.from_minor(100, Currency.gbp))    assert vat_on(penny_cake, 12.5%) == money.from_minor(12, Currency.gbp)    assert 50% * 20% == 10%    assert 20% + 5% == 25%}test "the offcut is exact in metres and shown in centimetres" {    assert offcut() == 0.37893metre    assert offcut_label(Locale.en_gb) == "37.893 cm"    assert offcut_label(Locale.de_de) == "37,893 cm"}test "a ratio becomes a percentage, and fractions are written as people write them" {    assert pass_rate(0.25) == 25%    assert rate_label(pass_rate(0.175), Locale.en_gb) == "17.5%"    assert text.from(pass_rate(0.175)) == "17.5%"    assert text.from(cake_share()) == "1/4"    assert cake_share().denominator == 4}

A voucher keeps its discount in one numeric column, as the fraction it is. Here Voucher is an entity, a kind of stored record:

example
use bakery/types { DiscountRate }/// A voucher a customer brings to the counter.entity Voucher {    id: Uuid    reduction: DiscountRate  // one `numeric` column: 0.15 for 15%}
example
use bakery/entities { Voucher }test "a voucher keeps its discount as it was issued" {    var voucher_made: Voucher = issue_voucher(15%)    var voucher_on_file: Voucher = attempt Voucher.get(voucher_made.id)    assert voucher_on_file.reduction == 15%    assert text.from(voucher_on_file.reduction) == "15%"}

Markdown for agents: /docs/the-standard-operator-table.md.Built from Jadpo main at a707efb.