Variables

We have seen some variables already, so this section will primarily be used to clarify a few things about them.

First, variables can be defined using let, for example

let x = 0;

Types

Spade is a strongly and statically typed language which means that every expression has a fixed and static type, and that almost all casts are explicit; the compiler will not automatically convert a bool to an int for example. Unlike languages such as C, C++ or Java though, Spade uses type inference to infer the type of variables based on its definition and use. For example, in the above example, x doesn't have a fully known type, it is a numeric value, but the exact number of bits is not known yet. However, if x is used later in a way that constrains its type, the compiler will infer it to that specific type:

fn takes_uint8(a: uint<8>) // ...

takes_uint8(x);

Again, Spade is statically typed, so conflicting types is not allowed:

fn takes_int8(a: int<16>) // ...

takes_uint8(x);
takes_int16(x); // Type mismatch. `x` was uint<8> previously but is now int<16>

In some cases, the compiler is unable to infer the type of a variable. In such cases, you can specify the type manually using : type after the variable name. For example:

let x: uint<8>: 0;

Scoping rules

Unlike most HDLs, Spade has more software-like scoping rules in the sense that variables are only visible below their definition. For example, this code would fail to compile

let x = y; // y used before its declaration
let y = 0;

this helps prevent combinational loops 1, and makes reading code easier to read as it forces its structure to be ordered "topologically" with values which depends on previous values being defined after those values.

decl

In some cases however, a hardware design requires feedback. For example, two registers which depend on each other's value. In this case, Spade has a special decl keyword which pre-declares a variable for later use.

decl y;
reg(clk) x = y;
reg(clk) y = x;

Generally, decl should be used sparringly, and unless you really know what you are doing, make sure to have a register in every "dependency loop", otherwise you will end up with combinational loops 1

1

A combinational loop is a value which depends on itself without any registers to break the dependency loop. In almost all cases, this will result in an undefined value.

Block scopes

Also like software, variables declared in a block as discussed in the previous section are local to that block and any sub-blocks.

let sub_result = {
  let x = true;

  {
    let a = !x; // Allowed, the use is in a deeper nesting than the definition
  }

};
let b = !x; // Disallowed, `x` is only visible inside the block it was declared

Variables are immutable

It is never possible to give a variable a new value. For example, as discussed in the previous chapter, you cannot write

let x = 0;
if cond {
  x = 1;
}

and you instead have to assign x to the result of an if condition:

let x = if cond {
    0
} else {
    1
}

Immutability by default is common in many modern software languages, but most allow opting out of it. Rust has the mut keyword, in javascript you can declare a variable with let instead of const, and in C-style languages you just don't declare a variable as const. However, Spade has no such feature, all variables are immutable and there is no way around that.

At this point, you may be asking if it is even possible to write anything useful with no mutable variables, or your mind may be wandering back to the initial blinky example where the value of our counter changed constantly. These two thoughts are related and the thing that ties them together is that the value of a variable is not immutable, it can change as the inputs to the circuit changes, but the subcircuit that a variable refers to is fixed forever.

As an example, in the following code

let sum = a + b;

the value of sum changes as a and b change, but sum really refers to a set of physical wire in the chip that we are compiling to -- the output of an adder that has a and b as inputs.