NOTE The combinational and sequential syntaxes are being deprecated, please use combinational2 and sequential2, documneted here NOTE The combinational and sequential syntax exetensions have only been tested with the "coreir" and "coreir-verilog" compile targets. Please set output="coreir-verilog" or output="coreir" when calling m.compile to use this feature.

Combinational Circuit Definitions

Circuit defintions can be marked with the @m.circuit.combinational decorator. This introduces a set of syntax level features for defining combinational magma circuits, including the use of if statements to generate Muxes.

This feature is currently experimental, and therefor expect bugs to occur. Please file any issues on the magma GitHub repository.

If and Ternary

The condition must be an expression that evaluates to a magma value.

Basic example:

@m.circuit.combinational
def basic_if(I: m.Bits[2], S: m.Bit) -> m.Bit:
    if S:
        return I[0]
    else:
        return I[1]

For reference, here is the code produced for the corresponding magma circuit definition where the if statement has been lowered into a multiplexer:

basic_if = DefineCircuit("basic_if", "I", In(Bits[2]), "S", In(Bit), "O", Out(Bit))
Mux2xOutBit_inst0 = Mux2xOutBit()
wire(basic_if.I[1], Mux2xOutBit_inst0.I0)
wire(basic_if.I[0], Mux2xOutBit_inst0.I1)
wire(basic_if.S, Mux2xOutBit_inst0.S)
wire(Mux2xOutBit_inst0.O, basic_if.O)
EndCircuit()

Basic nesting:

@m.circuit.combinational
def if_statement_nested(I: m.Bits[4], S: m.Bits[2]) -> m.Bit:
    if S[0]:
        if S[1]:
            return I[0]
        else:
            return I[1]
    else:
        if S[1]:
            return I[2]
        else:
            return I[3]

Terneray expressions

def ternary(I: m.Bits[2], S: m.Bit) -> m.Bit:
    return I[0] if S else I[1]

Nesting terneray expressions

@m.circuit.combinational
def ternary_nested(I: m.Bits[4], S: m.Bits[2]) -> m.Bit:
    return I[0] if S[0] else I[1] if S[1] else I[2]

Function composition:

@m.circuit.combinational
def basic_if_function_call(I: m.Bits[2], S: m.Bit) -> m.Bit:
    return basic_if(I, S)

Function calls must refer to another m.circuit.combinational element, or a function that accepts magma values, define instances and wires values, and returns a magma. Calling any other type of function has undefined behavior.

Returning multiple values (tuples)

There are two ways to return multiple values, first is to use a Python tuple. This is specified in the type signature as (m.Type, m.Type, ...). In the body of the definition, the values can be returned using the standard Python tuple syntax. The circuit defined with a Python tuple as an output type will default to the naming convetion O0, O1, ... for the output ports.

@m.circuit.combinational
def return_py_tuple(I: m.Bits[2]) -> (m.Bit, m.Bit):
    return I[0], I[1]

