logo Spade Blog

Spade 0.9.0

Posted 2024-07-04 by The Spade Developers

Happy summer everyone, today we're releasing v0.9.0 of Spade. This is one of our biggest releases yet and packs several really exciting improvements, especially around writing generic code.

As always, this blog post highlights the most exciting changes and you can read the whole change log here.

Generic impl blocks🔗

Spade has had impl blocks for a while, but until now they have only been usable on non-generic types limiting their usefulness. With 0.9.0 you can now write generic impl blocks such as the new standard library addition is_some

impl<T> Option<T> {
  fn is_some(self) -> bool {
    match self {
      Some(_) => true,
      None => false,
    }
  }
}

Thanks to Alex, Rene and Fabian for implementing this as part of their bachelor project at JKU!

Where clauses🔗

You can now use where clauses to constrain integer parameters in generic units. For example, it is now possible to define a function with the same type signature as the + operator for integers:

fn add<#In, #Out>(x: int<In>, y: int<In>) -> int<Out>
  where Out: In + 1

Fixed point library🔗

Combining generic impl blocks and where clauses, it is now possible to start working on a fixed point math library in Spade

As an example, here are the implementations of various arithmetic operators from that library

struct Fp<#Size, #FracBits> {
    inner: int<Size>
}

impl<#Size, #FracBits> Fp<Size, FracBits> {
    fn add<#OutSize>(self, other: Fp<Size, FracBits>) -> Fp<OutSize, FracBits>
        where OutSize: Size + 1,
    {
        Fp(self.inner + other.inner)
    }

    fn sub<#OutSize>(self, other: Fp<Size, FracBits>) -> Fp<OutSize, FracBits>
        where OutSize: Size + 1,
    {
        Fp(self.inner - other.inner)
    }

    fn mul<#OutSize, #OutBits>(self, other: Fp<Size, FracBits>) -> Fp<OutSize, OutBits>
        where OutSize: Size+Size,
              OutBits: FracBits+FracBits
    {
        Fp(self.inner * other.inner)
    }

    fn abs<#OutSize>(self) -> Fp<OutSize, FracBits>
        where OutSize: Size + 1
    {
        if self.inner < 0 {
            Fp(0 - self.inner)
        } else {
            Fp(sext(self.inner))
        }
    }
}

Named turbofish arguments🔗

The final improvements to generics in this release is named turbofish arguments. The turbofish (::<>) is used to specify generic parameters if they cannot be inferred, however, previously you had to specify them positionally which gets confusing if you have a complex generic, for example1:

  fn mul_fp<#Size, #FracBits, #OutSize, #OutBits>(self, other: Fp<Size, FracBits>) -> Fp<OutSize, OutBits>
    where OutSize: Size+Size,
          OutBits: FracBits+FracBits
{
    Fp(self.inner * other.inner)
}

fp_mul::<16, 8, 20, 16>(...)
      // ^^^^^^^^^^^^^ Hard to know what these numbers mean

With named turbofish parameters (::$<>), you can specify the arguments by name, similar to specifying unit arguments

fp_mul::$<Size: 16, FracBits: 8, OutSize: 20, OutBits: 16>(...)
1

The keen reader may wonder why we don't use new fancy mul method on our FixedPoint type. Unfortunately, methods don't support turbofish yet :(

impl blocks in modules🔗

The final change to generics is a fix for impl blocks being broken when used in a mod, for example

mod {
  impl X {
    ... // Oh no, compiler bug! 😱
  }
}

Thanks to DasLixou for fixing this and other issues after looking at the code around namespacing and realizing it was terrible in general!

Test improvements🔗

Moving on from generics and impl blocks, the cocotb test bench API has seen 2 nice convenience improvements.

Native python type support🔗

First, it is now possible to assign integers, booleans and lists directly to Spade types, resolving the need for some annoying stringifying

# Before you had to do this if you wanted to assign a python bool to a Spade input
s.i.bool_value = "true" if value else "false"
# Now you can just do
s.i.bool_value = value

# You also no longer need to stringify integers
s.i.int_value = 5

# And you can use python lists natively
s.i.some_integers = [i for i in range(0, 8)]

== operator🔗

This is a smaller change, but fixes an anoying issue where if you wanted to compare the output of a module with a Spade expression, you had to do

s.o.value_eq("[1,2,3]")

Now you can simply write

s.o == "[1,2,3]"

or combining both changes

s.o == [1,2,3]

Small fixes🔗

This release also includes several small convenience changes

  • You can now suffix int literals with a signedness and size to help the type inferer. For example 512u32 or 123i64
  • Added / and % operators. These work for dividing by constant powers of two. For other values, the resulting hardware is likely too slow to be what you want so the compiler gives an error suggesting a different approach. If you do want to do combinational division or mod, you can use std::ops::comb_div or std::ops::comb_mod which is suggested by the compiler
  • Fixed incorrect code gen for enums with a single variant
  • Swim cocotb tests now work on more operating systems