Guide
Variables

Variables

Variables associate a name with a value. Use the letlet keyword to declare a variable:

let x = 1;

The code above associates the name x with the value 1. Notice the name of the variable x is on the left-hand side of the = sign. x is an identifier, which is a name that can be used to refer to items. The value of the variable is on the right-hand side of the = sign. In this case, the value is 11. Since variable declarations are statements, they must terminate with a semicolon.

We can then use the name x to refer to the value 11:

// x + x is 1 + 1 which is 2
assert_eq(x + x, 2);
💡

What's an identifier?

Identifiers

An identifier is a name that can be used to refer to items. Only certain characters are allowed in identifiers. They must start with a letter or underscore, and then they can be followed by any number of letters, numbers, or underscores. Identifiers are case-sensitive, so x and X are different identifiers. Identifiers must also not be reserved keywords, which are words that have a special meaning in the language. For example, let is a reserved keyword, so it cannot be used as an identifier.

Actually, identifiers can also contain characters from a subset of Unicode characters (including emojis), but this is not advised so we won't cover it here.

Here are some examples of valid and invalid identifiers:

// Valid identifiers
x
X
name
first_name
firstName
name2
_name
_0
_
 
// Invalid identifiers
2name // cannot start with a number
8ball
first-name // cannot contain a dash
first name // cannot contain a space
else // cannot use reserved keywords

Raw identifiers

Sometimes you want to use a reserved keyword as an identifier. Or, you want to use a character that is not allowed in identifiers. In this case, you can use a raw identifier. A raw identifier is a restricted version of a string-literal that is surrounded by backticks. For example, the following code uses a raw identifier to declare a variable named else:

let `else` = 1;

Raw identifiers are generally not recommended, but they can be useful in some cases.

Naming conventions for variables

Variable names should be descriptive. For example, if you are storing a person's name, you should name the variable name or person_name instead of a single letter or abbreviation such as n. All variables should be named in snake_case, which means all lowercase letters with underscores separating words. For example, first_name is a good variable name, but firstName is not.


Assignment

We can also change the value of a variable after it has been declared. This is called assignment. To assign a new value to a variable, use the = operator:

let mut x = 1;
x = 2;
 
assert_eq(x, 2);

Variables are immutable by default in Terbium. When an immutable variable is declared, its value cannot be changed. Immutability by default is a feature of Terbium that helps prevent bugs.

Notice that we used the mut keyword when declaring the variable. This explicitly marks the variable as mutable, allowing us to change its value later. If we try to assign a new value to an immutable variable, we will get an error:

This will not compile
let x = 1;
x = 2;

Assignments as expressions

Assignments are expressions, which means they result in a value. The value of an assignment is the value that was assigned. For example, the value of the expression x = 2 is 2. This means we can use assignments in other expressions:

let mut x = 1;
let y = x = 2;
 
// Both x and y are 2
assert_eq(x, 2);
assert_eq(y, 2);
 
assert_eq(1 + (x = 3), 4);
assert_eq(x, 3);

Compound assignment operators

Terbium has a number of compound assignment operators that combine assignment with other operators. For example, the += operator adds a value to a variable and assigns the result back to the variable:

let mut x = 1;
x += 1;
 
assert_eq(x, 2);

x = x + yx = x + y has the same effect as x += yx += y. The following table lists all of the compound assignment operators:

Assignment operatorEquivalent operator
+=+
-=-
*=*
**=**
/=/
%=%
&=&
|=|
^=^
<<=<<
>>=>>
||=||
&&=&&

Shadowing

We can also declare a new variable with the same name as an existing variable. This is called shadowing. We can shadow a variable by using the same variable’s name and repeating the use of letlet:

let x = 1;
let x = "hello";
 
assert_eq(x, "hello");

The code above compiles. Shadowing is different from a mutable variable because a compile-time error occurs if we accidentally try to reassign to this variable without using the letlet keyword.

By using letlet, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

Type enforcement

Variables can hold values of different types, but once a variable is declared, its type cannot change. Learn more about types in the Data Types section.

This means that shadowing is also useful when we want to change the type of a variable without changing its name. As seen in the example above, we changed the type of x from an integer to a string. This is different from mutability, which only allows us to change the value of a variable, not its type. A compile-time error occurs if we try to change the type of a variable without using shadowing:

This will not compile
let mut x = 1;
x = "hello";

Declaring vs Initializing

When we declare a variable using letlet, we can optionally initialize it to a value. If we do not initialize it, the variable will be considered uninitialized and the compiler will forbid you from using it until it is initialized.

