logo Spade

The Spade programming language

A hardware description language inspired by modern software languages like Rust.

  • Pipelines as a language feature
  • Strong Rust-inspired type system
  • A helpful compiler with great error messages
  • Abstractions for common hardware constructs
  • Combinatorial logic by default, with explicit registers
  • Cute mascot
// A 3 stage CPU supporting Add, Sub, Set and Jump
pipeline(3) cpu(...) -> Option<int<32>> {
        let stall = stage(+1).is_jump;
        let pc = inst program_counter(
            clk, rst,
            stall,
            stage(execute).jump_target
        );
    reg;
        let insn = inst read_progmem(clk, pc);
        let is_jump = is_jump(insn);
        let insn = if stage(+1).is_jump {NOP} else {insn};
    reg;
        'execute
        let (opa, opb) = inst regfile(
            clk, insn, stage(writeback).result
        );

        let jump_target = match insn {
            Instruction::Jump(target) => Some(target),
            _ => None;
        };

        let alu_result = alu(insn, opa, opb);
    reg;
        'writeback
        let result = match insn {
            Instruction::Add(_, _, _) => Some(alu_result),
            Instruction::Sub(_, _, _) => Some(alu_result),
            Instruction::Set(_, _) => Some(alu_result),
            Instructions::Jump(_) => None
        };
        result
}

Spade

Spade is a new hardware description language which makes hardware description easier and less error-prone. It does this by taking lessons from software programming languages, and adding language level support for common hardware constructs, all without compromising low level control over what hardware gets generated.

Key Features

Language Level Pipelines

Pipelines are a first class construct in Spade, making re-timing and re-pipelining trivial. The reg separates code into stages, meaning you never have to manually define pipeline registers, and that behavior is separated from physical implementation.

When you need to update your design to meet timing, you can simply add or move reg statements. The compiler is smart enough to figure out when these changes affect the timing of instantiating modules and tells you where to update your code to keep the original behavior.

pipeline(4) X(clk: clk, a: int<32>, b: int<32>) -> int<64> {
        let product = a * b;
    reg * 3;
        let result = f(a, product);
    reg;
        result
}

For more complex pipelines with feedback, such as a processor data path, re-timing becomes less trivial, but being able to reason about signals in stages instead of individual registers is still very helpful.

Types and Enums

Spade has a powerful type system with structs, arrays, tuples and sum types called enums. A strong type system makes interoperability with external or internal modules easier, and means you can refactor your code with confidence. The compiler will tell you where your changes affect the behavior of your code.

Enums are of particular importance: unlike the enums of C and Verilog, they can have associated payload.

You can model a value which may or may not be available with the Option enum:

enum Option<T> {
    Some(val: T),
    None
}

Or why not model the instruction set of a CPU?

enum Insn {
    Set { dreg: int<5>, val: int<32> },
    Add { dreg: int<5>, lhs: int<5>, rhs: int<5> },
    Sub { dreg: int<5>, lhs: int<5>, rhs: int<5> },
    Jump { target: int<32> }
}

The compiler ensures that fields can only be accessed if the value is of the correct type. For example, if the instruction is a Jump, there is no way to access a dreg field.

Pattern Matching

Enums work really well with pattern matching which allows you to check conditions and easily bind sub-values to variables.

You can easily build an ALU:

fn alu(insn: Insn, rs1: int<32>, rs2: int<32>) -> int<32> {
    let adder_result = match insn {
        Insn::Add(_, _, _) => rs1 + rs2,
        Insn::Sub(_, _, _) => rs1 - rs2,
        Insn::Set(_, val) => sext(val),
        Insn::Jump(_) => 0,
    };

    trunc(adder_result)
}

Or select the first of two values, and 0 if none are present:

let result = match (a, b) {
    (Some (x), _) => x,
    (_, Some(x)) => x,
    _ => 0
}

The compiler makes sure that all cases are covered. For example, if you add a new instruction to the Insn type, it will force you to deal with that case in the ALU.

Type inference

Spade has powerful type inference which gives you the benefits of static types, without all the typing.

Great Error Messages

The compiler should be your friend. Error messages give you as much information as possible

error: Match branches have incompatible type
30 +-Insn::Add(_, _, _) => rs1 + rs2,
   |                       ^^^^^^^^^
   |                       This branch has type int<33>
32 | Insn::Set(_, val) => val,
   |                      ^^^ But this one has type int<32>
   |
   = Expected: 33 in: int<33>
   =      Got: 32 in: int<32>

Bad error messages are considered a bug. Please report them!

Helpful tooling

Spade comes with a great set of tools around the language

  • The official build tool Swim manages your dependencies, calls synthesis tools and runs your tests. It can even create a new project for you with a single command!
    • Of course you can put the generated Verilog into your regular build flow as well.
  • Tests are written in cocotb allowing you to take advantage of python for all your testing needs. When you need higher performance tests, you can also use verilator
  • VCDs coming out of tests are automatically translated to include Spade type information so you never have to decode individual bits in your waveform.

Planned features

There are also some planned features:

  • Integer ranges as types.
  • Generics with traits
  • Clock domain information on types.
    • Mixing domains without explicit synchronization is a compilation error.
    • Related: clock domain inference where the domain is obvious.
  • And more...

Using Spade

To get started, read the (work in progress) spade book.

For an overview of what Spade is, have a look at this talk from OSDA 2023. Documentation is very much a work in progress, but some is available in our book.

Spade is in its early stages, so everything is subject to change. You can build things with it but be prepared for bugs and missing features.

Learn more

Feel free to follow the development either on Gitlab or in our Discord community server.

Publications

To cite Spade itself, use the zenodo record.

Talks

Development

Linköping University logo

Spade is currently being developed as an Open Source project at the Division of Computer Engineering, Department of Electrical Engineering, Linköping University, Sweden.

License

The Spade compiler and other tooling is licensed under the EUPL-1.2 license. The Spade standard library and this website is licensed under the terms of both the MIT license and the Apache license.