The other method is to use an m.Tuple (magma's tuple type). Again, this is specified in the type signature, using m.Tuple(m.Type, m.Type, ...). You can also use the namedtuple pattern to give your multiple outputs explicit names with m.Tuple(O0=m.Bit, O1=m.Bit).

@m.circuit.combinational
def return_magma_tuple(I: m.Bits[2]) -> m.Tuple[m.Bit, m.Bit]:
    return m.tuple_([I[0], I[1]])
@m.circuit.combinational
def return_magma_named_tuple(I: m.Bits[2]) -> m.Product.from_fields("anon", {"x": m.Bit, "y": m.Bit}):
    return m.namedtuple(x=I[0], y=I[1])

Using non-combinational magma circuits

The combinational syntax allows the use of other combinational circuits using the function call syntax to wire inputs and retrieve outputs.

Here is an example:

class EQ(m.Circuit):
    IO = ["I0", m.In(m.Bit), "I1", m.In(m.Bit), "O", m.Out(m.Bit)]

@m.circuit.combinational
def logic(a: m.Bit) -> (m.Bit,):
    if EQ()(a, m.bit(0)):
        c = m.bit(1)
    else:
        c = m.bit(0)
    return (c,)

Using magma's higher order circuits

class Not(m.Circuit):
    IO = ["I", m.In(m.Bit), "O", m.Out(m.Bit)]

@m.circuit.combinational
def logic(a: m.Bits[10]) -> m.Bits[10]:
    return m.join(m.map_(Not, 10))(a)

Using combinational circuits as a standard circuit definition

Combinational circuits can also be used as standard circuit definitions by using the .circuit_definition attribute to retrieve the corresponding magma circuit.

@m.circuit.combinational
def invert(a: m.Bit) -> m.Bit:
    return Not()(a)

class Foo(m.Circuit):
    IO = ["I", m.In(m.Bit), "O", m.Out(m.Bit)]

    @classmethod
    def definition(io):
        # reference circuit_definition and instance
        inv = invert.circuit_definition()  
        inv.a <= io.I
        io.O <= inv.O

Statically Elaborated For Loops

Statically elaborated for loops are supported using the ast_tools loop unrolling macro. Here's an example:

from ast_tools.passes import begin_rewrite, loop_unroll, end_rewrite

n = 4
@m.circuit.combinational
@end_rewrite()
@loop_unroll()
@begin_rewrite()
def logic(a: m.Bits[n]) -> m.Bits[n]:
    O = []
    for i in ast_tools.macros.unroll(range(n)):
        O.append(a[n - 1 - i])
    return m.bits(O, n)

which compiles to this magma circuit

class logic(m.Circuit):
    IO = ['a', m.In(m.Bits[n]), 'O', m.Out(m.Bits[n])]

    @classmethod
    def definition(io):
        O_0 = []
        O_0.append(io.a[n - 1 - 0])
        O_0.append(io.a[n - 1 - 1])
        O_0.append(io.a[n - 1 - 2])
        O_0.append(io.a[n - 1 - 3])
        __magma_ssa_return_value_0 = m.bits(O_0, n)
        O = __magma_ssa_return_value_0
        m.wire(O, io.O)

Sequential Circuit Definition

The @m.circuit.sequential decorator extends the @m.circuit.combinational syntax with the ability to use Python's class system to describe stateful circuits.

The basic pattern uses the __init__ method to declare state, and a __call__ function that uses @m.circuit.combinational syntax to describe the transition function from the current state to the next state, as well as a function from the inputs to the outputs. State is referenced using the first argument self and is implicitly updated by writing to attributes of self (e.g. self.x = 3).

Here's an example of a basic 2 element shift register:

@m.circuit.sequential(async_reset=True)
class DelayBy2:
    def __init__(self):
        self.x: m.Bits[2] = m.bits(0, 2)
        self.y: m.Bits[2] = m.bits(0, 2)

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        O = self.y
        self.y = self.x
        self.x = I
        return O

In the __init__ method, the circuit declares two state elements self.x and self.y. Both are annotated with a type m.Bits[2] and initialized with a value m.bits(0, 2). The __call__ method accepts an input I with the same type as the state elements. It stores the current value of self.y in a temporary variable O, sets self.y to be the value of self.x, sets self.x to be the input value I and returns O. Notice that the inputs and output of the __call__ method have type annotations just like m.circuit.combinational functions. The __call__ method should be treated as a standard @m.circuit.combinational function, with the special parameter self that provides access to the state.

The sequential syntax is implemented by compiling the above class definition into a magma circuit definition instantiating the registers declared in the __init__ method and defining and wiring up a combinational function corresponding to the __call__ method. The references to self attributes are rewritten to be explicit arguments to the __call__ method. Here is the output code for the above example:

class DelayBy2(m.Circuit):
    IO = ['I', m.In(m.Bits[2]), 'CLK', m.In(m.Clock), 'ASYNCRESET', m.
        In(m.AsyncReset), 'O', m.Out(m.Bits[2])]

    @classmethod
    def definition(io):
        x = DefineRegister(2, init=0, has_async_reset=True)()
        y = DefineRegister(2, init=0, has_async_reset=True)()

        @combinational
        def DelayBy2_comb(I: m.Bits[2], self_x_O: m.Bits[2], self_y_O:
            m.Bits[2]) ->(m.Bits[2], m.Bits[2], m.Bits[2]):
            O = self_y_O
            self_y_I = self_x_O
            self_x_I = I
            return self_x_I, self_y_I, O
        comb_out = DelayBy2_comb(io.I, x, y)
        x.I <= comb_out[0]
        y.I <= comb_out[1]
        io.O <= comb_out[2]

Hierarchy

Besides declaring magma values as state, the sequential syntax also supports using instances of other sequential circuits. For example, suppose we have a register defined as follows:

@m.circuit.sequential(async_reset=True)
class Register:
    def __init__(self):
        self.value: m.Bits[2] = m.bits(init, 2)

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        O = self.value
        self.value = I
        return O

We can use the Register class in the definition of a ShiftRegister class:

@m.circuit.sequential(async_reset=True)
class TestShiftRegister:
    def __init__(self):
        self.x: Register = Register()
        self.y: Register = Register()

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        x_prev = self.x(I)
        y_prev = self.y(x_prev)
        return y_prev

Notice that we annotate the type of the attribute with the class (sequential circuit definition) and we initialize it with an instance of the class. Then, the attribute can be called with inputs to return the outputs. This corresponds to calling the __call__ method of the sub instance.

NOTE Currently it is required that every sub sequential circuit element receive an explicit invocation in the __call__ method. For example, if you have a sub sequential circuit self.x that you would like to keep constant, you must still call it with self.x(...) to ensure that some input value is provided every cycle (the sub sequential circuit must similarly be designed in such a way that the logic expects inputs every cycle, so enable logic must be explicitly defined).

Experimental: Direct to Verilog Compilation

Note: This feature requires the kratos package (install with pip install kratos)

@combinational_to_verilog and @sequential_to_verilog decorators provide support for an alternative compiler that passes if statements down to verilog and uses an always_ff block to implement the sequential registers.

For example,

@m.circuit.combinational_to_verilog
def execute_alu(a: m.UInt[16], b: m.UInt[16], config_: m.Bits[2]) -> \
        m.UInt[16]:
    if config_ == m.bits(0, 2):
        c = a + b
    elif config_ == m.bits(1, 2):
        c = a - b
    elif config_ == m.bits(2, 2):
        c = a * b
    else:
        c = m.bits(0, 16)
    return c

compiles to

module execute_alu (
  output logic [15:0] O,
  input logic [15:0] a,
  input logic [15:0] b,
  input logic [1:0] config_
);

logic [15:0] c;
always_comb begin
  unique case (config_)
    2'h0: c = a + b;
    2'h1: c = a - b;
    2'h2: c = a * b;
    default: c = 16'h0;
  endcase
  O = c;
end
endmodule   // execute_alu

and

@m.circuit.sequential_to_verilog(async_reset=True)
class TestBasic:
    def __init__(self):
        self.x: m.Bits[2] = m.bits(0, 2)
        self.y: m.Bits[2] = m.bits(0, 2)

    def __call__(self, I: m.Bits[2]) -> m.Bits[2]:
        _O = self.y
        self.y = self.x
        self.x = I
        return _O

compiles to

module TestBasic (
  input logic ASYNCRESET,
  input logic CLK,
  input logic [1:0] I,
  output logic [1:0] O
);

logic [1:0] _O;
logic [1:0] self_x_I;
logic [1:0] self_x_O;
logic [1:0] self_y_I;
logic [1:0] self_y_O;

always_ff @(posedge CLK, posedge ASYNCRESET) begin
  if (ASYNCRESET) begin
    self_x_O <= 2'h0;
    self_y_O <= 2'h0;
  end
  else begin
    self_x_O <= self_x_I;
    self_y_O <= self_y_I;
  end
end
always_comb begin
  _O = self_y_O;
  self_y_I = self_x_O;
  self_x_I = I;
  O = _O;
end
endmodule   // TestBasic