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
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.