Skip to content

Commit

Permalink
Merge 3d13552 into 36aa150
Browse files Browse the repository at this point in the history
  • Loading branch information
rsetaluri committed May 2, 2019
2 parents 36aa150 + 3d13552 commit f3ca9bf
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 9 deletions.
133 changes: 128 additions & 5 deletions magma/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .logging import warning
from .port import report_wiring_warning
from .is_definition import isdefinition
from .ref import DefnRef, InstRef


__all__ = ['AnonymousCircuitType']
Expand All @@ -32,6 +33,26 @@
__all__ += ['CopyInstance']
__all__ += ['circuit_type_method']
__all__ += ['circuit_generator']
__all__ += ['IO']


class IO:
"""
The `IO` object constructs a set of anonymous values to be used for the
ports of a circuit definition. The circuit definition is defined in terms
of the anonymous values. The `IO` object is processed at the end of the
`__new__` method of the circuit metaclass by the method
`process_new_style_definition`.
"""
def __init__(self, **kwargs):
self.ports = {}
for key, value in kwargs.items():
# Inputs to a circuit are outputs in the context of the circuit
# definition (and vice versa), so we flip the types for the body of
# the class definition
value = value.flip()()
setattr(self, key, value)
self.ports[key] = value


circuit_type_method = namedtuple('circuit_type_method', ['name', 'definition'])
Expand Down Expand Up @@ -475,9 +496,96 @@ def __new__(metacls, name, bases, dct):
self.definition()
self._is_definition = True
EndCircuit()
io = set(v for k, v in dct.items() if isinstance(v, IO))
if hasattr(self, 'IO') and io:
raise Exception("Found mixed new and old style IO specification")
if len(io) > 1:
raise Exception("Found multiple instances of IO")
elif io:
io = next(iter(io))
self.__process_new_style_definition(io, dct["renamed_ports"])

return self

def __process_new_style_definition(self, io, renamed_ports):
"""
The `__process_new_style_definition` method constructs an "old-style" IO
list (e.g. `["I0", m.In(m.Bit), ...]`) and passes this to the old logic
for setting up interfaces (`DeclareInterface`). This required the
minimal amount of changes to the existing code, although we may consider
rearchitecting the entire interface pipeline given this opportunity.
Because we use the old-style interface setup logic, we must traverse the
definition graph defined on the temporary anonymous values setup by the
`IO` object and replace the anonymous interface values with the concrete
values from the old-style interface setup logic.
`__process_new_style_definition` also traverses the definition graph and
"places" the instances (by using `self.place`) within the current
definition.
"""
# Generate the old IO list and reuse the logic to setup the
# interface
io_list = []
for key, value in io.ports.items():
io_list += [key, type(value).flip()]
self.IO = DeclareInterface(*io_list)
self.interface = self.IO(defn=self, renamed_ports=renamed_ports)
setports(self, self.interface.ports)

# Traverse the definition graph to fixup the anonymous instances by
# placing them in the current definition
# Done using a worke queue algorithm, intialized with the drivers of
# the outputs (viewed as inputs) to the definition
worklist = []
for key, value in io.ports.items():
value.name = DefnRef(self, key)
if value.isinput():
driver = value.value()
if driver is not None:
# Wire the driver up the definition bit setup by the old
# interface logic
getattr(self, key).wire(driver)
worklist.append(driver)
if not worklist:
# Nothing wired up to the outputs, assume this is a declaration
# then
return
self._is_definition = True

while worklist:
curr = worklist.pop(0)
assert curr is not None, "Found unwired port"

if isinstance(curr.name, InstRef):
# For instance references, we place the instance in the current
# definition, then add the driver of its inputs to the queue
inst = curr.name.inst
if inst.defn:
inst.defn.__unplace(inst)
self.place(inst)
for key, value in inst.interface.items():
if value.isinput():
value = getattr(inst, key)
driver = value.value()
if isinstance(driver.name, DefnRef):
# If it's driven by a definition port, remove
# temporary anonymous bit from the port wires and
# replace with definition bit setup by the old
# interface logic
outputs = value.port.wires.outputs
for i, output in enumerate(outputs):
if output.bit is driver:
del value.port.wires.outputs[i]
break
else:
assert False, "Could not find temp anon bit"
value.wire(getattr(self, driver.name.name))
else:
worklist.append(driver)
else:
raise NotImplementedError(type(value.name))

