Basic Expressions and Primitive Types
Expressions are the fundamental building block of Spade code. Anything with a
value is an expression - from an integer literal like 5
to arithmetic
operations like +
all the way up to blocks which at the end of the day
consist of several sub-expressions.
Integers and booleans
Like most languages, Spade has a few primitive types that basic operations are applied
to. The most common primitive types in Spade are bool
, int
and uint
which are
booleans, signed integers, and unsigned integers respectively. When building custom
hardware, we are not restricted to integers of a few fixed sizes like 8, 16 and 32 bits,
so both int
and uint
take a generic parameter that specifies its size. For example
uint<8>
or int<10>
.
Sometimes you will also encounter an error talking about Number
. This is a special type
which the compiler uses until it can figure out if a number is signed or unsigned. This
will become more relevant later when we talk about type inference
Operators
Spade's operators are generally the same as any C-like language both in terms of which operators are available and their precedence.
Arithmetic
To start off, Spade naturally has operators for arithmetic +
, -
, *
.
These prevent overflow by extending the output to guarantee that the result
fits. For addition and subtraction this means that the output is one bit larger
than the input and the input operands have to be the same size. For
multiplication, the output size is the sum of the input sizes.
It is often necessary to change the number of bits to accommodate this. The
sext
function sign extends signed integers, the zext
function zero extends
unsigned integers, and the trunc
function truncates (removes bits) both
signed and unsigned integers.
Logic
Spade supports logic not (!
), and (&&
), or (||
), as well as xor
(^^
) as well as the corresponding bitwise operators (~
, &
, |
, and ^
).
However, Spade does not allow implicit casts between integers and bool
, so using
a bitwise operator on a bool
or a logic operator on an integer is not possible.
Comparison
The comparison operators (==
, !=
, >
, <
, >=
, <=
) work as you would expect.1
with one small caveat, they can only be used on integers for now.
Shifts
Spade supports logic left and right shifts (<<
) and (>>
) as well as
arithmetic right shifts (>>>
).2
Arithmetic right shifts may be unfamiliar, so here is a short explanation of what it
does: When you right shift a value, the most significant bit needs to be filled in. With
a logic shift, this is done by a 0
. A consequence of this is that the sign of the shifted
value flips if it is negative. Arithmetic right shift instead replaces the most significant bits
with the most significant bits of the input. For example
-
+12
in binary0b01010
arithmetic shifted left by 2 becomes0b00010
-
-12
in binary0b10110
arithmetic shifted left by 2 becomes0b11101
Arithmetic left shift is the same operation as logic left shift.
Division and Modulo
Spade also has division and modulo operators, but because division and modulo by non-powers of two is more
expensive to implement than the arithmetic operations, the /
and %
operators can
only be used to divide by powers of two. With the std::ops::comb_div
function
being used if you absolutely need division anyway which the compiler helpfully informs you about.
error: Division can only be performed on powers of two
┌─ src/blinky.spade:10:24
│
10 │ count > duration / 3
│ ^ Division by non-power-of-two value
│
= help: Non-power-of-two division is generally slow and should usually be done over multiple cycles.
= If you are sure you want to divide by 3, use `std::ops::comb_div`
│
10 │ count > duration `std::ops::comb_div` 3
│ ~~~~~~~~~~~~~~~~~~~~
Integer Type Conversion
As mentioned previously, to cast a number to a lower number of bits, the
trunc
function is used, while sext
and zext
are used to add bits to
signed and unsigned integers respectively. In order to convert between signed
and unsigned types, the .to_int()
and .to_uint()
methods can be used.
Numbers
Numbers can be written in decimal without a prefix, in hexadecimal with a 0x
prefix,
and in binary with a 0b
prefix. You can also use _
in numbers to split up groups to
make them more readable. For example
1_000_000
for big numbers0b1100_0101
for grouping binary digits0xff00_1234
for grouping hexadecimal digits
You can also add a uN
or iN
suffix to numbers to specify their sign and size. For
example, 10u8
is a 10 bit unsigned value and 123i13
is a 13 bit signed value.
Integer literals without prefix do not have a size on their own, and unlike Verilog and VHDL in which integer literals are limited to 32 bits by default, Spade allows arbitrarily large integers 3. The compiler also guarantees that the value will be representable by the type it is used as. For example,
let x: uint<8> = 512;
will result in a compilation error.
Technically, there are implementation limits that will cause problems if you try to create an integer literal with more than \(2^{32}\) bits 😉
Booleans
Boolean literals are as you would expect: true
and false
Tuples and Arrays
Like many languages, Spade supports compound types in the form of arrays and tuples. Arrays are used when you want several values of the same type to process together, tuples are used when you want to group values of different type into one group.
Arrays are written as a list of values enclosed in []
, for example [1, 5, 3, x, y]
. You can also create arrays of N
copies of the same value using
[value; N]
. For example, an array of 10 zeros is [0; 10]
.
To access individual elements, use array[x]
where x
is an unsigned int.
You can also use array[N:M]
to access sub-arrays. These are inclusive on the
left and exclusive on the right, so [0, 1, 2, 3, 4, 5][1:5]
results in [1, 2, 3, 4]
. Range indices must be constant values while individual element
indices can be runtime values.
Tuples are written as values separated by ()
. For example (10, x, false)
.
Tuple elements can be accessed using the #
operator, for example (10, x, false)#0
is 10
. Most of the time, accessing tuples through pattern matching (destructuring) is more convenient. We will talk more about pattern matching later, but for now you can write
let (x, y, z) = some_tuple;
which will make x
take on the value of the first element, y
the second and z
the third.