Cella Documentation 0.1.0-dev Home Source

Constant functions

A constant function is a function the compiler can also run while compiling. Called where a value must be known at compile time, such as a constant's initial value or the length of an array type, it runs then and its result becomes a constant. Called anywhere else, it's an ordinary function call.

Declaring a constant function

const before fun makes a function constant:

square: const fun(x: i32) -> i32 => x * x side: const i32 = square(4)cells: val array[u8, usize(square(2))] = [1, 2, 3, 4] main: fun{	const area = square(side)	val n = 5	printf("%d %d %zu %d\n", side, area, cells.length, square(n))}
Output
16 256 4 25

side and area are worked out while compiling, and the type of cells uses square(2) as its length. square(n) in main runs when the program does, like any call.

A constant function's arguments must be constants where a constant is needed:

square: const fun(x: i32) -> i32 => x * x counter: var i32 = 3total: const i32 = square(counter)The initial value of 'total' must be a constant main: fun{}

What a body can do

A constant function's body is an ordinary function body. It can declare val, var and const locals, assign to them, branch, loop, match, take mut and imm parameters and call other constant functions:

crcTable: const fun -> array[u32, 8]{	var table = array[u32, 8]()	for i in 0..8	{		var c = u32(i)		for _ in 0..8		{			if c & 1 == 1				c = 0xEDB88320 ^ (c >> 1)			else				c = c >> 1		} 		table[i] = c	} 	ret table} bump: const fun(mut value: i32, by: i32 = 1){	value += by} countTo: const fun(limit: i32) -> i32{	var n = 0	while true	{		bump(mut n)		if n == limit			break	} 	ret n} table: const array[u32, 8] = crcTable() main: fun{	printf("%u %u %d\n", table[1], table[7], countTo(6))}
Output
1996959894 2657392035 6

The body may only call constant functions and operators. Built-in operators and conversions are always constant. It can't call other functions, including ext functions, read or write module and static values that aren't constants, use pointers, take addresses, call function values or use values with destructors:

counter: var i32 = 0 plain: fun(x: i32) -> i32 => x busy: const fun(x: i32) -> i32{	counter += 1Cannot use module values in constant functions	puts("hi")Cannot call 'puts' in constant functions	ret plain(x)Cannot call 'plain' in constant functions} stop: const fun -> neverCannot return 'never' from constant functions{	loop	{	}} main: fun{}

These rules are checked once, where the function is declared, whether or not anything calls it.

Operators, constructors and methods

Operators, constructors, conversions and methods are constant the same way, with const before op or fun:

Point: rec{	x: i32	y: i32 	new: const op(x: i32, y: i32)	{		self.x = x		self.y = y	} 	sum: const fun(self) -> i32 => self.x + self.y 	shift: const fun(mut self, by: i32)	{		self.x += by		self.y += by	} 	+: const op(a: Point, b: Point) -> Point => Point(a.x + b.x, a.y + b.y)} moved: const fun -> Point{	var p = Point(1, 2) + Point(3, 4)	p.shift(10)	ret p} corner: const Point = moved()total: const i32 = corner.sum() main: fun{	printf("%d %d %d\n", corner.x, corner.y, total)}
Output
14 16 30

A constructor declared new: op() = default is always constant, and so is building a record field by field. Destructors can't be constant.

Properties

A property's getter and setter are constant the same way, with const before get or set. Reading a constant getter or writing through a constant setter then works in constants and constant functions, compound assignments such as += included. A static getter with no receiver works like a function without parameters:

Bits: rec{	value: u32 	high: const get(self) -> u32 => self.value >> 16	none: const get -> u32 => 0 	level: prop u32	{		const get(self) => self.value		set(mut self, value)		{			self.value = value		}	} 	count: const prop u32	{		get(self) => self.value		set(mut self, value)		{			self.value = value		}	}} counted: const fun -> u32{	var bits = Bits(5)	bits.count += 4	ret bits.count + bits.level} top: const u32 = Bits(0x30001).hightotal: const u32 = counted() main: fun{	printf("%u %u %u\n", top, total, Bits.none)}
Output
3 18 0

