Control Flow

While hardware doesn't actually have control flow, it is often useful to have generative control flow for repeating hardware structures.

if

The humble if statement is the most basic form of control flow. It comes in two flavors: generation time, and runtime.

Generation time if

If its condition is met, then the code in its block is executed. It can optionally have an else block, which then also allows chaining else if statements.

Example

gen int a
gen bool b
if a == 5 {
    // Do the first thing
} else if b {
    // otherwise do something here
} else {
    // etc
}

Runtime if

In practice, the biggest difference between these variants is that the code within both branches of the runtime if is executed regardless of the condition. Only assignments are performed conditionally. This means any assignments within the block will have a combinatorial dependency on the condition wire. To avoid confusion, it is not allowed to assign to generative variables within a runtime if.

Example

module m {
    interface m : int a, bool b -> int c 
    if a == 5 {
        c = 4
    } else if b {
        c = 2
    } else {
        c = 1
    }
}

Conditional bindings

In hardware design, pretty much all data signals will be coupled with valid signals. Having dedicated syntactic sugar for this is thus valuable to lift some mental load for the hardware designer.

As an example, take the pop interface of a FIFO.

interface pop : bool do_pop -> bool pop_valid, T data

This is both an action (setting the do_pop signal), but also may fail (pop_valid). Both control signals can be hidden with this syntactic sugar. Furthermore, the output data of the FIFO is only available when the pop was successful. This adds nice implicit semantics that for example the formal verifier could then check.

FIFO myFifo
if myFifo.pop() : T data {
    ...
}

Which is equivalent to this:

FIFO myFifo
myFifo.do_pop = true
if myFifo.pop_valid {
    T data = myFifo.data_out
    ...
}

This syntax can also be used to approximate imperative control flow. We would want something in hardware like the lambda functions in software, but what semantics should they have? As a first approximation, we can have the submodule 'trigger' some of our hardware using this validity logic. In this example we use a submodule that generates an index stream of valid matrix indices, and calls our code with that:

MatrixIterator mit

state bool start
initial start = true

if start {
    mit.start(40, 40)
    start = false
}

if mit.next() : int x, int y {
    ...
}

Finally, this might be a good syntax alternative for implementing Sum Types. Sum types map weirdly to hardware, as their mere existence may or may not introduce wire dependencies on the variants, depending on how the wires were reused. Instead, we could use these conditional bindings to make a bootleg match:

if my_instruction.is_jump() : int target_addr {
    ...
}
if my_instruction.is_add() : int reg_a, int reg_b, int reg_target {
    ...
}

for

The for statement only comes in its generative form. It's used to generate repetitive hardware.

Example

module add_stuff_to_indices {
    interface add_stuff_to_indices : int[10] values -> int[10] added_values 
	int[5] arr
	for int i in 0..10 {
		int t = values[i]
		added_values[i] = t + i

		int tt = arr[i] + values[0]
	}
}

while

Similar to the for loop. Also generation only. Not yet implemented.

chain and first

the chain construct is one of SUS' unique features. Not yet implemented.

Often it is needed to have some kind of priority encoding in hardware. It only fires the first time it is valid.

As a bit of syntactic sugar, the first statement uses a chain to check if it's the first time the condition was valid.

It comes in two variants: standalone first and if first.

Examples

module first_enabled_bit {
    interface first_enabled_bit : bool[10] values -> bool[10] is_first_bit 
    chain bool found = false
	for int i in 0..10 {
        if values[i] {
            first in found {
                // First i for which values[i]==true
                is_first_bit[i] = true
            } else {
                // values[i]==true but not the first
                is_first_bit[i] = false
            }
        } else {
            // values[i]!=true
            is_first_bit[i] = false
        }
	}
}

With if first we can merge both else blocks.

module first_enabled_bit {
    interface first_enabled_bit : bool[10] values -> bool[10] is_first_bit 
    chain bool found = false
	for int i in 0..10 {
        if first values[i] in found {
            // First i for which values[i]==true
            is_first_bit[i] = true
        } else {
            // values[i]!=true or not first values[i]==true
            is_first_bit[i] = false
        }
	}
}

Often with uses of first one also wants to have a case where the condition never was valid.

module first_enabled_bit_index {
    interface first_enabled_bit_index : bool[10] values -> int first_bit, bool all_zero 
    chain bool found = false
	for int i in 0..10 {
		if first values[i] in found {
            first_bit = i
            all_zero = false
        }
	}
    if !found {
        all_zero = true
    }
}