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

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 binary 0b01010 arithmetic shifted left by 2 becomes 0b00010

  • -12 in binary 0b10110 arithmetic shifted left by 2 becomes 0b11101

  • 2

    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 numbers
  • 0b1100_0101 for grouping binary digits
  • 0xff00_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.

3

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.