Documentation / Branching

Branching

Use statement if for a Boolean decision. Use a full match when one value can take distinct literal, range, option or choice forms.

Unlike C-style switch, Moth never falls through between arms. else => alone marks the catch-all, and exhaustive matches can omit it when the compiler can prove every case appears.

Choose the explanation level: Statement if

Statement if

Use if when code should run only under one condition.

if ready:
    io.line("Ready!")
;

Add else for the other path:

if ready:
    io.line("Ready!")
else
    io.line("Not ready yet")
;

One semicolon closes the whole structure.

Moth does not have an else if chain. Nest another if inside else, or use pattern matching when several cases belong to one value.

A #Config of Bool value can specialise this same ordinary if before HIR; both branches remain valid source. #Config if is not a Moth form.

A statement if runs a block when its condition is true.

if ready:
    io.line("ready")
else
    io.line("waiting")
;

Contract

  • The ordinary condition must resolve to Bool.
  • else is optional.
  • The if body and optional else body are child scopes.
  • One final semicolon closes the complete if and else structure.
  • Statement if is non-exhaustive when no else is written.
  • A statement branch does not produce a value merely because its body contains expressions.

Compile-time-known conditions

The ordinary if form is the only conditional source form; there is no new static-branch syntax. Both bodies are frontend-validated before a known Bool selects the executable body before HIR. The selected lexical scope remains intact, and inactive downstream compiler work is absent. An unknown Bool lowers normally.

enabled #= false

if enabled:
    perform_optional_work()
;

An implemented #Config of Bool value is one source of the same folded value. It uses this ordinary folded-value path rather than introducing config-specific branch syntax.

No statement-level else if

A same-line else if chain is not syntax.

Use a nested if inside the else body:

if first_condition:
    io.line("first")
else
    if second_condition:
        io.line("second")
    else
        io.line("fallback")
    ;
;

Use a full match when one value is being compared against several patterns.

Option statement capture

A statement can test an optional value and bind its present payload:

if maybe_name is |name|:
    io.line(name)
;

It can also test for absence:

if maybe_name is none:
    io.line("missing")
;

Without else, the unmatched case does nothing.

Choose the explanation level: Value-producing if

Value-producing if

An if can choose a value.

The short form fits on one line:

label = if ready then "ready" else "waiting"

The inline form also handles option capture:

label = if maybe_name is |name| then name else "guest"

Use block form when a branch needs more room:

label = if ready:
    then "ready"
else
    then "waiting"
;

Block form also inspects a present option:

label = if maybe_name is |name|:
    then name
else
    then "guest"
;

then provides the value for the surrounding assignment.

Both branches must provide the value or leave the function through another valid path.

When a condition is a known Bool, the same ordinary if can select one frontend-valid branch before HIR. Runtime conditions keep their normal behavior.

A value-producing if sends values to an immediate receiving site.

Inline form

label = if ready then "ready" else "waiting"

The inline then and else form stays on one logical line.

Inline option and choice predicates

The inline form also supports option-present capture and choice-variant predicates:

label = if maybe_name is |name| then name else "guest"
score = if status is Ready then 1 else 0

Block form

label = if ready:
    then "ready"
else
    then "waiting"
;

then sends one or more values from the active branch to the receiver.

The block form with then is supported at the same closed receiving sites as the inline form: declarations, assignments, multi-bind and returns.

Block option and choice predicates

The block form also supports option-present capture and choice-variant predicates:

label = if maybe_name is |name|:
    then name
else
    then "guest"
;

score = if status is Ready:
    then 1
else
    then 0
;

These are one-arm single-predicate value matches, not full if value is: matches. They require else.

Receiving sites

Value-producing control flow is accepted only at these closed receiving sites:

  • declarations
  • assignments
  • multi-bind
  • returns

It is not a general expression form. Nested then receivers are deferred.

A value-producing block is invalid directly inside:

  • function arguments
  • operator operands
  • constructor arguments
  • collection literals
  • template interpolation
  • expression statements

Completeness

  • A value-producing if requires else.
  • Every reachable branch must produce the required values or terminate.
  • Mixed producing and terminating paths are complete.
  • A real fallthrough path is rejected.
  • At least one reachable branch must produce values.
  • Every producing path must match the receiver's arity.
  • Produced values must be compatible with the receiver's types.

Compile-time-known conditions

Both branches must satisfy normal completeness, receiving arity and type rules first. A known Bool then selects one validated branch, so no runtime branch or hidden merge value is needed. Runtime conditions retain the existing value-producing behavior.

label = if enabled:
    then "on"
else
    then "off"
;

The selected body keeps its child scope. Calls and other executable work from the inactive body do not reach generated materialisation, HIR or later analysis.

Option-present and choice-predicate inline and block forms use the same receiving-site model and are covered by the pattern references.

Choose the explanation level: Pattern matching

Pattern matching

Pattern matching is useful when one value can take several meaningful shapes.

if score is:
    < 0 => io.line("invalid")
    0 => io.line("zero")
    >= 90 => io.line("excellent")
    else => io.line("other")