In a prop, const can go on each accessor, as level does to make only its getter constant, or on prop itself to make both constant, as count does. Writing it on an accessor of a const prop gets the hint "Redundant 'const'".

Reading or writing a property that isn't constant inside a constant function is an error:

Bits: rec{	value: u32 	low: get(self) -> u32 => self.value & 0xFFFF} masked: const fun -> u32 => Bits(3).lowCannot read 'Bits.low' in constant functions

Functions without parameters

A constant function that takes no parameters and isn't generic works like a constant. The compiler runs it once, where it's declared, and every call to it is that value. Its errors are reported where it's declared, even if nothing calls it:

largest: const fun -> i32Panic in 'largest': addition overflowOn line 6{	val top: i32 = 2147483647	ret top + 1} main: fun{}

It's still compiled as a function when something needs one: when it's pub, when it's used as a function value, or when it meets a trait's requirement. Otherwise only its value is kept.

Errors while running

A constant function that fails while compiling stops the build. The error points at the call, and its note shows where in the function it happened:

square: const fun(x: i32) -> i32 => x * xhalve: const fun(x: i32, by: i32) -> i32 => x / bypick: const fun(values: array[i32, 3], at: usize) -> i32 => values[at] big: const i32 = square(100000)Panic in 'square': multiplication overflowOn line 3divided: const i32 = halve(1, 0)Panic in 'halve': division by zeroOn line 4picked: const i32 = pick([1, 2, 3], 5)Panic in 'pick': index out of boundsOn line 5 main: fun{}

A panic is allowed in a constant function, whichever panic it resolves to. Reached while compiling, it stops the build, and its message, when it can be worked out while compiling, becomes part of the error:

fit: const fun(x: i32, low: i32, high: i32) -> i32{	if high < low		panic "{nameOf(fit)}'s bounds are reversed" 	if x < low		ret low 	if high < x		ret high 	ret x} fitted: const i32 = fit(1, 5, 1)Panic in 'fit': fit's bounds are reversedOn line 6 main: fun{}

A function that runs for too long or recurses too deeply also stops with an error, such as "Evaluating 'spin' takes too many steps". The same function called while the program runs fails the way any call does, with a panic.

Generic constant functions

A constant function can be generic. Since it can't use values with destructors, a type parameter it stores or copies needs copy:

twice[T: copy]: const fun(x: T) -> array[T, 2] => [x, x] pair: const array[i64, 2] = twice(7i64) main: fun{	printf("%d %d\n", i32(pair[0]), i32(pair[1]))}
Output
7 7
same[T]: const fun(x: T) -> T => x'same' requires 'T: copy' main: fun{}

Constant traits

Generic code can call a trait's members at compile time only when every type that meets the bound has constant ones. There are two ways to promise that.

Constant requirements

A trait member declared const is constant for every type that implements the trait. Each implementation must give a constant member, and built-in operators count:

Bits: trait req copy + noref{	|: const op(a: Self, b: Self) -> Self} u32: impl Bits combine[T: Bits]: const fun(a: T, b: T) -> T => a | b mask: const u32 = combine(0x10u32, 0x01u32) main: fun{	printf("%u\n", mask)}
Output
17
Bits: trait req copy + noref{	|: const op(a: Self, b: Self) -> Self} Mask: rec impl Bits{	value: u8 	|: op(a: Mask, b: Mask) -> Mask => Mask(a.value | b.value)'|' must be 'const' to match 'Bits.|'} main: fun{}

Property requirements work the same way: width: const get(imm self) -> u32 in a trait needs a constant getter, and a missing const gets "'width' must be 'const' to match 'Sized.width'". For a prop with both accessors, the message names the accessor, as in 'depth.get'.

Constant implementations and bounds

A trait whose members aren't const can still be implemented as constant. impl const promises that every member of that implementation is constant, and T: const Trait accepts only types with such an implementation. Inside the generic function, the trait's members, and those of the traits it requires, can then be called at compile time:

