logo Spade Blog

Spade 0.11.0

Posted 2024-11-27 by The Spade Developers

Today we're releasing Spade 0.11.0, a release which makes working with ports a lot easier, among other things.

&mut is dead, long live inv &🔗

Until this release, Spade had values (T), wires (&T), mutable wires (&mut T) and inverted ports (~T). Importantly ~&T and &mut T were effectively the same thing, but did not mix well together. Mutable wires also required the user of ugly intrinsic functions like new_mut_wire() and read_mut_wire for creation and reading, while (T, ~T) pairs could be created just by let (x, x_inv) = port.

With 0.11, we're cleaning up this mess. First &mut T is gone, and ~&T has been fixed to work in all places that previously expected &mut T. Second, in order to avoid operator soup, the syntax of inverted types has changed to inv &T.

This is obviously a breaking change, but the compiler should tell you what you need to change. Just replacing all &mut T with inv &T should be enough.

These changes also have fresh documentation at https://docs.spade-lang.org/wires.html

Testing of ports🔗

With wires and ports being cleaned up, we also refactored the cocotb testbench wrappers to make testing ports easier. Previously, if you wanted to test a function like

impl IpStream {
    entity into_ethernet(
        self,
        clk: clock,
        rst: bool,
        source_mac: MacAddr,
        dest_mac: MacAddr,
    ) -> EthernetStream {
        let HeaderPayloadStreamO$(headers, bytes) = self
            .inner
            .inst lower(clk, rst, EthStreamLowerer$(source_mac, dest_mac));
        EthernetStream$(header: headers, payload: bytes)
    }

    entity buffer_header(self, clk: clock, rst: bool) -> IpStream {
        IpStream(self.inner.inst buffer_header(clk, rst))
    }
}

You had to write a wrapper to turn all ports with inverted wires into non-inverted wires,

struct IpStreamOut {
    eth_byte: Option<uint<8>>,
    headers_ready: bool,
    payload_ready: bool,
}

entity ip_stream_th(
    clk: clock,
    rst: bool,
    ip_headers: Option<IpHeader>,
    payload: Option<uint<8>>
) -> IpStreamOut {
    let headers_ready = inst new_mut_wire();
    let bytes_ready = inst new_mut_wire();
    let s = IpStream(HeaderPayloadStreamO(
        Rv(&ip_headers, headers_ready),
        Rv(&payload, bytes_ready),
    ));
    let out = s
        .inst into_ethernet$(
            clk,
            rst,
            source_mac: MacAddr([1,2,3,4,5,6]),
            dest_mac: MacAddr([11,12,13,14,15,16])
        )
        .inst into_ethernet_bytes(clk, rst);

    IpStreamOut$(
        eth_byte: match out {
            Some(b) => Some(b.inner),
            None => None
        },
        headers_ready: inst read_mut_wire(headers_ready),
        payload_ready: inst read_mut_wire(bytes_ready)
    )
}

With the updates to 0.11, this is no longer necessary, you can now set values on inverted inputs, and read values on inverted outputs directly from testbenches.

(In this case, you still need a small wrapper because we don't directly support testing methods, but it is much less tedious to write)

entity into_ethernet_th(
    _self: IpStream,
    clk: clock,
    rst: bool,
    source_mac: MacAddr,
    dest_mac: MacAddr,
) -> EthernetStream {
  _self.into_ethernet$(clk, rst, source_mac, dest_mac)
}

Support for parametric Verilog🔗

Ethan Uppal has added support for mapping Spade generics to Verilog parameters when instantiating external Verilog.

As a simple example, this means we can now directly instantiate

module add_constant #(parameter int N = 0, parameter int M = 0) (
    input[7:0] in, 
    output[7:0] out
);
    assign out = in + N + M;
endmodule

as

mod extern {
    #[no_mangle]
    entity add_constant<#uint N, #uint M>(
        #[no_mangle] in: int<8>,
        #[no_mangle] out: inv &int<8>
    ) __builtin__
}

entity harness(input: int<8>, named: bool) -> int<8> {
  let (out, out_inv) = port;
  let _ = inst extern::add_constant::$<N: 1, M: 2>(input, out_inv);
  out
}

This brings new exciting possibilities such as interop with the Berkeley HardFloat library.

Parser Recovery🔗

Spade has long been plagued by the parser bailing out on the first error. With this release, this has been improved so it can now do recovery in a lot of cases.

entity main() {
    not valid syntax
}

struct X {
    bool
}

enum Y {
    Variant(a)
}

would previously fail on the first line with no more diagnostics, but now keeps going:

error: Unexpected `identifier`, expected `}`
  ┌─ testinput:2:9
  │
2 │     not valid syntax
  │         ^^^^^ expected `}`

error: Unexpected `}`, expected `:`
  ┌─ testinput:7:1
  │
7 │ }
  │ ^ expected `:`

error: Unexpected `(`, expected `{`, `,` or `}`
   ┌─ testinput:10:12
   │
10 │     Variant(a)
   │            ^  

Finally reporting around semicolons are now better to avoid the "unexpected X expected ;" in confusing places, and we're now resistant to the infamous greek semicolon prank :)

Const generics in arrays🔗

You can now use const generics in range index expressions and array shorthand initialization. This makes it possible to define a generic "shift register" of array elements

entity sreg<#uint ISize, #uint N>(
  clk: clock,
  rst: bool,
  next: Option<int<ISize>>
) -> [int<ISize>; N]  {
  reg(clk) val reset(rst: [0; N]) = match next {
    Some(x) => val[1:N] `concat_arrays` [x],
    None => val,
  };
  val
}

Test bench chattyness reduction🔗

The output of test benches is now much cleaner

  • There is no longer excessive indentation on messages
  • When Spade stuff fails, you no longer get a generic python backtrace, instead you get something much easier to parse quickly.
error: Expected type uint<8>, got bool
  ┌─ py:1:1
  │
1 │ true
  │ ^^^^ Expected uint<8>
  │
  ┌─ py:1:1
  │
1 │ o.a
  │ --- Type uint<8> inferred here
  │
  = note: Expected: uint<8>
               Got: bool



note: A Spade expression failed to compile
    ┌─ field_accesses.py:47
    │
 47 │ _ = s.o.a == "true"
    │ ^^^^^^^^^^^^^^^^^^^ When executing this