Move code from arch-defs

Signed-off-by: Karol Gugala <kgugala@antmicro.com>
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8a18954
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[metadata]
+license_files = COPYING
+
+[bdist_wheel]
+universal=1
+
+[aliases]
+test=pytest
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..d5d3bef
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,28 @@
+import setuptools
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+setuptools.setup(
+    name="v2x",
+    version="0.0.1",
+    author="SymbiFlow Authors",
+    author_email="symbiflow@lists.librecores.org",
+    description="Python library for generating VPR architecture \
+                description files from Verilog models.",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://github.com/SymbiFlow/python-symbiflow-v2x",
+    packages=setuptools.find_packages(),
+    install_requires=['ply', 'pyjson'],
+    setup_requires=["pytest-runner"],
+    tests_require=["pytest"],
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "License :: OSI Approved :: ISC License",
+        "Operating System :: OS Independent",
+    ],
+    #entry_points={
+    #    'console_scripts': ['sdf_timing_parse=sdf_timing.sdfparse:main'],
+    #},
+)
diff --git a/v2x/__init__.py b/v2x/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/v2x/__init__.py
diff --git a/v2x/__main__.py b/v2x/__main__.py
new file mode 100644
index 0000000..f038ba4
--- /dev/null
+++ b/v2x/__main__.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+
+from .v2x import vlog_to_pbtype
+
+def main():
+    echo("main")
+
+if __name__ = '__main__':
+    main()
diff --git a/v2x/vlog_to_model.py b/v2x/vlog_to_model.py
new file mode 100755
index 0000000..4280d0d
--- /dev/null
+++ b/v2x/vlog_to_model.py
@@ -0,0 +1,240 @@
+#!/usr/bin/env python3
+"""
+Convert a Verilog simulation model to a VPR `model.xml`
+
+The following Verilog attributes are considered on ports:
+    - `(* CLOCK *)` : force a given port to be a clock
+
+    - `(* ASSOC_CLOCK="RDCLK" *)` : force a port's associated clock to a given value
+
+The following Verilog attributes are considered on modules:
+    - `(* MODEL_NAME="model" *)` : override the name used for <model> and for
+    ".subckt name" in the BLIF model. Mostly intended for use with w.py, when several
+    different pb_types implement the same model.
+
+    - `(* CLASS="lut|routing|mux|flipflop|mem" *)` : specify the class of an given
+    instance. A model will not be generated for the `lut`, `routing` or `flipflop`
+    class.
+"""
+import argparse
+import os
+import re
+import sys
+
+import lxml.etree as ET
+
+from .yosys import run
+from .yosys.json import YosysJSON
+
+from .lib import xmlinc
+
+
+def is_clock_assoc(infiles, module, clk, port, direction):
+    """Checks if a specific port is associated with a clk clock
+
+    Returns a boolean value
+    -------
+    is_clock_assoc: bool
+    """
+    clock_assoc_signals = run.get_clock_assoc_signals(
+        infiles, module, clk
+    )
+
+    if direction == "input":
+        assoc_outputs = run.get_related_output_for_input(
+            infiles, module, port
+        )
+        for out in assoc_outputs:
+            if out in clock_assoc_signals:
+                return True
+    elif direction == "output":
+        if port in clock_assoc_signals:
+            return True
+    else:
+        assert False, "Bidirectional ports are not supported yet"
+
+    return False
+
+
+def is_registered_path(tmod, pin, pout):
+    """Checks if a i/o path is sequential. If that is the case
+    no combinational_sink_port is needed
+
+    Returns a boolean value
+    """
+
+    for cell, ctype in tmod.all_cells:
+        if ctype != "$dff":
+            continue
+
+        if tmod.port_conns(pin) == tmod.cell_conn_list(
+                cell, "D") and tmod.port_conns(pout) == tmod.cell_conn_list(
+                    cell, "Q"):
+            return True
+
+    return False
+
+def vlog_to_model(infiles, includes, top, outfile=None):
+    iname = os.path.basename(infiles[0])
+
+    if outfile is None:
+        outfile = "model.xml"
+
+    if includes:
+        for include in includes.split(','):
+            run.add_include(include)
+
+    aig_json = run.vlog_to_json(infiles, flatten=True, aig=True)
+
+    if top is not None:
+        top = top.upper()
+    else:
+        yj = YosysJSON(aig_json)
+        if yj.top is not None:
+            top = yj.top
+        else:
+            wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", iname)
+            if wm:
+                top = wm.group(1).upper()
+            else:
+                print(
+                    """\
+    ERROR file name not of format %.sim.v ({}), cannot detect top level.
+    Manually specify the top level module using --top"""
+                ).format(iname)
+                sys.exit(1)
+
+    assert top is not None
+    yj = YosysJSON(aig_json, top)
+    if top is None:
+        print(
+            """\
+    ERROR: more than one module in design, cannot detect top level.
+    Manually specify the top level module using --top"""
+        )
+        sys.exit(1)
+
+    tmod = yj.top_module
+    models_xml = ET.Element("models", nsmap={'xi': xmlinc.xi_url})
+
+    inc_re = re.compile(r'^\s*`include\s+"([^"]+)"')
+
+    deps_files = set()
+    # XML dependencies need to correspond 1:1 with Verilog includes, so we have
+    # to do this manually rather than using Yosys
+    with open(infiles[0], 'r') as f:
+        for line in f:
+            im = inc_re.match(line)
+            if not im:
+                continue
+            deps_files.add(im.group(1))
+
+    if len(deps_files) > 0:
+        # Has dependencies, not a leaf model
+        for df in sorted(deps_files):
+            abs_base = os.path.dirname(os.path.abspath(infiles[0]))
+            abs_dep = os.path.normpath(os.path.join(abs_base, df))
+            module_path = os.path.dirname(abs_dep)
+            module_basename = os.path.basename(abs_dep)
+            wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", module_basename)
+            if wm:
+                model_path = "{}/{}.model.xml".format(
+                    module_path,
+                    wm.group(1).lower()
+                )
+            else:
+                assert False, "included Verilog file name {} does not follow pattern %%.sim.v".format(
+                    module_basename
+                )
+            xmlinc.include_xml(
+                parent=models_xml,
+                href=model_path,
+                outfile=outfile,
+                xptr="xpointer(models/child::node())"
+            )
+    else:
+        # Is a leaf model
+        topname = tmod.attr("MODEL_NAME", top)
+        assert topname == topname.upper(
+        ), "Leaf model names should be all uppercase!"
+        modclass = tmod.attr("CLASS", "")
+
+        if modclass not in ("lut", "routing", "flipflop"):
+            model_xml = ET.SubElement(models_xml, "model", {'name': topname})
+            ports = tmod.ports
+
+            inports_xml = ET.SubElement(model_xml, "input_ports")
+            outports_xml = ET.SubElement(model_xml, "output_ports")
+
+            clocks = run.list_clocks(infiles, top)
+
+            for name, width, bits, iodir in ports:
+                attrs = dict(name=name)
+                sinks = run.get_combinational_sinks(infiles, top, name)
+
+                # Removes comb sinks if path from in to out goes through a dff
+                for sink in sinks:
+                    if is_registered_path(tmod, name, sink):
+                        sinks.remove(sink)
+
+                # FIXME: Check if ignoring clock for "combination_sink_ports" is a
+                # valid thing to do.
+                if name in clocks or "clk" in name.lower():
+                    attrs["is_clock"] = "1"
+                else:
+                    clks = list()
+                    if len(sinks) > 0 and iodir == "input":
+                        attrs["combinational_sink_ports"] = " ".join(sinks)
+                    for clk in clocks:
+                        if is_clock_assoc(infiles, top, clk, name, iodir):
+                            clks.append(clk)
+                        if clks:
+                            attrs["clock"] = " ".join(clks)
+                if iodir == "input":
+                    ET.SubElement(inports_xml, "port", attrs)
+                elif iodir == "output":
+                    ET.SubElement(outports_xml, "port", attrs)
+                else:
+                    assert False, "bidirectional ports not permitted in VPR models"
+
+    if len(models_xml) == 0:
+        models_xml.insert(0, ET.Comment("this file is intentionally left blank"))
+
+    f = open(outfile, 'w')
+    f.write(ET.tostring(models_xml, pretty_print=True).decode('utf-8'))
+    f.close()
+    print("Generated {} from {}".format(outfile, iname))
+
+if __name__ == "__main__":
+
+    parser = argparse.ArgumentParser(description=__doc__.strip())
+    parser.add_argument(
+        'infiles',
+        metavar='input.v',
+        type=str,
+        nargs='+',
+        help="""\
+    One or more Verilog input files, that will be passed to Yosys internally.
+    They should be enough to generate a flattened representation of the model,
+    so that paths through the model can be determined.
+    """
+    )
+    parser.add_argument(
+        '--top',
+        help="""\
+    Top level module, will usually be automatically determined from the file name
+    %.sim.v
+    """
+    )
+    parser.add_argument(
+        '--includes',
+        help="""\
+    Comma separate list of include directories.
+    """,
+        default=""
+    )
+    parser.add_argument('-o', help="""\
+    Output filename, default 'model.xml'
+    """)
+
+    args = parser.parse_args()
diff --git a/v2x/vlog_to_pbtype.py b/v2x/vlog_to_pbtype.py
new file mode 100755
index 0000000..9cb7a6c
--- /dev/null
+++ b/v2x/vlog_to_pbtype.py
@@ -0,0 +1,878 @@
+#!/usr/bin/env python3
+"""\
+Convert a Verilog simulation model to a VPR `pb_type.xml`
+
+The following are allowed on a top level module:
+
+    - `(* blackbox *)` : specify that the module has no interconnect or child
+    pb_types (but if modes are used then its modes are allowed to have these).
+    This will also set the BLIF model to be `.subckt <name>` unless CLASS is
+    also specified.
+
+    - `(* CLASS="lut|routing|mux|flipflop|mem" *)` : specify the class of an given
+    instance.
+
+    - `(* MODES="mode1; mode2; ..." *)` : specify that the module has more than one functional
+    mode, each with a given name. The module will be evaluated n times, each time setting
+    the MODE parameter to the nth value in the list of mode names. Each evaluation will be
+    put in a pb_type `<mode>` section named accordingly.
+
+    - `(* MODEL_NAME="model" *)` : override the name used for <model> and for
+    ".subckt name" in the BLIF model. Mostly intended for use with w.py, when several
+    different pb_types implement the same model.
+
+The following are allowed on nets within modules (TODO: use proper Verilog timing):
+    - `(* SETUP="clk 10e-12" *)` : specify setup time for a given clock
+
+    - `(* HOLD="clk 10e-12" *)` : specify hold time for a given clock
+
+    - `(* CLK_TO_Q="clk 10e-12" *)` : specify clock-to-output time for a given clock
+
+    - `(* DELAY_CONST_{input}="30e-12" *)` : specify a constant max delay from an input
+                                            (applied to the output)
+
+    - `(* DELAY_MATRIX_{input}="30e-12 35e-12; 20e-12 25e-12; ..." *)` : specify a VPR
+        delay matrix (semicolons indicate rows). In this format columns specify
+        inputs bits and rows specify output bits. This should be applied to the output.
+
+    - `(* carry = ADDER *)` : specify carry chain pack_pattern associated with this wire
+
+The following are allowed on ports:
+    - `(* CLOCK *)` : force a given port to be a clock
+
+    - `(* ASSOC_CLOCK="RDCLK" *)` : force a port's associated clock to a given value
+
+    - `(* PORT_CLASS="clock" *)` : specify the VPR "port_class"
+
+    - `(* carry = ADDER *)` : specify carry chain pack_pattern associated with this port
+
+The Verilog define "PB_TYPE" is set during generation.
+"""
+
+import os
+import sys
+import argparse
+import re
+
+from typing import List, Dict, Tuple
+from collections import defaultdict
+
+import lxml.etree as ET
+
+from .yosys import run
+from .yosys.json import YosysJSON
+
+from .lib import xmlinc  # noqa: E402
+
+
+def normalize_pb_name(pb_name):
+    """ Some pb_type names generatedby the tool
+        are illegal in VPR. This function converts them to
+        legal ones e.g:
+
+        output_dffs_gen[0].q_out_ff -> output_dffs_gen_q_out_ff_0
+    """
+    if pb_name is None:
+        return None
+
+    index = re.search(r'\[[0-9]+\](?!$)', pb_name)
+    normalized_name = pb_name.replace('.', '_')
+    if index is not None:
+        normalized_name = normalized_name.replace(
+            index.group(0), ""
+        ) + index.group(0).replace('[', '_').replace(']', '')
+
+    return normalized_name
+
+
+def is_mod_blackbox(mod):
+    """ Returns true if module is annotated with blackbox (or equivalent).
+
+    Yosys supports 3 attributes that denote blackbox behavior:
+
+    "blackbox" - Blackbox with no internal wiring
+    "whitebox" - Blackbox with internal connections and timing.
+    "lib_whitebox" - Like "whitebox" when read with "-lib", otherwise
+        attribute is removed.
+
+    """
+
+    return (mod.attr("lib_whitebox", 0) == 1) or \
+           (mod.attr("whitebox", 0) == 1) or \
+           (mod.attr("blackbox", 0) == 1)
+
+
+# $genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:12$64[57].\comb
+GENBLOCK_REGEX = re.compile(
+    "^\\$genblock\\$.*:[0-9]+\\$[0-9]+\\[(.*)\\]\\.\\\\(.*)"
+)
+
+
+def strip_name(name: str, include_index=True) -> str:
+    """Convert generate block into normal array form.
+
+    >>> n = r"$genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:12$64[57].\\comb"
+    >>> strip_name(n)
+    'comb[57]'
+    >>> strip_name(n, False)
+    'comb'
+    >>> n = r"$genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:15$10[3].\\comba"
+    >>> strip_name(n)
+    'comba[3]'
+    """
+    if name.startswith('$genblock$'):
+        index, name = GENBLOCK_REGEX.match(name).groups()
+        if include_index:
+            return "{}[{}]".format(name, index)
+        else:
+            return name
+    return name
+
+
+CellName = str
+CellType = str
+PinName = str
+CellPin = Tuple[CellName, PinName]
+
+
+def create_port(
+        dir_xml: ET.Element, cell_pin: CellPin, direction: str, metadata=None
+) -> ET.Element:
+    cell_name, pin_name = cell_pin
+    port = dict()
+    port['name'] = pin_name
+    port['type'] = direction
+    if cell_name:
+        port['from'] = cell_name
+    port_xml = ET.SubElement(dir_xml, 'port', port)
+
+    if metadata:
+        meta_root = ET.SubElement(port_xml, "metadata")
+        for name, value in metadata.items():
+            meta_type = ET.SubElement(meta_root, 'meta', {'name': name})
+            meta_type.text = value
+
+    return port_xml
+
+
+def copy_attrs(dst, srcs):
+    # Find attributes which are on all srcs dictionaries
+    all_have = []
+    for attr in set(sum((list(s.keys()) for s in srcs), [])):
+        if len(srcs) != sum(bool(attr in s) for s in srcs):
+            continue
+        all_have.append(attr)
+
+    for attr in all_have:
+        avalue = srcs[0][attr]
+        avalues = [s[attr] for s in srcs[1:]]
+
+        for other_avalue in avalues:
+            if avalue == other_avalue:
+                continue
+            raise ValueError('{} values: {}'.format(attr, [avalue] + avalues))
+
+        if attr in dst:
+            assert avalue == dst[attr]
+            raise ValueError(
+                '{} on net has value {} but pins have {}'.format(
+                    attr, dst[attr], avalue
+                )
+            )
+        dst[attr] = avalue
+
+
+def net_and_pin_attrs(yj, mod, driver: CellPin, sink: CellPin, netid: int):
+    def filter_src(x):
+        d = {}
+        for k, v in x.items():
+            if k == 'src':
+                continue
+            d[k] = v
+        return d
+
+    potential_attrs = []
+
+    driver_cell, driver_pin = driver
+    driver_type = mod.cell_type(driver_cell)
+    if driver_type is not None:
+        dmod = yj.module(driver_type)
+        potential_attrs.append(filter_src(dmod.port_attrs(driver_pin)))
+
+    sink_cell, sink_pin = sink
+    sink_type = mod.cell_type(sink_cell)
+    if sink_type is not None:
+        smod = yj.module(sink_type)
+        potential_attrs.append(filter_src(smod.port_attrs(sink_pin)))
+
+    net_attrs = filter_src(mod.net_attrs(mod.net_name(netid)))
+    copy_attrs(net_attrs, potential_attrs)
+    return net_attrs
+
+
+def make_direct_conn(
+        ic_xml: ET.Element, driver: CellPin, sink: CellPin, path_attr: dict
+) -> ET.Element:
+    dir_xml = ET.SubElement(ic_xml, 'direct')
+    create_port(dir_xml, driver, "input")
+    create_port(dir_xml, sink, "output")
+
+    pack_name = path_attr.get('pack', False)
+    if pack_name:
+        pp_xml = ET.SubElement(
+            dir_xml, 'pack_pattern', {
+                'name': pack_name,
+                'type': 'pack'
+            }
+        )
+        create_port(pp_xml, driver, "input")
+        create_port(pp_xml, sink, "output")
+
+    carry_name = path_attr.get('carry', None)
+    if carry_name:
+        pp_xml = ET.SubElement(
+            dir_xml, 'pack_pattern', {
+                'name': carry_name,
+                'type': 'carry'
+            }
+        )
+        create_port(pp_xml, driver, "input")
+        create_port(pp_xml, sink, "output")
+
+    return dir_xml
+
+
+def make_mux_conn(
+        ic_xml: ET.Element, mux_name: str, mux_inputs: Dict[CellPin, CellPin],
+        mux_outputs: Dict[CellPin, List[CellPin]]
+) -> ET.Element:
+
+    mux_xml = ET.SubElement(ic_xml, "mux", {"name": mux_name})
+    for mux_input, driver in mux_inputs.items():
+        create_port(mux_xml, driver, "input", metadata={'fasm_mux': mux_input})
+    assert len(mux_outputs) == 1, mux_outputs
+    for mux_pin, sinks in mux_outputs.items():
+        assert len(sinks) == 1, sinks
+        for sink_pin, path_attr in sinks:
+            create_port(mux_xml, sink_pin, "output")
+
+    #  <metadata>
+    meta_root = ET.SubElement(mux_xml, 'metadata')
+    #    <meta name="type">bel</meta>
+    meta_type = ET.SubElement(meta_root, 'meta', {'name': 'type'})
+    meta_type.text = "bel"
+    #    <meta name="subtype">routing</meta>
+    meta_subtype = ET.SubElement(meta_root, 'meta', {'name': 'subtype'})
+    meta_subtype.text = "routing"
+
+    return mux_xml
+
+
+def get_interconnects(yj, mod, mod_pname: str,
+                      valid_names) -> Dict[CellPin, List[CellPin]]:
+    """Get the connectivity of module.
+
+    Returns:
+        A dictionary containing with a list of sink pins for each driver pin.
+    """
+    interconn = defaultdict(list)
+    for cname, ctype in mod.cells:
+        pb_name = strip_name(cname)
+        assert pb_name in valid_names
+        if pb_name == mod_pname:
+            pb_name = None
+
+        # All interconnect going INTO a cell (top level or children).
+        inp_cons = mod.cell_conns(cname, "input")
+        for pin, net in inp_cons:
+            drvs = mod.net_drivers(net)
+            assert len(drvs) > 0, (
+                "ERROR: pin {}.{} has no driver, interconnect will be missing\n{}"
+                .format(cname, pin, mod)
+            )
+            assert len(drvs) < 2, (
+                "ERROR: pin {}.{} has multiple drivers, interconnect will be overspecified"
+                .format(cname, pin)
+            )
+            for drv_cell, drv_pin in drvs:
+                net_attr = net_and_pin_attrs(
+                    yj, mod, (drv_cell, drv_pin), (pb_name, pin), net
+                )
+
+                drv_cell_name = strip_name(drv_cell)
+                assert drv_cell_name in valid_names
+                if drv_cell_name == mod_pname:
+                    drv_cell_name = None
+                interconn[(drv_cell_name,
+                           drv_pin)].append(((pb_name, pin), net_attr))
+
+        # Only consider outputs from cell to top level IO.
+        # Inputs to other cells will be dealt with in those cells.
+        out_cons = mod.cell_conns(cname, "output")
+        for pin, net in out_cons:
+            sinks = mod.net_sinks(net)
+            for sink_cell, sink_pin in sinks:
+                if sink_cell != mod.name:
+                    continue
+                net_attr = net_and_pin_attrs(
+                    yj, mod, (pb_name, pin), (None, sink_pin), net
+                )
+                interconn[(pb_name, pin)].append(((None, sink_pin), net_attr))
+
+    import pprint
+    pprint.pprint(list(interconn.values()))
+
+    def pin_sort(p):
+        pin, attr = p
+        if pin[0] is None:
+            return ('', pin[1])
+        else:
+            return pin
+
+    for l in interconn.values():
+        l.sort(key=pin_sort)
+
+    return interconn
+
+
+def mode_interconnects(mod, mode_name) -> List[(CellPin)]:
+    interconn = []
+    for name, width, bits, iodir in mod.ports:
+        if iodir == "input":
+            interconn.append(((None, name), (mode_name, name)))
+        else:
+            interconn.append(((mode_name, name), (None, name)))
+    return interconn
+
+
+CellPrefix = str
+ChildrenDict = Dict[CellPrefix, Tuple[CellType, List[CellName]]]
+
+
+def get_children(yj, mod) -> Tuple[ChildrenDict, ChildrenDict]:
+    routing = dict()
+    children = dict()
+    for cname, ctype in mod.cells:
+        # We currently special case routing muxes
+        cell = yj.module(ctype)
+        if cell.CLASS == "routing":
+            d = routing
+        else:
+            d = children
+        cname_prefix = strip_name(cname, False)
+        if cname_prefix not in children:
+            d[cname_prefix] = (ctype, [])
+        assert d[cname_prefix][
+            0
+        ] == ctype, \
+            "Type of {} with prefix {} doesn't match existing. Type: {}, existing: {}".format(
+                cname, cname_prefix, ctype, children[cname_prefix]
+        )
+        d[cname_prefix][-1].append(strip_name(cname))
+
+    for d in (routing, children):
+        for _, l in children.values():
+            if len(l) > 1:
+                l.sort()
+                _, _ = get_list_name_and_length(l)
+
+    return routing, children
+
+
+def get_cellname_from_shortname(shortname, mod):
+    for cname, ctype in mod.cells:
+        if shortname != strip_name(cname):
+            continue
+        return cname
+    raise NameError("No cell named {}".format(shortname))
+
+
+def get_list_name_and_length(l: List[str]) -> Tuple[str, int]:
+    """
+    >>> get_list_name_and_length(['i[{}]'.format(i) for i in range(10)])
+    ('i', 10)
+
+    Assertion failure on missing value
+    >>> get_list_name_and_length(['i[0]', 'i[2]'])
+    Traceback (most recent call last):
+        ...
+    AssertionError: index 1 expected: i[1] != actual: i[2]
+    ['i[0]', 'i[2]']
+
+    Assertion failure when not starting at zero
+    >>> get_list_name_and_length(['i[1]', 'i[2]'])
+    Traceback (most recent call last):
+        ...
+    AssertionError: index 0 expected: i[0] != actual: i[1]
+    ['i[1]', 'i[2]']
+
+    Assertion failure when duplicate values
+
+    >>> get_list_name_and_length(['i[0]', 'i[0]'])
+    Traceback (most recent call last):
+        ...
+    AssertionError: index 1 expected: i[1] != actual: i[0]
+    ['i[0]', 'i[0]']
+
+    >>> get_list_name_and_length(['i[0]', 'i[1]', 'i[1]'])
+    Traceback (most recent call last):
+        ...
+    AssertionError: index 2 expected: i[2] != actual: i[1]
+    ['i[0]', 'i[1]', 'i[1]']
+
+    >>> get_list_name_and_length(['i[0]', 'i[1]', 'i[1]', 'i[2]'])
+    Traceback (most recent call last):
+        ...
+    AssertionError: index 2 expected: i[2] != actual: i[1]
+    ['i[0]', 'i[1]', 'i[1]', 'i[2]']
+
+    Assertion failure on incorrect formatting
+    >>> get_list_name_and_length(['i{}'.format(i) for i in range(4)])
+    Traceback (most recent call last):
+        ...
+    AssertionError: No index brackets found in item 0: i0
+    ['i0', 'i1', 'i2', 'i3']
+
+    Allow square brackets in name
+    >>> get_list_name_and_length(['i[1][{}]'.format(i) for i in range(4)])
+    ('i[1]', 4)
+    """
+    if not l:
+        return True
+
+    assert '[' in l[0], "No index brackets found in item 0: {}\n{}".format(
+        l[0], l
+    )
+    list_name = l[0][:l[0].rfind('[')]
+    sl = sorted(l, key=len)
+    for i in range(0, len(l)):
+        expected_item = "{}[{}]".format(list_name, i)
+        assert expected_item == sl[
+            i], "index {} expected: {} != actual: {}\n{}".format(
+                i, expected_item, sl[i], sl
+            )
+    return list_name, len(l)
+
+
+def make_ports(clocks, mod, pb_type_xml, only_type=None):
+    for name, width, bits, iodir in mod.ports:
+        ioattrs = {"name": name, "num_pins": str(width)}
+        pclass = mod.net_attr(name, "PORT_CLASS")
+        if pclass is not None:
+            ioattrs["port_class"] = pclass
+        if name in clocks:
+            if only_type and only_type != "clocks":
+                continue
+            port_xml = ET.SubElement(pb_type_xml, "clock", ioattrs)
+        elif iodir == "input":
+            if only_type and only_type != "inputs":
+                continue
+            port_xml = ET.SubElement(pb_type_xml, "input", ioattrs)
+        elif iodir == "output":
+            if only_type and only_type != "outputs":
+                continue
+            port_xml = ET.SubElement(pb_type_xml, "output", ioattrs)
+        else:
+            assert False, "bidirectional ports not supported in VPR pb_types"
+
+        port_attrs = mod.port_attrs(name)
+
+        carry_name = port_attrs.get('carry', None)
+        if carry_name:
+            ET.SubElement(
+                port_xml, 'pack_pattern', {
+                    'name': carry_name,
+                    'type': 'carry'
+                }
+            )
+
+
+def make_container_pb(
+        outfile, yj, mod, mod_pname, pb_type_xml, routing, children
+):
+    # Containers have to include children
+    # ------------------------------------------------------------
+    for child_prefix, (child_type, children_names) in children.items():
+        # Work out were the child pb_type file can be found
+        module_file = yj.get_module_file(child_type)
+        module_path = os.path.dirname(module_file)
+        module_basename = os.path.basename(module_file)
+        module_prefix = re.match(r"([A-Za-z0-9_]+)\.sim\.v",
+                                 module_basename).groups()[0]
+
+        pb_type_path = "{}/{}.pb_type.xml".format(module_path, module_prefix)
+
+        include_as_is = True
+        comment_str = ""
+        # Read the top level properties of the pb_type
+        with open(pb_type_path, 'r') as inc_xml:
+            xml_inc = ET.fromstring(inc_xml.read().encode('utf-8'))
+            inc_attrib = xml_inc.attrib
+            normalized_name = normalize_pb_name(child_prefix)
+            num_pb = str(len(children_names))
+            if normalized_name != inc_attrib['name']:
+                comment_str += "old_name {}".format(inc_attrib['name'])
+                inc_attrib['name'] = normalize_pb_name(child_prefix)
+                include_as_is = False
+            if num_pb != inc_attrib['num_pb']:
+                comment_str += " old_num_pb {}".format(inc_attrib['num_pb'])
+                inc_attrib['num_pb'] = str(len(children_names))
+                include_as_is = False
+
+        xptr = None
+        parent_xml = pb_type_xml
+        if include_as_is is not True:
+            xptr = "xpointer(pb_type/child::node())"
+            parent_xml = ET.SubElement(pb_type_xml, 'pb_type', inc_attrib)
+            parent_xml.append(ET.Comment(comment_str))
+
+        xmlinc.include_xml(
+            parent=parent_xml, href=pb_type_path, outfile=outfile, xptr=xptr
+        )
+
+    # Contains need interconnect to their children
+    # ------------------------------------------------------------
+    # Work out valid names for cells to sanity check the interconnects.
+    valid_names = [mod_pname]
+
+    routing_cells = []
+    for _, routing_names in routing.values():
+        routing_cells.extend(routing_names)
+    valid_names.extend(routing_cells)
+
+    for _, children_names in children.values():
+        valid_names.extend(children_names)
+
+    # Extract the interconnect from the module
+    interconn = get_interconnects(yj, mod, mod_pname, valid_names)
+    import pprint
+    print(mod_pname)
+    print("--")
+    pprint.pprint(interconn)
+    print("--")
+    print(routing_cells)
+    pprint.pprint(routing)
+    print("--")
+
+    # Generate the actual interconnect
+    ic_xml = ET.SubElement(pb_type_xml, "interconnect")
+    for (driver_cell, driver_pin), sinks in interconn.items():
+        if driver_cell in routing_cells:
+            continue
+        for (sink_cell, sink_pin), path_attr in sinks:
+            if sink_cell in routing_cells:
+                continue
+            make_direct_conn(
+                ic_xml, (normalize_pb_name(driver_cell), driver_pin),
+                (normalize_pb_name(sink_cell), sink_pin), path_attr
+            )
+
+    # Generate the mux interconnects
+    for mux_cell in routing_cells:
+        mux_outputs = defaultdict(list)
+        for (driver_cell, driver_pin), sinks in interconn.items():
+            if driver_cell != mux_cell:
+                continue
+            mux_outputs[driver_pin].extend(sinks)
+
+        assert len(mux_outputs) == 1, """\
+Mux {} has multiple outputs ({})!
+Currently muxes can only drive a single output.""".format(
+            mux_cell, ", ".join(mux_outputs.keys())
+        )
+        for mux_output_pin, sinks in mux_outputs.items():
+            assert len(sinks) == 1, """\
+Mux {}.{} has multiple outputs ({})!
+Currently muxes can only drive a single output.""".format(
+                mux_cell, mux_output_pin,
+                ", ".join("{}.{}".format(*pin) for pin, path_attr in sinks)
+            )
+            for (sink_cell, sink_pin), path_attr in sinks:
+                assert sink_cell not in routing_names, """\
+Mux {}.{} is trying to drive mux input pin {}.{}""".format(
+                    mux_cell, mux_output_pin, sink_cell, sink_pin
+                )
+
+        mux_inputs = {}
+        for (driver_cell, driver_pin), sinks in interconn.items():
+            for (sink_cell, mux_pin), path_attr in sinks:
+                if sink_cell != mux_cell:
+                    continue
+                assert driver_cell not in routing_names, \
+                    "Mux {}.{} is trying to drive mux {}.{}".format(
+                        driver_cell, driver_pin, mux_cell, sink_pin
+                    )
+                assert sink_pin not in mux_inputs, """\
+Pin {}.{} is trying to drive mux pin {}.{} (already driving by {}.{})""".format(
+                    driver_cell, driver_pin, mux_cell, mux_pin,
+                    *mux_inputs[sink_pin]
+                )
+                mux_inputs[mux_pin] = (driver_cell, driver_pin)
+
+        make_mux_conn(ic_xml, mux_cell, mux_inputs, mux_outputs)
+
+
+def make_leaf_pb(outfile, yj, mod, mod_pname, pb_type_xml):
+    # As leaf node, need to generate timing information
+    def process_clocked_tmg(tmgspec, port, iodir, xmltype, xml_parent):
+        """Add a suitable timing spec if necessary to the pb_type"""
+        if tmgspec is not None:
+            splitspec = tmgspec.split(" ")
+            assert len(
+                splitspec
+            ) == 2, 'bad timing specification "{}", must be of format "clock value"'.format(
+                tmgspec
+            )
+            attrs = {"port": port, "clock": splitspec[0]}
+            if xmltype == "T_clock_to_Q":
+                assert iodir == "output", \
+                    "Only output ports can have T_clock_to_Q timing definition."
+                "Port {}, direction {}.".format(port, iodir)
+                attrs["max"] = splitspec[1]
+            else:
+                assert iodir == "input", \
+                    "Only input ports can have {} timing definition."
+                "Port {}, direction {}.".format(xmltype, port, iodir)
+                attrs["value"] = splitspec[1]
+            ET.SubElement(xml_parent, xmltype, attrs)
+
+    for name, width, bits, iodir in mod.ports:
+        port = "{}".format(name)
+        # Clocked timing
+        Tsetup = mod.net_attr(name, "SETUP")
+        Thold = mod.net_attr(name, "HOLD")
+        Tctoq = mod.net_attr(name, "CLK_TO_Q")
+        process_clocked_tmg(Tsetup, port, iodir, "T_setup", pb_type_xml)
+        process_clocked_tmg(Thold, port, iodir, "T_hold", pb_type_xml)
+        process_clocked_tmg(Tctoq, port, iodir, "T_clock_to_Q", pb_type_xml)
+
+        # Combinational delays
+        dly_prefix = "DELAY_CONST_"
+        dly_mat_prefix = "DELAY_MATRIX_"
+        for attr, atvalue in mod.net_attrs(name).items():
+            if attr.startswith(dly_prefix):
+                # Single, constant delays
+                inp = attr[len(dly_prefix):]
+                inport = "{}".format(inp)
+                ET.SubElement(
+                    pb_type_xml, "delay_constant", {
+                        "in_port": inport,
+                        "out_port": port,
+                        "max": str(atvalue)
+                    }
+                )
+            elif attr.startswith(dly_mat_prefix):
+                # Constant delay matrices
+                inp = attr[len(dly_mat_prefix):]
+                inport = "{}".format(inp)
+                mat = "\n" + atvalue.replace(";", "\n") + "\n"
+                xml_mat = ET.SubElement(
+                    pb_type_xml, "delay_matrix", {
+                        "in_port": inport,
+                        "out_port": port,
+                        "type": "max"
+                    }
+                )
+                xml_mat.text = mat
+
+
+def make_pb_type(
+        infiles, outfile, yj, mod, mode_processing=False, mode_xml=None, mode_name=None
+):
+    """Build the pb_type for a given module. mod is the YosysModule object to
+    generate."""
+
+    modes = mod.attr("MODES", None)
+    if modes is not None:
+        modes = modes.split(";")
+    mod_pname = mod.name
+    assert mod_pname == mod_pname.upper(
+    ), "pb_type name should be all uppercase. {}".format(mod_pname)
+
+    pb_attrs = dict()
+    # If we are a blackbox with no modes, then generate a blif_model
+    is_blackbox = is_mod_blackbox(mod) or not mod.cells
+    has_modes = modes is not None
+
+    print("is_blackbox", is_blackbox, "has_modes?", has_modes)
+
+    # Process type and class of module
+    model_name = mod.attr("MODEL_NAME", mod.name)
+    assert model_name == model_name.upper(
+    ), "Model name should be uppercase. {}".format(model_name)
+    mod_cls = mod.CLASS
+    if mod_cls is not None:
+        if mod_cls == "lut":
+            pb_attrs["blif_model"] = ".names"
+            pb_attrs["class"] = "lut"
+        elif mod_cls == "routing":
+            # TODO: pb_attrs["class"] = "routing"
+            pass
+        elif mod_cls == "mux":
+            # TODO: ?
+            pass
+        elif mod_cls == "flipflop":
+            pb_attrs["blif_model"] = ".latch"
+            pb_attrs["class"] = "flipflop"
+        else:
+            assert False, "unknown class {}".format(mod_cls)
+    elif is_blackbox and not has_modes:
+        pb_attrs["blif_model"] = ".subckt " + model_name
+
+    # set num_pb to 1, it will be updated if this pb_type
+    # will be included by another one
+    if mode_xml is None:
+        pb_type_xml = ET.Element(
+            "pb_type", {
+                "num_pb": "1",
+                "name": mod_pname
+            },
+            nsmap={'xi': xmlinc.xi_url}
+        )
+    else:
+        pb_type_xml = ET.SubElement(
+            mode_xml,
+            "pb_type", {
+                "num_pb": "1",
+                "name": mode_name
+            },
+            nsmap={'xi': xmlinc.xi_url}
+        )
+
+    if 'blif_model' in pb_attrs:
+        ET.SubElement(pb_type_xml, "blif_model",
+                      {}).text = pb_attrs["blif_model"]
+
+    if 'class' in pb_attrs:
+        ET.SubElement(pb_type_xml, "pb_class", {}).text = pb_attrs["class"]
+
+    # Create the pins for this pb_type
+    clocks = run.list_clocks(infiles, mod.name)
+    make_ports(clocks, mod, pb_type_xml, "clocks")
+    make_ports(clocks, mod, pb_type_xml, "inputs")
+    make_ports(clocks, mod, pb_type_xml, "outputs")
+
+    if modes and not mode_processing:
+        for mode in modes:
+            smode = mode.strip()
+            mode_xml = ET.SubElement(pb_type_xml, "mode", {"name": smode})
+            # Rerun Yosys with mode parameter
+            mode_yj = YosysJSON(
+                run.vlog_to_json(
+                    infiles,
+                    flatten=False,
+                    aig=False,
+                    mode=smode,
+                    module_with_mode=mod.name
+                )
+            )
+            mode_mod = mode_yj.module(mod.name)
+            make_pb_type(infiles, outfile, mode_yj, mode_mod, True, mode_xml, smode)
+
+            # if mode pb_type contains interconnect tag, add new connctions there
+            ic_xml = mode_xml.find("interconnect")
+            print("ic_xml is", ic_xml, file=sys.stderr)
+            if ic_xml is None:
+                ic_xml = ET.SubElement(mode_xml, "interconnect")
+
+            for (driver_cell,
+                 driver_pin), (sink_cell,
+                               sink_pin) in mode_interconnects(mod, smode):
+                make_direct_conn(
+                    ic_xml, (driver_cell, driver_pin), (sink_cell, sink_pin),
+                    {}
+                )
+
+    if not modes or mode_processing:
+        routing = children = []
+        if not is_blackbox:
+            routing, children = get_children(yj, mod)
+
+        if routing or children:
+            make_container_pb(
+                outfile, yj, mod, mod_pname, pb_type_xml, routing, children
+            )
+        else:
+            make_leaf_pb(outfile, yj, mod, mod_pname, pb_type_xml)
+
+    return pb_type_xml
+
+
+parser = argparse.ArgumentParser(
+    description=__doc__.strip(), formatter_class=argparse.RawTextHelpFormatter
+)
+parser.add_argument(
+    'infiles',
+    metavar='input.v',
+    type=str,
+    nargs='+',
+    help="""\
+One or more Verilog input files, that will be passed to Yosys internally.
+They should be enough to generate a flattened representation of the model,
+so that paths through the model can be determined.
+"""
+)
+parser.add_argument(
+    '--top',
+    help="""\
+Top level module, will usually be automatically determined from the file name
+%.sim.v
+"""
+)
+parser.add_argument(
+    '--outfile',
+    '-o',
+    type=argparse.FileType('w'),
+    default="pb_type.xml",
+    help="""\
+Output filename, default 'model.xml'
+"""
+)
+
+
+def vlog_to_pbtype(infiles, outfile, top=None):
+    iname = os.path.basename(infiles[0])
+
+    outfile = open(outfile, 'w')
+
+    run.add_define("PB_TYPE")
+    vjson = run.vlog_to_json(infiles, flatten=False, aig=False)
+    yj = YosysJSON(vjson)
+
+    if top is not None:
+        top = top
+    else:
+        wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", iname)
+        if wm:
+            top = wm.group(1).upper()
+        else:
+            print(
+                "ERROR file name not of format %.sim.v ({}), cannot detect top level."
+                " Manually specify the top level module using --top".
+                format(iname)
+            )
+            sys.exit(1)
+
+    top = top.upper()
+
+    tmod = yj.module(top)
+
+    pb_type_xml = make_pb_type(infiles, outfile.name, yj, tmod)
+
+    outfile.write(
+        ET.tostring(
+            pb_type_xml,
+            pretty_print=True,
+            encoding="utf-8",
+            xml_declaration=True
+        ).decode('utf-8')
+    )
+    print("Generated {} from {}".format(outfile.name, iname))
+    outfile.close()
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
+    args = parser.parse_args()
+    sys.exit(main(args))
diff --git a/v2x/yosys/__init__.py b/v2x/yosys/__init__.py
new file mode 100644
index 0000000..e5a0d9b
--- /dev/null
+++ b/v2x/yosys/__init__.py
@@ -0,0 +1 @@
+#!/usr/bin/env python3
diff --git a/v2x/yosys/json.py b/v2x/yosys/json.py
new file mode 100755
index 0000000..d743127
--- /dev/null
+++ b/v2x/yosys/json.py
@@ -0,0 +1,364 @@
+#!/usr/bin/env python3
+"""
+This is intended to provide a range of helper functions around the output of
+Yosys' `write_json`. Depending on the tasks, this may need to be flattened
+and/or in AIG format. In any case, at minimum `proc` and usually `prep` should
+be used before outputting the JSON.
+"""
+
+import json
+import pprint
+import re
+
+
+class YosysModule:
+    def __init__(self, name, module_data):
+        self.name = name
+        self.data = module_data
+
+    def __str__(self):
+        return "YosysModule({},\n{})".format(
+            self.name, pprint.pformat(self.data)
+        )
+
+    @property
+    def ports(self):
+        """List of ports on a module.
+
+        Returns a list of tuples:
+        -------
+        name : str
+        width : int
+            The width in bits
+        dir : str
+            The direction, should be either `input` or `output`
+        """
+        plist = []
+        for port, pdata in sorted(self.data["ports"].items()):
+            plist.append(
+                (port, len(pdata["bits"]), pdata["bits"], pdata["direction"])
+            )
+        return plist
+
+    @property
+    def cells(self):
+        """List of cells of a module, excluding Yosys-internal cells
+        beginning with $.
+
+        Returns a list of tuples:
+        -------
+        name : str
+        type : str
+        """
+        clist = []
+        for cell, cdata in sorted(self.data["cells"].items()):
+            if cdata["type"].startswith('$'):
+                continue
+            clist.append((cell, cdata["type"]))
+        return clist
+
+    @property
+    def all_cells(self):
+        """List of cells of a module, including Yosis-internal cells
+
+        Returns a list of tuples:
+        -------
+        name : str
+        type : str
+        """
+        clist = []
+        for cell, cdata in sorted(self.data["cells"].items()):
+            clist.append((cell, cdata["type"]))
+        return clist
+
+    @property
+    def net_names(self):
+        """List the net names avilable in the design."""
+        return self.data["netnames"].keys()
+
+    @property
+    def nets(self):
+        """List the net ids available in the design."""
+        return list(
+            sorted(set(n['bits'][0] for n in self.data["netnames"].values()))
+        )
+
+    def cell_type(self, cell):
+        """Return the type of a given cell"""
+        for cname, cdata in self.data["cells"].items():
+            if cname == cell:
+                return cdata["type"]
+        return None
+
+    @property
+    def module_attrs(self):
+        """All attributes of a module as a dictionary"""
+        return self.data["attributes"]
+
+    def attr(self, attr, defval=None):
+        """Get an attribute, or defval is not set"""
+        if attr in self.module_attrs:
+            return self.module_attrs[attr]
+        else:
+            return defval
+
+    def __getattr__(self, attr):
+        return self.attr(attr)
+
+    def has_attr(self, attr):
+        """Return true if an attribute exists"""
+        return attr in self.module_attrs
+
+    def cell_attrs(self, cell):
+        """All attributes of a given cell as a dictionary"""
+        return self.data["cells"][cell]["attributes"]
+
+    def cell_attr(self, cell, attr, defval=None):
+        """Get an attribute of a given cell, or defval is not set"""
+        if attr in self.cell_attrs(cell):
+            return self.cell_attrs(cell)[attr]
+        else:
+            return defval
+
+    def net_attrs(self, netname):
+        """Get all attributes of a given net as a dictionary"""
+        return self.data["netnames"][netname]["attributes"]
+
+    def net_attr(self, netname, attr, defval=None):
+        """Get an attribute of a given net (specified by name), or defval is not set"""
+        if attr in self.net_attrs(netname):
+            return self.net_attrs(netname)[attr]
+        else:
+            return defval
+
+    PORT_REGEX = re.compile('(.*)(\\[([0-9]+)\\])')
+
+    def port_attrs(self, port_pin):
+        """Gets all the attributes for for given pin
+
+        Returns attributes dictionary:
+        -------
+
+        port_pin: str
+        """
+        m = self.PORT_REGEX.match(port_pin)
+        if not m:
+            return self.net_attrs(port_pin)
+        port_name, _, port_idx = m.groups()
+        return self.net_attrs(port_name)
+
+    # TODO: the below code is kind of ugly, but because module and cell IO
+    # specifications are inconsistent in how they are represented in the JSON,
+    # it's hard to make any nicer...
+
+    def port_conns(self, port):
+        """The connections of a port
+
+        Returns a list of connections:
+        -------
+        net : int
+        """
+        for pname, pdata in sorted(self.data["ports"].items()):
+            if pname == port:
+                return pdata["bits"]
+
+    def cell_conns(self, cell, direction="input"):
+        """The connections of a cell in a given direction as a 2-tuple
+
+        Returns a list of tuples:
+        -------
+        port : str
+        net : int
+        """
+        cdata = self.data["cells"][cell]
+        conns = []
+        for port, condata in sorted(cdata["connections"].items()):
+            if cdata["port_directions"][port] == direction:
+                N = len(condata)
+                if N == 1:
+                    conns.append((port, condata[0]))
+                else:
+                    for i in range(N):
+                        conns.append(("{}[{}]".format(port, i), condata[i]))
+        return conns
+
+    def cell_conn_list(self, cell, port):
+        """The connections of a cell in a given port as a list
+
+        Returns a list of connections:
+        -------
+        net : list
+
+        """
+        data = self.data["cells"][cell]
+
+        conns = []
+
+        for cport, condata in sorted(data["connections"].items()):
+            if cport == port:
+                conns = condata
+
+        return conns
+
+    def cell_clk_conn(self, cell):
+        """The clock net related to a given cell
+
+        If exists returns the clock net
+
+        net : list
+        """
+        data = self.data["cells"][cell]
+
+        if "CLK" in data["connections"].keys():
+            return data["connections"]["CLK"]
+        else:
+            return None
+
+    def conn_io(self, net, iodir):
+        """Returns a list of top level IO matching a direction and connected net number
+
+        Returns a list:
+        -------
+        port : str
+        """
+        conn_io = []
+        for port, pdata in sorted(self.data["ports"].items()):
+            if pdata["direction"] == iodir:
+                if net in pdata["bits"]:
+                    if len(pdata["bits"]) == 1:
+                        conn_io.append(port)
+                    else:
+                        conn_io.append(
+                            "{}[{}]".format(port, pdata["bits"].index(net))
+                        )
+        return conn_io
+
+    def conn_ports(self, net, pdir):
+        """Returns any cell ports matching a direction and connected net number
+
+        Returns a list of tuples:
+        -------
+        cell : str
+        port : str
+        """
+        conn_ports = []
+        for cell in sorted(self.data["cells"].keys()):
+            cdata = self.data["cells"][cell]
+            for port, condata in sorted(cdata["connections"].items()):
+                if cdata["port_directions"][port] == pdir:
+                    if net in condata:
+                        if len(condata) == 1:
+                            conn_ports.append((cell, port))
+                        else:
+                            conn_ports.append(
+                                (
+                                    cell,
+                                    "{}[{}]".format(port, condata.index(net))
+                                )
+                            )
+        return conn_ports
+
+    def net_name(self, netid):
+        """Get a net name from a given netid. "netid" is a design net unique id
+        number.
+
+        Returns string:
+        -------
+        netid : int
+        """
+        import sys
+        print("Netid", netid, file=sys.stderr)
+        names = []
+        for n, props in self.data["netnames"].items():
+            if netid in props['bits']:
+                names.append(n)
+        if len(names) != 1:
+            raise KeyError("Net id {} not found".format(netid))
+        return names[0]
+
+    def net_drivers(self, net):
+        """Returns a list of drivers of a given net, both top level inputs.
+        and cell outputs. "cell" is set to the name of the module for top level
+        IO.
+
+        Returns a list of tuples:
+        -------
+        cell : str
+        port : str
+        """
+        # top level *inputs* and cell outputs are both drivers
+        io_drivers = [(self.name, _) for _ in self.conn_io(net, "input")]
+        cell_drivers = self.conn_ports(net, "output")
+        return io_drivers + cell_drivers
+
+    def net_sinks(self, net):
+        """Returns a list of sinks of a given net, both top level outputs.
+        and cell inputs. "cell" is set to the name of the module for top level
+        IO.
+
+        Returns a list of tuples:
+        -------
+        cell : str
+        port : str
+        """
+        # top level *outputs* and cell inputs are both drivers
+        io_drivers = [(self.name, _) for _ in self.conn_io(net, "output")]
+        cell_drivers = self.conn_ports(net, "input")
+        return io_drivers + cell_drivers
+
+
+class YosysJSON:
+    def __init__(self, j, top=None):
+        """Takes either the filename to a JSON file, or already parsed JSON
+        data as a dictionary. Optionally the top level module can be specified
+        too."""
+        if (isinstance(j, str)):
+            with open(j, 'r') as f:
+                self.data = json.load(f)
+        else:
+            self.data = j
+        if top is None:
+            if len(self.data["modules"]) == 1:
+                self.top = list(self.data["modules"].keys())[0]
+            else:
+                self.top = None
+        else:
+            self.top = top
+
+    def module(self, module):
+        """Get a given module (by name) as a `YosysModule`"""
+        if module not in self.data["modules"]:
+            raise KeyError(
+                "No yosys module named {} (only have {})".format(
+                    module, self.data["modules"].keys()
+                )
+            )
+        return YosysModule(module, self.data["modules"][module])
+
+    def modules_with_attr(self, attr_name, attr_value):
+        """Return a list of `YosysModule`s, selecting based on a given attribute"""
+        mods = []
+        for mod in self.data["modules"]:
+            ymod = self.module(mod)
+            if ymod.has_attr(attr_name) and ymod.attr(attr_name) == attr_value:
+                mods.append(ymod)
+        return mods
+
+    def all_modules(self):
+        """Return a list of the names of all modules in the design, sorted alphabetically"""
+        return sorted(self.data["modules"].keys())
+
+    @property
+    def top_module(self):
+        """Get a given module (by name) as a `YosysModule`"""
+        return self.module(self.top)
+
+    def has_module(self, module):
+        """Return true if a module exists"""
+        return module in self.data["modules"]
+
+    def get_module_file(self, module):
+        """Return the filename in which a given module lives"""
+        src = self.module(module).attr("src")
+        cpos = src.rfind(":")
+        return src[0:cpos]
diff --git a/v2x/yosys/run.py b/v2x/yosys/run.py
new file mode 100755
index 0000000..842f7e9
--- /dev/null
+++ b/v2x/yosys/run.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+import json
+import os
+import re
+import subprocess
+import tempfile
+from . import utils
+
+
+def get_verbose():
+    """Return if in verbose mode."""
+    verbose = 0
+    for e in ["V", "VERBOSE"]:
+        if e not in os.environ:
+            continue
+        verbose = int(os.environ[e])
+        break
+    return verbose > 0
+
+
+def get_yosys():
+    """Return how to execute Yosys: the value of $YOSYS if set, otherwise just
+    `yosys`."""
+    return os.getenv("YOSYS", "yosys")
+
+
+def get_yosys_common_args():
+    return ["-e", "wire '[^']*' is assigned in a block", "-q"]
+
+
+def get_output(params):
+    """Run Yosys with given command line parameters, and return stdout as a string"""
+
+    verbose = get_verbose()
+
+    cmd = [get_yosys()] + get_yosys_common_args() + params
+    if verbose:
+        print(cmd)
+
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+    # Get the output
+    stdout, stderr = p.communicate()
+    stdout = stdout.decode("utf-8")
+    stderr = stderr.decode("utf-8")
+
+    retcode = p.wait()
+
+    msg = ""
+    msg += "stdout" + "=" * 75 + "\n"
+    msg += stdout + "\n"
+    msg += "stderr" + "-" * 75 + "\n"
+    msg += stderr + "\n"
+    msg += "=" * 75 + "\n"
+    msg += "Return Code: {}".format(retcode)
+
+    if verbose:
+        print(msg)
+
+    if retcode != 0:
+        emsg = ""
+        emsg += "Failed to run {}".join(" ".join(cmd)) + "\n"
+        emsg += msg
+
+        raise subprocess.CalledProcessError(retcode, cmd, emsg)
+    return stdout
+
+
+defines = []
+includes = []
+
+
+def add_define(defname):
+    """Add a Verilog define to the list of defines to set in Yosys"""
+    defines.append(defname)
+
+
+def get_defines():
+    """Return a list of set Verilog defines, as a list of arguments to pass to Yosys
+`read_verilog`"""
+    return " ".join(["-D" + _ for _ in defines])
+
+
+def add_include(path):
+    """ Add a path to search when reading verilog to the list of includes set in Yosys"""
+    includes.append(path)
+
+
+def get_includes():
+    """Return a list of include directories, as a list of arguments to pass to Yosys
+`read_verilog`"""
+    return " ".join(["-I" + _ for _ in includes])
+
+
+def commands(commands, infiles=[]):
+    """Run a given string containing Yosys commands
+
+    Inputs
+    -------
+    commands : string of Yosys commands to run
+    infiles : list of input files
+    """
+    commands = "read_verilog {} {} {}; ".format(
+        get_defines(), get_includes(), " ".join(infiles)
+    ) + commands
+    params = ["-q", "-p", commands]
+    return get_output(params)
+
+
+def script(script, infiles=[]):
+    """Run a Yosys script given a path to the script
+
+    Inputs
+    -------
+    script : path to Yosys script to run
+    infiles : list of input files
+    """
+    params = ["-q", "-s", script] + infiles
+    return get_output(params)
+
+
+def vlog_to_json(
+        infiles, flatten=False, aig=False, mode=None, module_with_mode=None
+):
+    """
+    Convert Verilog to a JSON representation using Yosys
+
+    Inputs
+    -------
+    infiles : list of input files
+    flatten : set to flatten output hierarchy
+    aig : generate And-Inverter-Graph modules for gates
+    mode : set to a value other than None to use `chparam` to set the value of the MODE parameter
+    module_with_mode : the name of the module to apply `mode` to
+    """
+    prep_opts = "-flatten" if flatten else ""
+    json_opts = "-aig" if aig else ""
+    if mode is not None:
+        mode_str = 'chparam -set MODE "{}" {}; '.format(mode, module_with_mode)
+    else:
+        mode_str = ""
+    cmds = "{}prep {}; write_json {}".format(mode_str, prep_opts, json_opts)
+    j = utils.strip_yosys_json(commands(cmds, infiles))
+    """with open('dump.json', 'w') as dbg:
+        print(j,file=dbg)"""
+    return json.loads(j)
+
+
+def extract_pin(module, pstr, _regex=re.compile(r"([^/]+)/([^/]+)")):
+    """
+    Extract the pin from a line of the result of a Yosys select command, or
+    None if the command result is irrelevant (e.g. does not correspond to the
+    correct module)
+
+    Inputs
+    -------
+    module: Name of module to extract pins from
+    pstr: Line from Yosys select command (`module/pin` format)
+    """
+    m = re.match(r"([^/]+)/([^/]+)", pstr)
+    if m and m.group(1) == module:
+        return m.group(2)
+    else:
+        return None
+
+
+def do_select(infiles, module, expr, prep=False, flatten=False):
+    """
+    Run a Yosys select command (given the expression and input files) on a module
+    and return the result as a list of pins
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    expr: Yosys selector expression for select command
+
+    prep: Run prep command before selecting.
+    flatten: Flatten module when running prep.
+    """
+
+    # TODO: All of these functions involve a fairly large number of calls to
+    # Yosys. Although performance here is unlikely to be a major priority any
+    # time soon, it might be worth investigating better options?
+
+    f = ""
+    if flatten:
+        f = "-flatten"
+
+    p = ""
+    if prep:
+        p = "prep -top {} {};".format(module, f)
+    else:
+        p = "proc;"
+
+    outfile = tempfile.mktemp()
+    sel_cmd = "{} cd {}; select -write {} {}".format(p, module, outfile, expr)
+    commands(sel_cmd, infiles)
+    pins = []
+    with open(outfile, 'r') as f:
+        for net in f:
+            snet = net.strip()
+            if (len(snet) > 0):
+                pin = extract_pin(module, snet)
+                if pin is not None:
+                    pins.append(pin)
+    os.remove(outfile)
+    return pins
+
+
+def get_combinational_sinks(infiles, module, innet):
+    """Return a list of output ports which are combinational sinks of a given
+    input.
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    innet: Name of input net to find sinks of
+    """
+    return do_select(
+        infiles, module, "{} %co* o:* %i {} %d".format(innet, innet)
+    )
+
+
+def list_clocks(infiles, module):
+    """Return a list of clocks in the module
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    """
+    return do_select(
+        infiles, module,
+        "c:* %x:+[CLK]:+[clk]:+[clock]:+[CLOCK] a:CLOCK=1 %u c:* %d x:* %i"
+    )
+
+
+def get_clock_assoc_signals(infiles, module, clk):
+    """Return the list of signals associated with a given clock.
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    clk: Name of clock to find associated signals
+    """
+    return do_select(
+        infiles, module,
+        "select -list {} %a %co* %x i:* o:* %u %i a:ASSOC_CLOCK={} %u {} %d".
+        format(clk, clk, clk)
+    )
+
+
+# Find things which affect the given output
+# show w:*D_IN_0 %a %ci*
+
+# Find things which are affected by the given clock.
+# show w:*INPUT_CLK %a %co*
+
+# Find things which are affect by the given signal - combinational only.
+# select -list w:*INPUT_CLK %a %co* %x x:* %i
+
+
+def get_related_output_for_input(infiles, module, signal):
+    """.
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    clk: Name of clock to find associated signals
+    """
+    return do_select(
+        infiles, module, "select -list w:*{} %a %co* o:* %i".format(signal)
+    )
+
+
+def get_related_inputs_for_input(infiles, module, signal):
+    """.
+
+    Inputs
+    -------
+    infiles: List of Verilog source files to pass to Yosys
+    module: Name of module to run command on
+    clk: Name of clock to find associated signals
+    """
+    return [
+        x for x in do_select(
+            infiles, module,
+            "select -list w:*{} %a %co* %x i:* %i".format(signal)
+        ) if x != signal
+    ]
diff --git a/v2x/yosys/utils.py b/v2x/yosys/utils.py
new file mode 100644
index 0000000..722ac9b
--- /dev/null
+++ b/v2x/yosys/utils.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+import re
+"""The JSON Yosys outputs isn't acutally compliant JSON, as it contains C-style
+comments. These must be stripped."""
+
+
+def strip_yosys_json(text):
+    stripped = re.sub(r'\\\n', '', text)
+    stripped = re.sub(r'//.*\n', '\n', stripped)
+    stripped = re.sub(r'/\*.*\*/', '', stripped)
+    return stripped