Conditional Compilation

when is the compile-time counterpart of if. Its condition is folded while compiling, the branch it selects is kept, and every other branch is discarded before names are resolved — so an untaken branch may call functions, name types or link libraries that do not exist in this build. That is what makes one source tree serve several targets and configurations.

when-chain = "when" condition "{" items "}"
             { "else" "when" condition "{" items "}" }
             [ "else" "{" items "}" ]

when-match = "when" subject "{" arm { [ "," ] arm } [ "," ] "}"
arm        = ( pattern { "," pattern } | "else" ) "=>" arm-body

items are declarations or statements, depending on where the when stands.

Chains

A chain is tested from top to bottom; the first condition that holds selects its branch, and a final bare else is taken when none does. Without an else, a chain whose conditions all fail compiles to nothing.

import Core::{ #compiler, SemanticVersion };

const Rux040 = SemanticVersion { major: 0, minor: 4, patch: 0 };
const Rux100 = SemanticVersion { major: 1, minor: 0, patch: 0 };

when #compiler.version >= Rux100 {
    func Channel() -> char8[..] { return "stable"; }
} else when #compiler.version >= Rux040 {
    func Channel() -> char8[..] { return "preview"; }
} else {
    // Never resolved by this compiler, so a function nobody wrote is not an error.
    func Channel() -> char8[..] { return LegacyChannel(); }
}

A chain keeps the keyword it opened with. else if inside a when chain — or else when inside an if chain — is an error:

error: expected 'when' after 'else' in a compile-time 'when' chain; 'if' is the run-time conditional

Match form

When the condition picks between several values of one subject, the match form is shorter than a chain of ==. Each arm lists one or more patterns before =>; the first arm with a matching pattern is kept. Patterns are compile-time values compared for equality — enum members (the .Linux shorthand takes its enum from the subject), integers and strings:

import Core::{ #target, #Error };

when #target.os {
    .Linux, .FreeBSD => const Family: char8[..] = "unix";
    .macOS => const Family: char8[..] = "darwin";
    .Windows => const Family: char8[..] = "windows";
    else => #Error("unsupported operating system")
}

The else arm must be last (error: the 'else' arm must be last in a 'when' match). Without one, a build that no arm matches stops:

error: no arm of this 'when' matches .FreeBSD

That is the right outcome when the code truly cannot work elsewhere; when it can, add an else arm. Commas between arms are optional, except after an arm whose body is a bare expression: there the comma is what ends the expression, so .Windows => Setup(), .Linux => … needs it.

Arm bodies

PositionAn arm body may be
between declarationsone declaration, a { … } block of declarations, an import, or an #Error/#Warn directive
inside a bodyone expression — usually a call — or a { … } block of statements

A statement written bare as an arm body is refused, because it is not an expression:

error: a 'when' arm body that is a statement must be written as a block, as in '.Windows => { return 1; }'

Where when can appear

  • Between declarations — at the package root, inside a module, and inside another when branch. The taken branch's declarations are spliced into the enclosing list, as if written there.
  • Inside an extend block — selecting methods and associated constants.
  • Inside a function body — selecting statements.

when cannot stand inside a struct's field list, an extern block or an expression.

No new scope

A taken branch opens no scope. A let in it, or a declaration, is still there after the when:

import Core::{ #target };
import Io::PrintLine;

func Main() -> int {
    when #target.os {
        .Windows => { let separator = "\\"; },
        else => { let separator = "/"; }
    }
    PrintLine("{}", separator);
    return 0;
}

The same code with if would fail twice: each separator would end with its branch, and both branches would have to compile.

Untaken branches

An untaken branch is parsed — it must be well-formed Rux — but never resolved or type-checked. Calls to missing functions, unknown types, a library absent on this system, assembly for another architecture: none of them is an error there. The flip side is that a mistake in an untaken branch stays hidden until a build takes it. Build for each target that matters (rux build --target linux-x86_64, for instance) to check every branch.

Conditions

A condition — or a match subject — must be something the compiler can fold before name resolution:

OperandExample
bool, integer, floating-point, character and string literalstrue, 64, "windows-x86_64"
constants with foldable initializerswhen Chosen == 20
primitive constants (with the extension imported from Core)int32::Bits == 32
sizeof and alignof of a primitive typesizeof(int) == 8
fields of #target, #build, #compiler, #source#target.pointerBits, #build.mode
the queries #target.HasFeature, #compiler.HasFeature, #config.Has, #config.Get#config.Has("Verbose")
#compiler.version and SemanticVersion constants#compiler.version >= Rux040

They combine with == != on any of these, < <= > >= on numbers, characters and versions, the SemanticVersion methods (IsAtLeast, IsLessThan, …), && || !, and arithmetic. Floating-point values retain their declared width and use exact compiler arithmetic. Conditions can read the intrinsic finite limits, infinities and NaNs at every width, including float512::Max, and compare values beyond the float64 range. Integer casts retain or truncate all bits at the requested width. A cast to a character width never truncates: the value is checked against the width, and one it cannot hold makes the condition invalid. sizeof and alignof of a non-primitive type, and function calls, are not available:

error: 'when' condition is not a valid compile-time expression

A let binding is never a compile-time value (error: 'minor' is not a compile-time constant). Enum-valued fields compare only with == and !=; ordering #target.os < .Windows is the same "not a valid compile-time expression" error. A misspelt member lists the real ones:

error: '.MacOS' is not a variant of 'OperatingSystem'; the variants are: .FreeBSD, .Linux, .macOS, .Windows

See also

  • Context — the fields and queries a condition reads
  • Config — conditions on user defines
  • Diagnostics — #Error and #Warn in an untaken-by-design branch
  • if — the run-time conditional
  • Learn: When, Target