Documentation / Constants and compile-time behaviour

Constants and compile-time behaviour

Constants move work out of runtime and into the compiler. They stay strict on purpose: the value must be known, stable and finished before the program starts.

Templates can join in too. A const page fragment forms a template with nowhere left to hide.

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} = {"Ana", "Bo"}
    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 importable 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.

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.

Circular constant dependencies are compilation errors.

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 optimization.

Those facts do not:

  • change source semantics
  • create importable declarations
  • participate in header dependency sorting
  • turn a runtime binding into a source constant
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.

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.

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.