@property
def is_definition(self):
return self._is_definition or self.verilog or self.verilogFile
Expand All @@ -486,18 +594,33 @@ def is_definition(self):
def instances(self):
return self._instances

#
# place a circuit instance in this definition
#
def place(cls, inst):
"""
Place a circuit instance in this definition.
"""
if inst.defn:
raise Exception(f"Can not place instance twice. Instance "
f"{repr(inst)} already placed in defn {cls.name}.")
type_ = type(inst)
inst.prev_name = inst.name
if not inst.name:
inst.name = f"{type(inst).name}_inst{str(cls.instanced_circuits_counter[type(inst).name])}"
cls.instanced_circuits_counter[type(inst).name] += 1
count = cls.instanced_circuits_counter[type_.name]
inst.name = f"{type(inst).name}_inst{str(count)}"
cls.instanced_circuits_counter[type_.name] += 1
inst.defn = cls
if get_debug_mode():
inst.stack = inspect.stack()
cls.instances.append(inst)

def __unplace(cls, inst):
if inst not in cls.instances:
raise Exception(f"Instance {repr(inst)} not placed in {cls.name}")
inst.defn = None
inst.name = inst.prev_name
if hasattr(inst, "stack"):
delattr(inst, "stack")
cls.instances.remove(inst)


# Register graphviz repr if running in IPython.
# There's a bug in IPython which breaks visual reprs
Expand Down
11 changes: 9 additions & 2 deletions magma/port.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,14 @@ def connect( self, o, i , debug_info):
#print(str(o), o.anon(), o.bit.isinput(), o.bit.isoutput())
#print(str(i), i.anon(), i.bit.isinput(), i.bit.isoutput())

if not o.anon():
# TODO: Need this check for tests/test_type/test_anon.py, basically if
# we have two anonymous ports, they share the same wire rather than
# using the same wiring logic as everything else. Why?
# For the new syntax without `def definition` we leverage the fact that
# we can normally wire up anonymous ports and give them names later. To
# do this, we need to restrict this check to only skip anonymous ports
# that are undirected (before it skipped all anonymous ports)
if not (o.anon() and o.bit.direction is None):
#assert o.bit.direction is not None
if o.bit.isinput():
report_wiring_error(f"Using `{o.bit.debug_name}` (an input) as an output", debug_info)
Expand All @@ -91,7 +98,7 @@ def connect( self, o, i , debug_info):
#print('adding output', o)
self.outputs.append(o)

if not i.anon():
if not (i.anon() and i.bit.direction is None):
#assert i.bit.direction is not None
if i.bit.isoutput():
report_wiring_error(f"Using `{i.bit.debug_name}` (an output) as an input", debug_info)
Expand Down
90 changes: 90 additions & 0 deletions tests/test_circuit/gold/test_new_style_simple_mux.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* External Modules
module or (
input I0,
input I1,
output O
);
endmodule // or
*/
/* External Modules
module invert (
input I,
output O
);
endmodule // invert
*/
/* External Modules
module and (
input I0,
input I1,
output O
);
endmodule // and
*/
module Test (
input I0,
input I1,
output O,
input S
);


wire and_inst0__I0;
wire and_inst0__I1;
wire and_inst0__O;
and and_inst0(
.I0(and_inst0__I0),
.I1(and_inst0__I1),
.O(and_inst0__O)
);

wire and_inst1__I0;
wire and_inst1__I1;
wire and_inst1__O;
and and_inst1(
.I0(and_inst1__I0),
.I1(and_inst1__I1),
.O(and_inst1__O)
);

wire invert_inst0__I;
wire invert_inst0__O;
invert invert_inst0(
.I(invert_inst0__I),
.O(invert_inst0__O)
);

wire or_inst0__I0;
wire or_inst0__I1;
wire or_inst0__O;
or or_inst0(
.I0(or_inst0__I0),
.I1(or_inst0__I1),
.O(or_inst0__O)
);

assign and_inst0__I0 = S;

assign and_inst0__I1 = I1;

assign or_inst0__I0 = and_inst0__O;

assign and_inst1__I0 = invert_inst0__O;

assign and_inst1__I1 = I0;

assign or_inst0__I1 = and_inst1__O;

assign invert_inst0__I = S;

assign O = or_inst0__O;


endmodule // Test

