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

Logic

Constants

On this page

A constant names a fixed value at the top of a file. It is the same in every deployment, worked out while compiling, and never changes.

example
type Stock = Int {    min: 0}constant bulk_from: Stock = 5000constant small_batch: Stock = 250function batch_for(needed: Stock) -> Stock {    if (needed >= bulk_from) {        return 0    }    return small_batch}
  • Form. constant name: Type = value. The name is lower_snake_case, like any value. There is no mutable form: constant mut is syntax.expected_name, whose edit removes mut.
  • Only at the top of a file. Inside a body a fixed value is always a var, and at the top of a file always a constant, so each place has one way to write it. constant in a body is syntax.expected_statement, whose edit writes var. A var at the top of a file is syntax.expected_declaration, whose next: says to write constant. const at the top of a file is syntax.variable_keyword, whose edit writes constant; in a body it is also syntax.variable_keyword, whose edit writes var.
  • A named type. A constant's type follows the same rule as a signature's (Signatures use named types): constant limit: Int = 5 is type.primitive_signature, as limit: Int is in a parameter. A list of a named type, an object, an enum, Email, Url and the other types a signature may use are all fine.
  • A value the compiler can work out. Literals, enum values, other constants, values constructed from those (Stock(5000), an object, a list), and the operators of Expressions and operators on them. No call of any kind (functions, library functions, the standard library), no config, no clock, and no constant that depends on itself (constant.value_not_fixed, constant.cycle). Arithmetic gives the built-in type underneath, as it does in a body, so Stock(bulk_from * 2) constructs a Stock from it again.
  • Checked while compiling. The value is checked against every rule of its type and of the types it is built on, as a literal is (type.invalid_literal, which names the value worked out). So constructing a value in a constant never needs attempt. When the app runs, the constant's value is worked out where it is read, and that can't fail.
  • Kept together. jadpo fmt keeps constants written one after another on consecutive lines, as you grouped them, and puts a blank line between every other pair of declarations.
  • Unused is an error, as for any other declaration. Beside a file's primary declaration (the one the file is named after) it is layout.unused_declaration. In a library, a constant nothing reads is unused, as a function nothing calls is.

Constants built from other constants, a list of them, and both read in functions:

example
type Stock = Int {    min: 0    max: 100000}type LargeOrder = Bool {}type BatchCharged = Bool {}constant small_batch: Stock = 250constant large_batch: Stock = Stock(small_batch * 2)constant batch_sizes: List<Stock> = [small_batch, large_batch]function batch_chosen(large: LargeOrder) -> Stock {    if (large) {        return large_batch    }    return small_batch}function charged(batch: Stock) -> BatchCharged {    return BatchCharged(batch in batch_sizes)}

Each of these mistakes is one error, with its edit:

example
type Stock = Int {    min: 0}function batch() -> Stock {    constant small_batch: Stock = 250    return small_batch}
text
error: `constant` doesn't start a statement --> main.jadpo:6:5  |6 |     constant small_batch: Stock = 250  |     ^^^^^^^^ write `var`  reason: A constant lives at the top of a file or in a library; inside a body a value that never changes is a `var`.  next: Use `var` instead of `constant` (agent)  rule: syntax.expected_statement  help: https://jadpo.dev/docs/errors/syntax.expected_statementcheck failed: 1 error, 1 file
example
type Stock = Int {    min: 0}constant mut small_batch: Stock = 250
text
error: A constant can't be `mut` --> main.jadpo:5:10  |5 | constant mut small_batch: Stock = 250  |          ^^^ remove `mut`  reason: A constant never changes, so it has no mutable form; a value that changes is a `var mut` inside a body.  next: Remove `mut` (agent)  rule: syntax.expected_name  help: https://jadpo.dev/docs/errors/syntax.expected_namecheck failed: 1 error, 1 file
example
type Stock = Int {    min: 0}const small_batch: Stock = 250
text
error: Jadpo writes `constant`, not `const` --> main.jadpo:5:1  |5 | const small_batch: Stock = 250  | ^^^^^ write `constant`  reason: `const` declares a value that never changes in JavaScript, TypeScript, Rust, Go and C#; at the top of a Jadpo file a fixed value is `constant name: Type = value`.  next: Use `constant` instead of `const` (agent)  rule: syntax.variable_keyword  help: https://jadpo.dev/docs/errors/syntax.variable_keywordcheck failed: 1 error, 1 file
example
constant max_login_attempts: Int = 5
text
error: This constant uses `Int` directly --> main.jadpo:1:30  |1 | constant max_login_attempts: Int = 5  |                              ^^^ a built-in type  reason: A constant's type is named as a signature's is, so its value says what it means and carries its rules; `Int` says neither.  next: Declare a named type built on `Int` and use it here (agent)  rule: type.primitive_signature  help: https://jadpo.dev/docs/errors/type.primitive_signaturecheck failed: 1 error, 1 file

Where a constant lives

A constant goes where a helper function would (Sharing and promotion). A constant one file uses lives at the top of that file, beside the declaration that uses it. It is never a file's primary declaration, so there is no constants/ folder, and its name never clashes with a file's name: constant vat_rate: VatRate is fine, because naming.folded_collision leaves constants out.

