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.

Value type

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:

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.

Literal

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.

Variable

A variable holds a single value of a specific value type. A variable's value is set and retrieved using its "unique" name.

Variable declarations

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

Permissions

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.

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:

Thus:

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

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:

Assignment

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

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

_