Documentation / Constants and compile-time behaviour

Constants and compile-time behaviour

A constant tells the compiler to finish a value before runtime. Unlike a read-only runtime binding, a Moth constant can depend only on other compile-time values and must fold completely.

The same rule covers scalar values, const records and templates. Direct const fragments can contribute finished content to an HTML page.

Choose the explanation level: Constant bindings

Constant bindings

Add # when a value must be known during compilation.

site_name #= "Moth"
version #Int = 1

Constants cannot change.

They can be used by other compile-time values:

full_name #= [: [site_name] v[version]]

Use a normal binding when the value belongs to runtime code.

# marks a compile-time constant binding.

site_name #= "Moth"
major_version #Int = 1
names #{String} = {"Priya", "Rob"}
maybe_name #String? = none

Declaration forms

name #= value
name #Type = value
  • Every constant must have an initializer.
  • A constant is immutable.
  • A mutable marker cannot be combined with #.
  • Without an annotation, the initializer supplies the type.
  • An explicit annotation supplies a compile-time receiving type.
  • The initializer must fully fold.
  • A constant may reference only compile-time values.

# controls compile-time evaluation. It does not control visibility.

Top-level constants follow ordinary module and public-surface visibility rules.

Body-local constant declarations remain local to their scope.

A top-level runtime binding without # belongs to entry-selected start code rather than the dependency-bindable constant surface.

Choose the explanation level: Constant folding

Constant folding

The compiler must finish a constant before runtime code begins.

base #Int = 4
larger #Int = base + 2

A constant can use another constant that is already available.

It cannot use a runtime variable or call a runtime function.

When a constant expression is invalid, the compiler reports it during the build instead of leaving the failure for runtime.

Static Bool if specialisation is implemented for ordinary if syntax, including conditions from #Config of Bool values:

enabled #Config of Bool = false

if enabled:
    perform_optional_work()
;

A constant initializer must fully evaluate during AST construction.

base #Int = 4
larger #Int = base + 2
label #= [: size [larger]]

Allowed dependency shape

A constant may depend on:

  • literals
  • earlier same-file constants
  • imported constants
  • foldable operators
  • foldable casts
  • foldable struct construction
  • foldable templates
  • other compile-time values accepted by the current language surface

It may not depend on:

  • runtime bindings
  • runtime function calls
  • host calls
  • runtime-only template content
  • mutable state

Ordering

Same-file constant evaluation follows source order.

A same-file forward reference is invalid.

Cross-file constant dependencies are represented in the top-level dependency graph and evaluated in dependency order. A direct .mtf or .md file value creates one of these without a top-level clause, because it consumes that file's content constant.

Circular constant dependencies are compilation errors. That includes a cycle formed through content-file values.

Structural values fold without final text

A folded String is not always plain text. A file path, and the bare site root @/, fold to a String that carries a structural anchor rather than characters, because the final URL depends on output placement the frontend has not chosen yet.

logo #= @assets/logo.svg
image #= [: <img src="[logo]">]

Both are complete constants. The compiler knows the whole semantic value, so composition, storage, export and template insertion all fold normally.

What cannot fold is an operation that needs the final characters while an anchor is unresolved, such as comparing one of these strings against a literal URL. That is diagnosed rather than folded against a guessed path. The restriction is about placement being undecided, not about the value being a file: once a builder resolves each anchor, ordinary runtime string behaviour applies.

See File values.

Compile-time and runtime agreement

Folding must preserve runtime language semantics.

Examples:

  • a statically known checked-numeric failure is a compile-time diagnostic
  • String -> Int and Float -> Int enforce the signed 32-bit range
  • String -> Float rejects non-finite materialization
  • Float -> String uses Moth's stable formatter
  • compile-time and runtime template interpolation use the same Float formatting

The compiler must not accept one result during folding and produce another at runtime.

Advisory const facts

The compiler may discover private const facts for optimisation.

Those facts do not:

  • change source semantics
  • create dependency-bindable declarations
  • participate in header dependency sorting
  • turn a runtime binding into a source constant

Static Bool control flow

An ordinary if condition may fold to a known Bool, including an implemented #Config of Bool value. Both branches first parse, resolve and type-check as normal source. Stage 4 then selects one branch, preserving the selected branch's lexical scope; inactive executable work does not reach HIR or later analysis. Runtime conditions remain runtime branches.

