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

Numbers

Numbers have two namespaces: integer operations take values built on Int, and decimal operations take values built on Decimal. The operators / and % are in Expressions and operators; these operations do what an operator can't.

Operation Gives Fails SQL form
integer.divide(dividend, divisor, rounding:) Int (Expressions and operators) every mode: div and mod with a CASE
integer.abs(value) Int, at least 0 overflow on the smallest Int is a defect abs(x)
integer.min(a, b), integer.max(a, b) the smaller or larger, keeping their named type (both the same, as + needs) LEAST(a, b), GREATEST(a, b)
integer.from_decimal(value, rounding:) Int, value rounded to a whole number overflow is a defect round(x)::bigint for half_up; a CASE on the dropped digits for the others
decimal.divide(dividend, divisor, rounding:) Decimal (Expressions and operators) every mode: as integer.divide, on the values scaled to the dividend's places
decimal.abs(value) Decimal, at least 0, the same scale abs(x)
decimal.min(a, b), decimal.max(a, b) as for integer LEAST(a, b), GREATEST(a, b)
decimal.round(value, scale:, rounding:) Decimal with at most scale places; scale: a whole number written out, at least 0 x when it has at most s places, where round would add zeros; otherwise round(x, s) for half_up, trunc(x, s) for down and a CASE on the dropped digits for the others
decimal.sqrt(value, scale:, rounding:) Decimal; value never negative none
decimal.power(value, exponent) Decimal, exact; exponent a whole number, never negative overflow is a defect none
decimal.exp(value, scale:, rounding:), decimal.ln(value, scale:, rounding:) Decimal; ln's value above 0 ln: out_of_range none
decimal.sin(radians, scale:, rounding:), decimal.cos(radians, scale:, rounding:) Decimal none
decimal.atan2(y, x, scale:, rounding:) Decimal, radians none
decimal.pi(scale:, rounding:) Decimal none
  • Rounding to a type's scale needs no call. Building a value of a type with a fixed scale rounds it half to even (Conditions as proof), so decimal.round is for another rounding mode, or for fewer places than the type keeps.
  • The rounding modes. Rounding has four: half_even takes a half to the even digit, half_up takes a half away from zero, up rounds away from zero, and down rounds toward zero, its SQL form trunc. There is no floor, ceiling or half_down.
  • An Int becomes a Decimal by multiplying, as in count * 1.00, since * may mix the two (Expressions and operators). There is no decimal.from_integer.
  • Parsing and formatting work as for every standard type, through the contract (Every standard type meets one contract): text.parse(raw, as: Quantity) and text.format(n, locale:). Parsing takes only the form a number has on the wire: no grouping, no decimal comma, no exponent. A spreadsheet's 1.234,50 is a job for a cell decoder, a package's decode:.
  • Formatting for people uses CLDR's decimal format for the locale, with its digits, grouping and decimal separator, and shows every place the value keeps, as a translation's {n} writes it (Using a message): 1234.50 is 1,234.50 in en_gb and 1.234,50 in de_de. It takes no options, so zone: or format: is type.temporal_option_unused. A Bool has no format for people.
  • min and max take two values of one type, as + does, and give one of them as it is. Two named types that aren't one type after widening are type.mismatch. Where one widens to the other, the result has the wider type (When one type can stand in for another). A literal takes the other value's type and is checked against all its rules, since the result may be the literal. A plain number, which carries no type of its own, makes the result the plain representation, and so do two literals. Of two equal values, decimal.min and decimal.max give the first, so decimal.min(1.50, 1.5) keeps 1.50's places.
  • scale: is a whole number from 0 to 18, the range a type's scale takes (Representation types), written out at the call. A value there is type.mismatch, as it is for text.parse's as:. A literal below 0 is type.argument_negative, and any other literal that isn't a whole number up to 18 is type.mismatch. decimal.round gives at most scale places, so it keeps a value that has no more places than scale: as it is. The maths operations give exactly scale places: decimal.sqrt(4.00, scale: 2, ...) is 2.00.
  • What the checker knows about results. These facts let it prove a result fits a type without a check (Conditions as proof). abs is at least 0, as the table says. min and max carry their arguments' bounds: the smaller of two values is never above either's top. integer.from_decimal and decimal.round keep the sign of a value bounded at zero, since no mode rounds across zero. sqrt and exp are at least 0, and pi is at least 3. The checker knows nothing finer: a rounded value's other bounds, and anything built from sin or cos, which carry no bounds, need a check.
  • Arguments that must be proved. sqrt's value and power's exponent are never negative, proved as a count is (never_negative), or the call is type.argument_negative. ln fails out_of_range, and that is settled by proof, as a construction's rule is. A literal above 0, or a value whose type has a min above 0, can't fail, so its call takes no attempt; one written with an attempt is failure.attempt_infallible. Any other value may fail, and the call maps out_of_range or invalid:; its failure is invalid_value.
  • Edges of the maths. atan2(0, 0) is 0, where no angle is meant, rather than a failure. atan2(0, x) for x below 0 is π, as Decimal has no negative zero to make it -π. power(value, 0) is 1 at value's scale, 0^0 included, and every other power keeps the places repeated * keeps: 1.50 squared is 2.25. A power past 38 digits or 38 places, or an exp past 38 digits, is overflow, a defect. An exp so far below 0 that it is under half of 10^-18 gives 0, or, with up, which rounds away from zero, one unit of the last place.
  • The same digits on every target. The maths works on Decimal, as there is no Float (Representation types). Jadpo's own decimal code rounds each result correctly to scale with the named rounding, so it is the same wherever the program runs.
  • In SQL, PostgreSQL's round ties away from zero, which is half_up. The other modes are spelled out with a CASE, never left to the database.

