<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Add2 Circuit
Now 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 adder
to 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
Step2:  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 wiring
up 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.O
COUT = full_adder.COUT
The outputs of the circuit are returned.
Inside this loop we append single bit outputs from the full adders
to 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 convert
between different types. 
In this example, m.uint(C) converts the list of bits to a UInt[len(C)].
Add Generator
One 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 Python
to create powerful hardware generators.
Here is the code
Step3:  To generate a Circuit from a Generator, we can directly call the generate static method.
Step4:  Let's inspected the generated code
Step5:  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.
Step6:  Here's an example of using the convenience add function which handles the Generator instantiation for us
<ASSISTANT_TASK:>
Python 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))
Explanation: Add2 Circuit
Now 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 adder
to 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:
<img src="logisim/adder.png" width="500"/>
End of explanation
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)
Explanation: 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 wiring
up 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.O
COUT = full_adder.COUT
The outputs of the circuit are returned.
Inside this loop we append single bit outputs from the full adders
to 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 convert
between different types. 
In this example, m.uint(C) converts the list of bits to a UInt[len(C)].
Add Generator
One 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 Python
to create powerful hardware generators.
Here is the code:
End of explanation
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!")
Explanation: To generate a Circuit from a Generator, we can directly call the generate static method.
End of explanation
m.compile("build/Add2", Add2, inline=True)
%cat build/Add2.v
!coreir -i build/Add2.json -p instancecount
Explanation: Let's inspected the generated code
End of explanation
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))
Explanation: 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.
End of explanation
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))
Explanation: Here's an example of using the convenience add function which handles the Generator instantiation for us
End of explanation
<END_TASK>