const names a value the compiler folds before your program runs. Constants can
be top-level, local to a block, or members of a type. Their names use
SCREAMING_SNAKE_CASE.
const MAX_RETRIES = 5
const PORT: u16 = 8080
const MASK = 0xFF
const SERVICE = "orders"
fn f(): void {
println("${SERVICE} on ${PORT}, retries=${MAX_RETRIES}, mask=${MASK}")
}Inference and annotation
The annotation is optional when the initializer determines the type.
const RETRIES = 5 // int
const LABEL = "worker" // string
const ENABLED = true // bool
const RATIO = 0.5 // float
fn f(): void {
println("${RETRIES} ${LABEL} ${ENABLED} ${RATIO}")
}Annotating fixes the type and turns on range checking at the declaration:
const FLAG: bool = true
const TEXT: string = "x"
const PRECISE: f64 = 0.5
const INITIAL: char = 'q'
const MARKER: byte = 0x41
fn f(): void {
println("${FLAG} ${TEXT} ${PRECISE} ${INITIAL} ${MARKER}")
}const TOO_LARGE: u8 = 999
fn f(): u8 {
return TOO_LARGE
}An unannotated numeric constant keeps its contextual flexibility, so one declaration can serve several widths:
const CAP = 255
fn f(): void {
byte_cap: u8 = CAP
wide_cap: u64 = CAP
println("${byte_cap} ${wide_cap}")
}The use site must still be representable — flexibility is not conversion:
const CAP = 255
fn f(): void {
too_small: i8 = CAP
println("${too_small}")
}Once a constant is annotated, ordinary type compatibility applies; the
compiler does not reinterpret a declared u32 as every other width.
What folds
The constant evaluator handles literals, other constants, and pure scalar operators. This is the full arithmetic, bitwise, comparison, and logical surface:
const A = 10
const B = 3
const SUM = A + B
const DIFF = A - B
const PROD = A * B
const QUOT = A / B
const REM = A % B
const NEG = -A
const SHIFT_L = A << 2
const SHIFT_R = A >> 1
const MASKED = A & B
const MERGED = A | B
const TOGGLED = A ^ B
const INVERTED = ~A
const SMALLER = A < B
const SAME = A == B
const BOTH = SMALLER && SAME
const EITHER = SMALLER || SAME
const FLIPPED = !SMALLER
fn f(): int {
return SUM + DIFF + PROD + QUOT + REM + SHIFT_L + SHIFT_R + MASKED + MERGED + TOGGLED
}Float arithmetic folds too, and so does string concatenation when every operand is constant:
const PI = 3.14159
const TAU = PI * 2.0
const PRODUCT = "atoll"
const VERSION = "1"
const USER_AGENT = PRODUCT + "/" + VERSION
fn f(): void {
println("${USER_AGENT} tau=${TAU}")
}Parentheses work as usual, which matters for bit layouts:
const LOW_NIBBLE = 0xFF & 0x0F
const HEADER_FLAG = (1 << 4) | 0b0011
const SECONDS_PER_DAY = 60 * 60 * 24
fn f(): int {
return LOW_NIBBLE + HEADER_FLAG + SECONDS_PER_DAY
}What does not fold
The evaluator is a deliberately small language, not the runtime running inside
the compiler. Each of the following is ATOLL1019: expression is not const-evaluable.
A function call, even of a function that looks pure:
fn side(): int { return 1 }
const X = side()
fn f(): int {
return X
}A method call — including on a constant string:
const NAME = "atoll"
const LENGTH = NAME.len()
fn f(): int {
return LENGTH
}An aggregate — list, tuple, record, Some, or an enum variant:
const NUMBERS = [1, 2, 3]
fn f(): int {
return NUMBERS.len()
}enum Mode { Fast, Slow }
const DEFAULT_MODE = Mode.Fast
fn f(): Mode {
return DEFAULT_MODE
}A branch expression:
const DEBUG = true
const LEVEL = if DEBUG { 3 } else { 0 }
fn f(): int {
return LEVEL
}And anything that reaches a runtime value:
fn f(n: int): int {
const DOUBLED = n * 2
return DOUBLED
}When you need a constructed value, use a function. It is folded by the optimizer where possible and it costs nothing at the source level:
enum Mode { Fast, Slow }
fn default_mode(): Mode {
return Mode.Fast
}
fn default_ports(): []int {
return [80, 443, 8080]
}
fn f(): int {
return default_ports().len()
}Treat the scalar/operator subset as the portable core, and verify anything larger against the compiler before relying on it.
Errors the evaluator reports
Division and remainder by zero are caught at compile time:
const X = 1 / 0
fn f(): int {
return X
}So are dependency cycles, reported once per member:
const A = B + 1
const B = A + 1
fn f(): int {
return A
}Forward references are fine — the evaluator resolves by declaration identity and recurses, so declaration order does not matter:
const BUFFER_SIZE = HEADER_SIZE + PAYLOAD_SIZE
const HEADER_SIZE = 16
const PAYLOAD_SIZE = 4096
fn f(): int {
return BUFFER_SIZE
}SCREAMING_SNAKE_CASE and reserved words
Query keywords are lexed in uppercase, so a handful of otherwise natural
constant names are unavailable: LIMIT, SELECT, FROM, WHERE, ORDER,
AND, OR, and their neighbours.
const LIMIT = 5
fn f(): int {
return LIMIT
}Pick a qualified name instead — which usually reads better anyway:
const PAGE_LIMIT = 5
const SORT_ORDER = 1
fn f(): int {
return PAGE_LIMIT + SORT_ORDER
}Lowercase limit and order are ordinary identifiers and are unaffected.
Local constants
A const inside a function body is block-scoped. It may reference other visible
constants but not parameters or locals, and it may shadow a top-level constant
of the same name.
const TIMEOUT_MS = 1000
fn packet_limit(): int {
const HEADER = 16
const PAYLOAD = 4096
return HEADER + PAYLOAD
}
fn adjusted_timeout(): int {
const TIMEOUT_MS = 250 // shadows the top-level constant
return TIMEOUT_MS
}Use a normal binding when the value depends on runtime state — a local const
buys nothing a let-style binding does not, except the guarantee that it folded.
Constants in types and defaults
A constant can size a fixed array and can supply a parameter default:
const BUFFER_SLOTS = 4
const DEFAULT_LIMIT = 10
fn take_some(values: []int, limit: int = DEFAULT_LIMIT): int {
return values.take(limit).len()
}
fn f(): int {
mut buffer: [BUFFER_SLOTS]int
buffer[0] = 7
return take_some([1, 2, 3]) + (buffer[0] ?? 0)
}Constants in patterns
A visible constant name used in a pattern matches that constant’s value instead of introducing a binding. Fresh names still bind normally; name resolution decides which reading applies.
const QUIT = 0
const HELP = 1
fn dispatch(command: int): string {
return match command {
QUIT => "quit"
HELP => "help"
other => "unknown ${other}"
}
}
fn f(): void {
println("${dispatch(0)} ${dispatch(1)} ${dispatch(9)}")
}Reach for an enum when the cases form a closed domain. Constant patterns fit protocol numbers, status codes, masks, and other values that already have a stable scalar representation.
Members and associated constants
A struct body may hold a const, and a trait may declare an associated constant
that implementations bind:
trait Limited {
const MAX: int
fn cap(self): int
}
struct Small { size: int }
impl Limited for Small {
const MAX = 8
fn cap(self): int => 8
}
fn f(): int {
s := Small { size: 1 }
return s.cap()
}Both forms declare and fold correctly. Reading one back through the type — the
Small.MAX spelling — is not yet supported; it is reported as a missing
field. Until it is, expose the value through a method as shown above, or keep it
as a top-level constant.
Constants are not places
A constant has no storage at runtime; its folded value is substituted into each use. Assignment to a constant is meaningless and the current checker does not reject it:
const RETRY_BUDGET = 1
fn f(): void {
RETRY_BUDGET = 2 // accepted today; do not write this
println("${RETRY_BUDGET}")
}Treat that as unsupported. Use a mut binding when a value has to change.
Because folding happens before bodies are lowered, constants have no runtime initialization order, and they never allocate. Anything needing allocation, I/O, a host call, or another effect belongs in a function.
Visibility
Top-level constants take the usual visibility modifiers and participate in normal imports.
pub const DEFAULT_TIMEOUT = 30
private const INTERNAL_TAG = 7
const MODULE_LOCAL = 3
fn budget(): int {
return DEFAULT_TIMEOUT + INTERNAL_TAG + MODULE_LOCAL
}Changing a public constant’s value is observable API behavior even when the type does not change — consumers fold it into control flow, array lengths, and generated code. Version and review constant changes the same way you would a signature change.
Composed example
const PROTOCOL_VERSION = 2
const HEADER_BYTES = 8
const MAX_PAYLOAD = 1 << 16
const MAX_FRAME = HEADER_BYTES + MAX_PAYLOAD
const FLAG_COMPRESSED = 0b0000_0001
const FLAG_ENCRYPTED = 0b0000_0010
const FLAG_MASK = FLAG_COMPRESSED | FLAG_ENCRYPTED
error FrameError {
TooLarge { size: int }
BadVersion { got: int }
}
struct Frame {
version: int
flags: int
payload: []byte
}
fn validate(frame: Frame): int ! FrameError {
if frame.version != PROTOCOL_VERSION {
error BadVersion { got: frame.version }
}
size := HEADER_BYTES + frame.payload.len()
if size > MAX_FRAME {
error TooLarge { size }
}
return size
}
fn describe(frame: Frame): string {
set := frame.flags & FLAG_MASK
compressed := (set & FLAG_COMPRESSED) != 0
encrypted := (set & FLAG_ENCRYPTED) != 0
return "v${frame.version} compressed=${compressed} encrypted=${encrypted}"
}
fn main(): void {
frame := Frame {
version: PROTOCOL_VERSION,
flags: FLAG_COMPRESSED,
payload: b"hello",
}
println(describe(frame))
println("size: ${validate(frame).unwrap_or(0)}")
}