Compile-Time Evaluation
Part of every Rux program is settled while it is being compiled, before any of it runs. Constants are folded to values, sizeof and alignof to numbers, and a when keeps one branch and throws the others away unread. The compiler also describes the build itself — the target machine, the build profile, the compiler, the source location and user defines — as ordinary values a program can read. None of this costs anything at run time: by then a compile-time value is a plain constant, and a discarded branch does not exist.
There is no preprocessor and no separate build language. Everything in this chapter is written in Rux and checked by the same compiler.
What is known at compile time
| Construct | Known while compiling | Page |
|---|---|---|
literals and const declarations | the value, folded from literals, operators and casts, or computed by calling functions | Constants |
sizeof(T), alignof(T) | a type's size and alignment in bytes, as uint | Layout |
primitive constants such as int32::Max, uint::Bits | their values | Primitive types |
#target, #build, #compiler, #source | facts about the build and the source location | Context |
#config | user-defined values from Rux.toml or --define | Config |
when | which branch is compiled | Conditional compilation |
#Error, #Warn | diagnostics that stop or annotate the build | Diagnostics |
intrinsic declarations | the values and functions the compiler itself supplies | Intrinsics |
A const initializer may use literals, operators, casts, struct, array, tuple and case values, other constants, sizeof/alignof, primitive constants and the scalar fields of the # values — and it may call functions. The compiler runs such a call while it compiles, so a lookup table is computed from its definition rather than written out by hand:
func Square(x: int) -> int { return x * x; }
func Tabulate() -> int[10] {
var table: int[10] = [0; 10];
for i in 0..10 {
table[i] = Square(i);
}
return table;
}
const Nine = Square(3);
const Squares: int[10] = Tabulate();
The function needs no marker; what decides whether it may be called is its body and everything it calls. None of them may use a raw pointer, an unsafe block or unsafe func, an extern function or variable, an asm function, or a module or thread-local variable. A call that breaks a rule names the first such operation and the calls that lead to it:
error: constant 'Table' cannot be computed at compile time: 'Seed' reads module variable 'Counter' (in 'Build' -> 'Seed')
Evaluation follows the program's own rules at the target's widths. What would stop the program instead stops the compilation, at the constant: an integer overflow, a division by zero, an index past the end of an array, an invalid character cast, and an error that is thrown — even one a catch would handle at run time. Evaluation also stops after 4,000,000 steps and when calls nest more than 64 deep. Optional and sum values, strings and slices, calls through an interface, defer, ?, try? and try blocks are not evaluated; reaching one is an error that says so. An argument must itself be a compile-time value, so const C = Square(v); with a local v is error: 'v' is not a compile-time constant.
A computed constant is laid out exactly as the literal that spells it would be, so reading it costs nothing more at run time, and the call never runs in the program. It can be read by other constants, but it cannot size an array or count a repeat, and a when condition reads only constants that call nothing. A module variable's initial value calls nothing either: there a call is error: call to 'Seed' is not a compile-time value.
The # values come from Core
#target, #build, #compiler, #source and #config, and the directives #Error and #Warn, are declared in the Core package and imported like any other name:
import Core::{ #target, #build, #Error };
They are not keywords. A package that declares them itself — see Intrinsics — can stand in for Core.
Compile time and run time
Rux keeps the two kinds of decision visibly apart. A chain is if/else if throughout or when/else when throughout, never a mixture, so a reader always knows which branches the compiler has already settled:
if | when | |
|---|---|---|
| Decided | while the program runs | while the program is compiled |
| Condition | any bool | an expression the compiler can fold |
| Untaken branch | compiled and type-checked | parsed, then discarded before name resolution |
| Scope of a branch | its own | none: declarations and bindings stay after it |
| Where it can appear | inside a function body | between declarations and inside bodies |
flowchart LR
src["source"] --> parse["parse"]
parse --> fold["fold `when`<br/>keep one branch"]
fold --> resolve["resolve names,<br/>check types"]
resolve --> code["generate code"]
fold -. "untaken branches<br/>never reach here" .-> resolveA # value read in ordinary code is just as free: #build.debugAssertions in an expression compiles to the constant true or false.
See also
- Conditional compilation —
whenin full - Context — every field of
#target,#build,#compilerand#source - Config — values a build passes in
- Constants —
constdeclarations - Learn: Compile time, When