A bakery's counts, weights and prices, rounded and compared:

example
type Portions = Int {    min: 0}type TrayCount = Int {    min: 1    max: 12}type LoafCount = Int {}type FlourWeight = Mass {    min: 0g}type Hydration = Decimal {    scale: 3    min: 0.000}type LoafPrice = Decimal {    scale: 2    min: 0.00}/// How far a counted batch is from its recipe, either way. `abs` is never/// below 0, so building `Portions` from it needs no arm.function drift(in_batch: Portions, recipe: Portions) -> Portions {    return Portions(integer.abs(in_batch - recipe))}/// The trays to bake: what was ordered, but never more than the oven/// holds. `min` gives one of its two values as it is, still a `TrayCount`.function trays_to_bake(requested: TrayCount, oven: TrayCount) -> TrayCount {    return integer.min(requested, oven)}/// Whole loaves a sack of flour makes at 450g each: part of a loaf isn't/// one, so the count rounds down, toward zero. Dividing a weight by a weight/// gives a plain `Decimal`.function loaves_from(flour: FlourWeight) -> LoafCount {    return LoafCount(integer.from_decimal(flour / 450g, rounding: Rounding.down))}/// A loaf's price at a fifth off, never below the lowest price.function sale_price(price: LoafPrice, lowest: LoafPrice) -> LoafPrice {    return decimal.max(LoafPrice(price * 0.80), lowest)}/// A dough's hydration to one place, for its label.function label_hydration(hydration: Hydration) -> Hydration {    return Hydration(decimal.round(hydration, scale: 1, rounding: Rounding.half_up))}/// The price as a customer reads it, in their own locale.function price_label(price: LoafPrice, customer: Locale) -> PresentationText {    return text.format(price, locale: customer)}

A literal passed to min or max takes the other value's type and is checked against it, so integer.min(requested, 20) is an error, because TrayCount stops at 12. A plain number, such as requested + 1, makes the result a plain Int. The tests check these functions, and each rounding mode to one place:

example
test "number operations give what the page says" {    assert drift(980, 1000) == 20    assert drift(1020, 1000) == 20    assert trays_to_bake(10, 8) == 8    assert trays_to_bake(3, 8) == 3    assert loaves_from(10kg) == 22    assert loaves_from(449g) == 0    assert sale_price(4.20, 3.50) == 3.50    assert sale_price(5.00, 3.50) == 4.00    assert label_hydration(1.250) == 1.3    assert label_hydration(1.249) == 1.2    assert integer.max(3, 8) == 8    assert decimal.abs(-2.50) == 2.50    assert decimal.min(4.20, 3.80) == 3.80    assert price_label(1234.50, Locale.en_gb) == "1,234.50"    assert price_label(1234.50, Locale.de_de) == "1.234,50"}test "every rounding mode, to one place" {    assert decimal.round(2.25, scale: 1, rounding: Rounding.half_even) == 2.2    assert decimal.round(2.35, scale: 1, rounding: Rounding.half_even) == 2.4    assert decimal.round(2.25, scale: 1, rounding: Rounding.half_up) == 2.3    assert decimal.round(-2.25, scale: 1, rounding: Rounding.half_up) == -2.3    assert decimal.round(2.21, scale: 1, rounding: Rounding.up) == 2.3    assert decimal.round(-2.21, scale: 1, rounding: Rounding.up) == -2.3    assert decimal.round(2.29, scale: 1, rounding: Rounding.down) == 2.2    assert decimal.round(-2.29, scale: 1, rounding: Rounding.down) == -2.2    assert integer.from_decimal(2.5, rounding: Rounding.half_even) == 2    assert integer.from_decimal(-2.5, rounding: Rounding.half_up) == -3    assert integer.divide(7, 2, rounding: Rounding.down) == 3}

