<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 FullAdder - Combinational Circuits
This notebook walks through the implementation of a basic combinational circuit, a full adder. This example introduces many of the features of Magma including circuits, wiring, operators, and the type system.
Start by importing magma and mantle. magma is the core system which implements circuits and the methods to compose them, and mantle is a library of useful circuits.
Step1:  A full adder has three single bit inputs, and returns the sum and the carry. The sum is the exclusive or of the 3 bits, the carry is 1 if any two of the inputs bits are 1. Here is a schematic of a full adder circuit (from logisim).
<img src="images/full_adder_logisim.png" width="500"/>
We start by defining a magma combinational function that implements a full adder. 
The full adder function takes three single bit inputs (type m.Bit) and returns two single bit outputs as a tuple.
The first element of tuple is the sum, the second element is the carry.  Note that the arguments and return values of the functions have type annotations using Python 3's typing syntax.
We compute the sum and carry using standard Python bitwise operators &amp;, |, and ^.
Step2:  We can test our combinational function to verify that our implementation behaves as expected fault.
We'll use the fault.PythonTester which will simulate the circuit using magma's Python simulator.
Step3:  combinational functions are polymorphic over Python and magma types.  If the function is called with magma values, it will produce a circuit instance, wire up the inputs, and return references to the outputs.   Otherwise, it will invoke the function in Python.  For example, we can use the Python function to verify the circuit simulation.
Step4:  Circuits
Now that we have an implementation of full_adder as a combinational function, 
we'll use it to construct a magma Circuit. 
A Circuit in magma corresponds to a module in verilog.
This example shows using the combinational function inside a circuit definition, as opposed to using the Python implementation shown before.
Step5:  First, notice that the FullAdder is a subclass of Circuit. All magma circuits are classes in python.
Second, the function IO creates the interface to the circuit. 
The arguments toIO are keyword arguments. 
The key is the name of the argument in the circuit, and the value is its type. 
In this circuit, all the inputs and outputs have Magma type Bit. 
We also qualify each type as an input or an output using the functions In and Out.
Note that when we call the python function fulladder
it is passed magma values not standard python values.
In the previous cell,  we tested fulladder with standard python ints,
while in this case, the values passed to the Python fulladder function 
are magma values of type Bit.
The Python bitwise operators for Magma types are overloaded to automatically create subcircuits to compute logical functions.
fulladder returns two values.
These values are assigned to the python variables O and COUT. 
Remember that assigning to a Python variable 
sets the variable to refer to the object.
magma values are Python objects,
so assigning an object to a variable creates a reference to that magma value.
In order to complete the definition of the circuit, 
O and COUT need to be wired to the outputs in the interface.
The python @= operator is overloaded to perform wiring.
Let's inspect the circuit definition by printing the __repr__.
Step6:  We see that it has created an instance of the full_adder combinational function and wired up the interface.
We can also inspect the contents of the full_adder circuit definition.  Notice that it has lowered the Python operators into a structural representation of the primitive logicoperations.
Step7:  We can also inspect the code generated by the m.circuit.combinational decorator by looking in the .magma directory for a file named .magma/full_adder.py.  When using m.circuit.combinational, magma will generate a file matching the name of the decorated function.  You'll notice that the generated code introduces an extra temporary variable (this is an artifact of the SSA pass that magma runs to handle if/else statements).
Step8:  In the code above, a mux is imported and named phi. If the combinational circuit contains any if-then-else constructs, they will be transformed into muxes.
Note also the m.wire function. m.wire(O0, io.I0) is equivalent to io.O0 @= O0.
Staged testing with Fault
fault is a python package for testing magma circuits.  By default, fault is quiet, so we begin by enabling logging using the built-in logging module
Step9:  Earlier in the notebook, we showed an example using fault.PythonTester to simulate a circuit.  This uses an interactive programming model where test actions are immediately dispatched to the underlying simulator (which is why we can perform assertions on the simulation values in Python.
fault also provides a staged metaprogramming environment built upon the Tester class.  Using the staged environment means values are not returned immediately to Python.  Instead, the Python test code records a sequence of actions that are compiled and run in a later stage.
A Tester is instantiated with a magma circuit.
Step10:  An instance of a Tester has an attribute .circuit that enables the user to record test actions.  For example, inputs to a circuit can be poked by setting the attribute corresponding to the input port name.
Step11:  fault's default Tester provides the semantics of a cycle accurate simulator, so, unlike verilog, pokes do not create events that trigger computation.  Instead, these poke values are staged, and the propogation of their effect occurs when the user calls the eval action.
Step12:  To assert that the output of the circuit is equal to a value, we use the expect method that are defined on the attributes corresponding to circuit output ports
Step13:  Because fault is a staged programming environment, the above actions are not executed until we have advanced to the next stage.  In the first stage, the user records test actions (e.g. poke, eval, expect).  In the second stage, the test is compiled and run using a target runtime.  Here's examples of running the test using magma's python simulator, the coreir c++ simulator, and verilator.
Step14:  The tester also provides the same convenient __call__ interface we saw before.
Step15:  Generate Verilog
Magma's default compiler will generate verilog using CoreIR
Step16:  Generate CoreIR
We can also inspect the intermediate CoreIR used in the generation process.
Step17:  Here's an example of running a CoreIR pass on the intermediate representation.
<ASSISTANT_TASK:>
Python Code:
import magma as m
import mantle
Explanation: FullAdder - Combinational Circuits
This notebook walks through the implementation of a basic combinational circuit, a full adder. This example introduces many of the features of Magma including circuits, wiring, operators, and the type system.
Start by importing magma and mantle. magma is the core system which implements circuits and the methods to compose them, and mantle is a library of useful circuits.
End of explanation
@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
Explanation: A full adder has three single bit inputs, and returns the sum and the carry. The sum is the exclusive or of the 3 bits, the carry is 1 if any two of the inputs bits are 1. Here is a schematic of a full adder circuit (from logisim).
<img src="images/full_adder_logisim.png" width="500"/>
We start by defining a magma combinational function that implements a full adder. 
The full adder function takes three single bit inputs (type m.Bit) and returns two single bit outputs as a tuple.
The first element of tuple is the sum, the second element is the carry.  Note that the arguments and return values of the functions have type annotations using Python 3's typing syntax.
We compute the sum and carry using standard Python bitwise operators &amp;, |, and ^.
End of explanation
import fault
tester = fault.PythonTester(full_adder)
assert tester(1, 0, 0) == (1, 0), "Failed"
assert tester(0, 1, 0) == (1, 0), "Failed"
assert tester(1, 1, 0) == (0, 1), "Failed"
assert tester(1, 0, 1) == (0, 1), "Failed"
assert tester(1, 1, 1) == (1, 1), "Failed"
print("Success!")
Explanation: We can test our combinational function to verify that our implementation behaves as expected fault.
We'll use the fault.PythonTester which will simulate the circuit using magma's Python simulator.
End of explanation
assert tester(1, 0, 0) == full_adder(1, 0, 0), "Failed"
assert tester(0, 1, 0) == full_adder(0, 1, 0), "Failed"
assert tester(1, 1, 0) == full_adder(1, 1, 0), "Failed"
assert tester(1, 0, 1) == full_adder(1, 0, 1), "Failed"
assert tester(1, 1, 1) == full_adder(1, 1, 1), "Failed"
print("Success!")
Explanation: combinational functions are polymorphic over Python and magma types.  If the function is called with magma values, it will produce a circuit instance, wire up the inputs, and return references to the outputs.   Otherwise, it will invoke the function in Python.  For example, we can use the Python function to verify the circuit simulation.
End of explanation
class FullAdder(m.Circuit):
    io = m.IO(I0=m.In(m.Bit),
              I1=m.In(m.Bit),
              CIN=m.In(m.Bit),
              O=m.Out(m.Bit),
              COUT=m.Out(m.Bit))
    
    O, COUT = full_adder(io.I0, io.I1, io.CIN)
    io.O @= O
    io.COUT @= COUT
Explanation: Circuits
Now that we have an implementation of full_adder as a combinational function, 
we'll use it to construct a magma Circuit. 
A Circuit in magma corresponds to a module in verilog.
This example shows using the combinational function inside a circuit definition, as opposed to using the Python implementation shown before.
End of explanation
print(repr(FullAdder))
Explanation: First, notice that the FullAdder is a subclass of Circuit. All magma circuits are classes in python.
Second, the function IO creates the interface to the circuit. 
The arguments toIO are keyword arguments. 
The key is the name of the argument in the circuit, and the value is its type. 
In this circuit, all the inputs and outputs have Magma type Bit. 
We also qualify each type as an input or an output using the functions In and Out.
Note that when we call the python function fulladder
it is passed magma values not standard python values.
In the previous cell,  we tested fulladder with standard python ints,
while in this case, the values passed to the Python fulladder function 
are magma values of type Bit.
The Python bitwise operators for Magma types are overloaded to automatically create subcircuits to compute logical functions.
fulladder returns two values.
These values are assigned to the python variables O and COUT. 
Remember that assigning to a Python variable 
sets the variable to refer to the object.
magma values are Python objects,
so assigning an object to a variable creates a reference to that magma value.
In order to complete the definition of the circuit, 
O and COUT need to be wired to the outputs in the interface.
The python @= operator is overloaded to perform wiring.
Let's inspect the circuit definition by printing the __repr__.
End of explanation
print(repr(full_adder.circuit_definition))
Explanation: We see that it has created an instance of the full_adder combinational function and wired up the interface.
We can also inspect the contents of the full_adder circuit definition.  Notice that it has lowered the Python operators into a structural representation of the primitive logicoperations.
End of explanation
with open(".magma/full_adder.py") as f:
    print(f.read())
Explanation: We can also inspect the code generated by the m.circuit.combinational decorator by looking in the .magma directory for a file named .magma/full_adder.py.  When using m.circuit.combinational, magma will generate a file matching the name of the decorated function.  You'll notice that the generated code introduces an extra temporary variable (this is an artifact of the SSA pass that magma runs to handle if/else statements).
End of explanation
import logging
logging.basicConfig(level=logging.INFO)
import fault
Explanation: In the code above, a mux is imported and named phi. If the combinational circuit contains any if-then-else constructs, they will be transformed into muxes.
Note also the m.wire function. m.wire(O0, io.I0) is equivalent to io.O0 @= O0.
Staged testing with Fault
fault is a python package for testing magma circuits.  By default, fault is quiet, so we begin by enabling logging using the built-in logging module
End of explanation
tester = fault.Tester(FullAdder)
Explanation: Earlier in the notebook, we showed an example using fault.PythonTester to simulate a circuit.  This uses an interactive programming model where test actions are immediately dispatched to the underlying simulator (which is why we can perform assertions on the simulation values in Python.
fault also provides a staged metaprogramming environment built upon the Tester class.  Using the staged environment means values are not returned immediately to Python.  Instead, the Python test code records a sequence of actions that are compiled and run in a later stage.
A Tester is instantiated with a magma circuit.
End of explanation
tester.circuit.I0 = 1
tester.circuit.I1 = 1
tester.circuit.CIN = 1
Explanation: An instance of a Tester has an attribute .circuit that enables the user to record test actions.  For example, inputs to a circuit can be poked by setting the attribute corresponding to the input port name.
End of explanation
tester.eval()
Explanation: fault's default Tester provides the semantics of a cycle accurate simulator, so, unlike verilog, pokes do not create events that trigger computation.  Instead, these poke values are staged, and the propogation of their effect occurs when the user calls the eval action.
End of explanation
tester.circuit.O.expect(1)
tester.circuit.COUT.expect(1)
Explanation: To assert that the output of the circuit is equal to a value, we use the expect method that are defined on the attributes corresponding to circuit output ports
End of explanation
# compile_and_run throws an exception if the test fails
tester.compile_and_run("verilator")
Explanation: Because fault is a staged programming environment, the above actions are not executed until we have advanced to the next stage.  In the first stage, the user records test actions (e.g. poke, eval, expect).  In the second stage, the test is compiled and run using a target runtime.  Here's examples of running the test using magma's python simulator, the coreir c++ simulator, and verilator.
End of explanation
O, COUT = tester(1, 0, 0)
tester.expect(O, 1)
tester.expect(COUT, 0)
tester.compile_and_run("verilator")
Explanation: The tester also provides the same convenient __call__ interface we saw before.
End of explanation
m.compile("build/FullAdder", FullAdder, inline=True)
%cat build/FullAdder.v
Explanation: Generate Verilog
Magma's default compiler will generate verilog using CoreIR
End of explanation
%cat build/FullAdder.json
Explanation: Generate CoreIR
We can also inspect the intermediate CoreIR used in the generation process.
End of explanation
!coreir -i build/FullAdder.json -p instancecount
Explanation: Here's an example of running a CoreIR pass on the intermediate representation.
End of explanation
<END_TASK>