Spade 0.15.0

Posted 2025-11-20 by The Spade Developers

It is finally time for Spade v0.15.0 πŸŽ‰. Like the previous release, this one is also delayed from our usual 6 week release schedule, but this time it also means that the release is unusually big!

More powerful Lambda unitsπŸ”—

In the last update, we introduced lambda functions and some corresponding utilities in the standard library. In this update we're lifting two significant limitations on the lambda functions

CapturesπŸ”—

Lambdas can now "capture" variables from their environment which allows you to do things like

fn fir_filter(x: Option<uint<8>>, n: uint<8>) -> Option<uint<9>> {
  // Capture the `n` parameter in this scope and use it inside the lambda.
  x.map(fn |x| {x + n})
}

PipelinesπŸ”—

Lambdas can now be pipelines. The primary use case for this is transformations of data that spans multiple clock cycles, where you want to preserve things like validity. For example:

pipeline(3) pipelined_multiplier(val: Option<(int<18>, int<18>)>) -> Option<int<36>> {
  val
    .inst(_) pmap(pipeline(3) |(x, y)| {
      let result = x * y;
    reg * 3;
      result
    })
}

New syntaxπŸ”—

The syntax for defining lambdas has been changed from fn (a, b) ... to fn |a, b| to more closely align with Rust.

Pipeline methodsπŸ”—

Previously, methods were limited to entities and functions, but now you can also implement pipeline methods. For example, you can now make the read mehtod on memories a method, making it nicer to use in things like method chains.

struct port MemoryReadPort<T> {
  addr: inv &uint<16>,
  data: &T
}

impl MemoryReadPort {
  pipeline(1) read(self, clk: clock, addr: uint<16>) {
    set self.addr = addr;
  reg;
    *self.data
  }
}

// Before
inst(1) read_memory(clk, read_port)
// Now
read_port
  .inst(1) read(clk)

Inferred pipeline depthπŸ”—

When instantiating pipelines, it is now possible to use inst(_) to infer the depth of the instantiated pipeline.

Transitive dependenciesπŸ”—

Until now, if multiple libraries depended on the same library in a project, values could not be passed between them despite ostensibly being of the same type. With this release, swim now allows this allowing more complex projects to be built.

Note that this comes with a minor caveat that only one version of each library will be used throughout the project.

Standard library improvementsπŸ”—

The standard library has received several new additions in this release

  • zip on arrays joins two arrays together element by element ([a, b].zip([c, d]) becomes [(a, c), (b, d)])
  • interleave interleaves the elements of two arrays ([a, b].interleave([c, d]) becomes [a, c, b, d])
  • pmap on the Option type behaves like map, but with a pipeline instead of a function
  • The memory ports in the standard library now have .read and .write methods instead of the now legacy read_read_port

Improvements to External VerilogπŸ”—

The language now supports type level strings, for example

fn tlstring<#str message>() {...}

while they don't do anything in Spade, they are very useful when interfacing with external Verilog modules which often take strings as configuration parameters, for example, PLLs https://gitlab.com/TheZoq2/spade-sdram/-/blob/959b1cb17357aea591e5a4870f316027e6bae423/hwtest_tangnano20k/src/pll.spade

With this change, nearly every Verilog module is instantiatable from Spade, the only thing missing as far as we know is being able to add Verilog attributes to instances which is also used by some PLLs.

UnsafeπŸ”—

In the past, a few methods in the standard library were in an unsafe namespace because they violate some guarantees that spade provide and assume to be true. In particular, Spade does not allow you to create a value of a type with a bit pattern that isn't valid for that specific type. In some cases, you know what you are doing and want to work around such a constraint, which is when you want to use an "unsafe" function.

With 0.15, we're adding an unsafe keyword which you can add as a label on a unit, like the transmute function in the standard library

/// Reinterpret the bits of `t` as an `O`
unsafe fn transmute<T, O>(t: T) -> O {/* */}

unsafe functions can only be called in the newly introduced unsafe blocks

let reinterpreted: uint<16> = unsafe { transmute((uint<8>, uint<8>)) }

which allows you to quickly find all instances of potentially dangerous code.

As the language evolves in the future to provide more guarantees, such as clock domains, unsafe functions will become more important.

Improved inout supportπŸ”—

The inout<T> type has also seen some improvements, including a new read_write_inout for interacting with them inside Spade.

Changes to setπŸ”—

The set statement now requires the right hand side to be a wire. This allows the use set on inv clock. Existing code needs to be patched to replace set x = y; with set x = &y;

Structs and enums in const scenariosπŸ”—

You can now use structs and enums in places which expect compile time values, most importantly clocked_memory_init.

Automatic Surfer integrationπŸ”—

swim will now download or compiler the Surfer integration plugin making the Spade integration seamless. If you have not used Surfer with Spade integration yet, make sure to install the latest Surfer and swim versions to take advantage of this.

Mac support for install-toolsπŸ”—

The swim subcommand for installing the oss-cad-suite now works on Mac, making initial setup easier.

ContributorsπŸ”—

This release contains contributions from Astrid Lauenstein, DasLixou, Ethan Uppal, JosΓ© Miguel SΓ‘nchez GarcΓ­a, and Oscar Gustafsson making it the release with the largest number of contributors to date, thanks everyone!