Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Equality and Comparison

Spade provides equality operators (==, !=) as well as a full set of comparison operators (>, <, >=, <=). They work as you would expect, with some restrictions though.

For booleans only == and != are available: we can test booleans for (in)equality.

For integers, all six operators are supported, but note that the types of both sides have to match exactly (in signedness and bit-width). For example:

  • 10u4 != 12u4 yields true
  • 10u5 >= -12i5 gives a compilation error

Shifts

Spade supports logic left and right shifts (<<) and (>>) as well as arithmetic right shifts (>>>).1

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 was negative. Arithmetic right shift instead replaces the most significant bits with the most significant bits of the input. Examples of arithmetic shifts:

  • +12 in binary 0b01100 arithmetic shifted right by 2 (>>> 2) becomes 0b00011 (decimal +3)
  • -12 in binary 0b10100 arithmetic shifted right by 2 (>>> 2) becomes 0b11101 (decimal -3)
  • +3 in binary 0b00011 arithmetic shifted left by 2 (<< 2) becomes 0b01100 (decimal +12)
  • -3 in binary 0b11101 arithmetic shifted left by 2 (<< 2) becomes 0b10100 (decimal -12)

and logic shifts:

  • +12 in binary 0b01100 logic shifted right by 2 (>> 2) becomes 0b00011 (decimal +3)
  • -12 in binary 0b10100 logic shifted right by 2 (>> 2) becomes 0b00101 (decimal +5)

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 8-bit unsigned value and 123i13 is a 13-bit signed value.

Integer literals without suffix 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 2. 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.

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 , and enclosed in (). 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.


  1. Arithmetic left shift is the same operation as logic left shift.

  2. Technically, there are implementation limits that will cause problems if you try to create an integer literal with more than \(2^{32}\) bits 😉