This is not target conditional compilation. #Config of Bool uses the same general folded constant and ordinary-if mechanism. Advisory const facts do not create source constants or structural conditional compilation.

Stage 4 performs this selection after final constant values and full branch validation, then derives terminality and executable facts from the selected AST.

Choose the explanation level: Const records

Const records

A constant can hold a fully known struct value.

Defaults = |
    title String,
    year Int,
|

defaults #= Defaults("Moth", 2026)

Read its fields during compilation or use one field in runtime code:

page_title #= defaults.title
io.line(defaults.title)

The complete const record is not an ordinary runtime object. Use its fields instead.

An anonymous | name = value | const record uses the same |...| parameters syntax as structs and functions. Every parameter needs a value, and the list does not create a constructable type. Declare a child struct or record first, then name it as a parameter. Do not nest one |...| list inside another.

A fully folded struct instance stored in a constant may become a const record.

Defaults = |
    title String,
    year Int,
|

defaults #= Defaults("Moth", 2026)

page_title #= defaults.title

Contract

A const record is a data-only compile-time member group.

Every constructor argument must fold.

Const-record field projection may participate in another constant expression.

The complete const record cannot be used as an ordinary runtime value.

It cannot be:

  • assigned to a runtime binding
  • passed as a runtime argument
  • returned from a runtime function
  • stored in a collection or map
  • used through a runtime receiver method

Use one folded field when runtime code needs the value.

io.line(defaults.title)

Const records preserve their struct-backed field names and types.

They do not introduce structural typing or anonymous runtime record identity.

Anonymous const records

|...| is Moth's pipe-delimited member-list syntax. Context decides whether its entries are function parameters, typed struct fields or named const-record values.

A compile-time receiving context turns a parameter list into a const record:

labels #= |
    first = "a",
    second = "b",
|

Every parameter must have a value. Types may be inferred from those values. The list does not declare a constructable type. A struct uses the same |...| list to declare a type that constructors can build.

Nested parameter lists

A parameter list is one |...| group. It does not contain another |...| list. That is the same composition rule as structs and function signatures.

Declare a child struct or const record first, then name it as a parameter value:

Size = |
    width Int,
    height Int,
|

size #= Size(8, 4)

inner #= |
    x = 3,
|

nested #= |
    size = size,
    inner = inner,
|

A const record still adds compile-time folding and forbids using the complete record as a runtime value. Those rules do not change parameters syntax.

Choose the explanation level: Const templates

Const templates

A constant template is finished during compilation.

title #= "Moth"

heading #= [$md:
    # [title]
]

A direct top-level const template in an active HTML module root can also become part of the generated page:

#[$md:
    # Compile-time heading
]

Const template loops have an iteration limit so an accidental build-time loop cannot run forever.

A template can be required to finish during compilation.

Named const templates

A normal constant binding can hold a folded template string.

site_name #= "Moth"

heading #= [$md:
    # [site_name]
]

This creates a compile-time String value.

It does not contribute a page fragment by itself.

Direct top-level const fragments

In an active HTML module root, the direct top-level const-template form contributes a compile-time page fragment.

#[$md:
    # Compile-time heading
]

The direct form:

  • is valid only as top-level entry-selected fragment syntax
  • must fully fold
  • records its position relative to entry-selected runtime fragments
  • is handed to the project builder
  • never becomes HIR runtime code

Const template if

#[if show_heading:
    Visible heading
[else]
    Hidden heading
]

A const-required template if validates every branch body.

The selected result must fold completely.

Const template loops

#[loop items |item|:
    [item]
]

Const range and collection loops may use structural break and continue controls:

#[loop items |item|:
    [if item.done:
        [break]
    ]

    [item]
]

They use the project config guard:

template_const_loop_iteration_limit

The default is 10_000.

The maximum accepted configured value is 1_000_000.

The configured value must be a positive folded Int. Zero, negative values and values above 1_000_000 are rejected.

A const conditional loop folds to no-output only when its condition is compile-time false.

A compile-time true condition and a runtime or unknown condition are rejected because the compiler does not prove loop termination.

Runtime template control flow remains owned by Template control flow.