;

Each arm begins with a pattern and =>.

There are no semicolons between arms. One semicolon closes the whole match.

Use else => for everything that was not matched earlier. It can have an empty body when the remaining cases should do nothing.

A full match checks one value against a sequence of patterns.

if score is:
    < 0 => io.line("invalid")
    0 => io.line("zero")
    >= 90 => io.line("excellent")
    else => io.line("other")
;

Match shape

  • The header is if value is:.
  • An arm is <pattern> => <body>.
  • A guarded arm is <pattern> if condition => <body>.
  • The guard must resolve to Bool.
  • Arm headers begin at the start of a logical line.
  • The next line-initial arm or the final semicolon ends the current arm body.
  • Match arms do not use individual semicolons.
  • One final semicolon closes the complete match.

Default arm

else => is the only catch-all arm.

if value is:
    0 => io.line("zero")
    else =>
;

In a statement match, bodyless else => explicitly ignores every remaining case.

_ => is not wildcard syntax. else => _ is also invalid.

Value-producing match

A match can produce values with then at a closed receiving site.

label = if status is:
    Ready => then "ready"
    Waiting => then "waiting"
;

Every selected value-producing arm must produce the receiver's required values or terminate.

Single-predicate value match

A closed receiver can also use one option-present capture or one choice-variant predicate. The inline and block forms are equivalent one-arm matches with a required else. They are not full if value is: matches and do not use => arms.

label = if maybe_name is |name| then name else "guest"

label = if maybe_name is |name|:
    then name
else
    then "guest"
;

score = if status is Ready then 1 else 0

score = if status is Ready:
    then 1
else
    then 0
;
Choose the explanation level: Patterns and exhaustiveness

Patterns and exhaustiveness

Patterns can match exact values, ranges, choice variants or option shapes.

if status is:
    Ready => io.line("ready")
    Waiting => io.line("waiting")
;

A choice match does not need else => when every variant is covered.

Payload variants expose their fields through captures:

if response is:
    Success => io.line("done")
    Err(message) => io.line(message)
;

The capture name must match the field name.

Use as when the local name should be different:

Err(message as error_text) => io.line(error_text)

Options have two complete shapes: absent and present.

if maybe_name is:
    none => io.line("missing")
    |name| => io.line(name)
;

Most other value matches need else => because the compiler cannot list every possible Int, String or other scalar value.

Match patterns describe the values an arm accepts.

Literal patterns

A literal pattern must be compatible with the scrutinee type.

if value is:
    1 => io.line("one")
    2 => io.line("two")
    3 => io.line("three")
    else =>
;

One statically typed match can't mix unrelated Int, String and Bool literal patterns.

Choice variants

if status is:
    Ready => io.line("ready")
    Status::Waiting => io.line("waiting")
;

Choice payload captures

if response is:
    Success => io.line("done")
    Err(message) => io.line(message)
    Pending(retry_count, message as pending_message) =>
        io.line(pending_message)
;

Option patterns

Options support:

  • none
  • literal present-value patterns
  • relational present-value patterns
  • |name| to capture any present value

An option match may omit else => only when it has both:

  • an unguarded none arm
  • an unguarded |name| present-value capture
if maybe_name is:
    none => io.line("missing")
    |name| => io.line(name)
;

Literal or relational option arms don't replace the unguarded present-value capture when else => is omitted.

Relational patterns

< value
<= value
> value
>= value

Relational patterns support Int, Float and Char.

The catch-all arm

else => is the only full-match catch-all. A bare unknown name in choice pattern position is treated as an unknown or invalid variant. A bare name in other full-match pattern positions is invalid syntax.

if value is:
    0 => io.line("zero")
    else => io.line("other")
;

General bare-name capture has been removed from full-match positions, and String is not a relational pattern subject. Relational patterns remain available for Int, Float and Char; option |name| and declared choice payload captures remain the explicit capture forms. See Progress for current backend and coverage status.

Choice payload capture contract

  • Payload captures must list every declared field.
  • Captures stay in declaration order.
  • The source name before as must exactly match the declared field name.
  • as local_name changes only the arm-local binding.
  • Duplicate local bindings are invalid.
  • Wildcard payload captures aren't supported.
  • Named assignment syntax such as Err(message = text) isn't payload-pattern syntax.
  • Nested payload patterns are deferred.

Exhaustiveness

  • An ordinary non-choice full match requires else =>.
  • An option match may instead use unguarded none plus unguarded |name|.
  • A choice match may omit else => only when every variant is covered.
  • Any guarded choice arm requires else =>.
  • The same choice variant can't be matched more than once.
  • Payload exhaustiveness is tag-level. One valid payload capture arm covers every value of that variant.
  • A statement match may use bodyless else =>.
  • A value-producing match must produce the required values on every selected path.

Single-predicate value matches

Inline and block single-predicate value matches are a narrower subset: one option-present capture or one choice variant, plus a required else. They are not exhaustive full matches. They cannot omit else even when the predicate looks total.