What does it mean for a variable to be initialized? Well, when we wrote let x = 1;let x = 1;, we provided a value 11 to be associated with the variable x. This is called initializing the variable providing it with an initial value.

However, we can also declare a variable, but not initialize it. This means that we are not providing it with an initial value. This is useful when we want to declare a variable, but we do not yet know what value it will hold. An uninitialized variable can be declared by using the letlet keyword without a value:

let x;

Then, you can initialize it later by using assignment:

x = 1;

Notice that the mutmut was not used when declaring x, even though we are assigning to it later. This is because the compiler can guarantee that the user has not used x (it is impossible to do so without initializing it first), so there is no need to enforce explicit mutability. The mutmut keyword is only needed when we want to change the value of an already initialized variable.

Lexical Scope

Variables are scoped to the block in which they are declared. This means that a variable is only accessible within the block in which it is declared. This is called lexical scoping.

Inclusion of line 12 will cause this to not compile
let x = 1;
let y = 2;
// Surround a block of code with curly braces to create a new scope
{
    let x = 3;
    let z = 4;
    assert_eq(x, 3); // refers to the x declared on line 5
    assert_eq(y, 2); // refers to the y declared on line 2
} // z goes out of scope here
 
assert_eq(x, 1); // refers to the x declared on line 1
assert_eq(z, 4); // error: z is not defined

Constants

We can declare a constant variable by using the constconst keyword instead of letlet. Constants are similar to variables in that they let us associate a name with that value.

Constants are declared using the constconst keyword instead of letlet:

const PI = 3.14;

Constants are useful for values that we know will never change, such as mathematical constants or configuration values.

So what's the difference? Why would you use a constant instead of a variable? Well, constants are different from variables in that:

  • Constants are always immutable, not just by default.
  • Constants are always initialized.
  • Constants cannot be shadowed.
  • Constants can be declared in any order, as long as they aren't cyclic.
  • Constants are evaluated at compile-time, so no runtime overhead is incurred by calculating the value.

That's right, constants are a compile-time construct. This means that the value of a constant must be known at compile-time. This means that constants can only be initialized with constant expressions, which are a subset of expressions that can be evaluated at compile-time, not the result of a value that could only be computed at runtime.

Constants are restrictive in this way, but they are also more powerful. Because constants are evaluated at compile-time, there is no runtime overhead for using them. This also means that constants can be used in places where variables cannot, such as in type declarations.

Aliases

Expression-aliases associate a name with an expression, however they are not evaluated but rather inlined at compile-time. Defining an expression-alias is similar to defining a constant, with the aliasalias keyword:

alias PI = 3.14;

Expression-aliases are useful for reducing code duplication, and for giving names to complex expressions, however they should only be used when a constant or immutable variable does not make sense to be used instead. Since aliases are not evaluated, they do not have a type and are not checked for correctness.

Aliases are also unhygenic, meaning that they are not lexically scoped:

alias X_PLUS_Y = x + y;
 
let x = 5;
let y = 3;
assert_eq(X_PLUS_Y, 8);
 
// in another scope...
{
    let x = 1;
    let y = 2;
    assert_eq(X_PLUS_Y, 3);
}

This can lead to unexpected behavior, which is why aliases should be used sparingly.

Syntax sugar

Syntax sugar is a term used to describe a syntax feature that desugars, or "expands" or is "rewritten" into another, more verbose syntax.

When we use a constant, the compiler will desugar it into the value it represents. For example, when we compile the following code:

const PI = 3.14;
const TAU = 2 * PI;
 
println(TAU);

The compiler will desugar the code into:

const PI = 3.14;
const TAU = 6.28; // evaluated 2 * 3.14 at compile-time
 
println(6.28); // replaced TAU with its value

And then compile the desugared code.

With the knowledge of what desugaring is, the difference between constants and aliases becomes clear. While constants inline the evaluated value, aliases inline the expression itself. For example, when we compile the following code:

alias PI = 3.14;
alias TAU = 2 * PI;
 
println(TAU);

The compiler will desugar the code into:

alias PI = 3.14;
alias TAU = 2 * 3.14; // inlined 3.14
 
println( (2 * 3.14) ); // replaced TAU with its expression

It's also why aliases are unhygenic, because they are not evaluated and are inlined as-is.

Aliased expressions are implicitly wrapped in parentheses to prevent ambiguity. For example:

alias X = 1 + 2;
 
assert_eq(X * 3, 9); // desugars to: assert_eq((1 + 2) * 3, 9);