This chapter talks about the term: a literal, a variable, or a pseudo-variable (e.g., 'this' or 'self'). Each term references a single value of a specific type. The term is the basic building block for an expression.
You might be tempted to skip over this chapter, because it seems to cover concepts familiar to you from other languages. That is somewhat, but not entirely, true.
Importantly, this chapter introduces the distinction that Cone makes between different kinds of types: value, permission, allocator and lifetime. This fundamental distinction impacts many aspects of the language, including how variables are declared and how assignment works.
A value type specifies how a value is encoded inside the computer. For example, an integer is encoded differently than a collection of characters. Value type also establishes what sort of things you can do with a value (e.g., add two numbers together).
Cone provides a number of built-in value types. Later on, we will describe how to define new compound value types using type constructors like struct, array, pointer, class, interface, etc.
The names of value types are capitalized by convention. This helps cleanly separate the global namespace between value types and variables, reducing unexpected naming conflicts and improving code clarity. One notable exception to this naming convention is the type names for the primitive numeric types:
- i8, i16, i32 and i64 are the names of types for signed integers of the specified bit-size.
- u8, u16, u32 and u64 are the type names for unsigned integers.
- f32 and f64 are the type names for floating point numbers.
Although there are other kinds of types (e.g., allocator or permission type), when we talk about the type of a value, we are typically referring to its value type.
A literal is a specific unchangeable value, as represented by a number token, string token, character token, or the reserved names null, true or false. For example (with comments to indicate each literal's type):
123 // i32: signed 32-bit integer 3.4 // f32: 32-bit floating point number "Hello" // String 'a' // u32: utf-8 character code false // Bool null // Null (represents the absence of a value)
Numeric literals default to the appropriate signed, 32-bit value type. If a different value type is desired for a number, simply append the desired type to the number (e.g., 65u8 is an unsigned 8-bit byte literal).
The presence of a decimal point, exponent or floating point type suffix signals that a number is a floating point literal. d may be used instead of f64 to specify a 64-bit floating point number.
Note All literals are flagged as immutable. Any attempt to modify them will generate an error message. This ensures literals can never be corrupted by any downstream code.
A variable holds a single value of a specific value type. A variable's value is set and retrieved using its "unique" name.
Variables must be declared prior to use. The purpose of a variable declaration is to specify its value type, permission, and scope (lexical lifetime) in advance of its use later in the program. Doing so provides invaluable context to the variable's use, allowing the compiler to optimize execution performance and memory use, as well as enforce safety constraints.
A variable declaration statement typically begins with either imm or mut followed by the variable name (an identifier token) and then the value type. For example:
imm height f32 // height holds a floating point number
The difference between imm and mut rests with the permission you are granting the variable. A variable's permission enables and constrains how it may be used, properties which can help improve a program's safety and comprehensibility. The compiler enforces a permission's constraints on every use of the variable.
- imm (immutable) declares that the variable's value will never change after it is initialized.
- mut (mutable) allows a variable value to be changed after initialization.
Cone supports a number of other permissions in addition to imm and mut. The Permissions chapter offers a more thorough treatment of all permissions, the capabilities they enable and deny, and the implications for using each. There is no one perfect permission that can do everything. Each has its advantages and limitations. That said, imm and mut are a good starter set.
Scope, Lifetime and Allocator
The location of the variable declaration statement implicitly determines the variable's scope. This scope establishes several things about the variable: which part of memory it resides within, how long the it lives (its lexical lifetime) and which parts of a program's code can access it.
There are two key places where a variable can be located:
- global variables are declared outside of all function blocks. Space for them is allocated automatically when a program is loaded for execution. Global variables are shared across and accessible by any function at any time.
- local variables are declared within a function block.
Every time a function is called, space is allocated on the current stack
for all its declared local variables.
Local variables are exclusive to that call. Two identically-named local variables in different functions, or even two calls to the same function, will not collide with each other. Local variables provide a working state for the function. When the function is finished and returns to its caller, its local variable space is automatically freed from the stack.
imm glowy i32 // Global immutable variable fn a_function() mut loco f32 // Local mutable variable
The lifetime of global variables is the same as the program's lifetime. Global variables come into being when the program starts and disappear when the program stops.
By contrast, the lifetime of a local variable is bounded by the inner-most lexical block the variable is declared within. (Additional indentation of lines typically signals entrance into a new inner block.) The variable is unknown outside its declared scope; any attempt to access it outside its lexical block will fail.
fn a_function() mut loco f32 while true mut x i32 // indentation shows x's lifetime is bounded by while block x // Error!! x is not accessible here outside the while block
As a side note: Cone considers global and local to be allocators. Cone has other allocators which focus on heap and reference pointers. The Pointers and Allocators chapter offers an extensive treatment of allocators and reference pointers. It explains how variable lifetimes can constrain the aliasing and use of variables and reference pointers.
Pseudo-variables look like variables, but are not, as their value is managed by Acorn based on the context where they are encountered. They need not be declared, as the compiler already knows their value type, scope, allocator, and permission (usually imm).
Two commonly used pseudo-variables are:
- self, the value which is passed to a method, indicating what the method is supposed to be acting on.
- this, the value that is the focus of attention within a 'this' block.
The value of a variable is set using an assignment. Variables placed to the left of the assignment operator = set the variable's value. Variables found to the right (or most anywhere else) retrieve the value of the variable.
height = 3.4 // height's value is now 3.4 depth = height // weight's value is now 3.4 number = 42 // number's value has discovered the meaning of life
Assignment type checking
In order for an assignment to be valid, the variable on the left has to be mutable and the value types have to match on both sides of the =. The most obvious way for them to match is if they are the same. However, in some cases it is possible for them to be different and still match, as happens with automatic number conversion and subtyping.
Copy, move and reference pointers
For most value types, the assignment operator stores a copy of the value on the right into the variable on the left.
However, if the value type is a reference pointer (or contains one), aliasing rules kick into effect. Depending on the pointer's information regarding permissions, allocators, lifetime and value types, the assignment might be invalid (with a compiler error), trigger a change of ownership, or be wrapped in aliasing/dealiasing logic specific to the allocator. These rules are detailed in the Pointers and Allocators chapter.
Variable declaration initialization
Variable declarations permit assignments as part of the declaration. These assignments establish the initial value of the declared variable:
mut height f32 = 5.2
Variable declarations offer one more trick up their sleeve: type inference. If typing information is omitted on the left-hand variable, the variable's value type is declared to be the value type of the expression to the right.
mut height = 5.2
Parallel assignment makes it possible to simultaneously assign multiple values to multiple variables. This can be convenient when swapping the values held by two variables:
// Swap the variable values for a and b a,b = b,a