98 changes: 98 additions & 0 deletions tests/test_circuit/test_new_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import magma as m
from magma.testing import check_files_equal

# Stub operators for test
m.BitType.__and__ = \
lambda x, y: m.DeclareCircuit("and", "I0", m.In(m.Bit),
"I1", m.In(m.Bit),
"O", m.Out(m.Bit))()(x, y)
m.BitType.__or__ = \
lambda x, y: m.DeclareCircuit("or", "I0", m.In(m.Bit),
"I1", m.In(m.Bit),
"O", m.Out(m.Bit))()(x, y)
m.BitType.__invert__ = \
lambda x: m.DeclareCircuit("invert", "I", m.In(m.Bit),
"O", m.Out(m.Bit))()(x)


def test_new_style_simple_mux():
class Test(m.Circuit):
io = m.IO(
S=m.In(m.Bit),
I0=m.In(m.Bit),
I1=m.In(m.Bit),
O=m.Out(m.Bit),
)

io.O <= (io.S & io.I1) | (~io.S & io.I0)
print(repr(Test))
assert repr(Test) == """\
Test = DefineCircuit("Test", "S", In(Bit), "I0", In(Bit), "I1", In(Bit), "O", Out(Bit))
or_inst0 = or()
and_inst0 = and()
and_inst1 = and()
invert_inst0 = invert()
wire(and_inst0.O, or_inst0.I0)
wire(and_inst1.O, or_inst0.I1)
wire(Test.S, and_inst0.I0)
wire(Test.I1, and_inst0.I1)
wire(invert_inst0.O, and_inst1.I0)
wire(Test.I0, and_inst1.I1)
wire(Test.S, invert_inst0.I)
wire(or_inst0.O, Test.O)
EndCircuit()\
"""

m.compile('build/test_new_style_simple_mux', Test, output="coreir-verilog")
assert check_files_equal(__file__, f"build/test_new_style_simple_mux.v",
f"gold/test_new_style_simple_mux.v")


def test_new_style_nested():
Top = m.DefineCircuit("Top", "S", m.In(m.Bit), "I0", m.In(m.Bit),
"I1", m.In(m.Bit), "O", m.Out(m.Bit),)

# Begin definition of Mux while in the middle of defining Top.
class Mux(m.Circuit):
io = m.IO(
S=m.In(m.Bit),
I0=m.In(m.Bit),
I1=m.In(m.Bit),
O=m.Out(m.Bit),
)

io.O <= (io.S & io.I1) | (~io.S & io.I0)

# Continue defining Top.
mux = Mux()
mux.I0 <= Top.I0
mux.I1 <= Top.I1
mux.S <= Top.S
Top.O <= mux.O
m.EndDefine()

assert repr(Mux) == """\
Mux = DefineCircuit("Mux", "S", In(Bit), "I0", In(Bit), "I1", In(Bit), "O", Out(Bit))
or_inst0 = or()
and_inst0 = and()
and_inst1 = and()
invert_inst0 = invert()
wire(and_inst0.O, or_inst0.I0)
wire(and_inst1.O, or_inst0.I1)
wire(Mux.S, and_inst0.I0)
wire(Mux.I1, and_inst0.I1)
wire(invert_inst0.O, and_inst1.I0)
wire(Mux.I0, and_inst1.I1)
wire(Mux.S, invert_inst0.I)
wire(or_inst0.O, Mux.O)
EndCircuit()\
"""
assert repr(Top) == """\
Top = DefineCircuit("Top", "S", In(Bit), "I0", In(Bit), "I1", In(Bit), "O", Out(Bit))
Mux_inst0 = Mux()
wire(Top.S, Mux_inst0.S)
wire(Top.I0, Mux_inst0.I0)
wire(Top.I1, Mux_inst0.I1)
wire(Mux_inst0.O, Top.O)
EndCircuit()\
"""
4 changes: 2 additions & 2 deletions tests/test_type/test_anon.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test():
assert b0.port.wires is b1.port.wires

wires = b0.port.wires
assert len(wires.inputs) == 0
assert len(wires.outputs) == 1
print('inputs:', [str(p) for p in wires.inputs])
print('outputs:', [str(p) for p in wires.outputs])
assert len(wires.inputs) == 0
assert len(wires.outputs) == 1

0 comments on commit f3ca9bf

Please sign in to comment.