When a second file uses it, layout.promotion_required asks you to move it into a library. Code then reads it through the library, like a library function but without brackets:

example
type Stock = Int {    min: 0}type HoldbackRate = Decimal {    scale: 4    min: 0.0000    max: 1.0000}library stocking {    /// The standard share held back.    constant holdback_rate: HoldbackRate = 0.2000    function holdback_of(per_pack: Stock) -> Stock {        return Stock(per_pack / 5)    }}function standard_holdback() -> HoldbackRate {    var uk_rate: HoldbackRate = stocking.holdback_rate    return uk_rate}function holdback(per_pack: Stock) -> Stock {    return stocking.holdback_of(per_pack)}

A library's own functions and constants read its constants by name alone, as they call its functions.

A library a second domain uses moves to shared/libraries/, or, when it depends on its domain's types, stays and becomes public, as it would for functions. A private constant's name must be unique only within its domain, as private functions' and types' names are (Files and names). Its identity key, the name Jadpo's records know it by (Stable identity), carries its domain: constant billing.free_delivery_from.

Where a constant can be read

Anywhere a value can, functions included, because it never changes. It is also what "constants" means in the places that allow only fixed values:

  • A query accepts a constant wherever it accepts a literal; anything else there is query.value_unsupported (Named queries).
  • An update by id may use parameters, constants, clock.now and the stored fields (The operations every entity gets).
  • A computed backfill may use the record's stored fields, constants and pure functions (Computed backfills).

A setting's default: and test: take a literal, never a constant (see A constant or a setting).

Two constants holding one value

Agents working at the same time tend to invent the same constant twice, each writing its own vat_rate. Two constants of the same type holding the same value are an error, constant.similar_exists, as two alike functions are library.similar_exists. The error names the other constant and its file, so the agent reads that one and removes its own. The first in file and source order is the one named, and the others are reported.

A VAT rate and a fee can share a value by coincidence, so the error is a judgement call you can decline: a decline constant.similar_exists: "<why>" line above the constant answers it (Declining a judgement call).

The library catalogue, the list of shared code that jadpo check writes into build/catalogue/ (Tooling), lists each constant with its type, value, /// description and the code that reads it.

A constant or a setting

If a deployment could ever need a different value, make it a setting in config.jadpo (Configuration and time); otherwise make it a constant. A page size an operator might tune is a setting. The number of pence in a pound is a constant. The Jadpo agent skill gives agents the same rule (Tools for agents).

A value is one or the other, never both, so it has one source and one way to read it. A constant is fixed and read as security.max_login_attempts. A setting is chosen per deployment and read as config.max_login_attempts.

So a setting's default: never names a constant (configuration.default_literal_required). If it could, code reading the constant directly would quietly ignore the deployment's value. Say security.standard_attempts (5) were max_login_attempts's default, and someone set MAX_LOGIN_ATTEMPTS=10: code reading security.standard_attempts would still allow 5. You don't need the constant anyway, because the setting's default: already holds the default.

From fixed to live, there are three kinds of value:

  • a constant is fixed in the code;
  • a config setting is chosen per deployment;
  • a flag in flags { } changes while the app runs (Feature flags).

Making a constant a setting is a job for a tool: jadpo config move <constant>, or the editor's "Make this a setting" (Tooling). It adds a field of the same name and type to config.jadpo whose default: is the constant's value, deletes the constant, and rewrites every read (security.max_login_attempts becomes config.max_login_attempts). The reviewer then sees each place whose behaviour can now differ per deployment.

It refuses, editing nothing, a constant:

  • whose type a setting can't hold;
  • whose name a setting already has;
  • read where a setting can't be: in a query, an update by id, a backfill or another constant's value.

The details are in Making a constant a setting.

Values that change on a known date

Some values change on a date you know in advance, such as the UK's VAT rate. Jadpo has no setting for this (there is no effective:). Write a constant holding a dated list, and read it through a library function:

example
type VatRate = Decimal {    scale: 4    min: 0.0000    max: 1.0000}type VatDate = CalendarDate {}type VatBand = Object {    valid_from: VatDate    band_rate: VatRate}type VatBands = List<VatBand> {    min_length: 1}library vat {    constant uk_bands: VatBands = [        VatBand {            valid_from: "2010-01-01"            band_rate: 0.1750        },        VatBand {            valid_from: "2011-01-04"            band_rate: 0.2000        }    ]    /// The UK standard rate on a date.    function rate_on(day: VatDate) -> VatRate {        var mut found: VatRate = 0.0000        for band in uk_bands {            if (band.valid_from <= day) {                found = band.band_rate            }        }        return found    }}function vat_rate_on(day: VatDate) -> VatRate {    var vat_due_rate: VatRate = vat.rate_on(day)    return vat_due_rate}
  • A change of rate is an edit to the list, reviewed as a diff. Test it with the test clock on each side of the date (Testing).
  • Make it an entity only when someone who isn't a developer must change it while the app runs.
  • A record stores the rate it used (an invoice's vat_rate field), so a later change never rewrites what was charged.

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