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

Exchange rates

An ExchangeRate says how much one currency is worth in another at a moment, so you can convert an amount. It is a standard object with four fields: from and to, each a Currency; rate, a Decimal; and at, the Instant it was quoted at. You build one with its object literal and read its fields. One unit of from is rate of to, as a currency pair is quoted: GBP/EUR 1.17 means £1 is €1.17.

A rate can be any Decimal. It isn't checked to be above 0, because no rule a field can carry says "greater than" and a rate is a provider's data. A rate between a currency and itself is allowed.

  • Text. Its canonical text is the pair, the rate and the instant, GBP/EUR 1.17 2026-10-18T09:00:00Z: the ISO 4217 codes, base first, joined by / as a currency pair is written; the rate with the places it was given; and the instant in UTC. Codes are read ignoring case. No standard writes a rate with its time, so this is the narrowest text that round-trips. text.format shows that same text, whatever the locale, as for a Uuid.
  • JSON is an object of the four fields, each in its own canonical text.
  • Storage is a column per field, <column>_from and <column>_to (each a char(3) code), <column>_rate (numeric) and <column>_at (timestamptz(3)). A rate type's from: and to: add check constraints holding those columns to its currencies (<table>_<column>_from_check and <table>_<column>_to_check), as a Money type's currency: holds its currency column.
  • Shape. type.standard_exists, the error for a declared type with a standard type's shape, matches an object of from, to, rate and at, by their common names.

A shop that takes orders from abroad is given a rate each morning, and prices a cake in the customer's currency. money.convert takes a rate only from the amount's currency. Where the checker knows both currencies, a rate the wrong way round doesn't compile. Otherwise the call fails currency_mismatch, and a condition on rate.from proves that away:

