Cella Documentation 0.1.0-dev Home Source

Variables and constants

Variables hold values a program works with. This chapter covers the three kinds of declaration, val, var and const, how a variable's type is chosen, variables that start without a value, assignment, scopes, and variables and constants declared at module level.

val and var

val declares a variable that can't be changed after it gets its value. var declares one that can:

main: fun{	val width = 4	var total = 0	total = width * 3	total = total + 1	printf("%d %d\n", width, total)}
Output
4 13

Assigning to a val, or to any part of one such as a field of a record or an element of an array, is an error:

Point: rec{	x: i32	y: i32} main: fun{	val limit = 5	limit = 6Cannot reassign values	val origin = Point(0, 0)	origin.x = 1Cannot reassign values	val values = [1, 2, 3]	values[0] = 4Cannot reassign values}

Use val unless the variable needs to change. A var that is never changed gets a hint saying so, shown when the build is given --hints (see Projects and building):

Hint at main.ce:5:64 │ {5 │     var count = 1            ╘═══╛ 'count' is never changed

Types

A type annotation after the name, separated by a colon, says what type the variable has. The value must convert to it:

main: fun{	val small: u8 = 200	val wide: i64 = 7	val ratio: f32 = 0.25	printf("%d %lld %.2f\n", small, wide, ratio)}
Output
200 7 0.25
main: fun{	val ready: bool = 1Cannot convert type 'integer literal' to 'bool'}

Numbers and arithmetic covers the number types and which literals fit in them, and Conversions covers which values convert to which types.

Inferred types

Without an annotation, the variable takes the type of its initial value. A value that already has a type, such as the result of a call or another variable, gives it that type.

A literal on its own, like 10 or 0.5, doesn't have a fixed type yet. A variable that starts from one takes its type from how the variable is used later in the function. When nothing decides it, an integer literal makes an i32 and a float literal an f64:

half: fun(value: i64) -> i64 => value / 2 main: fun{	val count = 10	val ratio = 0.5	val total = 7	printf("%lld\n", half(total))	var level = 0	val step: u16 = 3	level = step	printf("%zu %zu %zu %zu\n", sizeOf(count), sizeOf(ratio), sizeOf(total), sizeOf(level))}
Output
3
4 8 8 2

count and ratio are only printed, so they keep the default types. total is passed where an i64 is expected, so it is an i64. level is given a u16, so it is a u16.

The rules are:

  • Where the variable is read as a certain type, such as an argument or the value of a typed variable, it takes that type. When it's read as several types, it takes the one of them that converts to all the others.
  • Values assigned to it must convert to that type.
  • A variable that is only assigned to takes the type that all the assigned values convert to.

When no type works, the declaration is an error that lists the types that disagree:

wide: fun(value: u16) -> i32 => 0signed: fun(value: i16) -> i32 => 0 main: fun{	val count = 7Cannot infer the type of 'count' from 'u16' and 'i16'	wide(count)	signed(count)}

A declaration needs a type or a value. With neither, there is nothing to take a type from:

main: fun{	var totalCannot infer the type of 'total'}

Variables without a value

A var declared with a type and no value starts as all zeros, when zeros are a valid value of its type. That holds for numbers, bool, pointers, arrays of such values, and records made of them that don't declare their own constructors:

Point: rec{	x: i32	y: i32} main: fun{	var count: i32	var ready: bool	var origin: Point	var grid: array[i32, 3]	count += 1	grid[1] = 5	printf("%d %d %d %d %d %d\n", count, ready, origin.x, origin.y, grid[0], grid[1])}
Output
1 0 0 0 0 5

A type whose values are made by its own constructors has no zero value of its own (see Records). A variable of that type starts uninitialized, and it must be assigned before it's used:

Celsius: rec{	degrees: f64 	new: op(degrees: f64)	{		self.degrees = degrees	}} main: fun{	var room: Celsius	val reading = room.degreesCannot use uninitialized values}

Assigning a val later

A val declared without a value gets it from a later assignment. Every path through the function must assign it exactly once before it's read. Here the paths are the two branches of an if (Control flow covers if):

describe: fun(score: i32) -> i32{	val bonus: i32	if score > 50		bonus = 10	else		bonus = 0 	ret score + bonus} main: fun{	printf("%d %d\n", describe(70), describe(20))}
Output
80 20

Reading it before it's assigned, reading it when only some paths assign it, and assigning it twice are errors:

early: fun -> i32{	val x: i32	ret xCannot use uninitialized values} maybe: fun(flag: bool) -> i32{	val x: i32	if flag		x = 1 	ret xCannot use uninitialized values} twice: fun -> i32{	val x: i32	x = 1	x = 2Cannot reassign valuesAssigned on line 21	ret x} main: fun{}

The last error also points at the first assignment, with a note "Assigned on line" and its line number.

undef

undef as the initial value leaves a variable uninitialized on purpose, even when its type has a zero value. It saves the cost of zeroing storage that is about to be filled anyway:

main: fun{	var squares: array[i32, 4] = undef	for i in 0..4		squares[i] = i32(i * i) 	printf("%d %d\n", squares[1], squares[3])}
Output
1 9

The compiler checks that undef storage is written before it's read:

main: fun{	var count: i32 = undef	count += 1Cannot use uninitialized values}

var x = undef without a type takes its type from later uses, like a variable that starts from a literal. undef is only allowed as the initial value of a variable:

take: fun(value: i32){} main: fun{	take(undef)Cannot use 'undef' outside variable initializers}

Pointers and raw memory shows how to fill undef storage through a pointer.

Assignment

= stores a new value in a variable, a field or an array element. The compound assignments +=, -=, *=, /=, %= and the others combine an operator with assignment: total += 4 is total = total + 4, with total evaluated once. Numbers and arithmetic lists them all.

An assignment is also an expression. Its value is the variable after the assignment, so assignments can be chained and used as arguments:

main: fun{	var x = 0	var y = 0	x = y = 10	var count = 1	printf("%d %d %d\n", x, y, count += 4)	printf("%d\n", count)}
Output
10 10 5
5

Assignment groups from the right, so x = y = 10 assigns 10 to y and then y to x. Assigning a new value to a variable whose old value has a destructor runs that destructor; Ownership and destructors covers that.

Constants

const declares a constant: a value computed while compiling. Its initial value must be built from literals, other constants, operators on them and calls to constant functions:

main: fun{	const rows = 3	const cells = rows * 4	val grid: array[i32, cells] = undef	printf("%zu\n", grid.length)}
Output
12

Constants can be used where a value must be known while compiling, such as the length of an array type. A val can't, even when its value is a literal: a val is storage that holds a value at run time, and a constant isn't.

main: fun{	val rows = 3	val grid: array[i32, rows] = undef'rows' is not a constant}

A constant needs a value, the value must be a constant, and its type can't have a destructor:

Buffer: own rec{	id: i32 	drop: op	{	}} main: fun{	val n = 2	const m = n + 1The initial value of 'm' must be a constant	const later: i32Cannot declare constants without values	const handle = Buffer(1)Cannot store values with destructors in constants}

A constant has no address, so @ can't be applied to it (see Pointers and raw memory):

Cannot take the address of constants

Scopes

A variable can be used from its declaration to the end of the block it's declared in. A block is a list of statements in { }, such as a function body, the body of an if or a loop, or a block written on its own:

main: fun{	{		val inner = 1		printf("%d\n", inner)	} 	printf("%d\n", inner)Symbol 'inner' not found in this scope}

When a block ends, its variables are dropped, in the reverse order of their declarations. Ownership and destructors covers what that means for values with destructors.

Shadowing

A variable in an inner block may have the same name as one outside it. Inside the block, the name means the inner variable. After the block, it means the outer one again. The inner variable doesn't have to have the same type:

double: fun(value: i32) -> i32{	val value = value * 2	ret value} main: fun{	val x = 1	{		val x = "inner"		printf("%s\n", x)	} 	printf("%d %d\n", x, double(4))}
Output
inner
1 8

A function's body is a block inside its parameters, so a variable in the body can reuse a parameter's name, as double does. The new variable's initial value still sees the old name, since the new one only starts after its declaration.

Two variables with the same name in the same block are an error, reported at both:

main: fun{	var total = 1'total' is declared more than once in this scope	var total = 2'total' is declared more than once in this scope}

Module-level variables and constants

Variables and constants can also be declared at the top of a file, outside any function. They are written like other declarations, with the name first: name: var Type = value, name: val Type = value or name: const Type = value. The type and the value are both required. Every function in the module can use them:

maxPlayers: const i32 = 4startingLives: val i32 = 3joined: var i32 = 0 join: fun -> i32{	joined += 1	ret joined} main: fun{	join()	join()	printf("%d of %d, %d lives\n", joined, maxPlayers, startingLives)}
Output
2 of 4, 3 lives
  • A const is a constant, as inside a function.
  • A val is storage that can't be written.
  • A var is storage that any function can change. It lives for the whole run of the program.

Their initial values are set before the program starts, so each one must be a constant, for a var and a val too:

seed: var i32 = rand()The initial value of 'seed' must be a constantlives: val i32 = 3doubled: val i32 = lives * 2The initial value of 'doubled' must be a constant main: fun{}

Declaration order doesn't matter at module level. A constant can use one declared further down, as long as no constant depends on its own value:

doubled: const i32 = base * 2base: const i32 = 3 main: fun{	printf("%d\n", doubled)}
Output
6
first: const i32 = second + 1'first' depends on its own valuesecond: const i32 = first + 1'second' depends on its own value main: fun{}

A local variable may have the same name as a module-level one. Inside its scope, the name means the local variable.

Module-level declarations can be made visible to other modules or projects; Modules and visibility covers that. Constants and variables declared inside a type belong to the type; Methods and properties covers them.