Enums and Pattern Matching
The previously discussed type system features behave very similarly to other languages, but the enums in Spade are considerably more powerful than those found in languages like C and VHDL.
Like C and VHDL enums, an enum in Spade takes on one of several values, for example it can be used to represent different colors:
enum Color {
Red,
Green,
Blue
}
Unlike C, enums are namespaced and statically typed, so you cannot convert them to or from integers, and to create them, you need to use their full name, i.e. Color::Red, not just Red. (The namespacing system will be discussed in more detail in a later chapter.)
To access an enum, you use the match expression, which like if “returns” a value for every branch. For example, to convert the Color enum into RGB values, you would write:
Functions together with Entities together with fn and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike fn, entity can contain registers and therefore have state and unlike Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline., they do not have a statically known latency or Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. structure. and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike entities and pipelines, functions have no internal state, they are combinational (pure). to_rgb(color: Color) -> (Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits) {
match color {
Color::Red => (255, 0, 0),
Color::Green => (0, 255, 0),
Color::Blue => (0, 0, 255)
}
}
Payload
So far, enums do not seem to be “considerably more powerful” than what you may be used to, but that is because we haven’t discussed payloads. In addition to being one of several variants (Red, Green, Blue in this case), each variant can have associated values which are only present when the enum is that particular variant. For example, we can augment our Color enum with two new variants: Grayscale and Custom like this:
enum Color {
Red,
Green,
Blue,
Gray{brightness: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits},
Custom{r: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, g: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, b: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits}
}
When the enum variant is Gray, there is an additional field available containing the brightness of the color, and when the variant is Custom, the full RGB value is available. However, these are only accessible when the enum is of the right variant, and this is checked by the compiler. To access these variants, we use the match block again:
Functions together with Entities together with fn and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike fn, entity can contain registers and therefore have state and unlike Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline., they do not have a statically known latency or Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. structure. and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike entities and pipelines, functions have no internal state, they are combinational (pure). to_rgb(color: Color) -> (Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits) {
match color {
Color::Red => (255, 0, 0),
Color::Green => (0, 255, 0),
Color::Blue => (0, 0, 255),
// Grayscale has the brightness value in all three channels
Color::Gray(br) => (br, br, br),
// For custom colors, just map the channels directly
Color::Custom(r, g, b) => (r, g, b)
}
}
Initializing enum variants is done the same way as structs, they are simply functions and as such can be instantiated with both named and positional arguments:
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. red = Color::Red; // Variants with no members do not need ()
// Positional arguments
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. red = Color::Custom(255, 0, 0);
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. bright_gray = Color::Gray(200);
// Named arguments
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. red = Color::Custom$(r: 255, g: 0, b: 0);
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. bright_gray = Color::Gray$(brightness: 200);
Pattern Matching
The branches in a pattern can be much more complex than simply matching on enum variants. To showcase this, we’ll define a new enum with some more interesting variants:
enum Example {
Empty,
Int{val: Type used to represent signed integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits},
Tuple{value: (Type used to represent signed integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, Type used to represent 1 bit values)},
Struct{value: IntAndBool},
Color{color: Color}
}
struct IntAndBool { a: Type used to represent signed integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, b: Type used to represent 1 bit values }
Like the destructuring we saw of structs and tuples before, you can destructure things recursively inside the match statement. For example:
match example {
Example::Empty => 0,
Example::Int(val) => val,
Example::Tuple((a, b)) => a,
Example::Struct(IntAndBool(a, b)) => a,
Example::Color(_) => 0
};
In the last branch, we use _ to ignore a member we don’t care about, in this case the color.
If we do care about the color, we can do pattern matching recursively on enums too:
match example {
Example::Empty => 0,
Example::Int(_) => 1,
Example::Tuple(_) => 2,
Example::Struct(_) => 3,
Example::Color(Color::Red) => 4,
Example::Color(Color::Green) => 5,
Example::Color(Color::Blue) => 6,
Example::Color(_) => 7
};
You may notice that the last branch in this example leaves out a few of the Color members we defined previously, and has a branch with Example::Color(_). This demonstrates an important aspect of the match expression: the first branch which matches a value will be taken, so in this case Red, Green and Blue are handled by the explicit branches, while Gray and Custom are handled by the “fallback branch”. If the Example::Color(_) branch was placed before the others, it would match all color values.
The compiler checks that all match expressions are complete, i.e. there are no values for which no branch will match. As an example, if the final Example::Color(_) branch is omitted, the following error is produced:
error: Non-exhaustive match: patterns
Example::Color(color: Color::Gray(brightness: 0..255)),
Example::Color(color: Color::Custom(r: 0..255, g: 0..255, b: 0..255)) not covered
┌─ src/enums.spade:86:1
│
86 │ ╭ match example {
87 │ │ Example::Empty => 0,
88 │ │ Example::Int(_) => 1,
89 │ │ Example::Tuple(_) => 2,
· │
93 │ │ Example::Color(Color::Blue) => 6,
94 │ │ };
│ ╰─^
patterns Example::Color(color: Color::Gray(brightness: 0..255)),
Example::Color(color: Color::Custom(r: 0..255, g: 0..255, b: 0..255))
not coveredPatterns can also contain values. For example, you can pattern match on a bool:
match b {
true => 1,
false => 0
};
or integers:
match 0u8 {
0 => 0,
1 => 0,
2 => 0,
_ => 1
};
Of course, these value patterns can be used recursively in other patterns:
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. is_black = match color {
Color::Gray(0) => true,
Color::Custom(0, 0, 0) => true,
_ => false
};
Example: A state machine
One common use case of enums and match statements in Spade is to write state machines. To showcase this, we will write a simple state machine that blinks an LED thrice after a button is pressed. We will have two “main states”: Idle and Blinking, where idle is waiting for the button to be pressed, and blinking is doing the actual blinking. To blink, we also have to keep track of:
- How many times we have left to blink
- How long we have blinked
We can encode this as an enum like this:
enum State {
Idle,
Blink{blinks_left: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits, duration_left: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits}
}
And then use pattern matching to implement the state machine itself like this:
reg(clk) is used to define registers which maintain the state of your circuit.
All registers have a Type used to represent clock signals. (clk), a name and a new value after the = which is given as a function of the current value. Registers can also have a Defines the reset value of a register as (trigger: value). When trigger ist rue, the reset is synchronously reset to valuetrigger: value) wich means that the reset is synchronously set to value when trigger is true.
In pipelines, you can also define registers with reg; is used in a Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. to separate stages. When you refer to a variable defined above a reg; statement below a reg; statement you refer to a registered version of the original value. Registers defined with an explicit Type used to represent clock signals. (reg(clk)) are used for state registers both inside and outside pipelines. which are used to separate stages. state Defines the reset value of a register as (trigger: value). When trigger ist rue, the reset is synchronously reset to valuerst: State::Idle) =
match (state, btn) {
// If we're not blinking and the user isn't pressing the button,
// stay in the idle state
(State::Idle, false) => state,
// If we're in idle, and the user clicks the button, go to the blink
// state
(State::Idle, true) => {
State::Blink$(blinks_left: 2, duration_left: 5_000)
},
// If we have no blinks left, and are done with this blink,
// go back to idle
(State::Blink$(blinks_left: 0, duration_left: 0), _) => {
State::Idle
},
// If we have blinks left (blinks_left != 0), but we're done
// with this blink, start the next blink
(State::Blink$(blinks_left, duration_left: 0), _) => {
State::Blink$(blinks_left: Truncates the input to a smaller size, throwing away the most signReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.icant bits. The size of the resulting value is type inferred.blinks_left-1), duration_left: 5_000)
},
// Otherwise, decrement the duration
(State::Blink$(blinks_left, duration_left), _) => {
State::Blink$(blinks_left, duration_left: Truncates the input to a smaller size, throwing away the most signReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.icant bits. The size of the resulting value is type inferred.duration_left - 1))
},
};
We match on both the current state, and input ((state, btn)), and ignore the input while we are blinking with a wildcard (_). In the blinking state, we use integer patterns and priority to handle three cases:
- We are done blinking
- We are done with this blink
- None of the above
The output of the module can also be written with a match block on the current state:
match state {
State::Idle => false,
State::Blink$(blinks_left: _, duration_left) => {
Returns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it. duration_left < 2_500 {
false
} else {
true
}
}
}
You can try this example in the ▶️ playground.
Exercises
Modify the blink_thrice entity to:
- Blink 4 times.
- Have a cooldown of one second between button presses.
Here is a link to the code on the ▶️ playground.