Score: trait{	score: fun(self) -> i32} Ranked: trait req Score{	rank: fun(self) -> i32} Gem: rec impl const Ranked{	value: i32 	score: const fun(self) -> i32 => self.value * 10	rank: const fun(self) -> i32 => self.value} Rock: rec impl Ranked{	value: i32 	score: fun(self) -> i32 => self.value	rank: fun(self) -> i32 => 0} total[T: copy + const Ranked]: const fun(x: T) -> i32 => x.score() + x.rank() gem: const i32 = total(Gem(3)) main: fun{	printf("%d %d\n", gem, Rock(5).score())}
Output
33 5

Gem's implementation is constant, so total accepts it while compiling. Rock's isn't, so it doesn't meet const Ranked. An impl const checks its members, and the traits it requires must be implemented as constant too. A default body the implementation uses is checked as a constant function in which Self's own members count as constant; one that can't be, such as a default that calls a non-constant function, is an error at the impl const:

Score: trait{	score: fun(self) -> i32	bonus: fun(self) -> i32 => plain(self.score())} plain: fun(x: i32) -> i32 => x Ranked: trait req Score{	rank: fun(self) -> i32} Gem: rec impl const Score'Score.bonus' has no 'const' default'Score.bonus' requires 'Self: copy' on line 6{	value: i32 	score: fun(self) -> i32 => self.value'score' must be 'const' in 'impl const Score'} Rock: rec impl Ranked{	value: i32 	score: fun(self) -> i32 => self.value	rank: fun(self) -> i32 => 0} total[T: copy + const Ranked]: const fun(x: T) -> i32 => x.rank() rock: const i32 = total(Rock(1))'total' requires 'T: const Ranked''Rock' doesn't implement 'const Ranked' main: fun{}

A constant implementation is only ever the written impl const: a conformance whose members happen to all be constant doesn't meet const Trait.

A plain bound works in a constant function too. abs below takes any Number. A call to it runs at compile time when the type argument has an impl const Number, and at run time for any type. Use T: const Number instead when every type the function takes must have a constant implementation:

Number: trait req copy{	abs: fun(x: Self) -> Self} i32: impl const Number{	abs: const fun(x: i32) -> i32 => match x < 0 { true => -x, else => x }} Big: rec impl Number{	value: i32 	abs: fun(x: Big) -> Big => match x.value < 0 { true => Big(-x.value), else => x }} abs[T: Number]: const fun(x: T) -> T => T.abs(x) small: const i32 = abs(-7) main: fun{	printf("%d %d\n", small, abs(Big(-3)).value)}
Output
7 3

Using abs(Big(-7)) where a constant is needed gives "Cannot evaluate 'abs' at compile time", with the hint "'Big' doesn't implement 'const Number'".

A type can implement one trait as constant and another as not. f32 below implements Float constantly and Transcendental, which requires Float, at run time only. Bounds can mix them the same way: [T: const Float + Transcendental] can use Float's members in constants and Transcendental's at run time. Naming Float there isn't redundant, since Transcendental alone doesn't make it constant.

A trait can also require another trait as constant with req const. Every type that implements it, constant or not, needs an impl const of the required trait, and a bound on it lets constant functions use the required trait's members:

Float: trait req copy{	floor: fun(x: Self) -> Self} Transcendental: trait req Float{	root: fun(x: Self) -> Self} f32: impl const Float + Transcendental{	floor: const fun(x: f32) -> f32 => x - 0.5f32	root: fun(x: f32) -> f32 => x / 3} mixed[T: const Float + Transcendental]: fun(x: T) -> T => T.root(T.floor(x)) Measure: trait req copy{	unit: fun(x: Self) -> Self} Scaled: trait req const Measure{	scale: fun(x: Self) -> Self} i32: impl Scaled{	unit: const fun(x: i32) -> i32 => x * 10	scale: fun(x: i32) -> i32 => x + 1} units[T: Scaled]: const fun(x: T) -> T => T.unit(x) tens: const i32 = units(3) main: fun{	printf("%.1f %d %d\n", f64(mixed(6.5f32)), tens, i32.scale(tens))}
Output
2.0 30 31

i32: impl Scaled makes i32's Measure constant, so unit has to be a constant function. A type with a non-constant impl Measure can't implement Scaled: "'u8' needs 'impl const Measure'".