<jupyter_start><jupyter_text>Add2 CircuitNow let's build a 2-bit adder using `full_adder`. We'll use a simple ripple carry adder design by connecting the carry out of one full adderto the carry in of the next full adder.The resulting adder will accept as input a carry in,and generate a final carry out. Here's a `logisim` diagram of the circuit we will construct:<jupyter_code>import ast_tools
from ast_tools.transformers.loop_unroller import unroll_for_loops
from ast_tools.passes import begin_rewrite, end_rewrite, loop_unroll

@m.circuit.combinational
def full_adder(A: m.Bit, B: m.Bit, C: m.Bit) -> (m.Bit, m.Bit):
    return A ^ B ^ C, A & B | B & C | C & A  # sum, carry

@m.circuit.combinational
@end_rewrite()
@loop_unroll()
@begin_rewrite()
def _add(I0: m.Bits[2], I1: m.Bits[2], CIN: m.Bit) -> (m.Bits[2], m.Bit):
    O = []
    COUT = io.CIN
    for i in ast_tools.macros.unroll(range(2)):
        Oi, COUT = full_adder(io.I0[i], io.I1[i], COUT)
        O.append(Oi)

    return m.uint(O), COUT

print(repr(_add.circuit_definition))<jupyter_output>_add = DefineCircuit("_add", "I0", In(Bits[2]), "I1", In(Bits[2]), "CIN", In(Bit), "O0", Out(Bits[2]), "O1", Out(Bit))
full_adder_inst0 = full_adder()
full_adder_inst1 = full_adder()
wire(_add.I0[0], full_adder_inst0.A)
wire(_add.I1[0], full_adder_inst0.B)
wire(_add.CIN, full_adder_inst0.C)
wire(_add.I0[1], full_adder_inst1.A)
wire(_add.I1[1], full_adder_inst1.B)
wire(full_adder_inst0.O1, full_adder_inst1.C)
wire(full_adder_inst0.O0, _add.O0[0])
wire(full_adder_inst1.O0, _add.O0[1])
wire(full_adder_inst1.O1, _add.O1)
EndCircuit()<jupyter_text>Although we are making an 2-bit adder,we do this using a for loop that can be generalized to construct an n-bit adder.To use a for loop inside combinational, we use the `ast_tools` package's macro support.These `loop_unroll` macro will expand the for loop before passing the function to `m.circuit.combinational`.Each time through the for loop we call full adder.Calling an circuit instance has the effect of wiringup the arguments to the inputs of the circuit.That is,```O, COUT = full_adder(I0, I1, CIN)```is equivalent to```m.wire(IO, full_adder.I0)m.wire(I1, full_adder.I1)m.wire(CIN, full_adder.CIN)O = full_adder.OCOUT = full_adder.COUT```The outputs of the circuit are returned.Inside this loop we append single bit outputs from the full addersto the Python list `O`. We also set the `CIN` of the next full adder to the `COUT` of the previous instance.Finally, we then convert the list `O` to a `UInt[n]`. In addition to `Bits[n]`,`magma` also has built in types `UInt[n]` and `SInt[n]` to represent unsigned and signed ints.`magma` also has type conversion functions `bits`, `uint`, and `sint` to convertbetween different types. In this example, `m.uint(C)` converts the list of bits to a `UInt[len(C)]`.  Add GeneratorOne question you may be asking yourself, is how can this code be generalized to produce an n-bit adder. We do this by creating an add *Generator*.A `Generator` is a Python class that defines a static `generate` method which takes parameters and returns a circuit class.Calling the generator with different parameter values will create and instantiate different circuits.The power of `magma` results from being to use all the features of Pythonto create powerful hardware generators.Here is the code:<jupyter_code>class Add(m.Generator):
    @staticmethod
    def generate(width: int):
        T = m.UInt[width]
        
        @m.circuit.combinational
        @end_rewrite()
        @loop_unroll()
        @begin_rewrite()
        def _add(I0: T, I1: T, CIN: m.Bit) -> (T, m.Bit):
            O = []
            COUT = io.CIN
            for i in ast_tools.macros.unroll(range(width)):
                Oi, COUT = full_adder(io.I0[i], io.I1[i], COUT)
                O.append(Oi)
                
            return m.uint(O), COUT
        return _add