A scale: that isn't a whole number from 0 to 18 written out at the call, as a type's scale is, is type.mismatch, and one below 0 is type.argument_negative (Errors).

The maths operations, from sqrt to pi, on the bakery's tins, starters and deliveries. Each result is the true value rounded to scale places by the mode named, so the same call gives the same digits on every target:

example
type Centimetres = Decimal {    scale: 2    min: 0.00}type SquareCentimetres = Decimal {    scale: 2    min: 0.00}type Tier = Int {    min: 0    max: 6}type RecipeScale = Decimal {    scale: 4}type Growth = Decimal {    scale: 4    min: 0.0000}type Rise = Decimal {    scale: 6}type ProvingTime = Decimal {    scale: 1    min: 0.0}type Metres = Decimal {    scale: 1}type Course = Decimal {    scale: 6}type NeverRose = Failure {    kind: FailureKind.invalid_value}/// The surface of a round tin, π r².function tin_area(radius: Centimetres) -> SquareCentimetres {    return SquareCentimetres(decimal.pi(scale: 6, rounding: Rounding.half_even) * radius * radius)}/// The side of a square tin the same size, rounded down so the cake/// fits. A size is never negative, so its square root needs no check.function square_side(surface: SquareCentimetres) -> Centimetres {    return Centimetres(decimal.sqrt(surface, scale: 2, rounding: Rounding.down))}/// How much more each tier below the surface needs, at half again a tier:/// exact, as `1.5 * 1.5 * 1.5` is.function tier_scale(tier: Tier) -> RecipeScale {    return RecipeScale(decimal.power(1.5, tier))}/// How much a starter grows over `proving`, rising all the while by/// `rise` an hour. `e` to any power is never below 0, so `Growth` needs no arm.function risen(rise: Rise, proving: ProvingTime) -> Growth {    return Growth(decimal.exp(rise * proving, scale: 4, rounding: Rounding.half_even))}/// How fast a starter rose an hour, from how much bigger it had `grown`/// in a day. Growth of 0 has no logarithm: a starter that shrank to/// nothing never rose.function hourly_rise(grown: Growth) fails NeverRose -> Rise {    var daily: Decimal = attempt decimal.ln(grown, scale: 6, rounding: Rounding.half_even) {        out_of_range: NeverRose    }    return Rise(daily / 24.0)}/// The direction of a customer at `northing` and `easting` from the/// bakery, in radians clockwise from north.function course(northing: Metres, easting: Metres) -> Course {    return Course(decimal.atan2(easting, northing, scale: 6, rounding: Rounding.half_even))}/// Where a van is, east and north of the bakery, `driven` metres on/// `course`.function easting_after(driven: Metres, course: Course) -> Metres {    return Metres(driven * decimal.sin(course, scale: 6, rounding: Rounding.half_even))}function northing_after(driven: Metres, course: Course) -> Metres {    return Metres(driven * decimal.cos(course, scale: 6, rounding: Rounding.half_even))}

sqrt's value and power's exponent must be proved never negative, as a count is, or the call is type.argument_negative. ln fails with out_of_range unless its value is proved above 0, by a literal above 0 or a type whose min is above 0. Growth allows 0, so hourly_rise maps the failure. A result past 38 digits, such as decimal.exp(100.0, ...), is a defect, as overflow is for *. The tests check these functions, and each mode on the true value:

example
test "the maths gives what the page says" {    assert tin_area(10.00) == 314.16    assert square_side(314.16) == 17.72    assert tier_scale(3) == 3.375    assert tier_scale(0) == 1.0    assert risen(0.100000, 6.0) == 1.8221    var rise: Rise = attempt hourly_rise(2.0000)    assert rise == 0.028881    assert hourly_rise(0.0000) fails NeverRose    assert course(100.0, 100.0) == 0.785398    assert course(-100.0, 0.0) == 3.141593    assert easting_after(1000.0, 0.523599) == 500.0    assert northing_after(1000.0, 0.523599) == 866.0}test "each mode rounds the true value" {    assert decimal.sqrt(2.0, scale: 3, rounding: Rounding.down) == 1.414    assert decimal.sqrt(2.0, scale: 3, rounding: Rounding.up) == 1.415    assert decimal.pi(scale: 4, rounding: Rounding.half_even) == 3.1416    assert decimal.pi(scale: 4, rounding: Rounding.down) == 3.1415    assert decimal.exp(1.0, scale: 5, rounding: Rounding.half_up) == 2.71828    assert decimal.ln(10.0, scale: 5, rounding: Rounding.up) == 2.30259}

Markdown for agents: /docs/numbers.md.Built from Jadpo main at a707efb.