example
type CakePrice = Money {    currency: Currency.gbp    min: 0}type Payment = Money {}type WrongRate = Failure {    kind: FailureKind.invalid_value}/// A cake's price in the rate's other currency, at a rate the shop was/// given; a rate from any other currency fails.function priced_abroad(cake: CakePrice, rate: ExchangeRate) fails WrongRate -> Payment {    var charged: Money = attempt money.convert(cake, rate) {        currency_mismatch: WrongRate  // a rate from euros, the wrong way round    }    return Payment(charged)}/// The same, with the rate's currency checked first, so it needs no/// `attempt`.function priced_if_from_pounds(cake: CakePrice, rate: ExchangeRate) -> Payment? {    if (rate.from == Currency.gbp) {        return Payment(money.convert(cake, rate))    }    return none}
example
test "a rate converts from its own currency only" {    var pounds_to_euros: ExchangeRate = ExchangeRate {        from: Currency.gbp        to: Currency.eur        rate: 1.17        at: "2026-10-18T09:00:00Z"    }    var euros_to_pounds: ExchangeRate = ExchangeRate {        from: Currency.eur        to: Currency.gbp        rate: 0.85        at: "2026-10-18T09:00:00Z"    }    var cake: CakePrice = CakePrice(money.from_minor(1050, Currency.gbp))    var in_euros: Payment = attempt priced_abroad(cake, pounds_to_euros)    assert in_euros == money.from_minor(1228, Currency.eur)  // €12.285, half to even    assert priced_abroad(cake, euros_to_pounds) fails WrongRate    assert priced_if_from_pounds(cake, pounds_to_euros) == money.from_minor(1228, Currency.eur)    assert priced_if_from_pounds(cake, euros_to_pounds) == none    assert text.from(pounds_to_euros) == "GBP/EUR 1.17 2026-10-18T09:00:00Z"}

The checker knows an amount's currency from its type or from how it was made. It knows a rate's from and to from its object literal written out, or from a condition such as rate.from == Currency.gbp or rate.from == money.currency(price); such a condition also settles currency_mismatch, with no attempt. Where it knows both, a rate from another currency is a type error, type.mismatch, not a rule of its own. Elsewhere the call fails currency_mismatch under attempt. The result is in the rate's to currency where that is known, so building a type that fixes that currency needs no arm.

Here the condition says the rate is from euros, and the cake is in pounds:

example
type CakePrice = Money {    currency: Currency.gbp}type Payment = Money {}/// A cake's price at a rate quoted from euros.function priced_abroad(cake: CakePrice, rate: ExchangeRate) -> Payment? {    if (rate.from == Currency.eur) {        return Payment(money.convert(cake, rate))    }    return none}
text
error: `rate` converts from EUR, and `cake` is in GBP --> main.jadpo:10:44   |10 |         return Payment(money.convert(cake, rate))   |                                            ^^^^ a rate from EUR  reason: `money.convert` applies a rate to an amount in the rate's `from` currency. `cake` is in GBP and `rate` converts from EUR, as their types, how they were made or a condition around the call fix, so the rate is the wrong way round or for another currency.  next: Pass a rate from GBP, or an amount in EUR (agent)  rule: type.mismatch  help: https://jadpo.dev/docs/errors/type.mismatchcheck failed: 1 error, 1 file

A type built on ExchangeRate can fix its currencies with from: and to:, as a Money type fixes its currency with currency:. A signature then carries them. The right rate needs no attempt, the wrong one doesn't compile wherever it is passed, and a rate the app was given, or reads from JSON or text, is checked against them where it becomes one:

  • from: and to: are a rate type's settings, each a Currency variant written out: type PoundsToEuros = ExchangeRate { from: Currency.gbp, to: Currency.eur }. Either can be used alone, as in type FromPounds = ExchangeRate { from: Currency.gbp }. On any other type they are type.constraint_base, and so is currency: on a rate type.
  • The checker reads the rate's type first, before a rate written out or a condition on rate.from. An amount whose type or making fixes the rate type's from needs no attempt. One fixed to another currency is type.mismatch wherever the rate was passed from, its label naming both currencies (a rate from EUR to GBP). The result is in the type's to. An amount whose currency only the running program knows still takes currency_mismatch under attempt.
  • A rate type is built from a value of ExchangeRate, which is its representation, as for any type built on an object (Representation types): PoundsToEuros(rate), from an ExchangeRate or from another type built on one. The value is checked against the rules the type adds. These fail as from and to, rule keys named for their settings, and are settled with no arm by the value's own type, a rate written out, or a condition on rate.from and rate.to. An arm for a currency the type leaves open is failure.unknown_mapping. Braces don't build a rate type: PoundsToEuros { ... } is type.not_record, so a rate written out is PoundsToEuros(ExchangeRate { ... }).
  • A rate read at the boundary is checked against its type. json.read and a request body check the whole rate once its fields are read, failing mismatch with the rule from or to in its DecodeProblem. text.parse fails from or to, beside malformed. OpenAPI shows each fixed currency as an enum of its one code, as it does for a Money type's currency. The API record writes the type as ExchangeRate { from: GBP, to: EUR }, so changing either shows as an API change.
example
type CakePrice = Money {    currency: Currency.gbp    min: 0}type EuroPrice = Money {    currency: Currency.eur}/// The rate a shop is given each morning for pricing in euros.type PoundsToEuros = ExchangeRate {    from: Currency.gbp  // fixed: a rate from euros doesn't compile    to: Currency.eur}type QuoteJson = Text {}type WrongRate = Failure {    kind: FailureKind.invalid_value}type NotARate = Failure {    kind: FailureKind.invalid_value}/// A cake's price in euros: the rate's type fixes both currencies, so/// the call needs no `attempt` and the result is in euros.function priced_in_euros(cake: CakePrice, rate: PoundsToEuros) -> EuroPrice {    return EuroPrice(money.convert(cake, rate))}/// The morning's rate, checked once, where it arrives.function morning_rate(todays_quote: ExchangeRate) fails WrongRate -> PoundsToEuros {    return attempt PoundsToEuros(todays_quote) {        from: WrongRate  // a rate from euros, the wrong way round        to: WrongRate  // a rate to dollars    }}/// A rate a provider sent as JSON.function sent_rate(sent: QuoteJson) fails NotARate -> PoundsToEuros {    return attempt json.read(sent, as: PoundsToEuros) {        malformed: NotARate        mismatch: NotARate  // other currencies    }}
example
test "a rate type converts from its own currency only" {    var todays_quote: ExchangeRate = ExchangeRate {        from: Currency.gbp        to: Currency.eur        rate: 1.17        at: "2026-10-18T09:00:00Z"    }    var backwards_quote: ExchangeRate = ExchangeRate {        from: Currency.eur        to: Currency.gbp        rate: 0.85        at: "2026-10-18T09:00:00Z"    }    var rate: PoundsToEuros = attempt morning_rate(todays_quote)    var cake: CakePrice = CakePrice(money.from_minor(1050, Currency.gbp))    assert priced_in_euros(cake, rate) == money.from_minor(1228, Currency.eur)    assert morning_rate(backwards_quote) fails WrongRate    var sent: PoundsToEuros = attempt sent_rate(        "{\"from\":\"GBP\",\"to\":\"EUR\",\"rate\":\"1.17\",\"at\":\"2026-10-18T09:00:00Z\"}"    )    assert sent == rate    assert sent_rate(        "{\"from\":\"EUR\",\"to\":\"GBP\",\"rate\":\"0.85\",\"at\":\"2026-10-18T09:00:00Z\"}"    ) fails NotARate}

A rate whose type is the wrong way round is a type error in the function it is passed to, with no condition in sight:

example
type CakePrice = Money {    currency: Currency.gbp}type Payment = Money {}type EurosToPounds = ExchangeRate {    from: Currency.eur    to: Currency.gbp}/// A cake's price, at a rate whose type is the wrong way round.function priced_abroad(cake: CakePrice, rate: EurosToPounds) -> Payment {    return Payment(money.convert(cake, rate))}
text
error: `rate` converts from EUR, and `cake` is in GBP --> main.jadpo:14:40   |14 |     return Payment(money.convert(cake, rate))   |                                        ^^^^ a rate from EUR to GBP  reason: `money.convert` applies a rate to an amount in the rate's `from` currency. `cake` is in GBP and `rate` converts from EUR, as their types, how they were made or a condition around the call fix, so the rate is the wrong way round or for another currency.  next: Pass a rate from GBP, or an amount in EUR (agent)  rule: type.mismatch  help: https://jadpo.dev/docs/errors/type.mismatchcheck failed: 1 error, 1 file

A type built on one that fixes a currency repeats that currency or leaves it out, because a type built on another can only narrow it (Constraints). A different currency, in an amount's currency: or a rate's from: and to:, leaves no value that could follow both. It is type.constraint_contradiction, as a scale: other than the inherited one is, and its one-step fix removes the setting. A currency the parent leaves open, such as to: on a type built on FromPounds, may be fixed:

example
type CakePrice = Money {    currency: Currency.gbp    min: 0}/// A cake's price for the shop in Paris.type ParisPrice = CakePrice {    currency: Currency.eur}
text
error: `currency: Currency.eur` is different from `CakePrice`'s `currency: Currency.gbp` --> main.jadpo:8:15  |8 |     currency: Currency.eur  |               ^^^^^^^^^^^^ different from `CakePrice`'s `currency: Currency.gbp`  reason: `CakePrice` fixes the currency at GBP, and a type built on another only narrows it, so it repeats that currency or leaves it out; with another one no value could follow both, and the type could never be built.  next: Remove `currency:`, so the type keeps `CakePrice`'s GBP (agent)  rule: type.constraint_contradiction  help: https://jadpo.dev/docs/errors/type.constraint_contradictioncheck failed: 1 error, 1 file

Markdown for agents: /docs/exchange-rates.md.Built from Jadpo main at a707efb.