def add(i0, i1, cin):
    """
    We define a convenience function that instantiates the
    add generator for us based on the width of the inputs.
    """
    if len(i0) != len(i1):
        raise TypeError("add arguments must have same length")
    if not isinstance(cin, m.Bit):
        raise TypeError("add cin must be a Bit")
    if (not isinstance(i0, m.UInt) and 
        not isinstance(i1, m.UInt)):
            raise TypeError("add expects UInt inputs")
    return Add(len(i0))(i0, i1, cin)<jupyter_output><empty_output><jupyter_text>To generate a `Circuit` from a `Generator`, we can directly call the `generate` static method.<jupyter_code>from fault import PythonTester

Add2 = Add.generate(2)
add2 = PythonTester(Add2)

print(add2(1,2,0)[0] == 3)
assert add2(1, 2, 0) == (3, 0), "Failed"
print("Success!")<jupyter_output>Bit(True)
Success!<jupyter_text>Let's inspected the generated code<jupyter_code>m.compile("build/Add2", Add2, inline=True)
%cat build/Add2.v
!coreir -i build/Add2.json -p instancecount<jupyter_output>An instance count of all the primitives
=======================================
full_adder | instances in current | instances in children | 
  corebit_and | 3 | 0
  corebit_or | 2 | 0
  corebit_xor | 2 | 0

_add | instances in current | instances in children | 
  corebit_and | 0 | 6
  corebit_or | 0 | 4
  corebit_xor | 0 | 4

=======================================
{"top":"global._add",
"namespaces":{
  "global":{
    "modules":{
      "_add":{
        "type":["Record",[
          ["I0",["Array",2,"BitIn"]],
          ["I1",["Array",2,"BitIn"]],
          ["CIN","BitIn"],
          ["O0",["Array",2,"Bit"]],
          ["O1","Bit"]
        ]],
        "instances":{
          "full_adder_inst0":{
            "modref":"global.full_adder"
          },
          "full_adder_inst1":{
            "modref":"global.full_adder"
          }
        },
        "connections":[
          ["self.I0.0","full_adder_inst0.A"],
          ["self.I1.0","full_adder_inst0.B"[...]<jupyter_text>We can instantiate a `Generator` using the standard object syntax, which will implicitly call the `generate` method based on teh parameters, and return an instance of the generated `Circuit`.  By default, this logic will cache definitions based on the generator parameters.<jupyter_code>class Main(m.Circuit):
    io = m.IO(I0=m.In(m.UInt[3]), I1=m.In(m.UInt[3]), CIN=m.In(m.Bit),
              O=m.Out(m.UInt[3]), COUT=m.Out(m.Bit))
    O, COUT = Add(3)(io.I0, io.I1, io.CIN)
    io.O @= O
    io.COUT @= COUT
    
print(repr(Main))<jupyter_output>Main = DefineCircuit("Main", "I0", In(UInt[3]), "I1", In(UInt[3]), "CIN", In(Bit), "O", Out(UInt[3]), "COUT", Out(Bit))
_add_inst0 = _add()
wire(Main.I0, _add_inst0.I0)
wire(Main.I1, _add_inst0.I1)
wire(Main.CIN, _add_inst0.CIN)
wire(_add_inst0.O0, Main.O)
wire(_add_inst0.O1, Main.COUT)
EndCircuit()<jupyter_text>Here's an example of using the convenience `add` function which handles the `Generator` instantiation for us<jupyter_code>class Main(m.Circuit):
    io = m.IO(I0=m.In(m.UInt[3]), I1=m.In(m.UInt[3]), CIN=m.In(m.Bit),
              O=m.Out(m.UInt[3]), COUT=m.Out(m.Bit))
    O, COUT = add(io.I0, io.I1, io.CIN)
    io.O @= O
    io.COUT @= COUT
    
print(repr(Main))<jupyter_output>Main = DefineCircuit("Main", "I0", In(UInt[3]), "I1", In(UInt[3]), "CIN", In(Bit), "O", Out(UInt[3]), "COUT", Out(Bit))
_add_inst0 = _add()
wire(Main.I0, _add_inst0.I0)
wire(Main.I1, _add_inst0.I1)
wire(Main.CIN, _add_inst0.CIN)
wire(_add_inst0.O0, Main.O)
wire(_add_inst0.O1, Main.COUT)
EndCircuit()
{'fraction_non_alphanumeric': 0.14896459585838343, 'fraction_numerical': 0.021509686038744154, 'mean_word_length': 3.7289955780164243, 'pattern_counts': {'":': 12, '<': 20, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '369549', 'license': 'MIT', 'n_tokens_mistral': 2822, 'n_tokens_neox': 2685, 'n_words': 746, 'path': 'notebooks/tutorial/coreir/Add.ipynb', 'repo_name': 'phanrahan/magmathon', 'size': '7485'}